aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord
diff options
context:
space:
mode:
Diffstat (limited to 'activerecord')
-rw-r--r--activerecord/CHANGELOG.md1605
-rw-r--r--activerecord/MIT-LICENSE2
-rw-r--r--activerecord/README.rdoc8
-rw-r--r--activerecord/RUNNING_UNIT_TESTS.rdoc20
-rw-r--r--activerecord/Rakefile62
-rw-r--r--activerecord/activerecord.gemspec4
-rwxr-xr-xactiverecord/bin/test19
-rw-r--r--activerecord/examples/performance.rb4
-rw-r--r--activerecord/lib/active_record.rb9
-rw-r--r--activerecord/lib/active_record/aggregations.rb57
-rw-r--r--activerecord/lib/active_record/association_relation.rb17
-rw-r--r--activerecord/lib/active_record/associations.rb643
-rw-r--r--activerecord/lib/active_record/associations/alias_tracker.rb46
-rw-r--r--activerecord/lib/active_record/associations/association.rb23
-rw-r--r--activerecord/lib/active_record/associations/association_scope.rb212
-rw-r--r--activerecord/lib/active_record/associations/belongs_to_association.rb54
-rw-r--r--activerecord/lib/active_record/associations/builder/association.rb62
-rw-r--r--activerecord/lib/active_record/associations/builder/belongs_to.rb57
-rw-r--r--activerecord/lib/active_record/associations/builder/collection_association.rb26
-rw-r--r--activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb36
-rw-r--r--activerecord/lib/active_record/associations/builder/has_many.rb8
-rw-r--r--activerecord/lib/active_record/associations/builder/has_one.rb17
-rw-r--r--activerecord/lib/active_record/associations/builder/singular_association.rb11
-rw-r--r--activerecord/lib/active_record/associations/collection_association.rb89
-rw-r--r--activerecord/lib/active_record/associations/collection_proxy.rb133
-rw-r--r--activerecord/lib/active_record/associations/foreign_association.rb11
-rw-r--r--activerecord/lib/active_record/associations/has_many_association.rb78
-rw-r--r--activerecord/lib/active_record/associations/has_many_through_association.rb54
-rw-r--r--activerecord/lib/active_record/associations/has_one_association.rb18
-rw-r--r--activerecord/lib/active_record/associations/join_dependency.rb50
-rw-r--r--activerecord/lib/active_record/associations/join_dependency/join_association.rb36
-rw-r--r--activerecord/lib/active_record/associations/join_dependency/join_part.rb1
-rw-r--r--activerecord/lib/active_record/associations/preloader.rb77
-rw-r--r--activerecord/lib/active_record/associations/preloader/association.rb100
-rw-r--r--activerecord/lib/active_record/associations/preloader/has_many_through.rb2
-rw-r--r--activerecord/lib/active_record/associations/preloader/through_association.rb6
-rw-r--r--activerecord/lib/active_record/associations/singular_association.rb10
-rw-r--r--activerecord/lib/active_record/associations/through_association.rb33
-rw-r--r--activerecord/lib/active_record/attribute.rb113
-rw-r--r--activerecord/lib/active_record/attribute/user_provided_default.rb23
-rw-r--r--activerecord/lib/active_record/attribute_assignment.rb165
-rw-r--r--activerecord/lib/active_record/attribute_decorators.rb11
-rw-r--r--activerecord/lib/active_record/attribute_methods.rb111
-rw-r--r--activerecord/lib/active_record/attribute_methods/before_type_cast.rb7
-rw-r--r--activerecord/lib/active_record/attribute_methods/dirty.rb138
-rw-r--r--activerecord/lib/active_record/attribute_methods/primary_key.rb15
-rw-r--r--activerecord/lib/active_record/attribute_methods/query.rb6
-rw-r--r--activerecord/lib/active_record/attribute_methods/read.rb74
-rw-r--r--activerecord/lib/active_record/attribute_methods/serialization.rb29
-rw-r--r--activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb53
-rw-r--r--activerecord/lib/active_record/attribute_methods/write.rb37
-rw-r--r--activerecord/lib/active_record/attribute_mutation_tracker.rb70
-rw-r--r--activerecord/lib/active_record/attribute_set.rb49
-rw-r--r--activerecord/lib/active_record/attribute_set/builder.rb100
-rw-r--r--activerecord/lib/active_record/attributes.rb252
-rw-r--r--activerecord/lib/active_record/autosave_association.rb91
-rw-r--r--activerecord/lib/active_record/base.rb57
-rw-r--r--activerecord/lib/active_record/callbacks.rb65
-rw-r--r--activerecord/lib/active_record/coders/yaml_column.rb28
-rw-r--r--activerecord/lib/active_record/collection_cache_key.rb31
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb603
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb6
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb90
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/quoting.rb71
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/savepoints.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb88
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb457
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb73
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb414
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/transaction.rb300
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_adapter.rb143
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb644
-rw-r--r--activerecord/lib/active_record/connection_adapters/column.rb68
-rw-r--r--activerecord/lib/active_record/connection_adapters/connection_specification.rb22
-rw-r--r--activerecord/lib/active_record/connection_adapters/determine_if_preparable_visitor.rb22
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb57
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb69
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb56
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb119
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql_adapter.rb94
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb93
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/column.rb16
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb12
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid.rb7
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb80
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb4
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb3
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb8
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb11
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb20
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb4
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/float.rb21
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb6
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/infinity.rb13
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/integer.rb11
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb27
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb6
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb8
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/rails_5_1_point.rb50
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb40
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/time.rb11
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb50
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb15
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/vector.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/xml.rb6
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb51
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb39
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb194
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb54
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb303
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb35
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/utils.rb19
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb297
-rw-r--r--activerecord/lib/active_record/connection_adapters/schema_cache.rb60
-rw-r--r--activerecord/lib/active_record/connection_adapters/sql_type_metadata.rb32
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3/schema_creation.rb15
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb260
-rw-r--r--activerecord/lib/active_record/connection_adapters/statement_pool.rb39
-rw-r--r--activerecord/lib/active_record/connection_handling.rb12
-rw-r--r--activerecord/lib/active_record/core.rb219
-rw-r--r--activerecord/lib/active_record/counter_cache.rb35
-rw-r--r--activerecord/lib/active_record/dynamic_matchers.rb21
-rw-r--r--activerecord/lib/active_record/enum.rb187
-rw-r--r--activerecord/lib/active_record/errors.rb121
-rw-r--r--activerecord/lib/active_record/explain_registry.rb2
-rw-r--r--activerecord/lib/active_record/explain_subscriber.rb4
-rw-r--r--activerecord/lib/active_record/fixture_set/file.rb23
-rw-r--r--activerecord/lib/active_record/fixtures.rb183
-rw-r--r--activerecord/lib/active_record/gem_version.rb6
-rw-r--r--activerecord/lib/active_record/inheritance.rb54
-rw-r--r--activerecord/lib/active_record/integration.rb16
-rw-r--r--activerecord/lib/active_record/legacy_yaml_adapter.rb46
-rw-r--r--activerecord/lib/active_record/locale/en.yml5
-rw-r--r--activerecord/lib/active_record/locking/optimistic.rb50
-rw-r--r--activerecord/lib/active_record/locking/pessimistic.rb2
-rw-r--r--activerecord/lib/active_record/log_subscriber.rb62
-rw-r--r--activerecord/lib/active_record/migration.rb290
-rw-r--r--activerecord/lib/active_record/migration/command_recorder.rb77
-rw-r--r--activerecord/lib/active_record/model_schema.rb134
-rw-r--r--activerecord/lib/active_record/nested_attributes.rb67
-rw-r--r--activerecord/lib/active_record/no_touching.rb2
-rw-r--r--activerecord/lib/active_record/null_relation.rb22
-rw-r--r--activerecord/lib/active_record/persistence.rb198
-rw-r--r--activerecord/lib/active_record/querying.rb25
-rw-r--r--activerecord/lib/active_record/railtie.rb27
-rw-r--r--activerecord/lib/active_record/railties/controller_runtime.rb2
-rw-r--r--activerecord/lib/active_record/railties/databases.rake104
-rw-r--r--activerecord/lib/active_record/readonly_attributes.rb2
-rw-r--r--activerecord/lib/active_record/reflection.rb362
-rw-r--r--activerecord/lib/active_record/relation.rb273
-rw-r--r--activerecord/lib/active_record/relation/batches.rb166
-rw-r--r--activerecord/lib/active_record/relation/batches/batch_enumerator.rb67
-rw-r--r--activerecord/lib/active_record/relation/calculations.rb189
-rw-r--r--activerecord/lib/active_record/relation/delegation.rb11
-rw-r--r--activerecord/lib/active_record/relation/finder_methods.rb146
-rw-r--r--activerecord/lib/active_record/relation/from_clause.rb32
-rw-r--r--activerecord/lib/active_record/relation/merger.rb110
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder.rb183
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder/array_handler.rb25
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb78
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder/base_handler.rb17
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder/basic_object_handler.rb17
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder/class_handler.rb27
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder/range_handler.rb17
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder/relation_handler.rb2
-rw-r--r--activerecord/lib/active_record/relation/query_attribute.rb19
-rw-r--r--activerecord/lib/active_record/relation/query_methods.rb451
-rw-r--r--activerecord/lib/active_record/relation/record_fetch_warning.rb49
-rw-r--r--activerecord/lib/active_record/relation/spawn_methods.rb9
-rw-r--r--activerecord/lib/active_record/relation/where_clause.rb173
-rw-r--r--activerecord/lib/active_record/relation/where_clause_factory.rb37
-rw-r--r--activerecord/lib/active_record/result.rb11
-rw-r--r--activerecord/lib/active_record/runtime_registry.rb2
-rw-r--r--activerecord/lib/active_record/sanitization.rb133
-rw-r--r--activerecord/lib/active_record/schema.rb43
-rw-r--r--activerecord/lib/active_record/schema_dumper.rb100
-rw-r--r--activerecord/lib/active_record/schema_migration.rb7
-rw-r--r--activerecord/lib/active_record/scoping.rb23
-rw-r--r--activerecord/lib/active_record/scoping/default.rb26
-rw-r--r--activerecord/lib/active_record/scoping/named.rb71
-rw-r--r--activerecord/lib/active_record/secure_token.rb38
-rw-r--r--activerecord/lib/active_record/serialization.rb6
-rw-r--r--activerecord/lib/active_record/serializers/xml_serializer.rb193
-rw-r--r--activerecord/lib/active_record/statement_cache.rb59
-rw-r--r--activerecord/lib/active_record/store.rb11
-rw-r--r--activerecord/lib/active_record/suppressor.rb54
-rw-r--r--activerecord/lib/active_record/table_metadata.rb64
-rw-r--r--activerecord/lib/active_record/tasks/database_tasks.rb72
-rw-r--r--activerecord/lib/active_record/tasks/mysql_database_tasks.rb49
-rw-r--r--activerecord/lib/active_record/tasks/postgresql_database_tasks.rb40
-rw-r--r--activerecord/lib/active_record/tasks/sqlite_database_tasks.rb12
-rw-r--r--activerecord/lib/active_record/timestamp.rb32
-rw-r--r--activerecord/lib/active_record/touch_later.rb50
-rw-r--r--activerecord/lib/active_record/transactions.rb184
-rw-r--r--activerecord/lib/active_record/type.rb78
-rw-r--r--activerecord/lib/active_record/type/adapter_specific_registry.rb130
-rw-r--r--activerecord/lib/active_record/type/binary.rb40
-rw-r--r--activerecord/lib/active_record/type/boolean.rb19
-rw-r--r--activerecord/lib/active_record/type/date.rb43
-rw-r--r--activerecord/lib/active_record/type/date_time.rb40
-rw-r--r--activerecord/lib/active_record/type/decimal.rb27
-rw-r--r--activerecord/lib/active_record/type/decimal_without_scale.rb11
-rw-r--r--activerecord/lib/active_record/type/float.rb19
-rw-r--r--activerecord/lib/active_record/type/hash_lookup_type_map.rb18
-rw-r--r--activerecord/lib/active_record/type/integer.rb23
-rw-r--r--activerecord/lib/active_record/type/internal/abstract_json.rb33
-rw-r--r--activerecord/lib/active_record/type/internal/timezone.rb15
-rw-r--r--activerecord/lib/active_record/type/mutable.rb16
-rw-r--r--activerecord/lib/active_record/type/numeric.rb36
-rw-r--r--activerecord/lib/active_record/type/serialized.rb38
-rw-r--r--activerecord/lib/active_record/type/string.rb36
-rw-r--r--activerecord/lib/active_record/type/text.rb11
-rw-r--r--activerecord/lib/active_record/type/time.rb24
-rw-r--r--activerecord/lib/active_record/type/time_value.rb38
-rw-r--r--activerecord/lib/active_record/type/type_map.rb32
-rw-r--r--activerecord/lib/active_record/type/value.rb94
-rw-r--r--activerecord/lib/active_record/type_caster.rb7
-rw-r--r--activerecord/lib/active_record/type_caster/connection.rb29
-rw-r--r--activerecord/lib/active_record/type_caster/map.rb19
-rw-r--r--activerecord/lib/active_record/validations.rb85
-rw-r--r--activerecord/lib/active_record/validations/absence.rb24
-rw-r--r--activerecord/lib/active_record/validations/associated.rb11
-rw-r--r--activerecord/lib/active_record/validations/length.rb36
-rw-r--r--activerecord/lib/active_record/validations/presence.rb32
-rw-r--r--activerecord/lib/active_record/validations/uniqueness.rb34
-rw-r--r--activerecord/lib/rails/generators/active_record/migration.rb7
-rw-r--r--activerecord/lib/rails/generators/active_record/migration/migration_generator.rb13
-rw-r--r--activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb7
-rw-r--r--activerecord/lib/rails/generators/active_record/migration/templates/migration.rb3
-rw-r--r--activerecord/lib/rails/generators/active_record/model/model_generator.rb11
-rw-r--r--activerecord/lib/rails/generators/active_record/model/templates/model.rb5
-rw-r--r--activerecord/test/active_record/connection_adapters/fake_adapter.rb13
-rw-r--r--activerecord/test/cases/adapter_test.rb33
-rw-r--r--activerecord/test/cases/adapters/mysql/active_schema_test.rb63
-rw-r--r--activerecord/test/cases/adapters/mysql/case_sensitivity_test.rb3
-rw-r--r--activerecord/test/cases/adapters/mysql/charset_collation_test.rb54
-rw-r--r--activerecord/test/cases/adapters/mysql/connection_test.rb41
-rw-r--r--activerecord/test/cases/adapters/mysql/consistency_test.rb5
-rw-r--r--activerecord/test/cases/adapters/mysql/enum_test.rb2
-rw-r--r--activerecord/test/cases/adapters/mysql/explain_test.rb21
-rw-r--r--activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb48
-rw-r--r--activerecord/test/cases/adapters/mysql/quoting_test.rb42
-rw-r--r--activerecord/test/cases/adapters/mysql/reserved_word_test.rb48
-rw-r--r--activerecord/test/cases/adapters/mysql/schema_test.rb7
-rw-r--r--activerecord/test/cases/adapters/mysql/sp_test.rb29
-rw-r--r--activerecord/test/cases/adapters/mysql/sql_types_test.rb4
-rw-r--r--activerecord/test/cases/adapters/mysql/statement_pool_test.rb28
-rw-r--r--activerecord/test/cases/adapters/mysql/table_options_test.rb42
-rw-r--r--activerecord/test/cases/adapters/mysql/unsigned_type_test.rb65
-rw-r--r--activerecord/test/cases/adapters/mysql2/active_schema_test.rb63
-rw-r--r--activerecord/test/cases/adapters/mysql2/bind_parameter_test.rb2
-rw-r--r--activerecord/test/cases/adapters/mysql2/boolean_test.rb11
-rw-r--r--activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb3
-rw-r--r--activerecord/test/cases/adapters/mysql2/charset_collation_test.rb54
-rw-r--r--activerecord/test/cases/adapters/mysql2/connection_test.rb44
-rw-r--r--activerecord/test/cases/adapters/mysql2/enum_test.rb2
-rw-r--r--activerecord/test/cases/adapters/mysql2/explain_test.rb5
-rw-r--r--activerecord/test/cases/adapters/mysql2/json_test.rb172
-rw-r--r--activerecord/test/cases/adapters/mysql2/quoting_test.rb21
-rw-r--r--activerecord/test/cases/adapters/mysql2/reserved_word_test.rb48
-rw-r--r--activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb73
-rw-r--r--activerecord/test/cases/adapters/mysql2/schema_test.rb27
-rw-r--r--activerecord/test/cases/adapters/mysql2/sp_test.rb30
-rw-r--r--activerecord/test/cases/adapters/mysql2/sql_types_test.rb4
-rw-r--r--activerecord/test/cases/adapters/mysql2/table_options_test.rb42
-rw-r--r--activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb65
-rw-r--r--activerecord/test/cases/adapters/postgresql/active_schema_test.rb20
-rw-r--r--activerecord/test/cases/adapters/postgresql/array_test.rb90
-rw-r--r--activerecord/test/cases/adapters/postgresql/bit_string_test.rb21
-rw-r--r--activerecord/test/cases/adapters/postgresql/bytea_test.rb21
-rw-r--r--activerecord/test/cases/adapters/postgresql/change_schema_test.rb38
-rw-r--r--activerecord/test/cases/adapters/postgresql/cidr_test.rb25
-rw-r--r--activerecord/test/cases/adapters/postgresql/citext_test.rb28
-rw-r--r--activerecord/test/cases/adapters/postgresql/collation_test.rb53
-rw-r--r--activerecord/test/cases/adapters/postgresql/composite_test.rb27
-rw-r--r--activerecord/test/cases/adapters/postgresql/connection_test.rb24
-rw-r--r--activerecord/test/cases/adapters/postgresql/datatype_test.rb49
-rw-r--r--activerecord/test/cases/adapters/postgresql/domain_test.rb12
-rw-r--r--activerecord/test/cases/adapters/postgresql/enum_test.rb22
-rw-r--r--activerecord/test/cases/adapters/postgresql/explain_test.rb34
-rw-r--r--activerecord/test/cases/adapters/postgresql/extension_migration_test.rb4
-rw-r--r--activerecord/test/cases/adapters/postgresql/full_text_test.rb36
-rw-r--r--activerecord/test/cases/adapters/postgresql/geometric_test.rb210
-rw-r--r--activerecord/test/cases/adapters/postgresql/hstore_test.rb98
-rw-r--r--activerecord/test/cases/adapters/postgresql/infinity_test.rb29
-rw-r--r--activerecord/test/cases/adapters/postgresql/integer_test.rb25
-rw-r--r--activerecord/test/cases/adapters/postgresql/json_test.rb57
-rw-r--r--activerecord/test/cases/adapters/postgresql/ltree_test.rb23
-rw-r--r--activerecord/test/cases/adapters/postgresql/money_test.rb30
-rw-r--r--activerecord/test/cases/adapters/postgresql/network_test.rb47
-rw-r--r--activerecord/test/cases/adapters/postgresql/numbers_test.rb49
-rw-r--r--activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb86
-rw-r--r--activerecord/test/cases/adapters/postgresql/quoting_test.rb50
-rw-r--r--activerecord/test/cases/adapters/postgresql/range_test.rb40
-rw-r--r--activerecord/test/cases/adapters/postgresql/referential_integrity_test.rb111
-rw-r--r--activerecord/test/cases/adapters/postgresql/rename_table_test.rb34
-rw-r--r--activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb13
-rw-r--r--activerecord/test/cases/adapters/postgresql/schema_test.rb196
-rw-r--r--activerecord/test/cases/adapters/postgresql/serial_test.rb60
-rw-r--r--activerecord/test/cases/adapters/postgresql/sql_types_test.rb18
-rw-r--r--activerecord/test/cases/adapters/postgresql/statement_pool_test.rb2
-rw-r--r--activerecord/test/cases/adapters/postgresql/timestamp_test.rb70
-rw-r--r--activerecord/test/cases/adapters/postgresql/type_lookup_test.rb20
-rw-r--r--activerecord/test/cases/adapters/postgresql/utils_test.rb5
-rw-r--r--activerecord/test/cases/adapters/postgresql/uuid_test.rb101
-rw-r--r--activerecord/test/cases/adapters/postgresql/view_test.rb67
-rw-r--r--activerecord/test/cases/adapters/postgresql/xml_test.rb12
-rw-r--r--activerecord/test/cases/adapters/sqlite3/collation_test.rb53
-rw-r--r--activerecord/test/cases/adapters/sqlite3/copy_table_test.rb2
-rw-r--r--activerecord/test/cases/adapters/sqlite3/explain_test.rb5
-rw-r--r--activerecord/test/cases/adapters/sqlite3/quoting_test.rb53
-rw-r--r--activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb61
-rw-r--r--activerecord/test/cases/adapters/sqlite3/sqlite3_create_folder_test.rb3
-rw-r--r--activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb5
-rw-r--r--activerecord/test/cases/ar_schema_test.rb42
-rw-r--r--activerecord/test/cases/associations/association_scope_test.rb7
-rw-r--r--activerecord/test/cases/associations/belongs_to_associations_test.rb176
-rw-r--r--activerecord/test/cases/associations/bidirectional_destroy_dependencies_test.rb41
-rw-r--r--activerecord/test/cases/associations/callbacks_test.rb1
-rw-r--r--activerecord/test/cases/associations/deprecated_counter_cache_on_has_many_through_test.rb26
-rw-r--r--activerecord/test/cases/associations/eager_load_nested_include_test.rb4
-rw-r--r--activerecord/test/cases/associations/eager_test.rb244
-rw-r--r--activerecord/test/cases/associations/extension_test.rb4
-rw-r--r--activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb161
-rw-r--r--activerecord/test/cases/associations/has_many_associations_test.rb599
-rw-r--r--activerecord/test/cases/associations/has_many_through_associations_test.rb161
-rw-r--r--activerecord/test/cases/associations/has_one_associations_test.rb105
-rw-r--r--activerecord/test/cases/associations/has_one_through_associations_test.rb47
-rw-r--r--activerecord/test/cases/associations/inner_join_association_test.rb4
-rw-r--r--activerecord/test/cases/associations/inverse_associations_test.rb33
-rw-r--r--activerecord/test/cases/associations/join_model_test.rb50
-rw-r--r--activerecord/test/cases/associations/nested_through_associations_test.rb2
-rw-r--r--activerecord/test/cases/associations/required_test.rb30
-rw-r--r--activerecord/test/cases/associations_test.rb68
-rw-r--r--activerecord/test/cases/attribute_decorators_test.rb17
-rw-r--r--activerecord/test/cases/attribute_methods/read_test.rb11
-rw-r--r--activerecord/test/cases/attribute_methods_test.rb137
-rw-r--r--activerecord/test/cases/attribute_set_test.rb96
-rw-r--r--activerecord/test/cases/attribute_test.rb140
-rw-r--r--activerecord/test/cases/attributes_test.rb123
-rw-r--r--activerecord/test/cases/autosave_association_test.rb190
-rw-r--r--activerecord/test/cases/base_test.rb278
-rw-r--r--activerecord/test/cases/batches_test.rb308
-rw-r--r--activerecord/test/cases/binary_test.rb1
-rw-r--r--activerecord/test/cases/bind_parameter_test.rb38
-rw-r--r--activerecord/test/cases/cache_key_test.rb25
-rw-r--r--activerecord/test/cases/calculations_test.rb197
-rw-r--r--activerecord/test/cases/callbacks_test.rb223
-rw-r--r--activerecord/test/cases/collection_cache_key_test.rb70
-rw-r--r--activerecord/test/cases/column_definition_test.rb79
-rw-r--r--activerecord/test/cases/connection_adapters/adapter_leasing_test.rb6
-rw-r--r--activerecord/test/cases/connection_adapters/connection_handler_test.rb48
-rw-r--r--activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb79
-rw-r--r--activerecord/test/cases/connection_adapters/mysql_type_lookup_test.rb8
-rw-r--r--activerecord/test/cases/connection_adapters/schema_cache_test.rb11
-rw-r--r--activerecord/test/cases/connection_adapters/type_lookup_test.rb11
-rw-r--r--activerecord/test/cases/connection_management_test.rb33
-rw-r--r--activerecord/test/cases/connection_pool_test.rb199
-rw-r--r--activerecord/test/cases/core_test.rb11
-rw-r--r--activerecord/test/cases/counter_cache_test.rb31
-rw-r--r--activerecord/test/cases/date_time_precision_test.rb111
-rw-r--r--activerecord/test/cases/date_time_test.rb18
-rw-r--r--activerecord/test/cases/defaults_test.rb131
-rw-r--r--activerecord/test/cases/dirty_test.rb135
-rw-r--r--activerecord/test/cases/disconnected_test.rb6
-rw-r--r--activerecord/test/cases/enum_test.rb175
-rw-r--r--activerecord/test/cases/errors_test.rb16
-rw-r--r--activerecord/test/cases/explain_subscriber_test.rb5
-rw-r--r--activerecord/test/cases/explain_test.rb49
-rw-r--r--activerecord/test/cases/finder_test.rb198
-rw-r--r--activerecord/test/cases/fixture_set/file_test.rb12
-rw-r--r--activerecord/test/cases/fixtures_test.rb182
-rw-r--r--activerecord/test/cases/forbidden_attributes_protection_test.rb30
-rw-r--r--activerecord/test/cases/helper.rb45
-rw-r--r--activerecord/test/cases/hot_compatibility_test.rb2
-rw-r--r--activerecord/test/cases/inheritance_test.rb133
-rw-r--r--activerecord/test/cases/integration_test.rb25
-rw-r--r--activerecord/test/cases/invalid_connection_test.rb2
-rw-r--r--activerecord/test/cases/invertible_migration_test.rb86
-rw-r--r--activerecord/test/cases/locking_test.rb49
-rw-r--r--activerecord/test/cases/log_subscriber_test.rb113
-rw-r--r--activerecord/test/cases/migration/change_schema_test.rb79
-rw-r--r--activerecord/test/cases/migration/change_table_test.rb50
-rw-r--r--activerecord/test/cases/migration/column_attributes_test.rb2
-rw-r--r--activerecord/test/cases/migration/column_positioning_test.rb14
-rw-r--r--activerecord/test/cases/migration/columns_test.rb20
-rw-r--r--activerecord/test/cases/migration/command_recorder_test.rb58
-rw-r--r--activerecord/test/cases/migration/create_join_table_test.rb2
-rw-r--r--activerecord/test/cases/migration/foreign_key_test.rb74
-rw-r--r--activerecord/test/cases/migration/helper.rb8
-rw-r--r--activerecord/test/cases/migration/index_test.rb20
-rw-r--r--activerecord/test/cases/migration/logger_test.rb2
-rw-r--r--activerecord/test/cases/migration/pending_migrations_test.rb5
-rw-r--r--activerecord/test/cases/migration/postgresql_geometric_types_test.rb93
-rw-r--r--activerecord/test/cases/migration/references_foreign_key_test.rb170
-rw-r--r--activerecord/test/cases/migration/references_index_test.rb4
-rw-r--r--activerecord/test/cases/migration/references_statements_test.rb4
-rw-r--r--activerecord/test/cases/migration/rename_table_test.rb47
-rw-r--r--activerecord/test/cases/migration/table_and_index_test.rb4
-rw-r--r--activerecord/test/cases/migration_test.rb117
-rw-r--r--activerecord/test/cases/migrator_test.rb569
-rw-r--r--activerecord/test/cases/mixin_test.rb2
-rw-r--r--activerecord/test/cases/modules_test.rb4
-rw-r--r--activerecord/test/cases/multiparameter_attributes_test.rb12
-rw-r--r--activerecord/test/cases/multiple_db_test.rb9
-rw-r--r--activerecord/test/cases/nested_attributes_test.rb114
-rw-r--r--activerecord/test/cases/persistence_test.rb98
-rw-r--r--activerecord/test/cases/pooled_connections_test.rb34
-rw-r--r--activerecord/test/cases/primary_keys_test.rb131
-rw-r--r--activerecord/test/cases/query_cache_test.rb103
-rw-r--r--activerecord/test/cases/quoting_test.rb23
-rw-r--r--activerecord/test/cases/readonly_test.rb14
-rw-r--r--activerecord/test/cases/reaper_test.rb2
-rw-r--r--activerecord/test/cases/reflection_test.rb84
-rw-r--r--activerecord/test/cases/relation/delegation_test.rb2
-rw-r--r--activerecord/test/cases/relation/merging_test.rb31
-rw-r--r--activerecord/test/cases/relation/mutation_test.rb42
-rw-r--r--activerecord/test/cases/relation/or_test.rb84
-rw-r--r--activerecord/test/cases/relation/predicate_builder_test.rb6
-rw-r--r--activerecord/test/cases/relation/record_fetch_warning_test.rb28
-rw-r--r--activerecord/test/cases/relation/where_chain_test.rb130
-rw-r--r--activerecord/test/cases/relation/where_clause_test.rb182
-rw-r--r--activerecord/test/cases/relation/where_test.rb114
-rw-r--r--activerecord/test/cases/relation_test.rb129
-rw-r--r--activerecord/test/cases/relations_test.rb250
-rw-r--r--activerecord/test/cases/reload_models_test.rb2
-rw-r--r--activerecord/test/cases/result_test.rb4
-rw-r--r--activerecord/test/cases/sanitize_test.rb17
-rw-r--r--activerecord/test/cases/schema_dumper_test.rb237
-rw-r--r--activerecord/test/cases/scoping/default_scoping_test.rb124
-rw-r--r--activerecord/test/cases/scoping/named_scoping_test.rb35
-rw-r--r--activerecord/test/cases/scoping/relation_scoping_test.rb37
-rw-r--r--activerecord/test/cases/secure_token_test.rb32
-rw-r--r--activerecord/test/cases/serialization_test.rb11
-rw-r--r--activerecord/test/cases/serialized_attribute_test.rb64
-rw-r--r--activerecord/test/cases/suppressor_test.rb52
-rw-r--r--activerecord/test/cases/tasks/database_tasks_test.rb32
-rw-r--r--activerecord/test/cases/tasks/mysql_rake_test.rb27
-rw-r--r--activerecord/test/cases/tasks/postgresql_rake_test.rb51
-rw-r--r--activerecord/test/cases/tasks/sqlite_rake_test.rb2
-rw-r--r--activerecord/test/cases/test_case.rb52
-rw-r--r--activerecord/test/cases/test_fixtures_test.rb36
-rw-r--r--activerecord/test/cases/time_precision_test.rb108
-rw-r--r--activerecord/test/cases/timestamp_test.rb72
-rw-r--r--activerecord/test/cases/touch_later_test.rb114
-rw-r--r--activerecord/test/cases/transaction_callbacks_test.rb201
-rw-r--r--activerecord/test/cases/transaction_isolation_test.rb4
-rw-r--r--activerecord/test/cases/transactions_test.rb164
-rw-r--r--activerecord/test/cases/type/adapter_specific_registry_test.rb133
-rw-r--r--activerecord/test/cases/type/date_time_test.rb14
-rw-r--r--activerecord/test/cases/type/decimal_test.rb33
-rw-r--r--activerecord/test/cases/type/integer_test.rb27
-rw-r--r--activerecord/test/cases/type/string_test.rb14
-rw-r--r--activerecord/test/cases/type/type_map_test.rb47
-rw-r--r--activerecord/test/cases/type_test.rb39
-rw-r--r--activerecord/test/cases/types_test.rb163
-rw-r--r--activerecord/test/cases/unconnected_test.rb2
-rw-r--r--activerecord/test/cases/validations/absence_validation_test.rb75
-rw-r--r--activerecord/test/cases/validations/association_validation_test.rb3
-rw-r--r--activerecord/test/cases/validations/i18n_validation_test.rb11
-rw-r--r--activerecord/test/cases/validations/length_validation_test.rb92
-rw-r--r--activerecord/test/cases/validations/presence_validation_test.rb17
-rw-r--r--activerecord/test/cases/validations/uniqueness_validation_test.rb104
-rw-r--r--activerecord/test/cases/validations_repair_helper.rb10
-rw-r--r--activerecord/test/cases/validations_test.rb33
-rw-r--r--activerecord/test/cases/view_test.rb216
-rw-r--r--activerecord/test/cases/xml_serialization_test.rb447
-rw-r--r--activerecord/test/cases/yaml_serialization_test.rb35
-rw-r--r--activerecord/test/config.example.yml8
-rw-r--r--activerecord/test/fixtures/bad_posts.yml9
-rw-r--r--activerecord/test/fixtures/books.yml20
-rw-r--r--activerecord/test/fixtures/bulbs.yml5
-rw-r--r--activerecord/test/fixtures/computers.yml5
-rw-r--r--activerecord/test/fixtures/content.yml3
-rw-r--r--activerecord/test/fixtures/content_positions.yml3
-rw-r--r--activerecord/test/fixtures/dead_parrots.yml5
-rw-r--r--activerecord/test/fixtures/developers.yml3
-rw-r--r--activerecord/test/fixtures/doubloons.yml3
-rw-r--r--activerecord/test/fixtures/live_parrots.yml4
-rw-r--r--activerecord/test/fixtures/naked/csv/accounts.csv1
-rw-r--r--activerecord/test/fixtures/naked/yml/parrots.yml2
-rw-r--r--activerecord/test/fixtures/nodes.yml29
-rw-r--r--activerecord/test/fixtures/other_comments.yml6
-rw-r--r--activerecord/test/fixtures/other_posts.yml7
-rw-r--r--activerecord/test/fixtures/pirates.yml3
-rw-r--r--activerecord/test/fixtures/trees.yml3
-rw-r--r--activerecord/test/migrations/missing/1000_people_have_middle_names.rb2
-rw-r--r--activerecord/test/migrations/missing/1_people_have_last_names.rb2
-rw-r--r--activerecord/test/migrations/missing/3_we_need_reminders.rb2
-rw-r--r--activerecord/test/migrations/missing/4_innocent_jointable.rb2
-rw-r--r--activerecord/test/migrations/rename/1_we_need_things.rb2
-rw-r--r--activerecord/test/migrations/rename/2_rename_things.rb2
-rw-r--r--activerecord/test/migrations/valid/2_we_need_reminders.rb2
-rw-r--r--activerecord/test/migrations/valid/3_innocent_jointable.rb2
-rw-r--r--activerecord/test/migrations/valid_with_subdirectories/sub/2_we_need_reminders.rb2
-rw-r--r--activerecord/test/migrations/valid_with_subdirectories/sub1/3_innocent_jointable.rb2
-rw-r--r--activerecord/test/models/admin.rb2
-rw-r--r--activerecord/test/models/admin/account.rb2
-rw-r--r--activerecord/test/models/admin/randomly_named_c1.rb8
-rw-r--r--activerecord/test/models/aircraft.rb1
-rw-r--r--activerecord/test/models/author.rb11
-rw-r--r--activerecord/test/models/binary.rb2
-rw-r--r--activerecord/test/models/bird.rb2
-rw-r--r--activerecord/test/models/book.rb4
-rw-r--r--activerecord/test/models/bulb.rb2
-rw-r--r--activerecord/test/models/car.rb2
-rw-r--r--activerecord/test/models/carrier.rb2
-rw-r--r--activerecord/test/models/categorization.rb2
-rw-r--r--activerecord/test/models/chef.rb1
-rw-r--r--activerecord/test/models/comment.rb6
-rw-r--r--activerecord/test/models/company.rb9
-rw-r--r--activerecord/test/models/company_in_module.rb2
-rw-r--r--activerecord/test/models/contact.rb2
-rw-r--r--activerecord/test/models/content.rb40
-rw-r--r--activerecord/test/models/customer.rb2
-rw-r--r--activerecord/test/models/customer_carrier.rb14
-rw-r--r--activerecord/test/models/developer.rb15
-rw-r--r--activerecord/test/models/doubloon.rb12
-rw-r--r--activerecord/test/models/event.rb2
-rw-r--r--activerecord/test/models/face.rb2
-rw-r--r--activerecord/test/models/guid.rb2
-rw-r--r--activerecord/test/models/guitar.rb4
-rw-r--r--activerecord/test/models/hotel.rb1
-rw-r--r--activerecord/test/models/image.rb3
-rw-r--r--activerecord/test/models/member.rb6
-rw-r--r--activerecord/test/models/member_detail.rb7
-rw-r--r--activerecord/test/models/membership.rb15
-rw-r--r--activerecord/test/models/mentor.rb3
-rw-r--r--activerecord/test/models/node.rb5
-rw-r--r--activerecord/test/models/notification.rb2
-rw-r--r--activerecord/test/models/organization.rb2
-rw-r--r--activerecord/test/models/owner.rb2
-rw-r--r--activerecord/test/models/parrot.rb10
-rw-r--r--activerecord/test/models/person.rb3
-rw-r--r--activerecord/test/models/personal_legacy_thing.rb4
-rw-r--r--activerecord/test/models/pirate.rb8
-rw-r--r--activerecord/test/models/post.rb64
-rw-r--r--activerecord/test/models/professor.rb5
-rw-r--r--activerecord/test/models/project.rb3
-rw-r--r--activerecord/test/models/randomly_named_c1.rb2
-rw-r--r--activerecord/test/models/recipe.rb3
-rw-r--r--activerecord/test/models/ship.rb18
-rw-r--r--activerecord/test/models/ship_part.rb3
-rw-r--r--activerecord/test/models/shop_account.rb6
-rw-r--r--activerecord/test/models/topic.rb4
-rw-r--r--activerecord/test/models/treasure.rb2
-rw-r--r--activerecord/test/models/tree.rb3
-rw-r--r--activerecord/test/models/tuning_peg.rb4
-rw-r--r--activerecord/test/models/tyre.rb8
-rw-r--r--activerecord/test/models/user.rb8
-rw-r--r--activerecord/test/models/vehicle.rb7
-rw-r--r--activerecord/test/schema/mysql2_specific_schema.rb21
-rw-r--r--activerecord/test/schema/mysql_specific_schema.rb26
-rw-r--r--activerecord/test/schema/oracle_specific_schema.rb5
-rw-r--r--activerecord/test/schema/postgresql_specific_schema.rb131
-rw-r--r--activerecord/test/schema/schema.rb225
-rw-r--r--activerecord/test/support/connection.rb1
-rw-r--r--activerecord/test/support/schema_dumping_helper.rb9
-rw-r--r--activerecord/test/support/yaml_compatibility_fixtures/rails_4_1.yml22
-rw-r--r--activerecord/test/support/yaml_compatibility_fixtures/rails_4_2_0.yml182
562 files changed, 22606 insertions, 10964 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md
index b679d64472..614cee4449 100644
--- a/activerecord/CHANGELOG.md
+++ b/activerecord/CHANGELOG.md
@@ -1,1028 +1,1487 @@
-* No verbose backtrace by db:drop when database does not exist.
+* Set `scope.reordering_value` to `true` if :reordering values are specified.
- Fixes #16295.
+ Fixes #21886.
- *Kenn Ejima*
+ *Hiroaki Izu*
-* Add support for Postgresql JSONB.
+* Add support for bidirectional destroy dependencies.
+
+ Fixes #13609.
Example:
- create_table :posts do |t|
- t.jsonb :meta_data
+ class Content < ActiveRecord::Base
+ has_one :position, dependent: :destroy
end
- *Philippe Creux*, *Chris Teague*
+ class Position < ActiveRecord::Base
+ belongs_to :content, dependent: :destroy
+ end
-* `db:purge` with MySQL respects `Rails.env`.
+ *Seb Jacobs*
- *Yves Senn*
+* Includes HABTM returns correct size now. It's caused by the join dependency
+ only instantiates one HABTM object because the join table hasn't a primary key.
-* `change_column_default :table, :column, nil` with PostgreSQL will issue a
- `DROP DEFAULT` instead of a `DEFAULT NULL` query.
+ Fixes #16032.
- Fixes #16261.
+ Examples:
- *Matthew Draper*, *Yves Senn*
+ before:
+
+ Project.first.salaried_developers.size # => 3
+ Project.includes(:salaried_developers).first.salaried_developers.size # => 1
-* Allow to specify a type for the foreign key column in `references`
- and `add_reference`.
+ after:
+
+ Project.first.salaried_developers.size # => 3
+ Project.includes(:salaried_developers).first.salaried_developers.size # => 3
+
+ *Bigxiang*
+
+* Add option to index errors in nested attributes
+
+ For models which have nested attributes, errors within those models will
+ now be indexed if :index_errors is specified when defining a
+ has_many relationship, or if its set in the global config.
Example:
- change_table :vehicle do |t|
- t.references :station, type: :uuid
+ class Guitar < ActiveRecord::Base
+ has_many :tuning_pegs
+ accepts_nested_attributes_for :tuning_pegs
end
- *Andrey Novikov*, *Łukasz Sarnacki*
+ class TuningPeg < ActiveRecord::Base
+ belongs_to :guitar
+ validates_numericality_of :pitch
+ end
-* `create_join_table` removes a common prefix when generating the join table.
- This matches the existing behavior of HABTM associations.
+ - Old style
+ - `guitar.errors["tuning_pegs.pitch"] = ["is not a number"]`
- Fixes #13683.
+ - New style (if defined globally, or set in has_many_relationship)
+ - `guitar.errors["tuning_pegs[1].pitch"] = ["is not a number"]`
- *Stefan Kanev*
+ *Michael Probber and Terence Sun*
-* Dont swallow errors on compute_type when having a bad alias_method on
- a class.
+* Exit with non-zero status for failed database rake tasks.
- *arthurnn*
+ *Jay Hayes*
-* PostgreSQL invalid `uuid` are convert to nil.
+* Queries such as `Computer.joins(:monitor).group(:status).count` will now be
+ interpreted as `Computer.joins(:monitor).group('computers.status').count`
+ so that when `Computer` and `Monitor` have both `status` columns we don't
+ have conflicts in projection.
- *Abdelkader Boudih*
+ *Rafael Sales*
-* Restore 4.0 behavior for using serialize attributes with `JSON` as coder.
+* Add ability to default to `uuid` as primary key when generating database migrations
- With 4.1.x, `serialize` started returning a string when `JSON` was passed as
- the second attribute. It will now return a hash as per previous versions.
+ config.generators do |g|
+ g.orm :active_record, primary_key_type: :uuid
+ end
- Example:
+ *Jon McCartie*
- class Post < ActiveRecord::Base
- serialize :comment, JSON
- end
+* Don't cache arguments in #find_by if they are an ActiveRecord::Relation
- class Comment
- include ActiveModel::Model
- attr_accessor :category, :text
- end
+ Fixes #20817
- post = Post.create!
- post.comment = Comment.new(category: "Animals", text: "This is a comment about squirrels.")
- post.save!
+ *Hiroaki Izu*
- # 4.0
- post.comment # => {"category"=>"Animals", "text"=>"This is a comment about squirrels."}
+* Qualify column name inserted by `group` in calculation
- # 4.1 before
- post.comment # => "#<Comment:0x007f80ab48ff98>"
+ Giving `group` an unqualified column name now works, even if the relation
+ has `JOIN` with another table which also has a column of the name.
- # 4.1 after
- post.comment # => {"category"=>"Animals", "text"=>"This is a comment about squirrels."}
+ *Soutaro Matsumoto*
- When using `JSON` as the coder in `serialize`, Active Record will use the
- new `ActiveRecord::Coders::JSON` coder which delegates its `dump/load` to
- `ActiveSupport::JSON.encode/decode`. This ensures special objects are dumped
- correctly using the `#as_json` hook.
+* Don't cache prepared statements containing an IN clause or a SQL literal, as
+ these queries will change often and are unlikely to have a cache hit.
- To keep the previous behaviour, supply a custom coder instead
- ([example](https://gist.github.com/jenncoop/8c4142bbe59da77daa63)).
+ *Sean Griffin*
- Fixes #15594.
+* Fix `rewhere` in a `has_many` association.
- *Jenn Cooper*
+ Fixes #21955.
-* Do not use `RENAME INDEX` syntax for MariaDB 10.0.
+ *Josh Branchaud*, *Kal*
- Fixes #15931.
+* `where` raises ArgumentError on unsupported types.
- *Jeff Browning*
+ Fixes #20473.
-* Calling `#empty?` on a `has_many` association would use the value from the
- counter cache if one exists.
+ *Jake Worth*
- *David Verhasselt*
+* Add an immutable string type to help reduce memory usage for apps which do
+ not need mutation detection on Strings.
-* Fix the schema dump generated for tables without constraints and with
- primary key with default value of custom PostgreSQL function result.
+ *Sean Griffin*
- Fixes #16111.
+* Give `AcriveRecord::Relation#update` its own deprecation warning when
+ passed an `ActiveRecord::Base` instance.
- *Andrey Novikov*
+ Fixes #21945.
-* Fix the SQL generated when a `delete_all` is run on an association to not
- produce an `IN` statements.
+ *Ted Johansson*
- Before:
+* Make it possible to pass `:to_table` when adding a foreign key through
+ `add_reference`.
- UPDATE "categorizations" SET "category_id" = NULL WHERE
- "categorizations"."category_id" = 1 AND "categorizations"."id" IN (1, 2)
+ Fixes #21563.
- After:
+ *Yves Senn*
- UPDATE "categorizations" SET "category_id" = NULL WHERE
- "categorizations"."category_id" = 1
+* No longer pass depreacted option `-i` to `pg_dump`.
- *Eileen M. Uchitelle, Aaron Patterson*
+ *Paul Sadauskas*
-* Avoid type casting boolean and ActiveSupport::Duration values to numeric
- values for string columns. Otherwise, in some database, the string column
- values will be coerced to a numeric allowing false or 0.seconds match any
- string starting with a non-digit.
+* Concurrent `AR::Base#increment!` and `#decrement!` on the same record
+ are all reflected in the database rather than overwriting each other.
- Example:
+ *Bogdan Gusiev*
- App.where(apikey: false) # => SELECT * FROM users WHERE apikey = '0'
+* Avoid leaking the first relation we call `first` on, per model.
- *Dylan Thacker-Smith*
+ Fixes #21921.
-* Add a `:required` option to singular associations, providing a nicer
- API for presence validations on associations.
+ *Matthew Draper*, *Jean Boussier*
+
+* Remove unused `pk_and_sequence_for` in AbstractMysqlAdapter.
+
+ *Ryuta Kamizono*
+
+* Allow fixtures files to set the model class in the YAML file itself.
+
+ To load the fixtures file `accounts.yml` as the `User` model, use:
+
+ _fixture:
+ model_class: User
+ david:
+ name: David
+
+ Fixes #9516.
+
+ *Roque Pinel*
+
+* Don't require a database connection to load a class which uses acceptance
+ validations.
*Sean Griffin*
-* Fixed error in `reset_counters` when associations have `select` scope.
- (Call to `count` generates invalid SQL.)
+* Correctly apply `unscope` when preloading through associations.
- *Cade Truitt*
+ *Jimmy Bourassa*
-* After a successful `reload`, `new_record?` is always false.
+* Fixed taking precision into count when assigning a value to timestamp attribute
- Fixes #12101.
+ Timestamp column can have less precision than ruby timestamp
+ In result in how big a fraction of a second can be stored in the
+ database.
- *Matthew Draper*
-* PostgreSQL renaming table doesn't attempt to rename non existent sequences.
+ m = Model.create!
+ m.created_at.usec == m.reload.created_at.usec # => false
+ # due to different precision in Time.now and database column
- *Abdelkader Boudih*
+ If the precision is low enough, (mysql default is 0, so it is always low
+ enough by default) the value changes when model is reloaded from the
+ database. This patch fixes that issue ensuring that any timestamp
+ assigned as an attribute is converted to column precision under the
+ attribute.
-* Move 'dependent: :destroy' handling for 'belongs_to'
- from 'before_destroy' to 'after_destroy' callback chain
+ *Bogdan Gusiev*
- Fixes #12380.
+* Introduce `connection.data_sources` and `connection.data_source_exists?`.
+ These methods determine what relations can be used to back Active Record
+ models (usually tables and views).
- *Ivan Antropov*
+ Also deprecate `SchemaCache#tables`, `SchemaCache#table_exists?` and
+ `SchemaCache#clear_table_cache!` in favor of their new data source
+ counterparts.
-* Detect in-place modifications on String attributes.
+ *Yves Senn*, *Matthew Draper*
- Before this change user have to mark the attribute as changed to it be persisted
- in the database. Now it is not required anymore.
+* Add `ActiveRecord::Base.ignored_columns` to make some columns
+ invisible from ActiveRecord.
- Before:
+ *Jean Boussier*
- user = User.first
- user.name << ' Griffin'
- user.name_will_change!
- user.save
- user.reload.name # => "Sean Griffin"
+* `ActiveRecord::Tasks::MySQLDatabaseTasks` fails if shellout to
+ mysql commands (like `mysqldump`) is not successful.
- After:
+ *Steve Mitchell*
- user = User.first
- user.name << ' Griffin'
- user.save
- user.reload.name # => "Sean Griffin"
+* Ensure `select` quotes aliased attributes, even when using `from`.
- *Sean Griffin*
+ Fixes #21488
+
+ *Sean Griffin & @johanlunds*
+
+* MySQL: support `unsigned` numeric data types.
+
+ Example:
+
+ create_table :foos do |t|
+ t.unsigned_integer :quantity
+ t.unsigned_bigint :total
+ t.unsigned_float :percentage
+ t.unsigned_decimal :price, precision: 10, scale: 2
+ end
+
+ The `unsigned: true` option may be used for the primary key:
-* Add `ActiveRecord::Base#validate!` that raises `RecordInvalid` if the record
- is invalid.
+ create_table :foos, id: :bigint, unsigned: true do |t|
+ …
+ end
+
+ *Ryuta Kamizono*
+
+* Add `#views` and `#view_exists?` methods on connection adapters.
+
+ *Ryuta Kamizono*
+
+* Correctly dump composite primary key.
+
+ Example:
+
+ create_table :barcodes, primary_key: ["region", "code"] do |t|
+ t.string :region
+ t.integer :code
+ end
+
+ *Ryuta Kamizono*
+
+* Lookup the attribute name for `restrict_with_error` messages on the
+ model class that defines the association.
+
+ *kuboon*, *Ronak Jangir*
+
+* Correct query for PostgreSQL 8.2 compatibility.
+
+ *Ben Murphy*, *Matthew Draper*
+
+* `bin/rake db:migrate` uses
+ `ActiveRecord::Tasks::DatabaseTasks.migrations_paths` instead of
+ `Migrator.migrations_paths`.
+
+ *Tobias Bielohlawek*
+
+* Support dropping indexes concurrently in PostgreSQL.
- *Bogdan Gusiev*, *Marc Schütz*
+ See http://www.postgresql.org/docs/9.4/static/sql-dropindex.html for more
+ details.
-* Support for adding and removing foreign keys. Foreign keys are now
- a part of `schema.rb`. This is supported by Mysql2Adapter, MysqlAdapter
- and PostgreSQLAdapter.
+ *Grey Baker*
- Many thanks to *Matthew Higgins* for laying the foundation with his work on
- [foreigner](https://github.com/matthuhiggins/foreigner).
+* Deprecate passing conditions to `ActiveRecord::Relation#delete_all`
+ and `ActiveRecord::Relation#destroy_all`.
+
+ *Wojciech Wnętrzak*
+
+* PostgreSQL, `create_schema`, `drop_schema` and `rename_table` now quote
+ schema names.
+
+ Fixes #21418.
Example:
- # within your migrations:
- add_foreign_key :articles, :authors
- remove_foreign_key :articles, :authors
+ create_schema("my.schema")
+ # CREATE SCHEMA "my.schema";
*Yves Senn*
-* Fix subtle bugs regarding attribute assignment on models with no primary
- key. `'id'` will no longer be part of the attributes hash.
+* PostgreSQL, add `:if_exists` option to `#drop_schema`. This makes it
+ possible to drop a schema that might exist without raising an exception if
+ it doesn't.
- *Sean Griffin*
+ *Yves Senn*
-* Deprecate automatic counter caches on `has_many :through`. The behavior was
- broken and inconsistent.
+* Only try to nullify has_one target association if the record is persisted.
- *Sean Griffin*
+ Fixes #21223.
+
+ *Agis Anastasopoulos*
+
+* Uniqueness validator raises descriptive error when running on a persisted
+ record without primary key.
+
+ Fixes #21304.
+
+ *Yves Senn*
+
+* Add a native JSON data type support in MySQL.
+
+ Example:
+
+ create_table :json_data_type do |t|
+ t.json :settings
+ end
+
+ *Ryuta Kamizono*
-* `preload` preserves readonly flag for associations.
+* Descriptive error message when fixtures contain a missing column.
- See #15853.
+ Fixes #21201.
*Yves Senn*
-* Assume numeric types have changed if they were assigned to a value that
- would fail numericality validation, regardless of the old value. Previously
- this would only occur if the old value was 0.
+* `ActiveRecord::Tasks::PostgreSQLDatabaseTasks` fail if shellout to
+ postgresql commands (like `pg_dump`) is not successful.
+
+ *Bryan Paxton*, *Nate Berkopec*
+
+* Add `ActiveRecord::Relation#in_batches` to work with records and relations
+ in batches.
+
+ Available options are `of` (batch size), `load`, `begin_at`, and `end_at`.
+
+ Examples:
+
+ Person.in_batches.each_record(&:party_all_night!)
+ Person.in_batches.update_all(awesome: true)
+ Person.in_batches.delete_all
+ Person.in_batches.each do |relation|
+ relation.delete_all
+ sleep 10 # Throttles the delete queries
+ end
+
+ Fixes #20933.
+
+ *Sina Siadat*
+
+* Added methods for PostgreSQL geometric data types to use in migrations.
+
+ Example:
+
+ create_table :foo do |t|
+ t.line :foo_line
+ t.lseg :foo_lseg
+ t.box :foo_box
+ t.path :foo_path
+ t.polygon :foo_polygon
+ t.circle :foo_circle
+ end
+
+ *Mehmet Emin İNAÇ*
+
+* Add `cache_key` to ActiveRecord::Relation.
Example:
- model = Model.create!(number: 5)
- model.number = '5wibble'
- model.number_changed? # => true
+ @users = User.where("name like ?", "%Alberto%")
+ @users.cache_key
+ # => "/users/query-5942b155a43b139f2471b872ac54251f-3-20150714212107656125000"
+
+ *Alberto Fernández-Capel*
+
+* Properly allow uniqueness validations on primary keys.
- Fixes #14731.
+ Fixes #20966.
+
+ *Sean Griffin*, *presskey*
+
+* Don't raise an error if an association failed to destroy when `destroy` was
+ called on the parent (as opposed to `destroy!`).
+
+ Fixes #20991.
*Sean Griffin*
-* `reload` no longer merges with the existing attributes.
- The attribute hash is fully replaced. The record is put into the same state
- as it would be with `Model.find(model.id)`.
+* `ActiveRecord::RecordNotFound` modified to store model name, primary_key and
+ id of the caller model. It allows the catcher of this exception to make
+ a better decision to what to do with it.
+
+ Example:
+
+ class SomeAbstractController < ActionController::Base
+ rescue_from ActiveRecord::RecordNotFound, with: :redirect_to_404
+
+ private def redirect_to_404(e)
+ return redirect_to(posts_url) if e.model == 'Post'
+ raise
+ end
+ end
+
+ *Sameer Rahmani*
+
+* Deprecate the keys for association `restrict_dependent_destroy` errors in favor
+ of new key names.
+
+ Previously `has_one` and `has_many` associations were using the
+ `one` and `many` keys respectively. Both of these keys have special
+ meaning in I18n (they are considered to be pluralizations) so by
+ renaming them to `has_one` and `has_many` we make the messages more explicit
+ and most importantly they don't clash with linguistical systems that need to
+ validate translation keys (and their pluralizations).
+
+ The `:'restrict_dependent_destroy.one'` key should be replaced with
+ `:'restrict_dependent_destroy.has_one'`, and `:'restrict_dependent_destroy.many'`
+ with `:'restrict_dependent_destroy.has_many'`.
+
+ *Roque Pinel*, *Christopher Dell*
+
+* Fix state being carried over from previous transaction.
+
+ Considering the following example where `name` is a required attribute.
+ Before we had `new_record?` returning `true` for a persisted record:
+
+ author = Author.create! name: 'foo'
+ author.name = nil
+ author.save # => false
+ author.new_record? # => true
+
+ Fixes #20824.
+
+ *Roque Pinel*
+
+* Correctly ignore `mark_for_destruction` when `autosave` isn't set to `true`
+ when validating associations.
+
+ Fixes #20882.
*Sean Griffin*
-* The object returned from `select_all` must respond to `column_types`.
- If this is not the case a `NoMethodError` is raised.
+* Fix a bug where counter_cache doesn't always work with polymorphic
+ relations.
+
+ Fixes #16407.
+
+ *Stefan Kanev*, *Sean Griffin*
+
+* Ensure that cyclic associations with autosave don't cause duplicate errors
+ to be added to the parent record.
+
+ Fixes #20874.
*Sean Griffin*
-* `has_many :through` associations will no longer save the through record
- twice when added in an `after_create` callback defined before the
- associations.
+* Ensure that `ActionController::Parameters` can still be passed to nested
+ attributes.
- Fixes #3798.
+ Fixes #20922.
*Sean Griffin*
-* Detect in-place modifications of PG array types
+* Deprecate force association reload by passing a truthy argument to
+ association method.
+
+ For collection association, you can call `#reload` on association proxy to
+ force a reload:
+
+ @user.posts.reload # Instead of @user.posts(true)
+
+ For singular association, you can call `#reload` on the parent object to
+ clear its association cache then call the association method:
+
+ @user.reload.profile # Instead of @user.profile(true)
+
+ Passing a truthy argument to force association to reload will be removed in
+ Rails 5.1.
+
+ *Prem Sichanugrist*
+
+* Replaced `ActiveSupport::Concurrency::Latch` with `Concurrent::CountDownLatch`
+ from the concurrent-ruby gem.
+
+ *Jerry D'Antonio*
+
+* Fix through associations using scopes having the scope merged multiple
+ times.
+
+ Fixes #20721.
+ Fixes #20727.
*Sean Griffin*
-* Add `bin/rake db:purge` task to empty the current database.
+* `ActiveRecord::Base.dump_schema_after_migration` applies migration tasks
+ other than `db:migrate`. (eg. `db:rollback`, `db:migrate:dup`, ...)
+
+ Fixes #20743.
*Yves Senn*
-* Deprecate `serialized_attributes` without replacement.
+* Add alternate syntax to make `change_column_default` reversible.
- *Sean Griffin*
+ User can pass in `:from` and `:to` to make `change_column_default` command
+ become reversible.
+
+ Example:
+
+ change_column_default :posts, :status, from: nil, to: "draft"
+ change_column_default :users, :authorized, from: true, to: false
+
+ *Prem Sichanugrist*
+
+* Prevent error when using `force_reload: true` on an unassigned polymorphic
+ belongs_to association.
+
+ Fixes #20426.
+
+ *James Dabbs*
-* Correctly extract IPv6 addresses from `DATABASE_URI`: the square brackets
- are part of the URI structure, not the actual host.
+* Correctly raise `ActiveRecord::AssociationTypeMismatch` when assigning
+ a wrong type to a namespaced association.
- Fixes #15705.
+ Fixes #20545.
- *Andy Bakun*, *Aaron Stone*
+ *Diego Carrion*
-* Ensure both parent IDs are set on join records when both sides of a
- through association are new.
+* `validates_absence_of` respects `marked_for_destruction?`.
+
+ Fixes #20449.
+
+ *Yves Senn*
+
+* Include the `Enumerable` module in `ActiveRecord::Relation`
+
+ *Sean Griffin & bogdan*
+
+* Use `Enumerable#sum` in `ActiveRecord::Relation` if a block is given.
*Sean Griffin*
-* `ActiveRecord::Dirty` now detects in-place changes to mutable values.
- Serialized attributes on ActiveRecord models will no longer save when
- unchanged.
+* Let `WITH` queries (Common Table Expressions) be explainable.
+
+ *Vladimir Kochnev*
+
+* Make `remove_index :table, :column` reversible.
+
+ *Yves Senn*
+
+* Fixed an error which would occur in dirty checking when calling
+ `update_attributes` from a getter.
- Fixes #8328.
+ Fixes #20531.
*Sean Griffin*
-* Pluck now works when selecting columns from different tables with the same
- name.
+* Make `remove_foreign_key` reversible. Any foreign key options must be
+ specified, similar to `remove_column`.
+
+ *Aster Ryan*
+
+* Add `:_prefix` and `:_suffix` options to `enum` definition.
+
+ Fixes #17511, #17415.
+
+ *Igor Kapkov*
- Fixes #15649.
+* Correctly handle decimal arrays with defaults in the schema dumper.
+
+ Fixes #20515.
+
+ *Sean Griffin & jmondo*
+
+* Deprecate the PostgreSQL `:point` type in favor of a new one which will return
+ `Point` objects instead of an `Array`
*Sean Griffin*
-* Remove `cache_attributes` and friends. All attributes are cached.
+* Ensure symbols passed to `ActiveRecord::Relation#select` are always treated
+ as columns.
+
+ Fixes #20360.
*Sean Griffin*
-* Remove deprecated method `ActiveRecord::Base.quoted_locking_column`.
+* Do not set `sql_mode` if `strict: :default` is specified.
+
+ # config/database.yml
+ production:
+ adapter: mysql2
+ database: foo_prod
+ user: foo
+ strict: :default
+
+ *Ryuta Kamizono*
+
+* Allow proc defaults to be passed to the attributes API. See documentation
+ for examples.
+
+ *Sean Griffin*, *Kir Shatrov*
+
+* SQLite: `:collation` support for string and text columns.
+
+ Example:
+
+ create_table :foo do |t|
+ t.string :string_nocase, collation: 'NOCASE'
+ t.text :text_rtrim, collation: 'RTRIM'
+ end
+
+ add_column :foo, :title, :string, collation: 'RTRIM'
+
+ change_column :foo, :title, :string, collation: 'NOCASE'
*Akshay Vishnoi*
-* `ActiveRecord::FinderMethods.find` with block can handle proc parameter as
- `Enumerable#find` does.
+* Allow the use of symbols or strings to specify enum values in test
+ fixtures:
- Fixes #15382.
+ awdr:
+ title: "Agile Web Development with Rails"
+ status: :proposed
- *James Yang*
+ *George Claghorn*
-* Make timezone aware attributes work with PostgreSQL array columns.
+* Clear query cache when `ActiveRecord::Base#reload` is called.
- Fixes #13402.
+ *Shane Hender, Pierre Nespo*
- *Kuldeep Aggarwal*, *Sean Griffin*
+* Include stored procedures and function on the MySQL structure dump.
-* `ActiveRecord::SchemaMigration` has no primary key regardless of the
- `primary_key_prefix_type` configuration.
+ *Jonathan Worek*
- Fixes #15051.
+* Pass `:extend` option for `has_and_belongs_to_many` associations to the
+ underlying `has_many :through`.
- *JoseLuis Torres*, *Yves Senn*
+ *Jaehyun Shin*
-* `rake db:migrate:status` works with legacy migration numbers like `00018_xyz.rb`.
+* Deprecate `Relation#uniq` use `Relation#distinct` instead.
- Fixes #15538.
+ See #9683.
*Yves Senn*
-* Baseclass becomes! subclass.
+* Allow single table inheritance instantiation to work when storing
+ demodulized class names.
- Before this change, a record which changed its STI type, could not be
- updated.
+ *Alex Robbin*
- Fixes #14785.
+* Correctly pass MySQL options when using `structure_dump` or
+ `structure_load`.
- *Matthew Draper*, *Earl St Sauver*, *Edo Balvers*
+ Specifically, it fixes an issue when using SSL authentication.
-* Remove deprecated `ActiveRecord::Migrator.proper_table_name`. Use the
- `proper_table_name` instance method on `ActiveRecord::Migration` instead.
+ *Alex Coomans*
- *Akshay Vishnoi*
+* Dump indexes in `create_table` instead of `add_index`.
-* Fix regression on eager loading association based on SQL query rather than
- existing column.
+ If the adapter supports indexes in `create_table`, generated SQL is
+ slightly more efficient.
- Fixes #15480.
+ *Ryuta Kamizono*
- *Lauro Caetano*, *Carlos Antonio da Silva*
+* Correctly dump `:options` on `create_table` for MySQL.
-* Deprecate returning `nil` from `column_for_attribute` when no column exists.
- It will return a null object in Rails 5.0
+ *Ryuta Kamizono*
- *Sean Griffin*
+* PostgreSQL: `:collation` support for string and text columns.
-* Implemented ActiveRecord::Base#pretty_print to work with PP.
+ Example:
- *Ethan*
+ create_table :foos do |t|
+ t.string :string_en, collation: 'en_US.UTF-8'
+ t.text :text_ja, collation: 'ja_JP.UTF-8'
+ end
-* Preserve type when dumping PostgreSQL point, bit, bit varying and money
- columns.
+ *Ryuta Kamizono*
- *Yves Senn*
+* Remove `ActiveRecord::Serialization::XmlSerializer` from core.
-* New records remain new after YAML serialization.
+ *Zachary Scott*
- *Sean Griffin*
+* Make `unscope` aware of "less than" and "greater than" conditions.
-* PostgreSQL support default values for enum types. Fixes #7814.
+ *TAKAHASHI Kazuaki*
- *Yves Senn*
+* `find_by` and `find_by!` raise `ArgumentError` when called without
+ arguments.
+
+ *Kohei Suzuki*
-* PostgreSQL `default_sequence_name` respects schema. Fixes #7516.
+* Revert behavior of `db:schema:load` back to loading the full
+ environment. This ensures that initializers are run.
+
+ Fixes #19545.
*Yves Senn*
-* Fixed `columns_for_distinct` of postgresql adapter to work correctly
- with orders without sort direction modifiers.
+* Fix missing index when using `timestamps` with the `index` option.
- *Nikolay Kondratyev*
+ The `index` option used with `timestamps` should be passed to both
+ `column` definitions for `created_at` and `updated_at` rather than just
+ the first.
-* PostgreSQL `reset_pk_sequence!` respects schemas. Fixes #14719.
+ *Paul Mucur*
- *Yves Senn*
+* Rename `:class` to `:anonymous_class` in association options.
-* Keep PostgreSQL `hstore` and `json` attributes as `Hash` in `@attributes`.
- Fixes duplication in combination with `store_accessor`.
+ Fixes #19659.
- Fixes #15369.
+ *Andrew White*
- *Yves Senn*
+* Autosave existing records on a has many through association when the parent
+ is new.
-* `rake railties:install:migrations` respects the order of railties.
+ Fixes #19782.
- *Arun Agrawal*
+ *Sean Griffin*
-* Fix redefine a has_and_belongs_to_many inside inherited class
- Fixing regression case, where redefining the same has_an_belongs_to_many
- definition into a subclass would raise.
+* Fixed a bug where uniqueness validations would error on out of range values,
+ even if an validation should have prevented it from hitting the database.
- Fixes #14983.
+ *Andrey Voronkov*
- *arthurnn*
+* MySQL: `:charset` and `:collation` support for string and text columns.
+
+ Example:
-* Fix has_and_belongs_to_many public reflection.
- When defining a has_and_belongs_to_many, internally we convert that to two has_many.
- But as `reflections` is a public API, people expect to see the right macro.
+ create_table :foos do |t|
+ t.string :string_utf8_bin, charset: 'utf8', collation: 'utf8_bin'
+ t.text :text_ascii, charset: 'ascii'
+ end
- Fixes #14682.
+ *Ryuta Kamizono*
- *arthurnn*
+* Foreign key related methods in the migration DSL respect
+ `ActiveRecord::Base.pluralize_table_names = false`.
+
+ Fixes #19643.
+
+ *Mehmet Emin İNAÇ*
-* Fixed serialization for records with an attribute named `format`.
+* Reduce memory usage from loading types on PostgreSQL.
- Fixes #15188.
+ Fixes #19578.
- *Godfrey Chan*
+ *Sean Griffin*
-* When a `group` is set, `sum`, `size`, `average`, `minimum` and `maximum`
- on a NullRelation should return a Hash.
+* Add `config.active_record.warn_on_records_fetched_greater_than` option.
- *Kuldeep Aggarwal*
+ When set to an integer, a warning will be logged whenever a result set
+ larger than the specified size is returned by a query.
-* Fixed serialized fields returning serialized data after being updated with
- `update_column`.
+ Fixes #16463.
- *Simon Hørup Eskildsen*
+ *Jason Nochlin*
-* Fixed polymorphic eager loading when using a String as foreign key.
+* Ignore `.psqlrc` when loading database structure.
- Fixes #14734.
+ *Jason Weathered*
- *Lauro Caetano*
+* Fix referencing wrong table aliases while joining tables of has many through
+ association (only when calling calculation methods).
-* Change belongs_to touch to be consistent with timestamp updates
+ Fixes #19276.
- If a model is set up with a belongs_to: touch relationship the parent
- record will only be touched if the record was modified. This makes it
- consistent with timestamp updating on the record itself.
+ *pinglamb*
- *Brock Trappitt*
+* Correctly persist a serialized attribute that has been returned to
+ its default value by an in-place modification.
-* Fixed the inferred table name of a has_and_belongs_to_many auxiliar
- table inside a schema.
+ Fixes #19467.
- Fixes #14824.
+ *Matthew Draper*
- *Eric Chahin*
+* Fix generating the schema file when using PostgreSQL `BigInt[]` data type.
+ Previously the `limit: 8` was not coming through, and this caused it to
+ become `Int[]` data type after rebuilding from the schema.
-* Remove unused `:timestamp` type. Transparently alias it to `:datetime`
- in all cases. Fixes inconsistencies when column types are sent outside of
- `ActiveRecord`, such as for XML Serialization.
+ Fixes #19420.
- *Sean Griffin*
+ *Jake Waller*
-* Fix bug that added `table_name_prefix` and `table_name_suffix` to
- extension names in PostgreSQL when migrating.
+* Reuse the `CollectionAssociation#reader` cache when the foreign key is
+ available prior to save.
- *Joao Carlos*
+ *Ben Woosley*
-* The `:index` option in migrations, which previously was only available for
- `references`, now works with any column types.
+* Add `config.active_record.dump_schemas` to fix `db:structure:dump`
+ when using schema_search_path and PostgreSQL extensions.
- *Marc Schütz*
+ Fixes #17157.
-* Add support for counter name to be passed as parameter on `CounterCache::ClassMethods#reset_counters`.
+ *Ryan Wallace*
- *jnormore*
+* Renaming `use_transactional_fixtures` to `use_transactional_tests` for clarity.
-* Restrict deletion of record when using `delete_all` with `uniq`, `group`, `having`
- or `offset`.
+ Fixes #18864.
- In these cases the generated query ignored them and that caused unintended
- records to be deleted.
+ *Brandon Weiss*
- Fixes #11985.
+* Increase pg gem version requirement to `~> 0.18`. Earlier versions of the
+ pg gem are known to have problems with Ruby 2.2.
- *Leandro Facchinetti*
+ *Matt Brictson*
-* Floats with limit >= 25 that get turned into doubles in MySQL no longer have
- their limit dropped from the schema.
+* Correctly dump `serial` and `bigserial`.
- Fixes #14135.
+ *Ryuta Kamizono*
- *Aaron Nelson*
+* Fix default `format` value in `ActiveRecord::Tasks::DatabaseTasks#schema_file`.
-* Fix how to calculate associated class name when using namespaced has_and_belongs_to_many
- association.
+ *James Cox*
- Fixes #14709.
+* Don't enroll records in the transaction if they don't have commit callbacks.
+ This was causing a memory leak when creating many records inside a transaction.
- *Kassio Borges*
+ Fixes #15549.
-* `ActiveRecord::Relation::Merger#filter_binds` now compares equivalent symbols and
- strings in column names as equal.
+ *Will Bryant*, *Aaron Patterson*
- This fixes a rare case in which more bind values are passed than there are
- placeholders for them in the generated SQL statement, which can make PostgreSQL
- throw a `StatementInvalid` exception.
+* Correctly create through records when created on a has many through
+ association when using `where`.
- *Nat Budin*
+ Fixes #19073.
-* Fix `stored_attributes` to correctly merge the details of stored
- attributes defined in parent classes.
+ *Sean Griffin*
- Fixes #14672.
+* Add `SchemaMigration.create_table` support for any unicode charsets with MySQL.
- *Brad Bennett*, *Jessica Yao*, *Lakshmi Parthasarathy*
+ *Ryuta Kamizono*
-* `change_column_default` allows `[]` as argument to `change_column_default`.
+* PostgreSQL no longer disables user triggers if system triggers can't be
+ disabled. Disabling user triggers does not fulfill what the method promises.
+ Rails currently requires superuser privileges for this method.
- Fixes #11586.
+ If you absolutely rely on this behavior, consider patching
+ `disable_referential_integrity`.
*Yves Senn*
-* Handle `name` and `"char"` column types in the PostgreSQL adapter.
+* Restore aborted transaction state when `disable_referential_integrity` fails
+ due to missing permissions.
- `name` and `"char"` are special character types used internally by
- PostgreSQL and are used by internal system catalogs. These field types
- can sometimes show up in structure-sniffing queries that feature internal system
- structures or with certain PostgreSQL extensions.
+ *Toby Ovod-Everett*, *Yves Senn*
- *J Smith*, *Yves Senn*
+* In PostgreSQL, print a warning message if `disable_referential_integrity`
+ fails due to missing permissions.
-* Fix `PostgreSQLAdapter::OID::Float#type_cast` to convert Infinity and
- NaN PostgreSQL values into a native Ruby `Float::INFINITY` and `Float::NAN`
+ *Andrey Nering*, *Yves Senn*
- Before:
+* Allow a `:limit` option for MySQL bigint primary key support.
- Point.create(value: 1.0/0)
- Point.last.value # => 0.0
+ Example:
- After:
+ create_table :foos, id: :primary_key, limit: 8 do |t|
+ end
- Point.create(value: 1.0/0)
- Point.last.value # => Infinity
+ # or
+
+ create_table :foos, id: false do |t|
+ t.primary_key :id, limit: 8
+ end
- *Innokenty Mikhailov*
+ *Ryuta Kamizono*
-* Allow the PostgreSQL adapter to handle bigserial primary key types again.
+* `belongs_to` will now trigger a validation error by default if the association is not present.
+ You can turn this off on a per-association basis with `optional: true`.
+ (Note this new default only applies to new Rails apps that will be generated with
+ `config.active_record.belongs_to_required_by_default = true` in initializer.)
- Fixes #10410.
+ *Josef Šimánek*
- *Patrick Robertson*
+* Fixed `ActiveRecord::Relation#becomes!` and `changed_attributes` issues for type
+ columns.
-* Deprecate joining, eager loading and preloading of instance dependent
- associations without replacement. These operations happen before instances
- are created. The current behavior is unexpected and can result in broken
- behavior.
+ Fixes #17139.
- Fixes #15024.
+ *Miklos Fazekas*
- *Yves Senn*
+* Format the time string according to the precision of the time column.
-* Fixed has_and_belongs_to_many's CollectionAssociation size calculation.
+ *Ryuta Kamizono*
- has_and_belongs_to_many should fall back to using the normal CollectionAssociation's
- size calculation if the collection is not cached or loaded.
+* Allow a `:precision` option for time type columns.
- Fixes #14913, #14914.
+ *Ryuta Kamizono*
- *Fred Wu*
+* Add `ActiveRecord::Base.suppress` to prevent the receiver from being saved
+ during the given block.
-* Return a non zero status when running `rake db:migrate:status` and migration table does
- not exist.
+ For example, here's a pattern of creating notifications when new comments
+ are posted. (The notification may in turn trigger an email, a push
+ notification, or just appear in the UI somewhere):
- *Paul B.*
+ class Comment < ActiveRecord::Base
+ belongs_to :commentable, polymorphic: true
+ after_create -> { Notification.create! comment: self,
+ recipients: commentable.recipients }
+ end
-* Add support for module-level `table_name_suffix` in models.
+ That's what you want the bulk of the time. A new comment creates a new
+ Notification. There may be edge cases where you don't want that, like
+ when copying a commentable and its comments, in which case write a
+ concern with something like this:
+
+ module Copyable
+ def copy_to(destination)
+ Notification.suppress do
+ # Copy logic that creates new comments that we do not want triggering
+ # notifications.
+ end
+ end
+ end
- This makes `table_name_suffix` work the same way as `table_name_prefix` when
- using namespaced models.
+ *Michael Ryan*
- *Jenner LaFave*
+* `:time` option added for `#touch`.
-* Revert the behaviour of `ActiveRecord::Relation#join` changed through 4.0 => 4.1 to 4.0.
+ Fixes #18905.
- In 4.1.0 `Relation#join` is delegated to `Arel#SelectManager`.
- In 4.0 series it is delegated to `Array#join`.
+ *Hyonjee Joo*
- *Bogdan Gusiev*
+* Deprecate passing of `start` value to `find_in_batches` and `find_each`
+ in favour of `begin_at` value.
-* Log nil binary column values correctly.
+ *Vipul A M*
- When an object with a binary column is updated with a nil value
- in that column, the SQL logger would throw an exception when trying
- to log that nil value. This only occurs when updating a record
- that already has a non-nil value in that column since an initial nil
- value isn't included in the SQL anyway (at least, when dirty checking
- is enabled.) The column's new value will now be logged as `<NULL binary data>`
- to parallel the existing `<N bytes of binary data>` for non-nil values.
+* Add `foreign_key_exists?` method.
- *James Coleman*
+ *Tõnis Simo*
-* Rails will now pass a custom validation context through to autosave associations
- in order to validate child associations with the same context.
+* Use SQL COUNT and LIMIT 1 queries for `none?` and `one?` methods
+ if no block or limit is given, instead of loading the entire
+ collection into memory. This applies to relations (e.g. `User.all`)
+ as well as associations (e.g. `account.users`)
- Fixes #13854.
+ # Before:
- *Eric Chahin*, *Aaron Nelson*, *Kevin Casey*
+ users.none?
+ # SELECT "users".* FROM "users"
-* Stringify all variables keys of MySQL connection configuration.
+ users.one?
+ # SELECT "users".* FROM "users"
- When `sql_mode` variable for MySQL adapters set in configuration as `String`
- was ignored and overwritten by strict mode option.
+ # After:
- Fixes #14895.
+ users.none?
+ # SELECT 1 AS one FROM "users" LIMIT 1
- *Paul Nikitochkin*
+ users.one?
+ # SELECT COUNT(*) FROM "users"
-* Ensure SQLite3 statements are closed on errors.
+ *Eugene Gilburg*
- Fixes #13631.
+* Have `enum` perform type casting consistently with the rest of Active
+ Record, such as `where`.
- *Timur Alperovich*
+ *Sean Griffin*
-* Give ActiveRecord::PredicateBuilder private methods the privacy they deserve.
+* `scoping` no longer pollutes the current scope of sibling classes when using
+ STI. e.x.
- *Hector Satre*
+ StiOne.none.scoping do
+ StiTwo.all
+ end
-* When using a custom `join_table` name on a `habtm`, rails was not saving it
- on Reflections. This causes a problem when rails loads fixtures, because it
- uses the reflections to set database with fixtures.
+ Fixes #18806.
- Fixes #14845.
+ *Sean Griffin*
- *Kassio Borges*
+* `remove_reference` with `foreign_key: true` removes the foreign key before
+ removing the column. This fixes a bug where it was not possible to remove
+ the column on MySQL.
-* Reset the cache when modifying a Relation with cached Arel.
- Additionally display a warning message to make the user aware.
+ Fixes #18664.
*Yves Senn*
-* PostgreSQL should internally use `:datetime` consistently for TimeStamp. Assures
- different spellings of timestamps are treated the same.
+* `find_in_batches` now accepts an `:end_at` parameter that complements the `:start`
+ parameter to specify where to stop batch processing.
- Example:
+ *Vipul A M*
- mytimestamp.simplified_type('timestamp without time zone')
- # => :datetime
- mytimestamp.simplified_type('timestamp(6) without time zone')
- # => also :datetime (previously would be :timestamp)
+* Fix a rounding problem for PostgreSQL timestamp columns.
- See #14513.
+ If a timestamp column has a precision specified, it needs to
+ format according to that.
- *Jefferson Lai*
+ *Ryuta Kamizono*
-* `ActiveRecord::Base.no_touching` no longer triggers callbacks or start empty transactions.
+* Respect the database default charset for `schema_migrations` table.
- Fixes #14841.
+ The charset of `version` column in `schema_migrations` table depends
+ on the database default charset and collation rather than the encoding
+ of the connection.
- *Lucas Mazza*
+ *Ryuta Kamizono*
-* Fix name collision with `Array#select!` with `Relation#select!`.
+* Raise `ArgumentError` when passing `nil` or `false` to `Relation#merge`.
- Fixes #14752.
+ These are not valid values to merge in a relation, so it should warn users
+ early.
- *Earl St Sauver*
+ *Rafael Mendonça França*
-* Fixed unexpected behavior for `has_many :through` associations going through a scoped `has_many`.
+* Use `SCHEMA` instead of `DB_STRUCTURE` for specifying a structure file.
- If a `has_many` association is adjusted using a scope, and another `has_many :through`
- uses this association, then the scope adjustment is unexpectedly neglected.
+ This makes the db:structure tasks consistent with test:load_structure.
- Fixes #14537.
+ *Dieter Komendera*
- *Jan Habermann*
+* Respect custom primary keys for associations when calling `Relation#where`
-* `@destroyed` should always be set to `false` when an object is duped.
+ Fixes #18813.
- *Kuldeep Aggarwal*
+ *Sean Griffin*
-* Fixed has_many association to make it support irregular inflections.
+* Fix several edge cases which could result in a counter cache updating
+ twice or not updating at all for `has_many` and `has_many :through`.
- Fixes #8928.
+ Fixes #10865.
- *arthurnn*, *Javier Goizueta*
+ *Sean Griffin*
-* Fixed a problem where count used with a grouping was not returning a Hash.
+* Foreign keys added by migrations were given random, generated names. This
+ meant a different `structure.sql` would be generated every time a developer
+ ran migrations on their machine.
- Fixes #14721.
+ The generated part of foreign key names is now a hash of the table name and
+ column name, which is consistent every time you run the migration.
- *Eric Chahin*
+ *Chris Sinjakli*
-* `sanitize_sql_like` helper method to escape a string for safe use in an SQL
- LIKE statement.
+* Validation errors would be raised for parent records when an association
+ was saved when the parent had `validate: false`. It should not be the
+ responsibility of the model to validate an associated object unless the
+ object was created or modified by the parent.
- Example:
+ This fixes the issue by skipping validations if the parent record is
+ persisted, not changed, and not marked for destruction.
- class Article
- def self.search(term)
- where("title LIKE ?", sanitize_sql_like(term))
- end
- end
+ Fixes #17621.
- Article.search("20% _reduction_")
- # => Query looks like "... title LIKE '20\% \_reduction\_' ..."
+ *Eileen M. Uchitelle, Aaron Patterson*
- *Rob Gilson*, *Yves Senn*
+* Fix n+1 query problem when eager loading nil associations (fixes #18312)
-* Do not quote uuid default value on `change_column`.
+ *Sammy Larbi*
- Fixes #14604.
+* Change the default error message from `can't be blank` to `must exist` for
+ the presence validator of the `:required` option on `belongs_to`/`has_one`
+ associations.
- *Eric Chahin*
+ *Henrik Nygren*
-* The comparison between `Relation` and `CollectionProxy` should be consistent.
+* Fixed `ActiveRecord::Relation#group` method when an argument is an SQL
+ reserved keyword:
Example:
- author.posts == Post.where(author_id: author.id)
- # => true
- Post.where(author_id: author.id) == author.posts
- # => true
+ SplitTest.group(:key).count
+ Property.group(:value).count
- Fixes #13506.
+ *Bogdan Gusiev*
- *Lauro Caetano*
+* Added the `#or` method on `ActiveRecord::Relation`, allowing use of the OR
+ operator to combine WHERE or HAVING clauses.
-* Calling `delete_all` on an unloaded `CollectionProxy` no longer
- generates an SQL statement containing each id of the collection:
+ Example:
- Before:
+ Post.where('id = 1').or(Post.where('id = 2'))
+ # => SELECT * FROM posts WHERE (id = 1) OR (id = 2)
- DELETE FROM `model` WHERE `model`.`parent_id` = 1
- AND `model`.`id` IN (1, 2, 3...)
+ *Sean Griffin*, *Matthew Draper*, *Gael Muller*, *Olivier El Mekki*
- After:
+* Don't define autosave association callbacks twice from
+ `accepts_nested_attributes_for`.
- DELETE FROM `model` WHERE `model`.`parent_id` = 1
+ Fixes #18704.
- *Eileen M. Uchitelle*, *Aaron Patterson*
+ *Sean Griffin*
-* Fixed error for aggregate methods (`empty?`, `any?`, `count`) with `select`
- which created invalid SQL.
+* Integer types will no longer raise a `RangeError` when assigning an
+ attribute, but will instead raise when going to the database.
- Fixes #13648.
+ Fixes several vague issues which were never reported directly. See the
+ commit message from the commit which added this line for some examples.
- *Simon Woker*
+ *Sean Griffin*
-* PostgreSQL adapter only warns once for every missing OID per connection.
+* Values which would error while being sent to the database (such as an
+ ASCII-8BIT string with invalid UTF-8 bytes on SQLite3), no longer error on
+ assignment. They will still error when sent to the database, but you are
+ given the ability to re-assign it to a valid value.
- Fixes #14275.
+ Fixes #18580.
- *Matthew Draper*, *Yves Senn*
+ *Sean Griffin*
-* PostgreSQL adapter automatically reloads it's type map when encountering
- unknown OIDs.
+* Don't remove join dependencies in `Relation#exists?`
- Fixes #14678.
+ Fixes #18632.
- *Matthew Draper*, *Yves Senn*
+ *Sean Griffin*
-* Fix insertion of records via `has_many :through` association with scope.
+* Invalid values assigned to a JSON column are assumed to be `nil`.
- Fixes #3548.
+ Fixes #18629.
- *Ivan Antropov*
+ *Sean Griffin*
-* Auto-generate stable fixture UUIDs on PostgreSQL.
+* Add `ActiveRecord::Base#accessed_fields`, which can be used to quickly
+ discover which fields were read from a model when you are looking to only
+ select the data you need from the database.
- Fixes #11524.
+ *Sean Griffin*
- *Roderick van Domburg*
+* Introduce the `:if_exists` option for `drop_table`.
-* Fixed a problem where an enum would overwrite values of another enum
- with the same name in an unrelated class.
+ Example:
- Fixes #14607.
+ drop_table(:posts, if_exists: true)
- *Evan Whalen*
+ That would execute:
-* PostgreSQL and SQLite string columns no longer have a default limit of 255.
+ DROP TABLE IF EXISTS posts
- Fixes #13435, #9153.
+ If the table doesn't exist, `if_exists: false` (the default) raises an
+ exception whereas `if_exists: true` does nothing.
- *Vladimir Sazhin*, *Toms Mikoss*, *Yves Senn*
+ *Cody Cutrer*, *Stefan Kanev*, *Ryuta Kamizono*
-* Make possible to have an association called `records`.
+* Don't run SQL if attribute value is not changed for update_attribute method.
- Fixes #11645.
+ *Prathamesh Sonpatki*
- *prathamesh-sonpatki*
+* `time` columns can now get affected by `time_zone_aware_attributes`. If you have
+ set `config.time_zone` to a value other than `'UTC'`, they will be treated
+ as in that time zone by default in Rails 5.1. If this is not the desired
+ behavior, you can set
-* `to_sql` on an association now matches the query that is actually executed, where it
- could previously have incorrectly accrued additional conditions (e.g. as a result of
- a previous query). CollectionProxy now always defers to the association scope's
- `arel` method so the (incorrect) inherited one should be entirely concealed.
+ ActiveRecord::Base.time_zone_aware_types = [:datetime]
- Fixes #14003.
+ A deprecation warning will be emitted if you have a `:time` column, and have
+ not explicitly opted out.
- *Jefferson Lai*
+ Fixes #3145.
-* Block a few default Class methods as scope name.
+ *Sean Griffin*
- For instance, this will raise:
+* Tests now run after_commit callbacks. You no longer have to declare
+ `uses_transaction ‘test name’` to test the results of an after_commit.
- scope :public, -> { where(status: 1) }
+ after_commit callbacks run after committing a transaction whose parent
+ is not `joinable?`: un-nested transactions, transactions within test cases,
+ and transactions in `console --sandbox`.
- *arthurnn*
+ *arthurnn*, *Ravil Bayramgalin*, *Matthew Draper*
-* Fixed error when using `with_options` with lambda.
+* `nil` as a value for a binary column in a query no longer logs as
+ "<NULL binary data>", and instead logs as just "nil".
- Fixes #9805.
+ *Sean Griffin*
- *Lauro Caetano*
+* `attribute_will_change!` will no longer cause non-persistable attributes to
+ be sent to the database.
-* Switch `sqlite3:///` URLs (which were temporarily
- deprecated in 4.1) from relative to absolute.
+ Fixes #18407.
- If you still want the previous interpretation, you should replace
- `sqlite3:///my/path` with `sqlite3:my/path`.
+ *Sean Griffin*
- *Matthew Draper*
+* Remove support for the `protected_attributes` gem.
-* Treat blank UUID values as `nil`.
+ *Carlos Antonio da Silva*, *Roberto Miranda*
- Example:
+* Fix accessing of fixtures having non-string labels like Fixnum.
- Sample.new(uuid_field: '') #=> <Sample id: nil, uuid_field: nil>
+ *Prathamesh Sonpatki*
- *Dmitry Lavrov*
+* Remove deprecated support to preload instance-dependent associations.
-* Enable support for materialized views on PostgreSQL >= 9.3.
+ *Yves Senn*
- *Dave Lee*
+* Remove deprecated support for PostgreSQL ranges with exclusive lower bounds.
-* The PostgreSQL adapter supports custom domains. Fixes #14305.
+ *Yves Senn*
+
+* Remove deprecation when modifying a relation with cached Arel.
+ This raises an `ImmutableRelation` error instead.
*Yves Senn*
-* PostgreSQL `Column#type` is now determined through the corresponding OID.
- The column types stay the same except for enum columns. They no longer have
- `nil` as type but `enum`.
+* Added `ActiveRecord::SecureToken` in order to encapsulate generation of
+ unique tokens for attributes in a model using `SecureRandom`.
- See #7814.
+ *Roberto Miranda*
- *Yves Senn*
+* Change the behavior of boolean columns to be closer to Ruby's semantics.
-* Fixed error when specifying a non-empty default value on a PostgreSQL array column.
+ Before this change we had a small set of "truthy", and all others are "falsy".
- Fixes #10613.
+ Now, we have a small set of "falsy" values and all others are "truthy" matching
+ Ruby's semantics.
- *Luke Steensen*
+ *Rafael Mendonça França*
-* Fixed error where .persisted? throws SystemStackError for an unsaved model with a
- custom primary key that didn't save due to validation error.
+* Deprecate `ActiveRecord::Base.errors_in_transactional_callbacks=`.
- Fixes #14393.
+ *Rafael Mendonça França*
- *Chris Finne*
+* Change transaction callbacks to not swallow errors.
-* Introduce `validate` as an alias for `valid?`.
+ Before this change any errors raised inside a transaction callback
+ were getting rescued and printed in the logs.
- This is more intuitive when you want to run validations but don't care about the return value.
+ Now these errors are not rescued anymore and just bubble up, as the other callbacks.
- *Henrik Nyh*
+ *Rafael Mendonça França*
-* Create indexes inline in CREATE TABLE for MySQL.
+* Remove deprecated `sanitize_sql_hash_for_conditions`.
- This is important, because adding an index on a temporary table after it has been created
- would commit the transaction.
+ *Rafael Mendonça França*
- It also allows creating and dropping indexed tables with fewer queries and fewer permissions
- required.
+* Remove deprecated `Reflection#source_macro`.
- Example:
+ *Rafael Mendonça França*
- create_table :temp, temporary: true, as: "SELECT id, name, zip FROM a_really_complicated_query" do |t|
- t.index :zip
- end
- # => CREATE TEMPORARY TABLE temp (INDEX (zip)) AS SELECT id, name, zip FROM a_really_complicated_query
+* Remove deprecated `symbolized_base_class` and `symbolized_sti_name`.
- *Cody Cutrer*, *Steve Rice*, *Rafael Mendonça Franca*
+ *Rafael Mendonça França*
-* Use singular table name in generated migrations when
- `ActiveRecord::Base.pluralize_table_names` is `false`.
+* Remove deprecated `ActiveRecord::Base.disable_implicit_join_references=`.
- Fixes #13426.
+ *Rafael Mendonça França*
- *Kuldeep Aggarwal*
+* Remove deprecated access to connection specification using a string accessor.
-* `touch` accepts many attributes to be touched at once.
+ Now all strings will be handled as a URL.
- Example:
+ *Rafael Mendonça França*
- # touches :signed_at, :sealed_at, and :updated_at/on attributes.
- Photo.last.touch(:signed_at, :sealed_at)
+* Change the default `null` value for `timestamps` to `false`.
- *James Pinto*
+ *Rafael Mendonça França*
-* `rake db:structure:dump` only dumps schema information if the schema
- migration table exists.
+* Return an array of pools from `connection_pools`.
- Fixes #14217.
+ *Rafael Mendonça França*
- *Yves Senn*
+* Return a null column from `column_for_attribute` when no column exists.
-* Reap connections that were checked out by now-dead threads, instead
- of waiting until they disconnect by themselves. Before this change,
- a suitably constructed series of short-lived threads could starve
- the connection pool, without ever having more than a couple alive at
- the same time.
+ *Rafael Mendonça França*
- *Matthew Draper*
+* Remove deprecated `serialized_attributes`.
-* `pk_and_sequence_for` now ensures that only the pg_depend entries
- pointing to pg_class, and thus only sequence objects, are considered.
+ *Rafael Mendonça França*
- *Josh Williams*
+* Remove deprecated automatic counter caches on `has_many :through`.
-* `where.not` adds `references` for `includes` like normal `where` calls do.
+ *Rafael Mendonça França*
- Fixes #14406.
+* Change the way in which callback chains can be halted.
- *Yves Senn*
+ The preferred method to halt a callback chain from now on is to explicitly
+ `throw(:abort)`.
+ In the past, returning `false` in an Active Record `before_` callback had the
+ side effect of halting the callback chain.
+ This is not recommended anymore and, depending on the value of the
+ `ActiveSupport.halt_callback_chains_on_return_false` option, will
+ either not work at all or display a deprecation warning.
-* Extend fixture `$LABEL` replacement to allow string interpolation.
+ *claudiob*
- Example:
+* Clear query cache on rollback.
- martin:
- email: $LABEL@email.com
+ *Florian Weingarten*
- users(:martin).email # => martin@email.com
+* Fix setting of foreign_key for through associations when building a new record.
- *Eric Steele*
+ Fixes #12698.
-* Add support for `Relation` be passed as parameter on `QueryCache#select_all`.
+ *Ivan Antropov*
- Fixes #14361.
+* Improve dumping of the primary key. If it is not a default primary key,
+ correctly dump the type and options.
- *arthurnn*
+ Fixes #14169, #16599.
+
+ *Ryuta Kamizono*
+
+* Format the datetime string according to the precision of the datetime field.
-* Passing an Active Record object to `find` or `exists?` is now deprecated.
- Call `.id` on the object first.
+ Incompatible to rounding behavior between MySQL 5.6 and earlier.
- *Aaron Patterson*
+ In 5.5, when you insert `2014-08-17 12:30:00.999999` the fractional part
+ is ignored. In 5.6, it's rounded to `2014-08-17 12:30:01`:
-* Only use BINARY for MySQL case sensitive uniqueness check when column has a case insensitive collation.
+ http://bugs.mysql.com/bug.php?id=68760
*Ryuta Kamizono*
-* Support for MySQL 5.6 fractional seconds.
+* Allow a precision option for MySQL datetimes.
- *arthurnn*, *Tatsuhiko Miyagawa*
+ *Ryuta Kamizono*
-* Support for Postgres `citext` data type enabling case-insensitive where
- values without needing to wrap in UPPER/LOWER sql functions.
+* Fixed automatic `inverse_of` for models nested in a module.
- *Troy Kruthoff*, *Lachlan Sylvester*
+ *Andrew McCloud*
-* Only save has_one associations if record has changes.
- Previously after save related callbacks, such as `#after_commit`, were triggered when the has_one
- object did not get saved to the db.
+* Change `ActiveRecord::Relation#update` behavior so that it can
+ be called without passing ids of the records to be updated.
- *Alan Kennedy*
+ This change allows updating multiple records returned by
+ `ActiveRecord::Relation` with callbacks and validations.
-* Allow strings to specify the `#order` value.
+ # Before
+ # ArgumentError: wrong number of arguments (1 for 2)
+ Comment.where(group: 'expert').update(body: "Group of Rails Experts")
- Example:
+ # After
+ # Comments with group expert updated with body "Group of Rails Experts"
+ Comment.where(group: 'expert').update(body: "Group of Rails Experts")
- Model.order(id: 'asc').to_sql == Model.order(id: :asc).to_sql
+ *Prathamesh Sonpatki*
- *Marcelo Casiraghi*, *Robin Dupret*
+* Fix `reaping_frequency` option when the value is a string.
-* Dynamically register PostgreSQL enum OIDs. This prevents "unknown OID"
- warnings on enum columns.
+ This usually happens when it is configured using `DATABASE_URL`.
- *Dieter Komendera*
+ *korbin*
+
+* Fix error message when trying to create an associated record and the foreign
+ key is missing.
+
+ Before this fix the following exception was being raised:
+
+ NoMethodError: undefined method `val' for #<Arel::Nodes::BindParam:0x007fc64d19c218>
+
+ Now the message is:
+
+ ActiveRecord::UnknownAttributeError: unknown attribute 'foreign_key' for Model.
-* `includes` is able to detect the right preloading strategy when string
- joins are involved.
+ *Rafael Mendonça França*
- Fixes #14109.
+* Fix change detection problem for PostgreSQL bytea type and
+ `ArgumentError: string contains null byte` exception with pg-0.18.
- *Aaron Patterson*, *Yves Senn*
+ Fixes #17680.
-* Fixed error with validation with enum fields for records where the
- value for any enum attribute is always evaluated as 0 during
- uniqueness validation.
+ *Lars Kanis*
- Fixes #14172.
+* When a table has a composite primary key, the `primary_key` method for
+ SQLite3 and PostgreSQL adapters was only returning the first field of the key.
+ Ensures that it will return nil instead, as Active Record doesn't support
+ composite primary keys.
- *Vilius Luneckas* *Ahmed AbouElhamayed*
+ Fixes #18070.
+
+ *arthurnn*
+
+* `validates_size_of` / `validates_length_of` do not count records
+ which are `marked_for_destruction?`.
+
+ Fixes #7247.
+
+ *Yves Senn*
+
+* Ensure `first!` and friends work on loaded associations.
+
+ Fixes #18237.
+
+ *Sean Griffin*
-* `before_add` callbacks are fired before the record is saved on
- `has_and_belongs_to_many` associations *and* on `has_many :through`
- associations. Before this change, `before_add` callbacks would be fired
- before the record was saved on `has_and_belongs_to_many` associations, but
- *not* on `has_many :through` associations.
+* `eager_load` preserves readonly flag for associations.
- Fixes #14144.
+ Fixes #15853.
-* Fixed STI classes not defining an attribute method if there is a
- conflicting private method defined on its ancestors.
+ *Takashi Kokubun*
- Fixes #11569.
+* Provide `:touch` option to `save()` to accommodate saving without updating
+ timestamps.
- *Godfrey Chan*
+ Fixes #18202.
-* Coerce strings when reading attributes. Fixes #10485.
+ *Dan Olson*
+
+* Provide a more helpful error message when an unsupported class is passed to
+ `serialize`.
+
+ Fixes #18224.
+
+ *Sean Griffin*
+
+* Add bigint primary key support for MySQL.
Example:
- book = Book.new(title: 12345)
- book.save!
- book.title # => "12345"
+ create_table :foos, id: :bigint do |t|
+ end
+
+ *Ryuta Kamizono*
+
+* Support for any type of primary key.
+
+ Fixes #14194.
+
+ *Ryuta Kamizono*
+
+* Dump the default `nil` for PostgreSQL UUID primary key.
+
+ *Ryuta Kamizono*
+
+* Add a `:foreign_key` option to `references` and associated migration
+ methods. The model and migration generators now use this option, rather than
+ the `add_foreign_key` form.
+
+ *Sean Griffin*
+
+* Don't raise when writing an attribute with an out-of-range datetime passed
+ by the user.
+
+ *Grey Baker*
+
+* Replace deprecated `ActiveRecord::Tasks::DatabaseTasks#load_schema` with
+ `ActiveRecord::Tasks::DatabaseTasks#load_schema_for`.
*Yves Senn*
-* Deprecate half-baked support for PostgreSQL range values with excluding beginnings.
- We currently map PostgreSQL ranges to Ruby ranges. This conversion is not fully
- possible because the Ruby range does not support excluded beginnings.
+* Fix bug with `ActiveRecord::Type::Numeric` that caused negative values to
+ be marked as having changed when set to the same negative value.
+
+ Fixes #18161.
+
+ *Daniel Fox*
+
+* Introduce `force: :cascade` option for `create_table`. Using this option
+ will recreate tables even if they have dependent objects (like foreign keys).
+ `db/schema.rb` now uses `force: :cascade`. This makes it possible to
+ reload the schema when foreign keys are in place.
+
+ *Matthew Draper*, *Yves Senn*
+
+* `db:schema:load` and `db:structure:load` no longer purge the database
+ before loading the schema. This is left for the user to do.
+ `db:test:prepare` will still purge the database.
- The current solution of incrementing the beginning is not correct and is now
- deprecated. For subtypes where we don't know how to increment (e.g. `#succ`
- is not defined) it will raise an ArgumentException for ranges with excluding
- beginnings.
+ Fixes #17945.
*Yves Senn*
-* Support for user created range types in PostgreSQL.
+* Fix undesirable RangeError by `Type::Integer`. Add `Type::UnsignedInteger`.
+
+ *Ryuta Kamizono*
+
+* Add `foreign_type` option to `has_one` and `has_many` association macros.
+
+ This option enables to define the column name of associated object's type for polymorphic associations.
+
+ *Ulisses Almeida*, *Kassio Borges*
+
+* Remove deprecated behavior allowing nested arrays to be passed as query
+ values.
+
+ *Melanie Gilman*
+
+* Deprecate passing a class as a value in a query. Users should pass strings
+ instead.
+
+ *Melanie Gilman*
+
+* `add_timestamps` and `remove_timestamps` now properly reversible with
+ options.
+
+ *Noam Gagliardi-Rabinovich*
+
+* `ActiveRecord::ConnectionAdapters::ColumnDumper#column_spec` and
+ `ActiveRecord::ConnectionAdapters::ColumnDumper#prepare_column_options` no
+ longer have a `types` argument. They should access
+ `connection#native_database_types` directly.
*Yves Senn*
-Please check [4-1-stable](https://github.com/rails/rails/blob/4-1-stable/activerecord/CHANGELOG.md) for previous changes.
+Please check [4-2-stable](https://github.com/rails/rails/blob/4-2-stable/activerecord/CHANGELOG.md) for previous changes.
diff --git a/activerecord/MIT-LICENSE b/activerecord/MIT-LICENSE
index 2950f05b11..7c2197229d 100644
--- a/activerecord/MIT-LICENSE
+++ b/activerecord/MIT-LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2004-2014 David Heinemeier Hansson
+Copyright (c) 2004-2015 David Heinemeier Hansson
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 f4777919d3..3eac8cc422 100644
--- a/activerecord/README.rdoc
+++ b/activerecord/README.rdoc
@@ -26,13 +26,13 @@ The Product class is automatically mapped to the table named "products",
which might look like this:
CREATE TABLE products (
- id int(11) NOT NULL auto_increment,
+ id int NOT NULL auto_increment,
name varchar(255),
PRIMARY KEY (id)
);
-This would also define the following accessors: `Product#name` and
-`Product#name=(new_name)`.
+This would also define the following accessors: <tt>Product#name</tt> and
+<tt>Product#name=(new_name)</tt>.
* Associations between objects defined by simple class methods.
@@ -188,7 +188,7 @@ Admit the Database:
The latest version of Active Record can be installed with RubyGems:
- % [sudo] gem install activerecord
+ % gem install activerecord
Source code can be downloaded as part of the Rails project on GitHub:
diff --git a/activerecord/RUNNING_UNIT_TESTS.rdoc b/activerecord/RUNNING_UNIT_TESTS.rdoc
index ca1f2fd665..bae40604b1 100644
--- a/activerecord/RUNNING_UNIT_TESTS.rdoc
+++ b/activerecord/RUNNING_UNIT_TESTS.rdoc
@@ -16,15 +16,19 @@ To run a set of tests:
You can also run tests that depend upon a specific database backend. For
example:
- $ bundle exec rake test_sqlite3
+ $ bundle exec rake test:sqlite3
Simply executing <tt>bundle exec rake test</tt> is equivalent to the following:
- $ bundle exec rake test_mysql
- $ bundle exec rake test_mysql2
- $ bundle exec rake test_postgresql
- $ bundle exec rake test_sqlite3
- $ bundle exec rake test_sqlite3_mem
+ $ bundle exec rake test:mysql
+ $ bundle exec rake test:mysql2
+ $ bundle exec rake test:postgresql
+ $ bundle exec rake test:sqlite3
+
+Using the SQLite3 adapter with an in-memory database is the fastest way
+to run the tests:
+
+ $ bundle exec rake test:sqlite3_mem
There should be tests available for each database backend listed in the {Config
File}[rdoc-label:label-Config+File]. (the exact set of available tests is
@@ -32,8 +36,8 @@ defined in +Rakefile+)
== Config File
-If +test/config.yml+ is present, it's parameters are obeyed. Otherwise, the
-parameters in +test/config.example.yml+ are obeyed.
+If +test/config.yml+ is present, then its parameters are obeyed; otherwise, the
+parameters in +test/config.example.yml+ are.
You can override the +connections:+ parameter in either file using the +ARCONN+
(Active Record CONNection) environment variable:
diff --git a/activerecord/Rakefile b/activerecord/Rakefile
index 7769966a22..c93099a921 100644
--- a/activerecord/Rakefile
+++ b/activerecord/Rakefile
@@ -1,5 +1,4 @@
require 'rake/testtask'
-require 'rubygems/package_task'
require File.expand_path(File.dirname(__FILE__)) + "/test/config"
require File.expand_path(File.dirname(__FILE__)) + "/test/support/config"
@@ -51,10 +50,11 @@ end
t.libs << 'test'
t.test_files = (Dir.glob( "test/cases/**/*_test.rb" ).reject {
|x| x =~ /\/adapters\//
- } + Dir.glob("test/cases/adapters/#{adapter_short}/**/*_test.rb")).sort
+ } + Dir.glob("test/cases/adapters/#{adapter_short}/**/*_test.rb"))
t.warning = true
t.verbose = true
+ t.ruby_opts = ["--dev"] if defined?(JRUBY_VERSION)
}
namespace :isolated do
@@ -83,29 +83,20 @@ end
task "isolated_test_#{adapter}" => ["#{adapter}:env", "test:isolated:#{adapter}"]
end
-rule '.sqlite3' do |t|
- sh %Q{sqlite3 "#{t.name}" "create table a (a integer); drop table a;"}
-end
-
-task :test_sqlite3 => [
- 'test/fixtures/fixture_database.sqlite3',
- 'test/fixtures/fixture_database_2.sqlite3'
-]
-
namespace :db do
namespace :mysql do
desc 'Build the MySQL test databases'
task :build do
config = ARTest.config['connections']['mysql']
- %x( mysql --user=#{config['arunit']['username']} -e "create DATABASE #{config['arunit']['database']} DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_unicode_ci ")
- %x( mysql --user=#{config['arunit2']['username']} -e "create DATABASE #{config['arunit2']['database']} DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_unicode_ci ")
+ %x( mysql --user=#{config['arunit']['username']} --password=#{config['arunit']['password']} -e "create DATABASE #{config['arunit']['database']} DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_unicode_ci ")
+ %x( mysql --user=#{config['arunit2']['username']} --password=#{config['arunit2']['password']} -e "create DATABASE #{config['arunit2']['database']} DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_unicode_ci ")
end
desc 'Drop the MySQL test databases'
task :drop do
config = ARTest.config['connections']['mysql']
- %x( mysqladmin --user=#{config['arunit']['username']} -f drop #{config['arunit']['database']} )
- %x( mysqladmin --user=#{config['arunit2']['username']} -f drop #{config['arunit2']['database']} )
+ %x( mysqladmin --user=#{config['arunit']['username']} --password=#{config['arunit']['password']} -f drop #{config['arunit']['database']} )
+ %x( mysqladmin --user=#{config['arunit2']['username']} --password=#{config['arunit2']['password']} -f drop #{config['arunit2']['database']} )
end
desc 'Rebuild the MySQL test databases'
@@ -121,7 +112,7 @@ namespace :db do
# prepare hstore
if %x( createdb --version ).strip.gsub(/(.*)(\d\.\d\.\d)$/, "\\2") < "9.1.0"
- puts "Please prepare hstore data type. See http://www.postgresql.org/docs/9.0/static/hstore.html"
+ puts "Please prepare hstore data type. See http://www.postgresql.org/docs/current/static/hstore.html"
end
end
@@ -146,40 +137,7 @@ task :drop_postgresql_databases => 'db:postgresql:drop'
task :rebuild_postgresql_databases => 'db:postgresql:rebuild'
task :lines do
- lines, codelines, total_lines, total_codelines = 0, 0, 0, 0
-
- FileList["lib/active_record/**/*.rb"].each do |file_name|
- next if file_name =~ /vendor/
- File.open(file_name, 'r') do |f|
- while line = f.gets
- lines += 1
- next if line =~ /^\s*$/
- next if line =~ /^\s*#/
- codelines += 1
- end
- end
- puts "L: #{sprintf("%4d", lines)}, LOC #{sprintf("%4d", codelines)} | #{file_name}"
-
- total_lines += lines
- total_codelines += codelines
-
- lines, codelines = 0, 0
- end
-
- puts "Total: Lines #{total_lines}, LOC #{total_codelines}"
-end
-
-spec = eval(File.read('activerecord.gemspec'))
-
-Gem::PackageTask.new(spec) do |p|
- p.gem_spec = spec
-end
-
-# Publishing ------------------------------------------------------
-
-desc "Release to rubygems"
-task :release => :package do
- require 'rake/gemcutter'
- Rake::Gemcutter::Tasks.new(spec).define
- Rake::Task['gem:push'].invoke
+ load File.expand_path('..', File.dirname(__FILE__)) + '/tools/line_statistics'
+ files = FileList["lib/active_record/**/*.rb"]
+ CodeTools::LineStatistics.new(files).print_loc
end
diff --git a/activerecord/activerecord.gemspec b/activerecord/activerecord.gemspec
index 8075008574..bd95b57303 100644
--- a/activerecord/activerecord.gemspec
+++ b/activerecord/activerecord.gemspec
@@ -7,7 +7,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 = '>= 1.9.3'
+ s.required_ruby_version = '>= 2.2.2'
s.license = 'MIT'
@@ -24,5 +24,5 @@ Gem::Specification.new do |s|
s.add_dependency 'activesupport', version
s.add_dependency 'activemodel', version
- s.add_dependency 'arel', '~> 6.0.0'
+ s.add_dependency 'arel', '7.0.0.alpha'
end
diff --git a/activerecord/bin/test b/activerecord/bin/test
new file mode 100755
index 0000000000..f8adf2aabc
--- /dev/null
+++ b/activerecord/bin/test
@@ -0,0 +1,19 @@
+#!/usr/bin/env ruby
+COMPONENT_ROOT = File.expand_path("../../", __FILE__)
+require File.expand_path("../tools/test", COMPONENT_ROOT)
+module Minitest
+ def self.plugin_active_record_options(opts, options)
+ opts.separator ""
+ opts.separator "Active Record options:"
+ opts.on("-a", "--adapter [ADAPTER]",
+ "Run tests using a specific adapter (sqlite3, sqlite3_mem, mysql, mysql2, postgresql)") do |adapter|
+ ENV["ARCONN"] = adapter.strip
+ end
+
+ opts
+ end
+end
+
+Minitest.extensions.unshift 'active_record'
+
+exit Minitest.run(ARGV)
diff --git a/activerecord/examples/performance.rb b/activerecord/examples/performance.rb
index d3546ce948..a5a1f284a0 100644
--- a/activerecord/examples/performance.rb
+++ b/activerecord/examples/performance.rb
@@ -39,8 +39,8 @@ class Exhibit < ActiveRecord::Base
where("notes IS NOT NULL")
end
- def self.look(exhibits) exhibits.each { |e| e.look } end
- def self.feel(exhibits) exhibits.each { |e| e.feel } end
+ def self.look(exhibits) exhibits.each(&:look) end
+ def self.feel(exhibits) exhibits.each(&:feel) end
end
def progress_bar(int); print "." if (int%100).zero? ; end
diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb
index 9028970a3d..264f869c68 100644
--- a/activerecord/lib/active_record.rb
+++ b/activerecord/lib/active_record.rb
@@ -1,5 +1,5 @@
#--
-# Copyright (c) 2004-2014 David Heinemeier Hansson
+# Copyright (c) 2004-2015 David Heinemeier Hansson
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
@@ -43,15 +43,19 @@ module ActiveRecord
autoload :Explain
autoload :Inheritance
autoload :Integration
+ autoload :LegacyYamlAdapter
autoload :Migration
autoload :Migrator, 'active_record/migration'
autoload :ModelSchema
autoload :NestedAttributes
autoload :NoTouching
+ autoload :TouchLater
autoload :Persistence
autoload :QueryCache
autoload :Querying
+ autoload :CollectionCacheKey
autoload :ReadonlyAttributes
+ autoload :RecordInvalid, 'active_record/validations'
autoload :Reflection
autoload :RuntimeRegistry
autoload :Sanitization
@@ -62,10 +66,13 @@ module ActiveRecord
autoload :Serialization
autoload :StatementCache
autoload :Store
+ autoload :Suppressor
+ autoload :TableMetadata
autoload :Timestamp
autoload :Transactions
autoload :Translation
autoload :Validations
+ autoload :SecureToken
eager_autoload do
autoload :ActiveRecordError, 'active_record/errors'
diff --git a/activerecord/lib/active_record/aggregations.rb b/activerecord/lib/active_record/aggregations.rb
index e576ec4d40..be88c7c9e8 100644
--- a/activerecord/lib/active_record/aggregations.rb
+++ b/activerecord/lib/active_record/aggregations.rb
@@ -1,14 +1,31 @@
module ActiveRecord
- # = Active Record Aggregations
- module Aggregations # :nodoc:
+ # See ActiveRecord::Aggregations::ClassMethods for documentation
+ module Aggregations
extend ActiveSupport::Concern
- def clear_aggregation_cache #:nodoc:
- @aggregation_cache.clear if persisted?
+ def initialize_dup(*) # :nodoc:
+ @aggregation_cache = {}
+ super
end
- # Active Record implements aggregation through a macro-like class method called +composed_of+
- # for representing attributes as value objects. It expresses relationships like "Account [is]
+ def reload(*) # :nodoc:
+ clear_aggregation_cache
+ super
+ end
+
+ private
+
+ def clear_aggregation_cache # :nodoc:
+ @aggregation_cache.clear if persisted?
+ end
+
+ def init_internals # :nodoc:
+ @aggregation_cache = {}
+ super
+ end
+
+ # Active Record implements aggregation through a macro-like class method called #composed_of
+ # for representing attributes as value objects. It expresses relationships like "Account [is]
# composed of Money [among other things]" or "Person [is] composed of [an] address". Each call
# to the macro adds a description of how the value objects are created from the attributes of
# the entity object (when the entity is initialized either as a new object or from finding an
@@ -87,11 +104,6 @@ module ActiveRecord
# customer.address_city = "Copenhagen"
# customer.address # => Address.new("Hyancintvej", "Copenhagen")
#
- # customer.address_street = "Vesterbrogade"
- # customer.address # => Address.new("Hyancintvej", "Copenhagen")
- # customer.clear_aggregation_cache
- # customer.address # => Address.new("Vesterbrogade", "Copenhagen")
- #
# customer.address = Address.new("May Street", "Chicago")
# customer.address_street # => "May Street"
# customer.address_city # => "Chicago"
@@ -108,12 +120,12 @@ module ActiveRecord
#
# It's also important to treat the value objects as immutable. Don't allow the Money object to have
# its amount changed after creation. Create a new Money object with the new value instead. The
- # Money#exchange_to method is an example of this. It returns a new value object instead of changing
+ # <tt>Money#exchange_to</tt> method is an example of this. It returns a new value object instead of changing
# its own values. Active Record won't persist value objects that have been changed through means
# other than the writer method.
#
# The immutable requirement is enforced by Active Record by freezing any object assigned as a value
- # object. Attempting to change it afterwards will result in a RuntimeError.
+ # object. Attempting to change it afterwards will result in a +RuntimeError+.
#
# Read more about value objects on http://c2.com/cgi/wiki?ValueObject and on the dangers of not
# keeping value objects immutable on http://c2.com/cgi/wiki?ValueObjectsShouldBeImmutable
@@ -122,17 +134,17 @@ module ActiveRecord
#
# By default value objects are initialized by calling the <tt>new</tt> constructor of the value
# class passing each of the mapped attributes, in the order specified by the <tt>:mapping</tt>
- # option, as arguments. If the value class doesn't support this convention then +composed_of+ allows
+ # option, as arguments. If the value class doesn't support this convention then #composed_of allows
# a custom constructor to be specified.
#
# When a new value is assigned to the value object, the default assumption is that the new value
# is an instance of the value class. Specifying a custom converter allows the new value to be automatically
# converted to an instance of value class if necessary.
#
- # For example, the NetworkResource model has +network_address+ and +cidr_range+ attributes that should be
- # aggregated using the NetAddr::CIDR value class (http://www.ruby-doc.org/gems/docs/n/netaddr-1.5.0/NetAddr/CIDR.html).
+ # For example, the +NetworkResource+ model has +network_address+ and +cidr_range+ attributes that should be
+ # aggregated using the +NetAddr::CIDR+ value class (http://www.rubydoc.info/gems/netaddr/1.5.0/NetAddr/CIDR).
# The constructor for the value class is called +create+ and it expects a CIDR address string as a parameter.
- # New values can be assigned to the value object using either another NetAddr::CIDR object, a string
+ # New values can be assigned to the value object using either another +NetAddr::CIDR+ object, a string
# or an array. The <tt>:constructor</tt> and <tt>:converter</tt> options can be used to meet
# these requirements:
#
@@ -161,7 +173,7 @@ module ActiveRecord
#
# == Finding records by a value object
#
- # Once a +composed_of+ relationship is specified for a model, records can be loaded from the database
+ # 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":
#
@@ -174,7 +186,7 @@ module ActiveRecord
# Options are:
# * <tt>:class_name</tt> - Specifies the class name of the association. Use it only if that name
# can't be inferred from the part id. So <tt>composed_of :address</tt> will by default be linked
- # to the Address class, but if the real class name is CompanyAddress, you'll have to specify it
+ # to the Address class, but if the real class name is +CompanyAddress+, you'll have to specify it
# with this option.
# * <tt>:mapping</tt> - Specifies the mapping of entity attributes to attributes of the value
# object. Each mapping is represented as an array where the first item is the name of the
@@ -230,8 +242,8 @@ module ActiveRecord
private
def reader_method(name, class_name, mapping, allow_nil, constructor)
define_method(name) do
- if @aggregation_cache[name].nil? && (!allow_nil || mapping.any? {|key, _| !read_attribute(key).nil? })
- attrs = mapping.collect {|key, _| read_attribute(key)}
+ if @aggregation_cache[name].nil? && (!allow_nil || mapping.any? {|key, _| !_read_attribute(key).nil? })
+ attrs = mapping.collect {|key, _| _read_attribute(key)}
object = constructor.respond_to?(:call) ?
constructor.call(*attrs) :
class_name.constantize.send(constructor, *attrs)
@@ -245,7 +257,8 @@ module ActiveRecord
define_method("#{name}=") do |part|
klass = class_name.constantize
if part.is_a?(Hash)
- part = klass.new(*part.values)
+ raise ArgumentError unless part.size == part.keys.max
+ part = klass.new(*part.sort.map(&:last))
end
unless part.is_a?(klass) || converter.nil? || part.nil?
diff --git a/activerecord/lib/active_record/association_relation.rb b/activerecord/lib/active_record/association_relation.rb
index 5a84792f45..ee0bb8fafe 100644
--- a/activerecord/lib/active_record/association_relation.rb
+++ b/activerecord/lib/active_record/association_relation.rb
@@ -1,7 +1,7 @@
module ActiveRecord
class AssociationRelation < Relation
- def initialize(klass, table, association)
- super(klass, table)
+ def initialize(klass, table, predicate_builder, association)
+ super(klass, table, predicate_builder)
@association = association
end
@@ -13,6 +13,19 @@ module ActiveRecord
other == to_a
end
+ def build(*args, &block)
+ scoping { @association.build(*args, &block) }
+ end
+ alias new build
+
+ def create(*args, &block)
+ scoping { @association.create(*args, &block) }
+ end
+
+ def create!(*args, &block)
+ scoping { @association.create!(*args, &block) }
+ end
+
private
def exec_queries
diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb
index ec78d10124..b806a2f832 100644
--- a/activerecord/lib/active_record/associations.rb
+++ b/activerecord/lib/active_record/associations.rb
@@ -5,89 +5,170 @@ require 'active_record/errors'
module ActiveRecord
class AssociationNotFoundError < ConfigurationError #:nodoc:
- def initialize(record, association_name)
- super("Association named '#{association_name}' was not found on #{record.class.name}; perhaps you misspelled it?")
+ def initialize(record = nil, association_name = nil)
+ if record && association_name
+ super("Association named '#{association_name}' was not found on #{record.class.name}; perhaps you misspelled it?")
+ else
+ super("Association was not found.")
+ end
end
end
class InverseOfAssociationNotFoundError < ActiveRecordError #:nodoc:
- def initialize(reflection, associated_class = nil)
- super("Could not find the inverse association for #{reflection.name} (#{reflection.options[:inverse_of].inspect} in #{associated_class.nil? ? reflection.class_name : associated_class.name})")
+ def initialize(reflection = nil, associated_class = nil)
+ if reflection
+ super("Could not find the inverse association for #{reflection.name} (#{reflection.options[:inverse_of].inspect} in #{associated_class.nil? ? reflection.class_name : associated_class.name})")
+ else
+ super("Could not find the inverse association.")
+ end
end
end
class HasManyThroughAssociationNotFoundError < ActiveRecordError #:nodoc:
- def initialize(owner_class_name, reflection)
- super("Could not find the association #{reflection.options[:through].inspect} in model #{owner_class_name}")
+ def initialize(owner_class_name = nil, reflection = nil)
+ if owner_class_name && reflection
+ super("Could not find the association #{reflection.options[:through].inspect} in model #{owner_class_name}")
+ else
+ super("Could not find the association.")
+ end
end
end
class HasManyThroughAssociationPolymorphicSourceError < ActiveRecordError #:nodoc:
- def initialize(owner_class_name, reflection, source_reflection)
- super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' on the polymorphic object '#{source_reflection.class_name}##{source_reflection.name}' without 'source_type'. Try adding 'source_type: \"#{reflection.name.to_s.classify}\"' to 'has_many :through' definition.")
+ def initialize(owner_class_name = nil, reflection = nil, source_reflection = nil)
+ if owner_class_name && reflection && source_reflection
+ super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' on the polymorphic object '#{source_reflection.class_name}##{source_reflection.name}' without 'source_type'. Try adding 'source_type: \"#{reflection.name.to_s.classify}\"' to 'has_many :through' definition.")
+ else
+ super("Cannot have a has_many :through association.")
+ end
end
end
class HasManyThroughAssociationPolymorphicThroughError < ActiveRecordError #:nodoc:
- def initialize(owner_class_name, reflection)
- super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' which goes through the polymorphic association '#{owner_class_name}##{reflection.through_reflection.name}'.")
+ def initialize(owner_class_name = nil, reflection = nil)
+ if owner_class_name && reflection
+ super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' which goes through the polymorphic association '#{owner_class_name}##{reflection.through_reflection.name}'.")
+ else
+ super("Cannot have a has_many :through association.")
+ end
end
end
class HasManyThroughAssociationPointlessSourceTypeError < ActiveRecordError #:nodoc:
- def initialize(owner_class_name, reflection, source_reflection)
- super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' with a :source_type option if the '#{reflection.through_reflection.class_name}##{source_reflection.name}' is not polymorphic. Try removing :source_type on your association.")
+ def initialize(owner_class_name = nil, reflection = nil, source_reflection = nil)
+ if owner_class_name && reflection && source_reflection
+ super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' with a :source_type option if the '#{reflection.through_reflection.class_name}##{source_reflection.name}' is not polymorphic. Try removing :source_type on your association.")
+ else
+ super("Cannot have a has_many :through association.")
+ end
end
end
class HasOneThroughCantAssociateThroughCollection < ActiveRecordError #:nodoc:
- def initialize(owner_class_name, reflection, through_reflection)
- super("Cannot have a has_one :through association '#{owner_class_name}##{reflection.name}' where the :through association '#{owner_class_name}##{through_reflection.name}' is a collection. Specify a has_one or belongs_to association in the :through option instead.")
+ def initialize(owner_class_name = nil, reflection = nil, through_reflection = nil)
+ if owner_class_name && reflection && through_reflection
+ super("Cannot have a has_one :through association '#{owner_class_name}##{reflection.name}' where the :through association '#{owner_class_name}##{through_reflection.name}' is a collection. Specify a has_one or belongs_to association in the :through option instead.")
+ else
+ super("Cannot have a has_one :through association.")
+ end
+ end
+ end
+
+ class HasOneAssociationPolymorphicThroughError < ActiveRecordError #:nodoc:
+ def initialize(owner_class_name = nil, reflection = nil)
+ if owner_class_name && reflection
+ super("Cannot have a has_one :through association '#{owner_class_name}##{reflection.name}' which goes through the polymorphic association '#{owner_class_name}##{reflection.through_reflection.name}'.")
+ else
+ super("Cannot have a has_one :through association.")
+ end
end
end
class HasManyThroughSourceAssociationNotFoundError < ActiveRecordError #:nodoc:
- def initialize(reflection)
- through_reflection = reflection.through_reflection
- source_reflection_names = reflection.source_reflection_names
- source_associations = reflection.through_reflection.klass._reflections.keys
- super("Could not find the source association(s) #{source_reflection_names.collect{ |a| a.inspect }.to_sentence(:two_words_connector => ' or ', :last_word_connector => ', or ', :locale => :en)} in model #{through_reflection.klass}. Try 'has_many #{reflection.name.inspect}, :through => #{through_reflection.name.inspect}, :source => <name>'. Is it one of #{source_associations.to_sentence(:two_words_connector => ' or ', :last_word_connector => ', or ', :locale => :en)}?")
+ def initialize(reflection = nil)
+ if reflection
+ through_reflection = reflection.through_reflection
+ source_reflection_names = reflection.source_reflection_names
+ source_associations = reflection.through_reflection.klass._reflections.keys
+ super("Could not find the source association(s) #{source_reflection_names.collect(&:inspect).to_sentence(:two_words_connector => ' or ', :last_word_connector => ', or ', :locale => :en)} in model #{through_reflection.klass}. Try 'has_many #{reflection.name.inspect}, :through => #{through_reflection.name.inspect}, :source => <name>'. Is it one of #{source_associations.to_sentence(:two_words_connector => ' or ', :last_word_connector => ', or ', :locale => :en)}?")
+ else
+ super("Could not find the source association(s).")
+ end
end
end
- class HasManyThroughCantAssociateThroughHasOneOrManyReflection < ActiveRecordError #:nodoc:
- def initialize(owner, reflection)
- super("Cannot modify association '#{owner.class.name}##{reflection.name}' because the source reflection class '#{reflection.source_reflection.class_name}' is associated to '#{reflection.through_reflection.class_name}' via :#{reflection.source_reflection.macro}.")
+ class ThroughCantAssociateThroughHasOneOrManyReflection < ActiveRecordError #:nodoc:
+ def initialize(owner = nil, reflection = nil)
+ if owner && reflection
+ super("Cannot modify association '#{owner.class.name}##{reflection.name}' because the source reflection class '#{reflection.source_reflection.class_name}' is associated to '#{reflection.through_reflection.class_name}' via :#{reflection.source_reflection.macro}.")
+ else
+ super("Cannot modify association.")
+ end
end
end
+ class HasManyThroughCantAssociateThroughHasOneOrManyReflection < ThroughCantAssociateThroughHasOneOrManyReflection #:nodoc:
+ end
+
+ class HasOneThroughCantAssociateThroughHasOneOrManyReflection < ThroughCantAssociateThroughHasOneOrManyReflection #:nodoc:
+ end
+
class HasManyThroughCantAssociateNewRecords < ActiveRecordError #:nodoc:
- def initialize(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.")
+ 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, 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.")
+ 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 HasManyThroughNestedAssociationsAreReadonly < ActiveRecordError #:nodoc:
- def initialize(owner, reflection)
- super("Cannot modify association '#{owner.class.name}##{reflection.name}' because it goes through more than one other association.")
+ class ThroughNestedAssociationsAreReadonly < ActiveRecordError #:nodoc:
+ def initialize(owner = nil, reflection = nil)
+ if owner && reflection
+ super("Cannot modify association '#{owner.class.name}##{reflection.name}' because it goes through more than one other association.")
+ else
+ super("Through nested associations are read-only.")
+ end
end
end
- class EagerLoadPolymorphicError < ActiveRecordError #:nodoc:
- def initialize(reflection)
- super("Cannot eagerly load the polymorphic association #{reflection.name.inspect}")
+ class HasManyThroughNestedAssociationsAreReadonly < ThroughNestedAssociationsAreReadonly #:nodoc:
+ end
+
+ class HasOneThroughNestedAssociationsAreReadonly < ThroughNestedAssociationsAreReadonly #:nodoc:
+ end
+
+ # This error is raised when trying to eager load a polymorphic association using a JOIN.
+ # Eager loading polymorphic associations is only possible with
+ # {ActiveRecord::Relation#preload}[rdoc-ref:QueryMethods#preload].
+ class EagerLoadPolymorphicError < ActiveRecordError
+ def initialize(reflection = nil)
+ if reflection
+ super("Cannot eagerly load the polymorphic association #{reflection.name.inspect}")
+ else
+ super("Eager load polymorphic error.")
+ end
end
end
class ReadOnlyAssociation < ActiveRecordError #:nodoc:
- def initialize(reflection)
- super("Cannot add to a has_many :through association. Try adding to #{reflection.through_reflection.name.inspect}.")
+ 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
@@ -95,8 +176,12 @@ module ActiveRecord
# (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
class DeleteRestrictionError < ActiveRecordError #:nodoc:
- def initialize(name)
- super("Cannot delete record because of dependent #{name}")
+ def initialize(name = nil)
+ if name
+ super("Cannot delete record because of dependent #{name}")
+ else
+ super("Delete restriction error.")
+ end
end
end
@@ -107,18 +192,19 @@ module ActiveRecord
# These classes will be loaded when associations are created.
# So there is no need to eager load them.
- autoload :Association, 'active_record/associations/association'
- autoload :SingularAssociation, 'active_record/associations/singular_association'
- autoload :CollectionAssociation, 'active_record/associations/collection_association'
- autoload :CollectionProxy, 'active_record/associations/collection_proxy'
+ autoload :Association
+ autoload :SingularAssociation
+ autoload :CollectionAssociation
+ autoload :ForeignAssociation
+ autoload :CollectionProxy
- autoload :BelongsToAssociation, 'active_record/associations/belongs_to_association'
- autoload :BelongsToPolymorphicAssociation, 'active_record/associations/belongs_to_polymorphic_association'
- autoload :HasManyAssociation, 'active_record/associations/has_many_association'
- autoload :HasManyThroughAssociation, 'active_record/associations/has_many_through_association'
- autoload :HasOneAssociation, 'active_record/associations/has_one_association'
- autoload :HasOneThroughAssociation, 'active_record/associations/has_one_through_association'
- autoload :ThroughAssociation, 'active_record/associations/through_association'
+ autoload :BelongsToAssociation
+ autoload :BelongsToPolymorphicAssociation
+ autoload :HasManyAssociation
+ autoload :HasManyThroughAssociation
+ autoload :HasOneAssociation
+ autoload :HasOneThroughAssociation
+ autoload :ThroughAssociation
module Builder #:nodoc:
autoload :Association, 'active_record/associations/builder/association'
@@ -132,26 +218,20 @@ module ActiveRecord
end
eager_autoload do
- autoload :Preloader, 'active_record/associations/preloader'
- autoload :JoinDependency, 'active_record/associations/join_dependency'
- autoload :AssociationScope, 'active_record/associations/association_scope'
- autoload :AliasTracker, 'active_record/associations/alias_tracker'
- end
-
- # Clears out the association cache.
- def clear_association_cache #:nodoc:
- @association_cache.clear if persisted?
+ autoload :Preloader
+ autoload :JoinDependency
+ autoload :AssociationScope
+ autoload :AliasTracker
end
- # :nodoc:
- attr_reader :association_cache
-
# Returns the association instance for the given name, instantiating it if it doesn't already exist
def association(name) #:nodoc:
association = association_instance_get(name)
if association.nil?
- raise AssociationNotFoundError.new(self, name) unless reflection = self.class._reflect_on_association(name)
+ unless reflection = self.class._reflect_on_association(name)
+ raise AssociationNotFoundError.new(self, name)
+ end
association = reflection.association_class.new(self, reflection)
association_instance_set(name, association)
end
@@ -159,8 +239,32 @@ module ActiveRecord
association
end
+ def association_cached?(name) # :nodoc
+ @association_cache.key?(name)
+ end
+
+ def initialize_dup(*) # :nodoc:
+ @association_cache = {}
+ super
+ end
+
+ def reload(*) # :nodoc:
+ clear_association_cache
+ super
+ end
+
private
- # Returns the specified association instance if it responds to :loaded?, nil otherwise.
+ # Clears out the association cache.
+ def clear_association_cache # :nodoc:
+ @association_cache.clear if persisted?
+ end
+
+ def init_internals # :nodoc:
+ @association_cache = {}
+ super
+ end
+
+ # Returns the specified association instance if it exists, nil otherwise.
def association_instance_get(name)
@association_cache[name]
end
@@ -197,7 +301,7 @@ module ActiveRecord
# === A word of warning
#
# Don't create associations that have the same name as instance methods of
- # <tt>ActiveRecord::Base</tt>. Since the association adds a method with that name to
+ # ActiveRecord::Base. Since the association adds a method with that name to
# its model, it will override the inherited method and break things.
# For instance, +attributes+ and +connection+ would be bad choices for association names.
#
@@ -241,7 +345,6 @@ module ActiveRecord
# others.find(*args) | X | X | X
# others.exists? | X | X | X
# others.distinct | X | X | X
- # others.uniq | X | X | X
# others.reset | X | X | X
#
# === Overriding generated methods
@@ -260,7 +363,7 @@ module ActiveRecord
# end
#
# If your model class is <tt>Project</tt>, the module is
- # named <tt>Project::GeneratedFeatureMethods</tt>. The GeneratedFeatureMethods module is
+ # named <tt>Project::GeneratedAssociationMethods</tt>. The +GeneratedAssociationMethods+ module is
# included in the model class immediately after the (anonymous) generated attributes methods
# module, meaning an association will override the methods for an attribute with the same name.
#
@@ -268,12 +371,12 @@ module ActiveRecord
#
# Active Record associations can be used to describe one-to-one, one-to-many and many-to-many
# relationships between models. Each model uses an association to describe its role in
- # the relation. The +belongs_to+ association is always used in the model that has
+ # the relation. The #belongs_to association is always used in the model that has
# the foreign key.
#
# === One-to-one
#
- # Use +has_one+ in the base, and +belongs_to+ in the associated model.
+ # Use #has_one in the base, and #belongs_to in the associated model.
#
# class Employee < ActiveRecord::Base
# has_one :office
@@ -284,7 +387,7 @@ module ActiveRecord
#
# === One-to-many
#
- # Use +has_many+ in the base, and +belongs_to+ in the associated model.
+ # Use #has_many in the base, and #belongs_to in the associated model.
#
# class Manager < ActiveRecord::Base
# has_many :employees
@@ -297,7 +400,7 @@ module ActiveRecord
#
# There are two ways to build a many-to-many relationship.
#
- # The first way uses a +has_many+ association with the <tt>:through</tt> option and a join model, so
+ # The first way uses a #has_many association with the <tt>:through</tt> option and a join model, so
# there are two stages of associations.
#
# class Assignment < ActiveRecord::Base
@@ -313,7 +416,7 @@ module ActiveRecord
# has_many :programmers, through: :assignments
# end
#
- # For the second way, use +has_and_belongs_to_many+ in both models. This requires a join table
+ # For the second way, use #has_and_belongs_to_many in both models. This requires a join table
# that has no corresponding model or primary key.
#
# class Programmer < ActiveRecord::Base
@@ -325,13 +428,13 @@ module ActiveRecord
#
# Choosing which way to build a many-to-many relationship is not always simple.
# If you need to work with the relationship model as its own entity,
- # use <tt>has_many :through</tt>. Use +has_and_belongs_to_many+ when working with legacy schemas or when
+ # use #has_many <tt>:through</tt>. Use #has_and_belongs_to_many when working with legacy schemas or when
# you never work directly with the relationship itself.
#
- # == Is it a +belongs_to+ or +has_one+ association?
+ # == Is it a #belongs_to or #has_one association?
#
# Both express a 1-1 relationship. The difference is mostly where to place the foreign
- # key, which goes on the table for the class declaring the +belongs_to+ relationship.
+ # key, which goes on the table for the class declaring the #belongs_to relationship.
#
# class User < ActiveRecord::Base
# # I reference an account.
@@ -346,14 +449,14 @@ module ActiveRecord
# The tables for these classes could look something like:
#
# CREATE TABLE users (
- # id int(11) NOT NULL auto_increment,
- # account_id int(11) default NULL,
+ # id int NOT NULL auto_increment,
+ # account_id int default NULL,
# name varchar default NULL,
# PRIMARY KEY (id)
# )
#
# CREATE TABLE accounts (
- # id int(11) NOT NULL auto_increment,
+ # id int NOT NULL auto_increment,
# name varchar default NULL,
# PRIMARY KEY (id)
# )
@@ -364,35 +467,35 @@ module ActiveRecord
# there is some special behavior you should be aware of, mostly involving the saving of
# associated objects.
#
- # You can set the <tt>:autosave</tt> option on a <tt>has_one</tt>, <tt>belongs_to</tt>,
- # <tt>has_many</tt>, or <tt>has_and_belongs_to_many</tt> association. Setting it
+ # You can set the <tt>:autosave</tt> option on a #has_one, #belongs_to,
+ # #has_many, or #has_and_belongs_to_many association. Setting it
# to +true+ will _always_ save the members, whereas setting it to +false+ will
# _never_ save the members. More details about <tt>:autosave</tt> option is available at
# AutosaveAssociation.
#
# === One-to-one associations
#
- # * Assigning an object to a +has_one+ association automatically saves that object and
+ # * Assigning an object to a #has_one association automatically saves that object and
# the object being replaced (if there is one), in order to update their foreign
# keys - except if the parent object is unsaved (<tt>new_record? == true</tt>).
# * If either of these saves fail (due to one of the objects being invalid), an
- # <tt>ActiveRecord::RecordNotSaved</tt> exception is raised and the assignment is
+ # ActiveRecord::RecordNotSaved exception is raised and the assignment is
# cancelled.
- # * If you wish to assign an object to a +has_one+ association without saving it,
- # use the <tt>build_association</tt> method (documented below). The object being
+ # * If you wish to assign an object to a #has_one association without saving it,
+ # use the <tt>#build_association</tt> method (documented below). The object being
# replaced will still be saved to update its foreign key.
- # * Assigning an object to a +belongs_to+ association does not save the object, since
+ # * Assigning an object to a #belongs_to association does not save the object, since
# the foreign key field belongs on the parent. It does not save the parent either.
#
# === Collections
#
- # * Adding an object to a collection (+has_many+ or +has_and_belongs_to_many+) automatically
+ # * Adding an object to a collection (#has_many or #has_and_belongs_to_many) automatically
# saves that object, except if the parent object (the owner of the collection) is not yet
# stored in the database.
# * If saving any of the objects being added to a collection (via <tt>push</tt> or similar)
# fails, then <tt>push</tt> returns +false+.
# * If saving fails while replacing the collection (via <tt>association=</tt>), an
- # <tt>ActiveRecord::RecordNotSaved</tt> exception is raised and the assignment is
+ # ActiveRecord::RecordNotSaved exception is raised and the assignment is
# cancelled.
# * You can add an object to a collection without automatically saving it by using the
# <tt>collection.build</tt> method (documented below).
@@ -401,14 +504,14 @@ module ActiveRecord
#
# == Customizing the query
#
- # \Associations are built from <tt>Relation</tt>s, and you can use the <tt>Relation</tt> syntax
+ # \Associations are built from <tt>Relation</tt>s, and you can use the Relation syntax
# to customize them. For example, to add a condition:
#
# class Blog < ActiveRecord::Base
- # has_many :published_posts, -> { where published: true }, class_name: 'Post'
+ # has_many :published_posts, -> { where(published: true) }, class_name: 'Post'
# end
#
- # Inside the <tt>-> { ... }</tt> block you can use all of the usual <tt>Relation</tt> methods.
+ # Inside the <tt>-> { ... }</tt> block you can use all of the usual Relation methods.
#
# === Accessing the owner object
#
@@ -417,7 +520,7 @@ module ActiveRecord
# events that occur on the user's birthday:
#
# class User < ActiveRecord::Base
- # has_many :birthday_events, ->(user) { where starts_on: user.birthday }, class_name: 'Event'
+ # 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.
@@ -447,9 +550,11 @@ module ActiveRecord
#
# Possible callbacks are: +before_add+, +after_add+, +before_remove+ and +after_remove+.
#
- # Should any of the +before_add+ callbacks throw an exception, the object does not get
- # added to the collection. Same with the +before_remove+ callbacks; if an exception is
- # thrown the object doesn't get removed.
+ # If any of the +before_add+ callbacks throw an exception, the object will not be
+ # added to the collection.
+ #
+ # Similarly, if any of the +before_remove+ callbacks throw an exception, the object
+ # will not be removed from the collection.
#
# == Association extensions
#
@@ -494,8 +599,8 @@ module ActiveRecord
#
# * <tt>record.association(:items).owner</tt> - Returns the object the association is part of.
# * <tt>record.association(:items).reflection</tt> - Returns the reflection object that describes the association.
- # * <tt>record.association(:items).target</tt> - Returns the associated object for +belongs_to+ and +has_one+, or
- # the collection of associated objects for +has_many+ and +has_and_belongs_to_many+.
+ # * <tt>record.association(:items).target</tt> - Returns the associated object for #belongs_to and #has_one, or
+ # the collection of associated objects for #has_many and #has_and_belongs_to_many.
#
# However, inside the actual extension code, you will not have access to the <tt>record</tt> as
# above. In this case, you can access <tt>proxy_association</tt>. For example,
@@ -507,7 +612,7 @@ module ActiveRecord
#
# Has Many associations can be configured with the <tt>:through</tt> option to use an
# explicit join model to retrieve the data. This operates similarly to a
- # +has_and_belongs_to_many+ association. The advantage is that you're able to add validations,
+ # #has_and_belongs_to_many association. The advantage is that you're able to add validations,
# callbacks, and extra attributes on the join model. Consider the following schema:
#
# class Author < ActiveRecord::Base
@@ -524,7 +629,7 @@ module ActiveRecord
# @author.authorships.collect { |a| a.book } # selects all books that the author's authorships belong to
# @author.books # selects all books by using the Authorship join model
#
- # You can also go through a +has_many+ association on the join model:
+ # You can also go through a #has_many association on the join model:
#
# class Firm < ActiveRecord::Base
# has_many :clients
@@ -544,7 +649,7 @@ module ActiveRecord
# @firm.clients.flat_map { |c| c.invoices } # select all invoices for all clients of the firm
# @firm.invoices # selects all invoices by going through the Client join model
#
- # Similarly you can go through a +has_one+ association on the join model:
+ # Similarly you can go through a #has_one association on the join model:
#
# class Group < ActiveRecord::Base
# has_many :users
@@ -564,7 +669,7 @@ module ActiveRecord
# @group.users.collect { |u| u.avatar }.compact # select all avatars for all users in the group
# @group.avatars # selects all avatars by going through the User join model.
#
- # An important caveat with going through +has_one+ or +has_many+ associations on the
+ # An important caveat with going through #has_one or #has_many associations on the
# join model is that these associations are *read-only*. For example, the following
# would not work following the previous example:
#
@@ -573,26 +678,26 @@ module ActiveRecord
#
# == Setting Inverses
#
- # If you are using a +belongs_to+ on the join model, it is a good idea to set the
- # <tt>:inverse_of</tt> option on the +belongs_to+, which will mean that the following example
- # works correctly (where <tt>tags</tt> is a +has_many+ <tt>:through</tt> association):
+ # If you are using a #belongs_to on the join model, it is a good idea to set the
+ # <tt>:inverse_of</tt> option on the #belongs_to, which will mean that the following example
+ # works correctly (where <tt>tags</tt> is a #has_many <tt>:through</tt> association):
#
# @post = Post.first
# @tag = @post.tags.build name: "ruby"
# @tag.save
#
- # The last line ought to save the through record (a <tt>Taggable</tt>). This will only work if the
+ # The last line ought to save the through record (a <tt>Tagging</tt>). This will only work if the
# <tt>:inverse_of</tt> is set:
#
- # class Taggable < ActiveRecord::Base
+ # class Tagging < ActiveRecord::Base
# belongs_to :post
# belongs_to :tag, inverse_of: :taggings
# end
#
# If you do not set the <tt>:inverse_of</tt> record, the association will
# do its best to match itself up with the correct inverse. Automatic
- # inverse detection only works on <tt>has_many</tt>, <tt>has_one</tt>, and
- # <tt>belongs_to</tt> associations.
+ # inverse detection only works on #has_many, #has_one, and
+ # #belongs_to associations.
#
# Extra options on the associations, as defined in the
# <tt>AssociationReflection::INVALID_AUTOMATIC_INVERSE_OPTIONS</tt> constant, will
@@ -605,7 +710,7 @@ module ActiveRecord
# You can turn off the automatic detection of inverse associations by setting
# the <tt>:inverse_of</tt> option to <tt>false</tt> like so:
#
- # class Taggable < ActiveRecord::Base
+ # class Tagging < ActiveRecord::Base
# belongs_to :tag, inverse_of: false
# end
#
@@ -647,7 +752,7 @@ module ActiveRecord
# belongs_to :commenter
# end
#
- # When using nested association, you will not be able to modify the association because there
+ # When using a nested association, you will not be able to modify the association because there
# is not enough information to know what modification to make. For example, if you tried to
# add a <tt>Commenter</tt> in the example above, there would be no way to tell how to set up the
# intermediate <tt>Post</tt> and <tt>Comment</tt> objects.
@@ -655,7 +760,7 @@ module ActiveRecord
# == Polymorphic \Associations
#
# Polymorphic associations on models are not restricted on what types of models they
- # can be associated with. Rather, they specify an interface that a +has_many+ association
+ # can be associated with. Rather, they specify an interface that a #has_many association
# must adhere to.
#
# class Asset < ActiveRecord::Base
@@ -717,7 +822,7 @@ module ActiveRecord
# == Eager loading of associations
#
# Eager loading is a way to find objects of a certain class and a number of named associations.
- # This is one of the easiest ways of to prevent the dreaded N+1 problem in which fetching 100
+ # It is one of the easiest ways to prevent the dreaded N+1 problem in which fetching 100
# posts that each need to display their author triggers 101 database queries. Through the
# use of eager loading, the number of queries will be reduced from 101 to 2.
#
@@ -739,7 +844,7 @@ module ActiveRecord
#
# Post.includes(:author).each do |post|
#
- # This references the name of the +belongs_to+ association that also used the <tt>:author</tt>
+ # This references the name of the #belongs_to association that also used the <tt>:author</tt>
# symbol. After loading the posts, find will collect the +author_id+ from each one and load
# all the referenced authors with one query. Doing so will cut down the number of queries
# from 201 to 102.
@@ -749,16 +854,16 @@ module ActiveRecord
# Post.includes(:author, :comments).each do |post|
#
# This will load all comments with a single query. This reduces the total number of queries
- # to 3. More generally the number of queries will be 1 plus the number of associations
- # named (except if some of the associations are polymorphic +belongs_to+ - see below).
+ # to 3. In general, the number of queries will be 1 plus the number of associations
+ # named (except if some of the associations are polymorphic #belongs_to - see below).
#
# To include a deep hierarchy of associations, use a hash:
#
- # Post.includes(:author, {comments: {author: :gravatar}}).each do |post|
+ # Post.includes(:author, { comments: { author: :gravatar } }).each do |post|
#
- # That'll grab not only all the comments but all their authors and gravatar pictures.
- # You can mix and match symbols, arrays and hashes in any combination to describe the
- # associations you want to load.
+ # The above code will load all the comments and all of their associated
+ # authors and gravatars. You can mix and match any combination of symbols,
+ # arrays, and hashes to retrieve the associations you want to load.
#
# All of this power shouldn't fool you into thinking that you can pull out huge amounts
# of data with no performance penalty just because you've reduced the number of queries.
@@ -767,8 +872,8 @@ module ActiveRecord
# cut down on the number of queries in a situation as the one described above.
#
# Since only one table is loaded at a time, conditions or orders cannot reference tables
- # other than the main one. If this is the case Active Record falls back to the previously
- # used LEFT OUTER JOIN based strategy. For example
+ # other than the main one. If this is the case, Active Record falls back to the previously
+ # used LEFT OUTER JOIN based strategy. For example:
#
# Post.includes([:author, :comments]).where(['comments.approved = ?', true])
#
@@ -790,7 +895,7 @@ module ActiveRecord
# In this case it is usually more natural to include an association which has conditions defined on it:
#
# class Post < ActiveRecord::Base
- # has_many :approved_comments, -> { where approved: true }, class_name: 'Comment'
+ # has_many :approved_comments, -> { where(approved: true) }, class_name: 'Comment'
# end
#
# Post.includes(:approved_comments)
@@ -822,7 +927,7 @@ module ActiveRecord
# For example if all the addressables are either of class Person or Company then a total
# of 3 queries will be executed. The list of addressable types to load is determined on
# the back of the addresses loaded. This is not supported if Active Record has to fallback
- # to the previous implementation of eager loading and will raise <tt>ActiveRecord::EagerLoadPolymorphicError</tt>.
+ # to the previous implementation of eager loading and will raise ActiveRecord::EagerLoadPolymorphicError.
# The reason is that the parent model's type is a column value so its corresponding table
# name cannot be put in the +FROM+/+JOIN+ clauses of that query.
#
@@ -864,7 +969,7 @@ module ActiveRecord
# INNER JOIN categories_posts posts_categories_join INNER JOIN posts posts_categories
# INNER JOIN categories_posts categories_posts_join INNER JOIN categories categories_posts_2
#
- # If you wish to specify your own custom joins using <tt>joins</tt> method, those table
+ # If you wish to specify your own custom joins using ActiveRecord::QueryMethods#joins method, those table
# names will take precedence over the eager associations:
#
# Post.joins(:comments).joins("inner join comments ...")
@@ -929,20 +1034,16 @@ module ActiveRecord
# The +traps+ association on +Dungeon+ and the +dungeon+ association on +Trap+ are
# the inverse of each other and the inverse of the +dungeon+ association on +EvilWizard+
# is the +evil_wizard+ association on +Dungeon+ (and vice-versa). By default,
- # Active Record doesn't know anything about these inverse relationships and so no object
- # loading optimization is possible. For example:
+ # Active Record can guess the inverse of the association based on the name
+ # of the class. The result is the following:
#
# d = Dungeon.first
# t = d.traps.first
- # d.level == t.dungeon.level # => true
- # d.level = 10
- # d.level == t.dungeon.level # => false
+ # d.object_id == t.dungeon.object_id # => true
#
# The +Dungeon+ instances +d+ and <tt>t.dungeon</tt> in the above example refer to
- # the same object data from the database, but are actually different in-memory copies
- # of that data. Specifying the <tt>:inverse_of</tt> option on associations lets you tell
- # Active Record about inverse relationships and it will optimise object loading. For
- # example, if we changed our model definitions to:
+ # the same in-memory instance since the association matches the name of the class.
+ # The result would be the same if we added +:inverse_of+ to our model definitions:
#
# class Dungeon < ActiveRecord::Base
# has_many :traps, inverse_of: :dungeon
@@ -957,20 +1058,19 @@ module ActiveRecord
# belongs_to :dungeon, inverse_of: :evil_wizard
# end
#
- # Then, from our code snippet above, +d+ and <tt>t.dungeon</tt> are actually the same
- # in-memory instance and our final <tt>d.level == t.dungeon.level</tt> will return +true+.
- #
# 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.
- # * for +belongs_to+ associations +has_many+ inverse associations are ignored.
+ # * for #belongs_to associations #has_many inverse associations are ignored.
+ #
+ # For more information, see the documentation for the +:inverse_of+ option.
#
# == Deleting from associations
#
# === Dependent associations
#
- # +has_many+, +has_one+ and +belongs_to+ associations support the <tt>:dependent</tt> option.
+ # #has_many, #has_one and #belongs_to associations support the <tt>:dependent</tt> option.
# This allows you to specify that associated records should be deleted when the owner is
# deleted.
#
@@ -991,20 +1091,22 @@ module ActiveRecord
# callbacks declared either before or after the <tt>:dependent</tt> option
# can affect what it does.
#
+ # Note that <tt>:dependent</tt> option is ignored for #has_one <tt>:through</tt> associations.
+ #
# === Delete or destroy?
#
- # +has_many+ and +has_and_belongs_to_many+ associations have the methods <tt>destroy</tt>,
+ # #has_many and #has_and_belongs_to_many associations have the methods <tt>destroy</tt>,
# <tt>delete</tt>, <tt>destroy_all</tt> and <tt>delete_all</tt>.
#
- # For +has_and_belongs_to_many+, <tt>delete</tt> and <tt>destroy</tt> are the same: they
+ # For #has_and_belongs_to_many, <tt>delete</tt> and <tt>destroy</tt> are the same: they
# cause the records in the join table to be removed.
#
- # For +has_many+, <tt>destroy</tt> and <tt>destroy_all</tt> will always call the <tt>destroy</tt> method of the
+ # For #has_many, <tt>destroy</tt> and <tt>destroy_all</tt> will always call the <tt>destroy</tt> method of the
# record(s) being removed so that callbacks are run. However <tt>delete</tt> and <tt>delete_all</tt> will either
# do the deletion according to the strategy specified by the <tt>:dependent</tt> option, or
# if no <tt>:dependent</tt> option is given, then it will follow the default strategy.
- # The default strategy is <tt>:nullify</tt> (set the foreign keys to <tt>nil</tt>), except for
- # +has_many+ <tt>:through</tt>, where the default strategy is <tt>delete_all</tt> (delete
+ # The default strategy is to do nothing (leave the foreign keys with the parent ids set), except for
+ # #has_many <tt>:through</tt>, where the default strategy is <tt>delete_all</tt> (delete
# the join records, without running their callbacks).
#
# There is also a <tt>clear</tt> method which is the same as <tt>delete_all</tt>, except that
@@ -1012,13 +1114,13 @@ module ActiveRecord
#
# === What gets deleted?
#
- # There is a potential pitfall here: +has_and_belongs_to_many+ and +has_many+ <tt>:through</tt>
+ # There is a potential pitfall here: #has_and_belongs_to_many and #has_many <tt>:through</tt>
# associations have records in join tables, as well as the associated records. So when we
# call one of these deletion methods, what exactly should be deleted?
#
# The answer is that it is assumed that deletion on an association is about removing the
# <i>link</i> between the owner and the associated object(s), rather than necessarily the
- # associated objects themselves. So with +has_and_belongs_to_many+ and +has_many+
+ # associated objects themselves. So with #has_and_belongs_to_many and #has_many
# <tt>:through</tt>, the join records will be deleted, but the associated records won't.
#
# This makes sense if you think about it: if you were to call <tt>post.tags.delete(Tag.find_by(name: 'food'))</tt>
@@ -1029,20 +1131,20 @@ module ActiveRecord
# a person has many projects, and each project has many tasks. If we deleted one of a person's
# tasks, we would probably not want the project to be deleted. In this scenario, the delete method
# won't actually work: it can only be used if the association on the join model is a
- # +belongs_to+. In other situations you are expected to perform operations directly on
+ # #belongs_to. In other situations you are expected to perform operations directly on
# either the associated records or the <tt>:through</tt> association.
#
- # With a regular +has_many+ there is no distinction between the "associated records"
+ # With a regular #has_many there is no distinction between the "associated records"
# and the "link", so there is only one choice for what gets deleted.
#
- # With +has_and_belongs_to_many+ and +has_many+ <tt>:through</tt>, if you want to delete the
+ # With #has_and_belongs_to_many and #has_many <tt>:through</tt>, if you want to delete the
# associated records themselves, you can always do something along the lines of
# <tt>person.tasks.each(&:destroy)</tt>.
#
- # == Type safety with <tt>ActiveRecord::AssociationTypeMismatch</tt>
+ # == Type safety with ActiveRecord::AssociationTypeMismatch
#
# If you attempt to assign an object to an association that doesn't match the inferred
- # or specified <tt>:class_name</tt>, you'll get an <tt>ActiveRecord::AssociationTypeMismatch</tt>.
+ # or specified <tt>:class_name</tt>, you'll get an ActiveRecord::AssociationTypeMismatch.
#
# == Options
#
@@ -1052,7 +1154,7 @@ module ActiveRecord
# Specifies a one-to-many association. The following methods for retrieval and query of
# collections of associated objects will be added:
#
- # +collection+ is a placeholder for the symbol passed as the first argument, so
+ # +collection+ is a placeholder for the symbol passed as the +name+ argument, so
# <tt>has_many :clients</tt> would add among others <tt>clients.empty?</tt>.
#
# [collection(force_reload = false)]
@@ -1096,10 +1198,10 @@ module ActiveRecord
# [collection.size]
# Returns the number of associated objects.
# [collection.find(...)]
- # Finds an associated object according to the same rules as <tt>ActiveRecord::Base.find</tt>.
+ # Finds an associated object according to the same rules as ActiveRecord::FinderMethods#find.
# [collection.exists?(...)]
# Checks whether an associated object with the given conditions exists.
- # Uses the same rules as <tt>ActiveRecord::Base.exists?</tt>.
+ # Uses the same rules as ActiveRecord::FinderMethods#exists?.
# [collection.build(attributes = {}, ...)]
# Returns one or more new objects of the collection type that have been instantiated
# with +attributes+ and linked to this object through a foreign key, but have not yet
@@ -1110,7 +1212,7 @@ module ActiveRecord
# been saved (if it passed the validation). *Note*: This only works if the base model
# already exists in the DB, not if it is a new (unsaved) record!
# [collection.create!(attributes = {})]
- # Does the same as <tt>collection.create</tt>, but raises <tt>ActiveRecord::RecordInvalid</tt>
+ # Does the same as <tt>collection.create</tt>, but raises ActiveRecord::RecordInvalid
# if the record is invalid.
#
# === Example
@@ -1131,20 +1233,51 @@ module ActiveRecord
# * <tt>Firm#clients.build</tt> (similar to <tt>Client.new("firm_id" => id)</tt>)
# * <tt>Firm#clients.create</tt> (similar to <tt>c = Client.new("firm_id" => id); c.save; c</tt>)
# * <tt>Firm#clients.create!</tt> (similar to <tt>c = Client.new("firm_id" => id); c.save!</tt>)
- # The declaration can also include an options hash to specialize the behavior of the association.
+ # The declaration can also include an +options+ hash to specialize the behavior of the association.
+ #
+ # === Scopes
+ #
+ # You can pass a second argument +scope+ as a callable (i.e. proc or
+ # lambda) to retrieve a specific set of records or customize the generated
+ # query when you access the associated collection.
+ #
+ # Scope examples:
+ # has_many :comments, -> { where(author_id: 1) }
+ # has_many :employees, -> { joins(:address) }
+ # has_many :posts, ->(post) { where("max_post_length > ?", post.length) }
+ #
+ # === Extensions
+ #
+ # The +extension+ argument allows you to pass a block into a has_many
+ # association. This is useful for adding new finders, creators and other
+ # factory-type methods to be used as part of the association.
+ #
+ # Extension examples:
+ # has_many :employees do
+ # def find_or_create_by_name(name)
+ # first_name, last_name = name.split(" ", 2)
+ # find_or_create_by(first_name: first_name, last_name: last_name)
+ # end
+ # end
#
# === Options
# [:class_name]
# Specify the class name of the association. Use it only if that name can't be inferred
# from the association name. So <tt>has_many :products</tt> will by default be linked
- # to the Product class, but if the real class name is SpecialProduct, you'll have to
+ # to the +Product+ class, but if the real class name is +SpecialProduct+, you'll have to
# specify it with this option.
# [:foreign_key]
# 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+
+ # 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>.
+ # [: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
+ # specified on "as" option with a "_type" suffix. So a class that defines a
+ # <tt>has_many :tags, as: :taggable</tt> association will use "taggable_type" as the
+ # default <tt>:foreign_type</tt>.
# [:primary_key]
- # Specify the method that returns the primary key used for the association. By default this is +id+.
+ # Specify the name of the column to use as the primary key for the association. By default this is +id+.
# [:dependent]
# Controls what happens to the associated objects when
# their owner is destroyed. Note that these are implemented as
@@ -1159,20 +1292,20 @@ module ActiveRecord
# * <tt>:restrict_with_error</tt> causes an error to be added to the owner if there are any associated objects.
#
# If using with the <tt>:through</tt> option, the association on the join model must be
- # a +belongs_to+, and the records which get deleted are the join records, rather than
+ # a #belongs_to, and the records which get deleted are the join records, rather than
# the associated records.
# [:counter_cache]
# This option can be used to configure a custom named <tt>:counter_cache.</tt> You only need this option,
- # when you customized the name of your <tt>:counter_cache</tt> on the <tt>belongs_to</tt> association.
+ # when you customized the name of your <tt>:counter_cache</tt> on the #belongs_to association.
# [:as]
- # Specifies a polymorphic interface (See <tt>belongs_to</tt>).
+ # Specifies a polymorphic interface (See #belongs_to).
# [:through]
# Specifies an association through which to perform the query. This can be any other type
# of association, including other <tt>:through</tt> associations. Options for <tt>:class_name</tt>,
# <tt>:primary_key</tt> and <tt>:foreign_key</tt> are ignored, as the association uses the
# source reflection.
#
- # If the association on the join model is a +belongs_to+, the collection can be modified
+ # If the association on the join model is a #belongs_to, the collection can be modified
# and the records on the <tt>:through</tt> model will be automatically created and removed
# as appropriate. Otherwise, the collection is read-only, so you should manipulate the
# <tt>:through</tt> association directly.
@@ -1183,13 +1316,13 @@ module ActiveRecord
# the appropriate join model records when they are saved. (See the 'Association Join Models'
# section above.)
# [:source]
- # Specifies the source association name used by <tt>has_many :through</tt> queries.
+ # Specifies the source association name used by #has_many <tt>:through</tt> queries.
# Only use it if the name cannot be inferred from the association.
# <tt>has_many :subscribers, through: :subscriptions</tt> will look for either <tt>:subscribers</tt> or
# <tt>:subscriber</tt> on Subscription, unless a <tt>:source</tt> is given.
# [:source_type]
- # Specifies type of the source association used by <tt>has_many :through</tt> queries where the source
- # association is a polymorphic +belongs_to+.
+ # Specifies type of the source association used by #has_many <tt>:through</tt> queries where the source
+ # association is a polymorphic #belongs_to.
# [:validate]
# If +false+, don't validate the associated objects when saving the parent object. true by default.
# [:autosave]
@@ -1199,18 +1332,23 @@ module ActiveRecord
# +before_save+ callback. Because callbacks are run in the order they are defined, associated objects
# may need to be explicitly saved in any user-defined +before_save+ callbacks.
#
- # Note that <tt>accepts_nested_attributes_for</tt> sets <tt>:autosave</tt> to <tt>true</tt>.
+ # Note that NestedAttributes::ClassMethods#accepts_nested_attributes_for sets
+ # <tt>:autosave</tt> to <tt>true</tt>.
# [:inverse_of]
- # Specifies the name of the <tt>belongs_to</tt> association on the associated object
- # that is the inverse of this <tt>has_many</tt> association. Does not work in combination
+ # 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.
# 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.
+ # Useful for defining methods on associations, especially when they should be shared between multiple
+ # association objects.
#
# Option examples:
- # has_many :comments, -> { order "posted_on" }
- # has_many :comments, -> { includes :author }
- # has_many :people, -> { where("deleted = 0").order("name") }, class_name: "Person"
- # has_many :tracks, -> { order "position" }, dependent: :destroy
+ # has_many :comments, -> { order("posted_on") }
+ # has_many :comments, -> { includes(:author) }
+ # has_many :people, -> { where(deleted: false).order("name") }, class_name: "Person"
+ # has_many :tracks, -> { order("position") }, dependent: :destroy
# has_many :comments, dependent: :nullify
# has_many :tags, as: :taggable
# has_many :reports, -> { readonly }
@@ -1222,12 +1360,12 @@ module ActiveRecord
# Specifies a one-to-one association with another class. This method should only be used
# if the other class contains the foreign key. If the current class contains the foreign key,
- # then you should use +belongs_to+ instead. See also ActiveRecord::Associations::ClassMethods's overview
- # on when to use +has_one+ and when to use +belongs_to+.
+ # then you should use #belongs_to instead. See also ActiveRecord::Associations::ClassMethods's overview
+ # on when to use #has_one and when to use #belongs_to.
#
# The following methods for retrieval and query of a single associated object will be added:
#
- # +association+ is a placeholder for the symbol passed as the first argument, so
+ # +association+ is a placeholder for the symbol passed as the +name+ argument, so
# <tt>has_one :manager</tt> would add among others <tt>manager.nil?</tt>.
#
# [association(force_reload = false)]
@@ -1245,7 +1383,7 @@ module ActiveRecord
# with +attributes+, linked to this object through a foreign key, and that
# has already been saved (if it passed the validation).
# [create_association!(attributes = {})]
- # Does the same as <tt>create_association</tt>, but raises <tt>ActiveRecord::RecordInvalid</tt>
+ # Does the same as <tt>create_association</tt>, but raises ActiveRecord::RecordInvalid
# if the record is invalid.
#
# === Example
@@ -1257,9 +1395,20 @@ module ActiveRecord
# * <tt>Account#create_beneficiary</tt> (similar to <tt>b = Beneficiary.new("account_id" => id); b.save; b</tt>)
# * <tt>Account#create_beneficiary!</tt> (similar to <tt>b = Beneficiary.new("account_id" => id); b.save!; b</tt>)
#
+ # === Scopes
+ #
+ # You can pass a second argument +scope+ as a callable (i.e. proc or
+ # lambda) to retrieve a specific record or customize the generated query
+ # when you access the associated object.
+ #
+ # Scope examples:
+ # has_one :author, -> { where(comment_id: 1) }
+ # has_one :employer, -> { joins(:company) }
+ # has_one :dob, ->(dob) { where("Date.new(2000, 01, 01) > ?", dob) }
+ #
# === Options
#
- # The declaration can also include an options hash to specialize the behavior of the association.
+ # The declaration can also include an +options+ hash to specialize the behavior of the association.
#
# Options are:
# [:class_name]
@@ -1275,27 +1424,35 @@ module ActiveRecord
# * <tt>:nullify</tt> causes the foreign key to be set to +NULL+. Callbacks are not executed.
# * <tt>:restrict_with_exception</tt> causes an exception to be raised if there is an associated record
# * <tt>:restrict_with_error</tt> causes an error to be added to the owner if there is an associated object
+ #
+ # Note that <tt>:dependent</tt> option is ignored when using <tt>:through</tt> option.
# [:foreign_key]
# 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
+ # 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>.
+ # [: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
+ # specified on "as" option with a "_type" suffix. So a class that defines a
+ # <tt>has_one :tag, as: :taggable</tt> association will use "taggable_type" as the
+ # default <tt>:foreign_type</tt>.
# [:primary_key]
# Specify the method that returns the primary key used for the association. By default this is +id+.
# [:as]
- # Specifies a polymorphic interface (See <tt>belongs_to</tt>).
+ # Specifies a polymorphic interface (See #belongs_to).
# [:through]
# Specifies a Join Model through which to perform the query. Options for <tt>:class_name</tt>,
# <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 <tt>has_one</tt>
- # or <tt>belongs_to</tt> association on the join model.
+ # source reflection. You can only use a <tt>:through</tt> query through a #has_one
+ # or #belongs_to association on the join model.
# [:source]
- # Specifies the source association name used by <tt>has_one :through</tt> queries.
+ # 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.
# <tt>has_one :favorite, through: :favorites</tt> will look for a
# <tt>:favorite</tt> on Favorite, unless a <tt>:source</tt> is given.
# [:source_type]
- # Specifies type of the source association used by <tt>has_one :through</tt> queries where the source
- # association is a polymorphic +belongs_to+.
+ # Specifies type of the source association used by #has_one <tt>:through</tt> queries where the source
+ # association is a polymorphic #belongs_to.
# [:validate]
# If +false+, don't validate the associated object when saving the parent object. +false+ by default.
# [:autosave]
@@ -1303,10 +1460,11 @@ module ActiveRecord
# when saving the parent object. If false, never save or destroy the associated object.
# By default, only save the associated object if it's a new record.
#
- # Note that <tt>accepts_nested_attributes_for</tt> sets <tt>:autosave</tt> to <tt>true</tt>.
+ # Note that NestedAttributes::ClassMethods#accepts_nested_attributes_for sets
+ # <tt>:autosave</tt> to <tt>true</tt>.
# [:inverse_of]
- # Specifies the name of the <tt>belongs_to</tt> association on the associated object
- # that is the inverse of this <tt>has_one</tt> association. Does not work in combination
+ # 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.
# See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail.
# [:required]
@@ -1318,12 +1476,12 @@ module ActiveRecord
# has_one :credit_card, dependent: :destroy # destroys the associated credit card
# has_one :credit_card, dependent: :nullify # updates the associated records foreign
# # key value to NULL rather than destroying it
- # has_one :last_comment, -> { order 'posted_on' }, class_name: "Comment"
- # has_one :project_manager, -> { where role: 'project_manager' }, class_name: "Person"
+ # has_one :last_comment, -> { order('posted_on') }, class_name: "Comment"
+ # has_one :project_manager, -> { where(role: 'project_manager') }, class_name: "Person"
# has_one :attachment, as: :attachable
- # has_one :boss, readonly: :true
+ # has_one :boss, -> { readonly }
# has_one :club, through: :membership
- # has_one :primary_address, -> { where primary: true }, through: :addressables, source: :addressable
+ # has_one :primary_address, -> { where(primary: true) }, through: :addressables, source: :addressable
# has_one :credit_card, required: true
def has_one(name, scope = nil, options = {})
reflection = Builder::HasOne.build(self, name, scope, options)
@@ -1332,13 +1490,13 @@ module ActiveRecord
# Specifies a one-to-one association with another class. This method should only be used
# if this class contains the foreign key. If the other class contains the foreign key,
- # then you should use +has_one+ instead. See also ActiveRecord::Associations::ClassMethods's overview
- # on when to use +has_one+ and when to use +belongs_to+.
+ # then you should use #has_one instead. See also ActiveRecord::Associations::ClassMethods's overview
+ # on when to use #has_one and when to use #belongs_to.
#
# Methods will be added for retrieval and query for a single associated object, for which
# this object holds an id:
#
- # +association+ is a placeholder for the symbol passed as the first argument, so
+ # +association+ is a placeholder for the symbol passed as the +name+ argument, so
# <tt>belongs_to :author</tt> would add among others <tt>author.nil?</tt>.
#
# [association(force_reload = false)]
@@ -1353,7 +1511,7 @@ module ActiveRecord
# with +attributes+, linked to this object through a foreign key, and that
# has already been saved (if it passed the validation).
# [create_association!(attributes = {})]
- # Does the same as <tt>create_association</tt>, but raises <tt>ActiveRecord::RecordInvalid</tt>
+ # Does the same as <tt>create_association</tt>, but raises ActiveRecord::RecordInvalid
# if the record is invalid.
#
# === Example
@@ -1364,7 +1522,18 @@ module ActiveRecord
# * <tt>Post#build_author</tt> (similar to <tt>post.author = Author.new</tt>)
# * <tt>Post#create_author</tt> (similar to <tt>post.author = Author.new; post.author.save; post.author</tt>)
# * <tt>Post#create_author!</tt> (similar to <tt>post.author = Author.new; post.author.save!; post.author</tt>)
- # The declaration can also include an options hash to specialize the behavior of the association.
+ # The declaration can also include an +options+ hash to specialize the behavior of the association.
+ #
+ # === Scopes
+ #
+ # You can pass a second argument +scope+ as a callable (i.e. proc or
+ # lambda) to retrieve a specific record or customize the generated query
+ # when you access the associated object.
+ #
+ # Scope examples:
+ # belongs_to :firm, -> { where(id: 2) }
+ # belongs_to :user, -> { joins(:friends) }
+ # belongs_to :level, ->(level) { where("game_level > ?", level.current) }
#
# === Options
#
@@ -1389,12 +1558,12 @@ module ActiveRecord
# [:dependent]
# If set to <tt>:destroy</tt>, the associated object is destroyed when this object is. If set to
# <tt>:delete</tt>, the associated object is deleted *without* calling its destroy method.
- # This option should not be specified when <tt>belongs_to</tt> is used in conjunction with
- # a <tt>has_many</tt> relationship on another class because of the potential to leave
+ # This option should not be specified when #belongs_to is used in conjunction with
+ # a #has_many relationship on another class because of the potential to leave
# orphaned records behind.
# [:counter_cache]
- # Caches the number of belonging objects on the associate class through the use of +increment_counter+
- # and +decrement_counter+. The counter cache is incremented when an object of this
+ # Caches the number of belonging objects on the associate class through the use of CounterCache::ClassMethods#increment_counter
+ # and CounterCache::ClassMethods#decrement_counter. The counter cache is incremented when an object of this
# class is created and decremented when it's destroyed. This requires that a column
# named <tt>#{table_name}_count</tt> (such as +comments_count+ for a belonging Comment class)
# is used on the associate class (such as a Post class) - that is the migration for
@@ -1416,33 +1585,38 @@ module ActiveRecord
# If false, never save or destroy the associated object.
# By default, only save the associated object if it's a new record.
#
- # Note that <tt>accepts_nested_attributes_for</tt> sets <tt>:autosave</tt> to <tt>true</tt>.
+ # Note that NestedAttributes::ClassMethods#accepts_nested_attributes_for
+ # sets <tt>:autosave</tt> to <tt>true</tt>.
# [:touch]
# If true, the associated object will be touched (the updated_at/on attributes set to current time)
# when this record is either saved or destroyed. If you specify a symbol, that attribute
# will be updated with the current time in addition to the updated_at/on attribute.
# [:inverse_of]
- # Specifies the name of the <tt>has_one</tt> or <tt>has_many</tt> association on the associated
- # object that is the inverse of this <tt>belongs_to</tt> association. Does not work in
+ # 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.
# 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.
# [:required]
# When set to +true+, the association will also have its presence validated.
# This will validate the association itself, not the id. You can use
# +:inverse_of+ to avoid an extra query during validation.
+ # NOTE: <tt>required</tt> is set to <tt>true</tt> by default and is deprecated. If
+ # you don't want to have association presence validated, use <tt>optional: true</tt>.
#
# Option examples:
# belongs_to :firm, foreign_key: "client_of"
# belongs_to :person, primary_key: "name", foreign_key: "person_name"
# belongs_to :author, class_name: "Person", foreign_key: "author_id"
- # belongs_to :valid_coupon, ->(o) { where "discounts > #{o.payments_count}" },
+ # belongs_to :valid_coupon, ->(o) { where "discounts > ?", o.payments_count },
# class_name: "Coupon", foreign_key: "coupon_id"
# belongs_to :attachable, polymorphic: true
- # belongs_to :project, readonly: true
+ # belongs_to :project, -> { readonly }
# belongs_to :post, counter_cache: true
- # belongs_to :company, touch: true
+ # belongs_to :comment, touch: true
# belongs_to :company, touch: :employees_last_updated_at
- # belongs_to :company, required: true
+ # belongs_to :user, optional: true
def belongs_to(name, scope = nil, options = {})
reflection = Builder::BelongsTo.build(self, name, scope, options)
Reflection.add_reflection self, name, reflection
@@ -1467,10 +1641,7 @@ module ActiveRecord
#
# class CreateDevelopersProjectsJoinTable < ActiveRecord::Migration
# def change
- # create_table :developers_projects, id: false do |t|
- # t.integer :developer_id
- # t.integer :project_id
- # end
+ # create_join_table :developers, :projects
# end
# end
#
@@ -1480,7 +1651,7 @@ module ActiveRecord
#
# Adds the following methods for retrieval and query:
#
- # +collection+ is a placeholder for the symbol passed as the first argument, so
+ # +collection+ is a placeholder for the symbol passed as the +name+ argument, so
# <tt>has_and_belongs_to_many :categories</tt> would add among others <tt>categories.empty?</tt>.
#
# [collection(force_reload = false)]
@@ -1512,10 +1683,10 @@ module ActiveRecord
# [collection.find(id)]
# Finds an associated object responding to the +id+ and that
# meets the condition that it has to be associated with this object.
- # Uses the same rules as <tt>ActiveRecord::Base.find</tt>.
+ # Uses the same rules as ActiveRecord::FinderMethods#find.
# [collection.exists?(...)]
# Checks whether an associated object with the given conditions exists.
- # Uses the same rules as <tt>ActiveRecord::Base.exists?</tt>.
+ # Uses the same rules as ActiveRecord::FinderMethods#exists?.
# [collection.build(attributes = {})]
# Returns a new object of the collection type that has been instantiated
# with +attributes+ and linked to this object through the join table, but has not yet been saved.
@@ -1541,7 +1712,34 @@ module ActiveRecord
# * <tt>Developer#projects.exists?(...)</tt>
# * <tt>Developer#projects.build</tt> (similar to <tt>Project.new("developer_id" => id)</tt>)
# * <tt>Developer#projects.create</tt> (similar to <tt>c = Project.new("developer_id" => id); c.save; c</tt>)
- # The declaration may include an options hash to specialize the behavior of the association.
+ # The declaration may include an +options+ hash to specialize the behavior of the association.
+ #
+ # === Scopes
+ #
+ # You can pass a second argument +scope+ as a callable (i.e. proc or
+ # lambda) to retrieve a specific set of records or customize the generated
+ # query when you access the associated collection.
+ #
+ # Scope examples:
+ # has_and_belongs_to_many :projects, -> { includes(:milestones, :manager) }
+ # has_and_belongs_to_many :categories, ->(category) {
+ # where("default_category = ?", category.name)
+ # }
+ #
+ # === Extensions
+ #
+ # The +extension+ argument allows you to pass a block into a
+ # has_and_belongs_to_many association. This is useful for adding new
+ # finders, creators and other factory-type methods to be used as part of
+ # the association.
+ #
+ # Extension examples:
+ # has_and_belongs_to_many :contractors do
+ # def find_or_create_by_name(name)
+ # first_name, last_name = name.split(" ", 2)
+ # find_or_create_by(first_name: first_name, last_name: last_name)
+ # end
+ # end
#
# === Options
#
@@ -1552,19 +1750,17 @@ module ActiveRecord
# [:join_table]
# Specify the name of the join table if the default based on lexical order isn't what you want.
# <b>WARNING:</b> If you're overwriting the table name of either class, the +table_name+ method
- # MUST be declared underneath any +has_and_belongs_to_many+ declaration in order to work.
+ # MUST be declared underneath any #has_and_belongs_to_many declaration in order to work.
# [:foreign_key]
# 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_and_belongs_to_many+ association to Project will use "person_id" as the
+ # a #has_and_belongs_to_many association to Project will use "person_id" as the
# default <tt>:foreign_key</tt>.
# [: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.
- # So if a Person class makes a +has_and_belongs_to_many+ association to Project,
+ # So if a Person class makes a #has_and_belongs_to_many association to Project,
# the association will use "project_id" as the default <tt>:association_foreign_key</tt>.
- # [:readonly]
- # If true, all the associated objects are readonly through the association.
# [:validate]
# If +false+, don't validate the associated objects when saving the parent object. +true+ by default.
# [:autosave]
@@ -1573,11 +1769,12 @@ module ActiveRecord
# If false, never save or destroy the associated objects.
# By default, only save associated objects that are new records.
#
- # Note that <tt>accepts_nested_attributes_for</tt> sets <tt>:autosave</tt> to <tt>true</tt>.
+ # Note that NestedAttributes::ClassMethods#accepts_nested_attributes_for sets
+ # <tt>:autosave</tt> to <tt>true</tt>.
#
# Option examples:
# has_and_belongs_to_many :projects
- # has_and_belongs_to_many :projects, -> { includes :milestones, :manager }
+ # has_and_belongs_to_many :projects, -> { includes(:milestones, :manager) }
# has_and_belongs_to_many :nations, class_name: "Country"
# has_and_belongs_to_many :categories, join_table: "prods_cats"
# has_and_belongs_to_many :categories, -> { readonly }
@@ -1593,16 +1790,14 @@ module ActiveRecord
join_model = builder.through_model
- # FIXME: we should move this to the internal constants. Also people
- # should never directly access this constant so I'm not happy about
- # setting it.
const_set join_model.name, join_model
+ private_constant join_model.name
middle_reflection = builder.middle_reflection join_model
Builder::HasMany.define_callbacks self, middle_reflection
Reflection.add_reflection self, middle_reflection.name, middle_reflection
- middle_reflection.parent_reflection = [name.to_s, habtm_reflection]
+ middle_reflection.parent_reflection = habtm_reflection
include Module.new {
class_eval <<-RUBY, __FILE__, __LINE__ + 1
@@ -1618,12 +1813,12 @@ module ActiveRecord
hm_options[:through] = middle_reflection.name
hm_options[:source] = join_model.right_reflection.name
- [:before_add, :after_add, :before_remove, :after_remove, :autosave, :validate, :join_table].each do |k|
+ [:before_add, :after_add, :before_remove, :after_remove, :autosave, :validate, :join_table, :class_name, :extend].each do |k|
hm_options[k] = options[k] if options.key? k
end
has_many name, scope, hm_options, &extension
- self._reflections[name.to_s].parent_reflection = [name.to_s, habtm_reflection]
+ self._reflections[name.to_s].parent_reflection = habtm_reflection
end
end
end
diff --git a/activerecord/lib/active_record/associations/alias_tracker.rb b/activerecord/lib/active_record/associations/alias_tracker.rb
index a6a1947148..021bc32237 100644
--- a/activerecord/lib/active_record/associations/alias_tracker.rb
+++ b/activerecord/lib/active_record/associations/alias_tracker.rb
@@ -2,23 +2,25 @@ require 'active_support/core_ext/string/conversions'
module ActiveRecord
module Associations
- # Keeps track of table aliases for ActiveRecord::Associations::ClassMethods::JoinDependency and
- # ActiveRecord::Associations::ThroughAssociationScope
+ # Keeps track of table aliases for ActiveRecord::Associations::JoinDependency
class AliasTracker # :nodoc:
- attr_reader :aliases, :connection
+ attr_reader :aliases
- def self.empty(connection)
- new connection, Hash.new(0)
+ def self.create(connection, initial_table, type_caster)
+ aliases = Hash.new(0)
+ aliases[initial_table] = 1
+ new connection, aliases, type_caster
end
- def self.create(connection, table_joins)
- if table_joins.empty?
- empty connection
+ def self.create_with_joins(connection, initial_table, joins, type_caster)
+ if joins.empty?
+ create(connection, initial_table, type_caster)
else
- aliases = Hash.new { |h,k|
- h[k] = initial_count_for(connection, k, table_joins)
+ aliases = Hash.new { |h, k|
+ h[k] = initial_count_for(connection, k, joins)
}
- new connection, aliases
+ aliases[initial_table] = 1
+ new connection, aliases, type_caster
end
end
@@ -51,45 +53,37 @@ module ActiveRecord
end
# table_joins is an array of arel joins which might conflict with the aliases we assign here
- def initialize(connection, aliases)
+ def initialize(connection, aliases, type_caster)
@aliases = aliases
@connection = connection
+ @type_caster = type_caster
end
def aliased_table_for(table_name, aliased_name)
- table_alias = aliased_name_for(table_name, aliased_name)
-
- if table_alias == table_name
- Arel::Table.new(table_name)
- else
- Arel::Table.new(table_name).alias(table_alias)
- end
- end
-
- def aliased_name_for(table_name, aliased_name)
if aliases[table_name].zero?
# If it's zero, we can have our table_name
aliases[table_name] = 1
- table_name
+ Arel::Table.new(table_name, type_caster: @type_caster)
else
# Otherwise, we need to use an alias
- aliased_name = connection.table_alias_for(aliased_name)
+ aliased_name = @connection.table_alias_for(aliased_name)
# Update the count
aliases[aliased_name] += 1
- if aliases[aliased_name] > 1
+ table_alias = if aliases[aliased_name] > 1
"#{truncate(aliased_name)}_#{aliases[aliased_name]}"
else
aliased_name
end
+ Arel::Table.new(table_name, type_caster: @type_caster).alias(table_alias)
end
end
private
def truncate(name)
- name.slice(0, connection.table_alias_length - 2)
+ name.slice(0, @connection.table_alias_length - 2)
end
end
end
diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb
index f1c36cd047..c7b396f3d4 100644
--- a/activerecord/lib/active_record/associations/association.rb
+++ b/activerecord/lib/active_record/associations/association.rb
@@ -8,12 +8,12 @@ module ActiveRecord
#
# Association
# SingularAssociation
- # HasOneAssociation
+ # HasOneAssociation + ForeignAssociation
# HasOneThroughAssociation + ThroughAssociation
# BelongsToAssociation
# BelongsToPolymorphicAssociation
# CollectionAssociation
- # HasManyAssociation
+ # HasManyAssociation + ForeignAssociation
# HasManyThroughAssociation + ThroughAssociation
class Association #:nodoc:
attr_reader :owner, :target, :reflection
@@ -121,7 +121,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, self).merge!(klass.all)
+ AssociationRelation.create(klass, klass.arel_table, klass.predicate_builder, self).merge!(klass.all)
end
# Loads the \target if needed and returns it.
@@ -211,9 +211,12 @@ module ActiveRecord
# the kind of the class of the associated objects. Meant to be used as
# a sanity check when you are about to assign an associated record.
def raise_on_type_mismatch!(record)
- unless record.is_a?(reflection.klass) || record.is_a?(reflection.class_name.constantize)
- message = "#{reflection.class_name}(##{reflection.klass.object_id}) expected, got #{record.class}(##{record.class.object_id})"
- raise ActiveRecord::AssociationTypeMismatch, message
+ unless record.is_a?(reflection.klass)
+ fresh_class = reflection.class_name.safe_constantize
+ unless fresh_class && record.is_a?(fresh_class)
+ message = "#{reflection.class_name}(##{reflection.klass.object_id}) expected, got #{record.class}(##{record.class.object_id})"
+ raise ActiveRecord::AssociationTypeMismatch, message
+ end
end
end
@@ -248,6 +251,14 @@ module ActiveRecord
initialize_attributes(record)
end
end
+
+ # Returns true if statement cache should be skipped on the association reader.
+ def skip_statement_cache?
+ reflection.scope_chain.any?(&:any?) ||
+ scope.eager_loading? ||
+ klass.scope_attributes? ||
+ reflection.source_reflection.active_record.default_scopes.any?
+ end
end
end
end
diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb
index 519d4d8651..48437a1c9e 100644
--- a/activerecord/lib/active_record/associations/association_scope.rb
+++ b/activerecord/lib/active_record/associations/association_scope.rb
@@ -2,42 +2,30 @@ module ActiveRecord
module Associations
class AssociationScope #:nodoc:
def self.scope(association, connection)
- INSTANCE.scope association, connection
- end
-
- class BindSubstitution
- def initialize(block)
- @block = block
- end
-
- def bind_value(scope, column, value, alias_tracker)
- substitute = alias_tracker.connection.substitute_at(
- column, scope.bind_values.length)
- scope.bind_values += [[column, @block.call(value)]]
- substitute
- end
+ INSTANCE.scope(association, connection)
end
def self.create(&block)
- block = block ? block : lambda { |val| val }
- new BindSubstitution.new(block)
+ block ||= lambda { |val| val }
+ new(block)
end
- def initialize(bind_substitution)
- @bind_substitution = bind_substitution
+ def initialize(value_transformation)
+ @value_transformation = value_transformation
end
INSTANCE = create
def scope(association, connection)
- klass = association.klass
- reflection = association.reflection
- scope = klass.unscoped
- owner = association.owner
- alias_tracker = AliasTracker.empty connection
+ klass = association.klass
+ reflection = association.reflection
+ scope = klass.unscoped
+ owner = association.owner
+ alias_tracker = AliasTracker.create connection, association.klass.table_name, klass.type_caster
+ chain_head, chain_tail = get_chain(reflection, association, alias_tracker)
scope.extending! Array(reflection.options[:extend])
- add_constraints(scope, owner, klass, reflection, alias_tracker)
+ add_constraints(scope, owner, klass, reflection, chain_head, chain_tail)
end
def join_type
@@ -45,137 +33,133 @@ module ActiveRecord
end
def self.get_bind_values(owner, chain)
- bvs = []
- chain.each_with_index do |reflection, i|
- if reflection == chain.last
- bvs << reflection.join_id_for(owner)
- if reflection.type
- bvs << owner.class.base_class.name
- end
- else
- if reflection.type
- bvs << chain[i + 1].klass.base_class.name
- end
- end
- end
- bvs
- end
+ binds = []
+ last_reflection = chain.last
- private
+ binds << last_reflection.join_id_for(owner)
+ if last_reflection.type
+ binds << owner.class.base_class.name
+ end
- def construct_tables(chain, klass, refl, alias_tracker)
- chain.map do |reflection|
- alias_tracker.aliased_table_for(
- table_name_for(reflection, klass, refl),
- table_alias_for(reflection, refl, reflection != refl)
- )
+ chain.each_cons(2).each do |reflection, next_reflection|
+ if reflection.type
+ binds << next_reflection.klass.base_class.name
+ end
end
+ binds
end
- def table_alias_for(reflection, refl, join = false)
- name = "#{reflection.plural_name}_#{alias_suffix(refl)}"
- name << "_join" if join
- name
- end
+ protected
+
+ attr_reader :value_transformation
+ private
def join(table, constraint)
table.create_join(table, table.create_on(constraint), join_type)
end
- def column_for(table_name, column_name, alias_tracker)
- columns = alias_tracker.connection.schema_cache.columns_hash(table_name)
- columns[column_name]
- end
+ def last_chain_scope(scope, table, reflection, owner, association_klass)
+ join_keys = reflection.join_keys(association_klass)
+ key = join_keys.key
+ foreign_key = join_keys.foreign_key
+
+ value = transform_value(owner[foreign_key])
+ scope = scope.where(table.name => { key => value })
+
+ if reflection.type
+ polymorphic_type = transform_value(owner.class.base_class.name)
+ scope = scope.where(table.name => { reflection.type => polymorphic_type })
+ end
- def bind_value(scope, column, value, alias_tracker)
- @bind_substitution.bind_value scope, column, value, alias_tracker
+ scope
end
- def bind(scope, table_name, column_name, value, tracker)
- column = column_for table_name, column_name, tracker
- bind_value scope, column, value, tracker
+ def transform_value(value)
+ value_transformation.call(value)
end
- def add_constraints(scope, owner, assoc_klass, refl, tracker)
- chain = refl.chain
- scope_chain = refl.scope_chain
+ def next_chain_scope(scope, table, reflection, association_klass, foreign_table, next_reflection)
+ join_keys = reflection.join_keys(association_klass)
+ key = join_keys.key
+ foreign_key = join_keys.foreign_key
- tables = construct_tables(chain, assoc_klass, refl, tracker)
+ constraint = table[key].eq(foreign_table[foreign_key])
- chain.each_with_index do |reflection, i|
- table, foreign_table = tables.shift, tables.first
+ if reflection.type
+ value = transform_value(next_reflection.klass.base_class.name)
+ scope = scope.where(table.name => { reflection.type => value })
+ end
- join_keys = reflection.join_keys(assoc_klass)
- key = join_keys.key
- foreign_key = join_keys.foreign_key
+ scope = scope.joins(join(foreign_table, constraint))
+ end
- if reflection == chain.last
- bind_val = bind scope, table.table_name, key.to_s, owner[foreign_key], tracker
- scope = scope.where(table[key].eq(bind_val))
+ class ReflectionProxy < SimpleDelegator # :nodoc:
+ attr_accessor :next
+ attr_reader :alias_name
- if reflection.type
- value = owner.class.base_class.name
- bind_val = bind scope, table.table_name, reflection.type.to_s, value, tracker
- scope = scope.where(table[reflection.type].eq(bind_val))
- end
- else
- constraint = table[key].eq(foreign_table[foreign_key])
+ def initialize(reflection, alias_name)
+ super(reflection)
+ @alias_name = alias_name
+ end
- if reflection.type
- value = chain[i + 1].klass.base_class.name
- bind_val = bind scope, table.table_name, reflection.type.to_s, value, tracker
- scope = scope.where(table[reflection.type].eq(bind_val))
- end
+ def all_includes; nil; end
+ end
- scope = scope.joins(join(foreign_table, constraint))
- end
+ def get_chain(reflection, association, tracker)
+ name = reflection.name
+ runtime_reflection = Reflection::RuntimeReflection.new(reflection, association)
+ previous_reflection = runtime_reflection
+ reflection.chain.drop(1).each do |refl|
+ alias_name = tracker.aliased_table_for(refl.table_name, refl.alias_candidate(name))
+ proxy = ReflectionProxy.new(refl, alias_name)
+ previous_reflection.next = proxy
+ previous_reflection = proxy
+ end
+ [runtime_reflection, previous_reflection]
+ end
- is_first_chain = i == 0
- klass = is_first_chain ? assoc_klass : reflection.klass
+ def add_constraints(scope, owner, association_klass, refl, chain_head, chain_tail)
+ owner_reflection = chain_tail
+ table = owner_reflection.alias_name
+ scope = last_chain_scope(scope, table, owner_reflection, owner, association_klass)
+
+ reflection = chain_head
+ loop do
+ break unless reflection
+ table = reflection.alias_name
+
+ unless reflection == chain_tail
+ next_reflection = reflection.next
+ foreign_table = next_reflection.alias_name
+ scope = next_chain_scope(scope, table, reflection, association_klass, foreign_table, next_reflection)
+ end
# Exclude the scope of the association itself, because that
# was already merged in the #scope method.
- scope_chain[i].each do |scope_chain_item|
- item = eval_scope(klass, scope_chain_item, owner)
+ reflection.constraints.each do |scope_chain_item|
+ item = eval_scope(reflection.klass, scope_chain_item, owner)
if scope_chain_item == refl.scope
- scope.merge! item.except(:where, :includes, :bind)
+ scope.merge! item.except(:where, :includes)
end
- if is_first_chain
+ reflection.all_includes do
scope.includes! item.includes_values
end
- scope.where_values += item.where_values
- scope.bind_values += item.bind_values
+ scope.unscope!(*item.unscope_values)
+ scope.where_clause += item.where_clause
scope.order_values |= item.order_values
end
+
+ reflection = reflection.next
end
scope
end
- def alias_suffix(refl)
- refl.name
- end
-
- def table_name_for(reflection, klass, refl)
- if reflection == refl
- # If this is a polymorphic belongs_to, we want to get the klass from the
- # association because it depends on the polymorphic_type attribute of
- # the owner
- klass.table_name
- else
- reflection.table_name
- end
- end
-
def eval_scope(klass, scope, owner)
- if scope.is_a?(Relation)
- scope
- else
- klass.unscoped.instance_exec(owner, &scope)
- end
+ klass.unscoped.instance_exec(owner, &scope)
end
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 81fdd681de..41698c5360 100644
--- a/activerecord/lib/active_record/associations/belongs_to_association.rb
+++ b/activerecord/lib/active_record/associations/belongs_to_association.rb
@@ -10,7 +10,7 @@ module ActiveRecord
def replace(record)
if record
raise_on_type_mismatch!(record)
- update_counters(record)
+ update_counters_on_replace(record)
replace_keys(record)
set_inverse_instance(record)
@updated = true
@@ -32,52 +32,47 @@ module ActiveRecord
end
def decrement_counters # :nodoc:
- with_cache_name { |name| decrement_counter name }
+ update_counters(-1)
end
def increment_counters # :nodoc:
- with_cache_name { |name| increment_counter name }
+ update_counters(1)
end
private
- def find_target?
- !loaded? && foreign_key_present? && klass
- end
-
- def with_cache_name
- counter_cache_name = reflection.counter_cache_column
- return unless counter_cache_name && owner.persisted?
- yield counter_cache_name
+ def update_counters(by)
+ if require_counter_update? && foreign_key_present?
+ if target && !stale_target?
+ target.increment!(reflection.counter_cache_column, by)
+ else
+ klass.update_counters(target_id, reflection.counter_cache_column => by)
+ end
+ end
end
- def update_counters(record)
- with_cache_name do |name|
- return unless different_target? record
- record.class.increment_counter(name, record.id)
- decrement_counter name
- end
+ def find_target?
+ !loaded? && foreign_key_present? && klass
end
- def decrement_counter(counter_cache_name)
- if foreign_key_present?
- klass.decrement_counter(counter_cache_name, target_id)
- end
+ def require_counter_update?
+ reflection.counter_cache_column && owner.persisted?
end
- def increment_counter(counter_cache_name)
- if foreign_key_present?
- klass.increment_counter(counter_cache_name, target_id)
+ def update_counters_on_replace(record)
+ if require_counter_update? && different_target?(record)
+ record.increment!(reflection.counter_cache_column)
+ decrement_counters
end
end
# Checks whether record is different to the current target, without loading it
def different_target?(record)
- record.id != owner[reflection.foreign_key]
+ record.id != owner._read_attribute(reflection.foreign_key)
end
def replace_keys(record)
- owner[reflection.foreign_key] = record[reflection.association_primary_key(record.class)]
+ owner[reflection.foreign_key] = record._read_attribute(reflection.association_primary_key(record.class))
end
def remove_keys
@@ -85,7 +80,7 @@ module ActiveRecord
end
def foreign_key_present?
- owner[reflection.foreign_key]
+ owner._read_attribute(reflection.foreign_key)
end
# NOTE - for now, we're only supporting inverse setting from belongs_to back onto
@@ -99,12 +94,13 @@ module ActiveRecord
if options[:primary_key]
owner.send(reflection.name).try(:id)
else
- owner[reflection.foreign_key]
+ owner._read_attribute(reflection.foreign_key)
end
end
def stale_state
- owner[reflection.foreign_key] && owner[reflection.foreign_key].to_s
+ result = owner._read_attribute(reflection.foreign_key) { |n| owner.send(:missing_attribute, n, caller) }
+ result && result.to_s
end
end
end
diff --git a/activerecord/lib/active_record/associations/builder/association.rb b/activerecord/lib/active_record/associations/builder/association.rb
index 947d61ee7b..d0534056d9 100644
--- a/activerecord/lib/active_record/associations/builder/association.rb
+++ b/activerecord/lib/active_record/associations/builder/association.rb
@@ -1,5 +1,3 @@
-require 'active_support/core_ext/module/attribute_accessors'
-
# This is the parent Association class which defines the variables
# used by all associations.
#
@@ -11,19 +9,14 @@ require 'active_support/core_ext/module/attribute_accessors'
# - CollectionAssociation
# - HasManyAssociation
-module ActiveRecord::Associations::Builder
+module ActiveRecord::Associations::Builder # :nodoc:
class Association #:nodoc:
class << self
attr_accessor :extensions
- # TODO: This class accessor is needed to make activerecord-deprecated_finders work.
- # We can move it to a constant in 5.0.
- attr_accessor :valid_options
end
self.extensions = []
- self.valid_options = [:class_name, :class, :foreign_key, :validate]
-
- attr_reader :name, :scope, :options
+ VALID_OPTIONS = [:class_name, :anonymous_class, :foreign_key, :validate] # :nodoc:
def self.build(model, name, scope, options, &block)
if model.dangerous_attribute_method?(name)
@@ -32,57 +25,60 @@ module ActiveRecord::Associations::Builder
"Please choose a different association name."
end
- builder = create_builder model, name, scope, options, &block
- reflection = builder.build(model)
+ extension = define_extensions model, name, &block
+ reflection = create_reflection model, name, scope, options, extension
define_accessors model, reflection
define_callbacks model, reflection
define_validations model, reflection
- builder.define_extensions model
reflection
end
- def self.create_builder(model, name, scope, options, &block)
+ def self.create_reflection(model, name, scope, options, extension = nil)
raise ArgumentError, "association names must be a Symbol" unless name.kind_of?(Symbol)
- new(model, name, scope, options, &block)
- end
-
- def initialize(model, name, scope, options)
- # TODO: Move this to create_builder as soon we drop support to activerecord-deprecated_finders.
if scope.is_a?(Hash)
options = scope
scope = nil
end
- # TODO: Remove this model argument as soon we drop support to activerecord-deprecated_finders.
- @name = name
- @scope = scope
- @options = options
+ validate_options(options)
- validate_options
+ scope = build_scope(scope, extension)
+
+ ActiveRecord::Reflection.create(macro, name, scope, options, model)
+ end
+
+ def self.build_scope(scope, extension)
+ new_scope = scope
if scope && scope.arity == 0
- @scope = proc { instance_exec(&scope) }
+ new_scope = proc { instance_exec(&scope) }
+ end
+
+ if extension
+ new_scope = wrap_scope new_scope, extension
end
+
+ new_scope
end
- def build(model)
- ActiveRecord::Reflection.create(macro, name, scope, options, model)
+ def self.wrap_scope(scope, extension)
+ scope
end
- def macro
+ def self.macro
raise NotImplementedError
end
- def valid_options
- Association.valid_options + Association.extensions.flat_map(&:valid_options)
+ def self.valid_options(options)
+ VALID_OPTIONS + Association.extensions.flat_map(&:valid_options)
end
- def validate_options
- options.assert_valid_keys(valid_options)
+ def self.validate_options(options)
+ options.assert_valid_keys(valid_options(options))
end
- def define_extensions(model)
+ def self.define_extensions(model, name)
end
def self.define_callbacks(model, reflection)
@@ -133,8 +129,6 @@ module ActiveRecord::Associations::Builder
raise NotImplementedError
end
- private
-
def self.check_dependent_options(dependent)
unless valid_dependent_options.include? dependent
raise ArgumentError, "The :dependent option must be one of #{valid_dependent_options}, but is :#{dependent}"
diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb
index 954ea3878a..dae468ba54 100644
--- a/activerecord/lib/active_record/associations/builder/belongs_to.rb
+++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb
@@ -1,11 +1,11 @@
-module ActiveRecord::Associations::Builder
+module ActiveRecord::Associations::Builder # :nodoc:
class BelongsTo < SingularAssociation #:nodoc:
- def macro
+ def self.macro
:belongs_to
end
- def valid_options
- super + [:foreign_type, :polymorphic, :touch, :counter_cache]
+ def self.valid_options(options)
+ super + [:foreign_type, :polymorphic, :touch, :counter_cache, :optional]
end
def self.valid_dependent_options
@@ -23,8 +23,6 @@ module ActiveRecord::Associations::Builder
add_counter_cache_methods mixin
end
- private
-
def self.add_counter_cache_methods(mixin)
return if mixin.method_defined? :belongs_to_counter_cache_after_update
@@ -35,16 +33,24 @@ module ActiveRecord::Associations::Builder
if (@_after_create_counter_called ||= false)
@_after_create_counter_called = false
- elsif attribute_changed?(foreign_key) && !new_record? && reflection.constructable?
- model = reflection.klass
+ elsif attribute_changed?(foreign_key) && !new_record?
+ if reflection.polymorphic?
+ model = attribute(reflection.foreign_type).try(:constantize)
+ model_was = attribute_was(reflection.foreign_type).try(:constantize)
+ else
+ model = reflection.klass
+ model_was = reflection.klass
+ end
+
foreign_key_was = attribute_was foreign_key
foreign_key = attribute foreign_key
if foreign_key && model.respond_to?(:increment_counter)
model.increment_counter(cache_column, foreign_key)
end
- if foreign_key_was && model.respond_to?(:decrement_counter)
- model.decrement_counter(cache_column, foreign_key_was)
+
+ if foreign_key_was && model_was.respond_to?(:decrement_counter)
+ model_was.decrement_counter(cache_column, foreign_key_was)
end
end
end
@@ -62,7 +68,7 @@ module ActiveRecord::Associations::Builder
klass.attr_readonly cache_column if klass && klass.respond_to?(:attr_readonly)
end
- def self.touch_record(o, foreign_key, name, touch) # :nodoc:
+ def self.touch_record(o, foreign_key, name, touch, touch_method) # :nodoc:
old_foreign_id = o.changed_attributes[foreign_key]
if old_foreign_id
@@ -77,9 +83,9 @@ module ActiveRecord::Associations::Builder
if old_record
if touch != true
- old_record.touch touch
+ old_record.send(touch_method, touch)
else
- old_record.touch
+ old_record.send(touch_method)
end
end
end
@@ -87,9 +93,9 @@ module ActiveRecord::Associations::Builder
record = o.send name
if record && record.persisted?
if touch != true
- record.touch touch
+ record.send(touch_method, touch)
else
- record.touch
+ record.send(touch_method)
end
end
end
@@ -100,7 +106,8 @@ module ActiveRecord::Associations::Builder
touch = reflection.options[:touch]
callback = lambda { |record|
- BelongsTo.touch_record(record, foreign_key, n, touch)
+ touch_method = touching_delayed_records? ? :touch : :touch_later
+ BelongsTo.touch_record(record, foreign_key, n, touch, touch_method)
}
model.after_save callback, if: :changed?
@@ -112,5 +119,23 @@ module ActiveRecord::Associations::Builder
name = reflection.name
model.after_destroy lambda { |o| o.association(name).handle_dependency }
end
+
+ def self.define_validations(model, reflection)
+ if reflection.options.key?(:required)
+ reflection.options[:optional] = !reflection.options.delete(:required)
+ end
+
+ if reflection.options[:optional].nil?
+ required = model.belongs_to_required_by_default
+ else
+ required = !reflection.options[:optional]
+ end
+
+ super
+
+ if required
+ model.validates_presence_of reflection.name, message: :required
+ end
+ end
end
end
diff --git a/activerecord/lib/active_record/associations/builder/collection_association.rb b/activerecord/lib/active_record/associations/builder/collection_association.rb
index bc15a49996..56a8dc4e18 100644
--- a/activerecord/lib/active_record/associations/builder/collection_association.rb
+++ b/activerecord/lib/active_record/associations/builder/collection_association.rb
@@ -2,27 +2,16 @@
require 'active_record/associations'
-module ActiveRecord::Associations::Builder
+module ActiveRecord::Associations::Builder # :nodoc:
class CollectionAssociation < Association #:nodoc:
CALLBACKS = [:before_add, :after_add, :before_remove, :after_remove]
- def valid_options
+ def self.valid_options(options)
super + [:table_name, :before_add,
:after_add, :before_remove, :after_remove, :extend]
end
- attr_reader :block_extension
-
- def initialize(model, name, scope, options)
- super
- @mod = nil
- if block_given?
- @mod = Module.new(&Proc.new)
- @scope = wrap_scope @scope, @mod
- end
- end
-
def self.define_callbacks(model, reflection)
super
name = reflection.name
@@ -32,10 +21,11 @@ module ActiveRecord::Associations::Builder
}
end
- def define_extensions(model)
- if @mod
+ def self.define_extensions(model, name)
+ if block_given?
extension_module_name = "#{model.name.demodulize}#{name.to_s.camelize}AssociationExtension"
- model.parent.const_set(extension_module_name, @mod)
+ extension = Module.new(&Proc.new)
+ model.parent.const_set(extension_module_name, extension)
end
end
@@ -78,9 +68,7 @@ module ActiveRecord::Associations::Builder
CODE
end
- private
-
- def wrap_scope(scope, mod)
+ def self.wrap_scope(scope, mod)
if scope
proc { |owner| instance_exec(owner, &scope).extending(mod) }
else
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 34a555dfd4..a5c9f1666e 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
@@ -1,9 +1,9 @@
-module ActiveRecord::Associations::Builder
+module ActiveRecord::Associations::Builder # :nodoc:
class HasAndBelongsToMany # :nodoc:
- class JoinTableResolver
+ class JoinTableResolver # :nodoc:
KnownTable = Struct.new :join_table
- class KnownClass
+ class KnownClass # :nodoc:
def initialize(lhs_class, rhs_class_name)
@lhs_class = lhs_class
@rhs_class_name = rhs_class_name
@@ -11,7 +11,7 @@ module ActiveRecord::Associations::Builder
end
def join_table
- @join_table ||= [@lhs_class.table_name, klass.table_name].sort.join("\0").gsub(/^(.*[._])(.+)\0\1(.+)/, '\1\2_\3').gsub("\0", "_")
+ @join_table ||= [@lhs_class.table_name, klass.table_name].sort.join("\0").gsub(/^(.*[._])(.+)\0\1(.+)/, '\1\2_\3').tr("\0", "_")
end
private
@@ -46,7 +46,7 @@ module ActiveRecord::Associations::Builder
join_model = Class.new(ActiveRecord::Base) {
class << self;
- attr_accessor :class_resolver
+ attr_accessor :left_model
attr_accessor :name
attr_accessor :table_name_resolver
attr_accessor :left_reflection
@@ -58,7 +58,7 @@ module ActiveRecord::Associations::Builder
end
def self.compute_type(class_name)
- class_resolver.compute_type class_name
+ left_model.compute_type class_name
end
def self.add_left_association(name, options)
@@ -72,33 +72,37 @@ module ActiveRecord::Associations::Builder
self.right_reflection = _reflect_on_association(rhs_name)
end
+ def self.retrieve_connection
+ left_model.retrieve_connection
+ end
+
}
join_model.name = "HABTM_#{association_name.to_s.camelize}"
join_model.table_name_resolver = habtm
- join_model.class_resolver = lhs_model
+ join_model.left_model = lhs_model
- join_model.add_left_association :left_side, class: lhs_model
+ join_model.add_left_association :left_side, anonymous_class: lhs_model
join_model.add_right_association association_name, belongs_to_options(options)
join_model
end
def middle_reflection(join_model)
middle_name = [lhs_model.name.downcase.pluralize,
- association_name].join('_').gsub(/::/, '_').to_sym
+ association_name].join('_'.freeze).gsub('::'.freeze, '_'.freeze).to_sym
middle_options = middle_options join_model
- hm_builder = HasMany.create_builder(lhs_model,
- middle_name,
- nil,
- middle_options)
- hm_builder.build lhs_model
+
+ HasMany.create_reflection(lhs_model,
+ middle_name,
+ nil,
+ middle_options)
end
private
def middle_options(join_model)
middle_options = {}
- middle_options[:class] = join_model
+ middle_options[:class_name] = "#{lhs_model.name}::#{join_model.name}"
middle_options[:source] = join_model.left_reflection.name
if options.key? :foreign_key
middle_options[:foreign_key] = options[:foreign_key]
@@ -110,7 +114,7 @@ module ActiveRecord::Associations::Builder
rhs_options = {}
if options.key? :class_name
- rhs_options[:foreign_key] = options[:class_name].foreign_key
+ rhs_options[:foreign_key] = options[:class_name].to_s.foreign_key
rhs_options[:class_name] = options[:class_name]
end
diff --git a/activerecord/lib/active_record/associations/builder/has_many.rb b/activerecord/lib/active_record/associations/builder/has_many.rb
index 4c8c826f76..7864d4c536 100644
--- a/activerecord/lib/active_record/associations/builder/has_many.rb
+++ b/activerecord/lib/active_record/associations/builder/has_many.rb
@@ -1,11 +1,11 @@
-module ActiveRecord::Associations::Builder
+module ActiveRecord::Associations::Builder # :nodoc:
class HasMany < CollectionAssociation #:nodoc:
- def macro
+ def self.macro
:has_many
end
- def valid_options
- super + [:primary_key, :dependent, :as, :through, :source, :source_type, :inverse_of, :counter_cache, :join_table]
+ def self.valid_options(options)
+ super + [:primary_key, :dependent, :as, :through, :source, :source_type, :inverse_of, :counter_cache, :join_table, :foreign_type, :index_errors]
end
def self.valid_dependent_options
diff --git a/activerecord/lib/active_record/associations/builder/has_one.rb b/activerecord/lib/active_record/associations/builder/has_one.rb
index c194c8ae9a..9d64ae877b 100644
--- a/activerecord/lib/active_record/associations/builder/has_one.rb
+++ b/activerecord/lib/active_record/associations/builder/has_one.rb
@@ -1,11 +1,11 @@
-module ActiveRecord::Associations::Builder
+module ActiveRecord::Associations::Builder # :nodoc:
class HasOne < SingularAssociation #:nodoc:
- def macro
+ def self.macro
:has_one
end
- def valid_options
- valid = super + [:as]
+ def self.valid_options(options)
+ valid = super + [:as, :foreign_type]
valid += [:through, :source, :source_type] if options[:through]
valid
end
@@ -14,10 +14,15 @@ module ActiveRecord::Associations::Builder
[:destroy, :delete, :nullify, :restrict_with_error, :restrict_with_exception]
end
- private
-
def self.add_destroy_callbacks(model, reflection)
super unless reflection.options[:through]
end
+
+ def self.define_validations(model, reflection)
+ super
+ if reflection.options[:required]
+ model.validates_presence_of reflection.name, message: :required
+ end
+ end
end
end
diff --git a/activerecord/lib/active_record/associations/builder/singular_association.rb b/activerecord/lib/active_record/associations/builder/singular_association.rb
index 6e6dd7204c..58a9c8ff24 100644
--- a/activerecord/lib/active_record/associations/builder/singular_association.rb
+++ b/activerecord/lib/active_record/associations/builder/singular_association.rb
@@ -1,8 +1,8 @@
# This class is inherited by the has_one and belongs_to association classes
-module ActiveRecord::Associations::Builder
+module ActiveRecord::Associations::Builder # :nodoc:
class SingularAssociation < Association #:nodoc:
- def valid_options
+ def self.valid_options(options)
super + [:dependent, :primary_key, :inverse_of, :required]
end
@@ -27,12 +27,5 @@ module ActiveRecord::Associations::Builder
end
CODE
end
-
- def self.define_validations(model, reflection)
- super
- if reflection.options[:required]
- model.validates_presence_of reflection.name
- end
- end
end
end
diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb
index 065a2cff01..f32dddb8f0 100644
--- a/activerecord/lib/active_record/associations/collection_association.rb
+++ b/activerecord/lib/active_record/associations/collection_association.rb
@@ -28,12 +28,24 @@ module ActiveRecord
# Implements the reader method, e.g. foo.items for Foo.has_many :items
def reader(force_reload = false)
if force_reload
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ Passing an argument to force an association to reload is now
+ deprecated and will be removed in Rails 5.1. Please call `reload`
+ on the result collection proxy instead.
+ MSG
+
klass.uncached { reload }
elsif stale_target?
reload
end
- @proxy ||= CollectionProxy.create(klass, self)
+ if null_scope?
+ # Cache the proxy separately before the owner has an id
+ # or else a post-save proxy will still lack the id
+ @null_proxy ||= CollectionProxy.create(klass, self)
+ else
+ @proxy ||= CollectionProxy.create(klass, self)
+ end
end
# Implements the writer method, e.g. foo.items= for Foo.has_many :items
@@ -48,17 +60,19 @@ module ActiveRecord
record.send(reflection.association_primary_key)
end
else
- column = "#{reflection.quoted_table_name}.#{reflection.association_primary_key}"
- scope.pluck(column)
+ @association_ids ||= (
+ column = "#{reflection.quoted_table_name}.#{reflection.association_primary_key}"
+ scope.pluck(column)
+ )
end
end
# Implements the ids writer method, e.g. foo.item_ids= for Foo.has_many :items
def ids_writer(ids)
pk_type = reflection.primary_key_type
- ids = Array(ids).reject { |id| id.blank? }
- ids.map! { |i| pk_type.type_cast_from_user(i) }
- replace(klass.find(ids).index_by { |r| r.id }.values_at(*ids))
+ ids = Array(ids).reject(&:blank?)
+ ids.map! { |i| pk_type.cast(i) }
+ replace(klass.find(ids).index_by(&:id).values_at(*ids))
end
def reset
@@ -123,6 +137,16 @@ module ActiveRecord
first_nth_or_last(:last, *args)
end
+ def take(n = nil)
+ if loaded?
+ n ? target.take(n) : target.first
+ else
+ scope.take(n).tap do |record|
+ set_inverse_instance record if record.is_a? ActiveRecord::Base
+ end
+ end
+ end
+
def build(attributes = {}, &block)
if attributes.is_a?(Array)
attributes.collect { |attr| build(attr, &block) }
@@ -145,6 +169,7 @@ module ActiveRecord
# be chained. Since << flattens its argument list and inserts each record,
# +push+ and +concat+ behave identically.
def concat(*records)
+ records = records.flatten
if owner.new_record?
load_target
concat_records(records)
@@ -169,8 +194,8 @@ module ActiveRecord
end
# Removes all records from the association without calling callbacks
- # on the associated records. It honors the `:dependent` option. However
- # if the `:dependent` value is `:destroy` then in that case the `:delete_all`
+ # on the associated records. It honors the +:dependent+ option. However
+ # if the +:dependent+ value is +:destroy+ then in that case the +:delete_all+
# deletion strategy for the association is applied.
#
# You can force a particular deletion strategy by passing a parameter.
@@ -212,11 +237,7 @@ module ActiveRecord
# Count all records using SQL. Construct options and pass them with
# scope to the target class's +count+.
- def count(column_name = nil, count_options = {})
- # TODO: Remove count_options argument as soon we remove support to
- # activerecord-deprecated_finders.
- column_name, count_options = nil, column_name if column_name.is_a?(Hash)
-
+ def count(column_name = nil)
relation = scope
if association_scope.distinct_value
# This is needed because 'SELECT count(DISTINCT *)..' is not valid SQL.
@@ -283,7 +304,7 @@ module ActiveRecord
elsif !loaded? && !association_scope.group_values.empty?
load_target.size
elsif !loaded? && !association_scope.distinct_value && target.is_a?(Array)
- unsaved_records = target.select { |r| r.new_record? }
+ unsaved_records = target.select(&:new_record?)
unsaved_records.size + count_records
else
count_records
@@ -316,7 +337,8 @@ module ActiveRecord
end
# Returns true if the collections is not empty.
- # Equivalent to +!collection.empty?+.
+ # If block given, loads all records and checks for one or more matches.
+ # Otherwise, equivalent to +!collection.empty?+.
def any?
if block_given?
load_target.any? { |*block_args| yield(*block_args) }
@@ -326,7 +348,8 @@ module ActiveRecord
end
# Returns true if the collection has more than 1 record.
- # Equivalent to +collection.size > 1+.
+ # If block given, loads all records and checks for two or more matches.
+ # Otherwise, equivalent to +collection.size > 1+.
def many?
if block_given?
load_target.many? { |*block_args| yield(*block_args) }
@@ -352,8 +375,11 @@ module ActiveRecord
if owner.new_record?
replace_records(other_array, original_target)
else
+ replace_common_records_in_memory(other_array, original_target)
if other_array != original_target
transaction { replace_records(other_array, original_target) }
+ else
+ other_array
end
end
end
@@ -379,11 +405,18 @@ module ActiveRecord
target
end
- def add_to_target(record, skip_callbacks = false)
+ def add_to_target(record, skip_callbacks = false, &block)
+ if association_scope.distinct_value
+ index = @target.index(record)
+ end
+ replace_on_target(record, index, skip_callbacks, &block)
+ end
+
+ def replace_on_target(record, index, skip_callbacks)
callback(:before_add, record) unless skip_callbacks
yield(record) if block_given?
- if association_scope.distinct_value && index = @target.index(record)
+ if index
@target[index] = record
else
@target << record
@@ -407,7 +440,7 @@ module ActiveRecord
private
def get_records
- return scope.to_a if reflection.scope_chain.any?(&:any?)
+ return scope.to_a if skip_statement_cache?
conn = klass.connection
sc = reflection.association_scope_cache(conn, owner) do
@@ -486,7 +519,7 @@ module ActiveRecord
def delete_or_destroy(records, method)
records = records.flatten
records.each { |record| raise_on_type_mismatch!(record) }
- existing_records = records.reject { |r| r.new_record? }
+ existing_records = records.reject(&:new_record?)
if existing_records.empty?
remove_records(existing_records, records, method)
@@ -522,10 +555,18 @@ module ActiveRecord
target
end
+ def replace_common_records_in_memory(new_target, original_target)
+ common_records = new_target & original_target
+ common_records.each do |record|
+ skip_callbacks = true
+ replace_on_target(record, @target.index(record), skip_callbacks)
+ end
+ end
+
def concat_records(records, should_raise = false)
result = true
- records.flatten.each do |record|
+ records.each do |record|
raise_on_type_mismatch!(record)
add_to_target(record) do |rec|
result &&= insert_record(rec, true, should_raise) unless owner.new_record?
@@ -569,8 +610,8 @@ module ActiveRecord
if reflection.is_a?(ActiveRecord::Reflection::ThroughReflection)
assoc = owner.association(reflection.through_reflection.name)
assoc.reader.any? { |source|
- target = source.send(reflection.source_reflection.name)
- target.respond_to?(:include?) ? target.include?(record) : target == record
+ target_reflection = source.send(reflection.source_reflection.name)
+ target_reflection.respond_to?(:include?) ? target_reflection.include?(record) : target_reflection == record
} || target.include?(record)
else
target.include?(record)
@@ -581,7 +622,7 @@ module ActiveRecord
# specified, then #find scans the entire collection.
def find_by_scan(*args)
expects_array = args.first.kind_of?(Array)
- ids = args.flatten.compact.map{ |arg| arg.to_s }.uniq
+ ids = args.flatten.compact.map(&:to_s).uniq
if ids.size == 1
id = ids.first
diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb
index 84c8cfe72b..fe693cfbb6 100644
--- a/activerecord/lib/active_record/associations/collection_proxy.rb
+++ b/activerecord/lib/active_record/associations/collection_proxy.rb
@@ -29,10 +29,11 @@ module ActiveRecord
# instantiation of the actual post records.
class CollectionProxy < Relation
delegate(*(ActiveRecord::Calculations.public_instance_methods - [:count]), to: :scope)
+ delegate :find_nth, to: :scope
def initialize(klass, association) #:nodoc:
@association = association
- super klass, klass.arel_table
+ super klass, klass.arel_table, klass.predicate_builder
merge! association.scope(nullify: false)
end
@@ -111,7 +112,7 @@ module ActiveRecord
end
# Finds an object in the collection responding to the +id+. Uses the same
- # rules as <tt>ActiveRecord::Base.find</tt>. Returns <tt>ActiveRecord::RecordNotFound</tt>
+ # rules as ActiveRecord::Base.find. Returns ActiveRecord::RecordNotFound
# error if the object cannot be found.
#
# class Person < ActiveRecord::Base
@@ -126,7 +127,7 @@ module ActiveRecord
# # ]
#
# person.pets.find(1) # => #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>
- # person.pets.find(4) # => ActiveRecord::RecordNotFound: Couldn't find Pet with id=4
+ # person.pets.find(4) # => ActiveRecord::RecordNotFound: Couldn't find Pet with 'id'=4
#
# person.pets.find(2) { |pet| pet.name.downcase! }
# # => #<Pet id: 2, name: "fancy-fancy", person_id: 1>
@@ -170,27 +171,27 @@ module ActiveRecord
@association.first(*args)
end
- # Same as +first+ except returns only the second record.
+ # Same as #first except returns only the second record.
def second(*args)
@association.second(*args)
end
- # Same as +first+ except returns only the third record.
+ # Same as #first except returns only the third record.
def third(*args)
@association.third(*args)
end
- # Same as +first+ except returns only the fourth record.
+ # Same as #first except returns only the fourth record.
def fourth(*args)
@association.fourth(*args)
end
- # Same as +first+ except returns only the fifth record.
+ # Same as #first except returns only the fifth record.
def fifth(*args)
@association.fifth(*args)
end
- # Same as +first+ except returns only the forty second record.
+ # Same as #first except returns only the forty second record.
# Also known as accessing "the reddit".
def forty_two(*args)
@association.forty_two(*args)
@@ -226,6 +227,35 @@ module ActiveRecord
@association.last(*args)
end
+ # Gives a record (or N records if a parameter is supplied) from the collection
+ # using the same rules as <tt>ActiveRecord::Base.take</tt>.
+ #
+ # class Person < ActiveRecord::Base
+ # has_many :pets
+ # end
+ #
+ # person.pets
+ # # => [
+ # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
+ # # #<Pet id: 2, name: "Spook", person_id: 1>,
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
+ # # ]
+ #
+ # person.pets.take # => #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>
+ #
+ # person.pets.take(2)
+ # # => [
+ # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
+ # # #<Pet id: 2, name: "Spook", person_id: 1>
+ # # ]
+ #
+ # another_person_without.pets # => []
+ # another_person_without.pets.take # => nil
+ # another_person_without.pets.take(2) # => []
+ def take(n = nil)
+ @association.take(n)
+ end
+
# Returns a new object of the collection type that has been instantiated
# with +attributes+ and linked to this object, but have not yet been saved.
# You can pass an array of attributes hashes, this will return an array
@@ -285,7 +315,7 @@ module ActiveRecord
@association.create(attributes, &block)
end
- # Like +create+, except that if the record is invalid, raises an exception.
+ # Like #create, except that if the record is invalid, raises an exception.
#
# class Person
# has_many :pets
@@ -302,8 +332,8 @@ module ActiveRecord
end
# Add one or more records to the collection by setting their foreign keys
- # to the association's primary key. Since << flattens its argument list and
- # inserts each record, +push+ and +concat+ behave identically. Returns +self+
+ # to the association's primary key. Since #<< flattens its argument list and
+ # inserts each record, +push+ and #concat behave identically. Returns +self+
# so method calls may be chained.
#
# class Person < ActiveRecord::Base
@@ -355,14 +385,15 @@ module ActiveRecord
@association.replace(other_array)
end
- # Deletes all the records from the collection. For +has_many+ associations,
- # the deletion is done according to the strategy specified by the <tt>:dependent</tt>
- # option.
+ # Deletes all the records from the collection according to the strategy
+ # specified by the +:dependent+ option. If no +:dependent+ option is given,
+ # then it will follow the default strategy.
#
- # If no <tt>:dependent</tt> option is given, then it will follow the
- # default strategy. The default strategy is <tt>:nullify</tt>. This
- # sets the foreign keys to <tt>NULL</tt>. For, +has_many+ <tt>:through</tt>,
- # the default strategy is +delete_all+.
+ # For <tt>has_many :through</tt> associations, the default deletion strategy is
+ # +:delete_all+.
+ #
+ # For +has_many+ associations, the default deletion strategy is +:nullify+.
+ # This sets the foreign keys to +NULL+.
#
# class Person < ActiveRecord::Base
# has_many :pets # dependent: :nullify option by default
@@ -393,9 +424,9 @@ module ActiveRecord
# # #<Pet id: 3, name: "Choo-Choo", person_id: nil>
# # ]
#
- # If it is set to <tt>:destroy</tt> all the objects from the collection
- # are removed by calling their +destroy+ method. See +destroy+ for more
- # information.
+ # Both +has_many+ and <tt>has_many :through</tt> dependencies default to the
+ # +:delete_all+ strategy if the +:dependent+ option is set to +:destroy+.
+ # Records are not instantiated and callbacks will not be fired.
#
# class Person < ActiveRecord::Base
# has_many :pets, dependent: :destroy
@@ -410,14 +441,9 @@ module ActiveRecord
# # ]
#
# person.pets.delete_all
- # # => [
- # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
- # # #<Pet id: 2, name: "Spook", person_id: 1>,
- # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
- # # ]
#
# Pet.find(1, 2, 3)
- # # => ActiveRecord::RecordNotFound
+ # # => ActiveRecord::RecordNotFound: Couldn't find all Pets with 'id': (1, 2, 3)
#
# If it is set to <tt>:delete_all</tt>, all the objects are deleted
# *without* calling their +destroy+ method.
@@ -437,14 +463,15 @@ module ActiveRecord
# person.pets.delete_all
#
# Pet.find(1, 2, 3)
- # # => ActiveRecord::RecordNotFound
+ # # => ActiveRecord::RecordNotFound: Couldn't find all Pets with 'id': (1, 2, 3)
def delete_all(dependent = nil)
@association.delete_all(dependent)
end
# Deletes the records of the collection directly from the database
- # ignoring the +:dependent+ option. It invokes +before_remove+,
- # +after_remove+ , +before_destroy+ and +after_destroy+ callbacks.
+ # ignoring the +:dependent+ option. Records are instantiated and it
+ # invokes +before_remove+, +after_remove+ , +before_destroy+ and
+ # +after_destroy+ callbacks.
#
# class Person < ActiveRecord::Base
# has_many :pets
@@ -468,15 +495,16 @@ module ActiveRecord
@association.destroy_all
end
- # Deletes the +records+ supplied and removes them from the collection. For
- # +has_many+ associations, the deletion is done according to the strategy
- # specified by the <tt>:dependent</tt> option. Returns an array with the
+ # Deletes the +records+ supplied from the collection according to the strategy
+ # specified by the +:dependent+ option. If no +:dependent+ option is given,
+ # then it will follow the default strategy. Returns an array with the
# deleted records.
#
- # If no <tt>:dependent</tt> option is given, then it will follow the default
- # strategy. The default strategy is <tt>:nullify</tt>. This sets the foreign
- # keys to <tt>NULL</tt>. For, +has_many+ <tt>:through</tt>, the default
- # strategy is +delete_all+.
+ # For <tt>has_many :through</tt> associations, the default deletion strategy is
+ # +:delete_all+.
+ #
+ # For +has_many+ associations, the default deletion strategy is +:nullify+.
+ # This sets the foreign keys to +NULL+.
#
# class Person < ActiveRecord::Base
# has_many :pets # dependent: :nullify option by default
@@ -529,7 +557,7 @@ module ActiveRecord
# # => [#<Pet id: 2, name: "Spook", person_id: 1>]
#
# Pet.find(1, 3)
- # # => ActiveRecord::RecordNotFound: Couldn't find all Pets with IDs (1, 3)
+ # # => ActiveRecord::RecordNotFound: Couldn't find all Pets with 'id': (1, 3)
#
# If it is set to <tt>:delete_all</tt>, all the +records+ are deleted
# *without* calling their +destroy+ method.
@@ -557,7 +585,7 @@ module ActiveRecord
# # ]
#
# Pet.find(1)
- # # => ActiveRecord::RecordNotFound: Couldn't find Pet with id=1
+ # # => ActiveRecord::RecordNotFound: Couldn't find Pet with 'id'=1
#
# You can pass +Fixnum+ or +String+ values, it finds the records
# responding to the +id+ and executes delete on them.
@@ -621,7 +649,7 @@ module ActiveRecord
# person.pets.size # => 0
# person.pets # => []
#
- # Pet.find(1, 2, 3) # => ActiveRecord::RecordNotFound: Couldn't find all Pets with IDs (1, 2, 3)
+ # Pet.find(1, 2, 3) # => ActiveRecord::RecordNotFound: Couldn't find all Pets with 'id': (1, 2, 3)
#
# You can pass +Fixnum+ or +String+ values, it finds the records
# responding to the +id+ and then deletes them from the database.
@@ -653,7 +681,7 @@ module ActiveRecord
# person.pets.size # => 0
# person.pets # => []
#
- # Pet.find(4, 5, 6) # => ActiveRecord::RecordNotFound: Couldn't find all Pets with IDs (4, 5, 6)
+ # Pet.find(4, 5, 6) # => ActiveRecord::RecordNotFound: Couldn't find all Pets with 'id': (4, 5, 6)
def destroy(*records)
@association.destroy(*records)
end
@@ -690,10 +718,8 @@ module ActiveRecord
# # #<Pet id: 2, name: "Spook", person_id: 1>,
# # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
# # ]
- def count(column_name = nil, options = {})
- # TODO: Remove options argument as soon we remove support to
- # activerecord-deprecated_finders.
- @association.count(column_name, options)
+ def count(column_name = nil)
+ @association.count(column_name)
end
# Returns the size of the collection. If the collection hasn't been loaded,
@@ -780,10 +806,10 @@ module ActiveRecord
# person.pets.any? # => false
#
# person.pets << Pet.new(name: 'Snoop')
- # person.pets.count # => 0
+ # person.pets.count # => 1
# person.pets.any? # => true
#
- # You can also pass a block to define criteria. The behavior
+ # You can also pass a +block+ to define criteria. The behavior
# is the same, it returns true if the collection based on the
# criteria is not empty.
#
@@ -817,7 +843,7 @@ module ActiveRecord
# person.pets.count # => 2
# person.pets.many? # => true
#
- # You can also pass a block to define criteria. The
+ # You can also pass a +block+ to define criteria. The
# behavior is the same, it returns true if the collection
# based on the criteria has more than one record.
#
@@ -841,7 +867,7 @@ module ActiveRecord
@association.many?(&block)
end
- # Returns +true+ if the given object is present in the collection.
+ # Returns +true+ if the given +record+ is present in the collection.
#
# class Person < ActiveRecord::Base
# has_many :pets
@@ -855,7 +881,7 @@ module ActiveRecord
!!@association.include?(record)
end
- def arel
+ def arel #:nodoc:
scope.arel
end
@@ -879,7 +905,7 @@ module ActiveRecord
# Equivalent to <tt>Array#==</tt>. Returns +true+ if the two arrays
# contain the same number of elements and if each element is equal
- # to the corresponding element in the other array, otherwise returns
+ # to the corresponding element in the +other+ array, otherwise returns
# +false+.
#
# class Person < ActiveRecord::Base
@@ -970,12 +996,15 @@ module ActiveRecord
alias_method :append, :<<
def prepend(*args)
- raise NoMethodError, "prepend on association is not defined. Please use << or append"
+ raise NoMethodError, "prepend on association is not defined. Please use <<, push or append"
end
# Equivalent to +delete_all+. The difference is that returns +self+, instead
# of an array with the deleted objects, so methods can be chained. See
# +delete_all+ for more information.
+ # Note that because +delete_all+ removes records by directly
+ # running an SQL query into the database, the +updated_at+ column of
+ # the object is not changed.
def clear
delete_all
self
diff --git a/activerecord/lib/active_record/associations/foreign_association.rb b/activerecord/lib/active_record/associations/foreign_association.rb
new file mode 100644
index 0000000000..3ceec0ee46
--- /dev/null
+++ b/activerecord/lib/active_record/associations/foreign_association.rb
@@ -0,0 +1,11 @@
+module ActiveRecord::Associations
+ module ForeignAssociation # :nodoc:
+ def foreign_key_present?
+ if reflection.klass.primary_key
+ owner.attribute_present?(reflection.active_record_primary_key)
+ else
+ false
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb
index 2a97d0ed31..a9f6aaafef 100644
--- a/activerecord/lib/active_record/associations/has_many_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_association.rb
@@ -6,6 +6,7 @@ module ActiveRecord
# If the association has a <tt>:through</tt> option further specialization
# is provided by its child HasManyThroughAssociation.
class HasManyAssociation < CollectionAssociation #:nodoc:
+ include ForeignAssociation
def handle_dependency
case options[:dependent]
@@ -14,9 +15,16 @@ module ActiveRecord
when :restrict_with_error
unless empty?
- record = klass.human_attribute_name(reflection.name).downcase
- owner.errors.add(:base, :"restrict_dependent_destroy.many", record: record)
- false
+ record = owner.class.human_attribute_name(reflection.name).downcase
+ message = owner.errors.generate_message(:base, :'restrict_dependent_destroy.many', record: record, raise: true) rescue nil
+ if message
+ ActiveSupport::Deprecation.warn(<<-MESSAGE.squish)
+ The error key `:'restrict_dependent_destroy.many'` has been deprecated and will be removed in Rails 5.1.
+ Please use `:'restrict_dependent_destroy.has_many'` instead.
+ MESSAGE
+ end
+ owner.errors.add(:base, message || :'restrict_dependent_destroy.has_many', record: record)
+ throw(:abort)
end
else
@@ -42,7 +50,7 @@ module ActiveRecord
end
def empty?
- if has_cached_counter?
+ if reflection.has_cached_counter?
size.zero?
else
super
@@ -65,8 +73,8 @@ module ActiveRecord
# If the collection is empty the target is set to an empty array and
# the loaded flag is set to true as well.
def count_records
- count = if has_cached_counter?
- owner.read_attribute cached_counter_attribute_name
+ count = if reflection.has_cached_counter?
+ owner._read_attribute reflection.counter_cache_column
else
scope.count
end
@@ -79,56 +87,20 @@ module ActiveRecord
[association_scope.limit_value, count].compact.min
end
- def has_cached_counter?(reflection = reflection())
- owner.attribute_present?(cached_counter_attribute_name(reflection))
- end
-
- def cached_counter_attribute_name(reflection = reflection())
- options[:counter_cache] || "#{reflection.name}_count"
- end
-
def update_counter(difference, reflection = reflection())
- update_counter_in_database(difference, reflection)
- update_counter_in_memory(difference, reflection)
- end
-
- def update_counter_in_database(difference, reflection = reflection())
- if has_cached_counter?(reflection)
- counter = cached_counter_attribute_name(reflection)
- owner.class.update_counters(owner.id, counter => difference)
+ if reflection.has_cached_counter?
+ owner.increment!(reflection.counter_cache_column, difference)
end
end
def update_counter_in_memory(difference, reflection = reflection())
- if has_cached_counter?(reflection)
- counter = cached_counter_attribute_name(reflection)
- owner[counter] += difference
- owner.changed_attributes.delete(counter) # eww
+ if reflection.counter_must_be_updated_by_has_many?
+ counter = reflection.counter_cache_column
+ owner.increment(counter, difference)
+ owner.send(:clear_attribute_change, counter) # eww
end
end
- # This shit is nasty. We need to avoid the following situation:
- #
- # * An associated record is deleted via record.destroy
- # * Hence the callbacks run, and they find a belongs_to on the record with a
- # :counter_cache options which points back at our owner. So they update the
- # counter cache.
- # * In which case, we must make sure to *not* update the counter cache, or else
- # it will be decremented twice.
- #
- # Hence this method.
- def inverse_updates_counter_cache?(reflection = reflection())
- counter_name = cached_counter_attribute_name(reflection)
- inverse_updates_counter_named?(counter_name, reflection)
- end
-
- def inverse_updates_counter_named?(counter_name, reflection = reflection())
- reflection.klass._reflections.values.any? { |inverse_reflection|
- :belongs_to == inverse_reflection.macro &&
- inverse_reflection.counter_cache_column == counter_name
- }
- end
-
def delete_count(method, scope)
if method == :delete_all
scope.delete_all
@@ -146,21 +118,13 @@ module ActiveRecord
def delete_records(records, method)
if method == :destroy
records.each(&:destroy!)
- update_counter(-records.length) unless inverse_updates_counter_cache?
+ update_counter(-records.length) unless reflection.inverse_updates_counter_cache?
else
scope = self.scope.where(reflection.klass.primary_key => records)
update_counter(-delete_count(method, scope))
end
end
- def foreign_key_present?
- if reflection.klass.primary_key
- owner.attribute_present?(reflection.association_primary_key)
- else
- false
- end
- end
-
def concat_records(records, *)
update_counter_if_success(super, records.length)
end
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 44c4436e95..deb0f8c9f5 100644
--- a/activerecord/lib/active_record/associations/has_many_through_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_through_association.rb
@@ -11,21 +11,6 @@ module ActiveRecord
@through_association = nil
end
- # Returns the size of the collection by executing a SELECT COUNT(*) query
- # if the collection hasn't been loaded, and by calling collection.size if
- # it has. If the collection will likely have a size greater than zero,
- # and if fetching the collection will be needed afterwards, one less
- # SELECT query will be generated by using #length instead.
- def size
- if has_cached_counter?
- owner.read_attribute cached_counter_attribute_name(reflection)
- elsif loaded?
- target.size
- else
- super
- end
- end
-
def concat(*records)
unless owner.new_record?
records.flatten.each do |record|
@@ -53,24 +38,14 @@ module ActiveRecord
def insert_record(record, validate = true, raise = false)
ensure_not_nested
- if record.new_record?
- if raise
- record.save!(:validate => validate)
- else
- return unless record.save(:validate => validate)
- end
+ if raise
+ record.save!(:validate => validate)
+ else
+ return unless record.save(:validate => validate)
end
save_through_record(record)
- if has_cached_counter? && !through_reflection_updates_counter_cache?
- ActiveSupport::Deprecation.warn(<<-MESSAGE.strip_heredoc)
- Automatic updating of counter caches on through associations has been
- deprecated, and will be removed in Rails 5.0. Instead, please set the
- appropriate counter_cache options on the has_many and belongs_to for
- your associations to #{through_reflection.name}.
- MESSAGE
- update_counter_in_database(1)
- end
+
record
end
@@ -135,7 +110,7 @@ module ActiveRecord
def update_through_counter?(method)
case method
when :destroy
- !inverse_updates_counter_cache?(through_reflection)
+ !through_reflection.inverse_updates_counter_cache?
when :nullify
false
else
@@ -158,17 +133,15 @@ module ActiveRecord
if scope.klass.primary_key
count = scope.destroy_all.length
else
- scope.to_a.each do |record|
- record.run_callbacks :destroy
- end
+ scope.each(&:_run_destroy_callbacks)
arel = scope.arel
- stmt = Arel::DeleteManager.new arel.engine
+ stmt = Arel::DeleteManager.new
stmt.from scope.klass.arel_table
stmt.wheres = arel.constraints
- count = scope.klass.connection.delete(stmt, 'SQL', scope.bind_values)
+ count = scope.klass.connection.delete(stmt, 'SQL', scope.bound_attributes)
end
when :nullify
count = scope.update_all(source_reflection.foreign_key => nil)
@@ -185,9 +158,9 @@ module ActiveRecord
if through_reflection.collection? && update_through_counter?(method)
update_counter(-count, through_reflection)
+ else
+ update_counter(-count)
end
-
- update_counter(-count)
end
def through_records_for(record)
@@ -225,11 +198,6 @@ module ActiveRecord
def invertible_for?(record)
false
end
-
- def through_reflection_updates_counter_cache?
- counter_name = cached_counter_attribute_name
- inverse_updates_counter_named?(counter_name, through_reflection)
- end
end
end
end
diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb
index e6095d84dc..0fe9b2e81b 100644
--- a/activerecord/lib/active_record/associations/has_one_association.rb
+++ b/activerecord/lib/active_record/associations/has_one_association.rb
@@ -1,7 +1,8 @@
module ActiveRecord
- # = Active Record Belongs To Has One Association
+ # = Active Record Has One Association
module Associations
class HasOneAssociation < SingularAssociation #:nodoc:
+ include ForeignAssociation
def handle_dependency
case options[:dependent]
@@ -10,9 +11,16 @@ module ActiveRecord
when :restrict_with_error
if load_target
- record = klass.human_attribute_name(reflection.name).downcase
- owner.errors.add(:base, :"restrict_dependent_destroy.one", record: record)
- false
+ record = owner.class.human_attribute_name(reflection.name).downcase
+ message = owner.errors.generate_message(:base, :'restrict_dependent_destroy.one', record: record, raise: true) rescue nil
+ if message
+ ActiveSupport::Deprecation.warn(<<-MESSAGE.squish)
+ The error key `:'restrict_dependent_destroy.one'` has been deprecated and will be removed in Rails 5.1.
+ Please use `:'restrict_dependent_destroy.has_one'` instead.
+ MESSAGE
+ end
+ owner.errors.add(:base, message || :'restrict_dependent_destroy.has_one', record: record)
+ throw(:abort)
end
else
@@ -57,7 +65,7 @@ module ActiveRecord
when :destroy
target.destroy
when :nullify
- target.update_columns(reflection.foreign_key => nil)
+ target.update_columns(reflection.foreign_key => nil) if target.persisted?
end
end
end
diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb
index ec5c189cd3..0e98a3b3a4 100644
--- a/activerecord/lib/active_record/associations/join_dependency.rb
+++ b/activerecord/lib/active_record/associations/join_dependency.rb
@@ -20,7 +20,7 @@ module ActiveRecord
end
def columns
- @tables.flat_map { |t| t.column_aliases }
+ @tables.flat_map(&:column_aliases)
end
# An array of [column_name, alias] pairs for the table
@@ -93,8 +93,7 @@ module ActiveRecord
# joins # => []
#
def initialize(base, associations, joins)
- @alias_tracker = AliasTracker.create(base.connection, joins)
- @alias_tracker.aliased_name_for(base.table_name, base.table_name) # Updates the count for base.table_name to 1
+ @alias_tracker = AliasTracker.create_with_joins(base.connection, base.table_name, joins, base.type_caster)
tree = self.class.make_tree associations
@join_root = JoinBase.new base, build(tree, base)
@join_root.children.each { |child| construct_tables! @join_root, child }
@@ -132,9 +131,9 @@ module ActiveRecord
def instantiate(result_set, aliases)
primary_key = aliases.column_alias(join_root, join_root.primary_key)
- seen = Hash.new { |h,parent_klass|
- h[parent_klass] = Hash.new { |i,parent_id|
- i[parent_id] = Hash.new { |j,child_klass| j[child_klass] = {} }
+ seen = Hash.new { |i, object_id|
+ i[object_id] = Hash.new { |j, child_class|
+ j[child_class] = {}
}
}
@@ -142,11 +141,21 @@ module ActiveRecord
parents = model_cache[join_root]
column_aliases = aliases.column_aliases join_root
- result_set.each { |row_hash|
- parent = parents[row_hash[primary_key]] ||= join_root.instantiate(row_hash, column_aliases)
- construct(parent, join_root, row_hash, result_set, seen, model_cache, aliases)
+ message_bus = ActiveSupport::Notifications.instrumenter
+
+ payload = {
+ record_count: result_set.length,
+ class_name: join_root.base_klass.name
}
+ message_bus.instrument('instantiation.active_record', payload) do
+ result_set.each { |row_hash|
+ parent_key = primary_key ? row_hash[primary_key] : row_hash
+ parent = parents[parent_key] ||= join_root.instantiate(row_hash, column_aliases)
+ construct(parent, join_root, row_hash, result_set, seen, model_cache, aliases)
+ }
+ end
+
parents.values
end
@@ -224,31 +233,34 @@ module ActiveRecord
end
def construct(ar_parent, parent, row, rs, seen, model_cache, aliases)
- primary_id = ar_parent.id
+ return if ar_parent.nil?
parent.children.each do |node|
if node.reflection.collection?
other = ar_parent.association(node.reflection.name)
other.loaded!
- else
- if ar_parent.association_cache.key?(node.reflection.name)
- model = ar_parent.association(node.reflection.name).target
- construct(model, node, row, rs, seen, model_cache, aliases)
- next
- end
+ elsif ar_parent.association_cached?(node.reflection.name)
+ model = ar_parent.association(node.reflection.name).target
+ construct(model, node, row, rs, seen, model_cache, aliases)
+ next
end
key = aliases.column_alias(node, node.primary_key)
id = row[key]
- next if id.nil?
+ if id.nil?
+ nil_association = ar_parent.association(node.reflection.name)
+ nil_association.loaded!
+ next
+ end
- model = seen[parent.base_klass][primary_id][node.base_klass][id]
+ model = seen[ar_parent.object_id][node.base_klass][id]
if model
construct(model, node, row, rs, seen, model_cache, aliases)
else
model = construct_model(ar_parent, node, row, model_cache, id, aliases)
- seen[parent.base_klass][primary_id][node.base_klass][id] = model
+ model.readonly!
+ seen[ar_parent.object_id][node.base_klass][id] = model
construct(model, node, row, rs, seen, model_cache, aliases)
end
end
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 719eff9acc..a6ad09a38a 100644
--- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb
+++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb
@@ -25,7 +25,7 @@ module ActiveRecord
def join_constraints(foreign_table, foreign_klass, node, join_type, tables, scope_chain, chain)
joins = []
- bind_values = []
+ binds = []
tables = tables.reverse
scope_chain_index = 0
@@ -37,43 +37,45 @@ module ActiveRecord
table = tables.shift
klass = reflection.klass
- case reflection.source_macro
- when :belongs_to
- key = reflection.association_primary_key
- foreign_key = reflection.foreign_key
- else
- key = reflection.foreign_key
- foreign_key = reflection.active_record_primary_key
- end
+ join_keys = reflection.join_keys(klass)
+ key = join_keys.key
+ foreign_key = join_keys.foreign_key
constraint = build_constraint(klass, table, key, foreign_table, foreign_key)
+ predicate_builder = PredicateBuilder.new(TableMetadata.new(klass, table))
scope_chain_items = scope_chain[scope_chain_index].map do |item|
if item.is_a?(Relation)
item
else
- ActiveRecord::Relation.create(klass, table).instance_exec(node, &item)
+ ActiveRecord::Relation.create(klass, table, predicate_builder)
+ .instance_exec(node, &item)
end
end
scope_chain_index += 1
- scope_chain_items.concat [klass.send(:build_default_scope, ActiveRecord::Relation.create(klass, table))].compact
+ relation = ActiveRecord::Relation.create(
+ klass,
+ table,
+ predicate_builder,
+ )
+ scope_chain_items.concat [klass.send(:build_default_scope, relation)].compact
rel = scope_chain_items.inject(scope_chain_items.shift) do |left, right|
left.merge right
end
if rel && !rel.arel.constraints.empty?
- bind_values.concat rel.bind_values
+ binds += rel.bound_attributes
constraint = constraint.and rel.arel.constraints
end
if reflection.type
value = foreign_klass.base_class.name
- column = klass.columns_hash[column.to_s]
+ column = klass.columns_hash[reflection.type.to_s]
- substitute = klass.connection.substitute_at(column, bind_values.length)
- bind_values.push [column, value]
+ substitute = klass.connection.substitute_at(column)
+ binds << Relation::QueryAttribute.new(column.name, value, klass.type_for_attribute(column.name))
constraint = constraint.and table[reflection.type].eq substitute
end
@@ -83,7 +85,7 @@ module ActiveRecord
foreign_table, foreign_klass = table, klass
end
- JoinInformation.new joins, bind_values
+ JoinInformation.new joins, binds
end
# Builds equality condition.
@@ -95,7 +97,7 @@ module ActiveRecord
# end
#
# If I execute `Physician.joins(:appointments).to_a` then
- # reflection # => #<ActiveRecord::Reflection::HasManyReflection ...>
+ # klass # => Physician
# table # => #<Arel::Table @name="appointments" ...>
# key # => physician_id
# foreign_table # => #<Arel::Table @name="physicians" ...>
diff --git a/activerecord/lib/active_record/associations/join_dependency/join_part.rb b/activerecord/lib/active_record/associations/join_dependency/join_part.rb
index 91e1c6a9d7..9c6573f913 100644
--- a/activerecord/lib/active_record/associations/join_dependency/join_part.rb
+++ b/activerecord/lib/active_record/associations/join_dependency/join_part.rb
@@ -19,7 +19,6 @@ module ActiveRecord
def initialize(base_klass, children)
@base_klass = base_klass
- @column_names_with_alias = nil
@children = children
end
diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb
index 7519fec10a..ecf6fb8643 100644
--- a/activerecord/lib/active_record/associations/preloader.rb
+++ b/activerecord/lib/active_record/associations/preloader.rb
@@ -2,33 +2,42 @@ module ActiveRecord
module Associations
# Implements the details of eager loading of Active Record associations.
#
- # Note that 'eager loading' and 'preloading' are actually the same thing.
- # However, there are two different eager loading strategies.
+ # Suppose that you have the following two Active Record models:
#
- # The first one is by using table joins. This was only strategy available
- # prior to Rails 2.1. Suppose that you have an Author model with columns
- # 'name' and 'age', and a Book model with columns 'name' and 'sales'. Using
- # this strategy, Active Record would try to retrieve all data for an author
- # and all of its books via a single query:
+ # class Author < ActiveRecord::Base
+ # # columns: name, age
+ # has_many :books
+ # end
#
- # SELECT * FROM authors
- # LEFT OUTER JOIN books ON authors.id = books.author_id
- # WHERE authors.name = 'Ken Akamatsu'
+ # class Book < ActiveRecord::Base
+ # # columns: title, sales, author_id
+ # end
#
- # However, this could result in many rows that contain redundant data. After
- # having received the first row, we already have enough data to instantiate
- # the Author object. In all subsequent rows, only the data for the joined
- # 'books' table is useful; the joined 'authors' data is just redundant, and
- # processing this redundant data takes memory and CPU time. The problem
- # quickly becomes worse and worse as the level of eager loading increases
- # (i.e. if Active Record is to eager load the associations' associations as
- # well).
+ # When you load an author with all associated books Active Record will make
+ # multiple queries like this:
+ #
+ # Author.includes(:books).where(name: ['bell hooks', 'Homer']).to_a
+ #
+ # => SELECT `authors`.* FROM `authors` WHERE `name` IN ('bell hooks', 'Homer')
+ # => SELECT `books`.* FROM `books` WHERE `author_id` IN (2, 5)
+ #
+ # Active Record saves the ids of the records from the first query to use in
+ # the second. Depending on the number of associations involved there can be
+ # arbitrarily many SQL queries made.
+ #
+ # However, if there is a WHERE clause that spans across tables Active
+ # Record will fall back to a slightly more resource-intensive single query:
+ #
+ # Author.includes(:books).where(books: {title: 'Illiad'}).to_a
+ # => SELECT `authors`.`id` AS t0_r0, `authors`.`name` AS t0_r1, `authors`.`age` AS t0_r2,
+ # `books`.`id` AS t1_r0, `books`.`title` AS t1_r1, `books`.`sales` AS t1_r2
+ # FROM `authors`
+ # LEFT OUTER JOIN `books` ON `authors`.`id` = `books`.`author_id`
+ # WHERE `books`.`title` = 'Illiad'
+ #
+ # This could result in many rows that contain redundant data and it performs poorly at scale
+ # and is therefore only used when necessary.
#
- # The second strategy is to use multiple database queries, one for each
- # level of association. Since Rails 2.1, this is the default strategy. In
- # situations where a table join is necessary (e.g. when the +:conditions+
- # option references an association's column), it will fallback to the table
- # join strategy.
class Preloader #:nodoc:
extend ActiveSupport::Autoload
@@ -45,6 +54,8 @@ module ActiveRecord
autoload :BelongsTo, 'active_record/associations/preloader/belongs_to'
end
+ NULL_RELATION = Struct.new(:values, :where_clause, :joins_values).new({}, Relation::WhereClause.empty, [])
+
# Eager loads the named associations for the given Active Record record(s).
#
# In this description, 'association name' shall refer to the name passed
@@ -79,9 +90,6 @@ module ActiveRecord
# [ :books, :author ]
# { author: :avatar }
# [ :books, { author: :avatar } ]
-
- NULL_RELATION = Struct.new(:values, :bind_values).new({}, [])
-
def preload(records, associations, preload_scope = nil)
records = Array.wrap(records).compact.uniq
associations = Array.wrap(associations)
@@ -98,6 +106,7 @@ module ActiveRecord
private
+ # Loads all the given data into +records+ for the +association+.
def preloaders_on(association, records, scope)
case association
when Hash
@@ -107,7 +116,7 @@ module ActiveRecord
when String
preloaders_for_one(association.to_sym, records, scope)
else
- raise ArgumentError, "#{association.inspect} was not recognised for preload"
+ raise ArgumentError, "#{association.inspect} was not recognized for preload"
end
end
@@ -123,6 +132,11 @@ module ActiveRecord
}
end
+ # Loads all the given data into +records+ for a singular +association+.
+ #
+ # Functions by instantiating a preloader class such as Preloader::HasManyThrough and
+ # call the +run+ method for each passed in class in the +records+ argument.
+ #
# Not all records have the same class, so group then preload group on the reflection
# itself so that if various subclass share the same association then we do not split
# them unnecessarily
@@ -151,7 +165,7 @@ module ActiveRecord
h
end
- class AlreadyLoaded
+ class AlreadyLoaded # :nodoc:
attr_reader :owners, :reflection
def initialize(klass, owners, reflection, preload_scope)
@@ -166,11 +180,16 @@ module ActiveRecord
end
end
- class NullPreloader
+ class NullPreloader # :nodoc:
def self.new(klass, owners, reflection, preload_scope); self; end
def self.run(preloader); end
+ def self.preloaded_records; []; end
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
+ # that accepts a preloader.
def preloader_for(reflection, owners, rhs_klass)
return NullPreloader unless rhs_klass
diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb
index c0639742be..c43f13f3c4 100644
--- a/activerecord/lib/active_record/associations/preloader/association.rb
+++ b/activerecord/lib/active_record/associations/preloader/association.rb
@@ -12,7 +12,6 @@ module ActiveRecord
@preload_scope = preload_scope
@model = owners.first && owners.first.class
@scope = nil
- @owners_by_key = nil
@preloaded_records = []
end
@@ -33,7 +32,7 @@ module ActiveRecord
end
def query_scope(ids)
- scope.where(association_key.in(ids))
+ scope.where(association_key_name => ids)
end
def table
@@ -56,18 +55,6 @@ module ActiveRecord
raise NotImplementedError
end
- def owners_by_key
- @owners_by_key ||= if key_conversion_required?
- owners.group_by do |owner|
- owner[owner_key_name].to_s
- end
- else
- owners.group_by do |owner|
- owner[owner_key_name]
- end
- end
- end
-
def options
reflection.options
end
@@ -75,32 +62,33 @@ module ActiveRecord
private
def associated_records_by_owner(preloader)
- owners_map = owners_by_key
- owner_keys = owners_map.keys.compact
-
- # Each record may have multiple owners, and vice-versa
- records_by_owner = owners.each_with_object({}) do |owner,h|
- h[owner] = []
+ records = load_records
+ owners.each_with_object({}) do |owner, result|
+ result[owner] = records[convert_key(owner[owner_key_name])] || []
end
+ end
- if owner_keys.any?
- # Some databases impose a limit on the number of ids in a list (in Oracle it's 1000)
- # Make several smaller queries if necessary or make one query if the adapter supports it
- sliced = owner_keys.each_slice(klass.connection.in_clause_length || owner_keys.size)
-
- records = load_slices sliced
- records.each do |record, owner_key|
- owners_map[owner_key].each do |owner|
- records_by_owner[owner] << record
- 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
-
- records_by_owner
+ @owner_keys
end
def key_conversion_required?
- association_key_type != owner_key_type
+ @key_conversion_required ||= association_key_type != owner_key_type
+ end
+
+ def convert_key(key)
+ if key_conversion_required?
+ key.to_s
+ else
+ key
+ end
end
def association_key_type
@@ -111,17 +99,17 @@ module ActiveRecord
@model.type_for_attribute(owner_key_name.to_s).type
end
- def load_slices(slices)
- @preloaded_records = slices.flat_map { |slice|
+ def load_records
+ return {} if owner_keys.empty?
+ # Some databases impose a limit on the number of ids in a list (in Oracle it's 1000)
+ # Make several smaller queries if necessary or make one query if the adapter supports it
+ slices = owner_keys.each_slice(klass.connection.in_clause_length || owner_keys.size)
+ @preloaded_records = slices.flat_map do |slice|
records_for(slice)
- }
-
- @preloaded_records.map { |record|
- key = record[association_key_name]
- key = key.to_s if key_conversion_required?
-
- [record, key]
- }
+ end
+ @preloaded_records.group_by do |record|
+ convert_key(record[association_key_name])
+ end
end
def reflection_scope
@@ -131,24 +119,25 @@ module ActiveRecord
def build_scope
scope = klass.unscoped
- values = reflection_scope.values
- reflection_binds = reflection_scope.bind_values
+ values = reflection_scope.values
preload_values = preload_scope.values
- preload_binds = preload_scope.bind_values
- scope.where_values = Array(values[:where]) + Array(preload_values[:where])
+ scope.where_clause = reflection_scope.where_clause + preload_scope.where_clause
scope.references_values = Array(values[:references]) + Array(preload_values[:references])
- scope.bind_values = (reflection_binds + preload_binds)
- scope._select! preload_values[:select] || values[:select] || table[Arel.star]
+ if preload_values[:select] || values[:select]
+ scope._select!(preload_values[:select] || values[:select])
+ end
scope.includes! preload_values[:includes] || values[:includes]
-
- if preload_values.key? :order
- scope.order! preload_values[:order]
+ if preload_scope.joins_values.any?
+ scope.joins!(preload_scope.joins_values)
else
- if values.key? :order
- scope.order! values[:order]
- end
+ scope.joins!(reflection_scope.joins_values)
+ end
+ scope.order! preload_values[:order] || values[:order]
+
+ if preload_values[:reordering] || values[:reordering]
+ scope.reordering_value = true
end
if preload_values[:readonly] || values[:readonly]
@@ -159,6 +148,7 @@ module ActiveRecord
scope.where!(klass.table_name => { reflection.type => model.base_class.sti_name })
end
+ scope.unscope_values = Array(values[:unscope]) + Array(preload_values[:unscope])
klass.default_scoped.merge(scope)
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
index 7b37b5942d..2029871f39 100644
--- a/activerecord/lib/active_record/associations/preloader/has_many_through.rb
+++ b/activerecord/lib/active_record/associations/preloader/has_many_through.rb
@@ -8,7 +8,7 @@ module ActiveRecord
records_by_owner = super
if reflection_scope.distinct_value
- records_by_owner.each_value { |records| records.uniq! }
+ records_by_owner.each_value(&:uniq!)
end
records_by_owner
diff --git a/activerecord/lib/active_record/associations/preloader/through_association.rb b/activerecord/lib/active_record/associations/preloader/through_association.rb
index 1fed7f74e7..56aa23b173 100644
--- a/activerecord/lib/active_record/associations/preloader/through_association.rb
+++ b/activerecord/lib/active_record/associations/preloader/through_association.rb
@@ -63,7 +63,7 @@ module ActiveRecord
should_reset = (through_scope != through_reflection.klass.unscoped) ||
(reflection.options[:source_type] && through_reflection.collection?)
- # Dont cache the association - we would only be caching a subset
+ # Don't cache the association - we would only be caching a subset
if should_reset
owners.each { |owner|
owner.association(association_name).reset
@@ -78,9 +78,9 @@ module ActiveRecord
if options[:source_type]
scope.where! reflection.foreign_type => options[:source_type]
else
- unless reflection_scope.where_values.empty?
+ unless reflection_scope.where_clause.empty?
scope.includes_values = Array(reflection_scope.values[:includes] || options[:source])
- scope.where_values = reflection_scope.values[:where]
+ scope.where_clause = reflection_scope.where_clause
end
scope.references! reflection_scope.values[:references]
diff --git a/activerecord/lib/active_record/associations/singular_association.rb b/activerecord/lib/active_record/associations/singular_association.rb
index f2e3a4e40f..c7cc48ba16 100644
--- a/activerecord/lib/active_record/associations/singular_association.rb
+++ b/activerecord/lib/active_record/associations/singular_association.rb
@@ -3,7 +3,13 @@ module ActiveRecord
class SingularAssociation < Association #:nodoc:
# Implements the reader method, e.g. foo.bar for Foo.has_one :bar
def reader(force_reload = false)
- if force_reload
+ if force_reload && klass
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ Passing an argument to force an association to reload is now
+ deprecated and will be removed in Rails 5.1. Please call `reload`
+ on the parent object instead.
+ MSG
+
klass.uncached { reload }
elsif !loaded? || stale_target?
reload
@@ -39,7 +45,7 @@ module ActiveRecord
end
def get_records
- return scope.limit(1).to_a if reflection.scope_chain.any?(&:any?)
+ return scope.limit(1).to_a if skip_statement_cache?
conn = klass.connection
sc = reflection.association_scope_cache(conn, owner) do
diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb
index f00fef8b9e..d0ec3e8015 100644
--- a/activerecord/lib/active_record/associations/through_association.rb
+++ b/activerecord/lib/active_record/associations/through_association.rb
@@ -3,7 +3,7 @@ module ActiveRecord
module Associations
module ThroughAssociation #:nodoc:
- delegate :source_reflection, :through_reflection, :chain, :to => :reflection
+ delegate :source_reflection, :through_reflection, :to => :reflection
protected
@@ -13,10 +13,8 @@ module ActiveRecord
# 2. To get the type conditions for any STI models in the chain
def target_scope
scope = super
- chain.drop(1).each do |reflection|
+ reflection.chain.drop(1).each do |reflection|
relation = reflection.klass.all
- relation.merge!(reflection.scope) if reflection.scope
-
scope.merge!(
relation.except(:select, :create_with, :includes, :preload, :joins, :eager_load)
)
@@ -29,7 +27,7 @@ module ActiveRecord
# Construct attributes for :through pointing to owner and associate. This is used by the
# methods which create and delete records on the association.
#
- # We only support indirectly modifying through associations which has a belongs_to source.
+ # We only support indirectly modifying through associations which have a belongs_to source.
# This is the "has_many :tags, through: :taggings" situation, where the join model
# typically has a belongs_to on both side. In other words, associations which could also
# be represented as has_and_belongs_to_many associations.
@@ -77,15 +75,34 @@ module ActiveRecord
end
def ensure_mutable
- if source_reflection.macro != :belongs_to
- raise HasManyThroughCantAssociateThroughHasOneOrManyReflection.new(owner, reflection)
+ unless source_reflection.belongs_to?
+ if reflection.has_one?
+ raise HasOneThroughCantAssociateThroughHasOneOrManyReflection.new(owner, reflection)
+ else
+ raise HasManyThroughCantAssociateThroughHasOneOrManyReflection.new(owner, reflection)
+ end
end
end
def ensure_not_nested
if reflection.nested?
- raise HasManyThroughNestedAssociationsAreReadonly.new(owner, reflection)
+ if reflection.has_one?
+ raise HasOneThroughNestedAssociationsAreReadonly.new(owner, reflection)
+ else
+ raise HasManyThroughNestedAssociationsAreReadonly.new(owner, reflection)
+ end
+ end
+ end
+
+ def build_record(attributes)
+ inverse = source_reflection.inverse_of
+ target = through_association.target
+
+ if inverse && target && !target.is_a?(Array)
+ attributes[inverse.foreign_key] = target.id
end
+
+ super(attributes)
end
end
end
diff --git a/activerecord/lib/active_record/attribute.rb b/activerecord/lib/active_record/attribute.rb
index 6d38224830..3c4c8f10ec 100644
--- a/activerecord/lib/active_record/attribute.rb
+++ b/activerecord/lib/active_record/attribute.rb
@@ -5,8 +5,12 @@ module ActiveRecord
FromDatabase.new(name, value, type)
end
- def from_user(name, value, type)
- FromUser.new(name, value, type)
+ 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)
@@ -22,10 +26,11 @@ module ActiveRecord
# This method should not be called directly.
# Use #from_database or #from_user
- def initialize(name, value_before_type_cast, type)
+ 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
@@ -34,27 +39,48 @@ module ActiveRecord
@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.type_cast_for_database(value)
+ type.serialize(value)
end
- def changed_from?(old_value)
- type.changed?(old_value, value, value_before_type_cast)
+ def changed?
+ changed_from_assignment? || changed_in_place?
end
- def changed_in_place_from?(old_value)
- type.changed_in_place?(old_value, value)
+ 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)
- self.class.from_user(name, value, type)
+ type.assert_valid_value(value)
+ self.class.from_user(name, value, type, self)
end
def with_value_from_database(value)
self.class.from_database(name, value, type)
end
- def type_cast
+ def with_cast_value(value)
+ self.class.with_cast_value(name, value, type)
+ end
+
+ def with_type(type)
+ self.class.new(name, value_before_type_cast, type, original_attribute)
+ end
+
+ def type_cast(*)
raise NotImplementedError
end
@@ -62,23 +88,80 @@ module ActiveRecord
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
+
protected
+ attr_reader :original_attribute
+ alias_method :assigned?, :original_attribute
+
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
+ if assigned?
+ original_attribute.original_value_for_database
+ else
+ _original_value_for_database
+ end
+ end
+
+ def _original_value_for_database
+ value_for_database
+ end
+
class FromDatabase < Attribute # :nodoc:
def type_cast(value)
- type.type_cast_from_database(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.type_cast_from_user(value)
+ type.cast(value)
+ end
+
+ def came_from_user?
+ true
+ end
+ end
+
+ class WithCastValue < Attribute # :nodoc:
+ def type_cast(value)
+ value
+ end
+
+ def changed_in_place_from?(old_value)
+ false
end
end
@@ -91,6 +174,10 @@ module ActiveRecord
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
@@ -115,6 +202,6 @@ module ActiveRecord
false
end
end
- private_constant :FromDatabase, :FromUser, :Null, :Uninitialized
+ 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
new file mode 100644
index 0000000000..6dbd92ce28
--- /dev/null
+++ b/activerecord/lib/active_record/attribute/user_provided_default.rb
@@ -0,0 +1,23 @@
+require 'active_record/attribute'
+
+module ActiveRecord
+ class Attribute # :nodoc:
+ class UserProvidedDefault < FromUser # :nodoc:
+ def initialize(name, value, type, database_default)
+ super(name, value, type, database_default)
+ end
+
+ def type_cast(value)
+ if value.is_a?(Proc)
+ super(value.call)
+ else
+ super
+ end
+ end
+
+ def with_type(type)
+ self.class.new(name, value_before_type_cast, type, original_attribute)
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/attribute_assignment.rb b/activerecord/lib/active_record/attribute_assignment.rb
index 2887db3bf7..a6d81c82b4 100644
--- a/activerecord/lib/active_record/attribute_assignment.rb
+++ b/activerecord/lib/active_record/attribute_assignment.rb
@@ -3,61 +3,38 @@ require 'active_model/forbidden_attributes_protection'
module ActiveRecord
module AttributeAssignment
extend ActiveSupport::Concern
- include ActiveModel::ForbiddenAttributesProtection
-
- # Allows you to set all the attributes by passing in a hash of attributes with
- # keys matching the attribute names (which again matches the column names).
- #
- # If the passed hash responds to <tt>permitted?</tt> method and the return value
- # of this method is +false+ an <tt>ActiveModel::ForbiddenAttributesError</tt>
- # exception is raised.
- #
- # cat = Cat.new(name: "Gorby", status: "yawning")
- # cat.attributes # => { "name" => "Gorby", "status" => "yawning", "created_at" => nil, "updated_at" => nil}
- # cat.assign_attributes(status: "sleeping")
- # cat.attributes # => { "name" => "Gorby", "status" => "sleeping", "created_at" => nil, "updated_at" => nil }
- #
- # New attributes will be persisted in the database when the object is saved.
- #
- # Aliased to <tt>attributes=</tt>.
- def assign_attributes(new_attributes)
- if !new_attributes.respond_to?(:stringify_keys)
- raise ArgumentError, "When assigning attributes, you must pass a hash as an argument."
- end
- return if new_attributes.blank?
+ include ActiveModel::AttributeAssignment
+
+ # Alias for ActiveModel::AttributeAssignment#assign_attributes. See ActiveModel::AttributeAssignment.
+ def attributes=(attributes)
+ assign_attributes(attributes)
+ end
- attributes = new_attributes.stringify_keys
- multi_parameter_attributes = []
- nested_parameter_attributes = []
+ private
- attributes = sanitize_for_mass_assignment(attributes)
+ def _assign_attributes(attributes) # :nodoc:
+ multi_parameter_attributes = {}
+ nested_parameter_attributes = {}
attributes.each do |k, v|
if k.include?("(")
- multi_parameter_attributes << [ k, v ]
+ multi_parameter_attributes[k] = attributes.delete(k)
elsif v.is_a?(Hash)
- nested_parameter_attributes << [ k, v ]
- else
- _assign_attribute(k, v)
+ nested_parameter_attributes[k] = attributes.delete(k)
end
end
+ super(attributes)
assign_nested_parameter_attributes(nested_parameter_attributes) unless nested_parameter_attributes.empty?
assign_multiparameter_attributes(multi_parameter_attributes) unless multi_parameter_attributes.empty?
end
- alias attributes= assign_attributes
-
- private
-
- def _assign_attribute(k, v)
- public_send("#{k}=", v)
- rescue NoMethodError
- if respond_to?("#{k}=")
- raise
- else
- raise UnknownAttributeError.new(self, k)
- end
+ # Tries to assign given value to given attribute.
+ # In case of an error, re-raises with the ActiveRecord constant.
+ def _assign_attribute(k, v) # :nodoc:
+ super
+ rescue ActiveModel::UnknownAttributeError
+ raise UnknownAttributeError.new(self, k)
end
# Assign any deferred nested attributes after the base attributes have been set.
@@ -81,13 +58,18 @@ module ActiveRecord
errors = []
callstack.each do |name, values_with_empty_parameters|
begin
- send("#{name}=", MultiparameterAttribute.new(self, name, values_with_empty_parameters).read_value)
+ if values_with_empty_parameters.each_value.all?(&:nil?)
+ values = nil
+ else
+ values = values_with_empty_parameters
+ end
+ send("#{name}=", values)
rescue => ex
errors << AttributeAssignmentError.new("error on assignment #{values_with_empty_parameters.values.inspect} to #{name} (#{ex.message})", ex, name)
end
end
unless errors.empty?
- error_descriptions = errors.map { |ex| ex.message }.join(",")
+ error_descriptions = errors.map(&:message).join(",")
raise MultiparameterAssignmentErrors.new(errors), "#{errors.size} error(s) on assignment of multiparameter attributes [#{error_descriptions}]"
end
end
@@ -113,100 +95,5 @@ module ActiveRecord
def find_parameter_position(multiparameter_name)
multiparameter_name.scan(/\(([0-9]*).*\)/).first.first.to_i
end
-
- class MultiparameterAttribute #:nodoc:
- attr_reader :object, :name, :values, :cast_type
-
- def initialize(object, name, values)
- @object = object
- @name = name
- @values = values
- end
-
- def read_value
- return if values.values.compact.empty?
-
- @cast_type = object.type_for_attribute(name)
- klass = cast_type.klass
-
- if klass == Time
- read_time
- elsif klass == Date
- read_date
- else
- read_other
- end
- end
-
- private
-
- def instantiate_time_object(set_values)
- if object.class.send(:create_time_zone_conversion_attribute?, name, cast_type)
- Time.zone.local(*set_values)
- else
- Time.send(object.class.default_timezone, *set_values)
- end
- end
-
- def read_time
- # If column is a :time (and not :date or :datetime) there is no need to validate if
- # there are year/month/day fields
- if cast_type.type == :time
- # if the column is a time set the values to their defaults as January 1, 1970, but only if they're nil
- { 1 => 1970, 2 => 1, 3 => 1 }.each do |key,value|
- values[key] ||= value
- end
- else
- # else column is a timestamp, so if Date bits were not provided, error
- validate_required_parameters!([1,2,3])
-
- # If Date bits were provided but blank, then return nil
- return if blank_date_parameter?
- end
-
- max_position = extract_max_param(6)
- set_values = values.values_at(*(1..max_position))
- # If Time bits are not there, then default to 0
- (3..5).each { |i| set_values[i] = set_values[i].presence || 0 }
- instantiate_time_object(set_values)
- end
-
- def read_date
- return if blank_date_parameter?
- set_values = values.values_at(1,2,3)
- begin
- Date.new(*set_values)
- rescue ArgumentError # if Date.new raises an exception on an invalid date
- instantiate_time_object(set_values).to_date # we instantiate Time object and convert it back to a date thus using Time's logic in handling invalid dates
- end
- end
-
- def read_other
- max_position = extract_max_param
- positions = (1..max_position)
- validate_required_parameters!(positions)
-
- values.slice(*positions)
- end
-
- # Checks whether some blank date parameter exists. Note that this is different
- # than the validate_required_parameters! method, since it just checks for blank
- # positions instead of missing ones, and does not raise in case one blank position
- # exists. The caller is responsible to handle the case of this returning true.
- def blank_date_parameter?
- (1..3).any? { |position| values[position].blank? }
- end
-
- # If some position is not provided, it errors out a missing parameter exception.
- def validate_required_parameters!(positions)
- if missing_parameter = positions.detect { |position| !values.key?(position) }
- raise ArgumentError.new("Missing Parameter - #{name}(#{missing_parameter})")
- end
- end
-
- def extract_max_param(upper_cap = 100)
- [values.keys.max, upper_cap].min
- end
- end
end
end
diff --git a/activerecord/lib/active_record/attribute_decorators.rb b/activerecord/lib/active_record/attribute_decorators.rb
index 5b96623b6e..7d0ae32411 100644
--- a/activerecord/lib/active_record/attribute_decorators.rb
+++ b/activerecord/lib/active_record/attribute_decorators.rb
@@ -15,7 +15,7 @@ module ActiveRecord
end
def decorate_matching_attribute_types(matcher, decorator_name, &block)
- clear_caches_calculated_from_columns
+ reload_schema_from_cache
decorator_name = decorator_name.to_s
# Create new hashes so we don't modify parent classes
@@ -24,10 +24,11 @@ module ActiveRecord
private
- def add_user_provided_columns(*)
- super.map do |column|
- decorated_type = attribute_type_decorations.apply(column.name, column.cast_type)
- column.with_type(decorated_type)
+ def load_schema!
+ super
+ attribute_types.each do |name, type|
+ decorated_type = attribute_type_decorations.apply(name, type)
+ define_attribute(name, decorated_type)
end
end
end
diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb
index a2bb78dfcc..cbdd4950a6 100644
--- a/activerecord/lib/active_record/attribute_methods.rb
+++ b/activerecord/lib/active_record/attribute_methods.rb
@@ -1,6 +1,7 @@
require 'active_support/core_ext/enumerable'
+require 'active_support/core_ext/string/filters'
require 'mutex_m'
-require 'thread_safe'
+require 'concurrent'
module ActiveRecord
# = Active Record Attribute Methods
@@ -31,17 +32,17 @@ module ActiveRecord
end
}
- BLACKLISTED_CLASS_METHODS = %w(private public protected)
+ BLACKLISTED_CLASS_METHODS = %w(private public protected allocate new name parent superclass)
class AttributeMethodCache
def initialize
@module = Module.new
- @method_cache = ThreadSafe::Cache.new
+ @method_cache = Concurrent::Map.new
end
def [](name)
@method_cache.compute_if_absent(name) do
- safe_name = name.unpack('h*').first
+ safe_name = name.unpack('h*'.freeze).first
temp_method = "__temp__#{safe_name}"
ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name
@module.module_eval method_body(temp_method, safe_name), __FILE__, __LINE__
@@ -57,6 +58,8 @@ module ActiveRecord
end
end
+ class GeneratedAttributeMethods < Module; end # :nodoc:
+
module ClassMethods
def inherited(child_class) #:nodoc:
child_class.initialize_generated_modules
@@ -64,9 +67,11 @@ module ActiveRecord
end
def initialize_generated_modules # :nodoc:
- @generated_attribute_methods = Module.new { extend Mutex_m }
+ @generated_attribute_methods = GeneratedAttributeMethods.new { extend Mutex_m }
@attribute_methods_generated = false
include @generated_attribute_methods
+
+ super
end
# Generates all the attribute related methods for columns in the database
@@ -78,7 +83,7 @@ module ActiveRecord
generated_attribute_methods.synchronize do
return false if @attribute_methods_generated
superclass.define_attribute_methods unless self == base_class
- super(column_names)
+ super(attribute_names)
@attribute_methods_generated = true
end
true
@@ -86,12 +91,12 @@ module ActiveRecord
def undefine_attribute_methods # :nodoc:
generated_attribute_methods.synchronize do
- super if @attribute_methods_generated
+ super if defined?(@attribute_methods_generated) && @attribute_methods_generated
@attribute_methods_generated = false
end
end
- # Raises a <tt>ActiveRecord::DangerousAttributeError</tt> exception when an
+ # Raises an ActiveRecord::DangerousAttributeError exception when an
# \Active \Record method is defined in the model, otherwise +false+.
#
# class Person < ActiveRecord::Base
@@ -101,22 +106,23 @@ module ActiveRecord
# end
#
# Person.instance_method_already_implemented?(:save)
- # # => ActiveRecord::DangerousAttributeError: save is defined by ActiveRecord
+ # # => ActiveRecord::DangerousAttributeError: save is defined by Active Record. Check to make sure that you don't have an attribute or method with the same name.
#
# Person.instance_method_already_implemented?(:name)
# # => false
def instance_method_already_implemented?(method_name)
if dangerous_attribute_method?(method_name)
- raise DangerousAttributeError, "#{method_name} is defined by Active Record"
+ raise DangerousAttributeError, "#{method_name} is defined by Active Record. Check to make sure that you don't have an attribute or method with the same name."
end
if superclass == Base
super
else
- # If B < A and A defines its own attribute method, then we don't want to overwrite that.
- defined = method_defined_within?(method_name, superclass, superclass.generated_attribute_methods)
- base_defined = Base.method_defined?(method_name) || Base.private_method_defined?(method_name)
- defined && !base_defined || super
+ # If ThisClass < ... < SomeSuperClass < ... < Base and SomeSuperClass
+ # defines its own attribute method, then we don't want to overwrite that.
+ defined = method_defined_within?(method_name, superclass, Base) &&
+ ! superclass.instance_method(method_name).owner.is_a?(GeneratedAttributeMethods)
+ defined || super
end
end
@@ -144,7 +150,7 @@ module ActiveRecord
BLACKLISTED_CLASS_METHODS.include?(method_name.to_s) || class_method_defined_within?(method_name, Base)
end
- def class_method_defined_within?(name, klass, superklass = klass.superclass) # :nodoc
+ def class_method_defined_within?(name, klass, superklass = klass.superclass) # :nodoc:
if klass.respond_to?(name, true)
if superklass.respond_to?(name, true)
klass.method(name).owner != superklass.method(name).owner
@@ -179,14 +185,15 @@ module ActiveRecord
# # => ["id", "created_at", "updated_at", "name", "age"]
def attribute_names
@attribute_names ||= if !abstract_class? && table_exists?
- column_names
+ attribute_types.keys
else
[]
end
end
# Returns the column object for the named attribute.
- # Returns nil if the named attribute does not exist.
+ # Returns a +ActiveRecord::ConnectionAdapters::NullColumn+ if the
+ # named attribute does not exist.
#
# class Person < ActiveRecord::Base
# end
@@ -196,23 +203,18 @@ module ActiveRecord
# # => #<ActiveRecord::ConnectionAdapters::Column:0x007ff4ab083980 @name="name", @sql_type="varchar(255)", @null=true, ...>
#
# person.column_for_attribute(:nothing)
- # # => nil
+ # # => #<ActiveRecord::ConnectionAdapters::NullColumn:0xXXX @name=nil, @sql_type=nil, @cast_type=#<Type::Value>, ...>
def column_for_attribute(name)
- column = columns_hash[name.to_s]
- if column.nil?
- ActiveSupport::Deprecation.warn(<<-MESSAGE.strip_heredoc)
- `column_for_attribute` will return a null object for non-existent columns
- in Rails 5.0. Use `has_attribute?` if you need to check for an
- attribute's existence.
- MESSAGE
+ name = name.to_s
+ columns_hash.fetch(name) do
+ ConnectionAdapters::NullColumn.new(name)
end
- column
end
end
# A Person object with a name attribute can ask <tt>person.respond_to?(:name)</tt>,
# <tt>person.respond_to?(:name=)</tt>, and <tt>person.respond_to?(:name?)</tt>
- # which will all return +true+. It also define the attribute methods if they have
+ # which will all return +true+. It also defines the attribute methods if they have
# not been generated.
#
# class Person < ActiveRecord::Base
@@ -228,7 +230,15 @@ module ActiveRecord
# person.respond_to(:nothing) # => false
def respond_to?(name, include_private = false)
return false unless super
- name = name.to_s
+
+ case name
+ when :to_partial_path
+ name = "to_partial_path".freeze
+ when :to_model
+ name = "to_model".freeze
+ else
+ name = name.to_s
+ end
# If the result is true then check for the select case.
# For queries selecting a subset of columns, return false for unselected columns.
@@ -279,9 +289,9 @@ module ActiveRecord
end
# Returns an <tt>#inspect</tt>-like string for the value of the
- # attribute +attr_name+. String attributes are truncated upto 50
+ # attribute +attr_name+. String attributes are truncated up to 50
# characters, Date and Time attributes are returned in the
- # <tt>:db</tt> format, Array attributes are truncated upto 10 values.
+ # <tt>:db</tt> format, Array attributes are truncated up to 10 values.
# Other attributes return the value of <tt>#inspect</tt> without
# modification.
#
@@ -326,7 +336,7 @@ module ActiveRecord
# task.attribute_present?(:title) # => true
# task.attribute_present?(:is_done) # => true
def attribute_present?(attribute)
- value = read_attribute(attribute)
+ value = _read_attribute(attribute)
!value.nil? && !(value.respond_to?(:empty?) && value.empty?)
end
@@ -336,7 +346,7 @@ module ActiveRecord
#
# Note: +:id+ is always present.
#
- # Alias for the <tt>read_attribute</tt> method.
+ # Alias for the #read_attribute method.
#
# class Person < ActiveRecord::Base
# belongs_to :organization
@@ -354,7 +364,7 @@ module ActiveRecord
end
# Updates the attribute identified by <tt>attr_name</tt> with the specified +value+.
- # (Alias for the protected <tt>write_attribute</tt> method).
+ # (Alias for the protected #write_attribute method).
#
# class Person < ActiveRecord::Base
# end
@@ -367,6 +377,39 @@ module ActiveRecord
write_attribute(attr_name, value)
end
+ # Returns the name of all database fields which have been read from this
+ # model. This can be useful in development mode to determine which fields
+ # need to be selected. For performance critical pages, selecting only the
+ # required fields can be an easy performance win (assuming you aren't using
+ # all of the fields on the model).
+ #
+ # For example:
+ #
+ # class PostsController < ActionController::Base
+ # after_action :print_accessed_fields, only: :index
+ #
+ # def index
+ # @posts = Post.all
+ # end
+ #
+ # private
+ #
+ # def print_accessed_fields
+ # p @posts.first.accessed_fields
+ # end
+ # end
+ #
+ # Which allows you to quickly change your code to:
+ #
+ # class PostsController < ActionController::Base
+ # def index
+ # @posts = Post.select(:id, :title, :author_id, :updated_at)
+ # end
+ # end
+ def accessed_fields
+ @attributes.accessed
+ end
+
protected
def clone_attribute_value(reader_method, attribute_name) # :nodoc:
@@ -427,7 +470,7 @@ module ActiveRecord
end
def typecasted_attribute_value(name)
- read_attribute(name)
+ _read_attribute(name)
end
end
end
diff --git a/activerecord/lib/active_record/attribute_methods/before_type_cast.rb b/activerecord/lib/active_record/attribute_methods/before_type_cast.rb
index fd61febd57..1db6776688 100644
--- a/activerecord/lib/active_record/attribute_methods/before_type_cast.rb
+++ b/activerecord/lib/active_record/attribute_methods/before_type_cast.rb
@@ -2,7 +2,7 @@ module ActiveRecord
module AttributeMethods
# = Active Record Attribute Methods Before Type Cast
#
- # <tt>ActiveRecord::AttributeMethods::BeforeTypeCast</tt> provides a way to
+ # ActiveRecord::AttributeMethods::BeforeTypeCast provides a way to
# read the value of the attributes before typecasting and deserialization.
#
# class Task < ActiveRecord::Base
@@ -28,6 +28,7 @@ module ActiveRecord
included do
attribute_method_suffix "_before_type_cast"
+ attribute_method_suffix "_came_from_user?"
end
# Returns the value of the attribute identified by +attr_name+ before
@@ -66,6 +67,10 @@ module ActiveRecord
def attribute_before_type_cast(attribute_name)
read_attribute_before_type_cast(attribute_name)
end
+
+ def attribute_came_from_user?(attribute_name)
+ @attributes[attribute_name].came_from_user?
+ end
end
end
end
diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb
index b58295a106..0bcfa5f00d 100644
--- a/activerecord/lib/active_record/attribute_methods/dirty.rb
+++ b/activerecord/lib/active_record/attribute_methods/dirty.rb
@@ -1,4 +1,5 @@
require 'active_support/core_ext/module/attribute_accessors'
+require 'active_record/attribute_mutation_tracker'
module ActiveRecord
module AttributeMethods
@@ -34,84 +35,84 @@ module ActiveRecord
# <tt>reload</tt> the record and clears changed attributes.
def reload(*)
super.tap do
- clear_changes_information
+ @mutation_tracker = nil
+ @previous_mutation_tracker = nil
+ @changed_attributes = HashWithIndifferentAccess.new
end
end
def initialize_dup(other) # :nodoc:
super
- calculate_changes_from_defaults
+ @attributes = self.class._default_attributes.map do |attr|
+ attr.with_value_from_user(@attributes.fetch_value(attr.name))
+ end
+ @mutation_tracker = nil
end
- def changed?
- super || changed_in_place.any?
+ def changes_applied
+ @previous_mutation_tracker = mutation_tracker
+ @changed_attributes = HashWithIndifferentAccess.new
+ store_original_attributes
end
- def changed
- super | changed_in_place
+ def clear_changes_information
+ @previous_mutation_tracker = nil
+ @changed_attributes = HashWithIndifferentAccess.new
+ store_original_attributes
end
- def attribute_changed?(attr_name, options = {})
+ def raw_write_attribute(attr_name, *)
result = super
- # We can't change "from" something in place. Only setters can define
- # "from" and "to"
- result ||= changed_in_place?(attr_name) unless options.key?(:from)
+ clear_attribute_change(attr_name)
result
end
- def changes_applied
+ def clear_attribute_changes(attr_names)
super
- store_original_raw_attributes
+ attr_names.each do |attr_name|
+ clear_attribute_change(attr_name)
+ end
end
- def clear_changes_information
- super
- original_raw_attributes.clear
+ def changed_attributes
+ # 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
- private
-
- def calculate_changes_from_defaults
- @changed_attributes = nil
- self.class.column_defaults.each do |attr, orig_value|
- changed_attributes[attr] = orig_value if _field_changed?(attr, orig_value)
+ def changes
+ cache_changed_attributes do
+ super
end
end
- # Wrap write_attribute to remember original attribute value.
- def write_attribute(attr, value)
- attr = attr.to_s
-
- old_value = old_attribute_value(attr)
+ def previous_changes
+ previous_mutation_tracker.changes
+ end
- result = super
- store_original_raw_attribute(attr)
- save_changed_attribute(attr, old_value)
- result
+ def attribute_changed_in_place?(attr_name)
+ mutation_tracker.changed_in_place?(attr_name)
end
- def raw_write_attribute(attr, value)
- attr = attr.to_s
+ private
- result = super
- original_raw_attributes[attr] = value
- result
+ def mutation_tracker
+ unless defined?(@mutation_tracker)
+ @mutation_tracker = nil
+ end
+ @mutation_tracker ||= AttributeMutationTracker.new(@attributes)
end
- def save_changed_attribute(attr, old_value)
- if attribute_changed?(attr)
- changed_attributes.delete(attr) unless _field_changed?(attr, old_value)
- else
- changed_attributes[attr] = old_value if _field_changed?(attr, old_value)
- end
+ def changes_include?(attr_name)
+ super || mutation_tracker.changed?(attr_name)
end
- def old_attribute_value(attr)
- if attribute_changed?(attr)
- changed_attributes[attr]
- else
- clone_attribute_value(:read_attribute, attr)
- end
+ def clear_attribute_change(attr_name)
+ mutation_tracker.forget_change(attr_name)
end
def _update_record(*)
@@ -122,45 +123,28 @@ module ActiveRecord
partial_writes? ? super(keys_for_partial_write) : super
end
- # Serialized attributes should always be written in case they've been
- # changed in place.
def keys_for_partial_write
- changed
- end
-
- def _field_changed?(attr, old_value)
- @attributes[attr].changed_from?(old_value)
- end
-
- def changed_in_place
- self.class.attribute_names.select do |attr_name|
- changed_in_place?(attr_name)
- end
+ changed & self.class.column_names
end
- def changed_in_place?(attr_name)
- old_value = original_raw_attribute(attr_name)
- @attributes[attr_name].changed_in_place_from?(old_value)
+ def store_original_attributes
+ @attributes = @attributes.map(&:forgetting_assignment)
+ @mutation_tracker = nil
end
- def original_raw_attribute(attr_name)
- original_raw_attributes.fetch(attr_name) do
- read_attribute_before_type_cast(attr_name)
- end
- end
-
- def original_raw_attributes
- @original_raw_attributes ||= {}
+ def previous_mutation_tracker
+ @previous_mutation_tracker ||= NullMutationTracker.instance
end
- def store_original_raw_attribute(attr_name)
- original_raw_attributes[attr_name] = @attributes[attr_name].value_for_database
+ def cache_changed_attributes
+ @cached_changed_attributes = changed_attributes
+ yield
+ ensure
+ clear_changed_attributes_cache
end
- def store_original_raw_attributes
- attribute_names.each do |attr|
- store_original_raw_attribute(attr)
- end
+ def clear_changed_attributes_cache
+ remove_instance_variable(:@cached_changed_attributes) if defined?(@cached_changed_attributes)
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 cadad60ddd..0d5cb8b37c 100644
--- a/activerecord/lib/active_record/attribute_methods/primary_key.rb
+++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb
@@ -5,7 +5,7 @@ module ActiveRecord
module PrimaryKey
extend ActiveSupport::Concern
- # Returns this record's primary key value wrapped in an Array if one is
+ # Returns this record's primary key value wrapped in an array if one is
# available.
def to_key
sync_with_transaction_state
@@ -17,7 +17,7 @@ module ActiveRecord
def id
if pk = self.class.primary_key
sync_with_transaction_state
- read_attribute(pk)
+ _read_attribute(pk)
end
end
@@ -39,6 +39,12 @@ module ActiveRecord
read_attribute_before_type_cast(self.class.primary_key)
end
+ # Returns the primary key previous value.
+ def id_was
+ sync_with_transaction_state
+ attribute_was(self.class.primary_key)
+ end
+
protected
def attribute_method?(attr_name)
@@ -54,7 +60,7 @@ module ActiveRecord
end
end
- ID_ATTRIBUTE_METHODS = %w(id id= id? id_before_type_cast).to_set
+ ID_ATTRIBUTE_METHODS = %w(id id= id? id_before_type_cast id_was).to_set
def dangerous_attribute_method?(method_name)
super && !ID_ATTRIBUTE_METHODS.include?(method_name)
@@ -102,7 +108,7 @@ module ActiveRecord
# self.primary_key = 'sysid'
# end
#
- # You can also define the +primary_key+ method yourself:
+ # You can also define the #primary_key method yourself:
#
# class Project < ActiveRecord::Base
# def self.primary_key
@@ -114,6 +120,7 @@ module ActiveRecord
def primary_key=(value)
@primary_key = value && value.to_s
@quoted_primary_key = nil
+ @attributes_builder = nil
end
end
end
diff --git a/activerecord/lib/active_record/attribute_methods/query.rb b/activerecord/lib/active_record/attribute_methods/query.rb
index 0f9723febb..10498f4322 100644
--- a/activerecord/lib/active_record/attribute_methods/query.rb
+++ b/activerecord/lib/active_record/attribute_methods/query.rb
@@ -8,7 +8,7 @@ module ActiveRecord
end
def query_attribute(attr_name)
- value = read_attribute(attr_name) { |n| missing_attribute(n, caller) }
+ value = self[attr_name]
case value
when true then true
@@ -19,10 +19,10 @@ module ActiveRecord
if Numeric === value || value !~ /[^0-9]/
!value.to_i.zero?
else
- return false if ActiveRecord::ConnectionAdapters::Column::FALSE_VALUES.include?(value)
+ return false if ActiveModel::Type::Boolean::FALSE_VALUES.include?(value)
!value.blank?
end
- elsif column.number?
+ elsif value.respond_to?(:zero?)
!value.zero?
else
!value.blank?
diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb
index 10869dfc1e..5197e21fa4 100644
--- a/activerecord/lib/active_record/attribute_methods/read.rb
+++ b/activerecord/lib/active_record/attribute_methods/read.rb
@@ -1,5 +1,3 @@
-require 'active_support/core_ext/module/method_transplanting'
-
module ActiveRecord
module AttributeMethods
module Read
@@ -27,7 +25,7 @@ module ActiveRecord
<<-EOMETHOD
def #{method_name}
name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{const_name}
- read_attribute(name) { |n| missing_attribute(n, caller) }
+ _read_attribute(name) { |n| missing_attribute(n, caller) }
end
EOMETHOD
end
@@ -36,44 +34,24 @@ module ActiveRecord
extend ActiveSupport::Concern
module ClassMethods
- [:cache_attributes, :cached_attributes, :cache_attribute?].each do |method_name|
- define_method method_name do |*|
- cached_attributes_deprecation_warning(method_name)
- true
- end
- end
-
protected
- def cached_attributes_deprecation_warning(method_name)
- ActiveSupport::Deprecation.warn(<<-MESSAGE.strip_heredoc)
- Calling `#{method_name}` is no longer necessary. All attributes are cached.
- MESSAGE
- end
-
- if Module.methods_transplantable?
- def define_method_attribute(name)
- method = ReaderMethodCache[name]
- generated_attribute_methods.module_eval { define_method name, method }
- end
- else
- def define_method_attribute(name)
- safe_name = name.unpack('h*').first
- temp_method = "__temp__#{safe_name}"
-
- ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name
+ def define_method_attribute(name)
+ safe_name = name.unpack('h*'.freeze).first
+ temp_method = "__temp__#{safe_name}"
- generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
- def #{temp_method}
- name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{safe_name}
- read_attribute(name) { |n| missing_attribute(n, caller) }
- end
- STR
+ ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name
- generated_attribute_methods.module_eval do
- alias_method name, temp_method
- undef_method temp_method
+ generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
+ def #{temp_method}
+ name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{safe_name}
+ _read_attribute(name) { |n| missing_attribute(n, caller) }
end
+ STR
+
+ generated_attribute_methods.module_eval do
+ alias_method name, temp_method
+ undef_method temp_method
end
end
end
@@ -83,15 +61,27 @@ module ActiveRecord
# to a date object, like Date.new(2004, 12, 12)).
def read_attribute(attr_name, &block)
name = attr_name.to_s
- name = self.class.primary_key if name == 'id'
- @attributes.fetch_value(name, &block)
+ name = self.class.primary_key if name == 'id'.freeze
+ _read_attribute(name, &block)
end
- private
-
- def attribute(attribute_name)
- read_attribute(attribute_name)
+ # This method exists to avoid the expensive primary_key check internally, without
+ # breaking compatibility with the read_attribute API
+ 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
+ @attributes.fetch_value(attr_name.to_s, &block)
+ end
+ else
+ def _read_attribute(attr_name) # :nodoc:
+ @attributes.fetch_value(attr_name.to_s) { |n| yield n if block_given? }
+ end
end
+
+ alias :attribute :_read_attribute
+ private :attribute
+
end
end
end
diff --git a/activerecord/lib/active_record/attribute_methods/serialization.rb b/activerecord/lib/active_record/attribute_methods/serialization.rb
index 264ce2bdfa..65978aea2a 100644
--- a/activerecord/lib/active_record/attribute_methods/serialization.rb
+++ b/activerecord/lib/active_record/attribute_methods/serialization.rb
@@ -8,11 +8,20 @@ module ActiveRecord
# object, and retrieved as the same object, then specify the name of that
# attribute using this method and it will be handled automatically. The
# serialization is done through YAML. If +class_name+ is specified, the
- # serialized object must be of that class on retrieval or
- # <tt>SerializationTypeMismatch</tt> will be raised.
+ # serialized object must be of that class on assignment and retrieval.
+ # Otherwise SerializationTypeMismatch will be raised.
#
- # A notable side effect of serialized attributes is that the model will
- # be updated on every save, even if it is not dirty.
+ # Empty objects as <tt>{}</tt>, in the case of +Hash+, or <tt>[]</tt>, in the case of
+ # +Array+, will always be persisted as null.
+ #
+ # Keep in mind that database adapters handle certain serialization tasks
+ # for you. For instance: +json+ and +jsonb+ types in PostgreSQL will be
+ # converted between JSON object/array syntax and Ruby +Hash+ or +Array+
+ # objects transparently. There is no need to use #serialize in this
+ # case.
+ #
+ # For more complex cases, such as conversion to or from your application
+ # domain objects, consider using the ActiveRecord::Attributes API.
#
# ==== Parameters
#
@@ -52,18 +61,6 @@ module ActiveRecord
Type::Serialized.new(type, coder)
end
end
-
- def serialized_attributes
- ActiveSupport::Deprecation.warn(<<-WARNING.strip_heredoc)
- `serialized_attributes` is deprecated without replacement, and will
- be removed in Rails 5.0.
- WARNING
- @serialized_attributes ||= Hash[
- columns.select { |t| t.cast_type.is_a?(Type::Serialized) }.map { |c|
- [c.name, c.cast_type.coder]
- }
- ]
- end
end
end
end
diff --git a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
index f439bd1ffe..9e693b6aee 100644
--- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
+++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
@@ -1,19 +1,27 @@
module ActiveRecord
module AttributeMethods
module TimeZoneConversion
- class TimeZoneConverter < SimpleDelegator # :nodoc:
- def type_cast_from_database(value)
+ class TimeZoneConverter < DelegateClass(Type::Value) # :nodoc:
+ def deserialize(value)
convert_time_to_time_zone(super)
end
- def type_cast_from_user(value)
+ def cast(value)
if value.is_a?(Array)
- value.map { |v| type_cast_from_user(v) }
+ value.map { |v| cast(v) }
+ elsif value.is_a?(Hash)
+ set_time_zone_without_conversion(super)
elsif value.respond_to?(:in_time_zone)
- value.in_time_zone
+ begin
+ super(user_input_in_time_zone(value)) || super
+ rescue ArgumentError
+ nil
+ end
end
end
+ private
+
def convert_time_to_time_zone(value)
if value.is_a?(Array)
value.map { |v| convert_time_to_time_zone(v) }
@@ -23,6 +31,10 @@ module ActiveRecord
value
end
end
+
+ def set_time_zone_without_conversion(value)
+ ::Time.zone.local_to_utc(value).in_time_zone
+ end
end
extend ActiveSupport::Concern
@@ -33,6 +45,9 @@ module ActiveRecord
class_attribute :skip_time_zone_conversion_for_attributes, instance_writer: false
self.skip_time_zone_conversion_for_attributes = []
+
+ class_attribute :time_zone_aware_types, instance_writer: false
+ self.time_zone_aware_types = [:datetime, :not_explicitly_configured]
end
module ClassMethods
@@ -53,9 +68,31 @@ module ActiveRecord
end
def create_time_zone_conversion_attribute?(name, cast_type)
- time_zone_aware_attributes &&
- !self.skip_time_zone_conversion_for_attributes.include?(name.to_sym) &&
- (:datetime == cast_type.type)
+ enabled_for_column = time_zone_aware_attributes &&
+ !self.skip_time_zone_conversion_for_attributes.include?(name.to_sym)
+ result = enabled_for_column &&
+ time_zone_aware_types.include?(cast_type.type)
+
+ if enabled_for_column &&
+ !result &&
+ cast_type.type == :time &&
+ time_zone_aware_types.include?(:not_explicitly_configured)
+ ActiveSupport::Deprecation.warn(<<-MESSAGE)
+ Time columns will become time zone aware in Rails 5.1. This
+ still causes `String`s to be parsed as if they were in `Time.zone`,
+ and `Time`s to be converted to `Time.zone`.
+
+ To keep the old behavior, you must add the following to your initializer:
+
+ config.active_record.time_zone_aware_types = [:datetime]
+
+ To silence this deprecation warning, add the following:
+
+ config.active_record.time_zone_aware_types << :time
+ MESSAGE
+ end
+
+ result
end
end
end
diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb
index b3c8209a74..bbf2a51a0e 100644
--- a/activerecord/lib/active_record/attribute_methods/write.rb
+++ b/activerecord/lib/active_record/attribute_methods/write.rb
@@ -1,5 +1,3 @@
-require 'active_support/core_ext/module/method_transplanting'
-
module ActiveRecord
module AttributeMethods
module Write
@@ -25,27 +23,18 @@ module ActiveRecord
module ClassMethods
protected
- if Module.methods_transplantable?
- def define_method_attribute=(name)
- method = WriterMethodCache[name]
- generated_attribute_methods.module_eval {
- define_method "#{name}=", method
- }
- end
- else
- def define_method_attribute=(name)
- safe_name = name.unpack('h*').first
- ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name
+ def define_method_attribute=(name)
+ safe_name = name.unpack('h*'.freeze).first
+ ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name
- generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
- def __temp__#{safe_name}=(value)
- name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{safe_name}
- write_attribute(name, value)
- end
- alias_method #{(name + '=').inspect}, :__temp__#{safe_name}=
- undef_method :__temp__#{safe_name}=
- STR
- end
+ generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
+ def __temp__#{safe_name}=(value)
+ name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{safe_name}
+ write_attribute(name, value)
+ end
+ alias_method #{(name + '=').inspect}, :__temp__#{safe_name}=
+ undef_method :__temp__#{safe_name}=
+ STR
end
end
@@ -56,7 +45,7 @@ module ActiveRecord
write_attribute_with_type_cast(attr_name, value, true)
end
- def raw_write_attribute(attr_name, value)
+ def raw_write_attribute(attr_name, value) # :nodoc:
write_attribute_with_type_cast(attr_name, value, false)
end
@@ -73,7 +62,7 @@ module ActiveRecord
if should_type_cast
@attributes.write_from_user(attr_name, value)
else
- @attributes.write_from_database(attr_name, value)
+ @attributes.write_cast_value(attr_name, value)
end
value
diff --git a/activerecord/lib/active_record/attribute_mutation_tracker.rb b/activerecord/lib/active_record/attribute_mutation_tracker.rb
new file mode 100644
index 0000000000..0133b4d0be
--- /dev/null
+++ b/activerecord/lib/active_record/attribute_mutation_tracker.rb
@@ -0,0 +1,70 @@
+module ActiveRecord
+ class AttributeMutationTracker # :nodoc:
+ def initialize(attributes)
+ @attributes = attributes
+ 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|
+ if changed?(attr_name)
+ result[attr_name] = [attributes[attr_name].original_value, attributes.fetch_value(attr_name)]
+ end
+ end
+ end
+
+ def changed?(attr_name)
+ attr_name = attr_name.to_s
+ attributes[attr_name].changed?
+ end
+
+ def changed_in_place?(attr_name)
+ attributes[attr_name].changed_in_place?
+ end
+
+ def forget_change(attr_name)
+ attr_name = attr_name.to_s
+ attributes[attr_name] = attributes[attr_name].forgetting_assignment
+ end
+
+ protected
+
+ attr_reader :attributes
+
+ private
+
+ def attr_names
+ attributes.keys
+ end
+ end
+
+ class NullMutationTracker # :nodoc:
+ include Singleton
+
+ def changed_values
+ {}
+ end
+
+ def changes
+ {}
+ end
+
+ def changed?(*)
+ false
+ end
+
+ def changed_in_place?(*)
+ false
+ end
+
+ def forget_change(*)
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/attribute_set.rb b/activerecord/lib/active_record/attribute_set.rb
index 98ac63c7e1..be581ac2a9 100644
--- a/activerecord/lib/active_record/attribute_set.rb
+++ b/activerecord/lib/active_record/attribute_set.rb
@@ -2,8 +2,6 @@ require 'active_record/attribute_set/builder'
module ActiveRecord
class AttributeSet # :nodoc:
- delegate :keys, to: :initialized_attributes
-
def initialize(attributes)
@attributes = attributes
end
@@ -12,6 +10,10 @@ module ActiveRecord
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
@@ -25,8 +27,20 @@ module ActiveRecord
attributes.key?(name) && self[name].initialized?
end
- def fetch_value(name, &block)
- self[name].value(&block)
+ 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)
@@ -37,13 +51,23 @@ module ActiveRecord
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
+ dup.tap do |copy|
+ copy.instance_variable_set(:@attributes, attributes.deep_dup)
+ end
+ end
+
def initialize_dup(_)
- @attributes = attributes.transform_values(&:dup)
+ @attributes = attributes.dup
super
end
@@ -58,10 +82,17 @@ module ActiveRecord
end
end
- def ensure_initialized(key)
- unless self[key].initialized?
- write_from_database(key, nil)
- 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
diff --git a/activerecord/lib/active_record/attribute_set/builder.rb b/activerecord/lib/active_record/attribute_set/builder.rb
index 1e146a07da..3bd7c7997b 100644
--- a/activerecord/lib/active_record/attribute_set/builder.rb
+++ b/activerecord/lib/active_record/attribute_set/builder.rb
@@ -1,32 +1,108 @@
+require 'active_record/attribute'
+
module ActiveRecord
class AttributeSet # :nodoc:
class Builder # :nodoc:
- attr_reader :types
+ attr_reader :types, :always_initialized
- def initialize(types)
+ def initialize(types, always_initialized = nil)
@types = types
+ @always_initialized = always_initialized
end
def build_from_database(values = {}, additional_types = {})
- attributes = build_attributes_from_values(values, additional_types)
- add_uninitialized_attributes(attributes)
+ if always_initialized && !values.key?(always_initialized)
+ values[always_initialized] = nil
+ end
+
+ attributes = LazyAttributeHash.new(types, values, additional_types)
AttributeSet.new(attributes)
end
+ end
+ end
+
+ class LazyAttributeHash # :nodoc:
+ delegate :transform_values, :each_key, to: :materialize
+
+ def initialize(types, values, additional_types)
+ @types = types
+ @values = values
+ @additional_types = additional_types
+ @materialized = false
+ @delegate_hash = {}
+ 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
- private
+ def initialize_dup(_)
+ @delegate_hash = Hash[delegate_hash]
+ super
+ end
- def build_attributes_from_values(values, additional_types)
- values.each_with_object({}) do |(name, value), hash|
- type = additional_types.fetch(name, types[name])
- hash[name] = Attribute.from_database(name, value, type)
+ 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 add_uninitialized_attributes(attributes)
- types.except(*attributes.keys).each do |name, type|
- attributes[name] = Attribute.uninitialized(name, type)
+ protected
+
+ attr_reader :types, :values, :additional_types, :delegate_hash
+
+ 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] = Attribute.uninitialized(name, type)
+ end
end
end
end
diff --git a/activerecord/lib/active_record/attributes.rb b/activerecord/lib/active_record/attributes.rb
index 890a1314d9..5d0405c3be 100644
--- a/activerecord/lib/active_record/attributes.rb
+++ b/activerecord/lib/active_record/attributes.rb
@@ -1,29 +1,44 @@
+require 'active_record/attribute/user_provided_default'
+
module ActiveRecord
- module Attributes # :nodoc:
+ # See ActiveRecord::Attributes::ClassMethods for documentation
+ module Attributes
extend ActiveSupport::Concern
- Type = ActiveRecord::Type
-
included do
- class_attribute :user_provided_columns, instance_accessor: false # :internal:
- self.user_provided_columns = {}
+ class_attribute :attributes_to_define_after_schema_loads, instance_accessor: false # :internal:
+ self.attributes_to_define_after_schema_loads = {}
end
- module ClassMethods # :nodoc:
- # Defines or overrides a attribute on this model. This allows customization of
- # Active Record's type casting behavior, as well as adding support for user defined
- # types.
+ module ClassMethods
+ # Defines an attribute with a type on this model. It will override the
+ # type of existing attributes if needed. This allows control over how
+ # values are converted to and from SQL when assigned to a model. It also
+ # changes the behavior of values passed to
+ # {ActiveRecord::Base.where}[rdoc-ref:QueryMethods#where]. This will let you use
+ # your domain objects across much of Active Record, without having to
+ # rely on implementation details or monkey patching.
#
- # +name+ The name of the methods to define attribute methods for, and the column which
- # this will persist to.
+ # +name+ The name of the methods to define attribute methods for, and the
+ # column which this will persist to.
#
- # +cast_type+ A type object that contains information about how to type cast the value.
- # See the examples section for more information.
+ # +cast_type+ A symbol such as +:string+ or +:integer+, or a type object
+ # to be used for this attribute. See the examples below for more
+ # information about providing custom type objects.
#
# ==== Options
- # The options hash accepts the following options:
#
- # +default+ is the default value that the column should use on a new record.
+ # The following options are accepted:
+ #
+ # +default+ The default value to use when no value is provided. If this option
+ # is not passed, the previous default value (if any) will be used.
+ # Otherwise, the default will be +nil+.
+ #
+ # +array+ (PG only) specifies that the type should be an array (see the
+ # examples below).
+ #
+ # +range+ (PG only) specifies that the type should be a range (see the
+ # examples below).
#
# ==== Examples
#
@@ -44,78 +59,201 @@ module ActiveRecord
# store_listing.price_in_cents # => BigDecimal.new(10.1)
#
# class StoreListing < ActiveRecord::Base
- # attribute :price_in_cents, Type::Integer.new
+ # attribute :price_in_cents, :integer
# end
#
# # after
# store_listing.price_in_cents # => 10
#
- # Users may also define their own custom types, as long as they respond to the methods
- # defined on the value type. The `type_cast` method on your type object will be called
- # with values both from the database, and from your controllers. See
- # `ActiveRecord::Attributes::Type::Value` for the expected API. It is recommended that your
- # type objects inherit from an existing type, or the base value type.
+ # A default can also be provided.
+ #
+ # create_table :store_listings, force: true do |t|
+ # t.string :my_string, default: "original default"
+ # end
+ #
+ # StoreListing.new.my_string # => "original default"
+ #
+ # class StoreListing < ActiveRecord::Base
+ # attribute :my_string, :string, default: "new default"
+ # end
+ #
+ # StoreListing.new.my_string # => "new default"
+ #
+ # class Product < ActiveRecord::Base
+ # attribute :my_default_proc, :datetime, default: -> { Time.now }
+ # end
+ #
+ # Product.new.my_default_proc # => 2015-05-30 11:04:48 -0600
+ # sleep 1
+ # Product.new.my_default_proc # => 2015-05-30 11:04:49 -0600
+ #
+ # \Attributes do not need to be backed by a database column.
+ #
+ # class MyModel < ActiveRecord::Base
+ # attribute :my_string, :string
+ # attribute :my_int_array, :integer, array: true
+ # attribute :my_float_range, :float, range: true
+ # end
+ #
+ # model = MyModel.new(
+ # my_string: "string",
+ # my_int_array: ["1", "2", "3"],
+ # my_float_range: "[1,3.5]",
+ # )
+ # model.attributes
+ # # =>
+ # {
+ # my_string: "string",
+ # my_int_array: [1, 2, 3],
+ # my_float_range: 1.0..3.5
+ # }
+ #
+ # ==== Creating Custom Types
+ #
+ # Users may also define their own custom types, as long as they respond
+ # to the methods defined on the value type. The method +deserialize+ or
+ # +cast+ will be called on your type object, with raw input from the
+ # database or from your controllers. See ActiveRecord::Type::Value for the
+ # expected API. It is recommended that your type objects inherit from an
+ # existing type, or from ActiveRecord::Type::Value
#
# class MoneyType < ActiveRecord::Type::Integer
- # def type_cast(value)
- # if value.include?('$')
+ # def cast(value)
+ # if !value.kind_of(Numeric) && value.include?('$')
# price_in_dollars = value.gsub(/\$/, '').to_f
- # price_in_dollars * 100
+ # super(price_in_dollars * 100)
# else
- # value.to_i
+ # super
# end
# end
# end
#
+ # # config/initializers/types.rb
+ # ActiveRecord::Type.register(:money, MoneyType)
+ #
+ # # /app/models/store_listing.rb
# class StoreListing < ActiveRecord::Base
- # attribute :price_in_cents, MoneyType.new
+ # attribute :price_in_cents, :money
# end
#
# store_listing = StoreListing.new(price_in_cents: '$10.00')
# store_listing.price_in_cents # => 1000
- def attribute(name, cast_type, options = {})
+ #
+ # For more details on creating custom types, see the documentation for
+ # ActiveRecord::Type::Value. For more details on registering your types
+ # to be referenced by a symbol, see ActiveRecord::Type.register. You can
+ # also pass a type object directly, in place of a symbol.
+ #
+ # ==== \Querying
+ #
+ # When {ActiveRecord::Base.where}[rdoc-ref:QueryMethods#where] is called, it will
+ # use the type defined by the model class to convert the value to SQL,
+ # calling +serialize+ on your type object. For example:
+ #
+ # class Money < Struct.new(:amount, :currency)
+ # end
+ #
+ # class MoneyType < Type::Value
+ # def initialize(currency_converter)
+ # @currency_converter = currency_converter
+ # end
+ #
+ # # value will be the result of +deserialize+ or
+ # # +cast+. Assumed to be an instance of +Money+ in
+ # # this case.
+ # def serialize(value)
+ # value_in_bitcoins = @currency_converter.convert_to_bitcoins(value)
+ # value_in_bitcoins.amount
+ # end
+ # end
+ #
+ # ActiveRecord::Type.register(:money, MoneyType)
+ #
+ # class Product < ActiveRecord::Base
+ # currency_converter = ConversionRatesFromTheInternet.new
+ # attribute :price_in_bitcoins, :money, currency_converter
+ # end
+ #
+ # Product.where(price_in_bitcoins: Money.new(5, "USD"))
+ # # => SELECT * FROM products WHERE price_in_bitcoins = 0.02230
+ #
+ # Product.where(price_in_bitcoins: Money.new(5, "GBP"))
+ # # => SELECT * FROM products WHERE price_in_bitcoins = 0.03412
+ #
+ # ==== Dirty Tracking
+ #
+ # The type of an attribute is given the opportunity to change how dirty
+ # tracking is performed. The methods +changed?+ and +changed_in_place?+
+ # will be called from ActiveModel::Dirty. See the documentation for those
+ # methods in ActiveRecord::Type::Value for more details.
+ def attribute(name, cast_type, **options)
name = name.to_s
- clear_caches_calculated_from_columns
- # Assign a new hash to ensure that subclasses do not share a hash
- self.user_provided_columns = user_provided_columns.merge(name => connection.new_column(name, options[:default], cast_type))
- end
+ reload_schema_from_cache
- # Returns an array of column objects for the table associated with this class.
- def columns
- @columns ||= add_user_provided_columns(connection.schema_cache.columns(table_name))
+ self.attributes_to_define_after_schema_loads =
+ attributes_to_define_after_schema_loads.merge(
+ name => [cast_type, options]
+ )
end
- # Returns a hash of column objects for the table associated with this class.
- def columns_hash
- @columns_hash ||= Hash[columns.map { |c| [c.name, c] }]
+ # This is the low level API which sits beneath +attribute+. It only
+ # accepts type objects, and will do its work immediately instead of
+ # waiting for the schema to load. Automatic schema detection and
+ # ClassMethods#attribute both call this under the hood. While this method
+ # is provided so it can be used by plugin authors, application code
+ # should probably use ClassMethods#attribute.
+ #
+ # +name+ The name of the attribute being defined. Expected to be a +String+.
+ #
+ # +cast_type+ The type object to use for this attribute.
+ #
+ # +default+ The default value to use when no value is provided. If this option
+ # is not passed, the previous default value (if any) will be used.
+ # Otherwise, the default will be +nil+. A proc can also be passed, and
+ # will be called once each time a new value is needed.
+ #
+ # +user_provided_default+ Whether the default value should be cast using
+ # +cast+ or +deserialize+.
+ def define_attribute(
+ name,
+ cast_type,
+ default: NO_DEFAULT_PROVIDED,
+ user_provided_default: true
+ )
+ attribute_types[name] = cast_type
+ define_default_attribute(name, default, cast_type, from_user: user_provided_default)
end
- def reset_column_information # :nodoc:
+ def load_schema! # :nodoc:
super
- clear_caches_calculated_from_columns
- end
+ attributes_to_define_after_schema_loads.each do |name, (type, options)|
+ if type.is_a?(Symbol)
+ type = ActiveRecord::Type.lookup(type, **options.except(:default))
+ end
- private
-
- def add_user_provided_columns(schema_columns)
- existing_columns = schema_columns.map do |column|
- user_provided_columns[column.name] || column
+ define_attribute(name, type, **options.slice(:default))
end
+ end
- existing_column_names = existing_columns.map(&:name)
- new_columns = user_provided_columns.except(*existing_column_names).values
+ private
- existing_columns + new_columns
- end
+ NO_DEFAULT_PROVIDED = Object.new # :nodoc:
+ private_constant :NO_DEFAULT_PROVIDED
- def clear_caches_calculated_from_columns
- @attributes_builder = nil
- @column_names = nil
- @column_types = nil
- @columns = nil
- @columns_hash = nil
- @content_columns = nil
- @default_attributes = nil
+ def define_default_attribute(name, value, type, from_user:)
+ if value == NO_DEFAULT_PROVIDED
+ default_attribute = _default_attributes[name].with_type(type)
+ elsif from_user
+ default_attribute = Attribute::UserProvidedDefault.new(
+ name,
+ value,
+ type,
+ _default_attributes[name],
+ )
+ else
+ default_attribute = Attribute.from_database(name, value, type)
+ end
+ _default_attributes[name] = default_attribute
end
end
end
diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb
index dd92e29199..fc12c3f45a 100644
--- a/activerecord/lib/active_record/autosave_association.rb
+++ b/activerecord/lib/active_record/autosave_association.rb
@@ -1,10 +1,10 @@
module ActiveRecord
# = Active Record Autosave Association
#
- # +AutosaveAssociation+ is a module that takes care of automatically saving
+ # AutosaveAssociation is a module that takes care of automatically saving
# associated records when their parent is saved. In addition to saving, it
# also destroys any associated records that were marked for destruction.
- # (See +mark_for_destruction+ and <tt>marked_for_destruction?</tt>).
+ # (See #mark_for_destruction and #marked_for_destruction?).
#
# Saving of the parent, its associations, and the destruction of marked
# associations, all happen inside a transaction. This should never leave the
@@ -125,7 +125,6 @@ module ActiveRecord
# Now it _is_ removed from the database:
#
# Comment.find_by(id: id).nil? # => true
-
module AutosaveAssociation
extend ActiveSupport::Concern
@@ -141,9 +140,11 @@ module ActiveRecord
included do
Associations::Builder::Association.extensions << AssociationBuilderExtension
+ mattr_accessor :index_nested_attribute_errors, instance_writer: false
+ self.index_nested_attribute_errors = false
end
- module ClassMethods
+ module ClassMethods # :nodoc:
private
def define_non_cyclic_method(name, &block)
@@ -177,14 +178,14 @@ module ActiveRecord
# before actually defining them.
def add_autosave_association_callbacks(reflection)
save_method = :"autosave_associated_records_for_#{reflection.name}"
- validation_method = :"validate_associated_records_for_#{reflection.name}"
- collection = reflection.collection?
- if collection
+ if reflection.collection?
before_save :before_save_collection_association
define_non_cyclic_method(save_method) { save_collection_association(reflection) }
- after_save save_method
+ # Doesn't use after_save as that would save associations added in after_create/after_update twice
+ after_create save_method
+ after_update save_method
elsif reflection.has_one?
define_method(save_method) { save_has_one_association(reflection) } unless method_defined?(save_method)
# Configures two callbacks instead of a single after_save so that
@@ -198,14 +199,31 @@ module ActiveRecord
after_create save_method
after_update save_method
else
- define_non_cyclic_method(save_method) { save_belongs_to_association(reflection) }
+ define_non_cyclic_method(save_method) { throw(:abort) if save_belongs_to_association(reflection) == false }
before_save save_method
end
+ define_autosave_validation_callbacks(reflection)
+ end
+
+ def define_autosave_validation_callbacks(reflection)
+ validation_method = :"validate_associated_records_for_#{reflection.name}"
if reflection.validate? && !method_defined?(validation_method)
- method = (collection ? :validate_collection_association : :validate_single_association)
- define_non_cyclic_method(validation_method) { send(method, reflection) }
+ if reflection.collection?
+ method = :validate_collection_association
+ else
+ method = :validate_single_association
+ end
+
+ define_non_cyclic_method(validation_method) do
+ send(method, reflection)
+ # TODO: remove the following line as soon as the return value of
+ # callbacks is ignored, that is, returning `false` does not
+ # display a deprecation warning or halts the callback chain.
+ true
+ end
validate validation_method
+ after_validation :_ensure_no_duplicate_errors
end
end
end
@@ -217,7 +235,7 @@ module ActiveRecord
super
end
- # Marks this record to be destroyed as part of the parents save transaction.
+ # Marks this record to be destroyed as part of the parent's save transaction.
# This does _not_ actually destroy the record instantly, rather child record will be destroyed
# when <tt>parent.save</tt> is called.
#
@@ -226,7 +244,7 @@ module ActiveRecord
@marked_for_destruction = true
end
- # Returns whether or not this record will be destroyed as part of the parents save transaction.
+ # Returns whether or not this record will be destroyed as part of the parent's save transaction.
#
# Only useful if the <tt>:autosave</tt> option on the parent is enabled for this associated model.
def marked_for_destruction?
@@ -261,20 +279,27 @@ module ActiveRecord
if new_record
association && association.target
elsif autosave
- association.target.find_all { |record| record.changed_for_autosave? }
+ association.target.find_all(&:changed_for_autosave?)
else
- association.target.find_all { |record| record.new_record? }
+ association.target.find_all(&:new_record?)
end
end
# go through nested autosave associations that are loaded in memory (without loading
# any new ones), and return true if is changed for autosave
def nested_records_changed_for_autosave?
- self.class._reflections.values.any? do |reflection|
- if reflection.options[:autosave]
- association = association_instance_get(reflection.name)
- association && Array.wrap(association.target).any? { |a| a.changed_for_autosave? }
+ @_nested_records_changed_for_autosave_already_called ||= false
+ return false if @_nested_records_changed_for_autosave_already_called
+ begin
+ @_nested_records_changed_for_autosave_already_called = true
+ self.class._reflections.values.any? do |reflection|
+ if reflection.options[:autosave]
+ association = association_instance_get(reflection.name)
+ association && Array.wrap(association.target).any?(&:changed_for_autosave?)
+ end
end
+ ensure
+ @_nested_records_changed_for_autosave_already_called = false
end
end
@@ -292,7 +317,7 @@ module ActiveRecord
def validate_collection_association(reflection)
if association = association_instance_get(reflection.name)
if records = associated_records_to_validate_or_save(association, new_record?, reflection.options[:autosave])
- records.each { |record| association_valid?(reflection, record) }
+ records.each_with_index { |record, index| association_valid?(reflection, record, index) }
end
end
end
@@ -300,14 +325,18 @@ module ActiveRecord
# Returns whether or not the association is valid and applies any errors to
# the parent, <tt>self</tt>, if it wasn't. Skips any <tt>:autosave</tt>
# enabled records if they're marked_for_destruction? or destroyed.
- def association_valid?(reflection, record)
- return true if record.destroyed? || record.marked_for_destruction?
+ def association_valid?(reflection, record, index=nil)
+ return true if record.destroyed? || (reflection.options[:autosave] && record.marked_for_destruction?)
validation_context = self.validation_context unless [:create, :update].include?(self.validation_context)
unless valid = record.valid?(validation_context)
if reflection.options[:autosave]
record.errors.each do |attribute, message|
- attribute = "#{reflection.name}.#{attribute}"
+ if index.nil? || (!reflection.options[:index_errors] && !ActiveRecord::Base.index_nested_attribute_errors)
+ attribute = "#{reflection.name}.#{attribute}"
+ else
+ attribute = "#{reflection.name}[#{index}].#{attribute}"
+ end
errors[attribute] << message
errors[attribute].uniq!
end
@@ -329,7 +358,7 @@ module ActiveRecord
# <tt>:autosave</tt> is enabled on the association.
#
# In addition, it destroys all children that were marked for destruction
- # with mark_for_destruction.
+ # with #mark_for_destruction.
#
# This all happens inside a transaction, _if_ the Transactions module is included into
# ActiveRecord::Base after the AutosaveAssociation module, which it does by default.
@@ -338,7 +367,6 @@ module ActiveRecord
autosave = reflection.options[:autosave]
if records = associated_records_to_validate_or_save(association, @new_record_before_save, autosave)
-
if autosave
records_to_destroy = records.select(&:marked_for_destruction?)
records_to_destroy.each { |record| association.destroy(record) }
@@ -362,7 +390,6 @@ module ActiveRecord
raise ActiveRecord::Rollback unless saved
end
- @new_record_before_save = false
end
# reconstruct the scope now that we know the owner's id
@@ -374,7 +401,7 @@ module ActiveRecord
# on the association.
#
# In addition, it will destroy the association if it was marked for
- # destruction with mark_for_destruction.
+ # destruction with #mark_for_destruction.
#
# This all happens inside a transaction, _if_ the Transactions module is included into
# ActiveRecord::Base after the AutosaveAssociation module, which it does by default.
@@ -405,7 +432,9 @@ module ActiveRecord
# If the record is new or it has changed, returns true.
def record_changed?(reflection, record, key)
- record.new_record? || record[reflection.foreign_key] != key || record.attribute_changed?(reflection.foreign_key)
+ record.new_record? ||
+ (record.has_attribute?(reflection.foreign_key) && record[reflection.foreign_key] != key) ||
+ record.attribute_changed?(reflection.foreign_key)
end
# Saves the associated record if it's new or <tt>:autosave</tt> is enabled.
@@ -433,5 +462,11 @@ module ActiveRecord
end
end
end
+
+ def _ensure_no_duplicate_errors
+ errors.messages.each_key do |attribute|
+ errors[attribute].uniq!
+ end
+ end
end
end
diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb
index f978fbd0a4..9782e58299 100644
--- a/activerecord/lib/active_record/base.rb
+++ b/activerecord/lib/active_record/base.rb
@@ -1,11 +1,9 @@
require 'yaml'
-require 'set'
require 'active_support/benchmarkable'
require 'active_support/dependencies'
require 'active_support/descendants_tracker'
require 'active_support/time'
require 'active_support/core_ext/module/attribute_accessors'
-require 'active_support/core_ext/class/delegating_attributes'
require 'active_support/core_ext/array/extract_options'
require 'active_support/core_ext/hash/deep_merge'
require 'active_support/core_ext/hash/slice'
@@ -22,6 +20,7 @@ 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
@@ -119,29 +118,28 @@ module ActiveRecord #:nodoc:
# All column values are automatically available through basic accessors on the Active Record
# object, but sometimes you want to specialize this behavior. This can be done by overwriting
# the default accessors (using the same name as the attribute) and calling
- # <tt>read_attribute(attr_name)</tt> and <tt>write_attribute(attr_name, value)</tt> to actually
- # change things.
+ # +super+ to actually change things.
#
# class Song < ActiveRecord::Base
# # Uses an integer of seconds to hold the length of the song
#
# def length=(minutes)
- # write_attribute(:length, minutes.to_i * 60)
+ # super(minutes.to_i * 60)
# end
#
# def length
- # read_attribute(:length) / 60
+ # super / 60
# end
# end
#
# You can alternatively use <tt>self[:attribute]=(value)</tt> and <tt>self[:attribute]</tt>
- # instead of <tt>write_attribute(:attribute, value)</tt> and <tt>read_attribute(:attribute)</tt>.
+ # or <tt>write_attribute(:attribute, value)</tt> and <tt>read_attribute(:attribute)</tt>.
#
# == Attribute query methods
#
# In addition to the basic accessors, query methods are also automatically available on the Active Record object.
# Query methods allow you to test whether an attribute value is present.
- # For numeric values, present is defined as non-zero.
+ # Additionally, when dealing with numeric values, a query method will return false if the value is zero.
#
# For example, an Active Record User with the <tt>name</tt> attribute has a <tt>name?</tt> method that you can call
# to determine whether the user has a name:
@@ -172,7 +170,7 @@ module ActiveRecord #:nodoc:
# <tt>Person.find_by_user_name(user_name)</tt>.
#
# It's possible to add an exclamation point (!) on the end of the dynamic finders to get them to raise an
- # <tt>ActiveRecord::RecordNotFound</tt> error if they do not return any records,
+ # ActiveRecord::RecordNotFound error if they do not return any records,
# like <tt>Person.find_by_last_name!</tt>.
#
# It's also possible to use multiple attributes in the same find by separating them with "_and_".
@@ -187,7 +185,8 @@ module ActiveRecord #:nodoc:
# == Saving arrays, hashes, and other non-mappable objects in text columns
#
# Active Record can serialize any object in text columns using YAML. To do so, you must
- # specify this with a call to the class method +serialize+.
+ # specify this with a call to the class method
+ # {serialize}[rdoc-ref:AttributeMethods::Serialization::ClassMethods#serialize].
# This makes it possible to store arrays, hashes, and other non-mappable objects without doing
# any additional work.
#
@@ -227,39 +226,47 @@ module ActiveRecord #:nodoc:
#
# == Connection to multiple databases in different models
#
- # Connections are usually created through ActiveRecord::Base.establish_connection and retrieved
+ # Connections are usually created through
+ # {ActiveRecord::Base.establish_connection}[rdoc-ref:ConnectionHandling#establish_connection] and retrieved
# by ActiveRecord::Base.connection. All classes inheriting from ActiveRecord::Base will use this
# connection. But you can also set a class-specific connection. For example, if Course is an
# ActiveRecord::Base, but resides in a different database, you can just say <tt>Course.establish_connection</tt>
# and Course and all of its subclasses will use this connection instead.
#
# This feature is implemented by keeping a connection pool in ActiveRecord::Base that is
- # a Hash indexed by the class. If a connection is requested, the retrieve_connection method
+ # a hash indexed by the class. If a connection is requested, the
+ # {ActiveRecord::Base.retrieve_connection}[rdoc-ref:ConnectionHandling#retrieve_connection] method
# will go up the class-hierarchy until a connection is found in the connection pool.
#
# == Exceptions
#
# * ActiveRecordError - Generic error class and superclass of all other errors raised by Active Record.
- # * AdapterNotSpecified - The configuration hash used in <tt>establish_connection</tt> didn't include an
- # <tt>:adapter</tt> key.
- # * AdapterNotFound - The <tt>:adapter</tt> key used in <tt>establish_connection</tt> specified a
- # non-existent adapter
+ # * AdapterNotSpecified - The configuration hash used in
+ # {ActiveRecord::Base.establish_connection}[rdoc-ref:ConnectionHandling#establish_connection]
+ # didn't include an <tt>:adapter</tt> key.
+ # * AdapterNotFound - The <tt>:adapter</tt> key used in
+ # {ActiveRecord::Base.establish_connection}[rdoc-ref:ConnectionHandling#establish_connection]
+ # specified a non-existent adapter
# (or a bad spelling of an existing one).
# * AssociationTypeMismatch - The object assigned to the association wasn't of the type
# specified in the association definition.
# * AttributeAssignmentError - An error occurred while doing a mass assignment through the
- # <tt>attributes=</tt> method.
+ # {ActiveRecord::Base#attributes=}[rdoc-ref:AttributeAssignment#attributes=] method.
# You can inspect the +attribute+ property of the exception object to determine which attribute
# triggered the error.
- # * ConnectionNotEstablished - No connection has been established. Use <tt>establish_connection</tt>
- # before querying.
+ # * ConnectionNotEstablished - No connection has been established.
+ # Use {ActiveRecord::Base.establish_connection}[rdoc-ref:ConnectionHandling#establish_connection] before querying.
# * MultiparameterAssignmentErrors - Collection of errors that occurred during a mass assignment using the
- # <tt>attributes=</tt> method. The +errors+ property of this exception contains an array of
+ # {ActiveRecord::Base#attributes=}[rdoc-ref:AttributeAssignment#attributes=] method.
+ # The +errors+ property of this exception contains an array of
# AttributeAssignmentError
# objects that should be inspected to determine which attributes triggered the errors.
- # * RecordInvalid - raised by save! and create! when the record is invalid.
- # * RecordNotFound - No record responded to the +find+ method. Either the row with the given ID doesn't exist
- # or the row didn't meet the additional restrictions. Some +find+ calls do not raise this exception to signal
+ # * RecordInvalid - raised by {ActiveRecord::Base#save}[rdoc-ref:Persistence#save] and
+ # {ActiveRecord::Base.create!}[rdoc-ref:Persistence::ClassMethods#create!]
+ # when the record is invalid.
+ # * RecordNotFound - No record responded to the {ActiveRecord::Base.find}[rdoc-ref:FinderMethods#find] method.
+ # Either the row with the given ID doesn't exist or the row didn't meet the additional restrictions.
+ # Some {ActiveRecord::Base.find}[rdoc-ref:FinderMethods#find] calls do not raise this exception to signal
# nothing was found, please check its documentation for further details.
# * SerializationTypeMismatch - The serialized object wasn't of the class specified as the second parameter.
# * StatementInvalid - The database server rejected the SQL statement. The precise error is added in the message.
@@ -281,6 +288,7 @@ module ActiveRecord #:nodoc:
extend Explain
extend Enum
extend Delegation::DelegateCache
+ extend CollectionCacheKey
include Core
include Persistence
@@ -308,9 +316,12 @@ module ActiveRecord #:nodoc:
include Aggregations
include Transactions
include NoTouching
+ include TouchLater
include Reflection
include Serialization
include Store
+ include SecureToken
+ include Suppressor
end
ActiveSupport.run_load_hooks(:active_record, Base)
diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb
index 5955673b42..cfd8cbda67 100644
--- a/activerecord/lib/active_record/callbacks.rb
+++ b/activerecord/lib/active_record/callbacks.rb
@@ -1,11 +1,11 @@
module ActiveRecord
- # = Active Record Callbacks
+ # = Active Record \Callbacks
#
- # Callbacks are hooks into the life cycle of an Active Record object that allow you to trigger logic
+ # \Callbacks are hooks into the life cycle of an Active Record object that allow you to trigger logic
# before or after an alteration of the object state. This can be used to make sure that associated and
- # dependent objects are deleted when +destroy+ is called (by overwriting +before_destroy+) or to massage attributes
- # before they're validated (by overwriting +before_validation+). As an example of the callbacks initiated, consider
- # the <tt>Base#save</tt> call for a new record:
+ # dependent objects are deleted when {ActiveRecord::Base#destroy}[rdoc-ref:Persistence#destroy] is called (by overwriting +before_destroy+) or
+ # to massage attributes before they're validated (by overwriting +before_validation+).
+ # As an example of the callbacks initiated, consider the {ActiveRecord::Base#save}[rdoc-ref:Persistence#save] call for a new record:
#
# * (-) <tt>save</tt>
# * (-) <tt>valid</tt>
@@ -20,7 +20,7 @@ module ActiveRecord
# * (7) <tt>after_commit</tt>
#
# Also, an <tt>after_rollback</tt> callback can be configured to be triggered whenever a rollback is issued.
- # Check out <tt>ActiveRecord::Transactions</tt> for more details about <tt>after_commit</tt> and
+ # Check out ActiveRecord::Transactions for more details about <tt>after_commit</tt> and
# <tt>after_rollback</tt>.
#
# Additionally, an <tt>after_touch</tt> callback is triggered whenever an
@@ -31,7 +31,7 @@ module ActiveRecord
# are instantiated as well.
#
# There are nineteen callbacks in total, which give you immense power to react and prepare for each state in the
- # Active Record life cycle. The sequence for calling <tt>Base#save</tt> for an existing record is similar,
+ # Active Record life cycle. The sequence for calling {ActiveRecord::Base#save}[rdoc-ref:Persistence#save] for an existing record is similar,
# except that each <tt>_create</tt> callback is replaced by the corresponding <tt>_update</tt> callback.
#
# Examples:
@@ -192,21 +192,23 @@ module ActiveRecord
#
# == <tt>before_validation*</tt> returning statements
#
- # If the returning value of a +before_validation+ callback can be evaluated to +false+, the process will be
- # aborted and <tt>Base#save</tt> will return +false+. If Base#save! is called it will raise a
- # ActiveRecord::RecordInvalid exception. Nothing will be appended to the errors object.
+ # If the +before_validation+ callback throws +:abort+, the process will be
+ # aborted and {ActiveRecord::Base#save}[rdoc-ref:Persistence#save] will return +false+.
+ # If {ActiveRecord::Base#save!}[rdoc-ref:Persistence#save!] is called it will raise a ActiveRecord::RecordInvalid exception.
+ # Nothing will be appended to the errors object.
#
# == Canceling callbacks
#
- # If a <tt>before_*</tt> callback returns +false+, all the later callbacks and the associated action are
- # cancelled. If an <tt>after_*</tt> callback returns +false+, all the later callbacks are cancelled.
+ # If a <tt>before_*</tt> callback throws +:abort+, all the later callbacks and
+ # the associated action are cancelled.
# Callbacks are generally run in the order they are defined, with the exception of callbacks defined as
# methods on the model, which are called last.
#
# == Ordering callbacks
#
# Sometimes the code needs that the callbacks execute in a specific order. For example, a +before_destroy+
- # callback (+log_children+ in this case) should be executed before the children get destroyed by the +dependent: destroy+ option.
+ # callback (+log_children+ in this case) should be executed before the children get destroyed by the
+ # <tt>dependent: destroy</tt> option.
#
# Let's look at the code below:
#
@@ -222,7 +224,8 @@ module ActiveRecord
# end
#
# In this case, the problem is that when the +before_destroy+ callback is executed, the children are not available
- # because the +destroy+ callback gets executed first. You can use the +prepend+ option on the +before_destroy+ callback to avoid this.
+ # because the {ActiveRecord::Base#destroy}[rdoc-ref:Persistence#destroy] callback gets executed first.
+ # You can use the +prepend+ option on the +before_destroy+ callback to avoid this.
#
# class Topic < ActiveRecord::Base
# has_many :children, dependent: destroy
@@ -237,21 +240,21 @@ module ActiveRecord
#
# This way, the +before_destroy+ gets executed before the <tt>dependent: destroy</tt> is called, and the data is still available.
#
- # == Transactions
+ # == \Transactions
#
- # The entire callback chain of a +save+, <tt>save!</tt>, or +destroy+ call runs
- # within a transaction. That includes <tt>after_*</tt> hooks. If everything
- # goes fine a COMMIT is executed once the chain has been completed.
+ # The entire callback chain of a {#save}[rdoc-ref:Persistence#save], {#save!}[rdoc-ref:Persistence#save!],
+ # or {#destroy}[rdoc-ref:Persistence#destroy] call runs within a transaction. That includes <tt>after_*</tt> hooks.
+ # If everything goes fine a COMMIT is executed once the chain has been completed.
#
# If a <tt>before_*</tt> callback cancels the action a ROLLBACK is issued. You
# can also trigger a ROLLBACK raising an exception in any of the callbacks,
# including <tt>after_*</tt> hooks. Note, however, that in that case the client
- # needs to be aware of it because an ordinary +save+ will raise such exception
+ # needs to be aware of it because an ordinary {#save}[rdoc-ref:Persistence#save] will raise such exception
# instead of quietly returning +false+.
#
# == Debugging callbacks
#
- # The callback chain is accessible via the <tt>_*_callbacks</tt> method on an object. ActiveModel Callbacks support
+ # The callback chain is accessible via the <tt>_*_callbacks</tt> method on an object. Active Model \Callbacks support
# <tt>:before</tt>, <tt>:after</tt> and <tt>:around</tt> as values for the <tt>kind</tt> property. The <tt>kind</tt> property
# defines what part of the chain the callback runs in.
#
@@ -277,7 +280,7 @@ module ActiveRecord
:before_destroy, :around_destroy, :after_destroy, :after_commit, :after_rollback
]
- module ClassMethods
+ module ClassMethods # :nodoc:
include ActiveModel::Callbacks
end
@@ -289,25 +292,33 @@ module ActiveRecord
end
def destroy #:nodoc:
- run_callbacks(:destroy) { super }
+ @_destroy_callback_already_called ||= false
+ return if @_destroy_callback_already_called
+ @_destroy_callback_already_called = true
+ _run_destroy_callbacks { super }
+ rescue RecordNotDestroyed => e
+ @_association_destroy_exception = e
+ false
+ ensure
+ @_destroy_callback_already_called = false
end
def touch(*) #:nodoc:
- run_callbacks(:touch) { super }
+ _run_touch_callbacks { super }
end
private
- def create_or_update #:nodoc:
- run_callbacks(:save) { super }
+ def create_or_update(*) #:nodoc:
+ _run_save_callbacks { super }
end
def _create_record #:nodoc:
- run_callbacks(:create) { super }
+ _run_create_callbacks { super }
end
def _update_record(*) #:nodoc:
- run_callbacks(:update) { super }
+ _run_update_callbacks { super }
end
end
end
diff --git a/activerecord/lib/active_record/coders/yaml_column.rb b/activerecord/lib/active_record/coders/yaml_column.rb
index d3d7396c91..2456b8ad8c 100644
--- a/activerecord/lib/active_record/coders/yaml_column.rb
+++ b/activerecord/lib/active_record/coders/yaml_column.rb
@@ -8,15 +8,13 @@ module ActiveRecord
def initialize(object_class = Object)
@object_class = object_class
+ check_arity_of_constructor
end
def dump(obj)
return if obj.nil?
- unless obj.is_a?(object_class)
- raise SerializationTypeMismatch,
- "Attribute was supposed to be a #{object_class}, but was a #{obj.class}. -- #{obj.inspect}"
- end
+ assert_valid_value(obj)
YAML.dump obj
end
@@ -25,14 +23,28 @@ module ActiveRecord
return yaml unless yaml.is_a?(String) && yaml =~ /^---/
obj = YAML.load(yaml)
- unless obj.is_a?(object_class) || obj.nil?
- raise SerializationTypeMismatch,
- "Attribute was supposed to be a #{object_class}, but was a #{obj.class}"
- end
+ assert_valid_value(obj)
obj ||= object_class.new if object_class != Object
obj
end
+
+ def assert_valid_value(obj)
+ unless obj.nil? || obj.is_a?(object_class)
+ raise SerializationTypeMismatch,
+ "Attribute was supposed to be a #{object_class}, but was a #{obj.class}. -- #{obj.inspect}"
+ end
+ end
+
+ private
+
+ def check_arity_of_constructor
+ begin
+ load(nil)
+ rescue ArgumentError
+ raise ArgumentError, "Cannot serialize #{object_class}. Classes passed to `serialize` must have a 0 argument constructor."
+ end
+ end
end
end
end
diff --git a/activerecord/lib/active_record/collection_cache_key.rb b/activerecord/lib/active_record/collection_cache_key.rb
new file mode 100644
index 0000000000..3c4ca3d116
--- /dev/null
+++ b/activerecord/lib/active_record/collection_cache_key.rb
@@ -0,0 +1,31 @@
+module ActiveRecord
+ module CollectionCacheKey
+
+ def collection_cache_key(collection = all, timestamp_column = :updated_at) # :nodoc:
+ query_signature = Digest::MD5.hexdigest(collection.to_sql)
+ key = "#{collection.model_name.cache_key}/query-#{query_signature}"
+
+ if collection.loaded?
+ size = collection.size
+ timestamp = collection.max_by(&timestamp_column).public_send(timestamp_column)
+ else
+ column_type = type_for_attribute(timestamp_column.to_s)
+ column = "#{connection.quote_table_name(collection.table_name)}.#{connection.quote_column_name(timestamp_column)}"
+
+ query = collection
+ .select("COUNT(*) AS size", "MAX(#{column}) AS timestamp")
+ .unscope(:order)
+ result = connection.select_one(query)
+
+ size = result["size"]
+ timestamp = column_type.deserialize(result["timestamp"])
+ end
+
+ if timestamp
+ "#{key}-#{size}-#{timestamp.utc.to_s(cache_timestamp_format)}"
+ else
+ "#{key}-#{size}"
+ end
+ end
+ end
+end
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 cb75070e3a..0d850c7625 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
@@ -1,7 +1,6 @@
require 'thread'
-require 'thread_safe'
+require 'concurrent'
require 'monitor'
-require 'set'
module ActiveRecord
# Raised when a connection could not be obtained within the connection
@@ -10,6 +9,13 @@ module ActiveRecord
class ConnectionTimeoutError < ConnectionNotEstablished
end
+ # Raised when a pool was unable to get ahold of all its connections
+ # to perform a "group" action such as
+ # {ActiveRecord::Base.connection_pool.disconnect!}[rdoc-ref:ConnectionAdapters::ConnectionPool#disconnect!]
+ # or {ActiveRecord::Base.clear_reloadable_connections!}[rdoc-ref:ConnectionAdapters::ConnectionHandler#clear_reloadable_connections!].
+ class ExclusiveConnectionTimeoutError < ConnectionTimeoutError
+ end
+
module ConnectionAdapters
# Connection pool base class for managing Active Record database
# connections.
@@ -32,17 +38,18 @@ module ActiveRecord
# Connections can be obtained and used from a connection pool in several
# ways:
#
- # 1. Simply use ActiveRecord::Base.connection as with Active Record 2.1 and
+ # 1. Simply use {ActiveRecord::Base.connection}[rdoc-ref:ConnectionHandling.connection]
+ # as with Active Record 2.1 and
# earlier (pre-connection-pooling). Eventually, when you're done with
# the connection(s) and wish it to be returned to the pool, you call
- # ActiveRecord::Base.clear_active_connections!. This will be the
- # default behavior for Active Record when used in conjunction with
+ # {ActiveRecord::Base.clear_active_connections!}[rdoc-ref:ConnectionAdapters::ConnectionHandler#clear_active_connections!].
+ # This will be the default behavior for Active Record when used in conjunction with
# Action Pack's request handling cycle.
# 2. Manually check out a connection from the pool with
- # ActiveRecord::Base.connection_pool.checkout. You are responsible for
+ # {ActiveRecord::Base.connection_pool.checkout}[rdoc-ref:#checkout]. You are responsible for
# returning this connection to the pool when finished by calling
- # ActiveRecord::Base.connection_pool.checkin(connection).
- # 3. Use ActiveRecord::Base.connection_pool.with_connection(&block), which
+ # {ActiveRecord::Base.connection_pool.checkin(connection)}[rdoc-ref:#checkin].
+ # 3. Use {ActiveRecord::Base.connection_pool.with_connection(&block)}[rdoc-ref:#with_connection], which
# obtains a connection, yields it as the sole argument to the block,
# and returns it to the pool after the block completes.
#
@@ -63,6 +70,15 @@ module ActiveRecord
# 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).
+ #
+ #--
+ # Synchronization policy:
+ # * all public methods can be called outside +synchronize+
+ # * access to these i-vars needs to be in +synchronize+:
+ # * @connections
+ # * @now_connecting
+ # * 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.
@@ -121,25 +137,23 @@ module ActiveRecord
# greater than the number of threads currently waiting (that
# is, don't jump ahead in line). Otherwise, return nil.
#
- # If +timeout+ is given, block if it there is no element
+ # If +timeout+ is given, block if there is no element
# available, waiting up to +timeout+ seconds for an element to
# become available.
#
# Raises:
- # - ConnectionTimeoutError if +timeout+ is given and no element
- # becomes available after +timeout+ seconds,
+ # - ActiveRecord::ConnectionTimeoutError if +timeout+ is given and no element
+ # becomes available within +timeout+ seconds,
def poll(timeout = nil)
- synchronize do
- if timeout
- no_wait_poll || wait_poll(timeout)
- else
- no_wait_poll
- end
- end
+ synchronize { internal_poll(timeout) }
end
private
+ def internal_poll(timeout)
+ no_wait_poll || (timeout && wait_poll(timeout))
+ end
+
def synchronize(&block)
@lock.synchronize(&block)
end
@@ -150,7 +164,7 @@ module ActiveRecord
end
# A thread can remove an element from the queue without
- # waiting if an only if the number of currently available
+ # waiting if and only if the number of currently available
# connections is strictly greater than the number of waiting
# threads.
def can_remove_no_wait?
@@ -193,6 +207,80 @@ module ActiveRecord
end
end
+ # Adds the ability to turn a basic fair FIFO queue into one
+ # biased to some thread.
+ module BiasableQueue # :nodoc:
+ class BiasedConditionVariable # :nodoc:
+ # semantics of condition variables guarantee that +broadcast+, +broadcast_on_biased+,
+ # +signal+ and +wait+ methods are only called while holding a lock
+ def initialize(lock, other_cond, preferred_thread)
+ @real_cond = lock.new_cond
+ @other_cond = other_cond
+ @preferred_thread = preferred_thread
+ @num_waiting_on_real_cond = 0
+ end
+
+ def broadcast
+ broadcast_on_biased
+ @other_cond.broadcast
+ end
+
+ def broadcast_on_biased
+ @num_waiting_on_real_cond = 0
+ @real_cond.broadcast
+ end
+
+ def signal
+ if @num_waiting_on_real_cond > 0
+ @num_waiting_on_real_cond -= 1
+ @real_cond
+ else
+ @other_cond
+ end.signal
+ end
+
+ def wait(timeout)
+ if Thread.current == @preferred_thread
+ @num_waiting_on_real_cond += 1
+ @real_cond
+ else
+ @other_cond
+ end.wait(timeout)
+ end
+ end
+
+ def with_a_bias_for(thread)
+ previous_cond = nil
+ new_cond = nil
+ synchronize do
+ previous_cond = @cond
+ @cond = new_cond = BiasedConditionVariable.new(@lock, @cond, thread)
+ end
+ yield
+ ensure
+ synchronize do
+ @cond = previous_cond if previous_cond
+ new_cond.broadcast_on_biased if new_cond # wake up any remaining sleepers
+ end
+ end
+ end
+
+ # Connections must be leased while holding the main pool mutex. This is
+ # an internal subclass that also +.leases+ returned connections while
+ # still in queue's critical section (queue synchronizes with the same
+ # +@lock+ as the main pool) so that a returned connection is already
+ # leased and there is no need to re-enter synchronized block.
+ class ConnectionLeasingQueue < Queue # :nodoc:
+ include BiasableQueue
+
+ private
+ def internal_poll(timeout)
+ conn = super
+ conn.lease if conn
+ conn
+ 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.
@@ -220,7 +308,7 @@ module ActiveRecord
include MonitorMixin
- attr_accessor :automatic_reconnect, :checkout_timeout
+ attr_accessor :automatic_reconnect, :checkout_timeout, :schema_cache
attr_reader :spec, :connections, :size, :reaper
# Creates a new ConnectionPool object. +spec+ is a ConnectionSpecification
@@ -234,63 +322,82 @@ module ActiveRecord
@spec = spec
- @checkout_timeout = spec.config[:checkout_timeout] || 5
- @reaper = Reaper.new self, spec.config[:reaping_frequency]
+ @checkout_timeout = (spec.config[:checkout_timeout] && spec.config[:checkout_timeout].to_f) || 5
+ @reaper = Reaper.new(self, (spec.config[:reaping_frequency] && spec.config[:reaping_frequency].to_f))
@reaper.run
# default max pool size to 5
@size = (spec.config[:pool] && spec.config[:pool].to_i) || 5
- # The cache of reserved connections mapped to threads
- @reserved_connections = ThreadSafe::Cache.new(:initial_capacity => @size)
+ # The cache of threads mapped to reserved connections, the sole purpose
+ # of the cache is to speed-up +connection+ method, it is not the authoritative
+ # registry of which thread owns which connection, that is tracked by
+ # +connection.owner+ attr on each +connection+ instance.
+ # The invariant works like this: if there is mapping of <tt>thread => conn</tt>,
+ # then that +thread+ does indeed own that +conn+, however an absence of a such
+ # mapping does not mean that the +thread+ doesn't own the said connection, in
+ # that case +conn.owner+ attr should be consulted.
+ # Access and modification of +@thread_cached_conns+ does not require
+ # synchronization.
+ @thread_cached_conns = Concurrent::Map.new(:initial_capacity => @size)
@connections = []
@automatic_reconnect = true
- @available = Queue.new self
+ # Connection pool allows for concurrent (outside the main +synchronize+ section)
+ # establishment of new connections. This variable tracks the number of threads
+ # currently in the process of independently establishing connections to the DB.
+ @now_connecting = 0
+
+ # A boolean toggle that allows/disallows new connections.
+ @new_cons_enabled = true
+
+ @available = ConnectionLeasingQueue.new self
end
# Retrieve the connection associated with the current thread, or call
# #checkout to obtain one if necessary.
#
# #connection can be called any number of times; the connection is
- # held in a hash keyed by the thread id.
+ # held in a cache keyed by a thread.
def connection
- # this is correctly done double-checked locking
- # (ThreadSafe::Cache's lookups have volatile semantics)
- @reserved_connections[current_connection_id] || synchronize do
- @reserved_connections[current_connection_id] ||= checkout
- end
+ @thread_cached_conns[connection_cache_key(Thread.current)] ||= checkout
end
# Is there an open connection that is being used for the current thread?
+ #
+ # This method only works for connections that have been obtained through
+ # #connection or #with_connection methods, connections obtained through
+ # #checkout will not be detected by #active_connection?
def active_connection?
- synchronize do
- @reserved_connections.fetch(current_connection_id) {
- return false
- }.in_use?
- end
+ @thread_cached_conns[connection_cache_key(Thread.current)]
end
# Signal that the thread is finished with the current connection.
# #release_connection releases the connection-thread association
# and returns the connection to the pool.
- def release_connection(with_id = current_connection_id)
- synchronize do
- conn = @reserved_connections.delete(with_id)
- checkin conn if conn
+ #
+ # This method only works for connections that have been obtained through
+ # #connection or #with_connection methods, connections obtained through
+ # #checkout will not be automatically released.
+ def release_connection(owner_thread = Thread.current)
+ if conn = @thread_cached_conns.delete(connection_cache_key(owner_thread))
+ checkin conn
end
end
- # If a connection already exists yield it to the block. If no connection
+ # If a connection obtained through #connection or #with_connection methods
+ # already exists yield it to the block. If no such connection
# exists checkout a connection, yield it to the block, and checkin the
# connection when finished.
def with_connection
- connection_id = current_connection_id
- fresh_connection = true unless active_connection?
- yield connection
+ unless conn = @thread_cached_conns[connection_cache_key(Thread.current)]
+ conn = connection
+ fresh_connection = true
+ end
+ yield conn
ensure
- release_connection(connection_id) if fresh_connection
+ release_connection if fresh_connection
end
# Returns true if a connection has already been opened.
@@ -299,34 +406,81 @@ module ActiveRecord
end
# Disconnects all connections in the pool, and clears the pool.
- def disconnect!
- synchronize do
- @reserved_connections.clear
- @connections.each do |conn|
- checkin conn
- conn.disconnect!
+ #
+ # Raises:
+ # - ActiveRecord::ExclusiveConnectionTimeoutError if unable to gain ownership of all
+ # connections in the pool within a timeout interval (default duration is
+ # <tt>spec.config[:checkout_timeout] * 2</tt> seconds).
+ def disconnect(raise_on_acquisition_timeout = true)
+ with_exclusively_acquired_all_connections(raise_on_acquisition_timeout) do
+ synchronize do
+ @connections.each do |conn|
+ checkin conn
+ conn.disconnect!
+ end
+ @connections = []
+ @available.clear
end
- @connections = []
- @available.clear
end
end
- # Clears the cache which maps classes.
- def clear_reloadable_connections!
- synchronize do
- @reserved_connections.clear
- @connections.each do |conn|
- checkin conn
- conn.disconnect! if conn.requires_reloading?
- end
- @connections.delete_if do |conn|
- conn.requires_reloading?
- end
- @available.clear
- @connections.each do |conn|
- @available.add conn
+ # Disconnects all connections in the pool, and clears the pool.
+ #
+ # The pool first tries to gain ownership of all connections, if unable to
+ # do so within a timeout interval (default duration is
+ # <tt>spec.config[:checkout_timeout] * 2</tt> seconds), the pool is forcefully
+ # disconnected without any regard for other connection owning threads.
+ def disconnect!
+ disconnect(false)
+ end
+
+ # Clears the cache which maps classes and re-connects connections that
+ # require reloading.
+ #
+ # Raises:
+ # - ActiveRecord::ExclusiveConnectionTimeoutError if unable to gain ownership of all
+ # connections in the pool within a timeout interval (default duration is
+ # <tt>spec.config[:checkout_timeout] * 2</tt> seconds).
+ def clear_reloadable_connections(raise_on_acquisition_timeout = true)
+ num_new_conns_required = 0
+
+ with_exclusively_acquired_all_connections(raise_on_acquisition_timeout) do
+ synchronize do
+ @connections.each do |conn|
+ checkin conn
+ conn.disconnect! if conn.requires_reloading?
+ end
+ @connections.delete_if(&:requires_reloading?)
+
+ @available.clear
+
+ if @connections.size < @size
+ # because of the pruning done by this method, we might be running
+ # low on connections, while threads stuck in queue are helpless
+ # (not being able to establish new connections for themselves),
+ # see also more detailed explanation in +remove+
+ num_new_conns_required = num_waiting_in_queue - @connections.size
+ end
+
+ @connections.each do |conn|
+ @available.add conn
+ end
end
end
+
+ bulk_make_new_connections(num_new_conns_required) if num_new_conns_required > 0
+ end
+
+ # Clears the cache which maps classes and re-connects connections that
+ # require reloading.
+ #
+ # The pool first tries to gain ownership of all connections, if unable to
+ # do so within a timeout interval (default duration is
+ # <tt>spec.config[:checkout_timeout] * 2</tt> seconds), the pool forcefully
+ # clears the cache and reloads connections without any regard for other
+ # connection owning threads.
+ def clear_reloadable_connections!
+ clear_reloadable_connections(false)
end
# Check-out a database connection from the pool, indicating that you want
@@ -342,48 +496,60 @@ module ActiveRecord
# Returns: an AbstractAdapter object.
#
# Raises:
- # - ConnectionTimeoutError: no connection can be obtained from the pool.
- def checkout
- synchronize do
- conn = acquire_connection
- conn.lease
- checkout_and_verify(conn)
- end
+ # - ActiveRecord::ConnectionTimeoutError no connection can be obtained from the pool.
+ def checkout(checkout_timeout = @checkout_timeout)
+ checkout_and_verify(acquire_connection(checkout_timeout))
end
# Check-in a database connection back into the pool, indicating that you
# no longer need this connection.
#
# +conn+: an AbstractAdapter object, which was obtained by earlier by
- # calling +checkout+ on this pool.
+ # calling #checkout on this pool.
def checkin(conn)
synchronize do
- owner = conn.owner
+ remove_connection_from_thread_cache conn
- conn.run_callbacks :checkin do
+ conn._run_checkin_callbacks do
conn.expire
end
- release owner
-
@available.add conn
end
end
- # Remove a connection from the connection pool. The connection will
+ # Remove a connection from the connection pool. The connection will
# remain open and active but will no longer be managed by this pool.
def remove(conn)
+ needs_new_connection = false
+
synchronize do
+ remove_connection_from_thread_cache conn
+
@connections.delete conn
@available.delete conn
- release conn.owner
-
- @available.add checkout_new_connection if @available.any_waiting?
+ # @available.any_waiting? => true means that prior to removing this
+ # conn, the pool was at its max size (@connections.size == @size)
+ # this would mean that any threads stuck waiting in the queue wouldn't
+ # know they could checkout_new_connection, so let's do it for them.
+ # Because condition-wait loop is encapsulated in the Queue class
+ # (that in turn is oblivious to ConnectionPool implementation), threads
+ # that are "stuck" there are helpless, they have no way of creating
+ # new connections and are completely reliant on us feeding available
+ # connections into the Queue.
+ needs_new_connection = @available.any_waiting?
end
+
+ # This is intentionally done outside of the synchronized section as we
+ # would like not to hold the main mutex while checking out new connections,
+ # thus there is some chance that needs_new_connection information is now
+ # stale, we can live with that (bulk_make_new_connections will make
+ # sure not to exceed the pool's @size limit).
+ bulk_make_new_connections(1) if needs_new_connection
end
- # Recover lost connections for the pool. A lost connection can occur if
+ # Recover lost connections for the pool. A lost connection can occur if
# a programmer forgets to checkin a connection at the end of a thread
# or a thread dies unexpectedly.
def reap
@@ -405,7 +571,118 @@ module ActiveRecord
end
end
+ def num_waiting_in_queue # :nodoc:
+ @available.num_waiting
+ end
+
private
+ #--
+ # this is unfortunately not concurrent
+ def bulk_make_new_connections(num_new_conns_needed)
+ num_new_conns_needed.times do
+ # try_to_checkout_new_connection will not exceed pool's @size limit
+ if new_conn = try_to_checkout_new_connection
+ # make the new_conn available to the starving threads stuck @available Queue
+ checkin(new_conn)
+ end
+ end
+ end
+
+ #--
+ # From the discussion on GitHub:
+ # https://github.com/rails/rails/pull/14938#commitcomment-6601951
+ # This hook-in method allows for easier monkey-patching fixes needed by
+ # JRuby users that use Fibers.
+ def connection_cache_key(thread)
+ thread
+ end
+
+ # Take control of all existing connections so a "group" action such as
+ # reload/disconnect can be performed safely. It is no longer enough to
+ # wrap it in +synchronize+ because some pool's actions are allowed
+ # to be performed outside of the main +synchronize+ block.
+ def with_exclusively_acquired_all_connections(raise_on_acquisition_timeout = true)
+ with_new_connections_blocked do
+ attempt_to_checkout_all_existing_connections(raise_on_acquisition_timeout)
+ yield
+ end
+ end
+
+ def attempt_to_checkout_all_existing_connections(raise_on_acquisition_timeout = true)
+ collected_conns = synchronize do
+ # account for our own connections
+ @connections.select {|conn| conn.owner == Thread.current}
+ end
+
+ newly_checked_out = []
+ timeout_time = Time.now + (@checkout_timeout * 2)
+
+ @available.with_a_bias_for(Thread.current) do
+ while true
+ synchronize do
+ return if collected_conns.size == @connections.size && @now_connecting == 0
+ remaining_timeout = timeout_time - Time.now
+ remaining_timeout = 0 if remaining_timeout < 0
+ conn = checkout_for_exclusive_access(remaining_timeout)
+ collected_conns << conn
+ newly_checked_out << conn
+ end
+ end
+ end
+ rescue ExclusiveConnectionTimeoutError
+ # <tt>raise_on_acquisition_timeout == false</tt> means we are directed to ignore any
+ # timeouts and are expected to just give up: we've obtained as many connections
+ # as possible, note that in a case like that we don't return any of the
+ # +newly_checked_out+ connections.
+
+ if raise_on_acquisition_timeout
+ release_newly_checked_out = true
+ raise
+ end
+ rescue Exception # if something else went wrong
+ # this can't be a "naked" rescue, because we have should return conns
+ # even for non-StandardErrors
+ release_newly_checked_out = true
+ raise
+ ensure
+ if release_newly_checked_out && newly_checked_out
+ # releasing only those conns that were checked out in this method, conns
+ # checked outside this method (before it was called) are not for us to release
+ newly_checked_out.each {|conn| checkin(conn)}
+ end
+ end
+
+ #--
+ # Must be called in a synchronize block.
+ def checkout_for_exclusive_access(checkout_timeout)
+ checkout(checkout_timeout)
+ rescue ConnectionTimeoutError
+ # this block can't be easily moved into attempt_to_checkout_all_existing_connections's
+ # rescue block, because doing so would put it outside of synchronize section, without
+ # being in a critical section thread_report might become inaccurate
+ msg = "could not obtain ownership of all database connections in #{checkout_timeout} seconds"
+
+ thread_report = []
+ @connections.each do |conn|
+ unless conn.owner == Thread.current
+ thread_report << "#{conn} is owned by #{conn.owner}"
+ end
+ end
+
+ msg << " (#{thread_report.join(', ')})" if thread_report.any?
+
+ raise ExclusiveConnectionTimeoutError, msg
+ end
+
+ def with_new_connections_blocked
+ previous_value = nil
+ synchronize do
+ previous_value, @new_cons_enabled = @new_cons_enabled, false
+ end
+ yield
+ ensure
+ synchronize { @new_cons_enabled = previous_value }
+ end
# Acquire a connection by one of 1) immediately removing one
# from the queue of available connections, 2) creating a new
@@ -413,46 +690,90 @@ module ActiveRecord
# queue for a connection to become available.
#
# Raises:
- # - ConnectionTimeoutError if a connection could not be acquired
- def acquire_connection
- if conn = @available.poll
+ # - ActiveRecord::ConnectionTimeoutError if a connection could not be acquired
+ #
+ #--
+ # Implementation detail: the connection returned by +acquire_connection+
+ # will already be "+connection.lease+ -ed" to the current thread.
+ def acquire_connection(checkout_timeout)
+ # NOTE: we rely on +@available.poll+ and +try_to_checkout_new_connection+ to
+ # +conn.lease+ the returned connection (and to do this in a +synchronized+
+ # section), this is not the cleanest implementation, as ideally we would
+ # <tt>synchronize { conn.lease }</tt> in this method, but by leaving it to +@available.poll+
+ # and +try_to_checkout_new_connection+ we can piggyback on +synchronize+ sections
+ # of the said methods and avoid an additional +synchronize+ overhead.
+ if conn = @available.poll || try_to_checkout_new_connection
conn
- elsif @connections.size < @size
- checkout_new_connection
else
reap
- @available.poll(@checkout_timeout)
+ @available.poll(checkout_timeout)
end
end
- def release(owner)
- thread_id = owner.object_id
-
- @reserved_connections.delete thread_id
+ #--
+ # if owner_thread param is omitted, this must be called in synchronize block
+ def remove_connection_from_thread_cache(conn, owner_thread = conn.owner)
+ @thread_cached_conns.delete_pair(connection_cache_key(owner_thread), conn)
end
+ alias_method :release, :remove_connection_from_thread_cache
def new_connection
- Base.send(spec.adapter_method, spec.config)
+ Base.send(spec.adapter_method, spec.config).tap do |conn|
+ conn.schema_cache = schema_cache.dup if schema_cache
+ end
+ end
+
+ # If the pool is not at a +@size+ limit, establish new connection. Connecting
+ # to the DB is done outside main synchronized section.
+ #--
+ # Implementation constraint: a newly established connection returned by this
+ # method must be in the +.leased+ state.
+ def try_to_checkout_new_connection
+ # first in synchronized section check if establishing new conns is allowed
+ # and increment @now_connecting, to prevent overstepping this pool's @size
+ # constraint
+ do_checkout = synchronize do
+ if @new_cons_enabled && (@connections.size + @now_connecting) < @size
+ @now_connecting += 1
+ end
+ end
+ if do_checkout
+ begin
+ # if successfully incremented @now_connecting establish new connection
+ # outside of synchronized section
+ conn = checkout_new_connection
+ ensure
+ synchronize do
+ if conn
+ adopt_connection(conn)
+ # returned conn needs to be already leased
+ conn.lease
+ end
+ @now_connecting -= 1
+ end
+ end
+ end
end
- def current_connection_id #:nodoc:
- Base.connection_id ||= Thread.current.object_id
+ def adopt_connection(conn)
+ conn.pool = self
+ @connections << conn
end
def checkout_new_connection
raise ConnectionNotEstablished unless @automatic_reconnect
-
- c = new_connection
- c.pool = self
- @connections << c
- c
+ new_connection
end
def checkout_and_verify(c)
- c.run_callbacks :checkout do
+ c._run_checkout_callbacks do
c.verify!
end
c
+ rescue
+ remove c
+ c.disconnect!
+ raise
end
end
@@ -462,47 +783,61 @@ module ActiveRecord
#
# For example, suppose that you have 5 models, with the following hierarchy:
#
- # |
- # +-- Book
- # | |
- # | +-- ScaryBook
- # | +-- GoodBook
- # +-- Author
- # +-- BankAccount
+ # class Author < ActiveRecord::Base
+ # end
+ #
+ # class BankAccount < ActiveRecord::Base
+ # end
+ #
+ # class Book < ActiveRecord::Base
+ # establish_connection "library_db"
+ # end
+ #
+ # class ScaryBook < Book
+ # end
+ #
+ # class GoodBook < Book
+ # end
#
- # Suppose that Book is to connect to a separate database (i.e. one other
- # than the default database). Then Book, ScaryBook and GoodBook will all use
- # the same connection pool. Likewise, Author and BankAccount will use the
- # same connection pool. However, the connection pool used by Author/BankAccount
- # is not the same as the one used by Book/ScaryBook/GoodBook.
+ # And a database.yml that looked like this:
#
- # Normally there is only a single ConnectionHandler instance, accessible via
- # ActiveRecord::Base.connection_handler. Active Record models use this to
- # determine the connection pool that they should use.
+ # development:
+ # database: my_application
+ # host: localhost
+ #
+ # library_db:
+ # database: library
+ # host: some.library.org
+ #
+ # Your primary database in the development environment is "my_application"
+ # but the Book model connects to a separate database called "library_db"
+ # (this can even be a database on a different machine).
+ #
+ # Book, ScaryBook and GoodBook will all use the same connection pool to
+ # "library_db" while Author, BankAccount, and any other models you create
+ # will use the default connection pool to "my_application".
+ #
+ # The various connection pools are managed by a single instance of
+ # ConnectionHandler accessible via ActiveRecord::Base.connection_handler.
+ # All Active Record models use this handler to determine the connection pool that they
+ # should use.
class ConnectionHandler
def initialize
# These caches are keyed by klass.name, NOT klass. Keying them by klass
# alone would lead to memory leaks in development mode as all previous
# instances of the class would stay in memory.
- @owner_to_pool = ThreadSafe::Cache.new(:initial_capacity => 2) do |h,k|
- h[k] = ThreadSafe::Cache.new(:initial_capacity => 2)
+ @owner_to_pool = Concurrent::Map.new(:initial_capacity => 2) do |h,k|
+ h[k] = Concurrent::Map.new(:initial_capacity => 2)
end
- @class_to_pool = ThreadSafe::Cache.new(:initial_capacity => 2) do |h,k|
- h[k] = ThreadSafe::Cache.new
+ @class_to_pool = Concurrent::Map.new(:initial_capacity => 2) do |h,k|
+ h[k] = Concurrent::Map.new
end
end
def connection_pool_list
owner_to_pool.values.compact
end
-
- def connection_pools
- ActiveSupport::Deprecation.warn(
- "In the next release, this will return the same as #connection_pool_list. " \
- "(An array of pools, rather than a hash mapping specs to pools.)"
- )
- Hash[connection_pool_list.map { |pool| [pool.spec, pool] }]
- end
+ alias :connection_pools :connection_pool_list
def establish_connection(owner, spec)
@class_to_pool.clear
@@ -524,6 +859,8 @@ module ActiveRecord
end
# Clears the cache which maps classes.
+ #
+ # See ConnectionPool#clear_reloadable_connections! for details.
def clear_reloadable_connections!
connection_pool_list.each(&:clear_reloadable_connections!)
end
@@ -600,7 +937,9 @@ module ActiveRecord
# A connection was established in an ancestor process that must have
# subsequently forked. We can't reuse the connection, but we can copy
# the specification and establish a new connection with it.
- establish_connection owner, ancestor_pool.spec
+ establish_connection(owner, ancestor_pool.spec).tap do |pool|
+ pool.schema_cache = ancestor_pool.schema_cache if ancestor_pool.schema_cache
+ end
else
owner_to_pool[owner.name] = nil
end
@@ -619,7 +958,7 @@ module ActiveRecord
end
def call(env)
- testing = env.key?('rack.test')
+ testing = env['rack.test']
response = @app.call(env)
response[2] = ::Rack::BodyProxy.new(response[2]) do
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb
index c0a2111571..6711049588 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb
@@ -18,9 +18,9 @@ module ActiveRecord
end
# Returns the maximum allowed length for an index name. This
- # limit is enforced by rails and Is less than or equal to
- # <tt>index_name_length</tt>. The gap between
- # <tt>index_name_length</tt> is to allow internal rails
+ # limit is enforced by \Rails and is less than or equal to
+ # #index_name_length. The gap between
+ # #index_name_length is to allow internal \Rails
# operations to use prefixes in temporary operations.
def allowed_index_name_length
index_name_length
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 98e96099cb..848aeb821c 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
@@ -29,7 +29,17 @@ module ActiveRecord
# Returns an ActiveRecord::Result instance.
def select_all(arel, name = nil, binds = [])
arel, binds = binds_from_relation arel, binds
- select(to_sql(arel, binds), name, binds)
+ sql = to_sql(arel, binds)
+ if arel.is_a?(String)
+ preparable = false
+ else
+ preparable = visitor.preparable
+ end
+ if prepared_statements && preparable
+ select_prepared(sql, name, binds)
+ else
+ select(sql, name, binds)
+ end
end
# Returns a record hash with the column names as keys and column values
@@ -40,8 +50,9 @@ module ActiveRecord
# Returns a single value from a record
def select_value(arel, name = nil, binds = [])
- if result = select_one(arel, name, binds)
- result.values.first
+ arel, binds = binds_from_relation arel, binds
+ if result = select_rows(to_sql(arel, binds), name, binds).first
+ result.first
end
end
@@ -66,7 +77,7 @@ module ActiveRecord
# Executes +sql+ statement in the context of this connection using
# +binds+ as the bind substitutes. +name+ is logged along with
# the executed +sql+ statement.
- def exec_query(sql, name = 'SQL', binds = [])
+ def exec_query(sql, name = 'SQL', binds = [], prepare: false)
end
# Executes insert +sql+ statement in the context of this connection using
@@ -83,6 +94,11 @@ module ActiveRecord
exec_query(sql, name, binds)
end
+ # Executes the truncate statement.
+ def truncate(table_name, name = nil)
+ raise NotImplementedError
+ end
+
# Executes update +sql+ statement in the context of this connection using
# +binds+ as the bind substitutes. +name+ is logged along with
# the executed +sql+ statement.
@@ -131,7 +147,7 @@ module ActiveRecord
#
# In order to get around this problem, #transaction will emulate the effect
# of nested transactions, by using savepoints:
- # http://dev.mysql.com/doc/refman/5.0/en/savepoint.html
+ # http://dev.mysql.com/doc/refman/5.7/en/savepoint.html
# Savepoints are supported by MySQL and PostgreSQL. SQLite3 version >= '3.6.8'
# supports savepoints.
#
@@ -183,10 +199,10 @@ module ActiveRecord
# You should consult the documentation for your database to understand the
# semantics of these different levels:
#
- # * http://www.postgresql.org/docs/9.1/static/transaction-iso.html
- # * https://dev.mysql.com/doc/refman/5.0/en/set-transaction.html
+ # * http://www.postgresql.org/docs/current/static/transaction-iso.html
+ # * https://dev.mysql.com/doc/refman/5.7/en/set-transaction.html
#
- # An <tt>ActiveRecord::TransactionIsolationError</tt> will be raised if:
+ # An ActiveRecord::TransactionIsolationError will be raised if:
#
# * The adapter does not support setting the isolation level
# * You are joining an existing open transaction
@@ -196,16 +212,14 @@ module ActiveRecord
# isolation level. However, support is disabled for MySQL versions below 5,
# because they are affected by a bug[http://bugs.mysql.com/bug.php?id=39170]
# which means the isolation level gets persisted outside the transaction.
- def transaction(options = {})
- options.assert_valid_keys :requires_new, :joinable, :isolation
-
- if !options[:requires_new] && current_transaction.joinable?
- if options[:isolation]
+ def transaction(requires_new: nil, isolation: nil, joinable: true)
+ if !requires_new && current_transaction.joinable?
+ if isolation
raise ActiveRecord::TransactionIsolationError, "cannot set isolation when joining a transaction"
end
yield
else
- transaction_manager.within_new_transaction(options) { yield }
+ transaction_manager.within_new_transaction(isolation: isolation, joinable: joinable) { yield }
end
rescue ActiveRecord::Rollback
# rollbacks are silently swallowed
@@ -229,6 +243,10 @@ module ActiveRecord
current_transaction.add_record(record)
end
+ def transaction_state
+ current_transaction.state
+ end
+
# Begins the transaction (and turns off auto-committing).
def begin_db_transaction() end
@@ -253,7 +271,18 @@ module ActiveRecord
# Rolls back the transaction (and turns on auto-committing). Must be
# done if the transaction block raises an exception or returns false.
- def rollback_db_transaction() end
+ def rollback_db_transaction
+ exec_rollback_db_transaction
+ end
+
+ def exec_rollback_db_transaction() end #:nodoc:
+
+ def rollback_to_savepoint(name = nil)
+ exec_rollback_to_savepoint(name)
+ end
+
+ def exec_rollback_to_savepoint(name = nil) #:nodoc:
+ end
def default_sequence_name(table, column)
nil
@@ -269,10 +298,21 @@ module ActiveRecord
def insert_fixture(fixture, table_name)
columns = schema_cache.columns_hash(table_name)
- key_list = []
- value_list = fixture.map do |name, value|
- key_list << quote_column_name(name)
- quote(value, columns[name])
+ binds = fixture.map do |name, value|
+ if column = columns[name]
+ type = lookup_cast_type_from_column(column)
+ Relation::QueryAttribute.new(name, value, type)
+ else
+ raise Fixture::FixtureError, %(table "#{table_name}" has no column named "#{name}".)
+ end
+ end
+ key_list = fixture.keys.map { |name| quote_column_name(name) }
+ value_list = prepare_binds_for_database(binds).map do |value|
+ begin
+ quote(value)
+ rescue TypeError
+ quote(YAML.dump(value))
+ end
end
execute "INSERT INTO #{quote_table_name(table_name)} (#{key_list.join(', ')}) VALUES (#{value_list.join(', ')})", 'Fixture Insert'
@@ -282,10 +322,6 @@ module ActiveRecord
"DEFAULT VALUES"
end
- def limited_update_conditions(where_sql, quoted_table_name, quoted_primary_key)
- "WHERE #{quoted_primary_key} IN (SELECT #{quoted_primary_key} FROM #{quoted_table_name} #{where_sql})"
- end
-
# Sanitizes the given LIMIT parameter in order to prevent SQL injection.
#
# The +limit+ may be anything that can evaluate to a string via #to_s. It
@@ -332,8 +368,12 @@ module ActiveRecord
# Returns an ActiveRecord::Result instance.
def select(sql, name = nil, binds = [])
+ exec_query(sql, name, binds, prepare: false)
+ end
+
+ def select_prepared(sql, name = nil, binds = [])
+ exec_query(sql, name, binds, prepare: true)
end
- undef_method :select
# Returns the last auto-generated ID from the affected table.
def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
@@ -362,7 +402,7 @@ module ActiveRecord
def binds_from_relation(relation, binds)
if relation.is_a?(Relation) && binds.empty?
- relation, binds = relation.arel, relation.bind_values
+ relation, binds = relation.arel, relation.bound_attributes
end
[relation, binds]
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb
index 4a4506c7f5..5e27cfe507 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb
@@ -3,7 +3,7 @@ module ActiveRecord
module QueryCache
class << self
def included(base) #:nodoc:
- dirties_query_cache base, :insert, :update, :delete
+ dirties_query_cache base, :insert, :update, :delete, :rollback_to_savepoint, :rollback_db_transaction
end
def dirties_query_cache(base, *method_names)
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
index eb88845913..9ec0a67c8f 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
@@ -10,7 +10,13 @@ module ActiveRecord
return value.quoted_id if value.respond_to?(:quoted_id)
if column
- value = column.cast_type.type_cast_for_database(value)
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ Passing a column to `quote` has been deprecated. It is only used
+ for type casting, which should be handled elsewhere. See
+ https://github.com/rails/arel/commit/6160bfbda1d1781c3b08a33ec4955f170e95be11
+ for more information.
+ MSG
+ value = type_cast_from_column(column, value)
end
_quote(value)
@@ -19,13 +25,13 @@ module ActiveRecord
# Cast a +value+ to a type that the database understands. For example,
# SQLite does not understand dates, so this method will convert a Date
# to a String.
- def type_cast(value, column)
+ def type_cast(value, column = nil)
if value.respond_to?(:quoted_id) && value.respond_to?(:id)
return value.id
end
if column
- value = column.cast_type.type_cast_for_database(value)
+ value = type_cast_from_column(column, value)
end
_type_cast(value)
@@ -34,10 +40,44 @@ module ActiveRecord
raise TypeError, "can't cast #{value.class}#{to_type}"
end
+ # If you are having to call this function, you are likely doing something
+ # wrong. The column does not have sufficient type information if the user
+ # provided a custom type on the class level either explicitly (via
+ # Attributes::ClassMethods#attribute) or implicitly (via
+ # AttributeMethods::Serialization::ClassMethods#serialize, +time_zone_aware_attributes+).
+ # In almost all cases, the sql type should only be used to change quoting behavior, when the primitive to
+ # represent the type doesn't sufficiently reflect the differences
+ # (varchar vs binary) for example. The type used to get this primitive
+ # should have been provided before reaching the connection adapter.
+ def type_cast_from_column(column, value) # :nodoc:
+ if column
+ type = lookup_cast_type_from_column(column)
+ type.serialize(value)
+ else
+ value
+ end
+ end
+
+ # See docs for #type_cast_from_column
+ def lookup_cast_type_from_column(column) # :nodoc:
+ lookup_cast_type(column.sql_type)
+ end
+
+ def fetch_type_metadata(sql_type)
+ cast_type = lookup_cast_type(sql_type)
+ SqlTypeMetadata.new(
+ sql_type: sql_type,
+ type: cast_type.type,
+ limit: cast_type.limit,
+ precision: cast_type.precision,
+ scale: cast_type.scale,
+ )
+ end
+
# Quotes a string, escaping any ' (single quote) and \ (backslash)
# characters.
def quote_string(s)
- s.gsub(/\\/, '\&\&').gsub(/'/, "''") # ' (for ruby-mode)
+ s.gsub('\\'.freeze, '\&\&'.freeze).gsub("'".freeze, "''".freeze) # ' (for ruby-mode)
end
# Quotes the column name. Defaults to no quoting.
@@ -62,6 +102,11 @@ module ActiveRecord
quote_table_name("#{table}.#{attr}")
end
+ def quote_default_expression(value, column) #:nodoc:
+ value = lookup_cast_type(column.sql_type).serialize(value)
+ quote(value)
+ end
+
def quoted_true
"'t'"
end
@@ -78,6 +123,8 @@ module ActiveRecord
'f'
end
+ # Quote date/time values for use in SQL input. Includes microseconds
+ # if the value is a Time responding to usec.
def quoted_date(value)
if value.acts_like?(:time)
zone_conversion_method = ActiveRecord::Base.default_timezone == :utc ? :getutc : :getlocal
@@ -87,7 +134,16 @@ module ActiveRecord
end
end
- value.to_s(:db)
+ result = value.to_s(:db)
+ if value.respond_to?(:usec) && value.usec > 0
+ "#{result}.#{sprintf("%06d", value.usec)}"
+ else
+ result
+ end
+ end
+
+ def prepare_binds_for_database(binds) # :nodoc:
+ binds.map(&:value_for_database)
end
private
@@ -108,9 +164,8 @@ module ActiveRecord
when Numeric, ActiveSupport::Duration then value.to_s
when Date, Time then "'#{quoted_date(value)}'"
when Symbol then "'#{quote_string(value.to_s)}'"
- when Class then "'#{value.to_s}'"
- else
- "'#{quote_string(YAML.dump(value))}'"
+ when Class then "'#{value}'"
+ else raise TypeError, "can't quote #{value.class.name}"
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/savepoints.rb b/activerecord/lib/active_record/connection_adapters/abstract/savepoints.rb
index 25c17ce971..c0662f8473 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/savepoints.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/savepoints.rb
@@ -9,7 +9,7 @@ module ActiveRecord
execute("SAVEPOINT #{name}")
end
- def rollback_to_savepoint(name = current_savepoint_name)
+ def exec_rollback_to_savepoint(name = current_savepoint_name)
execute("ROLLBACK TO SAVEPOINT #{name}")
end
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 adad6cd542..0ba4d94e3c 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb
@@ -1,3 +1,5 @@
+require 'active_support/core_ext/string/strip'
+
module ActiveRecord
module ConnectionAdapters
class AbstractAdapter
@@ -12,40 +14,58 @@ module ActiveRecord
send m, o
end
- def visit_AddColumn(o)
- sql_type = type_to_sql(o.type, o.limit, o.precision, o.scale)
- sql = "ADD #{quote_column_name(o.name)} #{sql_type}"
- add_column_options!(sql, column_options(o))
- end
+ delegate :quote_column_name, :quote_table_name, :quote_default_expression, :type_to_sql,
+ :options_include_default?, :supports_indexes_in_create?, :supports_foreign_keys?, :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?, :foreign_key_options
private
def visit_AlterTable(o)
sql = "ALTER TABLE #{quote_table_name(o.name)} "
- sql << o.adds.map { |col| visit_AddColumn col }.join(' ')
+ sql << o.adds.map { |col| accept col }.join(' ')
sql << o.foreign_key_adds.map { |fk| visit_AddForeignKey fk }.join(' ')
sql << o.foreign_key_drops.map { |fk| visit_DropForeignKey fk }.join(' ')
end
def visit_ColumnDefinition(o)
- sql_type = type_to_sql(o.type, o.limit, o.precision, o.scale)
- column_sql = "#{quote_column_name(o.name)} #{sql_type}"
- add_column_options!(column_sql, column_options(o)) unless o.primary_key?
+ o.sql_type ||= type_to_sql(o.type, o.limit, o.precision, o.scale)
+ column_sql = "#{quote_column_name(o.name)} #{o.sql_type}"
+ add_column_options!(column_sql, column_options(o)) unless o.type == :primary_key
column_sql
end
+ def visit_AddColumnDefinition(o)
+ "ADD #{accept(o.column)}"
+ end
+
def visit_TableDefinition(o)
- create_sql = "CREATE#{' TEMPORARY' if o.temporary} TABLE "
- create_sql << "#{quote_table_name(o.name)} "
- create_sql << "(#{o.columns.map { |c| accept c }.join(', ')}) " unless o.as
+ create_sql = "CREATE#{' TEMPORARY' if o.temporary} TABLE #{quote_table_name(o.name)} "
+
+ statements = o.columns.map { |c| accept c }
+ statements << accept(o.primary_keys) if o.primary_keys
+
+ if supports_indexes_in_create?
+ statements.concat(o.indexes.map { |column_name, options| index_in_create(o.name, column_name, options) })
+ end
+
+ if supports_foreign_keys?
+ statements.concat(o.foreign_keys.map { |to_table, options| foreign_key_in_create(o.name, to_table, options) })
+ end
+
+ create_sql << "(#{statements.join(', ')}) " if statements.present?
create_sql << "#{o.options}"
create_sql << " AS #{@conn.to_sql(o.as)}" if o.as
create_sql
end
- def visit_AddForeignKey(o)
+ def visit_PrimaryKeyDefinition(o)
+ "PRIMARY KEY (#{o.name.join(', ')})"
+ end
+
+ def visit_ForeignKeyDefinition(o)
sql = <<-SQL.strip_heredoc
- ADD CONSTRAINT #{quote_column_name(o.name)}
+ 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)})
SQL
@@ -54,6 +74,10 @@ module ActiveRecord
sql
end
+ def visit_AddForeignKey(o)
+ "ADD #{accept(o)}"
+ end
+
def visit_DropForeignKey(name)
"DROP CONSTRAINT #{quote_column_name(name)}"
end
@@ -65,23 +89,14 @@ module ActiveRecord
column_options[:column] = o
column_options[:first] = o.first
column_options[:after] = o.after
+ column_options[:auto_increment] = o.auto_increment
+ column_options[:primary_key] = o.primary_key
+ column_options[:collation] = o.collation
column_options
end
- def quote_column_name(name)
- @conn.quote_column_name name
- end
-
- def quote_table_name(name)
- @conn.quote_table_name name
- end
-
- def type_to_sql(type, limit, precision, scale)
- @conn.type_to_sql type.to_sym, limit, precision, scale
- end
-
def add_column_options!(sql, options)
- sql << " DEFAULT #{quote_value(options[:default], options[:column])}" if options_include_default?(options)
+ sql << " DEFAULT #{quote_default_expression(options[:default], options[:column])}" if options_include_default?(options)
# must explicitly check for :null to allow change_column to work on migrations
if options[:null] == false
sql << " NOT NULL"
@@ -89,18 +104,15 @@ module ActiveRecord
if options[:auto_increment] == true
sql << " AUTO_INCREMENT"
end
+ if options[:primary_key] == true
+ sql << " PRIMARY KEY"
+ end
sql
end
- def quote_value(value, column)
- column.sql_type ||= type_to_sql(column.type, column.limit, column.precision, column.scale)
- column.cast_type ||= type_for_column(column)
-
- @conn.quote(value, column)
- end
-
- def options_include_default?(options)
- options.include?(:default) && !(options[:null] == false && options[:default].nil?)
+ def foreign_key_in_create(from_table, to_table, options)
+ options = foreign_key_options(from_table, to_table, options)
+ accept ForeignKeyDefinition.new(from_table, to_table, options)
end
def action_sql(action, dependency)
@@ -115,10 +127,6 @@ module ActiveRecord
MSG
end
end
-
- def type_for_column(column)
- @conn.lookup_cast_type(column.sql_type)
- end
end
end
end
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 e44ccb7d81..e2ef56798b 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
@@ -1,8 +1,3 @@
-require 'date'
-require 'set'
-require 'bigdecimal'
-require 'bigdecimal/util'
-
module ActiveRecord
module ConnectionAdapters #:nodoc:
# Abstract representation of an index definition on a table. Instances of
@@ -15,14 +10,20 @@ module ActiveRecord
# are typically created by methods in TableDefinition, and added to the
# +columns+ attribute of said TableDefinition object, in order to be used
# for generating a number of table creation or table changing SQL statements.
- class ColumnDefinition < Struct.new(:name, :type, :limit, :precision, :scale, :default, :null, :first, :after, :primary_key, :sql_type, :cast_type) #:nodoc:
+ class ColumnDefinition < Struct.new(:name, :type, :limit, :precision, :scale, :default, :null, :first, :after, :auto_increment, :primary_key, :collation, :sql_type) #:nodoc:
def primary_key?
primary_key || type.to_sym == :primary_key
end
end
- class ChangeColumnDefinition < Struct.new(:column, :type, :options) #:nodoc:
+ class AddColumnDefinition < Struct.new(:column) # :nodoc:
+ end
+
+ class ChangeColumnDefinition < Struct.new(:column, :name) #:nodoc:
+ end
+
+ class PrimaryKeyDefinition < Struct.new(:name) # :nodoc:
end
class ForeignKeyDefinition < Struct.new(:from_table, :to_table, :options) #:nodoc:
@@ -50,16 +51,143 @@ module ActiveRecord
options[:primary_key] != default_primary_key
end
+ def defined_for?(options_or_to_table = {})
+ if options_or_to_table.is_a?(Hash)
+ options_or_to_table.all? {|key, value| options[key].to_s == value.to_s }
+ else
+ to_table == options_or_to_table.to_s
+ end
+ end
+
private
def default_primary_key
"id"
end
end
+ class ReferenceDefinition # :nodoc:
+ def initialize(
+ name,
+ polymorphic: false,
+ index: false,
+ foreign_key: false,
+ type: :integer,
+ **options
+ )
+ @name = name
+ @polymorphic = polymorphic
+ @index = index
+ @foreign_key = foreign_key
+ @type = type
+ @options = options
+
+ if polymorphic && foreign_key
+ raise ArgumentError, "Cannot add a foreign key to a polymorphic relation"
+ end
+ end
+
+ def add_to(table)
+ columns.each do |column_options|
+ table.column(*column_options)
+ end
+
+ if index
+ table.index(column_names, index_options)
+ end
+
+ if foreign_key
+ table.foreign_key(foreign_table_name, foreign_key_options)
+ end
+ end
+
+ protected
+
+ attr_reader :name, :polymorphic, :index, :foreign_key, :type, :options
+
+ private
+
+ def as_options(value, default = {})
+ if value.is_a?(Hash)
+ value
+ else
+ default
+ end
+ end
+
+ def polymorphic_options
+ as_options(polymorphic, options)
+ end
+
+ def index_options
+ as_options(index)
+ end
+
+ def foreign_key_options
+ as_options(foreign_key).merge(column: column_name)
+ end
+
+ def columns
+ result = [[column_name, type, options]]
+ if polymorphic
+ result.unshift(["#{name}_type", :string, polymorphic_options])
+ end
+ result
+ end
+
+ def column_name
+ "#{name}_id"
+ end
+
+ def column_names
+ columns.map(&:first)
+ end
+
+ def foreign_table_name
+ foreign_key_options.fetch(:to_table) do
+ Base.pluralize_table_names ? name.to_s.pluralize : name
+ end
+ end
+ end
+
+ module ColumnMethods
+ # Appends a primary key definition to the table definition.
+ # Can be called multiple times, but this is probably not a good idea.
+ def primary_key(name, type = :primary_key, **options)
+ column(name, type, options.merge(primary_key: true))
+ end
+
+ # Appends a column or columns of a specified type.
+ #
+ # t.string(:goat)
+ # t.string(:goat, :sheep)
+ #
+ # See TableDefinition#column
+ [
+ :bigint,
+ :binary,
+ :boolean,
+ :date,
+ :datetime,
+ :decimal,
+ :float,
+ :integer,
+ :string,
+ :text,
+ :time,
+ :timestamp,
+ ].each do |column_type|
+ module_eval <<-CODE, __FILE__, __LINE__ + 1
+ def #{column_type}(*args, **options)
+ args.each { |name| column(name, :#{column_type}, options) }
+ end
+ CODE
+ end
+ end
+
# Represents the schema of an SQL table in an abstract way. This class
# provides methods for manipulating the schema representation.
#
- # Inside migration files, the +t+ object in +create_table+
+ # Inside migration files, the +t+ object in {create_table}[rdoc-ref:SchemaStatements#create_table]
# is actually of this type:
#
# class SomeMigration < ActiveRecord::Migration
@@ -75,16 +203,20 @@ module ActiveRecord
# end
#
# The table definitions
- # The Columns are stored as a ColumnDefinition in the +columns+ attribute.
+ # The Columns are stored as a ColumnDefinition in the #columns attribute.
class TableDefinition
+ include ColumnMethods
+
# An array of ColumnDefinition objects, representing the column changes
# that have been defined.
attr_accessor :indexes
- attr_reader :name, :temporary, :options, :as
+ attr_reader :name, :temporary, :options, :as, :foreign_keys, :native
def initialize(types, name, temporary, options, as = nil)
@columns_hash = {}
@indexes = {}
+ @foreign_keys = {}
+ @primary_keys = nil
@native = types
@temporary = temporary
@options = options
@@ -92,104 +224,37 @@ module ActiveRecord
@name = name
end
- def columns; @columns_hash.values; end
-
- # Appends a primary key definition to the table definition.
- # Can be called multiple times, but this is probably not a good idea.
- def primary_key(name, type = :primary_key, options = {})
- column(name, type, options.merge(:primary_key => true))
+ def primary_keys(name = nil) # :nodoc:
+ @primary_keys = PrimaryKeyDefinition.new(name) if name
+ @primary_keys
end
+ # Returns an array of ColumnDefinition objects for the columns of the table.
+ def columns; @columns_hash.values; end
+
# Returns a ColumnDefinition for the column with name +name+.
def [](name)
@columns_hash[name.to_s]
end
# Instantiates a new column for the table.
- # The +type+ parameter is normally one of the migrations native types,
- # which is one of the following:
- # <tt>:primary_key</tt>, <tt>:string</tt>, <tt>:text</tt>,
- # <tt>:integer</tt>, <tt>:float</tt>, <tt>:decimal</tt>,
- # <tt>:datetime</tt>, <tt>:timestamp</tt>, <tt>:time</tt>,
- # <tt>:date</tt>, <tt>:binary</tt>, <tt>:boolean</tt>.
- #
- # You may use a type not in this list as long as it is supported by your
- # database (for example, "polygon" in MySQL), but this will not be database
- # agnostic and should usually be avoided.
- #
- # Available options are (none of these exists by default):
- # * <tt>:limit</tt> -
- # Requests a maximum column length. This is number of characters for <tt>:string</tt> and
- # <tt>:text</tt> columns and number of bytes for <tt>:binary</tt> and <tt>:integer</tt> columns.
- # * <tt>:default</tt> -
- # The column's default value. Use nil for NULL.
- # * <tt>:null</tt> -
- # Allows or disallows +NULL+ values in the column. This option could
- # have been named <tt>:null_allowed</tt>.
- # * <tt>:precision</tt> -
- # Specifies the precision for a <tt>:decimal</tt> column.
- # * <tt>:scale</tt> -
- # Specifies the scale for a <tt>:decimal</tt> column.
+ # See {connection.add_column}[rdoc-ref:ConnectionAdapters::SchemaStatements#add_column]
+ # for available options.
+ #
+ # Additional options are:
# * <tt>:index</tt> -
# Create an index for the column. Can be either <tt>true</tt> or an options hash.
#
- # Note: The precision is the total number of significant digits
- # and the scale is the number of digits that can be stored following
- # the decimal point. For example, the number 123.45 has a precision of 5
- # and a scale of 2. A decimal with a precision of 5 and a scale of 2 can
- # range from -999.99 to 999.99.
- #
- # Please be aware of different RDBMS implementations behavior with
- # <tt>:decimal</tt> columns:
- # * The SQL standard says the default scale should be 0, <tt>:scale</tt> <=
- # <tt>:precision</tt>, and makes no comments about the requirements of
- # <tt>:precision</tt>.
- # * MySQL: <tt>:precision</tt> [1..63], <tt>:scale</tt> [0..30].
- # Default is (10,0).
- # * PostgreSQL: <tt>:precision</tt> [1..infinity],
- # <tt>:scale</tt> [0..infinity]. No default.
- # * SQLite2: Any <tt>:precision</tt> and <tt>:scale</tt> may be used.
- # Internal storage as strings. No default.
- # * SQLite3: No restrictions on <tt>:precision</tt> and <tt>:scale</tt>,
- # but the maximum supported <tt>:precision</tt> is 16. No default.
- # * Oracle: <tt>:precision</tt> [1..38], <tt>:scale</tt> [-84..127].
- # Default is (38,0).
- # * DB2: <tt>:precision</tt> [1..63], <tt>:scale</tt> [0..62].
- # Default unknown.
- # * SqlServer?: <tt>:precision</tt> [1..38], <tt>:scale</tt> [0..38].
- # Default (38,0).
- #
# This method returns <tt>self</tt>.
#
# == Examples
- # # Assuming +td+ is an instance of TableDefinition
- # td.column(:granted, :boolean)
- # # granted BOOLEAN
#
- # td.column(:picture, :binary, limit: 2.megabytes)
- # # => picture BLOB(2097152)
- #
- # td.column(:sales_stage, :string, limit: 20, default: 'new', null: false)
- # # => sales_stage VARCHAR(20) DEFAULT 'new' NOT NULL
- #
- # td.column(:bill_gates_money, :decimal, precision: 15, scale: 2)
- # # => bill_gates_money DECIMAL(15,2)
- #
- # td.column(:sensor_reading, :decimal, precision: 30, scale: 20)
- # # => sensor_reading DECIMAL(30,20)
- #
- # # While <tt>:scale</tt> defaults to zero on most databases, it
- # # probably wouldn't hurt to include it.
- # td.column(:huge_integer, :decimal, precision: 30)
- # # => huge_integer DECIMAL(30)
- #
- # # Defines a column with a database-specific type.
- # td.column(:foo, 'polygon')
- # # => foo polygon
+ # # Assuming +td+ is an instance of TableDefinition
+ # td.column(:granted, :boolean, index: true)
#
# == Short-hand examples
#
- # Instead of calling +column+ directly, you can also work with the short-hand definitions for the default types.
+ # Instead of calling #column directly, you can also work with the short-hand definitions for the default types.
# They use the type as the method name instead of as a parameter and allow for multiple columns to be defined
# in a single statement.
#
@@ -212,7 +277,7 @@ module ActiveRecord
# t.integer :shop_id, :creator_id
# t.string :item_number, index: true
# t.string :name, :value, default: "Untitled"
- # t.timestamps
+ # t.timestamps null: false
# end
#
# There's a short-hand method for each of the type values declared at the top. And then there's
@@ -221,7 +286,8 @@ module ActiveRecord
# TableDefinition#references will add an appropriately-named _id column, plus a corresponding _type
# column if the <tt>:polymorphic</tt> option is supplied. If <tt>:polymorphic</tt> is a hash of
# options, these will be used when creating the <tt>_type</tt> column. The <tt>:index</tt> option
- # will also create an index, similar to calling <tt>add_index</tt>. So what can be written like this:
+ # will also create an index, similar to calling {add_index}[rdoc-ref:ConnectionAdapters::SchemaStatements#add_index].
+ # So what can be written like this:
#
# create_table :taggings do |t|
# t.integer :tag_id, :tagger_id, :taggable_id
@@ -241,8 +307,9 @@ module ActiveRecord
def column(name, type, options = {})
name = name.to_s
type = type.to_sym
+ options = options.dup
- if primary_key_column_name == name
+ if @columns_hash[name] && @columns_hash[name].primary_key?
raise ArgumentError, "you can't redefine the primary key column '#{name}'. To define a custom primary key, pass { id: false } to create_table."
end
@@ -252,18 +319,12 @@ module ActiveRecord
self
end
+ # remove the column +name+ from the table.
+ # remove_column(:account_id)
def remove_column(name)
@columns_hash.delete name.to_s
end
- [:string, :text, :integer, :float, :decimal, :datetime, :timestamp, :time, :date, :binary, :boolean].each do |column_type|
- define_method column_type do |*args|
- options = args.extract_options!
- column_names = args
- column_names.each { |name| column(name, column_type, options) }
- end
- end
-
# Adds index options to the indexes hash, keyed by column name
# This is primarily used to track indexes that need to be created after the table
#
@@ -272,52 +333,53 @@ module ActiveRecord
indexes[column_name] = options
end
+ def foreign_key(table_name, options = {}) # :nodoc:
+ foreign_keys[table_name] = options
+ end
+
# Appends <tt>:datetime</tt> columns <tt>:created_at</tt> and
- # <tt>:updated_at</tt> to the table.
+ # <tt>:updated_at</tt> to the table. See {connection.add_timestamps}[rdoc-ref:SchemaStatements#add_timestamps]
+ #
+ # t.timestamps null: false
def timestamps(*args)
options = args.extract_options!
+
+ options[:null] = false if options[:null].nil?
+
column(:created_at, :datetime, options)
column(:updated_at, :datetime, options)
end
- # Adds a reference. Optionally adds a +type+ column, if <tt>:polymorphic</tt> option is provided.
- # <tt>references</tt> and <tt>belongs_to</tt> are acceptable. The reference column will be an +integer+
- # by default, the <tt>:type</tt> option can be used to specify a different type.
+ # Adds a reference.
#
# t.references(:user)
- # t.references(:user, type: "string")
- # t.belongs_to(:supplier, polymorphic: true)
+ # t.belongs_to(:supplier, foreign_key: true)
#
- # See SchemaStatements#add_reference
- def references(*args)
- options = args.extract_options!
- polymorphic = options.delete(:polymorphic)
- index_options = options.delete(:index)
- type = options.delete(:type) || :integer
+ # See {connection.add_reference}[rdoc-ref:SchemaStatements#add_reference] for details of the options you can use.
+ def references(*args, **options)
args.each do |col|
- column("#{col}_id", type, options)
- column("#{col}_type", :string, polymorphic.is_a?(Hash) ? polymorphic : options) if polymorphic
- index(polymorphic ? %w(id type).map { |t| "#{col}_#{t}" } : "#{col}_id", index_options.is_a?(Hash) ? index_options : {}) if index_options
+ ReferenceDefinition.new(col, **options).add_to(self)
end
end
alias :belongs_to :references
def new_column_definition(name, type, options) # :nodoc:
- type = aliased_types[type] || type
+ type = aliased_types(type.to_s, type)
column = create_column_definition name, type
limit = options.fetch(:limit) do
native[type][:limit] if native[type].is_a?(Hash)
end
column.limit = limit
- column.array = options[:array] if column.respond_to?(:array)
column.precision = options[:precision]
column.scale = options[:scale]
column.default = options[:default]
column.null = options[:null]
column.first = options[:first]
column.after = options[:after]
+ column.auto_increment = options[:auto_increment]
column.primary_key = type == :primary_key || options[:primary_key]
+ column.collation = options[:collation]
column
end
@@ -326,19 +388,8 @@ module ActiveRecord
ColumnDefinition.new name, type
end
- def primary_key_column_name
- primary_key_column = columns.detect { |c| c.primary_key? }
- primary_key_column && primary_key_column.name
- end
-
- def native
- @native
- end
-
- def aliased_types
- HashWithIndifferentAccess.new(
- timestamp: :datetime,
- )
+ def aliased_types(name, fallback)
+ 'timestamp' == name ? :datetime : fallback
end
end
@@ -367,16 +418,17 @@ module ActiveRecord
def add_column(name, type, options)
name = name.to_s
type = type.to_sym
- @adds << @td.new_column_definition(name, type, options)
+ @adds << AddColumnDefinition.new(@td.new_column_definition(name, type, options))
end
end
# Represents an SQL table in an abstract way for updating a table.
- # Also see TableDefinition and SchemaStatements#create_table
+ # Also see TableDefinition and {connection.create_table}[rdoc-ref:SchemaStatements#create_table]
#
# Available transformations are:
#
# change_table :table do |t|
+ # t.primary_key
# t.column
# t.index
# t.rename_index
@@ -389,6 +441,7 @@ module ActiveRecord
# t.string
# t.text
# t.integer
+ # t.bigint
# t.float
# t.decimal
# t.datetime
@@ -405,153 +458,178 @@ module ActiveRecord
# end
#
class Table
+ include ColumnMethods
+
+ attr_reader :name
+
def initialize(table_name, base)
- @table_name = table_name
+ @name = table_name
@base = base
end
# Adds a new column to the named table.
- # See TableDefinition#column for details of the options you can use.
#
- # ====== Creating a simple column
# t.column(:name, :string)
+ #
+ # See TableDefinition#column for details of the options you can use.
def column(column_name, type, options = {})
- @base.add_column(@table_name, column_name, type, options)
+ @base.add_column(name, column_name, type, options)
end
- # Checks to see if a column exists. See SchemaStatements#column_exists?
+ # Checks to see if a column exists.
+ #
+ # t.string(:name) unless t.column_exists?(:name, :string)
+ #
+ # See {connection.column_exists?}[rdoc-ref:SchemaStatements#column_exists?]
def column_exists?(column_name, type = nil, options = {})
- @base.column_exists?(@table_name, column_name, type, options)
+ @base.column_exists?(name, column_name, type, options)
end
# Adds a new index to the table. +column_name+ can be a single Symbol, or
- # an Array of Symbols. See SchemaStatements#add_index
+ # an Array of Symbols.
#
- # ====== Creating a simple index
# t.index(:name)
- # ====== Creating a unique index
# t.index([:branch_id, :party_id], unique: true)
- # ====== Creating a named index
# t.index([:branch_id, :party_id], unique: true, name: 'by_branch_party')
+ #
+ # See {connection.add_index}[rdoc-ref:SchemaStatements#add_index] for details of the options you can use.
def index(column_name, options = {})
- @base.add_index(@table_name, column_name, options)
+ @base.add_index(name, column_name, options)
end
- # Checks to see if an index exists. See SchemaStatements#index_exists?
+ # Checks to see if an index exists.
+ #
+ # unless t.index_exists?(:branch_id)
+ # t.index(:branch_id)
+ # end
+ #
+ # See {connection.index_exists?}[rdoc-ref:SchemaStatements#index_exists?]
def index_exists?(column_name, options = {})
- @base.index_exists?(@table_name, column_name, options)
+ @base.index_exists?(name, column_name, options)
end
# Renames the given index on the table.
#
# t.rename_index(:user_id, :account_id)
+ #
+ # See {connection.rename_index}[rdoc-ref:SchemaStatements#rename_index]
def rename_index(index_name, new_index_name)
- @base.rename_index(@table_name, index_name, new_index_name)
+ @base.rename_index(name, index_name, new_index_name)
end
- # Adds timestamps (+created_at+ and +updated_at+) columns to the table. See SchemaStatements#add_timestamps
+ # Adds timestamps (+created_at+ and +updated_at+) columns to the table.
#
- # t.timestamps
- def timestamps
- @base.add_timestamps(@table_name)
+ # t.timestamps(null: false)
+ #
+ # See {connection.add_timestamps}[rdoc-ref:SchemaStatements#add_timestamps]
+ def timestamps(options = {})
+ @base.add_timestamps(name, options)
end
# Changes the column's definition according to the new options.
- # See TableDefinition#column for details of the options you can use.
#
# t.change(:name, :string, limit: 80)
# t.change(:description, :text)
+ #
+ # See TableDefinition#column for details of the options you can use.
def change(column_name, type, options = {})
- @base.change_column(@table_name, column_name, type, options)
+ @base.change_column(name, column_name, type, options)
end
- # Sets a new default value for a column. See SchemaStatements#change_column_default
+ # Sets a new default value for a column.
#
# t.change_default(:qualification, 'new')
# t.change_default(:authorized, 1)
- def change_default(column_name, default)
- @base.change_column_default(@table_name, column_name, default)
+ # t.change_default(:status, from: nil, to: "draft")
+ #
+ # See {connection.change_column_default}[rdoc-ref:SchemaStatements#change_column_default]
+ def change_default(column_name, default_or_changes)
+ @base.change_column_default(name, column_name, default_or_changes)
end
# Removes the column(s) from the table definition.
#
# t.remove(:qualification)
# t.remove(:qualification, :experience)
+ #
+ # See {connection.remove_columns}[rdoc-ref:SchemaStatements#remove_columns]
def remove(*column_names)
- @base.remove_columns(@table_name, *column_names)
+ @base.remove_columns(name, *column_names)
end
# Removes the given index from the table.
#
- # ====== Remove the index_table_name_on_column in the table_name table
- # t.remove_index :column
- # ====== Remove the index named index_table_name_on_branch_id in the table_name table
- # t.remove_index column: :branch_id
- # ====== Remove the index named index_table_name_on_branch_id_and_party_id in the table_name table
- # t.remove_index column: [:branch_id, :party_id]
- # ====== Remove the index named by_branch_party in the table_name table
- # t.remove_index name: :by_branch_party
+ # t.remove_index(:branch_id)
+ # t.remove_index(column: [:branch_id, :party_id])
+ # t.remove_index(name: :by_branch_party)
+ #
+ # See {connection.remove_index}[rdoc-ref:SchemaStatements#remove_index]
def remove_index(options = {})
- @base.remove_index(@table_name, options)
+ @base.remove_index(name, options)
end
# Removes the timestamp columns (+created_at+ and +updated_at+) from the table.
#
# t.remove_timestamps
- def remove_timestamps
- @base.remove_timestamps(@table_name)
+ #
+ # See {connection.remove_timestamps}[rdoc-ref:SchemaStatements#remove_timestamps]
+ def remove_timestamps(options = {})
+ @base.remove_timestamps(name, options)
end
# Renames a column.
#
# t.rename(:description, :name)
+ #
+ # See {connection.rename_column}[rdoc-ref:SchemaStatements#rename_column]
def rename(column_name, new_column_name)
- @base.rename_column(@table_name, column_name, new_column_name)
+ @base.rename_column(name, column_name, new_column_name)
end
- # Adds a reference. Optionally adds a +type+ column, if <tt>:polymorphic</tt> option is provided.
- # <tt>references</tt> and <tt>belongs_to</tt> are acceptable. The reference column will be an +integer+
- # by default, the <tt>:type</tt> option can be used to specify a different type.
+ # Adds a reference.
#
# t.references(:user)
- # t.references(:user, type: "string")
- # t.belongs_to(:supplier, polymorphic: true)
+ # t.belongs_to(:supplier, foreign_key: true)
#
- # See SchemaStatements#add_reference
+ # See {connection.add_reference}[rdoc-ref:SchemaStatements#add_reference] for details of the options you can use.
def references(*args)
options = args.extract_options!
args.each do |ref_name|
- @base.add_reference(@table_name, ref_name, options)
+ @base.add_reference(name, ref_name, options)
end
end
alias :belongs_to :references
# Removes a reference. Optionally removes a +type+ column.
- # <tt>remove_references</tt> and <tt>remove_belongs_to</tt> are acceptable.
#
# t.remove_references(:user)
# t.remove_belongs_to(:supplier, polymorphic: true)
#
- # See SchemaStatements#remove_reference
+ # See {connection.remove_reference}[rdoc-ref:SchemaStatements#remove_reference]
def remove_references(*args)
options = args.extract_options!
args.each do |ref_name|
- @base.remove_reference(@table_name, ref_name, options)
+ @base.remove_reference(name, ref_name, options)
end
end
alias :remove_belongs_to :remove_references
- # Adds a column or columns of a specified type
+ # Adds a foreign key.
#
- # t.string(:goat)
- # t.string(:goat, :sheep)
- [:string, :text, :integer, :float, :decimal, :datetime, :timestamp, :time, :date, :binary, :boolean].each do |column_type|
- define_method column_type do |*args|
- options = args.extract_options!
- args.each do |name|
- @base.add_column(@table_name, name, column_type, options)
- end
- end
+ # t.foreign_key(:authors)
+ #
+ # See {connection.add_foreign_key}[rdoc-ref:SchemaStatements#add_foreign_key]
+ def foreign_key(*args) # :nodoc:
+ @base.add_foreign_key(name, *args)
+ end
+
+ # Checks to see if a foreign key exists.
+ #
+ # t.foreign_key(:authors) unless t.foreign_key_exists?(:authors)
+ #
+ # See {connection.foreign_key_exists?}[rdoc-ref:SchemaStatements#foreign_key_exists?]
+ def foreign_key_exists?(*args) # :nodoc:
+ @base.foreign_key_exists?(name, *args)
end
private
@@ -559,6 +637,5 @@ module ActiveRecord
@base.native_database_types
end
end
-
end
end
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 9bd0401e40..e252ddb4cf 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb
@@ -6,41 +6,84 @@ module ActiveRecord
# We can then redefine how certain data types may be handled in the schema dumper on the
# Adapter level by over-writing this code inside the database specific adapters
module ColumnDumper
- def column_spec(column, types)
- spec = prepare_column_options(column, types)
- (spec.keys - [:name, :type]).each{ |k| spec[k].insert(0, "#{k.to_s}: ")}
+ def column_spec(column)
+ spec = prepare_column_options(column)
+ (spec.keys - [:name, :type]).each{ |k| spec[k].insert(0, "#{k}: ")}
spec
end
- # This can be overridden on a Adapter level basis to support other
+ def column_spec_for_primary_key(column)
+ return if column.type == :integer
+ spec = { id: column.type.inspect }
+ spec.merge!(prepare_column_options(column).delete_if { |key, _| [:name, :type].include?(key) })
+ end
+
+ # This can be overridden on an Adapter level basis to support other
# extended datatypes (Example: Adding an array option in the
- # PostgreSQLAdapter)
- def prepare_column_options(column, types)
+ # PostgreSQL::ColumnDumper)
+ def prepare_column_options(column)
spec = {}
spec[:name] = column.name.inspect
- spec[:type] = column.type.to_s
- spec[:limit] = column.limit.inspect if column.limit != types[column.type][:limit]
- spec[:precision] = column.precision.inspect if column.precision
- spec[:scale] = column.scale.inspect if column.scale
+ spec[:type] = schema_type(column)
spec[:null] = 'false' unless column.null
- spec[:default] = schema_default(column) if column.has_default?
- spec.delete(:default) if spec[:default].nil?
+
+ if limit = schema_limit(column)
+ spec[:limit] = limit
+ end
+
+ if precision = schema_precision(column)
+ spec[:precision] = precision
+ end
+
+ if scale = schema_scale(column)
+ spec[:scale] = scale
+ end
+
+ default = schema_default(column) if column.has_default?
+ spec[:default] = default unless default.nil?
+
+ if collation = schema_collation(column)
+ spec[:collation] = collation
+ end
+
spec
end
# Lists the valid migration options
def migration_keys
- [:name, :limit, :precision, :scale, :default, :null]
+ [:name, :limit, :precision, :scale, :default, :null, :collation]
end
private
+ def schema_type(column)
+ column.type.to_s
+ end
+
+ def schema_limit(column)
+ limit = column.limit || native_database_types[column.type][:limit]
+ limit.inspect if limit
+ end
+
+ def schema_precision(column)
+ column.precision.inspect if column.precision
+ end
+
+ def schema_scale(column)
+ column.scale.inspect if column.scale
+ end
+
def schema_default(column)
- default = column.type_cast_from_database(column.default)
+ type = lookup_cast_type_from_column(column)
+ default = type.deserialize(column.default)
unless default.nil?
- column.type_cast_for_schema(default)
+ type.type_cast_for_schema(default)
end
end
+
+ def schema_collation(column)
+ column.collation.inspect if column.collation
+ end
end
end
end
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 10753defc2..d5f8dbc8fc 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
@@ -1,4 +1,6 @@
require 'active_record/migration/join_table'
+require 'active_support/core_ext/string/access'
+require 'digest'
module ActiveRecord
module ConnectionAdapters # :nodoc:
@@ -12,11 +14,34 @@ module ActiveRecord
{}
end
+ def table_options(table_name)
+ nil
+ end
+
# Truncates a table alias according to the limits of the current adapter.
def table_alias_for(table_name)
table_name[0...table_alias_length].tr('.', '_')
end
+ # Returns the relation names useable to back Active Record models.
+ # For most adapters this means all #tables and #views.
+ def data_sources
+ tables | views
+ end
+
+ # Checks to see if the data source +name+ exists on the database.
+ #
+ # data_source_exists?(:ebooks)
+ #
+ def data_source_exists?(name)
+ data_sources.include?(name.to_s)
+ end
+
+ # Returns an array of table names defined in the database.
+ def tables(name = nil)
+ raise NotImplementedError, "#tables is not implemented"
+ end
+
# Checks to see if the table +table_name+ exists on the database.
#
# table_exists?(:developers)
@@ -25,6 +50,19 @@ module ActiveRecord
tables.include?(table_name.to_s)
end
+ # Returns an array of view names defined in the database.
+ def views
+ raise NotImplementedError, "#views is not implemented"
+ end
+
+ # Checks to see if the view +view_name+ exists on the database.
+ #
+ # view_exists?(:ebooks)
+ #
+ def view_exists?(view_name)
+ views.include?(view_name.to_s)
+ end
+
# Returns an array of indexes for the given table.
# def indexes(table_name, name = nil) end
@@ -43,13 +81,14 @@ module ActiveRecord
# index_exists?(:suppliers, :company_id, name: "idx_company_id")
#
def index_exists?(table_name, column_name, options = {})
- column_names = Array(column_name)
- index_name = options.key?(:name) ? options[:name].to_s : index_name(table_name, :column => column_names)
- if options[:unique]
- indexes(table_name).any?{ |i| i.unique && i.name == index_name }
- else
- indexes(table_name).any?{ |i| i.name == index_name }
- end
+ column_names = Array(column_name).map(&:to_s)
+ index_name = options.key?(:name) ? options[:name].to_s : index_name(table_name, column: column_names)
+ checks = []
+ checks << lambda { |i| i.name == index_name }
+ checks << lambda { |i| i.columns == column_names }
+ checks << lambda { |i| i.unique } if options[:unique]
+
+ indexes(table_name).any? { |i| checks.all? { |check| check[i] } }
end
# Returns an array of Column objects for the table specified by +table_name+.
@@ -81,10 +120,16 @@ module ActiveRecord
(!options.key?(:null) || c.null == options[:null]) }
end
+ # Returns just a table's primary key
+ def primary_key(table_name)
+ pks = primary_keys(table_name)
+ pks.first if pks.one?
+ end
+
# Creates a new table with the name +table_name+. +table_name+ may either
# be a String or a Symbol.
#
- # There are two ways to work with +create_table+. You can use the block
+ # There are two ways to work with #create_table. You can use the block
# form or the regular form, like this:
#
# === Block form
@@ -116,13 +161,16 @@ module ActiveRecord
# The +options+ hash can include the following keys:
# [<tt>:id</tt>]
# Whether to automatically add a primary key column. Defaults to true.
- # Join tables for +has_and_belongs_to_many+ should set it to false.
+ # Join tables for {ActiveRecord::Base.has_and_belongs_to_many}[rdoc-ref:Associations::ClassMethods#has_and_belongs_to_many] should set it to false.
+ #
+ # A Symbol can be used to specify the type of the generated primary key column.
# [<tt>:primary_key</tt>]
# The name of the primary key, if one is to be added automatically.
# Defaults to +id+. If <tt>:id</tt> is false this option is ignored.
#
# Note that Active Record models will automatically detect their
- # primary key. This can be avoided by using +self.primary_key=+ on the model
+ # primary key. This can be avoided by using
+ # {self.primary_key=}[rdoc-ref:AttributeMethods::PrimaryKey::ClassMethods#primary_key=] on the model
# to define the key explicitly.
#
# [<tt>:options</tt>]
@@ -131,6 +179,7 @@ module ActiveRecord
# Make a temporary table.
# [<tt>:force</tt>]
# Set to true to drop the table before creating it.
+ # Set to +:cascade+ to drop dependent objects as well.
# Defaults to false.
# [<tt>:as</tt>]
# SQL to use to generate the table. When this option is used, the block is
@@ -143,7 +192,7 @@ module ActiveRecord
# generates:
#
# CREATE TABLE suppliers (
- # id int(11) DEFAULT NULL auto_increment PRIMARY KEY
+ # id int auto_increment PRIMARY KEY
# ) ENGINE=InnoDB DEFAULT CHARSET=utf8
#
# ====== Rename the primary key column
@@ -155,10 +204,23 @@ module ActiveRecord
# generates:
#
# CREATE TABLE objects (
- # guid int(11) DEFAULT NULL auto_increment PRIMARY KEY,
+ # guid int auto_increment PRIMARY KEY,
# name varchar(80)
# )
#
+ # ====== Change the primary key column type
+ #
+ # create_table(:tags, id: :string) do |t|
+ # t.column :label, :string
+ # end
+ #
+ # generates:
+ #
+ # CREATE TABLE tags (
+ # id varchar PRIMARY KEY,
+ # label varchar
+ # )
+ #
# ====== Do not add a primary key column
#
# create_table(:categories_suppliers, id: false) do |t|
@@ -192,7 +254,11 @@ module ActiveRecord
Base.get_primary_key table_name.to_s.singularize
end
- td.primary_key pk, options.fetch(:id, :primary_key), options
+ if pk.is_a?(Array)
+ td.primary_keys pk
+ else
+ td.primary_key pk, options.fetch(:id, :primary_key), options
+ end
end
yield td if block_given?
@@ -202,7 +268,13 @@ module ActiveRecord
end
result = execute schema_creation.accept td
- td.indexes.each_pair { |c, o| add_index(table_name, c, o) } unless supports_indexes_in_create?
+
+ unless supports_indexes_in_create?
+ td.indexes.each_pair do |column_name, index_options|
+ add_index(table_name, column_name, index_options)
+ end
+ end
+
result
end
@@ -225,7 +297,7 @@ module ActiveRecord
# Set to true to drop the table before creating it.
# Defaults to false.
#
- # Note that +create_join_table+ does not create any indices by default; you can use
+ # Note that #create_join_table does not create any indices by default; you can use
# its block form to do so yourself:
#
# create_join_table :products, :categories do |t|
@@ -260,11 +332,11 @@ module ActiveRecord
end
# Drops the join table specified by the given arguments.
- # See +create_join_table+ for details.
+ # See #create_join_table for details.
#
# Although this command ignores the block if one is given, it can be helpful
# to provide one in a migration's +change+ method so it can be reverted.
- # In that case, the block will be used by create_join_table.
+ # In that case, the block will be used by #create_join_table.
def drop_join_table(table_1, table_2, options = {})
join_table_name = find_join_table_name(table_1, table_2, options)
drop_table(join_table_name)
@@ -282,7 +354,7 @@ module ActiveRecord
# [<tt>:bulk</tt>]
# Set this to true to make this a bulk alter query, such as
#
- # ALTER TABLE `users` ADD COLUMN age INT(11), ADD COLUMN birthdate DATETIME ...
+ # ALTER TABLE `users` ADD COLUMN age INT, ADD COLUMN birthdate DATETIME ...
#
# Defaults to false.
#
@@ -360,15 +432,95 @@ module ActiveRecord
# Drops a table from the database.
#
- # Although this command ignores +options+ and the block if one is given, it can be helpful
- # to provide these in a migration's +change+ method so it can be reverted.
- # In that case, +options+ and the block will be used by create_table.
+ # [<tt>:force</tt>]
+ # Set to +:cascade+ to drop dependent objects as well.
+ # Defaults to false.
+ # [<tt>:if_exists</tt>]
+ # Set to +true+ to only drop the table if it exists.
+ # Defaults to false.
+ #
+ # Although this command ignores most +options+ and the block if one is given,
+ # it can be helpful to provide these in a migration's +change+ method so it can be reverted.
+ # In that case, +options+ and the block will be used by #create_table.
def drop_table(table_name, options = {})
- execute "DROP TABLE #{quote_table_name(table_name)}"
+ execute "DROP TABLE#{' IF EXISTS' if options[:if_exists]} #{quote_table_name(table_name)}"
end
- # Adds a new column to the named table.
- # See TableDefinition#column for details of the options you can use.
+ # Add a new +type+ column named +column_name+ to +table_name+.
+ #
+ # The +type+ parameter is normally one of the migrations native types,
+ # which is one of the following:
+ # <tt>:primary_key</tt>, <tt>:string</tt>, <tt>:text</tt>,
+ # <tt>:integer</tt>, <tt>:bigint</tt>, <tt>:float</tt>, <tt>:decimal</tt>,
+ # <tt>:datetime</tt>, <tt>:time</tt>, <tt>:date</tt>,
+ # <tt>:binary</tt>, <tt>:boolean</tt>.
+ #
+ # You may use a type not in this list as long as it is supported by your
+ # database (for example, "polygon" in MySQL), but this will not be database
+ # agnostic and should usually be avoided.
+ #
+ # Available options are (none of these exists by default):
+ # * <tt>:limit</tt> -
+ # Requests a maximum column length. This is number of characters for a <tt>:string</tt> column
+ # and number of bytes for <tt>:text</tt>, <tt>:binary</tt> and <tt>:integer</tt> columns.
+ # * <tt>:default</tt> -
+ # The column's default value. Use nil for NULL.
+ # * <tt>:null</tt> -
+ # Allows or disallows +NULL+ values in the column. This option could
+ # have been named <tt>:null_allowed</tt>.
+ # * <tt>:precision</tt> -
+ # Specifies the precision for a <tt>:decimal</tt> column.
+ # * <tt>:scale</tt> -
+ # Specifies the scale for a <tt>:decimal</tt> column.
+ #
+ # Note: The precision is the total number of significant digits
+ # and the scale is the number of digits that can be stored following
+ # the decimal point. For example, the number 123.45 has a precision of 5
+ # and a scale of 2. A decimal with a precision of 5 and a scale of 2 can
+ # range from -999.99 to 999.99.
+ #
+ # Please be aware of different RDBMS implementations behavior with
+ # <tt>:decimal</tt> columns:
+ # * The SQL standard says the default scale should be 0, <tt>:scale</tt> <=
+ # <tt>:precision</tt>, and makes no comments about the requirements of
+ # <tt>:precision</tt>.
+ # * MySQL: <tt>:precision</tt> [1..63], <tt>:scale</tt> [0..30].
+ # Default is (10,0).
+ # * PostgreSQL: <tt>:precision</tt> [1..infinity],
+ # <tt>:scale</tt> [0..infinity]. No default.
+ # * SQLite2: Any <tt>:precision</tt> and <tt>:scale</tt> may be used.
+ # Internal storage as strings. No default.
+ # * SQLite3: No restrictions on <tt>:precision</tt> and <tt>:scale</tt>,
+ # but the maximum supported <tt>:precision</tt> is 16. No default.
+ # * Oracle: <tt>:precision</tt> [1..38], <tt>:scale</tt> [-84..127].
+ # Default is (38,0).
+ # * DB2: <tt>:precision</tt> [1..63], <tt>:scale</tt> [0..62].
+ # Default unknown.
+ # * SqlServer?: <tt>:precision</tt> [1..38], <tt>:scale</tt> [0..38].
+ # Default (38,0).
+ #
+ # == Examples
+ #
+ # add_column(:users, :picture, :binary, limit: 2.megabytes)
+ # # ALTER TABLE "users" ADD "picture" blob(2097152)
+ #
+ # add_column(:articles, :status, :string, limit: 20, default: 'draft', null: false)
+ # # ALTER TABLE "articles" ADD "status" varchar(20) DEFAULT 'draft' NOT NULL
+ #
+ # add_column(:answers, :bill_gates_money, :decimal, precision: 15, scale: 2)
+ # # ALTER TABLE "answers" ADD "bill_gates_money" decimal(15,2)
+ #
+ # add_column(:measurements, :sensor_reading, :decimal, precision: 30, scale: 20)
+ # # ALTER TABLE "measurements" ADD "sensor_reading" decimal(30,20)
+ #
+ # # While :scale defaults to zero on most databases, it
+ # # probably wouldn't hurt to include it.
+ # add_column(:measurements, :huge_integer, :decimal, precision: 30)
+ # # ALTER TABLE "measurements" ADD "huge_integer" decimal(30)
+ #
+ # # Defines a column with a database-specific type.
+ # add_column(:shapes, :triangle, 'polygon')
+ # # ALTER TABLE "shapes" ADD "triangle" polygon
def add_column(table_name, column_name, type, options = {})
at = create_alter_table table_name
at.add_column(column_name, type, options)
@@ -416,11 +568,16 @@ module ActiveRecord
#
# change_column_default(:users, :email, nil)
#
- def change_column_default(table_name, column_name, default)
+ # Passing a hash containing +:from+ and +:to+ will make this change
+ # reversible in migration:
+ #
+ # change_column_default(:posts, :state, from: nil, to: "draft")
+ #
+ def change_column_default(table_name, column_name, default_or_changes)
raise NotImplementedError, "change_column_default is not implemented"
end
- # Sets or removes a +NOT NULL+ constraint on a column. The +null+ flag
+ # Sets or removes a <tt>NOT NULL</tt> constraint on a column. The +null+ flag
# indicates whether the value can be +NULL+. For example
#
# change_column_null(:users, :nickname, false)
@@ -432,7 +589,7 @@ module ActiveRecord
# allows them to be +NULL+ (drops the constraint).
#
# The method accepts an optional fourth argument to replace existing
- # +NULL+s with some other value. Use that one when enabling the
+ # <tt>NULL</tt>s with some other value. Use that one when enabling the
# constraint if needed, since otherwise those rows would not be valid.
#
# Please note the fourth argument does not set a column's default.
@@ -486,6 +643,8 @@ module ActiveRecord
#
# CREATE INDEX by_name ON accounts(name(10))
#
+ # ====== Creating an index with specific key lengths for multiple keys
+ #
# add_index(:accounts, [:name, :surname], name: 'by_name_surname', length: {name: 10, surname: 15})
#
# generates:
@@ -512,6 +671,8 @@ module ActiveRecord
#
# CREATE UNIQUE INDEX index_accounts_on_branch_id_and_party_id ON accounts(branch_id, party_id) WHERE active
#
+ # Note: Partial indexes are only supported for PostgreSQL and SQLite 3.8.0+.
+ #
# ====== Creating an index with a specific method
#
# add_index(:developers, :name, using: 'btree')
@@ -541,7 +702,7 @@ module ActiveRecord
#
# Removes the +index_accounts_on_column+ in the +accounts+ table.
#
- # remove_index :accounts, :column
+ # remove_index :accounts, :branch_id
#
# Removes the index named +index_accounts_on_branch_id+ in the +accounts+ table.
#
@@ -556,10 +717,7 @@ module ActiveRecord
# remove_index :accounts, name: :by_branch_party
#
def remove_index(table_name, options = {})
- remove_index!(table_name, index_name_for_remove(table_name, options))
- end
-
- def remove_index!(table_name, index_name) #:nodoc:
+ index_name = index_name_for_remove(table_name, options)
execute "DROP INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)}"
end
@@ -570,6 +728,8 @@ module ActiveRecord
# rename_index :people, 'index_people_on_last_name', 'index_users_on_last_name'
#
def rename_index(table_name, old_name, new_name)
+ validate_index_length!(table_name, new_name)
+
# this is a naive implementation; some DBs may support this more efficiently (Postgres, for instance)
old_index_def = indexes(table_name).detect { |i| i.name == old_name }
return unless old_index_def
@@ -601,10 +761,22 @@ module ActiveRecord
indexes(table_name).detect { |i| i.name == index_name }
end
- # Adds a reference. Optionally adds a +type+ column, if <tt>:polymorphic</tt> option is provided.
- # The reference column is an +integer+ by default, the <tt>:type</tt> option can be used to specify
- # a different type.
- # <tt>add_reference</tt> and <tt>add_belongs_to</tt> are acceptable.
+ # Adds a reference. The reference column is an integer 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+.
+ # [<tt>:index</tt>]
+ # Add an appropriate index. Defaults to false.
+ # [<tt>:foreign_key</tt>]
+ # Add an appropriate foreign key constraint. Defaults to false.
+ # [<tt>:polymorphic</tt>]
+ # Whether an additional +_type+ column should be added. Defaults to false.
+ # [<tt>:null</tt>]
+ # Whether the column allows nulls. Defaults to true.
#
# ====== Create a user_id integer column
#
@@ -614,26 +786,25 @@ module ActiveRecord
#
# add_reference(:products, :user, type: :string)
#
- # ====== Create a supplier_id and supplier_type columns
+ # ====== Create supplier_id, supplier_type columns and appropriate index
#
- # add_belongs_to(:products, :supplier, polymorphic: true)
+ # add_reference(:products, :supplier, polymorphic: true, index: true)
#
- # ====== Create a supplier_id, supplier_type columns and appropriate index
+ # ====== Create a supplier_id column and appropriate foreign key
#
- # add_reference(:products, :supplier, polymorphic: true, index: true)
+ # add_reference(:products, :supplier, foreign_key: true)
+ #
+ # ====== Create a supplier_id column and a foreign key to the firms table
#
- def add_reference(table_name, ref_name, options = {})
- polymorphic = options.delete(:polymorphic)
- index_options = options.delete(:index)
- type = options.delete(:type) || :integer
- add_column(table_name, "#{ref_name}_id", type, options)
- add_column(table_name, "#{ref_name}_type", :string, polymorphic.is_a?(Hash) ? polymorphic : options) if polymorphic
- add_index(table_name, polymorphic ? %w[id type].map{ |t| "#{ref_name}_#{t}" } : "#{ref_name}_id", index_options.is_a?(Hash) ? index_options : {}) if index_options
+ # add_reference(:products, :supplier, foreign_key: {to_table: :firms})
+ #
+ def add_reference(table_name, *args)
+ ReferenceDefinition.new(*args).add_to(update_table_definition(table_name, self))
end
alias :add_belongs_to :add_reference
# Removes the reference(s). Also removes a +type+ column if one exists.
- # <tt>remove_reference</tt>, <tt>remove_references</tt> and <tt>remove_belongs_to</tt> are acceptable.
+ # #remove_reference and #remove_belongs_to are acceptable.
#
# ====== Remove the reference
#
@@ -643,14 +814,23 @@ module ActiveRecord
#
# remove_reference(:products, :supplier, polymorphic: true)
#
+ # ====== Remove the reference with a foreign key
+ #
+ # remove_reference(:products, :user, index: true, foreign_key: true)
+ #
def remove_reference(table_name, ref_name, options = {})
+ if options[:foreign_key]
+ reference_name = Base.pluralize_table_names ? ref_name.to_s.pluralize : ref_name
+ remove_foreign_key(table_name, reference_name)
+ end
+
remove_column(table_name, "#{ref_name}_id")
remove_column(table_name, "#{ref_name}_type") if options[:polymorphic]
end
alias :remove_belongs_to :remove_reference
# Returns an array of foreign keys for the given table.
- # The foreign keys are represented as +ForeignKeyDefinition+ objects.
+ # The foreign keys are represented as ForeignKeyDefinition objects.
def foreign_keys(table_name)
raise NotImplementedError, "foreign_keys is not implemented"
end
@@ -659,8 +839,8 @@ module ActiveRecord
# +to_table+ contains the referenced primary key.
#
# The foreign key will be named after the following pattern: <tt>fk_rails_<identifier></tt>.
- # +identifier+ is a 10 character long random string. A custom name can be specified with
- # the <tt>:name</tt> option.
+ # +identifier+ is a 10 character long string which is deterministically generated from the
+ # +from_table+ and +column+. A custom name can be specified with the <tt>:name</tt> option.
#
# ====== Creating a simple foreign key
#
@@ -694,28 +874,23 @@ module ActiveRecord
# [<tt>:name</tt>]
# The constraint name. Defaults to <tt>fk_rails_<identifier></tt>.
# [<tt>:on_delete</tt>]
- # Action that happens <tt>ON DELETE</tt>. Valid values are +:nullify+, +:cascade:+ and +:restrict+
+ # 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+
+ # Action that happens <tt>ON UPDATE</tt>. Valid values are +:nullify+, +:cascade+ and +:restrict+
def add_foreign_key(from_table, to_table, options = {})
return unless supports_foreign_keys?
- options[:column] ||= foreign_key_column_for(to_table)
-
- options = {
- column: options[:column],
- primary_key: options[:primary_key],
- name: foreign_key_name(from_table, options),
- on_delete: options[:on_delete],
- on_update: options[:on_update]
- }
+ options = foreign_key_options(from_table, to_table, options)
at = create_alter_table from_table
at.add_foreign_key to_table, options
execute schema_creation.accept(at)
end
- # Removes the given foreign key from the table.
+ # Removes the given foreign key from the table. Any option parameters provided
+ # will be used to re-add the foreign key in case of a migration rollback.
+ # It is recommended that you provide any options used when creating the foreign
+ # key so that the migration can be reverted properly.
#
# Removes the foreign key on +accounts.branch_id+.
#
@@ -729,24 +904,11 @@ module ActiveRecord
#
# remove_foreign_key :accounts, name: :special_fk_name
#
+ # The +options+ hash accepts the same keys as SchemaStatements#add_foreign_key.
def remove_foreign_key(from_table, options_or_to_table = {})
return unless supports_foreign_keys?
- if options_or_to_table.is_a?(Hash)
- options = options_or_to_table
- else
- options = { column: foreign_key_column_for(options_or_to_table) }
- end
-
- fk_name_to_delete = options.fetch(:name) do
- fk_to_delete = foreign_keys(from_table).detect {|fk| fk.column == options[:column] }
-
- if fk_to_delete
- fk_to_delete.name
- else
- raise ArgumentError, "Table '#{from_table}' has no foreign key on column '#{options[:column]}'"
- end
- end
+ fk_name_to_delete = foreign_key_for!(from_table, options_or_to_table).name
at = create_alter_table from_table
at.drop_foreign_key fk_name_to_delete
@@ -754,8 +916,43 @@ module ActiveRecord
execute schema_creation.accept(at)
end
+ # Checks to see if a foreign key exists on a table for a given foreign key definition.
+ #
+ # # Check a foreign key exists
+ # foreign_key_exists?(:accounts, :branches)
+ #
+ # # Check a foreign key on a specified column exists
+ # foreign_key_exists?(:accounts, column: :owner_id)
+ #
+ # # Check a foreign key with a custom name exists
+ # foreign_key_exists?(:accounts, name: "special_fk_name")
+ #
+ def foreign_key_exists?(from_table, options_or_to_table = {})
+ foreign_key_for(from_table, options_or_to_table).present?
+ end
+
+ def foreign_key_for(from_table, options_or_to_table = {}) # :nodoc:
+ return unless supports_foreign_keys?
+ foreign_keys(from_table).detect {|fk| fk.defined_for? options_or_to_table }
+ end
+
+ def foreign_key_for!(from_table, options_or_to_table = {}) # :nodoc:
+ foreign_key_for(from_table, options_or_to_table) or \
+ raise ArgumentError, "Table '#{from_table}' has no foreign key for #{options_or_to_table}"
+ end
+
def foreign_key_column_for(table_name) # :nodoc:
- "#{table_name.to_s.singularize}_id"
+ prefix = Base.table_name_prefix
+ suffix = Base.table_name_suffix
+ name = table_name.to_s =~ /#{prefix}(.+)#{suffix}/ ? $1 : table_name.to_s
+ "#{name.singularize}_id"
+ end
+
+ def foreign_key_options(from_table, to_table, options) # :nodoc:
+ options = options.dup
+ options[:column] ||= foreign_key_column_for(to_table)
+ options[:name] ||= foreign_key_name(from_table, options)
+ options
end
def dump_schema_information #:nodoc:
@@ -772,12 +969,12 @@ module ActiveRecord
ActiveRecord::SchemaMigration.create_table
end
- def assume_migrated_upto_version(version, migrations_paths = ActiveRecord::Migrator.migrations_paths)
+ def assume_migrated_upto_version(version, migrations_paths)
migrations_paths = Array(migrations_paths)
version = version.to_i
sm_table = quote_table_name(ActiveRecord::Migrator.schema_migrations_table_name)
- migrated = select_values("SELECT version FROM #{sm_table}").map { |v| v.to_i }
+ migrated = select_values("SELECT version FROM #{sm_table}").map(&:to_i)
paths = migrations_paths.map {|p| "#{p}/[0-9]*_*.rb" }
versions = Dir[*paths].map do |filename|
filename.split('/').last.split('_').first.to_i
@@ -815,6 +1012,12 @@ module ActiveRecord
raise ArgumentError, "Error adding decimal column: precision cannot be empty if scale is specified"
end
+ elsif [:datetime, :time].include?(type) && precision ||= native[:precision]
+ if (0..6) === precision
+ column_type_sql << "(#{precision})"
+ else
+ raise(ActiveRecordError, "No #{native[:name]} type has precision of #{precision}. The allowed range of precision is from 0 to 6")
+ end
elsif (type != :primary_key) && (limit ||= native.is_a?(Hash) && native[:limit])
column_type_sql << "(#{limit})"
end
@@ -834,20 +1037,23 @@ module ActiveRecord
columns
end
- # Adds timestamps (+created_at+ and +updated_at+) columns to the named table.
+ # Adds timestamps (+created_at+ and +updated_at+) columns to +table_name+.
+ # Additional options (like <tt>null: false</tt>) are forwarded to #add_column.
#
- # add_timestamps(:suppliers)
+ # add_timestamps(:suppliers, null: false)
#
- def add_timestamps(table_name)
- add_column table_name, :created_at, :datetime
- add_column table_name, :updated_at, :datetime
+ def add_timestamps(table_name, options = {})
+ options[:null] = false if options[:null].nil?
+
+ add_column table_name, :created_at, :datetime, options
+ add_column table_name, :updated_at, :datetime, options
end
# Removes the timestamp columns (+created_at+ and +updated_at+) from the table definition.
#
# remove_timestamps(:suppliers)
#
- def remove_timestamps(table_name)
+ def remove_timestamps(table_name, options = {})
remove_column table_name, :updated_at
remove_column table_name, :created_at
end
@@ -858,13 +1064,13 @@ module ActiveRecord
def add_index_options(table_name, column_name, options = {}) #:nodoc:
column_names = Array(column_name)
- index_name = index_name(table_name, column: column_names)
options.assert_valid_keys(:unique, :order, :name, :where, :length, :internal, :using, :algorithm, :type)
- index_type = options[:unique] ? "UNIQUE" : ""
index_type = options[:type].to_s if options.key?(:type)
+ index_type ||= options[:unique] ? "UNIQUE" : ""
index_name = options[:name].to_s if options.key?(:name)
+ index_name ||= index_name(table_name, column: column_names)
max_index_length = options.fetch(:internal, false) ? index_name_length : allowed_index_name_length
if options.key?(:algorithm)
@@ -890,6 +1096,10 @@ module ActiveRecord
[index_name, index_type, index_columns, index_options, algorithm, using]
end
+ def options_include_default?(options)
+ options.include?(:default) && !(options[:null] == false && options[:default].nil?)
+ end
+
protected
def add_index_sort_order(option_strings, column_names, options = {})
if options.is_a?(Hash) && order = options[:order]
@@ -916,10 +1126,6 @@ module ActiveRecord
column_names.map {|name| quote_column_name(name) + option_strings[name]}
end
- def options_include_default?(options)
- options.include?(:default) && !(options[:null] == false && options[:default].nil?)
- end
-
def index_name_for_remove(table_name, options = {})
index_name = index_name(table_name, options)
@@ -961,17 +1167,33 @@ module ActiveRecord
end
private
- def create_table_definition(name, temporary, options, as = nil)
+ def create_table_definition(name, temporary = false, options = nil, as = nil)
TableDefinition.new native_database_types, name, temporary, options, as
end
def create_alter_table(name)
- AlterTable.new create_table_definition(name, false, {})
+ AlterTable.new create_table_definition(name)
end
def foreign_key_name(table_name, options) # :nodoc:
+ identifier = "#{table_name}_#{options.fetch(:column)}_fk"
+ hashed_identifier = Digest::SHA256.hexdigest(identifier).first(10)
options.fetch(:name) do
- "fk_rails_#{SecureRandom.hex(5)}"
+ "fk_rails_#{hashed_identifier}"
+ end
+ end
+
+ def validate_index_length!(table_name, new_name) # :nodoc:
+ if new_name.length > allowed_index_name_length
+ raise ArgumentError, "Index name '#{new_name}' on table '#{table_name}' is too long; the limit is #{allowed_index_name_length} characters"
+ end
+ end
+
+ def extract_new_default_value(default_or_changes)
+ if default_or_changes.is_a?(Hash) && default_or_changes.has_key?(:from) && default_or_changes.has_key?(:to)
+ default_or_changes[:to]
+ else
+ default_or_changes
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb
index 33cc22425d..295a7bed87 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb
@@ -1,84 +1,10 @@
module ActiveRecord
module ConnectionAdapters
- class TransactionManager #:nodoc:
- def initialize(connection)
- @stack = []
- @connection = connection
- end
-
- def begin_transaction(options = {})
- transaction_class = @stack.empty? ? RealTransaction : SavepointTransaction
- transaction = transaction_class.new(@connection, current_transaction, options)
-
- @stack.push(transaction)
- transaction
- end
-
- def commit_transaction
- @stack.pop.commit
- end
-
- def rollback_transaction
- @stack.pop.rollback
- end
-
- def within_new_transaction(options = {})
- transaction = begin_transaction options
- yield
- rescue Exception => error
- transaction.rollback if transaction
- raise
- ensure
- begin
- transaction.commit unless error
- rescue Exception
- transaction.rollback
- raise
- ensure
- @stack.pop if transaction
- end
- end
-
- def open_transactions
- @stack.size
- end
-
- def current_transaction
- @stack.last || closed_transaction
- end
-
- private
-
- def closed_transaction
- @closed_transaction ||= ClosedTransaction.new(@connection)
- end
- end
-
- class Transaction #:nodoc:
- attr_reader :connection
-
- def initialize(connection)
- @connection = connection
- @state = TransactionState.new
- end
-
- def state
- @state
- end
-
- def savepoint_name
- nil
- end
- end
-
class TransactionState
- attr_reader :parent
-
VALID_STATES = Set.new([:committed, :rolledback, nil])
def initialize(state = nil)
@state = state
- @parent = nil
end
def finalized?
@@ -93,118 +19,113 @@ module ActiveRecord
@state == :rolledback
end
+ def completed?
+ committed? || rolledback?
+ end
+
def set_state(state)
- if !VALID_STATES.include?(state)
+ unless VALID_STATES.include?(state)
raise ArgumentError, "Invalid transaction state: #{state}"
end
@state = state
end
end
- class ClosedTransaction < Transaction #:nodoc:
- def number
- 0
- end
-
- def begin(options = {})
- RealTransaction.new(connection, self, options)
- end
-
- def closed?
- true
- end
-
- def open?
- false
- end
-
- def joinable?
- false
- end
-
- # This is a noop when there are no open transactions
- def add_record(record)
- end
+ class NullTransaction #:nodoc:
+ def initialize; end
+ def closed?; true; end
+ def open?; false; end
+ def joinable?; false; end
+ def add_record(record); end
end
- class OpenTransaction < Transaction #:nodoc:
- attr_reader :parent, :records
- attr_writer :joinable
-
- def initialize(connection, parent, options = {})
- super connection
-
- @parent = parent
- @records = []
- @joinable = options.fetch(:joinable, true)
- end
+ class Transaction #:nodoc:
+ attr_reader :connection, :state, :records, :savepoint_name
+ attr_writer :joinable
- def joinable?
- @joinable
+ def initialize(connection, options, run_commit_callbacks: false)
+ @connection = connection
+ @state = TransactionState.new
+ @records = []
+ @joinable = options.fetch(:joinable, true)
+ @run_commit_callbacks = run_commit_callbacks
end
- def number
- parent.number + 1
+ def add_record(record)
+ records << record
end
- def begin(options = {})
- SavepointTransaction.new(connection, self, options)
+ def rollback
+ @state.set_state(:rolledback)
end
- def rollback
- perform_rollback
- parent
+ def rollback_records
+ ite = records.uniq
+ while record = ite.shift
+ record.rolledback!(force_restore_state: full_rollback?)
+ end
+ ensure
+ ite.each do |i|
+ i.rolledback!(force_restore_state: full_rollback?, should_run_callbacks: false)
+ end
end
def commit
- perform_commit
- parent
+ @state.set_state(:committed)
end
- def add_record(record)
- if record.has_transactional_callbacks?
- records << record
- else
- record.set_transaction_state(@state)
- end
+ def before_commit_records
+ records.uniq.each(&:before_committed!) if @run_commit_callbacks
end
- def rollback_records
- @state.set_state(:rolledback)
- records.uniq.each do |record|
- begin
- record.rolledback!(parent.closed?)
- rescue => e
- record.logger.error(e) if record.respond_to?(:logger) && record.logger
+ def commit_records
+ ite = records.uniq
+ while record = ite.shift
+ if @run_commit_callbacks
+ record.committed!
+ else
+ # if not running callbacks, only adds the record to the parent transaction
+ record.add_to_transaction
end
end
+ ensure
+ ite.each { |i| i.committed!(should_run_callbacks: false) }
end
- def commit_records
- @state.set_state(:committed)
- records.uniq.each do |record|
- begin
- record.committed!
- rescue => e
- record.logger.error(e) if record.respond_to?(:logger) && record.logger
- end
+ def full_rollback?; true; end
+ def joinable?; @joinable; end
+ def closed?; false; end
+ def open?; !closed?; end
+ end
+
+ class SavepointTransaction < Transaction
+
+ def initialize(connection, savepoint_name, options, *args)
+ super(connection, options, *args)
+ if options[:isolation]
+ raise ActiveRecord::TransactionIsolationError, "cannot set transaction isolation in a nested transaction"
end
+ connection.create_savepoint(@savepoint_name = savepoint_name)
end
- def closed?
- false
+ def rollback
+ connection.rollback_to_savepoint(savepoint_name)
+ super
end
- def open?
- true
+ def commit
+ connection.release_savepoint(savepoint_name)
+ super
end
+
+ def full_rollback?; false; end
end
- class RealTransaction < OpenTransaction #:nodoc:
- def initialize(connection, parent, options = {})
- super
+ class RealTransaction < Transaction
+ def initialize(connection, options, *args)
+ super
if options[:isolation]
connection.begin_isolated_db_transaction(options[:isolation])
else
@@ -212,41 +133,82 @@ module ActiveRecord
end
end
- def perform_rollback
+ def rollback
connection.rollback_db_transaction
- rollback_records
+ super
end
- def perform_commit
+ def commit
connection.commit_db_transaction
- commit_records
+ super
end
end
- class SavepointTransaction < OpenTransaction #:nodoc:
- attr_reader :savepoint_name
+ class TransactionManager #:nodoc:
+ def initialize(connection)
+ @stack = []
+ @connection = connection
+ end
- def initialize(connection, parent, options = {})
- if options[:isolation]
- raise ActiveRecord::TransactionIsolationError, "cannot set transaction isolation in a nested transaction"
- end
+ def begin_transaction(options = {})
+ run_commit_callbacks = !current_transaction.joinable?
+ transaction =
+ if @stack.empty?
+ RealTransaction.new(@connection, options, run_commit_callbacks: run_commit_callbacks)
+ else
+ SavepointTransaction.new(@connection, "active_record_#{@stack.size}", options,
+ run_commit_callbacks: run_commit_callbacks)
+ end
- super
+ @stack.push(transaction)
+ transaction
+ end
+
+ def commit_transaction
+ transaction = @stack.last
+ transaction.before_commit_records
+ @stack.pop
+ transaction.commit
+ transaction.commit_records
+ end
- # Savepoint name only counts the Savepoint transactions, so we need to subtract 1
- @savepoint_name = "active_record_#{number - 1}"
- connection.create_savepoint(@savepoint_name)
+ def rollback_transaction(transaction = nil)
+ transaction ||= @stack.pop
+ transaction.rollback
+ transaction.rollback_records
+ end
+
+ def within_new_transaction(options = {})
+ transaction = begin_transaction options
+ yield
+ rescue Exception => error
+ rollback_transaction if transaction
+ raise
+ ensure
+ unless error
+ if Thread.current.status == 'aborting'
+ rollback_transaction if transaction
+ else
+ begin
+ commit_transaction
+ rescue Exception
+ rollback_transaction(transaction) unless transaction.state.completed?
+ raise
+ end
+ end
+ end
end
- def perform_rollback
- connection.rollback_to_savepoint(@savepoint_name)
- rollback_records
+ def open_transactions
+ @stack.size
end
- def perform_commit
- @state.set_state(:committed)
- connection.release_savepoint(@savepoint_name)
+ def current_transaction
+ @stack.last || NULL_TRANSACTION
end
+
+ private
+ NULL_TRANSACTION = NullTransaction.new
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
index 99c728814a..402159ac13 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
@@ -1,12 +1,10 @@
-require 'date'
-require 'bigdecimal'
-require 'bigdecimal/util'
require 'active_record/type'
require 'active_support/core_ext/benchmark'
+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 'monitor'
require 'arel/collectors/bind'
require 'arel/collectors/sql_string'
@@ -14,16 +12,14 @@ module ActiveRecord
module ConnectionAdapters # :nodoc:
extend ActiveSupport::Autoload
- autoload_at 'active_record/connection_adapters/column' do
- autoload :Column
- autoload :NullColumn
- end
+ autoload :Column
autoload :ConnectionSpecification
autoload_at 'active_record/connection_adapters/abstract/schema_definitions' do
autoload :IndexDefinition
autoload :ColumnDefinition
autoload :ChangeColumnDefinition
+ autoload :ForeignKeyDefinition
autoload :TableDefinition
autoload :Table
autoload :AlterTable
@@ -46,7 +42,7 @@ module ActiveRecord
autoload_at 'active_record/connection_adapters/abstract/transaction' do
autoload :TransactionManager
- autoload :ClosedTransaction
+ autoload :NullTransaction
autoload :RealTransaction
autoload :SavepointTransaction
autoload :TransactionState
@@ -56,21 +52,21 @@ module ActiveRecord
# related classes form the abstraction layer which makes this possible.
# An AbstractAdapter represents a connection to a database, and provides an
# abstract interface for database-specific functionality such as establishing
- # a connection, escaping values, building the right SQL fragments for ':offset'
- # and ':limit' options, etc.
+ # a connection, escaping values, building the right SQL fragments for +:offset+
+ # and +:limit+ options, etc.
#
# All the concrete database adapters follow the interface laid down in this class.
- # ActiveRecord::Base.connection returns an AbstractAdapter object, which
+ # {ActiveRecord::Base.connection}[rdoc-ref:ConnectionHandling#connection] returns an AbstractAdapter object, which
# you can use.
#
# Most of the methods in the adapter are useful during migrations. Most
- # notably, the instance methods provided by SchemaStatement are very useful.
+ # notably, the instance methods provided by SchemaStatements are very useful.
class AbstractAdapter
+ ADAPTER_NAME = 'Abstract'.freeze
include Quoting, DatabaseStatements, SchemaStatements
include DatabaseLimits
include QueryCache
include ActiveSupport::Callbacks
- include MonitorMixin
include ColumnDumper
SIMPLE_INT = /\A\d+\z/
@@ -112,9 +108,22 @@ module ActiveRecord
@prepared_statements = false
end
+ class Version
+ include Comparable
+
+ def initialize(version_string)
+ @version = version_string.split('.').map(&:to_i)
+ end
+
+ def <=>(version_string)
+ @version <=> version_string.split('.').map(&:to_i)
+ end
+ end
+
class BindCollector < Arel::Collectors::Bind
def compile(bvs, conn)
- super(bvs.map { |bv| conn.quote(*bv.reverse) })
+ casted_binds = conn.prepare_binds_for_database(bvs)
+ super(casted_binds.map { |value| conn.quote(value) })
end
end
@@ -140,12 +149,20 @@ module ActiveRecord
SchemaCreation.new self
end
+ # this method must only be called while holding connection pool's mutex
def lease
- synchronize do
- unless in_use?
- @owner = Thread.current
+ if in_use?
+ msg = 'Cannot lease connection, '
+ if @owner == Thread.current
+ msg << 'it is already leased by the current thread.'
+ else
+ msg << "it is already in use by a different thread: #{@owner}. " <<
+ "Current thread: #{Thread.current}."
end
+ raise ActiveRecordError, msg
end
+
+ @owner = Thread.current
end
def schema_cache=(cache)
@@ -153,6 +170,7 @@ module ActiveRecord
@schema_cache = cache
end
+ # this method must only be called while holding connection pool's mutex
def expire
@owner = nil
end
@@ -167,7 +185,7 @@ module ActiveRecord
# Returns the human-readable name of the adapter. Use mixed case - one
# can always use downcase if needed.
def adapter_name
- 'Abstract'
+ self.class::ADAPTER_NAME
end
# Does this adapter support migrations?
@@ -239,6 +257,21 @@ module ActiveRecord
false
end
+ # Does this adapter support views?
+ def supports_views?
+ false
+ end
+
+ # Does this adapter support datetime with precision?
+ def supports_datetime_with_precision?
+ false
+ end
+
+ # Does this adapter support json data type?
+ def supports_json?
+ false
+ end
+
# This is meant to be implemented by the adapters that support extensions
def disable_extension(name)
end
@@ -257,12 +290,10 @@ module ActiveRecord
{}
end
- # QUOTING ==================================================
-
- # Returns a bind substitution value given a bind +index+ and +column+
+ # Returns a bind substitution value given a bind +column+
# NOTE: The column param is currently being used by the sqlserver-adapter
- def substitute_at(column, index)
- Arel::Nodes::BindParam.new '?'
+ def substitute_at(column, _unused = 0)
+ Arel::Nodes::BindParam.new
end
# REFERENTIAL INTEGRITY ====================================
@@ -318,7 +349,7 @@ module ActiveRecord
end
# Checks whether the connection to the database is still active (i.e. not stale).
- # This is done under the hood by calling <tt>active?</tt>. If the connection
+ # 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)
reconnect! unless active?
@@ -337,9 +368,6 @@ module ActiveRecord
def create_savepoint(name = nil)
end
- def rollback_to_savepoint(name = nil)
- end
-
def release_savepoint(name = nil)
end
@@ -354,9 +382,18 @@ module ActiveRecord
end
def case_insensitive_comparison(table, attribute, column, value)
- table[attribute].lower.eq(table.lower(value))
+ if can_perform_case_insensitive_comparison_for?(column)
+ table[attribute].lower.eq(table.lower(value))
+ else
+ case_sensitive_comparison(table, attribute, column, value)
+ end
end
+ def can_perform_case_insensitive_comparison_for?(column)
+ true
+ end
+ private :can_perform_case_insensitive_comparison_for?
+
def current_savepoint_name
current_transaction.savepoint_name
end
@@ -372,26 +409,30 @@ module ActiveRecord
end
end
- def new_column(name, default, cast_type, sql_type = nil, null = true)
- Column.new(name, default, cast_type, sql_type, null)
+ def new_column(name, default, sql_type_metadata = nil, null = true, default_function = nil, collation = nil)
+ Column.new(name, default, sql_type_metadata, null, default_function, collation)
end
def lookup_cast_type(sql_type) # :nodoc:
type_map.lookup(sql_type)
end
+ def column_name_for_operation(operation, node) # :nodoc:
+ visitor.accept(node, collector).value
+ end
+
protected
def initialize_type_map(m) # :nodoc:
- register_class_with_limit m, %r(boolean)i, Type::Boolean
- register_class_with_limit m, %r(char)i, Type::String
- register_class_with_limit m, %r(binary)i, Type::Binary
- register_class_with_limit m, %r(text)i, Type::Text
- register_class_with_limit m, %r(date)i, Type::Date
- register_class_with_limit m, %r(time)i, Type::Time
- register_class_with_limit m, %r(datetime)i, Type::DateTime
- register_class_with_limit m, %r(float)i, Type::Float
- register_class_with_limit m, %r(int)i, Type::Integer
+ register_class_with_limit m, %r(boolean)i, Type::Boolean
+ register_class_with_limit m, %r(char)i, Type::String
+ register_class_with_limit m, %r(binary)i, Type::Binary
+ register_class_with_limit m, %r(text)i, Type::Text
+ register_class_with_precision m, %r(date)i, Type::Date
+ register_class_with_precision m, %r(time)i, Type::Time
+ register_class_with_precision m, %r(datetime)i, Type::DateTime
+ register_class_with_limit m, %r(float)i, Type::Float
+ register_class_with_limit m, %r(int)i, Type::Integer
m.alias_type %r(blob)i, 'binary'
m.alias_type %r(clob)i, 'text'
@@ -425,6 +466,13 @@ module ActiveRecord
end
end
+ def register_class_with_precision(mapping, key, klass) # :nodoc:
+ mapping.register_type(key) do |*args|
+ precision = extract_precision(args.last)
+ klass.new(precision: precision)
+ end
+ end
+
def extract_scale(sql_type) # :nodoc:
case sql_type
when /\((\d+)\)/ then 0
@@ -437,12 +485,21 @@ module ActiveRecord
end
def extract_limit(sql_type) # :nodoc:
- $1.to_i if sql_type =~ /\((.*)\)/
+ case sql_type
+ when /^bigint/i
+ 8
+ when /\((.*)\)/
+ $1.to_i
+ end
end
def translate_exception_class(e, sql)
- message = "#{e.class.name}: #{e.message}: #{sql}"
- @logger.error message if @logger
+ begin
+ message = "#{e.class.name}: #{e.message}: #{sql}"
+ rescue Encoding::CompatibilityError
+ message = "#{e.class.name}: #{e.message.force_encoding sql.encoding}: #{sql}"
+ end
+
exception = translate_exception(e, message)
exception.set_backtrace e.backtrace
exception
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 e5417a9556..251acf1c83 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
@@ -1,70 +1,29 @@
-require 'arel/visitors/bind_visitor'
+require 'active_record/connection_adapters/abstract_adapter'
+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_support/core_ext/string/strip'
module ActiveRecord
module ConnectionAdapters
class AbstractMysqlAdapter < AbstractAdapter
+ include MySQL::ColumnDumper
include Savepoints
- class SchemaCreation < AbstractAdapter::SchemaCreation
- def visit_AddColumn(o)
- add_column_position!(super, column_options(o))
- end
-
- private
-
- def visit_DropForeignKey(name)
- "DROP FOREIGN KEY #{name}"
- end
-
- def visit_TableDefinition(o)
- name = o.name
- create_sql = "CREATE#{' TEMPORARY' if o.temporary} TABLE #{quote_table_name(name)} "
-
- statements = o.columns.map { |c| accept c }
- statements.concat(o.indexes.map { |column_name, options| index_in_create(name, column_name, options) })
-
- create_sql << "(#{statements.join(', ')}) " if statements.present?
- create_sql << "#{o.options}"
- create_sql << " AS #{@conn.to_sql(o.as)}" if o.as
- create_sql
- end
-
- def visit_ChangeColumnDefinition(o)
- column = o.column
- options = o.options
- sql_type = type_to_sql(o.type, options[:limit], options[:precision], options[:scale])
- change_column_sql = "CHANGE #{quote_column_name(column.name)} #{quote_column_name(options[:name])} #{sql_type}"
- add_column_options!(change_column_sql, options.merge(column: column))
- add_column_position!(change_column_sql, options)
- end
-
- def add_column_position!(sql, options)
- if options[:first]
- sql << " FIRST"
- elsif options[:after]
- sql << " AFTER #{quote_column_name(options[:after])}"
- end
- sql
- end
-
- def index_in_create(table_name, column_name, options)
- index_name, index_type, index_columns, index_options, index_algorithm, index_using = @conn.add_index_options(table_name, column_name, options)
- "#{index_type} INDEX #{quote_column_name(index_name)} #{index_using} (#{index_columns})#{index_options} #{index_algorithm}"
- end
+ def update_table_definition(table_name, base) # :nodoc:
+ MySQL::Table.new(table_name, base)
end
def schema_creation
- SchemaCreation.new self
+ MySQL::SchemaCreation.new(self)
end
class Column < ConnectionAdapters::Column # :nodoc:
- attr_reader :collation, :strict, :extra
+ delegate :strict, :extra, to: :sql_type_metadata, allow_nil: true
- def initialize(name, default, cast_type, sql_type = nil, null = true, collation = nil, strict = false, extra = "")
- @strict = strict
- @collation = collation
- @extra = extra
- super(name, default, cast_type, sql_type, null)
+ def initialize(*)
+ super
assert_valid_default(default)
extract_default
end
@@ -72,7 +31,7 @@ module ActiveRecord
def extract_default
if blob_or_text_column?
@default = null || strict ? nil : ''
- elsif missing_default_forged_as_empty_string?(@default)
+ elsif missing_default_forged_as_empty_string?(default)
@default = nil
end
end
@@ -86,10 +45,18 @@ module ActiveRecord
sql_type =~ /blob/i || type == :text
end
+ def unsigned?
+ /unsigned/ === sql_type
+ end
+
def case_sensitive?
collation && !collation.match(/_ci$/)
end
+ def auto_increment?
+ extra == 'auto_increment'
+ end
+
private
# MySQL misreports NOT NULL column default when none is given.
@@ -110,6 +77,33 @@ module ActiveRecord
end
end
+ class MysqlTypeMetadata < DelegateClass(SqlTypeMetadata) # :nodoc:
+ attr_reader :extra, :strict
+
+ def initialize(type_metadata, extra: "", strict: false)
+ super(type_metadata)
+ @type_metadata = type_metadata
+ @extra = extra
+ @strict = strict
+ end
+
+ def ==(other)
+ other.is_a?(MysqlTypeMetadata) &&
+ attributes_for_hash == other.attributes_for_hash
+ end
+ alias eql? ==
+
+ def hash
+ attributes_for_hash.hash
+ end
+
+ protected
+
+ def attributes_for_hash
+ [self.class, @type_metadata, extra, strict]
+ end
+ end
+
##
# :singleton-method:
# By default, the MysqlAdapter will consider all columns of type <tt>tinyint(1)</tt>
@@ -130,17 +124,20 @@ module ActiveRecord
QUOTED_TRUE, QUOTED_FALSE = '1', '0'
NATIVE_DATABASE_TYPES = {
- :primary_key => "int(11) auto_increment PRIMARY KEY",
- :string => { :name => "varchar", :limit => 255 },
- :text => { :name => "text" },
- :integer => { :name => "int", :limit => 4 },
- :float => { :name => "float" },
- :decimal => { :name => "decimal" },
- :datetime => { :name => "datetime" },
- :time => { :name => "time" },
- :date => { :name => "date" },
- :binary => { :name => "blob" },
- :boolean => { :name => "tinyint", :limit => 1 }
+ primary_key: "int auto_increment PRIMARY KEY",
+ string: { name: "varchar", limit: 255 },
+ text: { name: "text" },
+ integer: { name: "int", limit: 4 },
+ float: { name: "float" },
+ decimal: { name: "decimal" },
+ datetime: { name: "datetime" },
+ time: { name: "time" },
+ date: { name: "date" },
+ binary: { name: "blob" },
+ blob: { name: "blob" },
+ boolean: { name: "tinyint", limit: 1 },
+ bigint: { name: "bigint" },
+ json: { name: "json" },
}
INDEX_TYPES = [:fulltext, :spatial]
@@ -156,13 +153,24 @@ module ActiveRecord
if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true })
@prepared_statements = true
+ @visitor.extend(DetermineIfPreparableVisitor)
else
@prepared_statements = false
end
end
- def adapter_name #:nodoc:
- self.class::ADAPTER_NAME
+ MAX_INDEX_LENGTH_FOR_CHARSETS_OF_4BYTES_MAXLEN = 191
+ CHARSETS_OF_4BYTES_MAXLEN = ['utf8mb4', 'utf16', 'utf16le', 'utf32']
+ def initialize_schema_migrations_table
+ if CHARSETS_OF_4BYTES_MAXLEN.include?(charset)
+ ActiveRecord::SchemaMigration.create_table(MAX_INDEX_LENGTH_FOR_CHARSETS_OF_4BYTES_MAXLEN)
+ else
+ ActiveRecord::SchemaMigration.create_table
+ end
+ end
+
+ def version
+ @version ||= Version.new(full_version.match(/^\d+\.\d+\.\d+/)[0])
end
# Returns true, since this connection adapter supports migrations.
@@ -189,7 +197,11 @@ module ActiveRecord
#
# http://bugs.mysql.com/bug.php?id=39170
def supports_transaction_isolation?
- version[0] >= 5
+ version >= '5.0.0'
+ end
+
+ def supports_explain?
+ true
end
def supports_indexes_in_create?
@@ -200,6 +212,14 @@ module ActiveRecord
true
end
+ def supports_views?
+ version >= '5.0.0'
+ end
+
+ def supports_datetime_with_precision?
+ version >= '5.6.4'
+ end
+
def native_database_types
NATIVE_DATABASE_TYPES
end
@@ -216,8 +236,8 @@ module ActiveRecord
raise NotImplementedError
end
- def new_column(field, default, cast_type, sql_type = nil, null = true, collation = "", extra = "") # :nodoc:
- Column.new(field, default, cast_type, sql_type, null, collation, strict_mode?, extra)
+ def new_column(field, default, sql_type_metadata = nil, null = true, default_function = nil, collation = nil) # :nodoc:
+ Column.new(field, default, sql_type_metadata, null, default_function, collation)
end
# Must return the MySQL error number from the exception, if the exception has an
@@ -260,6 +280,14 @@ module ActiveRecord
0
end
+ def quoted_date(value)
+ if supports_datetime_with_precision?
+ super
+ else
+ super.sub(/\.\d{6}\z/, '')
+ end
+ end
+
# REFERENTIAL INTEGRITY ====================================
def disable_referential_integrity #:nodoc:
@@ -273,7 +301,83 @@ module ActiveRecord
end
end
+ #--
# DATABASE STATEMENTS ======================================
+ #++
+
+ def explain(arel, binds = [])
+ sql = "EXPLAIN #{to_sql(arel, binds)}"
+ start = Time.now
+ result = exec_query(sql, 'EXPLAIN', binds)
+ elapsed = Time.now - start
+
+ ExplainPrettyPrinter.new.pp(result, elapsed)
+ end
+
+ class ExplainPrettyPrinter # :nodoc:
+ # Pretty prints the result of an EXPLAIN in a way that resembles the output of the
+ # MySQL shell:
+ #
+ # +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+
+ # | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+ # +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+
+ # | 1 | SIMPLE | users | const | PRIMARY | PRIMARY | 4 | const | 1 | |
+ # | 1 | SIMPLE | posts | ALL | NULL | NULL | NULL | NULL | 1 | Using where |
+ # +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+
+ # 2 rows in set (0.00 sec)
+ #
+ # This is an exercise in Ruby hyperrealism :).
+ def pp(result, elapsed)
+ widths = compute_column_widths(result)
+ separator = build_separator(widths)
+
+ pp = []
+
+ pp << separator
+ pp << build_cells(result.columns, widths)
+ pp << separator
+
+ result.rows.each do |row|
+ pp << build_cells(row, widths)
+ end
+
+ pp << separator
+ pp << build_footer(result.rows.length, elapsed)
+
+ pp.join("\n") + "\n"
+ end
+
+ private
+
+ def compute_column_widths(result)
+ [].tap do |widths|
+ result.columns.each_with_index do |column, i|
+ cells_in_column = [column] + result.rows.map {|r| r[i].nil? ? 'NULL' : r[i].to_s}
+ widths << cells_in_column.map(&:length).max
+ end
+ end
+ end
+
+ def build_separator(widths)
+ padding = 1
+ '+' + widths.map {|w| '-' * (w + (padding*2))}.join('+') + '+'
+ end
+
+ def build_cells(items, widths)
+ cells = []
+ items.each_with_index do |item, i|
+ item = 'NULL' if item.nil?
+ justifier = item.is_a?(Numeric) ? 'rjust' : 'ljust'
+ cells << item.to_s.send(justifier, widths[i])
+ end
+ '| ' + cells.join(' | ') + ' |'
+ end
+
+ def build_footer(nrows, elapsed)
+ rows_label = nrows == 1 ? 'row' : 'rows'
+ "#{nrows} #{rows_label} in set (%.2f sec)" % elapsed
+ end
+ end
def clear_cache!
super
@@ -310,7 +414,7 @@ module ActiveRecord
execute "COMMIT"
end
- def rollback_db_transaction #:nodoc:
+ def exec_rollback_db_transaction #:nodoc:
execute "ROLLBACK"
end
@@ -378,29 +482,45 @@ module ActiveRecord
show_variable 'collation_database'
end
- def tables(name = nil, database = nil, like = nil) #:nodoc:
- sql = "SHOW TABLES "
- sql << "IN #{quote_table_name(database)} " if database
- sql << "LIKE #{quote(like)}" if like
+ def tables(name = nil) # :nodoc:
+ sql = "SELECT table_name FROM information_schema.tables "
+ sql << "WHERE table_schema = #{quote(@config[:database])}"
- execute_and_free(sql, 'SCHEMA') do |result|
- result.collect { |field| field.first }
- end
+ select_values(sql, 'SCHEMA')
end
+ alias data_sources tables
- def table_exists?(name)
- return false unless name.present?
- return true if tables(nil, nil, name).any?
+ def truncate(table_name, name = nil)
+ execute "TRUNCATE TABLE #{quote_table_name(table_name)}", name
+ end
- name = name.to_s
- schema, table = name.split('.', 2)
+ def table_exists?(table_name)
+ return false unless table_name.present?
- unless table # A table was provided without a schema
- table = schema
- schema = nil
- end
+ schema, name = table_name.to_s.split('.', 2)
+ schema, name = @config[:database], schema unless name # A table was provided without a schema
+
+ sql = "SELECT table_name FROM information_schema.tables "
+ sql << "WHERE table_schema = #{quote(schema)} AND table_name = #{quote(name)}"
+
+ select_values(sql, 'SCHEMA').any?
+ end
+ alias data_source_exists? table_exists?
- tables(nil, schema, table).any?
+ def views # :nodoc:
+ select_values("SHOW FULL TABLES WHERE table_type = 'VIEW'", 'SCHEMA')
+ end
+
+ def view_exists?(view_name) # :nodoc:
+ return false unless view_name.present?
+
+ schema, name = view_name.to_s.split('.', 2)
+ schema, name = @config[:database], schema unless name # A view was provided without a schema
+
+ sql = "SELECT table_name FROM information_schema.tables WHERE table_type = 'VIEW'"
+ sql << " AND table_schema = #{quote(schema)} AND table_name = #{quote(name)}"
+
+ select_values(sql, 'SCHEMA').any?
end
# Returns an array of indexes for the given table.
@@ -434,8 +554,8 @@ module ActiveRecord
each_hash(result).map do |field|
field_name = set_field_encoding(field[:Field])
sql_type = field[:Type]
- cast_type = lookup_cast_type(sql_type)
- new_column(field_name, field[:Default], cast_type, sql_type, field[:Null] == "YES", field[:Collation], field[:Extra])
+ type_metadata = fetch_type_metadata(sql_type, field[:Extra])
+ new_column(field_name, field[:Default], type_metadata, field[:Null] == "YES", nil, field[:Collation])
end
end
end
@@ -468,24 +588,42 @@ module ActiveRecord
rename_table_indexes(table_name, new_name)
end
+ # Drops a table from the database.
+ #
+ # [<tt>:force</tt>]
+ # Set to +:cascade+ to drop dependent objects as well.
+ # Defaults to false.
+ # [<tt>:if_exists</tt>]
+ # Set to +true+ to only drop the table if it exists.
+ # Defaults to false.
+ # [<tt>:temporary</tt>]
+ # Set to +true+ to drop temporary table.
+ # Defaults to false.
+ #
+ # Although this command ignores most +options+ and the block if one is given,
+ # it can be helpful to provide these in a migration's +change+ method so it can be reverted.
+ # In that case, +options+ and the block will be used by create_table.
def drop_table(table_name, options = {})
- execute "DROP#{' TEMPORARY' if options[:temporary]} TABLE #{quote_table_name(table_name)}"
+ execute "DROP#{' TEMPORARY' if options[:temporary]} TABLE#{' IF EXISTS' if options[:if_exists]} #{quote_table_name(table_name)}#{' CASCADE' if options[:force] == :cascade}"
end
def rename_index(table_name, old_name, new_name)
if supports_rename_index?
+ validate_index_length!(table_name, new_name)
+
execute "ALTER TABLE #{quote_table_name(table_name)} RENAME INDEX #{quote_table_name(old_name)} TO #{quote_table_name(new_name)}"
else
super
end
end
- def change_column_default(table_name, column_name, default)
+ 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
end
- def change_column_null(table_name, column_name, null, default = nil)
+ def change_column_null(table_name, column_name, null, default = nil) #:nodoc:
column = column_for(table_name, column_name)
unless null || default.nil?
@@ -505,8 +643,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 = add_index_options(table_name, column_name, options)
- execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} #{index_using} ON #{quote_table_name(table_name)} (#{index_columns})#{index_options} #{index_algorithm}"
+ index_name, index_type, index_columns, _, index_algorithm, index_using = add_index_options(table_name, column_name, options)
+ execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} #{index_using} ON #{quote_table_name(table_name)} (#{index_columns}) #{index_algorithm}"
end
def foreign_keys(table_name)
@@ -521,7 +659,7 @@ module ActiveRecord
AND fk.table_name = '#{table_name}'
SQL
- create_table_info = select_one("SHOW CREATE TABLE #{quote_table_name(table_name)}")["Create Table"]
+ create_table_info = create_table_info(table_name)
fk_info.map do |row|
options = {
@@ -537,69 +675,61 @@ module ActiveRecord
end
end
+ def table_options(table_name)
+ create_table_info = create_table_info(table_name)
+
+ # strip create_definitions and partition_options
+ raw_table_options = create_table_info.sub(/\A.*\n\) /m, '').sub(/\n\/\*!.*\*\/\n\z/m, '').strip
+
+ # strip AUTO_INCREMENT
+ raw_table_options.sub(/(ENGINE=\w+)(?: AUTO_INCREMENT=\d+)/, '\1')
+ end
+
# Maps logical Rails types to MySQL-specific data types.
- def type_to_sql(type, limit = nil, precision = nil, scale = nil)
- case type.to_s
- when 'binary'
- case limit
- when 0..0xfff; "varbinary(#{limit})"
- when nil; "blob"
- when 0x1000..0xffffffff; "blob(#{limit})"
- else raise(ActiveRecordError, "No binary type has character length #{limit}")
- end
+ def type_to_sql(type, limit = nil, precision = nil, scale = nil, unsigned = nil)
+ sql = case type.to_s
when 'integer'
- case limit
- when 1; 'tinyint'
- when 2; 'smallint'
- when 3; 'mediumint'
- when nil, 4, 11; 'int(11)' # compatibility with MySQL default
- when 5..8; 'bigint'
- else raise(ActiveRecordError, "No integer type has byte size #{limit}")
- end
+ integer_to_sql(limit)
when 'text'
- case limit
- when 0..0xff; 'tinytext'
- when nil, 0x100..0xffff; 'text'
- when 0x10000..0xffffff; 'mediumtext'
- when 0x1000000..0xffffffff; 'longtext'
- else raise(ActiveRecordError, "No text type has character length #{limit}")
+ text_to_sql(limit)
+ when 'blob'
+ binary_to_sql(limit)
+ when 'binary'
+ if (0..0xfff) === limit
+ "varbinary(#{limit})"
+ else
+ binary_to_sql(limit)
end
else
- super
+ super(type, limit, precision, scale)
end
- end
- def add_column_position!(sql, options)
- if options[:first]
- sql << " FIRST"
- elsif options[:after]
- sql << " AFTER #{quote_column_name(options[:after])}"
- end
+ sql << ' unsigned' if unsigned && type != :primary_key
+ sql
end
# SHOW VARIABLES LIKE 'name'
def show_variable(name)
- variables = select_all("SHOW VARIABLES LIKE '#{name}'", 'SCHEMA')
+ variables = select_all("select @@#{name} as 'Value'", 'SCHEMA')
variables.first['Value'] unless variables.empty?
+ rescue ActiveRecord::StatementInvalid
+ nil
end
- # Returns a table's primary key and belonging sequence.
- def pk_and_sequence_for(table)
- execute_and_free("SHOW CREATE TABLE #{quote_table_name(table)}", 'SCHEMA') do |result|
- create_table = each_hash(result).first[:"Create Table"]
- if create_table.to_s =~ /PRIMARY KEY\s+(?:USING\s+\w+\s+)?\((.+)\)/
- keys = $1.split(",").map { |key| key.delete('`"') }
- keys.length == 1 ? [keys.first, nil] : nil
- else
- nil
- end
- end
- end
+ def primary_keys(table_name) # :nodoc:
+ raise ArgumentError unless table_name.present?
- # Returns just a table's primary key
- def primary_key(table)
- pk_and_sequence = pk_and_sequence_for(table)
- pk_and_sequence && pk_and_sequence.first
+ schema, name = table_name.to_s.split('.', 2)
+ schema, name = @config[:database], schema unless name # A table was provided without a schema
+
+ select_values(<<-SQL.strip_heredoc, 'SCHEMA')
+ SELECT column_name
+ FROM information_schema.key_column_usage
+ WHERE constraint_name = 'PRIMARY'
+ AND table_schema = #{quote(schema)}
+ AND table_name = #{quote(name)}
+ ORDER BY ordinal_position
+ SQL
end
def case_sensitive_modifier(node, table_attribute)
@@ -623,10 +753,6 @@ module ActiveRecord
end
end
- def limited_update_conditions(where_sql, quoted_table_name, quoted_primary_key)
- where_sql
- end
-
def strict_mode?
self.class.type_cast_config_to_boolean(@config.fetch(:strict, true))
end
@@ -639,41 +765,64 @@ module ActiveRecord
def initialize_type_map(m) # :nodoc:
super
- m.register_type(%r(enum)i) do |sql_type|
- limit = sql_type[/^enum\((.+)\)/i, 1]
- .split(',').map{|enum| enum.strip.length - 2}.max
- Type::String.new(limit: limit)
- end
-
- m.register_type %r(tinytext)i, Type::Text.new(limit: 255)
- m.register_type %r(tinyblob)i, Type::Binary.new(limit: 255)
- m.register_type %r(mediumtext)i, Type::Text.new(limit: 16777215)
- m.register_type %r(mediumblob)i, Type::Binary.new(limit: 16777215)
- m.register_type %r(longtext)i, Type::Text.new(limit: 2147483647)
- m.register_type %r(longblob)i, Type::Binary.new(limit: 2147483647)
- m.register_type %r(^bigint)i, Type::Integer.new(limit: 8)
- m.register_type %r(^int)i, Type::Integer.new(limit: 4)
- m.register_type %r(^mediumint)i, Type::Integer.new(limit: 3)
- m.register_type %r(^smallint)i, Type::Integer.new(limit: 2)
- m.register_type %r(^tinyint)i, Type::Integer.new(limit: 1)
+
+ register_class_with_limit m, %r(char)i, MysqlString
+
+ m.register_type %r(tinytext)i, Type::Text.new(limit: 2**8 - 1)
+ m.register_type %r(tinyblob)i, Type::Binary.new(limit: 2**8 - 1)
+ m.register_type %r(text)i, Type::Text.new(limit: 2**16 - 1)
+ m.register_type %r(blob)i, Type::Binary.new(limit: 2**16 - 1)
+ m.register_type %r(mediumtext)i, Type::Text.new(limit: 2**24 - 1)
+ m.register_type %r(mediumblob)i, Type::Binary.new(limit: 2**24 - 1)
+ m.register_type %r(longtext)i, Type::Text.new(limit: 2**32 - 1)
+ 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, MysqlJson.new
+
+ register_integer_type m, %r(^bigint)i, limit: 8
+ register_integer_type m, %r(^int)i, limit: 4
+ register_integer_type m, %r(^mediumint)i, limit: 3
+ register_integer_type m, %r(^smallint)i, limit: 2
+ register_integer_type m, %r(^tinyint)i, limit: 1
m.alias_type %r(tinyint\(1\))i, 'boolean' if emulate_booleans
- m.alias_type %r(set)i, 'varchar'
m.alias_type %r(year)i, 'integer'
m.alias_type %r(bit)i, 'binary'
+
+ m.register_type(%r(enum)i) do |sql_type|
+ limit = sql_type[/^enum\((.+)\)/i, 1]
+ .split(',').map{|enum| enum.strip.length - 2}.max
+ MysqlString.new(limit: limit)
+ end
+
+ m.register_type(%r(^set)i) do |sql_type|
+ limit = sql_type[/^set\((.+)\)/i, 1]
+ .split(',').map{|set| set.strip.length - 1}.sum - 1
+ MysqlString.new(limit: limit)
+ end
end
- # MySQL is too stupid to create a temporary table for use subquery, so we have
- # to give it some prompting in the form of a subsubquery. Ugh!
- def subquery_for(key, select)
- subsubselect = select.clone
- subsubselect.projections = [key]
+ def register_integer_type(mapping, key, options) # :nodoc:
+ mapping.register_type(key) do |sql_type|
+ if /unsigned/i =~ sql_type
+ Type::UnsignedInteger.new(options)
+ else
+ Type::Integer.new(options)
+ end
+ end
+ end
- subselect = Arel::SelectManager.new(select.engine)
- subselect.project Arel.sql(key.name)
- subselect.from subsubselect.as('__active_record_temp')
+ def extract_precision(sql_type)
+ if /time/ === sql_type
+ super || 0
+ else
+ super
+ end
+ end
+
+ def fetch_type_metadata(sql_type, extra = "")
+ MysqlTypeMetadata.new(super(sql_type), extra: extra, strict: strict_mode?)
end
def add_index_length(option_strings, column_names, options = {})
@@ -713,9 +862,9 @@ module ActiveRecord
end
def add_column_sql(table_name, column_name, type, options = {})
- td = create_table_definition table_name, options[:temporary], options[:options]
+ td = create_table_definition(table_name)
cd = td.new_column_definition(column_name, type, options)
- schema_creation.visit_AddColumn cd
+ schema_creation.accept(AddColumnDefinition.new(cd))
end
def change_column_sql(table_name, column_name, type, options = {})
@@ -729,21 +878,23 @@ module ActiveRecord
options[:null] = column.null
end
- options[:name] = column.name
- schema_creation.accept ChangeColumnDefinition.new column, type, options
+ td = create_table_definition(table_name)
+ cd = td.new_column_definition(column.name, type, options)
+ schema_creation.accept(ChangeColumnDefinition.new(cd, column.name))
end
def rename_column_sql(table_name, column_name, new_column_name)
column = column_for(table_name, column_name)
options = {
- name: new_column_name,
default: column.default,
null: column.null,
- auto_increment: column.extra == "auto_increment"
+ auto_increment: column.auto_increment?
}
current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'", 'SCHEMA')["Type"]
- schema_creation.accept ChangeColumnDefinition.new column, current_type, options
+ td = create_table_definition(table_name)
+ cd = td.new_column_definition(new_column_name, current_type, options)
+ schema_creation.accept(ChangeColumnDefinition.new(cd, column.name))
end
def remove_column_sql(table_name, column_name, type = nil, options = {})
@@ -755,8 +906,9 @@ module ActiveRecord
end
def add_index_sql(table_name, column_name, options = {})
- index_name, index_type, index_columns = add_index_options(table_name, column_name, options)
- "ADD #{index_type} INDEX #{index_name} (#{index_columns})"
+ 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 = {})
@@ -764,37 +916,41 @@ module ActiveRecord
"DROP INDEX #{index_name}"
end
- def add_timestamps_sql(table_name)
- [add_column_sql(table_name, :created_at, :datetime), add_column_sql(table_name, :updated_at, :datetime)]
+ 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)]
end
- def remove_timestamps_sql(table_name)
+ def remove_timestamps_sql(table_name, options = {})
[remove_column_sql(table_name, :updated_at), remove_column_sql(table_name, :created_at)]
end
private
- def version
- @version ||= full_version.scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map { |v| v.to_i }
+ # MySQL is too stupid to create a temporary table for use subquery, so we have
+ # to give it some prompting in the form of a subsubquery. Ugh!
+ def subquery_for(key, select)
+ subsubselect = select.clone
+ subsubselect.projections = [key]
+
+ subselect = Arel::SelectManager.new(select.engine)
+ subselect.project Arel.sql(key.name)
+ # Materialized subquery by adding distinct
+ # to work with MySQL 5.7.6 which sets optimizer_switch='derived_merge=on'
+ subselect.from subsubselect.distinct.as('__active_record_temp')
end
def mariadb?
full_version =~ /mariadb/i
end
- def supports_views?
- version[0] >= 5
- end
-
def supports_rename_index?
- mariadb? ? false : (version[0] == 5 && version[1] >= 7) || version[0] >= 6
+ mariadb? ? false : version >= '5.7.6'
end
def configure_connection
variables = @config.fetch(:variables, {}).stringify_keys
- # By default, MySQL 'where id is null' selects the last inserted id.
- # Turn this off. http://dev.rubyonrails.org/ticket/6778
+ # By default, MySQL 'where id is null' selects the last inserted id; Turn this off.
variables['sql_auto_is_null'] = 0
# Increase timeout so the server doesn't disconnect us.
@@ -802,24 +958,30 @@ module ActiveRecord
wait_timeout = 2147483 unless wait_timeout.is_a?(Fixnum)
variables['wait_timeout'] = self.class.type_cast_config_to_integer(wait_timeout)
+ defaults = [':default', :default].to_set
+
# Make MySQL reject illegal values rather than truncating or blanking them, see
- # http://dev.mysql.com/doc/refman/5.0/en/server-sql-mode.html#sqlmode_strict_all_tables
+ # http://dev.mysql.com/doc/refman/5.7/en/sql-mode.html#sqlmode_strict_all_tables
# If the user has provided another value for sql_mode, don't replace it.
- unless variables.has_key?('sql_mode')
+ unless variables.has_key?('sql_mode') || defaults.include?(@config[:strict])
variables['sql_mode'] = strict_mode? ? 'STRICT_ALL_TABLES' : ''
end
# NAMES does not have an equals sign, see
- # http://dev.mysql.com/doc/refman/5.0/en/set-statement.html#id944430
+ # http://dev.mysql.com/doc/refman/5.7/en/set-statement.html#id944430
# (trailing comma because variable_assignments will always have content)
- encoding = "NAMES #{@config[:encoding]}, " if @config[:encoding]
+ if @config[:encoding]
+ encoding = "NAMES #{@config[:encoding]}"
+ encoding << " COLLATE #{@config[:collation]}" if @config[:collation]
+ encoding << ", "
+ end
# Gather up all of the SET variables...
variable_assignments = variables.map do |k, v|
- if v == ':default' || v == :default
- "@@SESSION.#{k.to_s} = DEFAULT" # Sets the value to the global or compile default
+ if defaults.include?(v)
+ "@@SESSION.#{k} = DEFAULT" # Sets the value to the global or compile default
elsif !v.nil?
- "@@SESSION.#{k.to_s} = #{quote(v)}"
+ "@@SESSION.#{k} = #{quote(v)}"
end
# or else nil; compact to clear nils out
end.compact.join(', ')
@@ -836,6 +998,82 @@ module ActiveRecord
end
end
end
+
+ def create_table_info(table_name) # :nodoc:
+ @create_table_info_cache = {}
+ @create_table_info_cache[table_name] ||= select_one("SHOW CREATE TABLE #{quote_table_name(table_name)}")["Create Table"]
+ end
+
+ def create_table_definition(name, temporary = false, options = nil, as = nil) # :nodoc:
+ MySQL::TableDefinition.new(native_database_types, name, temporary, options, as)
+ end
+
+ def integer_to_sql(limit) # :nodoc:
+ case limit
+ when 1; 'tinyint'
+ when 2; 'smallint'
+ when 3; 'mediumint'
+ when nil, 4; 'int'
+ when 5..8; 'bigint'
+ when 11; 'int(11)' # backward compatibility with Rails 2.0
+ else raise(ActiveRecordError, "No integer type has byte size #{limit}")
+ end
+ end
+
+ def text_to_sql(limit) # :nodoc:
+ case limit
+ when 0..0xff; 'tinytext'
+ when nil, 0x100..0xffff; 'text'
+ when 0x10000..0xffffff; 'mediumtext'
+ when 0x1000000..0xffffffff; 'longtext'
+ else raise(ActiveRecordError, "No text type has byte length #{limit}")
+ end
+ end
+
+ def binary_to_sql(limit) # :nodoc:
+ case limit
+ when 0..0xff; 'tinyblob'
+ when nil, 0x100..0xffff; 'blob'
+ when 0x10000..0xffffff; 'mediumblob'
+ when 0x1000000..0xffffffff; 'longblob'
+ else raise(ActiveRecordError, "No binary type has byte length #{limit}")
+ end
+ end
+
+ class MysqlJson < Type::Internal::AbstractJson # :nodoc:
+ def changed_in_place?(raw_old_value, new_value)
+ # Normalization is required because MySQL JSON data format includes
+ # the space between the elements.
+ super(serialize(deserialize(raw_old_value)), new_value)
+ end
+ end
+
+ class MysqlString < Type::String # :nodoc:
+ def serialize(value)
+ case value
+ when true then "1"
+ when false then "0"
+ else super
+ end
+ end
+
+ private
+
+ def cast_value(value)
+ case value
+ when true then "1"
+ when false then "0"
+ else super
+ end
+ end
+ end
+
+ ActiveRecord::Type.register(:json, MysqlJson, adapter: :mysql)
+ ActiveRecord::Type.register(:json, MysqlJson, adapter: :mysql2)
+ ActiveRecord::Type.register(:string, MysqlString, adapter: :mysql)
+ ActiveRecord::Type.register(:string, MysqlString, adapter: :mysql2)
+ ActiveRecord::Type.register(:unsigned_integer, Type::UnsignedInteger, adapter: :mysql)
+ ActiveRecord::Type.register(:unsigned_integer, Type::UnsignedInteger, adapter: :mysql2)
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb
index 1f1e2c46f4..81de7c03fb 100644
--- a/activerecord/lib/active_record/connection_adapters/column.rb
+++ b/activerecord/lib/active_record/connection_adapters/column.rb
@@ -5,42 +5,32 @@ module ActiveRecord
module ConnectionAdapters
# An abstract definition of a column in a table.
class Column
- TRUE_VALUES = [true, 1, '1', 't', 'T', 'true', 'TRUE', 'on', 'ON'].to_set
- FALSE_VALUES = [false, 0, '0', 'f', 'F', 'false', 'FALSE', 'off', 'OFF'].to_set
+ attr_reader :name, :null, :sql_type_metadata, :default, :default_function, :collation
- module Format
- ISO_DATE = /\A(\d{4})-(\d\d)-(\d\d)\z/
- ISO_DATETIME = /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)(\.\d+)?\z/
- end
-
- attr_reader :name, :cast_type, :null, :sql_type, :default, :default_function
-
- delegate :type, :precision, :scale, :limit, :klass, :accessor,
- :text?, :number?, :binary?, :changed?,
- :type_cast_from_user, :type_cast_from_database, :type_cast_for_database,
- :type_cast_for_schema,
- to: :cast_type
+ delegate :precision, :scale, :limit, :type, :sql_type, to: :sql_type_metadata, allow_nil: true
# Instantiates a new column in the table.
#
- # +name+ is the column's name, such as <tt>supplier_id</tt> in <tt>supplier_id int(11)</tt>.
+ # +name+ is the column's name, such as <tt>supplier_id</tt> in <tt>supplier_id int</tt>.
# +default+ is the type-casted default value, such as +new+ in <tt>sales_stage varchar(20) default 'new'</tt>.
- # +cast_type+ is the object used for type casting and type information.
- # +sql_type+ is used to extract the column's length, if necessary. For example +60+ in
- # <tt>company_name varchar(60)</tt>.
- # It will be mapped to one of the standard Rails SQL types in the <tt>type</tt> attribute.
+ # +sql_type_metadata+ is various information about the type of the column
# +null+ determines if this column allows +NULL+ values.
- def initialize(name, default, cast_type, sql_type = nil, null = true)
- @name = name
- @cast_type = cast_type
- @sql_type = sql_type
- @null = null
- @default = default
- @default_function = nil
+ def initialize(name, default, sql_type_metadata = nil, null = true, default_function = nil, collation = nil)
+ @name = name.freeze
+ @sql_type_metadata = sql_type_metadata
+ @null = null
+ @default = default
+ @default_function = default_function
+ @collation = collation
+ @table_name = nil
end
def has_default?
- !default.nil?
+ !default.nil? || default_function
+ end
+
+ def bigint?
+ /bigint/ === sql_type
end
# Returns the human name of the column name.
@@ -51,10 +41,26 @@ module ActiveRecord
Base.human_attribute_name(@name)
end
- def with_type(type)
- dup.tap do |clone|
- clone.instance_variable_set('@cast_type', type)
- end
+ def ==(other)
+ other.is_a?(Column) &&
+ attributes_for_hash == other.attributes_for_hash
+ end
+ alias :eql? :==
+
+ def hash
+ attributes_for_hash.hash
+ end
+
+ protected
+
+ def attributes_for_hash
+ [self.class, name, default, sql_type_metadata, null, default_function, collation]
+ end
+ end
+
+ class NullColumn < Column
+ def initialize(name)
+ super(name, nil)
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/connection_specification.rb
index 5693031053..08d46fca96 100644
--- a/activerecord/lib/active_record/connection_adapters/connection_specification.rb
+++ b/activerecord/lib/active_record/connection_adapters/connection_specification.rb
@@ -32,8 +32,8 @@ module ActiveRecord
# }
def initialize(url)
raise "Database URL cannot be empty" if url.blank?
- @uri = URI.parse(url)
- @adapter = @uri.scheme.gsub('-', '_')
+ @uri = uri_parser.parse(url)
+ @adapter = @uri.scheme.tr('-', '_')
@adapter = "postgresql" if @adapter == "postgres"
if @uri.opaque
@@ -209,27 +209,13 @@ module ActiveRecord
when Symbol
resolve_symbol_connection spec
when String
- resolve_string_connection spec
+ resolve_url_connection spec
when Hash
resolve_hash_connection spec
end
end
- def resolve_string_connection(spec)
- # Rails has historically accepted a string to mean either
- # an environment key or a URL spec, so we have deprecated
- # this ambiguous behaviour and in the future this function
- # can be removed in favor of resolve_url_connection.
- if configurations.key?(spec) || spec !~ /:/
- ActiveSupport::Deprecation.warn "Passing a string to ActiveRecord::Base.establish_connection " \
- "for a configuration lookup is deprecated, please pass a symbol (#{spec.to_sym.inspect}) instead"
- resolve_symbol_connection(spec)
- else
- resolve_url_connection(spec)
- end
- end
-
- # Takes the environment such as `:production` or `:development`.
+ # Takes the environment such as +:production+ or +:development+.
# This requires that the @configurations was initialized with a key that
# matches.
#
diff --git a/activerecord/lib/active_record/connection_adapters/determine_if_preparable_visitor.rb b/activerecord/lib/active_record/connection_adapters/determine_if_preparable_visitor.rb
new file mode 100644
index 0000000000..0fdc185c45
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/determine_if_preparable_visitor.rb
@@ -0,0 +1,22 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module DetermineIfPreparableVisitor
+ attr_reader :preparable
+
+ def accept(*)
+ @preparable = true
+ super
+ end
+
+ def visit_Arel_Nodes_In(*)
+ @preparable = false
+ super
+ end
+
+ def visit_Arel_Nodes_SqlLiteral(*)
+ @preparable = false
+ super
+ end
+ end
+ end
+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
new file mode 100644
index 0000000000..1e2c859af9
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb
@@ -0,0 +1,57 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module MySQL
+ class SchemaCreation < AbstractAdapter::SchemaCreation
+ private
+
+ def visit_DropForeignKey(name)
+ "DROP FOREIGN KEY #{name}"
+ end
+
+ def visit_ColumnDefinition(o)
+ o.sql_type = type_to_sql(o.type, o.limit, o.precision, o.scale, o.unsigned)
+ super
+ end
+
+ def visit_AddColumnDefinition(o)
+ add_column_position!(super, column_options(o.column))
+ end
+
+ def visit_ChangeColumnDefinition(o)
+ change_column_sql = "CHANGE #{quote_column_name(o.name)} #{accept(o.column)}"
+ add_column_position!(change_column_sql, column_options(o.column))
+ end
+
+ def column_options(o)
+ column_options = super
+ column_options[:charset] = o.charset
+ column_options
+ end
+
+ def add_column_options!(sql, options)
+ if options[:charset]
+ sql << " CHARACTER SET #{options[:charset]}"
+ end
+ if options[:collation]
+ sql << " COLLATE #{options[:collation]}"
+ end
+ super
+ end
+
+ def add_column_position!(sql, options)
+ if options[:first]
+ sql << " FIRST"
+ elsif options[:after]
+ sql << " AFTER #{quote_column_name(options[:after])}"
+ end
+ sql
+ end
+
+ def index_in_create(table_name, column_name, options)
+ index_name, index_type, index_columns, _, _, index_using = @conn.add_index_options(table_name, column_name, options)
+ "#{index_type} INDEX #{quote_column_name(index_name)} #{index_using} (#{index_columns}) "
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb
new file mode 100644
index 0000000000..29e8c73d46
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb
@@ -0,0 +1,69 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module MySQL
+ module ColumnMethods
+ def primary_key(name, type = :primary_key, **options)
+ options[:auto_increment] = true if type == :bigint
+ super
+ end
+
+ def blob(*args, **options)
+ args.each { |name| column(name, :blob, 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
+
+ def unsigned_bigint(*args, **options)
+ args.each { |name| column(name, :unsigned_bigint, options) }
+ end
+
+ def unsigned_float(*args, **options)
+ args.each { |name| column(name, :unsigned_float, options) }
+ end
+
+ def unsigned_decimal(*args, **options)
+ args.each { |name| column(name, :unsigned_decimal, options) }
+ end
+ end
+
+ class ColumnDefinition < ActiveRecord::ConnectionAdapters::ColumnDefinition
+ attr_accessor :charset, :unsigned
+ end
+
+ class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition
+ include ColumnMethods
+
+ def new_column_definition(name, type, options) # :nodoc:
+ column = super
+ case column.type
+ when :primary_key
+ column.type = :integer
+ column.auto_increment = true
+ when /\Aunsigned_(?<type>.+)\z/
+ column.type = $~[:type].to_sym
+ column.unsigned = true
+ end
+ column.unsigned ||= options[:unsigned]
+ column.charset = options[:charset]
+ column
+ end
+
+ private
+
+ def create_column_definition(name, type)
+ MySQL::ColumnDefinition.new(name, type)
+ end
+ end
+
+ class Table < ActiveRecord::ConnectionAdapters::Table
+ include ColumnMethods
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb
new file mode 100644
index 0000000000..3c48d0554e
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb
@@ -0,0 +1,56 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module MySQL
+ module ColumnDumper
+ def column_spec_for_primary_key(column)
+ spec = {}
+ if column.auto_increment?
+ spec[:id] = ':bigint' if column.bigint?
+ spec[:unsigned] = 'true' if column.unsigned?
+ return if spec.empty?
+ else
+ spec[:id] = column.type.inspect
+ spec.merge!(prepare_column_options(column).delete_if { |key, _| [:name, :type, :null].include?(key) })
+ end
+ spec
+ end
+
+ def prepare_column_options(column)
+ spec = super
+ spec[:unsigned] = 'true' if column.unsigned?
+ spec
+ end
+
+ def migration_keys
+ super + [:unsigned]
+ end
+
+ private
+
+ def schema_type(column)
+ if column.sql_type == 'tinyblob'
+ 'blob'
+ else
+ super
+ end
+ end
+
+ def schema_limit(column)
+ super unless column.type == :boolean
+ end
+
+ def schema_precision(column)
+ super unless /time/ === column.sql_type && column.precision == 0
+ end
+
+ def schema_collation(column)
+ if column.collation && table_name = column.instance_variable_get(:@table_name)
+ @table_collation_cache ||= {}
+ @table_collation_cache[table_name] ||= select_one("SHOW TABLE STATUS LIKE '#{table_name}'")["Collation"]
+ column.collation.inspect if column.collation != @table_collation_cache[table_name]
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
index 39d52e6349..42c4a14f00 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
@@ -1,6 +1,6 @@
require 'active_record/connection_adapters/abstract_mysql_adapter'
-gem 'mysql2', '~> 0.3.13'
+gem 'mysql2', '>= 0.3.18', '< 0.5'
require 'mysql2'
module ActiveRecord
@@ -29,7 +29,7 @@ module ActiveRecord
module ConnectionAdapters
class Mysql2Adapter < AbstractMysqlAdapter
- ADAPTER_NAME = 'Mysql2'
+ ADAPTER_NAME = 'Mysql2'.freeze
def initialize(connection, logger, connection_options, config)
super
@@ -37,17 +37,8 @@ module ActiveRecord
configure_connection
end
- MAX_INDEX_LENGTH_FOR_UTF8MB4 = 191
- def initialize_schema_migrations_table
- if @config[:encoding] == 'utf8mb4'
- ActiveRecord::SchemaMigration.create_table(MAX_INDEX_LENGTH_FOR_UTF8MB4)
- else
- ActiveRecord::SchemaMigration.create_table
- end
- end
-
- def supports_explain?
- true
+ def supports_json?
+ version >= '5.7.8'
end
# HELPER METHODS ===========================================
@@ -66,21 +57,17 @@ module ActiveRecord
exception.error_number if exception.respond_to?(:error_number)
end
+ #--
# QUOTING ==================================================
+ #++
def quote_string(string)
@connection.escape(string)
end
- def quoted_date(value)
- if value.acts_like?(:time) && value.respond_to?(:usec)
- "#{super}.#{sprintf("%06d", value.usec)}"
- else
- super
- end
- end
-
+ #--
# CONNECTION MANAGEMENT ====================================
+ #++
def active?
return false unless @connection
@@ -104,81 +91,9 @@ module ActiveRecord
end
end
+ #--
# DATABASE STATEMENTS ======================================
-
- def explain(arel, binds = [])
- sql = "EXPLAIN #{to_sql(arel, binds.dup)}"
- start = Time.now
- result = exec_query(sql, 'EXPLAIN', binds)
- elapsed = Time.now - start
-
- ExplainPrettyPrinter.new.pp(result, elapsed)
- end
-
- class ExplainPrettyPrinter # :nodoc:
- # Pretty prints the result of a EXPLAIN in a way that resembles the output of the
- # MySQL shell:
- #
- # +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+
- # | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
- # +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+
- # | 1 | SIMPLE | users | const | PRIMARY | PRIMARY | 4 | const | 1 | |
- # | 1 | SIMPLE | posts | ALL | NULL | NULL | NULL | NULL | 1 | Using where |
- # +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+
- # 2 rows in set (0.00 sec)
- #
- # This is an exercise in Ruby hyperrealism :).
- def pp(result, elapsed)
- widths = compute_column_widths(result)
- separator = build_separator(widths)
-
- pp = []
-
- pp << separator
- pp << build_cells(result.columns, widths)
- pp << separator
-
- result.rows.each do |row|
- pp << build_cells(row, widths)
- end
-
- pp << separator
- pp << build_footer(result.rows.length, elapsed)
-
- pp.join("\n") + "\n"
- end
-
- private
-
- def compute_column_widths(result)
- [].tap do |widths|
- result.columns.each_with_index do |column, i|
- cells_in_column = [column] + result.rows.map {|r| r[i].nil? ? 'NULL' : r[i].to_s}
- widths << cells_in_column.map(&:length).max
- end
- end
- end
-
- def build_separator(widths)
- padding = 1
- '+' + widths.map {|w| '-' * (w + (padding*2))}.join('+') + '+'
- end
-
- def build_cells(items, widths)
- cells = []
- items.each_with_index do |item, i|
- item = 'NULL' if item.nil?
- justifier = item.is_a?(Numeric) ? 'rjust' : 'ljust'
- cells << item.to_s.send(justifier, widths[i])
- end
- '| ' + cells.join(' | ') + ' |'
- end
-
- def build_footer(nrows, elapsed)
- rows_label = nrows == 1 ? 'row' : 'rows'
- "#{nrows} #{rows_label} in set (%.2f sec)" % elapsed
- end
- end
+ #++
# FIXME: re-enable the following once a "better" query_cache solution is in core
#
@@ -211,7 +126,9 @@ module ActiveRecord
# Returns an array of arrays containing the field values.
# Order is the same as that returned by +columns+.
def select_rows(sql, name = nil, binds = [])
- execute(sql, name).to_a
+ result = execute(sql, name)
+ @connection.next_result while @connection.more_results?
+ result.to_a
end
# Executes the SQL statement in the context of this connection.
@@ -225,18 +142,14 @@ module ActiveRecord
super
end
- def exec_query(sql, name = 'SQL', binds = [])
+ def exec_query(sql, name = 'SQL', binds = [], prepare: false)
result = execute(sql, name)
+ @connection.next_result while @connection.more_results?
ActiveRecord::Result.new(result.fields, result.to_a)
end
alias exec_without_stmt exec_query
- # Returns an ActiveRecord::Result instance.
- def select(sql, name = nil, binds = [])
- exec_query(sql, name)
- end
-
def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
super
id_value || @connection.last_id
@@ -270,7 +183,7 @@ module ActiveRecord
end
def full_version
- @full_version ||= @connection.info[:version]
+ @full_version ||= @connection.server_info[:version]
end
def set_field_encoding field_name
diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
index a03bc28744..fddb318553 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
@@ -5,8 +5,10 @@ require 'active_support/core_ext/hash/keys'
gem 'mysql', '~> 2.9'
require 'mysql'
-class Mysql
+class Mysql # :nodoc: all
class Time
+ # Used for casting DateTime fields to a MySQL friendly Time.
+ # This was documented in 48498da0dfed5239ea1eafb243ce47d7e3ce9e8e
def to_date
Date.new(year, month, day)
end
@@ -56,9 +58,9 @@ module ActiveRecord
# * <tt>:password</tt> - Defaults to nothing.
# * <tt>:database</tt> - The name of the database. No default, must be provided.
# * <tt>:encoding</tt> - (Optional) Sets the client encoding by executing "SET NAMES <encoding>" after connection.
- # * <tt>:reconnect</tt> - Defaults to false (See MySQL documentation: http://dev.mysql.com/doc/refman/5.0/en/auto-reconnect.html).
- # * <tt>:strict</tt> - Defaults to true. Enable STRICT_ALL_TABLES. (See MySQL documentation: http://dev.mysql.com/doc/refman/5.0/en/server-sql-mode.html)
- # * <tt>:variables</tt> - (Optional) A hash session variables to send as `SET @@SESSION.key = value` on each database connection. Use the value `:default` to set a variable to its DEFAULT value. (See MySQL documentation: http://dev.mysql.com/doc/refman/5.0/en/set-statement.html).
+ # * <tt>:reconnect</tt> - Defaults to false (See MySQL documentation: http://dev.mysql.com/doc/refman/5.7/en/auto-reconnect.html).
+ # * <tt>:strict</tt> - Defaults to true. Enable STRICT_ALL_TABLES. (See MySQL documentation: http://dev.mysql.com/doc/refman/5.7/en/sql-mode.html)
+ # * <tt>:variables</tt> - (Optional) A hash session variables to send as <tt>SET @@SESSION.key = value</tt> on each database connection. Use the value +:default+ to set a variable to its DEFAULT value. (See MySQL documentation: http://dev.mysql.com/doc/refman/5.7/en/set-statement.html).
# * <tt>:sslca</tt> - Necessary to use MySQL with an SSL connection.
# * <tt>:sslkey</tt> - Necessary to use MySQL with an SSL connection.
# * <tt>:sslcert</tt> - Necessary to use MySQL with an SSL connection.
@@ -66,44 +68,19 @@ module ActiveRecord
# * <tt>:sslcipher</tt> - Necessary to use MySQL with an SSL connection.
#
class MysqlAdapter < AbstractMysqlAdapter
- ADAPTER_NAME = 'MySQL'
+ ADAPTER_NAME = 'MySQL'.freeze
class StatementPool < ConnectionAdapters::StatementPool
- def initialize(connection, max = 1000)
- super
- @cache = Hash.new { |h,pid| h[pid] = {} }
- end
-
- def each(&block); cache.each(&block); end
- def key?(key); cache.key?(key); end
- def [](key); cache[key]; end
- def length; cache.length; end
- def delete(key); cache.delete(key); end
-
- def []=(sql, key)
- while @max <= cache.size
- cache.shift.last[:stmt].close
- end
- cache[sql] = key
- end
-
- def clear
- cache.values.each do |hash|
- hash[:stmt].close
- end
- cache.clear
- end
-
private
- def cache
- @cache[Process.pid]
+
+ def dealloc(stmt)
+ stmt[:stmt].close
end
end
def initialize(connection, logger, connection_options, config)
super
- @statements = StatementPool.new(@connection,
- self.class.type_cast_config_to_integer(config.fetch(:statement_limit) { 1000 }))
+ @statements = StatementPool.new(self.class.type_cast_config_to_integer(config.fetch(:statement_limit) { 1000 }))
@client_encoding = nil
connect
end
@@ -137,7 +114,9 @@ module ActiveRecord
@connection.quote(string)
end
+ #--
# CONNECTION MANAGEMENT ====================================
+ #++
def active?
if @connection.respond_to?(:stat)
@@ -178,7 +157,17 @@ module ActiveRecord
end
end
+ #--
# DATABASE STATEMENTS ======================================
+ #++
+
+ def select_all(arel, name = nil, binds = [])
+ if ExplainRegistry.collect? && prepared_statements
+ unprepared_statement { super }
+ else
+ super
+ end
+ end
def select_rows(sql, name = nil, binds = [])
@connection.query_with_result = true
@@ -241,16 +230,16 @@ module ActiveRecord
return @client_encoding if @client_encoding
result = exec_query(
- "SHOW VARIABLES WHERE Variable_name = 'character_set_client'",
+ "select @@character_set_client",
'SCHEMA')
@client_encoding = ENCODINGS[result.rows.last.last]
end
- def exec_query(sql, name = 'SQL', binds = [])
+ def exec_query(sql, name = 'SQL', binds = [], prepare: false)
if without_prepared_statement?(binds)
result_set, affected_rows = exec_without_stmt(sql, name)
else
- result_set, affected_rows = exec_stmt(sql, name, binds)
+ result_set, affected_rows = exec_stmt(sql, name, binds, cache_stmt: prepare)
end
yield affected_rows if block_given?
@@ -324,8 +313,8 @@ module ActiveRecord
def initialize_type_map(m) # :nodoc:
super
- m.register_type %r(datetime)i, Fields::DateTime.new
- m.register_type %r(time)i, Fields::Time.new
+ register_class_with_precision m, %r(datetime)i, Fields::DateTime
+ register_class_with_precision m, %r(time)i, Fields::Time
end
def exec_without_stmt(sql, name = 'SQL') # :nodoc:
@@ -389,14 +378,12 @@ module ActiveRecord
private
- def exec_stmt(sql, name, binds)
+ def exec_stmt(sql, name, binds, cache_stmt: false)
cache = {}
- type_casted_binds = binds.map { |col, val|
- [col, type_cast(val, col)]
- }
+ type_casted_binds = binds.map { |attr| type_cast(attr.value_for_database) }
- log(sql, name, type_casted_binds) do
- if binds.empty?
+ log(sql, name, binds) do
+ if !cache_stmt
stmt = @connection.prepare(sql)
else
cache = @statements[sql] ||= {
@@ -406,22 +393,23 @@ module ActiveRecord
end
begin
- stmt.execute(*type_casted_binds.map { |_, val| val })
+ stmt.execute(*type_casted_binds)
rescue Mysql::Error => e
# Older versions of MySQL leave the prepared statement in a bad
# place when an error occurs. To support older MySQL versions, we
# need to close the statement and delete the statement from the
# cache.
- stmt.close
- @statements.delete sql
+ if !cache_stmt
+ stmt.close
+ else
+ @statements.delete sql
+ end
raise e
end
cols = nil
if metadata = stmt.result_metadata
- cols = cache[:cols] ||= metadata.fetch_fields.map { |field|
- field.name
- }
+ cols = cache[:cols] ||= metadata.fetch_fields.map(&:name)
metadata.free
end
@@ -429,7 +417,7 @@ module ActiveRecord
affected_rows = stmt.affected_rows
stmt.free_result
- stmt.close if binds.empty?
+ stmt.close if !cache_stmt
[result_set, affected_rows]
end
@@ -465,7 +453,7 @@ module ActiveRecord
def select(sql, name = nil, binds = [])
@connection.query_with_result = true
- rows = exec_query(sql, name, binds)
+ rows = super
@connection.more_results && @connection.next_result # invoking stored procedures with CLIENT_MULTI_RESULTS requires this to tidy up else connection will be dropped
rows
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb b/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb
deleted file mode 100644
index 1b74c039ce..0000000000
--- a/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb
+++ /dev/null
@@ -1,93 +0,0 @@
-module ActiveRecord
- module ConnectionAdapters
- module PostgreSQL
- module ArrayParser # :nodoc:
-
- DOUBLE_QUOTE = '"'
- BACKSLASH = "\\"
- COMMA = ','
- BRACKET_OPEN = '{'
- BRACKET_CLOSE = '}'
-
- def parse_pg_array(string) # :nodoc:
- local_index = 0
- array = []
- while(local_index < string.length)
- case string[local_index]
- when BRACKET_OPEN
- local_index,array = parse_array_contents(array, string, local_index + 1)
- when BRACKET_CLOSE
- return array
- end
- local_index += 1
- end
-
- array
- end
-
- private
-
- def parse_array_contents(array, string, index)
- is_escaping = false
- is_quoted = false
- was_quoted = false
- current_item = ''
-
- local_index = index
- while local_index
- token = string[local_index]
- if is_escaping
- current_item << token
- is_escaping = false
- else
- if is_quoted
- case token
- when DOUBLE_QUOTE
- is_quoted = false
- was_quoted = true
- when BACKSLASH
- is_escaping = true
- else
- current_item << token
- end
- else
- case token
- when BACKSLASH
- is_escaping = true
- when COMMA
- add_item_to_array(array, current_item, was_quoted)
- current_item = ''
- was_quoted = false
- when DOUBLE_QUOTE
- is_quoted = true
- when BRACKET_OPEN
- internal_items = []
- local_index,internal_items = parse_array_contents(internal_items, string, local_index + 1)
- array.push(internal_items)
- when BRACKET_CLOSE
- add_item_to_array(array, current_item, was_quoted)
- return local_index,array
- else
- current_item << token
- end
- end
- end
-
- local_index += 1
- end
- return local_index,array
- end
-
- def add_item_to_array(array, current_item, quoted)
- return if !quoted && current_item.length == 0
-
- if !quoted && current_item == 'NULL'
- array.push nil
- else
- array.push current_item
- end
- end
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb
index 37e5c3859c..bfa03fa136 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb
@@ -2,18 +2,14 @@ module ActiveRecord
module ConnectionAdapters
# PostgreSQL-specific extensions to column definitions in a table.
class PostgreSQLColumn < Column #:nodoc:
- attr_accessor :array
+ delegate :array, :oid, :fmod, to: :sql_type_metadata
+ alias :array? :array
- def initialize(name, default, cast_type, sql_type = nil, null = true, default_function = nil)
- if sql_type =~ /\[\]$/
- @array = true
- super(name, default, cast_type, sql_type[0..sql_type.length - 3], null)
- else
- @array = false
- super(name, default, cast_type, sql_type, null)
- end
+ def serial?
+ return unless default_function
- @default_function = default_function
+ table_name = @table_name || '(?<table_name>.+)'
+ %r{\Anextval\('"?#{table_name}_#{name}_seq"?'::regclass\)\z} === default_function
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb
index 89a7257d77..0e0c0e993a 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb
@@ -8,7 +8,7 @@ module ActiveRecord
end
class ExplainPrettyPrinter # :nodoc:
- # Pretty prints the result of a EXPLAIN in a way that resembles the output of the
+ # Pretty prints the result of an EXPLAIN in a way that resembles the output of the
# PostgreSQL shell:
#
# QUERY PLAN
@@ -156,12 +156,8 @@ module ActiveRecord
end
end
- def substitute_at(column, index)
- Arel::Nodes::BindParam.new "$#{index + 1}"
- end
-
- def exec_query(sql, name = 'SQL', binds = [])
- execute_and_clear(sql, name, binds) do |result|
+ def exec_query(sql, name = 'SQL', binds = [], prepare: false)
+ execute_and_clear(sql, name, binds, prepare: prepare) do |result|
types = {}
fields = result.fields
fields.each_with_index do |fname, i|
@@ -227,7 +223,7 @@ module ActiveRecord
end
# Aborts a transaction.
- def rollback_db_transaction
+ def exec_rollback_db_transaction
execute "ROLLBACK"
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 d28a2b4fa0..68752cdd80 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb
@@ -1,25 +1,20 @@
-require 'active_record/connection_adapters/postgresql/oid/infinity'
-
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/float'
require 'active_record/connection_adapters/postgresql/oid/hstore'
require 'active_record/connection_adapters/postgresql/oid/inet'
-require 'active_record/connection_adapters/postgresql/oid/integer'
require 'active_record/connection_adapters/postgresql/oid/json'
require 'active_record/connection_adapters/postgresql/oid/jsonb'
require 'active_record/connection_adapters/postgresql/oid/money'
require 'active_record/connection_adapters/postgresql/oid/point'
+require 'active_record/connection_adapters/postgresql/oid/rails_5_1_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/time'
require 'active_record/connection_adapters/postgresql/oid/uuid'
require 'active_record/connection_adapters/postgresql/oid/vector'
require 'active_record/connection_adapters/postgresql/oid/xml'
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb
index cd5efe2bb8..25961a9869 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb
@@ -3,48 +3,53 @@ module ActiveRecord
module PostgreSQL
module OID # :nodoc:
class Array < Type::Value # :nodoc:
- include Type::Mutable
-
- # Loads pg_array_parser if available. String parsing can be
- # performed quicker by a native extension, which will not create
- # a large amount of Ruby objects that will need to be garbage
- # collected. pg_array_parser has a C and Java extension
- begin
- require 'pg_array_parser'
- include PgArrayParser
- rescue LoadError
- require 'active_record/connection_adapters/postgresql/array_parser'
- include PostgreSQL::ArrayParser
- end
+ include Type::Helpers::Mutable
attr_reader :subtype, :delimiter
- delegate :type, to: :subtype
+ delegate :type, :user_input_in_time_zone, :limit, to: :subtype
def initialize(subtype, delimiter = ',')
@subtype = subtype
@delimiter = delimiter
+
+ @pg_encoder = PG::TextEncoder::Array.new name: "#{type}[]", delimiter: delimiter
+ @pg_decoder = PG::TextDecoder::Array.new name: "#{type}[]", delimiter: delimiter
end
- def type_cast_from_database(value)
+ def deserialize(value)
if value.is_a?(::String)
- type_cast_array(parse_pg_array(value), :type_cast_from_database)
+ type_cast_array(@pg_decoder.decode(value), :deserialize)
else
super
end
end
- def type_cast_from_user(value)
- type_cast_array(value, :type_cast_from_user)
+ def cast(value)
+ if value.is_a?(::String)
+ value = @pg_decoder.decode(value)
+ end
+ type_cast_array(value, :cast)
end
- def type_cast_for_database(value)
+ def serialize(value)
if value.is_a?(::Array)
- cast_value_for_database(value)
+ @pg_encoder.encode(type_cast_array(value, :serialize))
else
super
end
end
+ def ==(other)
+ other.is_a?(Array) &&
+ subtype == other.subtype &&
+ delimiter == other.delimiter
+ end
+
+ def type_cast_for_schema(value)
+ return super unless value.is_a?(::Array)
+ "[" + value.map { |v| subtype.type_cast_for_schema(v) }.join(", ") + "]"
+ end
+
private
def type_cast_array(value, method)
@@ -54,41 +59,6 @@ module ActiveRecord
@subtype.public_send(method, value)
end
end
-
- def cast_value_for_database(value)
- if value.is_a?(::Array)
- casted_values = value.map { |item| cast_value_for_database(item) }
- "{#{casted_values.join(delimiter)}}"
- else
- quote_and_escape(subtype.type_cast_for_database(value))
- end
- end
-
- ARRAY_ESCAPE = "\\" * 2 * 2 # escape the backslash twice for PG arrays
-
- def quote_and_escape(value)
- case value
- when ::String
- if string_requires_quoting?(value)
- value = value.gsub(/\\/, ARRAY_ESCAPE)
- value.gsub!(/"/,"\\\"")
- %("#{value}")
- else
- value
- end
- when nil then "NULL"
- else value
- end
- end
-
- # See http://www.postgresql.org/docs/9.2/static/arrays.html#ARRAYS-IO
- # for a list of all cases in which strings will be quoted.
- def string_requires_quoting?(string)
- string.empty? ||
- string == "NULL" ||
- string =~ /[\{\}"\\\s]/ ||
- string.include?(delimiter)
- end
end
end
end
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 1dbb40ca1d..ea0fa2517f 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb
@@ -7,7 +7,7 @@ module ActiveRecord
:bit
end
- def type_cast(value)
+ def cast(value)
if ::String === value
case value
when /^0x/i
@@ -20,7 +20,7 @@ module ActiveRecord
end
end
- def type_cast_for_database(value)
+ def serialize(value)
Data.new(super) if value
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb
index 997613d7be..8f9d6e7f9b 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb
@@ -3,8 +3,9 @@ module ActiveRecord
module PostgreSQL
module OID # :nodoc:
class Bytea < Type::Binary # :nodoc:
- def type_cast_from_database(value)
+ def deserialize(value)
return if value.nil?
+ return value.to_s if value.is_a?(Type::Binary::Data)
PGconn.unescape_bytea(super)
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb
index a53b4ee8e2..eeccb09bdf 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb
@@ -12,15 +12,15 @@ module ActiveRecord
# If the subnet mask is equal to /32, don't output it
if subnet_mask == (2**32 - 1)
- "\"#{value.to_s}\""
+ "\"#{value}\""
else
- "\"#{value.to_s}/#{subnet_mask.to_s(2).count('1')}\""
+ "\"#{value}/#{subnet_mask.to_s(2).count('1')}\""
end
end
- def type_cast_for_database(value)
+ def serialize(value)
if IPAddr === value
- "#{value.to_s}/#{value.instance_variable_get(:@mask_addr).to_s(2).count('1')}"
+ "#{value}/#{value.instance_variable_get(:@mask_addr).to_s(2).count('1')}"
else
value
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
deleted file mode 100644
index 1d8d264530..0000000000
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-module ActiveRecord
- module ConnectionAdapters
- module PostgreSQL
- module OID # :nodoc:
- class Date < Type::Date # :nodoc:
- include Infinity
- end
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb
index b9e7894e5c..424769f765 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb
@@ -3,21 +3,15 @@ module ActiveRecord
module PostgreSQL
module OID # :nodoc:
class DateTime < Type::DateTime # :nodoc:
- include Infinity
-
def cast_value(value)
- if value.is_a?(::String)
- 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
+ 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
- value
+ super
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb
index 77d5038efd..91d339f32c 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb
@@ -7,7 +7,9 @@ module ActiveRecord
:enum
end
- def type_cast(value)
+ private
+
+ def cast_value(value)
value.to_s
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/float.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/float.rb
deleted file mode 100644
index 78ef94b912..0000000000
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/float.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-module ActiveRecord
- module ConnectionAdapters
- module PostgreSQL
- module OID # :nodoc:
- class Float < Type::Float # :nodoc:
- include Infinity
-
- def cast_value(value)
- case value
- when ::Float then value
- when 'Infinity' then ::Float::INFINITY
- when '-Infinity' then -::Float::INFINITY
- when 'NaN' then ::Float::NAN
- else value.to_f
- end
- end
- end
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb
index be4525c94f..9270fc9f21 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb
@@ -3,13 +3,13 @@ module ActiveRecord
module PostgreSQL
module OID # :nodoc:
class Hstore < Type::Value # :nodoc:
- include Type::Mutable
+ include Type::Helpers::Mutable
def type
:hstore
end
- def type_cast_from_database(value)
+ def deserialize(value)
if value.is_a?(::String)
::Hash[value.scan(HstorePair).map { |k, v|
v = v.upcase == 'NULL' ? nil : v.gsub(/\A"(.*)"\Z/m,'\1').gsub(/\\(.)/, '\1')
@@ -21,7 +21,7 @@ module ActiveRecord
end
end
- def type_cast_for_database(value)
+ def serialize(value)
if value.is_a?(::Hash)
value.map { |k, v| "#{escape_hstore(k)}=>#{escape_hstore(v)}" }.join(', ')
else
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/infinity.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/infinity.rb
deleted file mode 100644
index e47780399a..0000000000
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/infinity.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-module ActiveRecord
- module ConnectionAdapters
- module PostgreSQL
- module OID # :nodoc:
- module Infinity # :nodoc:
- def infinity(options = {})
- options[:negative] ? -::Float::INFINITY : ::Float::INFINITY
- end
- end
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/integer.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/integer.rb
deleted file mode 100644
index 59abdc0009..0000000000
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/integer.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-module ActiveRecord
- module ConnectionAdapters
- module PostgreSQL
- module OID # :nodoc:
- class Integer < Type::Integer # :nodoc:
- include Infinity
- end
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb
index e12ddd9901..dbc879ffd4 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb
@@ -2,32 +2,7 @@ module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module OID # :nodoc:
- class Json < Type::Value # :nodoc:
- include Type::Mutable
-
- def type
- :json
- end
-
- def type_cast_from_database(value)
- if value.is_a?(::String)
- ::ActiveSupport::JSON.decode(value)
- else
- super
- end
- end
-
- def type_cast_for_database(value)
- if value.is_a?(::Array) || value.is_a?(::Hash)
- ::ActiveSupport::JSON.encode(value)
- else
- super
- end
- end
-
- def accessor
- ActiveRecord::Store::StringKeyedHashAccessor
- end
+ class Json < Type::Internal::AbstractJson
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb
index 34ed32ad35..87391b5dc7 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb
@@ -9,11 +9,11 @@ module ActiveRecord
def changed_in_place?(raw_old_value, new_value)
# Postgres does not preserve insignificant whitespaces when
- # roundtripping jsonb columns. This causes some false positives for
+ # round-tripping jsonb columns. This causes some false positives for
# the comparison here. Therefore, we need to parse and re-dump the
# raw value here to ensure the insignificant whitespaces are
- # consitent with our encoder's output.
- raw_old_value = type_cast_for_database(type_cast_from_database(raw_old_value))
+ # consistent with our encoder's output.
+ raw_old_value = serialize(deserialize(raw_old_value))
super(raw_old_value, new_value)
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb
index df890c2ed6..2163674019 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb
@@ -3,8 +3,6 @@ module ActiveRecord
module PostgreSQL
module OID # :nodoc:
class Money < Type::Decimal # :nodoc:
- include Infinity
-
class_attribute :precision
def type
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb
index bac8b01d6b..bf565bcf47 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb
@@ -3,19 +3,19 @@ module ActiveRecord
module PostgreSQL
module OID # :nodoc:
class Point < Type::Value # :nodoc:
- include Type::Mutable
+ include Type::Helpers::Mutable
def type
:point
end
- def type_cast(value)
+ def cast(value)
case value
when ::String
if value[0] == '(' && value[-1] == ')'
value = value[1...-1]
end
- type_cast(value.split(','))
+ cast(value.split(','))
when ::Array
value.map { |v| Float(v) }
else
@@ -23,7 +23,7 @@ module ActiveRecord
end
end
- def type_cast_for_database(value)
+ def serialize(value)
if value.is_a?(::Array)
"(#{number_for_point(value[0])},#{number_for_point(value[1])})"
else
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/rails_5_1_point.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/rails_5_1_point.rb
new file mode 100644
index 0000000000..7427a25ad5
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/rails_5_1_point.rb
@@ -0,0 +1,50 @@
+module ActiveRecord
+ Point = Struct.new(:x, :y)
+
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Rails51Point < Type::Value # :nodoc:
+ include Type::Helpers::Mutable
+
+ def type
+ :point
+ end
+
+ def cast(value)
+ case value
+ when ::String
+ if value[0] == '(' && value[-1] == ')'
+ value = value[1...-1]
+ end
+ x, y = value.split(",")
+ build_point(x, y)
+ when ::Array
+ build_point(*value)
+ else
+ value
+ end
+ end
+
+ def serialize(value)
+ if value.is_a?(ActiveRecord::Point)
+ "(#{number_for_point(value.x)},#{number_for_point(value.y)})"
+ else
+ super
+ end
+ end
+
+ private
+
+ def number_for_point(number)
+ number.to_s.gsub(/\.0$/, '')
+ end
+
+ def build_point(x, y)
+ ActiveRecord::Point.new(Float(x), Float(y))
+ end
+ end
+ end
+ 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 ae967d5167..fc201f8fb9 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb
@@ -1,3 +1,5 @@
+require 'active_support/core_ext/string/filters'
+
module ActiveRecord
module ConnectionAdapters
module PostgreSQL
@@ -5,7 +7,7 @@ module ActiveRecord
class Range < Type::Value # :nodoc:
attr_reader :subtype, :type
- def initialize(subtype, type)
+ def initialize(subtype, type = :range)
@subtype = subtype
@type = type
end
@@ -23,20 +25,12 @@ module ActiveRecord
to = type_cast_single extracted[:to]
if !infinity?(from) && extracted[:exclude_start]
- if from.respond_to?(:succ)
- from = from.succ
- ActiveSupport::Deprecation.warn <<-MESSAGE
-Excluding the beginning of a Range is only partialy supported through `#succ`.
-This is not reliable and will be removed in the future.
- MESSAGE
- else
- raise ArgumentError, "The Ruby Range object does not support excluding the beginning of a Range. (unsupported value: '#{value}')"
- end
+ raise ArgumentError, "The Ruby Range object does not support excluding the beginning of a Range. (unsupported value: '#{value}')"
end
::Range.new(from, to, extracted[:exclude_end])
end
- def type_cast_for_database(value)
+ def serialize(value)
if value.is_a?(::Range)
from = type_cast_single_for_database(value.begin)
to = type_cast_single_for_database(value.end)
@@ -46,26 +40,42 @@ This is not reliable and will be removed in the future.
end
end
+ def ==(other)
+ other.is_a?(Range) &&
+ other.subtype == subtype &&
+ other.type == type
+ end
+
private
def type_cast_single(value)
- infinity?(value) ? value : @subtype.type_cast_from_database(value)
+ infinity?(value) ? value : @subtype.deserialize(value)
end
def type_cast_single_for_database(value)
- infinity?(value) ? '' : @subtype.type_cast_for_database(value)
+ infinity?(value) ? '' : @subtype.serialize(value)
end
def extract_bounds(value)
from, to = value[1..-2].split(',')
{
- from: (value[1] == ',' || from == '-infinity') ? @subtype.infinity(negative: true) : from,
- to: (value[-2] == ',' || to == 'infinity') ? @subtype.infinity : to,
+ from: (value[1] == ',' || from == '-infinity') ? infinity(negative: true) : from,
+ to: (value[-2] == ',' || to == 'infinity') ? infinity : to,
exclude_start: (value[0] == '('),
exclude_end: (value[-1] == ')')
}
end
+ def infinity(negative: false)
+ if subtype.respond_to?(:infinity)
+ subtype.infinity(negative: negative)
+ elsif negative
+ -::Float::INFINITY
+ else
+ ::Float::INFINITY
+ end
+ end
+
def infinity?(value)
value.respond_to?(:infinite?) && value.infinite?
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/time.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/time.rb
deleted file mode 100644
index 8f0246eddb..0000000000
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/time.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-module ActiveRecord
- module ConnectionAdapters
- module PostgreSQL
- module OID # :nodoc:
- class Time < Type::Time # :nodoc:
- include Infinity
- end
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb
index e396ff4a1e..6155e53632 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb
@@ -4,7 +4,7 @@ module ActiveRecord
module OID # :nodoc:
# This class uses the data from PostgreSQL pg_type table to build
# the OID -> Type mapping.
- # - OID is and integer representing the type.
+ # - OID is an integer representing the type.
# - Type is an OID::Type object.
# This class has side effects on the +store+ passed during initialization.
class TypeMapInitializer # :nodoc:
@@ -15,11 +15,11 @@ module ActiveRecord
def run(records)
nodes = records.reject { |row| @store.key? row['oid'].to_i }
mapped, nodes = nodes.partition { |row| @store.key? row['typname'] }
- ranges, nodes = nodes.partition { |row| row['typtype'] == 'r' }
- enums, nodes = nodes.partition { |row| row['typtype'] == 'e' }
- domains, nodes = nodes.partition { |row| row['typtype'] == 'd' }
- arrays, nodes = nodes.partition { |row| row['typinput'] == 'array_in' }
- composites, nodes = nodes.partition { |row| row['typelem'] != '0' }
+ ranges, nodes = nodes.partition { |row| row['typtype'] == 'r'.freeze }
+ enums, nodes = nodes.partition { |row| row['typtype'] == 'e'.freeze }
+ domains, nodes = nodes.partition { |row| row['typtype'] == 'd'.freeze }
+ arrays, nodes = nodes.partition { |row| row['typinput'] == 'array_in'.freeze }
+ composites, nodes = nodes.partition { |row| row['typelem'].to_i != 0 }
mapped.each { |row| register_mapped_type(row) }
enums.each { |row| register_enum_type(row) }
@@ -29,6 +29,18 @@ module ActiveRecord
composites.each { |row| register_composite_type(row) }
end
+ def query_conditions_for_initial_load(type_map)
+ known_type_names = type_map.keys.map { |n| "'#{n}'" }
+ known_type_types = %w('r' 'e' 'd')
+ <<-SQL % [known_type_names.join(", "), known_type_types.join(", ")]
+ WHERE
+ t.typname IN (%s)
+ OR t.typtype IN (%s)
+ OR t.typinput = 'array_in(cstring,oid,integer)'::regprocedure
+ OR t.typelem != 0
+ SQL
+ end
+
private
def register_mapped_type(row)
alias_type row['oid'], row['typname']
@@ -39,14 +51,14 @@ module ActiveRecord
end
def register_array_type(row)
- if subtype = @store.lookup(row['typelem'].to_i)
- register row['oid'], OID::Array.new(subtype, row['typdelim'])
+ register_with_subtype(row['oid'], row['typelem'].to_i) do |subtype|
+ OID::Array.new(subtype, row['typdelim'])
end
end
def register_range_type(row)
- if subtype = @store.lookup(row['rngsubtype'].to_i)
- register row['oid'], OID::Range.new(subtype, row['typname'].to_sym)
+ register_with_subtype(row['oid'], row['rngsubtype'].to_i) do |subtype|
+ OID::Range.new(subtype, row['typname'].to_sym)
end
end
@@ -64,9 +76,13 @@ module ActiveRecord
end
end
- def register(oid, oid_type)
- oid = assert_valid_registration(oid, oid_type)
- @store.register_type(oid, oid_type)
+ def register(oid, oid_type = nil, &block)
+ oid = assert_valid_registration(oid, oid_type || block)
+ if block_given?
+ @store.register_type(oid, &block)
+ else
+ @store.register_type(oid, oid_type)
+ end
end
def alias_type(oid, target)
@@ -74,6 +90,14 @@ module ActiveRecord
@store.alias_type(oid, target)
end
+ def register_with_subtype(oid, target_oid)
+ if @store.key?(target_oid)
+ register(oid) do |_, *args|
+ yield @store.lookup(target_oid, *args)
+ end
+ end
+ end
+
def assert_valid_registration(oid, oid_type)
raise ArgumentError, "can't register nil type for OID #{oid}" if oid_type.nil?
oid.to_i
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb
index dd97393eac..5e839228e9 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb
@@ -3,21 +3,16 @@ module ActiveRecord
module PostgreSQL
module OID # :nodoc:
class Uuid < Type::Value # :nodoc:
- RFC_4122 = %r{\A\{?[a-fA-F0-9]{4}-?
- [a-fA-F0-9]{4}-?
- [a-fA-F0-9]{4}-?
- [1-5][a-fA-F0-9]{3}-?
- [8-Bab][a-fA-F0-9]{3}-?
- [a-fA-F0-9]{4}-?
- [a-fA-F0-9]{4}-?
- [a-fA-F0-9]{4}-?\}?\z}x
+ ACCEPTABLE_UUID = %r{\A\{?([a-fA-F0-9]{4}-?){8}\}?\z}x
+
+ alias_method :serialize, :deserialize
def type
:uuid
end
- def type_cast(value)
- value.to_s[RFC_4122, 0]
+ def cast(value)
+ value.to_s[ACCEPTABLE_UUID, 0]
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/vector.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/vector.rb
index de4187b028..b26e876b54 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/vector.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/vector.rb
@@ -16,7 +16,7 @@ module ActiveRecord
# FIXME: this should probably split on +delim+ and use +subtype+
# to cast the values. Unfortunately, the current Rails behavior
# is to just return the string.
- def type_cast(value)
+ def cast(value)
value
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/xml.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/xml.rb
index 7323f12763..d40d837cee 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/xml.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/xml.rb
@@ -7,11 +7,7 @@ module ActiveRecord
:xml
end
- def text?
- false
- end
-
- def type_cast_for_database(value)
+ def serialize(value)
return unless value
Data.new(super)
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
index cf5c8d288e..d5879ea7df 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
@@ -14,22 +14,6 @@ module ActiveRecord
@connection.unescape_bytea(value) if value
end
- # Quotes PostgreSQL-specific data types for SQL input.
- def quote(value, column = nil) #:nodoc:
- return super unless column
-
- case value
- when Float
- if value.infinite? || value.nan?
- "'#{value.to_s}'"
- else
- super
- end
- else
- super
- end
- end
-
# Quotes strings for use in SQL input.
def quote_string(s) #:nodoc:
@connection.escape(s)
@@ -47,6 +31,11 @@ module ActiveRecord
Utils.extract_schema_qualified_name(name.to_s).quoted
end
+ # Quotes schema names for use in SQL queries.
+ def quote_schema_name(name)
+ PGconn.quote_ident(name)
+ end
+
def quote_table_name_for_assignment(table, attr)
quote_column_name(attr)
end
@@ -56,30 +45,32 @@ module ActiveRecord
PGconn.quote_ident(name.to_s)
end
- # Quote date/time values for use in SQL input. Includes microseconds
- # if the value is a Time responding to usec.
+ # Quote date/time values for use in SQL input.
def quoted_date(value) #:nodoc:
- result = super
- if value.acts_like?(:time) && value.respond_to?(:usec)
- result = "#{result}.#{sprintf("%06d", value.usec)}"
- end
-
if value.year <= 0
bce_year = format("%04d", -value.year + 1)
- result = result.sub(/^-?\d+/, bce_year) + " BC"
+ super.sub(/^-?\d+/, bce_year) + " BC"
+ else
+ super
end
- result
end
# Does not quote function default values for UUID columns
- def quote_default_value(value, column) #:nodoc:
+ def quote_default_expression(value, column) #:nodoc:
if column.type == :uuid && value =~ /\(\)/
value
+ elsif column.respond_to?(:array?)
+ value = type_cast_from_column(column, value)
+ quote(value)
else
- quote(value, column)
+ super
end
end
+ def lookup_cast_type_from_column(column) # :nodoc:
+ type_map.lookup(column.oid, column.fmod, column.sql_type)
+ end
+
private
def _quote(value)
@@ -94,6 +85,12 @@ module ActiveRecord
elsif value.hex?
"X'#{value}'"
end
+ when Float
+ if value.infinite? || value.nan?
+ "'#{value}'"
+ else
+ super
+ end
else
super
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 52b307c432..44a7338bf5 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb
@@ -8,20 +8,39 @@ module ActiveRecord
def disable_referential_integrity # :nodoc:
if supports_disable_referential_integrity?
+ original_exception = nil
+
begin
- execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} DISABLE TRIGGER ALL" }.join(";"))
- rescue
- execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} DISABLE TRIGGER USER" }.join(";"))
+ 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
- end
- yield
- ensure
- if supports_disable_referential_integrity?
+
+ 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.
+Rails needs superuser privileges to disable referential integrity.
+
+ cause: #{original_exception.try(:message)}
+
+ WARNING
+ raise e
+ end
+
begin
- execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} ENABLE TRIGGER ALL" }.join(";"))
- rescue
- execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} ENABLE TRIGGER USER" }.join(";"))
+ transaction(requires_new: true) do
+ execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} ENABLE TRIGGER ALL" }.join(";"))
+ end
+ rescue ActiveRecord::ActiveRecordError
end
+ else
+ yield
end
end
end
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 83554bbf74..6399bddbee 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb
@@ -2,90 +2,153 @@ module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module ColumnMethods
- def xml(*args)
- options = args.extract_options!
- column(args[0], :xml, options)
+ # Defines the primary key field.
+ # Use of the native PostgreSQL UUID type is supported, and can be used
+ # by defining your tables as such:
+ #
+ # create_table :stuffs, id: :uuid do |t|
+ # t.string :content
+ # t.timestamps
+ # end
+ #
+ # By default, this will use the +uuid_generate_v4()+ function from the
+ # +uuid-ossp+ extension, which MUST be enabled on your database. To enable
+ # the +uuid-ossp+ extension, you can use the +enable_extension+ method in your
+ # migrations. To use a UUID primary key without +uuid-ossp+ enabled, you can
+ # set the +:default+ option to +nil+:
+ #
+ # create_table :stuffs, id: false do |t|
+ # t.primary_key :id, :uuid, default: nil
+ # t.uuid :foo_id
+ # t.timestamps
+ # end
+ #
+ # You may also pass a different UUID generation function from +uuid-ossp+
+ # or another library.
+ #
+ # Note that setting the UUID primary key default value to +nil+ will
+ # require you to assure that you always provide a UUID value before saving
+ # 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[:default] = options.fetch(:default, 'uuid_generate_v4()') if type == :uuid
+ super
+ end
+
+ def bigserial(*args, **options)
+ args.each { |name| column(name, :bigserial, options) }
+ end
+
+ def bit(*args, **options)
+ args.each { |name| column(name, :bit, options) }
+ end
+
+ def bit_varying(*args, **options)
+ args.each { |name| column(name, :bit_varying, options) }
end
- def tsvector(*args)
- options = args.extract_options!
- column(args[0], :tsvector, options)
+ def cidr(*args, **options)
+ args.each { |name| column(name, :cidr, options) }
end
- def int4range(name, options = {})
- column(name, :int4range, options)
+ def citext(*args, **options)
+ args.each { |name| column(name, :citext, options) }
end
- def int8range(name, options = {})
- column(name, :int8range, options)
+ def daterange(*args, **options)
+ args.each { |name| column(name, :daterange, options) }
end
- def tsrange(name, options = {})
- column(name, :tsrange, options)
+ def hstore(*args, **options)
+ args.each { |name| column(name, :hstore, options) }
end
- def tstzrange(name, options = {})
- column(name, :tstzrange, options)
+ def inet(*args, **options)
+ args.each { |name| column(name, :inet, options) }
end
- def numrange(name, options = {})
- column(name, :numrange, options)
+ def int4range(*args, **options)
+ args.each { |name| column(name, :int4range, options) }
end
- def daterange(name, options = {})
- column(name, :daterange, options)
+ def int8range(*args, **options)
+ args.each { |name| column(name, :int8range, options) }
end
- def hstore(name, options = {})
- column(name, :hstore, options)
+ def json(*args, **options)
+ args.each { |name| column(name, :json, options) }
end
- def ltree(name, options = {})
- column(name, :ltree, options)
+ def jsonb(*args, **options)
+ args.each { |name| column(name, :jsonb, options) }
end
- def inet(name, options = {})
- column(name, :inet, options)
+ def ltree(*args, **options)
+ args.each { |name| column(name, :ltree, options) }
end
- def cidr(name, options = {})
- column(name, :cidr, options)
+ def macaddr(*args, **options)
+ args.each { |name| column(name, :macaddr, options) }
end
- def macaddr(name, options = {})
- column(name, :macaddr, options)
+ def money(*args, **options)
+ args.each { |name| column(name, :money, options) }
end
- def uuid(name, options = {})
- column(name, :uuid, options)
+ def numrange(*args, **options)
+ args.each { |name| column(name, :numrange, options) }
end
- def json(name, options = {})
- column(name, :json, options)
+ def point(*args, **options)
+ args.each { |name| column(name, :point, options) }
end
- def jsonb(name, options = {})
- column(name, :jsonb, options)
+ def line(*args, **options)
+ args.each { |name| column(name, :line, options) }
end
- def citext(name, options = {})
- column(name, :citext, options)
+ def lseg(*args, **options)
+ args.each { |name| column(name, :lseg, options) }
end
- def point(name, options = {})
- column(name, :point, options)
+ def box(*args, **options)
+ args.each { |name| column(name, :box, options) }
end
- def bit(name, options)
- column(name, :bit, options)
+ def path(*args, **options)
+ args.each { |name| column(name, :path, options) }
end
- def bit_varying(name, options)
- column(name, :bit_varying, options)
+ def polygon(*args, **options)
+ args.each { |name| column(name, :polygon, options) }
end
- def money(name, options)
- column(name, :money, options)
+ def circle(*args, **options)
+ args.each { |name| column(name, :circle, options) }
+ end
+
+ def serial(*args, **options)
+ args.each { |name| column(name, :serial, options) }
+ end
+
+ def tsrange(*args, **options)
+ args.each { |name| column(name, :tsrange, options) }
+ end
+
+ def tstzrange(*args, **options)
+ args.each { |name| column(name, :tstzrange, options) }
+ end
+
+ def tsvector(*args, **options)
+ args.each { |name| column(name, :tsvector, options) }
+ end
+
+ def uuid(*args, **options)
+ args.each { |name| column(name, :uuid, options) }
+ end
+
+ def xml(*args, **options)
+ args.each { |name| column(name, :xml, options) }
end
end
@@ -96,47 +159,10 @@ module ActiveRecord
class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition
include ColumnMethods
- # Defines the primary key field.
- # Use of the native PostgreSQL UUID type is supported, and can be used
- # by defining your tables as such:
- #
- # create_table :stuffs, id: :uuid do |t|
- # t.string :content
- # t.timestamps
- # end
- #
- # By default, this will use the +uuid_generate_v4()+ function from the
- # +uuid-ossp+ extension, which MUST be enabled on your database. To enable
- # the +uuid-ossp+ extension, you can use the +enable_extension+ method in your
- # migrations. To use a UUID primary key without +uuid-ossp+ enabled, you can
- # set the +:default+ option to +nil+:
- #
- # create_table :stuffs, id: false do |t|
- # t.primary_key :id, :uuid, default: nil
- # t.uuid :foo_id
- # t.timestamps
- # end
- #
- # You may also pass a different UUID generation function from +uuid-ossp+
- # or another library.
- #
- # Note that setting the UUID primary key default value to +nil+ will
- # require you to assure that you always provide a UUID value before saving
- # 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 = {})
- return super unless type == :uuid
- options[:default] = options.fetch(:default, 'uuid_generate_v4()')
- options[:primary_key] = true
- column name, type, options
- end
-
- def column(name, type = nil, options = {})
- super
- column = self[name]
+ def new_column_definition(name, type, options) # :nodoc:
+ column = super
column.array = options[:array]
-
- self
+ column
end
private
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb
new file mode 100644
index 0000000000..a4f0742516
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb
@@ -0,0 +1,54 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module ColumnDumper
+ def column_spec_for_primary_key(column)
+ spec = {}
+ if column.serial?
+ return unless column.bigint?
+ spec[:id] = ':bigserial'
+ elsif column.type == :uuid
+ spec[:id] = ':uuid'
+ spec[:default] = column.default_function.inspect
+ else
+ spec[:id] = column.type.inspect
+ spec.merge!(prepare_column_options(column).delete_if { |key, _| [:name, :type, :null].include?(key) })
+ end
+ spec
+ end
+
+ # Adds +:array+ option to the default set
+ def prepare_column_options(column)
+ spec = super
+ spec[:array] = 'true' if column.array?
+ spec
+ end
+
+ # Adds +:array+ as a valid migration key
+ def migration_keys
+ super + [:array]
+ end
+
+ private
+
+ def schema_type(column)
+ return super unless column.serial?
+
+ if column.bigint?
+ 'bigserial'
+ else
+ 'serial'
+ end
+ end
+
+ def schema_default(column)
+ if column.default_function
+ column.default_function.inspect unless column.serial?
+ else
+ super
+ end
+ end
+ end
+ end
+ end
+end
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 7042817672..aaf5b2898b 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
@@ -4,40 +4,16 @@ module ActiveRecord
class SchemaCreation < AbstractAdapter::SchemaCreation
private
- def visit_AddColumn(o)
- sql_type = type_to_sql(o.type, o.limit, o.precision, o.scale)
- sql = "ADD COLUMN #{quote_column_name(o.name)} #{sql_type}"
- add_column_options!(sql, column_options(o))
- end
-
def visit_ColumnDefinition(o)
- sql = super
- if o.primary_key? && o.type != :primary_key
- sql << " PRIMARY KEY "
- add_column_options!(sql, column_options(o))
- end
- sql
+ o.sql_type = type_to_sql(o.type, o.limit, o.precision, o.scale, o.array)
+ super
end
def add_column_options!(sql, options)
- if options[:array] || options[:column].try(:array)
- sql << '[]'
- end
-
- column = options.fetch(:column) { return super }
- if column.type == :uuid && options[:default] =~ /\(\)/
- sql << " DEFAULT #{options[:default]}"
- else
- super
- end
- end
-
- def type_for_column(column)
- if column.array
- @conn.lookup_cast_type("#{column.sql_type}[]")
- else
- super
+ if options[:collation]
+ sql << " COLLATE \"#{options[:collation]}\""
end
+ super
end
end
@@ -60,8 +36,8 @@ module ActiveRecord
def create_database(name, options = {})
options = { encoding: 'utf8' }.merge!(options.symbolize_keys)
- option_string = options.sum do |key, value|
- case key
+ option_string = options.inject("") do |memo, (key, value)|
+ memo += case key
when :owner
" OWNER = \"#{value}\""
when :template
@@ -92,12 +68,18 @@ module ActiveRecord
execute "DROP DATABASE IF EXISTS #{quote_table_name(name)}"
end
- # Returns the list of all tables in the schema search path or a specified schema.
+ # Returns the list of all tables in the schema search path.
def tables(name = nil)
- query(<<-SQL, 'SCHEMA').map { |row| row[0] }
- SELECT tablename
- FROM pg_tables
- WHERE schemaname = ANY (current_schemas(false))
+ select_values("SELECT tablename FROM pg_tables WHERE schemaname = ANY(current_schemas(false))", 'SCHEMA')
+ end
+
+ def data_sources # :nodoc
+ select_values(<<-SQL, 'SCHEMA')
+ SELECT c.relname
+ FROM pg_class c
+ LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
+ WHERE c.relkind IN ('r', 'v','m') -- (r)elation/table, (v)iew, (m)aterialized view
+ AND n.nspname = ANY (current_schemas(false))
SQL
end
@@ -108,7 +90,7 @@ module ActiveRecord
name = Utils.extract_schema_qualified_name(name.to_s)
return false unless name.identifier
- exec_query(<<-SQL, 'SCHEMA').rows.first[0].to_i > 0
+ select_value(<<-SQL, 'SCHEMA').to_i > 0
SELECT COUNT(*)
FROM pg_class c
LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
@@ -117,26 +99,56 @@ module ActiveRecord
AND n.nspname = #{name.schema ? "'#{name.schema}'" : 'ANY (current_schemas(false))'}
SQL
end
+ alias data_source_exists? table_exists?
+
+ def views # :nodoc:
+ select_values(<<-SQL, 'SCHEMA')
+ SELECT c.relname
+ FROM pg_class c
+ LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
+ WHERE c.relkind IN ('v','m') -- (v)iew, (m)aterialized view
+ AND n.nspname = ANY (current_schemas(false))
+ SQL
+ end
+
+ def view_exists?(view_name) # :nodoc:
+ name = Utils.extract_schema_qualified_name(view_name.to_s)
+ return false unless name.identifier
+
+ select_values(<<-SQL, 'SCHEMA').any?
+ SELECT c.relname
+ FROM pg_class c
+ LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
+ WHERE c.relkind IN ('v','m') -- (v)iew, (m)aterialized view
+ AND c.relname = '#{name.identifier}'
+ AND n.nspname = #{name.schema ? "'#{name.schema}'" : 'ANY (current_schemas(false))'}
+ SQL
+ end
+
+ def drop_table(table_name, options = {}) # :nodoc:
+ execute "DROP TABLE#{' IF EXISTS' if options[:if_exists]} #{quote_table_name(table_name)}#{' CASCADE' if options[:force] == :cascade}"
+ end
# Returns true if schema exists.
def schema_exists?(name)
- exec_query(<<-SQL, 'SCHEMA').rows.first[0].to_i > 0
- SELECT COUNT(*)
- FROM pg_namespace
- WHERE nspname = '#{name}'
- SQL
+ select_value("SELECT COUNT(*) FROM pg_namespace WHERE nspname = '#{name}'", 'SCHEMA').to_i > 0
end
+ # Verifies existence of an index with a given name.
def index_name_exists?(table_name, index_name, default)
- exec_query(<<-SQL, 'SCHEMA').rows.first[0].to_i > 0
+ table = Utils.extract_schema_qualified_name(table_name.to_s)
+ index = Utils.extract_schema_qualified_name(index_name.to_s)
+
+ select_value(<<-SQL, 'SCHEMA').to_i > 0
SELECT COUNT(*)
FROM pg_class t
INNER JOIN pg_index d ON t.oid = d.indrelid
INNER JOIN pg_class i ON d.indexrelid = i.oid
+ LEFT JOIN pg_namespace n ON n.oid = i.relnamespace
WHERE i.relkind = 'i'
- AND i.relname = '#{index_name}'
- AND t.relname = '#{table_name}'
- AND i.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname = ANY (current_schemas(false)) )
+ AND i.relname = '#{index.identifier}'
+ AND t.relname = '#{table.identifier}'
+ AND n.nspname = #{index.schema ? "'#{index.schema}'" : 'ANY (current_schemas(false))'}
SQL
end
@@ -156,8 +168,8 @@ module ActiveRecord
result.map do |row|
index_name = row[0]
- unique = row[1] == 't'
- indkey = row[2].split(" ")
+ unique = row[1]
+ indkey = row[2].split(" ").map(&:to_i)
inddef = row[3]
oid = row[4]
@@ -185,53 +197,48 @@ module ActiveRecord
# Returns the list of all column definitions for a table.
def columns(table_name)
# Limit, precision, and scale are all handled by the superclass.
- column_definitions(table_name).map do |column_name, type, default, notnull, oid, fmod|
- oid = get_oid_type(oid.to_i, fmod.to_i, column_name, type)
- default_value = extract_value_from_default(oid, default)
+ column_definitions(table_name).map do |column_name, type, default, notnull, oid, fmod, collation|
+ oid = oid.to_i
+ fmod = fmod.to_i
+ type_metadata = fetch_type_metadata(column_name, type, oid, fmod)
+ default_value = extract_value_from_default(default)
default_function = extract_default_function(default_value, default)
- new_column(column_name, default_value, oid, type, notnull == 'f', default_function)
+ new_column(column_name, default_value, type_metadata, !notnull, default_function, collation)
end
end
- def new_column(name, default, cast_type, sql_type = nil, null = true, default_function = nil) # :nodoc:
- PostgreSQLColumn.new(name, default, cast_type, sql_type, null, default_function)
+ def new_column(name, default, sql_type_metadata = nil, null = true, default_function = nil, collation = nil) # :nodoc:
+ PostgreSQLColumn.new(name, default, sql_type_metadata, null, default_function, collation)
end
# Returns the current database name.
def current_database
- query('select current_database()', 'SCHEMA')[0][0]
+ select_value('select current_database()', 'SCHEMA')
end
# Returns the current schema name.
def current_schema
- query('SELECT current_schema', 'SCHEMA')[0][0]
+ select_value('SELECT current_schema', 'SCHEMA')
end
# Returns the current database encoding format.
def encoding
- query(<<-end_sql, 'SCHEMA')[0][0]
- SELECT pg_encoding_to_char(pg_database.encoding) FROM pg_database
- WHERE pg_database.datname LIKE '#{current_database}'
- end_sql
+ select_value("SELECT pg_encoding_to_char(encoding) FROM pg_database WHERE datname LIKE '#{current_database}'", 'SCHEMA')
end
# Returns the current database collation.
def collation
- query(<<-end_sql, 'SCHEMA')[0][0]
- SELECT pg_database.datcollate FROM pg_database WHERE pg_database.datname LIKE '#{current_database}'
- end_sql
+ select_value("SELECT datcollate FROM pg_database WHERE datname LIKE '#{current_database}'", 'SCHEMA')
end
# Returns the current database ctype.
def ctype
- query(<<-end_sql, 'SCHEMA')[0][0]
- SELECT pg_database.datctype FROM pg_database WHERE pg_database.datname LIKE '#{current_database}'
- end_sql
+ select_value("SELECT datctype FROM pg_database WHERE datname LIKE '#{current_database}'", 'SCHEMA')
end
# Returns an array of schema names.
def schema_names
- query(<<-SQL, 'SCHEMA').flatten
+ select_values(<<-SQL, 'SCHEMA')
SELECT nspname
FROM pg_namespace
WHERE nspname !~ '^pg_.*'
@@ -242,12 +249,12 @@ module ActiveRecord
# Creates a schema for the given schema name.
def create_schema schema_name
- execute "CREATE SCHEMA #{schema_name}"
+ execute "CREATE SCHEMA #{quote_schema_name(schema_name)}"
end
# Drops the schema for the given schema name.
- def drop_schema schema_name
- execute "DROP SCHEMA #{schema_name} CASCADE"
+ def drop_schema(schema_name, options = {})
+ execute "DROP SCHEMA#{' IF EXISTS' if options[:if_exists]} #{quote_schema_name(schema_name)} CASCADE"
end
# Sets the schema search path to a string of comma-separated schema names.
@@ -264,12 +271,12 @@ module ActiveRecord
# Returns the active schema search path.
def schema_search_path
- @schema_search_path ||= query('SHOW search_path', 'SCHEMA')[0][0]
+ @schema_search_path ||= select_value('SHOW search_path', 'SCHEMA')
end
# Returns the current client message level.
def client_min_messages
- query('SHOW client_min_messages', 'SCHEMA')[0][0]
+ select_value('SHOW client_min_messages', 'SCHEMA')
end
# Set the client message level.
@@ -281,16 +288,28 @@ module ActiveRecord
def default_sequence_name(table_name, pk = nil) #:nodoc:
result = serial_sequence(table_name, pk || 'id')
return nil unless result
- Utils.extract_schema_qualified_name(result)
+ Utils.extract_schema_qualified_name(result).to_s
rescue ActiveRecord::StatementInvalid
- PostgreSQL::Name.new(nil, "#{table_name}_#{pk || 'id'}_seq")
+ PostgreSQL::Name.new(nil, "#{table_name}_#{pk || 'id'}_seq").to_s
end
def serial_sequence(table, column)
- result = exec_query(<<-eosql, 'SCHEMA')
- SELECT pg_get_serial_sequence('#{table}', '#{column}')
- eosql
- result.rows.first.first
+ select_value("SELECT pg_get_serial_sequence('#{table}', '#{column}')", 'SCHEMA')
+ end
+
+ # Sets the sequence of a table's primary key to the specified value.
+ def set_pk_sequence!(table, value) #:nodoc:
+ pk, sequence = pk_and_sequence_for(table)
+
+ if pk
+ if sequence
+ quoted_sequence = quote_table_name(sequence)
+
+ select_value("SELECT setval('#{quoted_sequence}', #{value})", 'SCHEMA')
+ else
+ @logger.warn "#{table} has primary key #{pk} with no default sequence" if @logger
+ end
+ end
end
# Resets the sequence of a table's primary key to the maximum value.
@@ -309,7 +328,7 @@ module ActiveRecord
if pk && sequence
quoted_sequence = quote_table_name(sequence)
- select_value <<-end_sql, 'SCHEMA'
+ select_value(<<-end_sql, 'SCHEMA')
SELECT setval('#{quoted_sequence}', (SELECT COALESCE(MAX(#{quote_column_name pk})+(SELECT increment_by FROM #{quoted_sequence}), (SELECT min_value FROM #{quoted_sequence})) FROM #{quote_table_name(table)}), false)
end_sql
end
@@ -369,17 +388,19 @@ module ActiveRecord
nil
end
- # Returns just a table's primary key
- def primary_key(table)
- row = exec_query(<<-end_sql, 'SCHEMA').rows.first
- SELECT attr.attname
- FROM pg_attribute attr
- INNER JOIN pg_constraint cons ON attr.attrelid = cons.conrelid AND attr.attnum = cons.conkey[1]
- WHERE cons.contype = 'p'
- AND cons.conrelid = '#{quote_table_name(table)}'::regclass
- end_sql
-
- row && row.first
+ def primary_keys(table_name) # :nodoc:
+ select_values(<<-SQL.strip_heredoc, 'SCHEMA')
+ WITH pk_constraint AS (
+ SELECT conrelid, unnest(conkey) AS connum FROM pg_constraint
+ WHERE contype = 'p'
+ AND conrelid = '#{quote_table_name(table_name)}'::regclass
+ ), cons AS (
+ SELECT conrelid, connum, row_number() OVER() AS rownum FROM pk_constraint
+ )
+ SELECT attr.attname FROM pg_attribute attr
+ INNER JOIN cons ON attr.attrelid = cons.conrelid AND attr.attnum = cons.connum
+ ORDER BY cons.rownum
+ SQL
end
# Renames a table.
@@ -394,58 +415,69 @@ module ActiveRecord
pk, seq = pk_and_sequence_for(new_name)
if seq && seq.identifier == "#{table_name}_#{pk}_seq"
new_seq = "#{new_name}_#{pk}_seq"
- execute "ALTER TABLE #{quote_table_name(seq)} RENAME TO #{quote_table_name(new_seq)}"
+ idx = "#{table_name}_pkey"
+ new_idx = "#{new_name}_pkey"
+ execute "ALTER TABLE #{seq.quoted} RENAME TO #{quote_table_name(new_seq)}"
+ execute "ALTER INDEX #{quote_table_name(idx)} RENAME TO #{quote_table_name(new_idx)}"
end
rename_table_indexes(table_name, new_name)
end
- # Adds a new column to the named table.
- # See TableDefinition#column for details of the options you can use.
- def add_column(table_name, column_name, type, options = {})
+ def add_column(table_name, column_name, type, options = {}) #:nodoc:
clear_cache!
super
end
- # Changes the column of a table.
- def change_column(table_name, column_name, type, options = {})
+ def change_column(table_name, column_name, type, options = {}) #:nodoc:
clear_cache!
quoted_table_name = quote_table_name(table_name)
- sql_type = type_to_sql(type, options[:limit], options[:precision], options[:scale])
- sql_type << "[]" if options[:array]
- execute "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quote_column_name(column_name)} TYPE #{sql_type}"
+ quoted_column_name = quote_column_name(column_name)
+ sql_type = type_to_sql(type, options[:limit], options[:precision], options[:scale], options[:array])
+ sql = "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quoted_column_name} TYPE #{sql_type}"
+ 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[:limit], options[:precision], options[:scale], options[:array])
+ sql << " USING CAST(#{quoted_column_name} AS #{cast_as_type})"
+ end
+ execute sql
change_column_default(table_name, column_name, options[:default]) if options_include_default?(options)
change_column_null(table_name, column_name, options[:null], options[:default]) if options.key?(:null)
end
# Changes the default value of a table column.
- def change_column_default(table_name, column_name, default)
+ 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_value(default, column)}"
+ execute alter_column_query % "SET DEFAULT #{quote_default_expression(default, column)}"
end
end
- def change_column_null(table_name, column_name, null, default = nil)
+ 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_value(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")
end
# Renames a column in a table.
- def rename_column(table_name, column_name, new_column_name)
+ def rename_column(table_name, column_name, new_column_name) #:nodoc:
clear_cache!
execute "ALTER TABLE #{quote_table_name(table_name)} RENAME COLUMN #{quote_column_name(column_name)} TO #{quote_column_name(new_column_name)}"
rename_column_indexes(table_name, column_name, new_column_name)
@@ -456,17 +488,28 @@ module ActiveRecord
execute "CREATE #{index_type} INDEX #{index_algorithm} #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} #{index_using} (#{index_columns})#{index_options}"
end
- def remove_index!(table_name, index_name) #:nodoc:
- execute "DROP INDEX #{quote_table_name(index_name)}"
+ def remove_index(table_name, options = {}) #:nodoc:
+ index_name = index_name_for_remove(table_name, options)
+ algorithm =
+ if Hash === options && options.key?(:algorithm)
+ index_algorithms.fetch(options[:algorithm]) do
+ raise ArgumentError.new("Algorithm must be one of the following: #{index_algorithms.keys.map(&:inspect).join(', ')}")
+ end
+ end
+ execute "DROP INDEX #{algorithm} #{quote_table_name(index_name)}"
end
+ # Renames an index of a table. Raises error if length of new
+ # index name is greater than allowed limit.
def rename_index(table_name, old_name, new_name)
+ validate_index_length!(table_name, new_name)
+
execute "ALTER INDEX #{quote_column_name(old_name)} RENAME TO #{quote_table_name(new_name)}"
end
def foreign_keys(table_name)
fk_info = select_all <<-SQL.strip_heredoc
- SELECT t2.relname 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
+ 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
FROM pg_constraint c
JOIN pg_class t1 ON c.conrelid = t1.oid
JOIN pg_class t2 ON c.confrelid = t2.oid
@@ -488,6 +531,7 @@ module ActiveRecord
options[:on_delete] = extract_foreign_key_action(row['on_delete'])
options[:on_update] = extract_foreign_key_action(row['on_update'])
+
ForeignKeyDefinition.new(table_name, row['to_table'], options)
end
end
@@ -505,41 +549,35 @@ module ActiveRecord
end
# Maps logical Rails types to PostgreSQL-specific data types.
- def type_to_sql(type, limit = nil, precision = nil, scale = nil)
- case type.to_s
+ def type_to_sql(type, limit = nil, precision = nil, scale = nil, array = nil)
+ sql = case type.to_s
when 'binary'
# PostgreSQL doesn't support limits on binary (bytea) columns.
- # The hard limit is 1Gb, because of a 32-bit size field, and TOAST.
+ # The hard limit is 1GB, because of a 32-bit size field, and TOAST.
case limit
when nil, 0..0x3fffffff; super(type)
else raise(ActiveRecordError, "No binary type has byte size #{limit}.")
end
when 'text'
# PostgreSQL doesn't support limits on text columns.
- # The hard limit is 1Gb, according to section 8.3 in the manual.
+ # The hard limit is 1GB, according to section 8.3 in the manual.
case limit
when nil, 0..0x3fffffff; super(type)
else raise(ActiveRecordError, "The limit on text can be at most 1GB - 1byte.")
end
when 'integer'
- return 'integer' unless limit
-
case limit
- when 1, 2; 'smallint'
- when 3, 4; 'integer'
- when 5..8; 'bigint'
- else raise(ActiveRecordError, "No integer type has byte size #{limit}. Use a numeric with precision 0 instead.")
- end
- when 'datetime'
- return super unless precision
-
- case precision
- when 0..6; "timestamp(#{precision})"
- else raise(ActiveRecordError, "No timestamp type has precision of #{precision}. The allowed range of precision is from 0 to 6")
+ when 1, 2; 'smallint'
+ when nil, 3, 4; 'integer'
+ when 5..8; 'bigint'
+ else raise(ActiveRecordError, "No integer type has byte size #{limit}. Use a numeric with precision 0 instead.")
end
else
- super
+ super(type, limit, precision, scale)
end
+
+ sql << '[]' if array && type != :primary_key
+ sql
end
# PostgreSQL requires the ORDER BY columns in the select list for distinct queries, and
@@ -549,11 +587,24 @@ module ActiveRecord
# Convert Arel node to string
s = s.to_sql unless s.is_a?(String)
# Remove any ASC/DESC modifiers
- s.gsub(/\s+(?:ASC|DESC)?\s*(?:NULLS\s+(?:FIRST|LAST)\s*)?/i, '')
+ s.gsub(/\s+(?:ASC|DESC)\b/i, '')
+ .gsub(/\s+NULLS\s+(?:FIRST|LAST)\b/i, '')
}.reject(&:blank?).map.with_index { |column, i| "#{column} AS alias_#{i}" }
[super, *order_columns].join(', ')
end
+
+ def fetch_type_metadata(column_name, sql_type, oid, fmod)
+ cast_type = get_oid_type(oid, fmod, column_name, sql_type)
+ simple_type = SqlTypeMetadata.new(
+ sql_type: sql_type,
+ type: cast_type.type,
+ limit: cast_type.limit,
+ precision: cast_type.precision,
+ scale: cast_type.scale,
+ )
+ PostgreSQLTypeMetadata.new(simple_type, oid: oid, fmod: fmod)
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb b/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb
new file mode 100644
index 0000000000..b2c49989a4
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb
@@ -0,0 +1,35 @@
+module ActiveRecord
+ module ConnectionAdapters
+ class PostgreSQLTypeMetadata < DelegateClass(SqlTypeMetadata)
+ attr_reader :oid, :fmod, :array
+
+ def initialize(type_metadata, oid: nil, fmod: nil)
+ super(type_metadata)
+ @type_metadata = type_metadata
+ @oid = oid
+ @fmod = fmod
+ @array = /\[\]$/ === type_metadata.sql_type
+ end
+
+ def sql_type
+ super.gsub(/\[\]$/, "".freeze)
+ end
+
+ def ==(other)
+ other.is_a?(PostgreSQLTypeMetadata) &&
+ attributes_for_hash == other.attributes_for_hash
+ end
+ alias eql? ==
+
+ def hash
+ attributes_for_hash.hash
+ end
+
+ protected
+
+ def attributes_for_hash
+ [self.class, @type_metadata, oid, fmod]
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb b/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb
index 0290bcb48c..9a0b80d7d3 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb
@@ -18,7 +18,11 @@ module ActiveRecord
end
def quoted
- parts.map { |p| PGconn.quote_ident(p) }.join SEPARATOR
+ if schema
+ PGconn.quote_ident(schema) << SEPARATOR << PGconn.quote_ident(identifier)
+ else
+ PGconn.quote_ident(identifier)
+ end
end
def ==(o)
@@ -32,8 +36,11 @@ module ActiveRecord
protected
def unquote(part)
- return unless part
- part.gsub(/(^"|"$)/,'')
+ if part && part.start_with?('"')
+ part[1..-2]
+ else
+ part
+ end
end
def parts
@@ -57,7 +64,11 @@ module ActiveRecord
# * <tt>"schema_name".table_name</tt>
# * <tt>"schema.name"."table name"</tt>
def extract_schema_qualified_name(string)
- table, schema = string.scan(/[^".\s]+|"[^"]*"/)[0..1].reverse
+ schema, table = string.scan(/[^".\s]+|"[^"]*"/)
+ if table.nil?
+ table = schema
+ schema = nil
+ end
PostgreSQL::Name.new(schema, table)
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
index eede374678..236c067fd5 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
@@ -1,21 +1,20 @@
-require 'active_record/connection_adapters/abstract_adapter'
-require 'active_record/connection_adapters/statement_pool'
-
-require 'active_record/connection_adapters/postgresql/utils'
-require 'active_record/connection_adapters/postgresql/column'
-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_definitions'
-require 'active_record/connection_adapters/postgresql/schema_statements'
-require 'active_record/connection_adapters/postgresql/database_statements'
-
-require 'arel/visitors/bind_visitor'
-
-# Make sure we're using pg high enough for PGResult#values
-gem 'pg', '~> 0.11'
+# Make sure we're using pg high enough for type casts and Ruby 2.2+ compatibility
+gem 'pg', '~> 0.18'
require 'pg'
+require "active_record/connection_adapters/abstract_adapter"
+require "active_record/connection_adapters/postgresql/column"
+require "active_record/connection_adapters/postgresql/database_statements"
+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_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"
+require "active_record/connection_adapters/statement_pool"
+
require 'ipaddr'
module ActiveRecord
@@ -64,20 +63,21 @@ module ActiveRecord
# <tt>SET client_min_messages TO <min_messages></tt> call on the connection.
# * <tt>:variables</tt> - An optional hash of additional parameters that
# will be used in <tt>SET SESSION key = val</tt> calls on the connection.
- # * <tt>:insert_returning</tt> - An optional boolean to control the use or <tt>RETURNING</tt> for <tt>INSERT</tt> statements
+ # * <tt>:insert_returning</tt> - An optional boolean to control the use of <tt>RETURNING</tt> for <tt>INSERT</tt> statements
# defaults to true.
#
# Any further options are used as connection parameters to libpq. See
- # http://www.postgresql.org/docs/9.1/static/libpq-connect.html for the
+ # http://www.postgresql.org/docs/current/static/libpq-connect.html for the
# list of parameters.
#
# In addition, default connection parameters of libpq can be set per environment variables.
- # See http://www.postgresql.org/docs/9.1/static/libpq-envars.html .
+ # See http://www.postgresql.org/docs/current/static/libpq-envars.html .
class PostgreSQLAdapter < AbstractAdapter
- ADAPTER_NAME = 'PostgreSQL'
+ ADAPTER_NAME = 'PostgreSQL'.freeze
NATIVE_DATABASE_TYPES = {
primary_key: "serial primary key",
+ bigserial: "bigserial",
string: { name: "character varying" },
text: { name: "text" },
integer: { name: "integer" },
@@ -94,6 +94,7 @@ module ActiveRecord
int8range: { name: "int8range" },
binary: { name: "bytea" },
boolean: { name: "boolean" },
+ bigint: { name: "bigint" },
xml: { name: "xml" },
tsvector: { name: "tsvector" },
hstore: { name: "hstore" },
@@ -102,6 +103,7 @@ module ActiveRecord
macaddr: { name: "macaddr" },
uuid: { name: "uuid" },
json: { name: "json" },
+ jsonb: { name: "jsonb" },
ltree: { name: "ltree" },
citext: { name: "citext" },
point: { name: "point" },
@@ -116,32 +118,14 @@ module ActiveRecord
include PostgreSQL::ReferentialIntegrity
include PostgreSQL::SchemaStatements
include PostgreSQL::DatabaseStatements
+ include PostgreSQL::ColumnDumper
include Savepoints
- # Returns 'PostgreSQL' as adapter name for identification purposes.
- def adapter_name
- ADAPTER_NAME
- end
-
def schema_creation # :nodoc:
PostgreSQL::SchemaCreation.new self
end
- # Adds `:array` option to the default set provided by the
- # AbstractAdapter
- def prepare_column_options(column, types) # :nodoc:
- spec = super
- spec[:array] = 'true' if column.respond_to?(:array) && column.array
- spec[:default] = "\"#{column.default_function}\"" if column.default_function
- spec
- end
-
- # Adds `:array` as a valid migration key
- def migration_keys
- super + [:array]
- end
-
- # Returns +true+, since this connection adapter supports prepared statement
+ # Returns true, since this connection adapter supports prepared statement
# caching.
def supports_statement_cache?
true
@@ -163,52 +147,39 @@ module ActiveRecord
true
end
+ def supports_views?
+ true
+ end
+
+ def supports_datetime_with_precision?
+ true
+ end
+
+ def supports_json?
+ postgresql_version >= 90200
+ end
+
def index_algorithms
{ concurrently: 'CONCURRENTLY' }
end
class StatementPool < ConnectionAdapters::StatementPool
def initialize(connection, max)
- super
+ super(max)
+ @connection = connection
@counter = 0
- @cache = Hash.new { |h,pid| h[pid] = {} }
end
- def each(&block); cache.each(&block); end
- def key?(key); cache.key?(key); end
- def [](key); cache[key]; end
- def length; cache.length; end
-
def next_key
"a#{@counter + 1}"
end
def []=(sql, key)
- while @max <= cache.size
- dealloc(cache.shift.last)
- end
- @counter += 1
- cache[sql] = key
- end
-
- def clear
- cache.each_value do |stmt_key|
- dealloc stmt_key
- end
- cache.clear
- end
-
- def delete(sql_key)
- dealloc cache[sql_key]
- cache.delete sql_key
+ super.tap { @counter += 1 }
end
private
- def cache
- @cache[Process.pid]
- end
-
def dealloc(key)
@connection.query "DEALLOCATE #{key}" if connection_active?
end
@@ -227,6 +198,7 @@ module ActiveRecord
@visitor = Arel::Visitors::PostgreSQL.new self
if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true })
@prepared_statements = true
+ @visitor.extend(DetermineIfPreparableVisitor)
else
@prepared_statements = false
end
@@ -238,6 +210,7 @@ module ActiveRecord
@table_alias_length = nil
connect
+ add_pg_encoders
@statements = StatementPool.new @connection,
self.class.type_cast_config_to_integer(config.fetch(:statement_limit) { 1000 })
@@ -245,6 +218,8 @@ module ActiveRecord
raise "Your version of PostgreSQL (#{postgresql_version}) is too old, please upgrade!"
end
+ add_pg_decoders
+
@type_map = Type::HashLookupTypeMap.new
initialize_type_map(type_map)
@local_tz = execute('SHOW TIME ZONE', 'SCHEMA').first["TimeZone"]
@@ -256,6 +231,10 @@ module ActiveRecord
@statements.clear
end
+ def truncate(table_name, name = nil)
+ exec_query "TRUNCATE TABLE #{quote_table_name(table_name)}", name, []
+ end
+
# Is this connection alive and ready for queries?
def active?
@connection.query 'SELECT 1'
@@ -388,6 +367,16 @@ module ActiveRecord
super(oid)
end
+ def column_name_for_operation(operation, node) # :nodoc:
+ OPERATION_ALIASES.fetch(operation) { operation.downcase }
+ end
+
+ OPERATION_ALIASES = { # :nodoc:
+ "maximum" => "max",
+ "minimum" => "min",
+ "average" => "avg",
+ }
+
protected
# Returns the version of the connected PostgreSQL server.
@@ -395,7 +384,7 @@ module ActiveRecord
@connection.server_version
end
- # See http://www.postgresql.org/docs/9.1/static/errcodes-appendix.html
+ # See http://www.postgresql.org/docs/current/static/errcodes-appendix.html
FOREIGN_KEY_VIOLATION = "23503"
UNIQUE_VIOLATION = "23505"
@@ -428,11 +417,11 @@ module ActiveRecord
end
def initialize_type_map(m) # :nodoc:
- register_class_with_limit m, 'int2', OID::Integer
- m.alias_type 'int4', 'int2'
- m.alias_type 'int8', 'int2'
+ register_class_with_limit m, 'int2', Type::Integer
+ register_class_with_limit m, 'int4', Type::Integer
+ register_class_with_limit m, 'int8', Type::Integer
m.alias_type 'oid', 'int2'
- m.register_type 'float4', OID::Float.new
+ m.register_type 'float4', Type::Float.new
m.alias_type 'float8', 'float4'
m.register_type 'text', Type::Text.new
register_class_with_limit m, 'varchar', Type::String
@@ -443,8 +432,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', OID::Date.new
- m.register_type 'time', OID::Time.new
+ m.register_type 'date', Type::Date.new
m.register_type 'money', OID::Money.new
m.register_type 'bytea', OID::Bytea.new
@@ -470,10 +458,8 @@ module ActiveRecord
m.alias_type 'lseg', 'varchar'
m.alias_type 'box', 'varchar'
- m.register_type 'timestamp' do |_, _, sql_type|
- precision = extract_precision(sql_type)
- OID::DateTime.new(precision: precision)
- end
+ register_class_with_precision m, 'time', Type::Time
+ register_class_with_precision m, 'timestamp', OID::DateTime
m.register_type 'numeric' do |_, fmod, sql_type|
precision = extract_precision(sql_type)
@@ -500,23 +486,26 @@ module ActiveRecord
def extract_limit(sql_type) # :nodoc:
case sql_type
- when /^bigint/i; 8
- when /^smallint/i; 2
- else super
+ when /^bigint/i, /^int8/i
+ 8
+ when /^smallint/i
+ 2
+ else
+ super
end
end
# Extracts the value from a PostgreSQL column default definition.
- def extract_value_from_default(oid, default) # :nodoc:
+ def extract_value_from_default(default) # :nodoc:
case default
# Quoted types
when /\A[\(B]?'(.*)'::/m
- $1.gsub(/''/, "'")
+ $1.gsub("''".freeze, "'".freeze)
# Boolean types
- when 'true', 'false'
+ when 'true'.freeze, 'false'.freeze
default
# Numeric types
- when /\A\(?(-?\d+(\.\d*)?\)?(::bigint)?)\z/
+ when /\A\(?(-?\d+(\.\d*)?)\)?(::bigint)?\z/
$1
# Object identifier types
when /\A-?\d+\z/
@@ -537,6 +526,8 @@ module ActiveRecord
end
def load_additional_types(type_map, oids = nil) # :nodoc:
+ initializer = OID::TypeMapInitializer.new(type_map)
+
if supports_ranges?
query = <<-SQL
SELECT t.oid, t.typname, t.typelem, t.typdelim, t.typinput, r.rngsubtype, t.typtype, t.typbasetype
@@ -552,37 +543,41 @@ module ActiveRecord
if oids
query += "WHERE t.oid::integer IN (%s)" % oids.join(", ")
+ else
+ query += initializer.query_conditions_for_initial_load(type_map)
end
- initializer = OID::TypeMapInitializer.new(type_map)
- records = execute(query, 'SCHEMA')
- initializer.run(records)
+ execute_and_clear(query, 'SCHEMA', []) do |records|
+ initializer.run(records)
+ end
end
FEATURE_NOT_SUPPORTED = "0A000" #:nodoc:
- def execute_and_clear(sql, name, binds)
- result = without_prepared_statement?(binds) ? exec_no_cache(sql, name, binds) :
- exec_cache(sql, name, binds)
+ def execute_and_clear(sql, name, binds, prepare: false)
+ if without_prepared_statement?(binds)
+ result = exec_no_cache(sql, name, [])
+ elsif !prepare
+ result = exec_no_cache(sql, name, binds)
+ else
+ result = exec_cache(sql, name, binds)
+ end
ret = yield result
result.clear
ret
end
def exec_no_cache(sql, name, binds)
- log(sql, name, binds) { @connection.async_exec(sql, []) }
+ type_casted_binds = binds.map { |attr| type_cast(attr.value_for_database) }
+ log(sql, name, binds) { @connection.async_exec(sql, type_casted_binds) }
end
def exec_cache(sql, name, binds)
stmt_key = prepare_statement(sql)
- type_casted_binds = binds.map { |col, val|
- [col, type_cast(val, col)]
- }
+ type_casted_binds = binds.map { |attr| type_cast(attr.value_for_database) }
- log(sql, name, type_casted_binds, stmt_key) do
- @connection.send_query_prepared(stmt_key, type_casted_binds.map { |_, val| val })
- @connection.block
- @connection.get_last_result
+ log(sql, name, binds, stmt_key) do
+ @connection.exec_prepared(stmt_key, type_casted_binds)
end
rescue ActiveRecord::StatementInvalid => e
pgerror = e.original_exception
@@ -669,14 +664,14 @@ module ActiveRecord
end
# SET statements from :variables config hash
- # http://www.postgresql.org/docs/8.3/static/sql-set.html
+ # http://www.postgresql.org/docs/current/static/sql-set.html
variables = @config[:variables] || {}
variables.map do |k, v|
if v == ':default' || v == :default
# Sets the value to the global or compile default
- execute("SET SESSION #{k.to_s} TO DEFAULT", 'SCHEMA')
+ execute("SET SESSION #{k} TO DEFAULT", 'SCHEMA')
elsif !v.nil?
- execute("SET SESSION #{k.to_s} TO #{quote(v)}", 'SCHEMA')
+ execute("SET SESSION #{k} TO #{quote(v)}", 'SCHEMA')
end
end
end
@@ -694,12 +689,6 @@ module ActiveRecord
exec_query("SELECT currval('#{sequence_name}')", 'SQL')
end
- # Executes a SELECT query and returns the results, performing any data type
- # conversions that are required to be performed here instead of in PostgreSQLColumn.
- def select(sql, name = nil, binds = [])
- exec_query(sql, name, binds)
- end
-
# Returns the list of a table's column names, data types, and default values.
#
# The underlying query is roughly:
@@ -719,9 +708,11 @@ module ActiveRecord
# - format_type includes the column size constraint, e.g. varchar(50)
# - ::regclass is a function that gives the id for a table name
def column_definitions(table_name) # :nodoc:
- exec_query(<<-end_sql, 'SCHEMA').rows
+ query(<<-end_sql, 'SCHEMA')
SELECT a.attname, format_type(a.atttypid, a.atttypmod),
- pg_get_expr(d.adbin, d.adrelid), a.attnotnull, a.atttypid, a.atttypmod
+ pg_get_expr(d.adbin, d.adrelid), a.attnotnull, a.atttypid, a.atttypmod,
+ (SELECT c.collname FROM pg_collation c, pg_type t
+ WHERE c.oid = a.attcollation AND t.oid = a.atttypid AND a.attcollation <> t.typcollation)
FROM pg_attribute a LEFT JOIN pg_attrdef d
ON a.attrelid = d.adrelid AND a.attnum = d.adnum
WHERE a.attrelid = '#{quote_table_name(table_name)}'::regclass
@@ -731,13 +722,93 @@ module ActiveRecord
end
def extract_table_ref_from_insert_sql(sql) # :nodoc:
- sql[/into\s+([^\(]*).*values\s*\(/im]
+ sql[/into\s("[A-Za-z0-9_."\[\]\s]+"|[A-Za-z0-9_."\[\]]+)\s*/im]
$1.strip if $1
end
- def create_table_definition(name, temporary, options, as = nil) # :nodoc:
+ def create_table_definition(name, temporary = false, options = nil, as = nil) # :nodoc:
PostgreSQL::TableDefinition.new native_database_types, name, temporary, options, as
end
+
+ def can_perform_case_insensitive_comparison_for?(column)
+ @case_insensitive_cache ||= {}
+ @case_insensitive_cache[column.sql_type] ||= begin
+ sql = <<-end_sql
+ SELECT exists(
+ SELECT * FROM pg_proc
+ INNER JOIN pg_cast
+ ON casttarget::text::oidvector = proargtypes
+ WHERE proname = 'lower'
+ AND castsource = '#{column.sql_type}'::regtype::oid
+ )
+ end_sql
+ execute_and_clear(sql, "SCHEMA", []) do |result|
+ result.getvalue(0, 0)
+ end
+ end
+ end
+
+ def add_pg_encoders
+ map = PG::TypeMapByClass.new
+ map[Integer] = PG::TextEncoder::Integer.new
+ map[TrueClass] = PG::TextEncoder::Boolean.new
+ map[FalseClass] = PG::TextEncoder::Boolean.new
+ map[Float] = PG::TextEncoder::Float.new
+ @connection.type_map_for_queries = map
+ end
+
+ def add_pg_decoders
+ coders_by_name = {
+ 'int2' => PG::TextDecoder::Integer,
+ 'int4' => PG::TextDecoder::Integer,
+ 'int8' => PG::TextDecoder::Integer,
+ 'oid' => PG::TextDecoder::Integer,
+ 'float4' => PG::TextDecoder::Float,
+ 'float8' => PG::TextDecoder::Float,
+ 'bool' => PG::TextDecoder::Boolean,
+ }
+ known_coder_types = coders_by_name.keys.map { |n| quote(n) }
+ query = <<-SQL % known_coder_types.join(", ")
+ SELECT t.oid, t.typname
+ FROM pg_type as t
+ WHERE t.typname IN (%s)
+ SQL
+ coders = execute_and_clear(query, "SCHEMA", []) do |result|
+ result
+ .map { |row| construct_coder(row, coders_by_name[row['typname']]) }
+ .compact
+ end
+
+ map = PG::TypeMapByOid.new
+ coders.each { |coder| map.add_coder(coder) }
+ @connection.type_map_for_results = map
+ end
+
+ def construct_coder(row, coder_class)
+ return unless coder_class
+ coder_class.new(oid: row['oid'].to_i, name: row['typname'])
+ end
+
+ ActiveRecord::Type.add_modifier({ array: true }, OID::Array, adapter: :postgresql)
+ ActiveRecord::Type.add_modifier({ range: true }, OID::Range, adapter: :postgresql)
+ ActiveRecord::Type.register(:bit, OID::Bit, adapter: :postgresql)
+ 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_time, OID::DateTime, adapter: :postgresql)
+ ActiveRecord::Type.register(:decimal, OID::Decimal, adapter: :postgresql)
+ ActiveRecord::Type.register(:enum, OID::Enum, adapter: :postgresql)
+ ActiveRecord::Type.register(:hstore, OID::Hstore, adapter: :postgresql)
+ ActiveRecord::Type.register(:inet, OID::Inet, adapter: :postgresql)
+ ActiveRecord::Type.register(:json, OID::Json, adapter: :postgresql)
+ ActiveRecord::Type.register(:jsonb, OID::Jsonb, adapter: :postgresql)
+ ActiveRecord::Type.register(:money, OID::Money, adapter: :postgresql)
+ ActiveRecord::Type.register(:point, OID::Point, adapter: :postgresql)
+ ActiveRecord::Type.register(:legacy_point, OID::Point, adapter: :postgresql)
+ ActiveRecord::Type.register(:rails_5_1_point, OID::Rails51Point, adapter: :postgresql)
+ ActiveRecord::Type.register(:uuid, OID::Uuid, adapter: :postgresql)
+ ActiveRecord::Type.register(:vector, OID::Vector, adapter: :postgresql)
+ ActiveRecord::Type.register(:xml, OID::Xml, adapter: :postgresql)
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/schema_cache.rb b/activerecord/lib/active_record/connection_adapters/schema_cache.rb
index 3116bed596..eee142378c 100644
--- a/activerecord/lib/active_record/connection_adapters/schema_cache.rb
+++ b/activerecord/lib/active_record/connection_adapters/schema_cache.rb
@@ -10,32 +10,46 @@ module ActiveRecord
@columns = {}
@columns_hash = {}
@primary_keys = {}
- @tables = {}
+ @data_sources = {}
+ end
+
+ def initialize_dup(other)
+ super
+ @columns = @columns.dup
+ @columns_hash = @columns_hash.dup
+ @primary_keys = @primary_keys.dup
+ @data_sources = @data_sources.dup
end
def primary_keys(table_name)
- @primary_keys[table_name] ||= table_exists?(table_name) ? connection.primary_key(table_name) : nil
+ @primary_keys[table_name] ||= data_source_exists?(table_name) ? connection.primary_key(table_name) : nil
end
# A cached lookup for table existence.
- def table_exists?(name)
- return @tables[name] if @tables.key? name
+ def data_source_exists?(name)
+ prepare_data_sources if @data_sources.empty?
+ return @data_sources[name] if @data_sources.key? name
- @tables[name] = connection.table_exists?(name)
+ @data_sources[name] = connection.data_source_exists?(name)
end
+ alias table_exists? data_source_exists?
+ deprecate :table_exists? => "use #data_source_exists? instead"
+
# Add internal cache for table with +table_name+.
def add(table_name)
- if table_exists?(table_name)
+ if data_source_exists?(table_name)
primary_keys(table_name)
columns(table_name)
columns_hash(table_name)
end
end
- def tables(name)
- @tables[name]
+ def data_sources(name)
+ @data_sources[name]
end
+ alias tables data_sources
+ deprecate :tables => "use #data_sources instead"
# Get the columns for a table
def columns(table_name)
@@ -55,33 +69,39 @@ module ActiveRecord
@columns.clear
@columns_hash.clear
@primary_keys.clear
- @tables.clear
+ @data_sources.clear
@version = nil
end
def size
- [@columns, @columns_hash, @primary_keys, @tables].map { |x|
- x.size
- }.inject :+
+ [@columns, @columns_hash, @primary_keys, @data_sources].map(&:size).inject :+
end
- # Clear out internal caches for table with +table_name+.
- def clear_table_cache!(table_name)
- @columns.delete table_name
- @columns_hash.delete table_name
- @primary_keys.delete table_name
- @tables.delete table_name
+ # Clear out internal caches for the data source +name+.
+ def clear_data_source_cache!(name)
+ @columns.delete name
+ @columns_hash.delete name
+ @primary_keys.delete name
+ @data_sources.delete name
end
+ alias clear_table_cache! clear_data_source_cache!
+ deprecate :clear_table_cache! => "use #clear_data_source_cache! instead"
def marshal_dump
# if we get current version during initialization, it happens stack over flow.
@version = ActiveRecord::Migrator.current_version
- [@version, @columns, @columns_hash, @primary_keys, @tables]
+ [@version, @columns, @columns_hash, @primary_keys, @data_sources]
end
def marshal_load(array)
- @version, @columns, @columns_hash, @primary_keys, @tables = array
+ @version, @columns, @columns_hash, @primary_keys, @data_sources = array
end
+
+ private
+
+ def prepare_data_sources
+ connection.data_sources.each { |source| @data_sources[source] = true }
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/sql_type_metadata.rb b/activerecord/lib/active_record/connection_adapters/sql_type_metadata.rb
new file mode 100644
index 0000000000..ccb7e154ee
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/sql_type_metadata.rb
@@ -0,0 +1,32 @@
+module ActiveRecord
+ # :stopdoc:
+ module ConnectionAdapters
+ class SqlTypeMetadata
+ attr_reader :sql_type, :type, :limit, :precision, :scale
+
+ def initialize(sql_type: nil, type: nil, limit: nil, precision: nil, scale: nil)
+ @sql_type = sql_type
+ @type = type
+ @limit = limit
+ @precision = precision
+ @scale = scale
+ end
+
+ def ==(other)
+ other.is_a?(SqlTypeMetadata) &&
+ attributes_for_hash == other.attributes_for_hash
+ end
+ alias eql? ==
+
+ def hash
+ attributes_for_hash.hash
+ end
+
+ protected
+
+ def attributes_for_hash
+ [self.class, sql_type, type, limit, precision, scale]
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_creation.rb
new file mode 100644
index 0000000000..fe1dcbd710
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_creation.rb
@@ -0,0 +1,15 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module SQLite3
+ class SchemaCreation < AbstractAdapter::SchemaCreation
+ private
+ def add_column_options!(sql, options)
+ if options[:collation]
+ sql << " COLLATE \"#{options[:collation]}\""
+ end
+ super
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
index faf1cdc686..9028c1fcb9 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
@@ -1,6 +1,6 @@
require 'active_record/connection_adapters/abstract_adapter'
require 'active_record/connection_adapters/statement_pool'
-require 'arel/visitors/bind_visitor'
+require 'active_record/connection_adapters/sqlite3/schema_creation'
gem 'sqlite3', '~> 1.3.6'
require 'sqlite3'
@@ -41,25 +41,6 @@ module ActiveRecord
end
module ConnectionAdapters #:nodoc:
- class SQLite3Binary < Type::Binary # :nodoc:
- def cast_value(value)
- if value.encoding != Encoding::ASCII_8BIT
- value = value.force_encoding(Encoding::ASCII_8BIT)
- end
- value
- end
- end
-
- class SQLite3String < Type::String # :nodoc:
- def type_cast_for_database(value)
- if value.is_a?(::String) && value.encoding == Encoding::ASCII_8BIT
- value.encode(Encoding::UTF_8)
- else
- super
- end
- end
- end
-
# The SQLite3 adapter works SQLite 3.6.16 or newer
# with the sqlite3-ruby drivers (available as gem from https://rubygems.org/gems/sqlite3).
#
@@ -67,6 +48,7 @@ module ActiveRecord
#
# * <tt>:database</tt> - Path to the database file.
class SQLite3Adapter < AbstractAdapter
+ ADAPTER_NAME = 'SQLite'.freeze
include Savepoints
NATIVE_DATABASE_TYPES = {
@@ -83,74 +65,36 @@ module ActiveRecord
boolean: { name: "boolean" }
}
- class Version
- include Comparable
-
- def initialize(version_string)
- @version = version_string.split('.').map { |v| v.to_i }
- end
-
- def <=>(version_string)
- @version <=> version_string.split('.').map { |v| v.to_i }
- end
- end
-
class StatementPool < ConnectionAdapters::StatementPool
- def initialize(connection, max)
- super
- @cache = Hash.new { |h,pid| h[pid] = {} }
- end
-
- def each(&block); cache.each(&block); end
- def key?(key); cache.key?(key); end
- def [](key); cache[key]; end
- def length; cache.length; end
-
- def []=(sql, key)
- while @max <= cache.size
- dealloc(cache.shift.last[:stmt])
- end
- cache[sql] = key
- end
-
- def clear
- cache.values.each do |hash|
- dealloc hash[:stmt]
- end
- cache.clear
- end
-
private
- def cache
- @cache[$$]
- end
def dealloc(stmt)
- stmt.close unless stmt.closed?
+ stmt[:stmt].close unless stmt[:stmt].closed?
end
end
+ def schema_creation # :nodoc:
+ SQLite3::SchemaCreation.new self
+ end
+
def initialize(connection, logger, connection_options, config)
super(connection, logger)
@active = nil
- @statements = StatementPool.new(@connection,
- self.class.type_cast_config_to_integer(config.fetch(:statement_limit) { 1000 }))
+ @statements = StatementPool.new(self.class.type_cast_config_to_integer(config.fetch(:statement_limit) { 1000 }))
@config = config
@visitor = Arel::Visitors::SQLite.new self
+ @quoted_column_names = {}
if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true })
@prepared_statements = true
+ @visitor.extend(DetermineIfPreparableVisitor)
else
@prepared_statements = false
end
end
- def adapter_name #:nodoc:
- 'SQLite'
- end
-
def supports_ddl_transactions?
true
end
@@ -182,7 +126,7 @@ module ActiveRecord
true
end
- def supports_add_column?
+ def supports_views?
true
end
@@ -242,6 +186,12 @@ module ActiveRecord
case value
when BigDecimal
value.to_f
+ when String
+ if value.encoding == Encoding::ASCII_8BIT
+ super(value.encode(Encoding::UTF_8))
+ else
+ super
+ end
else
super
end
@@ -256,20 +206,12 @@ module ActiveRecord
end
def quote_column_name(name) #:nodoc:
- %Q("#{name.to_s.gsub('"', '""')}")
- end
-
- # Quote date/time values for use in SQL input. Includes microseconds
- # if the value is a Time responding to usec.
- def quoted_date(value) #:nodoc:
- if value.respond_to?(:usec)
- "#{super}.#{sprintf("%06d", value.usec)}"
- else
- super
- end
+ @quoted_column_names[name] ||= %Q("#{name.to_s.gsub('"', '""')}")
end
+ #--
# DATABASE STATEMENTS ======================================
+ #++
def explain(arel, binds = [])
sql = "EXPLAIN QUERY PLAN #{to_sql(arel, binds)}"
@@ -277,7 +219,7 @@ module ActiveRecord
end
class ExplainPrettyPrinter
- # Pretty prints the result of a EXPLAIN QUERY PLAN in a way that resembles
+ # Pretty prints the result of an EXPLAIN QUERY PLAN in a way that resembles
# the output of the SQLite shell:
#
# 0|0|0|SEARCH TABLE users USING INTEGER PRIMARY KEY (rowid=?) (~1 rows)
@@ -290,17 +232,18 @@ module ActiveRecord
end
end
- def exec_query(sql, name = nil, binds = [])
- type_casted_binds = binds.map { |col, val|
- [col, type_cast(val, col)]
- }
+ def exec_query(sql, name = nil, binds = [], prepare: false)
+ type_casted_binds = binds.map { |attr| type_cast(attr.value_for_database) }
- log(sql, name, type_casted_binds) do
+ log(sql, name, binds) do
# Don't cache statements if they are not prepared
- if without_prepared_statement?(binds)
+ unless prepare
stmt = @connection.prepare(sql)
begin
cols = stmt.columns
+ unless without_prepared_statement?(binds)
+ stmt.bind_params(type_casted_binds)
+ end
records = stmt.to_a
ensure
stmt.close
@@ -313,7 +256,7 @@ module ActiveRecord
stmt = cache[:stmt]
cols = cache[:cols] ||= stmt.columns
stmt.reset!
- stmt.bind_params type_casted_binds.map { |_, val| val }
+ stmt.bind_params(type_casted_binds)
end
ActiveRecord::Result.new(cols, stmt.to_a)
@@ -362,27 +305,38 @@ module ActiveRecord
log('commit transaction',nil) { @connection.commit }
end
- def rollback_db_transaction #:nodoc:
+ def exec_rollback_db_transaction #:nodoc:
log('rollback transaction',nil) { @connection.rollback }
end
# SCHEMA STATEMENTS ========================================
- def tables(name = nil, table_name = nil) #:nodoc:
- sql = <<-SQL
- SELECT name
- FROM sqlite_master
- WHERE type = 'table' AND NOT name = 'sqlite_sequence'
- SQL
- sql << " AND name = #{quote_table_name(table_name)}" if table_name
-
- exec_query(sql, 'SCHEMA').map do |row|
- row['name']
- end
+ def tables(name = nil) # :nodoc:
+ select_values("SELECT name FROM sqlite_master WHERE type IN ('table','view') AND name <> 'sqlite_sequence'", 'SCHEMA')
end
+ alias data_sources tables
def table_exists?(table_name)
- table_name && tables(nil, table_name).any?
+ return false unless table_name.present?
+
+ sql = "SELECT name FROM sqlite_master WHERE type IN ('table','view') AND name <> 'sqlite_sequence'"
+ sql << " AND name = #{quote(table_name)}"
+
+ select_values(sql, 'SCHEMA').any?
+ end
+ alias data_source_exists? table_exists?
+
+ def views # :nodoc:
+ select_values("SELECT name FROM sqlite_master WHERE type = 'view' AND name <> 'sqlite_sequence'", 'SCHEMA')
+ end
+
+ def view_exists?(view_name) # :nodoc:
+ return false unless view_name.present?
+
+ sql = "SELECT name FROM sqlite_master WHERE type = 'view' AND name <> 'sqlite_sequence'"
+ sql << " AND name = #{quote(view_name)}"
+
+ select_values(sql, 'SCHEMA').any?
end
# Returns an array of +Column+ objects for the table specified by +table_name+.
@@ -397,9 +351,10 @@ module ActiveRecord
field["dflt_value"] = $1.gsub('""', '"')
end
+ collation = field['collation']
sql_type = field['type']
- cast_type = lookup_cast_type(sql_type)
- new_column(field['name'], field['dflt_value'], cast_type, sql_type, field['notnull'].to_i == 0)
+ type_metadata = fetch_type_metadata(sql_type)
+ new_column(field['name'], field['dflt_value'], type_metadata, field['notnull'].to_i == 0, nil, collation)
end
end
@@ -428,14 +383,13 @@ module ActiveRecord
end
end
- def primary_key(table_name) #:nodoc:
- column = table_structure(table_name).find { |field|
- field['pk'] == 1
- }
- column && column['name']
+ def primary_keys(table_name) # :nodoc:
+ pks = table_structure(table_name).select { |f| f['pk'] > 0 }
+ pks.sort_by { |f| f['pk'] }.map { |f| f['name'] }
end
- def remove_index!(table_name, index_name) #:nodoc:
+ def remove_index(table_name, options = {}) #:nodoc:
+ index_name = index_name_for_remove(table_name, options)
exec_query "DROP INDEX #{quote_column_name(index_name)}"
end
@@ -450,12 +404,12 @@ module ActiveRecord
# See: http://www.sqlite.org/lang_altertable.html
# SQLite has an additional restriction on the ALTER TABLE statement
- def valid_alter_table_options( type, options)
+ def valid_alter_table_type?(type)
type.to_sym != :primary_key
end
def add_column(table_name, column_name, type, options = {}) #:nodoc:
- if supports_add_column? && valid_alter_table_options( type, options )
+ if valid_alter_table_type?(type)
super(table_name, column_name, type, options)
else
alter_table(table_name) do |definition|
@@ -470,13 +424,15 @@ module ActiveRecord
end
end
- def change_column_default(table_name, column_name, default) #:nodoc:
+ def change_column_default(table_name, column_name, default_or_changes) #:nodoc:
+ default = extract_new_default_value(default_or_changes)
+
alter_table(table_name) do |definition|
definition[column_name].default = default
end
end
- def change_column_null(table_name, column_name, null, default = nil)
+ def change_column_null(table_name, column_name, null, default = nil) #:nodoc:
unless null || default.nil?
exec_query("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL")
end
@@ -495,6 +451,7 @@ module ActiveRecord
self.null = options[:null] if options.include?(:null)
self.precision = options[:precision] if options.include?(:precision)
self.scale = options[:scale] if options.include?(:scale)
+ self.collation = options[:collation] if options.include?(:collation)
end
end
end
@@ -507,20 +464,10 @@ module ActiveRecord
protected
- def initialize_type_map(m)
- super
- m.register_type(/binary/i, SQLite3Binary.new)
- register_class_with_limit m, %r(char)i, SQLite3String
- end
-
- def select(sql, name = nil, binds = []) #:nodoc:
- exec_query(sql, name, binds)
- end
-
def table_structure(table_name)
- structure = exec_query("PRAGMA table_info(#{quote_table_name(table_name)})", 'SCHEMA').to_hash
+ structure = exec_query("PRAGMA table_info(#{quote_table_name(table_name)})", 'SCHEMA')
raise(ActiveRecord::StatementInvalid, "Could not find table '#{table_name}'") if structure.empty?
- structure
+ table_structure_with_collation(table_name, structure)
end
def alter_table(table_name, options = {}) #:nodoc:
@@ -555,13 +502,13 @@ module ActiveRecord
@definition.column(column_name, column.type,
:limit => column.limit, :default => column.default,
:precision => column.precision, :scale => column.scale,
- :null => column.null)
+ :null => column.null, collation: column.collation)
end
yield @definition if block_given?
end
copy_table_indexes(from, to, options[:rename] || {})
copy_table_contents(from, to,
- @definition.columns.map {|column| column.name},
+ @definition.columns.map(&:name),
options[:rename] || {})
end
@@ -574,7 +521,7 @@ module ActiveRecord
name = name[1..-1]
end
- to_column_names = columns(to).map { |c| c.name }
+ to_column_names = columns(to).map(&:name)
columns = index.columns.map {|c| rename[c] || c }.select do |column|
to_column_names.include?(column)
end
@@ -591,25 +538,14 @@ module ActiveRecord
def copy_table_contents(from, to, columns, rename = {}) #:nodoc:
column_mappings = Hash[columns.map {|name| [name, name]}]
rename.each { |a| column_mappings[a.last] = a.first }
- from_columns = columns(from).collect {|col| col.name}
+ from_columns = columns(from).collect(&:name)
columns = columns.find_all{|col| from_columns.include?(column_mappings[col])}
+ from_columns_to_copy = columns.map { |col| column_mappings[col] }
quoted_columns = columns.map { |col| quote_column_name(col) } * ','
+ quoted_from_columns = from_columns_to_copy.map { |col| quote_column_name(col) } * ','
- quoted_to = quote_table_name(to)
-
- raw_column_mappings = Hash[columns(from).map { |c| [c.name, c] }]
-
- exec_query("SELECT * FROM #{quote_table_name(from)}").each do |row|
- sql = "INSERT INTO #{quoted_to} (#{quoted_columns}) VALUES ("
-
- column_values = columns.map do |col|
- quote(row[column_mappings[col]], raw_column_mappings[col])
- end
-
- sql << column_values * ', '
- sql << ')'
- exec_query sql
- end
+ exec_query("INSERT INTO #{quote_table_name(to)} (#{quoted_columns})
+ SELECT #{quoted_from_columns} FROM #{quote_table_name(from)}")
end
def sqlite_version
@@ -628,6 +564,46 @@ module ActiveRecord
super
end
end
+
+ private
+ COLLATE_REGEX = /.*\"(\w+)\".*collate\s+\"(\w+)\".*/i.freeze
+
+ def table_structure_with_collation(table_name, basic_structure)
+ collation_hash = {}
+ sql = "SELECT sql FROM
+ (SELECT * FROM sqlite_master UNION ALL
+ SELECT * FROM sqlite_temp_master)
+ WHERE type='table' and name='#{ table_name }' \;"
+
+ # Result will have following sample string
+ # CREATE TABLE "users" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
+ # "password_digest" varchar COLLATE "NOCASE");
+ result = exec_query(sql, 'SCHEMA').first
+
+ if result
+ # Splitting with left parantheses and picking up last will return all
+ # columns separated with comma(,).
+ columns_string = result["sql"].split('(').last
+
+ columns_string.split(',').each do |column_string|
+ # This regex will match the column name and collation type and will save
+ # the value in $1 and $2 respectively.
+ collation_hash[$1] = $2 if (COLLATE_REGEX =~ column_string)
+ end
+
+ basic_structure.map! do |column|
+ column_name = column['name']
+
+ if collation_hash.has_key? column_name
+ column['collation'] = collation_hash[column_name]
+ end
+
+ column
+ end
+ else
+ basic_structure.to_hash
+ end
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/statement_pool.rb b/activerecord/lib/active_record/connection_adapters/statement_pool.rb
index c6b1bc8b5b..57463dd749 100644
--- a/activerecord/lib/active_record/connection_adapters/statement_pool.rb
+++ b/activerecord/lib/active_record/connection_adapters/statement_pool.rb
@@ -3,36 +3,53 @@ module ActiveRecord
class StatementPool
include Enumerable
- def initialize(connection, max = 1000)
- @connection = connection
- @max = max
+ def initialize(max = 1000)
+ @cache = Hash.new { |h,pid| h[pid] = {} }
+ @max = max
end
- def each
- raise NotImplementedError
+ def each(&block)
+ cache.each(&block)
end
def key?(key)
- raise NotImplementedError
+ cache.key?(key)
end
def [](key)
- raise NotImplementedError
+ cache[key]
end
def length
- raise NotImplementedError
+ cache.length
end
- def []=(sql, key)
- raise NotImplementedError
+ def []=(sql, stmt)
+ while @max <= cache.size
+ dealloc(cache.shift.last)
+ end
+ cache[sql] = stmt
end
def clear
- raise NotImplementedError
+ cache.each_value do |stmt|
+ dealloc stmt
+ end
+ cache.clear
end
def delete(key)
+ dealloc cache[key]
+ cache.delete(key)
+ end
+
+ private
+
+ def cache
+ @cache[Process.pid]
+ end
+
+ def dealloc(stmt)
raise NotImplementedError
end
end
diff --git a/activerecord/lib/active_record/connection_handling.rb b/activerecord/lib/active_record/connection_handling.rb
index 31e7390bf7..aedef54928 100644
--- a/activerecord/lib/active_record/connection_handling.rb
+++ b/activerecord/lib/active_record/connection_handling.rb
@@ -1,11 +1,11 @@
module ActiveRecord
module ConnectionHandling
- RAILS_ENV = -> { Rails.env if defined?(Rails) }
+ RAILS_ENV = -> { (Rails.env if defined?(Rails.env)) || ENV["RAILS_ENV"] || ENV["RACK_ENV"] }
DEFAULT_ENV = -> { RAILS_ENV.call || "default_env" }
# Establishes the connection to the database. Accepts a hash as input where
# the <tt>:adapter</tt> key must be specified with the name of a database adapter (in lower-case)
- # example for regular databases (MySQL, Postgresql, etc):
+ # example for regular databases (MySQL, PostgreSQL, etc):
#
# ActiveRecord::Base.establish_connection(
# adapter: "mysql",
@@ -35,14 +35,14 @@ module ActiveRecord
# "postgres://myuser:mypass@localhost/somedatabase"
# )
#
- # In case <tt>ActiveRecord::Base.configurations</tt> is set (Rails
- # automatically loads the contents of config/database.yml into it),
+ # In case {ActiveRecord::Base.configurations}[rdoc-ref:Core.configurations]
+ # is set (Rails automatically loads the contents of config/database.yml into it),
# a symbol can also be given as argument, representing a key in the
# configuration hash:
#
# ActiveRecord::Base.establish_connection(:production)
#
- # The exceptions AdapterNotSpecified, AdapterNotFound and ArgumentError
+ # The exceptions AdapterNotSpecified, AdapterNotFound and +ArgumentError+
# may be returned on an error.
def establish_connection(spec = nil)
spec ||= DEFAULT_ENV.call.to_sym
@@ -88,7 +88,7 @@ module ActiveRecord
end
def connection_id
- ActiveRecord::RuntimeRegistry.connection_id
+ ActiveRecord::RuntimeRegistry.connection_id ||= Thread.current.object_id
end
def connection_id=(connection_id)
diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb
index d22806fbdf..142b6e8599 100644
--- a/activerecord/lib/active_record/core.rb
+++ b/activerecord/lib/active_record/core.rb
@@ -1,6 +1,7 @@
+require 'thread'
require 'active_support/core_ext/hash/indifferent_access'
require 'active_support/core_ext/object/duplicable'
-require 'thread'
+require 'active_support/core_ext/string/filters'
module ActiveRecord
module Core
@@ -84,16 +85,30 @@ module ActiveRecord
mattr_accessor :dump_schema_after_migration, instance_writer: false
self.dump_schema_after_migration = true
- # :nodoc:
+ ##
+ # :singleton-method:
+ # Specifies which database schemas to dump when calling db:structure:dump.
+ # If the value is :schema_search_path (the default), any schemas listed in
+ # schema_search_path are dumped. Use :all to dump all schemas regardless
+ # of schema_search_path, or a string of comma separated schemas for a
+ # custom list.
+ mattr_accessor :dump_schemas, instance_writer: false
+ self.dump_schemas = :schema_search_path
+
+ ##
+ # :singleton-method:
+ # Specify a threshold for the size of query result sets. If the number of
+ # records in the set exceeds the threshold, a warning is logged. This can
+ # be used to identify queries which load thousands of records and
+ # potentially cause memory bloat.
+ mattr_accessor :warn_on_records_fetched_greater_than, instance_writer: false
+ self.warn_on_records_fetched_greater_than = nil
+
mattr_accessor :maintain_test_schema, instance_accessor: false
- def self.disable_implicit_join_references=(value)
- ActiveSupport::Deprecation.warn("Implicit join references were removed with Rails 4.1." \
- "Make sure to remove this configuration because it does nothing.")
- end
+ mattr_accessor :belongs_to_required_by_default, instance_accessor: false
class_attribute :default_connection_handler, instance_writer: false
- class_attribute :find_by_statement_cache
def self.connection_handler
ActiveRecord::RuntimeRegistry.connection_handler || default_connection_handler
@@ -112,74 +127,84 @@ module ActiveRecord
super
end
- def initialize_find_by_cache
- self.find_by_statement_cache = {}.extend(Mutex_m)
+ def initialize_find_by_cache # :nodoc:
+ @find_by_statement_cache = {}.extend(Mutex_m)
end
- def inherited(child_class)
+ def inherited(child_class) # :nodoc:
+ # initialize cache at class definition for thread safety
child_class.initialize_find_by_cache
super
end
- def find(*ids)
+ def find(*ids) # :nodoc:
# We don't have cache keys for this stuff yet
return super unless ids.length == 1
return super if block_given? ||
primary_key.nil? ||
- default_scopes.any? ||
+ scope_attributes? ||
columns_hash.include?(inheritance_column) ||
ids.first.kind_of?(Array)
id = ids.first
if ActiveRecord::Base === id
id = id.id
- ActiveSupport::Deprecation.warn "You are passing an instance of ActiveRecord::Base to `find`." \
- "Please pass the id of the object by calling `.id`"
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ You are passing an instance of ActiveRecord::Base to `find`.
+ Please pass the id of the object by calling `.id`
+ MSG
end
+
key = primary_key
- s = find_by_statement_cache[key] || find_by_statement_cache.synchronize {
- find_by_statement_cache[key] ||= StatementCache.create(connection) { |params|
- where(key => params.bind).limit(1)
- }
+ statement = cached_find_by_statement(key) { |params|
+ where(key => params.bind).limit(1)
}
- record = s.execute([id], self, connection).first
+ record = statement.execute([id], self, connection).first
unless record
- raise RecordNotFound, "Couldn't find #{name} with '#{primary_key}'=#{id}"
+ raise RecordNotFound.new("Couldn't find #{name} with '#{primary_key}'=#{id}",
+ name, primary_key, id)
end
record
+ rescue RangeError
+ raise RecordNotFound.new("Couldn't find #{name} with an out of range value for '#{primary_key}'",
+ name, primary_key)
end
- def find_by(*args)
- return super if current_scope || args.length > 1 || reflect_on_all_aggregations.any?
+ def find_by(*args) # :nodoc:
+ return super if scope_attributes? || !(Hash === args.first) || reflect_on_all_aggregations.any?
hash = args.first
return super if hash.values.any? { |v|
- v.nil? || Array === v || Hash === v
+ v.nil? || Array === v || Hash === v || Relation === v
}
- key = hash.keys
+ # We can't cache Post.find_by(author: david) ...yet
+ return super unless hash.keys.all? { |k| columns_hash.has_key?(k.to_s) }
+
+ keys = hash.keys
- klass = self
- s = find_by_statement_cache[key] || find_by_statement_cache.synchronize {
- find_by_statement_cache[key] ||= StatementCache.create(connection) { |params|
- wheres = key.each_with_object({}) { |param,o|
- o[param] = params.bind
- }
- klass.where(wheres).limit(1)
+ statement = cached_find_by_statement(keys) { |params|
+ wheres = keys.each_with_object({}) { |param, o|
+ o[param] = params.bind
}
+ where(wheres).limit(1)
}
begin
- s.execute(hash.values, self, connection).first
+ statement.execute(hash.values, self, connection).first
rescue TypeError => e
raise ActiveRecord::StatementInvalid.new(e.message, e)
+ rescue RangeError
+ nil
end
end
- def initialize_generated_modules
- super
+ def find_by!(*args) # :nodoc:
+ find_by(*args) or raise RecordNotFound.new("Couldn't find #{name}", name)
+ end
+ def initialize_generated_modules # :nodoc:
generated_association_methods
end
@@ -200,7 +225,7 @@ module ActiveRecord
elsif !connected?
"#{super} (call '#{super}.connection' to establish a connection)"
elsif table_exists?
- attr_list = columns.map { |c| "#{c.name}: #{c.type}" } * ', '
+ attr_list = attribute_types.map { |name, type| "#{name}: #{type.type}" } * ', '
"#{super}(#{attr_list})"
else
"#{super}(Table doesn't exist)"
@@ -218,7 +243,7 @@ module ActiveRecord
# scope :published_and_commented, -> { published.and(self.arel_table[:comments_count].gt(0)) }
# end
def arel_table # :nodoc:
- @arel_table ||= Arel::Table.new(table_name, arel_engine)
+ @arel_table ||= Arel::Table.new(table_name, type_caster: type_caster)
end
# Returns the Arel engine.
@@ -231,10 +256,24 @@ module ActiveRecord
end
end
+ def predicate_builder # :nodoc:
+ @predicate_builder ||= PredicateBuilder.new(table_metadata)
+ end
+
+ def type_caster # :nodoc:
+ TypeCaster::Map.new(self)
+ end
+
private
- def relation #:nodoc:
- relation = Relation.create(self, arel_table)
+ def cached_find_by_statement(key, &block) # :nodoc:
+ @find_by_statement_cache[key] || @find_by_statement_cache.synchronize {
+ @find_by_statement_cache[key] ||= StatementCache.create(connection, &block)
+ }
+ end
+
+ def relation # :nodoc:
+ relation = Relation.create(self, arel_table, predicate_builder)
if finder_needs_type_condition?
relation.where(type_condition).create_with(inheritance_column.to_sym => sti_name)
@@ -242,6 +281,10 @@ module ActiveRecord
relation
end
end
+
+ def table_metadata # :nodoc:
+ TableMetadata.new(self, arel_table)
+ end
end
# New objects can be instantiated as either empty (pass no construction parameter) or pre-set with
@@ -252,32 +295,35 @@ module ActiveRecord
# ==== Example:
# # Instantiates a single new object
# User.new(first_name: 'Jamie')
- def initialize(attributes = nil, options = {})
- @attributes = self.class.default_attributes.dup
+ def initialize(attributes = nil)
+ @attributes = self.class._default_attributes.deep_dup
+ self.class.define_attribute_methods
init_internals
initialize_internals_callback
- self.class.define_attribute_methods
- # +options+ argument is only needed to make protected_attributes gem easier to hook.
- # Remove it when we drop support to this gem.
- init_attributes(attributes, options) if attributes
+ assign_attributes(attributes) if attributes
yield self if block_given?
- run_callbacks :initialize unless _initialize_callbacks.empty?
+ _run_initialize_callbacks
end
- # Initialize an empty model object from +coder+. +coder+ must contain
- # the attributes necessary for initializing an empty model object. For
- # example:
+ # Initialize an empty model object from +coder+. +coder+ should be
+ # the result of previously encoding an Active Record model, using
+ # #encode_with.
#
# class Post < ActiveRecord::Base
# end
#
+ # old_post = Post.new(title: "hello world")
+ # coder = {}
+ # old_post.encode_with(coder)
+ #
# post = Post.allocate
- # post.init_with('attributes' => { 'title' => 'hello world' })
+ # post.init_with(coder)
# post.title # => 'hello world'
def init_with(coder)
+ coder = LegacyYamlAdapter.convert(self.class, coder)
@attributes = coder['attributes']
init_internals
@@ -286,8 +332,8 @@ module ActiveRecord
self.class.define_attribute_methods
- run_callbacks :find
- run_callbacks :initialize
+ _run_find_callbacks
+ _run_initialize_callbacks
self
end
@@ -320,13 +366,10 @@ module ActiveRecord
##
def initialize_dup(other) # :nodoc:
- @attributes = @attributes.dup
+ @attributes = @attributes.deep_dup
@attributes.reset(self.class.primary_key)
- run_callbacks(:initialize) unless _initialize_callbacks.empty?
-
- @aggregation_cache = {}
- @association_cache = {}
+ _run_initialize_callbacks
@new_record = true
@destroyed = false
@@ -336,7 +379,7 @@ module ActiveRecord
# Populate +coder+ with attributes about this record that should be
# serialized. The structure of +coder+ defined in this method is
- # guaranteed to match the structure of +coder+ passed to the +init_with+
+ # guaranteed to match the structure of +coder+ passed to the #init_with
# method.
#
# Example:
@@ -351,6 +394,7 @@ module ActiveRecord
coder['raw_attributes'] = attributes_before_type_cast
coder['attributes'] = @attributes
coder['new_record'] = new_record?
+ coder['active_record_yaml_version'] = 1
end
# Returns true if +comparison_object+ is the same exact object, or +comparison_object+
@@ -433,9 +477,10 @@ module ActiveRecord
"#<#{self.class} #{inspection}>"
end
- # Takes a PP and prettily prints this record to it, allowing you to get a nice result from `pp record`
+ # Takes a PP and prettily prints this record to it, allowing you to get a nice result from <tt>pp record</tt>
# when pp is required.
def pretty_print(pp)
+ return super if custom_inspect_method_defined?
pp.object_address_group(self) do
if defined?(@attributes) && @attributes
column_names = self.class.column_names.select { |name| has_attribute?(name) || new_record? }
@@ -461,51 +506,8 @@ module ActiveRecord
Hash[methods.map! { |method| [method, public_send(method)] }].with_indifferent_access
end
- def set_transaction_state(state) # :nodoc:
- @transaction_state = state
- end
-
- def has_transactional_callbacks? # :nodoc:
- !_rollback_callbacks.empty? || !_commit_callbacks.empty? || !_create_callbacks.empty?
- end
-
private
- # Updates the attributes on this particular ActiveRecord object so that
- # if it is associated with a transaction, then the state of the AR object
- # will be updated to reflect the current state of the transaction
- #
- # The @transaction_state variable stores the states of the associated
- # transaction. This relies on the fact that a transaction can only be in
- # one rollback or commit (otherwise a list of states would be required)
- # Each AR object inside of a transaction carries that transaction's
- # TransactionState.
- #
- # This method checks to see if the ActiveRecord object's state reflects
- # the TransactionState, and rolls back or commits the ActiveRecord object
- # as appropriate.
- #
- # Since ActiveRecord objects can be inside multiple transactions, this
- # method recursively goes through the parent of the TransactionState and
- # checks if the ActiveRecord object reflects the state of the object.
- def sync_with_transaction_state
- update_attributes_from_transaction_state(@transaction_state, 0)
- end
-
- def update_attributes_from_transaction_state(transaction_state, depth)
- if transaction_state && transaction_state.finalized? && !has_transactional_callbacks?
- unless @reflects_state[depth]
- restore_transaction_record_state if transaction_state.rolledback?
- clear_transaction_record_state
- @reflects_state[depth] = true
- end
-
- if transaction_state.parent && !@reflects_state[depth+1]
- update_attributes_from_transaction_state(transaction_state.parent, depth+1)
- end
- end
- end
-
# Under Ruby 1.9, Array#flatten will call #to_ary (recursively) on each of the elements
# of the array, and then rescues from the possible NoMethodError. If those elements are
# ActiveRecord::Base's, then this triggers the various method_missing's that we have,
@@ -519,10 +521,6 @@ module ActiveRecord
end
def init_internals
- @attributes.ensure_initialized(self.class.primary_key)
-
- @aggregation_cache = {}
- @association_cache = {}
@readonly = false
@destroyed = false
@marked_for_destruction = false
@@ -531,22 +529,19 @@ module ActiveRecord
@txn = nil
@_start_transaction_state = {}
@transaction_state = nil
- @reflects_state = [false]
end
def initialize_internals_callback
end
- # This method is needed to make protected_attributes gem easier to hook.
- # Remove it when we drop support to this gem.
- def init_attributes(attributes, options)
- assign_attributes(attributes)
- end
-
def thaw
if frozen?
@attributes = @attributes.dup
end
end
+
+ def custom_inspect_method_defined?
+ self.class.instance_method(:inspect).owner != ActiveRecord::Base.instance_method(:inspect).owner
+ end
end
end
diff --git a/activerecord/lib/active_record/counter_cache.rb b/activerecord/lib/active_record/counter_cache.rb
index a33c7c64a7..9e7d391c70 100644
--- a/activerecord/lib/active_record/counter_cache.rb
+++ b/activerecord/lib/active_record/counter_cache.rb
@@ -20,7 +20,7 @@ module ActiveRecord
def reset_counters(id, *counters)
object = find(id)
counters.each do |counter_association|
- has_many_association = _reflect_on_association(counter_association.to_sym)
+ has_many_association = _reflect_on_association(counter_association)
unless has_many_association
has_many = reflect_on_all_associations(:has_many)
has_many_association = has_many.find { |association| association.counter_cache_column && association.counter_cache_column.to_sym == counter_association.to_sym }
@@ -34,26 +34,25 @@ module ActiveRecord
foreign_key = has_many_association.foreign_key.to_s
child_class = has_many_association.klass
- reflection = child_class._reflections.values.find { |e| :belongs_to == e.macro && e.foreign_key.to_s == foreign_key && e.options[:counter_cache].present? }
+ reflection = child_class._reflections.values.find { |e| e.belongs_to? && e.foreign_key.to_s == foreign_key && e.options[:counter_cache].present? }
counter_name = reflection.counter_cache_column
- stmt = unscoped.where(arel_table[primary_key].eq(object.id)).arel.compile_update({
- arel_table[counter_name] => object.send(counter_association).count(:all)
- }, primary_key)
- connection.update stmt
+ unscoped.where(primary_key => object.id).update_all(
+ counter_name => object.send(counter_association).count(:all)
+ )
end
return true
end
# A generic "counter updater" implementation, intended primarily to be
- # used by increment_counter and decrement_counter, but which may also
+ # used by #increment_counter and #decrement_counter, but which may also
# be useful on its own. It simply does a direct SQL update for the record
# with the given ID, altering the given hash of counters by the amount
# given by the corresponding value:
#
# ==== Parameters
#
- # * +id+ - The id of the object you wish to update a counter on or an Array of ids.
+ # * +id+ - The id of the object you wish to update a counter on or an array of ids.
# * +counters+ - A Hash containing the names of the fields
# to update as keys and the amount to update the field by as values.
#
@@ -87,14 +86,14 @@ module ActiveRecord
# Increment a numeric field by one, via a direct SQL update.
#
# This method is used primarily for maintaining counter_cache columns that are
- # used to store aggregate values. For example, a DiscussionBoard may cache
+ # used to store aggregate values. For example, a +DiscussionBoard+ may cache
# posts_count and comments_count to avoid running an SQL query to calculate the
# number of posts and comments there are, each time it is displayed.
#
# ==== Parameters
#
# * +counter_name+ - The name of the field that should be incremented.
- # * +id+ - The id of the object that should be incremented or an Array of ids.
+ # * +id+ - The id of the object that should be incremented or an array of ids.
#
# ==== Examples
#
@@ -106,13 +105,13 @@ module ActiveRecord
# Decrement a numeric field by one, via a direct SQL update.
#
- # This works the same as increment_counter but reduces the column value by
+ # This works the same as #increment_counter but reduces the column value by
# 1 instead of increasing it.
#
# ==== Parameters
#
# * +counter_name+ - The name of the field that should be decremented.
- # * +id+ - The id of the object that should be decremented or an Array of ids.
+ # * +id+ - The id of the object that should be decremented or an array of ids.
#
# ==== Examples
#
@@ -123,16 +122,6 @@ module ActiveRecord
end
end
- protected
-
- def actually_destroyed?
- @_actually_destroyed
- end
-
- def clear_destroy_state
- @_actually_destroyed = nil
- end
-
private
def _create_record(*)
@@ -167,7 +156,7 @@ module ActiveRecord
def each_counter_cached_associations
_reflections.each do |name, reflection|
- yield association(name) if reflection.belongs_to? && reflection.counter_cache_column
+ yield association(name.to_sym) if reflection.belongs_to? && reflection.counter_cache_column
end
end
diff --git a/activerecord/lib/active_record/dynamic_matchers.rb b/activerecord/lib/active_record/dynamic_matchers.rb
index e94b74063e..b6dd6814db 100644
--- a/activerecord/lib/active_record/dynamic_matchers.rb
+++ b/activerecord/lib/active_record/dynamic_matchers.rb
@@ -1,10 +1,5 @@
module ActiveRecord
module DynamicMatchers #:nodoc:
- # This code in this file seems to have a lot of indirection, but the indirection
- # is there to provide extension points for the activerecord-deprecated_finders
- # gem. When we stop supporting activerecord-deprecated_finders (from Rails 5),
- # then we can remove the indirection.
-
def respond_to?(name, include_private = false)
if self == Base
super
@@ -72,26 +67,14 @@ module ActiveRecord
CODE
end
- def body
- raise NotImplementedError
- end
- end
+ private
- module Finder
- # Extended in activerecord-deprecated_finders
def body
- result
- end
-
- # Extended in activerecord-deprecated_finders
- def result
"#{finder}(#{attributes_hash})"
end
# The parameters in the signature may have reserved Ruby words, in order
# to prevent errors, we start each param name with `_`.
- #
- # Extended in activerecord-deprecated_finders
def signature
attribute_names.map { |name| "_#{name}" }.join(', ')
end
@@ -109,7 +92,6 @@ module ActiveRecord
class FindBy < Method
Method.matchers << self
- include Finder
def self.prefix
"find_by"
@@ -122,7 +104,6 @@ module ActiveRecord
class FindByBang < Method
Method.matchers << self
- include Finder
def self.prefix
"find_by"
diff --git a/activerecord/lib/active_record/enum.rb b/activerecord/lib/active_record/enum.rb
index f0ee433d0b..8fba6fcc35 100644
--- a/activerecord/lib/active_record/enum.rb
+++ b/activerecord/lib/active_record/enum.rb
@@ -18,10 +18,9 @@ module ActiveRecord
# conversation.archived? # => true
# conversation.status # => "archived"
#
- # # conversation.update! status: 1
+ # # conversation.status = 1
# conversation.status = "archived"
#
- # # conversation.update! status: nil
# conversation.status = nil
# conversation.status.nil? # => true
# conversation.status # => nil
@@ -32,6 +31,12 @@ module ActiveRecord
# Conversation.active
# Conversation.archived
#
+ # Of course, you can also query them directly if the scopes don't fit your
+ # needs:
+ #
+ # Conversation.where(status: [:active, :archived])
+ # Conversation.where.not(status: :active)
+ #
# You can set the default value from the database declaration, like:
#
# create_table :conversations do |t|
@@ -41,13 +46,13 @@ module ActiveRecord
# Good practice is to let the first declared status be the default.
#
# Finally, it's also possible to explicitly map the relation between attribute and
- # database integer with a +Hash+:
+ # database integer with a hash:
#
# class Conversation < ActiveRecord::Base
# enum status: { active: 0, archived: 1 }
# end
#
- # Note that when an +Array+ is used, the implicit mapping from the values to database
+ # Note that when an array is used, the implicit mapping from the values to database
# integers is derived from the order the values appear in the array. In the example,
# <tt>:active</tt> is mapped to +0+ as it's the first element, and <tt>:archived</tt>
# is mapped to +1+. In general, the +i+-th element is mapped to <tt>i-1</tt> in the
@@ -55,19 +60,39 @@ module ActiveRecord
#
# Therefore, once a value is added to the enum array, its position in the array must
# be maintained, and new values should only be added to the end of the array. To
- # remove unused values, the explicit +Hash+ syntax should be used.
+ # remove unused values, the explicit hash syntax should be used.
#
# In rare circumstances you might need to access the mapping directly.
# The mappings are exposed through a class method with the pluralized attribute
- # name:
+ # name, which return the mapping in a +HashWithIndifferentAccess+:
#
- # Conversation.statuses # => { "active" => 0, "archived" => 1 }
+ # Conversation.statuses[:active] # => 0
+ # Conversation.statuses["archived"] # => 1
#
- # Use that class method when you need to know the ordinal value of an enum:
+ # Use that class method when you need to know the ordinal value of an enum.
+ # For example, you can use that when manually building SQL strings:
#
# Conversation.where("status <> ?", Conversation.statuses[:archived])
#
- # Where conditions on an enum attribute must use the ordinal value of an enum.
+ # You can use the +:_prefix+ or +:_suffix+ options when you need to define
+ # multiple enums with same values. If the passed value is +true+, the methods
+ # are prefixed/suffixed with the name of the enum. It is also possible to
+ # supply a custom value:
+ #
+ # class Conversation < ActiveRecord::Base
+ # enum status: [:active, :archived], _suffix: true
+ # enum comments_status: [:active, :inactive], _prefix: :comments
+ # end
+ #
+ # With the above example, the bang and predicate methods along with the
+ # associated scopes are now prefixed and/or suffixed accordingly:
+ #
+ # conversation.active_status!
+ # conversation.archived_status? # => false
+ #
+ # conversation.comments_inactive!
+ # conversation.comments_active? # => false
+
module Enum
def self.extended(base) # :nodoc:
base.class_attribute(:defined_enums)
@@ -79,8 +104,48 @@ module ActiveRecord
super
end
+ class EnumType < Type::Value
+ def initialize(name, mapping)
+ @name = name
+ @mapping = mapping
+ end
+
+ def cast(value)
+ return if value.blank?
+
+ if mapping.has_key?(value)
+ value.to_s
+ elsif mapping.has_value?(value)
+ mapping.key(value)
+ else
+ assert_valid_value(value)
+ end
+ end
+
+ def deserialize(value)
+ return if value.nil?
+ mapping.key(value.to_i)
+ end
+
+ def serialize(value)
+ mapping.fetch(value, value)
+ end
+
+ def assert_valid_value(value)
+ unless value.blank? || mapping.has_key?(value) || mapping.has_value?(value)
+ raise ArgumentError, "'#{value}' is not a valid #{name}"
+ end
+ end
+
+ protected
+
+ attr_reader :name, :mapping
+ end
+
def enum(definitions)
klass = self
+ enum_prefix = definitions.delete(:_prefix)
+ enum_suffix = definitions.delete(:_suffix)
definitions.each do |name, values|
# statuses = { }
enum_values = ActiveSupport::HashWithIndifferentAccess.new
@@ -90,45 +155,39 @@ module ActiveRecord
detect_enum_conflict!(name, name.to_s.pluralize, true)
klass.singleton_class.send(:define_method, name.to_s.pluralize) { enum_values }
- _enum_methods_module.module_eval do
- # def status=(value) self[:status] = statuses[value] end
- klass.send(:detect_enum_conflict!, name, "#{name}=")
- define_method("#{name}=") { |value|
- if enum_values.has_key?(value) || value.blank?
- self[name] = enum_values[value]
- elsif enum_values.has_value?(value)
- # Assigning a value directly is not a end-user feature, hence it's not documented.
- # This is used internally to make building objects from the generated scopes work
- # as expected, i.e. +Conversation.archived.build.archived?+ should be true.
- self[name] = value
- else
- raise ArgumentError, "'#{value}' is not a valid #{name}"
- end
- }
-
- # def status() statuses.key self[:status] end
- klass.send(:detect_enum_conflict!, name, name)
- define_method(name) { enum_values.key self[name] }
+ detect_enum_conflict!(name, name)
+ detect_enum_conflict!(name, "#{name}=")
- # def status_before_type_cast() statuses.key self[:status] end
- klass.send(:detect_enum_conflict!, name, "#{name}_before_type_cast")
- define_method("#{name}_before_type_cast") { enum_values.key self[name] }
+ attribute name, EnumType.new(name, enum_values)
+ _enum_methods_module.module_eval do
pairs = values.respond_to?(:each_pair) ? values.each_pair : values.each_with_index
pairs.each do |value, i|
+ if enum_prefix == true
+ prefix = "#{name}_"
+ elsif enum_prefix
+ prefix = "#{enum_prefix}_"
+ end
+ if enum_suffix == true
+ suffix = "_#{name}"
+ elsif enum_suffix
+ suffix = "_#{enum_suffix}"
+ end
+
+ value_method_name = "#{prefix}#{value}#{suffix}"
enum_values[value] = i
# def active?() status == 0 end
- klass.send(:detect_enum_conflict!, name, "#{value}?")
- define_method("#{value}?") { self[name] == i }
+ klass.send(:detect_enum_conflict!, name, "#{value_method_name}?")
+ define_method("#{value_method_name}?") { self[name] == value.to_s }
# def active!() update! status: :active end
- klass.send(:detect_enum_conflict!, name, "#{value}!")
- define_method("#{value}!") { update! name => value }
+ klass.send(:detect_enum_conflict!, name, "#{value_method_name}!")
+ define_method("#{value_method_name}!") { update! name => value }
# scope :active, -> { where status: 0 }
- klass.send(:detect_enum_conflict!, name, value, true)
- klass.scope value, -> { klass.where name => i }
+ klass.send(:detect_enum_conflict!, name, value_method_name, true)
+ klass.scope value_method_name, -> { klass.where name => value }
end
end
defined_enums[name.to_s] = enum_values
@@ -138,25 +197,7 @@ module ActiveRecord
private
def _enum_methods_module
@_enum_methods_module ||= begin
- mod = Module.new do
- private
- def save_changed_attribute(attr_name, old)
- if (mapping = self.class.defined_enums[attr_name.to_s])
- value = read_attribute(attr_name)
- if attribute_changed?(attr_name)
- if mapping[old] == value
- changed_attributes.delete(attr_name)
- end
- else
- if old != value
- changed_attributes[attr_name] = mapping.key old
- end
- end
- else
- super
- end
- end
- end
+ mod = Module.new
include mod
mod
end
@@ -169,30 +210,22 @@ module ActiveRecord
def detect_enum_conflict!(enum_name, method_name, klass_method = false)
if klass_method && dangerous_class_method?(method_name)
- raise ArgumentError, ENUM_CONFLICT_MESSAGE % {
- enum: enum_name,
- klass: self.name,
- type: 'class',
- method: method_name,
- source: 'Active Record'
- }
+ raise_conflict_error(enum_name, method_name, type: 'class')
elsif !klass_method && dangerous_attribute_method?(method_name)
- raise ArgumentError, ENUM_CONFLICT_MESSAGE % {
- enum: enum_name,
- klass: self.name,
- type: 'instance',
- method: method_name,
- source: 'Active Record'
- }
+ raise_conflict_error(enum_name, method_name)
elsif !klass_method && method_defined_within?(method_name, _enum_methods_module, Module)
- raise ArgumentError, ENUM_CONFLICT_MESSAGE % {
- enum: enum_name,
- klass: self.name,
- type: 'instance',
- method: method_name,
- source: 'another enum'
- }
+ raise_conflict_error(enum_name, method_name, source: 'another enum')
end
end
+
+ def raise_conflict_error(enum_name, method_name, type: 'instance', source: 'Active Record')
+ raise ArgumentError, ENUM_CONFLICT_MESSAGE % {
+ enum: enum_name,
+ klass: self.name,
+ type: type,
+ method: method_name,
+ source: source
+ }
+ end
end
end
diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb
index 52c70977ef..533c86a6a9 100644
--- a/activerecord/lib/active_record/errors.rb
+++ b/activerecord/lib/active_record/errors.rb
@@ -7,8 +7,10 @@ module ActiveRecord
end
# Raised when the single-table inheritance mechanism fails to locate the subclass
- # (for example due to improper usage of column that +inheritance_column+ points to).
- class SubclassNotFound < ActiveRecordError #:nodoc:
+ # (for example due to improper usage of column that
+ # {ActiveRecord::Base.inheritance_column}[rdoc-ref:ModelSchema::ClassMethods#inheritance_column]
+ # points to).
+ class SubclassNotFound < ActiveRecordError
end
# Raised when an object assigned to an association has an incorrect type.
@@ -40,22 +42,54 @@ module ActiveRecord
class AdapterNotFound < ActiveRecordError
end
- # Raised when connection to the database could not been established (for
- # example when +connection=+ is given a nil object).
+ # Raised when connection to the database could not been established (for example when
+ # {ActiveRecord::Base.connection=}[rdoc-ref:ConnectionHandling#connection]
+ # is given a nil object).
class ConnectionNotEstablished < ActiveRecordError
end
- # Raised when Active Record cannot find record by given id or set of ids.
+ # Raised when Active Record cannot find a record by given id or set of ids.
class RecordNotFound < ActiveRecordError
+ attr_reader :model, :primary_key, :id
+
+ def initialize(message = nil, model = nil, primary_key = nil, id = nil)
+ @primary_key = primary_key
+ @model = model
+ @id = id
+
+ super(message)
+ end
end
- # Raised by ActiveRecord::Base.save! and ActiveRecord::Base.create! methods when record cannot be
- # saved because record is invalid.
+ # Raised by {ActiveRecord::Base#save!}[rdoc-ref:Persistence#save!] and
+ # {ActiveRecord::Base.create!}[rdoc-ref:Persistence::ClassMethods#create!]
+ # methods when a record is invalid and can not be saved.
class RecordNotSaved < ActiveRecordError
+ attr_reader :record
+
+ def initialize(message = nil, record = nil)
+ @record = record
+ super(message)
+ end
end
- # Raised by ActiveRecord::Base.destroy! when a call to destroy would return false.
+ # Raised by {ActiveRecord::Base#destroy!}[rdoc-ref:Persistence#destroy!]
+ # when a call to {#destroy}[rdoc-ref:Persistence#destroy!]
+ # would return false.
+ #
+ # begin
+ # complex_operation_that_internally_calls_destroy!
+ # rescue ActiveRecord::RecordNotDestroyed => invalid
+ # puts invalid.record.errors
+ # end
+ #
class RecordNotDestroyed < ActiveRecordError
+ attr_reader :record
+
+ def initialize(message = nil, record = nil)
+ @record = record
+ super(message)
+ end
end
# Superclass for all database execution errors.
@@ -64,14 +98,14 @@ module ActiveRecord
class StatementInvalid < ActiveRecordError
attr_reader :original_exception
- def initialize(message, original_exception = nil)
- super(message)
+ def initialize(message = nil, original_exception = nil)
@original_exception = original_exception
+ super(message)
end
end
# Defunct wrapper class kept for compatibility.
- # +StatementInvalid+ wraps the original exception now.
+ # StatementInvalid wraps the original exception now.
class WrappedDatabaseException < StatementInvalid
end
@@ -84,8 +118,8 @@ module ActiveRecord
end
# Raised when number of bind variables in statement given to +:condition+ key
- # (for example, when using +find+ method) does not match number of expected
- # values supplied.
+ # (for example, when using {ActiveRecord::Base.find}[rdoc-ref:FinderMethods#find] method)
+ # does not match number of expected values supplied.
#
# For example, when there are two placeholders with only one value supplied:
#
@@ -106,16 +140,22 @@ module ActiveRecord
class StaleObjectError < ActiveRecordError
attr_reader :record, :attempted_action
- def initialize(record, attempted_action)
- super("Attempted to #{attempted_action} a stale object: #{record.class.name}")
- @record = record
- @attempted_action = attempted_action
+ def initialize(record = nil, attempted_action = nil)
+ if record && attempted_action
+ @record = record
+ @attempted_action = attempted_action
+ super("Attempted to #{attempted_action} a stale object: #{record.class.name}.")
+ else
+ super("Stale object error.")
+ end
end
end
# Raised when association is being configured improperly or user tries to use
- # offset and limit together with +has_many+ or +has_and_belongs_to_many+
+ # offset and limit together with
+ # {ActiveRecord::Base.has_many}[rdoc-ref:Associations::ClassMethods#has_many] or
+ # {ActiveRecord::Base.has_and_belongs_to_many}[rdoc-ref:Associations::ClassMethods#has_and_belongs_to_many]
# associations.
class ConfigurationError < ActiveRecordError
end
@@ -124,9 +164,10 @@ module ActiveRecord
class ReadOnlyRecord < ActiveRecordError
end
- # ActiveRecord::Transactions::ClassMethods.transaction uses this exception
- # to distinguish a deliberate rollback from other exceptional situations.
- # Normally, raising an exception will cause the +transaction+ method to rollback
+ # {ActiveRecord::Base.transaction}[rdoc-ref:Transactions::ClassMethods#transaction]
+ # uses this exception to distinguish a deliberate rollback from other exceptional situations.
+ # Normally, raising an exception will cause the
+ # {.transaction}[rdoc-ref:Transactions::ClassMethods#transaction] method to rollback
# the database transaction *and* pass on the exception. But if you raise an
# ActiveRecord::Rollback exception, then the database transaction will be rolled back,
# without passing on the exception.
@@ -160,36 +201,29 @@ module ActiveRecord
end
# Raised when unknown attributes are supplied via mass assignment.
- class UnknownAttributeError < NoMethodError
-
- attr_reader :record, :attribute
-
- def initialize(record, attribute)
- @record = record
- @attribute = attribute.to_s
- super("unknown attribute: #{attribute}")
- end
-
- end
+ UnknownAttributeError = ActiveModel::UnknownAttributeError
# Raised when an error occurred while doing a mass assignment to an attribute through the
- # +attributes=+ method. The exception has an +attribute+ property that is the name of the
- # offending attribute.
+ # {ActiveRecord::Base#attributes=}[rdoc-ref:AttributeAssignment#attributes=] method.
+ # The exception has an +attribute+ property that is the name of the offending attribute.
class AttributeAssignmentError < ActiveRecordError
attr_reader :exception, :attribute
- def initialize(message, exception, attribute)
+
+ def initialize(message = nil, exception = nil, attribute = nil)
super(message)
@exception = exception
@attribute = attribute
end
end
- # Raised when there are multiple errors while doing a mass assignment through the +attributes+
+ # Raised when there are multiple errors while doing a mass assignment through the
+ # {ActiveRecord::Base#attributes=}[rdoc-ref:AttributeAssignment#attributes=]
# method. The exception has an +errors+ property that contains an array of AttributeAssignmentError
# objects, each corresponding to the error while assigning to an attribute.
class MultiparameterAssignmentErrors < ActiveRecordError
attr_reader :errors
- def initialize(errors)
+
+ def initialize(errors = nil)
@errors = errors
end
end
@@ -198,11 +232,16 @@ module ActiveRecord
class UnknownPrimaryKey < ActiveRecordError
attr_reader :model
- def initialize(model)
- super("Unknown primary key for table #{model.table_name} in model #{model}.")
- @model = model
+ def initialize(model = nil, description = nil)
+ if model
+ message = "Unknown primary key for table #{model.table_name} in model #{model}."
+ message += "\n#{description}" if description
+ @model = model
+ super(message)
+ else
+ super("Unknown primary key.")
+ end
end
-
end
# Raised when a relation cannot be mutated because it's already loaded.
diff --git a/activerecord/lib/active_record/explain_registry.rb b/activerecord/lib/active_record/explain_registry.rb
index f5cd57e075..b652932f9c 100644
--- a/activerecord/lib/active_record/explain_registry.rb
+++ b/activerecord/lib/active_record/explain_registry.rb
@@ -7,7 +7,7 @@ module ActiveRecord
#
# returns the collected queries local to the current thread.
#
- # See the documentation of <tt>ActiveSupport::PerThreadRegistry</tt>
+ # See the documentation of ActiveSupport::PerThreadRegistry
# for further details.
class ExplainRegistry # :nodoc:
extend ActiveSupport::PerThreadRegistry
diff --git a/activerecord/lib/active_record/explain_subscriber.rb b/activerecord/lib/active_record/explain_subscriber.rb
index 6a49936644..90bcf5a205 100644
--- a/activerecord/lib/active_record/explain_subscriber.rb
+++ b/activerecord/lib/active_record/explain_subscriber.rb
@@ -14,12 +14,12 @@ module ActiveRecord
end
# SCHEMA queries cannot be EXPLAINed, also we do not want to run EXPLAIN on
- # our own EXPLAINs now matter how loopingly beautiful that would be.
+ # our own EXPLAINs no matter how loopingly beautiful that would be.
#
# On the other hand, we want to monitor the performance of our real database
# queries, not the performance of the access to the query cache.
IGNORED_PAYLOADS = %w(SCHEMA EXPLAIN CACHE)
- EXPLAINED_SQLS = /\A\s*(select|update|delete|insert)\b/i
+ EXPLAINED_SQLS = /\A\s*(with|select|update|delete|insert)\b/i
def ignore_payload?(payload)
payload[:exception] || IGNORED_PAYLOADS.include?(payload[:name]) || payload[:sql] !~ EXPLAINED_SQLS
end
diff --git a/activerecord/lib/active_record/fixture_set/file.rb b/activerecord/lib/active_record/fixture_set/file.rb
index 8132310c91..f969556c50 100644
--- a/activerecord/lib/active_record/fixture_set/file.rb
+++ b/activerecord/lib/active_record/fixture_set/file.rb
@@ -17,24 +17,39 @@ module ActiveRecord
def initialize(file)
@file = file
- @rows = nil
end
def each(&block)
rows.each(&block)
end
+ def model_class
+ config_row['model_class']
+ end
private
def rows
- return @rows if @rows
+ @rows ||= raw_rows.reject { |fixture_name, _| fixture_name == '_fixture' }
+ end
+
+ def config_row
+ @config_row ||= begin
+ row = raw_rows.find { |fixture_name, _| fixture_name == '_fixture' }
+ if row
+ row.last
+ else
+ {'model_class': nil}
+ end
+ end
+ end
- begin
+ def raw_rows
+ @raw_rows ||= begin
data = YAML.load(render(IO.read(@file)))
+ data ? validate(data).to_a : []
rescue ArgumentError, Psych::SyntaxError => error
raise Fixture::FormatError, "a YAML error occurred parsing #{@file}. Please note that YAML must be consistently indented using spaces. Tabs are not allowed. Please have a look at http://www.yaml.org/faq.html\nThe exact error was:\n #{error.class}: #{error}", error.backtrace
end
- @rows = data ? validate(data).to_a : []
end
def render(content)
diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb
index 4306b36ae1..17e7c828b9 100644
--- a/activerecord/lib/active_record/fixtures.rb
+++ b/activerecord/lib/active_record/fixtures.rb
@@ -1,6 +1,7 @@
require 'erb'
require 'yaml'
require 'zlib'
+require 'set'
require 'active_support/dependencies'
require 'active_support/core_ext/digest/uuid'
require 'active_record/fixture_set/file'
@@ -88,7 +89,7 @@ module ActiveRecord
# end
#
# In order to use these methods to access fixtured data within your testcases, you must specify one of the
- # following in your <tt>ActiveSupport::TestCase</tt>-derived class:
+ # following in your ActiveSupport::TestCase-derived class:
#
# - to fully enable instantiated fixtures (enable alternate methods #1 and #2 above)
# self.use_instantiated_fixtures = true
@@ -109,7 +110,7 @@ module ActiveRecord
# <% 1.upto(1000) do |i| %>
# fix_<%= i %>:
# id: <%= i %>
- # name: guy_<%= 1 %>
+ # name: guy_<%= i %>
# <% end %>
#
# This will create 1000 very simple fixtures.
@@ -123,28 +124,28 @@ module ActiveRecord
#
# Helper methods defined in a fixture will not be available in other fixtures, to prevent against
# unwanted inter-test dependencies. Methods used by multiple fixtures should be defined in a module
- # that is included in <tt>ActiveRecord::FixtureSet.context_class</tt>.
+ # that is included in ActiveRecord::FixtureSet.context_class.
#
# - define a helper method in `test_helper.rb`
- # class FixtureFileHelpers
+ # module FixtureFileHelpers
# def file_sha(path)
# Digest::SHA2.hexdigest(File.read(Rails.root.join('test/fixtures', path)))
# end
# end
- # ActiveRecord::FixtureSet.context_class.send :include, FixtureFileHelpers
+ # ActiveRecord::FixtureSet.context_class.include FixtureFileHelpers
#
# - use the helper method in a fixture
# photo:
# name: kitten.png
# sha: <%= file_sha 'files/kitten.png' %>
#
- # = Transactional Fixtures
+ # = Transactional Tests
#
# Test cases can use begin+rollback to isolate their changes to the database instead of having to
# delete+insert for every test case.
#
# class FooTest < ActiveSupport::TestCase
- # self.use_transactional_fixtures = true
+ # self.use_transactional_tests = true
#
# test "godzilla" do
# assert !Foo.all.empty?
@@ -158,14 +159,14 @@ module ActiveRecord
# end
#
# If you preload your test database with all fixture data (probably in the rake task) and use
- # transactional fixtures, then you may omit all fixtures declarations in your test cases since
+ # transactional tests, then you may omit all fixtures declarations in your test cases since
# all the data's already there and every case rolls back its changes.
#
# In order to use instantiated fixtures with preloaded data, set +self.pre_loaded_fixtures+ to
# true. This will provide access to fixture data for every table that has been loaded through
# fixtures (depending on the value of +use_instantiated_fixtures+).
#
- # When *not* to use transactional fixtures:
+ # When *not* to use transactional tests:
#
# 1. You're testing whether a transaction works correctly. Nested transactions don't commit until
# all parent transactions commit, particularly, the fixtures transaction which is begun in setup
@@ -181,6 +182,9 @@ module ActiveRecord
# * Stable, autogenerated IDs
# * Label references for associations (belongs_to, has_one, has_many)
# * HABTM associations as inline lists
+ #
+ # There are some more advanced features available even if the id is specified:
+ #
# * Autofilled timestamp columns
# * Fixture label interpolation
# * Support for YAML defaults
@@ -391,6 +395,20 @@ module ActiveRecord
# <<: *DEFAULTS
#
# Any fixture labeled "DEFAULTS" is safely ignored.
+ #
+ # == Configure the fixture model class
+ #
+ # It's possible to set the fixture's model class directly in the YAML file.
+ # This is helpful when fixtures are loaded outside tests and
+ # +set_fixture_class+ is not available (e.g.
+ # when running <tt>rake db:fixtures:load</tt>).
+ #
+ # _fixture:
+ # model_class: User
+ # david:
+ # name: David
+ #
+ # Any fixtures labeled "_fixture" are safely ignored.
class FixtureSet
#--
# An instance of FixtureSet is normally stored in a single YAML file and
@@ -515,15 +533,19 @@ module ActiveRecord
::File.join(fixtures_directory, fs_name))
end
- all_loaded_fixtures.update(fixtures_map)
+ update_all_loaded_fixtures fixtures_map
connection.transaction(:requires_new => true) do
+ deleted_tables = Set.new
fixture_sets.each do |fs|
conn = fs.model_class.respond_to?(:connection) ? fs.model_class.connection : connection
table_rows = fs.table_rows
- table_rows.keys.each do |table|
- conn.delete "DELETE FROM #{conn.quote_table_name(table)}", 'Fixture Delete'
+ table_rows.each_key do |table|
+ unless deleted_tables.include? table
+ conn.delete "DELETE FROM #{conn.quote_table_name(table)}", 'Fixture Delete'
+ end
+ deleted_tables << table
end
table_rows.each do |fixture_set_name, rows|
@@ -531,12 +553,10 @@ module ActiveRecord
conn.insert_fixture(row, fixture_set_name)
end
end
- end
- # Cap primary key sequences to max(pk).
- if connection.respond_to?(:reset_pk_sequence!)
- fixture_sets.each do |fs|
- connection.reset_pk_sequence!(fs.table_name)
+ # Cap primary key sequences to max(pk).
+ if conn.respond_to?(:reset_pk_sequence!)
+ conn.reset_pk_sequence!(fs.table_name)
end
end
end
@@ -562,27 +582,26 @@ module ActiveRecord
@context_class ||= Class.new
end
+ def self.update_all_loaded_fixtures(fixtures_map) # :nodoc:
+ all_loaded_fixtures.update(fixtures_map)
+ end
+
attr_reader :table_name, :name, :fixtures, :model_class, :config
def initialize(connection, name, class_name, path, config = ActiveRecord::Base)
@name = name
@path = path
@config = config
- @model_class = nil
- if class_name.is_a?(Class) # TODO: Should be an AR::Base type class, or any?
- @model_class = class_name
- else
- @model_class = class_name.safe_constantize if class_name
- end
+ self.model_class = class_name
+
+ @fixtures = read_fixture_files(path)
@connection = connection
@table_name = ( model_class.respond_to?(:table_name) ?
model_class.table_name :
self.class.default_fixture_table_name(name, config) )
-
- @fixtures = read_fixture_files path, @model_class
end
def [](x)
@@ -605,7 +624,6 @@ module ActiveRecord
# a list of rows to insert to that table.
def table_rows
now = config.default_timezone == :utc ? Time.now.utc : Time.now
- now = now.to_s(:db)
# allow a standard key to be used for doing defaults in YAML
fixtures.delete('DEFAULTS')
@@ -626,7 +644,7 @@ module ActiveRecord
# interpolate the fixture label
row.each do |key, value|
- row[key] = value.gsub("$LABEL", label) if value.is_a?(String)
+ row[key] = value.gsub("$LABEL", label.to_s) if value.is_a?(String)
end
# generate a primary key if necessary
@@ -634,6 +652,13 @@ module ActiveRecord
row[primary_key_name] = ActiveRecord::FixtureSet.identify(label, primary_key_type)
end
+ # Resolve enums
+ model_class.defined_enums.each do |name, values|
+ if row.include?(name)
+ row[name] = values.fetch(row[name], row[name])
+ end
+ end
+
# If STI is used, find the correct subclass for association reflection
reflection_class =
if row.include?(inheritance_column_name)
@@ -642,7 +667,7 @@ module ActiveRecord
model_class
end
- reflection_class._reflections.values.each do |association|
+ reflection_class._reflections.each_value do |association|
case association.macro
when :belongs_to
# Do not replace association name with association foreign key if they are named the same
@@ -654,7 +679,7 @@ module ActiveRecord
row[association.foreign_type] = $1
end
- fk_type = association.active_record.columns_hash[association.foreign_key].type
+ fk_type = reflection_class.type_for_attribute(fk_name).type
row[fk_name] = ActiveRecord::FixtureSet.identify(value, fk_type)
end
when :has_many
@@ -684,7 +709,7 @@ module ActiveRecord
end
def primary_key_type
- @association.klass.column_types[@association.klass.primary_key].type
+ @association.klass.type_for_attribute(@association.klass.primary_key).type
end
end
@@ -696,6 +721,10 @@ module ActiveRecord
def lhs_key
@association.through_reflection.foreign_key
end
+
+ def join_table
+ @association.through_reflection.table_name
+ end
end
private
@@ -704,7 +733,7 @@ module ActiveRecord
end
def primary_key_type
- @primary_key_type ||= model_class && model_class.column_types[model_class.primary_key].type
+ @primary_key_type ||= model_class && model_class.type_for_attribute(model_class.primary_key).type
end
def add_join_records(rows, row, association)
@@ -738,16 +767,28 @@ module ActiveRecord
end
def column_names
- @column_names ||= @connection.columns(@table_name).collect { |c| c.name }
+ @column_names ||= @connection.columns(@table_name).collect(&:name)
+ end
+
+ def model_class=(class_name)
+ if class_name.is_a?(Class) # TODO: Should be an AR::Base type class, or any?
+ @model_class = class_name
+ else
+ @model_class = class_name.safe_constantize if class_name
+ end
end
- def read_fixture_files(path, model_class)
+ # Loads the fixtures from the YAML file at +path+.
+ # If the file sets the +model_class+ and current instance value is not set,
+ # it uses the file value.
+ def read_fixture_files(path)
yaml_files = Dir["#{path}/{**,*}/*.yml"].select { |f|
::File.file?(f)
} + [yaml_file_path(path)]
yaml_files.each_with_object({}) do |file, fixtures|
FixtureSet::File.open(file) do |fh|
+ self.model_class ||= fh.model_class if fh.model_class
fh.each do |fixture_name, row|
fixtures[fixture_name] = ActiveRecord::Fixture.new(row, model_class)
end
@@ -761,12 +802,6 @@ module ActiveRecord
end
- #--
- # Deprecate 'Fixtures' in favor of 'FixtureSet'.
- #++
- # :nodoc:
- Fixtures = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('ActiveRecord::Fixtures', 'ActiveRecord::FixtureSet')
-
class Fixture #:nodoc:
include Enumerable
@@ -799,7 +834,9 @@ module ActiveRecord
def find
if model_class
- model_class.find(fixture[model_class.primary_key])
+ model_class.unscoped do
+ model_class.find(fixture[model_class.primary_key])
+ end
else
raise FixtureClassNotFound, "No class attached to find."
end
@@ -811,12 +848,12 @@ module ActiveRecord
module TestFixtures
extend ActiveSupport::Concern
- def before_setup
+ def before_setup # :nodoc:
setup_fixtures
super
end
- def after_teardown
+ def after_teardown # :nodoc:
super
teardown_fixtures
end
@@ -825,13 +862,15 @@ module ActiveRecord
class_attribute :fixture_path, :instance_writer => false
class_attribute :fixture_table_names
class_attribute :fixture_class_names
+ class_attribute :use_transactional_tests
class_attribute :use_transactional_fixtures
class_attribute :use_instantiated_fixtures # true, false, or :no_instances
class_attribute :pre_loaded_fixtures
class_attribute :config
+ singleton_class.deprecate 'use_transactional_fixtures=' => 'use use_transactional_tests= instead'
+
self.fixture_table_names = []
- self.use_transactional_fixtures = true
self.use_instantiated_fixtures = false
self.pre_loaded_fixtures = false
self.config = ActiveRecord::Base
@@ -839,6 +878,16 @@ module ActiveRecord
self.fixture_class_names = Hash.new do |h, fixture_set_name|
h[fixture_set_name] = ActiveRecord::FixtureSet.default_fixture_model_name(fixture_set_name, self.config)
end
+
+ silence_warnings do
+ define_singleton_method :use_transactional_tests do
+ if use_transactional_fixtures.nil?
+ true
+ else
+ use_transactional_fixtures
+ end
+ end
+ end
end
module ClassMethods
@@ -859,38 +908,13 @@ module ActiveRecord
fixture_set_names = Dir["#{fixture_path}/{**,*}/*.{yml}"]
fixture_set_names.map! { |f| f[(fixture_path.to_s.size + 1)..-5] }
else
- fixture_set_names = fixture_set_names.flatten.map { |n| n.to_s }
+ fixture_set_names = fixture_set_names.flatten.map(&:to_s)
end
self.fixture_table_names |= fixture_set_names
- require_fixture_classes(fixture_set_names, self.config)
setup_fixture_accessors(fixture_set_names)
end
- def try_to_load_dependency(file_name)
- require_dependency file_name
- rescue LoadError => e
- # Let's hope the developer has included it
- # Let's warn in case this is a subdependency, otherwise
- # subdependency error messages are totally cryptic
- if ActiveRecord::Base.logger
- ActiveRecord::Base.logger.warn("Unable to load #{file_name}, underlying cause #{e.message} \n\n #{e.backtrace.join("\n")}")
- end
- end
-
- def require_fixture_classes(fixture_set_names = nil, config = ActiveRecord::Base)
- if fixture_set_names
- fixture_set_names = fixture_set_names.map { |n| n.to_s }
- else
- fixture_set_names = fixture_table_names
- end
-
- fixture_set_names.each do |file_name|
- file_name = file_name.singularize if config.pluralize_table_names
- try_to_load_dependency(file_name)
- end
- end
-
def setup_fixture_accessors(fixture_set_names = nil)
fixture_set_names = Array(fixture_set_names || fixture_table_names)
methods = Module.new do
@@ -904,7 +928,7 @@ module ActiveRecord
@fixture_cache[fs_name] ||= {}
instances = fixture_names.map do |f_name|
- f_name = f_name.to_s
+ f_name = f_name.to_s if f_name.is_a?(Symbol)
@fixture_cache[fs_name].delete(f_name) if force_reload
if @loaded_fixtures[fs_name][f_name]
@@ -924,7 +948,7 @@ module ActiveRecord
def uses_transaction(*methods)
@uses_transaction = [] unless defined?(@uses_transaction)
- @uses_transaction.concat methods.map { |m| m.to_s }
+ @uses_transaction.concat methods.map(&:to_s)
end
def uses_transaction?(method)
@@ -934,13 +958,13 @@ module ActiveRecord
end
def run_in_transaction?
- use_transactional_fixtures &&
+ use_transactional_tests &&
!self.class.uses_transaction?(method_name)
end
def setup_fixtures(config = ActiveRecord::Base)
- if pre_loaded_fixtures && !use_transactional_fixtures
- raise RuntimeError, 'pre_loaded_fixtures requires use_transactional_fixtures'
+ if pre_loaded_fixtures && !use_transactional_tests
+ raise RuntimeError, 'pre_loaded_fixtures requires use_transactional_tests'
end
@fixture_cache = {}
@@ -967,7 +991,7 @@ module ActiveRecord
end
# Instantiate fixtures for every test if requested.
- instantiate_fixtures(config) if use_instantiated_fixtures
+ instantiate_fixtures if use_instantiated_fixtures
end
def teardown_fixtures
@@ -994,16 +1018,9 @@ module ActiveRecord
Hash[fixtures.map { |f| [f.name, f] }]
end
- # for pre_loaded_fixtures, only require the classes once. huge speed improvement
- @@required_fixture_classes = false
-
- def instantiate_fixtures(config)
+ def instantiate_fixtures
if pre_loaded_fixtures
raise RuntimeError, 'Load fixtures before instantiating them.' if ActiveRecord::FixtureSet.all_loaded_fixtures.empty?
- unless @@required_fixture_classes
- self.class.require_fixture_classes ActiveRecord::FixtureSet.all_loaded_fixtures.keys, config
- @@required_fixture_classes = true
- end
ActiveRecord::FixtureSet.instantiate_all_loaded_fixtures(self, load_instances?)
else
raise RuntimeError, 'Load fixtures before instantiating them.' if @loaded_fixtures.nil?
diff --git a/activerecord/lib/active_record/gem_version.rb b/activerecord/lib/active_record/gem_version.rb
index 4a7aace460..a388b529c9 100644
--- a/activerecord/lib/active_record/gem_version.rb
+++ b/activerecord/lib/active_record/gem_version.rb
@@ -1,12 +1,12 @@
module ActiveRecord
- # Returns the version of the currently loaded ActiveRecord as a <tt>Gem::Version</tt>
+ # Returns the version of the currently loaded Active Record as a <tt>Gem::Version</tt>
def self.gem_version
Gem::Version.new VERSION::STRING
end
module VERSION
- MAJOR = 4
- MINOR = 2
+ MAJOR = 5
+ MINOR = 0
TINY = 0
PRE = "alpha"
diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb
index 251d682a02..589c70db0d 100644
--- a/activerecord/lib/active_record/inheritance.rb
+++ b/activerecord/lib/active_record/inheritance.rb
@@ -55,7 +55,7 @@ module ActiveRecord
subclass = subclass_from_attributes(attrs)
end
- if subclass
+ if subclass && subclass != self
subclass.new(*args, &block)
else
super
@@ -79,20 +79,10 @@ module ActiveRecord
:true == (@finder_needs_type_condition ||= descends_from_active_record? ? :false : :true)
end
- def symbolized_base_class
- ActiveSupport::Deprecation.warn("ActiveRecord::Base.symbolized_base_class is deprecated and will be removed without replacement.")
- @symbolized_base_class ||= base_class.to_s.to_sym
- end
-
- def symbolized_sti_name
- ActiveSupport::Deprecation.warn("ActiveRecord::Base.symbolized_sti_name is deprecated and will be removed without replacement.")
- @symbolized_sti_name ||= sti_name.present? ? sti_name.to_sym : symbolized_base_class
- end
-
# Returns the class descending directly from ActiveRecord::Base, or
# an abstract class, if any, in the inheritance hierarchy.
#
- # If A extends AR::Base, A.base_class will return A. If B descends from A
+ # If A extends ActiveRecord::Base, A.base_class will return A. If B descends from A
# through some arbitrarily deep hierarchy, B.base_class will return A.
#
# If B < A and C < B and if A is an abstract_class then both B.base_class
@@ -177,22 +167,28 @@ module ActiveRecord
end
def find_sti_class(type_name)
- if store_full_sti_class
- ActiveSupport::Dependencies.constantize(type_name)
- else
- compute_type(type_name)
+ subclass = begin
+ if store_full_sti_class
+ ActiveSupport::Dependencies.constantize(type_name)
+ else
+ compute_type(type_name)
+ end
+ rescue NameError
+ raise SubclassNotFound,
+ "The single-table inheritance mechanism failed to locate the subclass: '#{type_name}'. " \
+ "This error is raised because the column '#{inheritance_column}' is reserved for storing the class in case of inheritance. " \
+ "Please rename this column if you didn't intend it to be used for storing the inheritance class " \
+ "or overwrite #{name}.inheritance_column to use another column for that information."
+ end
+ unless subclass == self || descendants.include?(subclass)
+ raise SubclassNotFound, "Invalid single-table inheritance type: #{subclass.name} is not a subclass of #{name}"
end
- rescue NameError
- raise SubclassNotFound,
- "The single-table inheritance mechanism failed to locate the subclass: '#{type_name}'. " +
- "This error is raised because the column '#{inheritance_column}' is reserved for storing the class in case of inheritance. " +
- "Please rename this column if you didn't intend it to be used for storing the inheritance class " +
- "or overwrite #{name}.inheritance_column to use another column for that information."
+ subclass
end
def type_condition(table = arel_table)
sti_column = table[inheritance_column]
- sti_names = ([self] + descendants).map { |model| model.sti_name }
+ sti_names = ([self] + descendants).map(&:sti_name)
sti_column.in(sti_names)
end
@@ -202,20 +198,14 @@ module ActiveRecord
# If this is a StrongParameters hash, and access to inheritance_column is not permitted,
# this will ignore the inheritance column and return nil
def subclass_from_attributes?(attrs)
- columns_hash.include?(inheritance_column) && attrs.is_a?(Hash)
+ attribute_names.include?(inheritance_column) && attrs.is_a?(Hash)
end
def subclass_from_attributes(attrs)
subclass_name = attrs.with_indifferent_access[inheritance_column]
- if subclass_name.present? && subclass_name != self.name
- subclass = subclass_name.safe_constantize
-
- unless descendants.include?(subclass)
- raise ActiveRecord::SubclassNotFound.new("Invalid single-table inheritance type: #{subclass_name} is not a subclass of #{name}")
- end
-
- subclass
+ if subclass_name.present?
+ find_sti_class(subclass_name)
end
end
end
diff --git a/activerecord/lib/active_record/integration.rb b/activerecord/lib/active_record/integration.rb
index 31e2518540..466c8509a4 100644
--- a/activerecord/lib/active_record/integration.rb
+++ b/activerecord/lib/active_record/integration.rb
@@ -10,12 +10,12 @@ module ActiveRecord
# Indicates the format used to generate the timestamp in the cache key.
# Accepts any of the symbols in <tt>Time::DATE_FORMATS</tt>.
#
- # This is +:nsec+, by default.
+ # This is +:usec+, by default.
class_attribute :cache_timestamp_format, :instance_writer => false
- self.cache_timestamp_format = :nsec
+ self.cache_timestamp_format = :usec
end
- # Returns a String, which Action Pack uses for constructing an URL to this
+ # Returns a String, which Action Pack uses for constructing a URL to this
# object. The default implementation returns this record's id as a String,
# or nil if this record's unsaved.
#
@@ -55,16 +55,16 @@ module ActiveRecord
def cache_key(*timestamp_names)
case
when new_record?
- "#{self.class.model_name.cache_key}/new"
+ "#{model_name.cache_key}/new"
when timestamp_names.any?
timestamp = max_updated_column_timestamp(timestamp_names)
timestamp = timestamp.utc.to_s(cache_timestamp_format)
- "#{self.class.model_name.cache_key}/#{id}-#{timestamp}"
+ "#{model_name.cache_key}/#{id}-#{timestamp}"
when timestamp = max_updated_column_timestamp
timestamp = timestamp.utc.to_s(cache_timestamp_format)
- "#{self.class.model_name.cache_key}/#{id}-#{timestamp}"
+ "#{model_name.cache_key}/#{id}-#{timestamp}"
else
- "#{self.class.model_name.cache_key}/#{id}"
+ "#{model_name.cache_key}/#{id}"
end
end
@@ -84,7 +84,7 @@ module ActiveRecord
# Values longer than 20 characters will be truncated. The value
# is truncated word by word.
#
- # user = User.find_by(name: 'David HeinemeierHansson')
+ # user = User.find_by(name: 'David Heinemeier Hansson')
# user.id # => 125
# user_path(user) # => "/users/125-david"
#
diff --git a/activerecord/lib/active_record/legacy_yaml_adapter.rb b/activerecord/lib/active_record/legacy_yaml_adapter.rb
new file mode 100644
index 0000000000..89dee58423
--- /dev/null
+++ b/activerecord/lib/active_record/legacy_yaml_adapter.rb
@@ -0,0 +1,46 @@
+module ActiveRecord
+ module LegacyYamlAdapter
+ def self.convert(klass, coder)
+ return coder unless coder.is_a?(Psych::Coder)
+
+ case coder["active_record_yaml_version"]
+ when 1 then coder
+ else
+ if coder["attributes"].is_a?(AttributeSet)
+ Rails420.convert(klass, coder)
+ else
+ Rails41.convert(klass, coder)
+ end
+ end
+ end
+
+ module Rails420
+ def self.convert(klass, coder)
+ attribute_set = coder["attributes"]
+
+ klass.attribute_names.each do |attr_name|
+ attribute = attribute_set[attr_name]
+ if attribute.type.is_a?(Delegator)
+ type_from_klass = klass.type_for_attribute(attr_name)
+ attribute_set[attr_name] = attribute.with_type(type_from_klass)
+ end
+ end
+
+ coder
+ end
+ end
+
+ module Rails41
+ def self.convert(klass, coder)
+ attributes = klass.attributes_builder
+ .build_from_database(coder["attributes"])
+ new_record = coder["attributes"][klass.primary_key].blank?
+
+ {
+ "attributes" => attributes,
+ "new_record" => new_record,
+ }
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/locale/en.yml b/activerecord/lib/active_record/locale/en.yml
index b1fbd38622..0b35027b2b 100644
--- a/activerecord/lib/active_record/locale/en.yml
+++ b/activerecord/lib/active_record/locale/en.yml
@@ -7,6 +7,7 @@ en:
# Default error messages
errors:
messages:
+ required: "must exist"
taken: "has already been taken"
# Active Record models configuration
@@ -15,8 +16,8 @@ en:
messages:
record_invalid: "Validation failed: %{errors}"
restrict_dependent_destroy:
- one: "Cannot delete record because a dependent %{record} exists"
- many: "Cannot delete record because dependent %{record} exist"
+ has_one: "Cannot delete record because a dependent %{record} exists"
+ has_many: "Cannot delete record because dependent %{record} exist"
# Append your own errors here or at the model/attributes scope.
# You can define own errors for models or model attributes.
diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb
index 52eeb8ae1f..2336d23a1c 100644
--- a/activerecord/lib/active_record/locking/optimistic.rb
+++ b/activerecord/lib/active_record/locking/optimistic.rb
@@ -11,7 +11,7 @@ module ActiveRecord
#
# == Usage
#
- # Active Records support optimistic locking if the field +lock_version+ is present. Each update to the
+ # Active Record supports optimistic locking if the +lock_version+ field is present. Each update to the
# record increments the +lock_version+ column and the locking facilities ensure that records instantiated twice
# will let the last one saved raise a +StaleObjectError+ if the first was also updated. Example:
#
@@ -22,7 +22,7 @@ module ActiveRecord
# p1.save
#
# p2.first_name = "should fail"
- # p2.save # Raises a ActiveRecord::StaleObjectError
+ # p2.save # Raises an ActiveRecord::StaleObjectError
#
# Optimistic locking will also check for stale data when objects are destroyed. Example:
#
@@ -32,7 +32,7 @@ module ActiveRecord
# p1.first_name = "Michael"
# p1.save
#
- # p2.destroy # Raises a ActiveRecord::StaleObjectError
+ # p2.destroy # Raises an ActiveRecord::StaleObjectError
#
# You're then responsible for dealing with the conflict by rescuing the exception and either rolling back, merging,
# or otherwise apply the business logic needed to resolve the conflict.
@@ -66,6 +66,15 @@ module ActiveRecord
send(lock_col + '=', previous_lock_value + 1)
end
+ def _create_record(attribute_names = self.attribute_names, *) # :nodoc:
+ if locking_enabled?
+ # We always want to persist the locking version, even if we don't detect
+ # a change from the default, since the database might have no default
+ attribute_names |= [self.class.locking_column]
+ end
+ super
+ end
+
def _update_record(attribute_names = self.attribute_names) #:nodoc:
return super unless locking_enabled?
return 0 if attribute_names.empty?
@@ -80,17 +89,15 @@ module ActiveRecord
begin
relation = self.class.unscoped
- stmt = relation.where(
- relation.table[self.class.primary_key].eq(id).and(
- relation.table[lock_col].eq(self.class.quote_value(previous_lock_value, column_for_attribute(lock_col)))
- )
- ).arel.compile_update(
- arel_attributes_with_values_for_update(attribute_names),
- self.class.primary_key
+ 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.connection.update stmt
-
unless affected_rows == 1
raise ActiveRecord::StaleObjectError.new(self, "update")
end
@@ -118,12 +125,8 @@ module ActiveRecord
relation = super
if locking_enabled?
- column_name = self.class.locking_column
- column = self.class.columns_hash[column_name]
- substitute = self.class.connection.substitute_at(column, relation.bind_values.length)
-
- relation = relation.where(self.class.arel_table[column_name].eq(substitute))
- relation.bind_values << [column, self[column_name].to_i]
+ locking_column = self.class.locking_column
+ relation = relation.where(locking_column => _read_attribute(locking_column))
end
relation
@@ -141,7 +144,7 @@ module ActiveRecord
# Set the column to use for optimistic locking. Defaults to +lock_version+.
def locking_column=(value)
- clear_caches_calculated_from_columns
+ reload_schema_from_cache
@locking_column = value.to_s
end
@@ -181,17 +184,12 @@ module ActiveRecord
end
end
- class LockingType < SimpleDelegator # :nodoc:
- def type_cast_from_database(value)
+ class LockingType < DelegateClass(Type::Value) # :nodoc:
+ def deserialize(value)
# `nil` *should* be changed to 0
super.to_i
end
- def changed?(old_value, *)
- # Ensure we save if the default was `nil`
- super || old_value == 0
- end
-
def init_with(coder)
__setobj__(coder['subtype'])
end
diff --git a/activerecord/lib/active_record/locking/pessimistic.rb b/activerecord/lib/active_record/locking/pessimistic.rb
index ff7102d35b..8ecdf76b72 100644
--- a/activerecord/lib/active_record/locking/pessimistic.rb
+++ b/activerecord/lib/active_record/locking/pessimistic.rb
@@ -51,7 +51,7 @@ module ActiveRecord
# end
#
# Database-specific information on row locking:
- # MySQL: http://dev.mysql.com/doc/refman/5.1/en/innodb-locking-reads.html
+ # MySQL: http://dev.mysql.com/doc/refman/5.7/en/innodb-locking-reads.html
# PostgreSQL: http://www.postgresql.org/docs/current/interactive/sql-select.html#SQL-FOR-UPDATE-SHARE
module Pessimistic
# Obtain a row lock on this record. Reloads the record to obtain the requested
diff --git a/activerecord/lib/active_record/log_subscriber.rb b/activerecord/lib/active_record/log_subscriber.rb
index eb64d197f0..b63caa4473 100644
--- a/activerecord/lib/active_record/log_subscriber.rb
+++ b/activerecord/lib/active_record/log_subscriber.rb
@@ -20,24 +20,21 @@ module ActiveRecord
@odd = false
end
- def render_bind(column, value)
- if column
- if column.binary?
- # This specifically deals with the PG adapter that casts bytea columns into a Hash.
- value = value[:value] if value.is_a?(Hash)
- value = value ? "<#{value.bytesize} bytes of binary data>" : "<NULL binary data>"
- end
-
- [column.name, value]
+ def render_bind(attribute)
+ value = if attribute.type.binary? && attribute.value
+ "<#{attribute.value.bytesize} bytes of binary data>"
else
- [nil, value]
+ attribute.value_for_database
end
+
+ [attribute.name, value]
end
def sql(event)
- self.class.runtime += event.duration
return unless logger.debug?
+ self.class.runtime += event.duration
+
payload = event.payload
return if IGNORE_PAYLOAD_NAMES.include?(payload[:name])
@@ -47,23 +44,44 @@ module ActiveRecord
binds = nil
unless (payload[:binds] || []).empty?
- binds = " " + payload[:binds].map { |col,v|
- render_bind(col, v)
- }.inspect
+ binds = " " + payload[:binds].map { |attr| render_bind(attr) }.inspect
end
- if odd?
- name = color(name, CYAN, true)
- sql = color(sql, nil, true)
- else
- name = color(name, MAGENTA, true)
- end
+ name = colorize_payload_name(name, payload[:name])
+ sql = color(sql, sql_color(sql), true)
debug " #{name} #{sql}#{binds}"
end
- def odd?
- @odd = !@odd
+ private
+
+ def colorize_payload_name(name, payload_name)
+ if payload_name.blank? || payload_name == "SQL" # SQL vs Model Load/Exists
+ color(name, MAGENTA, true)
+ else
+ color(name, CYAN, true)
+ end
+ end
+
+ def sql_color(sql)
+ case sql
+ when /\A\s*rollback/mi
+ RED
+ when /\s*.*?select .*for update/mi, /\A\s*lock/mi
+ WHITE
+ when /\A\s*select/i
+ BLUE
+ when /\A\s*insert/i
+ GREEN
+ when /\A\s*update/i
+ YELLOW
+ when /\A\s*delete/i
+ RED
+ when /transaction\s*\Z/i
+ CYAN
+ else
+ MAGENTA
+ end
end
def logger
diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb
index 7c4dad21a0..c8b96b8de0 100644
--- a/activerecord/lib/active_record/migration.rb
+++ b/activerecord/lib/active_record/migration.rb
@@ -9,40 +9,128 @@ module ActiveRecord
end
end
- # Exception that can be raised to stop migrations from going backwards.
+ # Exception that can be raised to stop migrations from being rolled back.
+ # For example the following migration is not reversible.
+ # Rolling back this migration will raise an ActiveRecord::IrreversibleMigration error.
+ #
+ # class IrreversibleMigrationExample < ActiveRecord::Migration
+ # def change
+ # create_table :distributors do |t|
+ # t.string :zipcode
+ # end
+ #
+ # execute <<-SQL
+ # ALTER TABLE distributors
+ # ADD CONSTRAINT zipchk
+ # CHECK (char_length(zipcode) = 5) NO INHERIT;
+ # SQL
+ # end
+ # end
+ #
+ # There are two ways to mitigate this problem.
+ #
+ # 1. Define <tt>#up</tt> and <tt>#down</tt> methods instead of <tt>#change</tt>:
+ #
+ # class ReversibleMigrationExample < ActiveRecord::Migration
+ # def up
+ # create_table :distributors do |t|
+ # t.string :zipcode
+ # end
+ #
+ # execute <<-SQL
+ # ALTER TABLE distributors
+ # ADD CONSTRAINT zipchk
+ # CHECK (char_length(zipcode) = 5) NO INHERIT;
+ # SQL
+ # end
+ #
+ # def down
+ # execute <<-SQL
+ # ALTER TABLE distributors
+ # DROP CONSTRAINT zipchk
+ # SQL
+ #
+ # drop_table :distributors
+ # end
+ # end
+ #
+ # 2. Use the #reversible method in <tt>#change</tt> method:
+ #
+ # class ReversibleMigrationExample < ActiveRecord::Migration
+ # def change
+ # create_table :distributors do |t|
+ # t.string :zipcode
+ # end
+ #
+ # reversible do |dir|
+ # dir.up do
+ # execute <<-SQL
+ # ALTER TABLE distributors
+ # ADD CONSTRAINT zipchk
+ # CHECK (char_length(zipcode) = 5) NO INHERIT;
+ # SQL
+ # end
+ #
+ # dir.down do
+ # execute <<-SQL
+ # ALTER TABLE distributors
+ # DROP CONSTRAINT zipchk
+ # SQL
+ # end
+ # end
+ # end
+ # end
class IrreversibleMigration < MigrationError
end
class DuplicateMigrationVersionError < MigrationError#:nodoc:
- def initialize(version)
- super("Multiple migrations have the version number #{version}")
+ def initialize(version = nil)
+ if version
+ super("Multiple migrations have the version number #{version}.")
+ else
+ super("Duplicate migration version error.")
+ end
end
end
class DuplicateMigrationNameError < MigrationError#:nodoc:
- def initialize(name)
- super("Multiple migrations have the name #{name}")
+ def initialize(name = nil)
+ if name
+ super("Multiple migrations have the name #{name}.")
+ else
+ super("Duplicate migration name.")
+ end
end
end
class UnknownMigrationVersionError < MigrationError #:nodoc:
- def initialize(version)
- super("No migration with version number #{version}")
+ def initialize(version = nil)
+ if version
+ super("No migration with version number #{version}.")
+ else
+ super("Unknown migration version.")
+ end
end
end
class IllegalMigrationNameError < MigrationError#:nodoc:
- def initialize(name)
- super("Illegal name for migration file: #{name}\n\t(only lower case letters, numbers, and '_' allowed)")
+ def initialize(name = nil)
+ if name
+ super("Illegal name for migration file: #{name}\n\t(only lower case letters, numbers, and '_' allowed).")
+ else
+ super("Illegal name for migration.")
+ end
end
end
class PendingMigrationError < MigrationError#:nodoc:
- def initialize
- if defined?(Rails)
- super("Migrations are pending. To resolve this issue, run:\n\n\tbin/rake db:migrate RAILS_ENV=#{::Rails.env}")
+ def initialize(message = nil)
+ if !message && defined?(Rails.env)
+ super("Migrations are pending. To resolve this issue, run:\n\n\tbin/rake db:migrate RAILS_ENV=#{::Rails.env}.")
+ elsif !message
+ super("Migrations are pending. To resolve this issue, run:\n\n\tbin/rake db:migrate.")
else
- super("Migrations are pending. To resolve this issue, run:\n\n\tbin/rake db:migrate")
+ super
end
end
end
@@ -106,17 +194,18 @@ module ActiveRecord
#
# == Available transformations
#
+ # === Creation
+ #
+ # * <tt>create_join_table(table_1, table_2, options)</tt>: Creates a join
+ # table having its name as the lexical order of the first two
+ # arguments. See
+ # ActiveRecord::ConnectionAdapters::SchemaStatements#create_join_table for
+ # details.
# * <tt>create_table(name, options)</tt>: Creates a table called +name+ and
# makes the table object available to a block that can then add columns to it,
# following the same format as +add_column+. See example above. The options hash
# is for fragments like "DEFAULT CHARSET=UTF-8" that are appended to the create
# table definition.
- # * <tt>drop_table(name)</tt>: Drops the table called +name+.
- # * <tt>change_table(name, options)</tt>: Allows to make column alterations to
- # the table called +name+. It makes the table object available to a block that
- # can then add/remove columns, indexes or foreign keys to it.
- # * <tt>rename_table(old_name, new_name)</tt>: Renames the table called +old_name+
- # to +new_name+.
# * <tt>add_column(table_name, column_name, type, options)</tt>: Adds a new column
# to the table called +table_name+
# named +column_name+ specified to be one of the following types:
@@ -127,21 +216,59 @@ module ActiveRecord
# Other options include <tt>:limit</tt> and <tt>:null</tt> (e.g.
# <tt>{ limit: 50, null: false }</tt>) -- see
# ActiveRecord::ConnectionAdapters::TableDefinition#column for details.
- # * <tt>rename_column(table_name, column_name, new_column_name)</tt>: Renames
- # a column but keeps the type and content.
- # * <tt>change_column(table_name, column_name, type, options)</tt>: Changes
- # the column to a different type using the same parameters as add_column.
- # * <tt>remove_column(table_name, column_name, type, options)</tt>: Removes the column
- # named +column_name+ from the table called +table_name+.
+ # * <tt>add_foreign_key(from_table, to_table, options)</tt>: Adds a new
+ # foreign key. +from_table+ is the table with the key column, +to_table+ contains
+ # the referenced primary key.
# * <tt>add_index(table_name, column_names, options)</tt>: Adds a new index
# with the name of the column. Other options include
# <tt>:name</tt>, <tt>:unique</tt> (e.g.
# <tt>{ name: 'users_name_index', unique: true }</tt>) and <tt>:order</tt>
# (e.g. <tt>{ order: { name: :desc } }</tt>).
- # * <tt>remove_index(table_name, column: column_name)</tt>: Removes the index
- # specified by +column_name+.
+ # * <tt>add_reference(:table_name, :reference_name)</tt>: Adds a new column
+ # +reference_name_id+ by default an integer. See
+ # ActiveRecord::ConnectionAdapters::SchemaStatements#add_reference for details.
+ # * <tt>add_timestamps(table_name, options)</tt>: Adds timestamps (+created_at+
+ # and +updated_at+) columns to +table_name+.
+ #
+ # === Modification
+ #
+ # * <tt>change_column(table_name, column_name, type, options)</tt>: Changes
+ # the column to a different type using the same parameters as add_column.
+ # * <tt>change_column_default(table_name, column_name, default)</tt>: Sets a
+ # default value for +column_name+ definded by +default+ on +table_name+.
+ # * <tt>change_column_null(table_name, column_name, null, default = nil)</tt>:
+ # Sets or removes a +NOT NULL+ constraint on +column_name+. The +null+ flag
+ # indicates whether the value can be +NULL+. See
+ # ActiveRecord::ConnectionAdapters::SchemaStatements#change_column_null for
+ # details.
+ # * <tt>change_table(name, options)</tt>: Allows to make column alterations to
+ # the table called +name+. It makes the table object available to a block that
+ # can then add/remove columns, indexes or foreign keys to it.
+ # * <tt>rename_column(table_name, column_name, new_column_name)</tt>: Renames
+ # a column but keeps the type and content.
+ # * <tt>rename_index(table_name, old_name, new_name)</tt>: Renames an index.
+ # * <tt>rename_table(old_name, new_name)</tt>: Renames the table called +old_name+
+ # to +new_name+.
+ #
+ # === Deletion
+ #
+ # * <tt>drop_table(name)</tt>: Drops the table called +name+.
+ # * <tt>drop_join_table(table_1, table_2, options)</tt>: Drops the join table
+ # specified by the given arguments.
+ # * <tt>remove_column(table_name, column_name, type, options)</tt>: Removes the column
+ # named +column_name+ from the table called +table_name+.
+ # * <tt>remove_columns(table_name, *column_names)</tt>: Removes the given
+ # columns from the table definition.
+ # * <tt>remove_foreign_key(from_table, options_or_to_table)</tt>: Removes the
+ # given foreign key from the table called +table_name+.
+ # * <tt>remove_index(table_name, column: column_names)</tt>: Removes the index
+ # specified by +column_names+.
# * <tt>remove_index(table_name, name: index_name)</tt>: Removes the index
# specified by +index_name+.
+ # * <tt>remove_reference(table_name, ref_name, options)</tt>: Removes the
+ # reference(s) on +table_name+ specified by +ref_name+.
+ # * <tt>remove_timestamps(table_name, options)</tt>: Removes the timestamp
+ # columns (+created_at+ and +updated_at+) from the table definition.
#
# == Irreversible transformations
#
@@ -161,22 +288,15 @@ module ActiveRecord
# in the <tt>db/migrate/</tt> directory where <tt>timestamp</tt> is the
# UTC formatted date and time that the migration was generated.
#
- # You may then edit the <tt>up</tt> and <tt>down</tt> methods of
- # MyNewMigration.
- #
# There is a special syntactic shortcut to generate migrations that add fields to a table.
#
# rails generate migration add_fieldname_to_tablename fieldname:string
#
- # This will generate the file <tt>timestamp_add_fieldname_to_tablename</tt>, which will look like this:
+ # This will generate the file <tt>timestamp_add_fieldname_to_tablename.rb</tt>, which will look like this:
# class AddFieldnameToTablename < ActiveRecord::Migration
- # def up
+ # def change
# add_column :tablenames, :fieldname, :string
# end
- #
- # def down
- # remove_column :tablenames, :fieldname
- # end
# end
#
# To run migrations against the currently configured database, use
@@ -188,9 +308,12 @@ module ActiveRecord
#
# To roll the database back to a previous migration version, use
# <tt>rake db:migrate VERSION=X</tt> where <tt>X</tt> is the version to which
- # you wish to downgrade. If any of the migrations throw an
- # <tt>ActiveRecord::IrreversibleMigration</tt> exception, that step will fail and you'll
- # have some manual work to do.
+ # you wish to downgrade. Alternatively, you can also use the STEP option if you
+ # wish to rollback last few migrations. <tt>rake db:migrate STEP=2</tt> will rollback
+ # the latest two migrations.
+ #
+ # If any of the migrations throw an <tt>ActiveRecord::IrreversibleMigration</tt> exception,
+ # that step will fail and you'll have some manual work to do.
#
# == Database support
#
@@ -279,21 +402,6 @@ module ActiveRecord
# The phrase "Updating salaries..." would then be printed, along with the
# benchmark for the block when the block completes.
#
- # == About the schema_migrations table
- #
- # Rails versions 2.0 and prior used to create a table called
- # <tt>schema_info</tt> when using migrations. This table contained the
- # version of the schema as of the last applied migration.
- #
- # Starting with Rails 2.1, the <tt>schema_info</tt> table is
- # (automatically) replaced by the <tt>schema_migrations</tt> table, which
- # contains the version numbers of all the migrations applied.
- #
- # As a result, it is now possible to add migration files that are numbered
- # lower than the current schema version: when migrating up, those
- # never-applied "interleaved" migrations will be automatically applied, and
- # when migrating down, never-applied "interleaved" migrations will be skipped.
- #
# == Timestamped Migrations
#
# By default, Rails generates migrations that look like:
@@ -311,9 +419,8 @@ module ActiveRecord
#
# == Reversible Migrations
#
- # Starting with Rails 3.1, you will be able to define reversible migrations.
# Reversible migrations are migrations that know how to go +down+ for you.
- # You simply supply the +up+ logic, and the Migration system will figure out
+ # You simply supply the +up+ logic, and the Migration system figures out
# how to execute the down commands for you.
#
# To define a reversible migration, define the +change+ method in your
@@ -393,13 +500,21 @@ module ActiveRecord
attr_accessor :delegate # :nodoc:
attr_accessor :disable_ddl_transaction # :nodoc:
+ # 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)
end
def load_schema_if_pending!
- if ActiveRecord::Migrator.needs_migration?
- ActiveRecord::Tasks::DatabaseTasks.load_schema
+ if ActiveRecord::Migrator.needs_migration? || !ActiveRecord::Migrator.any_migrations?
+ # Roundtrip to Rake to allow plugins to hook into database initialization.
+ FileUtils.cd Rails.root do
+ current_config = Base.connection_config
+ Base.clear_all_connections!
+ system("bin/rake db:test:prepare")
+ # Establish a new connection, the old database may be gone (db:test:prepare uses purge)
+ Base.establish_connection(current_config)
+ end
check_pending!
end
end
@@ -418,7 +533,10 @@ module ActiveRecord
new.migrate direction
end
- # Disable DDL transactions for this migration.
+ # Disable the transaction wrapping this migration.
+ # You can still create your own transactions even after calling #disable_ddl_transaction!
+ #
+ # For more details read the {"Transactional Migrations" section above}[rdoc-ref:Migration].
def disable_ddl_transaction!
@disable_ddl_transaction = true
end
@@ -465,7 +583,7 @@ module ActiveRecord
# Or equivalently, if +TenderloveMigration+ is defined as in the
# documentation for Migration:
#
- # require_relative '2012121212_tenderlove_migration'
+ # require_relative '20121212123456_tenderlove_migration'
#
# class FixupTLMigration < ActiveRecord::Migration
# def change
@@ -481,13 +599,13 @@ module ActiveRecord
def revert(*migration_classes)
run(*migration_classes.reverse, revert: true) unless migration_classes.empty?
if block_given?
- if @connection.respond_to? :revert
- @connection.revert { yield }
+ if connection.respond_to? :revert
+ connection.revert { yield }
else
- recorder = CommandRecorder.new(@connection)
+ recorder = CommandRecorder.new(connection)
@connection = recorder
suppress_messages do
- @connection.revert { yield }
+ connection.revert { yield }
end
@connection = recorder.delegate
recorder.commands.each do |cmd, args, block|
@@ -498,7 +616,7 @@ module ActiveRecord
end
def reverting?
- @connection.respond_to?(:reverting) && @connection.reverting
+ connection.respond_to?(:reverting) && connection.reverting
end
class ReversibleBlockHelper < Struct.new(:reverting) # :nodoc:
@@ -555,7 +673,7 @@ module ActiveRecord
revert { run(*migration_classes, direction: dir, revert: true) }
else
migration_classes.each do |migration_class|
- migration_class.new.exec_migration(@connection, dir)
+ migration_class.new.exec_migration(connection, dir)
end
end
end
@@ -644,13 +762,14 @@ module ActiveRecord
end
def method_missing(method, *arguments, &block)
- arg_list = arguments.map{ |a| a.inspect } * ', '
+ arg_list = arguments.map(&:inspect) * ', '
say_with_time "#{method}(#{arg_list})" do
- unless @connection.respond_to? :revert
+ unless connection.respond_to? :revert
unless arguments.empty? || [:execute, :enable_extension, :disable_extension].include?(method)
arguments[0] = proper_table_name(arguments.first, table_name_options)
- if [:rename_table, :add_foreign_key].include?(method)
+ if [:rename_table, :add_foreign_key].include?(method) ||
+ (method == :remove_foreign_key && !arguments.second.is_a?(Hash))
arguments[1] = proper_table_name(arguments.second, table_name_options)
end
end
@@ -725,7 +844,9 @@ module ActiveRecord
end
end
- def table_name_options(config = ActiveRecord::Base)
+ # Builds a hash for use in ActiveRecord::Migration#proper_table_name using
+ # the Active Record object's table_name prefix and suffix
+ def table_name_options(config = ActiveRecord::Base) #:nodoc:
{
table_name_prefix: config.table_name_prefix,
table_name_suffix: config.table_name_suffix
@@ -817,7 +938,7 @@ module ActiveRecord
new(:up, migrations, target_version).migrate
end
- def down(migrations_paths, target_version = nil, &block)
+ def down(migrations_paths, target_version = nil)
migrations = migrations(migrations_paths)
migrations.select! { |m| yield m } if block_given?
@@ -836,25 +957,24 @@ module ActiveRecord
SchemaMigration.table_name
end
- def get_all_versions
- SchemaMigration.all.map { |x| x.version.to_i }.sort
+ def get_all_versions(connection = Base.connection)
+ if connection.table_exists?(schema_migrations_table_name)
+ SchemaMigration.all.map { |x| x.version.to_i }.sort
+ else
+ []
+ end
end
def current_version(connection = Base.connection)
- sm_table = schema_migrations_table_name
- if connection.table_exists?(sm_table)
- get_all_versions.max || 0
- else
- 0
- end
+ get_all_versions(connection).max || 0
end
def needs_migration?(connection = Base.connection)
- current_version(connection) < last_version
+ (migrations(migrations_paths).collect(&:version) - get_all_versions(connection)).size > 0
end
- def last_version
- last_migration.version
+ def any_migrations?
+ migrations(migrations_paths).any?
end
def last_migration #:nodoc:
@@ -863,14 +983,10 @@ module ActiveRecord
def migrations_paths
@migrations_paths ||= ['db/migrate']
- # just to not break things if someone uses: migration_path = some_string
+ # just to not break things if someone uses: migrations_path = some_string
Array(@migrations_paths)
end
- def migrations_path
- migrations_paths.first
- end
-
def migrations(paths)
paths = Array(paths)
diff --git a/activerecord/lib/active_record/migration/command_recorder.rb b/activerecord/lib/active_record/migration/command_recorder.rb
index 36256415df..0fa665c7e0 100644
--- a/activerecord/lib/active_record/migration/command_recorder.rb
+++ b/activerecord/lib/active_record/migration/command_recorder.rb
@@ -5,15 +5,36 @@ module ActiveRecord
# knows how to invert the following commands:
#
# * add_column
+ # * add_foreign_key
# * add_index
+ # * add_reference
# * add_timestamps
- # * create_table
+ # * change_column
+ # * change_column_default (must supply a :from and :to option)
+ # * change_column_null
# * create_join_table
+ # * create_table
+ # * disable_extension
+ # * drop_join_table
+ # * drop_table (must supply a block)
+ # * enable_extension
+ # * remove_column (must supply a type)
+ # * remove_columns (must specify at least one column name or more)
+ # * remove_foreign_key (must supply a second table)
+ # * remove_index
+ # * remove_reference
# * remove_timestamps
# * rename_column
# * rename_index
# * rename_table
class CommandRecorder
+ ReversibleAndIrreversibleMethods = [:create_table, :create_join_table, :rename_table, :add_column, :remove_column,
+ :rename_index, :rename_column, :add_index, :remove_index, :add_timestamps, :remove_timestamps,
+ :change_column_default, :add_reference, :remove_reference, :transaction,
+ :drop_join_table, :drop_table, :execute_block, :enable_extension, :disable_extension,
+ :change_column, :execute, :remove_columns, :change_column_null,
+ :add_foreign_key, :remove_foreign_key
+ ]
include JoinTable
attr_accessor :commands, :delegate, :reverting
@@ -41,7 +62,7 @@ module ActiveRecord
@reverting = !@reverting
end
- # record +command+. +command+ should be a method name and arguments.
+ # Record +command+. +command+ should be a method name and arguments.
# For example:
#
# recorder.record(:method_name, [:arg1, :arg2])
@@ -62,7 +83,12 @@ module ActiveRecord
# invert the +command+.
def inverse_of(command, args, &block)
method = :"invert_#{command}"
- raise IrreversibleMigration unless respond_to?(method, true)
+ raise IrreversibleMigration, <<-MSG.strip_heredoc 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.
+ 2. Use the #reversible method to define reversible behavior.
+ MSG
send(method, args, &block)
end
@@ -70,14 +96,7 @@ module ActiveRecord
super || delegate.respond_to?(*args)
end
- [:create_table, :create_join_table, :rename_table, :add_column, :remove_column,
- :rename_index, :rename_column, :add_index, :remove_index, :add_timestamps, :remove_timestamps,
- :change_column_default, :add_reference, :remove_reference, :transaction,
- :drop_join_table, :drop_table, :execute_block, :enable_extension,
- :change_column, :execute, :remove_columns, :change_column_null,
- :add_foreign_key, :remove_foreign_key
- # irreversible methods need to be here too
- ].each do |method|
+ ReversibleAndIrreversibleMethods.each do |method|
class_eval <<-EOV, __FILE__, __LINE__ + 1
def #{method}(*args, &block) # def create_table(*args, &block)
record(:"#{method}", args, &block) # record(:create_table, args, &block)
@@ -151,19 +170,31 @@ module ActiveRecord
end
def invert_remove_index(args)
- table, options = *args
-
- unless options && options.is_a?(Hash) && options[:column]
- raise ActiveRecord::IrreversibleMigration, "remove_index is only reversible if given a :column option."
+ table, options_or_column = *args
+ if (options = options_or_column).is_a?(Hash)
+ unless options[:column]
+ raise ActiveRecord::IrreversibleMigration, "remove_index is only reversible if given a :column option."
+ end
+ options = options.dup
+ [:add_index, [table, options.delete(:column), options]]
+ elsif (column = options_or_column).present?
+ [:add_index, [table, column]]
end
-
- options = options.dup
- [:add_index, [table, options.delete(:column), options]]
end
alias :invert_add_belongs_to :invert_add_reference
alias :invert_remove_belongs_to :invert_remove_reference
+ def invert_change_column_default(args)
+ table, column, options = *args
+
+ unless options && options.is_a?(Hash) && options.has_key?(:from) && options.has_key?(:to)
+ raise ActiveRecord::IrreversibleMigration, "change_column_default is only reversible if given a :from and :to option."
+ end
+
+ [:change_column_default, [table, column, from: options[:to], to: options[:from]]]
+ end
+
def invert_change_column_null(args)
args[2] = !args[2]
[:change_column_null, args]
@@ -184,6 +215,16 @@ module ActiveRecord
[:remove_foreign_key, [from_table, options]]
end
+ def invert_remove_foreign_key(args)
+ from_table, to_table, remove_options = args
+ raise ActiveRecord::IrreversibleMigration, "remove_foreign_key is only reversible if given a second table" if to_table.nil? || to_table.is_a?(Hash)
+
+ reversed_args = [from_table, to_table]
+ reversed_args << remove_options if remove_options
+
+ [:add_foreign_key, reversed_args]
+ end
+
# Forwards any missing method call to the \target.
def method_missing(method, *args, &block)
if @delegate.respond_to?(method)
diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb
index 850220babd..a9bd094a66 100644
--- a/activerecord/lib/active_record/model_schema.rb
+++ b/activerecord/lib/active_record/model_schema.rb
@@ -50,6 +50,13 @@ module ActiveRecord
class_attribute :pluralize_table_names, instance_writer: false
self.pluralize_table_names = true
+ ##
+ # :singleton-method:
+ # Accessor for 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.
+ class_attribute :ignored_columns, instance_accessor: false
+ self.ignored_columns = [].freeze
+
self.inheritance_column = 'type'
delegate :type_for_attribute, to: :class
@@ -63,7 +70,7 @@ module ActiveRecord
# records, artists => artists_records
# music_artists, music_records => music_artists_records
def self.derive_join_table_name(first_table, second_table) # :nodoc:
- [first_table.to_s, second_table.to_s].sort.join("\0").gsub(/^(.*_)(.+)\0\1(.+)/, '\1\2_\3').gsub("\0", "_")
+ [first_table.to_s, second_table.to_s].sort.join("\0").gsub(/^(.*_)(.+)\0\1(.+)/, '\1\2_\3').tr("\0", "_")
end
module ClassMethods
@@ -111,17 +118,6 @@ module ActiveRecord
# class Mouse < ActiveRecord::Base
# self.table_name = "mice"
# end
- #
- # Alternatively, you can override the table_name method to define your
- # own computation. (Possibly using <tt>super</tt> to manipulate the default
- # table name.) Example:
- #
- # class Post < ActiveRecord::Base
- # def self.table_name
- # "special_" + super
- # end
- # end
- # Post.table_name # => "special_posts"
def table_name
reset_table_name unless defined?(@table_name)
@table_name
@@ -132,9 +128,6 @@ module ActiveRecord
# class Project < ActiveRecord::Base
# self.table_name = "project"
# end
- #
- # You can also just define your own <tt>self.table_name</tt> method; see
- # the documentation for ActiveRecord::Base#table_name.
def table_name=(value)
value = value && value.to_s
@@ -147,7 +140,7 @@ module ActiveRecord
@quoted_table_name = nil
@arel_table = nil
@sequence_name = nil unless defined?(@explicit_sequence_name) && @explicit_sequence_name
- @relation = Relation.create(self, arel_table)
+ @predicate_builder = nil
end
# Returns a quoted version of the table name, used to construct SQL statements.
@@ -227,37 +220,46 @@ module ActiveRecord
# Indicates whether the table associated with this class exists
def table_exists?
- connection.schema_cache.table_exists?(table_name)
+ connection.schema_cache.data_source_exists?(table_name)
end
def attributes_builder # :nodoc:
- @attributes_builder ||= AttributeSet::Builder.new(column_types)
+ @attributes_builder ||= AttributeSet::Builder.new(attribute_types, primary_key)
end
- def column_types # :nodoc:
- @column_types ||= columns_hash.transform_values(&:cast_type).tap do |h|
- h.default = Type::Value.new
- end
+ def columns_hash # :nodoc:
+ load_schema
+ @columns_hash
+ end
+
+ def columns
+ load_schema
+ @columns ||= columns_hash.values
+ end
+
+ def attribute_types # :nodoc:
+ load_schema
+ @attribute_types ||= Hash.new(Type::Value.new)
end
def type_for_attribute(attr_name) # :nodoc:
- column_types[attr_name]
+ attribute_types[attr_name]
end
# Returns a hash where the keys are column names and the values are
- # default values when instantiating the AR object for this table.
+ # default values when instantiating the Active Record object for this table.
def column_defaults
- default_attributes.to_hash
+ load_schema
+ _default_attributes.to_hash
end
- def default_attributes # :nodoc:
- @default_attributes ||= attributes_builder.build_from_database(
- columns_hash.transform_values(&:default))
+ def _default_attributes # :nodoc:
+ @default_attributes ||= AttributeSet.new({})
end
# Returns an array of column names as strings.
def column_names
- @column_names ||= columns.map { |column| column.name }
+ @column_names ||= columns.map(&:name)
end
# Returns an array of column objects where the primary id, all columns ending in "_id" or "_count",
@@ -295,22 +297,50 @@ module ActiveRecord
def reset_column_information
connection.clear_cache!
undefine_attribute_methods
- connection.schema_cache.clear_table_cache!(table_name) if table_exists?
-
- @arel_engine = nil
- @column_names = nil
- @column_types = nil
- @content_columns = nil
- @default_attributes = nil
- @dynamic_methods_hash = nil
- @inheritance_column = nil unless defined?(@explicit_inheritance_column) && @explicit_inheritance_column
- @relation = nil
- @time_zone_column_names = nil
- @cached_time_zone = nil
+ connection.schema_cache.clear_data_source_cache!(table_name)
+
+ reload_schema_from_cache
end
private
+ def schema_loaded?
+ defined?(@columns_hash) && @columns_hash
+ end
+
+ def load_schema
+ unless schema_loaded?
+ load_schema!
+ end
+ end
+
+ def load_schema!
+ @columns_hash = connection.schema_cache.columns_hash(table_name).except(*ignored_columns)
+ @columns_hash.each do |name, column|
+ warn_if_deprecated_type(column)
+ define_attribute(
+ name,
+ connection.lookup_cast_type_from_column(column),
+ default: column.default,
+ user_provided_default: false
+ )
+ end
+ end
+
+ def reload_schema_from_cache
+ @arel_engine = nil
+ @arel_table = nil
+ @column_names = nil
+ @attribute_types = nil
+ @content_columns = nil
+ @default_attributes = nil
+ @inheritance_column = nil unless defined?(@explicit_inheritance_column) && @explicit_inheritance_column
+ @attributes_builder = nil
+ @columns = nil
+ @columns_hash = nil
+ @attribute_names = nil
+ end
+
# Guesses the table name, but does not decorate it with prefix and suffix information.
def undecorated_table_name(class_name = base_class.name)
table_name = class_name.to_s.demodulize.underscore
@@ -334,6 +364,28 @@ module ActiveRecord
base.table_name
end
end
+
+ def warn_if_deprecated_type(column)
+ return if attributes_to_define_after_schema_loads.key?(column.name)
+ if column.respond_to?(:oid) && column.sql_type.start_with?("point")
+ if column.array?
+ array_arguments = ", array: true"
+ else
+ array_arguments = ""
+ end
+ ActiveSupport::Deprecation.warn(<<-WARNING.strip_heredoc)
+ The behavior of the `:point` type will be changing in Rails 5.1 to
+ return a `Point` object, rather than an `Array`. If you'd like to
+ keep the old behavior, you can add this line to #{self.name}:
+
+ attribute :#{column.name}, :legacy_point#{array_arguments}
+
+ If you'd like the new behavior today, you can add this line:
+
+ attribute :#{column.name}, :rails_5_1_point#{array_arguments}
+ WARNING
+ end
+ end
end
end
end
diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb
index 8a2a06f2ca..c5a1488588 100644
--- a/activerecord/lib/active_record/nested_attributes.rb
+++ b/activerecord/lib/active_record/nested_attributes.rb
@@ -81,6 +81,9 @@ module ActiveRecord
#
# Note that the model will _not_ be destroyed until the parent is saved.
#
+ # Also note that the model will not be destroyed unless you also specify
+ # its id in the updated hash.
+ #
# === One-to-many
#
# Consider a member that has a number of posts:
@@ -111,7 +114,7 @@ module ActiveRecord
# member.posts.first.title # => 'Kari, the awesome Ruby documentation browser!'
# member.posts.second.title # => 'The egalitarian assumption of the modern citizen'
#
- # You may also set a :reject_if proc to silently ignore any new record
+ # You may also set a +:reject_if+ proc to silently ignore any new record
# hashes if they fail to pass your criteria. For example, the previous
# example could be rewritten as:
#
@@ -133,7 +136,7 @@ module ActiveRecord
# member.posts.first.title # => 'Kari, the awesome Ruby documentation browser!'
# member.posts.second.title # => 'The egalitarian assumption of the modern citizen'
#
- # Alternatively, :reject_if also accepts a symbol for using methods:
+ # Alternatively, +:reject_if+ also accepts a symbol for using methods:
#
# class Member < ActiveRecord::Base
# has_many :posts
@@ -144,8 +147,8 @@ module ActiveRecord
# has_many :posts
# accepts_nested_attributes_for :posts, reject_if: :reject_posts
#
- # def reject_posts(attributed)
- # attributed['title'].blank?
+ # def reject_posts(attributes)
+ # attributes['title'].blank?
# end
# end
#
@@ -163,6 +166,11 @@ module ActiveRecord
# member.posts.first.title # => '[UPDATED] An, as of yet, undisclosed awesome Ruby documentation browser!'
# member.posts.second.title # => '[UPDATED] other post'
#
+ # However, the above applies if the parent model is being updated as well.
+ # For example, If you wanted to create a +member+ named _joe_ and wanted to
+ # update the +posts+ at the same time, that would give an
+ # ActiveRecord::RecordNotFound error.
+ #
# By default the associated records are protected from being destroyed. If
# you want to destroy any of the associated records through the attributes
# hash, you have to enable it first using the <tt>:allow_destroy</tt>
@@ -205,20 +213,20 @@ module ActiveRecord
#
# Passing attributes for an associated collection in the form of a hash
# of hashes can be used with hashes generated from HTTP/HTML parameters,
- # where there maybe no natural way to submit an array of hashes.
+ # where there may be no natural way to submit an array of hashes.
#
# === Saving
#
# All changes to models, including the destruction of those marked for
# destruction, are saved and destroyed automatically and atomically when
# the parent model is saved. This happens inside the transaction initiated
- # by the parents save method. See ActiveRecord::AutosaveAssociation.
+ # by the parent's save method. See ActiveRecord::AutosaveAssociation.
#
# === Validating the presence of a parent model
#
# If you want to validate that a child record is associated with a parent
- # record, you can use <tt>validates_presence_of</tt> and
- # <tt>inverse_of</tt> as this example illustrates:
+ # record, you can use the +validates_presence_of+ method and the +:inverse_of+
+ # key as this example illustrates:
#
# class Member < ActiveRecord::Base
# has_many :posts, inverse_of: :member
@@ -230,7 +238,7 @@ module ActiveRecord
# validates_presence_of :member
# end
#
- # Note that if you do not specify the <tt>inverse_of</tt> option, then
+ # Note that if you do not specify the +:inverse_of+ option, then
# Active Record will try to automatically guess the inverse association
# based on heuristics.
#
@@ -264,29 +272,31 @@ module ActiveRecord
# Allows you to specify a Proc or a Symbol pointing to a method
# that checks whether a record should be built for a certain attribute
# hash. The hash is passed to the supplied Proc or the method
- # and it should return either +true+ or +false+. When no :reject_if
+ # and it should return either +true+ or +false+. When no +:reject_if+
# is specified, a record will be built for all attribute hashes that
# do not have a <tt>_destroy</tt> value that evaluates to true.
# Passing <tt>:all_blank</tt> instead of a Proc will create a proc
# that will reject a record where all the attributes are blank excluding
- # any value for _destroy.
+ # any value for +_destroy+.
# [:limit]
- # Allows you to specify the maximum number of the associated records that
- # can be processed with the nested attributes. Limit also can be specified as a
- # Proc or a Symbol pointing to a method that should return number. If the size of the
- # nested attributes array exceeds the specified limit, NestedAttributes::TooManyRecords
- # exception is raised. If omitted, any number associations can be processed.
- # Note that the :limit option is only applicable to one-to-many associations.
+ # Allows you to specify the maximum number of associated records that
+ # can be processed with the nested attributes. Limit also can be specified
+ # as a Proc or a Symbol pointing to a method that should return a number.
+ # If the size of the nested attributes array exceeds the specified limit,
+ # NestedAttributes::TooManyRecords exception is raised. If omitted, any
+ # number of associations can be processed.
+ # Note that the +:limit+ option is only applicable to one-to-many
+ # associations.
# [:update_only]
# For a one-to-one association, this option allows you to specify how
- # nested attributes are to be used when an associated record already
+ # nested attributes are going to be used when an associated record already
# exists. In general, an existing record may either be updated with the
# new set of attribute values or be replaced by a wholly new record
- # containing those values. By default the :update_only option is +false+
+ # containing those values. By default the +:update_only+ option is +false+
# and the nested attributes are used to update the existing record only
# if they include the record's <tt>:id</tt> value. Otherwise a new
# record will be instantiated and used to replace the existing one.
- # However if the :update_only option is +true+, the nested attributes
+ # However if the +:update_only+ option is +true+, the nested attributes
# are used to update the record's attributes always, regardless of
# whether the <tt>:id</tt> is present. The option is ignored for collection
# associations.
@@ -307,7 +317,7 @@ module ActiveRecord
attr_names.each do |association_name|
if reflection = _reflect_on_association(association_name)
reflection.autosave = true
- add_autosave_association_callbacks(reflection)
+ define_autosave_validation_callbacks(reflection)
nested_attributes_options = self.nested_attributes_options.dup
nested_attributes_options[association_name.to_sym] = options
@@ -376,6 +386,9 @@ module ActiveRecord
# then the existing record will be marked for destruction.
def assign_nested_attributes_for_one_to_one_association(association_name, attributes)
options = self.nested_attributes_options[association_name]
+ if attributes.respond_to?(:permitted?)
+ attributes = attributes.to_h
+ end
attributes = attributes.with_indifferent_access
existing_record = send(association_name)
@@ -432,6 +445,9 @@ module ActiveRecord
# ])
def assign_nested_attributes_for_collection_association(association_name, attributes_collection)
options = self.nested_attributes_options[association_name]
+ if attributes_collection.respond_to?(:permitted?)
+ attributes_collection = attributes_collection.to_h
+ end
unless attributes_collection.is_a?(Hash) || attributes_collection.is_a?(Array)
raise ArgumentError, "Hash or Array expected, got #{attributes_collection.class.name} (#{attributes_collection.inspect})"
@@ -458,6 +474,9 @@ module ActiveRecord
end
attributes_collection.each do |attributes|
+ if attributes.respond_to?(:permitted?)
+ attributes = attributes.to_h
+ end
attributes = attributes.with_indifferent_access
if attributes['id'].blank?
@@ -516,7 +535,7 @@ module ActiveRecord
# Determines if a hash contains a truthy _destroy key.
def has_destroy_flag?(hash)
- Type::Boolean.new.type_cast_from_user(hash['_destroy'])
+ Type::Boolean.new.cast(hash['_destroy'])
end
# Determines if a new record should be rejected by checking
@@ -542,7 +561,9 @@ module ActiveRecord
end
def raise_nested_attributes_record_not_found!(association_name, record_id)
- raise RecordNotFound, "Couldn't find #{self.class._reflect_on_association(association_name).klass.name} with ID=#{record_id} for #{self.class.name} with ID=#{id}"
+ model = self.class._reflect_on_association(association_name).klass.name
+ raise RecordNotFound.new("Couldn't find #{model} with ID=#{record_id} for #{self.class.name} with ID=#{id}",
+ model, 'id', record_id)
end
end
end
diff --git a/activerecord/lib/active_record/no_touching.rb b/activerecord/lib/active_record/no_touching.rb
index dbf4564ae5..edb5066fa0 100644
--- a/activerecord/lib/active_record/no_touching.rb
+++ b/activerecord/lib/active_record/no_touching.rb
@@ -45,7 +45,7 @@ module ActiveRecord
NoTouching.applied_to?(self.class)
end
- def touch(*)
+ def touch(*) # :nodoc:
super unless no_touching?
end
end
diff --git a/activerecord/lib/active_record/null_relation.rb b/activerecord/lib/active_record/null_relation.rb
index 807c301596..0b500346bc 100644
--- a/activerecord/lib/active_record/null_relation.rb
+++ b/activerecord/lib/active_record/null_relation.rb
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-
module ActiveRecord
module NullRelation # :nodoc:
def exec_queries
@@ -14,7 +12,7 @@ module ActiveRecord
0
end
- def update_all(_updates, _conditions = nil, _options = {})
+ def update_all(_updates)
0
end
@@ -30,10 +28,18 @@ module ActiveRecord
true
end
+ def none?
+ true
+ end
+
def any?
false
end
+ def one?
+ false
+ end
+
def many?
false
end
@@ -62,9 +68,7 @@ module ActiveRecord
calculate :maximum, nil
end
- def calculate(operation, _column_name, _options = {})
- # TODO: Remove _options argument as soon we remove support to
- # activerecord-deprecated_finders.
+ def calculate(operation, _column_name)
if [:count, :sum, :size].include? operation
group_values.any? ? Hash.new : 0
elsif [:average, :minimum, :maximum].include?(operation) && group_values.any?
@@ -74,8 +78,12 @@ module ActiveRecord
end
end
- def exists?(_id = false)
+ def exists?(_conditions = :none)
false
end
+
+ def or(other)
+ other.spawn
+ end
end
end
diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb
index 96e44c2f59..94316d5249 100644
--- a/activerecord/lib/active_record/persistence.rb
+++ b/activerecord/lib/active_record/persistence.rb
@@ -1,5 +1,5 @@
module ActiveRecord
- # = Active Record Persistence
+ # = Active Record \Persistence
module Persistence
extend ActiveSupport::Concern
@@ -36,6 +36,23 @@ module ActiveRecord
end
end
+ # Creates an object (or multiple objects) and saves it to the database,
+ # if validations pass. Raises a RecordInvalid error if validations fail,
+ # unlike Base#create.
+ #
+ # The +attributes+ parameter can be either a Hash or an Array of Hashes.
+ # These describe which attributes to be created on the object, or
+ # multiple objects when given an Array of Hashes.
+ def create!(attributes = nil, &block)
+ if attributes.is_a?(Array)
+ attributes.collect { |attr| create!(attr, &block) }
+ else
+ object = new(attributes, &block)
+ object.save!
+ object
+ end
+ end
+
# Given an attributes hash, +instantiate+ returns a new instance of
# the appropriate class. Accepts only keys as strings.
#
@@ -79,7 +96,8 @@ module ActiveRecord
# Returns true if the record is persisted, i.e. it's not a new record and it was
# not destroyed, otherwise returns false.
def persisted?
- !(new_record? || destroyed?)
+ sync_with_transaction_state
+ !(@new_record || @destroyed)
end
# Saves the model.
@@ -88,41 +106,49 @@ module ActiveRecord
# the existing record gets updated.
#
# By default, save always run validations. If any of them fail the action
- # is cancelled and +save+ returns +false+. However, if you supply
+ # is cancelled and #save returns +false+. However, if you supply
# validate: false, validations are bypassed altogether. See
# ActiveRecord::Validations for more information.
#
- # There's a series of callbacks associated with +save+. If any of the
- # <tt>before_*</tt> callbacks return +false+ the action is cancelled and
- # +save+ returns +false+. See ActiveRecord::Callbacks for further
+ # By default, #save also sets the +updated_at+/+updated_on+ attributes to
+ # the current time. However, if you supply <tt>touch: false</tt>, these
+ # timestamps will not be updated.
+ #
+ # There's a series of callbacks associated with #save. If any of the
+ # <tt>before_*</tt> callbacks throws +:abort+ the action is cancelled and
+ # #save returns +false+. See ActiveRecord::Callbacks for further
# details.
#
# Attributes marked as readonly are silently ignored if the record is
# being updated.
- def save(*)
- create_or_update
+ def save(*args)
+ create_or_update(*args)
rescue ActiveRecord::RecordInvalid
false
end
# Saves the model.
#
- # If the model is new a record gets created in the database, otherwise
+ # If the model is new, a record gets created in the database, otherwise
# the existing record gets updated.
#
- # With <tt>save!</tt> validations always run. If any of them fail
+ # With #save! validations always run. If any of them fail
# ActiveRecord::RecordInvalid gets raised. See ActiveRecord::Validations
# for more information.
#
- # There's a series of callbacks associated with <tt>save!</tt>. If any of
- # the <tt>before_*</tt> callbacks return +false+ the action is cancelled
- # and <tt>save!</tt> raises ActiveRecord::RecordNotSaved. See
+ # By default, #save! also sets the +updated_at+/+updated_on+ attributes to
+ # the current time. However, if you supply <tt>touch: false</tt>, these
+ # timestamps will not be updated.
+ #
+ # There's a series of callbacks associated with #save!. If any of
+ # the <tt>before_*</tt> callbacks throws +:abort+ the action is cancelled
+ # and #save! raises ActiveRecord::RecordNotSaved. See
# ActiveRecord::Callbacks for further details.
#
# Attributes marked as readonly are silently ignored if the record is
# being updated.
- def save!(*)
- create_or_update || raise(RecordNotSaved)
+ def save!(*args)
+ create_or_update(*args) || raise(RecordNotSaved.new("Failed to save the record", self))
end
# Deletes the record in the database and freezes this instance to
@@ -132,6 +158,8 @@ module ActiveRecord
# The row is simply removed with an SQL +DELETE+ statement on the
# record's primary key, and no callbacks are executed.
#
+ # Note that this will also delete records marked as {#readonly?}[rdoc-ref:Core#readonly?].
+ #
# To enforce the object's +before_destroy+ and +after_destroy+
# callbacks or any <tt>:dependent</tt> association
# options, use <tt>#destroy</tt>.
@@ -144,13 +172,14 @@ module ActiveRecord
# Deletes the record in the database and freezes this instance to reflect
# that no changes should be made (since they can't be persisted).
#
- # There's a series of callbacks associated with <tt>destroy</tt>. If
- # the <tt>before_destroy</tt> callback return +false+ the action is cancelled
- # and <tt>destroy</tt> returns +false+. See
- # ActiveRecord::Callbacks for further details.
+ # There's a series of callbacks associated with #destroy. If the
+ # <tt>before_destroy</tt> callback throws +:abort+ the action is cancelled
+ # and #destroy returns +false+.
+ # See ActiveRecord::Callbacks for further details.
def destroy
- raise ReadOnlyRecord if readonly?
+ raise ReadOnlyRecord, "#{self.class} is marked as readonly" if readonly?
destroy_associations
+ self.class.connection.add_transaction_record(self)
destroy_row if persisted?
@destroyed = true
freeze
@@ -159,12 +188,12 @@ module ActiveRecord
# Deletes the record in the database and freezes this instance to reflect
# that no changes should be made (since they can't be persisted).
#
- # There's a series of callbacks associated with <tt>destroy!</tt>. If
- # the <tt>before_destroy</tt> callback return +false+ the action is cancelled
- # and <tt>destroy!</tt> raises ActiveRecord::RecordNotDestroyed. See
- # ActiveRecord::Callbacks for further details.
+ # There's a series of callbacks associated with #destroy!. If the
+ # <tt>before_destroy</tt> callback throws +:abort+ the action is cancelled
+ # and #destroy! raises ActiveRecord::RecordNotDestroyed.
+ # See ActiveRecord::Callbacks for further details.
def destroy!
- destroy || raise(ActiveRecord::RecordNotDestroyed)
+ destroy || _raise_record_not_destroyed
end
# Returns an instance of the specified +klass+ with the attributes of the
@@ -176,18 +205,21 @@ module ActiveRecord
# instance using the companies/company partial instead of clients/client.
#
# Note: The new instance will share a link to the same attributes as the original class.
- # So any change to the attributes in either instance will affect the other.
+ # Therefore the sti column value will still be the same.
+ # 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.instance_variable_set("@attributes", @attributes)
- became.instance_variable_set("@changed_attributes", @changed_attributes) if defined?(@changed_attributes)
+ became.instance_variable_set("@mutation_tracker", @mutation_tracker) if defined?(@mutation_tracker)
+ became.instance_variable_set("@changed_attributes", attributes_changed_by_setter)
became.instance_variable_set("@new_record", new_record?)
became.instance_variable_set("@destroyed", destroyed?)
became.instance_variable_set("@errors", errors)
became
end
- # Wrapper around +becomes+ that also changes the instance's sti column value.
+ # Wrapper around #becomes that also changes the instance's sti column value.
# This is especially useful if you want to persist the changed class in your
# database.
#
@@ -207,19 +239,19 @@ module ActiveRecord
# This is especially useful for boolean flags on existing records. Also note that
#
# * Validation is skipped.
- # * Callbacks are invoked.
+ # * \Callbacks are invoked.
# * updated_at/updated_on column is updated if that column is available.
# * Updates all the attributes that are dirty in this object.
#
- # This method raises an +ActiveRecord::ActiveRecordError+ if the
+ # This method raises an ActiveRecord::ActiveRecordError if the
# attribute is marked as readonly.
#
- # See also +update_column+.
+ # See also #update_column.
def update_attribute(name, value)
name = name.to_s
verify_readonly_attribute(name)
- send("#{name}=", value)
- save(validate: false)
+ public_send("#{name}=", value)
+ save(validate: false) if changed?
end
# Updates the attributes of the model from the passed-in hash and saves the
@@ -236,7 +268,7 @@ module ActiveRecord
alias update_attributes update
- # Updates its receiver just like +update+ but calls <tt>save!</tt> instead
+ # Updates its receiver just like #update but calls #save! instead
# of +save+, so an exception is raised if the record is invalid.
def update!(attributes)
# The following transaction covers any possible database side-effects of the
@@ -263,14 +295,15 @@ module ActiveRecord
# the database, but take into account that in consequence the regular update
# procedures are totally bypassed. In particular:
#
- # * Validations are skipped.
- # * Callbacks are skipped.
+ # * \Validations are skipped.
+ # * \Callbacks are skipped.
# * +updated_at+/+updated_on+ are not updated.
#
- # This method raises an +ActiveRecord::ActiveRecordError+ when called on new
+ # This method raises an ActiveRecord::ActiveRecordError when called on new
# objects, or when at least one of the attributes is marked as readonly.
def update_columns(attributes)
- raise ActiveRecordError, "cannot update on a new record object" unless persisted?
+ raise ActiveRecordError, "cannot update a new record" if new_record?
+ raise ActiveRecordError, "cannot update a destroyed record" if destroyed?
attributes.each_key do |key|
verify_readonly_attribute(key.to_s)
@@ -294,29 +327,31 @@ module ActiveRecord
self
end
- # Wrapper around +increment+ that saves the record. This method differs from
+ # Wrapper around #increment that saves the record. This method differs from
# its non-bang version in that it passes through the attribute setter.
# Saving is not subjected to validation checks. Returns +true+ if the
# record could be saved.
def increment!(attribute, by = 1)
- increment(attribute, by).update_attribute(attribute, self[attribute])
+ increment(attribute, by)
+ change = public_send(attribute) - (attribute_was(attribute.to_s) || 0)
+ self.class.update_counters(id, attribute => change)
+ clear_attribute_change(attribute) # eww
+ self
end
# Initializes +attribute+ to zero if +nil+ and subtracts the value passed as +by+ (default is 1).
# The decrement is performed directly on the underlying attribute, no setter is invoked.
# Only makes sense for number-based attributes. Returns +self+.
def decrement(attribute, by = 1)
- self[attribute] ||= 0
- self[attribute] -= by
- self
+ increment(attribute, -by)
end
- # Wrapper around +decrement+ that saves the record. This method differs from
+ # Wrapper around #decrement that saves the record. This method differs from
# its non-bang version in that it passes through the attribute setter.
# Saving is not subjected to validation checks. Returns +true+ if the
# record could be saved.
def decrement!(attribute, by = 1)
- decrement(attribute, by).update_attribute(attribute, self[attribute])
+ increment!(attribute, -by)
end
# Assigns to +attribute+ the boolean opposite of <tt>attribute?</tt>. So
@@ -324,11 +359,11 @@ module ActiveRecord
# method toggles directly the underlying value without calling any setter.
# Returns +self+.
def toggle(attribute)
- self[attribute] = !send("#{attribute}?")
+ self[attribute] = !public_send("#{attribute}?")
self
end
- # Wrapper around +toggle+ that saves the record. This method differs from
+ # Wrapper around #toggle that saves the record. This method differs from
# its non-bang version in that it passes through the attribute setter.
# Saving is not subjected to validation checks. Returns +true+ if the
# record could be saved.
@@ -349,9 +384,9 @@ module ActiveRecord
# # => #<Account id: 1, email: 'account@example.com'>
#
# Attributes are reloaded from the database, and caches busted, in
- # particular the associations cache.
+ # particular the associations cache and the QueryCache.
#
- # If the record no longer exists in the database <tt>ActiveRecord::RecordNotFound</tt>
+ # If the record no longer exists in the database ActiveRecord::RecordNotFound
# is raised. Otherwise, in addition to the in-place modification the method
# returns +self+ for convenience.
#
@@ -385,8 +420,7 @@ module ActiveRecord
# end
#
def reload(options = nil)
- clear_aggregation_cache
- clear_association_cache
+ self.class.connection.clear_query_cache
fresh_object =
if options && options[:lock]
@@ -400,19 +434,22 @@ module ActiveRecord
self
end
- # Saves the record with the updated_at/on attributes set to the current time.
+ # Saves the record with the updated_at/on attributes set to the current time
+ # or the time specified.
# Please note that no validation is performed and only the +after_touch+,
# +after_commit+ and +after_rollback+ callbacks are executed.
#
+ # 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.
+ # attributes. If no time argument is passed, the current time is used as default.
#
- # product.touch # updates updated_at/on
+ # product.touch # updates updated_at/on with current time
+ # product.touch(time: Time.new(2015, 2, 16, 0, 0, 0)) # updates updated_at/on with specified time
# product.touch(:designed_at) # updates the designed_at attribute and updated_at/on
# product.touch(:started_at, :ended_at) # updates started_at, ended_at and updated_at/on attributes
#
- # If used along with +belongs_to+ then +touch+ will invoke +touch+ method on
- # associated object.
+ # If used along with {belongs_to}[rdoc-ref:Associations::ClassMethods#belongs_to]
+ # then +touch+ will invoke +touch+ method on associated object.
#
# class Brake < ActiveRecord::Base
# belongs_to :car, touch: true
@@ -431,26 +468,38 @@ module ActiveRecord
# ball = Ball.new
# ball.touch(:updated_at) # => raises ActiveRecordError
#
- def touch(*names)
+ def touch(*names, time: nil)
raise ActiveRecordError, "cannot touch on a new record object" unless persisted?
+ time ||= current_time_from_proper_timezone
attributes = timestamp_attributes_for_update_in_model
attributes.concat(names)
unless attributes.empty?
- current_time = current_time_from_proper_timezone
changes = {}
attributes.each do |column|
column = column.to_s
- changes[column] = write_attribute(column, current_time)
+ changes[column] = write_attribute(column, time)
end
- changes[self.class.locking_column] = increment_lock if locking_enabled?
-
- changed_attributes.except!(*changes.keys)
+ clear_attribute_changes(changes.keys)
primary_key = self.class.primary_key
- self.class.unscoped.where(primary_key => self[primary_key]).update_all(changes) == 1
+ scope = self.class.unscoped.where(primary_key => _read_attribute(primary_key))
+
+ if locking_enabled?
+ locking_column = self.class.locking_column
+ scope = scope.where(locking_column => _read_attribute(locking_column))
+ changes[locking_column] = increment_lock
+ end
+
+ result = scope.update_all(changes) == 1
+
+ if !result && locking_enabled?
+ raise ActiveRecord::StaleObjectError.new(self, "touch")
+ end
+
+ result
else
true
end
@@ -467,20 +516,12 @@ module ActiveRecord
end
def relation_for_destroy
- pk = self.class.primary_key
- column = self.class.columns_hash[pk]
- substitute = self.class.connection.substitute_at(column, 0)
-
- relation = self.class.unscoped.where(
- self.class.arel_table[pk].eq(substitute))
-
- relation.bind_values = [[column, id]]
- relation
+ self.class.unscoped.where(self.class.primary_key => id)
end
- def create_or_update
- raise ReadOnlyRecord if readonly?
- result = new_record? ? _create_record : _update_record
+ def create_or_update(*args)
+ raise ReadOnlyRecord, "#{self.class} is marked as readonly" if readonly?
+ result = new_record? ? _create_record : _update_record(*args)
result != false
end
@@ -510,5 +551,12 @@ module ActiveRecord
def verify_readonly_attribute(name)
raise ActiveRecordError, "#{name} is marked as readonly" if self.class.readonly_attributes.include?(name)
end
+
+ def _raise_record_not_destroyed
+ @_association_destroy_exception ||= nil
+ raise @_association_destroy_exception || RecordNotDestroyed.new("Failed to destroy the record", self)
+ ensure
+ @_association_destroy_exception = nil
+ end
end
end
diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb
index a9ddd9141f..87a1988f2f 100644
--- a/activerecord/lib/active_record/querying.rb
+++ b/activerecord/lib/active_record/querying.rb
@@ -6,8 +6,8 @@ module ActiveRecord
delegate :find_or_create_by, :find_or_create_by!, :find_or_initialize_by, to: :all
delegate :find_by, :find_by!, to: :all
delegate :destroy, :destroy_all, :delete, :delete_all, :update, :update_all, to: :all
- delegate :find_each, :find_in_batches, to: :all
- delegate :select, :group, :order, :except, :reorder, :limit, :offset, :joins,
+ delegate :find_each, :find_in_batches, :in_batches, to: :all
+ delegate :select, :group, :order, :except, :reorder, :limit, :offset, :joins, :or,
:where, :rewhere, :preload, :eager_load, :includes, :from, :lock, :readonly,
:having, :create_with, :uniq, :distinct, :references, :none, :unscope, to: :all
delegate :count, :average, :minimum, :maximum, :sum, :calculate, to: :all
@@ -37,19 +37,30 @@ module ActiveRecord
# Post.find_by_sql ["SELECT body FROM comments WHERE author = :user_id OR approved_by = :user_id", { :user_id => user_id }]
def find_by_sql(sql, binds = [])
result_set = connection.select_all(sanitize_sql(sql), "#{name} Load", binds)
- column_types = result_set.column_types.except(*columns_hash.keys)
- result_set.map { |record| instantiate(record, column_types) }
+ column_types = result_set.column_types.dup
+ columns_hash.each_key { |k| column_types.delete k }
+ message_bus = ActiveSupport::Notifications.instrumenter
+
+ payload = {
+ record_count: result_set.length,
+ class_name: name
+ }
+
+ message_bus.instrument('instantiation.active_record', payload) do
+ result_set.map { |record| instantiate(record, column_types) }
+ end
end
# Returns the result of an SQL statement that should only include a COUNT(*) in the SELECT part.
# The use of this method should be restricted to complicated SQL queries that can't be executed
# using the ActiveRecord::Calculations class methods. Look into those before using this.
#
- # ==== Parameters
+ # Product.count_by_sql "SELECT COUNT(*) FROM sales s, customers c WHERE s.customer_id = c.id"
+ # # => 12
#
- # * +sql+ - An SQL statement which should return a count query from the database, see the example below.
+ # ==== Parameters
#
- # Product.count_by_sql "SELECT COUNT(*) FROM sales s, customers c WHERE s.customer_id = c.id"
+ # * +sql+ - An SQL statement which should return a count query from the database, see the example above.
def count_by_sql(sql)
sql = sanitize_conditions(sql)
connection.select_value(sql, "#{name} Count").to_i
diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb
index a4ceacbf44..2744673c12 100644
--- a/activerecord/lib/active_record/railtie.rb
+++ b/activerecord/lib/active_record/railtie.rb
@@ -16,11 +16,11 @@ module ActiveRecord
config.app_generators.orm :active_record, :migration => true,
:timestamps => true
- config.app_middleware.insert_after "::ActionDispatch::Callbacks",
- "ActiveRecord::QueryCache"
+ config.app_middleware.insert_after ::ActionDispatch::Callbacks,
+ ActiveRecord::QueryCache
- config.app_middleware.insert_after "::ActionDispatch::Callbacks",
- "ActiveRecord::ConnectionAdapters::ConnectionManagement"
+ config.app_middleware.insert_after ::ActionDispatch::Callbacks,
+ ActiveRecord::ConnectionAdapters::ConnectionManagement
config.action_dispatch.rescue_responses.merge!(
'ActiveRecord::RecordNotFound' => :not_found,
@@ -36,8 +36,6 @@ module ActiveRecord
config.eager_load_namespaces << ActiveRecord
rake_tasks do
- require "active_record/base"
-
namespace :db do
task :load_config do
ActiveRecord::Tasks::DatabaseTasks.database_configuration = Rails.application.config.database_configuration
@@ -80,8 +78,8 @@ module ActiveRecord
initializer "active_record.migration_error" do
if config.active_record.delete(:migration_error) == :page_load
- config.app_middleware.insert_after "::ActionDispatch::Callbacks",
- "ActiveRecord::Migration::CheckPending"
+ config.app_middleware.insert_after ::ActionDispatch::Callbacks,
+ ActiveRecord::Migration::CheckPending
end
end
@@ -95,6 +93,7 @@ module ActiveRecord
cache = Marshal.load File.binread filename
if cache.version == ActiveRecord::Migrator.current_version
self.connection.schema_cache = cache
+ self.connection_pool.schema_cache = cache.dup
else
warn "Ignoring db/schema_cache.dump because it has expired. The current schema version is #{ActiveRecord::Migrator.current_version}, but the one in the cache is #{cache.version}."
end
@@ -104,6 +103,14 @@ module ActiveRecord
end
end
+ 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 'active_record/relation/record_fetch_warning'
+ end
+ end
+ end
+
initializer "active_record.set_configs" do |app|
ActiveSupport.on_load(:active_record) do
app.config.active_record.each do |k,v|
@@ -114,7 +121,7 @@ module ActiveRecord
# This sets the database configuration from Configuration#database_configuration
# and then establishes the connection.
- initializer "active_record.initialize_database" do |app|
+ initializer "active_record.initialize_database" do
ActiveSupport.on_load(:active_record) do
self.configurations = Rails.application.config.database_configuration
@@ -149,8 +156,8 @@ end_warning
ActiveSupport.on_load(:active_record) do
ActionDispatch::Reloader.send(hook) do
if ActiveRecord::Base.connected?
- ActiveRecord::Base.clear_reloadable_connections!
ActiveRecord::Base.clear_cache!
+ ActiveRecord::Base.clear_reloadable_connections!
end
end
end
diff --git a/activerecord/lib/active_record/railties/controller_runtime.rb b/activerecord/lib/active_record/railties/controller_runtime.rb
index af4840476c..8727e46cb3 100644
--- a/activerecord/lib/active_record/railties/controller_runtime.rb
+++ b/activerecord/lib/active_record/railties/controller_runtime.rb
@@ -19,7 +19,7 @@ module ActiveRecord
end
def cleanup_view_runtime
- if ActiveRecord::Base.connected?
+ if 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
diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake
index fa94df7a52..b6f3695856 100644
--- a/activerecord/lib/active_record/railties/databases.rake
+++ b/activerecord/lib/active_record/railties/databases.rake
@@ -12,7 +12,7 @@ db_namespace = namespace :db do
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 it defaults to creating the development and test databases.'
+ 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, it defaults to creating the development and test databases.'
task :create => [:load_config] do
ActiveRecord::Tasks::DatabaseTasks.create_current
end
@@ -23,7 +23,7 @@ db_namespace = namespace :db do
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 it defaults to dropping the development and test databases.'
+ 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, it defaults to dropping the development and test databases.'
task :drop => [:load_config] do
ActiveRecord::Tasks::DatabaseTasks.drop_current
end
@@ -34,26 +34,26 @@ db_namespace = namespace :db do
end
end
- # desc "Empty 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 it defaults to purging the development and test databases."
+ # desc "Empty the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:purge:all to purge all databases in the config). Without RAILS_ENV it defaults to purging the development and test databases."
task :purge => [:load_config] do
ActiveRecord::Tasks::DatabaseTasks.purge_current
end
desc "Migrate the database (options: VERSION=x, VERBOSE=false, SCOPE=blog)."
task :migrate => [:environment, :load_config] do
- ActiveRecord::Migration.verbose = ENV["VERBOSE"] ? ENV["VERBOSE"] == "true" : true
- ActiveRecord::Migrator.migrate(ActiveRecord::Migrator.migrations_paths, ENV["VERSION"] ? ENV["VERSION"].to_i : nil) do |migration|
- ENV["SCOPE"].blank? || (ENV["SCOPE"] == migration.scope)
- end
- db_namespace['_dump'].invoke if ActiveRecord::Base.dump_schema_after_migration
+ ActiveRecord::Tasks::DatabaseTasks.migrate
+ db_namespace['_dump'].invoke
end
+ # IMPORTANT: This task won't dump the schema if ActiveRecord::Base.dump_schema_after_migration is set to false
task :_dump do
- case ActiveRecord::Base.schema_format
- when :ruby then db_namespace["schema:dump"].invoke
- when :sql then db_namespace["structure:dump"].invoke
- else
- raise "unknown schema format #{ActiveRecord::Base.schema_format}"
+ if ActiveRecord::Base.dump_schema_after_migration
+ case ActiveRecord::Base.schema_format
+ when :ruby then db_namespace["schema:dump"].invoke
+ when :sql then db_namespace["structure:dump"].invoke
+ else
+ raise "unknown schema format #{ActiveRecord::Base.schema_format}"
+ end
end
# Allow this task to be called as many times as required. An example is the
# migrate:redo task, which calls other two internally that depend on this one.
@@ -79,7 +79,7 @@ db_namespace = namespace :db do
task :up => [:environment, :load_config] do
version = ENV['VERSION'] ? ENV['VERSION'].to_i : nil
raise 'VERSION is required' unless version
- ActiveRecord::Migrator.run(:up, ActiveRecord::Migrator.migrations_paths, version)
+ ActiveRecord::Migrator.run(:up, ActiveRecord::Tasks::DatabaseTasks.migrations_paths, version)
db_namespace['_dump'].invoke
end
@@ -87,7 +87,7 @@ db_namespace = namespace :db do
task :down => [:environment, :load_config] do
version = ENV['VERSION'] ? ENV['VERSION'].to_i : nil
raise 'VERSION is required - To go down one migration, run db:rollback' unless version
- ActiveRecord::Migrator.run(:down, ActiveRecord::Migrator.migrations_paths, version)
+ ActiveRecord::Migrator.run(:down, ActiveRecord::Tasks::DatabaseTasks.migrations_paths, version)
db_namespace['_dump'].invoke
end
@@ -99,7 +99,7 @@ db_namespace = namespace :db do
db_list = ActiveRecord::SchemaMigration.normalized_versions
file_list =
- ActiveRecord::Migrator.migrations_paths.flat_map do |path|
+ ActiveRecord::Tasks::DatabaseTasks.migrations_paths.flat_map do |path|
# match "20091231235959_some_name.rb" and "001_some_name.rb" pattern
Dir.foreach(path).grep(/^(\d{3,})_(.+)\.rb$/) do
version = ActiveRecord::SchemaMigration.normalize_migration_number($1)
@@ -125,22 +125,19 @@ db_namespace = namespace :db do
desc 'Rolls the schema back to the previous version (specify steps w/ STEP=n).'
task :rollback => [:environment, :load_config] do
step = ENV['STEP'] ? ENV['STEP'].to_i : 1
- ActiveRecord::Migrator.rollback(ActiveRecord::Migrator.migrations_paths, step)
+ ActiveRecord::Migrator.rollback(ActiveRecord::Tasks::DatabaseTasks.migrations_paths, 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
step = ENV['STEP'] ? ENV['STEP'].to_i : 1
- ActiveRecord::Migrator.forward(ActiveRecord::Migrator.migrations_paths, step)
+ ActiveRecord::Migrator.forward(ActiveRecord::Tasks::DatabaseTasks.migrations_paths, step)
db_namespace['_dump'].invoke
end
# desc 'Drops and recreates the database from db/schema.rb for the current environment and loads the seeds.'
- task :reset => [:environment, :load_config] do
- db_namespace["drop"].invoke
- db_namespace["setup"].invoke
- end
+ task :reset => [ 'db:drop', 'db:setup' ]
# desc "Retrieves the charset for the current environment's database"
task :charset => [:environment, :load_config] do
@@ -162,8 +159,8 @@ db_namespace = namespace :db do
end
# desc "Raises an error if there are pending migrations"
- task :abort_if_pending_migrations => :environment do
- pending_migrations = ActiveRecord::Migrator.open(ActiveRecord::Migrator.migrations_paths).pending_migrations
+ task :abort_if_pending_migrations => [:environment, :load_config] do
+ pending_migrations = ActiveRecord::Migrator.open(ActiveRecord::Tasks::DatabaseTasks.migrations_paths).pending_migrations
if pending_migrations.any?
puts "You have #{pending_migrations.size} pending #{pending_migrations.size > 1 ? 'migrations:' : 'migration:'}"
@@ -174,17 +171,17 @@ db_namespace = namespace :db do
end
end
- desc 'Create the database, load the schema, and initialize with the seed data (use db:reset to also drop the database first)'
+ desc 'Creates the database, loads the schema, and initializes with the seed data (use db:reset to also drop the database first)'
task :setup => ['db:schema:load_if_ruby', 'db:structure:load_if_sql', :seed]
- desc 'Load the seed data from db/seeds.rb'
+ desc 'Loads the seed data from db/seeds.rb'
task :seed do
db_namespace['abort_if_pending_migrations'].invoke
ActiveRecord::Tasks::DatabaseTasks.load_seed
end
namespace :fixtures do
- desc "Load 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."
+ 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 'active_record/fixtures'
@@ -199,7 +196,8 @@ db_namespace = namespace :db do
fixture_files = if ENV['FIXTURES']
ENV['FIXTURES'].split(',')
else
- Pathname.glob("#{fixtures_dir}/**/*.yml").map {|f| f.basename.sub_ext('').to_s }
+ # The use of String#[] here is to support namespaced fixtures
+ Dir["#{fixtures_dir}/**/*.yml"].map {|f| f[(fixtures_dir.size + 1)..-5] }
end
ActiveRecord::FixtureSet.create_fixtures(fixtures_dir, fixture_files)
@@ -218,7 +216,7 @@ db_namespace = namespace :db do
Dir["#{base_dir}/**/*.yml"].each do |file|
if data = YAML::load(ERB.new(IO.read(file)).result)
- data.keys.each do |key|
+ data.each_key do |key|
key_id = ActiveRecord::FixtureSet.identify(key)
if key == label || key_id == id.to_i
@@ -231,7 +229,7 @@ db_namespace = namespace :db do
end
namespace :schema do
- desc 'Create a db/schema.rb file that is portable against any DB supported by AR'
+ desc 'Creates a db/schema.rb file that is portable against any DB supported by Active Record'
task :dump => [:environment, :load_config] do
require 'active_record/schema_dumper'
filename = ENV['SCHEMA'] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, 'schema.rb')
@@ -241,9 +239,9 @@ db_namespace = namespace :db do
db_namespace['schema:dump'].reenable
end
- desc 'Load a schema.rb file into the database'
+ desc 'Loads a schema.rb file into the database'
task :load => [:environment, :load_config] do
- ActiveRecord::Tasks::DatabaseTasks.load_schema(:ruby, ENV['SCHEMA'])
+ ActiveRecord::Tasks::DatabaseTasks.load_schema_current(:ruby, ENV['SCHEMA'])
end
task :load_if_ruby => ['db:create', :environment] do
@@ -251,17 +249,17 @@ db_namespace = namespace :db do
end
namespace :cache do
- desc 'Create a db/schema_cache.dump file.'
+ desc 'Creates a db/schema_cache.dump file.'
task :dump => [:environment, :load_config] do
con = ActiveRecord::Base.connection
filename = File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "schema_cache.dump")
con.schema_cache.clear!
- con.tables.each { |table| con.schema_cache.add(table) }
+ con.data_sources.each { |table| con.schema_cache.add(table) }
open(filename, 'wb') { |f| f.write(Marshal.dump(con.schema_cache)) }
end
- desc 'Clear a db/schema_cache.dump file.'
+ desc 'Clears a db/schema_cache.dump file.'
task :clear => [:environment, :load_config] do
filename = File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "schema_cache.dump")
FileUtils.rm(filename) if File.exist?(filename)
@@ -271,9 +269,9 @@ db_namespace = namespace :db do
end
namespace :structure do
- desc 'Dump the database structure to db/structure.sql. Specify another file with DB_STRUCTURE=db/my_structure.sql'
+ 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['DB_STRUCTURE'] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "structure.sql")
+ 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)
@@ -287,9 +285,9 @@ db_namespace = namespace :db do
db_namespace['structure:dump'].reenable
end
- desc "Recreate the databases from the structure.sql file"
- task :load => [:environment, :load_config] do
- ActiveRecord::Tasks::DatabaseTasks.load_schema(:sql, ENV['DB_STRUCTURE'])
+ desc "Recreates the databases from the structure.sql file"
+ task :load => [:load_config] do
+ ActiveRecord::Tasks::DatabaseTasks.load_schema_current(:sql, ENV['SCHEMA'])
end
task :load_if_sql => ['db:create', :environment] do
@@ -307,7 +305,7 @@ db_namespace = namespace :db do
end
# desc "Recreate the test database from the current schema"
- task :load => %w(db:test:deprecated db:test:purge) do
+ task :load => %w(db:test:purge) do
case ActiveRecord::Base.schema_format
when :ruby
db_namespace["test:load_schema"].invoke
@@ -317,12 +315,11 @@ db_namespace = namespace :db do
end
# desc "Recreate the test database from an existent schema.rb file"
- task :load_schema => %w(db:test:deprecated db:test:purge) do
+ task :load_schema => %w(db:test:purge) do
begin
should_reconnect = ActiveRecord::Base.connection_pool.active_connection?
- ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations['test'])
ActiveRecord::Schema.verbose = false
- db_namespace["schema:load"].invoke
+ ActiveRecord::Tasks::DatabaseTasks.load_schema ActiveRecord::Base.configurations['test'], :ruby, ENV['SCHEMA']
ensure
if should_reconnect
ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations[ActiveRecord::Tasks::DatabaseTasks.env])
@@ -331,13 +328,8 @@ db_namespace = namespace :db do
end
# desc "Recreate the test database from an existent structure.sql file"
- task :load_structure => %w(db:test:deprecated db:test:purge) do
- begin
- ActiveRecord::Tasks::DatabaseTasks.current_config(:config => ActiveRecord::Base.configurations['test'])
- db_namespace["structure:load"].invoke
- ensure
- ActiveRecord::Tasks::DatabaseTasks.current_config(:config => nil)
- end
+ task :load_structure => %w(db:test:purge) do
+ ActiveRecord::Tasks::DatabaseTasks.load_schema ActiveRecord::Base.configurations['test'], :sql, ENV['SCHEMA']
end
# desc "Recreate the test database from a fresh schema"
@@ -357,12 +349,12 @@ db_namespace = namespace :db do
task :clone_structure => %w(db:test:deprecated db:structure:dump db:test:load_structure)
# desc "Empty the test database"
- task :purge => %w(db:test:deprecated environment load_config) do
+ task :purge => %w(environment load_config) do
ActiveRecord::Tasks::DatabaseTasks.purge ActiveRecord::Base.configurations['test']
end
- # desc 'Check for pending migrations and load the test schema'
- task :prepare => %w(db:test:deprecated environment load_config) do
+ # desc 'Load the test schema'
+ task :prepare => %w(environment load_config) do
unless ActiveRecord::Base.configurations.blank?
db_namespace['test:load'].invoke
end
@@ -374,7 +366,7 @@ namespace :railties do
namespace :install do
# desc "Copies missing migrations from Railties (e.g. engines). You can specify Railties to use with FROM=railtie1,railtie2"
task :migrations => :'db:load_config' do
- to_load = ENV['FROM'].blank? ? :all : ENV['FROM'].split(",").map {|n| n.strip }
+ to_load = ENV['FROM'].blank? ? :all : ENV['FROM'].split(",").map(&:strip)
railties = {}
Rails.application.migration_railties.each do |railtie|
next unless to_load == :all || to_load.include?(railtie.railtie_name)
@@ -392,7 +384,7 @@ namespace :railties do
puts "Copied migration #{migration.basename} from #{name}"
end
- ActiveRecord::Migration.copy(ActiveRecord::Migrator.migrations_paths.first, railties,
+ ActiveRecord::Migration.copy(ActiveRecord::Tasks::DatabaseTasks.migrations_paths.first, railties,
:on_skip => on_skip, :on_copy => on_copy)
end
end
diff --git a/activerecord/lib/active_record/readonly_attributes.rb b/activerecord/lib/active_record/readonly_attributes.rb
index 85bbac43e4..ce78f1756d 100644
--- a/activerecord/lib/active_record/readonly_attributes.rb
+++ b/activerecord/lib/active_record/readonly_attributes.rb
@@ -11,7 +11,7 @@ module ActiveRecord
# Attributes listed as readonly will be used to create a new record but update operations will
# ignore these fields.
def attr_readonly(*attributes)
- self._attr_readonly = Set.new(attributes.map { |a| a.to_s }) + (self._attr_readonly || [])
+ self._attr_readonly = Set.new(attributes.map(&:to_s)) + (self._attr_readonly || [])
end
# Returns an array of all the attributes that have been specified as readonly.
diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb
index 1672128aa3..5b9d45d871 100644
--- a/activerecord/lib/active_record/reflection.rb
+++ b/activerecord/lib/active_record/reflection.rb
@@ -1,4 +1,5 @@
require 'thread'
+require 'active_support/core_ext/string/filters'
module ActiveRecord
# = Active Record Reflection
@@ -31,6 +32,7 @@ module ActiveRecord
end
def self.add_reflection(ar, name, reflection)
+ ar.clear_reflections_cache
ar._reflections = ar._reflections.merge(name.to_s => reflection)
end
@@ -38,9 +40,9 @@ module ActiveRecord
ar.aggregate_reflections = ar.aggregate_reflections.merge(name.to_s => reflection)
end
- # \Reflection enables to interrogate Active Record classes and objects
- # about their associations and aggregations. This information can,
- # for example, be used in a form builder that takes an Active Record object
+ # \Reflection enables the ability to examine the associations and aggregations of
+ # Active Record classes and objects. This information, for example,
+ # can be used in a form builder that takes an Active Record object
# and creates input fields for all of the attributes depending on their type
# and displays the associations to other objects.
#
@@ -60,22 +62,27 @@ module ActiveRecord
aggregate_reflections[aggregation.to_s]
end
- # Returns a Hash of name of the reflection as the key and a AssociationReflection as the value.
+ # Returns a Hash of name of the reflection as the key and an AssociationReflection as the value.
#
- # Account.reflections # => {balance: AggregateReflection}
+ # Account.reflections # => {"balance" => AggregateReflection}
#
- # @api public
def reflections
- ref = {}
- _reflections.each do |name, reflection|
- parent_name, parent_reflection = reflection.parent_reflection
- if parent_name
- ref[parent_name] = parent_reflection
- else
- ref[name] = reflection
+ @__reflections ||= begin
+ ref = {}
+
+ _reflections.each do |name, reflection|
+ parent_reflection = reflection.parent_reflection
+
+ if parent_reflection
+ parent_name = parent_reflection.name
+ ref[parent_name.to_s] = parent_reflection
+ else
+ ref[name] = reflection
+ end
end
+
+ ref
end
- ref
end
# Returns an array of AssociationReflection objects for all the
@@ -88,10 +95,10 @@ module ActiveRecord
# Account.reflect_on_all_associations # returns an array of all associations
# Account.reflect_on_all_associations(:has_many) # returns an array of all has_many associations
#
- # @api public
def reflect_on_all_associations(macro = nil)
association_reflections = reflections.values
- macro ? association_reflections.select { |reflection| reflection.macro == macro } : association_reflections
+ association_reflections.select! { |reflection| reflection.macro == macro } if macro
+ association_reflections
end
# Returns the AssociationReflection object for the +association+ (use the symbol).
@@ -99,22 +106,22 @@ module ActiveRecord
# Account.reflect_on_association(:owner) # returns the owner AssociationReflection
# Invoice.reflect_on_association(:line_items).macro # returns :has_many
#
- # @api public
def reflect_on_association(association)
reflections[association.to_s]
end
- # @api private
def _reflect_on_association(association) #:nodoc:
_reflections[association.to_s]
end
# Returns an array of AssociationReflection objects for all associations which have <tt>:autosave</tt> enabled.
- #
- # @api public
def reflect_on_all_autosave_associations
reflections.values.select { |reflection| reflection.options[:autosave] }
end
+
+ def clear_reflections_cache # :nodoc:
+ @__reflections = nil
+ end
end
# Holds all the methods that are shared between MacroReflection, AssociationReflection
@@ -148,27 +155,87 @@ module ActiveRecord
JoinKeys = Struct.new(:key, :foreign_key) # :nodoc:
- def join_keys(assoc_klass)
- if source_macro == :belongs_to
- if polymorphic?
- reflection_key = association_primary_key(assoc_klass)
- else
- reflection_key = association_primary_key
+ def join_keys(association_klass)
+ JoinKeys.new(foreign_key, active_record_primary_key)
+ end
+
+ def constraints
+ scope_chain.flatten
+ end
+
+ def counter_cache_column
+ if belongs_to?
+ if options[:counter_cache] == true
+ "#{active_record.name.demodulize.underscore.pluralize}_count"
+ elsif options[:counter_cache]
+ options[:counter_cache].to_s
end
- reflection_foreign_key = foreign_key
else
- reflection_foreign_key = active_record_primary_key
- reflection_key = foreign_key
+ options[:counter_cache] ? options[:counter_cache].to_s : "#{name}_count"
+ end
+ end
+
+ def inverse_of
+ return unless inverse_name
+
+ @inverse_of ||= klass._reflect_on_association inverse_name
+ end
+
+ def check_validity_of_inverse!
+ unless polymorphic?
+ if has_inverse? && inverse_of.nil?
+ raise InverseOfAssociationNotFoundError.new(self)
+ end
end
- JoinKeys.new(reflection_key, reflection_foreign_key)
+ end
+
+ # This shit is nasty. We need to avoid the following situation:
+ #
+ # * An associated record is deleted via record.destroy
+ # * Hence the callbacks run, and they find a belongs_to on the record with a
+ # :counter_cache options which points back at our owner. So they update the
+ # counter cache.
+ # * In which case, we must make sure to *not* update the counter cache, or else
+ # it will be decremented twice.
+ #
+ # Hence this method.
+ def inverse_which_updates_counter_cache
+ return @inverse_which_updates_counter_cache if defined?(@inverse_which_updates_counter_cache)
+ @inverse_which_updates_counter_cache = klass.reflect_on_all_associations(:belongs_to).find do |inverse|
+ inverse.counter_cache_column == counter_cache_column
+ end
+ end
+ alias inverse_updates_counter_cache? inverse_which_updates_counter_cache
+
+ def inverse_updates_counter_in_memory?
+ inverse_of && inverse_which_updates_counter_cache == inverse_of
+ end
+
+ # Returns whether a counter cache should be used for this association.
+ #
+ # The counter_cache option must be given on either the owner or inverse
+ # association, and the column must be present on the owner.
+ def has_cached_counter?
+ options[:counter_cache] ||
+ inverse_which_updates_counter_cache && inverse_which_updates_counter_cache.options[:counter_cache] &&
+ !!active_record.columns_hash[counter_cache_column]
+ end
+
+ def counter_must_be_updated_by_has_many?
+ !inverse_updates_counter_in_memory? && has_cached_counter?
+ end
+
+ def alias_candidate(name)
+ "#{plural_name}_#{name}"
end
end
+
# Base class for AggregateReflection and AssociationReflection. Objects of
# AggregateReflection and AssociationReflection are returned by the Reflection::ClassMethods.
#
# MacroReflection
+ # AggregateReflection
# AssociationReflection
- # AggregateReflection
# HasManyReflection
# HasOneReflection
# BelongsToReflection
@@ -197,7 +264,7 @@ module ActiveRecord
@scope = scope
@options = options
@active_record = active_record
- @klass = options[:class]
+ @klass = options[:anonymous_class]
@plural_name = active_record.pluralize_table_names ?
name.to_s.pluralize : name.to_s
end
@@ -205,7 +272,7 @@ module ActiveRecord
def autosave=(autosave)
@automatic_inverse_of = false
@options[:autosave] = autosave
- _, parent_reflection = self.parent_reflection
+ parent_reflection = self.parent_reflection
if parent_reflection
parent_reflection.autosave = autosave
end
@@ -273,12 +340,12 @@ module ActiveRecord
end
attr_reader :type, :foreign_type
- attr_accessor :parent_reflection # [:name, Reflection]
+ attr_accessor :parent_reflection # Reflection
def initialize(name, scope, options, active_record)
super
@automatic_inverse_of = nil
- @type = options[:as] && "#{options[:as]}_type"
+ @type = options[:as] && (options[:foreign_type] || "#{options[:as]}_type")
@foreign_type = options[:foreign_type] || "#{name}_type"
@constructable = calculate_constructable(macro, options)
@association_scope_cache = {}
@@ -288,7 +355,7 @@ module ActiveRecord
def association_scope_cache(conn, owner)
key = conn.prepared_statements
if polymorphic?
- key = [key, owner.read_attribute(@foreign_type)]
+ key = [key, owner._read_attribute(@foreign_type)]
end
@association_scope_cache[key] ||= @scope_lock.synchronize {
@association_scope_cache[key] ||= yield
@@ -320,43 +387,25 @@ module ActiveRecord
@active_record_primary_key ||= options[:primary_key] || primary_key(active_record)
end
- def counter_cache_column
- if options[:counter_cache] == true
- "#{active_record.name.demodulize.underscore.pluralize}_count"
- elsif options[:counter_cache]
- options[:counter_cache].to_s
- end
- end
-
def check_validity!
check_validity_of_inverse!
end
- def check_validity_of_inverse!
- unless polymorphic?
- if has_inverse? && inverse_of.nil?
- raise InverseOfAssociationNotFoundError.new(self)
- end
- end
- end
-
def check_preloadable!
return unless scope
if scope.arity > 0
- ActiveSupport::Deprecation.warn <<-WARNING
-The association scope '#{name}' is instance dependent (the scope block takes an argument).
-Preloading happens before the individual instances are created. This means that there is no instance
-being passed to the association scope. This will most likely result in broken or incorrect behavior.
-Joining, Preloading and eager loading of these associations is deprecated and will be removed in the future.
- WARNING
+ raise ArgumentError, <<-MSG.squish
+ The association scope '#{name}' is instance dependent (the scope
+ block takes an argument). Preloading instance dependent scopes is
+ not supported.
+ MSG
end
end
alias :check_eager_loadable! :check_preloadable!
- def join_id_for(owner) #:nodoc:
- key = (source_macro == :belongs_to) ? foreign_key : active_record_primary_key
- owner[key]
+ def join_id_for(owner) # :nodoc:
+ owner[active_record_primary_key]
end
def through_reflection
@@ -373,6 +422,12 @@ Joining, Preloading and eager loading of these associations is deprecated and wi
[self]
end
+ # This is for clearing cache on the reflection. Useful for tests that need to compare
+ # SQL queries on associations.
+ def clear_association_scope_cache # :nodoc:
+ @association_scope_cache.clear
+ end
+
def nested?
false
end
@@ -383,18 +438,10 @@ Joining, Preloading and eager loading of these associations is deprecated and wi
scope ? [[scope]] : [[]]
end
- def source_macro; macro; end
-
def has_inverse?
inverse_name
end
- def inverse_of
- return unless inverse_name
-
- @inverse_of ||= klass._reflect_on_association inverse_name
- end
-
def polymorphic_inverse_of(associated_class)
if has_inverse?
if inverse_relationship = associated_class._reflect_on_association(options[:inverse_of])
@@ -431,14 +478,10 @@ Joining, Preloading and eager loading of these associations is deprecated and wi
end
# Returns +true+ if +self+ is a +belongs_to+ reflection.
- def belongs_to?
- macro == :belongs_to
- end
+ def belongs_to?; false; end
# Returns +true+ if +self+ is a +has_one+ reflection.
- def has_one?
- macro == :has_one
- end
+ def has_one?; false; end
def association_class
case macro
@@ -505,7 +548,7 @@ Joining, Preloading and eager loading of these associations is deprecated and wi
# 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).to_sym
+ inverse_name = ActiveSupport::Inflector.underscore(options[:as] || active_record.name.demodulize).to_sym
begin
reflection = klass._reflect_on_association(inverse_name)
@@ -578,35 +621,46 @@ Joining, Preloading and eager loading of these associations is deprecated and wi
end
end
- class HasManyReflection < AssociationReflection #:nodoc:
+ class HasManyReflection < AssociationReflection # :nodoc:
def initialize(name, scope, options, active_record)
super(name, scope, options, active_record)
end
def macro; :has_many; end
- def collection?
- true
- end
+ def collection?; true; end
end
- class HasOneReflection < AssociationReflection #:nodoc:
+ class HasOneReflection < AssociationReflection # :nodoc:
def initialize(name, scope, options, active_record)
super(name, scope, options, active_record)
end
def macro; :has_one; end
+
+ def has_one?; true; end
end
- class BelongsToReflection < AssociationReflection #:nodoc:
+ class BelongsToReflection < AssociationReflection # :nodoc:
def initialize(name, scope, options, active_record)
super(name, scope, options, active_record)
end
def macro; :belongs_to; end
+
+ def belongs_to?; true; end
+
+ def join_keys(association_klass)
+ key = polymorphic? ? association_primary_key(association_klass) : association_primary_key
+ JoinKeys.new(key, foreign_key)
+ end
+
+ def join_id_for(owner) # :nodoc:
+ owner[foreign_key]
+ end
end
- class HasAndBelongsToManyReflection < AssociationReflection #:nodoc:
+ class HasAndBelongsToManyReflection < AssociationReflection # :nodoc:
def initialize(name, scope, options, active_record)
super
end
@@ -627,7 +681,7 @@ Joining, Preloading and eager loading of these associations is deprecated and wi
def initialize(delegate_reflection)
@delegate_reflection = delegate_reflection
- @klass = delegate_reflection.options[:class]
+ @klass = delegate_reflection.options[:anonymous_class]
@source_reflection_name = delegate_reflection.options[:source]
end
@@ -692,13 +746,27 @@ Joining, Preloading and eager loading of these associations is deprecated and wi
def chain
@chain ||= begin
a = source_reflection.chain
- b = through_reflection.chain
+ b = through_reflection.chain.map(&:dup)
+
+ if options[:source_type]
+ b[0] = PolymorphicReflection.new(b[0], self)
+ end
+
chain = a + b
chain[0] = self # Use self so we don't lose the information from :source_type
chain
end
end
+ # This is for clearing cache on the reflection. Useful for tests that need to compare
+ # SQL queries on associations.
+ def clear_association_scope_cache # :nodoc:
+ @chain = nil
+ delegate_reflection.clear_association_scope_cache
+ source_reflection.clear_association_scope_cache
+ through_reflection.clear_association_scope_cache
+ end
+
# Consider the following example:
#
# class Person
@@ -728,8 +796,11 @@ Joining, Preloading and eager loading of these associations is deprecated and wi
through_scope_chain = through_reflection.scope_chain.map(&:dup)
if options[:source_type]
- through_scope_chain.first <<
- through_reflection.klass.where(foreign_type => options[:source_type])
+ type = foreign_type
+ source_type = options[:source_type]
+ through_scope_chain.first << lambda { |object|
+ where(type => source_type)
+ }
end
# Recursively fill out the rest of the array from the through reflection
@@ -737,9 +808,8 @@ Joining, Preloading and eager loading of these associations is deprecated and wi
end
end
- # The macro used by the source association
- def source_macro
- source_reflection.source_macro
+ def join_keys(association_klass)
+ source_reflection.join_keys(association_klass)
end
# A through association is nested if there would be more than one join table
@@ -774,7 +844,7 @@ Joining, Preloading and eager loading of these associations is deprecated and wi
def source_reflection_name # :nodoc:
return @source_reflection_name if @source_reflection_name
- names = [name.to_s.singularize, name].collect { |n| n.to_sym }.uniq
+ names = [name.to_s.singularize, name].collect(&:to_sym).uniq
names = names.find_all { |n|
through_reflection.klass._reflect_on_association(n)
}
@@ -782,15 +852,13 @@ Joining, Preloading and eager loading of these associations is deprecated and wi
if names.length > 1
example_options = options.dup
example_options[:source] = source_reflection_names.first
- ActiveSupport::Deprecation.warn <<-eowarn
-Ambiguous source reflection for through association. Please specify a :source
-directive on your declaration like:
-
- class #{active_record.name} < ActiveRecord::Base
- #{macro} :#{name}, #{example_options}
- end
-
- eowarn
+ ActiveSupport::Deprecation.warn \
+ "Ambiguous source reflection for through association. Please " \
+ "specify a :source directive on your declaration like:\n" \
+ "\n" \
+ " class #{active_record.name} < ActiveRecord::Base\n" \
+ " #{macro} :#{name}, #{example_options}\n" \
+ " end"
end
@source_reflection_name = names.first
@@ -804,13 +872,21 @@ directive on your declaration like:
through_reflection.options
end
+ def join_id_for(owner) # :nodoc:
+ source_reflection.join_id_for(owner)
+ end
+
def check_validity!
if through_reflection.nil?
raise HasManyThroughAssociationNotFoundError.new(active_record.name, self)
end
if through_reflection.polymorphic?
- raise HasManyThroughAssociationPolymorphicThroughError.new(active_record.name, self)
+ if has_one?
+ raise HasOneAssociationPolymorphicThroughError.new(active_record.name, self)
+ else
+ raise HasManyThroughAssociationPolymorphicThroughError.new(active_record.name, self)
+ end
end
if source_reflection.nil?
@@ -832,6 +908,12 @@ directive on your declaration like:
check_validity_of_inverse!
end
+ def constraints
+ scope_chain = source_reflection.constraints
+ scope_chain << scope if scope
+ scope_chain
+ end
+
protected
def actual_source_reflection # FIXME: this is a horrible name
@@ -842,6 +924,8 @@ directive on your declaration like:
klass.primary_key || raise(UnknownPrimaryKey.new(klass))
end
+ def inverse_name; delegate_reflection.send(:inverse_name); end
+
private
def derive_class_name
# get the class_name of the belongs_to association of the through reflection
@@ -854,5 +938,81 @@ directive on your declaration like:
delegate(*delegate_methods, to: :delegate_reflection)
end
+
+ class PolymorphicReflection < ThroughReflection # :nodoc:
+ def initialize(reflection, previous_reflection)
+ @reflection = reflection
+ @previous_reflection = previous_reflection
+ end
+
+ def klass
+ @reflection.klass
+ end
+
+ def scope
+ @reflection.scope
+ end
+
+ def table_name
+ @reflection.table_name
+ end
+
+ def plural_name
+ @reflection.plural_name
+ end
+
+ def join_keys(association_klass)
+ @reflection.join_keys(association_klass)
+ end
+
+ def type
+ @reflection.type
+ end
+
+ def constraints
+ [source_type_info]
+ end
+
+ def source_type_info
+ type = @previous_reflection.foreign_type
+ source_type = @previous_reflection.options[:source_type]
+ lambda { |object| where(type => source_type) }
+ end
+ end
+
+ class RuntimeReflection < PolymorphicReflection # :nodoc:
+ attr_accessor :next
+
+ def initialize(reflection, association)
+ @reflection = reflection
+ @association = association
+ end
+
+ def klass
+ @association.klass
+ end
+
+ def table_name
+ klass.table_name
+ end
+
+ def constraints
+ @reflection.constraints
+ end
+
+ def source_type_info
+ @reflection.source_type_info
+ end
+
+ def alias_candidate(name)
+ "#{plural_name}_#{name}_join"
+ end
+
+ def alias_name
+ Arel::Table.new(table_name)
+ end
+
+ def all_includes; yield; end
+ end
end
end
diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb
index ad54d84665..392b462aa9 100644
--- a/activerecord/lib/active_record/relation.rb
+++ b/activerecord/lib/active_record/relation.rb
@@ -1,40 +1,39 @@
-# -*- coding: utf-8 -*-
-require 'arel/collectors/bind'
+require "arel/collectors/bind"
module ActiveRecord
- # = Active Record Relation
+ # = Active Record \Relation
class Relation
- JoinOperation = Struct.new(:relation, :join_class, :on)
-
MULTI_VALUE_METHODS = [:includes, :eager_load, :preload, :select, :group,
- :order, :joins, :where, :having, :bind, :references,
+ :order, :joins, :references,
:extending, :unscope]
- SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :from, :reordering,
- :reverse_order, :distinct, :create_with, :uniq]
+ SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :reordering,
+ :reverse_order, :distinct, :create_with]
+ CLAUSE_METHODS = [:where, :having, :from]
INVALID_METHODS_FOR_DELETE_ALL = [:limit, :distinct, :offset, :group, :having]
- VALUE_METHODS = MULTI_VALUE_METHODS + SINGLE_VALUE_METHODS
+ VALUE_METHODS = MULTI_VALUE_METHODS + SINGLE_VALUE_METHODS + CLAUSE_METHODS
+ include Enumerable
include FinderMethods, Calculations, SpawnMethods, QueryMethods, Batches, Explain, Delegation
- attr_reader :table, :klass, :loaded
+ attr_reader :table, :klass, :loaded, :predicate_builder
alias :model :klass
alias :loaded? :loaded
- def initialize(klass, table, values = {})
+ def initialize(klass, table, predicate_builder, values = {})
@klass = klass
@table = table
@values = values
@offsets = {}
@loaded = false
+ @predicate_builder = predicate_builder
end
def initialize_copy(other)
# This method is a hot spot, so for now, use Hash[] to dup the hash.
# https://bugs.ruby-lang.org/issues/7166
@values = Hash[@values]
- @values[:bind] = @values[:bind].dup if @values.key? :bind
reset
end
@@ -81,22 +80,26 @@ module ActiveRecord
scope.unscope!(where: @klass.inheritance_column)
end
- um = scope.where(@klass.arel_table[@klass.primary_key].eq(id_was || id)).arel.compile_update(substitutes, @klass.primary_key)
+ relation = scope.where(@klass.primary_key => (id_was || id))
+ bvs = binds + relation.bound_attributes
+ um = relation
+ .arel
+ .compile_update(substitutes, @klass.primary_key)
@klass.connection.update(
um,
'SQL',
- binds)
+ bvs,
+ )
end
def substitute_values(values) # :nodoc:
- substitutes = values.sort_by { |arel_attr,_| arel_attr.name }
- binds = substitutes.map do |arel_attr, value|
- [@klass.columns_hash[arel_attr.name], value]
+ binds = values.map do |arel_attr, value|
+ QueryAttribute.new(arel_attr.name, value, klass.type_for_attribute(arel_attr.name))
end
- substitutes.each_with_index do |tuple, i|
- tuple[1] = @klass.connection.substitute_at(binds[i][0], i)
+ substitutes = values.map do |(arel_attr, _)|
+ [arel_attr, connection.substitute_at(klass.columns_hash[arel_attr.name])]
end
[substitutes, binds]
@@ -105,7 +108,7 @@ module ActiveRecord
# Initializes new record from relation while maintaining the current
# scope.
#
- # Expects arguments in the same format as +Base.new+.
+ # Expects arguments in the same format as {ActiveRecord::Base.new}[rdoc-ref:Core.new].
#
# users = User.where(name: 'DHH')
# user = users.new # => #<User id: nil, name: "DHH", created_at: nil, updated_at: nil>
@@ -123,28 +126,32 @@ module ActiveRecord
# Tries to create a new record with the same scoped attributes
# defined in the relation. Returns the initialized object if validation fails.
#
- # Expects arguments in the same format as +Base.create+.
+ # Expects arguments in the same format as
+ # {ActiveRecord::Base.create}[rdoc-ref:Persistence::ClassMethods#create].
#
# ==== Examples
+ #
# users = User.where(name: 'Oscar')
- # users.create # #<User id: 3, name: "oscar", ...>
+ # users.create # => #<User id: 3, name: "oscar", ...>
#
# users.create(name: 'fxn')
- # users.create # #<User id: 4, name: "fxn", ...>
+ # users.create # => #<User id: 4, name: "fxn", ...>
#
# users.create { |user| user.name = 'tenderlove' }
- # # #<User id: 5, name: "tenderlove", ...>
+ # # => #<User id: 5, name: "tenderlove", ...>
#
# users.create(name: nil) # validation on name
- # # #<User id: nil, name: nil, ...>
+ # # => #<User id: nil, name: nil, ...>
def create(*args, &block)
scoping { @klass.create(*args, &block) }
end
- # Similar to #create, but calls +create!+ on the base class. Raises
- # an exception if a validation error occurs.
+ # Similar to #create, but calls
+ # {create!}[rdoc-ref:Persistence::ClassMethods#create!]
+ # on the base class. Raises an exception if a validation error occurs.
#
- # Expects arguments in the same format as <tt>Base.create!</tt>.
+ # Expects arguments in the same format as
+ # {ActiveRecord::Base.create!}[rdoc-ref:Persistence::ClassMethods#create!].
def create!(*args, &block)
scoping { @klass.create!(*args, &block) }
end
@@ -178,7 +185,7 @@ module ActiveRecord
# User.create_with(last_name: 'Johansson').find_or_create_by(first_name: 'Scarlett')
# # => #<User id: 2, first_name: "Scarlett", last_name: "Johansson">
#
- # This method accepts a block, which is passed down to +create+. The last example
+ # This method accepts a block, which is passed down to #create. The last example
# above can be alternatively written this way:
#
# # Find the first user named "Scarlett" or create a new one with a
@@ -190,7 +197,7 @@ module ActiveRecord
#
# This method always returns a record, but if creation was attempted and
# failed due to validation errors it won't be persisted, you get what
- # +create+ returns in such situation.
+ # #create returns in such situation.
#
# Please note *this method is not atomic*, it runs first a SELECT, and if
# there are no results an INSERT is attempted. If there are other threads
@@ -202,7 +209,9 @@ module ActiveRecord
# constraint an exception may be raised, just retry:
#
# begin
- # CreditAccount.find_or_create_by(user_id: user.id)
+ # CreditAccount.transaction(requires_new: true) do
+ # CreditAccount.find_or_create_by(user_id: user.id)
+ # end
# rescue ActiveRecord::RecordNotUnique
# retry
# end
@@ -211,13 +220,15 @@ module ActiveRecord
find_by(attributes) || create(attributes, &block)
end
- # Like <tt>find_or_create_by</tt>, but calls <tt>create!</tt> so an exception
+ # Like #find_or_create_by, but calls
+ # {create!}[rdoc-ref:Persistence::ClassMethods#create!] so an exception
# is raised if the created record is invalid.
def find_or_create_by!(attributes, &block)
find_by(attributes) || create!(attributes, &block)
end
- # Like <tt>find_or_create_by</tt>, but calls <tt>new</tt> instead of <tt>create</tt>.
+ # 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)
find_by(attributes) || new(attributes, &block)
end
@@ -268,22 +279,54 @@ module ActiveRecord
end
end
+ # Returns true if there are no records.
+ def none?
+ return super if block_given?
+ empty?
+ end
+
# Returns true if there are any records.
def any?
- if block_given?
- to_a.any? { |*block_args| yield(*block_args) }
- else
- !empty?
- end
+ return super if block_given?
+ !empty?
+ end
+
+ # Returns true if there is exactly one record.
+ def one?
+ return super if block_given?
+ limit_value ? to_a.one? : size == 1
end
# Returns true if there is more than one record.
def many?
- if block_given?
- to_a.many? { |*block_args| yield(*block_args) }
- else
- limit_value ? to_a.many? : size > 1
- end
+ return super if block_given?
+ limit_value ? to_a.many? : size > 1
+ end
+
+ # Returns a cache key that can be used to identify the records fetched by
+ # this query. The cache key is built with a fingerprint of the sql query,
+ # the number of records matched by the query and a timestamp of the last
+ # updated record. When a new record comes to match the query, or any of
+ # the existing records is updated or deleted, the cache key changes.
+ #
+ # Product.where("name like ?", "%Cosmic Encounter%").cache_key
+ # # => "products/query-1850ab3d302391b85b8693e941286659-1-20150714212553907087000"
+ #
+ # If the collection is loaded, the method will iterate through the records
+ # to generate the timestamp, otherwise it will trigger one SQL query like:
+ #
+ # SELECT COUNT(*), MAX("products"."updated_at") FROM "products" WHERE (name like '%Cosmic Encounter%')
+ #
+ # You can also pass a custom timestamp column to fetch the timestamp of the
+ # last updated record.
+ #
+ # Product.where("name like ?", "%Game%").cache_key(:last_reviewed_at)
+ #
+ # You can customize the strategy to generate the key on a per model basis
+ # overriding ActiveRecord::Base#collection_cache_key.
+ def cache_key(timestamp_column = :updated_at)
+ @cache_keys ||= {}
+ @cache_keys[timestamp_column] ||= @klass.collection_cache_key(self, timestamp_column)
end
# Scope all queries to the current scope.
@@ -302,10 +345,11 @@ module ActiveRecord
klass.current_scope = previous
end
- # Updates all records with details given if they match a set of conditions supplied, limits and order can
- # also be supplied. 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.
+ # 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. Values passed to #update_all will not go through
+ # Active Record's type-casting behavior. It should receive only values that can be passed as-is to the SQL
+ # database.
#
# ==== Parameters
#
@@ -324,7 +368,7 @@ module ActiveRecord
def update_all(updates)
raise ArgumentError, "Empty list of attributes to change" if updates.blank?
- stmt = Arel::UpdateManager.new(arel.engine)
+ stmt = Arel::UpdateManager.new
stmt.set Arel.sql(@klass.send(:sanitize_sql_for_assignment, updates))
stmt.table(table)
@@ -338,8 +382,7 @@ module ActiveRecord
stmt.wheres = arel.constraints
end
- bvs = bind_values + arel.bind_values
- @klass.connection.update stmt, 'SQL', bvs
+ @klass.connection.update stmt, 'SQL', bound_attributes
end
# Updates an object (or multiple objects) and saves it to the database, if validations pass.
@@ -358,20 +401,39 @@ module ActiveRecord
# # Updates multiple records
# people = { 1 => { "first_name" => "David" }, 2 => { "first_name" => "Jeremy" } }
# Person.update(people.keys, people.values)
- def update(id, attributes)
+ #
+ # # Updates multiple records from the result of a relation
+ # people = Person.where(group: 'expert')
+ # people.update(group: 'masters')
+ #
+ # Note: Updating a large number of records will run an
+ # UPDATE query for each record, which may cause a performance
+ # issue. So if it is not needed to run callbacks for each update, it is
+ # preferred to use #update_all for updating all records using
+ # 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]) }
+ elsif id == :all
+ to_a.each { |record| record.update(attributes) }
else
+ if ActiveRecord::Base === id
+ id = id.id
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ You are passing an instance of ActiveRecord::Base to `update`.
+ Please pass the id of the object by calling `.id`
+ MSG
+ end
object = find(id)
object.update(attributes)
object
end
end
- # Destroys the records matching +conditions+ by instantiating each
- # record and calling its +destroy+ method. Each object's callbacks are
- # executed (including <tt>:dependent</tt> association options). Returns the
- # collection of objects that were destroyed; each will be frozen, to
+ # Destroys the records by instantiating each
+ # record and calling its {#destroy}[rdoc-ref:Persistence#destroy] method.
+ # Each object's callbacks are executed (including <tt>:dependent</tt> association options).
+ # Returns the collection of objects that were destroyed; each will be frozen, to
# reflect that no changes should be made (since they can't be persisted).
#
# Note: Instantiation, callback execution, and deletion of each
@@ -379,31 +441,26 @@ module ActiveRecord
# once. It generates at least one SQL +DELETE+ query per record (or
# possibly more, to enforce your callbacks). If you want to delete many
# rows quickly, without concern for their associations or callbacks, use
- # +delete_all+ instead.
- #
- # ==== Parameters
- #
- # * +conditions+ - A string, array, or hash that specifies which records
- # to destroy. If omitted, all records are destroyed. See the
- # Conditions section in the introduction to ActiveRecord::Base for
- # more information.
+ # #delete_all instead.
#
# ==== Examples
#
- # Person.destroy_all("last_login < '2004-04-04'")
- # Person.destroy_all(status: "inactive")
# Person.where(age: 0..18).destroy_all
def destroy_all(conditions = nil)
if conditions
+ ActiveSupport::Deprecation.warn(<<-MESSAGE.squish)
+ Passing conditions to destroy_all is deprecated and will be removed in Rails 5.1.
+ To achieve the same use where(conditions).destroy_all
+ MESSAGE
where(conditions).destroy_all
else
- to_a.each {|object| object.destroy }.tap { reset }
+ to_a.each(&:destroy).tap { reset }
end
end
# Destroy an object (or multiple objects) that has the given id. The object is instantiated first,
# therefore all callbacks and filters are fired off before the object is deleted. This method is
- # less efficient than ActiveRecord#delete but allows cleanup methods and other actions to be run.
+ # less efficient than #delete but allows cleanup methods and other actions to be run.
#
# This essentially finds the object (or multiple objects) with the given id, creates a new object
# from the attributes, and then calls destroy on it.
@@ -428,22 +485,21 @@ module ActiveRecord
end
end
- # Deletes the records matching +conditions+ without instantiating the records
- # first, and hence not calling the +destroy+ method nor invoking callbacks. This
- # is a single SQL DELETE statement that goes straight to the database, much more
- # efficient than +destroy_all+. Be careful with relations though, in particular
+ # Deletes the records without instantiating the records
+ # first, and hence not calling the {#destroy}[rdoc-ref:Persistence#destroy]
+ # method nor invoking callbacks.
+ # This is a single SQL DELETE statement that goes straight to the database, much more
+ # efficient than #destroy_all. Be careful with relations though, in particular
# <tt>:dependent</tt> rules defined on associations are not honored. Returns the
# number of rows affected.
#
- # Post.delete_all("person_id = 5 AND (category = 'Something' OR category = 'Else')")
- # Post.delete_all(["person_id = ? AND (category = ? OR category = ?)", 5, 'Something', 'Else'])
# Post.where(person_id: 5).where(category: ['Something', 'Else']).delete_all
#
# Both calls delete the affected posts all at once with a single DELETE statement.
# If you need to destroy dependent associations or call your <tt>before_*</tt> or
- # +after_destroy+ callbacks, use the +destroy_all+ method instead.
+ # +after_destroy+ callbacks, use the #destroy_all method instead.
#
- # If an invalid method is supplied, +delete_all+ raises an ActiveRecord error:
+ # If an invalid method is supplied, #delete_all raises an ActiveRecordError:
#
# Post.limit(100).delete_all
# # => ActiveRecord::ActiveRecordError: delete_all doesn't support limit
@@ -451,8 +507,10 @@ module ActiveRecord
invalid_methods = INVALID_METHODS_FOR_DELETE_ALL.select { |method|
if MULTI_VALUE_METHODS.include?(method)
send("#{method}_values").any?
- else
+ elsif SINGLE_VALUE_METHODS.include?(method)
send("#{method}_value")
+ elsif CLAUSE_METHODS.include?(method)
+ send("#{method}_clause").any?
end
}
if invalid_methods.any?
@@ -460,9 +518,13 @@ module ActiveRecord
end
if conditions
+ ActiveSupport::Deprecation.warn(<<-MESSAGE.squish)
+ Passing conditions to delete_all is deprecated and will be removed in Rails 5.1.
+ To achieve the same use where(conditions).delete_all
+ MESSAGE
where(conditions).delete_all
else
- stmt = Arel::DeleteManager.new(arel.engine)
+ stmt = Arel::DeleteManager.new
stmt.from(table)
if joins_values.any?
@@ -471,7 +533,7 @@ module ActiveRecord
stmt.wheres = arel.constraints
end
- affected = @klass.connection.delete(stmt, 'SQL', bind_values)
+ affected = @klass.connection.delete(stmt, 'SQL', bound_attributes)
reset
affected
@@ -486,7 +548,7 @@ module ActiveRecord
# You can delete multiple rows at once by passing an Array of <tt>id</tt>s.
#
# Note: Although it is often much faster than the alternative,
- # <tt>#destroy</tt>, skipping callbacks might bypass business logic in
+ # #destroy, skipping callbacks might bypass business logic in
# your application that ensures referential integrity or performs other
# essential jobs.
#
@@ -541,10 +603,10 @@ module ActiveRecord
find_with_associations { |rel| relation = rel }
end
- arel = relation.arel
- binds = (arel.bind_values + relation.bind_values).dup
- binds.map! { |bv| connection.quote(*bv.reverse) }
- collect = visitor.accept(arel.ast, Arel::Collectors::Bind.new)
+ binds = relation.bound_attributes
+ binds = connection.prepare_binds_for_database(binds)
+ binds.map! { |value| connection.quote(value) }
+ collect = visitor.accept(relation.arel.ast, Arel::Collectors::Bind.new)
collect.substitute_binds(binds).join
end
end
@@ -554,22 +616,7 @@ module ActiveRecord
# User.where(name: 'Oscar').where_values_hash
# # => {name: "Oscar"}
def where_values_hash(relation_table_name = table_name)
- equalities = where_values.grep(Arel::Nodes::Equality).find_all { |node|
- node.left.relation.name == relation_table_name
- }
-
- binds = Hash[bind_values.find_all(&:first).map { |column, v| [column.name, v] }]
-
- Hash[equalities.map { |where|
- name = where.left.name
- [name, binds.fetch(name.to_s) {
- case where.right
- when Array then where.right.map(&:val)
- else
- where.right.val
- end
- }]
- }]
+ where_clause.to_h(relation_table_name)
end
def scope_for_create
@@ -591,11 +638,14 @@ module ActiveRecord
includes_values & joins_values
end
- # +uniq+ and +uniq!+ are silently deprecated. +uniq_value+ delegates to +distinct_value+
- # to maintain backwards compatibility. Use +distinct_value+ instead.
+ # {#uniq}[rdoc-ref:QueryMethods#uniq] and
+ # {#uniq!}[rdoc-ref:QueryMethods#uniq!] are silently deprecated.
+ # #uniq_value delegates to #distinct_value to maintain backwards compatibility.
+ # Use #distinct_value instead.
def uniq_value
distinct_value
end
+ deprecate uniq_value: :distinct_value
# Compares two relations for equality.
def ==(other)
@@ -629,24 +679,35 @@ module ActiveRecord
"#<#{self.class.name} [#{entries.join(', ')}]>"
end
+ protected
+
+ def load_records(records)
+ @records = records
+ @loaded = true
+ end
+
private
def exec_queries
- @records = eager_loading? ? find_with_associations : @klass.find_by_sql(arel, arel.bind_values + bind_values)
+ @records = eager_loading? ? find_with_associations : @klass.find_by_sql(arel, bound_attributes)
preload = preload_values
preload += includes_values unless eager_loading?
- preloader = ActiveRecord::Associations::Preloader.new
+ preloader = build_preloader
preload.each do |associations|
preloader.preload @records, associations
end
- @records.each { |record| record.readonly! } if readonly_value
+ @records.each(&:readonly!) if readonly_value
@loaded = true
@records
end
+ def build_preloader
+ ActiveRecord::Associations::Preloader.new
+ end
+
def references_eager_loaded_tables?
joined_tables = arel.join_sources.map do |join|
if join.is_a?(Arel::Nodes::StringJoin)
@@ -659,7 +720,7 @@ module ActiveRecord
joined_tables += [table.name, table.table_alias]
# always convert table names to downcase as in Oracle quoted table names are in uppercase
- joined_tables = joined_tables.flatten.compact.map { |t| t.downcase }.uniq
+ joined_tables = joined_tables.flatten.compact.map(&:downcase).uniq
(references_values - joined_tables).any?
end
@@ -668,7 +729,7 @@ module ActiveRecord
return [] if string.blank?
# always convert table names to downcase as in Oracle quoted table names are in uppercase
# ignore raw_sql_ that is used by Oracle adapter as alias for limit/offset subqueries
- string.scan(/([a-zA-Z_][.\w]+).?\./).flatten.map{ |s| s.downcase }.uniq - ['raw_sql_']
+ string.scan(/([a-zA-Z_][.\w]+).?\./).flatten.map(&:downcase).uniq - ['raw_sql_']
end
end
end
diff --git a/activerecord/lib/active_record/relation/batches.rb b/activerecord/lib/active_record/relation/batches.rb
index b069cdce7c..221bc73680 100644
--- a/activerecord/lib/active_record/relation/batches.rb
+++ b/activerecord/lib/active_record/relation/batches.rb
@@ -1,8 +1,10 @@
+require "active_record/relation/batches/batch_enumerator"
+
module ActiveRecord
module Batches
# Looping through a collection of records from the database
- # (using the +all+ method, for example) is very inefficient
- # since it will try to instantiate all the objects at once.
+ # (using the Scoping::Named::ClassMethods.all method, for example)
+ # is very inefficient since it will try to instantiate all the objects at once.
#
# In that case, batch processing methods allow you to work
# with the records in batches, thereby greatly reducing memory consumption.
@@ -27,37 +29,46 @@ module ActiveRecord
#
# ==== Options
# * <tt>:batch_size</tt> - Specifies the size of the batch. Default to 1000.
- # * <tt>:start</tt> - Specifies the starting point for the batch processing.
+ # * <tt>:begin_at</tt> - Specifies the primary key value to start from, inclusive of the value.
+ # * <tt>:end_at</tt> - Specifies the primary key value to end at, inclusive of the value.
# This is especially useful if you want multiple workers dealing with
# the same processing queue. You can make worker 1 handle all the records
# between id 0 and 10,000 and worker 2 handle from 10,000 and beyond
- # (by setting the +:start+ option on that worker).
+ # (by setting the +:begin_at+ and +:end_at+ option on each worker).
#
# # Let's process for a batch of 2000 records, skipping the first 2000 rows
- # Person.find_each(start: 2000, batch_size: 2000) do |person|
+ # Person.find_each(begin_at: 2000, batch_size: 2000) do |person|
# person.party_all_night!
# end
#
# NOTE: It's not possible to set the order. That is automatically set to
# ascending on the primary key ("id ASC") to make the batch ordering
- # work. This also means that this method only works with integer-based
- # primary keys.
+ # work. This also means that this method only works when the primary key is
+ # orderable (e.g. an integer or string).
#
# NOTE: You can't set the limit either, that's used to control
# the batch sizes.
- def find_each(options = {})
+ def find_each(begin_at: nil, end_at: nil, batch_size: 1000, start: nil)
+ if start
+ begin_at = start
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ Passing `start` value to find_each is deprecated, and will be removed in Rails 5.1.
+ Please pass `begin_at` instead.
+ MSG
+ end
if block_given?
- find_in_batches(options) do |records|
+ find_in_batches(begin_at: begin_at, end_at: end_at, batch_size: batch_size) do |records|
records.each { |record| yield record }
end
else
- enum_for :find_each, options do
- options[:start] ? where(table[primary_key].gteq(options[:start])).size : size
+ enum_for(:find_each, begin_at: begin_at, end_at: end_at, batch_size: batch_size) do
+ relation = self
+ apply_limits(relation, begin_at, end_at).size
end
end
end
- # Yields each batch of records that was found by the find +options+ as
+ # Yields each batch of records that was found by the find options as
# an array.
#
# Person.where("age > 21").find_in_batches do |group|
@@ -77,60 +88,149 @@ module ActiveRecord
#
# ==== Options
# * <tt>:batch_size</tt> - Specifies the size of the batch. Default to 1000.
- # * <tt>:start</tt> - Specifies the starting point for the batch processing.
+ # * <tt>:begin_at</tt> - Specifies the primary key value to start from, inclusive of the value.
+ # * <tt>:end_at</tt> - Specifies the primary key value to end at, inclusive of the value.
# This is especially useful if you want multiple workers dealing with
# the same processing queue. You can make worker 1 handle all the records
# between id 0 and 10,000 and worker 2 handle from 10,000 and beyond
- # (by setting the +:start+ option on that worker).
+ # (by setting the +:begin_at+ and +:end_at+ option on each worker).
#
# # Let's process the next 2000 records
- # Person.find_in_batches(start: 2000, batch_size: 2000) do |group|
+ # Person.find_in_batches(begin_at: 2000, batch_size: 2000) do |group|
# group.each { |person| person.party_all_night! }
# end
#
# NOTE: It's not possible to set the order. That is automatically set to
# ascending on the primary key ("id ASC") to make the batch ordering
- # work. This also means that this method only works with integer-based
- # primary keys.
+ # work. This also means that this method only works when the primary key is
+ # orderable (e.g. an integer or string).
#
# NOTE: You can't set the limit either, that's used to control
# the batch sizes.
- def find_in_batches(options = {})
- options.assert_valid_keys(:start, :batch_size)
+ def find_in_batches(begin_at: nil, end_at: nil, batch_size: 1000, start: nil)
+ if start
+ begin_at = start
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ Passing `start` value to find_in_batches is deprecated, and will be removed in Rails 5.1.
+ Please pass `begin_at` instead.
+ MSG
+ end
relation = self
- start = options[:start]
- batch_size = options[:batch_size] || 1000
-
unless block_given?
- return to_enum(:find_in_batches, options) do
- total = start ? where(table[primary_key].gteq(start)).size : size
+ return to_enum(:find_in_batches, begin_at: begin_at, end_at: end_at, batch_size: batch_size) do
+ total = apply_limits(relation, begin_at, end_at).size
(total - 1).div(batch_size) + 1
end
end
+ in_batches(of: batch_size, begin_at: begin_at, end_at: end_at, load: true) do |batch|
+ yield batch.to_a
+ end
+ end
+
+ # Yields ActiveRecord::Relation objects to work with a batch of records.
+ #
+ # Person.where("age > 21").in_batches do |relation|
+ # relation.delete_all
+ # sleep(10) # Throttle the delete queries
+ # end
+ #
+ # If you do not provide a block to #in_batches, it will return a
+ # BatchEnumerator which is enumerable.
+ #
+ # Person.in_batches.with_index do |relation, batch_index|
+ # puts "Processing relation ##{batch_index}"
+ # relation.each { |relation| relation.delete_all }
+ # end
+ #
+ # Examples of calling methods on the returned BatchEnumerator object:
+ #
+ # Person.in_batches.delete_all
+ # Person.in_batches.update_all(awesome: true)
+ # Person.in_batches.each_record(&:party_all_night!)
+ #
+ # ==== Options
+ # * <tt>:of</tt> - Specifies the size of the batch. Default to 1000.
+ # * <tt>:load</tt> - Specifies if the relation should be loaded. Default to false.
+ # * <tt>:begin_at</tt> - Specifies the primary key value to start from, inclusive of the value.
+ # * <tt>:end_at</tt> - Specifies the primary key value to end at, inclusive of the value.
+ #
+ # This is especially useful if you want to work with the
+ # ActiveRecord::Relation object instead of the array of records, or if
+ # you want multiple workers dealing with the same processing queue. You can
+ # make worker 1 handle all the records between id 0 and 10,000 and worker 2
+ # handle from 10,000 and beyond (by setting the +:begin_at+ and +:end_at+
+ # option on each worker).
+ #
+ # # Let's process the next 2000 records
+ # Person.in_batches(of: 2000, begin_at: 2000).update_all(awesome: true)
+ #
+ # An example of calling where query method on the relation:
+ #
+ # Person.in_batches.each do |relation|
+ # relation.update_all('age = age + 1')
+ # relation.where('age > 21').update_all(should_party: true)
+ # relation.where('age <= 21').delete_all
+ # end
+ #
+ # NOTE: If you are going to iterate through each record, you should call
+ # #each_record on the yielded BatchEnumerator:
+ #
+ # Person.in_batches.each_record(&:party_all_night!)
+ #
+ # NOTE: It's not possible to set the order. That is automatically set to
+ # ascending on the primary key ("id ASC") to make the batch ordering
+ # consistent. Therefore the primary key must be orderable, e.g an integer
+ # or a string.
+ #
+ # NOTE: You can't set the limit either, that's used to control the batch
+ # sizes.
+ def in_batches(of: 1000, begin_at: nil, end_at: nil, load: false)
+ relation = self
+ unless block_given?
+ return BatchEnumerator.new(of: of, begin_at: begin_at, end_at: end_at, relation: self)
+ end
+
if logger && (arel.orders.present? || arel.taken.present?)
logger.warn("Scoped order and limit are ignored, it's forced to be batch order and batch size")
end
- relation = relation.reorder(batch_order).limit(batch_size)
- records = start ? relation.where(table[primary_key].gteq(start)).to_a : relation.to_a
+ relation = relation.reorder(batch_order).limit(of)
+ relation = apply_limits(relation, begin_at, end_at)
+ batch_relation = relation
- while records.any?
- records_size = records.size
- primary_key_offset = records.last.id
- raise "Primary key not included in the custom select clause" unless primary_key_offset
+ loop do
+ if load
+ records = batch_relation.to_a
+ ids = records.map(&:id)
+ yielded_relation = self.where(primary_key => ids)
+ yielded_relation.load_records(records)
+ else
+ ids = batch_relation.pluck(primary_key)
+ yielded_relation = self.where(primary_key => ids)
+ end
+
+ break if ids.empty?
- yield records
+ primary_key_offset = ids.last
+ raise ArgumentError.new("Primary key not included in the custom select clause") unless primary_key_offset
- break if records_size < batch_size
+ yield yielded_relation
- records = relation.where(table[primary_key].gt(primary_key_offset)).to_a
+ break if ids.length < of
+ batch_relation = relation.where(table[primary_key].gt(primary_key_offset))
end
end
private
+ def apply_limits(relation, begin_at, end_at)
+ relation = relation.where(table[primary_key].gteq(begin_at)) if begin_at
+ relation = relation.where(table[primary_key].lteq(end_at)) if end_at
+ relation
+ end
+
def batch_order
"#{quoted_table_name}.#{quoted_primary_key} ASC"
end
diff --git a/activerecord/lib/active_record/relation/batches/batch_enumerator.rb b/activerecord/lib/active_record/relation/batches/batch_enumerator.rb
new file mode 100644
index 0000000000..153aae9584
--- /dev/null
+++ b/activerecord/lib/active_record/relation/batches/batch_enumerator.rb
@@ -0,0 +1,67 @@
+module ActiveRecord
+ module Batches
+ class BatchEnumerator
+ include Enumerable
+
+ def initialize(of: 1000, begin_at: nil, end_at: nil, relation:) #:nodoc:
+ @of = of
+ @relation = relation
+ @begin_at = begin_at
+ @end_at = end_at
+ end
+
+ # Looping through a collection of records from the database (using the
+ # +all+ method, for example) is very inefficient since it will try to
+ # instantiate all the objects at once.
+ #
+ # In that case, batch processing methods allow you to work with the
+ # records in batches, thereby greatly reducing memory consumption.
+ #
+ # Person.in_batches.each_record do |person|
+ # person.do_awesome_stuff
+ # end
+ #
+ # Person.where("age > 21").in_batches(of: 10).each_record do |person|
+ # person.party_all_night!
+ # end
+ #
+ # If you do not provide a block to #each_record, it will return an Enumerator
+ # for chaining with other methods:
+ #
+ # Person.in_batches.each_record.with_index do |person, index|
+ # person.award_trophy(index + 1)
+ # end
+ def each_record
+ return to_enum(:each_record) unless block_given?
+
+ @relation.to_enum(:in_batches, of: @of, begin_at: @begin_at, end_at: @end_at, load: true).each do |relation|
+ relation.to_a.each { |record| yield record }
+ end
+ end
+
+ # Delegates #delete_all, #update_all, #destroy_all methods to each batch.
+ #
+ # People.in_batches.delete_all
+ # People.in_batches.destroy_all('age < 10')
+ # People.in_batches.update_all('age = age + 1')
+ [:delete_all, :update_all, :destroy_all].each do |method|
+ define_method(method) do |*args, &block|
+ @relation.to_enum(:in_batches, of: @of, begin_at: @begin_at, end_at: @end_at, load: false).each do |relation|
+ relation.send(method, *args, &block)
+ end
+ end
+ end
+
+ # Yields an ActiveRecord::Relation object for each batch of records.
+ #
+ # Person.in_batches.each do |relation|
+ # relation.update_all(awesome: true)
+ # end
+ def each
+ enum = @relation.to_enum(:in_batches, of: @of, begin_at: @begin_at, end_at: @end_at, load: false)
+ return enum.each { |relation| yield relation } if block_given?
+ enum
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb
index 90e99957f6..f45844a9ea 100644
--- a/activerecord/lib/active_record/relation/calculations.rb
+++ b/activerecord/lib/active_record/relation/calculations.rb
@@ -14,121 +14,112 @@ module ActiveRecord
# Person.distinct.count(:age)
# # => counts the number of different age values
#
- # If +count+ is used with +group+, it returns a Hash whose keys represent the aggregated column,
+ # If #count is used with {Relation#group}[rdoc-ref:QueryMethods#group],
+ # it returns a Hash whose keys represent the aggregated column,
# and the values are the respective amounts:
#
# Person.group(:city).count
# # => { 'Rome' => 5, 'Paris' => 3 }
- #
- # If +count+ is used with +group+ for multiple columns, it returns a Hash whose
- # keys are an array containing the individual values of each column and the value
- # of each key would be the +count+.
- #
+ #
+ # If #count is used with {Relation#group}[rdoc-ref:QueryMethods#group] for multiple columns, it returns a Hash whose
+ # keys are an array containing the individual values of each column and the value
+ # of each key would be the #count.
+ #
# Article.group(:status, :category).count
- # # => {["draft", "business"]=>10, ["draft", "technology"]=>4,
+ # # => {["draft", "business"]=>10, ["draft", "technology"]=>4,
# ["published", "business"]=>0, ["published", "technology"]=>2}
- #
- # If +count+ is used with +select+, it will count the selected columns:
+ #
+ # If #count is used with {Relation#select}[rdoc-ref:QueryMethods#select], it will count the selected columns:
#
# Person.select(:age).count
# # => counts the number of different age values
#
- # Note: not all valid +select+ expressions are valid +count+ expressions. The specifics differ
+ # Note: not all valid {Relation#select}[rdoc-ref:QueryMethods#select] expressions are valid #count expressions. The specifics differ
# between databases. In invalid cases, an error from the database is thrown.
- def count(column_name = nil, options = {})
- # TODO: Remove options argument as soon we remove support to
- # activerecord-deprecated_finders.
- column_name, options = nil, column_name if column_name.is_a?(Hash)
- calculate(:count, column_name, options)
+ def count(column_name = nil)
+ calculate(:count, column_name)
end
# Calculates the average value on a given column. Returns +nil+ if there's
- # no row. See +calculate+ for examples with options.
+ # no row. See #calculate for examples with options.
#
# Person.average(:age) # => 35.8
- def average(column_name, options = {})
- # TODO: Remove options argument as soon we remove support to
- # activerecord-deprecated_finders.
- calculate(:average, column_name, options)
+ def average(column_name)
+ calculate(:average, column_name)
end
# Calculates the minimum value on a given column. The value is returned
# with the same data type of the column, or +nil+ if there's no row. See
- # +calculate+ for examples with options.
+ # #calculate for examples with options.
#
# Person.minimum(:age) # => 7
- def minimum(column_name, options = {})
- # TODO: Remove options argument as soon we remove support to
- # activerecord-deprecated_finders.
- calculate(:minimum, column_name, options)
+ def minimum(column_name)
+ calculate(:minimum, column_name)
end
# Calculates the maximum value on a given column. The value is returned
# with the same data type of the column, or +nil+ if there's no row. See
- # +calculate+ for examples with options.
+ # #calculate for examples with options.
#
# Person.maximum(:age) # => 93
- def maximum(column_name, options = {})
- # TODO: Remove options argument as soon we remove support to
- # activerecord-deprecated_finders.
- calculate(:maximum, column_name, options)
+ def maximum(column_name)
+ calculate(:maximum, column_name)
end
# Calculates the sum of values on a given column. The value is returned
- # with the same data type of the column, 0 if there's no row. See
- # +calculate+ for examples with options.
+ # with the same data type of the column, +0+ if there's no row. See
+ # #calculate for examples with options.
#
# Person.sum(:age) # => 4562
- def sum(*args)
- calculate(:sum, *args)
+ def sum(column_name = nil, &block)
+ return super(&block) if block_given?
+ calculate(:sum, column_name)
end
- # This calculates aggregate values in the given column. Methods for count, sum, average,
- # minimum, and maximum have been added as shortcuts.
+ # This calculates aggregate values in the given column. Methods for #count, #sum, #average,
+ # #minimum, and #maximum have been added as shortcuts.
#
- # There are two basic forms of output:
+ # Person.calculate(:count, :all) # The same as Person.count
+ # Person.average(:age) # SELECT AVG(age) FROM people...
#
- # * Single aggregate value: The single value is type cast to Fixnum for COUNT, Float
- # for AVG, and the given column's type for everything else.
+ # # Selects the minimum age for any family without any minors
+ # Person.group(:last_name).having("min(age) > 17").minimum(:age)
#
- # * Grouped values: This returns an ordered hash of the values and groups them. It
- # takes either a column name, or the name of a belongs_to association.
+ # Person.sum("2 * age")
#
- # values = Person.group('last_name').maximum(:age)
- # puts values["Drake"]
- # # => 43
+ # There are two basic forms of output:
#
- # drake = Family.find_by(last_name: 'Drake')
- # values = Person.group(:family).maximum(:age) # Person belongs_to :family
- # puts values[drake]
- # # => 43
+ # * Single aggregate value: The single value is type cast to Fixnum for COUNT, Float
+ # for AVG, and the given column's type for everything else.
#
- # values.each do |family, max_age|
- # ...
- # end
+ # * Grouped values: This returns an ordered hash of the values and groups them. It
+ # takes either a column name, or the name of a belongs_to association.
#
- # Person.calculate(:count, :all) # The same as Person.count
- # Person.average(:age) # SELECT AVG(age) FROM people...
+ # values = Person.group('last_name').maximum(:age)
+ # puts values["Drake"]
+ # # => 43
#
- # # Selects the minimum age for any family without any minors
- # Person.group(:last_name).having("min(age) > 17").minimum(:age)
+ # drake = Family.find_by(last_name: 'Drake')
+ # values = Person.group(:family).maximum(:age) # Person belongs_to :family
+ # puts values[drake]
+ # # => 43
#
- # Person.sum("2 * age")
- def calculate(operation, column_name, options = {})
- # TODO: Remove options argument as soon we remove support to
- # activerecord-deprecated_finders.
+ # values.each do |family, max_age|
+ # ...
+ # end
+ def calculate(operation, column_name)
if column_name.is_a?(Symbol) && attribute_alias?(column_name)
column_name = attribute_alias(column_name)
end
if has_include?(column_name)
- construct_relation_for_association_calculations.calculate(operation, column_name, options)
+ construct_relation_for_association_calculations.calculate(operation, column_name)
else
- perform_calculation(operation, column_name, options)
+ perform_calculation(operation, column_name)
end
end
- # Use <tt>pluck</tt> as a shortcut to select one or more attributes without
+ # Use #pluck as a shortcut to select one or more attributes without
# loading a bunch of records just to grab the attributes you want.
#
# Person.pluck(:name)
@@ -137,19 +128,19 @@ module ActiveRecord
#
# Person.all.map(&:name)
#
- # Pluck returns an <tt>Array</tt> of attribute values type-casted to match
+ # Pluck returns an Array of attribute values type-casted to match
# the plucked column names, if they can be deduced. Plucking an SQL fragment
# returns String values by default.
#
- # Person.pluck(:id)
- # # SELECT people.id FROM people
- # # => [1, 2, 3]
+ # Person.pluck(:name)
+ # # SELECT people.name FROM people
+ # # => ['David', 'Jeremy', 'Jose']
#
# Person.pluck(:id, :name)
# # SELECT people.id, people.name FROM people
# # => [[1, 'David'], [2, 'Jeremy'], [3, 'Jose']]
#
- # Person.pluck('DISTINCT role')
+ # Person.distinct.pluck(:role)
# # SELECT DISTINCT role FROM people
# # => ['admin', 'member', 'guest']
#
@@ -161,6 +152,8 @@ module ActiveRecord
# # SELECT DATEDIFF(updated_at, created_at) FROM people
# # => ['0', '27761', '173']
#
+ # See also #ids.
+ #
def pluck(*column_names)
column_names.map! do |column_name|
if column_name.is_a?(Symbol) && attribute_alias?(column_name)
@@ -170,6 +163,10 @@ module ActiveRecord
end
end
+ if loaded? && (column_names - @klass.column_names).empty?
+ return @records.pluck(*column_names)
+ end
+
if has_include?(column_names.first)
construct_relation_for_association_calculations.pluck(*column_names)
else
@@ -177,8 +174,8 @@ module ActiveRecord
relation.select_values = column_names.map { |cn|
columns_hash.key?(cn) ? arel_table[cn] : cn
}
- result = klass.connection.select_all(relation.arel, nil, bind_values)
- result.cast_values(klass.column_types)
+ result = klass.connection.select_all(relation.arel, nil, bound_attributes)
+ result.cast_values(klass.attribute_types)
end
end
@@ -193,15 +190,14 @@ module ActiveRecord
private
def has_include?(column_name)
- eager_loading? || (includes_values.present? && ((column_name && column_name != :all) || references_eager_loaded_tables?))
+ eager_loading? || (includes_values.present? && column_name && column_name != :all)
end
- def perform_calculation(operation, column_name, options = {})
- # TODO: Remove options argument as soon we remove support to
- # activerecord-deprecated_finders.
+ def perform_calculation(operation, column_name)
operation = operation.to_s.downcase
- # If #count is used with #distinct / #uniq it is considered distinct. (eg. relation.distinct.count)
+ # If #count is used with #distinct (i.e. `relation.distinct.count`) it is
+ # considered distinct.
distinct = self.distinct_value
if operation == "count"
@@ -223,6 +219,8 @@ module ActiveRecord
end
def aggregate_column(column_name)
+ return column_name if Arel::Expressions === column_name
+
if @klass.column_names.include?(column_name.to_s)
Arel::Attribute.new(@klass.unscoped.table, column_name)
else
@@ -235,32 +233,29 @@ module ActiveRecord
end
def execute_simple_calculation(operation, column_name, distinct) #:nodoc:
- # Postgresql doesn't like ORDER BY when there are no GROUP BY
+ # PostgreSQL doesn't like ORDER BY when there are no GROUP BY
relation = unscope(:order)
column_alias = column_name
- bind_values = nil
-
if operation == "count" && (relation.limit_value || relation.offset_value)
# Shortcut when limit is zero.
return 0 if relation.limit_value == 0
query_builder = build_count_subquery(relation, column_name, distinct)
- bind_values = query_builder.bind_values + relation.bind_values
else
column = aggregate_column(column_name)
select_value = operation_over_aggregate_column(column, operation, distinct)
column_alias = select_value.alias
+ column_alias ||= @klass.connection.column_name_for_operation(operation, select_value)
relation.select_values = [select_value]
query_builder = relation.arel
- bind_values = query_builder.bind_values + relation.bind_values
end
- result = @klass.connection.select_all(query_builder, nil, bind_values)
+ result = @klass.connection.select_all(query_builder, nil, bound_attributes)
row = result.first
value = row && row.values.first
column = result.column_types.fetch(column_alias) do
@@ -274,21 +269,16 @@ module ActiveRecord
group_attrs = group_values
if group_attrs.first.respond_to?(:to_sym)
- association = @klass._reflect_on_association(group_attrs.first.to_sym)
+ association = @klass._reflect_on_association(group_attrs.first)
associated = group_attrs.size == 1 && association && association.belongs_to? # only count belongs_to associations
group_fields = Array(associated ? association.foreign_key : group_attrs)
else
group_fields = group_attrs
end
+ group_fields = arel_columns(group_fields)
- group_aliases = group_fields.map { |field|
- column_alias_for(field)
- }
- group_columns = group_aliases.zip(group_fields).map { |aliaz,field|
- [aliaz, field]
- }
-
- group = group_fields
+ group_aliases = group_fields.map { |field| column_alias_for(field) }
+ group_columns = group_aliases.zip(group_fields)
if operation == 'count' && column_name == :all
aggregate_alias = 'count_all'
@@ -302,9 +292,9 @@ module ActiveRecord
operation,
distinct).as(aggregate_alias)
]
- select_values += select_values unless having_values.empty?
+ select_values += select_values unless having_clause.empty?
- select_values.concat group_fields.zip(group_aliases).map { |field,aliaz|
+ select_values.concat group_columns.map { |aliaz, field|
if field.respond_to?(:as)
field.as(aliaz)
else
@@ -313,14 +303,14 @@ module ActiveRecord
}
relation = except(:group)
- relation.group_values = group
+ relation.group_values = group_fields
relation.select_values = select_values
- calculated_data = @klass.connection.select_all(relation, nil, bind_values)
+ calculated_data = @klass.connection.select_all(relation, nil, relation.bound_attributes)
if association
key_ids = calculated_data.collect { |row| row[group_aliases.first] }
- key_records = association.klass.base_class.find(key_ids)
+ key_records = association.klass.base_class.where(association.klass.base_class.primary_key => key_ids)
key_records = Hash[key_records.map { |r| [r.id, r] }]
end
@@ -346,7 +336,6 @@ module ActiveRecord
# column_alias_for("sum(id)") # => "sum_id"
# column_alias_for("count(distinct users.id)") # => "count_distinct_users_id"
# column_alias_for("count(*)") # => "count_all"
- # column_alias_for("count", "id") # => "count_id"
def column_alias_for(keys)
if keys.respond_to? :name
keys = "#{keys.relation.name}.#{keys.name}"
@@ -369,15 +358,15 @@ module ActiveRecord
def type_cast_calculated_value(value, type, operation = nil)
case operation
when 'count' then value.to_i
- when 'sum' then type.type_cast_from_database(value || 0)
+ when 'sum' then type.deserialize(value || 0)
when 'average' then value.respond_to?(:to_d) ? value.to_d : value
- else type.type_cast_from_database(value)
+ else type.deserialize(value)
end
end
- # TODO: refactor to allow non-string `select_values` (eg. Arel nodes).
def select_for_count
if select_values.present?
+ return select_values.first if select_values.one?
select_values.join(", ")
else
:all
@@ -390,11 +379,9 @@ module ActiveRecord
aliased_column = aggregate_column(column_name == :all ? 1 : column_name).as(column_alias)
relation.select_values = [aliased_column]
- arel = relation.arel
- subquery = arel.as(subquery_alias)
+ subquery = relation.arel.as(subquery_alias)
sm = Arel::SelectManager.new relation.engine
- sm.bind_values = arel.bind_values
select_value = operation_over_aggregate_column(column_alias, 'count', distinct)
sm.project(select_value).from(subquery)
end
diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb
index 50f4d5c7ab..27de313d05 100644
--- a/activerecord/lib/active_record/relation/delegation.rb
+++ b/activerecord/lib/active_record/relation/delegation.rb
@@ -1,15 +1,14 @@
require 'set'
require 'active_support/concern'
-require 'active_support/deprecation'
module ActiveRecord
module Delegation # :nodoc:
- module DelegateCache
- def relation_delegate_class(klass) # :nodoc:
+ module DelegateCache # :nodoc:
+ def relation_delegate_class(klass)
@relation_delegate_cache[klass]
end
- def initialize_relation_delegate_cache # :nodoc:
+ def initialize_relation_delegate_cache
@relation_delegate_cache = cache = {}
[
ActiveRecord::Relation,
@@ -19,7 +18,7 @@ module ActiveRecord
delegate = Class.new(klass) {
include ClassSpecificRelation
}
- const_set klass.name.gsub('::', '_'), delegate
+ const_set klass.name.gsub('::'.freeze, '_'.freeze), delegate
cache[klass] = delegate
end
end
@@ -40,7 +39,7 @@ module ActiveRecord
BLACKLISTED_ARRAY_METHODS = [
:compact!, :flatten!, :reject!, :reverse!, :rotate!, :map!,
:shuffle!, :slice!, :sort!, :sort_by!, :delete_if,
- :keep_if, :pop, :shift, :delete_at, :compact, :select!
+ :keep_if, :pop, :shift, :delete_at, :select!
].to_set # :nodoc:
delegate :to_xml, :to_yaml, :length, :collect, :map, :each, :all?, :include?, :to_ary, :join, to: :to_a
diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb
index 0c9c761f97..435cef901b 100644
--- a/activerecord/lib/active_record/relation/finder_methods.rb
+++ b/activerecord/lib/active_record/relation/finder_methods.rb
@@ -1,11 +1,11 @@
-require 'active_support/deprecation'
+require 'active_support/core_ext/string/filters'
module ActiveRecord
module FinderMethods
ONE_AS_ONE = '1 AS one'
# Find by id - This can either be a specific id (1), a list of ids (1, 5, 6), or an array of ids ([5, 6, 10]).
- # If no record can be found for all of the listed ids, then RecordNotFound will be raised. If the primary key
+ # If one or more records can not be found for the requested ids, then RecordNotFound will be raised. If the primary key
# is an integer, find by id coerces its arguments using +to_i+.
#
# Person.find(1) # returns the object for ID = 1
@@ -16,10 +16,8 @@ module ActiveRecord
# Person.find([1]) # returns an array for the object with ID = 1
# Person.where("administrator = 1").order("created_on DESC").find(1)
#
- # <tt>ActiveRecord::RecordNotFound</tt> will be raised if one or more ids are not found.
- #
# NOTE: The returned records may not be in the same order as the ids you
- # provide since database rows are unordered. You'd need to provide an explicit <tt>order</tt>
+ # provide since database rows are unordered. You'd need to provide an explicit QueryMethods#order
# option if you want the results are sorted.
#
# ==== Find with lock
@@ -36,7 +34,7 @@ module ActiveRecord
# person.save!
# end
#
- # ==== Variations of +find+
+ # ==== Variations of #find
#
# Person.where(name: 'Spartacus', rating: 4)
# # returns a chainable list (which can be empty).
@@ -48,9 +46,9 @@ module ActiveRecord
# # returns the first item or returns a new instance (requires you call .save to persist against the database).
#
# Person.where(name: 'Spartacus', rating: 4).first_or_create
- # # returns the first item or creates it and returns it, available since Rails 3.2.1.
+ # # returns the first item or creates it and returns it.
#
- # ==== Alternatives for +find+
+ # ==== Alternatives for #find
#
# Person.where(name: 'Spartacus', rating: 4).exists?(conditions = :none)
# # returns a boolean indicating if any record with the given conditions exist.
@@ -59,16 +57,13 @@ module ActiveRecord
# # returns a chainable list of instances with only the mentioned fields.
#
# Person.where(name: 'Spartacus', rating: 4).ids
- # # returns an Array of ids, available since Rails 3.2.1.
+ # # returns an Array of ids.
#
# Person.where(name: 'Spartacus', rating: 4).pluck(:field1, :field2)
- # # returns an Array of the required fields, available since Rails 3.1.
+ # # returns an Array of the required fields.
def find(*args)
- if block_given?
- to_a.find(*args) { |*block_args| yield(*block_args) }
- else
- find_with_ids(*args)
- end
+ return super if block_given?
+ find_with_ids(*args)
end
# Finds the first record matching the specified conditions. There
@@ -79,14 +74,19 @@ module ActiveRecord
#
# Post.find_by name: 'Spartacus', rating: 4
# Post.find_by "published_at < ?", 2.weeks.ago
- def find_by(*args)
- where(*args).take
+ def find_by(arg, *args)
+ where(arg, *args).take
+ rescue RangeError
+ nil
end
- # Like <tt>find_by</tt>, except that if no record is found, raises
- # an <tt>ActiveRecord::RecordNotFound</tt> error.
- def find_by!(*args)
- where(*args).take!
+ # Like #find_by, except that if no record is found, raises
+ # an ActiveRecord::RecordNotFound error.
+ def find_by!(arg, *args)
+ where(arg, *args).take!
+ rescue RangeError
+ raise RecordNotFound.new("Couldn't find #{@klass.name} with an out of range value",
+ @klass.name)
end
# Gives a record (or N records if a parameter is supplied) without any implied
@@ -100,32 +100,20 @@ module ActiveRecord
limit ? limit(limit).to_a : find_take
end
- # Same as +take+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record
- # is found. Note that <tt>take!</tt> accepts no arguments.
+ # Same as #take but raises ActiveRecord::RecordNotFound if no record
+ # is found. Note that #take! accepts no arguments.
def take!
- take or raise RecordNotFound
+ take or raise RecordNotFound.new("Couldn't find #{@klass.name} with [#{arel.where_sql(@klass.arel_engine)}]")
end
# Find the first record (or first N records if a parameter is supplied).
# If no order is defined it will order by primary key.
#
- # Person.first # returns the first object fetched by SELECT * FROM people
+ # Person.first # returns the first object fetched by SELECT * FROM people ORDER BY people.id LIMIT 1
# Person.where(["user_name = ?", user_name]).first
# Person.where(["user_name = :u", { u: user_name }]).first
# Person.order("created_on DESC").offset(5).first
- # Person.first(3) # returns the first three objects fetched by SELECT * FROM people LIMIT 3
- #
- # ==== Rails 3
- #
- # Person.first # SELECT "people".* FROM "people" LIMIT 1
- #
- # NOTE: Rails 3 may not order this query by the primary key and the order
- # will depend on the database implementation. In order to ensure that behavior,
- # use <tt>User.order(:id).first</tt> instead.
- #
- # ==== Rails 4
- #
- # Person.first # SELECT "people".* FROM "people" ORDER BY "people"."id" ASC LIMIT 1
+ # Person.first(3) # returns the first three objects fetched by SELECT * FROM people ORDER BY people.id LIMIT 3
#
def first(limit = nil)
if limit
@@ -135,10 +123,10 @@ module ActiveRecord
end
end
- # Same as +first+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record
- # is found. Note that <tt>first!</tt> accepts no arguments.
+ # Same as #first but raises ActiveRecord::RecordNotFound if no record
+ # is found. Note that #first! accepts no arguments.
def first!
- first or raise RecordNotFound
+ find_nth! 0
end
# Find the last record (or last N records if a parameter is supplied).
@@ -168,10 +156,10 @@ module ActiveRecord
end
end
- # Same as +last+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record
- # is found. Note that <tt>last!</tt> accepts no arguments.
+ # Same as #last but raises ActiveRecord::RecordNotFound if no record
+ # is found. Note that #last! accepts no arguments.
def last!
- last or raise RecordNotFound
+ last or raise RecordNotFound.new("Couldn't find #{@klass.name} with [#{arel.where_sql(@klass.arel_engine)}]")
end
# Find the second record.
@@ -184,10 +172,10 @@ module ActiveRecord
find_nth(1, offset_index)
end
- # Same as +second+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record
+ # Same as #second but raises ActiveRecord::RecordNotFound if no record
# is found.
def second!
- second or raise RecordNotFound
+ find_nth! 1
end
# Find the third record.
@@ -200,10 +188,10 @@ module ActiveRecord
find_nth(2, offset_index)
end
- # Same as +third+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record
+ # Same as #third but raises ActiveRecord::RecordNotFound if no record
# is found.
def third!
- third or raise RecordNotFound
+ find_nth! 2
end
# Find the fourth record.
@@ -216,10 +204,10 @@ module ActiveRecord
find_nth(3, offset_index)
end
- # Same as +fourth+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record
+ # Same as #fourth but raises ActiveRecord::RecordNotFound if no record
# is found.
def fourth!
- fourth or raise RecordNotFound
+ find_nth! 3
end
# Find the fifth record.
@@ -232,10 +220,10 @@ module ActiveRecord
find_nth(4, offset_index)
end
- # Same as +fifth+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record
+ # Same as #fifth but raises ActiveRecord::RecordNotFound if no record
# is found.
def fifth!
- fifth or raise RecordNotFound
+ find_nth! 4
end
# Find the forty-second record. Also known as accessing "the reddit".
@@ -248,14 +236,14 @@ module ActiveRecord
find_nth(41, offset_index)
end
- # Same as +forty_two+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record
+ # Same as #forty_two but raises ActiveRecord::RecordNotFound if no record
# is found.
def forty_two!
- forty_two or raise RecordNotFound
+ find_nth! 41
end
- # Returns +true+ if a record exists in the table that matches the +id+ or
- # conditions given, or +false+ otherwise. The argument can take six forms:
+ # Returns true if a record exists in the table that matches the +id+ or
+ # conditions given, or false otherwise. The argument can take six forms:
#
# * Integer - Finds the record with this primary key.
# * String - Finds the record with a primary key corresponding to this
@@ -268,7 +256,7 @@ module ActiveRecord
# * No args - Returns +false+ if the table is empty, +true+ otherwise.
#
# For more information about specifying conditions as a hash or array,
- # see the Conditions section in the introduction to <tt>ActiveRecord::Base</tt>.
+ # see the Conditions section in the introduction to ActiveRecord::Base.
#
# Note: You can't pass in a condition as a string (like <tt>name =
# 'Jamie'</tt>), since it would be sanitized and then queried against
@@ -284,8 +272,10 @@ module ActiveRecord
def exists?(conditions = :none)
if Base === conditions
conditions = conditions.id
- ActiveSupport::Deprecation.warn "You are passing an instance of ActiveRecord::Base to `exists?`." \
- "Please pass the id of the object by calling `.id`"
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ You are passing an instance of ActiveRecord::Base to `exists?`.
+ Please pass the id of the object by calling `.id`
+ MSG
end
return false if !conditions
@@ -300,15 +290,15 @@ module ActiveRecord
relation = relation.where(conditions)
else
unless conditions == :none
- relation = where(primary_key => conditions)
+ relation = relation.where(primary_key => conditions)
end
end
- connection.select_value(relation, "#{name} Exists", relation.bind_values) ? true : false
+ connection.select_value(relation, "#{name} Exists", relation.bound_attributes) ? true : false
end
# This method is called whenever no records are found with either a single
- # id or multiple ids and raises a +ActiveRecord::RecordNotFound+ exception.
+ # id or multiple ids and raises a ActiveRecord::RecordNotFound exception.
#
# The error message is different depending on whether a single id or
# multiple ids are provided. If multiple ids are provided, then the number
@@ -316,7 +306,7 @@ module ActiveRecord
# the expected number of results should be provided in the +expected_size+
# argument.
def raise_record_not_found_exception!(ids, result_size, expected_size) #:nodoc:
- conditions = arel.where_sql
+ conditions = arel.where_sql(@klass.arel_engine)
conditions = " [#{conditions}]" if conditions
if Array(ids).size == 1
@@ -358,7 +348,7 @@ module ActiveRecord
[]
else
arel = relation.arel
- rows = connection.select_all(arel, 'SQL', arel.bind_values + relation.bind_values)
+ rows = connection.select_all(arel, 'SQL', relation.bound_attributes)
join_dependency.instantiate(rows, aliases)
end
end
@@ -372,7 +362,7 @@ module ActiveRecord
def construct_relation_for_association_calculations
from = arel.froms.first
if Arel::Table === from
- apply_join_dependency(self, construct_join_dependency)
+ apply_join_dependency(self, construct_join_dependency(joins_values))
else
# FIXME: as far as I can tell, `from` will always be an Arel::Table.
# There are no tests that test this branch, but presumably it's
@@ -390,7 +380,7 @@ module ActiveRecord
else
if relation.limit_value
limited_ids = limited_ids_for(relation)
- limited_ids.empty? ? relation.none! : relation.where!(table[primary_key].in(limited_ids))
+ limited_ids.empty? ? relation.none! : relation.where!(primary_key => limited_ids)
end
relation.except(:limit, :offset)
end
@@ -401,13 +391,14 @@ module ActiveRecord
"#{quoted_table_name}.#{quoted_primary_key}", relation.order_values)
relation = relation.except(:select).select(values).distinct!
+ arel = relation.arel
- id_rows = @klass.connection.select_all(relation.arel, 'SQL', relation.bind_values)
+ id_rows = @klass.connection.select_all(arel, 'SQL', relation.bound_attributes)
id_rows.map {|row| row[primary_key]}
end
def using_limitable_reflections?(reflections)
- reflections.none? { |r| r.collection? }
+ reflections.none?(&:collection?)
end
protected
@@ -429,19 +420,20 @@ module ActiveRecord
else
find_some(ids)
end
+ rescue RangeError
+ raise RecordNotFound, "Couldn't find #{@klass.name} with an out of range ID"
end
def find_one(id)
if ActiveRecord::Base === id
id = id.id
- ActiveSupport::Deprecation.warn "You are passing an instance of ActiveRecord::Base to `find`." \
- "Please pass the id of the object by calling `.id`"
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ You are passing an instance of ActiveRecord::Base to `find`.
+ Please pass the id of the object by calling `.id`
+ MSG
end
- column = columns_hash[primary_key]
- substitute = connection.substitute_at(column, bind_values.length)
- relation = where(table[primary_key].eq(substitute))
- relation.bind_values += [[column, id]]
+ relation = where(primary_key => id)
record = relation.take
raise_record_not_found_exception!(id, 0, 1) unless record
@@ -450,7 +442,7 @@ module ActiveRecord
end
def find_some(ids)
- result = where(table[primary_key].in(ids)).to_a
+ result = where(primary_key => ids).to_a
expected_size =
if limit_value && ids.size > limit_value
@@ -488,6 +480,10 @@ module ActiveRecord
end
end
+ def find_nth!(index)
+ find_nth(index, offset_index) or raise RecordNotFound.new("Couldn't find #{@klass.name} with [#{arel.where_sql(@klass.arel_engine)}]")
+ end
+
def find_nth_with_limit(offset, limit)
relation = if order_values.empty? && primary_key
order(arel_table[primary_key].asc)
diff --git a/activerecord/lib/active_record/relation/from_clause.rb b/activerecord/lib/active_record/relation/from_clause.rb
new file mode 100644
index 0000000000..92340216ed
--- /dev/null
+++ b/activerecord/lib/active_record/relation/from_clause.rb
@@ -0,0 +1,32 @@
+module ActiveRecord
+ class Relation
+ class FromClause # :nodoc:
+ attr_reader :value, :name
+
+ def initialize(value, name)
+ @value = value
+ @name = name
+ end
+
+ def binds
+ if value.is_a?(Relation)
+ value.bound_attributes
+ else
+ []
+ end
+ end
+
+ def merge(other)
+ self
+ end
+
+ def empty?
+ value.nil?
+ end
+
+ def self.empty
+ new(nil, nil)
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/relation/merger.rb b/activerecord/lib/active_record/relation/merger.rb
index ac41d0aa80..cb971eb255 100644
--- a/activerecord/lib/active_record/relation/merger.rb
+++ b/activerecord/lib/active_record/relation/merger.rb
@@ -1,5 +1,4 @@
require 'active_support/core_ext/hash/keys'
-require "set"
module ActiveRecord
class Relation
@@ -13,7 +12,7 @@ module ActiveRecord
@hash = hash
end
- def merge
+ def merge #:nodoc:
Merger.new(relation, other).merge
end
@@ -22,7 +21,7 @@ module ActiveRecord
# build a relation to merge in rather than directly merging
# the values.
def other
- other = Relation.create(relation.klass, relation.table)
+ other = Relation.create(relation.klass, relation.table, relation.predicate_builder)
hash.each { |k, v|
if k == :joins
if Hash === v
@@ -49,9 +48,9 @@ module ActiveRecord
@other = other
end
- NORMAL_VALUES = Relation::SINGLE_VALUE_METHODS +
- Relation::MULTI_VALUE_METHODS -
- [:joins, :where, :order, :bind, :reverse_order, :lock, :create_with, :reordering, :from] # :nodoc:
+ NORMAL_VALUES = Relation::VALUE_METHODS -
+ Relation::CLAUSE_METHODS -
+ [:includes, :preload, :joins, :order, :reverse_order, :lock, :create_with, :reordering] # :nodoc:
def normal_values
NORMAL_VALUES
@@ -75,6 +74,8 @@ module ActiveRecord
merge_multi_values
merge_single_values
+ merge_clauses
+ merge_preloads
merge_joins
relation
@@ -82,13 +83,34 @@ module ActiveRecord
private
+ def merge_preloads
+ return if other.preload_values.empty? && other.includes_values.empty?
+
+ if other.klass == relation.klass
+ relation.preload!(*other.preload_values) unless other.preload_values.empty?
+ relation.includes!(other.includes_values) unless other.includes_values.empty?
+ else
+ reflection = relation.klass.reflect_on_all_associations.find do |r|
+ r.class_name == other.klass.name
+ end || return
+
+ unless other.preload_values.empty?
+ relation.preload! reflection.name => other.preload_values
+ end
+
+ unless other.includes_values.empty?
+ relation.includes! reflection.name => other.includes_values
+ end
+ end
+ end
+
def merge_joins
- return if values[:joins].blank?
+ return if other.joins_values.blank?
if other.klass == relation.klass
- relation.joins!(*values[:joins])
+ relation.joins!(*other.joins_values)
else
- joins_dependency, rest = values[:joins].partition do |join|
+ joins_dependency, rest = other.joins_values.partition do |join|
case join
when Hash, Symbol, Array
true
@@ -107,74 +129,34 @@ module ActiveRecord
end
def merge_multi_values
- lhs_wheres = relation.where_values
- rhs_wheres = values[:where] || []
-
- lhs_binds = relation.bind_values
- rhs_binds = values[:bind] || []
-
- removed, kept = partition_overwrites(lhs_wheres, rhs_wheres)
-
- where_values = kept + rhs_wheres
- bind_values = filter_binds(lhs_binds, removed) + rhs_binds
-
- conn = relation.klass.connection
- bv_index = 0
- where_values.map! do |node|
- if Arel::Nodes::Equality === node && Arel::Nodes::BindParam === node.right
- substitute = conn.substitute_at(bind_values[bv_index].first, bv_index)
- bv_index += 1
- Arel::Nodes::Equality.new(node.left, substitute)
- else
- node
- end
- end
-
- relation.where_values = where_values
- relation.bind_values = bind_values
-
- if values[:reordering]
+ if other.reordering_value
# override any order specified in the original relation
- relation.reorder! values[:order]
- elsif values[:order]
+ relation.reorder! other.order_values
+ elsif other.order_values
# merge in order_values from relation
- relation.order! values[:order]
+ relation.order! other.order_values
end
- relation.extend(*values[:extending]) unless values[:extending].blank?
+ relation.extend(*other.extending_values) unless other.extending_values.blank?
end
def merge_single_values
- relation.from_value = values[:from] unless relation.from_value
- relation.lock_value = values[:lock] unless relation.lock_value
+ relation.lock_value ||= other.lock_value
- unless values[:create_with].blank?
- relation.create_with_value = (relation.create_with_value || {}).merge(values[:create_with])
+ unless other.create_with_value.blank?
+ relation.create_with_value = (relation.create_with_value || {}).merge(other.create_with_value)
end
end
- def filter_binds(lhs_binds, removed_wheres)
- return lhs_binds if removed_wheres.empty?
-
- set = Set.new removed_wheres.map { |x| x.left.name.to_s }
- lhs_binds.dup.delete_if { |col,_| set.include? col.name }
+ CLAUSE_METHOD_NAMES = CLAUSE_METHODS.map do |name|
+ ["#{name}_clause", "#{name}_clause="]
end
- # Remove equalities from the existing relation with a LHS which is
- # present in the relation being merged in.
- # returns [things_to_remove, things_to_keep]
- def partition_overwrites(lhs_wheres, rhs_wheres)
- if lhs_wheres.empty? || rhs_wheres.empty?
- return [[], lhs_wheres]
- end
-
- nodes = rhs_wheres.find_all do |w|
- w.respond_to?(:operator) && w.operator == :==
- end
- seen = Set.new(nodes) { |node| node.left }
-
- lhs_wheres.partition do |w|
- w.respond_to?(:operator) && w.operator == :== && seen.include?(w.left)
+ def merge_clauses
+ CLAUSE_METHOD_NAMES.each do |(reader, writer)|
+ clause = relation.send(reader)
+ other_clause = other.send(reader)
+ relation.send(writer, clause.merge(other_clause))
end
end
end
diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb
index eff5c8f09c..39e7b42629 100644
--- a/activerecord/lib/active_record/relation/predicate_builder.rb
+++ b/activerecord/lib/active_record/relation/predicate_builder.rb
@@ -1,82 +1,49 @@
module ActiveRecord
class PredicateBuilder # :nodoc:
- @handlers = []
-
- autoload :RelationHandler, 'active_record/relation/predicate_builder/relation_handler'
- autoload :ArrayHandler, 'active_record/relation/predicate_builder/array_handler'
-
- def self.resolve_column_aliases(klass, hash)
- hash = hash.dup
- hash.keys.grep(Symbol) do |key|
- if klass.attribute_alias? key
- hash[klass.attribute_alias(key)] = hash.delete key
- end
- end
- hash
+ require 'active_record/relation/predicate_builder/array_handler'
+ require 'active_record/relation/predicate_builder/association_query_handler'
+ require 'active_record/relation/predicate_builder/base_handler'
+ require 'active_record/relation/predicate_builder/basic_object_handler'
+ require 'active_record/relation/predicate_builder/class_handler'
+ require 'active_record/relation/predicate_builder/range_handler'
+ require 'active_record/relation/predicate_builder/relation_handler'
+
+ delegate :resolve_column_aliases, to: :table
+
+ def initialize(table)
+ @table = table
+ @handlers = []
+
+ register_handler(BasicObject, BasicObjectHandler.new(self))
+ register_handler(Class, ClassHandler.new(self))
+ register_handler(Base, BaseHandler.new(self))
+ register_handler(Range, RangeHandler.new(self))
+ register_handler(Relation, RelationHandler.new)
+ register_handler(Array, ArrayHandler.new(self))
+ register_handler(AssociationQueryValue, AssociationQueryHandler.new(self))
end
- def self.build_from_hash(klass, attributes, default_table)
- queries = []
-
- attributes.each do |column, value|
- table = default_table
-
- if value.is_a?(Hash)
- if value.empty?
- queries << '1=0'
- else
- table = Arel::Table.new(column, default_table.engine)
- association = klass._reflect_on_association(column.to_sym)
-
- value.each do |k, v|
- queries.concat expand(association && association.klass, table, k, v)
- end
- end
- else
- column = column.to_s
-
- if column.include?('.')
- table_name, column = column.split('.', 2)
- table = Arel::Table.new(table_name, default_table.engine)
- end
-
- queries.concat expand(klass, table, column, value)
- end
- end
-
- queries
+ def build_from_hash(attributes)
+ attributes = convert_dot_notation_to_hash(attributes)
+ expand_from_hash(attributes)
end
- def self.expand(klass, table, column, value)
- queries = []
+ def create_binds(attributes)
+ attributes = convert_dot_notation_to_hash(attributes)
+ create_binds_for_hash(attributes)
+ end
+ def expand(column, value)
# Find the foreign key when using queries such as:
# Post.where(author: author)
#
# For polymorphic relationships, find the foreign key and type:
# PriceEstimate.where(estimate_of: treasure)
- if klass && reflection = klass._reflect_on_association(column.to_sym)
- if reflection.polymorphic? && base_class = polymorphic_base_class_from_value(value)
- queries << build(table[reflection.foreign_type], base_class)
- end
-
- column = reflection.foreign_key
+ if table.associated_with?(column)
+ value = AssociationQueryValue.new(table.associated_table(column), value)
end
- queries << build(table[column], value)
- queries
- end
-
- def self.polymorphic_base_class_from_value(value)
- case value
- when Relation
- value.klass.base_class
- when Array
- val = value.compact.first
- val.class.base_class if val.is_a?(Base)
- when Base
- value.class.base_class
- end
+ build(table.arel_attribute(column), value)
end
def self.references(attributes)
@@ -85,7 +52,7 @@ module ActiveRecord
key
else
key = key.to_s
- key.split('.').first if key.include?('.')
+ key.split('.'.freeze).first if key.include?('.'.freeze)
end
end.compact
end
@@ -100,27 +67,83 @@ module ActiveRecord
# Arel::Nodes::And.new([range.start, range.end])
# )
# end
- # ActiveRecord::PredicateBuilder.register_handler(MyCustomDateRange, handler)
- def self.register_handler(klass, handler)
+ # ActiveRecord::PredicateBuilder.new("users").register_handler(MyCustomDateRange, handler)
+ def register_handler(klass, handler)
@handlers.unshift([klass, handler])
end
- register_handler(BasicObject, ->(attribute, value) { attribute.eq(value) })
- # FIXME: I think we need to deprecate this behavior
- register_handler(Class, ->(attribute, value) { attribute.eq(value.name) })
- register_handler(Base, ->(attribute, value) { attribute.eq(value.id) })
- register_handler(Range, ->(attribute, value) { attribute.in(value) })
- register_handler(Relation, RelationHandler.new)
- register_handler(Array, ArrayHandler.new)
-
- def self.build(attribute, value)
+ def build(attribute, value)
handler_for(value).call(attribute, value)
end
- private_class_method :build
- def self.handler_for(object)
+ protected
+
+ attr_reader :table
+
+ def expand_from_hash(attributes)
+ return ["1=0"] if attributes.empty?
+
+ attributes.flat_map do |key, value|
+ if value.is_a?(Hash)
+ associated_predicate_builder(key).expand_from_hash(value)
+ else
+ expand(key, value)
+ end
+ end
+ end
+
+
+ def create_binds_for_hash(attributes)
+ result = attributes.dup
+ binds = []
+
+ attributes.each do |column_name, value|
+ case value
+ when Hash
+ attrs, bvs = associated_predicate_builder(column_name).create_binds_for_hash(value)
+ result[column_name] = attrs
+ binds += bvs
+ when Relation
+ binds += value.bound_attributes
+ else
+ if can_be_bound?(column_name, value)
+ result[column_name] = Arel::Nodes::BindParam.new
+ binds << Relation::QueryAttribute.new(column_name.to_s, value, table.type(column_name))
+ end
+ end
+ end
+
+ [result, binds]
+ end
+
+ private
+
+ def associated_predicate_builder(association_name)
+ self.class.new(table.associated_table(association_name))
+ end
+
+ def convert_dot_notation_to_hash(attributes)
+ dot_notation = attributes.keys.select { |s| s.include?(".".freeze) }
+
+ dot_notation.each do |key|
+ table_name, column_name = key.split(".".freeze)
+ value = attributes.delete(key)
+ attributes[table_name] ||= {}
+
+ attributes[table_name] = attributes[table_name].merge(column_name => value)
+ end
+
+ attributes
+ end
+
+ def handler_for(object)
@handlers.detect { |klass, _| klass === object }.last
end
- private_class_method :handler_for
+
+ def can_be_bound?(column_name, value)
+ !value.nil? &&
+ handler_for(value).is_a?(BasicObjectHandler) &&
+ !table.associated_with?(column_name)
+ end
end
end
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 78dba8be06..95dbd6a77f 100644
--- a/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb
+++ b/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb
@@ -1,30 +1,39 @@
module ActiveRecord
class PredicateBuilder
class ArrayHandler # :nodoc:
- def call(attribute, value)
- return attribute.in([]) if value.empty?
+ def initialize(predicate_builder)
+ @predicate_builder = predicate_builder
+ end
+ def call(attribute, value)
values = value.map { |x| x.is_a?(Base) ? x.id : x }
- ranges, values = values.partition { |v| v.is_a?(Range) }
nils, values = values.partition(&:nil?)
+ return attribute.in([]) if values.empty? && nils.empty?
+
+ ranges, values = values.partition { |v| v.is_a?(Range) }
+
values_predicate =
case values.length
when 0 then NullPredicate
- when 1 then attribute.eq(values.first)
+ when 1 then predicate_builder.build(attribute, values.first)
else attribute.in(values)
end
unless nils.empty?
- values_predicate = values_predicate.or(attribute.eq(nil))
+ values_predicate = values_predicate.or(predicate_builder.build(attribute, nil))
end
- array_predicates = ranges.map { |range| attribute.in(range) }
- array_predicates << values_predicate
+ array_predicates = ranges.map { |range| predicate_builder.build(attribute, range) }
+ array_predicates.unshift(values_predicate)
array_predicates.inject { |composite, predicate| composite.or(predicate) }
end
- module NullPredicate
+ protected
+
+ attr_reader :predicate_builder
+
+ module NullPredicate # :nodoc:
def self.or(other)
other
end
diff --git a/activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb
new file mode 100644
index 0000000000..e81be63cd3
--- /dev/null
+++ b/activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb
@@ -0,0 +1,78 @@
+module ActiveRecord
+ class PredicateBuilder
+ class AssociationQueryHandler # :nodoc:
+ def initialize(predicate_builder)
+ @predicate_builder = predicate_builder
+ end
+
+ def call(attribute, value)
+ queries = {}
+
+ table = value.associated_table
+ if value.base_class
+ queries[table.association_foreign_type.to_s] = value.base_class.name
+ end
+
+ queries[table.association_foreign_key.to_s] = value.ids
+ predicate_builder.build_from_hash(queries)
+ end
+
+ protected
+
+ attr_reader :predicate_builder
+ end
+
+ class AssociationQueryValue # :nodoc:
+ attr_reader :associated_table, :value
+
+ def initialize(associated_table, value)
+ @associated_table = associated_table
+ @value = value
+ end
+
+ def ids
+ case value
+ when Relation
+ value.select(primary_key)
+ when Array
+ value.map { |v| convert_to_id(v) }
+ else
+ convert_to_id(value)
+ end
+ end
+
+ def base_class
+ if associated_table.polymorphic_association?
+ @base_class ||= polymorphic_base_class_from_value
+ end
+ end
+
+ private
+
+ def primary_key
+ associated_table.association_primary_key(base_class)
+ end
+
+ def polymorphic_base_class_from_value
+ case value
+ when Relation
+ value.klass.base_class
+ when Array
+ val = value.compact.first
+ val.class.base_class if val.is_a?(Base)
+ when Base
+ value.class.base_class
+ end
+ end
+
+ def convert_to_id(value)
+ case value
+ when Base
+ value._read_attribute(primary_key)
+ else
+ value
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/relation/predicate_builder/base_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/base_handler.rb
new file mode 100644
index 0000000000..6fa5b16f73
--- /dev/null
+++ b/activerecord/lib/active_record/relation/predicate_builder/base_handler.rb
@@ -0,0 +1,17 @@
+module ActiveRecord
+ class PredicateBuilder
+ class BaseHandler # :nodoc:
+ def initialize(predicate_builder)
+ @predicate_builder = predicate_builder
+ end
+
+ def call(attribute, value)
+ predicate_builder.build(attribute, value.id)
+ end
+
+ protected
+
+ attr_reader :predicate_builder
+ end
+ 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
new file mode 100644
index 0000000000..6cec75dc0a
--- /dev/null
+++ b/activerecord/lib/active_record/relation/predicate_builder/basic_object_handler.rb
@@ -0,0 +1,17 @@
+module ActiveRecord
+ class PredicateBuilder
+ class BasicObjectHandler # :nodoc:
+ def initialize(predicate_builder)
+ @predicate_builder = predicate_builder
+ end
+
+ def call(attribute, value)
+ attribute.eq(value)
+ end
+
+ protected
+
+ attr_reader :predicate_builder
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/relation/predicate_builder/class_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/class_handler.rb
new file mode 100644
index 0000000000..ed313fc9d4
--- /dev/null
+++ b/activerecord/lib/active_record/relation/predicate_builder/class_handler.rb
@@ -0,0 +1,27 @@
+module ActiveRecord
+ class PredicateBuilder
+ class ClassHandler # :nodoc:
+ def initialize(predicate_builder)
+ @predicate_builder = predicate_builder
+ end
+
+ def call(attribute, value)
+ print_deprecation_warning
+ predicate_builder.build(attribute, value.name)
+ end
+
+ protected
+
+ attr_reader :predicate_builder
+
+ private
+
+ def print_deprecation_warning
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ Passing a class as a value in an Active Record query is deprecated and
+ will be removed. Pass a string instead.
+ MSG
+ end
+ end
+ 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
new file mode 100644
index 0000000000..1b3849e3ad
--- /dev/null
+++ b/activerecord/lib/active_record/relation/predicate_builder/range_handler.rb
@@ -0,0 +1,17 @@
+module ActiveRecord
+ class PredicateBuilder
+ class RangeHandler # :nodoc:
+ def initialize(predicate_builder)
+ @predicate_builder = predicate_builder
+ end
+
+ def call(attribute, value)
+ attribute.between(value)
+ end
+
+ protected
+
+ attr_reader :predicate_builder
+ end
+ 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 618fa3cdd9..063150958a 100644
--- a/activerecord/lib/active_record/relation/predicate_builder/relation_handler.rb
+++ b/activerecord/lib/active_record/relation/predicate_builder/relation_handler.rb
@@ -6,7 +6,7 @@ module ActiveRecord
value = value.select(value.klass.arel_table[value.klass.primary_key])
end
- attribute.in(value.arel.ast)
+ attribute.in(value.arel)
end
end
end
diff --git a/activerecord/lib/active_record/relation/query_attribute.rb b/activerecord/lib/active_record/relation/query_attribute.rb
new file mode 100644
index 0000000000..7ba964e802
--- /dev/null
+++ b/activerecord/lib/active_record/relation/query_attribute.rb
@@ -0,0 +1,19 @@
+require 'active_record/attribute'
+
+module ActiveRecord
+ class Relation
+ class QueryAttribute < Attribute # :nodoc:
+ def type_cast(value)
+ value
+ end
+
+ def value_for_database
+ @value_for_database ||= super
+ end
+
+ def with_cast_value(value)
+ QueryAttribute.new(name, value, type)
+ 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 1262b2c291..f5afc1000d 100644
--- a/activerecord/lib/active_record/relation/query_methods.rb
+++ b/activerecord/lib/active_record/relation/query_methods.rb
@@ -1,9 +1,15 @@
-require 'active_support/core_ext/array/wrap'
+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
module QueryMethods
extend ActiveSupport::Concern
+ include ActiveModel::ForbiddenAttributesProtection
+
# WhereChain objects act as placeholder for queries in which #where does not have any parameter.
# In this case, #where must be chained with #not to return a new relation.
class WhereChain
@@ -14,7 +20,7 @@ module ActiveRecord
# Returns a new relation expressing WHERE + NOT condition according to
# the conditions in the arguments.
#
- # +not+ accepts conditions as a string, array, or hash. See #where for
+ # #not accepts conditions as a string, array, or hash. See QueryMethods#where for
# more details on each format.
#
# User.where.not("name = 'Jon'")
@@ -35,38 +41,24 @@ module ActiveRecord
# User.where.not(name: "Jon", role: "admin")
# # SELECT * FROM users WHERE name != 'Jon' AND role != 'admin'
def not(opts, *rest)
- where_value = @scope.send(:build_where, opts, rest).map do |rel|
- case rel
- when NilClass
- raise ArgumentError, 'Invalid argument for .where.not(), got nil.'
- when Arel::Nodes::In
- Arel::Nodes::NotIn.new(rel.left, rel.right)
- when Arel::Nodes::Equality
- Arel::Nodes::NotEqual.new(rel.left, rel.right)
- when String
- Arel::Nodes::Not.new(Arel::Nodes::SqlLiteral.new(rel))
- else
- Arel::Nodes::Not.new(rel)
- end
- end
+ where_clause = @scope.send(:where_clause_factory).build(opts, rest)
@scope.references!(PredicateBuilder.references(opts)) if Hash === opts
- @scope.where_values += where_value
+ @scope.where_clause += where_clause.invert
@scope
end
end
Relation::MULTI_VALUE_METHODS.each do |name|
class_eval <<-CODE, __FILE__, __LINE__ + 1
- def #{name}_values # def select_values
- @values[:#{name}] || [] # @values[:select] || []
- end # end
- #
- def #{name}_values=(values) # def select_values=(values)
- raise ImmutableRelation if @loaded # raise ImmutableRelation if @loaded
- check_cached_relation
- @values[:#{name}] = values # @values[:select] = values
- end # end
+ def #{name}_values # def select_values
+ @values[:#{name}] || [] # @values[:select] || []
+ end # end
+ #
+ def #{name}_values=(values) # def select_values=(values)
+ assert_mutability! # assert_mutability!
+ @values[:#{name}] = values # @values[:select] = values
+ end # end
CODE
end
@@ -81,21 +73,27 @@ module ActiveRecord
Relation::SINGLE_VALUE_METHODS.each do |name|
class_eval <<-CODE, __FILE__, __LINE__ + 1
def #{name}_value=(value) # def readonly_value=(value)
- raise ImmutableRelation if @loaded # raise ImmutableRelation if @loaded
- check_cached_relation
+ assert_mutability! # assert_mutability!
@values[:#{name}] = value # @values[:readonly] = value
end # end
CODE
end
- def check_cached_relation # :nodoc:
- if defined?(@arel) && @arel
- @arel = nil
- ActiveSupport::Deprecation.warn <<-WARNING
-Modifying already cached Relation. The cache will be reset.
-Use a cloned Relation to prevent this warning.
-WARNING
- end
+ Relation::CLAUSE_METHODS.each do |name|
+ class_eval <<-CODE, __FILE__, __LINE__ + 1
+ def #{name}_clause # def where_clause
+ @values[:#{name}] || new_#{name}_clause # @values[:where] || new_where_clause
+ end # end
+ #
+ def #{name}_clause=(value) # def where_clause=(value)
+ assert_mutability! # assert_mutability!
+ @values[:#{name}] = value # @values[:where] = value
+ end # end
+ CODE
+ end
+
+ def bound_attributes
+ from_clause.binds + arel.bind_values + where_clause.binds + having_clause.binds
end
def create_with_value # :nodoc:
@@ -114,7 +112,7 @@ WARNING
#
# allows you to access the +address+ attribute of the +User+ model without
# firing an additional query. This will often result in a
- # performance improvement over a simple +join+.
+ # performance improvement over a simple join.
#
# You can also specify multiple relationships, like this:
#
@@ -135,7 +133,7 @@ WARNING
#
# User.includes(:posts).where('posts.name = ?', 'example').references(:posts)
#
- # Note that +includes+ works with association names while +references+ needs
+ # Note that #includes works with association names while #references needs
# the actual table name.
def includes(*args)
check_if_method_has_arguments!(:includes, args)
@@ -153,9 +151,9 @@ WARNING
# Forces eager loading by performing a LEFT OUTER JOIN on +args+:
#
# User.eager_load(:posts)
- # => SELECT "users"."id" AS t0_r0, "users"."name" AS t0_r1, ...
- # FROM "users" LEFT OUTER JOIN "posts" ON "posts"."user_id" =
- # "users"."id"
+ # # SELECT "users"."id" AS t0_r0, "users"."name" AS t0_r1, ...
+ # # FROM "users" LEFT OUTER JOIN "posts" ON "posts"."user_id" =
+ # # "users"."id"
def eager_load(*args)
check_if_method_has_arguments!(:eager_load, args)
spawn.eager_load!(*args)
@@ -166,10 +164,10 @@ WARNING
self
end
- # Allows preloading of +args+, in the same way that +includes+ does:
+ # Allows preloading of +args+, in the same way that #includes does:
#
# User.preload(:posts)
- # => SELECT "posts".* FROM "posts" WHERE "posts"."user_id" IN (1, 2, 3)
+ # # SELECT "posts".* FROM "posts" WHERE "posts"."user_id" IN (1, 2, 3)
def preload(*args)
check_if_method_has_arguments!(:preload, args)
spawn.preload!(*args)
@@ -182,14 +180,14 @@ WARNING
# Use to indicate that the given +table_names+ are referenced by an SQL string,
# and should therefore be JOINed in any query rather than loaded separately.
- # This method only works in conjunction with +includes+.
+ # This method only works in conjunction with #includes.
# See #includes for more details.
#
# User.includes(:posts).where("posts.name = 'foo'")
- # # => Doesn't JOIN the posts table, resulting in an error.
+ # # Doesn't JOIN the posts table, resulting in an error.
#
# User.includes(:posts).where("posts.name = 'foo'").references(:posts)
- # # => Query now knows the string references posts, so adds a JOIN
+ # # Query now knows the string references posts, so adds a JOIN
def references(*table_names)
check_if_method_has_arguments!(:references, table_names)
spawn.references!(*table_names)
@@ -205,12 +203,12 @@ WARNING
# Works in two unique ways.
#
- # First: takes a block so it can be used just like Array#select.
+ # First: takes a block so it can be used just like +Array#select+.
#
# Model.all.select { |m| m.field == value }
#
# This will build an array of objects from the database for the scope,
- # converting them into an array and iterating through them using Array#select.
+ # converting them into an array and iterating through them using +Array#select+.
#
# Second: Modifies the SELECT statement for the query so that only certain
# fields are retrieved:
@@ -238,23 +236,20 @@ WARNING
# # => "value"
#
# Accessing attributes of an object that do not have fields retrieved by a select
- # except +id+ will throw <tt>ActiveModel::MissingAttributeError</tt>:
+ # except +id+ will throw ActiveModel::MissingAttributeError:
#
# Model.select(:field).first.other_field
# # => ActiveModel::MissingAttributeError: missing attribute: other_field
def select(*fields)
- if block_given?
- to_a.select { |*block_args| yield(*block_args) }
- else
- raise ArgumentError, 'Call this with at least one field' if fields.empty?
- spawn._select!(*fields)
- end
+ return super if block_given?
+ raise ArgumentError, 'Call this with at least one field' if fields.empty?
+ spawn._select!(*fields)
end
def _select!(*fields) # :nodoc:
fields.flatten!
fields.map! do |field|
- klass.attribute_alias?(field) ? klass.attribute_alias(field) : field
+ klass.attribute_alias?(field) ? klass.attribute_alias(field).to_sym : field
end
self.select_values += fields
self
@@ -263,22 +258,23 @@ WARNING
# Allows to specify a group attribute:
#
# User.group(:name)
- # => SELECT "users".* FROM "users" GROUP BY name
+ # # SELECT "users".* FROM "users" GROUP BY name
#
# Returns an array with distinct records based on the +group+ attribute:
#
# User.select([:id, :name])
- # => [#<User id: 1, name: "Oscar">, #<User id: 2, name: "Oscar">, #<User id: 3, name: "Foo">
+ # # => [#<User id: 1, name: "Oscar">, #<User id: 2, name: "Oscar">, #<User id: 3, name: "Foo">]
#
# User.group(:name)
- # => [#<User id: 3, name: "Foo", ...>, #<User id: 2, name: "Oscar", ...>]
+ # # => [#<User id: 3, name: "Foo", ...>, #<User id: 2, name: "Oscar", ...>]
#
# User.group('name AS grouped_name, age')
- # => [#<User id: 3, name: "Foo", age: 21, ...>, #<User id: 2, name: "Oscar", age: 21, ...>, #<User id: 5, name: "Foo", age: 23, ...>]
+ # # => [#<User id: 3, name: "Foo", age: 21, ...>, #<User id: 2, name: "Oscar", age: 21, ...>, #<User id: 5, name: "Foo", age: 23, ...>]
#
# Passing in an array of attributes to group by is also supported.
+ #
# User.select([:id, :first_name]).group(:id, :first_name).first(3)
- # => [#<User id: 1, first_name: "Bill">, #<User id: 2, first_name: "Earl">, #<User id: 3, first_name: "Beto">]
+ # # => [#<User id: 1, first_name: "Bill">, #<User id: 2, first_name: "Earl">, #<User id: 3, first_name: "Beto">]
def group(*args)
check_if_method_has_arguments!(:group, args)
spawn.group!(*args)
@@ -294,22 +290,22 @@ WARNING
# Allows to specify an order attribute:
#
# User.order(:name)
- # => SELECT "users".* FROM "users" ORDER BY "users"."name" ASC
+ # # SELECT "users".* FROM "users" ORDER BY "users"."name" ASC
#
# User.order(email: :desc)
- # => SELECT "users".* FROM "users" ORDER BY "users"."email" DESC
+ # # SELECT "users".* FROM "users" ORDER BY "users"."email" DESC
#
# User.order(:name, email: :desc)
- # => SELECT "users".* FROM "users" ORDER BY "users"."name" ASC, "users"."email" DESC
+ # # SELECT "users".* FROM "users" ORDER BY "users"."name" ASC, "users"."email" DESC
#
# User.order('name')
- # => SELECT "users".* FROM "users" ORDER BY name
+ # # SELECT "users".* FROM "users" ORDER BY name
#
# User.order('name DESC')
- # => SELECT "users".* FROM "users" ORDER BY name DESC
+ # # SELECT "users".* FROM "users" ORDER BY name DESC
#
# User.order('name DESC, email')
- # => SELECT "users".* FROM "users" ORDER BY name DESC, email
+ # # SELECT "users".* FROM "users" ORDER BY name DESC, email
def order(*args)
check_if_method_has_arguments!(:order, args)
spawn.order!(*args)
@@ -361,15 +357,15 @@ WARNING
# User.order('email DESC').select('id').where(name: "John")
# .unscope(:order, :select, :where) == User.all
#
- # One can additionally pass a hash as an argument to unscope specific :where values.
+ # One can additionally pass a hash as an argument to unscope specific +:where+ values.
# This is done by passing a hash with a single key-value pair. The key should be
- # :where and the value should be the where value to unscope. For example:
+ # +:where+ and the value should be the where value to unscope. For example:
#
# User.where(name: "John", active: true).unscope(where: :name)
# == User.where(active: true)
#
- # This method is similar to <tt>except</tt>, but unlike
- # <tt>except</tt>, it persists across merges:
+ # This method is similar to #except, but unlike
+ # #except, it persists across merges:
#
# User.order('email').merge(User.except(:order))
# == User.order('email')
@@ -379,7 +375,7 @@ WARNING
#
# This means it can be used in association definitions:
#
- # has_many :comments, -> { unscope where: :trashed }
+ # has_many :comments, -> { unscope(where: :trashed) }
#
def unscope(*args)
check_if_method_has_arguments!(:unscope, args)
@@ -400,9 +396,8 @@ WARNING
raise ArgumentError, "Hash arguments in .unscope(*args) must have :where as the key."
end
- Array(target_value).each do |val|
- where_unscoping(val)
- end
+ target_values = Array(target_value).map(&:to_s)
+ self.where_clause = where_clause.except(*target_values)
end
else
raise ArgumentError, "Unrecognized scoping: #{args.inspect}. Use .unscope(where: :attribute_name) or .unscope(:order), for example."
@@ -415,35 +410,24 @@ WARNING
# Performs a joins on +args+:
#
# User.joins(:posts)
- # => SELECT "users".* FROM "users" INNER JOIN "posts" ON "posts"."user_id" = "users"."id"
+ # # SELECT "users".* FROM "users" INNER JOIN "posts" ON "posts"."user_id" = "users"."id"
#
# You can use strings in order to customize your joins:
#
# User.joins("LEFT JOIN bookmarks ON bookmarks.bookmarkable_type = 'Post' AND bookmarks.user_id = users.id")
- # => SELECT "users".* FROM "users" LEFT JOIN bookmarks ON bookmarks.bookmarkable_type = 'Post' AND bookmarks.user_id = users.id
+ # # SELECT "users".* FROM "users" LEFT JOIN bookmarks ON bookmarks.bookmarkable_type = 'Post' AND bookmarks.user_id = users.id
def joins(*args)
check_if_method_has_arguments!(:joins, args)
-
- args.compact!
- args.flatten!
-
spawn.joins!(*args)
end
def joins!(*args) # :nodoc:
+ args.compact!
+ args.flatten!
self.joins_values += args
self
end
- def bind(value)
- spawn.bind!(value)
- end
-
- def bind!(value) # :nodoc:
- self.bind_values += [value]
- self
- end
-
# Returns a new relation, which is the result of filtering the current relation
# according to the conditions in the arguments.
#
@@ -487,7 +471,7 @@ WARNING
# than the previous methods; you are responsible for ensuring that the values in the template
# are properly quoted. The values are passed to the connector for quoting, but the caller
# is responsible for ensuring they are enclosed in quotes in the resulting SQL. After quoting,
- # the values are inserted using the same escapes as the Ruby core method <tt>Kernel::sprintf</tt>.
+ # the values are inserted using the same escapes as the Ruby core method +Kernel::sprintf+.
#
# User.where(["name = '%s' and email = '%s'", "Joe", "joe@example.com"])
# # SELECT * FROM users WHERE name = 'Joe' AND email = 'joe@example.com';
@@ -564,7 +548,7 @@ WARNING
# If the condition is any blank-ish object, then #where is a no-op and returns
# the current relation.
def where(opts = :chain, *rest)
- if opts == :chain
+ if :chain == opts
WhereChain.new(spawn)
elsif opts.blank?
self
@@ -574,24 +558,54 @@ WARNING
end
def where!(opts, *rest) # :nodoc:
+ opts = sanitize_forbidden_attributes(opts)
references!(PredicateBuilder.references(opts)) if Hash === opts
-
- self.where_values += build_where(opts, rest)
+ self.where_clause += where_clause_factory.build(opts, rest)
self
end
# Allows you to change a previously set where condition for a given attribute, instead of appending to that condition.
#
- # Post.where(trashed: true).where(trashed: false) # => WHERE `trashed` = 1 AND `trashed` = 0
- # Post.where(trashed: true).rewhere(trashed: false) # => WHERE `trashed` = 0
- # Post.where(active: true).where(trashed: true).rewhere(trashed: false) # => WHERE `active` = 1 AND `trashed` = 0
+ # Post.where(trashed: true).where(trashed: false)
+ # # WHERE `trashed` = 1 AND `trashed` = 0
#
- # This is short-hand for unscope(where: conditions.keys).where(conditions). Note that unlike reorder, we're only unscoping
- # the named conditions -- not the entire where statement.
+ # Post.where(trashed: true).rewhere(trashed: false)
+ # # WHERE `trashed` = 0
+ #
+ # Post.where(active: true).where(trashed: true).rewhere(trashed: false)
+ # # WHERE `active` = 1 AND `trashed` = 0
+ #
+ # This is short-hand for <tt>unscope(where: conditions.keys).where(conditions)</tt>.
+ # Note that unlike reorder, we're only unscoping the named conditions -- not the entire where statement.
def rewhere(conditions)
unscope(where: conditions.keys).where(conditions)
end
+ # Returns a new relation, which is the logical union of this relation and the one passed as an
+ # argument.
+ #
+ # The two relations must be structurally compatible: they must be scoping the same model, and
+ # they must differ only by #where (if no #group has been defined) or #having (if a #group is
+ # present). Neither relation may have a #limit, #offset, or #distinct set.
+ #
+ # Post.where("id = 1").or(Post.where("id = 2"))
+ # # SELECT `posts`.* FROM `posts` WHERE (('id = 1' OR 'id = 2'))
+ #
+ def or(other)
+ spawn.or!(other)
+ end
+
+ def or!(other) # :nodoc:
+ unless structurally_compatible_for_or?(other)
+ raise ArgumentError, 'Relation passed to #or must be structurally compatible'
+ end
+
+ self.where_clause = self.where_clause.or(other.where_clause)
+ self.having_clause = self.having_clause.or(other.having_clause)
+
+ self
+ end
+
# Allows to specify a HAVING clause. Note that you can't use HAVING
# without also specifying a GROUP clause.
#
@@ -601,9 +615,10 @@ WARNING
end
def having!(opts, *rest) # :nodoc:
+ opts = sanitize_forbidden_attributes(opts)
references!(PredicateBuilder.references(opts)) if Hash === opts
- self.having_values += build_where(opts, rest)
+ self.having_clause += having_clause_factory.build(opts, rest)
self
end
@@ -638,7 +653,7 @@ WARNING
end
# Specifies locking settings (default to +true+). For more information
- # on locking, please see +ActiveRecord::Locking+.
+ # on locking, please see ActiveRecord::Locking.
def lock(locks = true)
spawn.lock!(locks)
end
@@ -669,7 +684,7 @@ WARNING
# For example:
#
# @posts = current_user.visible_posts.where(name: params[:name])
- # # => the visible_posts method is expected to return a chainable Relation
+ # # the visible_posts method is expected to return a chainable Relation
#
# def visible_posts
# case role
@@ -683,11 +698,11 @@ WARNING
# end
#
def none
- extending(NullRelation)
+ where("1=0").extending!(NullRelation)
end
def none! # :nodoc:
- extending!(NullRelation)
+ where!("1=0").extending!(NullRelation)
end
# Sets readonly attributes for the returned relation. If value is
@@ -695,7 +710,7 @@ WARNING
#
# users = User.readonly
# users.first.save
- # => ActiveRecord::ReadOnlyRecord: ActiveRecord::ReadOnlyRecord
+ # => ActiveRecord::ReadOnlyRecord: User is marked as readonly
def readonly(value = true)
spawn.readonly!(value)
end
@@ -714,7 +729,7 @@ WARNING
# users = users.create_with(name: 'DHH')
# users.new.name # => 'DHH'
#
- # You can pass +nil+ to +create_with+ to reset attributes:
+ # You can pass +nil+ to #create_with to reset attributes:
#
# users = users.create_with(nil)
# users.new.name # => 'Oscar'
@@ -723,46 +738,53 @@ WARNING
end
def create_with!(value) # :nodoc:
- self.create_with_value = value ? create_with_value.merge(value) : {}
+ if value
+ value = sanitize_forbidden_attributes(value)
+ self.create_with_value = create_with_value.merge(value)
+ else
+ self.create_with_value = {}
+ end
+
self
end
# Specifies table from which the records will be fetched. For example:
#
# Topic.select('title').from('posts')
- # # => SELECT title FROM posts
+ # # SELECT title FROM posts
#
# Can accept other relation objects. For example:
#
# Topic.select('title').from(Topic.approved)
- # # => SELECT title FROM (SELECT * FROM topics WHERE approved = 't') subquery
+ # # SELECT title FROM (SELECT * FROM topics WHERE approved = 't') subquery
#
# Topic.select('a.title').from(Topic.approved, :a)
- # # => SELECT a.title FROM (SELECT * FROM topics WHERE approved = 't') a
+ # # SELECT a.title FROM (SELECT * FROM topics WHERE approved = 't') a
#
def from(value, subquery_name = nil)
spawn.from!(value, subquery_name)
end
def from!(value, subquery_name = nil) # :nodoc:
- self.from_value = [value, subquery_name]
+ self.from_clause = Relation::FromClause.new(value, subquery_name)
self
end
# Specifies whether the records should be unique or not. For example:
#
# User.select(:name)
- # # => Might return two records with the same name
+ # # Might return two records with the same name
#
# User.select(:name).distinct
- # # => Returns 1 record per distinct name
+ # # Returns 1 record per distinct name
#
# User.select(:name).distinct.distinct(false)
- # # => You can also remove the uniqueness
+ # # You can also remove the uniqueness
def distinct(value = true)
spawn.distinct!(value)
end
alias uniq distinct
+ deprecate uniq: :distinct
# Like #distinct, but modifies relation in place.
def distinct!(value = true) # :nodoc:
@@ -770,6 +792,7 @@ WARNING
self
end
alias uniq! distinct!
+ deprecate uniq!: :distinct!
# Used to extend a scope with additional methods, either through
# a module or through a block provided.
@@ -846,37 +869,30 @@ WARNING
private
+ def assert_mutability!
+ raise ImmutableRelation if @loaded
+ raise ImmutableRelation if defined?(@arel) && @arel
+ end
+
def build_arel
- arel = Arel::SelectManager.new(table.engine, table)
+ arel = Arel::SelectManager.new(table)
build_joins(arel, joins_values.flatten) unless joins_values.empty?
- collapse_wheres(arel, (where_values - [''])) #TODO: Add uniq with real value comparison / ignore uniqs that have binds
-
- arel.having(*having_values.uniq.reject(&:blank?)) unless having_values.empty?
-
+ arel.where(where_clause.ast) unless where_clause.empty?
+ arel.having(having_clause.ast) unless having_clause.empty?
arel.take(connection.sanitize_limit(limit_value)) if limit_value
arel.skip(offset_value.to_i) if offset_value
-
- arel.group(*group_values.uniq.reject(&:blank?)) unless group_values.empty?
+ arel.group(*arel_columns(group_values.uniq.reject(&:blank?))) unless group_values.empty?
build_order(arel)
- build_select(arel, select_values.uniq)
+ build_select(arel)
arel.distinct(distinct_value)
- arel.from(build_from) if from_value
+ arel.from(build_from) unless from_clause.empty?
arel.lock(lock_value) if lock_value
- # Reorder bind indexes if joins produced bind values
- if arel.bind_values.any?
- bvs = arel.bind_values + bind_values
- arel.ast.grep(Arel::Nodes::BindParam).each_with_index do |bp, i|
- column = bvs[i].first
- bp.replace connection.substitute_at(column, i)
- end
- end
-
arel
end
@@ -885,112 +901,36 @@ WARNING
raise ArgumentError, "Called unscope() with invalid unscoping argument ':#{scope}'. Valid arguments are :#{VALID_UNSCOPING_VALUES.to_a.join(", :")}."
end
- single_val_method = Relation::SINGLE_VALUE_METHODS.include?(scope)
- unscope_code = "#{scope}_value#{'s' unless single_val_method}="
+ clause_method = Relation::CLAUSE_METHODS.include?(scope)
+ multi_val_method = Relation::MULTI_VALUE_METHODS.include?(scope)
+ if clause_method
+ unscope_code = "#{scope}_clause="
+ else
+ unscope_code = "#{scope}_value#{'s' if multi_val_method}="
+ end
case scope
when :order
result = []
- when :where
- self.bind_values = []
else
- result = [] unless single_val_method
+ result = [] if multi_val_method
end
self.send(unscope_code, result)
end
- def where_unscoping(target_value)
- target_value = target_value.to_s
-
- where_values.reject! do |rel|
- case rel
- when Arel::Nodes::In, Arel::Nodes::NotIn, Arel::Nodes::Equality, Arel::Nodes::NotEqual
- subrelation = (rel.left.kind_of?(Arel::Attributes::Attribute) ? rel.left : rel.right)
- subrelation.name == target_value
- end
- end
-
- bind_values.reject! { |col,_| col.name == target_value }
- end
-
- def custom_join_ast(table, joins)
- joins = joins.reject(&:blank?)
-
- return [] if joins.empty?
-
- joins.map! do |join|
- case join
- when Array
- join = Arel.sql(join.join(' ')) if array_of_strings?(join)
- when String
- join = Arel.sql(join)
- end
- table.create_string_join(join)
- end
- end
-
- def collapse_wheres(arel, wheres)
- predicates = wheres.map do |where|
- next where if ::Arel::Nodes::Equality === where
- where = Arel.sql(where) if String === where
- Arel::Nodes::Grouping.new(where)
- end
-
- arel.where(Arel::Nodes::And.new(predicates)) if predicates.present?
- end
-
- def build_where(opts, other = [])
- case opts
- when String, Array
- [@klass.send(:sanitize_sql, other.empty? ? opts : ([opts] + other))]
- when Hash
- opts = PredicateBuilder.resolve_column_aliases(klass, opts)
-
- bv_len = bind_values.length
- tmp_opts, bind_values = create_binds(opts, bv_len)
- self.bind_values += bind_values
-
- attributes = @klass.send(:expand_hash_conditions_for_aggregates, tmp_opts)
- attributes.values.grep(ActiveRecord::Relation) do |rel|
- self.bind_values += rel.bind_values
- end
-
- PredicateBuilder.build_from_hash(klass, attributes, table)
- else
- [opts]
- end
- end
-
- def create_binds(opts, idx)
- bindable, non_binds = opts.partition do |column, value|
- case value
- when String, Integer, ActiveRecord::StatementCache::Substitute
- @klass.columns_hash.include? column.to_s
- else
- false
- end
- end
-
- new_opts = {}
- binds = []
-
- bindable.each_with_index do |(column,value), index|
- binds.push [@klass.columns_hash[column.to_s], value]
- new_opts[column] = connection.substitute_at(column, index + idx)
- end
-
- non_binds.each { |column,value| new_opts[column] = value }
-
- [new_opts, binds]
+ def association_for_table(table_name)
+ table_name = table_name.to_s
+ @klass._reflect_on_association(table_name) ||
+ @klass._reflect_on_association(table_name.singularize)
end
def build_from
- opts, name = from_value
+ opts = from_clause.value
+ name = from_clause.name
case opts
when Relation
name ||= 'subquery'
- self.bind_values = opts.bind_values + self.bind_values
opts.arel.as(name.to_s)
else
opts
@@ -1012,13 +952,14 @@ WARNING
raise 'unknown class: %s' % join.class.name
end
end
+ buckets.default = []
- association_joins = buckets[:association_join] || []
- stashed_association_joins = buckets[:stashed_join] || []
- join_nodes = (buckets[:join_node] || []).uniq
- string_joins = (buckets[:string_join] || []).map(&:strip).uniq
+ association_joins = buckets[:association_join]
+ stashed_association_joins = buckets[:stashed_join]
+ join_nodes = buckets[:join_node].uniq
+ string_joins = buckets[:string_join].map(&:strip).uniq
- join_list = join_nodes + custom_join_ast(manager, string_joins)
+ join_list = join_nodes + convert_join_strings_to_ast(manager, string_joins)
join_dependency = ActiveRecord::Associations::JoinDependency.new(
@klass,
@@ -1038,17 +979,33 @@ WARNING
manager
end
- def build_select(arel, selects)
- if !selects.empty?
- expanded_select = selects.map do |field|
- columns_hash.key?(field.to_s) ? arel_table[field] : field
- end
- arel.project(*expanded_select)
+ def convert_join_strings_to_ast(table, joins)
+ joins
+ .flatten
+ .reject(&:blank?)
+ .map { |join| table.create_string_join(Arel.sql(join)) }
+ end
+
+ def build_select(arel)
+ if select_values.any?
+ arel.project(*arel_columns(select_values.uniq))
else
arel.project(@klass.arel_table[Arel.star])
end
end
+ def arel_columns(columns)
+ columns.map do |field|
+ if (Symbol === field || String === field) && columns_hash.key?(field.to_s) && !from_clause.value
+ arel_table[field]
+ elsif Symbol === field
+ connection.quote_table_name(field.to_s)
+ else
+ field
+ end
+ end
+ end
+
def reverse_sql_order(order_query)
order_query = ["#{quoted_table_name}.#{quoted_primary_key} ASC"] if order_query.empty?
@@ -1067,10 +1024,6 @@ WARNING
end
end
- def array_of_strings?(o)
- o.is_a?(Array) && o.all? { |obj| obj.is_a?(String) }
- end
-
def build_order(arel)
orders = order_values.uniq
orders.reject!(&:blank?)
@@ -1122,8 +1075,8 @@ WARNING
#
# Example:
#
- # Post.references() # => raises an error
- # Post.references([]) # => does not raise an error
+ # Post.references() # raises an error
+ # Post.references([]) # does not raise an error
#
# This particular method should be called with a method_name and the args
# passed into that method as an input. For example:
@@ -1137,5 +1090,25 @@ WARNING
raise ArgumentError, "The method .#{method_name}() must contain arguments."
end
end
+
+ def structurally_compatible_for_or?(other)
+ Relation::SINGLE_VALUE_METHODS.all? { |m| send("#{m}_value") == other.send("#{m}_value") } &&
+ (Relation::MULTI_VALUE_METHODS - [:extending]).all? { |m| send("#{m}_values") == other.send("#{m}_values") } &&
+ (Relation::CLAUSE_METHODS - [:having, :where]).all? { |m| send("#{m}_clause") != other.send("#{m}_clause") }
+ end
+
+ def new_where_clause
+ Relation::WhereClause.empty
+ end
+ alias new_having_clause new_where_clause
+
+ def where_clause_factory
+ @where_clause_factory ||= Relation::WhereClauseFactory.new(klass, predicate_builder)
+ end
+ alias having_clause_factory where_clause_factory
+
+ def new_from_clause
+ Relation::FromClause.empty
+ end
end
end
diff --git a/activerecord/lib/active_record/relation/record_fetch_warning.rb b/activerecord/lib/active_record/relation/record_fetch_warning.rb
new file mode 100644
index 0000000000..14e1bf89fa
--- /dev/null
+++ b/activerecord/lib/active_record/relation/record_fetch_warning.rb
@@ -0,0 +1,49 @@
+module ActiveRecord
+ class Relation
+ module RecordFetchWarning
+ # When this module is prepended to ActiveRecord::Relation and
+ # `config.active_record.warn_on_records_fetched_greater_than` is
+ # set to an integer, if the number of records a query returns is
+ # greater than the value of `warn_on_records_fetched_greater_than`,
+ # a warning is logged. This allows for the detection of queries that
+ # return a large number of records, which could cause memory bloat.
+ #
+ # In most cases, fetching large number of records can be performed
+ # efficiently using the ActiveRecord::Batches methods.
+ # See active_record/lib/relation/batches.rb for more information.
+ def exec_queries
+ QueryRegistry.reset
+
+ super.tap do
+ if logger && warn_on_records_fetched_greater_than
+ if @records.length > warn_on_records_fetched_greater_than
+ logger.warn "Query fetched #{@records.size} #{@klass} records: #{QueryRegistry.queries.join(";")}"
+ end
+ end
+ end
+ end
+
+ ActiveSupport::Notifications.subscribe("sql.active_record") do |*args|
+ payload = args.last
+
+ QueryRegistry.queries << payload[:sql]
+ end
+
+ class QueryRegistry # :nodoc:
+ extend ActiveSupport::PerThreadRegistry
+
+ attr_accessor :queries
+
+ def initialize
+ reset
+ end
+
+ def reset
+ @queries = []
+ end
+ end
+ end
+ end
+end
+
+ActiveRecord::Relation.prepend ActiveRecord::Relation::RecordFetchWarning
diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb
index 57d66bce4b..5c3318651a 100644
--- a/activerecord/lib/active_record/relation/spawn_methods.rb
+++ b/activerecord/lib/active_record/relation/spawn_methods.rb
@@ -10,7 +10,7 @@ module ActiveRecord
clone
end
- # Merges in the conditions from <tt>other</tt>, if <tt>other</tt> is an <tt>ActiveRecord::Relation</tt>.
+ # 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.
# Post.where(published: true).joins(:comments).merge( Comment.where(spam: false) )
# # Performs a single join query with both where conditions.
@@ -32,7 +32,7 @@ module ActiveRecord
elsif other
spawn.merge!(other)
else
- self
+ raise ArgumentError, "invalid argument: #{other.inspect}."
end
end
@@ -58,16 +58,13 @@ module ActiveRecord
# Post.order('id asc').only(:where) # discards the order condition
# Post.order('id asc').only(:where, :order) # uses the specified order
def only(*onlies)
- if onlies.any? { |o| o == :where }
- onlies << :bind
- end
relation_with values.slice(*onlies)
end
private
def relation_with(values) # :nodoc:
- result = Relation.create(klass, table, values)
+ result = Relation.create(klass, table, predicate_builder, 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
new file mode 100644
index 0000000000..1f000b3f0f
--- /dev/null
+++ b/activerecord/lib/active_record/relation/where_clause.rb
@@ -0,0 +1,173 @@
+module ActiveRecord
+ class Relation
+ class WhereClause # :nodoc:
+ attr_reader :binds
+
+ delegate :any?, :empty?, to: :predicates
+
+ def initialize(predicates, binds)
+ @predicates = predicates
+ @binds = binds
+ end
+
+ def +(other)
+ WhereClause.new(
+ predicates + other.predicates,
+ binds + other.binds,
+ )
+ end
+
+ def merge(other)
+ WhereClause.new(
+ predicates_unreferenced_by(other) + other.predicates,
+ non_conflicting_binds(other) + other.binds,
+ )
+ end
+
+ def except(*columns)
+ WhereClause.new(
+ predicates_except(columns),
+ binds_except(columns),
+ )
+ end
+
+ def or(other)
+ if empty?
+ self
+ elsif other.empty?
+ other
+ else
+ WhereClause.new(
+ [ast.or(other.ast)],
+ binds + other.binds
+ )
+ end
+ end
+
+ def to_h(table_name = nil)
+ equalities = predicates.grep(Arel::Nodes::Equality)
+ if table_name
+ equalities = equalities.select do |node|
+ node.left.relation.name == table_name
+ end
+ end
+
+ binds = self.binds.map { |attr| [attr.name, attr.value] }.to_h
+
+ equalities.map { |node|
+ name = node.left.name
+ [name, binds.fetch(name.to_s) {
+ case node.right
+ when Array then node.right.map(&:val)
+ when Arel::Nodes::Casted, Arel::Nodes::Quoted
+ node.right.val
+ end
+ }]
+ }.to_h
+ end
+
+ def ast
+ Arel::Nodes::And.new(predicates_with_wrapped_sql_literals)
+ end
+
+ def ==(other)
+ other.is_a?(WhereClause) &&
+ predicates == other.predicates &&
+ binds == other.binds
+ end
+
+ def invert
+ WhereClause.new(inverted_predicates, binds)
+ end
+
+ def self.empty
+ new([], [])
+ end
+
+ protected
+
+ attr_reader :predicates
+
+ def referenced_columns
+ @referenced_columns ||= begin
+ equality_nodes = predicates.select { |n| equality_node?(n) }
+ Set.new(equality_nodes, &:left)
+ end
+ end
+
+ private
+
+ def predicates_unreferenced_by(other)
+ predicates.reject do |n|
+ equality_node?(n) && other.referenced_columns.include?(n.left)
+ end
+ end
+
+ def equality_node?(node)
+ node.respond_to?(:operator) && node.operator == :==
+ end
+
+ def non_conflicting_binds(other)
+ conflicts = referenced_columns & other.referenced_columns
+ conflicts.map! { |node| node.name.to_s }
+ binds.reject { |attr| conflicts.include?(attr.name) }
+ end
+
+ def inverted_predicates
+ predicates.map { |node| invert_predicate(node) }
+ end
+
+ def invert_predicate(node)
+ case node
+ when NilClass
+ raise ArgumentError, 'Invalid argument for .where.not(), got nil.'
+ when Arel::Nodes::In
+ Arel::Nodes::NotIn.new(node.left, node.right)
+ when Arel::Nodes::Equality
+ Arel::Nodes::NotEqual.new(node.left, node.right)
+ when String
+ Arel::Nodes::Not.new(Arel::Nodes::SqlLiteral.new(node))
+ else
+ Arel::Nodes::Not.new(node)
+ end
+ end
+
+ def predicates_except(columns)
+ 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)
+ columns.include?(subrelation.name.to_s)
+ end
+ end
+ end
+
+ def binds_except(columns)
+ binds.reject do |attr|
+ columns.include?(attr.name)
+ end
+ end
+
+ def predicates_with_wrapped_sql_literals
+ non_empty_predicates.map do |node|
+ if Arel::Nodes::Equality === node
+ node
+ else
+ wrap_sql_literal(node)
+ end
+ end
+ end
+
+ def non_empty_predicates
+ predicates - ['']
+ end
+
+ def wrap_sql_literal(node)
+ if ::String === node
+ node = Arel.sql(node)
+ end
+ Arel::Nodes::Grouping.new(node)
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/relation/where_clause_factory.rb b/activerecord/lib/active_record/relation/where_clause_factory.rb
new file mode 100644
index 0000000000..a81ff98e49
--- /dev/null
+++ b/activerecord/lib/active_record/relation/where_clause_factory.rb
@@ -0,0 +1,37 @@
+module ActiveRecord
+ class Relation
+ class WhereClauseFactory # :nodoc:
+ def initialize(klass, predicate_builder)
+ @klass = klass
+ @predicate_builder = predicate_builder
+ end
+
+ def build(opts, other)
+ binds = []
+
+ case opts
+ when String, Array
+ parts = [klass.send(: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!
+
+ attributes, binds = predicate_builder.create_binds(attributes)
+
+ parts = predicate_builder.build_from_hash(attributes)
+ when Arel::Nodes::Node
+ parts = [opts]
+ else
+ raise ArgumentError, "Unsupported argument type: #{opts} (#{opts.class})"
+ end
+
+ WhereClause.new(parts, binds)
+ end
+
+ protected
+
+ attr_reader :klass, :predicate_builder
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/result.rb b/activerecord/lib/active_record/result.rb
index 8405fdaeb9..8e6cd6c82f 100644
--- a/activerecord/lib/active_record/result.rb
+++ b/activerecord/lib/active_record/result.rb
@@ -1,7 +1,8 @@
module ActiveRecord
###
- # This class encapsulates a Result returned from calling +exec_query+ on any
- # database connection adapter. For example:
+ # This class encapsulates a result returned from calling
+ # {#exec_query}[rdoc-ref:ConnectionAdapters::DatabaseStatements#exec_query]
+ # on any database connection adapter. For example:
#
# result = ActiveRecord::Base.connection.exec_query('SELECT id, title, body FROM posts')
# result # => #<ActiveRecord::Result:0xdeadbeef>
@@ -42,6 +43,10 @@ module ActiveRecord
@column_types = column_types
end
+ def length
+ @rows.length
+ end
+
def each
if block_given?
hash_rows.each { |row| yield row }
@@ -77,7 +82,7 @@ module ActiveRecord
def cast_values(type_overrides = {}) # :nodoc:
types = columns.map { |name| column_type(name, type_overrides) }
result = rows.map do |values|
- types.zip(values).map { |type, value| type.type_cast_from_database(value) }
+ types.zip(values).map { |type, value| type.deserialize(value) }
end
columns.one? ? result.map!(&:first) : result
diff --git a/activerecord/lib/active_record/runtime_registry.rb b/activerecord/lib/active_record/runtime_registry.rb
index 9d605b826a..56e88bc661 100644
--- a/activerecord/lib/active_record/runtime_registry.rb
+++ b/activerecord/lib/active_record/runtime_registry.rb
@@ -7,7 +7,7 @@ module ActiveRecord
#
# returns the connection handler local to the current thread.
#
- # See the documentation of <tt>ActiveSupport::PerThreadRegistry</tt>
+ # See the documentation of ActiveSupport::PerThreadRegistry
# for further details.
class RuntimeRegistry # :nodoc:
extend ActiveSupport::PerThreadRegistry
diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb
index ff70cbed0f..1cf4b09bf3 100644
--- a/activerecord/lib/active_record/sanitization.rb
+++ b/activerecord/lib/active_record/sanitization.rb
@@ -3,28 +3,31 @@ module ActiveRecord
extend ActiveSupport::Concern
module ClassMethods
- def quote_value(value, column) #:nodoc:
- connection.quote(value, column)
- end
-
- # Used to sanitize objects before they're used in an SQL SELECT statement. Delegates to <tt>connection.quote</tt>.
- def sanitize(object) #:nodoc:
+ # Used to sanitize objects before they're used in an SQL SELECT statement.
+ # Delegates to {connection.quote}[rdoc-ref:ConnectionAdapters::Quoting#quote].
+ def sanitize(object) # :nodoc:
connection.quote(object)
end
+ alias_method :quote_value, :sanitize
protected
- # Accepts an array, hash, or string of SQL conditions and sanitizes
+ # Accepts an array or string of SQL conditions and sanitizes
# them into a valid SQL fragment for a WHERE clause.
- # ["name='%s' and group_id='%s'", "foo'bar", 4] returns "name='foo''bar' and group_id='4'"
- # { name: "foo'bar", group_id: 4 } returns "name='foo''bar' and group_id='4'"
- # "name='foo''bar' and group_id='4'" returns "name='foo''bar' and group_id='4'"
- def sanitize_sql_for_conditions(condition, table_name = self.table_name)
+ #
+ # sanitize_sql_for_conditions(["name=? and group_id=?", "foo'bar", 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)
- when Hash; sanitize_sql_hash_for_conditions(condition, table_name)
else condition
end
end
@@ -33,7 +36,15 @@ module ActiveRecord
# Accepts an array, hash, or string of SQL conditions and sanitizes
# them into a valid SQL fragment for a SET clause.
- # { name: nil, group_id: 4 } returns "name = NULL , group_id='4'"
+ #
+ # sanitize_sql_for_assignment(["name=? and group_id=?", nil, 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 = self.table_name)
case assignments
when Array; sanitize_sql_array(assignments)
@@ -43,16 +54,20 @@ module ActiveRecord
end
# Accepts a hash of SQL conditions and replaces those attributes
- # that correspond to a +composed_of+ relationship with their expanded
- # aggregate attribute values.
+ # that correspond to a {#composed_of}[rdoc-ref:Aggregations::ClassMethods#composed_of]
+ # relationship with their expanded aggregate attribute values.
+ #
# Given:
- # class Person < ActiveRecord::Base
- # composed_of :address, class_name: "Address",
- # mapping: [%w(address_street street), %w(address_city city)]
- # end
+ #
+ # class Person < ActiveRecord::Base
+ # composed_of :address, class_name: "Address",
+ # mapping: [%w(address_street street), %w(address_city city)]
+ # end
+ #
# Then:
- # { address: Address.new("813 abc st.", "chicago") }
- # # => { address_street: "813 abc st.", address_city: "chicago" }
+ #
+ # { address: Address.new("813 abc st.", "chicago") }
+ # # => { address_street: "813 abc st.", address_city: "chicago" }
def expand_hash_conditions_for_aggregates(attrs)
expanded_attrs = {}
attrs.each do |attr, value|
@@ -72,43 +87,32 @@ module ActiveRecord
expanded_attrs
end
- # Sanitizes a hash of attribute/value pairs into SQL conditions for a WHERE clause.
- # { name: "foo'bar", group_id: 4 }
- # # => "name='foo''bar' and group_id= 4"
- # { status: nil, group_id: [1,2,3] }
- # # => "status IS NULL and group_id IN (1,2,3)"
- # { age: 13..18 }
- # # => "age BETWEEN 13 AND 18"
- # { 'other_records.id' => 7 }
- # # => "`other_records`.`id` = 7"
- # { other_records: { id: 7 } }
- # # => "`other_records`.`id` = 7"
- # And for value objects on a composed_of relationship:
- # { address: Address.new("123 abc st.", "chicago") }
- # # => "address_street='123 abc st.' and address_city='chicago'"
- def sanitize_sql_hash_for_conditions(attrs, default_table_name = self.table_name)
- attrs = PredicateBuilder.resolve_column_aliases self, attrs
- attrs = expand_hash_conditions_for_aggregates(attrs)
-
- table = Arel::Table.new(table_name, arel_engine).alias(default_table_name)
- PredicateBuilder.build_from_hash(self, attrs, table).map { |b|
- connection.visitor.compile b
- }.join(' AND ')
- end
- alias_method :sanitize_sql_hash, :sanitize_sql_hash_for_conditions
-
# Sanitizes a hash of attribute/value pairs into SQL conditions for a SET clause.
- # { status: nil, group_id: 1 }
- # # => "status = NULL , group_id = 1"
+ #
+ # 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|
- "#{c.quote_table_name_for_assignment(table, attr)} = #{quote_bound_value(value, c, columns_hash[attr.to_s])}"
+ 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 "%"
+ # 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 }
@@ -116,7 +120,12 @@ module ActiveRecord
# Accepts an array of conditions. The array has each value
# sanitized and interpolated into the SQL statement.
- # ["name='%s' and group_id='%s'", "foo'bar", 4] returns "name='foo''bar' and group_id='4'"
+ #
+ # sanitize_sql_array(["name=? and group_id=?", "foo'bar", 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) && statement =~ /:\w+/
@@ -130,16 +139,16 @@ module ActiveRecord
end
end
- def replace_bind_variables(statement, values) #:nodoc:
+ def replace_bind_variables(statement, values) # :nodoc:
raise_if_bind_arity_mismatch(statement, statement.count('?'), values.size)
bound = values.dup
c = connection
- statement.gsub('?') do
+ statement.gsub(/\?/) do
replace_bind_variable(bound.shift, c)
end
end
- def replace_bind_variable(value, c = connection) #:nodoc:
+ def replace_bind_variable(value, c = connection) # :nodoc:
if ActiveRecord::Relation === value
value.to_sql
else
@@ -147,10 +156,10 @@ module ActiveRecord
end
end
- def replace_named_bind_variables(statement, bind_vars) #:nodoc:
- statement.gsub(/(:?):([a-zA-Z]\w*)/) do
+ def replace_named_bind_variables(statement, bind_vars) # :nodoc:
+ statement.gsub(/(:?):([a-zA-Z]\w*)/) do |match|
if $1 == ':' # skip postgresql casts
- $& # return the whole match
+ match # return the whole match
elsif bind_vars.include?(match = $2.to_sym)
replace_bind_variable(bind_vars[match])
else
@@ -159,10 +168,8 @@ module ActiveRecord
end
end
- def quote_bound_value(value, c = connection, column = nil) #:nodoc:
- if column
- c.quote(value, column)
- elsif value.respond_to?(:map) && !value.acts_like?(:string)
+ def quote_bound_value(value, c = connection) # :nodoc:
+ if value.respond_to?(:map) && !value.acts_like?(:string)
if value.respond_to?(:empty?) && value.empty?
c.quote(nil)
else
@@ -173,7 +180,7 @@ module ActiveRecord
end
end
- def raise_if_bind_arity_mismatch(statement, expected, provided) #:nodoc:
+ def raise_if_bind_arity_mismatch(statement, expected, provided) # :nodoc:
unless expected == provided
raise PreparedStatementInvalid, "wrong number of bind variables (#{provided} for #{expected}) in: #{statement}"
end
@@ -182,7 +189,7 @@ module ActiveRecord
# TODO: Deprecate this
def quoted_id
- self.class.quote_value(id, column_for_attribute(self.class.primary_key))
+ self.class.quote_value(@attributes[self.class.primary_key].value_for_database)
end
end
end
diff --git a/activerecord/lib/active_record/schema.rb b/activerecord/lib/active_record/schema.rb
index 0a5546a760..31dd584538 100644
--- a/activerecord/lib/active_record/schema.rb
+++ b/activerecord/lib/active_record/schema.rb
@@ -1,5 +1,5 @@
module ActiveRecord
- # = Active Record Schema
+ # = Active Record \Schema
#
# Allows programmers to programmatically define a schema in a portable
# DSL. This means you can define tables, indexes, etc. without using SQL
@@ -28,28 +28,11 @@ module ActiveRecord
# ActiveRecord::Schema is only supported by database adapters that also
# support migrations, the two features being very similar.
class Schema < Migration
-
- # Returns the migrations paths.
- #
- # ActiveRecord::Schema.new.migrations_paths
- # # => ["db/migrate"] # Rails migration path by default.
- def migrations_paths
- ActiveRecord::Migrator.migrations_paths
- end
-
- def define(info, &block) # :nodoc:
- instance_eval(&block)
-
- unless info[:version].blank?
- initialize_schema_migrations_table
- connection.assume_migrated_upto_version(info[:version], migrations_paths)
- end
- end
-
# Eval the given block. All methods available to the current connection
# adapter are available within the block, so you can easily use the
- # database definition DSL to build up your schema (+create_table+,
- # +add_index+, etc.).
+ # database definition DSL to build up your schema (
+ # {create_table}[rdoc-ref:ConnectionAdapters::SchemaStatements#create_table],
+ # {add_index}[rdoc-ref:ConnectionAdapters::SchemaStatements#add_index], etc.).
#
# The +info+ hash is optional, and if given is used to define metadata
# about the current schema (currently, only the schema's version):
@@ -60,5 +43,23 @@ module ActiveRecord
def self.define(info={}, &block)
new.define(info, &block)
end
+
+ def define(info, &block) # :nodoc:
+ instance_eval(&block)
+
+ if info[:version].present?
+ initialize_schema_migrations_table
+ connection.assume_migrated_upto_version(info[:version], migrations_paths)
+ end
+ end
+
+ private
+ # Returns the migrations paths.
+ #
+ # ActiveRecord::Schema.new.migrations_paths
+ # # => ["db/migrate"] # Rails migration path by default.
+ def migrations_paths # :nodoc:
+ ActiveRecord::Migrator.migrations_paths
+ end
end
end
diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb
index fae6427ea1..2362dae9fc 100644
--- a/activerecord/lib/active_record/schema_dumper.rb
+++ b/activerecord/lib/active_record/schema_dumper.rb
@@ -1,5 +1,4 @@
require 'stringio'
-require 'active_support/core_ext/big_decimal'
module ActiveRecord
# = Active Record Schema Dumper
@@ -44,7 +43,6 @@ module ActiveRecord
def initialize(connection, options = {})
@connection = connection
- @types = @connection.native_database_types
@version = Migrator::current_version rescue nil
@options = options
end
@@ -91,7 +89,7 @@ HEADER
end
def tables(stream)
- sorted_tables = @connection.tables.sort
+ sorted_tables = @connection.data_sources.sort - @connection.views
sorted_tables.each do |table_name|
table(table_name, stream) unless ignored?(table_name)
@@ -100,44 +98,56 @@ HEADER
# dump foreign keys at the end to make sure all dependent tables exist.
if @connection.supports_foreign_keys?
sorted_tables.each do |tbl|
- foreign_keys(tbl, stream)
+ foreign_keys(tbl, stream) unless ignored?(tbl)
end
end
end
def table(table, stream)
- columns = @connection.columns(table)
+ columns = @connection.columns(table).map do |column|
+ column.instance_variable_set(:@table_name, table)
+ column
+ end
begin
tbl = StringIO.new
# first dump primary key column
- if @connection.respond_to?(:pk_and_sequence_for)
- pk, _ = @connection.pk_and_sequence_for(table)
- end
- if !pk && @connection.respond_to?(:primary_key)
+ if @connection.respond_to?(:primary_keys)
+ pk = @connection.primary_keys(table)
+ pk = pk.first unless pk.size > 1
+ else
pk = @connection.primary_key(table)
end
tbl.print " create_table #{remove_prefix_and_suffix(table).inspect}"
- pkcol = columns.detect { |c| c.name == pk }
- if pkcol
- if pk != 'id'
- tbl.print %Q(, primary_key: "#{pk}")
- elsif pkcol.sql_type == 'uuid'
- tbl.print ", id: :uuid"
- tbl.print %Q(, default: "#{pkcol.default_function}") if pkcol.default_function
+
+ case pk
+ when String
+ tbl.print ", primary_key: #{pk.inspect}" unless pk == 'id'
+ pkcol = columns.detect { |c| c.name == pk }
+ pkcolspec = @connection.column_spec_for_primary_key(pkcol)
+ if pkcolspec
+ pkcolspec.each do |key, value|
+ tbl.print ", #{key}: #{value}"
+ end
end
+ when Array
+ tbl.print ", primary_key: #{pk.inspect}"
else
tbl.print ", id: false"
end
- tbl.print ", force: true"
+ tbl.print ", force: :cascade"
+
+ table_options = @connection.table_options(table)
+ tbl.print ", options: #{table_options.inspect}" unless table_options.blank?
+
tbl.puts " do |t|"
# then dump all non-primary key columns
column_specs = columns.map do |column|
raise StandardError, "Unknown type '#{column.sql_type}' for column '#{column.name}'" unless @connection.valid_type?(column.type)
next if column.name == pk
- @connection.column_spec(column, @types)
+ @connection.column_spec(column)
end.compact
# find all migration keys used in this table
@@ -168,11 +178,11 @@ HEADER
tbl.puts
end
+ indexes(table, tbl)
+
tbl.puts " end"
tbl.puts
- indexes(table, tbl)
-
tbl.rewind
stream.print tbl.read
rescue => e
@@ -188,29 +198,24 @@ HEADER
if (indexes = @connection.indexes(table)).any?
add_index_statements = indexes.map do |index|
statement_parts = [
- ('add_index ' + remove_prefix_and_suffix(index.table).inspect),
- index.columns.inspect,
- ('name: ' + index.name.inspect),
+ "t.index #{index.columns.inspect}",
+ "name: #{index.name.inspect}",
]
statement_parts << 'unique: true' if index.unique
index_lengths = (index.lengths || []).compact
- statement_parts << ('length: ' + Hash[index.columns.zip(index.lengths)].inspect) unless index_lengths.empty?
-
- index_orders = (index.orders || {})
- statement_parts << ('order: ' + index.orders.inspect) unless index_orders.empty?
-
- statement_parts << ('where: ' + index.where.inspect) if index.where
-
- statement_parts << ('using: ' + index.using.inspect) if index.using
+ statement_parts << "length: #{Hash[index.columns.zip(index.lengths)].inspect}" if index_lengths.any?
- statement_parts << ('type: ' + index.type.inspect) if index.type
+ index_orders = index.orders || {}
+ statement_parts << "order: #{index.orders.inspect}" if index_orders.any?
+ statement_parts << "where: #{index.where.inspect}" if index.where
+ statement_parts << "using: #{index.using.inspect}" if index.using
+ statement_parts << "type: #{index.type.inspect}" if index.type
- ' ' + statement_parts.join(', ')
+ " #{statement_parts.join(', ')}"
end
stream.puts add_index_statements.sort.join("\n")
- stream.puts
end
end
@@ -218,26 +223,26 @@ HEADER
if (foreign_keys = @connection.foreign_keys(table)).any?
add_foreign_key_statements = foreign_keys.map do |foreign_key|
parts = [
- 'add_foreign_key ' + remove_prefix_and_suffix(foreign_key.from_table).inspect,
- remove_prefix_and_suffix(foreign_key.to_table).inspect,
- ]
+ "add_foreign_key #{remove_prefix_and_suffix(foreign_key.from_table).inspect}",
+ remove_prefix_and_suffix(foreign_key.to_table).inspect,
+ ]
if foreign_key.column != @connection.foreign_key_column_for(foreign_key.to_table)
- parts << ('column: ' + foreign_key.column.inspect)
+ parts << "column: #{foreign_key.column.inspect}"
end
if foreign_key.custom_primary_key?
- parts << ('primary_key: ' + foreign_key.primary_key.inspect)
+ parts << "primary_key: #{foreign_key.primary_key.inspect}"
end
if foreign_key.name !~ /^fk_rails_[0-9a-f]{10}$/
- parts << ('name: ' + foreign_key.name.inspect)
+ parts << "name: #{foreign_key.name.inspect}"
end
- parts << ('on_update: ' + foreign_key.on_update.inspect) if foreign_key.on_update
- parts << ('on_delete: ' + foreign_key.on_delete.inspect) if foreign_key.on_delete
+ parts << "on_update: #{foreign_key.on_update.inspect}" if foreign_key.on_update
+ parts << "on_delete: #{foreign_key.on_delete.inspect}" if foreign_key.on_delete
- ' ' + parts.join(', ')
+ " #{parts.join(', ')}"
end
stream.puts add_foreign_key_statements.sort.join("\n")
@@ -249,13 +254,8 @@ HEADER
end
def ignored?(table_name)
- ['schema_migrations', ignore_tables].flatten.any? do |ignored|
- case ignored
- when String; remove_prefix_and_suffix(table_name) == ignored
- when Regexp; remove_prefix_and_suffix(table_name) =~ ignored
- else
- raise StandardError, 'ActiveRecord::SchemaDumper.ignore_tables accepts an array of String and / or Regexp values.'
- end
+ [ActiveRecord::Base.schema_migrations_table_name, ignore_tables].flatten.any? do |ignored|
+ ignored === remove_prefix_and_suffix(table_name)
end
end
end
diff --git a/activerecord/lib/active_record/schema_migration.rb b/activerecord/lib/active_record/schema_migration.rb
index b5038104ac..b384529e75 100644
--- a/activerecord/lib/active_record/schema_migration.rb
+++ b/activerecord/lib/active_record/schema_migration.rb
@@ -1,9 +1,12 @@
require 'active_record/scoping/default'
require 'active_record/scoping/named'
-require 'active_record/base'
module ActiveRecord
- class SchemaMigration < ActiveRecord::Base
+ # This class is used to create a table that keeps track of which migrations
+ # have been applied to a given database. When a migration is run, its schema
+ # number is inserted in to the `SchemaMigration.table_name` so it doesn't need
+ # to be executed the next time.
+ class SchemaMigration < ActiveRecord::Base # :nodoc:
class << self
def primary_key
nil
diff --git a/activerecord/lib/active_record/scoping.rb b/activerecord/lib/active_record/scoping.rb
index 3e43591672..e395970dc6 100644
--- a/activerecord/lib/active_record/scoping.rb
+++ b/activerecord/lib/active_record/scoping.rb
@@ -11,15 +11,26 @@ module ActiveRecord
module ClassMethods
def current_scope #:nodoc:
- ScopeRegistry.value_for(:current_scope, base_class.to_s)
+ ScopeRegistry.value_for(:current_scope, self.to_s)
end
def current_scope=(scope) #:nodoc:
- ScopeRegistry.set_value_for(:current_scope, base_class.to_s, scope)
+ ScopeRegistry.set_value_for(:current_scope, self.to_s, 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:
+ all.scope_for_create
+ end
+
+ # Are there attributes associated with this scope?
+ def scope_attributes? # :nodoc:
+ current_scope
end
end
- def populate_with_current_scope_attributes
+ def populate_with_current_scope_attributes # :nodoc:
return unless self.class.scope_attributes?
self.class.scope_attributes.each do |att,value|
@@ -27,7 +38,7 @@ module ActiveRecord
end
end
- def initialize_internals_callback
+ def initialize_internals_callback # :nodoc:
super
populate_with_current_scope_attributes
end
@@ -48,8 +59,8 @@ module ActiveRecord
#
# registry.value_for(:current_scope, "Board")
#
- # You will obtain whatever was defined in +some_new_scope+. The +value_for+
- # and +set_value_for+ methods are delegated to the current +ScopeRegistry+
+ # You will obtain whatever was defined in +some_new_scope+. The #value_for
+ # and #set_value_for methods are delegated to the current ScopeRegistry
# object, so the above example code can also be called as:
#
# ActiveRecord::Scoping::ScopeRegistry.set_value_for(:current_scope,
diff --git a/activerecord/lib/active_record/scoping/default.rb b/activerecord/lib/active_record/scoping/default.rb
index 18190cb535..cdcb73382f 100644
--- a/activerecord/lib/active_record/scoping/default.rb
+++ b/activerecord/lib/active_record/scoping/default.rb
@@ -6,8 +6,10 @@ module ActiveRecord
included do
# Stores the default scope for the class.
class_attribute :default_scopes, instance_writer: false, instance_predicate: false
+ class_attribute :default_scope_override, instance_predicate: false
self.default_scopes = []
+ self.default_scope_override = nil
end
module ClassMethods
@@ -15,7 +17,7 @@ module ActiveRecord
#
# class Post < ActiveRecord::Base
# def self.default_scope
- # where published: true
+ # where(published: true)
# end
# end
#
@@ -33,6 +35,11 @@ module ActiveRecord
block_given? ? relation.scoping { yield } : relation
end
+ # Are there attributes associated with this scope?
+ def scope_attributes? # :nodoc:
+ super || default_scopes.any? || respond_to?(:default_scope)
+ end
+
def before_remove_const #:nodoc:
self.current_scope = nil
end
@@ -48,7 +55,7 @@ module ActiveRecord
#
# Article.all # => SELECT * FROM articles WHERE published = true
#
- # The +default_scope+ is also applied while creating/building a record.
+ # The #default_scope is also applied while creating/building a record.
# It is not applied while updating a record.
#
# Article.new.published # => true
@@ -58,7 +65,7 @@ module ActiveRecord
# +default_scope+ macro, and it will be called when building the
# default scope.)
#
- # If you use multiple +default_scope+ declarations in your model then
+ # If you use multiple #default_scope declarations in your model then
# they will be merged together:
#
# class Article < ActiveRecord::Base
@@ -69,7 +76,7 @@ module ActiveRecord
# Article.all # => SELECT * FROM articles WHERE published = true AND rating = 'G'
#
# This is also the case with inheritance and module includes where the
- # parent or module defines a +default_scope+ and the child or including
+ # parent or module defines a #default_scope and the child or including
# class defines a second one.
#
# If you need to do more complex things with a default scope, you can
@@ -94,11 +101,18 @@ module ActiveRecord
self.default_scopes += [scope]
end
- def build_default_scope(base_rel = relation) # :nodoc:
- if !Base.is_a?(method(:default_scope).owner)
+ def build_default_scope(base_rel = nil) # :nodoc:
+ return if abstract_class?
+
+ if self.default_scope_override.nil?
+ self.default_scope_override = !Base.is_a?(method(:default_scope).owner)
+ end
+
+ if self.default_scope_override
# The user has defined their own default scope method, so call that
evaluate_default_scope { default_scope }
elsif default_scopes.any?
+ base_rel ||= relation
evaluate_default_scope do
default_scopes.inject(base_rel) do |default_scope, scope|
default_scope.merge(base_rel.scoping { scope.call })
diff --git a/activerecord/lib/active_record/scoping/named.rb b/activerecord/lib/active_record/scoping/named.rb
index 49cadb66d0..103569c84d 100644
--- a/activerecord/lib/active_record/scoping/named.rb
+++ b/activerecord/lib/active_record/scoping/named.rb
@@ -9,7 +9,7 @@ module ActiveRecord
extend ActiveSupport::Concern
module ClassMethods
- # Returns an <tt>ActiveRecord::Relation</tt> scope object.
+ # Returns an ActiveRecord::Relation scope object.
#
# posts = Post.all
# posts.size # Fires "select count(*) from posts" and returns the count
@@ -20,7 +20,7 @@ module ActiveRecord
# fruits = fruits.limit(10) if limited?
#
# You can define a scope that applies to all finders using
- # <tt>ActiveRecord::Base.default_scope</tt>.
+ # {default_scope}[rdoc-ref:Scoping::Default::ClassMethods#default_scope].
def all
if current_scope
current_scope.clone
@@ -30,22 +30,22 @@ module ActiveRecord
end
def default_scoped # :nodoc:
- relation.merge(build_default_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:
- all.scope_for_create
- end
+ scope = build_default_scope
- # Are there default attributes associated with this scope?
- def scope_attributes? # :nodoc:
- current_scope || default_scopes.any?
+ if scope
+ relation.spawn.merge!(scope)
+ else
+ relation
+ end
end
- # Adds a class method for retrieving and querying objects. A \scope
- # represents a narrowing of a database query, such as
+ # Adds a class method for retrieving and querying objects.
+ # The method is intended to return an ActiveRecord::Relation
+ # object, which is composable with other scopes.
+ # If it returns nil or false, an
+ # {all}[rdoc-ref:Scoping::Named::ClassMethods#all] scope is returned instead.
+ #
+ # A \scope represents a narrowing of a database query, such as
# <tt>where(color: :red).select('shirts.*').includes(:washing_instructions)</tt>.
#
# class Shirt < ActiveRecord::Base
@@ -53,12 +53,12 @@ module ActiveRecord
# scope :dry_clean_only, -> { joins(:washing_instructions).where('washing_instructions.dry_clean_only = ?', true) }
# end
#
- # The above calls to +scope+ define class methods <tt>Shirt.red</tt> and
+ # The above calls to #scope define class methods <tt>Shirt.red</tt> and
# <tt>Shirt.dry_clean_only</tt>. <tt>Shirt.red</tt>, in effect,
# represents the query <tt>Shirt.where(color: 'red')</tt>.
#
# You should always pass a callable object to the scopes defined
- # with +scope+. This ensures that the scope is re-evaluated each
+ # with #scope. This ensures that the scope is re-evaluated each
# time it is called.
#
# Note that this is simply 'syntactic sugar' for defining an actual
@@ -71,14 +71,15 @@ module ActiveRecord
# end
#
# Unlike <tt>Shirt.find(...)</tt>, however, the object returned by
- # <tt>Shirt.red</tt> is not an Array; it resembles the association object
- # constructed by a +has_many+ declaration. For instance, you can invoke
- # <tt>Shirt.red.first</tt>, <tt>Shirt.red.count</tt>,
+ # <tt>Shirt.red</tt> is not an Array but an ActiveRecord::Relation,
+ # which is composable with other scopes; it resembles the association object
+ # constructed by a {has_many}[rdoc-ref:Associations::ClassMethods#has_many]
+ # declaration. For instance, you can invoke <tt>Shirt.red.first</tt>, <tt>Shirt.red.count</tt>,
# <tt>Shirt.red.where(size: 'small')</tt>. Also, just as with the
# association objects, named \scopes act like an Array, implementing
# Enumerable; <tt>Shirt.red.each(&block)</tt>, <tt>Shirt.red.first</tt>,
# and <tt>Shirt.red.inject(memo, &block)</tt> all behave as if
- # <tt>Shirt.red</tt> really was an Array.
+ # <tt>Shirt.red</tt> really was an array.
#
# These named \scopes are composable. For instance,
# <tt>Shirt.red.dry_clean_only</tt> will produce all shirts that are
@@ -89,7 +90,8 @@ module ActiveRecord
#
# All scopes are available as class methods on the ActiveRecord::Base
# descendant upon which the \scopes were defined. But they are also
- # available to +has_many+ associations. If,
+ # available to {has_many}[rdoc-ref:Associations::ClassMethods#has_many]
+ # associations. If,
#
# class Person < ActiveRecord::Base
# has_many :shirts
@@ -98,8 +100,8 @@ module ActiveRecord
# then <tt>elton.shirts.red.dry_clean_only</tt> will return all of
# Elton's red, dry clean only shirts.
#
- # \Named scopes can also have extensions, just as with +has_many+
- # declarations:
+ # \Named scopes can also have extensions, just as with
+ # {has_many}[rdoc-ref:Associations::ClassMethods#has_many] declarations:
#
# class Shirt < ActiveRecord::Base
# scope :red, -> { where(color: 'red') } do
@@ -139,6 +141,10 @@ module ActiveRecord
# Article.published.featured.latest_article
# Article.featured.titles
def scope(name, body, &block)
+ unless body.respond_to?(:call)
+ raise ArgumentError, 'The scope body needs to be callable.'
+ end
+
if dangerous_class_method?(name)
raise ArgumentError, "You tried to define a scope named \"#{name}\" " \
"on the model \"#{self.name}\", but Active Record already defined " \
@@ -147,11 +153,20 @@ module ActiveRecord
extension = Module.new(&block) if block
- singleton_class.send(:define_method, name) do |*args|
- scope = all.scoping { body.call(*args) }
- scope = scope.extending(extension) if extension
+ if body.respond_to?(:to_proc)
+ singleton_class.send(:define_method, name) do |*args|
+ scope = all.scoping { instance_exec(*args, &body) }
+ scope = scope.extending(extension) if extension
+
+ scope || all
+ end
+ else
+ singleton_class.send(:define_method, name) do |*args|
+ scope = all.scoping { body.call(*args) }
+ scope = scope.extending(extension) if extension
- scope || all
+ scope || all
+ end
end
end
end
diff --git a/activerecord/lib/active_record/secure_token.rb b/activerecord/lib/active_record/secure_token.rb
new file mode 100644
index 0000000000..8abda2ac49
--- /dev/null
+++ b/activerecord/lib/active_record/secure_token.rb
@@ -0,0 +1,38 @@
+module ActiveRecord
+ module SecureToken
+ extend ActiveSupport::Concern
+
+ module ClassMethods
+ # Example using #has_secure_token
+ #
+ # # Schema: User(token:string, auth_token:string)
+ # class User < ActiveRecord::Base
+ # has_secure_token
+ # has_secure_token :auth_token
+ # end
+ #
+ # user = User.new
+ # user.save
+ # user.token # => "pX27zsMN2ViQKta1bGfLmVJE"
+ # user.auth_token # => "77TMHrHJFvFDwodq8w7Ev2m7"
+ # user.regenerate_token # => true
+ # user.regenerate_auth_token # => true
+ #
+ # <tt>SecureRandom::base58</tt> is used to generate the 24-character unique token, so collisions are highly unlikely.
+ #
+ # Note that it's still possible to generate a race condition in the database in the same way that
+ # {validates_uniqueness_of}[rdoc-ref:Validations::ClassMethods#validates_uniqueness_of] can.
+ # You're encouraged to add a unique index in the database to deal with this even more unlikely scenario.
+ def has_secure_token(attribute = :token)
+ # Load securerandom only when has_secure_token is used.
+ require 'active_support/core_ext/securerandom'
+ define_method("regenerate_#{attribute}") { update! attribute => self.class.generate_unique_secure_token }
+ before_create { self.send("#{attribute}=", self.class.generate_unique_secure_token) unless self.send("#{attribute}?")}
+ end
+
+ def generate_unique_secure_token
+ SecureRandom.base58(24)
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/serialization.rb b/activerecord/lib/active_record/serialization.rb
index bd9079b596..5a408e7b8e 100644
--- a/activerecord/lib/active_record/serialization.rb
+++ b/activerecord/lib/active_record/serialization.rb
@@ -1,5 +1,5 @@
module ActiveRecord #:nodoc:
- # = Active Record Serialization
+ # = Active Record \Serialization
module Serialization
extend ActiveSupport::Concern
include ActiveModel::Serializers::JSON
@@ -11,12 +11,10 @@ module ActiveRecord #:nodoc:
def serializable_hash(options = nil)
options = options.try(:clone) || {}
- options[:except] = Array(options[:except]).map { |n| n.to_s }
+ options[:except] = Array(options[:except]).map(&:to_s)
options[:except] |= Array(self.class.inheritance_column)
super(options)
end
end
end
-
-require 'active_record/serializers/xml_serializer'
diff --git a/activerecord/lib/active_record/serializers/xml_serializer.rb b/activerecord/lib/active_record/serializers/xml_serializer.rb
deleted file mode 100644
index c2484d02ed..0000000000
--- a/activerecord/lib/active_record/serializers/xml_serializer.rb
+++ /dev/null
@@ -1,193 +0,0 @@
-require 'active_support/core_ext/hash/conversions'
-
-module ActiveRecord #:nodoc:
- module Serialization
- include ActiveModel::Serializers::Xml
-
- # Builds an XML document to represent the model. Some configuration is
- # available through +options+. However more complicated cases should
- # override ActiveRecord::Base#to_xml.
- #
- # By default the generated XML document will include the processing
- # instruction and all the object's attributes. For example:
- #
- # <?xml version="1.0" encoding="UTF-8"?>
- # <topic>
- # <title>The First Topic</title>
- # <author-name>David</author-name>
- # <id type="integer">1</id>
- # <approved type="boolean">false</approved>
- # <replies-count type="integer">0</replies-count>
- # <bonus-time type="dateTime">2000-01-01T08:28:00+12:00</bonus-time>
- # <written-on type="dateTime">2003-07-16T09:28:00+1200</written-on>
- # <content>Have a nice day</content>
- # <author-email-address>david@loudthinking.com</author-email-address>
- # <parent-id></parent-id>
- # <last-read type="date">2004-04-15</last-read>
- # </topic>
- #
- # This behavior can be controlled with <tt>:only</tt>, <tt>:except</tt>,
- # <tt>:skip_instruct</tt>, <tt>:skip_types</tt>, <tt>:dasherize</tt> and <tt>:camelize</tt> .
- # The <tt>:only</tt> and <tt>:except</tt> options are the same as for the
- # +attributes+ method. The default is to dasherize all column names, but you
- # can disable this setting <tt>:dasherize</tt> to +false+. Setting <tt>:camelize</tt>
- # to +true+ will camelize all column names - this also overrides <tt>:dasherize</tt>.
- # To not have the column type included in the XML output set <tt>:skip_types</tt> to +true+.
- #
- # For instance:
- #
- # topic.to_xml(skip_instruct: true, except: [ :id, :bonus_time, :written_on, :replies_count ])
- #
- # <topic>
- # <title>The First Topic</title>
- # <author-name>David</author-name>
- # <approved type="boolean">false</approved>
- # <content>Have a nice day</content>
- # <author-email-address>david@loudthinking.com</author-email-address>
- # <parent-id></parent-id>
- # <last-read type="date">2004-04-15</last-read>
- # </topic>
- #
- # To include first level associations use <tt>:include</tt>:
- #
- # firm.to_xml include: [ :account, :clients ]
- #
- # <?xml version="1.0" encoding="UTF-8"?>
- # <firm>
- # <id type="integer">1</id>
- # <rating type="integer">1</rating>
- # <name>37signals</name>
- # <clients type="array">
- # <client>
- # <rating type="integer">1</rating>
- # <name>Summit</name>
- # </client>
- # <client>
- # <rating type="integer">1</rating>
- # <name>Microsoft</name>
- # </client>
- # </clients>
- # <account>
- # <id type="integer">1</id>
- # <credit-limit type="integer">50</credit-limit>
- # </account>
- # </firm>
- #
- # Additionally, the record being serialized will be passed to a Proc's second
- # parameter. This allows for ad hoc additions to the resultant document that
- # incorporate the context of the record being serialized. And by leveraging the
- # closure created by a Proc, to_xml can be used to add elements that normally fall
- # outside of the scope of the model -- for example, generating and appending URLs
- # associated with models.
- #
- # proc = Proc.new { |options, record| options[:builder].tag!('name-reverse', record.name.reverse) }
- # firm.to_xml procs: [ proc ]
- #
- # <firm>
- # # ... normal attributes as shown above ...
- # <name-reverse>slangis73</name-reverse>
- # </firm>
- #
- # To include deeper levels of associations pass a hash like this:
- #
- # firm.to_xml include: {account: {}, clients: {include: :address}}
- # <?xml version="1.0" encoding="UTF-8"?>
- # <firm>
- # <id type="integer">1</id>
- # <rating type="integer">1</rating>
- # <name>37signals</name>
- # <clients type="array">
- # <client>
- # <rating type="integer">1</rating>
- # <name>Summit</name>
- # <address>
- # ...
- # </address>
- # </client>
- # <client>
- # <rating type="integer">1</rating>
- # <name>Microsoft</name>
- # <address>
- # ...
- # </address>
- # </client>
- # </clients>
- # <account>
- # <id type="integer">1</id>
- # <credit-limit type="integer">50</credit-limit>
- # </account>
- # </firm>
- #
- # To include any methods on the model being called use <tt>:methods</tt>:
- #
- # firm.to_xml methods: [ :calculated_earnings, :real_earnings ]
- #
- # <firm>
- # # ... normal attributes as shown above ...
- # <calculated-earnings>100000000000000000</calculated-earnings>
- # <real-earnings>5</real-earnings>
- # </firm>
- #
- # To call any additional Procs use <tt>:procs</tt>. The Procs are passed a
- # modified version of the options hash that was given to +to_xml+:
- #
- # proc = Proc.new { |options| options[:builder].tag!('abc', 'def') }
- # firm.to_xml procs: [ proc ]
- #
- # <firm>
- # # ... normal attributes as shown above ...
- # <abc>def</abc>
- # </firm>
- #
- # Alternatively, you can yield the builder object as part of the +to_xml+ call:
- #
- # firm.to_xml do |xml|
- # xml.creator do
- # xml.first_name "David"
- # xml.last_name "Heinemeier Hansson"
- # end
- # end
- #
- # <firm>
- # # ... normal attributes as shown above ...
- # <creator>
- # <first_name>David</first_name>
- # <last_name>Heinemeier Hansson</last_name>
- # </creator>
- # </firm>
- #
- # As noted above, you may override +to_xml+ in your ActiveRecord::Base
- # subclasses to have complete control about what's generated. The general
- # form of doing this is:
- #
- # class IHaveMyOwnXML < ActiveRecord::Base
- # def to_xml(options = {})
- # require 'builder'
- # options[:indent] ||= 2
- # xml = options[:builder] ||= ::Builder::XmlMarkup.new(indent: options[:indent])
- # xml.instruct! unless options[:skip_instruct]
- # xml.level_one do
- # xml.tag!(:second_level, 'content')
- # end
- # end
- # end
- def to_xml(options = {}, &block)
- XmlSerializer.new(self, options).serialize(&block)
- end
- end
-
- class XmlSerializer < ActiveModel::Serializers::Xml::Serializer #:nodoc:
- class Attribute < ActiveModel::Serializers::Xml::Serializer::Attribute #:nodoc:
- def compute_type
- klass = @serializable.class
- column = klass.columns_hash[name] || Type::Value.new
-
- type = ActiveSupport::XmlMini::TYPE_NAMES[value.class.name] || column.type
-
- { :text => :string,
- :time => :datetime }[type] || type
- end
- protected :compute_type
- end
- end
-end
diff --git a/activerecord/lib/active_record/statement_cache.rb b/activerecord/lib/active_record/statement_cache.rb
index aece446384..f6b0efb88a 100644
--- a/activerecord/lib/active_record/statement_cache.rb
+++ b/activerecord/lib/active_record/statement_cache.rb
@@ -1,22 +1,35 @@
module ActiveRecord
# Statement cache is used to cache a single statement in order to avoid creating the AST again.
- # Initializing the cache is done by passing the statement in the initialization block:
+ # Initializing the cache is done by passing the statement in the create block:
#
- # cache = ActiveRecord::StatementCache.new do
- # Book.where(name: "my book").limit(100)
+ # cache = StatementCache.create(Book.connection) do |params|
+ # Book.where(name: "my book").where("author_id > 3")
# end
#
- # The cached statement is executed by using the +execute+ method:
+ # The cached statement is executed by using the
+ # [connection.execute]{rdoc-ref:ConnectionAdapters::DatabaseStatements#execute} method:
#
- # cache.execute
+ # cache.execute([], Book, Book.connection)
#
- # The relation returned by the block is cached, and for each +execute+ call the cached relation gets duped.
- # Database is queried when +to_a+ is called on the relation.
- class StatementCache
- class Substitute; end
+ # The relation returned by the block is cached, and for each
+ # [execute]{rdoc-ref:ConnectionAdapters::DatabaseStatements#execute}
+ # call the cached relation gets duped. Database is queried when +to_a+ is called on the relation.
+ #
+ # If you want to cache the statement without the values you can use the +bind+ method of the
+ # block parameter.
+ #
+ # cache = StatementCache.create(Book.connection) do |params|
+ # Book.where(name: params.bind)
+ # end
+ #
+ # And pass the bind values as the first argument of +execute+ call.
+ #
+ # cache.execute(["my book"], Book, Book.connection)
+ class StatementCache # :nodoc:
+ class Substitute; end # :nodoc:
- class Query
+ class Query # :nodoc:
def initialize(sql)
@sql = sql
end
@@ -26,7 +39,7 @@ module ActiveRecord
end
end
- class PartialQuery < Query
+ class PartialQuery < Query # :nodoc:
def initialize values
@values = values
@indexes = values.each_with_index.find_all { |thing,i|
@@ -36,8 +49,8 @@ module ActiveRecord
def sql_for(binds, connection)
val = @values.dup
- binds = binds.dup
- @indexes.each { |i| val[i] = connection.quote(*binds.shift.reverse) }
+ binds = connection.prepare_binds_for_database(binds)
+ @indexes.each { |i| val[i] = connection.quote(binds.shift) }
val.join
end
end
@@ -51,26 +64,26 @@ module ActiveRecord
PartialQuery.new collected
end
- class Params
+ class Params # :nodoc:
def bind; Substitute.new; end
end
- class BindMap
- def initialize(bind_values)
+ class BindMap # :nodoc:
+ def initialize(bound_attributes)
@indexes = []
- @bind_values = bind_values
+ @bound_attributes = bound_attributes
- bind_values.each_with_index do |(_, value), i|
- if Substitute === value
+ bound_attributes.each_with_index do |attr, i|
+ if Substitute === attr.value
@indexes << i
end
end
end
def bind(values)
- bvs = @bind_values.map { |pair| pair.dup }
- @indexes.each_with_index { |offset,i| bvs[offset][1] = values[i] }
- bvs
+ bas = @bound_attributes.dup
+ @indexes.each_with_index { |offset,i| bas[offset] = bas[offset].with_cast_value(values[i]) }
+ bas
end
end
@@ -78,7 +91,7 @@ module ActiveRecord
def self.create(connection, block = Proc.new)
relation = block.call Params.new
- bind_map = BindMap.new relation.bind_values
+ bind_map = BindMap.new relation.bound_attributes
query_builder = connection.cacheable_query relation.arel
new query_builder, bind_map
end
diff --git a/activerecord/lib/active_record/store.rb b/activerecord/lib/active_record/store.rb
index 3c291f28e3..1b407f7702 100644
--- a/activerecord/lib/active_record/store.rb
+++ b/activerecord/lib/active_record/store.rb
@@ -15,11 +15,16 @@ 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+. Simply use +store_accessor+ instead to generate
+ # 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].
+ # 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.
#
+ # NOTE: The default validations with the exception of +uniqueness+ will work.
+ # For example, if you want to check for +uniqueness+ with +hstore+ you will
+ # need to use a custom validation to handle it.
+ #
# Examples:
#
# class User < ActiveRecord::Base
@@ -39,7 +44,7 @@ module ActiveRecord
# store_accessor :settings, :privileges, :servants
# end
#
- # The stored attribute names can be retrieved using +stored_attributes+.
+ # The stored attribute names can be retrieved using {.stored_attributes}[rdoc-ref:rdoc-ref:ClassMethods#stored_attributes].
#
# User.stored_attributes[:settings] # [:color, :homepage]
#
diff --git a/activerecord/lib/active_record/suppressor.rb b/activerecord/lib/active_record/suppressor.rb
new file mode 100644
index 0000000000..b3644bf569
--- /dev/null
+++ b/activerecord/lib/active_record/suppressor.rb
@@ -0,0 +1,54 @@
+module ActiveRecord
+ # ActiveRecord::Suppressor prevents the receiver from being saved during
+ # a given block.
+ #
+ # For example, here's a pattern of creating notifications when new comments
+ # are posted. (The notification may in turn trigger an email, a push
+ # notification, or just appear in the UI somewhere):
+ #
+ # class Comment < ActiveRecord::Base
+ # belongs_to :commentable, polymorphic: true
+ # after_create -> { Notification.create! comment: self,
+ # recipients: commentable.recipients }
+ # end
+ #
+ # That's what you want the bulk of the time. New comment creates a new
+ # Notification. But there may well be off cases, like copying a commentable
+ # and its comments, where you don't want that. So you'd have a concern
+ # something like this:
+ #
+ # module Copyable
+ # def copy_to(destination)
+ # Notification.suppress do
+ # # Copy logic that creates new comments that we do not want
+ # # triggering notifications.
+ # end
+ # end
+ # end
+ module Suppressor
+ extend ActiveSupport::Concern
+
+ module ClassMethods
+ def suppress(&block)
+ SuppressorRegistry.suppressed[name] = true
+ yield
+ ensure
+ SuppressorRegistry.suppressed[name] = false
+ end
+ end
+
+ def create_or_update(*args) # :nodoc:
+ SuppressorRegistry.suppressed[self.class.name] ? true : super
+ end
+ end
+
+ class SuppressorRegistry # :nodoc:
+ extend ActiveSupport::PerThreadRegistry
+
+ attr_reader :suppressed
+
+ def initialize
+ @suppressed = {}
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/table_metadata.rb b/activerecord/lib/active_record/table_metadata.rb
new file mode 100644
index 0000000000..f9bb1cf5e0
--- /dev/null
+++ b/activerecord/lib/active_record/table_metadata.rb
@@ -0,0 +1,64 @@
+module ActiveRecord
+ class TableMetadata # :nodoc:
+ delegate :foreign_type, :foreign_key, to: :association, prefix: true
+ delegate :association_primary_key, to: :association
+
+ def initialize(klass, arel_table, association = nil)
+ @klass = klass
+ @arel_table = arel_table
+ @association = association
+ end
+
+ def resolve_column_aliases(hash)
+ # This method is a hot spot, so for now, use Hash[] to dup the hash.
+ # https://bugs.ruby-lang.org/issues/7166
+ new_hash = Hash[hash]
+ hash.each do |key, _|
+ if (key.is_a?(Symbol)) && klass.attribute_alias?(key)
+ new_hash[klass.attribute_alias(key)] = new_hash.delete(key)
+ end
+ end
+ new_hash
+ end
+
+ def arel_attribute(column_name)
+ arel_table[column_name]
+ end
+
+ def type(column_name)
+ if klass
+ klass.type_for_attribute(column_name.to_s)
+ else
+ Type::Value.new
+ end
+ end
+
+ def associated_with?(association_name)
+ klass && klass._reflect_on_association(association_name)
+ end
+
+ def associated_table(table_name)
+ return self if table_name == arel_table.name
+
+ association = klass._reflect_on_association(table_name)
+ if association && !association.polymorphic?
+ association_klass = association.klass
+ arel_table = association_klass.arel_table.alias(table_name)
+ else
+ type_caster = TypeCaster::Connection.new(klass, table_name)
+ association_klass = nil
+ arel_table = Arel::Table.new(table_name, type_caster: type_caster)
+ end
+
+ TableMetadata.new(association_klass, arel_table, association)
+ end
+
+ def polymorphic_association?
+ association && association.polymorphic?
+ end
+
+ protected
+
+ 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 b7315ed4b3..c0c29a618c 100644
--- a/activerecord/lib/active_record/tasks/database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/database_tasks.rb
@@ -1,9 +1,11 @@
+require 'active_support/core_ext/string/filters'
+
module ActiveRecord
module Tasks # :nodoc:
class DatabaseAlreadyExists < StandardError; end # :nodoc:
class DatabaseNotSupported < StandardError; end # :nodoc:
- # <tt>ActiveRecord::Tasks::DatabaseTasks</tt> is a utility class, which encapsulates
+ # ActiveRecord::Tasks::DatabaseTasks is a utility class, which encapsulates
# logic behind common tasks used to manage database and migrations.
#
# The tasks defined here are used with Rake tasks provided by Active Record.
@@ -16,15 +18,15 @@ module ActiveRecord
#
# The possible config values are:
#
- # * +env+: current environment (like Rails.env).
- # * +database_configuration+: configuration of your databases (as in +config/database.yml+).
- # * +db_dir+: your +db+ directory.
- # * +fixtures_path+: a path to fixtures directory.
- # * +migrations_paths+: a list of paths to directories with migrations.
- # * +seed_loader+: an object which will load seeds, it needs to respond to the +load_seed+ method.
- # * +root+: a path to the root of the application.
+ # * +env+: current environment (like Rails.env).
+ # * +database_configuration+: configuration of your databases (as in +config/database.yml+).
+ # * +db_dir+: your +db+ directory.
+ # * +fixtures_path+: a path to fixtures directory.
+ # * +migrations_paths+: a list of paths to directories with migrations.
+ # * +seed_loader+: an object which will load seeds, it needs to respond to the +load_seed+ method.
+ # * +root+: a path to the root of the application.
#
- # Example usage of +DatabaseTasks+ outside Rails could look as such:
+ # Example usage of DatabaseTasks outside Rails could look as such:
#
# include ActiveRecord::Tasks
# DatabaseTasks.database_configuration = YAML.load_file('my_database_config.yml')
@@ -92,8 +94,9 @@ module ActiveRecord
rescue DatabaseAlreadyExists
$stderr.puts "#{configuration['database']} already exists"
rescue Exception => error
- $stderr.puts error, *(error.backtrace)
+ $stderr.puts error
$stderr.puts "Couldn't create database for #{configuration.inspect}"
+ raise
end
def create_all
@@ -113,8 +116,9 @@ module ActiveRecord
rescue ActiveRecord::NoDatabaseError
$stderr.puts "Database '#{configuration['database']}' does not exist"
rescue Exception => error
- $stderr.puts error, *(error.backtrace)
+ $stderr.puts error
$stderr.puts "Couldn't drop #{configuration['database']}"
+ raise
end
def drop_all
@@ -127,6 +131,18 @@ module ActiveRecord
}
end
+ def migrate
+ verbose = ENV["VERBOSE"] ? ENV["VERBOSE"] == "true" : 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|
+ scope.blank? || scope == migration.scope
+ end
+ ensure
+ Migration.verbose = verbose_was
+ end
+
def charset_current(environment = env)
charset ActiveRecord::Base.configurations[environment]
end
@@ -159,6 +175,7 @@ module ActiveRecord
each_current_configuration(environment) { |configuration|
purge configuration
}
+ ActiveRecord::Base.establish_connection(environment.to_sym)
end
def structure_dump(*arguments)
@@ -173,21 +190,46 @@ module ActiveRecord
class_for_adapter(configuration['adapter']).new(*arguments).structure_load(filename)
end
- def load_schema(format = ActiveRecord::Base.schema_format, file = nil)
+ def load_schema(configuration, format = ActiveRecord::Base.schema_format, file = nil) # :nodoc:
+ file ||= schema_file(format)
+
case format
when :ruby
- file ||= File.join(db_dir, "schema.rb")
check_schema_file(file)
+ ActiveRecord::Base.establish_connection(configuration)
load(file)
when :sql
- file ||= File.join(db_dir, "structure.sql")
check_schema_file(file)
- structure_load(current_config, file)
+ structure_load(configuration, file)
else
raise ArgumentError, "unknown format #{format.inspect}"
end
end
+ def load_schema_for(*args)
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ This method was renamed to `#load_schema` and will be removed in the future.
+ Use `#load_schema` instead.
+ MSG
+ load_schema(*args)
+ end
+
+ def schema_file(format = ActiveRecord::Base.schema_format)
+ case format
+ when :ruby
+ File.join(db_dir, "schema.rb")
+ when :sql
+ File.join(db_dir, "structure.sql")
+ end
+ end
+
+ def load_schema_current(format = ActiveRecord::Base.schema_format, file = nil, environment = env)
+ each_current_configuration(environment) { |configuration|
+ load_schema configuration, format, file
+ }
+ ActiveRecord::Base.establish_connection(environment.to_sym)
+ end
+
def check_schema_file(filename)
unless File.exist?(filename)
message = %{#{filename} doesn't exist yet. Run `rake db:migrate` to create it, then try again.}
diff --git a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb
index d890196f47..8929aa85c8 100644
--- a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb
@@ -23,7 +23,7 @@ module ActiveRecord
end
rescue error_class => error
if error.respond_to?(:errno) && error.errno == ACCESS_DENIED_ERROR
- $stdout.print error.error
+ $stdout.print error.message
establish_connection root_configuration_without_database
connection.create_database configuration['database'], creation_options
if configuration['username'] != 'root'
@@ -31,6 +31,7 @@ module ActiveRecord
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
@@ -55,21 +56,21 @@ module ActiveRecord
end
def structure_dump(filename)
- args = prepare_command_options('mysqldump')
+ args = prepare_command_options
args.concat(["--result-file", "#{filename}"])
args.concat(["--no-data"])
+ args.concat(["--routines"])
args.concat(["#{configuration['database']}"])
- unless Kernel.system(*args)
- $stderr.puts "Could not dump the database structure. "\
- "Make sure `mysqldump` is in your PATH and check the command output for warnings."
- end
+
+ run_cmd('mysqldump', args, 'dumping')
end
def structure_load(filename)
- args = prepare_command_options('mysql')
+ args = prepare_command_options
args.concat(['--execute', %{SET FOREIGN_KEY_CHECKS = 0; SOURCE #{filename}; SET FOREIGN_KEY_CHECKS = 1}])
args.concat(["--database", "#{configuration['database']}"])
- Kernel.system(*args)
+
+ run_cmd('mysql', args, 'loading')
end
private
@@ -128,17 +129,33 @@ IDENTIFIED BY '#{configuration['password']}' WITH GRANT OPTION;
$stdin.gets.strip
end
- def prepare_command_options(command)
- args = [command]
- args.concat(['--user', configuration['username']]) if configuration['username']
- args << "--password=#{configuration['password']}" if configuration['password']
- args.concat(['--default-character-set', configuration['encoding']]) if configuration['encoding']
- configuration.slice('host', 'port', 'socket').each do |k, v|
- args.concat([ "--#{k}", v.to_s ]) if v
- end
+ def prepare_command_options
+ args = {
+ 'host' => '--host',
+ 'port' => '--port',
+ 'socket' => '--socket',
+ 'username' => '--user',
+ 'password' => '--password',
+ 'encoding' => '--default-character-set',
+ 'sslca' => '--ssl-ca',
+ 'sslcert' => '--ssl-cert',
+ 'sslcapath' => '--ssl-capath',
+ 'sslcipher' => '--ssh-cipher',
+ 'sslkey' => '--ssl-key'
+ }.map { |opt, arg| "#{arg}=#{configuration[opt]}" if configuration[opt] }.compact
args
end
+
+ def run_cmd(cmd, args, action)
+ fail run_cmd_error(cmd, args, action) unless Kernel.system(cmd, *args)
+ end
+
+ def run_cmd_error(cmd, args, action)
+ msg = "failed to execute: `#{cmd}`\n"
+ msg << "Please check the output above for any errors and make sure that `#{cmd}` is installed in your PATH and has proper permissions.\n\n"
+ msg
+ end
end
end
end
diff --git a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb
index 3d02ee07d0..cd7d949239 100644
--- a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb
@@ -1,5 +1,3 @@
-require 'shellwords'
-
module ActiveRecord
module Tasks # :nodoc:
class PostgreSQLDatabaseTasks # :nodoc:
@@ -46,20 +44,31 @@ module ActiveRecord
def structure_dump(filename)
set_psql_env
- search_path = configuration['schema_search_path']
- unless search_path.blank?
- search_path = search_path.split(",").map{|search_path_part| "--schema=#{Shellwords.escape(search_path_part.strip)}" }.join(" ")
- end
- command = "pg_dump -i -s -x -O -f #{Shellwords.escape(filename)} #{search_path} #{Shellwords.escape(configuration['database'])}"
- raise 'Error dumping database' unless Kernel.system(command)
+ search_path = case ActiveRecord::Base.dump_schemas
+ when :schema_search_path
+ configuration['schema_search_path']
+ when :all
+ nil
+ when String
+ ActiveRecord::Base.dump_schemas
+ end
- File.open(filename, "a") { |f| f << "SET search_path TO #{ActiveRecord::Base.connection.schema_search_path};\n\n" }
+ args = ['-s', '-x', '-O', '-f', filename]
+ unless search_path.blank?
+ args << search_path.split(',').map do |part|
+ "--schema=#{part.strip}"
+ end.join(' ')
+ end
+ args << configuration['database']
+ run_cmd('pg_dump', args, 'dumping')
+ File.open(filename, "a") { |f| f << "SET search_path TO #{connection.schema_search_path};\n\n" }
end
def structure_load(filename)
set_psql_env
- Kernel.system("psql -q -f #{Shellwords.escape(filename)} #{configuration['database']}")
+ args = [ '-q', '-f', filename, configuration['database'] ]
+ run_cmd('psql', args, 'loading' )
end
private
@@ -85,6 +94,17 @@ module ActiveRecord
ENV['PGPASSWORD'] = configuration['password'].to_s if configuration['password']
ENV['PGUSER'] = configuration['username'].to_s if configuration['username']
end
+
+ def run_cmd(cmd, args, action)
+ fail run_cmd_error(cmd, args, action) unless Kernel.system(cmd, *args)
+ end
+
+ def run_cmd_error(cmd, args, action)
+ msg = "failed to execute:\n"
+ msg << "#{cmd} #{args.join(' ')}\n\n"
+ msg << "Please check the output above for any errors and make sure that `#{cmd}` is installed in your PATH and has proper permissions.\n\n"
+ msg
+ end
end
end
end
diff --git a/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb b/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb
index 5688931db2..9ec3c8a94a 100644
--- a/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb
@@ -19,9 +19,17 @@ module ActiveRecord
path = Pathname.new configuration['database']
file = path.absolute? ? path.to_s : File.join(root, path)
- FileUtils.rm(file) if File.exist?(file)
+ FileUtils.rm(file)
+ rescue Errno::ENOENT => error
+ raise NoDatabaseError.new(error.message, error)
+ end
+
+ def purge
+ drop
+ rescue NoDatabaseError
+ ensure
+ create
end
- alias :purge :drop
def charset
connection.encoding
diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb
index ddf3e1804c..a572c109d8 100644
--- a/activerecord/lib/active_record/timestamp.rb
+++ b/activerecord/lib/active_record/timestamp.rb
@@ -1,5 +1,5 @@
module ActiveRecord
- # = Active Record Timestamp
+ # = Active Record \Timestamp
#
# Active Record automatically timestamps create and update operations if the
# table has fields named <tt>created_at/created_on</tt> or
@@ -15,14 +15,21 @@ module ActiveRecord
#
# == Time Zone aware attributes
#
- # By default, ActiveRecord::Base keeps all the datetime columns time zone aware by executing following code.
+ # Active Record keeps all the <tt>datetime</tt> and <tt>time</tt> columns
+ # time-zone aware. By default, these values are stored in the database as UTC
+ # and converted back to the current <tt>Time.zone</tt> when pulled from the database.
#
- # config.active_record.time_zone_aware_attributes = true
+ # This feature can be turned off completely by setting:
#
- # This feature can easily be turned off by assigning value <tt>false</tt> .
+ # config.active_record.time_zone_aware_attributes = false
#
- # If your attributes are time zone aware and you desire to skip time zone conversion to the current Time.zone
- # when reading certain attributes then you can do following:
+ # You can also specify that only <tt>datetime</tt> columns should be time-zone
+ # aware (while <tt>time</tt> should not) by setting:
+ #
+ # ActiveRecord::Base.time_zone_aware_types = [:datetime]
+ #
+ # Finally, you can indicate specific attributes of a model for which time zone
+ # conversion should not applied, for instance by setting:
#
# class Topic < ActiveRecord::Base
# self.skip_time_zone_conversion_for_attributes = [:written_on]
@@ -47,8 +54,9 @@ module ActiveRecord
current_time = current_time_from_proper_timezone
all_timestamp_attributes.each do |column|
- if respond_to?(column) && respond_to?("#{column}=") && self.send(column).nil?
- write_attribute(column.to_s, current_time)
+ column = column.to_s
+ if has_attribute?(column) && !attribute_present?(column)
+ write_attribute(column, current_time)
end
end
end
@@ -56,8 +64,8 @@ module ActiveRecord
super
end
- def _update_record(*args)
- if should_record_timestamps?
+ def _update_record(*args, touch: true, **options)
+ if touch && should_record_timestamps?
current_time = current_time_from_proper_timezone
timestamp_attributes_for_update_in_model.each do |column|
@@ -66,7 +74,7 @@ module ActiveRecord
write_attribute(column, current_time)
end
end
- super
+ super(*args)
end
def should_record_timestamps?
@@ -113,7 +121,7 @@ module ActiveRecord
def clear_timestamp_attributes
all_timestamp_attributes_in_model.each do |attribute_name|
self[attribute_name] = nil
- changed_attributes.delete(attribute_name)
+ clear_attribute_changes([attribute_name])
end
end
end
diff --git a/activerecord/lib/active_record/touch_later.rb b/activerecord/lib/active_record/touch_later.rb
new file mode 100644
index 0000000000..4352a0ffea
--- /dev/null
+++ b/activerecord/lib/active_record/touch_later.rb
@@ -0,0 +1,50 @@
+module ActiveRecord
+ # = Active Record Touch Later
+ module TouchLater
+ extend ActiveSupport::Concern
+
+ included do
+ before_commit_without_transaction_enrollment :touch_deferred_attributes
+ end
+
+ def touch_later(*names) # :nodoc:
+ raise ActiveRecordError, "cannot touch on a new record object" unless persisted?
+
+ @_defer_touch_attrs ||= timestamp_attributes_for_update_in_model
+ @_defer_touch_attrs |= names
+ @_touch_time = current_time_from_proper_timezone
+
+ surreptitiously_touch @_defer_touch_attrs
+ self.class.connection.add_transaction_record self
+ end
+
+ def touch(*names, time: nil) # :nodoc:
+ if has_defer_touch_attrs?
+ names |= @_defer_touch_attrs
+ end
+ super(*names, time: time)
+ end
+
+ private
+ def surreptitiously_touch(attrs)
+ attrs.each { |attr| write_attribute attr, @_touch_time }
+ clear_attribute_changes attrs
+ end
+
+ def touch_deferred_attributes
+ if has_defer_touch_attrs? && persisted?
+ @_touching_delayed_records = true
+ touch(*@_defer_touch_attrs, time: @_touch_time)
+ @_touching_delayed_records, @_defer_touch_attrs, @_touch_time = nil, nil, nil
+ end
+ end
+
+ def has_defer_touch_attrs?
+ defined?(@_defer_touch_attrs) && @_defer_touch_attrs.present?
+ end
+
+ def touching_delayed_records?
+ defined?(@_touching_delayed_records) && @_touching_delayed_records
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb
index 7e4dc4c895..8de82feae3 100644
--- a/activerecord/lib/active_record/transactions.rb
+++ b/activerecord/lib/active_record/transactions.rb
@@ -2,20 +2,25 @@ module ActiveRecord
# See ActiveRecord::Transactions::ClassMethods for documentation.
module Transactions
extend ActiveSupport::Concern
+ #:nodoc:
ACTIONS = [:create, :destroy, :update]
included do
define_callbacks :commit, :rollback,
- terminator: ->(_, result) { result == false },
+ :before_commit,
+ :before_commit_without_transaction_enrollment,
+ :commit_without_transaction_enrollment,
+ :rollback_without_transaction_enrollment,
+ terminator: deprecated_false_terminator,
scope: [:kind, :name]
end
# = Active Record Transactions
#
- # Transactions are protective blocks where SQL statements are only permanent
+ # \Transactions are protective blocks where SQL statements are only permanent
# if they can all succeed as one atomic action. The classic example is a
# transfer between two accounts where you can only have a deposit if the
- # withdrawal succeeded and vice versa. Transactions enforce the integrity of
+ # withdrawal succeeded and vice versa. \Transactions enforce the integrity of
# the database and guard the data against program errors or database
# break-downs. So basically you should use transaction blocks whenever you
# have a number of statements that must be executed together or not at all.
@@ -35,20 +40,20 @@ module ActiveRecord
#
# == Different Active Record classes in a single transaction
#
- # Though the transaction class method is called on some Active Record class,
+ # Though the #transaction class method is called on some Active Record class,
# the objects within the transaction block need not all be instances of
# that class. This is because transactions are per-database connection, not
# per-model.
#
# In this example a +balance+ record is transactionally saved even
- # though +transaction+ is called on the +Account+ class:
+ # though #transaction is called on the +Account+ class:
#
# Account.transaction do
# balance.save!
# account.save!
# end
#
- # The +transaction+ method is also available as a model instance method.
+ # The #transaction method is also available as a model instance method.
# For example, you can also do this:
#
# balance.transaction do
@@ -75,7 +80,8 @@ module ActiveRecord
#
# == +save+ and +destroy+ are automatically wrapped in a transaction
#
- # Both +save+ and +destroy+ come wrapped in a transaction that ensures
+ # Both {#save}[rdoc-ref:Persistence#save] and
+ # {#destroy}[rdoc-ref:Persistence#destroy] come wrapped in a transaction that ensures
# that whatever you do in validations or callbacks will happen under its
# protected cover. So you can use validations to check for values that
# the transaction depends on or you can raise exceptions in the callbacks
@@ -84,7 +90,7 @@ module ActiveRecord
# As a consequence changes to the database are not seen outside your connection
# until the operation is complete. For example, if you try to update the index
# of a search engine in +after_save+ the indexer won't see the updated record.
- # The +after_commit+ callback is the only one that is triggered once the update
+ # The #after_commit callback is the only one that is triggered once the update
# is committed. See below.
#
# == Exception handling and rolling back
@@ -93,11 +99,11 @@ module ActiveRecord
# be propagated (after triggering the ROLLBACK), so you should be ready to
# catch those in your application code.
#
- # One exception is the <tt>ActiveRecord::Rollback</tt> exception, which will trigger
+ # One exception is the ActiveRecord::Rollback exception, which will trigger
# a ROLLBACK when raised, but not be re-raised by the transaction block.
#
- # *Warning*: one should not catch <tt>ActiveRecord::StatementInvalid</tt> exceptions
- # inside a transaction block. <tt>ActiveRecord::StatementInvalid</tt> exceptions indicate that an
+ # *Warning*: one should not catch ActiveRecord::StatementInvalid exceptions
+ # inside a transaction block. ActiveRecord::StatementInvalid exceptions indicate that an
# error occurred at the database level, for example when a unique constraint
# is violated. On some database systems, such as PostgreSQL, database errors
# inside a transaction cause the entire transaction to become unusable
@@ -123,11 +129,11 @@ module ActiveRecord
# end
#
# One should restart the entire transaction if an
- # <tt>ActiveRecord::StatementInvalid</tt> occurred.
+ # ActiveRecord::StatementInvalid occurred.
#
# == Nested transactions
#
- # +transaction+ calls can be nested. By default, this makes all database
+ # #transaction calls can be nested. By default, this makes all database
# statements in the nested transaction block become part of the parent
# transaction. For example, the following behavior may be surprising:
#
@@ -139,7 +145,7 @@ module ActiveRecord
# end
# end
#
- # creates both "Kotori" and "Nemu". Reason is the <tt>ActiveRecord::Rollback</tt>
+ # creates both "Kotori" and "Nemu". Reason is the ActiveRecord::Rollback
# exception in the nested block does not issue a ROLLBACK. Since these exceptions
# are captured in transaction blocks, the parent block does not see it and the
# real transaction is committed.
@@ -163,22 +169,22 @@ module ActiveRecord
# writing, the only database that we're aware of that supports true nested
# transactions, is MS-SQL. Because of this, Active Record emulates nested
# transactions by using savepoints on MySQL and PostgreSQL. See
- # http://dev.mysql.com/doc/refman/5.6/en/savepoint.html
+ # http://dev.mysql.com/doc/refman/5.7/en/savepoint.html
# for more information about savepoints.
#
- # === Callbacks
+ # === \Callbacks
#
# There are two types of callbacks associated with committing and rolling back transactions:
- # +after_commit+ and +after_rollback+.
+ # #after_commit and #after_rollback.
#
- # +after_commit+ callbacks are called on every record saved or destroyed within a
- # transaction immediately after the transaction is committed. +after_rollback+ callbacks
+ # #after_commit callbacks are called on every record saved or destroyed within a
+ # transaction immediately after the transaction is committed. #after_rollback callbacks
# are called on every record saved or destroyed within a transaction immediately after the
# transaction or savepoint is rolled back.
#
# These callbacks are useful for interacting with other systems since you will be guaranteed
# that the callback is only executed when the database is in a permanent state. For example,
- # +after_commit+ is a good spot to put in a hook to clearing a cache since clearing it from
+ # #after_commit is a good spot to put in a hook to clearing a cache since clearing it from
# within a transaction could trigger the cache to be regenerated before the database is updated.
#
# === Caveats
@@ -192,20 +198,24 @@ module ActiveRecord
# automatically released. The following example demonstrates the problem:
#
# Model.connection.transaction do # BEGIN
- # Model.connection.transaction(requires_new: true) do # CREATE SAVEPOINT active_record_1
+ # Model.connection.transaction(requires_new: true) do # CREATE SAVEPOINT active_record_1
# Model.connection.create_table(...) # active_record_1 now automatically released
- # end # RELEASE savepoint active_record_1
+ # end # RELEASE SAVEPOINT active_record_1
# # ^^^^ BOOM! database error!
# end
#
# Note that "TRUNCATE" is also a MySQL DDL statement!
module ClassMethods
- # See ActiveRecord::Transactions::ClassMethods for detailed documentation.
+ # See the ConnectionAdapters::DatabaseStatements#transaction API docs.
def transaction(options = {}, &block)
- # See the ConnectionAdapters::DatabaseStatements#transaction API docs.
connection.transaction(options, &block)
end
+ def before_commit(*args, &block) # :nodoc:
+ set_options_for_callbacks!(args)
+ set_callback(:before_commit, :before, *args, &block)
+ end
+
# This callback is called after a record has been created, updated, or destroyed.
#
# You can specify that the callback should only be fired by a certain action with
@@ -218,8 +228,6 @@ module ActiveRecord
# after_commit :do_foo_bar, on: [:create, :update]
# after_commit :do_bar_baz, on: [:update, :destroy]
#
- # Note that transactional fixtures do not play well with this feature. Please
- # use the +test_after_commit+ gem to have these hooks fired in tests.
def after_commit(*args, &block)
set_options_for_callbacks!(args)
set_callback(:commit, :after, *args, &block)
@@ -227,12 +235,37 @@ module ActiveRecord
# This callback is called after a create, update, or destroy are rolled back.
#
- # Please check the documentation of +after_commit+ for options.
+ # Please check the documentation of #after_commit for options.
def after_rollback(*args, &block)
set_options_for_callbacks!(args)
set_callback(:rollback, :after, *args, &block)
end
+ def before_commit_without_transaction_enrollment(*args, &block) # :nodoc:
+ set_options_for_callbacks!(args)
+ set_callback(:before_commit_without_transaction_enrollment, :before, *args, &block)
+ end
+
+ def after_commit_without_transaction_enrollment(*args, &block) # :nodoc:
+ set_options_for_callbacks!(args)
+ set_callback(:commit_without_transaction_enrollment, :after, *args, &block)
+ end
+
+ def after_rollback_without_transaction_enrollment(*args, &block) # :nodoc:
+ set_options_for_callbacks!(args)
+ set_callback(:rollback_without_transaction_enrollment, :after, *args, &block)
+ end
+
+ def raise_in_transactional_callbacks
+ ActiveSupport::Deprecation.warn('ActiveRecord::Base.raise_in_transactional_callbacks is deprecated and will be removed without replacement.')
+ true
+ end
+
+ def raise_in_transactional_callbacks=(value)
+ ActiveSupport::Deprecation.warn('ActiveRecord::Base.raise_in_transactional_callbacks= is deprecated, has no effect and will be removed without replacement.')
+ value
+ end
+
private
def set_options_for_callbacks!(args)
@@ -247,7 +280,7 @@ module ActiveRecord
def assert_valid_transaction_action(actions)
if (actions - ACTIONS).any?
- raise ArgumentError, ":on conditions for after_commit and after_rollback callbacks have to be one of #{ACTIONS.join(",")}"
+ raise ArgumentError, ":on conditions for after_commit and after_rollback callbacks have to be one of #{ACTIONS}"
end
end
end
@@ -286,31 +319,46 @@ module ActiveRecord
clear_transaction_record_state
end
- # Call the +after_commit+ callbacks.
+ def before_committed! # :nodoc:
+ _run_before_commit_without_transaction_enrollment_callbacks
+ _run_before_commit_callbacks
+ end
+
+ # Call the #after_commit callbacks.
#
# Ensure that it is not called if the object was never persisted (failed create),
# but call it after the commit of a destroyed object.
- def committed! #:nodoc:
- run_callbacks :commit if destroyed? || persisted?
+ def committed!(should_run_callbacks: true) #:nodoc:
+ if should_run_callbacks && destroyed? || persisted?
+ _run_commit_without_transaction_enrollment_callbacks
+ _run_commit_callbacks
+ end
ensure
force_clear_transaction_record_state
end
- # Call the +after_rollback+ callbacks. The +force_restore_state+ argument indicates if the record
+ # Call the #after_rollback callbacks. The +force_restore_state+ argument indicates if the record
# state should be rolled back to the beginning or just to the last savepoint.
- def rolledback!(force_restore_state = false) #:nodoc:
- run_callbacks :rollback
+ def rolledback!(force_restore_state: false, should_run_callbacks: true) #:nodoc:
+ if should_run_callbacks
+ _run_rollback_callbacks
+ _run_rollback_without_transaction_enrollment_callbacks
+ end
ensure
restore_transaction_record_state(force_restore_state)
clear_transaction_record_state
end
- # Add the record to the current transaction so that the +after_rollback+ and +after_commit+ callbacks
+ # Add the record to the current transaction so that the #after_rollback and #after_commit callbacks
# can be called.
def add_to_transaction
- if self.class.connection.add_transaction_record(self)
- remember_transaction_record_state
+ if has_transactional_callbacks?
+ self.class.connection.add_transaction_record(self)
+ else
+ sync_with_transaction_state
+ set_transaction_state(self.class.connection.transaction_state)
end
+ remember_transaction_record_state
end
# Executes +method+ within a transaction and captures its return value as a
@@ -333,6 +381,10 @@ module ActiveRecord
raise ActiveRecord::Rollback unless status
end
status
+ ensure
+ if @transaction_state && @transaction_state.committed?
+ clear_transaction_record_state
+ end
end
protected
@@ -340,14 +392,12 @@ module ActiveRecord
# Save the new record state and id of a record so it can be restored later if a transaction fails.
def remember_transaction_record_state #:nodoc:
@_start_transaction_state[:id] = id
- unless @_start_transaction_state.include?(:new_record)
- @_start_transaction_state[:new_record] = @new_record
- end
- unless @_start_transaction_state.include?(:destroyed)
- @_start_transaction_state[:destroyed] = @destroyed
- end
+ @_start_transaction_state.reverse_merge!(
+ new_record: @new_record,
+ destroyed: @destroyed,
+ frozen?: frozen?,
+ )
@_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) + 1
- @_start_transaction_state[:frozen?] = frozen?
end
# Clear the new record state and id of a record.
@@ -367,10 +417,14 @@ module ActiveRecord
transaction_level = (@_start_transaction_state[:level] || 0) - 1
if transaction_level < 1 || force
restore_state = @_start_transaction_state
- thaw unless restore_state[:frozen?]
+ thaw
@new_record = restore_state[:new_record]
@destroyed = restore_state[:destroyed]
- write_attribute(self.class.primary_key, restore_state[:id])
+ pk = self.class.primary_key
+ if pk && read_attribute(pk) != restore_state[:id]
+ write_attribute(pk, restore_state[:id])
+ end
+ freeze if restore_state[:frozen?]
end
end
end
@@ -393,5 +447,43 @@ module ActiveRecord
end
end
end
+
+ private
+
+ def set_transaction_state(state) # :nodoc:
+ @transaction_state = state
+ end
+
+ def has_transactional_callbacks? # :nodoc:
+ !_rollback_callbacks.empty? || !_commit_callbacks.empty? || !_before_commit_callbacks.empty?
+ end
+
+ # Updates the attributes on this particular Active Record object so that
+ # if it's associated with a transaction, then the state of the Active Record
+ # object will be updated to reflect the current state of the transaction
+ #
+ # The +@transaction_state+ variable stores the states of the associated
+ # transaction. This relies on the fact that a transaction can only be in
+ # one rollback or commit (otherwise a list of states would be required)
+ # Each Active Record object inside of a transaction carries that transaction's
+ # TransactionState.
+ #
+ # This method checks to see if the ActiveRecord object's state reflects
+ # the TransactionState, and rolls back or commits the Active Record object
+ # as appropriate.
+ #
+ # Since Active Record objects can be inside multiple transactions, this
+ # method recursively goes through the parent of the TransactionState and
+ # checks if the Active Record object reflects the state of the object.
+ def sync_with_transaction_state
+ update_attributes_from_transaction_state(@transaction_state)
+ end
+
+ def update_attributes_from_transaction_state(transaction_state)
+ if transaction_state && transaction_state.finalized?
+ restore_transaction_record_state if transaction_state.rolledback?
+ clear_transaction_record_state
+ end
+ end
end
end
diff --git a/activerecord/lib/active_record/type.rb b/activerecord/lib/active_record/type.rb
index f1384e0bb2..e210e94f00 100644
--- a/activerecord/lib/active_record/type.rb
+++ b/activerecord/lib/active_record/type.rb
@@ -1,20 +1,72 @@
-require 'active_record/type/mutable'
-require 'active_record/type/numeric'
-require 'active_record/type/time_value'
-require 'active_record/type/value'
+require 'active_model/type'
+
+require 'active_record/type/internal/abstract_json'
+require 'active_record/type/internal/timezone'
-require 'active_record/type/binary'
-require 'active_record/type/boolean'
require 'active_record/type/date'
require 'active_record/type/date_time'
-require 'active_record/type/decimal'
-require 'active_record/type/decimal_without_scale'
-require 'active_record/type/float'
-require 'active_record/type/integer'
-require 'active_record/type/serialized'
-require 'active_record/type/string'
-require 'active_record/type/text'
require 'active_record/type/time'
+require 'active_record/type/serialized'
+require 'active_record/type/adapter_specific_registry'
+
require 'active_record/type/type_map'
require 'active_record/type/hash_lookup_type_map'
+
+module ActiveRecord
+ module Type
+ @registry = AdapterSpecificRegistry.new
+
+ class << self
+ attr_accessor :registry # :nodoc:
+ delegate :add_modifier, to: :registry
+
+ # Add a new type to the registry, allowing it to be referenced as a
+ # symbol by {ActiveRecord::Base.attribute}[rdoc-ref:Attributes::ClassMethods#attribute].
+ # If your type is only meant to be used with a specific database adapter, you can
+ # do so by passing <tt>adapter: :postgresql</tt>. If your type has the same
+ # name as a native type for the current adapter, an exception will be
+ # raised unless you specify an +:override+ option. <tt>override: true</tt> will
+ # cause your type to be used instead of the native type. <tt>override:
+ # false</tt> will cause the native type to be used over yours if one exists.
+ def register(type_name, klass = nil, **options, &block)
+ registry.register(type_name, klass, **options, &block)
+ end
+
+ def lookup(*args, adapter: current_adapter_name, **kwargs) # :nodoc:
+ registry.lookup(*args, adapter: adapter, **kwargs)
+ end
+
+ private
+
+ def current_adapter_name
+ ActiveRecord::Base.connection.adapter_name.downcase.to_sym
+ end
+ end
+
+ Helpers = ActiveModel::Type::Helpers
+ BigInteger = ActiveModel::Type::BigInteger
+ Binary = ActiveModel::Type::Binary
+ Boolean = ActiveModel::Type::Boolean
+ Decimal = ActiveModel::Type::Decimal
+ DecimalWithoutScale = ActiveModel::Type::DecimalWithoutScale
+ Float = ActiveModel::Type::Float
+ Integer = ActiveModel::Type::Integer
+ String = ActiveModel::Type::String
+ Text = ActiveModel::Type::Text
+ UnsignedInteger = ActiveModel::Type::UnsignedInteger
+ Value = ActiveModel::Type::Value
+
+ register(:big_integer, Type::BigInteger, override: false)
+ register(:binary, Type::Binary, override: false)
+ register(:boolean, Type::Boolean, override: false)
+ register(:date, Type::Date, override: false)
+ register(:date_time, Type::DateTime, override: false)
+ register(:decimal, Type::Decimal, override: false)
+ register(:float, Type::Float, override: false)
+ register(:integer, Type::Integer, override: false)
+ register(:string, Type::String, override: false)
+ register(:text, Type::Text, override: false)
+ register(:time, Type::Time, override: false)
+ end
+end
diff --git a/activerecord/lib/active_record/type/adapter_specific_registry.rb b/activerecord/lib/active_record/type/adapter_specific_registry.rb
new file mode 100644
index 0000000000..d440eac619
--- /dev/null
+++ b/activerecord/lib/active_record/type/adapter_specific_registry.rb
@@ -0,0 +1,130 @@
+require 'active_model/type/registry'
+
+module ActiveRecord
+ # :stopdoc:
+ module Type
+ class AdapterSpecificRegistry < ActiveModel::Type::Registry
+ def add_modifier(options, klass, **args)
+ registrations << DecorationRegistration.new(options, klass, **args)
+ end
+
+ private
+
+ def registration_klass
+ Registration
+ end
+
+ def find_registration(symbol, *args)
+ registrations
+ .select { |registration| registration.matches?(symbol, *args) }
+ .max
+ end
+ end
+
+ class Registration
+ def initialize(name, block, adapter: nil, override: nil)
+ @name = name
+ @block = block
+ @adapter = adapter
+ @override = override
+ end
+
+ def call(_registry, *args, adapter: nil, **kwargs)
+ if kwargs.any? # https://bugs.ruby-lang.org/issues/10856
+ block.call(*args, **kwargs)
+ else
+ block.call(*args)
+ end
+ end
+
+ def matches?(type_name, *args, **kwargs)
+ type_name == name && matches_adapter?(**kwargs)
+ end
+
+ def <=>(other)
+ if conflicts_with?(other)
+ raise TypeConflictError.new("Type #{name} was registered for all
+ adapters, but shadows a native type with
+ the same name for #{other.adapter}".squish)
+ end
+ priority <=> other.priority
+ end
+
+ protected
+
+ attr_reader :name, :block, :adapter, :override
+
+ def priority
+ result = 0
+ if adapter
+ result |= 1
+ end
+ if override
+ result |= 2
+ end
+ result
+ end
+
+ def priority_except_adapter
+ priority & 0b111111100
+ end
+
+ private
+
+ def matches_adapter?(adapter: nil, **)
+ (self.adapter.nil? || adapter == self.adapter)
+ end
+
+ def conflicts_with?(other)
+ same_priority_except_adapter?(other) &&
+ has_adapter_conflict?(other)
+ end
+
+ def same_priority_except_adapter?(other)
+ priority_except_adapter == other.priority_except_adapter
+ end
+
+ def has_adapter_conflict?(other)
+ (override.nil? && other.adapter) ||
+ (adapter && other.override.nil?)
+ end
+ end
+
+ class DecorationRegistration < Registration
+ def initialize(options, klass, adapter: nil)
+ @options = options
+ @klass = klass
+ @adapter = adapter
+ end
+
+ def call(registry, *args, **kwargs)
+ subtype = registry.lookup(*args, **kwargs.except(*options.keys))
+ klass.new(subtype)
+ end
+
+ def matches?(*args, **kwargs)
+ matches_adapter?(**kwargs) && matches_options?(**kwargs)
+ end
+
+ def priority
+ super | 4
+ end
+
+ protected
+
+ attr_reader :options, :klass
+
+ private
+
+ def matches_options?(**kwargs)
+ options.all? do |key, value|
+ kwargs[key] == value
+ end
+ end
+ end
+ end
+
+ class TypeConflictError < StandardError
+ end
+ # :startdoc:
+end
diff --git a/activerecord/lib/active_record/type/binary.rb b/activerecord/lib/active_record/type/binary.rb
deleted file mode 100644
index d29ff4e494..0000000000
--- a/activerecord/lib/active_record/type/binary.rb
+++ /dev/null
@@ -1,40 +0,0 @@
-module ActiveRecord
- module Type
- class Binary < Value # :nodoc:
- def type
- :binary
- end
-
- def binary?
- true
- end
-
- def type_cast(value)
- if value.is_a?(Data)
- value.to_s
- else
- super
- end
- end
-
- def type_cast_for_database(value)
- return if value.nil?
- Data.new(super)
- end
-
- class Data # :nodoc:
- def initialize(value)
- @value = value.to_s
- end
-
- def to_s
- @value
- end
-
- def hex
- @value.unpack('H*')[0]
- end
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/type/boolean.rb b/activerecord/lib/active_record/type/boolean.rb
deleted file mode 100644
index 06dd17ed28..0000000000
--- a/activerecord/lib/active_record/type/boolean.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-module ActiveRecord
- module Type
- class Boolean < Value # :nodoc:
- def type
- :boolean
- end
-
- private
-
- def cast_value(value)
- if value == ''
- nil
- else
- ConnectionAdapters::Column::TRUE_VALUES.include?(value)
- end
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/type/date.rb b/activerecord/lib/active_record/type/date.rb
index d90a6069b7..ccafed054e 100644
--- a/activerecord/lib/active_record/type/date.rb
+++ b/activerecord/lib/active_record/type/date.rb
@@ -1,46 +1,7 @@
module ActiveRecord
module Type
- class Date < Value # :nodoc:
- def type
- :date
- end
-
- def klass
- ::Date
- end
-
- def type_cast_for_schema(value)
- "'#{value.to_s(:db)}'"
- end
-
- private
-
- def cast_value(value)
- if value.is_a?(::String)
- return if value.empty?
- fast_string_to_date(value) || fallback_string_to_date(value)
- elsif value.respond_to?(:to_date)
- value.to_date
- else
- value
- end
- end
-
- def fast_string_to_date(string)
- if string =~ ConnectionAdapters::Column::Format::ISO_DATE
- new_date $1.to_i, $2.to_i, $3.to_i
- end
- end
-
- def fallback_string_to_date(string)
- new_date(*::Date._parse(string, false).values_at(:year, :mon, :mday))
- end
-
- def new_date(year, mon, mday)
- if year && year != 0
- ::Date.new(year, mon, mday) rescue nil
- end
- end
+ class Date < ActiveModel::Type::Date
+ include Internal::Timezone
end
end
end
diff --git a/activerecord/lib/active_record/type/date_time.rb b/activerecord/lib/active_record/type/date_time.rb
index 5f19608a33..1fb9380ecd 100644
--- a/activerecord/lib/active_record/type/date_time.rb
+++ b/activerecord/lib/active_record/type/date_time.rb
@@ -1,43 +1,7 @@
module ActiveRecord
module Type
- class DateTime < Value # :nodoc:
- include TimeValue
-
- def type
- :datetime
- end
-
- def type_cast_for_database(value)
- zone_conversion_method = ActiveRecord::Base.default_timezone == :utc ? :getutc : :getlocal
-
- if value.acts_like?(:time)
- value.send(zone_conversion_method)
- else
- super
- end
- end
-
- private
-
- def cast_value(string)
- return string unless string.is_a?(::String)
- return if string.empty?
-
- fast_string_to_time(string) || fallback_string_to_time(string)
- end
-
- # '0.123456' -> 123456
- # '1.123456' -> 123456
- def microseconds(time)
- time[:sec_fraction] ? (time[:sec_fraction] * 1_000_000).to_i : 0
- end
-
- def fallback_string_to_time(string)
- time_hash = ::Date._parse(string)
- time_hash[:sec_fraction] = microseconds(time_hash)
-
- new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction, :offset))
- end
+ class DateTime < ActiveModel::Type::DateTime
+ include Internal::Timezone
end
end
end
diff --git a/activerecord/lib/active_record/type/decimal.rb b/activerecord/lib/active_record/type/decimal.rb
deleted file mode 100644
index ba5d244729..0000000000
--- a/activerecord/lib/active_record/type/decimal.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-module ActiveRecord
- module Type
- class Decimal < Value # :nodoc:
- include Numeric
-
- def type
- :decimal
- end
-
- def type_cast_for_schema(value)
- value.to_s
- end
-
- private
-
- def cast_value(value)
- if value.is_a?(::Numeric) || value.is_a?(::String)
- BigDecimal(value, precision.to_i)
- elsif value.respond_to?(:to_d)
- value.to_d
- else
- cast_value(value.to_s)
- end
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/type/decimal_without_scale.rb b/activerecord/lib/active_record/type/decimal_without_scale.rb
deleted file mode 100644
index cabdcecdd7..0000000000
--- a/activerecord/lib/active_record/type/decimal_without_scale.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-require 'active_record/type/integer'
-
-module ActiveRecord
- module Type
- class DecimalWithoutScale < Integer # :nodoc:
- def type
- :decimal
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/type/float.rb b/activerecord/lib/active_record/type/float.rb
deleted file mode 100644
index 42eb44b9a9..0000000000
--- a/activerecord/lib/active_record/type/float.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-module ActiveRecord
- module Type
- class Float < Value # :nodoc:
- include Numeric
-
- def type
- :float
- end
-
- alias type_cast_for_database type_cast
-
- private
-
- def cast_value(value)
- value.to_f
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/type/hash_lookup_type_map.rb b/activerecord/lib/active_record/type/hash_lookup_type_map.rb
index bf92680268..3b01e3f8ca 100644
--- a/activerecord/lib/active_record/type/hash_lookup_type_map.rb
+++ b/activerecord/lib/active_record/type/hash_lookup_type_map.rb
@@ -1,18 +1,22 @@
module ActiveRecord
module Type
class HashLookupTypeMap < TypeMap # :nodoc:
- delegate :key?, to: :@mapping
+ def alias_type(type, alias_type)
+ register_type(type) { |_, *args| lookup(alias_type, *args) }
+ end
- def lookup(type, *args)
- @mapping.fetch(type, proc { default_value }).call(type, *args)
+ def key?(key)
+ @mapping.key?(key)
end
- def fetch(type, *args, &block)
- @mapping.fetch(type, block).call(type, *args)
+ def keys
+ @mapping.keys
end
- def alias_type(type, alias_type)
- register_type(type) { |_, *args| lookup(alias_type, *args) }
+ private
+
+ def perform_fetch(type, *args, &block)
+ @mapping.fetch(type, block).call(type, *args)
end
end
end
diff --git a/activerecord/lib/active_record/type/integer.rb b/activerecord/lib/active_record/type/integer.rb
deleted file mode 100644
index 08477d1303..0000000000
--- a/activerecord/lib/active_record/type/integer.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-module ActiveRecord
- module Type
- class Integer < Value # :nodoc:
- include Numeric
-
- def type
- :integer
- end
-
- alias type_cast_for_database type_cast
-
- private
-
- def cast_value(value)
- case value
- when true then 1
- when false then 0
- else value.to_i rescue nil
- end
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/type/internal/abstract_json.rb b/activerecord/lib/active_record/type/internal/abstract_json.rb
new file mode 100644
index 0000000000..097d1bd363
--- /dev/null
+++ b/activerecord/lib/active_record/type/internal/abstract_json.rb
@@ -0,0 +1,33 @@
+module ActiveRecord
+ module Type
+ module Internal # :nodoc:
+ class AbstractJson < ActiveModel::Type::Value # :nodoc:
+ include ActiveModel::Type::Helpers::Mutable
+
+ def type
+ :json
+ end
+
+ def deserialize(value)
+ if value.is_a?(::String)
+ ::ActiveSupport::JSON.decode(value) rescue nil
+ else
+ value
+ end
+ end
+
+ def serialize(value)
+ if value.is_a?(::Array) || value.is_a?(::Hash)
+ ::ActiveSupport::JSON.encode(value)
+ else
+ value
+ end
+ end
+
+ def accessor
+ ActiveRecord::Store::StringKeyedHashAccessor
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/internal/timezone.rb b/activerecord/lib/active_record/type/internal/timezone.rb
new file mode 100644
index 0000000000..947e06158a
--- /dev/null
+++ b/activerecord/lib/active_record/type/internal/timezone.rb
@@ -0,0 +1,15 @@
+module ActiveRecord
+ module Type
+ module Internal
+ module Timezone
+ def is_utc?
+ ActiveRecord::Base.default_timezone == :utc
+ end
+
+ def default_timezone
+ ActiveRecord::Base.default_timezone
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/mutable.rb b/activerecord/lib/active_record/type/mutable.rb
deleted file mode 100644
index 066617ea59..0000000000
--- a/activerecord/lib/active_record/type/mutable.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-module ActiveRecord
- module Type
- module Mutable # :nodoc:
- def type_cast_from_user(value)
- type_cast_from_database(type_cast_for_database(value))
- end
-
- # +raw_old_value+ will be the `_before_type_cast` version of the
- # value (likely a string). +new_value+ will be the current, type
- # cast value.
- def changed_in_place?(raw_old_value, new_value)
- raw_old_value != type_cast_for_database(new_value)
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/type/numeric.rb b/activerecord/lib/active_record/type/numeric.rb
deleted file mode 100644
index fa43266504..0000000000
--- a/activerecord/lib/active_record/type/numeric.rb
+++ /dev/null
@@ -1,36 +0,0 @@
-module ActiveRecord
- module Type
- module Numeric # :nodoc:
- def number?
- true
- end
-
- def type_cast(value)
- value = case value
- when true then 1
- when false then 0
- when ::String then value.presence
- else value
- end
- super(value)
- end
-
- def changed?(old_value, _new_value, new_value_before_type_cast) # :nodoc:
- super || number_to_non_number?(old_value, new_value_before_type_cast)
- end
-
- private
-
- def number_to_non_number?(old_value, new_value_before_type_cast)
- old_value != nil && non_numeric_string?(new_value_before_type_cast)
- end
-
- def non_numeric_string?(value)
- # 'wibble'.to_i will give zero, we want to make sure
- # that we aren't marking int zero to string zero as
- # changed.
- value.to_s !~ /\A\d+\.?\d*\z/
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/type/serialized.rb b/activerecord/lib/active_record/type/serialized.rb
index 42bbed7103..4ff0740cfb 100644
--- a/activerecord/lib/active_record/type/serialized.rb
+++ b/activerecord/lib/active_record/type/serialized.rb
@@ -1,7 +1,7 @@
module ActiveRecord
module Type
- class Serialized < SimpleDelegator # :nodoc:
- include Mutable
+ class Serialized < DelegateClass(ActiveModel::Type::Value) # :nodoc:
+ include ActiveModel::Type::Helpers::Mutable
attr_reader :subtype, :coder
@@ -11,39 +11,45 @@ module ActiveRecord
super(subtype)
end
- def type_cast_from_database(value)
- if is_default_value?(value)
+ def deserialize(value)
+ if default_value?(value)
value
else
coder.load(super)
end
end
- def type_cast_for_database(value)
+ def serialize(value)
return if value.nil?
- unless is_default_value?(value)
+ unless default_value?(value)
super coder.dump(value)
end
end
- def accessor
- ActiveRecord::Store::IndifferentHashAccessor
+ def inspect
+ Kernel.instance_method(:inspect).bind(self).call
end
- def init_with(coder)
- @subtype = coder['subtype']
- @coder = coder['coder']
- __setobj__(@subtype)
+ def changed_in_place?(raw_old_value, value)
+ return false if value.nil?
+ raw_new_value = serialize(value)
+ raw_old_value.nil? != raw_new_value.nil? ||
+ subtype.changed_in_place?(raw_old_value, raw_new_value)
end
- def encode_with(coder)
- coder['subtype'] = @subtype
- coder['coder'] = @coder
+ def accessor
+ ActiveRecord::Store::IndifferentHashAccessor
+ end
+
+ def assert_valid_value(value)
+ if coder.respond_to?(:assert_valid_value)
+ coder.assert_valid_value(value)
+ end
end
private
- def is_default_value?(value)
+ def default_value?(value)
value == coder.load(nil)
end
end
diff --git a/activerecord/lib/active_record/type/string.rb b/activerecord/lib/active_record/type/string.rb
deleted file mode 100644
index 150defb106..0000000000
--- a/activerecord/lib/active_record/type/string.rb
+++ /dev/null
@@ -1,36 +0,0 @@
-module ActiveRecord
- module Type
- class String < Value # :nodoc:
- def type
- :string
- end
-
- def changed_in_place?(raw_old_value, new_value)
- if new_value.is_a?(::String)
- raw_old_value != new_value
- end
- end
-
- def type_cast_for_database(value)
- case value
- when ::Numeric, ActiveSupport::Duration then value.to_s
- when ::String then ::String.new(value)
- when true then "1"
- when false then "0"
- else super
- end
- end
-
- private
-
- def cast_value(value)
- case value
- when true then "1"
- when false then "0"
- # String.new is slightly faster than dup
- else ::String.new(value.to_s)
- end
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/type/text.rb b/activerecord/lib/active_record/type/text.rb
deleted file mode 100644
index 26f980f060..0000000000
--- a/activerecord/lib/active_record/type/text.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-require 'active_record/type/string'
-
-module ActiveRecord
- module Type
- class Text < String # :nodoc:
- def type
- :text
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/type/time.rb b/activerecord/lib/active_record/type/time.rb
index 41f7d97f0c..70988d84ff 100644
--- a/activerecord/lib/active_record/type/time.rb
+++ b/activerecord/lib/active_record/type/time.rb
@@ -1,26 +1,8 @@
module ActiveRecord
module Type
- class Time < Value # :nodoc:
- include TimeValue
-
- def type
- :time
- end
-
- private
-
- def cast_value(value)
- return value unless value.is_a?(::String)
- return if value.empty?
-
- dummy_time_value = "2000-01-01 #{value}"
-
- fast_string_to_time(dummy_time_value) || begin
- time_hash = ::Date._parse(dummy_time_value)
- return if time_hash[:hour].nil?
- new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction))
- end
- end
+ class Time < ActiveModel::Type::Time
+ include Internal::Timezone
end
end
end
+
diff --git a/activerecord/lib/active_record/type/time_value.rb b/activerecord/lib/active_record/type/time_value.rb
deleted file mode 100644
index d611d72dd4..0000000000
--- a/activerecord/lib/active_record/type/time_value.rb
+++ /dev/null
@@ -1,38 +0,0 @@
-module ActiveRecord
- module Type
- module TimeValue # :nodoc:
- def klass
- ::Time
- end
-
- def type_cast_for_schema(value)
- "'#{value.to_s(:db)}'"
- end
-
- private
-
- def new_time(year, mon, mday, hour, min, sec, microsec, offset = nil)
- # Treat 0000-00-00 00:00:00 as nil.
- return if year.nil? || (year == 0 && mon == 0 && mday == 0)
-
- if offset
- time = ::Time.utc(year, mon, mday, hour, min, sec, microsec) rescue nil
- return unless time
-
- time -= offset
- Base.default_timezone == :utc ? time : time.getlocal
- else
- ::Time.public_send(Base.default_timezone, year, mon, mday, hour, min, sec, microsec) rescue nil
- end
- end
-
- # Doesn't handle time zones.
- def fast_string_to_time(string)
- if string =~ ConnectionAdapters::Column::Format::ISO_DATETIME
- microsec = ($7.to_r * 1_000_000).to_i
- new_time $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, microsec
- end
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/type/type_map.rb b/activerecord/lib/active_record/type/type_map.rb
index 88c5f9c497..81d7ed39bb 100644
--- a/activerecord/lib/active_record/type/type_map.rb
+++ b/activerecord/lib/active_record/type/type_map.rb
@@ -1,24 +1,28 @@
+require 'concurrent'
+
module ActiveRecord
module Type
class TypeMap # :nodoc:
def initialize
@mapping = {}
+ @cache = Concurrent::Map.new do |h, key|
+ h.fetch_or_store(key, Concurrent::Map.new)
+ end
end
def lookup(lookup_key, *args)
- matching_pair = @mapping.reverse_each.detect do |key, _|
- key === lookup_key
- end
+ fetch(lookup_key, *args) { default_value }
+ end
- if matching_pair
- matching_pair.last.call(lookup_key, *args)
- else
- default_value
+ def fetch(lookup_key, *args, &block)
+ @cache[lookup_key].fetch_or_store(args) do
+ perform_fetch(lookup_key, *args, &block)
end
end
def register_type(key, value = nil, &block)
raise ::ArgumentError unless value || block
+ @cache.clear
if block
@mapping[key] = block
@@ -40,8 +44,20 @@ module ActiveRecord
private
+ def perform_fetch(lookup_key, *args)
+ matching_pair = @mapping.reverse_each.detect do |key, _|
+ key === lookup_key
+ end
+
+ if matching_pair
+ matching_pair.last.call(lookup_key, *args)
+ else
+ yield lookup_key, *args
+ end
+ end
+
def default_value
- @default_value ||= Value.new
+ @default_value ||= ActiveModel::Type::Value.new
end
end
end
diff --git a/activerecord/lib/active_record/type/value.rb b/activerecord/lib/active_record/type/value.rb
deleted file mode 100644
index e0a783fb45..0000000000
--- a/activerecord/lib/active_record/type/value.rb
+++ /dev/null
@@ -1,94 +0,0 @@
-module ActiveRecord
- module Type
- class Value # :nodoc:
- attr_reader :precision, :scale, :limit
-
- # Valid options are +precision+, +scale+, and +limit+. They are only
- # used when dumping schema.
- def initialize(options = {})
- options.assert_valid_keys(:precision, :scale, :limit)
- @precision = options[:precision]
- @scale = options[:scale]
- @limit = options[:limit]
- end
-
- # The simplified type that this object represents. Returns a symbol such
- # as +:string+ or +:integer+
- def type; end
-
- # Type casts a string from the database into the appropriate ruby type.
- # Classes which do not need separate type casting behavior for database
- # and user provided values should override +cast_value+ instead.
- def type_cast_from_database(value)
- type_cast(value)
- end
-
- # Type casts a value from user input (e.g. from a setter). This value may
- # be a string from the form builder, or an already type cast value
- # provided manually to a setter.
- #
- # Classes which do not need separate type casting behavior for database
- # and user provided values should override +type_cast+ or +cast_value+
- # instead.
- def type_cast_from_user(value)
- type_cast(value)
- end
-
- # Cast a value from the ruby type to a type that the database knows how
- # to understand. The returned value from this method should be a
- # +String+, +Numeric+, +Date+, +Time+, +Symbol+, +true+, +false+, or
- # +nil+
- def type_cast_for_database(value)
- value
- end
-
- # Type cast a value for schema dumping. This method is private, as we are
- # hoping to remove it entirely.
- def type_cast_for_schema(value) # :nodoc:
- value.inspect
- end
-
- # These predicates are not documented, as I need to look further into
- # their use, and see if they can be removed entirely.
- def number? # :nodoc:
- false
- end
-
- def binary? # :nodoc:
- false
- end
-
- def klass # :nodoc:
- end
-
- # Determines whether a value has changed for dirty checking. +old_value+
- # and +new_value+ will always be type-cast. Types should not need to
- # override this method.
- def changed?(old_value, new_value, _new_value_before_type_cast)
- old_value != new_value
- end
-
- # Determines whether the mutable value has been modified since it was
- # read. Returns +false+ by default. This method should not need to be
- # overriden directly. Types which return a mutable value should include
- # +Type::Mutable+, which will define this method.
- def changed_in_place?(*)
- false
- end
-
- private
-
- def type_cast(value)
- cast_value(value) unless value.nil?
- end
-
- # Convenience method for types which do not need separate type casting
- # behavior for user and database inputs. Called by
- # `type_cast_from_database` and `type_cast_from_user` for all values
- # except `nil`.
- def cast_value(value) # :doc:
- value
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/type_caster.rb b/activerecord/lib/active_record/type_caster.rb
new file mode 100644
index 0000000000..63ba10c289
--- /dev/null
+++ b/activerecord/lib/active_record/type_caster.rb
@@ -0,0 +1,7 @@
+require 'active_record/type_caster/map'
+require 'active_record/type_caster/connection'
+
+module ActiveRecord
+ module TypeCaster
+ end
+end
diff --git a/activerecord/lib/active_record/type_caster/connection.rb b/activerecord/lib/active_record/type_caster/connection.rb
new file mode 100644
index 0000000000..868d08ed44
--- /dev/null
+++ b/activerecord/lib/active_record/type_caster/connection.rb
@@ -0,0 +1,29 @@
+module ActiveRecord
+ module TypeCaster
+ class Connection
+ def initialize(klass, table_name)
+ @klass = klass
+ @table_name = table_name
+ end
+
+ def type_cast_for_database(attribute_name, value)
+ return value if value.is_a?(Arel::Nodes::BindParam)
+ column = column_for(attribute_name)
+ connection.type_cast_from_column(column, value)
+ end
+
+ protected
+
+ 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]
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type_caster/map.rb b/activerecord/lib/active_record/type_caster/map.rb
new file mode 100644
index 0000000000..4b1941351c
--- /dev/null
+++ b/activerecord/lib/active_record/type_caster/map.rb
@@ -0,0 +1,19 @@
+module ActiveRecord
+ module TypeCaster
+ class Map
+ def initialize(types)
+ @types = types
+ end
+
+ 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.serialize(value)
+ end
+
+ protected
+
+ attr_reader :types
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb
index b4b33804de..6677e6dc5f 100644
--- a/activerecord/lib/active_record/validations.rb
+++ b/activerecord/lib/active_record/validations.rb
@@ -1,95 +1,80 @@
module ActiveRecord
- # = Active Record RecordInvalid
+ # = Active Record \RecordInvalid
#
- # Raised by <tt>save!</tt> and <tt>create!</tt> when the record is invalid. Use the
- # +record+ method to retrieve the record which did not validate.
+ # Raised by {ActiveRecord::Base#save!}[rdoc-ref:Persistence#save!] and
+ # {ActiveRecord::Base#create!}[rdoc-ref:Persistence::ClassMethods#create!] when the record is invalid.
+ # Use the #record method to retrieve the record which did not validate.
#
# begin
- # complex_operation_that_calls_save!_internally
+ # complex_operation_that_internally_calls_save!
# rescue ActiveRecord::RecordInvalid => invalid
# puts invalid.record.errors
# end
class RecordInvalid < ActiveRecordError
- attr_reader :record # :nodoc:
- def initialize(record) # :nodoc:
- @record = record
- errors = @record.errors.full_messages.join(", ")
- super(I18n.t(:"#{@record.class.i18n_scope}.errors.messages.record_invalid", :errors => errors, :default => :"errors.messages.record_invalid"))
+ attr_reader :record
+
+ def initialize(record = nil)
+ if record
+ @record = record
+ errors = @record.errors.full_messages.join(", ")
+ message = I18n.t(:"#{@record.class.i18n_scope}.errors.messages.record_invalid", errors: errors, default: :"errors.messages.record_invalid")
+ else
+ message = "Record invalid"
+ end
+
+ super(message)
end
end
- # = Active Record Validations
+ # = Active Record \Validations
#
- # Active Record includes the majority of its validations from <tt>ActiveModel::Validations</tt>
+ # Active Record includes the majority of its validations from ActiveModel::Validations
# all of which accept the <tt>:on</tt> argument to define the context where the
# validations are active. Active Record will always supply either the context of
# <tt>:create</tt> or <tt>:update</tt> dependent on whether the model is a
- # <tt>new_record?</tt>.
+ # {new_record?}[rdoc-ref:Persistence#new_record?].
module Validations
extend ActiveSupport::Concern
include ActiveModel::Validations
- module ClassMethods
- # Creates an object just like Base.create but calls <tt>save!</tt> instead of +save+
- # so an exception is raised if the record is invalid.
- def create!(attributes = nil, &block)
- if attributes.is_a?(Array)
- attributes.collect { |attr| create!(attr, &block) }
- else
- object = new(attributes)
- yield(object) if block_given?
- object.save!
- object
- end
- end
- end
-
# The validation process on save can be skipped by passing <tt>validate: false</tt>.
- # The regular Base#save method is replaced with this when the validations
- # module is mixed in, which it is by default.
+ # The regular {ActiveRecord::Base#save}[rdoc-ref:Persistence#save] method is replaced
+ # with this when the validations module is mixed in, which it is by default.
def save(options={})
perform_validations(options) ? super : false
end
- # Attempts to save the record just like Base#save but will raise a +RecordInvalid+
- # exception instead of returning +false+ if the record is not valid.
+ # Attempts to save the record just like {ActiveRecord::Base#save}[rdoc-ref:Base#save] but
+ # will raise a ActiveRecord::RecordInvalid exception instead of returning +false+ if the record is not valid.
def save!(options={})
- perform_validations(options) ? super : raise_record_invalid
+ perform_validations(options) ? super : raise_validation_error
end
# Runs all the validations within the specified context. Returns +true+ if
# no errors are found, +false+ otherwise.
#
- # Aliased as validate.
+ # Aliased as #validate.
#
# If the argument is +false+ (default is +nil+), the context is set to <tt>:create</tt> if
- # <tt>new_record?</tt> is +true+, and to <tt>:update</tt> if it is not.
+ # {new_record?}[rdoc-ref:Persistence#new_record?] is +true+, and to <tt>:update</tt> if it is not.
#
- # Validations with no <tt>:on</tt> option will run no matter the context. Validations with
+ # \Validations with no <tt>:on</tt> option will run no matter the context. \Validations with
# some <tt>:on</tt> option will only run in the specified context.
def valid?(context = nil)
- context ||= (new_record? ? :create : :update)
+ context ||= default_validation_context
output = super(context)
errors.empty? && output
end
alias_method :validate, :valid?
- # Runs all the validations within the specified context. Returns +true+ if
- # no errors are found, raises +RecordInvalid+ otherwise.
- #
- # If the argument is +false+ (default is +nil+), the context is set to <tt>:create</tt> if
- # <tt>new_record?</tt> is +true+, and to <tt>:update</tt> if it is not.
- #
- # Validations with no <tt>:on</tt> option will run no matter the context. Validations with
- # some <tt>:on</tt> option will only run in the specified context.
- def validate!(context = nil)
- valid?(context) || raise_record_invalid
- end
-
protected
- def raise_record_invalid
+ def default_validation_context
+ new_record? ? :create : :update
+ end
+
+ def raise_validation_error
raise(RecordInvalid.new(self))
end
@@ -102,3 +87,5 @@ end
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/absence.rb b/activerecord/lib/active_record/validations/absence.rb
new file mode 100644
index 0000000000..2e19e6dc5c
--- /dev/null
+++ b/activerecord/lib/active_record/validations/absence.rb
@@ -0,0 +1,24 @@
+module ActiveRecord
+ module Validations
+ class AbsenceValidator < ActiveModel::Validations::AbsenceValidator # :nodoc:
+ def validate_each(record, attribute, association_or_value)
+ return unless should_validate?(record)
+ if record.class._reflect_on_association(attribute)
+ association_or_value = Array.wrap(association_or_value).reject(&:marked_for_destruction?)
+ end
+ super
+ end
+ end
+
+ module ClassMethods
+ # Validates that the specified attributes are not present (as defined by
+ # Object#present?). If the attribute is an association, the associated object
+ # is considered absent if it was marked for destruction.
+ #
+ # See ActiveModel::Validations::HelperMethods.validates_absence_of for more information.
+ def validates_absence_of(*attr_names)
+ validates_with AbsenceValidator, _merge_attributes(attr_names)
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/validations/associated.rb b/activerecord/lib/active_record/validations/associated.rb
index b4785d3ba4..32fbaf0a91 100644
--- a/activerecord/lib/active_record/validations/associated.rb
+++ b/activerecord/lib/active_record/validations/associated.rb
@@ -24,14 +24,17 @@ module ActiveRecord
#
# NOTE: This validation will not fail if the association hasn't been
# assigned. If you want to ensure that the association is both present and
- # guaranteed to be valid, you also need to use +validates_presence_of+.
+ # guaranteed to be valid, you also need to use
+ # {validates_presence_of}[rdoc-ref:Validations::ClassMethods#validates_presence_of].
#
# Configuration options:
#
# * <tt>:message</tt> - A custom error message (default is: "is invalid").
- # * <tt>:on</tt> - Specifies when this validation is active. Runs in all
- # validation contexts by default (+nil+), other options are <tt>:create</tt>
- # and <tt>:update</tt>.
+ # * <tt>:on</tt> - Specifies the contexts where this validation is active.
+ # Runs in all validation contexts by default (nil). You can pass a symbol
+ # or an array of symbols. (e.g. <tt>on: :create</tt> or
+ # <tt>on: :custom_validation_context</tt> or
+ # <tt>on: [:create, :custom_validation_context]</tt>)
# * <tt>:if</tt> - Specifies a method, proc or string to call to determine
# if the validation should occur (e.g. <tt>if: :allow_validation</tt>,
# or <tt>if: Proc.new { |user| user.signup_step > 2 }</tt>). The method,
diff --git a/activerecord/lib/active_record/validations/length.rb b/activerecord/lib/active_record/validations/length.rb
new file mode 100644
index 0000000000..69e048eef1
--- /dev/null
+++ b/activerecord/lib/active_record/validations/length.rb
@@ -0,0 +1,36 @@
+module ActiveRecord
+ module Validations
+ class LengthValidator < ActiveModel::Validations::LengthValidator # :nodoc:
+ def validate_each(record, attribute, association_or_value)
+ return unless should_validate?(record) || associations_are_dirty?(record)
+ if association_or_value.respond_to?(:loaded?) && association_or_value.loaded?
+ association_or_value = association_or_value.target.reject(&:marked_for_destruction?)
+ end
+ super
+ end
+
+ def associations_are_dirty?(record)
+ attributes.any? do |attribute|
+ value = record.read_attribute_for_validation(attribute)
+ if value.respond_to?(:loaded?) && value.loaded?
+ value.target.any?(&:marked_for_destruction?)
+ else
+ false
+ end
+ end
+ end
+ end
+
+ module ClassMethods
+ # Validates that the specified attributes match the length restrictions supplied.
+ # If the attribute is an association, records that are marked for destruction are not counted.
+ #
+ # See ActiveModel::Validations::HelperMethods.validates_length_of for more information.
+ def validates_length_of(*attr_names)
+ validates_with LengthValidator, _merge_attributes(attr_names)
+ end
+
+ alias_method :validates_size_of, :validates_length_of
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/validations/presence.rb b/activerecord/lib/active_record/validations/presence.rb
index e586744818..7e85ed43ac 100644
--- a/activerecord/lib/active_record/validations/presence.rb
+++ b/activerecord/lib/active_record/validations/presence.rb
@@ -1,17 +1,12 @@
module ActiveRecord
module Validations
class PresenceValidator < ActiveModel::Validations::PresenceValidator # :nodoc:
- def validate(record)
- super
- attributes.each do |attribute|
- next unless record.class._reflect_on_association(attribute)
- associated_records = Array.wrap(record.send(attribute))
-
- # Superclass validates presence. Ensure present records aren't about to be destroyed.
- if associated_records.present? && associated_records.all? { |r| r.marked_for_destruction? }
- record.errors.add(attribute, :blank, options)
- end
+ def validate_each(record, attribute, association_or_value)
+ return unless should_validate?(record)
+ if record.class._reflect_on_association(attribute)
+ association_or_value = Array.wrap(association_or_value).reject(&:marked_for_destruction?)
end
+ super
end
end
@@ -36,17 +31,24 @@ module ActiveRecord
# This is due to the way Object#blank? handles boolean values:
# <tt>false.blank? # => true</tt>.
#
- # This validator defers to the ActiveModel validation for presence, adding the
+ # This validator defers to the Active Model validation for presence, adding the
# check to see that an associated object is not marked for destruction. This
# prevents the parent object from validating successfully and saving, which then
# deletes the associated object, thus putting the parent object into an invalid
# state.
#
+ # NOTE: This validation will not fail while using it with an association
+ # if the latter was assigned but not valid. If you want to ensure that
+ # it is both present and valid, you also need to use
+ # {validates_associated}[rdoc-ref:Validations::ClassMethods#validates_associated].
+ #
# Configuration options:
# * <tt>:message</tt> - A custom error message (default is: "can't be blank").
- # * <tt>:on</tt> - Specifies when this validation is active. Runs in all
- # validation contexts by default (+nil+), other options are <tt>:create</tt>
- # and <tt>:update</tt>.
+ # * <tt>:on</tt> - Specifies the contexts where this validation is active.
+ # Runs in all validation contexts by default (nil). You can pass a symbol
+ # or an array of symbols. (e.g. <tt>on: :create</tt> or
+ # <tt>on: :custom_validation_context</tt> or
+ # <tt>on: [:create, :custom_validation_context]</tt>)
# * <tt>:if</tt> - Specifies a method, proc or string to call to determine if
# the validation should occur (e.g. <tt>if: :allow_validation</tt>, or
# <tt>if: Proc.new { |user| user.signup_step > 2 }</tt>). The method, proc
@@ -56,7 +58,7 @@ module ActiveRecord
# or <tt>unless: Proc.new { |user| user.signup_step <= 2 }</tt>). The method,
# proc or string should return or evaluate to a +true+ or +false+ value.
# * <tt>:strict</tt> - Specifies whether validation should be strict.
- # See <tt>ActiveModel::Validation#validates!</tt> for more information.
+ # See ActiveModel::Validation#validates! for more information.
def validates_presence_of(*attr_names)
validates_with PresenceValidator, _merge_attributes(attr_names)
end
diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb
index 2dba4c7b94..aa2794f120 100644
--- a/activerecord/lib/active_record/validations/uniqueness.rb
+++ b/activerecord/lib/active_record/validations/uniqueness.rb
@@ -11,14 +11,20 @@ module ActiveRecord
end
def validate_each(record, attribute, value)
+ return unless should_validate?(record)
finder_class = find_finder_class_for(record)
table = finder_class.arel_table
value = map_enum_attribute(finder_class, attribute, value)
relation = build_relation(finder_class, table, attribute, value)
- relation = relation.and(table[finder_class.primary_key.to_sym].not_eq(record.id)) if record.persisted?
+ if record.persisted? && finder_class.primary_key.to_s != attribute.to_s
+ if finder_class.primary_key
+ relation = relation.where.not(finder_class.primary_key => record.id)
+ else
+ raise UnknownPrimaryKey.new(finder_class, "Can not validate uniqueness for persisted record without primary key.")
+ end
+ end
relation = scope_relation(record, table, relation)
- relation = finder_class.unscoped.where(relation)
relation = relation.merge(options[:conditions]) if options[:conditions]
if relation.exists?
@@ -60,17 +66,24 @@ module ActiveRecord
end
column = klass.columns_hash[attribute_name]
- value = klass.connection.type_cast(value, column)
+ cast_type = klass.type_for_attribute(attribute_name)
+ value = cast_type.serialize(value)
+ value = klass.connection.type_cast(value)
if value.is_a?(String) && column.limit
value = value.to_s[0, column.limit]
end
- if !options[:case_sensitive] && value.is_a?(String)
+ value = Arel::Nodes::Quoted.new(value)
+
+ comparison = if !options[:case_sensitive] && !value.nil?
# will use SQL LOWER function before comparison, unless it detects a case insensitive collation
klass.connection.case_insensitive_comparison(table, attribute, column, value)
else
klass.connection.case_sensitive_comparison(table, attribute, column, value)
end
+ klass.unscoped.where(comparison)
+ rescue RangeError
+ klass.none
end
def scope_relation(record, table, relation)
@@ -79,9 +92,9 @@ module ActiveRecord
scope_value = record.send(reflection.foreign_key)
scope_item = reflection.foreign_key
else
- scope_value = record.read_attribute(scope_item)
+ scope_value = record._read_attribute(scope_item)
end
- relation = relation.and(table[scope_item].eq(scope_value))
+ relation = relation.where(scope_item => scope_value)
end
relation
@@ -159,7 +172,8 @@ module ActiveRecord
#
# === Concurrency and integrity
#
- # Using this validation method in conjunction with ActiveRecord::Base#save
+ # Using this validation method in conjunction with
+ # {ActiveRecord::Base#save}[rdoc-ref:Persistence#save]
# does not guarantee the absence of duplicate record insertions, because
# uniqueness checks on the application level are inherently prone to race
# conditions. For example, suppose that two users try to post a Comment at
@@ -196,12 +210,12 @@ module ActiveRecord
# 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
- # ActiveRecord::ConnectionAdapters::SchemaStatements#add_index. In the
- # rare case that a race condition occurs, the database will guarantee
+ # {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.
#
# When the database catches such a duplicate insertion,
- # ActiveRecord::Base#save will raise an ActiveRecord::StatementInvalid
+ # {ActiveRecord::Base#save}[rdoc-ref:Persistence#save] will raise an ActiveRecord::StatementInvalid
# exception. You can either choose to let this error propagate (which
# will result in the default Rails exception page being shown), or you
# can catch it and restart the transaction (e.g. by telling the user
diff --git a/activerecord/lib/rails/generators/active_record/migration.rb b/activerecord/lib/rails/generators/active_record/migration.rb
index b7418cf42f..c2b2209638 100644
--- a/activerecord/lib/rails/generators/active_record/migration.rb
+++ b/activerecord/lib/rails/generators/active_record/migration.rb
@@ -13,6 +13,13 @@ module ActiveRecord
ActiveRecord::Migration.next_migration_number(next_migration_number)
end
end
+
+ private
+
+ def primary_key_type
+ key_type = options[:primary_key_type]
+ ", id: :#{key_type}" if key_type
+ end
end
end
end
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 d3c853cfea..4e5872b585 100644
--- a/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb
+++ b/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb
@@ -5,6 +5,8 @@ module ActiveRecord
class MigrationGenerator < Base # :nodoc:
argument :attributes, :type => :array, :default => [], :banner => "field[:type][:index] field[:type][:index]"
+ class_option :primary_key_type, type: :string, desc: "The type for primary key"
+
def create_migration_file
set_local_assigns!
validate_file_name!
@@ -14,10 +16,9 @@ module ActiveRecord
protected
attr_reader :migration_action, :join_tables
- # sets the default migration template that is being used for the generation of the migration
- # depending on the arguments which would be sent out in the command line, the migration template
- # and the table name instance variables are setup.
-
+ # 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
+ # variables are set up.
def set_local_assigns!
@migration_template = "migration.rb"
case file_name
@@ -55,7 +56,9 @@ module ActiveRecord
def attributes_with_index
attributes.select { |a| !a.reference? && a.has_index? }
end
-
+
+ # A migration file name can only contain underscores (_), lowercase characters,
+ # and numbers 0-9. Any other file name will raise an IllegalMigrationNameError.
def validate_file_name!
unless file_name =~ /^[_a-z0-9]+$/
raise IllegalMigrationNameError.new(file_name)
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
index fd94a2d038..fadab2a1e6 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
@@ -1,9 +1,11 @@
class <%= migration_class_name %> < ActiveRecord::Migration
def change
- create_table :<%= table_name %> do |t|
+ create_table :<%= table_name %><%= primary_key_type %> do |t|
<% attributes.each do |attribute| -%>
<% if attribute.password_digest? -%>
t.string :password_digest<%= attribute.inject_options %>
+<% elsif attribute.token? -%>
+ t.string :<%= attribute.name %><%= attribute.inject_options %>
<% else -%>
t.<%= attribute.type %> :<%= attribute.name %><%= attribute.inject_options %>
<% end -%>
@@ -12,6 +14,9 @@ class <%= migration_class_name %> < ActiveRecord::Migration
t.timestamps
<% end -%>
end
+<% attributes.select(&:token?).each do |attribute| -%>
+ add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %>, unique: true
+<% end -%>
<% attributes_with_index.each do |attribute| -%>
add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %>
<% end -%>
diff --git a/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb b/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb
index ae9c74fd05..23a377db6a 100644
--- a/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb
+++ b/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb
@@ -4,6 +4,9 @@ class <%= migration_class_name %> < ActiveRecord::Migration
<% attributes.each do |attribute| -%>
<%- if attribute.reference? -%>
add_reference :<%= table_name %>, :<%= attribute.name %><%= attribute.inject_options %>
+ <%- elsif attribute.token? -%>
+ add_column :<%= table_name %>, :<%= attribute.name %>, :string<%= attribute.inject_options %>
+ add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %>, unique: true
<%- else -%>
add_column :<%= table_name %>, :<%= attribute.name %>, :<%= attribute.type %><%= attribute.inject_options %>
<%- if attribute.has_index? -%>
diff --git a/activerecord/lib/rails/generators/active_record/model/model_generator.rb b/activerecord/lib/rails/generators/active_record/model/model_generator.rb
index 7e8d68ce69..395951ac9d 100644
--- a/activerecord/lib/rails/generators/active_record/model/model_generator.rb
+++ b/activerecord/lib/rails/generators/active_record/model/model_generator.rb
@@ -7,14 +7,13 @@ module ActiveRecord
check_class_collision
- class_option :migration, :type => :boolean
- class_option :timestamps, :type => :boolean
- class_option :parent, :type => :string, :desc => "The parent class for the generated model"
- class_option :indexes, :type => :boolean, :default => true, :desc => "Add indexes for references and belongs_to columns"
+ class_option :migration, type: :boolean
+ class_option :timestamps, type: :boolean
+ class_option :parent, type: :string, desc: "The parent class for the generated model"
+ class_option :indexes, type: :boolean, default: true, desc: "Add indexes for references and belongs_to columns"
+ class_option :primary_key_type, type: :string, desc: "The type for primary key"
-
# creates the migration file for the model.
-
def create_migration_file
return unless options[:migration] && options[:parent].nil?
attributes.each { |a| a.attr_options.delete(:index) if a.reference? && !a.has_index? } if options[:indexes] == false
diff --git a/activerecord/lib/rails/generators/active_record/model/templates/model.rb b/activerecord/lib/rails/generators/active_record/model/templates/model.rb
index 808598699b..55dc65c8ad 100644
--- a/activerecord/lib/rails/generators/active_record/model/templates/model.rb
+++ b/activerecord/lib/rails/generators/active_record/model/templates/model.rb
@@ -1,7 +1,10 @@
<% module_namespacing do -%>
class <%= class_name %> < <%= parent_class_name.classify %>
<% attributes.select(&:reference?).each do |attribute| -%>
- belongs_to :<%= attribute.name %><%= ', polymorphic: true' if attribute.polymorphic? %>
+ belongs_to :<%= attribute.name %><%= ', polymorphic: true' if attribute.polymorphic? %><%= ', required: true' if attribute.required? %>
+<% end -%>
+<% attributes.select(&:token?).each do |attribute| -%>
+ has_secure_token<% if attribute.name != "token" %> :<%= attribute.name %><% end %>
<% end -%>
<% if attributes.any?(&:password_digest?) -%>
has_secure_password
diff --git a/activerecord/test/active_record/connection_adapters/fake_adapter.rb b/activerecord/test/active_record/connection_adapters/fake_adapter.rb
index 64cde143a1..43c817e057 100644
--- a/activerecord/test/active_record/connection_adapters/fake_adapter.rb
+++ b/activerecord/test/active_record/connection_adapters/fake_adapter.rb
@@ -7,7 +7,7 @@ module ActiveRecord
module ConnectionAdapters
class FakeAdapter < AbstractAdapter
- attr_accessor :tables, :primary_keys
+ attr_accessor :data_sources, :primary_keys
@columns = Hash.new { |h,k| h[k] = [] }
class << self
@@ -16,21 +16,20 @@ module ActiveRecord
def initialize(connection, logger)
super
- @tables = []
+ @data_sources = []
@primary_keys = {}
@columns = self.class.columns
end
def primary_key(table)
- @primary_keys[table]
+ @primary_keys[table] || "id"
end
def merge_column(table_name, name, sql_type = nil, options = {})
@columns[table_name] << ActiveRecord::ConnectionAdapters::Column.new(
name.to_s,
options[:default],
- lookup_cast_type(sql_type.to_s),
- sql_type.to_s,
+ fetch_type_metadata(sql_type),
options[:null])
end
@@ -38,6 +37,10 @@ module ActiveRecord
@columns[table_name]
end
+ def data_source_exists?(*)
+ true
+ end
+
def active?
true
end
diff --git a/activerecord/test/cases/adapter_test.rb b/activerecord/test/cases/adapter_test.rb
index 6f84bae432..62579a4a7a 100644
--- a/activerecord/test/cases/adapter_test.rb
+++ b/activerecord/test/cases/adapter_test.rb
@@ -36,6 +36,21 @@ module ActiveRecord
assert !@connection.table_exists?(nil)
end
+ def test_data_sources
+ data_sources = @connection.data_sources
+ assert data_sources.include?("accounts")
+ assert data_sources.include?("authors")
+ assert data_sources.include?("tasks")
+ assert data_sources.include?("topics")
+ end
+
+ def test_data_source_exists?
+ assert @connection.data_source_exists?("accounts")
+ assert @connection.data_source_exists?(:accounts)
+ assert_not @connection.data_source_exists?("nonexistingtable")
+ assert_not @connection.data_source_exists?(nil)
+ end
+
def test_indexes
idx_name = "accounts_idx"
@@ -63,7 +78,7 @@ module ActiveRecord
end
end
- if current_adapter?(:MysqlAdapter)
+ if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
def test_charset
assert_not_nil @connection.charset
assert_not_equal 'character_set_database', @connection.charset
@@ -193,7 +208,7 @@ module ActiveRecord
author = Author.create!(name: 'john')
Post.create!(author: author, title: 'foo', body: 'bar')
query = author.posts.where(title: 'foo').select(:title)
- assert_equal({"title" => "foo"}, @connection.select_one(query.arel, nil, query.bind_values))
+ assert_equal({"title" => "foo"}, @connection.select_one(query.arel, nil, query.bound_attributes))
assert_equal({"title" => "foo"}, @connection.select_one(query))
assert @connection.select_all(query).is_a?(ActiveRecord::Result)
assert_equal "foo", @connection.select_value(query)
@@ -203,7 +218,7 @@ module ActiveRecord
def test_select_methods_passing_a_relation
Post.create!(title: 'foo', body: 'bar')
query = Post.where(title: 'foo').select(:title)
- assert_equal({"title" => "foo"}, @connection.select_one(query.arel, nil, query.bind_values))
+ assert_equal({"title" => "foo"}, @connection.select_one(query.arel, nil, query.bound_attributes))
assert_equal({"title" => "foo"}, @connection.select_one(query))
assert @connection.select_all(query).is_a?(ActiveRecord::Result)
assert_equal "foo", @connection.select_value(query)
@@ -213,10 +228,20 @@ module ActiveRecord
test "type_to_sql returns a String for unmapped types" do
assert_equal "special_db_type", @connection.type_to_sql(:special_db_type)
end
+
+ unless current_adapter?(:PostgreSQLAdapter)
+ def test_log_invalid_encoding
+ assert_raise ActiveRecord::StatementInvalid do
+ @connection.send :log, "SELECT 'ы' FROM DUAL" do
+ raise 'ы'.force_encoding(Encoding::ASCII_8BIT)
+ end
+ end
+ end
+ end
end
class AdapterTestWithoutTransaction < ActiveRecord::TestCase
- self.use_transactional_fixtures = false
+ self.use_transactional_tests = false
class Klass < ActiveRecord::Base
end
diff --git a/activerecord/test/cases/adapters/mysql/active_schema_test.rb b/activerecord/test/cases/adapters/mysql/active_schema_test.rb
index 7c0f11b033..0b5c9e1798 100644
--- a/activerecord/test/cases/adapters/mysql/active_schema_test.rb
+++ b/activerecord/test/cases/adapters/mysql/active_schema_test.rb
@@ -1,7 +1,7 @@
require "cases/helper"
require 'support/connection_helper'
-class ActiveSchemaTest < ActiveRecord::TestCase
+class MysqlActiveSchemaTest < ActiveRecord::MysqlTestCase
include ConnectionHelper
def setup
@@ -59,21 +59,56 @@ class ActiveSchemaTest < ActiveRecord::TestCase
assert_equal expected, add_index(:people, [:last_name, :first_name], :length => 15, :using => :btree)
end
- def test_drop_table
- assert_equal "DROP TABLE `people`", drop_table(:people)
+ def test_index_in_create
+ def (ActiveRecord::Base.connection).table_exists?(*); false; end
+
+ %w(SPATIAL FULLTEXT UNIQUE).each do |type|
+ expected = "CREATE TABLE `people` (#{type} INDEX `index_people_on_last_name` (`last_name`) ) ENGINE=InnoDB"
+ actual = ActiveRecord::Base.connection.create_table(:people, id: false) do |t|
+ t.index :last_name, type: type
+ end
+ assert_equal expected, actual
+ end
+
+ expected = "CREATE TABLE `people` ( INDEX `index_people_on_last_name` USING btree (`last_name`(10)) ) ENGINE=InnoDB"
+ actual = ActiveRecord::Base.connection.create_table(:people, id: false) do |t|
+ t.index :last_name, length: 10, using: :btree
+ end
+ assert_equal expected, actual
end
- if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
- def test_create_mysql_database_with_encoding
- assert_equal "CREATE DATABASE `matt` DEFAULT CHARACTER SET `utf8`", create_database(:matt)
- assert_equal "CREATE DATABASE `aimonetti` DEFAULT CHARACTER SET `latin1`", create_database(:aimonetti, {:charset => 'latin1'})
- assert_equal "CREATE DATABASE `matt_aimonetti` DEFAULT CHARACTER SET `big5` COLLATE `big5_chinese_ci`", create_database(:matt_aimonetti, {:charset => :big5, :collation => :big5_chinese_ci})
+ def test_index_in_bulk_change
+ def (ActiveRecord::Base.connection).table_exists?(*); true; end
+ def (ActiveRecord::Base.connection).index_name_exists?(*); false; end
+
+ %w(SPATIAL FULLTEXT UNIQUE).each do |type|
+ expected = "ALTER TABLE `people` ADD #{type} INDEX `index_people_on_last_name` (`last_name`)"
+ actual = ActiveRecord::Base.connection.change_table(:people, bulk: true) do |t|
+ t.index :last_name, type: type
+ end
+ assert_equal expected, actual
end
- def test_recreate_mysql_database_with_encoding
- create_database(:luca, {:charset => 'latin1'})
- assert_equal "CREATE DATABASE `luca` DEFAULT CHARACTER SET `latin1`", recreate_database(:luca, {:charset => 'latin1'})
+ expected = "ALTER TABLE `peaple` ADD INDEX `index_peaple_on_last_name` USING btree (`last_name`(10)), ALGORITHM = COPY"
+ actual = ActiveRecord::Base.connection.change_table(:peaple, bulk: true) do |t|
+ t.index :last_name, length: 10, using: :btree, algorithm: :copy
end
+ assert_equal expected, actual
+ end
+
+ def test_drop_table
+ assert_equal "DROP TABLE `people`", drop_table(:people)
+ end
+
+ def test_create_mysql_database_with_encoding
+ assert_equal "CREATE DATABASE `matt` DEFAULT CHARACTER SET `utf8`", create_database(:matt)
+ assert_equal "CREATE DATABASE `aimonetti` DEFAULT CHARACTER SET `latin1`", create_database(:aimonetti, {:charset => 'latin1'})
+ assert_equal "CREATE DATABASE `matt_aimonetti` DEFAULT CHARACTER SET `big5` COLLATE `big5_chinese_ci`", create_database(:matt_aimonetti, {:charset => :big5, :collation => :big5_chinese_ci})
+ end
+
+ def test_recreate_mysql_database_with_encoding
+ create_database(:luca, {:charset => 'latin1'})
+ assert_equal "CREATE DATABASE `luca` DEFAULT CHARACTER SET `latin1`", recreate_database(:luca, {:charset => 'latin1'})
end
def test_add_column
@@ -92,7 +127,7 @@ class ActiveSchemaTest < ActiveRecord::TestCase
with_real_execute do
begin
ActiveRecord::Base.connection.create_table :delete_me
- ActiveRecord::Base.connection.add_timestamps :delete_me
+ ActiveRecord::Base.connection.add_timestamps :delete_me, null: true
assert column_present?('delete_me', 'updated_at', 'datetime')
assert column_present?('delete_me', 'created_at', 'datetime')
ensure
@@ -105,9 +140,9 @@ class ActiveSchemaTest < ActiveRecord::TestCase
with_real_execute do
begin
ActiveRecord::Base.connection.create_table :delete_me do |t|
- t.timestamps
+ t.timestamps null: true
end
- ActiveRecord::Base.connection.remove_timestamps :delete_me
+ ActiveRecord::Base.connection.remove_timestamps :delete_me, { null: true }
assert !column_present?('delete_me', 'updated_at', 'datetime')
assert !column_present?('delete_me', 'created_at', 'datetime')
ensure
diff --git a/activerecord/test/cases/adapters/mysql/case_sensitivity_test.rb b/activerecord/test/cases/adapters/mysql/case_sensitivity_test.rb
index 340fc95503..98d44315dd 100644
--- a/activerecord/test/cases/adapters/mysql/case_sensitivity_test.rb
+++ b/activerecord/test/cases/adapters/mysql/case_sensitivity_test.rb
@@ -1,7 +1,6 @@
require "cases/helper"
-require 'models/person'
-class MysqlCaseSensitivityTest < ActiveRecord::TestCase
+class MysqlCaseSensitivityTest < ActiveRecord::MysqlTestCase
class CollationTest < ActiveRecord::Base
end
diff --git a/activerecord/test/cases/adapters/mysql/charset_collation_test.rb b/activerecord/test/cases/adapters/mysql/charset_collation_test.rb
new file mode 100644
index 0000000000..f2117a97e6
--- /dev/null
+++ b/activerecord/test/cases/adapters/mysql/charset_collation_test.rb
@@ -0,0 +1,54 @@
+require "cases/helper"
+require 'support/schema_dumping_helper'
+
+class MysqlCharsetCollationTest < ActiveRecord::MysqlTestCase
+ include SchemaDumpingHelper
+ self.use_transactional_tests = false
+
+ setup do
+ @connection = ActiveRecord::Base.connection
+ @connection.create_table :charset_collations, force: true do |t|
+ t.string :string_ascii_bin, charset: 'ascii', collation: 'ascii_bin'
+ t.text :text_ucs2_unicode_ci, charset: 'ucs2', collation: 'ucs2_unicode_ci'
+ end
+ end
+
+ teardown do
+ @connection.drop_table :charset_collations, if_exists: true
+ end
+
+ test "string column with charset and collation" do
+ column = @connection.columns(:charset_collations).find { |c| c.name == 'string_ascii_bin' }
+ assert_equal :string, column.type
+ assert_equal 'ascii_bin', column.collation
+ end
+
+ test "text column with charset and collation" do
+ column = @connection.columns(:charset_collations).find { |c| c.name == 'text_ucs2_unicode_ci' }
+ assert_equal :text, column.type
+ assert_equal 'ucs2_unicode_ci', column.collation
+ end
+
+ test "add column with charset and collation" do
+ @connection.add_column :charset_collations, :title, :string, charset: 'utf8', collation: 'utf8_bin'
+
+ column = @connection.columns(:charset_collations).find { |c| c.name == 'title' }
+ assert_equal :string, column.type
+ assert_equal 'utf8_bin', column.collation
+ end
+
+ test "change column with charset and collation" do
+ @connection.add_column :charset_collations, :description, :string, charset: 'utf8', collation: 'utf8_unicode_ci'
+ @connection.change_column :charset_collations, :description, :text, charset: 'utf8', collation: 'utf8_general_ci'
+
+ column = @connection.columns(:charset_collations).find { |c| c.name == 'description' }
+ assert_equal :text, column.type
+ assert_equal 'utf8_general_ci', column.collation
+ end
+
+ test "schema dump includes collation" do
+ output = dump_table_schema("charset_collations")
+ assert_match %r{t.string\s+"string_ascii_bin",\s+limit: 255,\s+collation: "ascii_bin"$}, output
+ assert_match %r{t.text\s+"text_ucs2_unicode_ci",\s+limit: 65535,\s+collation: "ucs2_unicode_ci"$}, output
+ end
+end
diff --git a/activerecord/test/cases/adapters/mysql/connection_test.rb b/activerecord/test/cases/adapters/mysql/connection_test.rb
index b0759dffde..decac9e83b 100644
--- a/activerecord/test/cases/adapters/mysql/connection_test.rb
+++ b/activerecord/test/cases/adapters/mysql/connection_test.rb
@@ -2,7 +2,7 @@ require "cases/helper"
require 'support/connection_helper'
require 'support/ddl_helper'
-class MysqlConnectionTest < ActiveRecord::TestCase
+class MysqlConnectionTest < ActiveRecord::MysqlTestCase
include ConnectionHelper
include DdlHelper
@@ -26,7 +26,7 @@ class MysqlConnectionTest < ActiveRecord::TestCase
run_without_connection do
ar_config = ARTest.connection_config['arunit']
- url = "mysql://#{ar_config["username"]}@localhost/#{ar_config["database"]}"
+ url = "mysql://#{ar_config["username"]}:#{ar_config["password"]}@localhost/#{ar_config["database"]}"
Klass.establish_connection(url)
assert_equal ar_config['database'], Klass.connection.current_database
end
@@ -47,9 +47,7 @@ class MysqlConnectionTest < ActiveRecord::TestCase
assert !@connection.active?
# Repair all fixture connections so other tests won't break.
- @fixture_connections.each do |c|
- c.verify!
- end
+ @fixture_connections.each(&:verify!)
end
def test_successful_reconnection_after_timeout_with_manual_reconnect
@@ -69,8 +67,8 @@ class MysqlConnectionTest < ActiveRecord::TestCase
end
def test_bind_value_substitute
- bind_param = @connection.substitute_at('foo', 0)
- assert_equal Arel.sql('?'), bind_param
+ bind_param = @connection.substitute_at('foo')
+ assert_equal Arel.sql('?'), bind_param.to_sql
end
def test_exec_no_binds
@@ -96,7 +94,7 @@ class MysqlConnectionTest < ActiveRecord::TestCase
with_example_table do
@connection.exec_query('INSERT INTO ex (id, data) VALUES (1, "foo")')
result = @connection.exec_query(
- 'SELECT id, data FROM ex WHERE id = ?', nil, [[nil, 1]])
+ 'SELECT id, data FROM ex WHERE id = ?', nil, [ActiveRecord::Relation::QueryAttribute.new("id", 1, ActiveRecord::Type::Value.new)])
assert_equal 1, result.rows.length
assert_equal 2, result.columns.length
@@ -108,10 +106,10 @@ class MysqlConnectionTest < ActiveRecord::TestCase
def test_exec_typecasts_bind_vals
with_example_table do
@connection.exec_query('INSERT INTO ex (id, data) VALUES (1, "foo")')
- column = @connection.columns('ex').find { |col| col.name == 'id' }
+ bind = ActiveRecord::Relation::QueryAttribute.new("id", "1-fuu", ActiveRecord::Type::Integer.new)
result = @connection.exec_query(
- 'SELECT id, data FROM ex WHERE id = ?', nil, [[column, '1-fuu']])
+ 'SELECT id, data FROM ex WHERE id = ?', nil, [bind])
assert_equal 1, result.rows.length
assert_equal 2, result.columns.length
@@ -120,13 +118,9 @@ class MysqlConnectionTest < ActiveRecord::TestCase
end
end
- # Test that MySQL allows multiple results for stored procedures
- if defined?(Mysql) && Mysql.const_defined?(:CLIENT_MULTI_RESULTS)
- def test_multi_results
- rows = ActiveRecord::Base.connection.select_rows('CALL ten();')
- assert_equal 10, rows[0][0].to_i, "ten() did not return 10 as expected: #{rows.inspect}"
- assert @connection.active?, "Bad connection use by 'MysqlAdapter.select_rows'"
- end
+ def test_mysql_connection_collation_is_configured
+ assert_equal 'utf8_unicode_ci', @connection.show_variable('collation_connection')
+ assert_equal 'utf8_general_ci', ARUnit2Model.connection.show_variable('collation_connection')
end
def test_mysql_default_in_strict_mode
@@ -142,6 +136,15 @@ class MysqlConnectionTest < ActiveRecord::TestCase
end
end
+ def test_mysql_strict_mode_specified_default
+ run_without_connection do |orig_connection|
+ ActiveRecord::Base.establish_connection(orig_connection.merge({strict: :default}))
+ global_sql_mode = ActiveRecord::Base.connection.exec_query "SELECT @@GLOBAL.sql_mode"
+ session_sql_mode = ActiveRecord::Base.connection.exec_query "SELECT @@SESSION.sql_mode"
+ assert_equal global_sql_mode.rows, session_sql_mode.rows
+ end
+ end
+
def test_mysql_set_session_variable
run_without_connection do |orig_connection|
ActiveRecord::Base.establish_connection(orig_connection.deep_merge({:variables => {:default_week_format => 3}}))
@@ -150,7 +153,7 @@ class MysqlConnectionTest < ActiveRecord::TestCase
end
end
- def test_mysql_sql_mode_variable_overides_strict_mode
+ def test_mysql_sql_mode_variable_overrides_strict_mode
run_without_connection do |orig_connection|
ActiveRecord::Base.establish_connection(orig_connection.deep_merge(variables: { 'sql_mode' => 'ansi' }))
result = ActiveRecord::Base.connection.exec_query 'SELECT @@SESSION.sql_mode'
@@ -171,7 +174,7 @@ class MysqlConnectionTest < ActiveRecord::TestCase
def with_example_table(&block)
definition ||= <<-SQL
- `id` int(11) auto_increment PRIMARY KEY,
+ `id` int auto_increment PRIMARY KEY,
`data` varchar(255)
SQL
super(@connection, 'ex', definition, &block)
diff --git a/activerecord/test/cases/adapters/mysql/consistency_test.rb b/activerecord/test/cases/adapters/mysql/consistency_test.rb
index 083d533bb2..743f6436e4 100644
--- a/activerecord/test/cases/adapters/mysql/consistency_test.rb
+++ b/activerecord/test/cases/adapters/mysql/consistency_test.rb
@@ -1,7 +1,7 @@
require "cases/helper"
-class MysqlConsistencyTest < ActiveRecord::TestCase
- self.use_transactional_fixtures = false
+class MysqlConsistencyTest < ActiveRecord::MysqlTestCase
+ self.use_transactional_tests = false
class Consistency < ActiveRecord::Base
self.table_name = "mysql_consistency"
@@ -12,6 +12,7 @@ class MysqlConsistencyTest < ActiveRecord::TestCase
ActiveRecord::ConnectionAdapters::MysqlAdapter.emulate_booleans = false
@connection = ActiveRecord::Base.connection
+ @connection.clear_cache!
@connection.create_table("mysql_consistency") do |t|
t.boolean "a_bool"
t.string "a_string"
diff --git a/activerecord/test/cases/adapters/mysql/enum_test.rb b/activerecord/test/cases/adapters/mysql/enum_test.rb
index f4e7a3ef0a..ef8ee0a6e3 100644
--- a/activerecord/test/cases/adapters/mysql/enum_test.rb
+++ b/activerecord/test/cases/adapters/mysql/enum_test.rb
@@ -1,6 +1,6 @@
require "cases/helper"
-class MysqlEnumTest < ActiveRecord::TestCase
+class MysqlEnumTest < ActiveRecord::MysqlTestCase
class EnumTest < ActiveRecord::Base
end
diff --git a/activerecord/test/cases/adapters/mysql/explain_test.rb b/activerecord/test/cases/adapters/mysql/explain_test.rb
new file mode 100644
index 0000000000..c44c1e6648
--- /dev/null
+++ b/activerecord/test/cases/adapters/mysql/explain_test.rb
@@ -0,0 +1,21 @@
+require "cases/helper"
+require 'models/developer'
+require 'models/computer'
+
+class MysqlExplainTest < ActiveRecord::MysqlTestCase
+ fixtures :developers
+
+ def test_explain_for_one_query
+ explain = Developer.where(id: 1).explain
+ assert_match %(EXPLAIN for: SELECT `developers`.* FROM `developers` WHERE `developers`.`id` = 1), explain
+ assert_match %r(developers |.* const), explain
+ end
+
+ def test_explain_with_eager_loading
+ explain = Developer.where(id: 1).includes(:audit_logs).explain
+ assert_match %(EXPLAIN for: SELECT `developers`.* FROM `developers` WHERE `developers`.`id` = 1), explain
+ assert_match %r(developers |.* const), explain
+ assert_match %(EXPLAIN for: SELECT `audit_logs`.* FROM `audit_logs` WHERE `audit_logs`.`developer_id` = 1), explain
+ assert_match %r(audit_logs |.* ALL), explain
+ end
+end
diff --git a/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb b/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb
index 28106d3772..d2ce48fc00 100644
--- a/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb
+++ b/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb
@@ -1,11 +1,9 @@
-# encoding: utf-8
-
require "cases/helper"
require 'support/ddl_helper'
module ActiveRecord
module ConnectionAdapters
- class MysqlAdapterTest < ActiveRecord::TestCase
+ class MysqlAdapterTest < ActiveRecord::MysqlTestCase
include DdlHelper
def setup
@@ -16,7 +14,7 @@ module ActiveRecord
assert_raise ActiveRecord::NoDatabaseError do
configuration = ActiveRecord::Base.configurations['arunit'].merge(database: 'inexistent_activerecord_unittest')
connection = ActiveRecord::Base.mysql_connection(configuration)
- connection.exec_query('drop table if exists ex')
+ connection.drop_table 'ex', if_exists: true
end
end
@@ -67,35 +65,9 @@ module ActiveRecord
end
end
- def test_tables_quoting
- @conn.tables(nil, "foo-bar", nil)
- flunk
- rescue => e
- # assertion for *quoted* database properly
- assert_match(/database 'foo-bar'/, e.inspect)
- end
-
- def test_pk_and_sequence_for
- with_example_table do
- pk, seq = @conn.pk_and_sequence_for('ex')
- assert_equal 'id', pk
- assert_equal @conn.default_sequence_name('ex', 'id'), seq
- end
- end
-
- def test_pk_and_sequence_for_with_non_standard_primary_key
- with_example_table '`code` INT(11) auto_increment, PRIMARY KEY (`code`)' do
- pk, seq = @conn.pk_and_sequence_for('ex')
- assert_equal 'code', pk
- assert_equal @conn.default_sequence_name('ex', 'code'), seq
- end
- end
-
- def test_pk_and_sequence_for_with_custom_index_type_pk
- with_example_table '`id` INT(11) auto_increment, PRIMARY KEY USING BTREE (`id`)' do
- pk, seq = @conn.pk_and_sequence_for('ex')
- assert_equal 'id', pk
- assert_equal @conn.default_sequence_name('ex', 'id'), seq
+ def test_composite_primary_key
+ with_example_table '`id` INT, `number` INT, foo INT, PRIMARY KEY (`id`, `number`)' do
+ assert_nil @conn.primary_key('ex')
end
end
@@ -105,7 +77,7 @@ module ActiveRecord
result = @conn.exec_query('SELECT status FROM ex')
- assert_equal 2, result.column_types['status'].type_cast_from_database(result.last['status'])
+ assert_equal 2, result.column_types['status'].deserialize(result.last['status'])
end
end
@@ -123,10 +95,10 @@ module ActiveRecord
private
def insert(ctx, data, table='ex')
- binds = data.map { |name, value|
- [ctx.columns(table).find { |x| x.name == name }, value]
+ binds = data.map { |name, value|
+ Relation::QueryAttribute.new(name, value, Type::Value.new)
}
- columns = binds.map(&:first).map(&:name)
+ columns = binds.map(&:name)
sql = "INSERT INTO #{table} (#{columns.join(", ")})
VALUES (#{(['?'] * columns.length).join(', ')})"
@@ -136,7 +108,7 @@ module ActiveRecord
def with_example_table(definition = nil, &block)
definition ||= <<-SQL
- `id` int(11) auto_increment PRIMARY KEY,
+ `id` int auto_increment PRIMARY KEY,
`number` integer,
`data` varchar(255)
SQL
diff --git a/activerecord/test/cases/adapters/mysql/quoting_test.rb b/activerecord/test/cases/adapters/mysql/quoting_test.rb
index d8a954efa8..2024aa36ab 100644
--- a/activerecord/test/cases/adapters/mysql/quoting_test.rb
+++ b/activerecord/test/cases/adapters/mysql/quoting_test.rb
@@ -1,25 +1,29 @@
require "cases/helper"
-module ActiveRecord
- module ConnectionAdapters
- class MysqlAdapter
- class QuotingTest < ActiveRecord::TestCase
- def setup
- @conn = ActiveRecord::Base.connection
- end
+class MysqlQuotingTest < ActiveRecord::MysqlTestCase
+ def setup
+ @conn = ActiveRecord::Base.connection
+ end
+
+ def test_type_cast_true
+ assert_equal 1, @conn.type_cast(true)
+ end
- def test_type_cast_true
- c = Column.new(nil, 1, Type::Boolean.new)
- assert_equal 1, @conn.type_cast(true, nil)
- assert_equal 1, @conn.type_cast(true, c)
- end
+ def test_type_cast_false
+ assert_equal 0, @conn.type_cast(false)
+ end
+
+ def test_quoted_date_precision_for_gte_564
+ @conn.stubs(:full_version).returns('5.6.4')
+ @conn.remove_instance_variable(:@version) if @conn.instance_variable_defined?(:@version)
+ t = Time.now.change(usec: 1)
+ assert_match(/\.000001\z/, @conn.quoted_date(t))
+ end
- def test_type_cast_false
- c = Column.new(nil, 1, Type::Boolean.new)
- assert_equal 0, @conn.type_cast(false, nil)
- assert_equal 0, @conn.type_cast(false, c)
- end
- end
- end
+ def test_quoted_date_precision_for_lt_564
+ @conn.stubs(:full_version).returns('5.6.3')
+ @conn.remove_instance_variable(:@version) if @conn.instance_variable_defined?(:@version)
+ t = Time.now.change(usec: 1)
+ assert_no_match(/\.000001\z/, @conn.quoted_date(t))
end
end
diff --git a/activerecord/test/cases/adapters/mysql/reserved_word_test.rb b/activerecord/test/cases/adapters/mysql/reserved_word_test.rb
index 61ae0abfd1..4ea1d9ad36 100644
--- a/activerecord/test/cases/adapters/mysql/reserved_word_test.rb
+++ b/activerecord/test/cases/adapters/mysql/reserved_word_test.rb
@@ -1,29 +1,29 @@
require "cases/helper"
-class Group < ActiveRecord::Base
- Group.table_name = 'group'
- belongs_to :select
- has_one :values
-end
+# a suite of tests to ensure the ConnectionAdapters#MysqlAdapter can handle tables with
+# reserved word names (ie: group, order, values, etc...)
+class MysqlReservedWordTest < ActiveRecord::MysqlTestCase
+ class Group < ActiveRecord::Base
+ Group.table_name = 'group'
+ belongs_to :select
+ has_one :values
+ end
-class Select < ActiveRecord::Base
- Select.table_name = 'select'
- has_many :groups
-end
+ class Select < ActiveRecord::Base
+ Select.table_name = 'select'
+ has_many :groups
+ end
-class Values < ActiveRecord::Base
- Values.table_name = 'values'
-end
+ class Values < ActiveRecord::Base
+ Values.table_name = 'values'
+ end
-class Distinct < ActiveRecord::Base
- Distinct.table_name = 'distinct'
- has_and_belongs_to_many :selects
- has_many :values, :through => :groups
-end
+ class Distinct < ActiveRecord::Base
+ Distinct.table_name = 'distinct'
+ has_and_belongs_to_many :selects
+ has_many :values, :through => :groups
+ end
-# a suite of tests to ensure the ConnectionAdapters#MysqlAdapter can handle tables with
-# reserved word names (ie: group, order, values, etc...)
-class MysqlReservedWordTest < ActiveRecord::TestCase
def setup
@connection = ActiveRecord::Base.connection
@@ -71,7 +71,7 @@ class MysqlReservedWordTest < ActiveRecord::TestCase
#fixtures
self.use_instantiated_fixtures = true
- self.use_transactional_fixtures = false
+ self.use_transactional_tests = false
#activerecord model class with reserved-word table name
def test_activerecord_model
@@ -101,7 +101,7 @@ class MysqlReservedWordTest < ActiveRecord::TestCase
gs = nil
assert_nothing_raised { gs = Select.find(2).groups }
assert_equal gs.length, 2
- assert(gs.collect{|x| x.id}.sort == [2, 3])
+ assert(gs.collect(&:id).sort == [2, 3])
end
# has_and_belongs_to_many with reserved-word table name
@@ -110,7 +110,7 @@ class MysqlReservedWordTest < ActiveRecord::TestCase
s = nil
assert_nothing_raised { s = Distinct.find(1).selects }
assert_equal s.length, 2
- assert(s.collect{|x|x.id}.sort == [1, 2])
+ assert(s.collect(&:id).sort == [1, 2])
end
# activerecord model introspection with reserved-word table and column names
@@ -139,7 +139,7 @@ class MysqlReservedWordTest < ActiveRecord::TestCase
# custom drop table, uses execute on connection to drop a table if it exists. note: escapes table_name
def drop_tables_directly(table_names, connection = @connection)
table_names.each do |name|
- connection.execute("DROP TABLE IF EXISTS `#{name}`")
+ connection.drop_table name, if_exists: true
end
end
diff --git a/activerecord/test/cases/adapters/mysql/schema_test.rb b/activerecord/test/cases/adapters/mysql/schema_test.rb
index 87c5277e64..a0f3c31e78 100644
--- a/activerecord/test/cases/adapters/mysql/schema_test.rb
+++ b/activerecord/test/cases/adapters/mysql/schema_test.rb
@@ -4,7 +4,7 @@ require 'models/comment'
module ActiveRecord
module ConnectionAdapters
- class MysqlSchemaTest < ActiveRecord::TestCase
+ class MysqlSchemaTest < ActiveRecord::MysqlTestCase
fixtures :posts
def setup
@@ -14,6 +14,7 @@ module ActiveRecord
@db_name = db
@omgpost = Class.new(ActiveRecord::Base) do
+ self.inheritance_column = :disabled
self.table_name = "#{db}.#{table}"
def self.name; 'Post'; end
end
@@ -22,7 +23,7 @@ module ActiveRecord
end
teardown do
- @connection.execute "drop table if exists mysql_doubles"
+ @connection.drop_table "mysql_doubles", if_exists: true
end
class MysqlDouble < ActiveRecord::Base
@@ -81,7 +82,7 @@ module ActiveRecord
table = 'key_tests'
- indexes = @connection.indexes(table).sort_by {|i| i.name}
+ indexes = @connection.indexes(table).sort_by(&:name)
assert_equal 3,indexes.size
index_a = indexes.select{|i| i.name == index_a_name}[0]
diff --git a/activerecord/test/cases/adapters/mysql/sp_test.rb b/activerecord/test/cases/adapters/mysql/sp_test.rb
index 3ca2917ca4..7849248dcc 100644
--- a/activerecord/test/cases/adapters/mysql/sp_test.rb
+++ b/activerecord/test/cases/adapters/mysql/sp_test.rb
@@ -1,15 +1,30 @@
require "cases/helper"
require 'models/topic'
+require 'models/reply'
-class StoredProcedureTest < ActiveRecord::TestCase
+class MysqlStoredProcedureTest < ActiveRecord::MysqlTestCase
fixtures :topics
- # Test that MySQL allows multiple results for stored procedures
- if Mysql.const_defined?(:CLIENT_MULTI_RESULTS)
- def test_multi_results_from_find_by_sql
- topics = Topic.find_by_sql 'CALL topics();'
- assert_equal 1, topics.size
- assert ActiveRecord::Base.connection.active?, "Bad connection use by 'MysqlAdapter.select'"
+ def setup
+ @connection = ActiveRecord::Base.connection
+ unless ActiveRecord::Base.connection.version >= '5.6.0' || Mysql.const_defined?(:CLIENT_MULTI_RESULTS)
+ skip("no stored procedure support")
end
end
+
+ # Test that MySQL allows multiple results for stored procedures
+ #
+ # In MySQL 5.6, CLIENT_MULTI_RESULTS is enabled by default.
+ # http://dev.mysql.com/doc/refman/5.6/en/call.html
+ def test_multi_results
+ rows = @connection.select_rows('CALL ten();')
+ assert_equal 10, rows[0][0].to_i, "ten() did not return 10 as expected: #{rows.inspect}"
+ assert @connection.active?, "Bad connection use by 'MysqlAdapter.select_rows'"
+ end
+
+ def test_multi_results_from_find_by_sql
+ topics = Topic.find_by_sql 'CALL topics(3);'
+ assert_equal 3, topics.size
+ assert @connection.active?, "Bad connection use by 'MysqlAdapter.select'"
+ end
end
diff --git a/activerecord/test/cases/adapters/mysql/sql_types_test.rb b/activerecord/test/cases/adapters/mysql/sql_types_test.rb
index 1ddb1b91c9..d18579f242 100644
--- a/activerecord/test/cases/adapters/mysql/sql_types_test.rb
+++ b/activerecord/test/cases/adapters/mysql/sql_types_test.rb
@@ -1,10 +1,10 @@
require "cases/helper"
-class SqlTypesTest < ActiveRecord::TestCase
+class MysqlSqlTypesTest < ActiveRecord::MysqlTestCase
def test_binary_types
assert_equal 'varbinary(64)', type_to_sql(:binary, 64)
assert_equal 'varbinary(4095)', type_to_sql(:binary, 4095)
- assert_equal 'blob(4096)', type_to_sql(:binary, 4096)
+ assert_equal 'blob', type_to_sql(:binary, 4096)
assert_equal 'blob', type_to_sql(:binary)
end
diff --git a/activerecord/test/cases/adapters/mysql/statement_pool_test.rb b/activerecord/test/cases/adapters/mysql/statement_pool_test.rb
index 209a0cf464..0d1f968022 100644
--- a/activerecord/test/cases/adapters/mysql/statement_pool_test.rb
+++ b/activerecord/test/cases/adapters/mysql/statement_pool_test.rb
@@ -1,23 +1,19 @@
require 'cases/helper'
-module ActiveRecord::ConnectionAdapters
- class MysqlAdapter
- class StatementPoolTest < ActiveRecord::TestCase
- if Process.respond_to?(:fork)
- def test_cache_is_per_pid
- cache = StatementPool.new nil, 10
- cache['foo'] = 'bar'
- assert_equal 'bar', cache['foo']
+class MysqlStatementPoolTest < ActiveRecord::MysqlTestCase
+ if Process.respond_to?(:fork)
+ def test_cache_is_per_pid
+ cache = ActiveRecord::ConnectionAdapters::MysqlAdapter::StatementPool.new(10)
+ cache['foo'] = 'bar'
+ assert_equal 'bar', cache['foo']
- pid = fork {
- lookup = cache['foo'];
- exit!(!lookup)
- }
+ pid = fork {
+ lookup = cache['foo'];
+ exit!(!lookup)
+ }
- Process.waitpid pid
- assert $?.success?, 'process should exit successfully'
- end
- end
+ Process.waitpid pid
+ assert $?.success?, 'process should exit successfully'
end
end
end
diff --git a/activerecord/test/cases/adapters/mysql/table_options_test.rb b/activerecord/test/cases/adapters/mysql/table_options_test.rb
new file mode 100644
index 0000000000..99df6d6cba
--- /dev/null
+++ b/activerecord/test/cases/adapters/mysql/table_options_test.rb
@@ -0,0 +1,42 @@
+require "cases/helper"
+require 'support/schema_dumping_helper'
+
+class MysqlTableOptionsTest < ActiveRecord::MysqlTestCase
+ include SchemaDumpingHelper
+
+ def setup
+ @connection = ActiveRecord::Base.connection
+ end
+
+ def teardown
+ @connection.drop_table "mysql_table_options", if_exists: true
+ end
+
+ test "table options with ENGINE" do
+ @connection.create_table "mysql_table_options", force: true, options: "ENGINE=MyISAM"
+ output = dump_table_schema("mysql_table_options")
+ options = %r{create_table "mysql_table_options", force: :cascade, options: "(?<options>.*)"}.match(output)[:options]
+ assert_match %r{ENGINE=MyISAM}, options
+ end
+
+ test "table options with ROW_FORMAT" do
+ @connection.create_table "mysql_table_options", force: true, options: "ROW_FORMAT=REDUNDANT"
+ output = dump_table_schema("mysql_table_options")
+ options = %r{create_table "mysql_table_options", force: :cascade, options: "(?<options>.*)"}.match(output)[:options]
+ assert_match %r{ROW_FORMAT=REDUNDANT}, options
+ end
+
+ test "table options with CHARSET" do
+ @connection.create_table "mysql_table_options", force: true, options: "CHARSET=utf8mb4"
+ output = dump_table_schema("mysql_table_options")
+ options = %r{create_table "mysql_table_options", force: :cascade, options: "(?<options>.*)"}.match(output)[:options]
+ assert_match %r{CHARSET=utf8mb4}, options
+ end
+
+ test "table options with COLLATE" do
+ @connection.create_table "mysql_table_options", force: true, options: "COLLATE=utf8mb4_bin"
+ output = dump_table_schema("mysql_table_options")
+ options = %r{create_table "mysql_table_options", force: :cascade, options: "(?<options>.*)"}.match(output)[:options]
+ assert_match %r{COLLATE=utf8mb4_bin}, options
+ end
+end
diff --git a/activerecord/test/cases/adapters/mysql/unsigned_type_test.rb b/activerecord/test/cases/adapters/mysql/unsigned_type_test.rb
new file mode 100644
index 0000000000..84c5394c2e
--- /dev/null
+++ b/activerecord/test/cases/adapters/mysql/unsigned_type_test.rb
@@ -0,0 +1,65 @@
+require "cases/helper"
+require "support/schema_dumping_helper"
+
+class MysqlUnsignedTypeTest < ActiveRecord::MysqlTestCase
+ include SchemaDumpingHelper
+ self.use_transactional_tests = false
+
+ class UnsignedType < ActiveRecord::Base
+ end
+
+ setup do
+ @connection = ActiveRecord::Base.connection
+ @connection.create_table("unsigned_types", force: true) do |t|
+ t.integer :unsigned_integer, unsigned: true
+ t.bigint :unsigned_bigint, unsigned: true
+ t.float :unsigned_float, unsigned: true
+ t.decimal :unsigned_decimal, unsigned: true, precision: 10, scale: 2
+ end
+ end
+
+ teardown do
+ @connection.drop_table "unsigned_types", if_exists: true
+ end
+
+ test "unsigned int max value is in range" do
+ assert expected = UnsignedType.create(unsigned_integer: 4294967295)
+ assert_equal expected, UnsignedType.find_by(unsigned_integer: 4294967295)
+ end
+
+ test "minus value is out of range" do
+ assert_raise(RangeError) do
+ UnsignedType.create(unsigned_integer: -10)
+ end
+ assert_raise(RangeError) do
+ UnsignedType.create(unsigned_bigint: -10)
+ end
+ assert_raise(ActiveRecord::StatementInvalid) do
+ UnsignedType.create(unsigned_float: -10.0)
+ end
+ assert_raise(ActiveRecord::StatementInvalid) do
+ UnsignedType.create(unsigned_decimal: -10.0)
+ end
+ end
+
+ test "schema definition can use unsigned as the type" do
+ @connection.change_table("unsigned_types") do |t|
+ t.unsigned_integer :unsigned_integer_t
+ t.unsigned_bigint :unsigned_bigint_t
+ t.unsigned_float :unsigned_float_t
+ t.unsigned_decimal :unsigned_decimal_t, precision: 10, scale: 2
+ end
+
+ @connection.columns("unsigned_types").select { |c| /^unsigned_/ === c.name }.each do |column|
+ assert column.unsigned?
+ end
+ end
+
+ test "schema dump includes unsigned option" do
+ schema = dump_table_schema "unsigned_types"
+ assert_match %r{t.integer\s+"unsigned_integer",\s+limit: 4,\s+unsigned: true$}, schema
+ assert_match %r{t.integer\s+"unsigned_bigint",\s+limit: 8,\s+unsigned: true$}, schema
+ assert_match %r{t.float\s+"unsigned_float",\s+limit: 24,\s+unsigned: true$}, schema
+ assert_match %r{t.decimal\s+"unsigned_decimal",\s+precision: 10,\s+scale: 2,\s+unsigned: true$}, schema
+ end
+end
diff --git a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb
index cefc3e3c7e..31dc69a45b 100644
--- a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb
@@ -1,7 +1,7 @@
require "cases/helper"
require 'support/connection_helper'
-class ActiveSchemaTest < ActiveRecord::TestCase
+class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase
include ConnectionHelper
def setup
@@ -59,21 +59,56 @@ class ActiveSchemaTest < ActiveRecord::TestCase
assert_equal expected, add_index(:people, [:last_name, :first_name], :length => 15, :using => :btree)
end
- def test_drop_table
- assert_equal "DROP TABLE `people`", drop_table(:people)
+ def test_index_in_create
+ def (ActiveRecord::Base.connection).table_exists?(*); false; end
+
+ %w(SPATIAL FULLTEXT UNIQUE).each do |type|
+ expected = "CREATE TABLE `people` (#{type} INDEX `index_people_on_last_name` (`last_name`) ) ENGINE=InnoDB"
+ actual = ActiveRecord::Base.connection.create_table(:people, id: false) do |t|
+ t.index :last_name, type: type
+ end
+ assert_equal expected, actual
+ end
+
+ expected = "CREATE TABLE `people` ( INDEX `index_people_on_last_name` USING btree (`last_name`(10)) ) ENGINE=InnoDB"
+ actual = ActiveRecord::Base.connection.create_table(:people, id: false) do |t|
+ t.index :last_name, length: 10, using: :btree
+ end
+ assert_equal expected, actual
end
- if current_adapter?(:Mysql2Adapter)
- def test_create_mysql_database_with_encoding
- assert_equal "CREATE DATABASE `matt` DEFAULT CHARACTER SET `utf8`", create_database(:matt)
- assert_equal "CREATE DATABASE `aimonetti` DEFAULT CHARACTER SET `latin1`", create_database(:aimonetti, {:charset => 'latin1'})
- assert_equal "CREATE DATABASE `matt_aimonetti` DEFAULT CHARACTER SET `big5` COLLATE `big5_chinese_ci`", create_database(:matt_aimonetti, {:charset => :big5, :collation => :big5_chinese_ci})
+ def test_index_in_bulk_change
+ def (ActiveRecord::Base.connection).table_exists?(*); true; end
+ def (ActiveRecord::Base.connection).index_name_exists?(*); false; end
+
+ %w(SPATIAL FULLTEXT UNIQUE).each do |type|
+ expected = "ALTER TABLE `people` ADD #{type} INDEX `index_people_on_last_name` (`last_name`)"
+ actual = ActiveRecord::Base.connection.change_table(:people, bulk: true) do |t|
+ t.index :last_name, type: type
+ end
+ assert_equal expected, actual
end
- def test_recreate_mysql_database_with_encoding
- create_database(:luca, {:charset => 'latin1'})
- assert_equal "CREATE DATABASE `luca` DEFAULT CHARACTER SET `latin1`", recreate_database(:luca, {:charset => 'latin1'})
+ expected = "ALTER TABLE `peaple` ADD INDEX `index_peaple_on_last_name` USING btree (`last_name`(10)), ALGORITHM = COPY"
+ actual = ActiveRecord::Base.connection.change_table(:peaple, bulk: true) do |t|
+ t.index :last_name, length: 10, using: :btree, algorithm: :copy
end
+ assert_equal expected, actual
+ end
+
+ def test_drop_table
+ assert_equal "DROP TABLE `people`", drop_table(:people)
+ end
+
+ def test_create_mysql_database_with_encoding
+ assert_equal "CREATE DATABASE `matt` DEFAULT CHARACTER SET `utf8`", create_database(:matt)
+ assert_equal "CREATE DATABASE `aimonetti` DEFAULT CHARACTER SET `latin1`", create_database(:aimonetti, {:charset => 'latin1'})
+ assert_equal "CREATE DATABASE `matt_aimonetti` DEFAULT CHARACTER SET `big5` COLLATE `big5_chinese_ci`", create_database(:matt_aimonetti, {:charset => :big5, :collation => :big5_chinese_ci})
+ end
+
+ def test_recreate_mysql_database_with_encoding
+ create_database(:luca, {:charset => 'latin1'})
+ assert_equal "CREATE DATABASE `luca` DEFAULT CHARACTER SET `latin1`", recreate_database(:luca, {:charset => 'latin1'})
end
def test_add_column
@@ -92,7 +127,7 @@ class ActiveSchemaTest < ActiveRecord::TestCase
with_real_execute do
begin
ActiveRecord::Base.connection.create_table :delete_me
- ActiveRecord::Base.connection.add_timestamps :delete_me
+ ActiveRecord::Base.connection.add_timestamps :delete_me, null: true
assert column_present?('delete_me', 'updated_at', 'datetime')
assert column_present?('delete_me', 'created_at', 'datetime')
ensure
@@ -105,9 +140,9 @@ class ActiveSchemaTest < ActiveRecord::TestCase
with_real_execute do
begin
ActiveRecord::Base.connection.create_table :delete_me do |t|
- t.timestamps
+ t.timestamps null: true
end
- ActiveRecord::Base.connection.remove_timestamps :delete_me
+ ActiveRecord::Base.connection.remove_timestamps :delete_me, { null: true }
assert !column_present?('delete_me', 'updated_at', 'datetime')
assert !column_present?('delete_me', 'created_at', 'datetime')
ensure
diff --git a/activerecord/test/cases/adapters/mysql2/bind_parameter_test.rb b/activerecord/test/cases/adapters/mysql2/bind_parameter_test.rb
index 5e8065d80d..abdf3dbf5b 100644
--- a/activerecord/test/cases/adapters/mysql2/bind_parameter_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/bind_parameter_test.rb
@@ -4,7 +4,7 @@ require 'models/topic'
module ActiveRecord
module ConnectionAdapters
class Mysql2Adapter
- class BindParameterTest < ActiveRecord::TestCase
+ class BindParameterTest < ActiveRecord::Mysql2TestCase
fixtures :topics
def test_update_question_marks
diff --git a/activerecord/test/cases/adapters/mysql2/boolean_test.rb b/activerecord/test/cases/adapters/mysql2/boolean_test.rb
index f3c711a64b..8575df9e43 100644
--- a/activerecord/test/cases/adapters/mysql2/boolean_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/boolean_test.rb
@@ -1,7 +1,7 @@
require "cases/helper"
-class Mysql2BooleanTest < ActiveRecord::TestCase
- self.use_transactional_fixtures = false
+class Mysql2BooleanTest < ActiveRecord::Mysql2TestCase
+ self.use_transactional_tests = false
class BooleanType < ActiveRecord::Base
self.table_name = "mysql_booleans"
@@ -9,6 +9,7 @@ class Mysql2BooleanTest < ActiveRecord::TestCase
setup do
@connection = ActiveRecord::Base.connection
+ @connection.clear_cache!
@connection.create_table("mysql_booleans") do |t|
t.boolean "archived"
t.string "published", limit: 1
@@ -46,8 +47,7 @@ class Mysql2BooleanTest < ActiveRecord::TestCase
assert_equal 1, attributes["archived"]
assert_equal "1", attributes["published"]
- assert_equal 1, @connection.type_cast(true, boolean_column)
- assert_equal "1", @connection.type_cast(true, string_column)
+ assert_equal 1, @connection.type_cast(true)
end
test "test type casting without emulated booleans" do
@@ -59,8 +59,7 @@ class Mysql2BooleanTest < ActiveRecord::TestCase
assert_equal 1, attributes["archived"]
assert_equal "1", attributes["published"]
- assert_equal 1, @connection.type_cast(true, boolean_column)
- assert_equal "1", @connection.type_cast(true, string_column)
+ assert_equal 1, @connection.type_cast(true)
end
test "with booleans stored as 1 and 0" do
diff --git a/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb b/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb
index 09bebf3071..963116f08a 100644
--- a/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb
@@ -1,7 +1,6 @@
require "cases/helper"
-require 'models/person'
-class Mysql2CaseSensitivityTest < ActiveRecord::TestCase
+class Mysql2CaseSensitivityTest < ActiveRecord::Mysql2TestCase
class CollationTest < ActiveRecord::Base
end
diff --git a/activerecord/test/cases/adapters/mysql2/charset_collation_test.rb b/activerecord/test/cases/adapters/mysql2/charset_collation_test.rb
new file mode 100644
index 0000000000..4fd34def15
--- /dev/null
+++ b/activerecord/test/cases/adapters/mysql2/charset_collation_test.rb
@@ -0,0 +1,54 @@
+require "cases/helper"
+require 'support/schema_dumping_helper'
+
+class Mysql2CharsetCollationTest < ActiveRecord::Mysql2TestCase
+ include SchemaDumpingHelper
+ self.use_transactional_tests = false
+
+ setup do
+ @connection = ActiveRecord::Base.connection
+ @connection.create_table :charset_collations, force: true do |t|
+ t.string :string_ascii_bin, charset: 'ascii', collation: 'ascii_bin'
+ t.text :text_ucs2_unicode_ci, charset: 'ucs2', collation: 'ucs2_unicode_ci'
+ end
+ end
+
+ teardown do
+ @connection.drop_table :charset_collations, if_exists: true
+ end
+
+ test "string column with charset and collation" do
+ column = @connection.columns(:charset_collations).find { |c| c.name == 'string_ascii_bin' }
+ assert_equal :string, column.type
+ assert_equal 'ascii_bin', column.collation
+ end
+
+ test "text column with charset and collation" do
+ column = @connection.columns(:charset_collations).find { |c| c.name == 'text_ucs2_unicode_ci' }
+ assert_equal :text, column.type
+ assert_equal 'ucs2_unicode_ci', column.collation
+ end
+
+ test "add column with charset and collation" do
+ @connection.add_column :charset_collations, :title, :string, charset: 'utf8', collation: 'utf8_bin'
+
+ column = @connection.columns(:charset_collations).find { |c| c.name == 'title' }
+ assert_equal :string, column.type
+ assert_equal 'utf8_bin', column.collation
+ end
+
+ test "change column with charset and collation" do
+ @connection.add_column :charset_collations, :description, :string, charset: 'utf8', collation: 'utf8_unicode_ci'
+ @connection.change_column :charset_collations, :description, :text, charset: 'utf8', collation: 'utf8_general_ci'
+
+ column = @connection.columns(:charset_collations).find { |c| c.name == 'description' }
+ assert_equal :text, column.type
+ assert_equal 'utf8_general_ci', column.collation
+ end
+
+ test "schema dump includes collation" do
+ output = dump_table_schema("charset_collations")
+ assert_match %r{t.string\s+"string_ascii_bin",\s+limit: 255,\s+collation: "ascii_bin"$}, output
+ assert_match %r{t.text\s+"text_ucs2_unicode_ci",\s+limit: 65535,\s+collation: "ucs2_unicode_ci"$}, output
+ end
+end
diff --git a/activerecord/test/cases/adapters/mysql2/connection_test.rb b/activerecord/test/cases/adapters/mysql2/connection_test.rb
index 3b35e69e0d..000bcadebe 100644
--- a/activerecord/test/cases/adapters/mysql2/connection_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/connection_test.rb
@@ -1,9 +1,11 @@
require "cases/helper"
require 'support/connection_helper'
-class MysqlConnectionTest < ActiveRecord::TestCase
+class Mysql2ConnectionTest < ActiveRecord::Mysql2TestCase
include ConnectionHelper
+ fixtures :comments
+
def setup
super
@subscriber = SQLSubscriber.new
@@ -20,10 +22,21 @@ class MysqlConnectionTest < ActiveRecord::TestCase
assert_raise ActiveRecord::NoDatabaseError do
configuration = ActiveRecord::Base.configurations['arunit'].merge(database: 'inexistent_activerecord_unittest')
connection = ActiveRecord::Base.mysql2_connection(configuration)
- connection.exec_query('drop table if exists ex')
+ connection.drop_table 'ex', if_exists: true
end
end
+ def test_truncate
+ rows = ActiveRecord::Base.connection.exec_query("select count(*) from comments")
+ count = rows.first.values.first
+ assert_operator count, :>, 0
+
+ ActiveRecord::Base.connection.truncate("comments")
+ rows = ActiveRecord::Base.connection.exec_query("select count(*) from comments")
+ count = rows.first.values.first
+ assert_equal 0, count
+ end
+
def test_no_automatic_reconnection_after_timeout
assert @connection.active?
@connection.update('set @@wait_timeout=1')
@@ -31,9 +44,7 @@ class MysqlConnectionTest < ActiveRecord::TestCase
assert !@connection.active?
# Repair all fixture connections so other tests won't break.
- @fixture_connections.each do |c|
- c.verify!
- end
+ @fixture_connections.each(&:verify!)
end
def test_successful_reconnection_after_timeout_with_manual_reconnect
@@ -52,6 +63,11 @@ class MysqlConnectionTest < ActiveRecord::TestCase
assert @connection.active?
end
+ def test_mysql_connection_collation_is_configured
+ assert_equal 'utf8_unicode_ci', @connection.show_variable('collation_connection')
+ assert_equal 'utf8_general_ci', ARUnit2Model.connection.show_variable('collation_connection')
+ end
+
# TODO: Below is a straight up copy/paste from mysql/connection_test.rb
# I'm not sure what the correct way is to share these tests between
# adapters in minitest.
@@ -68,6 +84,15 @@ class MysqlConnectionTest < ActiveRecord::TestCase
end
end
+ def test_mysql_strict_mode_specified_default
+ run_without_connection do |orig_connection|
+ ActiveRecord::Base.establish_connection(orig_connection.merge({strict: :default}))
+ global_sql_mode = ActiveRecord::Base.connection.exec_query "SELECT @@GLOBAL.sql_mode"
+ session_sql_mode = ActiveRecord::Base.connection.exec_query "SELECT @@SESSION.sql_mode"
+ assert_equal global_sql_mode.rows, session_sql_mode.rows
+ end
+ end
+
def test_mysql_set_session_variable
run_without_connection do |orig_connection|
ActiveRecord::Base.establish_connection(orig_connection.deep_merge({:variables => {:default_week_format => 3}}))
@@ -76,7 +101,7 @@ class MysqlConnectionTest < ActiveRecord::TestCase
end
end
- def test_mysql_sql_mode_variable_overides_strict_mode
+ def test_mysql_sql_mode_variable_overrides_strict_mode
run_without_connection do |orig_connection|
ActiveRecord::Base.establish_connection(orig_connection.deep_merge(variables: { 'sql_mode' => 'ansi' }))
result = ActiveRecord::Base.connection.exec_query 'SELECT @@SESSION.sql_mode'
@@ -106,11 +131,4 @@ class MysqlConnectionTest < ActiveRecord::TestCase
ensure
@connection.execute "DROP TABLE `bar_baz`"
end
-
- if mysql_56?
- def test_quote_time_usec
- assert_equal "'1970-01-01 00:00:00.000000'", @connection.quote(Time.at(0))
- assert_equal "'1970-01-01 00:00:00.000000'", @connection.quote(Time.at(0).to_datetime)
- end
- end
end
diff --git a/activerecord/test/cases/adapters/mysql2/enum_test.rb b/activerecord/test/cases/adapters/mysql2/enum_test.rb
index 6dd9a5ec87..bd732b5eca 100644
--- a/activerecord/test/cases/adapters/mysql2/enum_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/enum_test.rb
@@ -1,6 +1,6 @@
require "cases/helper"
-class Mysql2EnumTest < ActiveRecord::TestCase
+class Mysql2EnumTest < ActiveRecord::Mysql2TestCase
class EnumTest < ActiveRecord::Base
end
diff --git a/activerecord/test/cases/adapters/mysql2/explain_test.rb b/activerecord/test/cases/adapters/mysql2/explain_test.rb
index 675703caa1..4fc7414b18 100644
--- a/activerecord/test/cases/adapters/mysql2/explain_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/explain_test.rb
@@ -1,10 +1,11 @@
require "cases/helper"
require 'models/developer'
+require 'models/computer'
module ActiveRecord
module ConnectionAdapters
class Mysql2Adapter
- class ExplainTest < ActiveRecord::TestCase
+ class ExplainTest < ActiveRecord::Mysql2TestCase
fixtures :developers
def test_explain_for_one_query
@@ -17,7 +18,7 @@ module ActiveRecord
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` IN (1)), explain
+ assert_match %(EXPLAIN for: SELECT `audit_logs`.* FROM `audit_logs` WHERE `audit_logs`.`developer_id` = 1), explain
assert_match %r(audit_logs |.* ALL), explain
end
end
diff --git a/activerecord/test/cases/adapters/mysql2/json_test.rb b/activerecord/test/cases/adapters/mysql2/json_test.rb
new file mode 100644
index 0000000000..c8c933af5e
--- /dev/null
+++ b/activerecord/test/cases/adapters/mysql2/json_test.rb
@@ -0,0 +1,172 @@
+require 'cases/helper'
+require 'support/schema_dumping_helper'
+
+if ActiveRecord::Base.connection.supports_json?
+class Mysql2JSONTest < ActiveRecord::Mysql2TestCase
+ include SchemaDumpingHelper
+ self.use_transactional_tests = false
+
+ class JsonDataType < ActiveRecord::Base
+ self.table_name = 'json_data_type'
+
+ store_accessor :settings, :resolution
+ end
+
+ def setup
+ @connection = ActiveRecord::Base.connection
+ begin
+ @connection.create_table('json_data_type') do |t|
+ t.json 'payload'
+ t.json 'settings'
+ end
+ end
+ end
+
+ def teardown
+ @connection.drop_table :json_data_type, if_exists: true
+ JsonDataType.reset_column_information
+ end
+
+ def test_column
+ column = JsonDataType.columns_hash["payload"]
+ assert_equal :json, column.type
+ assert_equal 'json', column.sql_type
+
+ type = JsonDataType.type_for_attribute("payload")
+ assert_not type.binary?
+ end
+
+ def test_change_table_supports_json
+ @connection.change_table('json_data_type') do |t|
+ t.json 'users'
+ end
+ JsonDataType.reset_column_information
+ column = JsonDataType.columns_hash['users']
+ assert_equal :json, column.type
+ end
+
+ def test_schema_dumping
+ output = dump_table_schema("json_data_type")
+ assert_match(/t\.json\s+"settings"/, output)
+ end
+
+ def test_cast_value_on_write
+ x = JsonDataType.new payload: {"string" => "foo", :symbol => :bar}
+ assert_equal({"string" => "foo", :symbol => :bar}, x.payload_before_type_cast)
+ assert_equal({"string" => "foo", "symbol" => "bar"}, x.payload)
+ x.save
+ assert_equal({"string" => "foo", "symbol" => "bar"}, x.reload.payload)
+ end
+
+ def test_type_cast_json
+ type = JsonDataType.type_for_attribute("payload")
+
+ data = "{\"a_key\":\"a_value\"}"
+ hash = type.deserialize(data)
+ assert_equal({'a_key' => 'a_value'}, hash)
+ assert_equal({'a_key' => 'a_value'}, type.deserialize(data))
+
+ assert_equal({}, type.deserialize("{}"))
+ assert_equal({'key'=>nil}, type.deserialize('{"key": null}'))
+ assert_equal({'c'=>'}','"a"'=>'b "a b'}, type.deserialize(%q({"c":"}", "\"a\"":"b \"a b"})))
+ end
+
+ def test_rewrite
+ @connection.execute "insert into json_data_type (payload) VALUES ('{\"k\":\"v\"}')"
+ x = JsonDataType.first
+ x.payload = { '"a\'' => 'b' }
+ assert x.save!
+ end
+
+ def test_select
+ @connection.execute "insert into json_data_type (payload) VALUES ('{\"k\":\"v\"}')"
+ x = JsonDataType.first
+ assert_equal({'k' => 'v'}, x.payload)
+ end
+
+ def test_select_multikey
+ @connection.execute %q|insert into json_data_type (payload) VALUES ('{"k1":"v1", "k2":"v2", "k3":[1,2,3]}')|
+ x = JsonDataType.first
+ assert_equal({'k1' => 'v1', 'k2' => 'v2', 'k3' => [1,2,3]}, x.payload)
+ end
+
+ def test_null_json
+ @connection.execute %q|insert into json_data_type (payload) VALUES(null)|
+ x = JsonDataType.first
+ assert_equal(nil, x.payload)
+ end
+
+ def test_select_array_json_value
+ @connection.execute %q|insert into json_data_type (payload) VALUES ('["v0",{"k1":"v1"}]')|
+ x = JsonDataType.first
+ assert_equal(['v0', {'k1' => 'v1'}], x.payload)
+ end
+
+ def test_rewrite_array_json_value
+ @connection.execute %q|insert into json_data_type (payload) VALUES ('["v0",{"k1":"v1"}]')|
+ x = JsonDataType.first
+ x.payload = ['v1', {'k2' => 'v2'}, 'v3']
+ assert x.save!
+ end
+
+ def test_with_store_accessors
+ x = JsonDataType.new(resolution: "320×480")
+ assert_equal "320×480", x.resolution
+
+ x.save!
+ x = JsonDataType.first
+ assert_equal "320×480", x.resolution
+
+ x.resolution = "640×1136"
+ x.save!
+
+ x = JsonDataType.first
+ assert_equal "640×1136", x.resolution
+ end
+
+ def test_duplication_with_store_accessors
+ x = JsonDataType.new(resolution: "320×480")
+ assert_equal "320×480", x.resolution
+
+ y = x.dup
+ assert_equal "320×480", y.resolution
+ end
+
+ def test_yaml_round_trip_with_store_accessors
+ x = JsonDataType.new(resolution: "320×480")
+ assert_equal "320×480", x.resolution
+
+ y = YAML.load(YAML.dump(x))
+ assert_equal "320×480", y.resolution
+ end
+
+ def test_changes_in_place
+ json = JsonDataType.new
+ assert_not json.changed?
+
+ json.payload = { 'one' => 'two' }
+ assert json.changed?
+ assert json.payload_changed?
+
+ json.save!
+ assert_not json.changed?
+
+ json.payload['three'] = 'four'
+ assert json.payload_changed?
+
+ json.save!
+ json.reload
+
+ assert_equal({ 'one' => 'two', 'three' => 'four' }, json.payload)
+ assert_not json.changed?
+ end
+
+ def test_assigning_invalid_json
+ json = JsonDataType.new
+
+ json.payload = 'foo'
+
+ assert_nil json.payload
+ end
+end
+end
diff --git a/activerecord/test/cases/adapters/mysql2/quoting_test.rb b/activerecord/test/cases/adapters/mysql2/quoting_test.rb
new file mode 100644
index 0000000000..2de7e1b526
--- /dev/null
+++ b/activerecord/test/cases/adapters/mysql2/quoting_test.rb
@@ -0,0 +1,21 @@
+require "cases/helper"
+
+class Mysql2QuotingTest < ActiveRecord::Mysql2TestCase
+ setup do
+ @connection = ActiveRecord::Base.connection
+ end
+
+ test 'quoted date precision for gte 5.6.4' do
+ @connection.stubs(:full_version).returns('5.6.4')
+ @connection.remove_instance_variable(:@version) if @connection.instance_variable_defined?(:@version)
+ t = Time.now.change(usec: 1)
+ assert_match(/\.000001\z/, @connection.quoted_date(t))
+ end
+
+ test 'quoted date precision for lt 5.6.4' do
+ @connection.stubs(:full_version).returns('5.6.3')
+ @connection.remove_instance_variable(:@version) if @connection.instance_variable_defined?(:@version)
+ t = Time.now.change(usec: 1)
+ assert_no_match(/\.000001\z/, @connection.quoted_date(t))
+ end
+end
diff --git a/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb b/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb
index 799d927ee4..ffb4e2c5cf 100644
--- a/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb
@@ -1,29 +1,29 @@
require "cases/helper"
-class Group < ActiveRecord::Base
- Group.table_name = 'group'
- belongs_to :select
- has_one :values
-end
+# a suite of tests to ensure the ConnectionAdapters#MysqlAdapter can handle tables with
+# reserved word names (ie: group, order, values, etc...)
+class Mysql2ReservedWordTest < ActiveRecord::Mysql2TestCase
+ class Group < ActiveRecord::Base
+ Group.table_name = 'group'
+ belongs_to :select
+ has_one :values
+ end
-class Select < ActiveRecord::Base
- Select.table_name = 'select'
- has_many :groups
-end
+ class Select < ActiveRecord::Base
+ Select.table_name = 'select'
+ has_many :groups
+ end
-class Values < ActiveRecord::Base
- Values.table_name = 'values'
-end
+ class Values < ActiveRecord::Base
+ Values.table_name = 'values'
+ end
-class Distinct < ActiveRecord::Base
- Distinct.table_name = 'distinct'
- has_and_belongs_to_many :selects
- has_many :values, :through => :groups
-end
+ class Distinct < ActiveRecord::Base
+ Distinct.table_name = 'distinct'
+ has_and_belongs_to_many :selects
+ has_many :values, :through => :groups
+ end
-# a suite of tests to ensure the ConnectionAdapters#MysqlAdapter can handle tables with
-# reserved word names (ie: group, order, values, etc...)
-class MysqlReservedWordTest < ActiveRecord::TestCase
def setup
@connection = ActiveRecord::Base.connection
@@ -71,7 +71,7 @@ class MysqlReservedWordTest < ActiveRecord::TestCase
#fixtures
self.use_instantiated_fixtures = true
- self.use_transactional_fixtures = false
+ self.use_transactional_tests = false
#activerecord model class with reserved-word table name
def test_activerecord_model
@@ -100,7 +100,7 @@ class MysqlReservedWordTest < ActiveRecord::TestCase
gs = nil
assert_nothing_raised { gs = Select.find(2).groups }
assert_equal gs.length, 2
- assert(gs.collect{|x| x.id}.sort == [2, 3])
+ assert(gs.collect(&:id).sort == [2, 3])
end
# has_and_belongs_to_many with reserved-word table name
@@ -109,7 +109,7 @@ class MysqlReservedWordTest < ActiveRecord::TestCase
s = nil
assert_nothing_raised { s = Distinct.find(1).selects }
assert_equal s.length, 2
- assert(s.collect{|x|x.id}.sort == [1, 2])
+ assert(s.collect(&:id).sort == [1, 2])
end
# activerecord model introspection with reserved-word table and column names
@@ -138,7 +138,7 @@ class MysqlReservedWordTest < ActiveRecord::TestCase
# custom drop table, uses execute on connection to drop a table if it exists. note: escapes table_name
def drop_tables_directly(table_names, connection = @connection)
table_names.each do |name|
- connection.execute("DROP TABLE IF EXISTS `#{name}`")
+ connection.drop_table name, if_exists: true
end
end
diff --git a/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb b/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb
index 9c49599d34..396f235e77 100644
--- a/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb
@@ -1,39 +1,42 @@
require "cases/helper"
-module ActiveRecord
- module ConnectionAdapters
- class Mysql2Adapter
- class SchemaMigrationsTest < ActiveRecord::TestCase
- def test_renaming_index_on_foreign_key
- connection.add_index "engines", "car_id"
- connection.add_foreign_key :engines, :cars, name: "fk_engines_cars"
-
- connection.rename_index("engines", "index_engines_on_car_id", "idx_renamed")
- assert_equal ["idx_renamed"], connection.indexes("engines").map(&:name)
- ensure
- connection.remove_foreign_key :engines, name: "fk_engines_cars"
- end
-
- def test_initializes_schema_migrations_for_encoding_utf8mb4
- smtn = ActiveRecord::Migrator.schema_migrations_table_name
- connection.drop_table(smtn) if connection.table_exists?(smtn)
-
- config = connection.instance_variable_get(:@config)
- original_encoding = config[:encoding]
-
- config[:encoding] = 'utf8mb4'
- connection.initialize_schema_migrations_table
-
- assert connection.column_exists?(smtn, :version, :string, limit: Mysql2Adapter::MAX_INDEX_LENGTH_FOR_UTF8MB4)
- ensure
- config[:encoding] = original_encoding
- end
-
- private
- def connection
- @connection ||= ActiveRecord::Base.connection
- end
- end
- end
+class SchemaMigrationsTest < ActiveRecord::Mysql2TestCase
+ def test_renaming_index_on_foreign_key
+ connection.add_index "engines", "car_id"
+ connection.add_foreign_key :engines, :cars, name: "fk_engines_cars"
+
+ connection.rename_index("engines", "index_engines_on_car_id", "idx_renamed")
+ assert_equal ["idx_renamed"], connection.indexes("engines").map(&:name)
+ ensure
+ connection.remove_foreign_key :engines, name: "fk_engines_cars"
+ end
+
+ def test_initializes_schema_migrations_for_encoding_utf8mb4
+ smtn = ActiveRecord::Migrator.schema_migrations_table_name
+ connection.drop_table smtn, if_exists: true
+
+ database_name = connection.current_database
+ database_info = connection.select_one("SELECT * FROM information_schema.schemata WHERE schema_name = '#{database_name}'")
+
+ original_charset = database_info["DEFAULT_CHARACTER_SET_NAME"]
+ original_collation = database_info["DEFAULT_COLLATION_NAME"]
+
+ execute("ALTER DATABASE #{database_name} DEFAULT CHARACTER SET utf8mb4")
+
+ connection.initialize_schema_migrations_table
+
+ limit = ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter::MAX_INDEX_LENGTH_FOR_CHARSETS_OF_4BYTES_MAXLEN
+ assert connection.column_exists?(smtn, :version, :string, limit: limit)
+ ensure
+ execute("ALTER DATABASE #{database_name} DEFAULT CHARACTER SET #{original_charset} COLLATE #{original_collation}")
+ end
+
+ private
+ def connection
+ @connection ||= ActiveRecord::Base.connection
+ end
+
+ def execute(sql)
+ connection.execute(sql)
end
end
diff --git a/activerecord/test/cases/adapters/mysql2/schema_test.rb b/activerecord/test/cases/adapters/mysql2/schema_test.rb
index 43c9116b5a..1ebdca661c 100644
--- a/activerecord/test/cases/adapters/mysql2/schema_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/schema_test.rb
@@ -4,7 +4,7 @@ require 'models/comment'
module ActiveRecord
module ConnectionAdapters
- class Mysql2SchemaTest < ActiveRecord::TestCase
+ class Mysql2SchemaTest < ActiveRecord::Mysql2TestCase
fixtures :posts
def setup
@@ -14,6 +14,7 @@ module ActiveRecord
@db_name = db
@omgpost = Class.new(ActiveRecord::Base) do
+ self.inheritance_column = :disabled
self.table_name = "#{db}.#{table}"
def self.name; 'Post'; end
end
@@ -36,14 +37,6 @@ module ActiveRecord
assert(!@connection.table_exists?("#{@db_name}.zomg"), "table should not exist")
end
- def test_tables_quoting
- @connection.tables(nil, "foo-bar", nil)
- flunk
- rescue => e
- # assertion for *quoted* database properly
- assert_match(/database 'foo-bar'/, e.inspect)
- end
-
def test_dump_indexes
index_a_name = 'index_key_tests_on_snack'
index_b_name = 'index_key_tests_on_pizza'
@@ -51,7 +44,7 @@ module ActiveRecord
table = 'key_tests'
- indexes = @connection.indexes(table).sort_by {|i| i.name}
+ indexes = @connection.indexes(table).sort_by(&:name)
assert_equal 3,indexes.size
index_a = indexes.select{|i| i.name == index_a_name}[0]
@@ -66,12 +59,14 @@ module ActiveRecord
assert_equal :fulltext, index_c.type
end
- def test_drop_temporary_table
- @connection.transaction do
- @connection.create_table(:temp_table, temporary: true)
- # if it doesn't properly say DROP TEMPORARY TABLE, the transaction commit
- # will complain that no transaction is active
- @connection.drop_table(:temp_table, temporary: true)
+ unless mysql_enforcing_gtid_consistency?
+ def test_drop_temporary_table
+ @connection.transaction do
+ @connection.create_table(:temp_table, temporary: true)
+ # if it doesn't properly say DROP TEMPORARY TABLE, the transaction commit
+ # will complain that no transaction is active
+ @connection.drop_table(:temp_table, temporary: true)
+ end
end
end
end
diff --git a/activerecord/test/cases/adapters/mysql2/sp_test.rb b/activerecord/test/cases/adapters/mysql2/sp_test.rb
new file mode 100644
index 0000000000..cdaa2cca44
--- /dev/null
+++ b/activerecord/test/cases/adapters/mysql2/sp_test.rb
@@ -0,0 +1,30 @@
+require "cases/helper"
+require 'models/topic'
+require 'models/reply'
+
+class Mysql2StoredProcedureTest < ActiveRecord::Mysql2TestCase
+ fixtures :topics
+
+ def setup
+ @connection = ActiveRecord::Base.connection
+ unless ActiveRecord::Base.connection.version >= '5.6.0'
+ skip("no stored procedure support")
+ end
+ end
+
+ # Test that MySQL allows multiple results for stored procedures
+ #
+ # In MySQL 5.6, CLIENT_MULTI_RESULTS is enabled by default.
+ # http://dev.mysql.com/doc/refman/5.6/en/call.html
+ def test_multi_results
+ rows = @connection.select_rows('CALL ten();')
+ assert_equal 10, rows[0][0].to_i, "ten() did not return 10 as expected: #{rows.inspect}"
+ assert @connection.active?, "Bad connection use by 'Mysql2Adapter.select_rows'"
+ end
+
+ def test_multi_results_from_find_by_sql
+ topics = Topic.find_by_sql 'CALL topics(3);'
+ assert_equal 3, topics.size
+ assert @connection.active?, "Bad connection use by 'Mysql2Adapter.select'"
+ end
+end
diff --git a/activerecord/test/cases/adapters/mysql2/sql_types_test.rb b/activerecord/test/cases/adapters/mysql2/sql_types_test.rb
index 1ddb1b91c9..4926bc2267 100644
--- a/activerecord/test/cases/adapters/mysql2/sql_types_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/sql_types_test.rb
@@ -1,10 +1,10 @@
require "cases/helper"
-class SqlTypesTest < ActiveRecord::TestCase
+class Mysql2SqlTypesTest < ActiveRecord::Mysql2TestCase
def test_binary_types
assert_equal 'varbinary(64)', type_to_sql(:binary, 64)
assert_equal 'varbinary(4095)', type_to_sql(:binary, 4095)
- assert_equal 'blob(4096)', type_to_sql(:binary, 4096)
+ assert_equal 'blob', type_to_sql(:binary, 4096)
assert_equal 'blob', type_to_sql(:binary)
end
diff --git a/activerecord/test/cases/adapters/mysql2/table_options_test.rb b/activerecord/test/cases/adapters/mysql2/table_options_test.rb
new file mode 100644
index 0000000000..af121ee7d9
--- /dev/null
+++ b/activerecord/test/cases/adapters/mysql2/table_options_test.rb
@@ -0,0 +1,42 @@
+require "cases/helper"
+require 'support/schema_dumping_helper'
+
+class Mysql2TableOptionsTest < ActiveRecord::Mysql2TestCase
+ include SchemaDumpingHelper
+
+ def setup
+ @connection = ActiveRecord::Base.connection
+ end
+
+ def teardown
+ @connection.drop_table "mysql_table_options", if_exists: true
+ end
+
+ test "table options with ENGINE" do
+ @connection.create_table "mysql_table_options", force: true, options: "ENGINE=MyISAM"
+ output = dump_table_schema("mysql_table_options")
+ options = %r{create_table "mysql_table_options", force: :cascade, options: "(?<options>.*)"}.match(output)[:options]
+ assert_match %r{ENGINE=MyISAM}, options
+ end
+
+ test "table options with ROW_FORMAT" do
+ @connection.create_table "mysql_table_options", force: true, options: "ROW_FORMAT=REDUNDANT"
+ output = dump_table_schema("mysql_table_options")
+ options = %r{create_table "mysql_table_options", force: :cascade, options: "(?<options>.*)"}.match(output)[:options]
+ assert_match %r{ROW_FORMAT=REDUNDANT}, options
+ end
+
+ test "table options with CHARSET" do
+ @connection.create_table "mysql_table_options", force: true, options: "CHARSET=utf8mb4"
+ output = dump_table_schema("mysql_table_options")
+ options = %r{create_table "mysql_table_options", force: :cascade, options: "(?<options>.*)"}.match(output)[:options]
+ assert_match %r{CHARSET=utf8mb4}, options
+ end
+
+ test "table options with COLLATE" do
+ @connection.create_table "mysql_table_options", force: true, options: "COLLATE=utf8mb4_bin"
+ output = dump_table_schema("mysql_table_options")
+ options = %r{create_table "mysql_table_options", force: :cascade, options: "(?<options>.*)"}.match(output)[:options]
+ assert_match %r{COLLATE=utf8mb4_bin}, options
+ end
+end
diff --git a/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb b/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb
new file mode 100644
index 0000000000..a6f6dd21bb
--- /dev/null
+++ b/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb
@@ -0,0 +1,65 @@
+require "cases/helper"
+require "support/schema_dumping_helper"
+
+class Mysql2UnsignedTypeTest < ActiveRecord::Mysql2TestCase
+ include SchemaDumpingHelper
+ self.use_transactional_tests = false
+
+ class UnsignedType < ActiveRecord::Base
+ end
+
+ setup do
+ @connection = ActiveRecord::Base.connection
+ @connection.create_table("unsigned_types", force: true) do |t|
+ t.integer :unsigned_integer, unsigned: true
+ t.bigint :unsigned_bigint, unsigned: true
+ t.float :unsigned_float, unsigned: true
+ t.decimal :unsigned_decimal, unsigned: true, precision: 10, scale: 2
+ end
+ end
+
+ teardown do
+ @connection.drop_table "unsigned_types", if_exists: true
+ end
+
+ test "unsigned int max value is in range" do
+ assert expected = UnsignedType.create(unsigned_integer: 4294967295)
+ assert_equal expected, UnsignedType.find_by(unsigned_integer: 4294967295)
+ end
+
+ test "minus value is out of range" do
+ assert_raise(RangeError) do
+ UnsignedType.create(unsigned_integer: -10)
+ end
+ assert_raise(RangeError) do
+ UnsignedType.create(unsigned_bigint: -10)
+ end
+ assert_raise(ActiveRecord::StatementInvalid) do
+ UnsignedType.create(unsigned_float: -10.0)
+ end
+ assert_raise(ActiveRecord::StatementInvalid) do
+ UnsignedType.create(unsigned_decimal: -10.0)
+ end
+ end
+
+ test "schema definition can use unsigned as the type" do
+ @connection.change_table("unsigned_types") do |t|
+ t.unsigned_integer :unsigned_integer_t
+ t.unsigned_bigint :unsigned_bigint_t
+ t.unsigned_float :unsigned_float_t
+ t.unsigned_decimal :unsigned_decimal_t, precision: 10, scale: 2
+ end
+
+ @connection.columns("unsigned_types").select { |c| /^unsigned_/ === c.name }.each do |column|
+ assert column.unsigned?
+ end
+ end
+
+ test "schema dump includes unsigned option" do
+ schema = dump_table_schema "unsigned_types"
+ assert_match %r{t.integer\s+"unsigned_integer",\s+limit: 4,\s+unsigned: true$}, schema
+ assert_match %r{t.integer\s+"unsigned_bigint",\s+limit: 8,\s+unsigned: true$}, schema
+ assert_match %r{t.float\s+"unsigned_float",\s+limit: 24,\s+unsigned: true$}, schema
+ assert_match %r{t.decimal\s+"unsigned_decimal",\s+precision: 10,\s+scale: 2,\s+unsigned: true$}, schema
+ end
+end
diff --git a/activerecord/test/cases/adapters/postgresql/active_schema_test.rb b/activerecord/test/cases/adapters/postgresql/active_schema_test.rb
index 3808db5141..24def31e36 100644
--- a/activerecord/test/cases/adapters/postgresql/active_schema_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/active_schema_test.rb
@@ -1,6 +1,6 @@
require 'cases/helper'
-class PostgresqlActiveSchemaTest < ActiveRecord::TestCase
+class PostgresqlActiveSchemaTest < ActiveRecord::PostgreSQLTestCase
def setup
ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.class_eval do
def execute(sql, name = nil) sql end
@@ -25,7 +25,7 @@ class PostgresqlActiveSchemaTest < ActiveRecord::TestCase
def test_add_index
# add_index calls index_name_exists? which can't work since execute is stubbed
- ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.stubs(:index_name_exists?).returns(false)
+ ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.send(:define_method, :index_name_exists?) { |*| false }
expected = %(CREATE UNIQUE INDEX "index_people_on_last_name" ON "people" ("last_name") WHERE state = 'active')
assert_equal expected, add_index(:people, :last_name, :unique => true, :where => "state = 'active'")
@@ -49,6 +49,22 @@ class PostgresqlActiveSchemaTest < ActiveRecord::TestCase
expected = %(CREATE UNIQUE INDEX "index_people_on_last_name" ON "people" USING gist ("last_name") WHERE state = 'active')
assert_equal expected, add_index(:people, :last_name, :unique => true, :where => "state = 'active'", :using => :gist)
+
+ ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.send :remove_method, :index_name_exists?
+ end
+
+ def test_remove_index
+ # remove_index calls index_name_exists? which can't work since execute is stubbed
+ ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.send(:define_method, :index_name_exists?) { |*| true }
+
+ expected = %(DROP INDEX CONCURRENTLY "index_people_on_last_name")
+ assert_equal expected, remove_index(:people, name: "index_people_on_last_name", algorithm: :concurrently)
+
+ assert_raise ArgumentError do
+ add_index(:people, :last_name, algorithm: :copy)
+ end
+
+ ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.send :remove_method, :index_name_exists?
end
private
diff --git a/activerecord/test/cases/adapters/postgresql/array_test.rb b/activerecord/test/cases/adapters/postgresql/array_test.rb
index 8df1b7d18c..380a90d765 100644
--- a/activerecord/test/cases/adapters/postgresql/array_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/array_test.rb
@@ -1,9 +1,9 @@
-# encoding: utf-8
require "cases/helper"
+require 'support/schema_dumping_helper'
-class PostgresqlArrayTest < ActiveRecord::TestCase
+class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase
+ include SchemaDumpingHelper
include InTimeZone
- OID = ActiveRecord::ConnectionAdapters::PostgreSQL::OID
class PgArray < ActiveRecord::Base
self.table_name = 'pg_arrays'
@@ -12,12 +12,7 @@ class PostgresqlArrayTest < ActiveRecord::TestCase
def setup
@connection = ActiveRecord::Base.connection
- unless @connection.extension_enabled?('hstore')
- @connection.enable_extension 'hstore'
- @connection.commit_db_transaction
- end
-
- @connection.reconnect!
+ enable_extension!('hstore', @connection)
@connection.transaction do
@connection.create_table('pg_arrays') do |t|
@@ -27,24 +22,25 @@ class PostgresqlArrayTest < ActiveRecord::TestCase
t.hstore :hstores, array: true
end
end
+ PgArray.reset_column_information
@column = PgArray.columns_hash['tags']
+ @type = PgArray.type_for_attribute("tags")
end
teardown do
- @connection.execute 'drop table if exists pg_arrays'
+ @connection.drop_table 'pg_arrays', if_exists: true
+ disable_extension!('hstore', @connection)
end
def test_column
assert_equal :string, @column.type
assert_equal "character varying", @column.sql_type
- assert @column.array
- assert_not @column.number?
- assert_not @column.binary?
+ assert @column.array?
+ assert_not @type.binary?
ratings_column = PgArray.columns_hash['ratings']
assert_equal :integer, ratings_column.type
- assert ratings_column.array
- assert_not ratings_column.number?
+ assert ratings_column.array?
end
def test_default
@@ -76,7 +72,7 @@ class PostgresqlArrayTest < ActiveRecord::TestCase
assert_equal :text, column.type
assert_equal [], PgArray.column_defaults['snippets']
- assert column.array
+ assert column.array?
end
def test_change_column_cant_make_non_array_column_to_array
@@ -96,9 +92,9 @@ class PostgresqlArrayTest < ActiveRecord::TestCase
end
def test_type_cast_array
- assert_equal(['1', '2', '3'], @column.type_cast_from_database('{1,2,3}'))
- assert_equal([], @column.type_cast_from_database('{}'))
- assert_equal([nil], @column.type_cast_from_database('{NULL}'))
+ assert_equal(['1', '2', '3'], @type.deserialize('{1,2,3}'))
+ assert_equal([], @type.deserialize('{}'))
+ assert_equal([nil], @type.deserialize('{NULL}'))
end
def test_type_cast_integers
@@ -112,6 +108,12 @@ class PostgresqlArrayTest < ActiveRecord::TestCase
assert_equal([1, 2], x.ratings)
end
+ def test_schema_dump_with_shorthand
+ output = dump_table_schema "pg_arrays"
+ assert_match %r[t\.string\s+"tags",\s+array: true], output
+ assert_match %r[t\.integer\s+"ratings",\s+array: true], output
+ end
+
def test_select_with_strings
@connection.execute "insert into pg_arrays (tags) VALUES ('{1,2,3}')"
x = PgArray.first
@@ -204,16 +206,17 @@ class PostgresqlArrayTest < ActiveRecord::TestCase
x = PgArray.create!(tags: tags)
x.reload
- assert_equal x.tags_before_type_cast, PgArray.columns_hash['tags'].type_cast_for_database(tags)
+ assert_equal x.tags_before_type_cast, PgArray.type_for_attribute('tags').serialize(tags)
end
def test_quoting_non_standard_delimiters
strings = ["hello,", "world;"]
- comma_delim = OID::Array.new(ActiveRecord::Type::String.new, ',')
- semicolon_delim = OID::Array.new(ActiveRecord::Type::String.new, ';')
+ oid = ActiveRecord::ConnectionAdapters::PostgreSQL::OID
+ comma_delim = oid::Array.new(ActiveRecord::Type::String.new, ',')
+ semicolon_delim = oid::Array.new(ActiveRecord::Type::String.new, ';')
- assert_equal %({"hello,",world;}), comma_delim.type_cast_for_database(strings)
- assert_equal %({hello,;"world;"}), semicolon_delim.type_cast_for_database(strings)
+ assert_equal %({"hello,",world;}), comma_delim.serialize(strings)
+ assert_equal %({hello,;"world;"}), semicolon_delim.serialize(strings)
end
def test_mutate_array
@@ -258,6 +261,45 @@ class PostgresqlArrayTest < ActiveRecord::TestCase
end
end
+ def test_assigning_non_array_value
+ record = PgArray.new(tags: "not-an-array")
+ assert_equal [], record.tags
+ assert_equal "not-an-array", record.tags_before_type_cast
+ assert record.save
+ assert_equal record.tags, record.reload.tags
+ end
+
+ def test_assigning_empty_string
+ record = PgArray.new(tags: "")
+ assert_equal [], record.tags
+ assert_equal "", record.tags_before_type_cast
+ assert record.save
+ assert_equal record.tags, record.reload.tags
+ end
+
+ def test_assigning_valid_pg_array_literal
+ record = PgArray.new(tags: "{1,2,3}")
+ assert_equal ["1", "2", "3"], record.tags
+ assert_equal "{1,2,3}", record.tags_before_type_cast
+ assert record.save
+ assert_equal record.tags, record.reload.tags
+ end
+
+ def test_uniqueness_validation
+ klass = Class.new(PgArray) do
+ validates_uniqueness_of :tags
+
+ def self.model_name; ActiveModel::Name.new(PgArray) end
+ end
+ e1 = klass.create("tags" => ["black", "blue"])
+ assert e1.persisted?, "Saving e1"
+
+ e2 = klass.create("tags" => ["black", "blue"])
+ assert !e2.persisted?, "e2 shouldn't be valid"
+ assert e2.errors[:tags].any?, "Should have errors for tags"
+ assert_equal ["has already been taken"], e2.errors[:tags], "Should have uniqueness message for tags"
+ end
+
private
def assert_cycle field, array
# test creation
diff --git a/activerecord/test/cases/adapters/postgresql/bit_string_test.rb b/activerecord/test/cases/adapters/postgresql/bit_string_test.rb
index 72222c01fd..6f72fa6e0f 100644
--- a/activerecord/test/cases/adapters/postgresql/bit_string_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/bit_string_test.rb
@@ -1,9 +1,8 @@
-# -*- coding: utf-8 -*-
require "cases/helper"
require 'support/connection_helper'
require 'support/schema_dumping_helper'
-class PostgresqlBitStringTest < ActiveRecord::TestCase
+class PostgresqlBitStringTest < ActiveRecord::PostgreSQLTestCase
include ConnectionHelper
include SchemaDumpingHelper
@@ -14,30 +13,34 @@ class PostgresqlBitStringTest < ActiveRecord::TestCase
@connection.create_table('postgresql_bit_strings', :force => true) do |t|
t.bit :a_bit, default: "00000011", limit: 8
t.bit_varying :a_bit_varying, default: "0011", limit: 4
+ t.bit :another_bit
+ t.bit_varying :another_bit_varying
end
end
def teardown
return unless @connection
- @connection.execute 'DROP TABLE IF EXISTS postgresql_bit_strings'
+ @connection.drop_table 'postgresql_bit_strings', if_exists: true
end
def test_bit_string_column
column = PostgresqlBitString.columns_hash["a_bit"]
assert_equal :bit, column.type
assert_equal "bit(8)", column.sql_type
- assert_not column.number?
- assert_not column.binary?
- assert_not column.array
+ assert_not column.array?
+
+ type = PostgresqlBitString.type_for_attribute("a_bit")
+ assert_not 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.number?
- assert_not column.binary?
- assert_not column.array
+ assert_not column.array?
+
+ type = PostgresqlBitString.type_for_attribute("a_bit_varying")
+ assert_not 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 7872f91943..b6bb1929e6 100644
--- a/activerecord/test/cases/adapters/postgresql/bytea_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/bytea_test.rb
@@ -1,7 +1,6 @@
-# encoding: utf-8
require "cases/helper"
-class PostgresqlByteaTest < ActiveRecord::TestCase
+class PostgresqlByteaTest < ActiveRecord::PostgreSQLTestCase
class ByteaDataType < ActiveRecord::Base
self.table_name = 'bytea_data_type'
end
@@ -17,32 +16,40 @@ class PostgresqlByteaTest < ActiveRecord::TestCase
end
end
@column = ByteaDataType.columns_hash['payload']
- assert(@column.is_a?(ActiveRecord::ConnectionAdapters::PostgreSQLColumn))
+ @type = ByteaDataType.type_for_attribute("payload")
end
teardown do
- @connection.execute 'drop table if exists bytea_data_type'
+ @connection.drop_table 'bytea_data_type', if_exists: true
end
def test_column
+ assert @column.is_a?(ActiveRecord::ConnectionAdapters::PostgreSQLColumn)
assert_equal :binary, @column.type
end
+ def test_binary_columns_are_limitless_the_upper_limit_is_one_GB
+ assert_equal 'bytea', @connection.type_to_sql(:binary, 100_000)
+ assert_raise ActiveRecord::ActiveRecordError do
+ @connection.type_to_sql :binary, 4294967295
+ end
+ end
+
def test_type_cast_binary_converts_the_encoding
assert @column
data = "\u001F\x8B"
assert_equal('UTF-8', data.encoding.name)
- assert_equal('ASCII-8BIT', @column.type_cast_from_database(data).encoding.name)
+ assert_equal('ASCII-8BIT', @type.deserialize(data).encoding.name)
end
def test_type_cast_binary_value
data = "\u001F\x8B".force_encoding("BINARY")
- assert_equal(data, @column.type_cast_from_database(data))
+ assert_equal(data, @type.deserialize(data))
end
def test_type_case_nil
- assert_equal(nil, @column.type_cast_from_database(nil))
+ assert_equal(nil, @type.deserialize(nil))
end
def test_read_value
diff --git a/activerecord/test/cases/adapters/postgresql/change_schema_test.rb b/activerecord/test/cases/adapters/postgresql/change_schema_test.rb
new file mode 100644
index 0000000000..bc12df668d
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/change_schema_test.rb
@@ -0,0 +1,38 @@
+require 'cases/helper'
+
+module ActiveRecord
+ class Migration
+ class PGChangeSchemaTest < ActiveRecord::PostgreSQLTestCase
+ attr_reader :connection
+
+ def setup
+ super
+ @connection = ActiveRecord::Base.connection
+ connection.create_table(:strings) do |t|
+ t.string :somedate
+ end
+ end
+
+ def teardown
+ connection.drop_table :strings
+ end
+
+ def test_change_string_to_date
+ connection.change_column :strings, :somedate, :timestamp, using: 'CAST("somedate" AS timestamp)'
+ assert_equal :datetime, connection.columns(:strings).find { |c| c.name == 'somedate' }.type
+ end
+
+ def test_change_type_with_symbol
+ connection.change_column :strings, :somedate, :timestamp, cast_as: :timestamp
+ assert_equal :datetime, connection.columns(:strings).find { |c| c.name == 'somedate' }.type
+ end
+
+ def test_change_type_with_array
+ 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?
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/adapters/postgresql/cidr_test.rb b/activerecord/test/cases/adapters/postgresql/cidr_test.rb
new file mode 100644
index 0000000000..52f2a0096c
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/cidr_test.rb
@@ -0,0 +1,25 @@
+require "cases/helper"
+require "ipaddr"
+
+module ActiveRecord
+ module ConnectionAdapters
+ class PostgreSQLAdapter < AbstractAdapter
+ class CidrTest < ActiveRecord::PostgreSQLTestCase
+ test "type casting IPAddr for database" do
+ type = OID::Cidr.new
+ ip = IPAddr.new("255.0.0.0/8")
+ ip2 = IPAddr.new("127.0.0.1")
+
+ assert_equal "255.0.0.0/8", type.serialize(ip)
+ assert_equal "127.0.0.1/32", type.serialize(ip2)
+ end
+
+ test "casting does nothing with non-IPAddr objects" do
+ type = OID::Cidr.new
+
+ assert_equal "foo", type.serialize("foo")
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/adapters/postgresql/citext_test.rb b/activerecord/test/cases/adapters/postgresql/citext_test.rb
index 2acb64f81c..bd62041e79 100644
--- a/activerecord/test/cases/adapters/postgresql/citext_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/citext_test.rb
@@ -1,8 +1,9 @@
-# encoding: utf-8
require 'cases/helper'
+require 'support/schema_dumping_helper'
if ActiveRecord::Base.connection.supports_extensions?
- class PostgresqlCitextTest < ActiveRecord::TestCase
+ class PostgresqlCitextTest < ActiveRecord::PostgreSQLTestCase
+ include SchemaDumpingHelper
class Citext < ActiveRecord::Base
self.table_name = 'citexts'
end
@@ -10,12 +11,7 @@ if ActiveRecord::Base.connection.supports_extensions?
def setup
@connection = ActiveRecord::Base.connection
- unless @connection.extension_enabled?('citext')
- @connection.enable_extension 'citext'
- @connection.commit_db_transaction
- end
-
- @connection.reconnect!
+ enable_extension!('citext', @connection)
@connection.create_table('citexts') do |t|
t.citext 'cival'
@@ -23,8 +19,8 @@ if ActiveRecord::Base.connection.supports_extensions?
end
teardown do
- @connection.execute 'DROP TABLE IF EXISTS citexts;'
- @connection.execute 'DROP EXTENSION IF EXISTS citext CASCADE;'
+ @connection.drop_table 'citexts', if_exists: true
+ disable_extension!('citext', @connection)
end
def test_citext_enabled
@@ -35,9 +31,10 @@ if ActiveRecord::Base.connection.supports_extensions?
column = Citext.columns_hash['cival']
assert_equal :citext, column.type
assert_equal 'citext', column.sql_type
- assert_not column.number?
- assert_not column.binary?
- assert_not column.array
+ assert_not column.array?
+
+ type = Citext.type_for_attribute('cival')
+ assert_not type.binary?
end
def test_change_table_supports_json
@@ -72,5 +69,10 @@ if ActiveRecord::Base.connection.supports_extensions?
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
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/collation_test.rb b/activerecord/test/cases/adapters/postgresql/collation_test.rb
new file mode 100644
index 0000000000..8470329c35
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/collation_test.rb
@@ -0,0 +1,53 @@
+require "cases/helper"
+require 'support/schema_dumping_helper'
+
+class PostgresqlCollationTest < ActiveRecord::PostgreSQLTestCase
+ include SchemaDumpingHelper
+
+ def setup
+ @connection = ActiveRecord::Base.connection
+ @connection.create_table :postgresql_collations, force: true do |t|
+ t.string :string_c, collation: 'C'
+ t.text :text_posix, collation: 'POSIX'
+ end
+ end
+
+ def teardown
+ @connection.drop_table :postgresql_collations, if_exists: true
+ end
+
+ test "string column with collation" do
+ column = @connection.columns(:postgresql_collations).find { |c| c.name == 'string_c' }
+ assert_equal :string, column.type
+ assert_equal 'C', column.collation
+ end
+
+ test "text column with collation" do
+ column = @connection.columns(:postgresql_collations).find { |c| c.name == 'text_posix' }
+ assert_equal :text, column.type
+ assert_equal 'POSIX', column.collation
+ end
+
+ test "add column with collation" do
+ @connection.add_column :postgresql_collations, :title, :string, collation: 'C'
+
+ column = @connection.columns(:postgresql_collations).find { |c| c.name == 'title' }
+ assert_equal :string, column.type
+ assert_equal 'C', column.collation
+ end
+
+ test "change column with collation" do
+ @connection.add_column :postgresql_collations, :description, :string
+ @connection.change_column :postgresql_collations, :description, :text, collation: 'POSIX'
+
+ column = @connection.columns(:postgresql_collations).find { |c| c.name == 'description' }
+ assert_equal :text, column.type
+ assert_equal 'POSIX', column.collation
+ end
+
+ test "schema dump includes collation" do
+ output = dump_table_schema("postgresql_collations")
+ assert_match %r{t.string\s+"string_c",\s+collation: "C"$}, output
+ assert_match %r{t.text\s+"text_posix",\s+collation: "POSIX"$}, output
+ end
+end
diff --git a/activerecord/test/cases/adapters/postgresql/composite_test.rb b/activerecord/test/cases/adapters/postgresql/composite_test.rb
index cfab5ca902..1de87e5f01 100644
--- a/activerecord/test/cases/adapters/postgresql/composite_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/composite_test.rb
@@ -1,4 +1,3 @@
-# -*- coding: utf-8 -*-
require "cases/helper"
require 'support/connection_helper'
@@ -30,7 +29,7 @@ module PostgresqlCompositeBehavior
def teardown
super
- @connection.execute 'DROP TABLE IF EXISTS postgresql_composites'
+ @connection.drop_table 'postgresql_composites', if_exists: true
@connection.execute 'DROP TYPE IF EXISTS full_address'
reset_connection
PostgresqlComposite.reset_column_information
@@ -41,7 +40,7 @@ end
# "unknown OID 5653508: failed to recognize type of 'address'. It will be treated as String."
# To take full advantage of composite types, we suggest you register your own +OID::Type+.
# See PostgresqlCompositeWithCustomOIDTest
-class PostgresqlCompositeTest < ActiveRecord::TestCase
+class PostgresqlCompositeTest < ActiveRecord::PostgreSQLTestCase
include PostgresqlCompositeBehavior
def test_column
@@ -50,9 +49,10 @@ class PostgresqlCompositeTest < ActiveRecord::TestCase
column = PostgresqlComposite.columns_hash["address"]
assert_nil column.type
assert_equal "full_address", column.sql_type
- assert_not column.number?
- assert_not column.binary?
- assert_not column.array
+ assert_not column.array?
+
+ type = PostgresqlComposite.type_for_attribute("address")
+ assert_not type.binary?
end
def test_composite_mapping
@@ -77,23 +77,23 @@ class PostgresqlCompositeTest < ActiveRecord::TestCase
end
end
-class PostgresqlCompositeWithCustomOIDTest < ActiveRecord::TestCase
+class PostgresqlCompositeWithCustomOIDTest < ActiveRecord::PostgreSQLTestCase
include PostgresqlCompositeBehavior
class FullAddressType < ActiveRecord::Type::Value
def type; :full_address end
- def type_cast_from_database(value)
+ def deserialize(value)
if value =~ /\("?([^",]*)"?,"?([^",]*)"?\)/
FullAddress.new($1, $2)
end
end
- def type_cast_from_user(value)
+ def cast(value)
value
end
- def type_cast_for_database(value)
+ def serialize(value)
return if value.nil?
"(#{value.city},#{value.street})"
end
@@ -111,9 +111,10 @@ class PostgresqlCompositeWithCustomOIDTest < ActiveRecord::TestCase
column = PostgresqlComposite.columns_hash["address"]
assert_equal :full_address, column.type
assert_equal "full_address", column.sql_type
- assert_not column.number?
- assert_not column.binary?
- assert_not column.array
+ assert_not column.array?
+
+ type = PostgresqlComposite.type_for_attribute("address")
+ assert_not 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 d26cda46fa..722e2377c1 100644
--- a/activerecord/test/cases/adapters/postgresql/connection_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/connection_test.rb
@@ -2,12 +2,14 @@ require "cases/helper"
require 'support/connection_helper'
module ActiveRecord
- class PostgresqlConnectionTest < ActiveRecord::TestCase
+ class PostgresqlConnectionTest < ActiveRecord::PostgreSQLTestCase
include ConnectionHelper
class NonExistentTable < ActiveRecord::Base
end
+ fixtures :comments
+
def setup
super
@subscriber = SQLSubscriber.new
@@ -20,6 +22,14 @@ module ActiveRecord
super
end
+ def test_truncate
+ count = ActiveRecord::Base.connection.execute("select count(*) from comments").first['count'].to_i
+ assert_operator count, :>, 0
+ ActiveRecord::Base.connection.truncate("comments")
+ count = ActiveRecord::Base.connection.execute("select count(*) from comments").first['count'].to_i
+ assert_equal 0, count
+ end
+
def test_encoding
assert_not_nil @connection.encoding
end
@@ -116,12 +126,12 @@ module ActiveRecord
end
def test_statement_key_is_logged
- bindval = 1
- @connection.exec_query('SELECT $1::integer', 'SQL', [[nil, bindval]])
+ bind = Relation::QueryAttribute.new(nil, 1, Type::Value.new)
+ @connection.exec_query('SELECT $1::integer', 'SQL', [bind], prepare: true)
name = @subscriber.payloads.last[:statement_name]
assert name
- res = @connection.exec_query("EXPLAIN (FORMAT JSON) EXECUTE #{name}(#{bindval})")
- plan = res.column_types['QUERY PLAN'].type_cast_from_database res.rows.first.first
+ res = @connection.exec_query("EXPLAIN (FORMAT JSON) EXECUTE #{name}(1)")
+ plan = res.column_types['QUERY PLAN'].deserialize res.rows.first.first
assert_operator plan.length, :>, 0
end
@@ -167,9 +177,7 @@ module ActiveRecord
"successfully querying with the same connection pid."
# Repair all fixture connections so other tests won't break.
- @fixture_connections.each do |c|
- c.verify!
- end
+ @fixture_connections.each(&:verify!)
end
def test_set_session_variable_true
diff --git a/activerecord/test/cases/adapters/postgresql/datatype_test.rb b/activerecord/test/cases/adapters/postgresql/datatype_test.rb
index a0a34e4b87..232c25cb3b 100644
--- a/activerecord/test/cases/adapters/postgresql/datatype_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/datatype_test.rb
@@ -2,9 +2,6 @@ require "cases/helper"
require 'support/ddl_helper'
-class PostgresqlNumber < ActiveRecord::Base
-end
-
class PostgresqlTime < ActiveRecord::Base
end
@@ -14,19 +11,12 @@ end
class PostgresqlLtree < ActiveRecord::Base
end
-class PostgresqlDataTypeTest < ActiveRecord::TestCase
- self.use_transactional_fixtures = false
+class PostgresqlDataTypeTest < ActiveRecord::PostgreSQLTestCase
+ self.use_transactional_tests = false
def setup
@connection = ActiveRecord::Base.connection
- @connection.execute("INSERT INTO postgresql_numbers (id, single, double) VALUES (1, 123.456, 123456.789)")
- @connection.execute("INSERT INTO postgresql_numbers (id, single, double) VALUES (2, '-Infinity', 'Infinity')")
- @connection.execute("INSERT INTO postgresql_numbers (id, single, double) VALUES (3, 123.456, 'NaN')")
- @first_number = PostgresqlNumber.find(1)
- @second_number = PostgresqlNumber.find(2)
- @third_number = PostgresqlNumber.find(3)
-
@connection.execute("INSERT INTO postgresql_times (id, time_interval, scaled_time_interval) VALUES (1, '1 year 2 days ago', '3 weeks ago')")
@first_time = PostgresqlTime.find(1)
@@ -35,12 +25,7 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase
end
teardown do
- [PostgresqlNumber, PostgresqlTime, PostgresqlOid].each(&:delete_all)
- end
-
- def test_data_type_of_number_types
- assert_equal :float, @first_number.column_for_attribute(:single).type
- assert_equal :float, @first_number.column_for_attribute(:double).type
+ [PostgresqlTime, PostgresqlOid].each(&:delete_all)
end
def test_data_type_of_time_types
@@ -52,14 +37,6 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase
assert_equal :integer, @first_oid.column_for_attribute(:obj_id).type
end
- def test_number_values
- assert_equal 123.456, @first_number.single
- assert_equal 123456.789, @first_number.double
- assert_equal(-::Float::INFINITY, @second_number.single)
- assert_equal ::Float::INFINITY, @second_number.double
- assert_same ::Float::NAN, @third_number.double
- end
-
def test_time_values
assert_equal '-1 years -2 days', @first_time.time_interval
assert_equal '-21 days', @first_time.scaled_time_interval
@@ -69,17 +46,6 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase
assert_equal 1234, @first_oid.obj_id
end
- def test_update_number
- new_single = 789.012
- new_double = 789012.345
- @first_number.single = new_single
- @first_number.double = new_double
- assert @first_number.save
- assert @first_number.reload
- assert_equal new_single, @first_number.single
- assert_equal new_double, @first_number.double
- end
-
def test_update_time
@first_time.time_interval = '2 years 3 minutes'
assert @first_time.save
@@ -94,9 +60,16 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase
assert @first_oid.reload
assert_equal new_value, @first_oid.obj_id
end
+
+ def test_text_columns_are_limitless_the_upper_limit_is_one_GB
+ assert_equal 'text', @connection.type_to_sql(:text, 100_000)
+ assert_raise ActiveRecord::ActiveRecordError do
+ @connection.type_to_sql :text, 4294967295
+ end
+ end
end
-class PostgresqlInternalDataTypeTest < ActiveRecord::TestCase
+class PostgresqlInternalDataTypeTest < ActiveRecord::PostgreSQLTestCase
include DdlHelper
setup do
diff --git a/activerecord/test/cases/adapters/postgresql/domain_test.rb b/activerecord/test/cases/adapters/postgresql/domain_test.rb
index 1500adb42d..6102ddacd1 100644
--- a/activerecord/test/cases/adapters/postgresql/domain_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/domain_test.rb
@@ -1,8 +1,7 @@
-# -*- coding: utf-8 -*-
require "cases/helper"
require 'support/connection_helper'
-class PostgresqlDomainTest < ActiveRecord::TestCase
+class PostgresqlDomainTest < ActiveRecord::PostgreSQLTestCase
include ConnectionHelper
class PostgresqlDomain < ActiveRecord::Base
@@ -20,7 +19,7 @@ class PostgresqlDomainTest < ActiveRecord::TestCase
end
teardown do
- @connection.execute 'DROP TABLE IF EXISTS postgresql_domains'
+ @connection.drop_table 'postgresql_domains', if_exists: true
@connection.execute 'DROP DOMAIN IF EXISTS custom_money'
reset_connection
end
@@ -29,9 +28,10 @@ class PostgresqlDomainTest < ActiveRecord::TestCase
column = PostgresqlDomain.columns_hash["price"]
assert_equal :decimal, column.type
assert_equal "custom_money", column.sql_type
- assert column.number?
- assert_not column.binary?
- assert_not column.array
+ assert_not column.array?
+
+ type = PostgresqlDomain.type_for_attribute("price")
+ assert_not type.binary?
end
def test_domain_acts_like_basetype
diff --git a/activerecord/test/cases/adapters/postgresql/enum_test.rb b/activerecord/test/cases/adapters/postgresql/enum_test.rb
index d99c4a292e..6816a6514b 100644
--- a/activerecord/test/cases/adapters/postgresql/enum_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/enum_test.rb
@@ -1,10 +1,7 @@
-# -*- coding: utf-8 -*-
require "cases/helper"
require 'support/connection_helper'
-require 'active_record/base'
-require 'active_record/connection_adapters/postgresql_adapter'
-class PostgresqlEnumTest < ActiveRecord::TestCase
+class PostgresqlEnumTest < ActiveRecord::PostgreSQLTestCase
include ConnectionHelper
class PostgresqlEnum < ActiveRecord::Base
@@ -24,7 +21,7 @@ class PostgresqlEnumTest < ActiveRecord::TestCase
end
teardown do
- @connection.execute 'DROP TABLE IF EXISTS postgresql_enums'
+ @connection.drop_table 'postgresql_enums', if_exists: true
@connection.execute 'DROP TYPE IF EXISTS mood'
reset_connection
end
@@ -33,9 +30,10 @@ class PostgresqlEnumTest < ActiveRecord::TestCase
column = PostgresqlEnum.columns_hash["current_mood"]
assert_equal :enum, column.type
assert_equal "mood", column.sql_type
- assert_not column.number?
- assert_not column.binary?
- assert_not column.array
+ assert_not column.array?
+
+ type = PostgresqlEnum.type_for_attribute("current_mood")
+ assert_not type.binary?
end
def test_enum_defaults
@@ -82,4 +80,12 @@ class PostgresqlEnumTest < ActiveRecord::TestCase
assert_equal "happy", enum.current_mood
end
+
+ def test_assigning_enum_to_nil
+ model = PostgresqlEnum.new(current_mood: nil)
+
+ assert_nil model.current_mood
+ assert model.save
+ assert_nil model.reload.current_mood
+ end
end
diff --git a/activerecord/test/cases/adapters/postgresql/explain_test.rb b/activerecord/test/cases/adapters/postgresql/explain_test.rb
index 416f84cb38..4d0fd640aa 100644
--- a/activerecord/test/cases/adapters/postgresql/explain_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/explain_test.rb
@@ -1,28 +1,20 @@
require "cases/helper"
require 'models/developer'
+require 'models/computer'
-module ActiveRecord
- module ConnectionAdapters
- class PostgreSQLAdapter
- class ExplainTest < ActiveRecord::TestCase
- fixtures :developers
+class PostgreSQLExplainTest < ActiveRecord::PostgreSQLTestCase
+ fixtures :developers
- def test_explain_for_one_query
- explain = Developer.where(:id => 1).explain
- assert_match %(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = $1), explain
- assert_match %(QUERY PLAN), explain
- assert_match %(Index Scan using developers_pkey on developers), explain
- end
+ def test_explain_for_one_query
+ explain = Developer.where(:id => 1).explain
+ assert_match %(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = $1), explain
+ assert_match %(QUERY PLAN), explain
+ end
- def test_explain_with_eager_loading
- explain = Developer.where(:id => 1).includes(:audit_logs).explain
- assert_match %(QUERY PLAN), explain
- assert_match %(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = $1), explain
- assert_match %(Index Scan using developers_pkey on developers), explain
- assert_match %(EXPLAIN for: SELECT "audit_logs".* FROM "audit_logs" WHERE "audit_logs"."developer_id" IN (1)), explain
- assert_match %(Seq Scan on audit_logs), explain
- end
- end
- end
+ def test_explain_with_eager_loading
+ explain = Developer.where(:id => 1).includes(:audit_logs).explain
+ assert_match %(QUERY PLAN), explain
+ assert_match %(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = $1), explain
+ assert_match %(EXPLAIN for: SELECT "audit_logs".* FROM "audit_logs" WHERE "audit_logs"."developer_id" = 1), explain
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/extension_migration_test.rb b/activerecord/test/cases/adapters/postgresql/extension_migration_test.rb
index 7b99fcdda0..9cfc133308 100644
--- a/activerecord/test/cases/adapters/postgresql/extension_migration_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/extension_migration_test.rb
@@ -1,7 +1,7 @@
require "cases/helper"
-class PostgresqlExtensionMigrationTest < ActiveRecord::TestCase
- self.use_transactional_fixtures = false
+class PostgresqlExtensionMigrationTest < ActiveRecord::PostgreSQLTestCase
+ self.use_transactional_tests = false
class EnableHstore < ActiveRecord::Migration
def change
diff --git a/activerecord/test/cases/adapters/postgresql/full_text_test.rb b/activerecord/test/cases/adapters/postgresql/full_text_test.rb
index 9dadb177ca..bde7513339 100644
--- a/activerecord/test/cases/adapters/postgresql/full_text_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/full_text_test.rb
@@ -1,21 +1,34 @@
-# encoding: utf-8
require "cases/helper"
+require 'support/schema_dumping_helper'
-class PostgresqlFullTextTest < ActiveRecord::TestCase
- class PostgresqlTsvector < ActiveRecord::Base; end
+class PostgresqlFullTextTest < ActiveRecord::PostgreSQLTestCase
+ include SchemaDumpingHelper
+ class Tsvector < ActiveRecord::Base; end
+
+ setup do
+ @connection = ActiveRecord::Base.connection
+ @connection.create_table('tsvectors') do |t|
+ t.tsvector 'text_vector'
+ end
+ end
+
+ teardown do
+ @connection.drop_table 'tsvectors', if_exists: true
+ end
def test_tsvector_column
- column = PostgresqlTsvector.columns_hash["text_vector"]
+ column = Tsvector.columns_hash["text_vector"]
assert_equal :tsvector, column.type
assert_equal "tsvector", column.sql_type
- assert_not column.number?
- assert_not column.binary?
- assert_not column.array
+ assert_not column.array?
+
+ type = Tsvector.type_for_attribute("text_vector")
+ assert_not type.binary?
end
def test_update_tsvector
- PostgresqlTsvector.create text_vector: "'text' 'vector'"
- tsvector = PostgresqlTsvector.first
+ Tsvector.create text_vector: "'text' 'vector'"
+ tsvector = Tsvector.first
assert_equal "'text' 'vector'", tsvector.text_vector
tsvector.text_vector = "'new' 'text' 'vector'"
@@ -23,4 +36,9 @@ class PostgresqlFullTextTest < ActiveRecord::TestCase
assert tsvector.reload
assert_equal "'new' 'text' 'vector'", tsvector.text_vector
end
+
+ def test_schema_dump_with_shorthand
+ output = dump_table_schema("tsvectors")
+ assert_match %r{t\.tsvector "text_vector"}, output
+ end
end
diff --git a/activerecord/test/cases/adapters/postgresql/geometric_test.rb b/activerecord/test/cases/adapters/postgresql/geometric_test.rb
index 6c0adbbeaa..0baf985654 100644
--- a/activerecord/test/cases/adapters/postgresql/geometric_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/geometric_test.rb
@@ -1,44 +1,66 @@
-# -*- coding: utf-8 -*-
require "cases/helper"
require 'support/connection_helper'
require 'support/schema_dumping_helper'
-class PostgresqlPointTest < ActiveRecord::TestCase
+class PostgresqlPointTest < ActiveRecord::PostgreSQLTestCase
include ConnectionHelper
include SchemaDumpingHelper
- class PostgresqlPoint < ActiveRecord::Base; end
+ class PostgresqlPoint < ActiveRecord::Base
+ attribute :x, :rails_5_1_point
+ attribute :y, :rails_5_1_point
+ attribute :z, :rails_5_1_point
+ attribute :array_of_points, :rails_5_1_point, array: true
+ attribute :legacy_x, :legacy_point
+ attribute :legacy_y, :legacy_point
+ attribute :legacy_z, :legacy_point
+ end
def setup
@connection = ActiveRecord::Base.connection
- @connection.transaction do
- @connection.create_table('postgresql_points') do |t|
- t.point :x
- t.point :y, default: [12.2, 13.3]
- t.point :z, default: "(14.4,15.5)"
- end
+ @connection.create_table('postgresql_points') do |t|
+ t.point :x
+ t.point :y, default: [12.2, 13.3]
+ t.point :z, default: "(14.4,15.5)"
+ t.point :array_of_points, array: true
+ t.point :legacy_x
+ t.point :legacy_y, default: [12.2, 13.3]
+ t.point :legacy_z, default: "(14.4,15.5)"
+ end
+ @connection.create_table('deprecated_points') do |t|
+ t.point :x
end
end
teardown do
- @connection.execute 'DROP TABLE IF EXISTS postgresql_points'
+ @connection.drop_table 'postgresql_points', if_exists: true
+ @connection.drop_table 'deprecated_points', if_exists: true
+ end
+
+ class DeprecatedPoint < ActiveRecord::Base; end
+
+ def test_deprecated_legacy_type
+ assert_deprecated do
+ DeprecatedPoint.new
+ end
end
def test_column
column = PostgresqlPoint.columns_hash["x"]
assert_equal :point, column.type
assert_equal "point", column.sql_type
- assert_not column.number?
- assert_not column.binary?
- assert_not column.array
+ assert_not column.array?
+
+ type = PostgresqlPoint.type_for_attribute("x")
+ assert_not type.binary?
end
def test_default
- assert_equal [12.2, 13.3], PostgresqlPoint.column_defaults['y']
- assert_equal [12.2, 13.3], PostgresqlPoint.new.y
+ assert_equal ActiveRecord::Point.new(12.2, 13.3), PostgresqlPoint.column_defaults['y']
+ assert_equal ActiveRecord::Point.new(12.2, 13.3), PostgresqlPoint.new.y
- assert_equal [14.4, 15.5], PostgresqlPoint.column_defaults['z']
- assert_equal [14.4, 15.5], PostgresqlPoint.new.z
+ assert_equal ActiveRecord::Point.new(14.4, 15.5), PostgresqlPoint.column_defaults['z']
+ assert_equal ActiveRecord::Point.new(14.4, 15.5), PostgresqlPoint.new.z
end
def test_schema_dumping
@@ -51,22 +73,164 @@ class PostgresqlPointTest < ActiveRecord::TestCase
def test_roundtrip
PostgresqlPoint.create! x: [10, 25.2]
record = PostgresqlPoint.first
- assert_equal [10, 25.2], record.x
+ assert_equal ActiveRecord::Point.new(10, 25.2), record.x
- record.x = [1.1, 2.2]
+ record.x = ActiveRecord::Point.new(1.1, 2.2)
record.save!
assert record.reload
- assert_equal [1.1, 2.2], record.x
+ assert_equal ActiveRecord::Point.new(1.1, 2.2), record.x
end
def test_mutation
- p = PostgresqlPoint.create! x: [10, 20]
+ p = PostgresqlPoint.create! x: ActiveRecord::Point.new(10, 20)
+
+ p.x.y = 25
+ p.save!
+ p.reload
+
+ assert_equal ActiveRecord::Point.new(10.0, 25.0), p.x
+ assert_not p.changed?
+ end
+
+ def test_array_assignment
+ p = PostgresqlPoint.new(x: [1, 2])
+
+ assert_equal ActiveRecord::Point.new(1, 2), p.x
+ end
+
+ def test_string_assignment
+ p = PostgresqlPoint.new(x: "(1, 2)")
+
+ assert_equal ActiveRecord::Point.new(1, 2), p.x
+ end
+
+ def test_array_of_points_round_trip
+ expected_value = [
+ ActiveRecord::Point.new(1, 2),
+ ActiveRecord::Point.new(2, 3),
+ ActiveRecord::Point.new(3, 4),
+ ]
+ p = PostgresqlPoint.new(array_of_points: expected_value)
+
+ assert_equal expected_value, p.array_of_points
+ p.save!
+ p.reload
+ assert_equal expected_value, p.array_of_points
+ end
+
+ def test_legacy_column
+ column = PostgresqlPoint.columns_hash["legacy_x"]
+ assert_equal :point, column.type
+ assert_equal "point", column.sql_type
+ assert_not column.array?
+
+ type = PostgresqlPoint.type_for_attribute("legacy_x")
+ assert_not type.binary?
+ end
+
+ def test_legacy_default
+ assert_equal [12.2, 13.3], PostgresqlPoint.column_defaults['legacy_y']
+ assert_equal [12.2, 13.3], PostgresqlPoint.new.legacy_y
+
+ assert_equal [14.4, 15.5], PostgresqlPoint.column_defaults['legacy_z']
+ assert_equal [14.4, 15.5], PostgresqlPoint.new.legacy_z
+ end
- p.x[1] = 25
+ def test_legacy_schema_dumping
+ output = dump_table_schema("postgresql_points")
+ assert_match %r{t\.point\s+"legacy_x"$}, output
+ assert_match %r{t\.point\s+"legacy_y",\s+default: \[12\.2, 13\.3\]$}, output
+ assert_match %r{t\.point\s+"legacy_z",\s+default: \[14\.4, 15\.5\]$}, output
+ end
+
+ def test_legacy_roundtrip
+ PostgresqlPoint.create! legacy_x: [10, 25.2]
+ record = PostgresqlPoint.first
+ assert_equal [10, 25.2], record.legacy_x
+
+ record.legacy_x = [1.1, 2.2]
+ record.save!
+ assert record.reload
+ assert_equal [1.1, 2.2], record.legacy_x
+ end
+
+ def test_legacy_mutation
+ p = PostgresqlPoint.create! legacy_x: [10, 20]
+
+ p.legacy_x[1] = 25
p.save!
p.reload
- assert_equal [10.0, 25.0], p.x
+ assert_equal [10.0, 25.0], p.legacy_x
assert_not p.changed?
end
end
+
+class PostgresqlGeometricTest < ActiveRecord::PostgreSQLTestCase
+ class PostgresqlGeometric < ActiveRecord::Base; end
+
+ setup do
+ @connection = ActiveRecord::Base.connection
+ @connection.create_table("postgresql_geometrics") do |t|
+ t.column :a_line_segment, :lseg
+ t.column :a_box, :box
+ t.column :a_path, :path
+ t.column :a_polygon, :polygon
+ t.column :a_circle, :circle
+ end
+ end
+
+ teardown do
+ @connection.drop_table 'postgresql_geometrics', if_exists: true
+ end
+
+ def test_geometric_types
+ g = PostgresqlGeometric.new(
+ :a_line_segment => '(2.0, 3), (5.5, 7.0)',
+ :a_box => '2.0, 3, 5.5, 7.0',
+ :a_path => '[(2.0, 3), (5.5, 7.0), (8.5, 11.0)]',
+ :a_polygon => '((2.0, 3), (5.5, 7.0), (8.5, 11.0))',
+ :a_circle => '<(5.3, 10.4), 2>'
+ )
+
+ g.save!
+
+ h = PostgresqlGeometric.find(g.id)
+
+ assert_equal '[(2,3),(5.5,7)]', h.a_line_segment
+ assert_equal '(5.5,7),(2,3)', h.a_box # reordered to store upper right corner then bottom left corner
+ assert_equal '[(2,3),(5.5,7),(8.5,11)]', h.a_path
+ assert_equal '((2,3),(5.5,7),(8.5,11))', h.a_polygon
+ assert_equal '<(5.3,10.4),2>', h.a_circle
+ end
+
+ def test_alternative_format
+ g = PostgresqlGeometric.new(
+ :a_line_segment => '((2.0, 3), (5.5, 7.0))',
+ :a_box => '(2.0, 3), (5.5, 7.0)',
+ :a_path => '((2.0, 3), (5.5, 7.0), (8.5, 11.0))',
+ :a_polygon => '2.0, 3, 5.5, 7.0, 8.5, 11.0',
+ :a_circle => '((5.3, 10.4), 2)'
+ )
+
+ g.save!
+
+ h = PostgresqlGeometric.find(g.id)
+ assert_equal '[(2,3),(5.5,7)]', h.a_line_segment
+ assert_equal '(5.5,7),(2,3)', h.a_box # reordered to store upper right corner then bottom left corner
+ assert_equal '((2,3),(5.5,7),(8.5,11))', h.a_path
+ assert_equal '((2,3),(5.5,7),(8.5,11))', h.a_polygon
+ assert_equal '<(5.3,10.4),2>', h.a_circle
+ end
+
+ def test_geometric_function
+ PostgresqlGeometric.create! a_path: '[(2.0, 3), (5.5, 7.0), (8.5, 11.0)]' # [ ] is an open path
+ PostgresqlGeometric.create! a_path: '((2.0, 3), (5.5, 7.0), (8.5, 11.0))' # ( ) is a closed path
+
+ objs = PostgresqlGeometric.find_by_sql "SELECT isopen(a_path) FROM postgresql_geometrics ORDER BY id ASC"
+ assert_equal [true, false], objs.map(&:isopen)
+
+ objs = PostgresqlGeometric.find_by_sql "SELECT isclosed(a_path) FROM postgresql_geometrics ORDER BY id ASC"
+ assert_equal [false, true], objs.map(&:isclosed)
+ end
+end
diff --git a/activerecord/test/cases/adapters/postgresql/hstore_test.rb b/activerecord/test/cases/adapters/postgresql/hstore_test.rb
index 1296eb72c0..6a2d501646 100644
--- a/activerecord/test/cases/adapters/postgresql/hstore_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/hstore_test.rb
@@ -1,41 +1,41 @@
-# encoding: utf-8
-
require "cases/helper"
-require 'active_record/base'
-require 'active_record/connection_adapters/postgresql_adapter'
+require 'support/schema_dumping_helper'
-class PostgresqlHstoreTest < ActiveRecord::TestCase
- class Hstore < ActiveRecord::Base
- self.table_name = 'hstores'
+if ActiveRecord::Base.connection.supports_extensions?
+ 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
- def setup
- @connection = ActiveRecord::Base.connection
+ def setup
+ @connection = ActiveRecord::Base.connection
- unless @connection.extension_enabled?('hstore')
- @connection.enable_extension 'hstore'
- @connection.commit_db_transaction
- end
+ unless @connection.extension_enabled?('hstore')
+ @connection.enable_extension 'hstore'
+ @connection.commit_db_transaction
+ end
- @connection.reconnect!
+ @connection.reconnect!
- @connection.transaction do
- @connection.create_table('hstores') do |t|
- t.hstore 'tags', :default => ''
- t.hstore 'payload', array: true
- t.hstore 'settings'
+ @connection.transaction do
+ @connection.create_table('hstores') do |t|
+ t.hstore 'tags', :default => ''
+ t.hstore 'payload', array: true
+ t.hstore 'settings'
+ end
end
+ Hstore.reset_column_information
+ @column = Hstore.columns_hash['tags']
+ @type = Hstore.type_for_attribute("tags")
end
- @column = Hstore.columns_hash['tags']
- end
- teardown do
- @connection.execute 'drop table if exists hstores'
- end
+ teardown do
+ @connection.drop_table 'hstores', if_exists: true
+ end
- if ActiveRecord::Base.connection.supports_extensions?
def test_hstore_included_in_extensions
assert @connection.respond_to?(:extensions), "connection should have a list of extensions"
assert @connection.extensions.include?('hstore'), "extension list should include hstore"
@@ -55,9 +55,9 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase
def test_column
assert_equal :hstore, @column.type
assert_equal "hstore", @column.sql_type
- assert_not @column.number?
- assert_not @column.binary?
- assert_not @column.array
+ assert_not @column.array?
+
+ assert_not @type.binary?
end
def test_default
@@ -111,10 +111,10 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase
end
def test_type_cast_hstore
- assert_equal({'1' => '2'}, @column.type_cast_from_database("\"1\"=>\"2\""))
- assert_equal({}, @column.type_cast_from_database(""))
- assert_equal({'key'=>nil}, @column.type_cast_from_database('key => NULL'))
- assert_equal({'c'=>'}','"a"'=>'b "a b'}, @column.type_cast_from_database(%q(c=>"}", "\"a\""=>"b \"a b")))
+ 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_with_store_accessors
@@ -166,47 +166,47 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase
end
def test_gen1
- assert_equal(%q(" "=>""), @column.cast_type.type_cast_for_database({' '=>''}))
+ assert_equal(%q(" "=>""), @type.serialize({' '=>''}))
end
def test_gen2
- assert_equal(%q(","=>""), @column.cast_type.type_cast_for_database({','=>''}))
+ assert_equal(%q(","=>""), @type.serialize({','=>''}))
end
def test_gen3
- assert_equal(%q("="=>""), @column.cast_type.type_cast_for_database({'='=>''}))
+ assert_equal(%q("="=>""), @type.serialize({'='=>''}))
end
def test_gen4
- assert_equal(%q(">"=>""), @column.cast_type.type_cast_for_database({'>'=>''}))
+ assert_equal(%q(">"=>""), @type.serialize({'>'=>''}))
end
def test_parse1
- assert_equal({'a'=>nil,'b'=>nil,'c'=>'NuLl','null'=>'c'}, @column.type_cast_from_database('a=>null,b=>NuLl,c=>"NuLl",null=>c'))
+ assert_equal({'a'=>nil,'b'=>nil,'c'=>'NuLl','null'=>'c'}, @type.deserialize('a=>null,b=>NuLl,c=>"NuLl",null=>c'))
end
def test_parse2
- assert_equal({" " => " "}, @column.type_cast_from_database("\\ =>\\ "))
+ assert_equal({" " => " "}, @type.deserialize("\\ =>\\ "))
end
def test_parse3
- assert_equal({"=" => ">"}, @column.type_cast_from_database("==>>"))
+ assert_equal({"=" => ">"}, @type.deserialize("==>>"))
end
def test_parse4
- assert_equal({"=a"=>"q=w"}, @column.type_cast_from_database('\=a=>q=w'))
+ assert_equal({"=a"=>"q=w"}, @type.deserialize('\=a=>q=w'))
end
def test_parse5
- assert_equal({"=a"=>"q=w"}, @column.type_cast_from_database('"=a"=>q\=w'))
+ assert_equal({"=a"=>"q=w"}, @type.deserialize('"=a"=>q\=w'))
end
def test_parse6
- assert_equal({"\"a"=>"q>w"}, @column.type_cast_from_database('"\"a"=>q>w'))
+ assert_equal({"\"a"=>"q>w"}, @type.deserialize('"\"a"=>q>w'))
end
def test_parse7
- assert_equal({"\"a"=>"q\"w"}, @column.type_cast_from_database('\"a=>q"w'))
+ assert_equal({"\"a"=>"q\"w"}, @type.deserialize('\"a=>q"w'))
end
def test_rewrite
@@ -315,10 +315,13 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase
dupe = record.dup
assert_equal({"one" => "two"}, dupe.tags.to_hash)
end
- end
- private
+ def test_schema_dump_with_shorthand
+ output = dump_table_schema("hstores")
+ assert_match %r[t\.hstore "tags",\s+default: {}], output
+ end
+ private
def assert_array_cycle(array)
# test creation
x = Hstore.create!(payload: array)
@@ -346,4 +349,5 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase
x.reload
assert_equal(hash, x.tags)
end
+ end
end
diff --git a/activerecord/test/cases/adapters/postgresql/infinity_test.rb b/activerecord/test/cases/adapters/postgresql/infinity_test.rb
index 22e8873333..bfda933fa4 100644
--- a/activerecord/test/cases/adapters/postgresql/infinity_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/infinity_test.rb
@@ -1,6 +1,8 @@
require "cases/helper"
-class PostgresqlInfinityTest < ActiveRecord::TestCase
+class PostgresqlInfinityTest < ActiveRecord::PostgreSQLTestCase
+ include InTimeZone
+
class PostgresqlInfinity < ActiveRecord::Base
end
@@ -13,7 +15,7 @@ class PostgresqlInfinityTest < ActiveRecord::TestCase
end
teardown do
- @connection.execute("DROP TABLE IF EXISTS postgresql_infinities")
+ @connection.drop_table 'postgresql_infinities', if_exists: true
end
test "type casting infinity on a float column" do
@@ -22,6 +24,15 @@ class PostgresqlInfinityTest < ActiveRecord::TestCase
assert_equal Float::INFINITY, record.float
end
+ test "type casting string on a float column" do
+ record = PostgresqlInfinity.new(float: 'Infinity')
+ assert_equal Float::INFINITY, record.float
+ record = PostgresqlInfinity.new(float: '-Infinity')
+ assert_equal(-Float::INFINITY, record.float)
+ record = PostgresqlInfinity.new(float: 'NaN')
+ assert_send [record.float, :nan?]
+ end
+
test "update_all with infinity on a float column" do
record = PostgresqlInfinity.create!
PostgresqlInfinity.update_all(float: Float::INFINITY)
@@ -41,4 +52,18 @@ class PostgresqlInfinityTest < ActiveRecord::TestCase
record.reload
assert_equal Float::INFINITY, record.datetime
end
+
+ test "assigning 'infinity' on a datetime column with TZ aware attributes" do
+ begin
+ in_time_zone "Pacific Time (US & Canada)" do
+ record = PostgresqlInfinity.create!(datetime: "infinity")
+ assert_equal Float::INFINITY, record.datetime
+ assert_equal record.datetime, record.reload.datetime
+ end
+ ensure
+ # setting time_zone_aware_attributes causes the types to change.
+ # There is no way to do this automatically since it can be set on a superclass
+ PostgresqlInfinity.reset_column_information
+ end
+ end
end
diff --git a/activerecord/test/cases/adapters/postgresql/integer_test.rb b/activerecord/test/cases/adapters/postgresql/integer_test.rb
new file mode 100644
index 0000000000..b4e55964b9
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/integer_test.rb
@@ -0,0 +1,25 @@
+require "cases/helper"
+require "active_support/core_ext/numeric/bytes"
+
+class PostgresqlIntegerTest < ActiveRecord::PostgreSQLTestCase
+ class PgInteger < ActiveRecord::Base
+ end
+
+ def setup
+ @connection = ActiveRecord::Base.connection
+
+ @connection.transaction do
+ @connection.create_table "pg_integers", force: true do |t|
+ t.integer :quota, limit: 8, default: 2.gigabytes
+ end
+ end
+ end
+
+ teardown do
+ @connection.drop_table "pg_integers", if_exists: true
+ end
+
+ test "schema properly respects bigint ranges" do
+ assert_equal 2.gigabytes, PgInteger.new.quota
+ end
+end
diff --git a/activerecord/test/cases/adapters/postgresql/json_test.rb b/activerecord/test/cases/adapters/postgresql/json_test.rb
index 86ba849445..b3b121b4fb 100644
--- a/activerecord/test/cases/adapters/postgresql/json_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/json_test.rb
@@ -1,10 +1,9 @@
-# encoding: utf-8
-
require "cases/helper"
-require 'active_record/base'
-require 'active_record/connection_adapters/postgresql_adapter'
+require 'support/schema_dumping_helper'
module PostgresqlJSONSharedTestCases
+ include SchemaDumpingHelper
+
class JsonDataType < ActiveRecord::Base
self.table_name = 'json_data_type'
@@ -14,29 +13,28 @@ module PostgresqlJSONSharedTestCases
def setup
@connection = ActiveRecord::Base.connection
begin
- @connection.transaction do
- @connection.create_table('json_data_type') do |t|
- t.public_send column_type, 'payload', default: {} # t.json 'payload', default: {}
- t.public_send column_type, 'settings' # t.json 'settings'
- end
+ @connection.create_table('json_data_type') do |t|
+ t.public_send column_type, 'payload', default: {} # t.json 'payload', default: {}
+ t.public_send column_type, 'settings' # t.json 'settings'
end
rescue ActiveRecord::StatementInvalid
- skip "do not test on PG without json"
+ skip "do not test on PostgreSQL without #{column_type} type."
end
- @column = JsonDataType.columns_hash['payload']
end
def teardown
- @connection.execute 'drop table if exists json_data_type'
+ @connection.drop_table :json_data_type, if_exists: true
+ JsonDataType.reset_column_information
end
def test_column
column = JsonDataType.columns_hash["payload"]
assert_equal column_type, column.type
assert_equal column_type.to_s, column.sql_type
- assert_not column.number?
- assert_not column.binary?
- assert_not column.array
+ assert_not column.array?
+
+ type = JsonDataType.type_for_attribute("payload")
+ assert_not type.binary?
end
def test_default
@@ -64,6 +62,11 @@ module PostgresqlJSONSharedTestCases
JsonDataType.reset_column_information
end
+ def test_schema_dumping
+ output = dump_table_schema("json_data_type")
+ assert_match(/t\.#{column_type.to_s}\s+"payload",\s+default: {}/, output)
+ end
+
def test_cast_value_on_write
x = JsonDataType.new payload: {"string" => "foo", :symbol => :bar}
assert_equal({"string" => "foo", :symbol => :bar}, x.payload_before_type_cast)
@@ -73,16 +76,16 @@ module PostgresqlJSONSharedTestCases
end
def test_type_cast_json
- column = JsonDataType.columns_hash["payload"]
+ type = JsonDataType.type_for_attribute("payload")
data = "{\"a_key\":\"a_value\"}"
- hash = column.type_cast_from_database(data)
+ hash = type.deserialize(data)
assert_equal({'a_key' => 'a_value'}, hash)
- assert_equal({'a_key' => 'a_value'}, column.type_cast_from_database(data))
+ assert_equal({'a_key' => 'a_value'}, type.deserialize(data))
- assert_equal({}, column.type_cast_from_database("{}"))
- assert_equal({'key'=>nil}, column.type_cast_from_database('{"key": null}'))
- assert_equal({'c'=>'}','"a"'=>'b "a b'}, column.type_cast_from_database(%q({"c":"}", "\"a\"":"b \"a b"})))
+ assert_equal({}, type.deserialize("{}"))
+ assert_equal({'key'=>nil}, type.deserialize('{"key": null}'))
+ assert_equal({'c'=>'}','"a"'=>'b "a b'}, type.deserialize(%q({"c":"}", "\"a\"":"b \"a b"})))
end
def test_rewrite
@@ -174,9 +177,17 @@ module PostgresqlJSONSharedTestCases
assert_equal({ 'one' => 'two', 'three' => 'four' }, json.payload)
assert_not json.changed?
end
+
+ def test_assigning_invalid_json
+ json = JsonDataType.new
+
+ json.payload = 'foo'
+
+ assert_nil json.payload
+ end
end
-class PostgresqlJSONTest < ActiveRecord::TestCase
+class PostgresqlJSONTest < ActiveRecord::PostgreSQLTestCase
include PostgresqlJSONSharedTestCases
def column_type
@@ -184,7 +195,7 @@ class PostgresqlJSONTest < ActiveRecord::TestCase
end
end
-class PostgresqlJSONBTest < ActiveRecord::TestCase
+class PostgresqlJSONBTest < ActiveRecord::PostgreSQLTestCase
include PostgresqlJSONSharedTestCases
def column_type
diff --git a/activerecord/test/cases/adapters/postgresql/ltree_test.rb b/activerecord/test/cases/adapters/postgresql/ltree_test.rb
index 889e369bd6..56516c82b4 100644
--- a/activerecord/test/cases/adapters/postgresql/ltree_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/ltree_test.rb
@@ -1,7 +1,8 @@
-# encoding: utf-8
require "cases/helper"
+require 'support/schema_dumping_helper'
-class PostgresqlLtreeTest < ActiveRecord::TestCase
+class PostgresqlLtreeTest < ActiveRecord::PostgreSQLTestCase
+ include SchemaDumpingHelper
class Ltree < ActiveRecord::Base
self.table_name = 'ltrees'
end
@@ -9,9 +10,7 @@ class PostgresqlLtreeTest < ActiveRecord::TestCase
def setup
@connection = ActiveRecord::Base.connection
- unless @connection.extension_enabled?('ltree')
- @connection.enable_extension 'ltree'
- end
+ enable_extension!('ltree', @connection)
@connection.transaction do
@connection.create_table('ltrees') do |t|
@@ -23,16 +22,17 @@ class PostgresqlLtreeTest < ActiveRecord::TestCase
end
teardown do
- @connection.execute 'drop table if exists ltrees'
+ @connection.drop_table 'ltrees', if_exists: true
end
def test_column
column = Ltree.columns_hash['path']
assert_equal :ltree, column.type
assert_equal "ltree", column.sql_type
- assert_not column.number?
- assert_not column.binary?
- assert_not column.array
+ assert_not column.array?
+
+ type = Ltree.type_for_attribute('path')
+ assert_not type.binary?
end
def test_write
@@ -45,4 +45,9 @@ class PostgresqlLtreeTest < ActiveRecord::TestCase
ltree = Ltree.first
assert_equal '1.2.3', ltree.path
end
+
+ def test_schema_dump_with_shorthand
+ output = dump_table_schema("ltrees")
+ assert_match %r[t\.ltree "path"], output
+ end
end
diff --git a/activerecord/test/cases/adapters/postgresql/money_test.rb b/activerecord/test/cases/adapters/postgresql/money_test.rb
index 87183174f2..c031178479 100644
--- a/activerecord/test/cases/adapters/postgresql/money_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/money_test.rb
@@ -1,8 +1,7 @@
-# encoding: utf-8
require "cases/helper"
require 'support/schema_dumping_helper'
-class PostgresqlMoneyTest < ActiveRecord::TestCase
+class PostgresqlMoneyTest < ActiveRecord::PostgreSQLTestCase
include SchemaDumpingHelper
class PostgresqlMoney < ActiveRecord::Base; end
@@ -10,14 +9,14 @@ class PostgresqlMoneyTest < ActiveRecord::TestCase
setup do
@connection = ActiveRecord::Base.connection
@connection.execute("set lc_monetary = 'C'")
- @connection.create_table('postgresql_moneys') do |t|
- t.column "wealth", "money"
- t.column "depth", "money", default: "150.55"
+ @connection.create_table('postgresql_moneys', force: true) do |t|
+ t.money "wealth"
+ t.money "depth", default: "150.55"
end
end
teardown do
- @connection.execute 'DROP TABLE IF EXISTS postgresql_moneys'
+ @connection.drop_table 'postgresql_moneys', if_exists: true
end
def test_column
@@ -25,9 +24,10 @@ class PostgresqlMoneyTest < ActiveRecord::TestCase
assert_equal :money, column.type
assert_equal "money", column.sql_type
assert_equal 2, column.scale
- assert column.number?
- assert_not column.binary?
- assert_not column.array
+ assert_not column.array?
+
+ type = PostgresqlMoney.type_for_attribute("wealth")
+ assert_not type.binary?
end
def test_default
@@ -46,17 +46,17 @@ class PostgresqlMoneyTest < ActiveRecord::TestCase
end
def test_money_type_cast
- column = PostgresqlMoney.columns_hash['wealth']
- assert_equal(12345678.12, column.type_cast_from_user("$12,345,678.12"))
- assert_equal(12345678.12, column.type_cast_from_user("$12.345.678,12"))
- assert_equal(-1.15, column.type_cast_from_user("-$1.15"))
- assert_equal(-2.25, column.type_cast_from_user("($2.25)"))
+ type = PostgresqlMoney.type_for_attribute('wealth')
+ assert_equal(12345678.12, type.cast("$12,345,678.12"))
+ assert_equal(12345678.12, type.cast("$12.345.678,12"))
+ assert_equal(-1.15, type.cast("-$1.15"))
+ assert_equal(-2.25, type.cast("($2.25)"))
end
def test_schema_dumping
output = dump_table_schema("postgresql_moneys")
assert_match %r{t\.money\s+"wealth",\s+scale: 2$}, output
- assert_match %r{t\.money\s+"depth",\s+scale: 2,\s+default: 150.55$}, output
+ assert_match %r{t\.money\s+"depth",\s+scale: 2,\s+default: "150\.55"$}, output
end
def test_create_and_update_money
diff --git a/activerecord/test/cases/adapters/postgresql/network_test.rb b/activerecord/test/cases/adapters/postgresql/network_test.rb
index 4f4c1103fa..fe6ee4e2d9 100644
--- a/activerecord/test/cases/adapters/postgresql/network_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/network_test.rb
@@ -1,35 +1,51 @@
-# encoding: utf-8
require "cases/helper"
+require 'support/schema_dumping_helper'
-class PostgresqlNetworkTest < ActiveRecord::TestCase
- class PostgresqlNetworkAddress < ActiveRecord::Base
+class PostgresqlNetworkTest < ActiveRecord::PostgreSQLTestCase
+ include SchemaDumpingHelper
+ class PostgresqlNetworkAddress < ActiveRecord::Base; end
+
+ setup do
+ @connection = ActiveRecord::Base.connection
+ @connection.create_table('postgresql_network_addresses', force: true) do |t|
+ t.inet 'inet_address', default: "192.168.1.1"
+ t.cidr 'cidr_address', default: "192.168.1.0/24"
+ t.macaddr 'mac_address', default: "ff:ff:ff:ff:ff:ff"
+ end
+ end
+
+ teardown do
+ @connection.drop_table 'postgresql_network_addresses', if_exists: true
end
def test_cidr_column
column = PostgresqlNetworkAddress.columns_hash["cidr_address"]
assert_equal :cidr, column.type
assert_equal "cidr", column.sql_type
- assert_not column.number?
- assert_not column.binary?
- assert_not column.array
+ assert_not column.array?
+
+ type = PostgresqlNetworkAddress.type_for_attribute("cidr_address")
+ assert_not 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.number?
- assert_not column.binary?
- assert_not column.array
+ assert_not column.array?
+
+ type = PostgresqlNetworkAddress.type_for_attribute("inet_address")
+ assert_not 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.number?
- assert_not column.binary?
- assert_not column.array
+ assert_not column.array?
+
+ type = PostgresqlNetworkAddress.type_for_attribute("mac_address")
+ assert_not type.binary?
end
def test_network_types
@@ -68,4 +84,11 @@ class PostgresqlNetworkTest < ActiveRecord::TestCase
assert_nil invalid_address.cidr_address_before_type_cast
assert_nil invalid_address.inet_address_before_type_cast
end
+
+ def test_schema_dump_with_shorthand
+ output = dump_table_schema("postgresql_network_addresses")
+ assert_match %r{t\.inet\s+"inet_address",\s+default: "192\.168\.1\.1"}, output
+ assert_match %r{t\.cidr\s+"cidr_address",\s+default: "192\.168\.1\.0/24"}, output
+ assert_match %r{t\.macaddr\s+"mac_address",\s+default: "ff:ff:ff:ff:ff:ff"}, output
+ end
end
diff --git a/activerecord/test/cases/adapters/postgresql/numbers_test.rb b/activerecord/test/cases/adapters/postgresql/numbers_test.rb
new file mode 100644
index 0000000000..ba7e7dc9a3
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/numbers_test.rb
@@ -0,0 +1,49 @@
+require "cases/helper"
+
+class PostgresqlNumberTest < ActiveRecord::PostgreSQLTestCase
+ class PostgresqlNumber < ActiveRecord::Base; end
+
+ setup do
+ @connection = ActiveRecord::Base.connection
+ @connection.create_table('postgresql_numbers', force: true) do |t|
+ t.column 'single', 'REAL'
+ t.column 'double', 'DOUBLE PRECISION'
+ end
+ end
+
+ teardown do
+ @connection.drop_table 'postgresql_numbers', if_exists: true
+ end
+
+ def test_data_type
+ assert_equal :float, PostgresqlNumber.columns_hash["single"].type
+ assert_equal :float, PostgresqlNumber.columns_hash["double"].type
+ end
+
+ def test_values
+ @connection.execute("INSERT INTO postgresql_numbers (id, single, double) VALUES (1, 123.456, 123456.789)")
+ @connection.execute("INSERT INTO postgresql_numbers (id, single, double) VALUES (2, '-Infinity', 'Infinity')")
+ @connection.execute("INSERT INTO postgresql_numbers (id, single, double) VALUES (3, 123.456, 'NaN')")
+
+ first, second, third = PostgresqlNumber.find(1, 2, 3)
+
+ assert_equal 123.456, first.single
+ assert_equal 123456.789, first.double
+ assert_equal(-::Float::INFINITY, second.single)
+ assert_equal ::Float::INFINITY, second.double
+ assert_send [third.double, :nan?]
+ end
+
+ def test_update
+ record = PostgresqlNumber.create! single: "123.456", double: "123456.789"
+ new_single = 789.012
+ new_double = 789012.345
+ record.single = new_single
+ record.double = new_double
+ record.save!
+
+ record.reload
+ assert_equal new_single, record.single
+ assert_equal new_double, record.double
+ end
+end
diff --git a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb
index cfff1f980b..e361521155 100644
--- a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb
@@ -1,11 +1,10 @@
-# encoding: utf-8
require "cases/helper"
require 'support/ddl_helper'
require 'support/connection_helper'
module ActiveRecord
module ConnectionAdapters
- class PostgreSQLAdapterTest < ActiveRecord::TestCase
+ class PostgreSQLAdapterTest < ActiveRecord::PostgreSQLTestCase
include DdlHelper
include ConnectionHelper
@@ -54,6 +53,12 @@ module ActiveRecord
end
end
+ def test_composite_primary_key
+ with_example_table 'id serial, number serial, PRIMARY KEY (id, number)' do
+ assert_nil @connection.primary_key('ex')
+ end
+ end
+
def test_primary_key_raises_error_if_table_not_found
assert_raises(ActiveRecord::StatementInvalid) do
@connection.primary_key('unobtainium')
@@ -63,7 +68,7 @@ module ActiveRecord
def test_insert_sql_with_proprietary_returning_clause
with_example_table do
id = @connection.insert_sql("insert into ex (number) values(5150)", nil, "number")
- assert_equal "5150", id
+ assert_equal 5150, id
end
end
@@ -101,21 +106,35 @@ module ActiveRecord
connection = connection_without_insert_returning
id = connection.insert_sql("insert into postgresql_partitioned_table_parent (number) VALUES (1)")
expect = connection.query('select max(id) from postgresql_partitioned_table_parent').first.first
- assert_equal expect, id
+ assert_equal expect.to_i, id
end
def test_exec_insert_with_returning_disabled
connection = connection_without_insert_returning
result = connection.exec_insert("insert into postgresql_partitioned_table_parent (number) VALUES (1)", nil, [], 'id', 'postgresql_partitioned_table_parent_id_seq')
expect = connection.query('select max(id) from postgresql_partitioned_table_parent').first.first
- assert_equal expect, result.rows.first.first
+ assert_equal expect.to_i, result.rows.first.first
end
def test_exec_insert_with_returning_disabled_and_no_sequence_name_given
connection = connection_without_insert_returning
result = connection.exec_insert("insert into postgresql_partitioned_table_parent (number) VALUES (1)", nil, [], 'id')
expect = connection.query('select max(id) from postgresql_partitioned_table_parent').first.first
- assert_equal expect, result.rows.first.first
+ assert_equal expect.to_i, result.rows.first.first
+ end
+
+ def test_exec_insert_default_values_with_returning_disabled_and_no_sequence_name_given
+ connection = connection_without_insert_returning
+ result = connection.exec_insert("insert into postgresql_partitioned_table_parent DEFAULT VALUES", nil, [], 'id')
+ expect = connection.query('select max(id) from postgresql_partitioned_table_parent').first.first
+ assert_equal expect.to_i, result.rows.first.first
+ end
+
+ def test_exec_insert_default_values_quoted_schema_with_returning_disabled_and_no_sequence_name_given
+ connection = connection_without_insert_returning
+ result = connection.exec_insert('insert into "public"."postgresql_partitioned_table_parent" DEFAULT VALUES', nil, [], 'id')
+ expect = connection.query('select max(id) from postgresql_partitioned_table_parent').first.first
+ assert_equal expect.to_i, result.rows.first.first
end
def test_sql_for_insert_with_returning_disabled
@@ -134,18 +153,18 @@ module ActiveRecord
end
def test_default_sequence_name
- assert_equal PostgreSQL::Name.new('public', 'accounts_id_seq'),
+ assert_equal 'public.accounts_id_seq',
@connection.default_sequence_name('accounts', 'id')
- assert_equal PostgreSQL::Name.new('public', 'accounts_id_seq'),
+ assert_equal 'public.accounts_id_seq',
@connection.default_sequence_name('accounts')
end
def test_default_sequence_name_bad_table
- assert_equal PostgreSQL::Name.new(nil, 'zomg_id_seq'),
+ assert_equal 'zomg_id_seq',
@connection.default_sequence_name('zomg', 'id')
- assert_equal PostgreSQL::Name.new(nil, 'zomg_id_seq'),
+ assert_equal 'zomg_id_seq',
@connection.default_sequence_name('zomg')
end
@@ -153,7 +172,7 @@ module ActiveRecord
with_example_table do
pk, seq = @connection.pk_and_sequence_for('ex')
assert_equal 'id', pk
- assert_equal @connection.default_sequence_name('ex', 'id'), seq
+ assert_equal @connection.default_sequence_name('ex', 'id'), seq.to_s
end
end
@@ -161,7 +180,7 @@ module ActiveRecord
with_example_table 'code serial primary key' do
pk, seq = @connection.pk_and_sequence_for('ex')
assert_equal 'code', pk
- assert_equal @connection.default_sequence_name('ex', 'code'), seq
+ assert_equal @connection.default_sequence_name('ex', 'code'), seq.to_s
end
end
@@ -222,8 +241,8 @@ module ActiveRecord
"DELETE FROM pg_depend WHERE objid = 'ex2_id_seq'::regclass AND refobjid = 'ex'::regclass AND deptype = 'a'"
)
ensure
- @connection.exec_query('DROP TABLE IF EXISTS ex')
- @connection.exec_query('DROP TABLE IF EXISTS ex2')
+ @connection.drop_table 'ex', if_exists: true
+ @connection.drop_table 'ex2', if_exists: true
end
def test_exec_insert_number
@@ -233,7 +252,7 @@ module ActiveRecord
result = @connection.exec_query('SELECT number FROM ex WHERE number = 10')
assert_equal 1, result.rows.length
- assert_equal "10", result.rows.last.last
+ assert_equal 10, result.rows.last.last
end
end
@@ -269,7 +288,7 @@ module ActiveRecord
assert_equal 1, result.rows.length
assert_equal 2, result.columns.length
- assert_equal [['1', 'foo']], result.rows
+ assert_equal [[1, 'foo']], result.rows
end
end
@@ -278,12 +297,12 @@ module ActiveRecord
string = @connection.quote('foo')
@connection.exec_query("INSERT INTO ex (id, data) VALUES (1, #{string})")
result = @connection.exec_query(
- 'SELECT id, data FROM ex WHERE id = $1', nil, [[nil, 1]])
+ 'SELECT id, data FROM ex WHERE id = $1', nil, [bind_param(1)])
assert_equal 1, result.rows.length
assert_equal 2, result.columns.length
- assert_equal [['1', 'foo']], result.rows
+ assert_equal [[1, 'foo']], result.rows
end
end
@@ -292,23 +311,20 @@ module ActiveRecord
string = @connection.quote('foo')
@connection.exec_query("INSERT INTO ex (id, data) VALUES (1, #{string})")
- column = @connection.columns('ex').find { |col| col.name == 'id' }
+ bind = ActiveRecord::Relation::QueryAttribute.new("id", "1-fuu", ActiveRecord::Type::Integer.new)
result = @connection.exec_query(
- 'SELECT id, data FROM ex WHERE id = $1', nil, [[column, '1-fuu']])
+ 'SELECT id, data FROM ex WHERE id = $1', nil, [bind])
assert_equal 1, result.rows.length
assert_equal 2, result.columns.length
- assert_equal [['1', 'foo']], result.rows
+ assert_equal [[1, 'foo']], result.rows
end
end
def test_substitute_at
- bind = @connection.substitute_at(nil, 0)
- assert_equal Arel.sql('$1'), bind
-
- bind = @connection.substitute_at(nil, 1)
- assert_equal Arel.sql('$2'), bind
+ bind = @connection.substitute_at(nil)
+ assert_equal Arel.sql('$1'), bind.to_sql
end
def test_partial_index
@@ -334,6 +350,14 @@ module ActiveRecord
@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',
+ @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",
@connection.columns_for_distinct("posts.id", ["posts.created_at desc", "", " "])
@@ -426,10 +450,10 @@ module ActiveRecord
private
def insert(ctx, data)
- binds = data.map { |name, value|
- [ctx.columns('ex').find { |x| x.name == name }, value]
+ binds = data.map { |name, value|
+ bind_param(value, name)
}
- columns = binds.map(&:first).map(&:name)
+ columns = binds.map(&:name)
bind_subs = columns.length.times.map { |x| "$#{x + 1}" }
@@ -446,6 +470,10 @@ module ActiveRecord
def connection_without_insert_returning
ActiveRecord::Base.postgresql_connection(ActiveRecord::Base.configurations['arunit'].merge(:insert_returning => false))
end
+
+ def bind_param(value, name = nil)
+ ActiveRecord::Relation::QueryAttribute.new(name, value, ActiveRecord::Type::Value.new)
+ end
end
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/quoting_test.rb b/activerecord/test/cases/adapters/postgresql/quoting_test.rb
index 11d5173d37..5e6f4dbbb8 100644
--- a/activerecord/test/cases/adapters/postgresql/quoting_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/quoting_test.rb
@@ -4,69 +4,39 @@ require 'ipaddr'
module ActiveRecord
module ConnectionAdapters
class PostgreSQLAdapter
- class QuotingTest < ActiveRecord::TestCase
+ class QuotingTest < ActiveRecord::PostgreSQLTestCase
def setup
@conn = ActiveRecord::Base.connection
end
def test_type_cast_true
- c = PostgreSQLColumn.new(nil, 1, Type::Boolean.new, 'boolean')
- assert_equal 't', @conn.type_cast(true, nil)
- assert_equal 't', @conn.type_cast(true, c)
+ assert_equal 't', @conn.type_cast(true)
end
def test_type_cast_false
- c = PostgreSQLColumn.new(nil, 1, Type::Boolean.new, 'boolean')
- assert_equal 'f', @conn.type_cast(false, nil)
- assert_equal 'f', @conn.type_cast(false, c)
- end
-
- def test_type_cast_cidr
- ip = IPAddr.new('255.0.0.0/8')
- c = PostgreSQLColumn.new(nil, ip, OID::Cidr.new, 'cidr')
- assert_equal ip, @conn.type_cast(ip, c)
- end
-
- def test_type_cast_inet
- ip = IPAddr.new('255.1.0.0/8')
- c = PostgreSQLColumn.new(nil, ip, OID::Cidr.new, 'inet')
- assert_equal ip, @conn.type_cast(ip, c)
+ assert_equal 'f', @conn.type_cast(false)
end
def test_quote_float_nan
nan = 0.0/0
- c = PostgreSQLColumn.new(nil, 1, OID::Float.new, 'float')
- assert_equal "'NaN'", @conn.quote(nan, c)
+ assert_equal "'NaN'", @conn.quote(nan)
end
def test_quote_float_infinity
infinity = 1.0/0
- c = PostgreSQLColumn.new(nil, 1, OID::Float.new, 'float')
- assert_equal "'Infinity'", @conn.quote(infinity, c)
- end
-
- def test_quote_cast_numeric
- fixnum = 666
- c = PostgreSQLColumn.new(nil, nil, Type::String.new, 'varchar')
- assert_equal "'666'", @conn.quote(fixnum, c)
- c = PostgreSQLColumn.new(nil, nil, Type::Text.new, 'text')
- assert_equal "'666'", @conn.quote(fixnum, c)
- end
-
- def test_quote_time_usec
- assert_equal "'1970-01-01 00:00:00.000000'", @conn.quote(Time.at(0))
- assert_equal "'1970-01-01 00:00:00.000000'", @conn.quote(Time.at(0).to_datetime)
+ assert_equal "'Infinity'", @conn.quote(infinity)
end
def test_quote_range
range = "1,2]'; SELECT * FROM users; --".."a"
- c = PostgreSQLColumn.new(nil, nil, OID::Range.new(Type::Integer.new, :int8range))
- assert_equal "'[1,0]'", @conn.quote(range, c)
+ type = OID::Range.new(Type::Integer.new, :int8range)
+ assert_equal "'[1,0]'", @conn.quote(type.serialize(range))
end
def test_quote_bit_string
- c = PostgreSQLColumn.new(nil, 1, OID::Bit.new)
- assert_equal nil, @conn.quote("'); SELECT * FROM users; /*\n01\n*/--", c)
+ value = "'); SELECT * FROM users; /*\n01\n*/--"
+ type = OID::Bit.new
+ assert_equal nil, @conn.quote(type.serialize(value))
end
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/range_test.rb b/activerecord/test/cases/adapters/postgresql/range_test.rb
index d812cd01c4..02b1083430 100644
--- a/activerecord/test/cases/adapters/postgresql/range_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/range_test.rb
@@ -1,13 +1,13 @@
require "cases/helper"
require 'support/connection_helper'
-if ActiveRecord::Base.connection.supports_ranges?
+if ActiveRecord::Base.connection.respond_to?(:supports_ranges?) && ActiveRecord::Base.connection.supports_ranges?
class PostgresqlRange < ActiveRecord::Base
self.table_name = "postgresql_ranges"
end
- class PostgresqlRangeTest < ActiveRecord::TestCase
- self.use_transactional_fixtures = false
+ class PostgresqlRangeTest < ActiveRecord::PostgreSQLTestCase
+ self.use_transactional_tests = false
include ConnectionHelper
def setup
@@ -91,7 +91,7 @@ _SQL
end
teardown do
- @connection.execute 'DROP TABLE IF EXISTS postgresql_ranges'
+ @connection.drop_table 'postgresql_ranges', if_exists: true
@connection.execute 'DROP TYPE IF EXISTS floatrange'
reset_connection
end
@@ -230,36 +230,14 @@ _SQL
assert_nil_round_trip(@first_range, :int8_range, 39999...39999)
end
- def test_exclude_beginning_for_subtypes_with_succ_method_is_deprecated
- tz = ::ActiveRecord::Base.default_timezone
-
- silence_warnings {
- assert_deprecated {
- range = PostgresqlRange.create!(date_range: "(''2012-01-02'', ''2012-01-04'']")
- assert_equal Date.new(2012, 1, 3)..Date.new(2012, 1, 4), range.date_range
- }
- assert_deprecated {
- range = PostgresqlRange.create!(ts_range: "(''2010-01-01 14:30'', ''2011-01-01 14:30'']")
- assert_equal Time.send(tz, 2010, 1, 1, 14, 30, 1)..Time.send(tz, 2011, 1, 1, 14, 30, 0), range.ts_range
- }
- assert_deprecated {
- range = PostgresqlRange.create!(tstz_range: "(''2010-01-01 14:30:00+05'', ''2011-01-01 14:30:00-03'']")
- assert_equal Time.parse('2010-01-01 09:30:01 UTC')..Time.parse('2011-01-01 17:30:00 UTC'), range.tstz_range
- }
- assert_deprecated {
- range = PostgresqlRange.create!(int4_range: "(1, 10]")
- assert_equal 2..10, range.int4_range
- }
- assert_deprecated {
- range = PostgresqlRange.create!(int8_range: "(10, 100]")
- assert_equal 11..100, range.int8_range
- }
- }
- end
-
def test_exclude_beginning_for_subtypes_without_succ_method_is_not_supported
assert_raises(ArgumentError) { PostgresqlRange.create!(num_range: "(0.1, 0.2]") }
assert_raises(ArgumentError) { PostgresqlRange.create!(float_range: "(0.5, 0.7]") }
+ assert_raises(ArgumentError) { PostgresqlRange.create!(int4_range: "(1, 10]") }
+ assert_raises(ArgumentError) { PostgresqlRange.create!(int8_range: "(10, 100]") }
+ assert_raises(ArgumentError) { PostgresqlRange.create!(date_range: "(''2012-01-02'', ''2012-01-04'']") }
+ assert_raises(ArgumentError) { PostgresqlRange.create!(ts_range: "(''2010-01-01 14:30'', ''2011-01-01 14:30'']") }
+ assert_raises(ArgumentError) { PostgresqlRange.create!(tstz_range: "(''2010-01-01 14:30:00+05'', ''2011-01-01 14:30:00-03'']") }
end
def test_update_all_with_ranges
diff --git a/activerecord/test/cases/adapters/postgresql/referential_integrity_test.rb b/activerecord/test/cases/adapters/postgresql/referential_integrity_test.rb
new file mode 100644
index 0000000000..c895ab9db5
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/referential_integrity_test.rb
@@ -0,0 +1,111 @@
+require 'cases/helper'
+require 'support/connection_helper'
+
+class PostgreSQLReferentialIntegrityTest < ActiveRecord::PostgreSQLTestCase
+ self.use_transactional_tests = false
+
+ include ConnectionHelper
+
+ IS_REFERENTIAL_INTEGRITY_SQL = lambda do |sql|
+ sql.match(/DISABLE TRIGGER ALL/) || sql.match(/ENABLE TRIGGER ALL/)
+ end
+
+ module MissingSuperuserPrivileges
+ def execute(sql)
+ if IS_REFERENTIAL_INTEGRITY_SQL.call(sql)
+ super "BROKEN;" rescue nil # put transaction in broken state
+ raise ActiveRecord::StatementInvalid, 'PG::InsufficientPrivilege'
+ else
+ super
+ end
+ end
+ end
+
+ module ProgrammerMistake
+ def execute(sql)
+ if IS_REFERENTIAL_INTEGRITY_SQL.call(sql)
+ raise ArgumentError, 'something is not right.'
+ else
+ super
+ end
+ end
+ end
+
+ def setup
+ @connection = ActiveRecord::Base.connection
+ end
+
+ def teardown
+ reset_connection
+ if ActiveRecord::Base.connection.is_a?(MissingSuperuserPrivileges)
+ raise "MissingSuperuserPrivileges patch was not removed"
+ end
+ end
+
+ def test_should_reraise_invalid_foreign_key_exception_and_show_warning
+ @connection.extend MissingSuperuserPrivileges
+
+ warning = capture(:stderr) do
+ e = assert_raises(ActiveRecord::InvalidForeignKey) do
+ @connection.disable_referential_integrity do
+ raise ActiveRecord::InvalidForeignKey, 'Should be re-raised'
+ end
+ end
+ assert_equal 'Should be re-raised', e.message
+ end
+ assert_match (/WARNING: Rails was not able to disable referential integrity/), warning
+ assert_match (/cause: PG::InsufficientPrivilege/), warning
+ end
+
+ def test_does_not_print_warning_if_no_invalid_foreign_key_exception_was_raised
+ @connection.extend MissingSuperuserPrivileges
+
+ warning = capture(:stderr) do
+ e = assert_raises(ActiveRecord::StatementInvalid) do
+ @connection.disable_referential_integrity do
+ raise ActiveRecord::StatementInvalid, 'Should be re-raised'
+ end
+ end
+ assert_equal 'Should be re-raised', e.message
+ end
+ assert warning.blank?, "expected no warnings but got:\n#{warning}"
+ end
+
+ def test_does_not_break_transactions
+ @connection.extend MissingSuperuserPrivileges
+
+ @connection.transaction do
+ @connection.disable_referential_integrity do
+ assert_transaction_is_not_broken
+ end
+ assert_transaction_is_not_broken
+ end
+ end
+
+ def test_does_not_break_nested_transactions
+ @connection.extend MissingSuperuserPrivileges
+
+ @connection.transaction do
+ @connection.transaction(requires_new: true) do
+ @connection.disable_referential_integrity do
+ assert_transaction_is_not_broken
+ end
+ end
+ assert_transaction_is_not_broken
+ end
+ end
+
+ def test_only_catch_active_record_errors_others_bubble_up
+ @connection.extend ProgrammerMistake
+
+ assert_raises ArgumentError do
+ @connection.disable_referential_integrity {}
+ end
+ end
+
+ private
+
+ def assert_transaction_is_not_broken
+ assert_equal 1, @connection.select_value("SELECT 1")
+ end
+end
diff --git a/activerecord/test/cases/adapters/postgresql/rename_table_test.rb b/activerecord/test/cases/adapters/postgresql/rename_table_test.rb
new file mode 100644
index 0000000000..bd64bae308
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/rename_table_test.rb
@@ -0,0 +1,34 @@
+require "cases/helper"
+
+class PostgresqlRenameTableTest < ActiveRecord::PostgreSQLTestCase
+ def setup
+ @connection = ActiveRecord::Base.connection
+ @connection.create_table :before_rename, force: true
+ end
+
+ def teardown
+ @connection.drop_table "before_rename", if_exists: true
+ @connection.drop_table "after_rename", if_exists: true
+ end
+
+ test "renaming a table also renames the primary key index" do
+ # sanity check
+ assert_equal 1, num_indices_named("before_rename_pkey")
+ assert_equal 0, num_indices_named("after_rename_pkey")
+
+ @connection.rename_table :before_rename, :after_rename
+
+ assert_equal 0, num_indices_named("before_rename_pkey")
+ assert_equal 1, num_indices_named("after_rename_pkey")
+ end
+
+ private
+
+ def num_indices_named(name)
+ @connection.execute(<<-SQL).values.length
+ SELECT 1 FROM "pg_index"
+ JOIN "pg_class" ON "pg_index"."indexrelid" = "pg_class"."oid"
+ WHERE "pg_class"."relname" = '#{name}'
+ SQL
+ end
+end
diff --git a/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb b/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb
index 99c26c4bf7..a0afd922b2 100644
--- a/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb
@@ -3,8 +3,8 @@ require "cases/helper"
class SchemaThing < ActiveRecord::Base
end
-class SchemaAuthorizationTest < ActiveRecord::TestCase
- self.use_transactional_fixtures = false
+class SchemaAuthorizationTest < ActiveRecord::PostgreSQLTestCase
+ self.use_transactional_tests = false
TABLE_NAME = 'schema_things'
COLUMNS = [
@@ -31,7 +31,7 @@ class SchemaAuthorizationTest < ActiveRecord::TestCase
set_session_auth
@connection.execute "RESET search_path"
USERS.each do |u|
- @connection.execute "DROP SCHEMA #{u} CASCADE"
+ @connection.drop_schema u
@connection.execute "DROP USER #{u}"
end
end
@@ -55,7 +55,7 @@ class SchemaAuthorizationTest < ActiveRecord::TestCase
set_session_auth
USERS.each do |u|
set_session_auth u
- assert_equal u, @connection.exec_query("SELECT name FROM #{TABLE_NAME} WHERE id = $1", 'SQL', [[nil, 1]]).first['name']
+ assert_equal u, @connection.exec_query("SELECT name FROM #{TABLE_NAME} WHERE id = $1", 'SQL', [bind_param(1)]).first['name']
set_session_auth
end
end
@@ -67,7 +67,7 @@ class SchemaAuthorizationTest < ActiveRecord::TestCase
USERS.each do |u|
@connection.clear_cache!
set_session_auth u
- assert_equal u, @connection.exec_query("SELECT name FROM #{TABLE_NAME} WHERE id = $1", 'SQL', [[nil, 1]]).first['name']
+ assert_equal u, @connection.exec_query("SELECT name FROM #{TABLE_NAME} WHERE id = $1", 'SQL', [bind_param(1)]).first['name']
set_session_auth
end
end
@@ -111,4 +111,7 @@ class SchemaAuthorizationTest < ActiveRecord::TestCase
@connection.session_auth = auth || 'default'
end
+ def bind_param(value)
+ ActiveRecord::Relation::QueryAttribute.new(nil, value, ActiveRecord::Type::Value.new)
+ end
end
diff --git a/activerecord/test/cases/adapters/postgresql/schema_test.rb b/activerecord/test/cases/adapters/postgresql/schema_test.rb
index 9e5fd17dc4..93e98ec872 100644
--- a/activerecord/test/cases/adapters/postgresql/schema_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/schema_test.rb
@@ -1,7 +1,21 @@
require "cases/helper"
+require 'models/default'
+require 'support/schema_dumping_helper'
+
+module PGSchemaHelper
+ def with_schema_search_path(schema_search_path)
+ @connection.schema_search_path = schema_search_path
+ @connection.schema_cache.clear!
+ yield if block_given?
+ ensure
+ @connection.schema_search_path = "'$user', public"
+ @connection.schema_cache.clear!
+ end
+end
-class SchemaTest < ActiveRecord::TestCase
- self.use_transactional_fixtures = false
+class SchemaTest < ActiveRecord::PostgreSQLTestCase
+ include PGSchemaHelper
+ self.use_transactional_tests = false
SCHEMA_NAME = 'test_schema'
SCHEMA2_NAME = 'test_schema2'
@@ -82,12 +96,12 @@ class SchemaTest < ActiveRecord::TestCase
end
teardown do
- @connection.execute "DROP SCHEMA #{SCHEMA2_NAME} CASCADE"
- @connection.execute "DROP SCHEMA #{SCHEMA_NAME} CASCADE"
+ @connection.drop_schema SCHEMA2_NAME, if_exists: true
+ @connection.drop_schema SCHEMA_NAME, if_exists: true
end
def test_schema_names
- assert_equal ["public", "schema_1", "test_schema", "test_schema2"], @connection.schema_names
+ assert_equal ["public", "test_schema", "test_schema2"], @connection.schema_names
end
def test_create_schema
@@ -119,10 +133,17 @@ class SchemaTest < ActiveRecord::TestCase
assert !@connection.schema_names.include?("test_schema3")
end
+ def test_drop_schema_if_exists
+ @connection.create_schema "some_schema"
+ assert_includes @connection.schema_names, "some_schema"
+ @connection.drop_schema "some_schema", if_exists: true
+ assert_not_includes @connection.schema_names, "some_schema"
+ end
+
def test_habtm_table_name_with_schema
+ ActiveRecord::Base.connection.drop_schema "music", if_exists: true
+ ActiveRecord::Base.connection.create_schema "music"
ActiveRecord::Base.connection.execute <<-SQL
- DROP SCHEMA IF EXISTS music CASCADE;
- CREATE SCHEMA music;
CREATE TABLE music.albums (id serial primary key);
CREATE TABLE music.songs (id serial primary key);
CREATE TABLE music.albums_songs (album_id integer, song_id integer);
@@ -132,27 +153,31 @@ class SchemaTest < ActiveRecord::TestCase
Album.create
assert_equal song, Song.includes(:albums).references(:albums).first
ensure
- ActiveRecord::Base.connection.execute "DROP SCHEMA music CASCADE;"
+ ActiveRecord::Base.connection.drop_schema "music", if_exists: true
end
- def test_raise_drop_schema_with_nonexisting_schema
+ def test_drop_schema_with_nonexisting_schema
assert_raises(ActiveRecord::StatementInvalid) do
- @connection.drop_schema "test_schema3"
+ @connection.drop_schema "idontexist"
+ end
+
+ assert_nothing_raised do
+ @connection.drop_schema "idontexist", if_exists: true
end
end
def test_raise_wraped_exception_on_bad_prepare
assert_raises(ActiveRecord::StatementInvalid) do
- @connection.exec_query "select * from developers where id = ?", 'sql', [[nil, 1]]
+ @connection.exec_query "select * from developers where id = ?", 'sql', [bind_param(1)]
end
end
def test_schema_change_with_prepared_stmt
altered = false
- @connection.exec_query "select * from developers where id = $1", 'sql', [[nil, 1]]
+ @connection.exec_query "select * from developers where id = $1", 'sql', [bind_param(1)]
@connection.exec_query "alter table developers add column zomg int", 'sql', []
altered = true
- @connection.exec_query "select * from developers where id = $1", 'sql', [[nil, 1]]
+ @connection.exec_query "select * from developers where id = $1", 'sql', [bind_param(1)]
ensure
# We are not using DROP COLUMN IF EXISTS because that syntax is only
# supported by pg 9.X
@@ -298,11 +323,11 @@ class SchemaTest < ActiveRecord::TestCase
def test_with_uppercase_index_name
@connection.execute "CREATE INDEX \"things_Index\" ON #{SCHEMA_NAME}.things (name)"
- assert_nothing_raised { @connection.remove_index! "things", "#{SCHEMA_NAME}.things_Index"}
+ assert_nothing_raised { @connection.remove_index "things", name: "#{SCHEMA_NAME}.things_Index"}
@connection.execute "CREATE INDEX \"things_Index\" ON #{SCHEMA_NAME}.things (name)"
with_schema_search_path SCHEMA_NAME do
- assert_nothing_raised { @connection.remove_index! "things", "things_Index"}
+ assert_nothing_raised { @connection.remove_index "things", name: "things_Index"}
end
end
@@ -382,9 +407,17 @@ class SchemaTest < ActiveRecord::TestCase
def test_reset_pk_sequence
sequence_name = "#{SCHEMA_NAME}.#{UNMATCHED_SEQUENCE_NAME}"
@connection.execute "SELECT setval('#{sequence_name}', 123)"
- assert_equal "124", @connection.select_value("SELECT nextval('#{sequence_name}')")
+ assert_equal 124, @connection.select_value("SELECT nextval('#{sequence_name}')")
@connection.reset_pk_sequence!("#{SCHEMA_NAME}.#{UNMATCHED_PK_TABLE_NAME}")
- assert_equal "1", @connection.select_value("SELECT nextval('#{sequence_name}')")
+ assert_equal 1, @connection.select_value("SELECT nextval('#{sequence_name}')")
+ end
+
+ def test_set_pk_sequence
+ table_name = "#{SCHEMA_NAME}.#{PK_TABLE_NAME}"
+ _, sequence_name = @connection.pk_and_sequence_for table_name
+ @connection.set_pk_sequence! table_name, 123
+ assert_equal 124, @connection.select_value("SELECT nextval('#{sequence_name}')")
+ @connection.reset_pk_sequence! table_name
end
private
@@ -394,16 +427,9 @@ class SchemaTest < ActiveRecord::TestCase
end
end
- def with_schema_search_path(schema_search_path)
- @connection.schema_search_path = schema_search_path
- yield if block_given?
- ensure
- @connection.schema_search_path = "'$user', public"
- end
-
def do_dump_index_tests_for_schema(this_schema_name, first_index_column_name, second_index_column_name, third_index_column_name, fourth_index_column_name)
with_schema_search_path(this_schema_name) do
- indexes = @connection.indexes(TABLE_NAME).sort_by {|i| i.name}
+ indexes = @connection.indexes(TABLE_NAME).sort_by(&:name)
assert_equal 4,indexes.size
do_dump_index_assertions_for_one_index(indexes[0], INDEX_A_NAME, first_index_column_name)
@@ -425,4 +451,124 @@ class SchemaTest < ActiveRecord::TestCase
assert_equal this_index_column, this_index.columns[0]
assert_equal this_index_name, this_index.name
end
+
+ def bind_param(value)
+ ActiveRecord::Relation::QueryAttribute.new(nil, value, ActiveRecord::Type::Value.new)
+ end
+end
+
+class SchemaForeignKeyTest < ActiveRecord::PostgreSQLTestCase
+ include SchemaDumpingHelper
+
+ setup do
+ @connection = ActiveRecord::Base.connection
+ end
+
+ def test_dump_foreign_key_targeting_different_schema
+ @connection.create_schema "my_schema"
+ @connection.create_table "my_schema.trains" do |t|
+ t.string :name
+ end
+ @connection.create_table "wagons" do |t|
+ t.integer :train_id
+ end
+ @connection.add_foreign_key "wagons", "my_schema.trains", column: "train_id"
+ output = dump_table_schema "wagons"
+ assert_match %r{\s+add_foreign_key "wagons", "my_schema\.trains", column: "train_id"$}, output
+ ensure
+ @connection.drop_table "wagons", if_exists: true
+ @connection.drop_table "my_schema.trains", if_exists: true
+ @connection.drop_schema "my_schema", if_exists: true
+ end
+end
+
+class DefaultsUsingMultipleSchemasAndDomainTest < ActiveRecord::PostgreSQLTestCase
+ setup do
+ @connection = ActiveRecord::Base.connection
+ @connection.drop_schema "schema_1", if_exists: true
+ @connection.execute "CREATE SCHEMA schema_1"
+ @connection.execute "CREATE DOMAIN schema_1.text AS text"
+ @connection.execute "CREATE DOMAIN schema_1.varchar AS varchar"
+ @connection.execute "CREATE DOMAIN schema_1.bpchar AS bpchar"
+
+ @old_search_path = @connection.schema_search_path
+ @connection.schema_search_path = "schema_1, pg_catalog"
+ @connection.create_table "defaults" do |t|
+ t.text "text_col", default: "some value"
+ t.string "string_col", default: "some value"
+ t.decimal "decimal_col", default: "3.14159265358979323846"
+ end
+ Default.reset_column_information
+ end
+
+ teardown do
+ @connection.schema_search_path = @old_search_path
+ @connection.drop_schema "schema_1", if_exists: true
+ Default.reset_column_information
+ end
+
+ def test_text_defaults_in_new_schema_when_overriding_domain
+ assert_equal "some value", Default.new.text_col, "Default of text column was not correctly parsed"
+ end
+
+ def test_string_defaults_in_new_schema_when_overriding_domain
+ assert_equal "some value", Default.new.string_col, "Default of string column was not correctly parsed"
+ end
+
+ def test_decimal_defaults_in_new_schema_when_overriding_domain
+ assert_equal BigDecimal.new("3.14159265358979323846"), Default.new.decimal_col, "Default of decimal column was not correctly parsed"
+ end
+
+ def test_bpchar_defaults_in_new_schema_when_overriding_domain
+ @connection.execute "ALTER TABLE defaults ADD bpchar_col bpchar DEFAULT 'some value'"
+ Default.reset_column_information
+ assert_equal "some value", Default.new.bpchar_col, "Default of bpchar column was not correctly parsed"
+ end
+
+ def test_text_defaults_after_updating_column_default
+ @connection.execute "ALTER TABLE defaults ALTER COLUMN text_col SET DEFAULT 'some text'::schema_1.text"
+ assert_equal "some text", Default.new.text_col, "Default of text column was not correctly parsed after updating default using '::text' since postgreSQL will add parens to the default in db"
+ end
+
+ def test_default_containing_quote_and_colons
+ @connection.execute "ALTER TABLE defaults ALTER COLUMN string_col SET DEFAULT 'foo''::bar'"
+ assert_equal "foo'::bar", Default.new.string_col
+ end
+end
+
+class SchemaWithDotsTest < ActiveRecord::PostgreSQLTestCase
+ include PGSchemaHelper
+ self.use_transactional_tests = false
+
+ setup do
+ @connection = ActiveRecord::Base.connection
+ @connection.create_schema "my.schema"
+ end
+
+ teardown do
+ @connection.drop_schema "my.schema", if_exists: true
+ end
+
+ test "rename_table" do
+ with_schema_search_path('"my.schema"') do
+ @connection.create_table :posts
+ @connection.rename_table :posts, :articles
+ assert_equal ["articles"], @connection.tables
+ end
+ end
+
+ test "Active Record basics" do
+ with_schema_search_path('"my.schema"') do
+ @connection.create_table :articles do |t|
+ t.string :title
+ end
+ article_class = Class.new(ActiveRecord::Base) do
+ self.table_name = '"my.schema".articles'
+ end
+
+ article_class.create!(title: "zOMG, welcome to my blorgh!")
+ welcome_article = article_class.last
+ assert_equal "zOMG, welcome to my blorgh!", welcome_article.title
+ end
+ end
end
diff --git a/activerecord/test/cases/adapters/postgresql/serial_test.rb b/activerecord/test/cases/adapters/postgresql/serial_test.rb
new file mode 100644
index 0000000000..7d30db247b
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/serial_test.rb
@@ -0,0 +1,60 @@
+require "cases/helper"
+require 'support/schema_dumping_helper'
+
+class PostgresqlSerialTest < ActiveRecord::PostgreSQLTestCase
+ include SchemaDumpingHelper
+
+ class PostgresqlSerial < ActiveRecord::Base; end
+
+ setup do
+ @connection = ActiveRecord::Base.connection
+ @connection.create_table "postgresql_serials", force: true do |t|
+ t.serial :seq
+ end
+ end
+
+ teardown do
+ @connection.drop_table "postgresql_serials", if_exists: true
+ end
+
+ def test_serial_column
+ column = PostgresqlSerial.columns_hash["seq"]
+ assert_equal :integer, column.type
+ assert_equal "integer", column.sql_type
+ assert column.serial?
+ end
+
+ def test_schema_dump_with_shorthand
+ output = dump_table_schema "postgresql_serials"
+ assert_match %r{t\.serial\s+"seq"}, output
+ end
+end
+
+class PostgresqlBigSerialTest < ActiveRecord::PostgreSQLTestCase
+ include SchemaDumpingHelper
+
+ class PostgresqlBigSerial < ActiveRecord::Base; end
+
+ setup do
+ @connection = ActiveRecord::Base.connection
+ @connection.create_table "postgresql_big_serials", force: true do |t|
+ t.bigserial :seq
+ end
+ end
+
+ teardown do
+ @connection.drop_table "postgresql_big_serials", if_exists: true
+ end
+
+ def test_bigserial_column
+ column = PostgresqlBigSerial.columns_hash["seq"]
+ assert_equal :integer, column.type
+ assert_equal "bigint", column.sql_type
+ assert column.serial?
+ end
+
+ def test_schema_dump_with_shorthand
+ output = dump_table_schema "postgresql_big_serials"
+ assert_match %r{t\.bigserial\s+"seq"}, output
+ end
+end
diff --git a/activerecord/test/cases/adapters/postgresql/sql_types_test.rb b/activerecord/test/cases/adapters/postgresql/sql_types_test.rb
deleted file mode 100644
index d7d40f6385..0000000000
--- a/activerecord/test/cases/adapters/postgresql/sql_types_test.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-require "cases/helper"
-
-class SqlTypesTest < ActiveRecord::TestCase
- def test_binary_types
- assert_equal 'bytea', type_to_sql(:binary, 100_000)
- assert_raise ActiveRecord::ActiveRecordError do
- type_to_sql :binary, 4294967295
- end
- assert_equal 'text', type_to_sql(:text, 100_000)
- assert_raise ActiveRecord::ActiveRecordError do
- type_to_sql :text, 4294967295
- end
- end
-
- def type_to_sql(*args)
- ActiveRecord::Base.connection.type_to_sql(*args)
- end
-end
diff --git a/activerecord/test/cases/adapters/postgresql/statement_pool_test.rb b/activerecord/test/cases/adapters/postgresql/statement_pool_test.rb
index 1497b0abc7..5aab246c99 100644
--- a/activerecord/test/cases/adapters/postgresql/statement_pool_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/statement_pool_test.rb
@@ -13,7 +13,7 @@ module ActiveRecord
end
end
- class StatementPoolTest < ActiveRecord::TestCase
+ class StatementPoolTest < ActiveRecord::PostgreSQLTestCase
if Process.respond_to?(:fork)
def test_cache_is_per_pid
cache = StatementPool.new nil, 10
diff --git a/activerecord/test/cases/adapters/postgresql/timestamp_test.rb b/activerecord/test/cases/adapters/postgresql/timestamp_test.rb
index 3614b29190..4c4866b46b 100644
--- a/activerecord/test/cases/adapters/postgresql/timestamp_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/timestamp_test.rb
@@ -2,10 +2,10 @@ require 'cases/helper'
require 'models/developer'
require 'models/topic'
-class PostgresqlTimestampTest < ActiveRecord::TestCase
+class PostgresqlTimestampTest < ActiveRecord::PostgreSQLTestCase
class PostgresqlTimestampWithZone < ActiveRecord::Base; end
- self.use_transactional_fixtures = false
+ self.use_transactional_tests = false
setup do
@connection = ActiveRecord::Base.connection
@@ -43,7 +43,7 @@ class PostgresqlTimestampTest < ActiveRecord::TestCase
end
end
-class TimestampTest < ActiveRecord::TestCase
+class PostgresqlTimestampFixtureTest < ActiveRecord::PostgreSQLTestCase
fixtures :topics
def test_group_by_date
@@ -70,53 +70,6 @@ class TimestampTest < ActiveRecord::TestCase
assert_equal(-1.0 / 0.0, d.updated_at)
end
- def test_default_datetime_precision
- ActiveRecord::Base.connection.create_table(:foos)
- ActiveRecord::Base.connection.add_column :foos, :created_at, :datetime
- ActiveRecord::Base.connection.add_column :foos, :updated_at, :datetime
- assert_nil activerecord_column_option('foos', 'created_at', 'precision')
- end
-
- def test_timestamp_data_type_with_precision
- ActiveRecord::Base.connection.create_table(:foos)
- ActiveRecord::Base.connection.add_column :foos, :created_at, :datetime, :precision => 0
- ActiveRecord::Base.connection.add_column :foos, :updated_at, :datetime, :precision => 5
- assert_equal 0, activerecord_column_option('foos', 'created_at', 'precision')
- assert_equal 5, activerecord_column_option('foos', 'updated_at', 'precision')
- end
-
- def test_timestamps_helper_with_custom_precision
- ActiveRecord::Base.connection.create_table(:foos) do |t|
- t.timestamps :precision => 4
- end
- assert_equal 4, activerecord_column_option('foos', 'created_at', 'precision')
- assert_equal 4, activerecord_column_option('foos', 'updated_at', 'precision')
- end
-
- def test_passing_precision_to_timestamp_does_not_set_limit
- ActiveRecord::Base.connection.create_table(:foos) do |t|
- t.timestamps :precision => 4
- end
- assert_nil activerecord_column_option("foos", "created_at", "limit")
- assert_nil activerecord_column_option("foos", "updated_at", "limit")
- end
-
- def test_invalid_timestamp_precision_raises_error
- assert_raises ActiveRecord::ActiveRecordError do
- ActiveRecord::Base.connection.create_table(:foos) do |t|
- t.timestamps :precision => 7
- end
- end
- end
-
- def test_postgres_agrees_with_activerecord_about_precision
- ActiveRecord::Base.connection.create_table(:foos) do |t|
- t.timestamps :precision => 4
- end
- assert_equal '4', pg_datetime_precision('foos', 'created_at')
- assert_equal '4', pg_datetime_precision('foos', 'updated_at')
- end
-
def test_bc_timestamp
date = Date.new(0) - 1.week
Developer.create!(:name => "aaron", :updated_at => date)
@@ -134,21 +87,4 @@ class TimestampTest < ActiveRecord::TestCase
Developer.create!(:name => "yahagi", :updated_at => date)
assert_equal date, Developer.find_by_name("yahagi").updated_at
end
-
- private
-
- def pg_datetime_precision(table_name, column_name)
- results = ActiveRecord::Base.connection.execute("SELECT column_name, datetime_precision FROM information_schema.columns WHERE table_name ='#{table_name}'")
- result = results.find do |result_hash|
- result_hash["column_name"] == column_name
- end
- result && result["datetime_precision"]
- end
-
- def activerecord_column_option(tablename, column_name, option)
- result = ActiveRecord::Base.connection.columns(tablename).find do |column|
- column.name == column_name
- end
- result && result.send(option)
- end
end
diff --git a/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb b/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb
index 23817198b1..77a99ca778 100644
--- a/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb
@@ -1,6 +1,6 @@
require 'cases/helper'
-class PostgresqlTypeLookupTest < ActiveRecord::TestCase
+class PostgresqlTypeLookupTest < ActiveRecord::PostgreSQLTestCase
setup do
@connection = ActiveRecord::Base.connection
end
@@ -12,4 +12,22 @@ class PostgresqlTypeLookupTest < ActiveRecord::TestCase
assert_equal ';', box_array.delimiter
assert_equal ',', int_array.delimiter
end
+
+ test "array types correctly respect registration of subtypes" do
+ int_array = @connection.type_map.lookup(1007, -1, "integer[]")
+ bigint_array = @connection.type_map.lookup(1016, -1, "bigint[]")
+ big_array = [123456789123456789]
+
+ assert_raises(RangeError) { int_array.serialize(big_array) }
+ assert_equal "{123456789123456789}", bigint_array.serialize(big_array)
+ end
+
+ test "range types correctly respect registration of subtypes" do
+ int_range = @connection.type_map.lookup(3904, -1, "int4range")
+ bigint_range = @connection.type_map.lookup(3926, -1, "int8range")
+ big_range = 0..123456789123456789
+
+ assert_raises(RangeError) { int_range.serialize(big_range) }
+ assert_equal "[0,123456789123456789]", bigint_range.serialize(big_range)
+ end
end
diff --git a/activerecord/test/cases/adapters/postgresql/utils_test.rb b/activerecord/test/cases/adapters/postgresql/utils_test.rb
index 3fdb6888d9..095c1826e5 100644
--- a/activerecord/test/cases/adapters/postgresql/utils_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/utils_test.rb
@@ -1,6 +1,7 @@
require 'cases/helper'
+require 'active_record/connection_adapters/postgresql/utils'
-class PostgreSQLUtilsTest < ActiveSupport::TestCase
+class PostgreSQLUtilsTest < ActiveRecord::PostgreSQLTestCase
Name = ActiveRecord::ConnectionAdapters::PostgreSQL::Name
include ActiveRecord::ConnectionAdapters::PostgreSQL::Utils
@@ -20,7 +21,7 @@ class PostgreSQLUtilsTest < ActiveSupport::TestCase
end
end
-class PostgreSQLNameTest < ActiveSupport::TestCase
+class PostgreSQLNameTest < ActiveRecord::PostgreSQLTestCase
Name = ActiveRecord::ConnectionAdapters::PostgreSQL::Name
test "represents itself as schema.name" do
diff --git a/activerecord/test/cases/adapters/postgresql/uuid_test.rb b/activerecord/test/cases/adapters/postgresql/uuid_test.rb
index 66006d718f..7127d69e9e 100644
--- a/activerecord/test/cases/adapters/postgresql/uuid_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/uuid_test.rb
@@ -1,8 +1,5 @@
-# encoding: utf-8
-
require "cases/helper"
-require 'active_record/base'
-require 'active_record/connection_adapters/postgresql_adapter'
+require 'support/schema_dumping_helper'
module PostgresqlUUIDHelper
def connection
@@ -10,12 +7,13 @@ module PostgresqlUUIDHelper
end
def drop_table(name)
- connection.execute "drop table if exists #{name}"
+ connection.drop_table name, if_exists: true
end
end
-class PostgresqlUUIDTest < ActiveRecord::TestCase
+class PostgresqlUUIDTest < ActiveRecord::PostgreSQLTestCase
include PostgresqlUUIDHelper
+ include SchemaDumpingHelper
class UUIDType < ActiveRecord::Base
self.table_name = "uuid_data_type"
@@ -50,9 +48,10 @@ class PostgresqlUUIDTest < ActiveRecord::TestCase
column = UUIDType.columns_hash["guid"]
assert_equal :uuid, column.type
assert_equal "uuid", column.sql_type
- assert_not column.number?
- assert_not column.binary?
- assert_not column.array
+ assert_not column.array?
+
+ type = UUIDType.type_for_attribute("guid")
+ assert_not type.binary?
end
def test_treat_blank_uuid_as_nil
@@ -70,13 +69,18 @@ class PostgresqlUUIDTest < ActiveRecord::TestCase
assert_equal 'foobar', uuid.guid_before_type_cast
end
- def test_rfc_4122_regex
+ def test_acceptable_uuid_regex
# Valid uuids
['A0EEBC99-9C0B-4EF8-BB6D-6BB9BD380A11',
'{a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11}',
'a0eebc999c0b4ef8bb6d6bb9bd380a11',
'a0ee-bc99-9c0b-4ef8-bb6d-6bb9-bd38-0a11',
- '{a0eebc99-9c0b4ef8-bb6d6bb9-bd380a11}'].each do |valid_uuid|
+ '{a0eebc99-9c0b4ef8-bb6d6bb9-bd380a11}',
+ # The following is not a valid RFC 4122 UUID, but PG doesn't seem to care,
+ # so we shouldn't block it either. (Pay attention to "fb6d" – the "f" here
+ # is invalid – it must be one of 8, 9, A, B, a, b according to the spec.)
+ '{a0eebc99-9c0b-4ef8-fb6d-6bb9bd380a11}',
+ ].each do |valid_uuid|
uuid = UUIDType.new guid: valid_uuid
assert_not_nil uuid.guid
end
@@ -88,7 +92,6 @@ class PostgresqlUUIDTest < ActiveRecord::TestCase
0.0,
true,
'Z0000C99-9C0B-4EF8-BB6D-6BB9BD380A11',
- '{a0eebc99-9c0b-4ef8-fb6d-6bb9bd380a11}',
'a0eebc999r0b4ef8ab6d6bb9bd380a11',
'a0ee-bc99------4ef8-bb6d-6bb9-bd38-0a11',
'{a0eebc99-bb6d6bb9-bd380a11}'].each do |invalid_uuid|
@@ -108,17 +111,40 @@ class PostgresqlUUIDTest < ActiveRecord::TestCase
assert_equal "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11", uuid.guid
end
end
+
+ def test_schema_dump_with_shorthand
+ output = dump_table_schema "uuid_data_type"
+ assert_match %r{t\.uuid "guid"}, output
+ end
+
+ def test_uniqueness_validation_ignores_uuid
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = "uuid_data_type"
+ validates :guid, uniqueness: { case_sensitive: false }
+
+ def self.name
+ "UUIDType"
+ end
+ end
+
+ record = klass.create!(guid: "a0ee-bc99-9c0b-4ef8-bb6d-6bb9-bd38-0a11")
+ duplicate = klass.new(guid: record.guid)
+
+ assert record.guid.present? # Ensure we actually are testing a UUID
+ assert_not duplicate.valid?
+ end
end
-class PostgresqlUUIDGenerationTest < ActiveRecord::TestCase
+class PostgresqlUUIDGenerationTest < ActiveRecord::PostgreSQLTestCase
include PostgresqlUUIDHelper
+ include SchemaDumpingHelper
class UUID < ActiveRecord::Base
self.table_name = 'pg_uuids'
end
setup do
- enable_uuid_ossp!(connection)
+ enable_extension!('uuid-ossp', connection)
connection.create_table('pg_uuids', id: :uuid, default: 'uuid_generate_v1()') do |t|
t.string 'name'
@@ -144,6 +170,7 @@ class PostgresqlUUIDGenerationTest < ActiveRecord::TestCase
drop_table "pg_uuids"
drop_table 'pg_uuids_2'
connection.execute 'DROP FUNCTION IF EXISTS my_uuid_generator();'
+ disable_extension!('uuid-ossp', connection)
end
if ActiveRecord::Base.connection.supports_extensions?
@@ -170,26 +197,25 @@ class PostgresqlUUIDGenerationTest < ActiveRecord::TestCase
end
def test_schema_dumper_for_uuid_primary_key
- schema = StringIO.new
- ActiveRecord::SchemaDumper.dump(connection, schema)
- assert_match(/\bcreate_table "pg_uuids", id: :uuid, default: "uuid_generate_v1\(\)"/, schema.string)
- assert_match(/t\.uuid "other_uuid", default: "uuid_generate_v4\(\)"/, schema.string)
+ 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 = StringIO.new
- ActiveRecord::SchemaDumper.dump(connection, schema)
- assert_match(/\bcreate_table "pg_uuids_2", id: :uuid, default: "my_uuid_generator\(\)"/, schema.string)
- assert_match(/t\.uuid "other_uuid_2", default: "my_uuid_generator\(\)"/, schema.string)
+ 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
end
end
-class PostgresqlUUIDTestNilDefault < ActiveRecord::TestCase
+class PostgresqlUUIDTestNilDefault < ActiveRecord::PostgreSQLTestCase
include PostgresqlUUIDHelper
+ include SchemaDumpingHelper
setup do
- enable_uuid_ossp!(connection)
+ enable_extension!('uuid-ossp', connection)
connection.create_table('pg_uuids', id: false) do |t|
t.primary_key :id, :uuid, default: nil
@@ -199,6 +225,7 @@ class PostgresqlUUIDTestNilDefault < ActiveRecord::TestCase
teardown do
drop_table "pg_uuids"
+ disable_extension!('uuid-ossp', connection)
end
if ActiveRecord::Base.connection.supports_extensions?
@@ -209,10 +236,15 @@ class PostgresqlUUIDTestNilDefault < ActiveRecord::TestCase
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
end
end
-class PostgresqlUUIDTestInverseOf < ActiveRecord::TestCase
+class PostgresqlUUIDTestInverseOf < ActiveRecord::PostgreSQLTestCase
include PostgresqlUUIDHelper
class UuidPost < ActiveRecord::Base
@@ -226,7 +258,7 @@ class PostgresqlUUIDTestInverseOf < ActiveRecord::TestCase
end
setup do
- enable_uuid_ossp!(connection)
+ enable_extension!('uuid-ossp', connection)
connection.transaction do
connection.create_table('pg_uuid_posts', id: :uuid) do |t|
@@ -240,10 +272,9 @@ class PostgresqlUUIDTestInverseOf < ActiveRecord::TestCase
end
teardown do
- connection.transaction do
drop_table "pg_uuid_comments"
drop_table "pg_uuid_posts"
- end
+ disable_extension!('uuid-ossp', connection)
end
if ActiveRecord::Base.connection.supports_extensions?
@@ -252,5 +283,19 @@ class PostgresqlUUIDTestInverseOf < ActiveRecord::TestCase
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
+
+ end
+
+ def test_find_by_with_uuid
+ UuidPost.create!
+ assert_nil UuidPost.find_by(id: 789)
+ end
end
+
end
diff --git a/activerecord/test/cases/adapters/postgresql/view_test.rb b/activerecord/test/cases/adapters/postgresql/view_test.rb
deleted file mode 100644
index 47b7d38eda..0000000000
--- a/activerecord/test/cases/adapters/postgresql/view_test.rb
+++ /dev/null
@@ -1,67 +0,0 @@
-require "cases/helper"
-
-module ViewTestConcern
- extend ActiveSupport::Concern
-
- included do
- self.use_transactional_fixtures = false
- mattr_accessor :view_type
- end
-
- SCHEMA_NAME = 'test_schema'
- TABLE_NAME = 'things'
- COLUMNS = [
- 'id integer',
- 'name character varying(50)',
- 'email character varying(50)',
- 'moment timestamp without time zone'
- ]
-
- class ThingView < ActiveRecord::Base
- end
-
- def setup
- super
- ThingView.table_name = "#{SCHEMA_NAME}.#{view_type}_things"
-
- @connection = ActiveRecord::Base.connection
- @connection.execute "CREATE SCHEMA #{SCHEMA_NAME} CREATE TABLE #{TABLE_NAME} (#{COLUMNS.join(',')})"
- @connection.execute "CREATE #{view_type.humanize} #{ThingView.table_name} AS SELECT * FROM #{SCHEMA_NAME}.#{TABLE_NAME}"
- end
-
- def teardown
- super
- @connection.execute "DROP SCHEMA #{SCHEMA_NAME} CASCADE"
- end
-
- def test_table_exists
- name = ThingView.table_name
- assert @connection.table_exists?(name), "'#{name}' table should exist"
- end
-
- def test_column_definitions
- assert_nothing_raised do
- assert_equal COLUMNS, columns(ThingView.table_name)
- end
- end
-
- private
- def columns(table_name)
- @connection.send(:column_definitions, table_name).map do |name, type, default|
- "#{name} #{type}" + (default ? " default #{default}" : '')
- end
- end
-
-end
-
-class ViewTest < ActiveRecord::TestCase
- include ViewTestConcern
- self.view_type = 'view'
-end
-
-if ActiveRecord::Base.connection.supports_materialized_views?
- class MaterializedViewTest < ActiveRecord::TestCase
- include ViewTestConcern
- self.view_type = 'materialized_view'
- end
-end
diff --git a/activerecord/test/cases/adapters/postgresql/xml_test.rb b/activerecord/test/cases/adapters/postgresql/xml_test.rb
index 4165dd5ac9..add32699fa 100644
--- a/activerecord/test/cases/adapters/postgresql/xml_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/xml_test.rb
@@ -1,7 +1,8 @@
-# encoding: utf-8
require 'cases/helper'
+require 'support/schema_dumping_helper'
-class PostgresqlXMLTest < ActiveRecord::TestCase
+class PostgresqlXMLTest < ActiveRecord::PostgreSQLTestCase
+ include SchemaDumpingHelper
class XmlDataType < ActiveRecord::Base
self.table_name = 'xml_data_type'
end
@@ -21,7 +22,7 @@ class PostgresqlXMLTest < ActiveRecord::TestCase
end
teardown do
- @connection.execute 'drop table if exists xml_data_type'
+ @connection.drop_table 'xml_data_type', if_exists: true
end
def test_column
@@ -45,4 +46,9 @@ class PostgresqlXMLTest < ActiveRecord::TestCase
XmlDataType.update_all(payload: "<bar>baz</bar>")
assert_equal "<bar>baz</bar>", data.reload.payload
end
+
+ def test_schema_dump_with_shorthand
+ output = dump_table_schema("xml_data_type")
+ assert_match %r{t\.xml "payload"}, output
+ end
end
diff --git a/activerecord/test/cases/adapters/sqlite3/collation_test.rb b/activerecord/test/cases/adapters/sqlite3/collation_test.rb
new file mode 100644
index 0000000000..58a9469ce5
--- /dev/null
+++ b/activerecord/test/cases/adapters/sqlite3/collation_test.rb
@@ -0,0 +1,53 @@
+require "cases/helper"
+require 'support/schema_dumping_helper'
+
+class SQLite3CollationTest < ActiveRecord::SQLite3TestCase
+ include SchemaDumpingHelper
+
+ def setup
+ @connection = ActiveRecord::Base.connection
+ @connection.create_table :collation_table_sqlite3, force: true do |t|
+ t.string :string_nocase, collation: 'NOCASE'
+ t.text :text_rtrim, collation: 'RTRIM'
+ end
+ end
+
+ def teardown
+ @connection.drop_table :collation_table_sqlite3, if_exists: true
+ end
+
+ test "string column with collation" do
+ column = @connection.columns(:collation_table_sqlite3).find { |c| c.name == 'string_nocase' }
+ assert_equal :string, column.type
+ assert_equal 'NOCASE', column.collation
+ end
+
+ test "text column with collation" do
+ column = @connection.columns(:collation_table_sqlite3).find { |c| c.name == 'text_rtrim' }
+ assert_equal :text, column.type
+ assert_equal 'RTRIM', column.collation
+ end
+
+ test "add column with collation" do
+ @connection.add_column :collation_table_sqlite3, :title, :string, collation: 'RTRIM'
+
+ column = @connection.columns(:collation_table_sqlite3).find { |c| c.name == 'title' }
+ assert_equal :string, column.type
+ assert_equal 'RTRIM', column.collation
+ end
+
+ test "change column with collation" do
+ @connection.add_column :collation_table_sqlite3, :description, :string
+ @connection.change_column :collation_table_sqlite3, :description, :text, collation: 'RTRIM'
+
+ column = @connection.columns(:collation_table_sqlite3).find { |c| c.name == 'description' }
+ assert_equal :text, column.type
+ assert_equal 'RTRIM', column.collation
+ end
+
+ test "schema dump includes collation" do
+ output = dump_table_schema("collation_table_sqlite3")
+ assert_match %r{t.string\s+"string_nocase",\s+collation: "NOCASE"$}, output
+ assert_match %r{t.text\s+"text_rtrim",\s+collation: "RTRIM"$}, output
+ end
+end
diff --git a/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb b/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb
index 13b754d226..34e3b2e023 100644
--- a/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb
+++ b/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb
@@ -1,6 +1,6 @@
require "cases/helper"
-class CopyTableTest < ActiveRecord::TestCase
+class CopyTableTest < ActiveRecord::SQLite3TestCase
fixtures :customers
def setup
diff --git a/activerecord/test/cases/adapters/sqlite3/explain_test.rb b/activerecord/test/cases/adapters/sqlite3/explain_test.rb
index f1d6119d2e..2aec322582 100644
--- a/activerecord/test/cases/adapters/sqlite3/explain_test.rb
+++ b/activerecord/test/cases/adapters/sqlite3/explain_test.rb
@@ -1,10 +1,11 @@
require "cases/helper"
require 'models/developer'
+require 'models/computer'
module ActiveRecord
module ConnectionAdapters
class SQLite3Adapter
- class ExplainTest < ActiveRecord::TestCase
+ class ExplainTest < ActiveRecord::SQLite3TestCase
fixtures :developers
def test_explain_for_one_query
@@ -17,7 +18,7 @@ module ActiveRecord
explain = Developer.where(:id => 1).includes(:audit_logs).explain
assert_match %(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = ?), explain
assert_match(/(SEARCH )?TABLE developers USING (INTEGER )?PRIMARY KEY/, explain)
- assert_match %(EXPLAIN for: SELECT "audit_logs".* FROM "audit_logs" WHERE "audit_logs"."developer_id" IN (1)), explain
+ assert_match %(EXPLAIN for: SELECT "audit_logs".* FROM "audit_logs" WHERE "audit_logs"."developer_id" = 1), explain
assert_match(/(SCAN )?TABLE audit_logs/, explain)
end
end
diff --git a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb
index ac8332e2fa..87a892db37 100644
--- a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb
+++ b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb
@@ -6,7 +6,7 @@ require 'securerandom'
module ActiveRecord
module ConnectionAdapters
class SQLite3Adapter
- class QuotingTest < ActiveRecord::TestCase
+ class QuotingTest < ActiveRecord::SQLite3TestCase
def setup
@conn = Base.sqlite3_connection :database => ':memory:',
:adapter => 'sqlite3',
@@ -15,73 +15,52 @@ module ActiveRecord
def test_type_cast_binary_encoding_without_logger
@conn.extend(Module.new { def logger; end })
- column = Column.new(nil, nil, Type::String.new)
binary = SecureRandom.hex
expected = binary.dup.encode!(Encoding::UTF_8)
- assert_equal expected, @conn.type_cast(binary, column)
+ assert_equal expected, @conn.type_cast(binary)
end
def test_type_cast_symbol
- assert_equal 'foo', @conn.type_cast(:foo, nil)
+ assert_equal 'foo', @conn.type_cast(:foo)
end
def test_type_cast_date
date = Date.today
expected = @conn.quoted_date(date)
- assert_equal expected, @conn.type_cast(date, nil)
+ assert_equal expected, @conn.type_cast(date)
end
def test_type_cast_time
time = Time.now
expected = @conn.quoted_date(time)
- assert_equal expected, @conn.type_cast(time, nil)
+ assert_equal expected, @conn.type_cast(time)
end
def test_type_cast_numeric
- assert_equal 10, @conn.type_cast(10, nil)
- assert_equal 2.2, @conn.type_cast(2.2, nil)
+ assert_equal 10, @conn.type_cast(10)
+ assert_equal 2.2, @conn.type_cast(2.2)
end
def test_type_cast_nil
- assert_equal nil, @conn.type_cast(nil, nil)
+ assert_equal nil, @conn.type_cast(nil)
end
def test_type_cast_true
- c = Column.new(nil, 1, Type::Integer.new)
- assert_equal 't', @conn.type_cast(true, nil)
- assert_equal 1, @conn.type_cast(true, c)
+ assert_equal 't', @conn.type_cast(true)
end
def test_type_cast_false
- c = Column.new(nil, 1, Type::Integer.new)
- assert_equal 'f', @conn.type_cast(false, nil)
- assert_equal 0, @conn.type_cast(false, c)
- end
-
- def test_type_cast_string
- assert_equal '10', @conn.type_cast('10', nil)
-
- c = Column.new(nil, 1, Type::Integer.new)
- assert_equal 10, @conn.type_cast('10', c)
-
- c = Column.new(nil, 1, Type::Float.new)
- assert_equal 10.1, @conn.type_cast('10.1', c)
-
- c = Column.new(nil, 1, Type::Binary.new)
- assert_equal '10.1', @conn.type_cast('10.1', c)
-
- c = Column.new(nil, 1, Type::Date.new)
- assert_equal '10.1', @conn.type_cast('10.1', c)
+ assert_equal 'f', @conn.type_cast(false)
end
def test_type_cast_bigdecimal
bd = BigDecimal.new '10.0'
- assert_equal bd.to_f, @conn.type_cast(bd, nil)
+ assert_equal bd.to_f, @conn.type_cast(bd)
end
def test_type_cast_unknown_should_raise_error
obj = Class.new.new
- assert_raise(TypeError) { @conn.type_cast(obj, nil) }
+ assert_raise(TypeError) { @conn.type_cast(obj) }
end
def test_type_cast_object_which_responds_to_quoted_id
@@ -94,21 +73,21 @@ module ActiveRecord
10
end
}.new
- assert_equal 10, @conn.type_cast(quoted_id_obj, nil)
+ 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, nil) }
+ assert_raise(TypeError) { @conn.type_cast(quoted_id_obj) }
end
def test_quoting_binary_strings
value = "hello".encode('ascii-8bit')
- column = Column.new(nil, 1, SQLite3String.new)
+ type = Type::String.new
- assert_equal "'hello'", @conn.quote(value, column)
+ assert_equal "'hello'", @conn.quote(type.serialize(value))
end
end
end
diff --git a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb
index b2bf9480dd..640df31e2e 100644
--- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb
+++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb
@@ -1,4 +1,3 @@
-# encoding: utf-8
require "cases/helper"
require 'models/owner'
require 'tempfile'
@@ -6,10 +5,10 @@ require 'support/ddl_helper'
module ActiveRecord
module ConnectionAdapters
- class SQLite3AdapterTest < ActiveRecord::TestCase
+ class SQLite3AdapterTest < ActiveRecord::SQLite3TestCase
include DdlHelper
- self.use_transactional_fixtures = false
+ self.use_transactional_tests = false
class DualEncoding < ActiveRecord::Base
end
@@ -23,7 +22,7 @@ module ActiveRecord
def test_bad_connection
assert_raise ActiveRecord::NoDatabaseError do
connection = ActiveRecord::Base.sqlite3_connection(adapter: "sqlite3", database: "/tmp/should/_not/_exist/-cinco-dog.db")
- connection.exec_query('drop table if exists ex')
+ connection.drop_table 'ex', if_exists: true
end
end
@@ -83,8 +82,7 @@ module ActiveRecord
def test_exec_insert
with_example_table do
- column = @conn.columns('ex').find { |col| col.name == 'number' }
- vals = [[column, 10]]
+ vals = [Relation::QueryAttribute.new("number", 10, Type::Value.new)]
@conn.exec_insert('insert into ex (number) VALUES (?)', 'SQL', vals)
result = @conn.exec_query(
@@ -133,8 +131,8 @@ module ActiveRecord
end
def test_bind_value_substitute
- bind_param = @conn.substitute_at('foo', 0)
- assert_equal Arel.sql('?'), bind_param
+ bind_param = @conn.substitute_at('foo')
+ assert_equal Arel.sql('?'), bind_param.to_sql
end
def test_exec_no_binds
@@ -157,7 +155,7 @@ module ActiveRecord
with_example_table 'id int, data string' do
@conn.exec_query('INSERT INTO ex (id, data) VALUES (1, "foo")')
result = @conn.exec_query(
- 'SELECT id, data FROM ex WHERE id = ?', nil, [[nil, 1]])
+ 'SELECT id, data FROM ex WHERE id = ?', nil, [Relation::QueryAttribute.new(nil, 1, Type::Value.new)])
assert_equal 1, result.rows.length
assert_equal 2, result.columns.length
@@ -169,10 +167,9 @@ module ActiveRecord
def test_exec_query_typecasts_bind_vals
with_example_table 'id int, data string' do
@conn.exec_query('INSERT INTO ex (id, data) VALUES (1, "foo")')
- column = @conn.columns('ex').find { |col| col.name == 'id' }
result = @conn.exec_query(
- 'SELECT id, data FROM ex WHERE id = ?', nil, [[column, '1-fuu']])
+ 'SELECT id, data FROM ex WHERE id = ?', nil, [Relation::QueryAttribute.new("id", "1-fuu", Type::Integer.new)])
assert_equal 1, result.rows.length
assert_equal 2, result.columns.length
@@ -194,7 +191,7 @@ module ActiveRecord
binary.save!
assert_equal str, binary.data
ensure
- DualEncoding.connection.execute('DROP TABLE IF EXISTS dual_encodings')
+ DualEncoding.connection.drop_table 'dual_encodings', if_exists: true
end
def test_type_cast_should_not_mutate_encoding
@@ -297,7 +294,7 @@ module ActiveRecord
def test_tables_logs_name
sql = <<-SQL
SELECT name FROM sqlite_master
- WHERE type = 'table' AND NOT name = 'sqlite_sequence'
+ WHERE type IN ('table','view') AND name <> 'sqlite_sequence'
SQL
assert_logged [[sql.squish, 'SCHEMA', []]] do
@conn.tables('hello')
@@ -316,8 +313,7 @@ module ActiveRecord
with_example_table do
sql = <<-SQL
SELECT name FROM sqlite_master
- WHERE type = 'table'
- AND NOT name = 'sqlite_sequence' AND name = \"ex\"
+ WHERE type IN ('table','view') AND name <> 'sqlite_sequence' AND name = 'ex'
SQL
assert_logged [[sql.squish, 'SCHEMA', []]] do
assert @conn.table_exists?('ex')
@@ -327,11 +323,11 @@ module ActiveRecord
def test_columns
with_example_table do
- columns = @conn.columns('ex').sort_by { |x| x.name }
+ columns = @conn.columns('ex').sort_by(&:name)
assert_equal 2, columns.length
- assert_equal %w{ id number }.sort, columns.map { |x| x.name }
- assert_equal [nil, nil], columns.map { |x| x.default }
- assert_equal [true, true], columns.map { |x| x.null }
+ assert_equal %w{ id number }.sort, columns.map(&:name)
+ assert_equal [nil, nil], columns.map(&:default)
+ assert_equal [true, true], columns.map(&:null)
end
end
@@ -405,6 +401,12 @@ module ActiveRecord
end
end
+ def test_composite_primary_key
+ with_example_table 'id integer, number integer, foo integer, PRIMARY KEY (id, number)' do
+ assert_nil @conn.primary_key('ex')
+ end
+ end
+
def test_supports_extensions
assert_not @conn.supports_extensions?, 'does not support extensions'
end
@@ -418,17 +420,20 @@ module ActiveRecord
end
def test_statement_closed
- db = SQLite3::Database.new(ActiveRecord::Base.
+ db = ::SQLite3::Database.new(ActiveRecord::Base.
configurations['arunit']['database'])
- statement = SQLite3::Statement.new(db,
+ statement = ::SQLite3::Statement.new(db,
'CREATE TABLE statement_test (number integer not null)')
- statement.stubs(:step).raises(SQLite3::BusyException, 'busy')
- statement.stubs(:columns).once.returns([])
- statement.expects(:close).once
- SQLite3::Statement.stubs(:new).returns(statement)
-
- assert_raises ActiveRecord::StatementInvalid do
- @conn.exec_query 'select * from statement_test'
+ statement.stub(:step, ->{ raise ::SQLite3::BusyException.new('busy') }) do
+ assert_called(statement, :columns, returns: []) do
+ assert_called(statement, :close) do
+ ::SQLite3::Statement.stub(:new, statement) do
+ assert_raises ActiveRecord::StatementInvalid do
+ @conn.exec_query 'select * from statement_test'
+ end
+ end
+ end
+ end
end
end
diff --git a/activerecord/test/cases/adapters/sqlite3/sqlite3_create_folder_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_create_folder_test.rb
index f545fc2011..887dcfc96c 100644
--- a/activerecord/test/cases/adapters/sqlite3/sqlite3_create_folder_test.rb
+++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_create_folder_test.rb
@@ -1,10 +1,9 @@
-# encoding: utf-8
require "cases/helper"
require 'models/owner'
module ActiveRecord
module ConnectionAdapters
- class SQLite3CreateFolder < ActiveRecord::TestCase
+ class SQLite3CreateFolder < ActiveRecord::SQLite3TestCase
def test_sqlite_creates_directory
Dir.mktmpdir do |dir|
dir = Pathname.new(dir)
diff --git a/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb b/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb
index fd0044ac05..559b951109 100644
--- a/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb
+++ b/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb
@@ -2,11 +2,11 @@ require 'cases/helper'
module ActiveRecord::ConnectionAdapters
class SQLite3Adapter
- class StatementPoolTest < ActiveRecord::TestCase
+ class StatementPoolTest < ActiveRecord::SQLite3TestCase
if Process.respond_to?(:fork)
def test_cache_is_per_pid
- cache = StatementPool.new nil, 10
+ cache = StatementPool.new(10)
cache['foo'] = 'bar'
assert_equal 'bar', cache['foo']
@@ -22,4 +22,3 @@ module ActiveRecord::ConnectionAdapters
end
end
end
-
diff --git a/activerecord/test/cases/ar_schema_test.rb b/activerecord/test/cases/ar_schema_test.rb
index 8700b20dee..9d5327bf35 100644
--- a/activerecord/test/cases/ar_schema_test.rb
+++ b/activerecord/test/cases/ar_schema_test.rb
@@ -3,9 +3,11 @@ require "cases/helper"
if ActiveRecord::Base.connection.supports_migrations?
class ActiveRecordSchemaTest < ActiveRecord::TestCase
- self.use_transactional_fixtures = false
+ self.use_transactional_tests = false
- def setup
+ setup do
+ @original_verbose = ActiveRecord::Migration.verbose
+ ActiveRecord::Migration.verbose = false
@connection = ActiveRecord::Base.connection
ActiveRecord::SchemaMigration.drop_table
end
@@ -14,7 +16,9 @@ if ActiveRecord::Base.connection.supports_migrations?
@connection.drop_table :fruits rescue nil
@connection.drop_table :nep_fruits rescue nil
@connection.drop_table :nep_schema_migrations rescue nil
+ @connection.drop_table :has_timestamps rescue nil
ActiveRecord::SchemaMigration.delete_all rescue nil
+ ActiveRecord::Migration.verbose = @original_verbose
end
def test_has_no_primary_key
@@ -88,5 +92,39 @@ if ActiveRecord::Base.connection.supports_migrations?
assert_equal "017", ActiveRecord::SchemaMigration.normalize_migration_number("0017")
assert_equal "20131219224947", ActiveRecord::SchemaMigration.normalize_migration_number("20131219224947")
end
+
+ def test_timestamps_without_null_set_null_to_false_on_create_table
+ ActiveRecord::Schema.define do
+ create_table :has_timestamps do |t|
+ t.timestamps
+ 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
+ end
+
+ def test_timestamps_without_null_set_null_to_false_on_change_table
+ ActiveRecord::Schema.define do
+ create_table :has_timestamps
+
+ change_table :has_timestamps do |t|
+ t.timestamps default: Time.now
+ end
+ end
+
+ assert !@connection.columns(:has_timestamps).find { |c| c.name == 'created_at' }.null
+ assert !@connection.columns(:has_timestamps).find { |c| c.name == 'updated_at' }.null
+ end
+
+ def test_timestamps_without_null_set_null_to_false_on_add_timestamps
+ ActiveRecord::Schema.define do
+ create_table :has_timestamps
+ 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
+ end
end
end
diff --git a/activerecord/test/cases/associations/association_scope_test.rb b/activerecord/test/cases/associations/association_scope_test.rb
index 3e0032ec73..472e270f8c 100644
--- a/activerecord/test/cases/associations/association_scope_test.rb
+++ b/activerecord/test/cases/associations/association_scope_test.rb
@@ -8,12 +8,7 @@ module ActiveRecord
test 'does not duplicate conditions' do
scope = AssociationScope.scope(Author.new.association(:welcome_posts),
Author.connection)
- wheres = scope.where_values.map(&:right)
- binds = scope.bind_values.map(&:last)
- wheres = scope.where_values.map(&:right).reject { |node|
- Arel::Nodes::BindParam === node
- }
- assert_equal wheres.uniq, wheres
+ binds = scope.where_clause.binds.map(&:value)
assert_equal binds.uniq, binds
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 25555bd75c..938350627f 100644
--- a/activerecord/test/cases/associations/belongs_to_associations_test.rb
+++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb
@@ -18,6 +18,11 @@ require 'models/invoice'
require 'models/line_item'
require 'models/column'
require 'models/record'
+require 'models/admin'
+require 'models/admin/user'
+require 'models/ship'
+require 'models/treasure'
+require 'models/parrot'
class BelongsToAssociationsTest < ActiveRecord::TestCase
fixtures :accounts, :companies, :developers, :projects, :topics,
@@ -30,6 +35,10 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
assert_equal companies(:first_firm).name, firm.name
end
+ def test_missing_attribute_error_is_raised_when_no_foreign_key_attribute
+ assert_raises(ActiveModel::MissingAttributeError) { Client.select(:id).first.firm }
+ end
+
def test_belongs_to_does_not_use_order_by
ActiveRecord::SQLCounter.clear_log
Client.find(3).firm
@@ -57,6 +66,85 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
end
end
+ def test_optional_relation
+ original_value = ActiveRecord::Base.belongs_to_required_by_default
+ ActiveRecord::Base.belongs_to_required_by_default = true
+
+ model = Class.new(ActiveRecord::Base) do
+ self.table_name = "accounts"
+ def self.name; "Temp"; end
+ belongs_to :company, optional: true
+ end
+
+ account = model.new
+ assert account.valid?
+ ensure
+ ActiveRecord::Base.belongs_to_required_by_default = original_value
+ end
+
+ def test_not_optional_relation
+ original_value = ActiveRecord::Base.belongs_to_required_by_default
+ ActiveRecord::Base.belongs_to_required_by_default = true
+
+ model = Class.new(ActiveRecord::Base) do
+ self.table_name = "accounts"
+ def self.name; "Temp"; end
+ belongs_to :company, optional: false
+ end
+
+ account = model.new
+ assert_not account.valid?
+ assert_equal [{error: :blank}], account.errors.details[:company]
+ ensure
+ ActiveRecord::Base.belongs_to_required_by_default = original_value
+ end
+
+ def test_required_belongs_to_config
+ original_value = ActiveRecord::Base.belongs_to_required_by_default
+ ActiveRecord::Base.belongs_to_required_by_default = true
+
+ model = Class.new(ActiveRecord::Base) do
+ self.table_name = "accounts"
+ def self.name; "Temp"; end
+ belongs_to :company
+ end
+
+ account = model.new
+ assert_not account.valid?
+ assert_equal [{error: :blank}], account.errors.details[:company]
+ ensure
+ ActiveRecord::Base.belongs_to_required_by_default = original_value
+ end
+
+ def test_default_scope_on_relations_is_not_cached
+ counter = 0
+
+ comments = Class.new(ActiveRecord::Base) {
+ self.table_name = 'comments'
+ self.inheritance_column = 'not_there'
+
+ posts = Class.new(ActiveRecord::Base) {
+ self.table_name = 'posts'
+ self.inheritance_column = 'not_there'
+
+ default_scope -> {
+ counter += 1
+ where("id = :inc", :inc => counter)
+ }
+
+ has_many :comments, :anonymous_class => comments
+ }
+ belongs_to :post, :anonymous_class => posts, :inverse_of => false
+ }
+
+ assert_equal 0, counter
+ comment = comments.first
+ assert_equal 0, counter
+ sql = capture_sql { comment.post }
+ comment.reload
+ assert_not_equal sql, capture_sql { comment.post }
+ end
+
def test_proxy_assignment
account = Account.find(1)
assert_nothing_raised { account.firm = account.firm }
@@ -67,6 +155,30 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
assert_raise(ActiveRecord::AssociationTypeMismatch) { Account.find(1).firm = Project.find(1) }
end
+ def test_raises_type_mismatch_with_namespaced_class
+ assert_nil defined?(Region), "This test requires that there is no top-level Region class"
+
+ ActiveRecord::Base.connection.instance_eval do
+ create_table(:admin_regions) { |t| t.string :name }
+ add_column :admin_users, :region_id, :integer
+ end
+ Admin.const_set "RegionalUser", Class.new(Admin::User) { belongs_to(:region) }
+ Admin.const_set "Region", Class.new(ActiveRecord::Base)
+
+ e = assert_raise(ActiveRecord::AssociationTypeMismatch) {
+ Admin::RegionalUser.new(region: 'wrong value')
+ }
+ assert_match(/^Region\([^)]+\) expected, got String\([^)]+\)$/, e.message)
+ ensure
+ Admin.send :remove_const, "Region" if Admin.const_defined?("Region")
+ Admin.send :remove_const, "RegionalUser" if Admin.const_defined?("RegionalUser")
+
+ ActiveRecord::Base.connection.instance_eval do
+ remove_column :admin_users, :region_id if column_exists?(:admin_users, :region_id)
+ drop_table :admin_regions, if_exists: true
+ end
+ end
+
def test_natural_assignment
apple = Firm.create("name" => "Apple")
citibank = Account.create("credit_limit" => 10)
@@ -92,14 +204,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_cache.key?(:firm_with_primary_key)
+ assert 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_cache.key?(:firm_with_primary_key_symbols)
+ assert citibank_result.association(:firm_with_primary_key_symbols).loaded?
end
def test_creating_the_belonging_object
@@ -183,7 +295,8 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
client = Client.find(3)
client.firm = nil
client.save
- assert_nil client.firm(true)
+ client.association(:firm).reload
+ assert_nil client.firm
assert_nil client.client_of
end
@@ -191,7 +304,8 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
client = Client.create(:name => "Primary key client", :firm_name => companies(:first_firm).name)
client.firm_with_primary_key = nil
client.save
- assert_nil client.firm_with_primary_key(true)
+ client.association(:firm_with_primary_key).reload
+ assert_nil client.firm_with_primary_key
assert_nil client.client_of
end
@@ -208,9 +322,13 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
def test_polymorphic_association_class
sponsor = Sponsor.new
assert_nil sponsor.association(:sponsorable).send(:klass)
+ sponsor.association(:sponsorable).reload
+ assert_nil sponsor.sponsorable
sponsor.sponsorable_type = '' # the column doesn't have to be declared NOT NULL
assert_nil sponsor.association(:sponsorable).send(:klass)
+ sponsor.association(:sponsorable).reload
+ assert_nil sponsor.sponsorable
sponsor.sponsorable = Member.new :name => "Bert"
assert_equal Member, sponsor.association(:sponsorable).send(:klass)
@@ -231,6 +349,22 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
assert_equal 1, Company.all.merge!(:includes => :firm_with_select ).find(2).firm_with_select.attributes.size
end
+ def test_belongs_to_without_counter_cache_option
+ # Ship has a conventionally named `treasures_count` column, but the counter_cache
+ # option is not given on the association.
+ ship = Ship.create(name: 'Countless')
+
+ assert_no_difference lambda { ship.reload.treasures_count }, "treasures_count should not be changed unless counter_cache is given on the relation" do
+ treasure = Treasure.new(name: 'Gold', ship: ship)
+ treasure.save
+ end
+
+ assert_no_difference lambda { ship.reload.treasures_count }, "treasures_count should not be changed unless counter_cache is given on the relation" do
+ treasure = ship.treasures.first
+ treasure.destroy
+ end
+ end
+
def test_belongs_to_counter
debate = Topic.create("title" => "debate")
assert_equal 0, debate.read_attribute("replies_count"), "No replies yet"
@@ -342,13 +476,33 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
assert_queries(1) { line_item.touch }
end
+ def test_belongs_to_with_touch_on_multiple_records
+ line_item = LineItem.create!(amount: 1)
+ line_item2 = LineItem.create!(amount: 2)
+ Invoice.create!(line_items: [line_item, line_item2])
+
+ assert_queries(1) do
+ LineItem.transaction do
+ line_item.touch
+ line_item2.touch
+ end
+ end
+
+ assert_queries(2) do
+ line_item.touch
+ line_item2.touch
+ end
+ end
+
def test_belongs_to_with_touch_option_on_touch_without_updated_at_attributes
assert_not LineItem.column_names.include?("updated_at")
line_item = LineItem.create!
invoice = Invoice.create!(line_items: [line_item])
initial = invoice.updated_at
- line_item.touch
+ travel(1.second) do
+ line_item.touch
+ end
assert_not_equal initial, invoice.reload.updated_at
end
@@ -427,7 +581,8 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
assert final_cut.persisted?
assert firm.persisted?
assert_equal firm, final_cut.firm
- assert_equal firm, final_cut.firm(true)
+ final_cut.association(:firm).reload
+ assert_equal firm, final_cut.firm
end
def test_assignment_before_child_saved_with_primary_key
@@ -439,7 +594,8 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
assert final_cut.persisted?
assert firm.persisted?
assert_equal firm, final_cut.firm_with_primary_key
- assert_equal firm, final_cut.firm_with_primary_key(true)
+ final_cut.association(:firm_with_primary_key).reload
+ assert_equal firm, final_cut.firm_with_primary_key
end
def test_new_record_with_foreign_key_but_no_object
@@ -934,6 +1090,12 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
Column.create! record: record
assert_equal 1, Column.count
end
+
+ def test_association_force_reload_with_only_true_is_deprecated
+ client = Client.find(3)
+
+ assert_deprecated { client.firm(true) }
+ end
end
class BelongsToWithForeignKeyTest < ActiveRecord::TestCase
diff --git a/activerecord/test/cases/associations/bidirectional_destroy_dependencies_test.rb b/activerecord/test/cases/associations/bidirectional_destroy_dependencies_test.rb
new file mode 100644
index 0000000000..2b867965ba
--- /dev/null
+++ b/activerecord/test/cases/associations/bidirectional_destroy_dependencies_test.rb
@@ -0,0 +1,41 @@
+require 'cases/helper'
+require 'models/content'
+
+class BidirectionalDestroyDependenciesTest < ActiveRecord::TestCase
+ fixtures :content, :content_positions
+
+ def setup
+ Content.destroyed_ids.clear
+ ContentPosition.destroyed_ids.clear
+ end
+
+ def test_bidirectional_dependence_when_destroying_item_with_belongs_to_association
+ content_position = ContentPosition.find(1)
+ content = content_position.content
+ assert_not_nil content
+
+ content_position.destroy
+
+ assert_equal [content_position.id], ContentPosition.destroyed_ids
+ assert_equal [content.id], Content.destroyed_ids
+ end
+
+ def test_bidirectional_dependence_when_destroying_item_with_has_one_association
+ content = Content.find(1)
+ content_position = content.content_position
+ assert_not_nil content_position
+
+ content.destroy
+
+ assert_equal [content.id], Content.destroyed_ids
+ assert_equal [content_position.id], ContentPosition.destroyed_ids
+ end
+
+ def test_bidirectional_dependence_when_destroying_item_with_has_one_association_fails_first_time
+ content = ContentWhichRequiresTwoDestroyCalls.find(1)
+
+ 2.times { content.destroy }
+
+ assert_equal content.destroyed?, true
+ end
+end
diff --git a/activerecord/test/cases/associations/callbacks_test.rb b/activerecord/test/cases/associations/callbacks_test.rb
index 5b7e462f64..a531e0e02c 100644
--- a/activerecord/test/cases/associations/callbacks_test.rb
+++ b/activerecord/test/cases/associations/callbacks_test.rb
@@ -3,6 +3,7 @@ require 'models/post'
require 'models/author'
require 'models/project'
require 'models/developer'
+require 'models/computer'
require 'models/company'
class AssociationCallbacksTest < ActiveRecord::TestCase
diff --git a/activerecord/test/cases/associations/deprecated_counter_cache_on_has_many_through_test.rb b/activerecord/test/cases/associations/deprecated_counter_cache_on_has_many_through_test.rb
deleted file mode 100644
index 48f7ddbe83..0000000000
--- a/activerecord/test/cases/associations/deprecated_counter_cache_on_has_many_through_test.rb
+++ /dev/null
@@ -1,26 +0,0 @@
-require "cases/helper"
-
-class DeprecatedCounterCacheOnHasManyThroughTest < ActiveRecord::TestCase
- class Post < ActiveRecord::Base
- has_many :taggings, as: :taggable
- has_many :tags, through: :taggings
- end
-
- class Tagging < ActiveRecord::Base
- belongs_to :taggable, polymorphic: true
- belongs_to :tag
- end
-
- class Tag < ActiveRecord::Base
- end
-
- test "counter caches are updated in the database if the belongs_to association doesn't specify a counter cache" do
- post = Post.create!(title: 'Hello', body: 'World!')
- assert_deprecated { post.tags << Tag.create!(name: 'whatever') }
-
- assert_equal 1, post.tags.size
- assert_equal 1, post.tags_count
- assert_equal 1, post.reload.tags.size
- assert_equal 1, post.reload.tags_count
- end
-end
diff --git a/activerecord/test/cases/associations/eager_load_nested_include_test.rb b/activerecord/test/cases/associations/eager_load_nested_include_test.rb
index 0ff87d53ea..f571198079 100644
--- a/activerecord/test/cases/associations/eager_load_nested_include_test.rb
+++ b/activerecord/test/cases/associations/eager_load_nested_include_test.rb
@@ -70,9 +70,7 @@ class EagerLoadPolyAssocsTest < ActiveRecord::TestCase
teardown do
[Circle, Square, Triangle, PaintColor, PaintTexture,
- ShapeExpression, NonPolyOne, NonPolyTwo].each do |c|
- c.delete_all
- end
+ ShapeExpression, NonPolyOne, NonPolyTwo].each(&:delete_all)
end
def generate_test_object_graphs
diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb
index 21912fdf0f..628ea1c764 100644
--- a/activerecord/test/cases/associations/eager_test.rb
+++ b/activerecord/test/cases/associations/eager_test.rb
@@ -17,12 +17,15 @@ require 'models/subscriber'
require 'models/subscription'
require 'models/book'
require 'models/developer'
+require 'models/computer'
require 'models/project'
require 'models/member'
require 'models/membership'
require 'models/club'
require 'models/categorization'
require 'models/sponsor'
+require 'models/mentor'
+require 'models/contract'
class EagerAssociationTest < ActiveRecord::TestCase
fixtures :posts, :comments, :authors, :essays, :author_addresses, :categories, :categories_posts,
@@ -76,9 +79,17 @@ class EagerAssociationTest < ActiveRecord::TestCase
def test_has_many_through_with_order
authors = Author.includes(:favorite_authors).to_a
+ assert authors.count > 0
assert_no_queries { authors.map(&:favorite_authors) }
end
+ def test_eager_loaded_has_one_association_with_references_does_not_run_additional_queries
+ Post.update_all(author_id: nil)
+ authors = Author.includes(:post).references(:post).to_a
+ assert authors.count > 0
+ assert_no_queries { authors.map(&:post) }
+ end
+
def test_with_two_tables_in_from_without_getting_double_quoted
posts = Post.select("posts.*").from("authors, posts").eager_load(:comments).where("posts.author_id = authors.id").order("posts.id").to_a
assert_equal 2, posts.first.comments.size
@@ -99,53 +110,57 @@ class EagerAssociationTest < ActiveRecord::TestCase
end
def test_preloading_has_many_in_multiple_queries_with_more_ids_than_database_can_handle
- Comment.connection.expects(:in_clause_length).at_least_once.returns(5)
- posts = Post.all.merge!(:includes=>:comments).to_a
- assert_equal 11, posts.size
+ assert_called(Comment.connection, :in_clause_length, returns: 5) do
+ posts = Post.all.merge!(:includes=>:comments).to_a
+ assert_equal 11, posts.size
+ end
end
def test_preloading_has_many_in_one_queries_when_database_has_no_limit_on_ids_it_can_handle
- Comment.connection.expects(:in_clause_length).at_least_once.returns(nil)
- posts = Post.all.merge!(:includes=>:comments).to_a
- assert_equal 11, posts.size
+ assert_called(Comment.connection, :in_clause_length, returns: nil) do
+ posts = Post.all.merge!(:includes=>:comments).to_a
+ assert_equal 11, posts.size
+ end
end
def test_preloading_habtm_in_multiple_queries_with_more_ids_than_database_can_handle
- Comment.connection.expects(:in_clause_length).at_least_once.returns(5)
- posts = Post.all.merge!(:includes=>:categories).to_a
- assert_equal 11, posts.size
+ assert_called(Comment.connection, :in_clause_length, times: 2, returns: 5) do
+ posts = Post.all.merge!(:includes=>:categories).to_a
+ assert_equal 11, posts.size
+ end
end
def test_preloading_habtm_in_one_queries_when_database_has_no_limit_on_ids_it_can_handle
- Comment.connection.expects(:in_clause_length).at_least_once.returns(nil)
- posts = Post.all.merge!(:includes=>:categories).to_a
- assert_equal 11, posts.size
+ assert_called(Comment.connection, :in_clause_length, times: 2, returns: nil) do
+ posts = Post.all.merge!(:includes=>:categories).to_a
+ assert_equal 11, posts.size
+ end
end
def test_load_associated_records_in_one_query_when_adapter_has_no_limit
- Comment.connection.expects(:in_clause_length).at_least_once.returns(nil)
-
- post = posts(:welcome)
- assert_queries(2) do
- Post.includes(:comments).where(:id => post.id).to_a
+ assert_called(Comment.connection, :in_clause_length, returns: nil) do
+ post = posts(:welcome)
+ assert_queries(2) do
+ Post.includes(:comments).where(:id => post.id).to_a
+ end
end
end
def test_load_associated_records_in_several_queries_when_many_ids_passed
- Comment.connection.expects(:in_clause_length).at_least_once.returns(1)
-
- post1, post2 = posts(:welcome), posts(:thinking)
- assert_queries(3) do
- Post.includes(:comments).where(:id => [post1.id, post2.id]).to_a
+ assert_called(Comment.connection, :in_clause_length, returns: 1) do
+ post1, post2 = posts(:welcome), posts(:thinking)
+ assert_queries(3) do
+ Post.includes(:comments).where(:id => [post1.id, post2.id]).to_a
+ end
end
end
def test_load_associated_records_in_one_query_when_a_few_ids_passed
- Comment.connection.expects(:in_clause_length).at_least_once.returns(3)
-
- post = posts(:welcome)
- assert_queries(2) do
- Post.includes(:comments).where(:id => post.id).to_a
+ assert_called(Comment.connection, :in_clause_length, returns: 3) do
+ post = posts(:welcome)
+ assert_queries(2) do
+ Post.includes(:comments).where(:id => post.id).to_a
+ end
end
end
@@ -269,6 +284,14 @@ class EagerAssociationTest < ActiveRecord::TestCase
end
end
+ def test_three_level_nested_preloading_does_not_raise_exception_when_association_does_not_exist
+ post_id = Comment.where(author_id: nil).where.not(post_id: nil).first.post_id
+
+ assert_nothing_raised do
+ Post.preload(:comments => [{:author => :essays}]).find(post_id)
+ end
+ end
+
def test_nested_loading_through_has_one_association
aa = AuthorAddress.all.merge!(:includes => {:author => :posts}).find(author_addresses(:david_address).id)
assert_equal aa.author.posts.count, aa.author.posts.length
@@ -329,31 +352,31 @@ class EagerAssociationTest < ActiveRecord::TestCase
def test_eager_association_loading_with_belongs_to_and_limit
comments = Comment.all.merge!(:includes => :post, :limit => 5, :order => 'comments.id').to_a
assert_equal 5, comments.length
- assert_equal [1,2,3,5,6], comments.collect { |c| c.id }
+ assert_equal [1,2,3,5,6], comments.collect(&:id)
end
def test_eager_association_loading_with_belongs_to_and_limit_and_conditions
comments = Comment.all.merge!(:includes => :post, :where => 'post_id = 4', :limit => 3, :order => 'comments.id').to_a
assert_equal 3, comments.length
- assert_equal [5,6,7], comments.collect { |c| c.id }
+ assert_equal [5,6,7], comments.collect(&:id)
end
def test_eager_association_loading_with_belongs_to_and_limit_and_offset
comments = Comment.all.merge!(:includes => :post, :limit => 3, :offset => 2, :order => 'comments.id').to_a
assert_equal 3, comments.length
- assert_equal [3,5,6], comments.collect { |c| c.id }
+ assert_equal [3,5,6], comments.collect(&:id)
end
def test_eager_association_loading_with_belongs_to_and_limit_and_offset_and_conditions
comments = Comment.all.merge!(:includes => :post, :where => 'post_id = 4', :limit => 3, :offset => 1, :order => 'comments.id').to_a
assert_equal 3, comments.length
- assert_equal [6,7,8], comments.collect { |c| c.id }
+ assert_equal [6,7,8], comments.collect(&:id)
end
def test_eager_association_loading_with_belongs_to_and_limit_and_offset_and_conditions_array
comments = Comment.all.merge!(:includes => :post, :where => ['post_id = ?',4], :limit => 3, :offset => 1, :order => 'comments.id').to_a
assert_equal 3, comments.length
- assert_equal [6,7,8], comments.collect { |c| c.id }
+ assert_equal [6,7,8], comments.collect(&:id)
end
def test_eager_association_loading_with_belongs_to_and_conditions_string_with_unquoted_table_name
@@ -368,7 +391,7 @@ class EagerAssociationTest < ActiveRecord::TestCase
comments = Comment.all.merge!(:includes => :post, :where => {:posts => {:id => 4}}, :limit => 3, :order => 'comments.id').to_a
end
assert_equal 3, comments.length
- assert_equal [5,6,7], comments.collect { |c| c.id }
+ assert_equal [5,6,7], comments.collect(&:id)
assert_no_queries do
comments.first.post
end
@@ -397,13 +420,13 @@ class EagerAssociationTest < ActiveRecord::TestCase
def test_eager_association_loading_with_belongs_to_and_limit_and_multiple_associations
posts = Post.all.merge!(:includes => [:author, :very_special_comment], :limit => 1, :order => 'posts.id').to_a
assert_equal 1, posts.length
- assert_equal [1], posts.collect { |p| p.id }
+ assert_equal [1], posts.collect(&:id)
end
def test_eager_association_loading_with_belongs_to_and_limit_and_offset_and_multiple_associations
posts = Post.all.merge!(:includes => [:author, :very_special_comment], :limit => 1, :offset => 1, :order => 'posts.id').to_a
assert_equal 1, posts.length
- assert_equal [2], posts.collect { |p| p.id }
+ assert_equal [2], posts.collect(&:id)
end
def test_eager_association_loading_with_belongs_to_inferred_foreign_key_from_association_name
@@ -494,8 +517,8 @@ class EagerAssociationTest < ActiveRecord::TestCase
end
def test_eager_with_has_many_through_an_sti_join_model_with_conditions_on_both
- author = Author.all.merge!(:includes => :special_nonexistant_post_comments, :order => 'authors.id').first
- assert_equal [], author.special_nonexistant_post_comments
+ author = Author.all.merge!(:includes => :special_nonexistent_post_comments, :order => 'authors.id').first
+ assert_equal [], author.special_nonexistent_post_comments
end
def test_eager_with_has_many_through_join_model_with_conditions
@@ -536,13 +559,13 @@ class EagerAssociationTest < ActiveRecord::TestCase
def test_eager_with_has_many_and_limit_and_conditions
posts = Post.all.merge!(:includes => [ :author, :comments ], :limit => 2, :where => "posts.body = 'hello'", :order => "posts.id").to_a
assert_equal 2, posts.size
- assert_equal [4,5], posts.collect { |p| p.id }
+ assert_equal [4,5], posts.collect(&:id)
end
def test_eager_with_has_many_and_limit_and_conditions_array
posts = Post.all.merge!(:includes => [ :author, :comments ], :limit => 2, :where => [ "posts.body = ?", 'hello' ], :order => "posts.id").to_a
assert_equal 2, posts.size
- assert_equal [4,5], posts.collect { |p| p.id }
+ assert_equal [4,5], posts.collect(&:id)
end
def test_eager_with_has_many_and_limit_and_conditions_array_on_the_eagers
@@ -742,6 +765,23 @@ class EagerAssociationTest < ActiveRecord::TestCase
end
end
+ def test_eager_with_default_scope_as_class_method_using_find_method
+ david = developers(:david)
+ developer = EagerDeveloperWithClassMethodDefaultScope.find(david.id)
+ projects = Project.order(:id).to_a
+ assert_no_queries do
+ assert_equal(projects, developer.projects)
+ end
+ end
+
+ def test_eager_with_default_scope_as_class_method_using_find_by_method
+ developer = EagerDeveloperWithClassMethodDefaultScope.find_by(name: 'David')
+ projects = Project.order(:id).to_a
+ assert_no_queries do
+ assert_equal(projects, developer.projects)
+ end
+ end
+
def test_eager_with_default_scope_as_lambda
developer = EagerDeveloperWithLambdaDefaultScope.where(:name => 'David').first
projects = Project.order(:id).to_a
@@ -817,18 +857,6 @@ class EagerAssociationTest < ActiveRecord::TestCase
)
end
- def test_preload_with_interpolation
- assert_deprecated do
- post = Post.includes(:comments_with_interpolated_conditions).find(posts(:welcome).id)
- assert_equal [comments(:greetings)], post.comments_with_interpolated_conditions
- end
-
- assert_deprecated do
- post = Post.joins(:comments_with_interpolated_conditions).find(posts(:welcome).id)
- assert_equal [comments(:greetings)], post.comments_with_interpolated_conditions
- end
- end
-
def test_polymorphic_type_condition
post = Post.all.merge!(:includes => :taggings).find(posts(:thinking).id)
assert post.taggings.include?(taggings(:thinking_general))
@@ -903,6 +931,12 @@ class EagerAssociationTest < ActiveRecord::TestCase
assert_no_queries {assert_equal posts(:sti_comments), comment.post}
end
+ def test_eager_association_with_scope_with_joins
+ assert_nothing_raised do
+ Post.includes(:very_special_comment_with_post_with_joins).to_a
+ end
+ end
+
def test_preconfigured_includes_with_has_many
posts = authors(:david).posts_with_comments
one = posts.detect { |p| p.id == 1 }
@@ -935,6 +969,42 @@ class EagerAssociationTest < ActiveRecord::TestCase
assert_equal 3, authors(:david).posts_with_comments.where("length(comments.body) > 15").references(:comments).count
end
+ def test_association_loading_notification
+ notifications = messages_for('instantiation.active_record') do
+ Developer.all.merge!(:includes => 'projects', :where => { 'developers_projects.access_level' => 1 }, :limit => 5).to_a.size
+ end
+
+ message = notifications.first
+ payload = message.last
+ count = Developer.all.merge!(:includes => 'projects', :where => { 'developers_projects.access_level' => 1 }, :limit => 5).to_a.size
+
+ # eagerloaded row count should be greater than just developer count
+ assert_operator payload[:record_count], :>, count
+ assert_equal Developer.name, payload[:class_name]
+ end
+
+ def test_base_messages
+ notifications = messages_for('instantiation.active_record') do
+ Developer.all.to_a
+ end
+ message = notifications.first
+ payload = message.last
+
+ assert_equal Developer.all.to_a.count, payload[:record_count]
+ assert_equal Developer.name, payload[:class_name]
+ end
+
+ def messages_for(name)
+ notifications = []
+ ActiveSupport::Notifications.subscribe(name) do |*args|
+ notifications << args
+ end
+ yield
+ notifications
+ ensure
+ ActiveSupport::Notifications.unsubscribe(name)
+ end
+
def test_load_with_sti_sharing_association
assert_queries(2) do #should not do 1 query per subclass
Comment.includes(:post).to_a
@@ -1103,12 +1173,30 @@ class EagerAssociationTest < ActiveRecord::TestCase
assert_no_queries { assert client.accounts.empty? }
end
- def test_preloading_has_many_through_with_uniq
+ def test_preloading_has_many_through_with_distinct
mary = Author.includes(:unique_categorized_posts).where(:id => authors(:mary).id).first
assert_equal 1, mary.unique_categorized_posts.length
assert_equal 1, mary.unique_categorized_post_ids.length
end
+ def test_preloading_has_one_using_reorder
+ klass = Class.new(ActiveRecord::Base) do
+ def self.name; "TempAuthor"; end
+ self.table_name = "authors"
+ has_one :post, class_name: "PostWithDefaultScope", foreign_key: :author_id
+ has_one :reorderd_post, -> { reorder(title: :desc) }, class_name: "PostWithDefaultScope", foreign_key: :author_id
+ end
+
+ author = klass.first
+ # PRECONDITION: make sure ordering results in different results
+ assert_not_equal author.post, author.reorderd_post
+
+ preloaded_reorderd_post = klass.preload(:reorderd_post).first.reorderd_post
+
+ assert_equal author.reorderd_post, preloaded_reorderd_post
+ assert_equal posts(:sti_post_and_comments).title, preloaded_reorderd_post.title
+ end
+
def test_preloading_polymorphic_with_custom_foreign_type
sponsor = sponsors(:moustache_club_sponsor_for_groucho)
groucho = members(:groucho)
@@ -1150,6 +1238,16 @@ class EagerAssociationTest < ActiveRecord::TestCase
assert_no_queries { assert_equal 2, posts[1].categories[0].categorizations.length }
end
+ def test_eager_load_multiple_associations_with_references
+ mentor = Mentor.create!(name: "Barış Can DAYLIK")
+ developer = Developer.create!(name: "Mehmet Emin İNAÇ", mentor: mentor)
+ Contract.create!(developer: developer)
+ project = Project.create!(name: "VNGRS", mentor: mentor)
+ project.developers << developer
+ projects = Project.references(:mentors).includes(mentor: { developers: :contracts }, developers: :contracts)
+ assert_equal projects.last.mentor.developers.first.contracts, projects.last.developers.last.contracts
+ end
+
test "scoping with a circular preload" do
assert_equal Comment.find(1), Comment.preload(:post => :comments).scoping { Comment.find(1) }
end
@@ -1243,25 +1341,32 @@ class EagerAssociationTest < ActiveRecord::TestCase
assert_equal pets(:parrot), Owner.including_last_pet.first.last_pet
end
- test "include instance dependent associations is deprecated" do
+ test "preloading and eager loading of instance dependent associations is not supported" do
message = "association scope 'posts_with_signature' is"
- assert_deprecated message do
- begin
- Author.includes(:posts_with_signature).to_a
- rescue NoMethodError
- # it's expected that preloading of this association fails
- end
+ error = assert_raises(ArgumentError) do
+ Author.includes(:posts_with_signature).to_a
end
+ assert_match message, error.message
- assert_deprecated message do
- Author.preload(:posts_with_signature).to_a rescue NoMethodError
+ error = assert_raises(ArgumentError) do
+ Author.preload(:posts_with_signature).to_a
end
+ assert_match message, error.message
- assert_deprecated message do
+ error = assert_raises(ArgumentError) do
Author.eager_load(:posts_with_signature).to_a
end
+ assert_match message, error.message
end
+ test "preload with invalid argument" do
+ exception = assert_raises(ArgumentError) do
+ Author.preload(10).to_a
+ end
+ assert_equal('10 was not recognized for preload', exception.message)
+ end
+
+
test "preloading readonly association" do
# has-one
firm = Firm.where(id: "1").preload(:readonly_account).first!
@@ -1277,7 +1382,6 @@ class EagerAssociationTest < ActiveRecord::TestCase
end
test "eager-loading readonly association" do
- skip "eager_load does not yet preserve readonly associations"
# has-one
firm = Firm.where(id: "1").eager_load(:readonly_account).first!
assert firm.readonly_account.readonly?
@@ -1289,5 +1393,19 @@ class EagerAssociationTest < ActiveRecord::TestCase
# has-many :through
david = Author.where(id: "1").eager_load(:readonly_comments).first!
assert david.readonly_comments.first.readonly?
+
+ # belongs_to
+ post = Post.where(id: "1").eager_load(:author).first!
+ assert post.author.readonly?
+ end
+
+ test "preloading a polymorphic association with references to the associated table" do
+ post = Post.includes(:tags).references(:tags).where('tags.name = ?', 'General').first
+ assert_equal posts(:welcome), post
+ end
+
+ test "eager-loading a polymorphic association with references to the associated table" do
+ post = Post.eager_load(:tags).where('tags.name = ?', 'General').first
+ assert_equal posts(:welcome), post
end
end
diff --git a/activerecord/test/cases/associations/extension_test.rb b/activerecord/test/cases/associations/extension_test.rb
index 4c1fdfdd9a..b161cde335 100644
--- a/activerecord/test/cases/associations/extension_test.rb
+++ b/activerecord/test/cases/associations/extension_test.rb
@@ -3,6 +3,7 @@ require 'models/post'
require 'models/comment'
require 'models/project'
require 'models/developer'
+require 'models/computer'
require 'models/company_in_module'
class AssociationsExtensionsTest < ActiveRecord::TestCase
@@ -75,7 +76,6 @@ class AssociationsExtensionsTest < ActiveRecord::TestCase
private
def extend!(model)
- builder = ActiveRecord::Associations::Builder::HasMany.new(model, :association_name, nil, {}) { }
- builder.define_extensions(model)
+ ActiveRecord::Associations::Builder::HasMany.define_extensions(model, :association_name) { }
end
end
diff --git a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb
index cc58a4a1a2..e9f679e6de 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
@@ -1,7 +1,9 @@
require "cases/helper"
require 'models/developer'
+require 'models/computer'
require 'models/project'
require 'models/company'
+require 'models/course'
require 'models/customer'
require 'models/order'
require 'models/categorization'
@@ -13,6 +15,7 @@ require 'models/tagging'
require 'models/parrot'
require 'models/person'
require 'models/pirate'
+require 'models/professor'
require 'models/treasure'
require 'models/price_estimate'
require 'models/club'
@@ -78,9 +81,32 @@ class SubDeveloper < Developer
:association_foreign_key => "developer_id"
end
+class DeveloperWithSymbolClassName < Developer
+ has_and_belongs_to_many :projects, class_name: :ProjectWithSymbolsForKeys
+end
+
+class DeveloperWithExtendOption < Developer
+ module NamedExtension
+ def category
+ 'sns'
+ end
+ end
+
+ has_and_belongs_to_many :projects, extend: NamedExtension
+end
+
+class ProjectUnscopingDavidDefaultScope < ActiveRecord::Base
+ self.table_name = 'projects'
+ has_and_belongs_to_many :developers, -> { unscope(where: 'name') },
+ class_name: "LazyBlockDeveloperCalledDavid",
+ join_table: "developers_projects",
+ foreign_key: "project_id",
+ association_foreign_key: "developer_id"
+end
+
class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
fixtures :accounts, :companies, :categories, :posts, :categories_posts, :developers, :projects, :developers_projects,
- :parrots, :pirates, :parrots_pirates, :treasures, :price_estimates, :tags, :taggings
+ :parrots, :pirates, :parrots_pirates, :treasures, :price_estimates, :tags, :taggings, :computers
def setup_data_for_habtm_case
ActiveRecord::Base.connection.execute('delete from countries_treaties')
@@ -142,8 +168,8 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
jamis.projects << action_controller
assert_equal 2, jamis.projects.size
- assert_equal 2, jamis.projects(true).size
- assert_equal 2, action_controller.developers(true).size
+ assert_equal 2, jamis.projects.reload.size
+ assert_equal 2, action_controller.developers.reload.size
end
def test_adding_type_mismatch
@@ -161,9 +187,9 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
action_controller.developers << jamis
- assert_equal 2, jamis.projects(true).size
+ assert_equal 2, jamis.projects.reload.size
assert_equal 2, action_controller.developers.size
- assert_equal 2, action_controller.developers(true).size
+ assert_equal 2, action_controller.developers.reload.size
end
def test_adding_from_the_project_fixed_timestamp
@@ -177,9 +203,9 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
action_controller.developers << jamis
assert_equal updated_at, jamis.updated_at
- assert_equal 2, jamis.projects(true).size
+ assert_equal 2, jamis.projects.reload.size
assert_equal 2, action_controller.developers.size
- assert_equal 2, action_controller.developers(true).size
+ assert_equal 2, action_controller.developers.reload.size
end
def test_adding_multiple
@@ -188,7 +214,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
aredridel.projects.reload
aredridel.projects.push(Project.find(1), Project.find(2))
assert_equal 2, aredridel.projects.size
- assert_equal 2, aredridel.projects(true).size
+ assert_equal 2, aredridel.projects.reload.size
end
def test_adding_a_collection
@@ -197,7 +223,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
aredridel.projects.reload
aredridel.projects.concat([Project.find(1), Project.find(2)])
assert_equal 2, aredridel.projects.size
- assert_equal 2, aredridel.projects(true).size
+ assert_equal 2, aredridel.projects.reload.size
end
def test_habtm_adding_before_save
@@ -212,7 +238,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
assert_equal no_of_devels+1, Developer.count
assert_equal no_of_projects+1, Project.count
assert_equal 2, aredridel.projects.size
- assert_equal 2, aredridel.projects(true).size
+ assert_equal 2, aredridel.projects.reload.size
end
def test_habtm_saving_multiple_relationships
@@ -229,7 +255,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
assert_equal developers, new_project.developers
end
- def test_habtm_unique_order_preserved
+ def test_habtm_distinct_order_preserved
assert_equal developers(:poor_jamis, :jamis, :david), projects(:active_record).non_unique_developers
assert_equal developers(:poor_jamis, :jamis, :david), projects(:active_record).developers
end
@@ -254,7 +280,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
def test_build
devel = Developer.find(1)
- proj = assert_no_queries { devel.projects.build("name" => "Projekt") }
+ proj = assert_no_queries(ignore_none: false) { devel.projects.build("name" => "Projekt") }
assert !devel.projects.loaded?
assert_equal devel.projects.last, proj
@@ -269,7 +295,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
def test_new_aliased_to_build
devel = Developer.find(1)
- proj = assert_no_queries { devel.projects.new("name" => "Projekt") }
+ proj = assert_no_queries(ignore_none: false) { devel.projects.new("name" => "Projekt") }
assert !devel.projects.loaded?
assert_equal devel.projects.last, proj
@@ -334,7 +360,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
assert_equal 'Yet Another Testing Title', another_post.title
end
- def test_uniq_after_the_fact
+ def test_distinct_after_the_fact
dev = developers(:jamis)
dev.projects << projects(:active_record)
dev.projects << projects(:active_record)
@@ -343,13 +369,13 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
assert_equal 1, dev.projects.distinct.size
end
- def test_uniq_before_the_fact
+ def test_distinct_before_the_fact
projects(:active_record).developers << developers(:jamis)
projects(:active_record).developers << developers(:david)
assert_equal 3, projects(:active_record, :reload).developers.size
end
- def test_uniq_option_prevents_duplicate_push
+ def test_distinct_option_prevents_duplicate_push
project = projects(:active_record)
project.developers << developers(:jamis)
project.developers << developers(:david)
@@ -360,7 +386,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
assert_equal 3, project.developers.size
end
- def test_uniq_when_association_already_loaded
+ def test_distinct_when_association_already_loaded
project = projects(:active_record)
project.developers << [ developers(:jamis), developers(:david), developers(:jamis), developers(:david) ]
assert_equal 3, Project.includes(:developers).find(project.id).developers.size
@@ -376,8 +402,8 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
david.projects.delete(active_record)
assert_equal 1, david.projects.size
- assert_equal 1, david.projects(true).size
- assert_equal 2, active_record.developers(true).size
+ assert_equal 1, david.projects.reload.size
+ assert_equal 2, active_record.developers.reload.size
end
def test_deleting_array
@@ -385,7 +411,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
david.projects.reload
david.projects.delete(Project.all.to_a)
assert_equal 0, david.projects.size
- assert_equal 0, david.projects(true).size
+ assert_equal 0, david.projects.reload.size
end
def test_deleting_all
@@ -393,7 +419,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
david.projects.reload
david.projects.clear
assert_equal 0, david.projects.size
- assert_equal 0, david.projects(true).size
+ assert_equal 0, david.projects.reload.size
end
def test_removing_associations_on_destroy
@@ -419,7 +445,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
assert join_records.empty?
assert_equal 1, david.reload.projects.size
- assert_equal 1, david.projects(true).size
+ assert_equal 1, david.projects.reload.size
end
def test_destroying_many
@@ -435,7 +461,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
assert join_records.empty?
assert_equal 0, david.reload.projects.size
- assert_equal 0, david.projects(true).size
+ assert_equal 0, david.projects.reload.size
end
def test_destroy_all
@@ -451,7 +477,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
assert join_records.empty?
assert david.projects.empty?
- assert david.projects(true).empty?
+ assert david.projects.reload.empty?
end
def test_destroy_associations_destroys_multiple_associations
@@ -467,11 +493,11 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
join_records = Parrot.connection.select_all("SELECT * FROM parrots_pirates WHERE parrot_id = #{george.id}")
assert join_records.empty?
- assert george.pirates(true).empty?
+ assert george.pirates.reload.empty?
join_records = Parrot.connection.select_all("SELECT * FROM parrots_treasures WHERE parrot_id = #{george.id}")
assert join_records.empty?
- assert george.treasures(true).empty?
+ assert george.treasures.reload.empty?
end
def test_associations_with_conditions
@@ -503,7 +529,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
developer = project.developers.first
- assert_no_queries do
+ assert_no_queries(ignore_none: false) do
assert project.developers.loaded?
assert project.developers.include?(developer)
end
@@ -550,7 +576,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
def test_dynamic_find_all_should_respect_readonly_access
projects(:active_record).readonly_developers.each { |d| assert_raise(ActiveRecord::ReadOnlyRecord) { d.save! } if d.valid?}
- projects(:active_record).readonly_developers.each { |d| d.readonly? }
+ projects(:active_record).readonly_developers.each(&:readonly?)
end
def test_new_with_values_in_collection
@@ -572,6 +598,11 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
assert_equal developers(:poor_jamis), projects(:active_record).developers.where("salary < 10000").first
end
+ def test_association_with_extend_option
+ eponine = DeveloperWithExtendOption.create(name: 'Eponine')
+ assert_equal 'sns', eponine.projects.category
+ end
+
def test_replace_with_less
david = developers(:david)
david.projects = [projects(:action_controller)]
@@ -634,7 +665,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
end
def test_habtm_respects_select
- categories(:technology).select_testing_posts(true).each do |o|
+ categories(:technology).select_testing_posts.reload.each do |o|
assert_respond_to o, :correctness_marker
end
assert_respond_to categories(:technology).select_testing_posts.first, :correctness_marker
@@ -706,7 +737,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
def test_get_ids_for_loaded_associations
developer = developers(:david)
- developer.projects(true)
+ developer.projects.reload
assert_queries(0) do
developer.project_ids
developer.project_ids
@@ -774,9 +805,10 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
end
def test_association_proxy_transaction_method_starts_transaction_in_association_class
- Post.expects(:transaction)
- Category.first.posts.transaction do
- # nothing
+ assert_called(Post, :transaction) do
+ Category.first.posts.transaction do
+ # nothing
+ end
end
end
@@ -824,7 +856,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
def test_has_and_belongs_to_many_associations_on_new_records_use_null_relations
projects = Developer.new.projects
- assert_no_queries do
+ assert_no_queries(ignore_none: false) do
assert_equal [], projects
assert_equal [], projects.where(title: 'omg')
assert_equal [], projects.pluck(:title)
@@ -883,4 +915,65 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
child.special_projects << SpecialProject.new("name" => "Special Project")
assert child.save, 'child object should be saved'
end
+
+ def test_habtm_with_reflection_using_class_name_and_fixtures
+ assert_not_nil Developer._reflections['shared_computers']
+ # Checking the fixture for named association is important here, because it's the only way
+ # we've been able to reproduce this bug
+ assert_not_nil File.read(File.expand_path("../../../fixtures/developers.yml", __FILE__)).index("shared_computers")
+ assert_equal developers(:david).shared_computers.first, computers(:laptop)
+ end
+
+ def test_with_symbol_class_name
+ assert_nothing_raised NoMethodError do
+ DeveloperWithSymbolClassName.new
+ end
+ end
+
+ def test_association_force_reload_with_only_true_is_deprecated
+ developer = Developer.find(1)
+
+ assert_deprecated { developer.projects(true) }
+ end
+
+ def test_alternate_database
+ professor = Professor.create(name: "Plum")
+ course = Course.create(name: "Forensics")
+ assert_equal 0, professor.courses.count
+ assert_nothing_raised do
+ professor.courses << course
+ end
+ assert_equal 1, professor.courses.count
+ end
+
+ def test_habtm_scope_can_unscope
+ project = ProjectUnscopingDavidDefaultScope.new
+ project.save!
+
+ developer = LazyBlockDeveloperCalledDavid.new(name: "Not David")
+ developer.save!
+ project.developers << developer
+
+ projects = ProjectUnscopingDavidDefaultScope.includes(:developers).where(id: project.id)
+ assert_equal 1, projects.first.developers.size
+ end
+
+ def test_preloaded_associations_size
+ assert_equal Project.first.salaried_developers.size,
+ Project.preload(:salaried_developers).first.salaried_developers.size
+
+ assert_equal Project.includes(:salaried_developers).references(:salaried_developers).first.salaried_developers.size,
+ Project.preload(:salaried_developers).first.salaried_developers.size
+
+ # Nested HATBM
+ first_project = Developer.first.projects.first
+ preloaded_first_project =
+ Developer.preload(projects: :salaried_developers).
+ first.
+ projects.
+ detect { |p| p.id == first_project.id }
+
+ assert preloaded_first_project.salaried_developers.loaded?, true
+ assert_equal first_project.salaried_developers.size, preloaded_first_project.salaried_developers.size
+ end
end
diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb
index fe961e871c..eb94870a35 100644
--- a/activerecord/test/cases/associations/has_many_associations_test.rb
+++ b/activerecord/test/cases/associations/has_many_associations_test.rb
@@ -1,11 +1,13 @@
require "cases/helper"
require 'models/developer'
+require 'models/computer'
require 'models/project'
require 'models/company'
require 'models/contract'
require 'models/topic'
require 'models/reply'
require 'models/category'
+require 'models/image'
require 'models/post'
require 'models/author'
require 'models/essay'
@@ -28,6 +30,14 @@ require 'models/college'
require 'models/student'
require 'models/pirate'
require 'models/ship'
+require 'models/ship_part'
+require 'models/treasure'
+require 'models/parrot'
+require 'models/tyre'
+require 'models/subscriber'
+require 'models/subscription'
+require 'models/zine'
+require 'models/interest'
class HasManyAssociationsTestForReorderWithJoinDependency < ActiveRecord::TestCase
fixtures :authors, :posts, :comments
@@ -40,12 +50,59 @@ class HasManyAssociationsTestForReorderWithJoinDependency < ActiveRecord::TestCa
end
end
+class HasManyAssociationsTestPrimaryKeys < ActiveRecord::TestCase
+ fixtures :authors, :essays, :subscribers, :subscriptions, :people
+
+ def test_custom_primary_key_on_new_record_should_fetch_with_query
+ subscriber = Subscriber.new(nick: 'webster132')
+ assert !subscriber.subscriptions.loaded?
+
+ assert_queries 1 do
+ assert_equal 2, subscriber.subscriptions.size
+ end
+
+ assert_equal subscriber.subscriptions, Subscription.where(subscriber_id: 'webster132')
+ end
+
+ def test_association_primary_key_on_new_record_should_fetch_with_query
+ author = Author.new(:name => "David")
+ assert !author.essays.loaded?
+
+ assert_queries 1 do
+ assert_equal 1, author.essays.size
+ end
+
+ assert_equal author.essays, Essay.where(writer_id: "David")
+ end
+
+ def test_has_many_custom_primary_key
+ david = authors(:david)
+ assert_equal david.essays, Essay.where(writer_id: "David")
+ end
+
+ def test_has_many_assignment_with_custom_primary_key
+ david = people(:david)
+
+ assert_equal ["A Modest Proposal"], david.essays.map(&:name)
+ david.essays = [Essay.create!(name: "Remote Work" )]
+ assert_equal ["Remote Work"], david.essays.map(&:name)
+ end
+
+ def test_blank_custom_primary_key_on_new_record_should_not_run_queries
+ author = Author.new
+ assert !author.essays.loaded?
+
+ assert_queries 0 do
+ assert_equal 0, author.essays.size
+ end
+ end
+end
class HasManyAssociationsTest < ActiveRecord::TestCase
fixtures :accounts, :categories, :companies, :developers, :projects,
:developers_projects, :topics, :authors, :comments,
- :people, :posts, :readers, :taggings, :cars, :essays,
- :categorizations, :jobs, :tags
+ :posts, :readers, :taggings, :cars, :jobs, :tags,
+ :categorizations, :zines, :interests
def setup
Client.destroyed_client_ids.clear
@@ -64,9 +121,9 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
developer_project = Class.new(ActiveRecord::Base) {
self.table_name = 'developers_projects'
- belongs_to :developer, :class => dev
+ belongs_to :developer, :anonymous_class => dev
}
- has_many :developer_projects, :class => developer_project, :foreign_key => 'developer_id'
+ has_many :developer_projects, :anonymous_class => developer_project, :foreign_key => 'developer_id'
}
dev = developer.first
named = Developer.find(dev.id)
@@ -75,6 +132,32 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
dev.developer_projects.map(&:project_id).sort
end
+ def test_default_scope_on_relations_is_not_cached
+ counter = 0
+ posts = Class.new(ActiveRecord::Base) {
+ self.table_name = 'posts'
+ self.inheritance_column = 'not_there'
+ post = self
+
+ comments = Class.new(ActiveRecord::Base) {
+ self.table_name = 'comments'
+ self.inheritance_column = 'not_there'
+ belongs_to :post, :anonymous_class => post
+ default_scope -> {
+ counter += 1
+ where("id = :inc", :inc => counter)
+ }
+ }
+ has_many :comments, :anonymous_class => comments, :foreign_key => 'post_id'
+ }
+ assert_equal 0, counter
+ post = posts.first
+ assert_equal 0, counter
+ sql = capture_sql { post.comments.to_a }
+ post.comments.reset
+ assert_not_equal sql, capture_sql { post.comments.to_a }
+ end
+
def test_has_many_build_with_options
college = College.create(name: 'UFMT')
Student.create(active: true, college_id: college.id, name: 'Sarah')
@@ -82,6 +165,32 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal college.students, Student.where(active: true, college_id: college.id)
end
+ def test_add_record_to_collection_should_change_its_updated_at
+ ship = Ship.create(name: 'dauntless')
+ part = ShipPart.create(name: 'cockpit')
+ updated_at = part.updated_at
+
+ travel(1.second) do
+ ship.parts << part
+ end
+
+ assert_equal part.ship, ship
+ assert_not_equal part.updated_at, updated_at
+ end
+
+ def test_clear_collection_should_not_change_updated_at
+ # GH#17161: .clear calls delete_all (and returns the association),
+ # which is intended to not touch associated objects's updated_at field
+ ship = Ship.create(name: 'dauntless')
+ part = ShipPart.create(name: 'cockpit', ship_id: ship.id)
+
+ ship.parts.clear
+ part.reload
+
+ assert_equal nil, part.ship
+ assert !part.updated_at_changed?
+ end
+
def test_create_from_association_should_respect_default_scope
car = Car.create(:name => 'honda')
assert_equal 'honda', car.name
@@ -238,16 +347,16 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
# would be convenient), because this would cause that scope to be applied to any callbacks etc.
def test_build_and_create_should_not_happen_within_scope
car = cars(:honda)
- scoped_count = car.foo_bulbs.where_values.count
+ scope = car.foo_bulbs.where_values_hash
bulb = car.foo_bulbs.build
- assert_not_equal scoped_count, bulb.scope_after_initialize.where_values.count
+ assert_not_equal scope, bulb.scope_after_initialize.where_values_hash
bulb = car.foo_bulbs.create
- assert_not_equal scoped_count, bulb.scope_after_initialize.where_values.count
+ assert_not_equal scope, bulb.scope_after_initialize.where_values_hash
bulb = car.foo_bulbs.create!
- assert_not_equal scoped_count, bulb.scope_after_initialize.where_values.count
+ assert_not_equal scope, bulb.scope_after_initialize.where_values_hash
end
def test_no_sql_should_be_fired_if_association_already_loaded
@@ -357,6 +466,45 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal companies(:another_first_firm_client), companies(:first_firm).clients_sorted_desc.find_by_type('Client')
end
+ def test_taking
+ posts(:other_by_bob).destroy
+ assert_equal posts(:misc_by_bob), authors(:bob).posts.take
+ assert_equal posts(:misc_by_bob), authors(:bob).posts.take!
+ authors(:bob).posts.to_a
+ assert_equal posts(:misc_by_bob), authors(:bob).posts.take
+ assert_equal posts(:misc_by_bob), authors(:bob).posts.take!
+ end
+
+ def test_taking_not_found
+ authors(:bob).posts.delete_all
+ assert_raise(ActiveRecord::RecordNotFound) { authors(:bob).posts.take! }
+ authors(:bob).posts.to_a
+ assert_raise(ActiveRecord::RecordNotFound) { authors(:bob).posts.take! }
+ end
+
+ def test_taking_with_a_number
+ # taking from unloaded Relation
+ bob = Author.find(authors(:bob).id)
+ assert_equal [posts(:misc_by_bob)], bob.posts.take(1)
+ bob = Author.find(authors(:bob).id)
+ assert_equal [posts(:misc_by_bob), posts(:other_by_bob)], bob.posts.take(2)
+
+ # taking from loaded Relation
+ bob.posts.to_a
+ assert_equal [posts(:misc_by_bob)], authors(:bob).posts.take(1)
+ assert_equal [posts(:misc_by_bob), posts(:other_by_bob)], authors(:bob).posts.take(2)
+ end
+
+ def test_taking_with_inverse_of
+ interests(:woodsmanship).destroy
+ interests(:survival).destroy
+
+ zine = zines(:going_out)
+ interest = zine.interests.take
+ assert_equal interests(:hunting), interest
+ assert_same zine, interest.zine
+ end
+
def test_cant_save_has_many_readonly_association
authors(:david).readonly_comments.each { |c| assert_raise(ActiveRecord::ReadOnlyRecord) { c.save! } }
authors(:david).readonly_comments.each { |c| assert c.readonly? }
@@ -386,6 +534,25 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal "Summit", Firm.all.merge!(:order => "id").first.clients_using_primary_key.first.name
end
+ def test_update_all_on_association_accessed_before_save
+ firm = Firm.new(name: 'Firm')
+ clients_proxy_id = firm.clients.object_id
+ firm.clients << Client.first
+ firm.save!
+ assert_equal firm.clients.count, firm.clients.update_all(description: 'Great!')
+ assert_not_equal clients_proxy_id, firm.clients.object_id
+ end
+
+ def test_update_all_on_association_accessed_before_save_with_explicit_foreign_key
+ # We can use the same cached proxy object because the id is available for the scope
+ firm = Firm.new(name: 'Firm', id: 100)
+ clients_proxy_id = firm.clients.object_id
+ firm.clients << Client.first
+ firm.save!
+ assert_equal firm.clients.count, firm.clients.update_all(description: 'Great!')
+ assert_equal clients_proxy_id, firm.clients.object_id
+ end
+
def test_belongs_to_sanity
c = Client.new
assert_nil c.firm, "belongs_to failed sanity check on new object"
@@ -541,7 +708,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
natural = Client.new("name" => "Natural Company")
companies(:first_firm).clients_of_firm << natural
assert_equal 3, companies(:first_firm).clients_of_firm.size # checking via the collection
- assert_equal 3, companies(:first_firm).clients_of_firm(true).size # checking using the db
+ assert_equal 3, companies(:first_firm).clients_of_firm.reload.size # checking using the db
assert_equal natural, companies(:first_firm).clients_of_firm.last
end
@@ -554,17 +721,21 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
def test_create_with_bang_on_has_many_when_parent_is_new_raises
- assert_raise(ActiveRecord::RecordNotSaved) do
+ error = assert_raise(ActiveRecord::RecordNotSaved) do
firm = Firm.new
firm.plain_clients.create! :name=>"Whoever"
end
+
+ assert_equal "You cannot call create unless the parent is saved", error.message
end
def test_regular_create_on_has_many_when_parent_is_new_raises
- assert_raise(ActiveRecord::RecordNotSaved) do
+ error = assert_raise(ActiveRecord::RecordNotSaved) do
firm = Firm.new
firm.plain_clients.create :name=>"Whoever"
end
+
+ assert_equal "You cannot call create unless the parent is saved", error.message
end
def test_create_with_bang_on_has_many_raises_when_record_not_saved
@@ -575,9 +746,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
def test_create_with_bang_on_habtm_when_parent_is_new_raises
- assert_raise(ActiveRecord::RecordNotSaved) do
+ error = assert_raise(ActiveRecord::RecordNotSaved) do
Developer.new("name" => "Aredridel").projects.create!
end
+
+ assert_equal "You cannot call create unless the parent is saved", error.message
end
def test_adding_a_mismatch_class
@@ -590,7 +763,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
force_signal37_to_load_all_clients_of_firm
companies(:first_firm).clients_of_firm.concat([Client.new("name" => "Natural Company"), Client.new("name" => "Apple")])
assert_equal 4, companies(:first_firm).clients_of_firm.size
- assert_equal 4, companies(:first_firm).clients_of_firm(true).size
+ assert_equal 4, companies(:first_firm).clients_of_firm.reload.size
end
def test_transactions_when_adding_to_persisted
@@ -602,11 +775,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
rescue Client::RaisedOnSave
end
- assert !companies(:first_firm).clients_of_firm(true).include?(good)
+ assert !companies(:first_firm).clients_of_firm.reload.include?(good)
end
def test_transactions_when_adding_to_new_record
- assert_no_queries do
+ assert_no_queries(ignore_none: false) do
firm = Firm.new
firm.clients_of_firm.concat(Client.new("name" => "Natural Company"))
end
@@ -621,7 +794,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_new_aliased_to_build
company = companies(:first_firm)
- new_client = assert_no_queries { company.clients_of_firm.new("name" => "Another Client") }
+ new_client = assert_no_queries(ignore_none: false) { company.clients_of_firm.new("name" => "Another Client") }
assert !company.clients_of_firm.loaded?
assert_equal "Another Client", new_client.name
@@ -631,7 +804,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_build
company = companies(:first_firm)
- new_client = assert_no_queries { company.clients_of_firm.build("name" => "Another Client") }
+ new_client = assert_no_queries(ignore_none: false) { company.clients_of_firm.build("name" => "Another Client") }
assert !company.clients_of_firm.loaded?
assert_equal "Another Client", new_client.name
@@ -667,7 +840,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_build_many
company = companies(:first_firm)
- new_clients = assert_no_queries { company.clients_of_firm.build([{"name" => "Another Client"}, {"name" => "Another Client II"}]) }
+ new_clients = assert_no_queries(ignore_none: false) { company.clients_of_firm.build([{"name" => "Another Client"}, {"name" => "Another Client II"}]) }
assert_equal 2, new_clients.size
end
@@ -693,7 +866,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_build_via_block
company = companies(:first_firm)
- new_client = assert_no_queries { company.clients_of_firm.build {|client| client.name = "Another Client" } }
+ 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_equal "Another Client", new_client.name
@@ -703,7 +876,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_build_many_via_block
company = companies(:first_firm)
- new_clients = assert_no_queries do
+ new_clients = assert_no_queries(ignore_none: false) do
company.clients_of_firm.build([{"name" => "Another Client"}, {"name" => "Another Client II"}]) do |client|
client.name = "changed"
end
@@ -734,12 +907,12 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
new_client = companies(:first_firm).clients_of_firm.create("name" => "Another Client")
assert new_client.persisted?
assert_equal new_client, companies(:first_firm).clients_of_firm.last
- assert_equal new_client, companies(:first_firm).clients_of_firm(true).last
+ assert_equal new_client, companies(:first_firm).clients_of_firm.reload.last
end
def test_create_many
companies(:first_firm).clients_of_firm.create([{"name" => "Another Client"}, {"name" => "Another Client II"}])
- assert_equal 4, companies(:first_firm).clients_of_firm(true).size
+ assert_equal 4, companies(:first_firm).clients_of_firm.reload.size
end
def test_create_followed_by_save_does_not_load_target
@@ -752,7 +925,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
force_signal37_to_load_all_clients_of_firm
companies(:first_firm).clients_of_firm.delete(companies(:first_firm).clients_of_firm.first)
assert_equal 1, companies(:first_firm).clients_of_firm.size
- assert_equal 1, companies(:first_firm).clients_of_firm(true).size
+ assert_equal 1, companies(:first_firm).clients_of_firm.reload.size
end
def test_deleting_before_save
@@ -763,6 +936,25 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal 0, new_firm.clients_of_firm.size
end
+ def test_has_many_without_counter_cache_option
+ # Ship has a conventionally named `treasures_count` column, but the counter_cache
+ # option is not given on the association.
+ ship = Ship.create(name: 'Countless', treasures_count: 10)
+
+ assert_not Ship.reflect_on_association(:treasures).has_cached_counter?
+
+ # Count should come from sql count() of treasures rather than treasures_count attribute
+ assert_equal ship.treasures.size, 0
+
+ assert_no_difference lambda { ship.reload.treasures_count }, "treasures_count should not be changed" do
+ ship.treasures.create(name: 'Gold')
+ end
+
+ assert_no_difference lambda { ship.reload.treasures_count }, "treasures_count should not be changed" do
+ ship.treasures.destroy_all
+ end
+ end
+
def test_deleting_updates_counter_cache
topic = Topic.order("id ASC").first
assert_equal topic.replies.to_a.size, topic.replies_count
@@ -889,7 +1081,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal 3, companies(:first_firm).clients_of_firm.size
companies(:first_firm).clients_of_firm.delete([companies(:first_firm).clients_of_firm[0], companies(:first_firm).clients_of_firm[1], companies(:first_firm).clients_of_firm[2]])
assert_equal 0, companies(:first_firm).clients_of_firm.size
- assert_equal 0, companies(:first_firm).clients_of_firm(true).size
+ assert_equal 0, companies(:first_firm).clients_of_firm.reload.size
end
def test_delete_all
@@ -910,7 +1102,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
companies(:first_firm).clients_of_firm.reset
companies(:first_firm).clients_of_firm.delete_all
assert_equal 0, companies(:first_firm).clients_of_firm.size
- assert_equal 0, companies(:first_firm).clients_of_firm(true).size
+ assert_equal 0, companies(:first_firm).clients_of_firm.reload.size
end
def test_transaction_when_deleting_persisted
@@ -924,11 +1116,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
rescue Client::RaisedOnDestroy
end
- assert_equal [good, bad], companies(:first_firm).clients_of_firm(true)
+ assert_equal [good, bad], companies(:first_firm).clients_of_firm.reload
end
def test_transaction_when_deleting_new_record
- assert_no_queries do
+ assert_no_queries(ignore_none: false) do
firm = Firm.new
client = Client.new("name" => "New Client")
firm.clients_of_firm << client
@@ -944,7 +1136,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
firm.clients_of_firm.clear
assert_equal 0, firm.clients_of_firm.size
- assert_equal 0, firm.clients_of_firm(true).size
+ assert_equal 0, firm.clients_of_firm.reload.size
assert_equal [], Client.destroyed_client_ids[firm.id]
# Should not be destroyed since the association is not dependent.
@@ -980,7 +1172,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
firm.dependent_clients_of_firm.clear
assert_equal 0, firm.dependent_clients_of_firm.size
- assert_equal 0, firm.dependent_clients_of_firm(true).size
+ assert_equal 0, firm.dependent_clients_of_firm.reload.size
assert_equal [], Client.destroyed_client_ids[firm.id]
# Should be destroyed since the association is dependent.
@@ -1013,7 +1205,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
firm.exclusively_dependent_clients_of_firm.clear
assert_equal 0, firm.exclusively_dependent_clients_of_firm.size
- assert_equal 0, firm.exclusively_dependent_clients_of_firm(true).size
+ assert_equal 0, firm.exclusively_dependent_clients_of_firm.reload.size
# no destroy-filters should have been called
assert_equal [], Client.destroyed_client_ids[firm.id]
@@ -1062,7 +1254,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
# break the vanilla firm_id foreign key
assert_equal 3, firm.clients.count
firm.clients.first.update_columns(firm_id: nil)
- assert_equal 2, firm.clients(true).count
+ assert_equal 2, firm.clients.reload.count
assert_equal 2, firm.clients_using_primary_key_with_delete_all.count
old_record = firm.clients_using_primary_key_with_delete_all.first
firm = Firm.first
@@ -1088,7 +1280,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
firm.clients_of_firm.clear
assert_equal 0, firm.clients_of_firm.size
- assert_equal 0, firm.clients_of_firm(true).size
+ assert_equal 0, firm.clients_of_firm.reload.size
end
def test_deleting_a_item_which_is_not_in_the_collection
@@ -1096,7 +1288,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
summit = Client.find_by_name('Summit')
companies(:first_firm).clients_of_firm.delete(summit)
assert_equal 2, companies(:first_firm).clients_of_firm.size
- assert_equal 2, companies(:first_firm).clients_of_firm(true).size
+ assert_equal 2, companies(:first_firm).clients_of_firm.reload.size
assert_equal 2, summit.client_of
end
@@ -1134,7 +1326,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
assert_equal 1, companies(:first_firm).reload.clients_of_firm.size
- assert_equal 1, companies(:first_firm).clients_of_firm(true).size
+ assert_equal 1, companies(:first_firm).clients_of_firm.reload.size
end
def test_destroying_by_fixnum_id
@@ -1145,7 +1337,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
assert_equal 1, companies(:first_firm).reload.clients_of_firm.size
- assert_equal 1, companies(:first_firm).clients_of_firm(true).size
+ assert_equal 1, companies(:first_firm).clients_of_firm.reload.size
end
def test_destroying_by_string_id
@@ -1156,7 +1348,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
assert_equal 1, companies(:first_firm).reload.clients_of_firm.size
- assert_equal 1, companies(:first_firm).clients_of_firm(true).size
+ assert_equal 1, companies(:first_firm).clients_of_firm.reload.size
end
def test_destroying_a_collection
@@ -1169,7 +1361,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
assert_equal 1, companies(:first_firm).reload.clients_of_firm.size
- assert_equal 1, companies(:first_firm).clients_of_firm(true).size
+ assert_equal 1, companies(:first_firm).clients_of_firm.reload.size
end
def test_destroy_all
@@ -1178,9 +1370,9 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert !clients.empty?, "37signals has clients after load"
destroyed = companies(:first_firm).clients_of_firm.destroy_all
assert_equal clients.sort_by(&:id), destroyed.sort_by(&:id)
- assert destroyed.all? { |client| client.frozen? }, "destroyed clients should be frozen"
+ assert destroyed.all?(&:frozen?), "destroyed clients should be frozen"
assert companies(:first_firm).clients_of_firm.empty?, "37signals has no clients after destroy all"
- assert companies(:first_firm).clients_of_firm(true).empty?, "37signals has no clients after destroy all and refresh"
+ assert companies(:first_firm).clients_of_firm.reload.empty?, "37signals has no clients after destroy all and refresh"
end
def test_dependence
@@ -1216,7 +1408,6 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_nothing_raised { topic.destroy }
end
- uses_transaction :test_dependence_with_transaction_support_on_failure
def test_dependence_with_transaction_support_on_failure
firm = companies(:first_firm)
clients = firm.clients
@@ -1258,6 +1449,26 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert firm.companies.exists?(:name => 'child')
end
+ def test_restrict_with_error_is_deprecated_using_key_many
+ I18n.backend = I18n::Backend::Simple.new
+ I18n.backend.store_translations :en, activerecord: { errors: { messages: { restrict_dependent_destroy: { many: 'message for deprecated key' } } } }
+
+ firm = RestrictedWithErrorFirm.create!(name: 'restrict')
+ firm.companies.create(name: 'child')
+
+ assert !firm.companies.empty?
+
+ assert_deprecated { firm.destroy }
+
+ assert !firm.errors.empty?
+
+ assert_equal 'message for deprecated key', firm.errors[:base].first
+ assert RestrictedWithErrorFirm.exists?(name: 'restrict')
+ assert firm.companies.exists?(name: 'child')
+ ensure
+ I18n.backend.reload!
+ end
+
def test_restrict_with_error
firm = RestrictedWithErrorFirm.create!(:name => 'restrict')
firm.companies.create(:name => 'child')
@@ -1273,6 +1484,25 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert firm.companies.exists?(:name => 'child')
end
+ def test_restrict_with_error_with_locale
+ I18n.backend = I18n::Backend::Simple.new
+ I18n.backend.store_translations 'en', activerecord: {attributes: {restricted_with_error_firm: {companies: 'client companies'}}}
+ firm = RestrictedWithErrorFirm.create!(name: 'restrict')
+ firm.companies.create(name: 'child')
+
+ assert !firm.companies.empty?
+
+ firm.destroy
+
+ assert !firm.errors.empty?
+
+ assert_equal "Cannot delete record because dependent client companies exist", firm.errors[:base].first
+ assert RestrictedWithErrorFirm.exists?(name: 'restrict')
+ assert firm.companies.exists?(name: 'child')
+ ensure
+ I18n.backend.reload!
+ end
+
def test_included_in_collection
assert_equal true, companies(:first_firm).clients.include?(Client.find(2))
end
@@ -1318,10 +1548,13 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert !account.valid?
assert !orig_accounts.empty?
- assert_raise ActiveRecord::RecordNotSaved do
+ error = assert_raise ActiveRecord::RecordNotSaved do
firm.accounts = [account]
end
+
assert_equal orig_accounts, firm.accounts
+ assert_equal "Failed to replace accounts because one or more of the " \
+ "new records could not be saved.", error.message
end
def test_replace_with_same_content
@@ -1332,6 +1565,8 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_queries(0, ignore_none: true) do
firm.clients = []
end
+
+ assert_equal [], firm.send('clients=', [])
end
def test_transactions_when_replacing_on_persisted
@@ -1345,11 +1580,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
rescue Client::RaisedOnSave
end
- assert_equal [good], companies(:first_firm).clients_of_firm(true)
+ assert_equal [good], companies(:first_firm).clients_of_firm.reload
end
def test_transactions_when_replacing_on_new_record
- assert_no_queries do
+ assert_no_queries(ignore_none: false) do
firm = Firm.new
firm.clients_of_firm = [Client.new("name" => "New Client")]
end
@@ -1361,7 +1596,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_get_ids_for_loaded_associations
company = companies(:first_firm)
- company.clients(true)
+ company.clients.reload
assert_queries(0) do
company.client_ids
company.client_ids
@@ -1415,7 +1650,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
firm.client_ids = [companies(:first_client).id, nil, companies(:second_client).id, '']
firm.save!
- assert_equal 2, firm.clients(true).size
+ assert_equal 2, firm.clients.reload.size
assert_equal true, firm.clients.include?(companies(:second_client))
end
@@ -1487,7 +1722,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
firm.clients.load_target
assert firm.clients.loaded?
- assert_no_queries do
+ assert_no_queries(ignore_none: false) do
firm.clients.first
assert_equal 2, firm.clients.first(2).size
firm.clients.last
@@ -1533,39 +1768,6 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
end
- def test_custom_primary_key_on_new_record_should_fetch_with_query
- author = Author.new(:name => "David")
- assert !author.essays.loaded?
-
- assert_queries 1 do
- assert_equal 1, author.essays.size
- end
-
- assert_equal author.essays, Essay.where(writer_id: "David")
- end
-
- def test_has_many_custom_primary_key
- david = authors(:david)
- assert_equal david.essays, Essay.where(writer_id: "David")
- end
-
- def test_has_many_assignment_with_custom_primary_key
- david = people(:david)
-
- assert_equal ["A Modest Proposal"], david.essays.map(&:name)
- david.essays = [Essay.create!(name: "Remote Work" )]
- assert_equal ["Remote Work"], david.essays.map(&:name)
- end
-
- def test_blank_custom_primary_key_on_new_record_should_not_run_queries
- author = Author.new
- assert !author.essays.loaded?
-
- assert_queries 0 do
- assert_equal 0, author.essays.size
- end
- end
-
def test_calling_first_or_last_with_integer_on_association_should_not_load_association
firm = companies(:first_firm)
firm.clients.create(:name => 'Foo')
@@ -1618,6 +1820,82 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal 3, firm.clients.size
end
+ def test_calling_none_should_count_instead_of_loading_association
+ firm = companies(:first_firm)
+ assert_queries(1) do
+ firm.clients.none? # use count query
+ end
+ assert !firm.clients.loaded?
+ end
+
+ def test_calling_none_on_loaded_association_should_not_use_query
+ firm = companies(:first_firm)
+ firm.clients.collect # force load
+ assert_no_queries { assert ! 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 }
+ end
+ assert firm.clients.loaded?
+ end
+
+ def test_calling_none_should_return_true_if_none
+ firm = companies(:another_firm)
+ assert 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_equal 1, firm.limited_clients.size
+ end
+
+ def test_calling_one_should_count_instead_of_loading_association
+ firm = companies(:first_firm)
+ assert_queries(1) do
+ firm.clients.one? # use count query
+ end
+ assert !firm.clients.loaded?
+ end
+
+ def test_calling_one_on_loaded_association_should_not_use_query
+ firm = companies(:first_firm)
+ firm.clients.collect # force load
+ assert_no_queries { assert ! 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 }
+ end
+ assert firm.clients.loaded?
+ end
+
+ def test_calling_one_should_return_false_if_zero
+ firm = companies(:another_firm)
+ assert ! 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_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_equal 3, firm.clients.size
+ end
+
def test_joins_with_namespaced_model_should_use_correct_type
old = ActiveRecord::Base.store_full_sti_class
ActiveRecord::Base.store_full_sti_class = true
@@ -1739,6 +2017,15 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal [tagging], post.taggings
end
+ def test_with_polymorphic_has_many_with_custom_columns_name
+ post = Post.create! :title => 'foo', :body => 'bar'
+ image = Image.create!
+
+ post.images << image
+
+ assert_equal [image], post.images
+ end
+
def test_build_with_polymorphic_has_many_does_not_allow_to_override_type_and_id
welcome = posts(:welcome)
tagging = welcome.taggings.build(:taggable_id => 99, :taggable_type => 'ShouldNotChange')
@@ -1835,7 +2122,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
test "has many associations on new records use null relations" do
post = Post.new
- assert_no_queries do
+ assert_no_queries(ignore_none: false) do
assert_equal [], post.comments
assert_equal [], post.comments.where(body: 'omg')
assert_equal [], post.comments.pluck(:body)
@@ -1894,15 +2181,44 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal [bulb1, bulb2], car.all_bulbs.sort_by(&:id)
end
+ test "can unscope and where the default scope of the associated model" do
+ Car.has_many :other_bulbs, -> { unscope(where: [:name]).where(name: 'other') }, class_name: "Bulb"
+ car = Car.create!
+ bulb1 = Bulb.create! name: "defaulty", car: car
+ bulb2 = Bulb.create! name: "other", car: car
+
+ assert_equal [bulb1], car.bulbs
+ assert_equal [bulb2], car.other_bulbs
+ end
+
+ test "can rewhere the default scope of the associated model" do
+ Car.has_many :old_bulbs, -> { rewhere(name: 'old') }, class_name: "Bulb"
+ car = Car.create!
+ bulb1 = Bulb.create! name: "defaulty", car: car
+ bulb2 = Bulb.create! name: "old", car: car
+
+ assert_equal [bulb1], car.bulbs
+ assert_equal [bulb2], car.old_bulbs
+ end
+
+ test 'unscopes the default scope of associated model when used with include' do
+ car = Car.create!
+ bulb = Bulb.create! name: "other", car: car
+
+ assert_equal bulb, Car.find(car.id).all_bulbs.first
+ assert_equal bulb, Car.includes(:all_bulbs).find(car.id).all_bulbs.first
+ end
+
test "raises RecordNotDestroyed when replaced child can't be destroyed" do
car = Car.create!
original_child = FailedBulb.create!(car: car)
- assert_raise(ActiveRecord::RecordNotDestroyed) do
+ error = assert_raise(ActiveRecord::RecordNotDestroyed) do
car.failed_bulbs = [FailedBulb.create!]
end
assert_equal [original_child], car.reload.failed_bulbs
+ assert_equal "Failed to destroy the record", error.message
end
test 'updates counter cache when default scope is given' do
@@ -1941,4 +2257,125 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal [], authors(:david).posts_with_signature.map(&:title)
end
+
+ test 'associations autosaves when object is already persited' do
+ bulb = Bulb.create!
+ tyre = Tyre.create!
+
+ car = Car.create! do |c|
+ c.bulbs << bulb
+ c.tyres << tyre
+ end
+
+ assert_equal 1, car.bulbs.count
+ assert_equal 1, car.tyres.count
+ end
+
+ test 'associations replace in memory when records have the same id' do
+ bulb = Bulb.create!
+ car = Car.create!(bulbs: [bulb])
+
+ new_bulb = Bulb.find(bulb.id)
+ new_bulb.name = "foo"
+ car.bulbs = [new_bulb]
+
+ assert_equal "foo", car.bulbs.first.name
+ end
+
+ test 'in memory replacement executes no queries' do
+ bulb = Bulb.create!
+ car = Car.create!(bulbs: [bulb])
+
+ new_bulb = Bulb.find(bulb.id)
+
+ assert_no_queries do
+ car.bulbs = [new_bulb]
+ end
+ end
+
+ test 'in memory replacements do not execute callbacks' do
+ raise_after_add = false
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = :cars
+ has_many :bulbs, after_add: proc { raise if raise_after_add }
+
+ def self.name
+ "Car"
+ end
+ end
+ bulb = Bulb.create!
+ car = klass.create!(bulbs: [bulb])
+
+ new_bulb = Bulb.find(bulb.id)
+ raise_after_add = true
+
+ assert_nothing_raised do
+ car.bulbs = [new_bulb]
+ end
+ end
+
+ test 'in memory replacements sets inverse instance' do
+ bulb = Bulb.create!
+ car = Car.create!(bulbs: [bulb])
+
+ new_bulb = Bulb.find(bulb.id)
+ car.bulbs = [new_bulb]
+
+ assert_same car, new_bulb.car
+ end
+
+ test 'in memory replacement maintains order' do
+ first_bulb = Bulb.create!
+ second_bulb = Bulb.create!
+ car = Car.create!(bulbs: [first_bulb, second_bulb])
+
+ same_bulb = Bulb.find(first_bulb.id)
+ car.bulbs = [second_bulb, same_bulb]
+
+ assert_equal [first_bulb, second_bulb], car.bulbs
+ end
+
+ def test_association_force_reload_with_only_true_is_deprecated
+ company = Company.find(1)
+
+ assert_deprecated { company.clients_of_firm(true) }
+ end
+
+ class AuthorWithErrorDestroyingAssociation < ActiveRecord::Base
+ self.table_name = "authors"
+ has_many :posts_with_error_destroying,
+ class_name: "PostWithErrorDestroying",
+ foreign_key: :author_id,
+ dependent: :destroy
+ end
+
+ class PostWithErrorDestroying < ActiveRecord::Base
+ self.table_name = "posts"
+ self.inheritance_column = nil
+ before_destroy -> { throw :abort }
+ end
+
+ def test_destroy_does_not_raise_when_association_errors_on_destroy
+ assert_no_difference "AuthorWithErrorDestroyingAssociation.count" do
+ author = AuthorWithErrorDestroyingAssociation.first
+
+ assert_not author.destroy
+ end
+ end
+
+ def test_destroy_with_bang_bubbles_errors_from_associations
+ error = assert_raises ActiveRecord::RecordNotDestroyed do
+ AuthorWithErrorDestroyingAssociation.first.destroy!
+ end
+
+ assert_instance_of PostWithErrorDestroying, error.record
+ end
+
+ def test_ids_reader_memoization
+ car = Car.create!(name: 'Tofaş')
+ bulb = Bulb.create!(car: car)
+
+ assert_equal [bulb.id], car.bulb_ids
+ assert_no_queries { car.bulb_ids }
+ end
end
diff --git a/activerecord/test/cases/associations/has_many_through_associations_test.rb b/activerecord/test/cases/associations/has_many_through_associations_test.rb
index a85e020f0c..226ecf5447 100644
--- a/activerecord/test/cases/associations/has_many_through_associations_test.rb
+++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb
@@ -15,6 +15,7 @@ require 'models/toy'
require 'models/contract'
require 'models/company'
require 'models/developer'
+require 'models/computer'
require 'models/subscriber'
require 'models/book'
require 'models/subscription'
@@ -24,12 +25,13 @@ require 'models/categorization'
require 'models/member'
require 'models/membership'
require 'models/club'
+require 'models/organization'
class HasManyThroughAssociationsTest < ActiveRecord::TestCase
fixtures :posts, :readers, :people, :comments, :authors, :categories, :taggings, :tags,
:owners, :pets, :toys, :jobs, :references, :companies, :members, :author_addresses,
:subscribers, :books, :subscriptions, :developers, :categorizations, :essays,
- :categories_posts, :clubs, :memberships
+ :categories_posts, :clubs, :memberships, :organizations
# Dummies to force column loads so query counts are clean.
def setup
@@ -40,7 +42,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
def test_preload_sti_rhs_class
developers = Developer.includes(:firms).all.to_a
assert_no_queries do
- developers.each { |d| d.firms }
+ developers.each(&:firms)
end
end
@@ -82,11 +84,11 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
subscriber = make_model "Subscriber"
subscriber.primary_key = 'nick'
- subscription.belongs_to :book, class: book
- subscription.belongs_to :subscriber, class: subscriber
+ subscription.belongs_to :book, anonymous_class: book
+ subscription.belongs_to :subscriber, anonymous_class: subscriber
- book.has_many :subscriptions, class: subscription
- book.has_many :subscribers, through: :subscriptions, class: subscriber
+ book.has_many :subscriptions, anonymous_class: subscription
+ book.has_many :subscribers, through: :subscriptions, anonymous_class: subscriber
anonbook = book.first
namebook = Book.find anonbook.id
@@ -152,10 +154,10 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
lesson_student = make_model 'LessonStudent'
lesson_student.table_name = 'lessons_students'
- lesson_student.belongs_to :lesson, :class => lesson
- lesson_student.belongs_to :student, :class => student
- lesson.has_many :lesson_students, :class => lesson_student
- lesson.has_many :students, :through => :lesson_students, :class => student
+ lesson_student.belongs_to :lesson, :anonymous_class => lesson
+ lesson_student.belongs_to :student, :anonymous_class => student
+ lesson.has_many :lesson_students, :anonymous_class => lesson_student
+ lesson.has_many :students, :through => :lesson_students, :anonymous_class => student
[lesson, lesson_student, student]
end
@@ -186,7 +188,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
assert post.people.include?(person)
end
- assert post.reload.people(true).include?(person)
+ assert post.reload.people.reload.include?(person)
end
def test_delete_all_for_with_dependent_option_destroy
@@ -227,7 +229,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
post = posts(:thinking)
post.people.concat [person]
assert_equal 1, post.people.size
- assert_equal 1, post.people(true).size
+ assert_equal 1, post.people.reload.size
end
def test_associate_existing_record_twice_should_add_to_target_twice
@@ -283,7 +285,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
assert posts(:thinking).people.include?(new_person)
end
- assert posts(:thinking).reload.people(true).include?(new_person)
+ assert posts(:thinking).reload.people.reload.include?(new_person)
end
def test_associate_new_by_building
@@ -308,8 +310,8 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
posts(:thinking).save
end
- assert posts(:thinking).reload.people(true).collect(&:first_name).include?("Bob")
- assert posts(:thinking).reload.people(true).collect(&:first_name).include?("Ted")
+ assert posts(:thinking).reload.people.reload.collect(&:first_name).include?("Bob")
+ assert posts(:thinking).reload.people.reload.collect(&:first_name).include?("Ted")
end
def test_build_then_save_with_has_many_inverse
@@ -354,7 +356,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
assert posts(:welcome).people.empty?
end
- assert posts(:welcome).reload.people(true).empty?
+ assert posts(:welcome).reload.people.reload.empty?
end
def test_destroy_association
@@ -365,7 +367,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
end
assert posts(:welcome).reload.people.empty?
- assert posts(:welcome).people(true).empty?
+ assert posts(:welcome).people.reload.empty?
end
def test_destroy_all
@@ -376,7 +378,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
end
assert posts(:welcome).reload.people.empty?
- assert posts(:welcome).people(true).empty?
+ assert posts(:welcome).people.reload.empty?
end
def test_should_raise_exception_for_destroying_mismatching_records
@@ -537,7 +539,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
end
def test_replace_association
- assert_queries(4){posts(:welcome);people(:david);people(:michael); posts(:welcome).people(true)}
+ assert_queries(4){posts(:welcome);people(:david);people(:michael); posts(:welcome).people.reload}
# 1 query to delete the existing reader (michael)
# 1 query to associate the new reader (david)
@@ -550,8 +552,8 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
assert !posts(:welcome).people.include?(people(:michael))
}
- assert posts(:welcome).reload.people(true).include?(people(:david))
- assert !posts(:welcome).reload.people(true).include?(people(:michael))
+ assert posts(:welcome).reload.people.reload.include?(people(:david))
+ assert !posts(:welcome).reload.people.reload.include?(people(:michael))
end
def test_replace_order_is_preserved
@@ -590,7 +592,13 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
assert posts(:thinking).people.collect(&:first_name).include?("Jeb")
end
- assert posts(:thinking).reload.people(true).collect(&:first_name).include?("Jeb")
+ assert posts(:thinking).reload.people.reload.collect(&:first_name).include?("Jeb")
+ end
+
+ def test_through_record_is_built_when_created_with_where
+ assert_difference("posts(:thinking).readers.count", 1) do
+ posts(:thinking).people.where(first_name: "Jeb").create
+ end
end
def test_associate_with_create_and_no_options
@@ -614,8 +622,11 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
def test_create_on_new_record
p = Post.new
- assert_raises(ActiveRecord::RecordNotSaved) { p.people.create(:first_name => "mew") }
- assert_raises(ActiveRecord::RecordNotSaved) { p.people.create!(:first_name => "snow") }
+ error = assert_raises(ActiveRecord::RecordNotSaved) { p.people.create(:first_name => "mew") }
+ assert_equal "You cannot call create unless the parent is saved", error.message
+
+ error = assert_raises(ActiveRecord::RecordNotSaved) { p.people.create!(:first_name => "snow") }
+ assert_equal "You cannot call create unless the parent is saved", error.message
end
def test_associate_with_create_and_invalid_options
@@ -657,7 +668,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
end
def test_clear_associations
- assert_queries(2) { posts(:welcome);posts(:welcome).people(true) }
+ assert_queries(2) { posts(:welcome);posts(:welcome).people.reload }
assert_queries(1) do
posts(:welcome).people.clear
@@ -667,7 +678,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
assert posts(:welcome).people.empty?
end
- assert posts(:welcome).reload.people(true).empty?
+ assert posts(:welcome).reload.people.reload.empty?
end
def test_association_callback_ordering
@@ -733,13 +744,14 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
def test_get_ids_for_has_many_through_with_conditions_should_not_preload
Tagging.create!(:taggable_type => 'Post', :taggable_id => posts(:welcome).id, :tag => tags(:misc))
- ActiveRecord::Associations::Preloader.expects(:new).never
- posts(:welcome).misc_tag_ids
+ assert_not_called(ActiveRecord::Associations::Preloader, :new) do
+ posts(:welcome).misc_tag_ids
+ end
end
def test_get_ids_for_loaded_associations
person = people(:michael)
- person.posts(true)
+ person.posts.reload
assert_queries(0) do
person.post_ids
person.post_ids
@@ -754,9 +766,10 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
end
def test_association_proxy_transaction_method_starts_transaction_in_association_class
- Tag.expects(:transaction)
- Post.first.tags.transaction do
- # nothing
+ assert_called(Tag, :transaction) do
+ Post.first.tags.transaction do
+ # nothing
+ end
end
end
@@ -817,14 +830,14 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
category = author.named_categories.build(:name => "Primary")
author.save
assert Categorization.exists?(:author_id => author.id, :named_category_name => category.name)
- assert author.named_categories(true).include?(category)
+ assert author.named_categories.reload.include?(category)
end
def test_collection_create_with_nonstandard_primary_key_on_belongs_to
author = authors(:mary)
category = author.named_categories.create(:name => "Primary")
assert Categorization.exists?(:author_id => author.id, :named_category_name => category.name)
- assert author.named_categories(true).include?(category)
+ assert author.named_categories.reload.include?(category)
end
def test_collection_exists
@@ -839,7 +852,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
category = author.named_categories.create(:name => "Primary")
author.named_categories.delete(category)
assert !Categorization.exists?(:author_id => author.id, :named_category_name => category.name)
- assert author.named_categories(true).empty?
+ assert author.named_categories.reload.empty?
end
def test_collection_singular_ids_getter_with_string_primary_keys
@@ -860,10 +873,10 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
assert_nothing_raised do
book = books(:awdr)
book.subscriber_ids = [subscribers(:second).nick]
- assert_equal [subscribers(:second)], book.subscribers(true)
+ assert_equal [subscribers(:second)], book.subscribers.reload
book.subscriber_ids = []
- assert_equal [], book.subscribers(true)
+ assert_equal [], book.subscribers.reload
end
end
@@ -949,7 +962,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
assert_equal 1, category.categorizations.where(:special => true).count
end
- def test_joining_has_many_through_with_uniq
+ def test_joining_has_many_through_with_distinct
mary = Author.joins(:unique_categorized_posts).where(:id => authors(:mary).id).first
assert_equal 1, mary.unique_categorized_posts.length
assert_equal 1, mary.unique_categorized_post_ids.length
@@ -1029,14 +1042,6 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
end
end
- def test_save_should_not_raise_exception_when_join_record_has_errors
- repair_validations(Categorization) do
- Categorization.validate { |r| r.errors[:base] << 'Invalid Categorization' }
- c = Category.create(:name => 'Fishing', :authors => [Author.first])
- c.save
- end
- end
-
def test_assign_array_to_new_record_builds_join_records
c = Category.new(:name => 'Fishing', :authors => [Author.first])
assert_equal 1, c.categorizations.size
@@ -1061,11 +1066,11 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
end
end
- def test_create_bang_returns_falsy_when_join_record_has_errors
+ def test_save_returns_falsy_when_join_record_has_errors
repair_validations(Categorization) do
Categorization.validate { |r| r.errors[:base] << 'Invalid Categorization' }
c = Category.new(:name => 'Fishing', :authors => [Author.first])
- assert !c.save
+ assert_not c.save
end
end
@@ -1095,7 +1100,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
def test_has_many_through_associations_on_new_records_use_null_relations
person = Person.new
- assert_no_queries do
+ assert_no_queries(ignore_none: false) do
assert_equal [], person.posts
assert_equal [], person.posts.where(body: 'omg')
assert_equal [], person.posts.pluck(:body)
@@ -1106,10 +1111,10 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
def test_has_many_through_with_default_scope_on_the_target
person = people(:michael)
- assert_equal [posts(:thinking)], person.first_posts
+ assert_equal [posts(:thinking).id], person.first_posts.map(&:id)
readers(:michael_authorless).update(first_post_id: 1)
- assert_equal [posts(:thinking)], person.reload.first_posts
+ assert_equal [posts(:thinking).id], person.reload.first_posts.map(&:id)
end
def test_has_many_through_with_includes_in_through_association_scope
@@ -1148,22 +1153,52 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
assert_equal 1, SuperMembership.where(member_id: member.id, club_id: club.id).count
end
- class ClubWithCallbacks < ActiveRecord::Base
- self.table_name = 'clubs'
- after_create :add_a_member
+ def test_build_for_has_many_through_association
+ organization = organizations(:nsa)
+ author = organization.author
+ post_direct = author.posts.build
+ post_through = organization.posts.build
+ assert_equal post_direct.author_id, post_through.author_id
+ end
- has_many :memberships, inverse_of: :club, foreign_key: :club_id
- has_many :members, through: :memberships
+ def test_has_many_through_with_scope_that_should_not_be_fully_merged
+ Club.has_many :distinct_memberships, -> { distinct }, class_name: "Membership"
+ Club.has_many :special_favourites, through: :distinct_memberships, source: :member
- def add_a_member
- members << Member.last
- end
+ assert_nil Club.new.special_favourites.distinct_value
+ end
+
+ def test_association_force_reload_with_only_true_is_deprecated
+ post = Post.find(1)
+
+ assert_deprecated { post.people(true) }
end
- def test_has_many_with_callback_before_association
- Member.create!
- club = ClubWithCallbacks.create!
+ def test_has_many_through_do_not_cache_association_reader_if_the_though_method_has_default_scopes
+ member = Member.create!
+ club = Club.create!
+ TenantMembership.create!(
+ member: member,
+ club: club
+ )
+
+ TenantMembership.current_member = member
+
+ tenant_clubs = member.tenant_clubs
+ assert_equal [club], tenant_clubs
- assert_equal 1, club.reload.memberships.count
+ TenantMembership.current_member = nil
+
+ other_member = Member.create!
+ other_club = Club.create!
+ TenantMembership.create!(
+ member: other_member,
+ club: other_club
+ )
+
+ tenant_clubs = other_member.tenant_clubs
+ assert_equal [other_club], tenant_clubs
+ ensure
+ TenantMembership.current_member = nil
end
end
diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb
index a4650ccdf2..c9d9e29f09 100644
--- a/activerecord/test/cases/associations/has_one_associations_test.rb
+++ b/activerecord/test/cases/associations/has_one_associations_test.rb
@@ -1,5 +1,6 @@
require "cases/helper"
require 'models/developer'
+require 'models/computer'
require 'models/project'
require 'models/company'
require 'models/ship'
@@ -7,10 +8,11 @@ require 'models/pirate'
require 'models/car'
require 'models/bulb'
require 'models/author'
+require 'models/image'
require 'models/post'
class HasOneAssociationsTest < ActiveRecord::TestCase
- self.use_transactional_fixtures = false unless supports_savepoints?
+ self.use_transactional_tests = false unless supports_savepoints?
fixtures :accounts, :companies, :developers, :projects, :developers_projects, :ships, :pirates
def setup
@@ -105,6 +107,14 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
assert_nil Account.find(old_account_id).firm_id
end
+ def test_nullification_on_destroyed_association
+ developer = Developer.create!(name: "Someone")
+ ship = Ship.create!(name: "Planet Caravan", developer: developer)
+ ship.destroy
+ assert !ship.persisted?
+ assert !developer.persisted?
+ end
+
def test_natural_assignment_to_nil_after_destroy
firm = companies(:rails_core)
old_account_id = firm.account.id
@@ -176,6 +186,25 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
assert firm.account.present?
end
+ def test_restrict_with_error_is_deprecated_using_key_one
+ I18n.backend = I18n::Backend::Simple.new
+ I18n.backend.store_translations :en, activerecord: { errors: { messages: { restrict_dependent_destroy: { one: 'message for deprecated key' } } } }
+
+ firm = RestrictedWithErrorFirm.create!(name: 'restrict')
+ firm.create_account(credit_limit: 10)
+
+ assert_not_nil firm.account
+
+ assert_deprecated { firm.destroy }
+
+ assert !firm.errors.empty?
+ assert_equal 'message for deprecated key', firm.errors[:base].first
+ assert RestrictedWithErrorFirm.exists?(name: 'restrict')
+ assert firm.account.present?
+ ensure
+ I18n.backend.reload!
+ end
+
def test_restrict_with_error
firm = RestrictedWithErrorFirm.create!(:name => 'restrict')
firm.create_account(:credit_limit => 10)
@@ -190,6 +219,24 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
assert firm.account.present?
end
+ def test_restrict_with_error_with_locale
+ I18n.backend = I18n::Backend::Simple.new
+ I18n.backend.store_translations 'en', activerecord: {attributes: {restricted_with_error_firm: {account: 'firm account'}}}
+ firm = RestrictedWithErrorFirm.create!(name: 'restrict')
+ firm.create_account(credit_limit: 10)
+
+ assert_not_nil firm.account
+
+ firm.destroy
+
+ assert !firm.errors.empty?
+ assert_equal "Cannot delete record because a dependent firm account exists", firm.errors[:base].first
+ assert RestrictedWithErrorFirm.exists?(name: 'restrict')
+ assert firm.account.present?
+ ensure
+ I18n.backend.reload!
+ end
+
def test_successful_build_association
firm = Firm.new("name" => "GlobalMegaCorp")
firm.save
@@ -200,7 +247,7 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
end
def test_build_association_dont_create_transaction
- assert_no_queries {
+ assert_no_queries(ignore_none: false) {
Firm.new.build_account
}
end
@@ -235,16 +282,16 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
def test_build_and_create_should_not_happen_within_scope
pirate = pirates(:blackbeard)
- scoped_count = pirate.association(:foo_bulb).scope.where_values.count
+ scope = pirate.association(:foo_bulb).scope.where_values_hash
bulb = pirate.build_foo_bulb
- assert_not_equal scoped_count, bulb.scope_after_initialize.where_values.count
+ assert_not_equal scope, bulb.scope_after_initialize.where_values_hash
bulb = pirate.create_foo_bulb
- assert_not_equal scoped_count, bulb.scope_after_initialize.where_values.count
+ assert_not_equal scope, bulb.scope_after_initialize.where_values_hash
bulb = pirate.create_foo_bulb!
- assert_not_equal scoped_count, bulb.scope_after_initialize.where_values.count
+ assert_not_equal scope, bulb.scope_after_initialize.where_values_hash
end
def test_create_association
@@ -271,6 +318,14 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
assert_equal account, firm.reload.account
end
+ def test_create_with_inexistent_foreign_key_failing
+ firm = Firm.create(name: 'GlobalMegaCorp')
+
+ assert_raises(ActiveRecord::UnknownAttributeError) do
+ firm.create_account_with_inexistent_foreign_key
+ end
+ end
+
def test_build
firm = Firm.new("name" => "GlobalMegaCorp")
firm.save
@@ -322,7 +377,8 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
assert a.persisted?
assert_equal a, firm.account
assert_equal a, firm.account
- assert_equal a, firm.account(true)
+ firm.association(:account).reload
+ assert_equal a, firm.account
end
def test_save_still_works_after_accessing_nil_has_one
@@ -409,9 +465,11 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
pirate = pirates(:redbeard)
new_ship = Ship.new
- assert_raise(ActiveRecord::RecordNotSaved) do
+ error = assert_raise(ActiveRecord::RecordNotSaved) do
pirate.ship = new_ship
end
+
+ assert_equal "Failed to save the new associated ship.", error.message
assert_nil pirate.ship
assert_nil new_ship.pirate_id
end
@@ -421,20 +479,25 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
pirate.ship.name = nil
assert !pirate.ship.valid?
- assert_raise(ActiveRecord::RecordNotSaved) do
+ error = assert_raise(ActiveRecord::RecordNotSaved) do
pirate.ship = ships(:interceptor)
end
+
assert_equal ships(:black_pearl), pirate.ship
assert_equal pirate.id, pirate.ship.pirate_id
+ assert_equal "Failed to remove the existing associated ship. " +
+ "The record failed to save after its foreign key was set to nil.", error.message
end
def test_replacement_failure_due_to_new_record_should_raise_error
pirate = pirates(:blackbeard)
new_ship = Ship.new
- assert_raise(ActiveRecord::RecordNotSaved) do
+ error = assert_raise(ActiveRecord::RecordNotSaved) do
pirate.ship = new_ship
end
+
+ assert_equal "Failed to save the new associated ship.", error.message
assert_equal ships(:black_pearl), pirate.ship
assert_equal pirate.id, pirate.ship.pirate_id
assert_equal pirate.id, ships(:black_pearl).reload.pirate_id
@@ -557,6 +620,12 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
assert_equal author.post, post
end
+ def test_has_one_loading_for_new_record
+ post = Post.create!(author_id: 42, title: 'foo', body: 'bar')
+ author = Author.new(id: 42)
+ assert_equal post, author.post
+ end
+
def test_has_one_relationship_cannot_have_a_counter_cache
assert_raise(ArgumentError) do
Class.new(ActiveRecord::Base) do
@@ -565,6 +634,16 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
end
end
+ def test_with_polymorphic_has_one_with_custom_columns_name
+ post = Post.create! :title => 'foo', :body => 'bar'
+ image = Image.create!
+
+ post.main_image = image
+ post.reload
+
+ assert_equal image, post.main_image
+ end
+
test 'dangerous association name raises ArgumentError' do
[:errors, 'errors', :save, 'save'].each do |name|
assert_raises(ArgumentError, "Association #{name} should not be allowed") do
@@ -574,4 +653,10 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
end
end
end
+
+ def test_association_force_reload_with_only_true_is_deprecated
+ firm = Firm.find(1)
+
+ assert_deprecated { firm.account(true) }
+ end
end
diff --git a/activerecord/test/cases/associations/has_one_through_associations_test.rb b/activerecord/test/cases/associations/has_one_through_associations_test.rb
index 089cb0a3a2..b2b46812b9 100644
--- a/activerecord/test/cases/associations/has_one_through_associations_test.rb
+++ b/activerecord/test/cases/associations/has_one_through_associations_test.rb
@@ -15,6 +15,11 @@ require 'models/essay'
require 'models/owner'
require 'models/post'
require 'models/comment'
+require 'models/categorization'
+require 'models/customer'
+require 'models/carrier'
+require 'models/shop_account'
+require 'models/customer_carrier'
class HasOneThroughAssociationsTest < ActiveRecord::TestCase
fixtures :member_types, :members, :clubs, :memberships, :sponsors, :organizations, :minivans,
@@ -244,12 +249,14 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase
assert_not_nil @member_detail.member_type
@member_detail.destroy
assert_queries(1) do
- assert_not_nil @member_detail.member_type(true)
+ @member_detail.association(:member_type).reload
+ assert_not_nil @member_detail.member_type
end
@member_detail.member.destroy
assert_queries(1) do
- assert_nil @member_detail.member_type(true)
+ @member_detail.association(:member_type).reload
+ assert_nil @member_detail.member_type
end
end
@@ -289,6 +296,12 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase
end
end
+ def test_has_one_through_polymorphic_association
+ assert_raise(ActiveRecord::HasOneAssociationPolymorphicThroughError) do
+ @member.premium_club
+ end
+ end
+
def test_has_one_through_belongs_to_should_update_when_the_through_foreign_key_changes
minivan = minivans(:cool_first)
@@ -337,4 +350,34 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase
end
end
end
+
+ def test_has_one_through_do_not_cache_association_reader_if_the_though_method_has_default_scopes
+ customer = Customer.create!
+ carrier = Carrier.create!
+ customer_carrier = CustomerCarrier.create!(
+ customer: customer,
+ carrier: carrier,
+ )
+ account = ShopAccount.create!(customer_carrier: customer_carrier)
+
+ CustomerCarrier.current_customer = customer
+
+ account_carrier = account.carrier
+ assert_equal carrier, account_carrier
+
+ CustomerCarrier.current_customer = nil
+
+ other_carrier = Carrier.create!
+ other_customer = Customer.create!
+ other_customer_carrier = CustomerCarrier.create!(
+ customer: other_customer,
+ carrier: other_carrier,
+ )
+ other_account = ShopAccount.create!(customer_carrier: other_customer_carrier)
+
+ account_carrier = other_account.carrier
+ assert_equal other_carrier, account_carrier
+ ensure
+ CustomerCarrier.current_customer = nil
+ end
end
diff --git a/activerecord/test/cases/associations/inner_join_association_test.rb b/activerecord/test/cases/associations/inner_join_association_test.rb
index 07cf65a760..b3fe759ad9 100644
--- a/activerecord/test/cases/associations/inner_join_association_test.rb
+++ b/activerecord/test/cases/associations/inner_join_association_test.rb
@@ -54,7 +54,7 @@ class InnerJoinAssociationTest < ActiveRecord::TestCase
def test_find_with_implicit_inner_joins_without_select_does_not_imply_readonly
authors = Author.joins(:posts)
assert_not authors.empty?, "expected authors to be non-empty"
- assert authors.none? {|a| a.readonly? }, "expected no authors to be readonly"
+ assert authors.none?(&:readonly?), "expected no authors to be readonly"
end
def test_find_with_implicit_inner_joins_honors_readonly_with_select
@@ -102,7 +102,7 @@ class InnerJoinAssociationTest < ActiveRecord::TestCase
def test_find_with_conditions_on_reflection
assert !posts(:welcome).comments.empty?
- assert Post.joins(:nonexistant_comments).where(:id => posts(:welcome).id).empty? # [sic!]
+ assert Post.joins(:nonexistent_comments).where(:id => posts(:welcome).id).empty? # [sic!]
end
def test_find_with_conditions_on_through_reflection
diff --git a/activerecord/test/cases/associations/inverse_associations_test.rb b/activerecord/test/cases/associations/inverse_associations_test.rb
index 60df4e14dd..57d1c8feda 100644
--- a/activerecord/test/cases/associations/inverse_associations_test.rb
+++ b/activerecord/test/cases/associations/inverse_associations_test.rb
@@ -10,6 +10,12 @@ require 'models/comment'
require 'models/car'
require 'models/bulb'
require 'models/mixed_case_monkey'
+require 'models/admin'
+require 'models/admin/account'
+require 'models/admin/user'
+require 'models/developer'
+require 'models/company'
+require 'models/project'
class AutomaticInverseFindingTests < ActiveRecord::TestCase
fixtures :ratings, :comments, :cars
@@ -27,6 +33,15 @@ class AutomaticInverseFindingTests < ActiveRecord::TestCase
assert_equal monkey_reflection, man_reflection.inverse_of, "The man reflection's inverse should be the monkey reflection"
end
+ def test_has_many_and_belongs_to_should_find_inverse_automatically_for_model_in_module
+ account_reflection = Admin::Account.reflect_on_association(:users)
+ user_reflection = Admin::User.reflect_on_association(:account)
+
+ assert_respond_to account_reflection, :has_inverse?
+ assert account_reflection.has_inverse?, "The Admin::Account reflection should have an inverse"
+ assert_equal user_reflection, account_reflection.inverse_of, "The Admin::Account reflection's inverse should be the Admin::User reflection"
+ end
+
def test_has_one_and_belongs_to_should_find_inverse_automatically
car_reflection = Car.reflect_on_association(:bulb)
bulb_reflection = Bulb.reflect_on_association(:car)
@@ -68,10 +83,10 @@ class AutomaticInverseFindingTests < ActiveRecord::TestCase
assert_equal rating.comment, comment, "The Rating's comment should be the original Comment"
- rating.comment.body = "Brogramming is the act of programming, like a bro."
+ rating.comment.body = "Fennec foxes are the smallest of the foxes."
assert_equal rating.comment.body, comment.body, "Changing the Comment's body on the association should change the original Comment's body"
- comment.body = "Broseiden is the king of the sea of bros."
+ comment.body = "Kittens are adorable."
assert_equal comment.body, rating.comment.body, "Changing the original Comment's body should change the Comment's body on the association"
end
@@ -82,10 +97,10 @@ class AutomaticInverseFindingTests < ActiveRecord::TestCase
assert_equal rating.comment, comment, "The Rating's comment should be the original Comment"
- rating.comment.body = "Brogramming is the act of programming, like a bro."
+ rating.comment.body = "Fennec foxes are the smallest of the foxes."
assert_equal rating.comment.body, comment.body, "Changing the Comment's body on the association should change the original Comment's body"
- comment.body = "Broseiden is the king of the sea of bros."
+ comment.body = "Kittens are adorable."
assert_equal comment.body, rating.comment.body, "Changing the original Comment's body should change the Comment's body on the association"
end
@@ -186,6 +201,16 @@ class InverseAssociationTests < ActiveRecord::TestCase
belongs_to_ref = Sponsor.reflect_on_association(:sponsor_club)
assert_nil belongs_to_ref.inverse_of
end
+
+ def test_this_inverse_stuff
+ firm = Firm.create!(name: 'Adequate Holdings')
+ Project.create!(name: 'Project 1', firm: firm)
+ Developer.create!(name: 'Gorbypuff', firm: firm)
+
+ new_project = Project.last
+ assert Project.reflect_on_association(:lead_developer).inverse_of.present?, "Expected inverse of to be present"
+ assert new_project.lead_developer.present?, "Expected lead developer to be present on the project"
+ end
end
class InverseHasOneTests < ActiveRecord::TestCase
diff --git a/activerecord/test/cases/associations/join_model_test.rb b/activerecord/test/cases/associations/join_model_test.rb
index cace7ba142..f6dddaf5b4 100644
--- a/activerecord/test/cases/associations/join_model_test.rb
+++ b/activerecord/test/cases/associations/join_model_test.rb
@@ -17,7 +17,7 @@ require 'models/engine'
require 'models/car'
class AssociationsJoinModelTest < ActiveRecord::TestCase
- self.use_transactional_fixtures = false unless supports_savepoints?
+ self.use_transactional_tests = false unless supports_savepoints?
fixtures :posts, :authors, :categories, :categorizations, :comments, :tags, :taggings, :author_favorites, :vertices, :items, :books,
# Reload edges table from fixtures as otherwise repeated test was failing
@@ -35,12 +35,12 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
assert categories(:sti_test).authors.include?(authors(:mary))
end
- def test_has_many_uniq_through_join_model
+ def test_has_many_distinct_through_join_model
assert_equal 2, authors(:mary).categorized_posts.size
assert_equal 1, authors(:mary).unique_categorized_posts.size
end
- def test_has_many_uniq_through_count
+ def test_has_many_distinct_through_count
author = authors(:mary)
assert !authors(:mary).unique_categorized_posts.loaded?
assert_queries(1) { assert_equal 1, author.unique_categorized_posts.count }
@@ -49,7 +49,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
assert !authors(:mary).unique_categorized_posts.loaded?
end
- def test_has_many_uniq_through_find
+ def test_has_many_distinct_through_find
assert_equal 1, authors(:mary).unique_categorized_posts.to_a.size
end
@@ -213,7 +213,8 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
old_count = Tagging.count
post.destroy
assert_equal old_count-1, Tagging.count
- assert_nil posts(:welcome).tagging(true)
+ posts(:welcome).association(:tagging).reload
+ assert_nil posts(:welcome).tagging
end
def test_delete_polymorphic_has_one_with_nullify
@@ -224,7 +225,8 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
old_count = Tagging.count
post.destroy
assert_equal old_count, Tagging.count
- assert_nil posts(:welcome).tagging(true)
+ posts(:welcome).association(:tagging).reload
+ assert_nil posts(:welcome).tagging
end
def test_has_many_with_piggyback
@@ -393,18 +395,18 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
end
def test_has_many_through_polymorphic_has_one
- assert_equal Tagging.find(1,2).sort_by { |t| t.id }, authors(:david).taggings_2
+ assert_equal Tagging.find(1,2).sort_by(&:id), authors(:david).taggings_2
end
def test_has_many_through_polymorphic_has_many
- assert_equal taggings(:welcome_general, :thinking_general), authors(:david).taggings.distinct.sort_by { |t| t.id }
+ assert_equal taggings(:welcome_general, :thinking_general), authors(:david).taggings.distinct.sort_by(&:id)
end
def test_include_has_many_through_polymorphic_has_many
author = Author.includes(:taggings).find authors(:david).id
expected_taggings = taggings(:welcome_general, :thinking_general)
assert_no_queries do
- assert_equal expected_taggings, author.taggings.distinct.sort_by { |t| t.id }
+ assert_equal expected_taggings, author.taggings.distinct.sort_by(&:id)
end
end
@@ -444,7 +446,7 @@ 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.nonexistant_comments.blank?
+ assert author.nonexistent_comments.blank?
end
def test_has_many_through_uses_correct_attributes
@@ -461,7 +463,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
assert saved_post.tags.include?(new_tag)
assert new_tag.persisted?
- assert saved_post.reload.tags(true).include?(new_tag)
+ assert saved_post.reload.tags.reload.include?(new_tag)
new_post = Post.new(:title => "Association replacement works!", :body => "You best believe it.")
@@ -474,7 +476,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
new_post.save!
assert new_post.persisted?
- assert new_post.reload.tags(true).include?(saved_tag)
+ assert new_post.reload.tags.reload.include?(saved_tag)
assert !posts(:thinking).tags.build.persisted?
assert !posts(:thinking).tags.new.persisted?
@@ -490,7 +492,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
assert_nil( wrong = post_thinking.taggings.detect { |t| t.class != Tagging },
message = "Expected a Tagging in taggings collection, got #{wrong.class}.")
assert_equal(count + 1, post_thinking.reload.tags.size)
- assert_equal(count + 1, post_thinking.tags(true).size)
+ assert_equal(count + 1, post_thinking.tags.reload.size)
assert_kind_of Tag, post_thinking.tags.create!(:name => 'foo')
assert_nil( wrong = post_thinking.tags.detect { |t| t.class != Tag },
@@ -498,7 +500,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
assert_nil( wrong = post_thinking.taggings.detect { |t| t.class != Tagging },
message = "Expected a Tagging in taggings collection, got #{wrong.class}.")
assert_equal(count + 2, post_thinking.reload.tags.size)
- assert_equal(count + 2, post_thinking.tags(true).size)
+ assert_equal(count + 2, post_thinking.tags.reload.size)
assert_nothing_raised { post_thinking.tags.concat(Tag.create!(:name => 'abc'), Tag.create!(:name => 'def')) }
assert_nil( wrong = post_thinking.tags.detect { |t| t.class != Tag },
@@ -506,7 +508,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
assert_nil( wrong = post_thinking.taggings.detect { |t| t.class != Tagging },
message = "Expected a Tagging in taggings collection, got #{wrong.class}.")
assert_equal(count + 4, post_thinking.reload.tags.size)
- assert_equal(count + 4, post_thinking.tags(true).size)
+ assert_equal(count + 4, post_thinking.tags.reload.size)
# Raises if the wrong reflection name is used to set the Edge belongs_to
assert_nothing_raised { vertices(:vertex_1).sinks << vertices(:vertex_5) }
@@ -544,11 +546,11 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
book = Book.create!(:name => 'Getting Real')
book_awdr = books(:awdr)
book_awdr.references << book
- assert_equal(count + 1, book_awdr.references(true).size)
+ assert_equal(count + 1, book_awdr.references.reload.size)
assert_nothing_raised { book_awdr.references.delete(book) }
assert_equal(count, book_awdr.references.size)
- assert_equal(count, book_awdr.references(true).size)
+ assert_equal(count, book_awdr.references.reload.size)
assert_equal(references_before.sort, book_awdr.references.sort)
end
@@ -558,14 +560,14 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
tag = Tag.create!(:name => 'doomed')
post_thinking = posts(:thinking)
post_thinking.tags << tag
- assert_equal(count + 1, post_thinking.taggings(true).size)
- assert_equal(count + 1, post_thinking.reload.tags(true).size)
+ assert_equal(count + 1, post_thinking.taggings.reload.size)
+ assert_equal(count + 1, post_thinking.reload.tags.reload.size)
assert_not_equal(tags_before, post_thinking.tags.sort)
assert_nothing_raised { post_thinking.tags.delete(tag) }
assert_equal(count, post_thinking.tags.size)
- assert_equal(count, post_thinking.tags(true).size)
- assert_equal(count, post_thinking.taggings(true).size)
+ assert_equal(count, post_thinking.tags.reload.size)
+ assert_equal(count, post_thinking.taggings.reload.size)
assert_equal(tags_before, post_thinking.tags.sort)
end
@@ -577,11 +579,11 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
quaked = Tag.create!(:name => 'quaked')
post_thinking = posts(:thinking)
post_thinking.tags << doomed << doomed2
- assert_equal(count + 2, post_thinking.reload.tags(true).size)
+ assert_equal(count + 2, post_thinking.reload.tags.reload.size)
assert_nothing_raised { post_thinking.tags.delete(doomed, doomed2, quaked) }
assert_equal(count, post_thinking.tags.size)
- assert_equal(count, post_thinking.tags(true).size)
+ assert_equal(count, post_thinking.tags.reload.size)
assert_equal(tags_before, post_thinking.tags.sort)
end
@@ -625,7 +627,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
assert_equal [comments(:does_it_hurt)], authors(:david).special_post_comments
end
- def test_uniq_has_many_through_should_retain_order
+ def test_distinct_has_many_through_should_retain_order
comment_ids = authors(:david).comments.map(&:id)
assert_equal comment_ids.sort, authors(:david).ordered_uniq_comments.map(&:id)
assert_equal comment_ids.sort.reverse, authors(:david).ordered_uniq_comments_desc.map(&:id)
diff --git a/activerecord/test/cases/associations/nested_through_associations_test.rb b/activerecord/test/cases/associations/nested_through_associations_test.rb
index 31b68c940e..b040485d99 100644
--- a/activerecord/test/cases/associations/nested_through_associations_test.rb
+++ b/activerecord/test/cases/associations/nested_through_associations_test.rb
@@ -495,7 +495,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase
groucho = members(:groucho)
founding = member_types(:founding)
- assert_raises(ActiveRecord::HasManyThroughNestedAssociationsAreReadonly) do
+ assert_raises(ActiveRecord::HasOneThroughNestedAssociationsAreReadonly) do
groucho.nested_member_type = founding
end
end
diff --git a/activerecord/test/cases/associations/required_test.rb b/activerecord/test/cases/associations/required_test.rb
index a6934a056e..3e5494e897 100644
--- a/activerecord/test/cases/associations/required_test.rb
+++ b/activerecord/test/cases/associations/required_test.rb
@@ -1,7 +1,7 @@
require "cases/helper"
class RequiredAssociationsTest < ActiveRecord::TestCase
- self.use_transactional_fixtures = false
+ self.use_transactional_tests = false
class Parent < ActiveRecord::Base
end
@@ -18,8 +18,8 @@ class RequiredAssociationsTest < ActiveRecord::TestCase
end
teardown do
- @connection.execute("DROP TABLE IF EXISTS parents")
- @connection.execute("DROP TABLE IF EXISTS children")
+ @connection.drop_table 'parents', if_exists: true
+ @connection.drop_table 'children', if_exists: true
end
test "belongs_to associations are not required by default" do
@@ -40,7 +40,7 @@ class RequiredAssociationsTest < ActiveRecord::TestCase
record = model.new
assert_not record.save
- assert_equal ["Parent can't be blank"], record.errors.full_messages
+ assert_equal ["Parent must exist"], record.errors.full_messages
record.parent = Parent.new
assert record.save
@@ -64,12 +64,32 @@ class RequiredAssociationsTest < ActiveRecord::TestCase
record = model.new
assert_not record.save
- assert_equal ["Child can't be blank"], record.errors.full_messages
+ assert_equal ["Child must exist"], record.errors.full_messages
record.child = Child.new
assert record.save
end
+ test "required has_one associations have a correct error message" do
+ model = subclass_of(Parent) do
+ has_one :child, required: true, inverse_of: false,
+ class_name: "RequiredAssociationsTest::Child"
+ end
+
+ record = model.create
+ assert_equal ["Child must exist"], record.errors.full_messages
+ end
+
+ test "required belongs_to associations have a correct error message" do
+ model = subclass_of(Child) do
+ belongs_to :parent, required: true, inverse_of: false,
+ class_name: "RequiredAssociationsTest::Parent"
+ end
+
+ record = model.create
+ assert_equal ["Parent must exist"], record.errors.full_messages
+ end
+
private
def subclass_of(klass, &block)
diff --git a/activerecord/test/cases/associations_test.rb b/activerecord/test/cases/associations_test.rb
index f663b5490c..01a058918a 100644
--- a/activerecord/test/cases/associations_test.rb
+++ b/activerecord/test/cases/associations_test.rb
@@ -12,7 +12,6 @@ require 'models/tag'
require 'models/tagging'
require 'models/person'
require 'models/reader'
-require 'models/parrot'
require 'models/ship_part'
require 'models/ship'
require 'models/liquid'
@@ -23,7 +22,7 @@ require 'models/interest'
class AssociationsTest < ActiveRecord::TestCase
fixtures :accounts, :companies, :developers, :projects, :developers_projects,
- :computers, :people, :readers
+ :computers, :people, :readers, :authors, :author_favorites
def test_eager_loading_should_not_change_count_of_children
liquid = Liquid.create(:name => 'salty')
@@ -35,26 +34,11 @@ class AssociationsTest < ActiveRecord::TestCase
assert_equal 1, liquids[0].molecules.length
end
- def test_clear_association_cache_stored
- firm = Firm.find(1)
- assert_kind_of Firm, firm
-
- firm.clear_association_cache
- assert_equal Firm.find(1).clients.collect{ |x| x.name }.sort, firm.clients.collect{ |x| x.name }.sort
- end
-
- def test_clear_association_cache_new_record
- firm = Firm.new
- client_stored = Client.find(3)
- client_new = Client.new
- client_new.name = "The Joneses"
- clients = [ client_stored, client_new ]
-
- firm.clients << clients
- assert_equal clients.map(&:name).to_set, firm.clients.map(&:name).to_set
-
- firm.clear_association_cache
- assert_equal clients.map(&:name).to_set, firm.clients.map(&:name).to_set
+ def test_subselect
+ author = authors :david
+ favs = author.author_favorites
+ fav2 = author.author_favorites.where(:author => Author.where(id: author.id)).to_a
+ assert_equal favs, fav2
end
def test_loading_the_association_target_should_keep_child_records_marked_for_destruction
@@ -107,8 +91,10 @@ class AssociationsTest < ActiveRecord::TestCase
assert firm.clients.empty?, "New firm should have cached no client objects"
assert_equal 0, firm.clients.size, "New firm should have cached 0 clients count"
- assert !firm.clients(true).empty?, "New firm should have reloaded client objects"
- assert_equal 1, firm.clients(true).size, "New firm should have reloaded clients count"
+ ActiveSupport::Deprecation.silence do
+ assert !firm.clients(true).empty?, "New firm should have reloaded client objects"
+ assert_equal 1, firm.clients(true).size, "New firm should have reloaded clients count"
+ end
end
def test_using_limitable_reflections_helper
@@ -124,16 +110,19 @@ class AssociationsTest < ActiveRecord::TestCase
def test_force_reload_is_uncached
firm = Firm.create!("name" => "A New Firm, Inc")
Client.create!("name" => "TheClient.com", :firm => firm)
- ActiveRecord::Base.cache do
- firm.clients.each {}
- assert_queries(0) { assert_not_nil firm.clients.each {} }
- assert_queries(1) { assert_not_nil firm.clients(true).each {} }
+
+ ActiveSupport::Deprecation.silence do
+ ActiveRecord::Base.cache do
+ firm.clients.each {}
+ assert_queries(0) { assert_not_nil firm.clients.each {} }
+ assert_queries(1) { assert_not_nil firm.clients(true).each {} }
+ end
end
end
def test_association_with_references
firm = companies(:first_firm)
- assert_equal ['foo'], firm.association_with_references.references_values
+ assert_includes firm.association_with_references.references_values, 'foo'
end
end
@@ -230,7 +219,7 @@ class AssociationProxyTest < ActiveRecord::TestCase
end
def test_scoped_allows_conditions
- assert developers(:david).projects.merge!(where: 'foo').where_values.include?('foo')
+ assert developers(:david).projects.merge(where: 'foo').to_sql.include?('foo')
end
test "getting a scope from an association" do
@@ -256,6 +245,11 @@ class AssociationProxyTest < ActiveRecord::TestCase
end
end
+ test "first! works on loaded associations" do
+ david = authors(:david)
+ assert_equal david.posts.first, david.posts.reload.first!
+ end
+
def test_reset_unloads_target
david = authors(:david)
david.posts.reload
@@ -350,4 +344,18 @@ class GeneratedMethodsTest < ActiveRecord::TestCase
def test_model_method_overrides_association_method
assert_equal(comments(:greetings).body, posts(:welcome).first_comment)
end
+
+ module MyModule
+ def comments; :none end
+ end
+
+ class MyArticle < ActiveRecord::Base
+ self.table_name = "articles"
+ include MyModule
+ has_many :comments, inverse_of: false
+ end
+
+ def test_included_module_overwrites_association_methods
+ assert_equal :none, MyArticle.new.comments
+ end
end
diff --git a/activerecord/test/cases/attribute_decorators_test.rb b/activerecord/test/cases/attribute_decorators_test.rb
index cbc2c4e5d7..2aeb2601c2 100644
--- a/activerecord/test/cases/attribute_decorators_test.rb
+++ b/activerecord/test/cases/attribute_decorators_test.rb
@@ -12,11 +12,11 @@ module ActiveRecord
super(delegate)
end
- def type_cast_from_user(value)
+ def cast(value)
"#{super} #{@decoration}"
end
- alias type_cast_from_database type_cast_from_user
+ alias deserialize cast
end
setup do
@@ -28,7 +28,7 @@ module ActiveRecord
teardown do
return unless @connection
- @connection.execute 'DROP TABLE IF EXISTS attribute_decorators_model'
+ @connection.drop_table 'attribute_decorators_model', if_exists: true
Model.attribute_type_decorations.clear
Model.reset_column_information
end
@@ -44,13 +44,14 @@ module ActiveRecord
end
test "decoration does not eagerly load existing columns" do
+ Model.reset_column_information
assert_no_queries do
Model.decorate_attribute_type(:a_string, :test) { |t| StringDecorator.new(t) }
end
end
test "undecorated columns are not touched" do
- Model.attribute :another_string, Type::String.new, default: 'something or other'
+ Model.attribute :another_string, :string, default: 'something or other'
Model.decorate_attribute_type(:a_string, :test) { |t| StringDecorator.new(t) }
assert_equal 'something or other', Model.new.another_string
@@ -85,7 +86,7 @@ module ActiveRecord
end
test "decorating attributes does not modify parent classes" do
- Model.attribute :another_string, Type::String.new, default: 'whatever'
+ Model.attribute :another_string, :string, default: 'whatever'
Model.decorate_attribute_type(:a_string, :test) { |t| StringDecorator.new(t) }
child_class = Class.new(Model)
child_class.decorate_attribute_type(:another_string, :test) { |t| StringDecorator.new(t) }
@@ -101,15 +102,15 @@ module ActiveRecord
end
class Multiplier < SimpleDelegator
- def type_cast_from_user(value)
+ def cast(value)
return if value.nil?
value * 2
end
- alias type_cast_from_database type_cast_from_user
+ alias deserialize cast
end
test "decorating with a proc" do
- Model.attribute :an_int, Type::Integer.new
+ Model.attribute :an_int, :integer
type_is_integer = proc { |_, type| type.type == :integer }
Model.decorate_matching_attribute_types type_is_integer, :multiplier do |type|
Multiplier.new(type)
diff --git a/activerecord/test/cases/attribute_methods/read_test.rb b/activerecord/test/cases/attribute_methods/read_test.rb
index 4741ee8799..74e556211b 100644
--- a/activerecord/test/cases/attribute_methods/read_test.rb
+++ b/activerecord/test/cases/attribute_methods/read_test.rb
@@ -13,10 +13,11 @@ module ActiveRecord
def self.superclass; Base; end
def self.base_class; self; end
def self.decorate_matching_attribute_types(*); end
+ def self.initialize_generated_modules; end
include ActiveRecord::AttributeMethods
- def self.column_names
+ def self.attribute_names
%w{ one two three }
end
@@ -24,11 +25,11 @@ module ActiveRecord
end
def self.columns
- column_names.map { FakeColumn.new(name) }
+ attribute_names.map { FakeColumn.new(name) }
end
def self.columns_hash
- Hash[column_names.map { |name|
+ Hash[attribute_names.map { |name|
[name, FakeColumn.new(name)]
}]
end
@@ -38,13 +39,13 @@ module ActiveRecord
def test_define_attribute_methods
instance = @klass.new
- @klass.column_names.each do |name|
+ @klass.attribute_names.each do |name|
assert !instance.methods.map(&:to_s).include?(name)
end
@klass.define_attribute_methods
- @klass.column_names.each do |name|
+ @klass.attribute_names.each do |name|
assert instance.methods.map(&:to_s).include?(name), "#{name} is not defined"
end
end
diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb
index ab67cf4085..52d197718e 100644
--- a/activerecord/test/cases/attribute_methods_test.rb
+++ b/activerecord/test/cases/attribute_methods_test.rb
@@ -66,8 +66,9 @@ class AttributeMethodsTest < ActiveRecord::TestCase
def test_caching_nil_primary_key
klass = Class.new(Minimalistic)
- klass.expects(:reset_primary_key).returns(nil).once
- 2.times { klass.primary_key }
+ assert_called(klass, :reset_primary_key, returns: nil) do
+ 2.times { klass.primary_key }
+ end
end
def test_attribute_keys_on_new_instance
@@ -174,9 +175,9 @@ class AttributeMethodsTest < ActiveRecord::TestCase
assert_equal category_attrs , category.attributes_before_type_cast
end
- if current_adapter?(:MysqlAdapter)
+ if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
def test_read_attributes_before_type_cast_on_boolean
- bool = Boolean.create({ "value" => false })
+ bool = Boolean.create!({ "value" => false })
if RUBY_PLATFORM =~ /java/
# JRuby will return the value before typecast as string
assert_equal "0", bool.reload.attributes_before_type_cast["value"]
@@ -263,7 +264,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase
end
assert_equal klass.column_names, klass.new.attributes.keys
- assert_not klass.new.attributes.key?('id')
+ assert_not klass.new.has_attribute?('id')
end
def test_hashes_not_mangled
@@ -501,7 +502,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase
def test_typecast_attribute_from_select_to_false
Topic.create(:title => 'Budget')
# Oracle does not support boolean expressions in SELECT
- if current_adapter?(:OracleAdapter)
+ if current_adapter?(:OracleAdapter, :FbAdapter)
topic = Topic.all.merge!(:select => "topics.*, 0 as is_test").first
else
topic = Topic.all.merge!(:select => "topics.*, 1=2 as is_test").first
@@ -512,7 +513,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase
def test_typecast_attribute_from_select_to_true
Topic.create(:title => 'Budget')
# Oracle does not support boolean expressions in SELECT
- if current_adapter?(:OracleAdapter)
+ if current_adapter?(:OracleAdapter, :FbAdapter)
topic = Topic.all.merge!(:select => "topics.*, 1 as is_test").first
else
topic = Topic.all.merge!(:select => "topics.*, 2=2 as is_test").first
@@ -530,20 +531,6 @@ class AttributeMethodsTest < ActiveRecord::TestCase
end
end
- def test_deprecated_cache_attributes
- assert_deprecated do
- Topic.cache_attributes :replies_count
- end
-
- assert_deprecated do
- Topic.cached_attributes
- end
-
- assert_deprecated do
- Topic.cache_attribute? :replies_count
- end
- end
-
def test_converted_values_are_returned_after_assignment
developer = Developer.new(name: 1337, salary: "50000")
@@ -555,9 +542,6 @@ class AttributeMethodsTest < ActiveRecord::TestCase
developer.save!
- assert_equal "50000", developer.salary_before_type_cast
- assert_equal 1337, developer.name_before_type_cast
-
assert_equal 50000, developer.salary
assert_equal "1337", developer.name
end
@@ -670,7 +654,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase
end
end
- def test_setting_time_zone_aware_attribute_in_current_time_zone
+ def test_setting_time_zone_aware_datetime_in_current_time_zone
utc_time = Time.utc(2008, 1, 1)
in_time_zone "Pacific Time (US & Canada)" do
record = @target.new
@@ -681,6 +665,55 @@ class AttributeMethodsTest < ActiveRecord::TestCase
end
end
+ def test_yaml_dumping_record_with_time_zone_aware_attribute
+ in_time_zone "Pacific Time (US & Canada)" do
+ record = Topic.new(id: 1)
+ record.written_on = "Jan 01 00:00:00 2014"
+ assert_equal record, YAML.load(YAML.dump(record))
+ end
+ end
+
+ def test_setting_time_zone_aware_time_in_current_time_zone
+ in_time_zone "Pacific Time (US & Canada)" do
+ record = @target.new
+ time_string = "10:00:00"
+ expected_time = Time.zone.parse("2000-01-01 #{time_string}")
+
+ record.bonus_time = time_string
+ assert_equal expected_time, record.bonus_time
+ assert_equal ActiveSupport::TimeZone["Pacific Time (US & Canada)"], record.bonus_time.time_zone
+
+ record.bonus_time = ''
+ assert_nil record.bonus_time
+ end
+ end
+
+ def test_setting_time_zone_aware_time_with_dst
+ in_time_zone "Pacific Time (US & Canada)" do
+ current_time = Time.zone.local(2014, 06, 15, 10)
+ record = @target.new(bonus_time: current_time)
+ time_before_save = record.bonus_time
+
+ record.save
+ record.reload
+
+ assert_equal time_before_save, record.bonus_time
+ assert_equal ActiveSupport::TimeZone["Pacific Time (US & Canada)"], record.bonus_time.time_zone
+ end
+ end
+
+ def test_removing_time_zone_aware_types
+ with_time_zone_aware_types(:datetime) do
+ in_time_zone "Pacific Time (US & Canada)" do
+ record = @target.new(bonus_time: "10:00:00")
+ expected_time = Time.utc(2000, 01, 01, 10)
+
+ assert_equal expected_time, record.bonus_time
+ assert record.bonus_time.utc?
+ end
+ end
+ end
+
def test_setting_time_zone_conversion_for_attributes_should_write_value_on_class_variable
Topic.skip_time_zone_conversion_for_attributes = [:field_a]
Minimalistic.skip_time_zone_conversion_for_attributes = [:field_b]
@@ -726,13 +759,13 @@ class AttributeMethodsTest < ActiveRecord::TestCase
assert_raise(ActiveRecord::UnknownAttributeError) { @target.new.attributes = { :title => "Ants in pants" } }
end
- def test_bulk_update_raise_unknown_attribute_errro
+ def test_bulk_update_raise_unknown_attribute_error
error = assert_raises(ActiveRecord::UnknownAttributeError) {
- @target.new(:hello => "world")
+ Topic.new(hello: "world")
}
- assert @target, error.record
- assert "hello", error.attribute
- assert "unknown attribute: hello", error.message
+ assert_instance_of Topic, error.record
+ assert_equal "hello", error.attribute
+ assert_equal "unknown attribute 'hello' for Topic.", error.message
end
def test_methods_override_in_multi_level_subclass
@@ -810,6 +843,24 @@ class AttributeMethodsTest < ActiveRecord::TestCase
assert_equal "lol", topic.author_name
end
+ def test_inherited_custom_accessors_with_reserved_names
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = 'computers'
+ self.abstract_class = true
+ def system; "omg"; end
+ def system=(val); self.developer = val; end
+ end
+
+ subklass = Class.new(klass)
+ [klass, subklass].each(&:define_attribute_methods)
+
+ computer = subklass.find(1)
+ assert_equal "omg", computer.system
+
+ computer.developer = 99
+ assert_equal 99, computer.developer
+ end
+
def test_on_the_fly_super_invokable_generated_attribute_methods_via_method_missing
klass = new_topic_like_ar_class do
def title
@@ -875,6 +926,24 @@ class AttributeMethodsTest < ActiveRecord::TestCase
assert_not_equal ['id'], @target.column_names
end
+ def test_came_from_user
+ model = @target.first
+
+ assert_not model.id_came_from_user?
+ model.id = "omg"
+ assert model.id_came_from_user?
+ end
+
+ def test_accessed_fields
+ model = @target.first
+
+ assert_equal [], model.accessed_fields
+
+ model.title
+
+ assert_equal ["title"], model.accessed_fields
+ end
+
private
def new_topic_like_ar_class(&block)
@@ -887,6 +956,14 @@ class AttributeMethodsTest < ActiveRecord::TestCase
klass
end
+ def with_time_zone_aware_types(*types)
+ old_types = ActiveRecord::Base.time_zone_aware_types
+ ActiveRecord::Base.time_zone_aware_types = types
+ yield
+ ensure
+ ActiveRecord::Base.time_zone_aware_types = old_types
+ end
+
def cached_columns
Topic.columns.map(&:name)
end
diff --git a/activerecord/test/cases/attribute_set_test.rb b/activerecord/test/cases/attribute_set_test.rb
index dc20c3c676..7a24b85a36 100644
--- a/activerecord/test/cases/attribute_set_test.rb
+++ b/activerecord/test/cases/attribute_set_test.rb
@@ -29,7 +29,7 @@ module ActiveRecord
assert_equal :bar, attributes[:bar].name
end
- test "duping creates a new hash and dups each attribute" do
+ test "duping creates a new hash, but does not dup the attributes" do
builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::String.new)
attributes = builder.build_from_database(foo: 1, bar: 'foo')
@@ -43,6 +43,24 @@ module ActiveRecord
assert_equal 1, attributes[:foo].value
assert_equal 2, duped[:foo].value
+ assert_equal 'foobar', attributes[:bar].value
+ assert_equal 'foobar', duped[:bar].value
+ end
+
+ test "deep_duping creates a new hash and dups each attribute" do
+ builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::String.new)
+ attributes = builder.build_from_database(foo: 1, bar: 'foo')
+
+ # Ensure the type cast value is cached
+ attributes[:foo].value
+ attributes[:bar].value
+
+ duped = attributes.deep_dup
+ duped.write_from_database(:foo, 2)
+ duped[:bar].value << 'bar'
+
+ assert_equal 1, attributes[:foo].value
+ assert_equal 2, duped[:foo].value
assert_equal 'foo', attributes[:bar].value
assert_equal 'foobar', duped[:bar].value
end
@@ -65,6 +83,16 @@ module ActiveRecord
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')
@@ -109,7 +137,15 @@ module ActiveRecord
test "fetch_value returns nil for unknown attributes" do
attributes = attributes_with_uninitialized_key
- assert_nil attributes.fetch_value(:wibble)
+ 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
@@ -123,16 +159,28 @@ module ActiveRecord
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 type_cast_from_user(value)
+ def cast(value)
return if value.nil?
value + " from user"
end
- def type_cast_from_database(value)
+ 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
@@ -161,5 +209,45 @@ module ActiveRecord
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
index 24452fdec2..a24a4fc6a4 100644
--- a/activerecord/test/cases/attribute_test.rb
+++ b/activerecord/test/cases/attribute_test.rb
@@ -1,10 +1,9 @@
require 'cases/helper'
-require 'minitest/mock'
module ActiveRecord
class AttributeTest < ActiveRecord::TestCase
setup do
- @type = MiniTest::Mock.new
+ @type = Minitest::Mock.new
end
teardown do
@@ -12,7 +11,7 @@ module ActiveRecord
end
test "from_database + read type casts from database" do
- @type.expect(:type_cast_from_database, 'type cast from database', ['a value'])
+ @type.expect(:deserialize, 'type cast from database', ['a value'])
attribute = Attribute.from_database(nil, 'a value', @type)
type_cast_value = attribute.value
@@ -21,7 +20,7 @@ module ActiveRecord
end
test "from_user + read type casts from user" do
- @type.expect(:type_cast_from_user, 'type cast from user', ['a value'])
+ @type.expect(:cast, 'type cast from user', ['a value'])
attribute = Attribute.from_user(nil, 'a value', @type)
type_cast_value = attribute.value
@@ -30,7 +29,7 @@ module ActiveRecord
end
test "reading memoizes the value" do
- @type.expect(:type_cast_from_database, 'from the database', ['whatever'])
+ @type.expect(:deserialize, 'from the database', ['whatever'])
attribute = Attribute.from_database(nil, 'whatever', @type)
type_cast_value = attribute.value
@@ -41,7 +40,7 @@ module ActiveRecord
end
test "reading memoizes falsy values" do
- @type.expect(:type_cast_from_database, false, ['whatever'])
+ @type.expect(:deserialize, false, ['whatever'])
attribute = Attribute.from_database(nil, 'whatever', @type)
attribute.value
@@ -57,27 +56,27 @@ module ActiveRecord
end
test "from_database + read_for_database type casts to and from database" do
- @type.expect(:type_cast_from_database, 'read from database', ['whatever'])
- @type.expect(:type_cast_for_database, 'ready for database', ['read from database'])
+ @type.expect(:deserialize, 'read from database', ['whatever'])
+ @type.expect(:serialize, 'ready for database', ['read from database'])
attribute = Attribute.from_database(nil, 'whatever', @type)
- type_cast_for_database = attribute.value_for_database
+ serialize = attribute.value_for_database
- assert_equal 'ready for database', type_cast_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(:type_cast_from_user, 'read from user', ['whatever'])
- @type.expect(:type_cast_for_database, 'ready for database', ['read from user'])
+ @type.expect(:cast, 'read from user', ['whatever'])
+ @type.expect(:serialize, 'ready for database', ['read from user'])
attribute = Attribute.from_user(nil, 'whatever', @type)
- type_cast_for_database = attribute.value_for_database
+ serialize = attribute.value_for_database
- assert_equal 'ready for database', type_cast_for_database
+ assert_equal 'ready for database', serialize
end
test "duping dups the value" do
- @type.expect(:type_cast_from_database, 'type cast', ['a value'])
+ @type.expect(:deserialize, 'type cast', ['a value'])
attribute = Attribute.from_database(nil, 'a value', @type)
value_from_orig = attribute.value
@@ -89,7 +88,7 @@ module ActiveRecord
end
test "duping does not dup the value if it is not dupable" do
- @type.expect(:type_cast_from_database, false, ['a value'])
+ @type.expect(:deserialize, false, ['a value'])
attribute = Attribute.from_database(nil, 'a value', @type)
assert_same attribute.value, attribute.dup.value
@@ -101,13 +100,16 @@ module ActiveRecord
end
class MyType
- def type_cast_from_user(value)
+ def cast(value)
value + " from user"
end
- def type_cast_from_database(value)
+ 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
@@ -138,5 +140,107 @@ module ActiveRecord
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
end
end
diff --git a/activerecord/test/cases/attributes_test.rb b/activerecord/test/cases/attributes_test.rb
index 79ef0502cb..264b275181 100644
--- a/activerecord/test/cases/attributes_test.rb
+++ b/activerecord/test/cases/attributes_test.rb
@@ -1,17 +1,17 @@
require 'cases/helper'
class OverloadedType < ActiveRecord::Base
- attribute :overloaded_float, Type::Integer.new
- attribute :overloaded_string_with_limit, Type::String.new(limit: 50)
- attribute :non_existent_decimal, Type::Decimal.new
- attribute :string_with_default, Type::String.new, default: 'the overloaded default'
+ attribute :overloaded_float, :integer
+ attribute :overloaded_string_with_limit, :string, limit: 50
+ attribute :non_existent_decimal, :decimal
+ attribute :string_with_default, :string, default: 'the overloaded default'
end
class ChildOfOverloadedType < OverloadedType
end
class GrandchildOfOverloadedType < ChildOfOverloadedType
- attribute :overloaded_float, Type::Float.new
+ attribute :overloaded_float, :float
end
class UnoverloadedType < ActiveRecord::Base
@@ -20,7 +20,7 @@ end
module ActiveRecord
class CustomPropertiesTest < ActiveRecord::TestCase
- def test_overloading_types
+ test "overloading types" do
data = OverloadedType.new
data.overloaded_float = "1.1"
@@ -30,7 +30,7 @@ module ActiveRecord
assert_equal 1.1, data.unoverloaded_float
end
- def test_overloaded_properties_save
+ test "overloaded properties save" do
data = OverloadedType.new
data.overloaded_float = "2.2"
@@ -43,18 +43,18 @@ module ActiveRecord
assert_kind_of Float, UnoverloadedType.last.overloaded_float
end
- def test_properties_assigned_in_constructor
+ test "properties assigned in constructor" do
data = OverloadedType.new(overloaded_float: '3.3')
assert_equal 3, data.overloaded_float
end
- def test_overloaded_properties_with_limit
- assert_equal 50, OverloadedType.columns_hash['overloaded_string_with_limit'].limit
- assert_equal 255, UnoverloadedType.columns_hash['overloaded_string_with_limit'].limit
+ test "overloaded properties with limit" do
+ assert_equal 50, OverloadedType.type_for_attribute('overloaded_string_with_limit').limit
+ assert_equal 255, UnoverloadedType.type_for_attribute('overloaded_string_with_limit').limit
end
- def test_nonexistent_attribute
+ test "nonexistent attribute" do
data = OverloadedType.new(non_existent_decimal: 1)
assert_equal BigDecimal.new(1), data.non_existent_decimal
@@ -63,7 +63,7 @@ module ActiveRecord
end
end
- def test_changing_defaults
+ test "changing defaults" do
data = OverloadedType.new
unoverloaded_data = UnoverloadedType.new
@@ -71,41 +71,106 @@ module ActiveRecord
assert_equal 'the original default', unoverloaded_data.string_with_default
end
- def test_children_inherit_custom_properties
+ test "defaults are not touched on the columns" do
+ assert_equal 'the original default', OverloadedType.columns_hash['string_with_default'].default
+ end
+
+ test "children inherit custom properties" do
data = ChildOfOverloadedType.new(overloaded_float: '4.4')
assert_equal 4, data.overloaded_float
end
- def test_children_can_override_parents
+ test "children can override parents" do
data = GrandchildOfOverloadedType.new(overloaded_float: '4.4')
assert_equal 4.4, data.overloaded_float
end
- def test_overloading_properties_does_not_change_column_order
- column_names = OverloadedType.column_names
- assert_equal %w(id overloaded_float unoverloaded_float overloaded_string_with_limit string_with_default non_existent_decimal), column_names
+ test "overloading properties does not attribute method order" do
+ attribute_names = OverloadedType.attribute_names
+ assert_equal %w(id overloaded_float unoverloaded_float overloaded_string_with_limit string_with_default non_existent_decimal), attribute_names
end
- def test_caches_are_cleared
+ test "caches are cleared" do
klass = Class.new(OverloadedType)
- assert_equal 6, klass.columns.length
- assert_not klass.columns_hash.key?('wibble')
- assert_equal 6, klass.column_types.length
+ assert_equal 6, klass.attribute_types.length
assert_equal 6, klass.column_defaults.length
- assert_not klass.column_names.include?('wibble')
- assert_equal 5, klass.content_columns.length
+ assert_not klass.attribute_types.include?('wibble')
klass.attribute :wibble, Type::Value.new
- assert_equal 7, klass.columns.length
- assert klass.columns_hash.key?('wibble')
- assert_equal 7, klass.column_types.length
+ assert_equal 7, klass.attribute_types.length
assert_equal 7, klass.column_defaults.length
- assert klass.column_names.include?('wibble')
- assert_equal 6, klass.content_columns.length
+ assert klass.attribute_types.include?('wibble')
+ end
+
+ test "the given default value is cast from user" do
+ custom_type = Class.new(Type::Value) do
+ def cast(*)
+ "from user"
+ end
+
+ def deserialize(*)
+ "from database"
+ end
+ end
+
+ klass = Class.new(OverloadedType) do
+ attribute :wibble, custom_type.new, default: "default"
+ end
+ model = klass.new
+
+ assert_equal "from user", model.wibble
+ end
+
+ test "procs for default values" do
+ klass = Class.new(OverloadedType) do
+ @@counter = 0
+ attribute :counter, :integer, default: -> { @@counter += 1 }
+ end
+
+ assert_equal 1, klass.new.counter
+ assert_equal 2, klass.new.counter
+ end
+
+ test "user provided defaults are persisted even if unchanged" do
+ model = OverloadedType.create!
+
+ assert_equal "the overloaded default", model.reload.string_with_default
+ end
+
+ if current_adapter?(:PostgreSQLAdapter)
+ test "array types can be specified" do
+ klass = Class.new(OverloadedType) do
+ attribute :my_array, :string, limit: 50, array: true
+ attribute :my_int_array, :integer, array: true
+ end
+
+ string_array = ConnectionAdapters::PostgreSQL::OID::Array.new(
+ Type::String.new(limit: 50))
+ int_array = ConnectionAdapters::PostgreSQL::OID::Array.new(
+ Type::Integer.new)
+ assert_not_equal string_array, int_array
+ assert_equal string_array, klass.type_for_attribute("my_array")
+ assert_equal int_array, klass.type_for_attribute("my_int_array")
+ end
+
+ test "range types can be specified" do
+ klass = Class.new(OverloadedType) do
+ attribute :my_range, :string, limit: 50, range: true
+ attribute :my_int_range, :integer, range: true
+ end
+
+ string_range = ConnectionAdapters::PostgreSQL::OID::Range.new(
+ Type::String.new(limit: 50))
+ int_range = ConnectionAdapters::PostgreSQL::OID::Range.new(
+ Type::Integer.new)
+ assert_not_equal string_range, int_range
+ assert_equal string_range, klass.type_for_attribute("my_range")
+ assert_equal int_range, klass.type_for_attribute("my_int_range")
+ end
end
end
end
diff --git a/activerecord/test/cases/autosave_association_test.rb b/activerecord/test/cases/autosave_association_test.rb
index 09892d50ba..0df8f1f798 100644
--- a/activerecord/test/cases/autosave_association_test.rb
+++ b/activerecord/test/cases/autosave_association_test.rb
@@ -1,8 +1,10 @@
require 'cases/helper'
require 'models/bird'
+require 'models/comment'
require 'models/company'
require 'models/customer'
require 'models/developer'
+require 'models/computer'
require 'models/invoice'
require 'models/line_item'
require 'models/order'
@@ -19,6 +21,11 @@ require 'models/treasure'
require 'models/eye'
require 'models/electron'
require 'models/molecule'
+require 'models/member'
+require 'models/member_detail'
+require 'models/organization'
+require 'models/guitar'
+require 'models/tuning_peg'
class TestAutosaveAssociationsInGeneral < ActiveRecord::TestCase
def test_autosave_validation
@@ -38,7 +45,7 @@ class TestAutosaveAssociationsInGeneral < ActiveRecord::TestCase
reference = Class.new(ActiveRecord::Base) {
self.table_name = "references"
def self.name; 'Reference'; end
- belongs_to :person, autosave: true, class: person
+ belongs_to :person, autosave: true, anonymous_class: person
}
u = person.create!(first_name: 'cool')
@@ -62,6 +69,14 @@ class TestAutosaveAssociationsInGeneral < ActiveRecord::TestCase
assert_no_difference_when_adding_callbacks_twice_for Pirate, :parrots
end
+ def test_cyclic_autosaves_do_not_add_multiple_validations
+ ship = ShipWithoutNestedAttributes.new
+ ship.prisoners.build
+
+ assert_not ship.valid?
+ assert_equal 1, ship.errors[:name].length
+ end
+
private
def assert_no_difference_when_adding_callbacks_twice_for(model, association_name)
@@ -144,7 +159,8 @@ class TestDefaultAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCas
assert_equal a, firm.account
assert firm.save
assert_equal a, firm.account
- assert_equal a, firm.account(true)
+ firm.association(:account).reload
+ assert_equal a, firm.account
end
def test_assignment_before_either_saved
@@ -157,7 +173,8 @@ class TestDefaultAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCas
assert firm.persisted?
assert a.persisted?
assert_equal a, firm.account
- assert_equal a, firm.account(true)
+ firm.association(:account).reload
+ assert_equal a, firm.account
end
def test_not_resaved_when_unchanged
@@ -243,7 +260,8 @@ class TestDefaultAutosaveAssociationOnABelongsToAssociation < ActiveRecord::Test
assert apple.save
assert apple.persisted?
assert_equal apple, client.firm
- assert_equal apple, client.firm(true)
+ client.association(:firm).reload
+ assert_equal apple, client.firm
end
def test_assignment_before_either_saved
@@ -256,7 +274,8 @@ class TestDefaultAutosaveAssociationOnABelongsToAssociation < ActiveRecord::Test
assert final_cut.persisted?
assert apple.persisted?
assert_equal apple, final_cut.firm
- assert_equal apple, final_cut.firm(true)
+ final_cut.association(:firm).reload
+ assert_equal apple, final_cut.firm
end
def test_store_two_association_with_one_save
@@ -380,6 +399,40 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociationWithAcceptsNestedAttrib
assert_not molecule.persisted?, 'Molecule should not be persisted when its electrons are invalid'
end
+ def test_errors_should_be_indexed_when_passed_as_array
+ guitar = Guitar.new
+ tuning_peg_valid = TuningPeg.new
+ tuning_peg_valid.pitch = 440.0
+ tuning_peg_invalid = TuningPeg.new
+
+ guitar.tuning_pegs = [tuning_peg_valid, tuning_peg_invalid]
+
+ assert_not tuning_peg_invalid.valid?
+ assert tuning_peg_valid.valid?
+ assert_not guitar.valid?
+ assert_equal ["is not a number"], guitar.errors["tuning_pegs[1].pitch"]
+ assert_not_equal ["is not a number"], guitar.errors["tuning_pegs.pitch"]
+ end
+
+ def test_errors_should_be_indexed_when_global_flag_is_set
+ old_attribute_config = ActiveRecord::Base.index_nested_attribute_errors
+ ActiveRecord::Base.index_nested_attribute_errors = true
+
+ molecule = Molecule.new
+ valid_electron = Electron.new(name: 'electron')
+ invalid_electron = Electron.new
+
+ molecule.electrons = [valid_electron, invalid_electron]
+
+ assert_not invalid_electron.valid?
+ assert valid_electron.valid?
+ assert_not molecule.valid?
+ assert_equal ["can't be blank"], molecule.errors["electrons[1].name"]
+ assert_not_equal ["can't be blank"], molecule.errors["electrons.name"]
+ ensure
+ ActiveRecord::Base.index_nested_attribute_errors = old_attribute_config
+ end
+
def test_valid_adding_with_nested_attributes
molecule = Molecule.new
valid_electron = Electron.new(name: 'electron')
@@ -451,7 +504,7 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa
assert_equal new_client, companies(:first_firm).clients_of_firm.last
assert !companies(:first_firm).save
assert !new_client.persisted?
- assert_equal 2, companies(:first_firm).clients_of_firm(true).size
+ assert_equal 2, companies(:first_firm).clients_of_firm.reload.size
end
def test_adding_before_save
@@ -476,7 +529,7 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa
assert_equal no_of_clients + 2, Client.count # Clients were saved to database.
assert_equal 2, new_firm.clients_of_firm.size
- assert_equal 2, new_firm.clients_of_firm(true).size
+ assert_equal 2, new_firm.clients_of_firm.reload.size
end
def test_assign_ids
@@ -499,38 +552,38 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa
def test_build_before_save
company = companies(:first_firm)
- new_client = assert_no_queries { company.clients_of_firm.build("name" => "Another Client") }
+ new_client = assert_no_queries(ignore_none: false) { company.clients_of_firm.build("name" => "Another Client") }
assert !company.clients_of_firm.loaded?
company.name += '-changed'
assert_queries(2) { assert company.save }
assert new_client.persisted?
- assert_equal 3, company.clients_of_firm(true).size
+ assert_equal 3, company.clients_of_firm.reload.size
end
def test_build_many_before_save
company = companies(:first_firm)
- assert_no_queries { company.clients_of_firm.build([{"name" => "Another Client"}, {"name" => "Another Client II"}]) }
+ assert_no_queries(ignore_none: false) { company.clients_of_firm.build([{"name" => "Another Client"}, {"name" => "Another Client II"}]) }
company.name += '-changed'
assert_queries(3) { assert company.save }
- assert_equal 4, company.clients_of_firm(true).size
+ assert_equal 4, company.clients_of_firm.reload.size
end
def test_build_via_block_before_save
company = companies(:first_firm)
- new_client = assert_no_queries { company.clients_of_firm.build {|client| client.name = "Another Client" } }
+ new_client = assert_no_queries(ignore_none: false) { company.clients_of_firm.build {|client| client.name = "Another Client" } }
assert !company.clients_of_firm.loaded?
company.name += '-changed'
assert_queries(2) { assert company.save }
assert new_client.persisted?
- assert_equal 3, company.clients_of_firm(true).size
+ assert_equal 3, company.clients_of_firm.reload.size
end
def test_build_many_via_block_before_save
company = companies(:first_firm)
- assert_no_queries do
+ assert_no_queries(ignore_none: false) do
company.clients_of_firm.build([{"name" => "Another Client"}, {"name" => "Another Client II"}]) do |client|
client.name = "changed"
end
@@ -538,7 +591,7 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa
company.name += '-changed'
assert_queries(3) { assert company.save }
- assert_equal 4, company.clients_of_firm(true).size
+ assert_equal 4, company.clients_of_firm.reload.size
end
def test_replace_on_new_object
@@ -613,10 +666,18 @@ class TestDefaultAutosaveAssociationOnNewRecord < ActiveRecord::TestCase
firm.save!
assert !account.persisted?
end
+
+ def test_autosave_new_record_with_after_create_callback
+ post = PostWithAfterCreateCallback.new(title: 'Captain Murphy', body: 'is back')
+ post.comments.build(body: 'foo')
+ post.save!
+
+ assert_not_nil post.author_id
+ end
end
class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
- self.use_transactional_fixtures = false
+ self.use_transactional_tests = false
setup do
@pirate = Pirate.create(:catchphrase => "Don' botharrr talkin' like one, savvy?")
@@ -624,7 +685,7 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
end
teardown do
- # We are running without transactional fixtures and need to cleanup.
+ # We are running without transactional tests and need to cleanup.
Bird.delete_all
Parrot.delete_all
@ship.delete
@@ -761,13 +822,13 @@ 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? { |child| child.marked_for_destruction? }
+ assert !@pirate.birds.any?(&:marked_for_destruction?)
- @pirate.birds.each { |child| child.mark_for_destruction }
+ @pirate.birds.each(&:mark_for_destruction)
klass = @pirate.birds.first.class
ids = @pirate.birds.map(&:id)
- assert @pirate.birds.all? { |child| child.marked_for_destruction? }
+ assert @pirate.birds.all?(&:marked_for_destruction?)
ids.each { |id| assert klass.find_by_id(id) }
@pirate.save
@@ -801,14 +862,14 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
@pirate.birds.each { |bird| bird.name = '' }
assert !@pirate.valid?
- @pirate.birds.each { |bird| bird.destroy }
+ @pirate.birds.each(&:destroy)
assert @pirate.valid?
end
def test_a_child_marked_for_destruction_should_not_be_destroyed_twice_while_saving_has_many
@pirate.birds.create!(:name => "birds_1")
- @pirate.birds.each { |bird| bird.mark_for_destruction }
+ @pirate.birds.each(&:mark_for_destruction)
assert @pirate.save
@pirate.birds.each { |bird| bird.expects(:destroy).never }
@@ -875,7 +936,7 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
association_name_with_callbacks = "birds_with_#{callback_type}_callbacks"
@pirate.send(association_name_with_callbacks).create!(:name => "Crowe the One-Eyed")
- @pirate.send(association_name_with_callbacks).each { |c| c.mark_for_destruction }
+ @pirate.send(association_name_with_callbacks).each(&:mark_for_destruction)
child_id = @pirate.send(association_name_with_callbacks).first.id
@pirate.ship_log.clear
@@ -893,8 +954,8 @@ 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? { |parrot| parrot.marked_for_destruction? }
- @pirate.parrots.each { |parrot| parrot.mark_for_destruction }
+ assert !@pirate.parrots.any?(&:marked_for_destruction?)
+ @pirate.parrots.each(&:mark_for_destruction)
assert_no_difference "Parrot.count" do
@pirate.save
@@ -927,14 +988,14 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
@pirate.parrots.each { |parrot| parrot.name = '' }
assert !@pirate.valid?
- @pirate.parrots.each { |parrot| parrot.destroy }
+ @pirate.parrots.each(&:destroy)
assert @pirate.valid?
end
def test_a_child_marked_for_destruction_should_not_be_destroyed_twice_while_saving_habtm
@pirate.parrots.create!(:name => "parrots_1")
- @pirate.parrots.each { |parrot| parrot.mark_for_destruction }
+ @pirate.parrots.each(&:mark_for_destruction)
assert @pirate.save
Pirate.transaction do
@@ -979,7 +1040,7 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
association_name_with_callbacks = "parrots_with_#{callback_type}_callbacks"
@pirate.send(association_name_with_callbacks).create!(:name => "Crowe the One-Eyed")
- @pirate.send(association_name_with_callbacks).each { |c| c.mark_for_destruction }
+ @pirate.send(association_name_with_callbacks).each(&:mark_for_destruction)
child_id = @pirate.send(association_name_with_callbacks).first.id
@pirate.ship_log.clear
@@ -996,7 +1057,7 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
end
class TestAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCase
- self.use_transactional_fixtures = false unless supports_savepoints?
+ self.use_transactional_tests = false unless supports_savepoints?
def setup
super
@@ -1017,6 +1078,16 @@ class TestAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCase
assert_equal 'The Vile Insanity', @pirate.reload.ship.name
end
+ def test_changed_for_autosave_should_handle_cycles
+ @ship.pirate = @pirate
+ assert_queries(0) { @ship.save! }
+
+ @parrot = @pirate.parrots.create(name: "some_name")
+ @parrot.name="changed_name"
+ assert_queries(1) { @ship.save! }
+ assert_queries(0) { @ship.save! }
+ end
+
def test_should_automatically_save_bang_the_associated_model
@pirate.ship.name = 'The Vile Insanity'
@pirate.save!
@@ -1038,11 +1109,16 @@ class TestAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCase
end
def test_should_not_ignore_different_error_messages_on_the_same_attribute
+ old_validators = Ship._validators.deep_dup
+ old_callbacks = Ship._validate_callbacks.deep_dup
Ship.validates_format_of :name, :with => /\w/
@pirate.ship.name = ""
@pirate.catchphrase = nil
assert @pirate.invalid?
assert_equal ["can't be blank", "is invalid"], @pirate.errors[:"ship.name"]
+ ensure
+ Ship._validators = old_validators if old_validators
+ Ship._validate_callbacks = old_callbacks if old_callbacks
end
def test_should_still_allow_to_bypass_validations_on_the_associated_model
@@ -1114,10 +1190,38 @@ class TestAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCase
def test_should_not_load_the_associated_model
assert_queries(1) { @pirate.catchphrase = 'Arr'; @pirate.save! }
end
+
+ def test_mark_for_destruction_is_ignored_without_autosave_true
+ ship = ShipWithoutNestedAttributes.new(name: "The Black Flag")
+ ship.parts.build.mark_for_destruction
+
+ assert_not ship.valid?
+ end
+end
+
+class TestAutosaveAssociationOnAHasOneThroughAssociation < ActiveRecord::TestCase
+ self.use_transactional_tests = false unless supports_savepoints?
+
+ def setup
+ super
+ organization = Organization.create
+ @member = Member.create
+ MemberDetail.create(organization: organization, member: @member)
+ end
+
+ def test_should_not_has_one_through_model
+ class << @member.organization
+ def save(*args)
+ super
+ raise 'Oh noes!'
+ end
+ end
+ assert_nothing_raised { @member.save }
+ end
end
class TestAutosaveAssociationOnABelongsToAssociation < ActiveRecord::TestCase
- self.use_transactional_fixtures = false unless supports_savepoints?
+ self.use_transactional_tests = false unless supports_savepoints?
def setup
super
@@ -1229,6 +1333,16 @@ module AutosaveAssociationOnACollectionAssociationTests
assert_equal new_names, @pirate.reload.send(@association_name).map(&:name)
end
+ def test_should_update_children_when_autosave_is_true_and_parent_is_new_but_child_is_not
+ parrot = Parrot.create!(name: "Polly")
+ parrot.name = "Squawky"
+ pirate = Pirate.new(parrots: [parrot], catchphrase: "Arrrr")
+
+ pirate.save!
+
+ assert_equal "Squawky", parrot.reload.name
+ end
+
def test_should_automatically_validate_the_associated_models
@pirate.send(@association_name).each { |child| child.name = '' }
@@ -1365,7 +1479,7 @@ module AutosaveAssociationOnACollectionAssociationTests
end
class TestAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCase
- self.use_transactional_fixtures = false unless supports_savepoints?
+ self.use_transactional_tests = false unless supports_savepoints?
def setup
super
@@ -1381,7 +1495,7 @@ class TestAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCase
end
class TestAutosaveAssociationOnAHasAndBelongsToManyAssociation < ActiveRecord::TestCase
- self.use_transactional_fixtures = false unless supports_savepoints?
+ self.use_transactional_tests = false unless supports_savepoints?
def setup
super
@@ -1398,7 +1512,7 @@ class TestAutosaveAssociationOnAHasAndBelongsToManyAssociation < ActiveRecord::T
end
class TestAutosaveAssociationOnAHasAndBelongsToManyAssociationWithAcceptsNestedAttributes < ActiveRecord::TestCase
- self.use_transactional_fixtures = false unless supports_savepoints?
+ self.use_transactional_tests = false unless supports_savepoints?
def setup
super
@@ -1415,7 +1529,7 @@ class TestAutosaveAssociationOnAHasAndBelongsToManyAssociationWithAcceptsNestedA
end
class TestAutosaveAssociationValidationsOnAHasManyAssociation < ActiveRecord::TestCase
- self.use_transactional_fixtures = false unless supports_savepoints?
+ self.use_transactional_tests = false unless supports_savepoints?
def setup
super
@@ -1432,7 +1546,7 @@ class TestAutosaveAssociationValidationsOnAHasManyAssociation < ActiveRecord::Te
end
class TestAutosaveAssociationValidationsOnAHasOneAssociation < ActiveRecord::TestCase
- self.use_transactional_fixtures = false unless supports_savepoints?
+ self.use_transactional_tests = false unless supports_savepoints?
def setup
super
@@ -1455,7 +1569,7 @@ class TestAutosaveAssociationValidationsOnAHasOneAssociation < ActiveRecord::Tes
end
class TestAutosaveAssociationValidationsOnABelongsToAssociation < ActiveRecord::TestCase
- self.use_transactional_fixtures = false unless supports_savepoints?
+ self.use_transactional_tests = false unless supports_savepoints?
def setup
super
@@ -1476,7 +1590,7 @@ class TestAutosaveAssociationValidationsOnABelongsToAssociation < ActiveRecord::
end
class TestAutosaveAssociationValidationsOnAHABTMAssociation < ActiveRecord::TestCase
- self.use_transactional_fixtures = false unless supports_savepoints?
+ self.use_transactional_tests = false unless supports_savepoints?
def setup
super
@@ -1499,7 +1613,7 @@ class TestAutosaveAssociationValidationsOnAHABTMAssociation < ActiveRecord::Test
end
class TestAutosaveAssociationValidationMethodsGeneration < ActiveRecord::TestCase
- self.use_transactional_fixtures = false unless supports_savepoints?
+ self.use_transactional_tests = false unless supports_savepoints?
def setup
super
diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb
index 4c0b0c868a..dbbcaa075d 100644
--- a/activerecord/test/cases/base_test.rb
+++ b/activerecord/test/cases/base_test.rb
@@ -1,7 +1,4 @@
-# encoding: utf-8
-
require "cases/helper"
-require 'active_support/concurrency/latch'
require 'models/post'
require 'models/author'
require 'models/topic'
@@ -10,6 +7,7 @@ require 'models/category'
require 'models/company'
require 'models/customer'
require 'models/developer'
+require 'models/computer'
require 'models/project'
require 'models/default'
require 'models/auto_id'
@@ -28,6 +26,7 @@ require 'models/bird'
require 'models/car'
require 'models/bulb'
require 'rexml/document'
+require 'concurrent/atomics'
class FirstAbstractClass < ActiveRecord::Base
self.abstract_class = true
@@ -87,6 +86,7 @@ class BasicsTest < ActiveRecord::TestCase
'Mysql2Adapter' => '`',
'PostgreSQLAdapter' => '"',
'OracleAdapter' => '"',
+ 'FbAdapter' => '"'
}.fetch(classname) {
raise "need a bad char for #{classname}"
}
@@ -110,7 +110,7 @@ class BasicsTest < ActiveRecord::TestCase
assert_nil Edge.primary_key
end
- unless current_adapter?(:PostgreSQLAdapter, :OracleAdapter, :SQLServerAdapter)
+ unless current_adapter?(:PostgreSQLAdapter, :OracleAdapter, :SQLServerAdapter, :FbAdapter)
def test_limit_with_comma
assert Topic.limit("1,2").to_a
end
@@ -204,7 +204,7 @@ class BasicsTest < ActiveRecord::TestCase
)
# For adapters which support microsecond resolution.
- if current_adapter?(:PostgreSQLAdapter, :SQLite3Adapter) || mysql_56?
+ if subsecond_precision_supported?
assert_equal 11, Topic.find(1).written_on.sec
assert_equal 223300, Topic.find(1).written_on.usec
assert_equal 9900, Topic.find(2).written_on.usec
@@ -522,6 +522,10 @@ class BasicsTest < ActiveRecord::TestCase
assert_equal Topic.find(['1-meowmeow', '2-hello']), Topic.find([1, 2])
end
+ def test_find_by_slug_with_range
+ assert_equal Topic.where(id: '1-meowmeow'..'2-hello'), Topic.where(id: 1..2)
+ end
+
def test_equality_of_new_records
assert_not_equal Topic.new, Topic.new
assert_equal false, Topic.new == Topic.new
@@ -807,7 +811,6 @@ class BasicsTest < ActiveRecord::TestCase
def test_dup_does_not_copy_associations
author = authors(:david)
assert_not_equal [], author.posts
- author.send(:clear_association_cache)
author_dup = author.dup
assert_equal [], author_dup.posts
@@ -893,101 +896,13 @@ class BasicsTest < ActiveRecord::TestCase
assert_equal 'a text field', default.char3
end
end
-
- class Geometric < ActiveRecord::Base; end
- def test_geometric_content
-
- # accepted format notes:
- # ()'s aren't required
- # values can be a mix of float or integer
-
- g = Geometric.new(
- :a_point => '(5.0, 6.1)',
- #:a_line => '((2.0, 3), (5.5, 7.0))' # line type is currently unsupported in postgresql
- :a_line_segment => '(2.0, 3), (5.5, 7.0)',
- :a_box => '2.0, 3, 5.5, 7.0',
- :a_path => '[(2.0, 3), (5.5, 7.0), (8.5, 11.0)]', # [ ] is an open path
- :a_polygon => '((2.0, 3), (5.5, 7.0), (8.5, 11.0))',
- :a_circle => '<(5.3, 10.4), 2>'
- )
-
- assert g.save
-
- # Reload and check that we have all the geometric attributes.
- h = Geometric.find(g.id)
-
- assert_equal [5.0, 6.1], h.a_point
- assert_equal '[(2,3),(5.5,7)]', h.a_line_segment
- assert_equal '(5.5,7),(2,3)', h.a_box # reordered to store upper right corner then bottom left corner
- assert_equal '[(2,3),(5.5,7),(8.5,11)]', h.a_path
- assert_equal '((2,3),(5.5,7),(8.5,11))', h.a_polygon
- assert_equal '<(5.3,10.4),2>', h.a_circle
-
- # use a geometric function to test for an open path
- objs = Geometric.find_by_sql ["select isopen(a_path) from geometrics where id = ?", g.id]
-
- assert_equal true, objs[0].isopen
-
- # test alternate formats when defining the geometric types
-
- g = Geometric.new(
- :a_point => '5.0, 6.1',
- #:a_line => '((2.0, 3), (5.5, 7.0))' # line type is currently unsupported in postgresql
- :a_line_segment => '((2.0, 3), (5.5, 7.0))',
- :a_box => '(2.0, 3), (5.5, 7.0)',
- :a_path => '((2.0, 3), (5.5, 7.0), (8.5, 11.0))', # ( ) is a closed path
- :a_polygon => '2.0, 3, 5.5, 7.0, 8.5, 11.0',
- :a_circle => '((5.3, 10.4), 2)'
- )
-
- assert g.save
-
- # Reload and check that we have all the geometric attributes.
- h = Geometric.find(g.id)
-
- assert_equal [5.0, 6.1], h.a_point
- assert_equal '[(2,3),(5.5,7)]', h.a_line_segment
- assert_equal '(5.5,7),(2,3)', h.a_box # reordered to store upper right corner then bottom left corner
- assert_equal '((2,3),(5.5,7),(8.5,11))', h.a_path
- assert_equal '((2,3),(5.5,7),(8.5,11))', h.a_polygon
- assert_equal '<(5.3,10.4),2>', h.a_circle
-
- # use a geometric function to test for an closed path
- objs = Geometric.find_by_sql ["select isclosed(a_path) from geometrics where id = ?", g.id]
-
- assert_equal true, objs[0].isclosed
-
- # test native ruby formats when defining the geometric types
- g = Geometric.new(
- :a_point => [5.0, 6.1],
- #:a_line => '((2.0, 3), (5.5, 7.0))' # line type is currently unsupported in postgresql
- :a_line_segment => '((2.0, 3), (5.5, 7.0))',
- :a_box => '(2.0, 3), (5.5, 7.0)',
- :a_path => '((2.0, 3), (5.5, 7.0), (8.5, 11.0))', # ( ) is a closed path
- :a_polygon => '2.0, 3, 5.5, 7.0, 8.5, 11.0',
- :a_circle => '((5.3, 10.4), 2)'
- )
-
- assert g.save
-
- # Reload and check that we have all the geometric attributes.
- h = Geometric.find(g.id)
-
- assert_equal [5.0, 6.1], h.a_point
- assert_equal '[(2,3),(5.5,7)]', h.a_line_segment
- assert_equal '(5.5,7),(2,3)', h.a_box # reordered to store upper right corner then bottom left corner
- assert_equal '((2,3),(5.5,7),(8.5,11))', h.a_path
- assert_equal '((2,3),(5.5,7),(8.5,11))', h.a_polygon
- assert_equal '<(5.3,10.4),2>', h.a_circle
- end
end
class NumericData < ActiveRecord::Base
self.table_name = 'numeric_data'
- attribute :world_population, Type::Integer.new
- attribute :my_house_population, Type::Integer.new
- attribute :atoms_in_universe, Type::Integer.new
+ attribute :my_house_population, :integer
+ attribute :atoms_in_universe, :integer
end
def test_big_decimal_conditions
@@ -1029,6 +944,34 @@ class BasicsTest < ActiveRecord::TestCase
assert_equal BigDecimal("1000234000567.95"), m1.big_bank_balance
end
+ def test_numeric_fields_with_scale
+ m = NumericData.new(
+ :bank_balance => 1586.43122334,
+ :big_bank_balance => BigDecimal("234000567.952344"),
+ :world_population => 6000000000,
+ :my_house_population => 3
+ )
+ assert m.save
+
+ m1 = NumericData.find(m.id)
+ assert_not_nil m1
+
+ # As with migration_test.rb, we should make world_population >= 2**62
+ # to cover 64-bit platforms and test it is a Bignum, but the main thing
+ # is that it's an Integer.
+ assert_kind_of Integer, m1.world_population
+ assert_equal 6000000000, m1.world_population
+
+ assert_kind_of Fixnum, m1.my_house_population
+ assert_equal 3, m1.my_house_population
+
+ assert_kind_of BigDecimal, m1.bank_balance
+ assert_equal BigDecimal("1586.43"), m1.bank_balance
+
+ assert_kind_of BigDecimal, m1.big_bank_balance
+ assert_equal BigDecimal("234000567.95"), m1.big_bank_balance
+ end
+
def test_auto_id
auto = AutoId.new
auto.save
@@ -1092,54 +1035,61 @@ class BasicsTest < ActiveRecord::TestCase
end
def test_switching_between_table_name
+ k = Class.new(Joke)
+
assert_difference("GoodJoke.count") do
- Joke.table_name = "cold_jokes"
- Joke.create
+ k.table_name = "cold_jokes"
+ k.create
- Joke.table_name = "funny_jokes"
- Joke.create
+ k.table_name = "funny_jokes"
+ k.create
end
end
def test_clear_cash_when_setting_table_name
- Joke.table_name = "cold_jokes"
- before_columns = Joke.columns
- before_seq = Joke.sequence_name
+ original_table_name = Joke.table_name
Joke.table_name = "funny_jokes"
+ before_columns = Joke.columns
+ before_seq = Joke.sequence_name
+
+ Joke.table_name = "cold_jokes"
after_columns = Joke.columns
- after_seq = Joke.sequence_name
+ after_seq = Joke.sequence_name
assert_not_equal before_columns, after_columns
assert_not_equal before_seq, after_seq unless before_seq.nil? && after_seq.nil?
+ ensure
+ Joke.table_name = original_table_name
end
def test_dont_clear_sequence_name_when_setting_explicitly
- Joke.sequence_name = "black_jokes_seq"
- Joke.table_name = "cold_jokes"
- before_seq = Joke.sequence_name
+ k = Class.new(Joke)
+ k.sequence_name = "black_jokes_seq"
+ k.table_name = "cold_jokes"
+ before_seq = k.sequence_name
- Joke.table_name = "funny_jokes"
- after_seq = Joke.sequence_name
+ k.table_name = "funny_jokes"
+ after_seq = k.sequence_name
assert_equal before_seq, after_seq unless before_seq.nil? && after_seq.nil?
- ensure
- Joke.reset_sequence_name
end
def test_dont_clear_inheritance_column_when_setting_explicitly
- Joke.inheritance_column = "my_type"
- before_inherit = Joke.inheritance_column
+ k = Class.new(Joke)
+ k.inheritance_column = "my_type"
+ before_inherit = k.inheritance_column
- Joke.reset_column_information
- after_inherit = Joke.inheritance_column
+ k.reset_column_information
+ after_inherit = k.inheritance_column
assert_equal before_inherit, after_inherit unless before_inherit.blank? && after_inherit.blank?
end
def test_set_table_name_symbol_converted_to_string
- Joke.table_name = :cold_jokes
- assert_equal 'cold_jokes', Joke.table_name
+ k = Class.new(Joke)
+ k.table_name = :cold_jokes
+ assert_equal 'cold_jokes', k.table_name
end
def test_quoted_table_name_after_set_table_name
@@ -1347,9 +1297,10 @@ class BasicsTest < ActiveRecord::TestCase
end
def test_compute_type_no_method_error
- ActiveSupport::Dependencies.stubs(:safe_constantize).raises(NoMethodError)
- assert_raises NoMethodError do
- ActiveRecord::Base.send :compute_type, 'InvalidModel'
+ ActiveSupport::Dependencies.stub(:safe_constantize, proc{ raise NoMethodError }) do
+ assert_raises NoMethodError do
+ ActiveRecord::Base.send :compute_type, 'InvalidModel'
+ end
end
end
@@ -1363,18 +1314,20 @@ class BasicsTest < ActiveRecord::TestCase
error = e
end
- ActiveSupport::Dependencies.stubs(:safe_constantize).raises(e)
+ ActiveSupport::Dependencies.stub(:safe_constantize, proc{ raise e }) do
- exception = assert_raises NameError do
- ActiveRecord::Base.send :compute_type, 'InvalidModel'
+ exception = assert_raises NameError do
+ ActiveRecord::Base.send :compute_type, 'InvalidModel'
+ end
+ assert_equal error.message, exception.message
end
- assert_equal error.message, exception.message
end
def test_compute_type_argument_error
- ActiveSupport::Dependencies.stubs(:safe_constantize).raises(ArgumentError)
- assert_raises ArgumentError do
- ActiveRecord::Base.send :compute_type, 'InvalidModel'
+ ActiveSupport::Dependencies.stub(:safe_constantize, proc{ raise ArgumentError }) do
+ assert_raises ArgumentError do
+ ActiveRecord::Base.send :compute_type, 'InvalidModel'
+ end
end
end
@@ -1383,7 +1336,10 @@ class BasicsTest < ActiveRecord::TestCase
c1 = Post.connection.schema_cache.columns('posts')
ActiveRecord::Base.clear_cache!
c2 = Post.connection.schema_cache.columns('posts')
- assert_not_equal c1, c2
+ c1.each_with_index do |v, i|
+ assert_not_same v, c2[i]
+ end
+ assert_equal c1, c2
end
def test_current_scope_is_reset
@@ -1484,15 +1440,13 @@ class BasicsTest < ActiveRecord::TestCase
end
def test_uniq_delegates_to_scoped
- scope = stub
- Bird.stubs(:all).returns(mock(:uniq => scope))
- assert_equal scope, Bird.uniq
+ assert_deprecated do
+ assert_equal Bird.all.distinct, Bird.uniq
+ end
end
def test_distinct_delegates_to_scoped
- scope = stub
- Bird.stubs(:all).returns(mock(:distinct => scope))
- assert_equal scope, Bird.distinct
+ assert_equal Bird.all.distinct, Bird.distinct
end
def test_table_name_with_2_abstract_subclasses
@@ -1507,7 +1461,7 @@ class BasicsTest < ActiveRecord::TestCase
attrs.delete 'id'
typecast = Class.new(ActiveRecord::Type::Value) {
- def type_cast value
+ def cast value
"t.lo"
end
}
@@ -1540,20 +1494,6 @@ class BasicsTest < ActiveRecord::TestCase
assert_equal "", Company.new.description
end
- ["find_by", "find_by!"].each do |meth|
- test "#{meth} delegates to scoped" do
- record = stub
-
- scope = mock
- scope.expects(meth).with(:foo, :bar).returns(record)
-
- klass = Class.new(ActiveRecord::Base)
- klass.stubs(:all => scope)
-
- assert_equal record, klass.public_send(meth, :foo, :bar)
- end
- end
-
test "scoped can take a values hash" do
klass = Class.new(ActiveRecord::Base)
assert_equal ['foo'], klass.all.merge!(select: 'foo').select_values
@@ -1595,20 +1535,20 @@ class BasicsTest < ActiveRecord::TestCase
orig_handler = klass.connection_handler
new_handler = ActiveRecord::ConnectionAdapters::ConnectionHandler.new
after_handler = nil
- latch1 = ActiveSupport::Concurrency::Latch.new
- latch2 = ActiveSupport::Concurrency::Latch.new
+ latch1 = Concurrent::CountDownLatch.new
+ latch2 = Concurrent::CountDownLatch.new
t = Thread.new do
klass.connection_handler = new_handler
- latch1.release
- latch2.await
+ latch1.count_down
+ latch2.wait
after_handler = klass.connection_handler
end
- latch1.await
+ latch1.wait
klass.connection_handler = orig_handler
- latch2.release
+ latch2.count_down
t.join
assert_equal after_handler, new_handler
@@ -1621,4 +1561,32 @@ class BasicsTest < ActiveRecord::TestCase
test "records without an id have unique hashes" do
assert_not_equal Post.new.hash, Post.new.hash
end
+
+ test "resetting column information doesn't remove attribute methods" do
+ topic = topics(:first)
+
+ assert_not topic.id_changed?
+
+ Topic.reset_column_information
+
+ assert_not topic.id_changed?
+ end
+
+ test "ignored columns are not present in columns_hash" do
+ cache_columns = Developer.connection.schema_cache.columns_hash(Developer.table_name)
+ assert_includes cache_columns.keys, 'first_name'
+ refute_includes Developer.columns_hash.keys, 'first_name'
+ end
+
+ test "ignored columns have no attribute methods" do
+ refute Developer.new.respond_to?(:first_name)
+ refute Developer.new.respond_to?(:first_name=)
+ refute Developer.new.respond_to?(:first_name?)
+ end
+
+ test "ignored columns don't prevent explicit declaration of attribute methods" do
+ assert Developer.new.respond_to?(:last_name)
+ assert Developer.new.respond_to?(:last_name=)
+ assert Developer.new.respond_to?(:last_name?)
+ end
end
diff --git a/activerecord/test/cases/batches_test.rb b/activerecord/test/cases/batches_test.rb
index c12fa03015..da65336305 100644
--- a/activerecord/test/cases/batches_test.rb
+++ b/activerecord/test/cases/batches_test.rb
@@ -37,9 +37,9 @@ class EachTest < ActiveRecord::TestCase
if Enumerator.method_defined? :size
def test_each_should_return_a_sized_enumerator
- assert_equal 11, Post.find_each(:batch_size => 1).size
- assert_equal 5, Post.find_each(:batch_size => 2, :start => 7).size
- assert_equal 11, Post.find_each(:batch_size => 10_000).size
+ assert_equal 11, Post.find_each(batch_size: 1).size
+ assert_equal 5, Post.find_each(batch_size: 2, begin_at: 7).size
+ assert_equal 11, Post.find_each(batch_size: 10_000).size
end
end
@@ -53,7 +53,7 @@ class EachTest < ActiveRecord::TestCase
end
def test_each_should_raise_if_select_is_set_without_id
- assert_raise(RuntimeError) do
+ assert_raise(ArgumentError) do
Post.select(:title).find_each(batch_size: 1) { |post|
flunk "should not call this block"
}
@@ -69,13 +69,15 @@ class EachTest < ActiveRecord::TestCase
end
def test_warn_if_limit_scope_is_set
- ActiveRecord::Base.logger.expects(:warn)
- Post.limit(1).find_each { |post| post }
+ assert_called(ActiveRecord::Base.logger, :warn) do
+ Post.limit(1).find_each { |post| post }
+ end
end
def test_warn_if_order_scope_is_set
- ActiveRecord::Base.logger.expects(:warn)
- Post.order("title").find_each { |post| post }
+ assert_called(ActiveRecord::Base.logger, :warn) do
+ Post.order("title").find_each { |post| post }
+ end
end
def test_logger_not_required
@@ -99,7 +101,16 @@ class EachTest < ActiveRecord::TestCase
def test_find_in_batches_should_start_from_the_start_option
assert_queries(@total) do
- Post.find_in_batches(:batch_size => 1, :start => 2) do |batch|
+ Post.find_in_batches(batch_size: 1, begin_at: 2) do |batch|
+ assert_kind_of Array, batch
+ assert_kind_of Post, batch.first
+ end
+ end
+ end
+
+ def test_find_in_batches_should_end_at_the_end_option
+ assert_queries(6) do
+ Post.find_in_batches(batch_size: 1, end_at: 5) do |batch|
assert_kind_of Array, batch
assert_kind_of Post, batch.first
end
@@ -128,14 +139,15 @@ class EachTest < ActiveRecord::TestCase
def test_find_in_batches_should_not_use_records_after_yielding_them_in_case_original_array_is_modified
not_a_post = "not a post"
- not_a_post.stubs(:id).raises(StandardError, "not_a_post had #id called on it")
+ def not_a_post.id; end
+ not_a_post.stub(:id, ->{ raise StandardError.new("not_a_post had #id called on it") }) do
+ assert_nothing_raised do
+ Post.find_in_batches(:batch_size => 1) do |batch|
+ assert_kind_of Array, batch
+ assert_kind_of Post, batch.first
- assert_nothing_raised do
- Post.find_in_batches(:batch_size => 1) do |batch|
- assert_kind_of Array, batch
- assert_kind_of Post, batch.first
-
- batch.map! { not_a_post }
+ batch.map! { not_a_post }
+ end
end
end
end
@@ -149,7 +161,7 @@ class EachTest < ActiveRecord::TestCase
end
# posts.first will be ordered using id only. Title order scope should not apply here
assert_not_equal first_post, posts.first
- assert_equal posts(:welcome), posts.first
+ assert_equal posts(:welcome).id, posts.first.id
end
def test_find_in_batches_should_not_ignore_the_default_scope_if_it_is_other_then_order
@@ -163,7 +175,7 @@ class EachTest < ActiveRecord::TestCase
def test_find_in_batches_should_not_modify_passed_options
assert_nothing_raised do
- Post.find_in_batches({ batch_size: 42, start: 1 }.freeze){}
+ Post.find_in_batches({ batch_size: 42, begin_at: 1 }.freeze){}
end
end
@@ -172,7 +184,7 @@ class EachTest < ActiveRecord::TestCase
start_nick = nick_order_subscribers.second.nick
subscribers = []
- Subscriber.find_in_batches(:batch_size => 1, :start => start_nick) do |batch|
+ Subscriber.find_in_batches(batch_size: 1, begin_at: start_nick) do |batch|
subscribers.concat(batch)
end
@@ -181,15 +193,16 @@ class EachTest < ActiveRecord::TestCase
def test_find_in_batches_should_use_any_column_as_primary_key_when_start_is_not_specified
assert_queries(Subscriber.count + 1) do
- Subscriber.find_each(:batch_size => 1) do |subscriber|
- assert_kind_of Subscriber, subscriber
+ Subscriber.find_in_batches(batch_size: 1) do |batch|
+ assert_kind_of Array, batch
+ assert_kind_of Subscriber, batch.first
end
end
end
def test_find_in_batches_should_return_an_enumerator
enum = nil
- assert_queries(0) do
+ assert_no_queries do
enum = Post.find_in_batches(:batch_size => 1)
end
assert_queries(4) do
@@ -200,11 +213,260 @@ class EachTest < ActiveRecord::TestCase
end
end
+ def test_in_batches_should_not_execute_any_query
+ assert_no_queries do
+ assert_kind_of ActiveRecord::Batches::BatchEnumerator, Post.in_batches(of: 2)
+ end
+ end
+
+ def test_in_batches_should_yield_relation_if_block_given
+ assert_queries(6) do
+ Post.in_batches(of: 2) do |relation|
+ assert_kind_of ActiveRecord::Relation, relation
+ end
+ end
+ end
+
+ def test_in_batches_should_be_enumerable_if_no_block_given
+ assert_queries(6) do
+ Post.in_batches(of: 2).each do |relation|
+ assert_kind_of ActiveRecord::Relation, relation
+ end
+ end
+ end
+
+ def test_in_batches_each_record_should_yield_record_if_block_is_given
+ assert_queries(6) do
+ Post.in_batches(of: 2).each_record do |post|
+ assert post.title.present?
+ assert_kind_of Post, post
+ end
+ end
+ end
+
+ def test_in_batches_each_record_should_return_enumerator_if_no_block_given
+ assert_queries(6) do
+ Post.in_batches(of: 2).each_record.with_index do |post, i|
+ assert post.title.present?
+ assert_kind_of Post, post
+ end
+ end
+ end
+
+ def test_in_batches_each_record_should_be_ordered_by_id
+ ids = Post.order('id ASC').pluck(:id)
+ assert_queries(6) do
+ Post.in_batches(of: 2).each_record.with_index do |post, i|
+ assert_equal ids[i], post.id
+ end
+ end
+ end
+
+ def test_in_batches_update_all_affect_all_records
+ assert_queries(6 + 6) do # 6 selects, 6 updates
+ Post.in_batches(of: 2).update_all(title: "updated-title")
+ end
+ assert_equal Post.all.pluck(:title), ["updated-title"] * Post.count
+ end
+
+ def test_in_batches_delete_all_should_not_delete_records_in_other_batches
+ not_deleted_count = Post.where('id <= 2').count
+ Post.where('id > 2').in_batches(of: 2).delete_all
+ assert_equal 0, Post.where('id > 2').count
+ assert_equal not_deleted_count, Post.count
+ end
+
+ def test_in_batches_should_not_be_loaded
+ Post.in_batches(of: 1) do |relation|
+ assert_not relation.loaded?
+ end
+
+ Post.in_batches(of: 1, load: false) do |relation|
+ assert_not relation.loaded?
+ end
+ end
+
+ def test_in_batches_should_be_loaded
+ Post.in_batches(of: 1, load: true) do |relation|
+ assert relation.loaded?
+ end
+ end
+
+ def test_in_batches_if_not_loaded_executes_more_queries
+ assert_queries(@total + 1) do
+ Post.in_batches(of: 1, load: false) do |relation|
+ assert_not relation.loaded?
+ end
+ end
+ end
+
+ def test_in_batches_should_return_relations
+ assert_queries(@total + 1) do
+ Post.in_batches(of: 1) do |relation|
+ assert_kind_of ActiveRecord::Relation, relation
+ end
+ end
+ end
+
+ def test_in_batches_should_start_from_the_start_option
+ post = Post.order('id ASC').where('id >= ?', 2).first
+ assert_queries(2) do
+ relation = Post.in_batches(of: 1, begin_at: 2).first
+ assert_equal post, relation.first
+ end
+ end
+
+ def test_in_batches_should_end_at_the_end_option
+ post = Post.order('id DESC').where('id <= ?', 5).first
+ assert_queries(7) do
+ relation = Post.in_batches(of: 1, end_at: 5, load: true).reverse_each.first
+ assert_equal post, relation.last
+ end
+ end
+
+ def test_in_batches_shouldnt_execute_query_unless_needed
+ assert_queries(2) do
+ Post.in_batches(of: @total) { |relation| assert_kind_of ActiveRecord::Relation, relation }
+ end
+
+ assert_queries(1) do
+ Post.in_batches(of: @total + 1) { |relation| assert_kind_of ActiveRecord::Relation, relation }
+ end
+ end
+
+ def test_in_batches_should_quote_batch_order
+ c = Post.connection
+ assert_sql(/ORDER BY #{c.quote_table_name('posts')}.#{c.quote_column_name('id')}/) do
+ Post.in_batches(of: 1) do |relation|
+ assert_kind_of ActiveRecord::Relation, relation
+ assert_kind_of Post, relation.first
+ end
+ end
+ end
+
+ def test_in_batches_should_not_use_records_after_yielding_them_in_case_original_array_is_modified
+ not_a_post = "not a post"
+ def not_a_post.id
+ raise StandardError.new("not_a_post had #id called on it")
+ end
+
+ assert_nothing_raised do
+ Post.in_batches(of: 1) do |relation|
+ assert_kind_of ActiveRecord::Relation, relation
+ assert_kind_of Post, relation.first
+
+ relation = [not_a_post] * relation.count
+ end
+ end
+ end
+
+ def test_in_batches_should_not_ignore_default_scope_without_order_statements
+ special_posts_ids = SpecialPostWithDefaultScope.all.map(&:id).sort
+ posts = []
+ SpecialPostWithDefaultScope.in_batches do |relation|
+ posts.concat(relation)
+ end
+ assert_equal special_posts_ids, posts.map(&:id)
+ end
+
+ def test_in_batches_should_not_modify_passed_options
+ assert_nothing_raised do
+ Post.in_batches({ of: 42, begin_at: 1 }.freeze){}
+ end
+ end
+
+ def test_in_batches_should_use_any_column_as_primary_key
+ nick_order_subscribers = Subscriber.order('nick asc')
+ start_nick = nick_order_subscribers.second.nick
+
+ subscribers = []
+ Subscriber.in_batches(of: 1, begin_at: start_nick) do |relation|
+ subscribers.concat(relation)
+ end
+
+ assert_equal nick_order_subscribers[1..-1].map(&:id), subscribers.map(&:id)
+ end
+
+ def test_in_batches_should_use_any_column_as_primary_key_when_start_is_not_specified
+ assert_queries(Subscriber.count + 1) do
+ Subscriber.in_batches(of: 1, load: true) do |relation|
+ assert_kind_of ActiveRecord::Relation, relation
+ assert_kind_of Subscriber, relation.first
+ end
+ end
+ end
+
+ def test_in_batches_should_return_an_enumerator
+ enum = nil
+ assert_no_queries do
+ enum = Post.in_batches(of: 1)
+ end
+ assert_queries(4) do
+ enum.first(4) do |relation|
+ assert_kind_of ActiveRecord::Relation, relation
+ assert_kind_of Post, relation.first
+ end
+ end
+ end
+
+ def test_in_batches_relations_should_not_overlap_with_each_other
+ seen_posts = []
+ Post.in_batches(of: 2, load: true) do |relation|
+ relation.to_a.each do |post|
+ assert_not seen_posts.include?(post)
+ seen_posts << post
+ end
+ end
+ end
+
+ def test_in_batches_relations_with_condition_should_not_overlap_with_each_other
+ seen_posts = []
+ author_id = Post.first.author_id
+ posts_by_author = Post.where(author_id: author_id)
+ Post.in_batches(of: 2) do |batch|
+ seen_posts += batch.where(author_id: author_id)
+ end
+
+ assert_equal posts_by_author.pluck(:id).sort, seen_posts.map(&:id).sort
+ end
+
+ def test_in_batches_relations_update_all_should_not_affect_matching_records_in_other_batches
+ Post.update_all(author_id: 0)
+ person = Post.last
+ person.update_attributes(author_id: 1)
+
+ Post.in_batches(of: 2) do |batch|
+ batch.where('author_id >= 1').update_all('author_id = author_id + 1')
+ end
+ assert_equal 2, person.reload.author_id # incremented only once
+ end
+
+ def test_find_in_batches_start_deprecated
+ assert_deprecated do
+ assert_queries(@total) do
+ Post.find_in_batches(batch_size: 1, start: 2) do |batch|
+ assert_kind_of Array, batch
+ assert_kind_of Post, batch.first
+ end
+ end
+ end
+ end
+
+ def test_find_each_start_deprecated
+ assert_deprecated do
+ assert_queries(@total) do
+ Post.find_each(batch_size: 1, start: 2) do |post|
+ assert_kind_of Post, post
+ end
+ end
+ end
+ end
+
if Enumerator.method_defined? :size
def test_find_in_batches_should_return_a_sized_enumerator
assert_equal 11, Post.find_in_batches(:batch_size => 1).size
assert_equal 6, Post.find_in_batches(:batch_size => 2).size
- assert_equal 4, Post.find_in_batches(:batch_size => 2, :start => 4).size
+ assert_equal 4, Post.find_in_batches(batch_size: 2, begin_at: 4).size
assert_equal 4, Post.find_in_batches(:batch_size => 3).size
assert_equal 1, Post.find_in_batches(:batch_size => 10_000).size
end
diff --git a/activerecord/test/cases/binary_test.rb b/activerecord/test/cases/binary_test.rb
index ccf2be369d..86dee929bf 100644
--- a/activerecord/test/cases/binary_test.rb
+++ b/activerecord/test/cases/binary_test.rb
@@ -1,4 +1,3 @@
-# encoding: utf-8
require "cases/helper"
# Without using prepared statements, it makes no sense to test
diff --git a/activerecord/test/cases/bind_parameter_test.rb b/activerecord/test/cases/bind_parameter_test.rb
index 0bc7ee6d64..1e38b97c4a 100644
--- a/activerecord/test/cases/bind_parameter_test.rb
+++ b/activerecord/test/cases/bind_parameter_test.rb
@@ -1,9 +1,11 @@
require 'cases/helper'
require 'models/topic'
+require 'models/author'
+require 'models/post'
module ActiveRecord
class BindParameterTest < ActiveRecord::TestCase
- fixtures :topics
+ fixtures :topics, :authors, :posts
class LogListener
attr_accessor :calls
@@ -20,8 +22,8 @@ module ActiveRecord
def setup
super
@connection = ActiveRecord::Base.connection
- @subscriber = LogListener.new
- @pk = Topic.columns_hash[Topic.primary_key]
+ @subscriber = LogListener.new
+ @pk = Topic.columns_hash[Topic.primary_key]
@subscription = ActiveSupport::Notifications.subscribe('sql.active_record', @subscriber)
end
@@ -30,40 +32,34 @@ module ActiveRecord
end
if ActiveRecord::Base.connection.supports_statement_cache?
- def test_binds_are_logged
- sub = @connection.substitute_at(@pk, 0)
- binds = [[@pk, 1]]
- sql = "select * from topics where id = #{sub}"
-
- @connection.exec_query(sql, 'SQL', binds)
-
- message = @subscriber.calls.find { |args| args[4][:sql] == sql }
- assert_equal binds, message[4][:binds]
+ def test_bind_from_join_in_subquery
+ subquery = Author.joins(:thinking_posts).where(name: 'David')
+ scope = Author.from(subquery, 'authors').where(id: 1)
+ assert_equal 1, scope.count
end
- def test_binds_are_logged_after_type_cast
- sub = @connection.substitute_at(@pk, 0)
- binds = [[@pk, "3"]]
- sql = "select * from topics where id = #{sub}"
+ def test_binds_are_logged
+ sub = @connection.substitute_at(@pk)
+ binds = [Relation::QueryAttribute.new("id", 1, Type::Value.new)]
+ sql = "select * from topics where id = #{sub.to_sql}"
@connection.exec_query(sql, 'SQL', binds)
message = @subscriber.calls.find { |args| args[4][:sql] == sql }
- assert_equal [[@pk, 3]], message[4][:binds]
+ assert_equal binds, message[4][:binds]
end
def test_find_one_uses_binds
Topic.find(1)
- binds = [[@pk, 1]]
- message = @subscriber.calls.find { |args| args[4][:binds] == binds }
+ message = @subscriber.calls.find { |args| args[4][:binds].any? { |attr| attr.value == 1 } }
assert message, 'expected a message with binds'
end
- def test_logs_bind_vars
+ def test_logs_bind_vars_after_type_cast
payload = {
:name => 'SQL',
:sql => 'select * from topics where id = ?',
- :binds => [[@pk, 10]]
+ :binds => [Relation::QueryAttribute.new("id", "10", Type::Integer.new)]
}
event = ActiveSupport::Notifications::Event.new(
'foo',
diff --git a/activerecord/test/cases/cache_key_test.rb b/activerecord/test/cases/cache_key_test.rb
new file mode 100644
index 0000000000..bb2829b3c1
--- /dev/null
+++ b/activerecord/test/cases/cache_key_test.rb
@@ -0,0 +1,25 @@
+require "cases/helper"
+
+module ActiveRecord
+ class CacheKeyTest < ActiveRecord::TestCase
+ self.use_transactional_tests = false
+
+ class CacheMe < ActiveRecord::Base; end
+
+ setup do
+ @connection = ActiveRecord::Base.connection
+ @connection.create_table(:cache_mes) { |t| t.timestamps }
+ end
+
+ teardown do
+ @connection.drop_table :cache_mes, if_exists: true
+ end
+
+ test "test_cache_key_format_is_not_too_precise" do
+ record = CacheMe.create
+ key = record.cache_key
+
+ assert_equal key, record.reload.cache_key
+ end
+ end
+end
diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb
index 319ea9260a..4a0e6f497f 100644
--- a/activerecord/test/cases/calculations_test.rb
+++ b/activerecord/test/cases/calculations_test.rb
@@ -10,29 +10,41 @@ require 'models/reply'
require 'models/minivan'
require 'models/speedometer'
require 'models/ship_part'
-
-Company.has_many :accounts
+require 'models/treasure'
+require 'models/developer'
+require 'models/comment'
+require 'models/rating'
+require 'models/post'
class NumericData < ActiveRecord::Base
self.table_name = 'numeric_data'
- attribute :world_population, Type::Integer.new
- attribute :my_house_population, Type::Integer.new
- attribute :atoms_in_universe, Type::Integer.new
+ attribute :world_population, :integer
+ attribute :my_house_population, :integer
+ attribute :atoms_in_universe, :integer
end
class CalculationsTest < ActiveRecord::TestCase
- fixtures :companies, :accounts, :topics
+ fixtures :companies, :accounts, :topics, :speedometers, :minivans
def test_should_sum_field
assert_equal 318, Account.sum(:credit_limit)
end
+ def test_should_sum_arel_attribute
+ assert_equal 318, Account.sum(Account.arel_table[:credit_limit])
+ end
+
def test_should_average_field
value = Account.average(:credit_limit)
assert_equal 53.0, value
end
+ def test_should_average_arel_attribute
+ value = Account.average(Account.arel_table[:credit_limit])
+ assert_equal 53.0, value
+ end
+
def test_should_resolve_aliased_attributes
assert_equal 318, Account.sum(:available_credit)
end
@@ -57,14 +69,26 @@ class CalculationsTest < ActiveRecord::TestCase
assert_equal 60, Account.maximum(:credit_limit)
end
+ def test_should_get_maximum_of_arel_attribute
+ assert_equal 60, Account.maximum(Account.arel_table[:credit_limit])
+ end
+
def test_should_get_maximum_of_field_with_include
assert_equal 55, Account.where("companies.name != 'Summit'").references(:companies).includes(:firm).maximum(:credit_limit)
end
+ def test_should_get_maximum_of_arel_attribute_with_include
+ assert_equal 55, Account.where("companies.name != 'Summit'").references(:companies).includes(:firm).maximum(Account.arel_table[:credit_limit])
+ end
+
def test_should_get_minimum_of_field
assert_equal 50, Account.minimum(:credit_limit)
end
+ def test_should_get_minimum_of_arel_attribute
+ assert_equal 50, Account.minimum(Account.arel_table[:credit_limit])
+ end
+
def test_should_group_by_field
c = Account.group(:firm_id).sum(:credit_limit)
[1,6,2].each do |firm_id|
@@ -99,6 +123,25 @@ class CalculationsTest < ActiveRecord::TestCase
assert_equal 60, c[2]
end
+ def test_should_generate_valid_sql_with_joins_and_group
+ assert_nothing_raised ActiveRecord::StatementInvalid do
+ AuditLog.joins(:developer).group(:id).count
+ end
+ end
+
+ def test_should_calculate_against_given_relation
+ developer = Developer.create!(name: "developer")
+ developer.audit_logs.create!(message: "first log")
+ developer.audit_logs.create!(message: "second log")
+
+ c = developer.audit_logs.joins(:developer).group(:id).count
+
+ assert_equal developer.audit_logs.count, c.size
+ developer.audit_logs.each do |log|
+ assert_equal 1, c[log.id]
+ end
+ end
+
def test_should_order_by_grouped_field
c = Account.group(:firm_id).order("firm_id").sum(:credit_limit)
assert_equal [1, 2, 6, 9], c.keys.compact
@@ -128,6 +171,14 @@ class CalculationsTest < ActiveRecord::TestCase
assert_equal 3, accounts.select(:firm_id).count
end
+ def test_limit_should_apply_before_count_arel_attribute
+ accounts = Account.limit(3).where('firm_id IS NOT NULL')
+
+ firm_id_attribute = Account.arel_table[:firm_id]
+ assert_equal 3, accounts.count(firm_id_attribute)
+ assert_equal 3, accounts.select(firm_id_attribute).count
+ end
+
def test_count_should_shortcut_with_limit_zero
accounts = Account.limit(0)
@@ -350,13 +401,29 @@ class CalculationsTest < ActiveRecord::TestCase
assert_equal 6, Account.select("DISTINCT accounts.id").includes(:firm).count
end
+ def test_count_selected_arel_attribute
+ assert_equal 5, Account.select(Account.arel_table[:firm_id]).count
+ assert_equal 4, Account.distinct.select(Account.arel_table[:firm_id]).count
+ end
+
def test_count_with_column_parameter
assert_equal 5, Account.count(:firm_id)
end
+ def test_count_with_arel_attribute
+ assert_equal 5, Account.count(Account.arel_table[:firm_id])
+ end
+
+ def test_count_with_arel_star
+ assert_equal 6, Account.count(Arel.star)
+ end
+
def test_count_with_distinct
assert_equal 4, Account.select(:credit_limit).distinct.count
- assert_equal 4, Account.select(:credit_limit).uniq.count
+
+ assert_deprecated do
+ assert_equal 4, Account.select(:credit_limit).uniq.count
+ end
end
def test_count_with_aliased_attribute
@@ -372,12 +439,27 @@ class CalculationsTest < ActiveRecord::TestCase
assert_equal 4, Account.joins(:firm).distinct.count('companies.id')
end
+ def test_count_arel_attribute_in_joined_table_with
+ assert_equal 5, Account.joins(:firm).count(Company.arel_table[:id])
+ assert_equal 4, Account.joins(:firm).distinct.count(Company.arel_table[:id])
+ end
+
+ def test_count_selected_arel_attribute_in_joined_table
+ assert_equal 5, Account.joins(:firm).select(Company.arel_table[:id]).count
+ assert_equal 4, Account.joins(:firm).distinct.select(Company.arel_table[:id]).count
+ end
+
def test_should_count_field_in_joined_table_with_group_by
c = Account.group('accounts.firm_id').joins(:firm).count('companies.id')
[1,6,2,9].each { |firm_id| assert c.keys.include?(firm_id) }
end
+ def test_should_count_field_of_root_table_with_conflicting_group_by_column
+ assert_equal({ 1 => 1 }, Firm.joins(:accounts).group(:firm_id).count)
+ assert_equal({ 1 => 1 }, Firm.joins(:accounts).group('accounts.firm_id').count)
+ end
+
def test_count_with_no_parameters_isnt_deprecated
assert_not_deprecated { Account.count }
end
@@ -463,7 +545,6 @@ class CalculationsTest < ActiveRecord::TestCase
assert_equal 7, Company.includes(:contracts).sum(:developer_id)
end
-
def test_from_option_with_specified_index
if Edge.connection.adapter_name == 'MySQL' or Edge.connection.adapter_name == 'Mysql2'
assert_equal Edge.count(:all), Edge.from('edges USE INDEX(unique_edge_index)').count(:all)
@@ -502,8 +583,8 @@ class CalculationsTest < ActiveRecord::TestCase
assert_equal [ topic.written_on ], relation.pluck(:written_on)
end
- def test_pluck_and_uniq
- assert_equal [50, 53, 55, 60], Account.order(:credit_limit).uniq.pluck(:credit_limit)
+ def test_pluck_and_distinct
+ assert_equal [50, 53, 55, 60], Account.order(:credit_limit).distinct.pluck(:credit_limit)
end
def test_pluck_in_relation
@@ -612,4 +693,100 @@ class CalculationsTest < ActiveRecord::TestCase
.pluck('topics.title', 'replies_topics.title')
assert_equal expected, actual
end
+
+ def test_calculation_with_polymorphic_relation
+ part = ShipPart.create!(name: "has trinket")
+ part.trinkets.create!
+
+ assert_equal part.id, ShipPart.joins(:trinkets).sum(:id)
+ end
+
+ def test_pluck_joined_with_polymorphic_relation
+ part = ShipPart.create!(name: "has trinket")
+ part.trinkets.create!
+
+ assert_equal [part.id], ShipPart.joins(:trinkets).pluck(:id)
+ end
+
+ def test_pluck_loaded_relation
+ companies = Company.order(:id).limit(3).load
+ assert_no_queries do
+ assert_equal ['37signals', 'Summit', 'Microsoft'], companies.pluck(:name)
+ end
+ end
+
+ def test_pluck_loaded_relation_multiple_columns
+ companies = Company.order(:id).limit(3).load
+ assert_no_queries do
+ assert_equal [[1, '37signals'], [2, 'Summit'], [3, 'Microsoft']], companies.pluck(:id, :name)
+ end
+ end
+
+ def test_pluck_loaded_relation_sql_fragment
+ companies = Company.order(:name).limit(3).load
+ assert_queries 1 do
+ assert_equal ['37signals', 'Apex', 'Ex Nihilo'], companies.pluck('DISTINCT name')
+ end
+ end
+
+ def test_grouped_calculation_with_polymorphic_relation
+ part = ShipPart.create!(name: "has trinket")
+ part.trinkets.create!
+
+ assert_equal({ "has trinket" => part.id }, ShipPart.joins(:trinkets).group("ship_parts.name").sum(:id))
+ end
+
+ def test_calculation_grouped_by_association_doesnt_error_when_no_records_have_association
+ Client.update_all(client_of: nil)
+ assert_equal({ nil => Client.count }, Client.group(:firm).count)
+ end
+
+ def test_should_reference_correct_aliases_while_joining_tables_of_has_many_through_association
+ assert_nothing_raised ActiveRecord::StatementInvalid do
+ developer = Developer.create!(name: 'developer')
+ developer.ratings.includes(comment: :post).where(posts: { id: 1 }).count
+ end
+ end
+
+ def test_sum_uses_enumerable_version_when_block_is_given
+ block_called = false
+ relation = Client.all.load
+
+ assert_no_queries do
+ assert_equal 0, relation.sum { block_called = true; 0 }
+ end
+ assert block_called
+ end
+
+ def test_having_with_strong_parameters
+ protected_params = Class.new do
+ attr_reader :permitted
+ alias :permitted? :permitted
+
+ def initialize(parameters)
+ @parameters = parameters
+ @permitted = false
+ end
+
+ def to_h
+ @parameters
+ end
+
+ def permit!
+ @permitted = true
+ self
+ end
+ end
+
+ params = protected_params.new(credit_limit: '50')
+
+ assert_raises(ActiveModel::ForbiddenAttributesError) do
+ Account.group(:id).having(params)
+ end
+
+ result = Account.group(:id).having(params.permit!)
+ assert_equal 50, result[0].credit_limit
+ assert_equal 50, result[1].credit_limit
+ assert_equal 50, result[2].credit_limit
+ end
end
diff --git a/activerecord/test/cases/callbacks_test.rb b/activerecord/test/cases/callbacks_test.rb
index c8f56e3c73..73ac30e547 100644
--- a/activerecord/test/cases/callbacks_test.rb
+++ b/activerecord/test/cases/callbacks_test.rb
@@ -1,4 +1,6 @@
require "cases/helper"
+require 'models/developer'
+require 'models/computer'
class CallbackDeveloper < ActiveRecord::Base
self.table_name = 'developers'
@@ -47,6 +49,11 @@ class CallbackDeveloperWithFalseValidation < CallbackDeveloper
before_validation proc { |model| model.history << [:before_validation, :should_never_get_here] }
end
+class CallbackDeveloperWithHaltedValidation < CallbackDeveloper
+ before_validation proc { |model| model.history << [:before_validation, :throwing_abort]; throw(:abort) }
+ before_validation proc { |model| model.history << [:before_validation, :should_never_get_here] }
+end
+
class ParentDeveloper < ActiveRecord::Base
self.table_name = 'developers'
attr_accessor :after_save_called
@@ -57,27 +64,6 @@ class ChildDeveloper < ParentDeveloper
end
-class RecursiveCallbackDeveloper < ActiveRecord::Base
- self.table_name = 'developers'
-
- before_save :on_before_save
- after_save :on_after_save
-
- attr_reader :on_before_save_called, :on_after_save_called
-
- def on_before_save
- @on_before_save_called ||= 0
- @on_before_save_called += 1
- save unless @on_before_save_called > 1
- end
-
- def on_after_save
- @on_after_save_called ||= 0
- @on_after_save_called += 1
- save unless @on_after_save_called > 1
- end
-end
-
class ImmutableDeveloper < ActiveRecord::Base
self.table_name = 'developers'
@@ -86,35 +72,24 @@ class ImmutableDeveloper < ActiveRecord::Base
before_save :cancel
before_destroy :cancel
- def cancelled?
- @cancelled == true
- end
-
private
def cancel
- @cancelled = true
false
end
end
-class ImmutableMethodDeveloper < ActiveRecord::Base
+class DeveloperWithCanceledCallbacks < ActiveRecord::Base
self.table_name = 'developers'
- validates_inclusion_of :salary, :in => 50000..200000
-
- def cancelled?
- @cancelled == true
- end
+ validates_inclusion_of :salary, in: 50000..200000
- before_save do
- @cancelled = true
- false
- end
+ before_save :cancel
+ before_destroy :cancel
- before_destroy do
- @cancelled = true
- false
- end
+ private
+ def cancel
+ throw(:abort)
+ end
end
class OnCallbacksDeveloper < ActiveRecord::Base
@@ -180,6 +155,23 @@ class CallbackCancellationDeveloper < ActiveRecord::Base
after_destroy { @after_destroy_called = true }
end
+class CallbackHaltedDeveloper < ActiveRecord::Base
+ self.table_name = 'developers'
+
+ attr_reader :after_save_called, :after_create_called, :after_update_called, :after_destroy_called
+ attr_accessor :cancel_before_save, :cancel_before_create, :cancel_before_update, :cancel_before_destroy
+
+ before_save { throw(:abort) if defined?(@cancel_before_save) }
+ before_create { throw(:abort) if @cancel_before_create }
+ before_update { throw(:abort) if @cancel_before_update }
+ before_destroy { throw(:abort) if @cancel_before_destroy }
+
+ after_save { @after_save_called = true }
+ after_update { @after_update_called = true }
+ after_create { @after_create_called = true }
+ after_destroy { @after_destroy_called = true }
+end
+
class CallbacksTest < ActiveRecord::TestCase
fixtures :developers
@@ -296,7 +288,12 @@ class CallbacksTest < ActiveRecord::TestCase
[ :after_save, :string ],
[ :after_save, :proc ],
[ :after_save, :object ],
- [ :after_save, :block ]
+ [ :after_save, :block ],
+ [ :after_commit, :block ],
+ [ :after_commit, :object ],
+ [ :after_commit, :proc ],
+ [ :after_commit, :string ],
+ [ :after_commit, :method ]
], david.history
end
@@ -365,7 +362,12 @@ class CallbacksTest < ActiveRecord::TestCase
[ :after_save, :string ],
[ :after_save, :proc ],
[ :after_save, :object ],
- [ :after_save, :block ]
+ [ :after_save, :block ],
+ [ :after_commit, :block ],
+ [ :after_commit, :object ],
+ [ :after_commit, :proc ],
+ [ :after_commit, :string ],
+ [ :after_commit, :method ]
], david.history
end
@@ -416,7 +418,12 @@ class CallbacksTest < ActiveRecord::TestCase
[ :after_destroy, :string ],
[ :after_destroy, :proc ],
[ :after_destroy, :object ],
- [ :after_destroy, :block ]
+ [ :after_destroy, :block ],
+ [ :after_commit, :block ],
+ [ :after_commit, :object ],
+ [ :after_commit, :proc ],
+ [ :after_commit, :string ],
+ [ :after_commit, :method ]
], david.history
end
@@ -437,11 +444,15 @@ class CallbacksTest < ActiveRecord::TestCase
], david.history
end
- def test_before_save_returning_false
+ def test_deprecated_before_save_returning_false
david = ImmutableDeveloper.find(1)
- assert david.valid?
- assert !david.save
- assert_raise(ActiveRecord::RecordNotSaved) { david.save! }
+ assert_deprecated do
+ assert david.valid?
+ assert !david.save
+ exc = assert_raise(ActiveRecord::RecordNotSaved) { david.save! }
+ assert_equal exc.record, david
+ assert_equal "Failed to save the record", exc.message
+ end
david = ImmutableDeveloper.find(1)
david.salary = 10_000_000
@@ -451,37 +462,49 @@ class CallbacksTest < ActiveRecord::TestCase
someone = CallbackCancellationDeveloper.find(1)
someone.cancel_before_save = true
- assert someone.valid?
- assert !someone.save
+ assert_deprecated do
+ assert someone.valid?
+ assert !someone.save
+ end
assert_save_callbacks_not_called(someone)
end
- def test_before_create_returning_false
+ def test_deprecated_before_create_returning_false
someone = CallbackCancellationDeveloper.new
someone.cancel_before_create = true
- assert someone.valid?
- assert !someone.save
+ assert_deprecated do
+ assert someone.valid?
+ assert !someone.save
+ end
assert_save_callbacks_not_called(someone)
end
- def test_before_update_returning_false
+ def test_deprecated_before_update_returning_false
someone = CallbackCancellationDeveloper.find(1)
someone.cancel_before_update = true
- assert someone.valid?
- assert !someone.save
+ assert_deprecated do
+ assert someone.valid?
+ assert !someone.save
+ end
assert_save_callbacks_not_called(someone)
end
- def test_before_destroy_returning_false
+ def test_deprecated_before_destroy_returning_false
david = ImmutableDeveloper.find(1)
- assert !david.destroy
- assert_raise(ActiveRecord::RecordNotDestroyed) { david.destroy! }
+ assert_deprecated do
+ assert !david.destroy
+ exc = assert_raise(ActiveRecord::RecordNotDestroyed) { david.destroy! }
+ assert_equal exc.record, david
+ assert_equal "Failed to destroy the record", exc.message
+ end
assert_not_nil ImmutableDeveloper.find_by_id(1)
someone = CallbackCancellationDeveloper.find(1)
someone.cancel_before_destroy = true
- assert !someone.destroy
- assert_raise(ActiveRecord::RecordNotDestroyed) { someone.destroy! }
+ assert_deprecated do
+ assert !someone.destroy
+ assert_raise(ActiveRecord::RecordNotDestroyed) { someone.destroy! }
+ end
assert !someone.after_destroy_called
end
@@ -492,9 +515,59 @@ class CallbacksTest < ActiveRecord::TestCase
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_save_callbacks_not_called(someone)
+ end
+
+ def test_before_save_throwing_abort
+ david = DeveloperWithCanceledCallbacks.find(1)
+ assert david.valid?
+ assert !david.save
+ exc = assert_raise(ActiveRecord::RecordNotSaved) { david.save! }
+ assert_equal exc.record, david
+
+ david = DeveloperWithCanceledCallbacks.find(1)
+ david.salary = 10_000_000
+ assert !david.valid?
+ assert !david.save
+ assert_raise(ActiveRecord::RecordInvalid) { david.save! }
+
+ someone = CallbackHaltedDeveloper.find(1)
+ someone.cancel_before_save = true
+ assert someone.valid?
+ assert !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_save_callbacks_not_called(someone)
+ end
+
+ def test_before_destroy_throwing_abort
+ david = DeveloperWithCanceledCallbacks.find(1)
+ assert !david.destroy
+ exc = assert_raise(ActiveRecord::RecordNotDestroyed) { david.destroy! }
+ assert_equal exc.record, david
+ assert_not_nil ImmutableDeveloper.find_by_id(1)
+
+ someone = CallbackHaltedDeveloper.find(1)
+ someone.cancel_before_destroy = true
+ assert !someone.destroy
+ assert_raise(ActiveRecord::RecordNotDestroyed) { someone.destroy! }
+ assert !someone.after_destroy_called
+ end
+
def test_callback_returning_false
david = CallbackDeveloperWithFalseValidation.find(1)
- david.save
+ assert_deprecated { david.save }
assert_equal [
[ :after_find, :method ],
[ :after_find, :string ],
@@ -520,6 +593,34 @@ class CallbacksTest < ActiveRecord::TestCase
], david.history
end
+ def test_callback_throwing_abort
+ david = CallbackDeveloperWithHaltedValidation.find(1)
+ david.save
+ assert_equal [
+ [ :after_find, :method ],
+ [ :after_find, :string ],
+ [ :after_find, :proc ],
+ [ :after_find, :object ],
+ [ :after_find, :block ],
+ [ :after_initialize, :method ],
+ [ :after_initialize, :string ],
+ [ :after_initialize, :proc ],
+ [ :after_initialize, :object ],
+ [ :after_initialize, :block ],
+ [ :before_validation, :method ],
+ [ :before_validation, :string ],
+ [ :before_validation, :proc ],
+ [ :before_validation, :object ],
+ [ :before_validation, :block ],
+ [ :before_validation, :throwing_abort ],
+ [ :after_rollback, :block ],
+ [ :after_rollback, :object ],
+ [ :after_rollback, :proc ],
+ [ :after_rollback, :string ],
+ [ :after_rollback, :method ],
+ ], david.history
+ end
+
def test_inheritance_of_callbacks
parent = ParentDeveloper.new
assert !parent.after_save_called
diff --git a/activerecord/test/cases/collection_cache_key_test.rb b/activerecord/test/cases/collection_cache_key_test.rb
new file mode 100644
index 0000000000..53058c5a4a
--- /dev/null
+++ b/activerecord/test/cases/collection_cache_key_test.rb
@@ -0,0 +1,70 @@
+require "cases/helper"
+require "models/computer"
+require "models/developer"
+require "models/project"
+require "models/topic"
+require "models/post"
+require "models/comment"
+
+module ActiveRecord
+ class CollectionCacheKeyTest < ActiveRecord::TestCase
+ fixtures :developers, :projects, :developers_projects, :topics, :comments, :posts
+
+ test "collection_cache_key on model" do
+ assert_match(/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\Z/, Developer.collection_cache_key)
+ end
+
+ test "cache_key for relation" do
+ developers = Developer.where(name: "David")
+ last_developer_timestamp = developers.order(updated_at: :desc).first.updated_at
+
+ assert_match(/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\Z/, developers.cache_key)
+
+ /\Adevelopers\/query-(\h+)-(\d+)-(\d+)\Z/ =~ developers.cache_key
+
+ assert_equal Digest::MD5.hexdigest(developers.to_sql), $1
+ assert_equal developers.count.to_s, $2
+ assert_equal last_developer_timestamp.to_s(ActiveRecord::Base.cache_timestamp_format), $3
+ end
+
+ test "it triggers at most one query" do
+ developers = Developer.where(name: "David")
+
+ assert_queries(1) { developers.cache_key }
+ assert_queries(0) { developers.cache_key }
+ end
+
+ test "it doesn't trigger any query if the relation is already loaded" do
+ developers = Developer.where(name: "David").load
+ assert_queries(0) { developers.cache_key }
+ end
+
+ test "relation cache_key changes when the sql query changes" do
+ developers = Developer.where(name: "David")
+ other_relation = Developer.where(name: "David").where("1 = 1")
+
+ assert_not_equal developers.cache_key, other_relation.cache_key
+ end
+
+ test "cache_key for empty relation" do
+ developers = Developer.where(name: "Non Existent Developer")
+ assert_match(/\Adevelopers\/query-(\h+)-0\Z/, developers.cache_key)
+ end
+
+ test "cache_key with custom timestamp column" do
+ topics = Topic.where("title like ?", "%Topic%")
+ last_topic_timestamp = topics(:fifth).written_on.utc.to_s(:usec)
+ assert_match(last_topic_timestamp, topics.cache_key(:written_on))
+ end
+
+ test "cache_key with unknown timestamp column" do
+ topics = Topic.where("title like ?", "%Topic%")
+ assert_raises(ActiveRecord::StatementInvalid) { topics.cache_key(:published_at) }
+ end
+
+ test "collection proxy provides a cache_key" do
+ developers = projects(:active_record).developers
+ assert_match(/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\Z/, developers.cache_key)
+ end
+ end
+end
diff --git a/activerecord/test/cases/column_definition_test.rb b/activerecord/test/cases/column_definition_test.rb
index bcfd66b4bf..14b95ecab1 100644
--- a/activerecord/test/cases/column_definition_test.rb
+++ b/activerecord/test/cases/column_definition_test.rb
@@ -14,7 +14,7 @@ module ActiveRecord
# Avoid column definitions in create table statements like:
# `title` varchar(255) DEFAULT NULL
def test_should_not_include_default_clause_when_default_is_null
- column = Column.new("title", nil, Type::String.new(limit: 20))
+ column = Column.new("title", nil, SqlTypeMetadata.new(limit: 20))
column_def = ColumnDefinition.new(
column.name, "string",
column.limit, column.precision, column.scale, column.default, column.null)
@@ -22,7 +22,7 @@ module ActiveRecord
end
def test_should_include_default_clause_when_default_is_present
- column = Column.new("title", "Hello", Type::String.new(limit: 20))
+ column = Column.new("title", "Hello", SqlTypeMetadata.new(limit: 20))
column_def = ColumnDefinition.new(
column.name, "string",
column.limit, column.precision, column.scale, column.default, column.null)
@@ -30,94 +30,53 @@ module ActiveRecord
end
def test_should_specify_not_null_if_null_option_is_false
- column = Column.new("title", "Hello", Type::String.new(limit: 20), "varchar(20)", false)
+ type_metadata = SqlTypeMetadata.new(limit: 20)
+ column = Column.new("title", "Hello", type_metadata, false)
column_def = ColumnDefinition.new(
column.name, "string",
column.limit, column.precision, column.scale, column.default, column.null)
assert_equal %Q{title varchar(20) DEFAULT 'Hello' NOT NULL}, @viz.accept(column_def)
end
- if current_adapter?(:MysqlAdapter)
+ if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
def test_should_set_default_for_mysql_binary_data_types
- binary_column = MysqlAdapter::Column.new("title", "a", Type::Binary.new, "binary(1)")
+ type = SqlTypeMetadata.new(type: :binary, sql_type: "binary(1)")
+ binary_column = AbstractMysqlAdapter::Column.new("title", "a", type)
assert_equal "a", binary_column.default
- varbinary_column = MysqlAdapter::Column.new("title", "a", Type::Binary.new, "varbinary(1)")
+ type = SqlTypeMetadata.new(type: :binary, sql_type: "varbinary")
+ varbinary_column = AbstractMysqlAdapter::Column.new("title", "a", type)
assert_equal "a", varbinary_column.default
end
def test_should_not_set_default_for_blob_and_text_data_types
assert_raise ArgumentError do
- MysqlAdapter::Column.new("title", "a", Type::Binary.new, "blob")
+ AbstractMysqlAdapter::Column.new("title", "a", SqlTypeMetadata.new(sql_type: "blob"))
end
+ text_type = AbstractMysqlAdapter::MysqlTypeMetadata.new(
+ SqlTypeMetadata.new(type: :text))
assert_raise ArgumentError do
- MysqlAdapter::Column.new("title", "Hello", Type::Text.new)
+ AbstractMysqlAdapter::Column.new("title", "Hello", text_type)
end
- text_column = MysqlAdapter::Column.new("title", nil, Type::Text.new)
+ text_column = AbstractMysqlAdapter::Column.new("title", nil, text_type)
assert_equal nil, text_column.default
- not_null_text_column = MysqlAdapter::Column.new("title", nil, Type::Text.new, "text", false)
+ not_null_text_column = AbstractMysqlAdapter::Column.new("title", nil, text_type, false)
assert_equal "", not_null_text_column.default
end
def test_has_default_should_return_false_for_blob_and_text_data_types
- blob_column = MysqlAdapter::Column.new("title", nil, Type::Binary.new, "blob")
+ binary_type = SqlTypeMetadata.new(sql_type: "blob")
+ blob_column = AbstractMysqlAdapter::Column.new("title", nil, binary_type)
assert !blob_column.has_default?
- text_column = MysqlAdapter::Column.new("title", nil, Type::Text.new)
+ text_type = SqlTypeMetadata.new(type: :text)
+ text_column = AbstractMysqlAdapter::Column.new("title", nil, text_type)
assert !text_column.has_default?
end
end
-
- if current_adapter?(:Mysql2Adapter)
- def test_should_set_default_for_mysql_binary_data_types
- binary_column = Mysql2Adapter::Column.new("title", "a", Type::Binary.new, "binary(1)")
- assert_equal "a", binary_column.default
-
- varbinary_column = Mysql2Adapter::Column.new("title", "a", Type::Binary.new, "varbinary(1)")
- assert_equal "a", varbinary_column.default
- end
-
- def test_should_not_set_default_for_blob_and_text_data_types
- assert_raise ArgumentError do
- Mysql2Adapter::Column.new("title", "a", Type::Binary.new, "blob")
- end
-
- assert_raise ArgumentError do
- Mysql2Adapter::Column.new("title", "Hello", Type::Text.new)
- end
-
- text_column = Mysql2Adapter::Column.new("title", nil, Type::Text.new)
- assert_equal nil, text_column.default
-
- not_null_text_column = Mysql2Adapter::Column.new("title", nil, Type::Text.new, "text", false)
- assert_equal "", not_null_text_column.default
- end
-
- def test_has_default_should_return_false_for_blob_and_text_data_types
- blob_column = Mysql2Adapter::Column.new("title", nil, Type::Binary.new, "blob")
- assert !blob_column.has_default?
-
- text_column = Mysql2Adapter::Column.new("title", nil, Type::Text.new)
- assert !text_column.has_default?
- end
- end
-
- if current_adapter?(:PostgreSQLAdapter)
- def test_bigint_column_should_map_to_integer
- oid = PostgreSQLAdapter::OID::Integer.new
- bigint_column = PostgreSQLColumn.new('number', nil, oid, "bigint")
- assert_equal :integer, bigint_column.type
- end
-
- def test_smallint_column_should_map_to_integer
- oid = PostgreSQLAdapter::OID::Integer.new
- smallint_column = PostgreSQLColumn.new('number', nil, oid, "smallint")
- assert_equal :integer, smallint_column.type
- end
- end
end
end
end
diff --git a/activerecord/test/cases/connection_adapters/adapter_leasing_test.rb b/activerecord/test/cases/connection_adapters/adapter_leasing_test.rb
index 662e19f35e..580568c8ac 100644
--- a/activerecord/test/cases/connection_adapters/adapter_leasing_test.rb
+++ b/activerecord/test/cases/connection_adapters/adapter_leasing_test.rb
@@ -6,7 +6,7 @@ module ActiveRecord
class Pool < ConnectionPool
def insert_connection_for_test!(c)
synchronize do
- @connections << c
+ adopt_connection(c)
@available.add c
end
end
@@ -24,7 +24,9 @@ module ActiveRecord
def test_lease_twice
assert @adapter.lease, 'should lease adapter'
- assert_not @adapter.lease, 'should not lease adapter'
+ assert_raises(ActiveRecordError) do
+ @adapter.lease
+ end
end
def test_expire_mutates_in_use
diff --git a/activerecord/test/cases/connection_adapters/connection_handler_test.rb b/activerecord/test/cases/connection_adapters/connection_handler_test.rb
index 3e33b30144..9b1865e8bb 100644
--- a/activerecord/test/cases/connection_adapters/connection_handler_test.rb
+++ b/activerecord/test/cases/connection_adapters/connection_handler_test.rb
@@ -44,8 +44,52 @@ module ActiveRecord
end
def test_connection_pools
- assert_deprecated do
- assert_equal({ Base.connection_pool.spec => @pool }, @handler.connection_pools)
+ assert_equal([@pool], @handler.connection_pools)
+ end
+
+ if Process.respond_to?(:fork)
+ def test_connection_pool_per_pid
+ object_id = ActiveRecord::Base.connection.object_id
+
+ rd, wr = IO.pipe
+ rd.binmode
+ wr.binmode
+
+ pid = fork {
+ rd.close
+ wr.write Marshal.dump ActiveRecord::Base.connection.object_id
+ wr.close
+ exit!
+ }
+
+ wr.close
+
+ Process.waitpid pid
+ assert_not_equal object_id, Marshal.load(rd.read)
+ rd.close
+ end
+
+ def test_retrieve_connection_pool_copies_schema_cache_from_ancestor_pool
+ @pool.schema_cache = @pool.connection.schema_cache
+ @pool.schema_cache.add('posts')
+
+ rd, wr = IO.pipe
+ rd.binmode
+ wr.binmode
+
+ pid = fork {
+ rd.close
+ pool = @handler.retrieve_connection_pool(@klass)
+ wr.write Marshal.dump pool.schema_cache.size
+ wr.close
+ exit!
+ }
+
+ wr.close
+
+ Process.waitpid pid
+ assert_equal @pool.schema_cache.size, Marshal.load(rd.read)
+ rd.close
end
end
end
diff --git a/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb b/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb
index e1b2804a18..9ee92a3cd2 100644
--- a/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb
+++ b/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb
@@ -5,10 +5,14 @@ module ActiveRecord
class MergeAndResolveDefaultUrlConfigTest < ActiveRecord::TestCase
def setup
@previous_database_url = ENV.delete("DATABASE_URL")
+ @previous_rack_env = ENV.delete("RACK_ENV")
+ @previous_rails_env = ENV.delete("RAILS_ENV")
end
teardown do
ENV["DATABASE_URL"] = @previous_database_url
+ ENV["RACK_ENV"] = @previous_rack_env
+ ENV["RAILS_ENV"] = @previous_rails_env
end
def resolve_config(config)
@@ -27,11 +31,23 @@ module ActiveRecord
assert_equal expected, actual
end
- def test_resolver_with_database_uri_and_and_current_env_string_key
+ def test_resolver_with_database_uri_and_current_env_symbol_key_and_rails_env
ENV['DATABASE_URL'] = "postgres://localhost/foo"
- config = { "default_env" => { "adapter" => "not_postgres", "database" => "not_foo" } }
- actual = assert_deprecated { resolve_spec("default_env", config) }
- expected = { "adapter"=>"postgresql", "database"=>"foo", "host"=>"localhost" }
+ ENV['RAILS_ENV'] = "foo"
+
+ config = { "not_production" => { "adapter" => "not_postgres", "database" => "not_foo" } }
+ actual = resolve_spec(:foo, config)
+ expected = { "adapter" => "postgresql", "database" => "foo", "host" => "localhost" }
+ assert_equal expected, actual
+ end
+
+ def test_resolver_with_database_uri_and_current_env_symbol_key_and_rack_env
+ ENV['DATABASE_URL'] = "postgres://localhost/foo"
+ ENV['RACK_ENV'] = "foo"
+
+ config = { "not_production" => { "adapter" => "not_postgres", "database" => "not_foo" } }
+ actual = resolve_spec(:foo, config)
+ expected = { "adapter" => "postgresql", "database" => "foo", "host" => "localhost" }
assert_equal expected, actual
end
@@ -51,16 +67,6 @@ module ActiveRecord
end
end
- def test_resolver_with_database_uri_and_unknown_string_key
- ENV['DATABASE_URL'] = "postgres://localhost/foo"
- config = { "not_production" => { "adapter" => "not_postgres", "database" => "not_foo" } }
- assert_deprecated do
- assert_raises AdapterNotSpecified do
- resolve_spec("production", config)
- end
- end
- end
-
def test_resolver_with_database_uri_and_supplied_url
ENV['DATABASE_URL'] = "not-postgres://not-localhost/not_foo"
config = { "production" => { "adapter" => "also_not_postgres", "database" => "also_not_foo" } }
@@ -139,6 +145,51 @@ module ActiveRecord
assert_equal nil, actual["production"]
assert_equal nil, actual["development"]
assert_equal nil, actual["test"]
+ assert_equal nil, actual[:default_env]
+ assert_equal nil, actual[:production]
+ assert_equal nil, actual[:development]
+ assert_equal nil, actual[:test]
+ end
+
+ def test_blank_with_database_url_with_rails_env
+ ENV['RAILS_ENV'] = "not_production"
+ ENV['DATABASE_URL'] = "postgres://localhost/foo"
+
+ config = {}
+ actual = resolve_config(config)
+ expected = { "adapter" => "postgresql",
+ "database" => "foo",
+ "host" => "localhost" }
+
+ assert_equal expected, actual["not_production"]
+ assert_equal nil, actual["production"]
+ assert_equal nil, actual["default_env"]
+ assert_equal nil, actual["development"]
+ assert_equal nil, actual["test"]
+ assert_equal nil, actual[:default_env]
+ assert_equal nil, actual[:not_production]
+ assert_equal nil, actual[:production]
+ assert_equal nil, actual[:development]
+ assert_equal nil, actual[:test]
+ end
+
+ def test_blank_with_database_url_with_rack_env
+ ENV['RACK_ENV'] = "not_production"
+ ENV['DATABASE_URL'] = "postgres://localhost/foo"
+
+ config = {}
+ actual = resolve_config(config)
+ expected = { "adapter" => "postgresql",
+ "database" => "foo",
+ "host" => "localhost" }
+
+ assert_equal expected, actual["not_production"]
+ assert_equal nil, actual["production"]
+ assert_equal nil, actual["default_env"]
+ assert_equal nil, actual["development"]
+ assert_equal nil, actual["test"]
+ assert_equal nil, actual[:default_env]
+ assert_equal nil, actual[:not_production]
assert_equal nil, actual[:production]
assert_equal nil, actual[:development]
assert_equal nil, actual[:test]
diff --git a/activerecord/test/cases/connection_adapters/mysql_type_lookup_test.rb b/activerecord/test/cases/connection_adapters/mysql_type_lookup_test.rb
index d4d67487db..2749273884 100644
--- a/activerecord/test/cases/connection_adapters/mysql_type_lookup_test.rb
+++ b/activerecord/test/cases/connection_adapters/mysql_type_lookup_test.rb
@@ -22,6 +22,14 @@ module ActiveRecord
assert_lookup_type :string, "SET('one', 'two', 'three')"
end
+ def test_set_type_with_value_matching_other_type
+ assert_lookup_type :string, "SET('unicode', '8bit', 'none', 'time')"
+ end
+
+ def test_enum_type_with_value_matching_other_type
+ assert_lookup_type :string, "ENUM('unicode', '8bit', 'none')"
+ end
+
def test_binary_types
assert_lookup_type :binary, 'bit'
assert_lookup_type :binary, 'BIT'
diff --git a/activerecord/test/cases/connection_adapters/schema_cache_test.rb b/activerecord/test/cases/connection_adapters/schema_cache_test.rb
index c7531f5418..db832fe55d 100644
--- a/activerecord/test/cases/connection_adapters/schema_cache_test.rb
+++ b/activerecord/test/cases/connection_adapters/schema_cache_test.rb
@@ -29,7 +29,7 @@ module ActiveRecord
def test_clearing
@cache.columns('posts')
@cache.columns_hash('posts')
- @cache.tables('posts')
+ @cache.data_sources('posts')
@cache.primary_keys('posts')
@cache.clear!
@@ -40,17 +40,22 @@ module ActiveRecord
def test_dump_and_load
@cache.columns('posts')
@cache.columns_hash('posts')
- @cache.tables('posts')
+ @cache.data_sources('posts')
@cache.primary_keys('posts')
@cache = Marshal.load(Marshal.dump(@cache))
assert_equal 11, @cache.columns('posts').size
assert_equal 11, @cache.columns_hash('posts').size
- assert @cache.tables('posts')
+ assert @cache.data_sources('posts')
assert_equal 'id', @cache.primary_keys('posts')
end
+ def test_table_methods_deprecation
+ assert_deprecated { assert @cache.table_exists?('posts') }
+ assert_deprecated { assert @cache.tables('posts') }
+ assert_deprecated { @cache.clear_table_cache!('posts') }
+ end
end
end
end
diff --git a/activerecord/test/cases/connection_adapters/type_lookup_test.rb b/activerecord/test/cases/connection_adapters/type_lookup_test.rb
index d5c1dc1e5d..7566863653 100644
--- a/activerecord/test/cases/connection_adapters/type_lookup_test.rb
+++ b/activerecord/test/cases/connection_adapters/type_lookup_test.rb
@@ -79,13 +79,22 @@ module ActiveRecord
assert_lookup_type :integer, 'bigint'
end
+ def test_bigint_limit
+ cast_type = @connection.type_map.lookup("bigint")
+ if current_adapter?(:OracleAdapter)
+ assert_equal 19, cast_type.limit
+ else
+ assert_equal 8, cast_type.limit
+ end
+ end
+
def test_decimal_without_scale
types = %w{decimal(2) decimal(2,0) numeric(2) numeric(2,0) number(2) number(2,0)}
types.each do |type|
cast_type = @connection.type_map.lookup(type)
assert_equal :decimal, cast_type.type
- assert_equal 2, cast_type.type_cast_from_user(2.1)
+ assert_equal 2, cast_type.cast(2.1)
end
end
diff --git a/activerecord/test/cases/connection_management_test.rb b/activerecord/test/cases/connection_management_test.rb
index 77d9ae9b8e..dff6ea0fb0 100644
--- a/activerecord/test/cases/connection_management_test.rb
+++ b/activerecord/test/cases/connection_management_test.rb
@@ -26,29 +26,6 @@ module ActiveRecord
assert ActiveRecord::Base.connection_handler.active_connections?
end
- if Process.respond_to?(:fork)
- def test_connection_pool_per_pid
- object_id = ActiveRecord::Base.connection.object_id
-
- rd, wr = IO.pipe
- rd.binmode
- wr.binmode
-
- pid = fork {
- rd.close
- wr.write Marshal.dump ActiveRecord::Base.connection.object_id
- wr.close
- exit!
- }
-
- wr.close
-
- Process.waitpid pid
- assert_not_equal object_id, Marshal.load(rd.read)
- rd.close
- end
- end
-
def test_app_delegation
manager = ConnectionManagement.new(@app)
@@ -96,13 +73,21 @@ module ActiveRecord
assert ActiveRecord::Base.connection_handler.active_connections?
end
+ def test_connections_closed_if_exception_and_explicitly_not_test
+ @env['rack.test'] = false
+ app = Class.new(App) { def call(env); raise NotImplementedError; end }.new
+ explosive = ConnectionManagement.new(app)
+ assert_raises(NotImplementedError) { explosive.call(@env) }
+ assert !ActiveRecord::Base.connection_handler.active_connections?
+ end
+
test "doesn't clear active connections when running in a test case" do
@env['rack.test'] = true
@management.call(@env)
assert ActiveRecord::Base.connection_handler.active_connections?
end
- test "proxy is polite to it's body and responds to it" do
+ test "proxy is polite to its body and responds to it" do
body = Class.new(String) { def to_path; "/path"; end }.new
app = lambda { |_| [200, {}, body] }
response_body = ConnectionManagement.new(app).call(@env)[2]
diff --git a/activerecord/test/cases/connection_pool_test.rb b/activerecord/test/cases/connection_pool_test.rb
index 8d15a76735..7ef5c93a48 100644
--- a/activerecord/test/cases/connection_pool_test.rb
+++ b/activerecord/test/cases/connection_pool_test.rb
@@ -1,5 +1,5 @@
require "cases/helper"
-require 'active_support/concurrency/latch'
+require 'concurrent/atomics'
module ActiveRecord
module ConnectionAdapters
@@ -100,7 +100,7 @@ module ActiveRecord
t = Thread.new { @pool.checkout }
# make sure our thread is in the timeout section
- Thread.pass until t.status == "sleep"
+ Thread.pass until @pool.num_waiting_in_queue == 1
connection = cs.first
connection.close
@@ -112,7 +112,7 @@ module ActiveRecord
t = Thread.new { @pool.checkout }
# make sure our thread is in the timeout section
- Thread.pass until t.status == "sleep"
+ Thread.pass until @pool.num_waiting_in_queue == 1
connection = cs.first
@pool.remove connection
@@ -133,15 +133,15 @@ module ActiveRecord
end
def test_reap_inactive
- ready = ActiveSupport::Concurrency::Latch.new
+ ready = Concurrent::CountDownLatch.new
@pool.checkout
child = Thread.new do
@pool.checkout
@pool.checkout
- ready.release
+ ready.count_down
Thread.stop
end
- ready.await
+ ready.wait
assert_equal 3, active_connections(@pool).size
@@ -204,13 +204,13 @@ module ActiveRecord
end
# The connection pool is "fair" if threads waiting for
- # connections receive them the order in which they began
+ # connections receive them in the order in which they began
# waiting. This ensures that we don't timeout one HTTP request
# even while well under capacity in a multi-threaded environment
# such as a Java servlet container.
#
# We don't need strict fairness: if two connections become
- # available at the same time, it's fine of two threads that were
+ # available at the same time, it's fine if two threads that were
# waiting acquire the connections out of order.
#
# Thus this test prepares waiting threads and then trickles in
@@ -234,7 +234,7 @@ module ActiveRecord
mutex.synchronize { errors << e }
end
}
- Thread.pass until t.status == "sleep"
+ Thread.pass until @pool.num_waiting_in_queue == i
t
end
@@ -271,7 +271,7 @@ module ActiveRecord
mutex.synchronize { errors << e }
end
}
- Thread.pass until t.status == "sleep"
+ Thread.pass until @pool.num_waiting_in_queue == i
t
end
@@ -341,6 +341,185 @@ module ActiveRecord
handler.establish_connection anonymous, nil
}
end
+
+ def test_pool_sets_connection_schema_cache
+ connection = pool.checkout
+ schema_cache = SchemaCache.new connection
+ schema_cache.add(:posts)
+ pool.schema_cache = schema_cache
+
+ pool.with_connection do |conn|
+ assert_not_same pool.schema_cache, conn.schema_cache
+ assert_equal pool.schema_cache.size, conn.schema_cache.size
+ assert_same pool.schema_cache.columns(:posts), conn.schema_cache.columns(:posts)
+ end
+
+ pool.checkin connection
+ end
+
+ def test_concurrent_connection_establishment
+ assert_operator @pool.connections.size, :<=, 1
+
+ all_threads_in_new_connection = Concurrent::CountDownLatch.new(@pool.size - @pool.connections.size)
+ all_go = Concurrent::CountDownLatch.new
+
+ @pool.singleton_class.class_eval do
+ define_method(:new_connection) do
+ all_threads_in_new_connection.count_down
+ all_go.wait
+ super()
+ end
+ end
+
+ connecting_threads = []
+ @pool.size.times do
+ connecting_threads << Thread.new { @pool.checkout }
+ end
+
+ begin
+ Timeout.timeout(5) do
+ # the kernel of the whole test is here, everything else is just scaffolding,
+ # this latch will not be released unless conn. pool allows for concurrent
+ # connection creation
+ all_threads_in_new_connection.wait
+ end
+ rescue Timeout::Error
+ flunk 'pool unable to establish connections concurrently or implementation has ' <<
+ 'changed, this test then needs to patch a different :new_connection method'
+ ensure
+ # clean up the threads
+ all_go.count_down
+ connecting_threads.map(&:join)
+ end
+ end
+
+ def test_non_bang_disconnect_and_clear_reloadable_connections_throw_exception_if_threads_dont_return_their_conns
+ @pool.checkout_timeout = 0.001 # no need to delay test suite by waiting the whole full default timeout
+ [:disconnect, :clear_reloadable_connections].each do |group_action_method|
+ @pool.with_connection do |connection|
+ assert_raises(ExclusiveConnectionTimeoutError) do
+ Thread.new { @pool.send(group_action_method) }.join
+ end
+ end
+ end
+ end
+
+ def test_disconnect_and_clear_reloadable_connections_attempt_to_wait_for_threads_to_return_their_conns
+ [:disconnect, :disconnect!, :clear_reloadable_connections, :clear_reloadable_connections!].each do |group_action_method|
+ begin
+ thread = timed_join_result = nil
+ @pool.with_connection do |connection|
+ thread = Thread.new { @pool.send(group_action_method) }
+
+ # give the other `thread` some time to get stuck in `group_action_method`
+ timed_join_result = thread.join(0.3)
+ # thread.join # => `nil` means the other thread hasn't finished running and is still waiting for us to
+ # release our connection
+ assert_nil timed_join_result
+
+ # assert that since this is within default timeout our connection hasn't been forcefully taken away from us
+ assert @pool.active_connection?
+ end
+ ensure
+ thread.join if thread && !timed_join_result # clean up the other thread
+ end
+ end
+ end
+
+ def test_bang_versions_of_disconnect_and_clear_reloadable_connections_if_unable_to_aquire_all_connections_proceed_anyway
+ @pool.checkout_timeout = 0.001 # no need to delay test suite by waiting the whole full default timeout
+ [:disconnect!, :clear_reloadable_connections!].each do |group_action_method|
+ @pool.with_connection do |connection|
+ Thread.new { @pool.send(group_action_method) }.join
+ # assert connection has been forcefully taken away from us
+ assert_not @pool.active_connection?
+ end
+ end
+ end
+
+ def test_disconnect_and_clear_reloadable_connections_are_able_to_preempt_other_waiting_threads
+ with_single_connection_pool do |pool|
+ [:disconnect, :disconnect!, :clear_reloadable_connections, :clear_reloadable_connections!].each do |group_action_method|
+ conn = pool.connection # drain the only available connection
+ second_thread_done = Concurrent::CountDownLatch.new
+
+ # create a first_thread and let it get into the FIFO queue first
+ first_thread = Thread.new do
+ pool.with_connection { second_thread_done.wait }
+ end
+
+ # wait for first_thread to get in queue
+ Thread.pass until pool.num_waiting_in_queue == 1
+
+ # create a different, later thread, that will attempt to do a "group action",
+ # but because of the group action semantics it should be able to preempt the
+ # first_thread when a connection is made available
+ second_thread = Thread.new do
+ pool.send(group_action_method)
+ second_thread_done.count_down
+ end
+
+ # wait for second_thread to get in queue
+ Thread.pass until pool.num_waiting_in_queue == 2
+
+ # return the only available connection
+ pool.checkin(conn)
+
+ # if the second_thread is not able to preempt the first_thread,
+ # they will temporarily (until either of them timeouts with ConnectionTimeoutError)
+ # deadlock and a join(2) timeout will be reached
+ failed = true unless second_thread.join(2)
+
+ #--- post test clean up start
+ second_thread_done.count_down if failed
+
+ # after `pool.disconnect()` the first thread will be left stuck in queue, no need to wait for
+ # it to timeout with ConnectionTimeoutError
+ if (group_action_method == :disconnect || group_action_method == :disconnect!) && pool.num_waiting_in_queue > 0
+ pool.with_connection {} # create a new connection in case there are threads still stuck in a queue
+ end
+
+ first_thread.join
+ second_thread.join
+ #--- post test clean up end
+
+ flunk "#{group_action_method} is not able to preempt other waiting threads" if failed
+ end
+ end
+ end
+
+ def test_clear_reloadable_connections_creates_new_connections_for_waiting_threads_if_necessary
+ with_single_connection_pool do |pool|
+ conn = pool.connection # drain the only available connection
+ def conn.requires_reloading? # make sure it gets removed from the pool by clear_reloadable_connections
+ true
+ end
+
+ stuck_thread = Thread.new do
+ pool.with_connection {}
+ end
+
+ # wait for stuck_thread to get in queue
+ Thread.pass until pool.num_waiting_in_queue == 1
+
+ pool.clear_reloadable_connections
+
+ unless stuck_thread.join(2)
+ flunk 'clear_reloadable_connections must not let other connection waiting threads get stuck in queue'
+ end
+
+ assert_equal 0, pool.num_waiting_in_queue
+ end
+ end
+
+ private
+ def with_single_connection_pool
+ one_conn_spec = ActiveRecord::Base.connection_pool.spec.dup
+ one_conn_spec.config[:pool] = 1 # this is safe to do, because .dupped ConnectionSpecification also auto-dups its config
+ yield(pool = ConnectionPool.new(one_conn_spec))
+ ensure
+ pool.disconnect! if pool
+ end
end
end
end
diff --git a/activerecord/test/cases/core_test.rb b/activerecord/test/cases/core_test.rb
index 715d92af99..3cb98832c5 100644
--- a/activerecord/test/cases/core_test.rb
+++ b/activerecord/test/cases/core_test.rb
@@ -98,4 +98,15 @@ class CoreTest < ActiveRecord::TestCase
assert actual.start_with?(expected.split('XXXXXX').first)
assert actual.end_with?(expected.split('XXXXXX').last)
end
+
+ def test_pretty_print_overridden_by_inspect
+ subtopic = Class.new(Topic) do
+ def inspect
+ "inspecting topic"
+ end
+ end
+ actual = ''
+ PP.pp(subtopic.new, StringIO.new(actual))
+ assert_equal "inspecting topic\n", actual
+ end
end
diff --git a/activerecord/test/cases/counter_cache_test.rb b/activerecord/test/cases/counter_cache_test.rb
index 07a182070b..922cb59280 100644
--- a/activerecord/test/cases/counter_cache_test.rb
+++ b/activerecord/test/cases/counter_cache_test.rb
@@ -1,6 +1,7 @@
require 'cases/helper'
require 'models/topic'
require 'models/car'
+require 'models/aircraft'
require 'models/wheel'
require 'models/engine'
require 'models/reply'
@@ -180,4 +181,34 @@ class CounterCacheTest < ActiveRecord::TestCase
SpecialTopic.reset_counters(special.id, :lightweight_special_replies)
end
end
+
+ test "counters are updated both in memory and in the database on create" do
+ car = Car.new(engines_count: 0)
+ car.engines = [Engine.new, Engine.new]
+ car.save!
+
+ assert_equal 2, car.engines_count
+ assert_equal 2, car.reload.engines_count
+ end
+
+ test "counter caches are updated in memory when the default value is nil" do
+ car = Car.new(engines_count: nil)
+ car.engines = [Engine.new, Engine.new]
+ car.save!
+
+ assert_equal 2, car.engines_count
+ assert_equal 2, car.reload.engines_count
+ end
+
+ test "update counters in a polymorphic relationship" do
+ aircraft = Aircraft.create!
+
+ assert_difference 'aircraft.reload.wheels_count' do
+ aircraft.wheels << Wheel.create!
+ end
+
+ assert_difference 'aircraft.reload.wheels_count', -1 do
+ aircraft.wheels.first.destroy
+ end
+ end
end
diff --git a/activerecord/test/cases/date_time_precision_test.rb b/activerecord/test/cases/date_time_precision_test.rb
new file mode 100644
index 0000000000..698f1b852e
--- /dev/null
+++ b/activerecord/test/cases/date_time_precision_test.rb
@@ -0,0 +1,111 @@
+require 'cases/helper'
+require 'support/schema_dumping_helper'
+
+if ActiveRecord::Base.connection.supports_datetime_with_precision?
+class DateTimePrecisionTest < ActiveRecord::TestCase
+ include SchemaDumpingHelper
+ self.use_transactional_tests = false
+
+ class Foo < ActiveRecord::Base; end
+
+ setup do
+ @connection = ActiveRecord::Base.connection
+ end
+
+ teardown do
+ @connection.drop_table :foos, if_exists: true
+ end
+
+ def test_datetime_data_type_with_precision
+ @connection.create_table(:foos, force: true)
+ @connection.add_column :foos, :created_at, :datetime, precision: 0
+ @connection.add_column :foos, :updated_at, :datetime, precision: 5
+ assert_equal 0, activerecord_column_option('foos', 'created_at', 'precision')
+ assert_equal 5, activerecord_column_option('foos', 'updated_at', 'precision')
+ end
+
+ def test_timestamps_helper_with_custom_precision
+ @connection.create_table(:foos, force: true) do |t|
+ t.timestamps precision: 4
+ end
+ assert_equal 4, activerecord_column_option('foos', 'created_at', 'precision')
+ assert_equal 4, activerecord_column_option('foos', 'updated_at', 'precision')
+ end
+
+ def test_passing_precision_to_datetime_does_not_set_limit
+ @connection.create_table(:foos, force: true) do |t|
+ t.timestamps precision: 4
+ end
+ assert_nil activerecord_column_option('foos', 'created_at', 'limit')
+ assert_nil activerecord_column_option('foos', 'updated_at', 'limit')
+ end
+
+ def test_invalid_datetime_precision_raises_error
+ assert_raises ActiveRecord::ActiveRecordError do
+ @connection.create_table(:foos, force: true) do |t|
+ t.timestamps precision: 7
+ end
+ end
+ end
+
+ def test_database_agrees_with_activerecord_about_precision
+ @connection.create_table(:foos, force: true) do |t|
+ t.timestamps precision: 4
+ end
+ assert_equal 4, database_datetime_precision('foos', 'created_at')
+ assert_equal 4, database_datetime_precision('foos', 'updated_at')
+ end
+
+ def test_formatting_datetime_according_to_precision
+ @connection.create_table(:foos, force: true) do |t|
+ t.datetime :created_at, precision: 0
+ t.datetime :updated_at, precision: 4
+ end
+ date = ::Time.utc(2014, 8, 17, 12, 30, 0, 999999)
+ Foo.create!(created_at: date, updated_at: date)
+ assert foo = Foo.find_by(created_at: date)
+ assert_equal 1, Foo.where(updated_at: date).count
+ assert_equal date.to_s, foo.created_at.to_s
+ assert_equal date.to_s, foo.updated_at.to_s
+ assert_equal 000000, foo.created_at.usec
+ assert_equal 999900, foo.updated_at.usec
+ end
+
+ def test_schema_dump_includes_datetime_precision
+ @connection.create_table(:foos, force: true) do |t|
+ t.timestamps precision: 6
+ end
+ output = dump_table_schema("foos")
+ assert_match %r{t\.datetime\s+"created_at",\s+precision: 6,\s+null: false$}, output
+ assert_match %r{t\.datetime\s+"updated_at",\s+precision: 6,\s+null: false$}, output
+ end
+
+ if current_adapter?(:PostgreSQLAdapter)
+ def test_datetime_precision_with_zero_should_be_dumped
+ @connection.create_table(:foos, force: true) do |t|
+ t.timestamps precision: 0
+ end
+ output = dump_table_schema("foos")
+ assert_match %r{t\.datetime\s+"created_at",\s+precision: 0,\s+null: false$}, output
+ assert_match %r{t\.datetime\s+"updated_at",\s+precision: 0,\s+null: false$}, output
+ end
+ end
+
+ private
+
+ def database_datetime_precision(table_name, column_name)
+ results = @connection.exec_query("SELECT column_name, datetime_precision FROM information_schema.columns WHERE table_name = '#{table_name}'")
+ result = results.find do |result_hash|
+ result_hash["column_name"] == column_name
+ end
+ result && result["datetime_precision"].to_i
+ end
+
+ def activerecord_column_option(tablename, column_name, option)
+ result = @connection.columns(tablename).find do |column|
+ column.name == column_name
+ end
+ result && result.send(option)
+ end
+end
+end
diff --git a/activerecord/test/cases/date_time_test.rb b/activerecord/test/cases/date_time_test.rb
index c0491bbee5..4cbff564aa 100644
--- a/activerecord/test/cases/date_time_test.rb
+++ b/activerecord/test/cases/date_time_test.rb
@@ -3,6 +3,8 @@ require 'models/topic'
require 'models/task'
class DateTimeTest < ActiveRecord::TestCase
+ include InTimeZone
+
def test_saves_both_date_and_time
with_env_tz 'America/New_York' do
with_timezone_config default: :utc do
@@ -29,6 +31,14 @@ class DateTimeTest < ActiveRecord::TestCase
assert_nil task.ending
end
+ def test_assign_bad_date_time_with_timezone
+ in_time_zone "Pacific Time (US & Canada)" do
+ task = Task.new
+ task.starting = '2014-07-01T24:59:59GMT'
+ assert_nil task.starting
+ end
+ end
+
def test_assign_empty_date
topic = Topic.new
topic.last_read = ''
@@ -40,4 +50,12 @@ class DateTimeTest < ActiveRecord::TestCase
topic.bonus_time = ''
assert_nil topic.bonus_time
end
+
+ def test_assign_in_local_timezone
+ now = DateTime.now
+ with_timezone_config default: :local do
+ task = Task.new starting: now
+ assert_equal now, task.starting
+ end
+ end
end
diff --git a/activerecord/test/cases/defaults_test.rb b/activerecord/test/cases/defaults_test.rb
index c089e63128..67fddebf45 100644
--- a/activerecord/test/cases/defaults_test.rb
+++ b/activerecord/test/cases/defaults_test.rb
@@ -18,25 +18,48 @@ class DefaultTest < ActiveRecord::TestCase
end
end
- if current_adapter?(:PostgreSQLAdapter, :OracleAdapter)
- def test_default_integers
- default = Default.new
- assert_instance_of Fixnum, default.positive_integer
- assert_equal 1, default.positive_integer
- assert_instance_of Fixnum, default.negative_integer
- assert_equal(-1, default.negative_integer)
- assert_instance_of BigDecimal, default.decimal_number
- assert_equal BigDecimal.new("2.78"), default.decimal_number
- end
- end
-
if current_adapter?(:PostgreSQLAdapter)
def test_multiline_default_text
+ record = Default.new
# older postgres versions represent the default with escapes ("\\012" for a newline)
- assert( "--- []\n\n" == Default.columns_hash['multiline_default'].default ||
- "--- []\\012\\012" == Default.columns_hash['multiline_default'].default)
+ assert("--- []\n\n" == record.multiline_default || "--- []\\012\\012" == record.multiline_default)
+ end
+ end
+end
+
+class DefaultNumbersTest < ActiveRecord::TestCase
+ class DefaultNumber < ActiveRecord::Base; end
+
+ setup do
+ @connection = ActiveRecord::Base.connection
+ @connection.create_table :default_numbers do |t|
+ t.integer :positive_integer, default: 7
+ t.integer :negative_integer, default: -5
+ t.decimal :decimal_number, default: "2.78", precision: 5, scale: 2
end
end
+
+ teardown do
+ @connection.drop_table :default_numbers, if_exists: true
+ end
+
+ def test_default_positive_integer
+ record = DefaultNumber.new
+ assert_equal 7, record.positive_integer
+ assert_equal "7", record.positive_integer_before_type_cast
+ end
+
+ def test_default_negative_integer
+ record = DefaultNumber.new
+ assert_equal (-5), record.negative_integer
+ assert_equal "-5", record.negative_integer_before_type_cast
+ end
+
+ def test_default_decimal_number
+ record = DefaultNumber.new
+ assert_equal BigDecimal.new("2.78"), record.decimal_number
+ assert_equal "2.78", record.decimal_number_before_type_cast
+ end
end
class DefaultStringsTest < ActiveRecord::TestCase
@@ -67,14 +90,14 @@ end
if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
class DefaultsTestWithoutTransactionalFixtures < ActiveRecord::TestCase
# ActiveRecord::Base#create! (and #save and other related methods) will
- # open a new transaction. When in transactional fixtures mode, this will
+ # open a new transaction. When in transactional tests mode, this will
# cause Active Record to create a new savepoint. However, since MySQL doesn't
# support DDL transactions, creating a table will result in any created
# savepoints to be automatically released. This in turn causes the savepoint
# release code in AbstractAdapter#transaction to fail.
#
- # We don't want that to happen, so we disable transactional fixtures here.
- self.use_transactional_fixtures = false
+ # We don't want that to happen, so we disable transactional tests here.
+ self.use_transactional_tests = false
def using_strict(strict)
connection = ActiveRecord::Base.remove_connection
@@ -99,19 +122,21 @@ if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
def test_mysql_text_not_null_defaults_non_strict
using_strict(false) do
with_text_blob_not_null_table do |klass|
- assert_equal '', klass.columns_hash['non_null_blob'].default
- assert_equal '', klass.columns_hash['non_null_text'].default
+ record = klass.new
+ assert_equal '', record.non_null_blob
+ assert_equal '', record.non_null_text
- assert_nil klass.columns_hash['null_blob'].default
- assert_nil klass.columns_hash['null_text'].default
+ assert_nil record.null_blob
+ assert_nil record.null_text
- instance = klass.create!
+ record.save!
+ record.reload
- assert_equal '', instance.non_null_text
- assert_equal '', instance.non_null_blob
+ assert_equal '', record.non_null_text
+ assert_equal '', record.non_null_blob
- assert_nil instance.null_text
- assert_nil instance.null_blob
+ assert_nil record.null_text
+ assert_nil record.null_blob
end
end
end
@@ -119,10 +144,11 @@ if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
def test_mysql_text_not_null_defaults_strict
using_strict(true) do
with_text_blob_not_null_table do |klass|
- assert_nil klass.columns_hash['non_null_blob'].default
- assert_nil klass.columns_hash['non_null_text'].default
- assert_nil klass.columns_hash['null_blob'].default
- assert_nil klass.columns_hash['null_text'].default
+ record = klass.new
+ assert_nil record.non_null_blob
+ assert_nil record.non_null_text
+ assert_nil record.null_blob
+ assert_nil record.null_text
assert_raises(ActiveRecord::StatementInvalid) { klass.create }
end
@@ -172,48 +198,3 @@ if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
end
end
end
-
-if current_adapter?(:PostgreSQLAdapter)
- class DefaultsUsingMultipleSchemasAndDomainTest < ActiveSupport::TestCase
- def setup
- @connection = ActiveRecord::Base.connection
-
- @old_search_path = @connection.schema_search_path
- @connection.schema_search_path = "schema_1, pg_catalog"
- @connection.create_table "defaults" do |t|
- t.text "text_col", :default => "some value"
- t.string "string_col", :default => "some value"
- end
- Default.reset_column_information
- end
-
- def test_text_defaults_in_new_schema_when_overriding_domain
- assert_equal "some value", Default.new.text_col, "Default of text column was not correctly parse"
- end
-
- def test_string_defaults_in_new_schema_when_overriding_domain
- assert_equal "some value", Default.new.string_col, "Default of string column was not correctly parse"
- end
-
- def test_bpchar_defaults_in_new_schema_when_overriding_domain
- @connection.execute "ALTER TABLE defaults ADD bpchar_col bpchar DEFAULT 'some value'"
- Default.reset_column_information
- assert_equal "some value", Default.new.bpchar_col, "Default of bpchar column was not correctly parse"
- end
-
- def test_text_defaults_after_updating_column_default
- @connection.execute "ALTER TABLE defaults ALTER COLUMN text_col SET DEFAULT 'some text'::schema_1.text"
- assert_equal "some text", Default.new.text_col, "Default of text column was not correctly parse after updating default using '::text' since postgreSQL will add parens to the default in db"
- end
-
- def test_default_containing_quote_and_colons
- @connection.execute "ALTER TABLE defaults ALTER COLUMN string_col SET DEFAULT 'foo''::bar'"
- assert_equal "foo'::bar", Default.new.string_col
- end
-
- teardown do
- @connection.schema_search_path = @old_search_path
- Default.reset_column_information
- end
- end
-end
diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb
index 69a7f25213..cd1967c373 100644
--- a/activerecord/test/cases/dirty_test.rb
+++ b/activerecord/test/cases/dirty_test.rb
@@ -89,7 +89,7 @@ class DirtyTest < ActiveRecord::TestCase
target = Class.new(ActiveRecord::Base)
target.table_name = 'pirates'
- pirate = target.create
+ pirate = target.create!
pirate.created_on = pirate.created_on
assert !pirate.created_on_changed?
end
@@ -165,18 +165,6 @@ class DirtyTest < ActiveRecord::TestCase
assert_equal parrot.name_change, parrot.title_change
end
- def test_reset_attribute!
- pirate = Pirate.create!(:catchphrase => 'Yar!')
- pirate.catchphrase = 'Ahoy!'
-
- assert_deprecated do
- pirate.reset_catchphrase!
- end
- assert_equal "Yar!", pirate.catchphrase
- assert_equal Hash.new, pirate.changes
- assert !pirate.catchphrase_changed?
- end
-
def test_restore_attribute!
pirate = Pirate.create!(:catchphrase => 'Yar!')
pirate.catchphrase = 'Ahoy!'
@@ -479,8 +467,10 @@ class DirtyTest < ActiveRecord::TestCase
topic.save!
updated_at = topic.updated_at
- topic.content[:hello] = 'world'
- topic.save!
+ travel(1.second) do
+ topic.content[:hello] = 'world'
+ topic.save!
+ end
assert_not_equal updated_at, topic.updated_at
assert_equal 'world', topic.content[:hello]
@@ -533,6 +523,9 @@ class DirtyTest < ActiveRecord::TestCase
assert_equal Hash.new, pirate.previous_changes
pirate = Pirate.find_by_catchphrase("arrr")
+
+ travel(1.second)
+
pirate.catchphrase = "Me Maties!"
pirate.save!
@@ -544,6 +537,9 @@ class DirtyTest < ActiveRecord::TestCase
assert !pirate.previous_changes.key?('created_on')
pirate = Pirate.find_by_catchphrase("Me Maties!")
+
+ travel(1.second)
+
pirate.catchphrase = "Thar She Blows!"
pirate.save
@@ -554,6 +550,8 @@ class DirtyTest < ActiveRecord::TestCase
assert !pirate.previous_changes.key?('parrot_id')
assert !pirate.previous_changes.key?('created_on')
+ travel(1.second)
+
pirate = Pirate.find_by_catchphrase("Thar She Blows!")
pirate.update(catchphrase: "Ahoy!")
@@ -564,6 +562,8 @@ class DirtyTest < ActiveRecord::TestCase
assert !pirate.previous_changes.key?('parrot_id')
assert !pirate.previous_changes.key?('created_on')
+ travel(1.second)
+
pirate = Pirate.find_by_catchphrase("Ahoy!")
pirate.update_attribute(:catchphrase, "Ninjas suck!")
@@ -573,6 +573,8 @@ class DirtyTest < ActiveRecord::TestCase
assert_not_nil pirate.previous_changes['updated_on'][1]
assert !pirate.previous_changes.key?('parrot_id')
assert !pirate.previous_changes.key?('created_on')
+ ensure
+ travel_back
end
if ActiveRecord::Base.connection.supports_migrations?
@@ -590,6 +592,7 @@ class DirtyTest < ActiveRecord::TestCase
end
def test_datetime_attribute_can_be_updated_with_fractional_seconds
+ skip "Fractional seconds are not supported" unless subsecond_precision_supported?
in_time_zone 'Paris' do
target = Class.new(ActiveRecord::Base)
target.table_name = 'topics'
@@ -635,30 +638,100 @@ class DirtyTest < ActiveRecord::TestCase
end
end
- test "defaults with type that implements `type_cast_for_database`" do
- type = Class.new(ActiveRecord::Type::Value) do
- def type_cast(value)
- value.to_i
- end
+ test "in place mutation detection" do
+ pirate = Pirate.create!(catchphrase: "arrrr")
+ pirate.catchphrase << " matey!"
- def type_cast_for_database(value)
- value.to_s
+ assert pirate.catchphrase_changed?
+ expected_changes = {
+ "catchphrase" => ["arrrr", "arrrr matey!"]
+ }
+ assert_equal(expected_changes, pirate.changes)
+ assert_equal("arrrr", pirate.catchphrase_was)
+ assert pirate.catchphrase_changed?(from: "arrrr")
+ assert_not pirate.catchphrase_changed?(from: "anything else")
+ assert pirate.changed_attributes.include?(:catchphrase)
+
+ pirate.save!
+ pirate.reload
+
+ assert_equal "arrrr matey!", pirate.catchphrase
+ assert_not pirate.changed?
+ end
+
+ test "in place mutation for binary" do
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = :binaries
+ serialize :data
+ end
+
+ binary = klass.create!(data: "\\\\foo")
+
+ assert_not binary.changed?
+
+ binary.data = binary.data.dup
+
+ assert_not binary.changed?
+
+ binary = klass.last
+
+ assert_not binary.changed?
+
+ binary.data << "bar"
+
+ assert binary.changed?
+ end
+
+ test "attribute_changed? doesn't compute in-place changes for unrelated attributes" do
+ test_type_class = Class.new(ActiveRecord::Type::Value) do
+ define_method(:changed_in_place?) do |*|
+ raise
end
end
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = 'people'
+ attribute :foo, test_type_class.new
+ end
+
+ model = klass.new(first_name: "Jim")
+ assert model.first_name_changed?
+ end
- model_class = Class.new(ActiveRecord::Base) do
- self.table_name = 'numeric_data'
- attribute :foo, type.new, default: 1
+ test "attribute_will_change! doesn't try to save non-persistable attributes" do
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = 'people'
+ attribute :non_persisted_attribute, :string
end
- model = model_class.new
- assert_not model.foo_changed?
+ record = klass.new(first_name: "Sean")
+ record.non_persisted_attribute_will_change!
+
+ assert record.non_persisted_attribute_changed?
+ assert record.save
+ end
+
+ test "mutating and then assigning doesn't remove the change" do
+ pirate = Pirate.create!(catchphrase: "arrrr")
+ pirate.catchphrase << " matey!"
+ pirate.catchphrase = "arrrr matey!"
+
+ assert pirate.catchphrase_changed?(from: "arrrr", to: "arrrr matey!")
+ end
+
+ test "getters with side effects are allowed" do
+ klass = Class.new(Pirate) do
+ def catchphrase
+ if super.blank?
+ update_attribute(:catchphrase, "arr") # what could possibly go wrong?
+ end
+ super
+ end
+ end
- model = model_class.new(foo: 1)
- assert_not model.foo_changed?
+ pirate = klass.create!(catchphrase: "lol")
+ pirate.update_attribute(:catchphrase, nil)
- model = model_class.new(foo: '1')
- assert_not model.foo_changed?
+ assert_equal "arr", pirate.catchphrase
end
private
diff --git a/activerecord/test/cases/disconnected_test.rb b/activerecord/test/cases/disconnected_test.rb
index 94447addc1..c25089a420 100644
--- a/activerecord/test/cases/disconnected_test.rb
+++ b/activerecord/test/cases/disconnected_test.rb
@@ -4,7 +4,7 @@ class TestRecord < ActiveRecord::Base
end
class TestDisconnectedAdapter < ActiveRecord::TestCase
- self.use_transactional_fixtures = false
+ self.use_transactional_tests = false
def setup
@connection = ActiveRecord::Base.connection
@@ -21,7 +21,9 @@ class TestDisconnectedAdapter < ActiveRecord::TestCase
@connection.execute "SELECT count(*) from products"
@connection.disconnect!
assert_raises(ActiveRecord::StatementInvalid) do
- @connection.execute "SELECT count(*) from products"
+ silence_warnings do
+ @connection.execute "SELECT count(*) from products"
+ end
end
end
end
diff --git a/activerecord/test/cases/enum_test.rb b/activerecord/test/cases/enum_test.rb
index 3b2f0dfe07..7c930de97b 100644
--- a/activerecord/test/cases/enum_test.rb
+++ b/activerecord/test/cases/enum_test.rb
@@ -9,26 +9,83 @@ class EnumTest < ActiveRecord::TestCase
end
test "query state by predicate" do
- assert @book.proposed?
+ assert @book.published?
assert_not @book.written?
- assert_not @book.published?
+ assert_not @book.proposed?
- assert @book.unread?
+ assert @book.read?
+ assert @book.in_english?
+ assert @book.author_visibility_visible?
+ assert @book.illustrator_visibility_visible?
+ assert @book.with_medium_font_size?
end
test "query state with strings" do
- assert_equal "proposed", @book.status
- assert_equal "unread", @book.read_status
+ assert_equal "published", @book.status
+ assert_equal "read", @book.read_status
+ assert_equal "english", @book.language
+ assert_equal "visible", @book.author_visibility
+ assert_equal "visible", @book.illustrator_visibility
end
test "find via scope" do
- assert_equal @book, Book.proposed.first
- assert_equal @book, Book.unread.first
+ assert_equal @book, Book.published.first
+ assert_equal @book, Book.read.first
+ assert_equal @book, Book.in_english.first
+ assert_equal @book, Book.author_visibility_visible.first
+ assert_equal @book, Book.illustrator_visibility_visible.first
+ end
+
+ test "find via where with values" do
+ published, written = Book.statuses[:published], Book.statuses[:written]
+
+ assert_equal @book, Book.where(status: published).first
+ assert_not_equal @book, Book.where(status: written).first
+ assert_equal @book, Book.where(status: [published]).first
+ assert_not_equal @book, Book.where(status: [written]).first
+ assert_not_equal @book, Book.where("status <> ?", published).first
+ assert_equal @book, Book.where("status <> ?", written).first
+ end
+
+ test "find via where with symbols" do
+ assert_equal @book, Book.where(status: :published).first
+ assert_not_equal @book, Book.where(status: :written).first
+ assert_equal @book, Book.where(status: [:published]).first
+ assert_not_equal @book, Book.where(status: [:written]).first
+ assert_not_equal @book, Book.where.not(status: :published).first
+ assert_equal @book, Book.where.not(status: :written).first
+ end
+
+ test "find via where with strings" do
+ assert_equal @book, Book.where(status: "published").first
+ assert_not_equal @book, Book.where(status: "written").first
+ assert_equal @book, Book.where(status: ["published"]).first
+ assert_not_equal @book, Book.where(status: ["written"]).first
+ assert_not_equal @book, Book.where.not(status: "published").first
+ assert_equal @book, Book.where.not(status: "written").first
+ end
+
+ test "build from scope" do
+ assert Book.written.build.written?
+ assert_not 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?
end
test "update by declaration" do
@book.written!
assert @book.written?
+ @book.in_english!
+ assert @book.in_english?
+ @book.author_visibility_visible!
+ assert @book.author_visibility_visible?
end
test "update by setter" do
@@ -53,42 +110,61 @@ class EnumTest < ActiveRecord::TestCase
test "enum changed attributes" do
old_status = @book.status
- @book.status = :published
+ old_language = @book.language
+ @book.status = :proposed
+ @book.language = :spanish
assert_equal old_status, @book.changed_attributes[:status]
+ assert_equal old_language, @book.changed_attributes[:language]
end
test "enum changes" do
old_status = @book.status
- @book.status = :published
- assert_equal [old_status, 'published'], @book.changes[:status]
+ old_language = @book.language
+ @book.status = :proposed
+ @book.language = :spanish
+ assert_equal [old_status, 'proposed'], @book.changes[:status]
+ assert_equal [old_language, 'spanish'], @book.changes[:language]
end
test "enum attribute was" do
old_status = @book.status
+ old_language = @book.language
@book.status = :published
+ @book.language = :spanish
assert_equal old_status, @book.attribute_was(:status)
+ assert_equal old_language, @book.attribute_was(:language)
end
test "enum attribute changed" do
- @book.status = :published
+ @book.status = :proposed
+ @book.language = :french
assert @book.attribute_changed?(:status)
+ assert @book.attribute_changed?(:language)
end
test "enum attribute changed to" do
- @book.status = :published
- assert @book.attribute_changed?(:status, to: 'published')
+ @book.status = :proposed
+ @book.language = :french
+ assert @book.attribute_changed?(:status, to: 'proposed')
+ assert @book.attribute_changed?(:language, to: 'french')
end
test "enum attribute changed from" do
old_status = @book.status
- @book.status = :published
+ old_language = @book.language
+ @book.status = :proposed
+ @book.language = :french
assert @book.attribute_changed?(:status, from: old_status)
+ assert @book.attribute_changed?(:language, from: old_language)
end
test "enum attribute changed from old status to new status" do
old_status = @book.status
- @book.status = :published
- assert @book.attribute_changed?(:status, from: old_status, to: 'published')
+ old_language = @book.language
+ @book.status = :proposed
+ @book.language = :french
+ assert @book.attribute_changed?(:status, from: old_status, to: 'proposed')
+ assert @book.attribute_changed?(:language, from: old_language, to: 'french')
end
test "enum didn't change" do
@@ -98,7 +174,7 @@ class EnumTest < ActiveRecord::TestCase
end
test "persist changes that are dirty" do
- @book.status = :published
+ @book.status = :proposed
assert @book.attribute_changed?(:status)
@book.status = :written
assert @book.attribute_changed?(:status)
@@ -106,7 +182,7 @@ class EnumTest < ActiveRecord::TestCase
test "reverted changes that are not dirty" do
old_status = @book.status
- @book.status = :published
+ @book.status = :proposed
assert @book.attribute_changed?(:status)
@book.status = old_status
assert_not @book.attribute_changed?(:status)
@@ -129,19 +205,24 @@ class EnumTest < ActiveRecord::TestCase
assert_equal "'unknown' is not a valid status", e.message
end
+ test "NULL values from database should be casted to nil" do
+ Book.where(id: @book.id).update_all("status = NULL")
+ assert_nil @book.reload.status
+ end
+
test "assign nil value" do
@book.status = nil
- assert @book.status.nil?
+ assert_nil @book.status
end
test "assign empty string value" do
@book.status = ''
- assert @book.status.nil?
+ assert_nil @book.status
end
test "assign long empty string value" do
@book.status = ' '
- assert @book.status.nil?
+ assert_nil @book.status
end
test "constant to access the mapping" do
@@ -153,15 +234,23 @@ class EnumTest < ActiveRecord::TestCase
test "building new objects with enum scopes" do
assert Book.written.build.written?
assert Book.read.build.read?
+ assert Book.in_spanish.build.in_spanish?
+ assert Book.illustrator_visibility_invisible.build.illustrator_visibility_invisible?
end
test "creating new objects with enum scopes" do
assert Book.written.create.written?
assert Book.read.create.read?
+ assert Book.in_spanish.create.in_spanish?
+ assert Book.illustrator_visibility_invisible.create.illustrator_visibility_invisible?
end
test "_before_type_cast returns the enum label (required for form fields)" do
- assert_equal "proposed", @book.status_before_type_cast
+ if @book.status_came_from_user?
+ assert_equal "published", @book.status_before_type_cast
+ else
+ assert_equal "published", @book.status
+ end
end
test "reserved enum names" do
@@ -177,9 +266,10 @@ class EnumTest < ActiveRecord::TestCase
]
conflicts.each_with_index do |name, i|
- assert_raises(ArgumentError, "enum name `#{name}` should not be allowed") do
+ e = assert_raises(ArgumentError) do
klass.class_eval { enum name => ["value_#{i}"] }
end
+ assert_match(/You tried to define an enum named \"#{name}\" on the model/, e.message)
end
end
@@ -194,13 +284,15 @@ class EnumTest < ActiveRecord::TestCase
:valid, # generates #valid?, which conflicts with an AR method
:save, # generates #save!, which conflicts with an AR method
:proposed, # same value as an existing enum
- :public, :private, :protected, # generates a method that conflict with ruby words
+ :public, :private, :protected, # some important methods on Module and Class
+ :name, :parent, :superclass
]
conflicts.each_with_index do |value, i|
- assert_raises(ArgumentError, "enum value `#{value}` should not be allowed") do
+ e = assert_raises(ArgumentError, "enum value `#{value}` should not be allowed") do
klass.class_eval { enum "status_#{i}" => [value] }
end
+ assert_match(/You tried to define an enum named .* on the model/, e.message)
end
end
@@ -286,4 +378,37 @@ class EnumTest < ActiveRecord::TestCase
book2.status = :uploaded
assert_equal ['drafted', 'uploaded'], book2.status_change
end
+
+ test "declare multiple enums at a time" do
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = "books"
+ enum status: [:proposed, :written, :published],
+ nullable_status: [:single, :married]
+ end
+
+ book1 = klass.proposed.create!
+ assert book1.proposed?
+
+ book2 = klass.single.create!
+ assert book2.single?
+ end
+
+ test "query state by predicate with prefix" do
+ assert @book.author_visibility_visible?
+ assert_not @book.author_visibility_invisible?
+ assert @book.illustrator_visibility_visible?
+ assert_not @book.illustrator_visibility_invisible?
+ end
+
+ test "query state by predicate with custom prefix" do
+ assert @book.in_english?
+ assert_not @book.in_spanish?
+ assert_not @book.in_french?
+ end
+
+ test "uses default status when no status is provided in fixtures" do
+ book = books(:tlg)
+ assert book.proposed?, "expected fixture to default to proposed status"
+ assert book.in_english?, "expected fixture to default to english language"
+ end
end
diff --git a/activerecord/test/cases/errors_test.rb b/activerecord/test/cases/errors_test.rb
new file mode 100644
index 0000000000..0711a372f2
--- /dev/null
+++ b/activerecord/test/cases/errors_test.rb
@@ -0,0 +1,16 @@
+require_relative "../cases/helper"
+
+class ErrorsTest < ActiveRecord::TestCase
+ def test_can_be_instantiated_with_no_args
+ base = ActiveRecord::ActiveRecordError
+ error_klasses = ObjectSpace.each_object(Class).select { |klass| klass < base }
+
+ error_klasses.each do |error_klass|
+ begin
+ error_klass.new.inspect
+ rescue ArgumentError
+ raise "Instance of #{error_klass} can't be initialized with no arguments"
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/explain_subscriber_test.rb b/activerecord/test/cases/explain_subscriber_test.rb
index 8de2ddb10d..2dee8a26a5 100644
--- a/activerecord/test/cases/explain_subscriber_test.rb
+++ b/activerecord/test/cases/explain_subscriber_test.rb
@@ -48,6 +48,11 @@ if ActiveRecord::Base.connection.supports_explain?
assert queries.empty?
end
+ def test_collects_cte_queries
+ SUBSCRIBER.finish(nil, nil, name: 'SQL', sql: 'with s as (values(3)) select 1 from s')
+ assert_equal 1, queries.size
+ end
+
teardown do
ActiveRecord::ExplainRegistry.reset
end
diff --git a/activerecord/test/cases/explain_test.rb b/activerecord/test/cases/explain_test.rb
index 9d25bdd82a..64dfd86ce2 100644
--- a/activerecord/test/cases/explain_test.rb
+++ b/activerecord/test/cases/explain_test.rb
@@ -28,7 +28,7 @@ if ActiveRecord::Base.connection.supports_explain?
assert_match "SELECT", sql
if binds.any?
assert_equal 1, binds.length
- assert_equal "honda", binds.flatten.last
+ assert_equal "honda", binds.last.value
else
assert_match 'honda', sql
end
@@ -39,38 +39,49 @@ if ActiveRecord::Base.connection.supports_explain?
binds = [[], []]
queries = sqls.zip(binds)
- connection.stubs(:explain).returns('query plan foo', 'query plan bar')
- expected = sqls.map {|sql| "EXPLAIN for: #{sql}\nquery plan #{sql}"}.join("\n")
- assert_equal expected, base.exec_explain(queries)
+ stub_explain_for_query_plans do
+ expected = sqls.map {|sql| "EXPLAIN for: #{sql}\nquery plan #{sql}"}.join("\n")
+ assert_equal expected, base.exec_explain(queries)
+ end
end
def test_exec_explain_with_binds
- cols = [Object.new, Object.new]
- cols[0].expects(:name).returns('wadus')
- cols[1].expects(:name).returns('chaflan')
+ object = Struct.new(:name)
+ cols = [object.new('wadus'), object.new('chaflan')]
sqls = %w(foo bar)
binds = [[[cols[0], 1]], [[cols[1], 2]]]
queries = sqls.zip(binds)
- connection.stubs(:explain).returns("query plan foo\n", "query plan bar\n")
- expected = <<-SQL.strip_heredoc
- EXPLAIN for: #{sqls[0]} [["wadus", 1]]
- query plan foo
+ stub_explain_for_query_plans(["query plan foo\n", "query plan bar\n"]) do
+ expected = <<-SQL.strip_heredoc
+ EXPLAIN for: #{sqls[0]} [["wadus", 1]]
+ query plan foo
- EXPLAIN for: #{sqls[1]} [["chaflan", 2]]
- query plan bar
- SQL
- assert_equal expected, base.exec_explain(queries)
+ EXPLAIN for: #{sqls[1]} [["chaflan", 2]]
+ query plan bar
+ SQL
+ assert_equal expected, base.exec_explain(queries)
+ end
end
def test_unsupported_connection_adapter
- connection.stubs(:supports_explain?).returns(false)
+ connection.stub(:supports_explain?, false) do
+ assert_not_called(base.logger, :warn) do
+ Car.where(:name => 'honda').to_a
+ end
+ end
+ end
- base.logger.expects(:warn).never
+ private
- Car.where(:name => 'honda').to_a
- end
+ def stub_explain_for_query_plans(query_plans = ['query plan foo', 'query plan bar'])
+ explain_called = 0
+
+ connection.stub(:explain, proc{ explain_called += 1; query_plans[explain_called - 1] }) do
+ yield
+ end
+ end
end
end
diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb
index 40e51a0cdc..6686ce012d 100644
--- a/activerecord/test/cases/finder_test.rb
+++ b/activerecord/test/cases/finder_test.rb
@@ -4,18 +4,22 @@ require 'models/author'
require 'models/categorization'
require 'models/comment'
require 'models/company'
+require 'models/tagging'
require 'models/topic'
require 'models/reply'
require 'models/entrant'
require 'models/project'
require 'models/developer'
+require 'models/computer'
require 'models/customer'
require 'models/toy'
require 'models/matey'
require 'models/dog'
+require 'models/car'
+require 'models/tyre'
class FinderTest < ActiveRecord::TestCase
- fixtures :companies, :topics, :entrants, :developers, :developers_projects, :posts, :comments, :accounts, :authors, :customers, :categories, :categorizations
+ fixtures :companies, :topics, :entrants, :developers, :developers_projects, :posts, :comments, :accounts, :authors, :customers, :categories, :categorizations, :cars
def test_find_by_id_with_hash
assert_raises(ActiveRecord::StatementInvalid) do
@@ -51,10 +55,13 @@ class FinderTest < ActiveRecord::TestCase
end
def test_symbols_table_ref
- Post.first # warm up
+ gc_disabled = GC.disable
+ Post.where("author_id" => nil) # warm up
x = Symbol.all_symbols.count
Post.where("title" => {"xxxqqqq" => "bar"})
assert_equal x, Symbol.all_symbols.count
+ ensure
+ GC.enable if gc_disabled == false
end
# find should handle strings that come from URLs
@@ -78,6 +85,19 @@ class FinderTest < ActiveRecord::TestCase
assert_raise(NoMethodError) { Topic.exists?([1,2]) }
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')
+
+ assert_equal true, relation.exists?(title: ['Post'])
+ assert_equal true, relation.exists?(['title LIKE ?', 'Post%'])
+ assert_equal true, relation.exists?
+ assert_equal true, relation.exists?(post.id)
+ assert_equal true, relation.exists?(post.id.to_s)
+
+ assert_equal false, relation.exists?(false)
+ end
+
def test_exists_passing_active_record_object_is_deprecated
assert_deprecated do
Topic.exists?(Topic.new)
@@ -85,21 +105,10 @@ class FinderTest < ActiveRecord::TestCase
end
def test_exists_fails_when_parameter_has_invalid_type
- if current_adapter?(:PostgreSQLAdapter, :MysqlAdapter)
- assert_raises ActiveRecord::StatementInvalid do
- Topic.exists?(("9"*53).to_i) # number that's bigger than int
- end
- else
+ assert_raises(RangeError) do
assert_equal false, Topic.exists?(("9"*53).to_i) # number that's bigger than int
end
-
- if current_adapter?(:PostgreSQLAdapter)
- assert_raises ActiveRecord::StatementInvalid do
- Topic.exists?("foo")
- end
- else
- assert_equal false, Topic.exists?("foo")
- end
+ assert_equal false, Topic.exists?("foo")
end
def test_exists_does_not_select_columns_without_alias
@@ -169,8 +178,9 @@ class FinderTest < ActiveRecord::TestCase
end
def test_exists_does_not_instantiate_records
- Developer.expects(:instantiate).never
- Developer.exists?
+ assert_not_called(Developer, :instantiate) do
+ Developer.exists?
+ end
end
def test_find_by_array_of_one_id
@@ -195,6 +205,28 @@ class FinderTest < ActiveRecord::TestCase
assert_equal 2, last_devs.size
end
+ def test_find_with_large_number
+ assert_raises(ActiveRecord::RecordNotFound) { Topic.find('9999999999999999999999999999999') }
+ end
+
+ def test_find_by_with_large_number
+ assert_nil Topic.find_by(id: '9999999999999999999999999999999')
+ end
+
+ def test_find_by_id_with_large_number
+ assert_nil Topic.find_by_id('9999999999999999999999999999999')
+ end
+
+ def test_find_on_relation_with_large_number
+ assert_nil Topic.where('1=1').find_by(id: 9999999999999999999999999999999)
+ end
+
+ def test_find_by_bang_on_relation_with_large_number
+ assert_raises(ActiveRecord::RecordNotFound) do
+ Topic.where('1=1').find_by!(id: 9999999999999999999999999999999)
+ end
+ end
+
def test_find_an_empty_array
assert_equal [], Topic.find([])
end
@@ -233,6 +265,12 @@ class FinderTest < ActiveRecord::TestCase
assert_equal [Account], accounts.collect(&:class).uniq
end
+ def test_find_by_association_subquery
+ author = authors(:david)
+ assert_equal author.post, Post.find_by(author: Author.where(id: author))
+ assert_equal author.post, Post.find_by(author_id: Author.where(id: author))
+ end
+
def test_take
assert_equal topics(:first), Topic.take
end
@@ -248,7 +286,7 @@ class FinderTest < ActiveRecord::TestCase
end
def test_take_bang_missing
- assert_raises ActiveRecord::RecordNotFound do
+ assert_raises_with_message ActiveRecord::RecordNotFound, "Couldn't find Topic" do
Topic.where("title = 'This title does not exist'").take!
end
end
@@ -268,7 +306,7 @@ class FinderTest < ActiveRecord::TestCase
end
def test_first_bang_missing
- assert_raises ActiveRecord::RecordNotFound do
+ assert_raises_with_message ActiveRecord::RecordNotFound, "Couldn't find Topic" do
Topic.where("title = 'This title does not exist'").first!
end
end
@@ -282,7 +320,7 @@ class FinderTest < ActiveRecord::TestCase
def test_model_class_responds_to_first_bang
assert Topic.first!
Topic.delete_all
- assert_raises ActiveRecord::RecordNotFound do
+ assert_raises_with_message ActiveRecord::RecordNotFound, "Couldn't find Topic" do
Topic.first!
end
end
@@ -304,7 +342,7 @@ class FinderTest < ActiveRecord::TestCase
def test_model_class_responds_to_second_bang
assert Topic.second!
Topic.delete_all
- assert_raises ActiveRecord::RecordNotFound do
+ assert_raises_with_message ActiveRecord::RecordNotFound, "Couldn't find Topic" do
Topic.second!
end
end
@@ -326,7 +364,7 @@ class FinderTest < ActiveRecord::TestCase
def test_model_class_responds_to_third_bang
assert Topic.third!
Topic.delete_all
- assert_raises ActiveRecord::RecordNotFound do
+ assert_raises_with_message ActiveRecord::RecordNotFound, "Couldn't find Topic" do
Topic.third!
end
end
@@ -348,7 +386,7 @@ class FinderTest < ActiveRecord::TestCase
def test_model_class_responds_to_fourth_bang
assert Topic.fourth!
Topic.delete_all
- assert_raises ActiveRecord::RecordNotFound do
+ assert_raises_with_message ActiveRecord::RecordNotFound, "Couldn't find Topic" do
Topic.fourth!
end
end
@@ -370,7 +408,7 @@ class FinderTest < ActiveRecord::TestCase
def test_model_class_responds_to_fifth_bang
assert Topic.fifth!
Topic.delete_all
- assert_raises ActiveRecord::RecordNotFound do
+ assert_raises_with_message ActiveRecord::RecordNotFound, "Couldn't find Topic" do
Topic.fifth!
end
end
@@ -382,14 +420,14 @@ class FinderTest < ActiveRecord::TestCase
end
def test_last_bang_missing
- assert_raises ActiveRecord::RecordNotFound do
+ assert_raises_with_message ActiveRecord::RecordNotFound, "Couldn't find Topic" do
Topic.where("title = 'This title does not exist'").last!
end
end
def test_model_class_responds_to_last_bang
assert_equal topics(:fifth), Topic.last!
- assert_raises ActiveRecord::RecordNotFound do
+ assert_raises_with_message ActiveRecord::RecordNotFound, "Couldn't find Topic" do
Topic.delete_all
Topic.last!
end
@@ -463,6 +501,12 @@ class FinderTest < ActiveRecord::TestCase
assert_raise(ActiveRecord::RecordNotFound) { Topic.where(topics: { approved: true }).find(1) }
end
+ def test_find_on_combined_explicit_and_hashed_table_names
+ assert Topic.where('topics.approved' => false, topics: { author_name: "David" }).find(1)
+ assert_raise(ActiveRecord::RecordNotFound) { Topic.where('topics.approved' => true, topics: { author_name: "David" }).find(1) }
+ assert_raise(ActiveRecord::RecordNotFound) { Topic.where('topics.approved' => false, topics: { author_name: "Melanie" }).find(1) }
+ end
+
def test_find_with_hash_conditions_on_joined_table
firms = Firm.joins(:account).where(:accounts => { :credit_limit => 50 })
assert_equal 1, firms.size
@@ -507,6 +551,10 @@ class FinderTest < ActiveRecord::TestCase
assert_equal [1,2,3,5,6,7,8,9], Comment.where(id: [1..2, 3, 5, 6..8, 9]).to_a.map(&:id).sort
end
+ def test_find_on_hash_conditions_with_array_of_ranges
+ assert_equal [1,2,6,7,8], Comment.where(id: [1..2, 6..8]).to_a.map(&:id).sort
+ 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) }
@@ -659,12 +707,12 @@ class FinderTest < ActiveRecord::TestCase
end
def test_bind_arity
- assert_nothing_raised { bind '' }
+ assert_nothing_raised { bind '' }
assert_raise(ActiveRecord::PreparedStatementInvalid) { bind '', 1 }
assert_raise(ActiveRecord::PreparedStatementInvalid) { bind '?' }
- assert_nothing_raised { bind '?', 1 }
- assert_raise(ActiveRecord::PreparedStatementInvalid) { bind '?', 1, 1 }
+ assert_nothing_raised { bind '?', 1 }
+ assert_raise(ActiveRecord::PreparedStatementInvalid) { bind '?', 1, 1 }
end
def test_named_bind_variables
@@ -679,6 +727,12 @@ class FinderTest < ActiveRecord::TestCase
assert_kind_of Time, Topic.where(["id = :id", { id: 1 }]).first.written_on
end
+ def test_named_bind_arity
+ assert_nothing_raised { bind "name = :name", { name: "37signals" } }
+ assert_nothing_raised { bind "name = :name", { name: "37signals", id: 1 } }
+ assert_raise(ActiveRecord::PreparedStatementInvalid) { bind "name = :name", { id: 1 } }
+ end
+
class SimpleEnumerable
include Enumerable
@@ -760,7 +814,9 @@ class FinderTest < ActiveRecord::TestCase
def test_find_by_one_attribute_bang
assert_equal topics(:first), Topic.find_by_title!("The First Topic")
- assert_raise(ActiveRecord::RecordNotFound) { Topic.find_by_title!("The First Topic!") }
+ assert_raises_with_message(ActiveRecord::RecordNotFound, "Couldn't find Topic") do
+ Topic.find_by_title!("The First Topic!")
+ end
end
def test_find_by_on_attribute_that_is_a_reserved_word
@@ -878,7 +934,7 @@ class FinderTest < ActiveRecord::TestCase
joins('LEFT JOIN developers_projects ON developers.id = developers_projects.developer_id').
where('project_id=1').to_a
assert_equal 3, developers_on_project_one.length
- developer_names = developers_on_project_one.map { |d| d.name }
+ developer_names = developers_on_project_one.map(&:name)
assert developer_names.include?('David')
assert developer_names.include?('Jamis')
end
@@ -904,7 +960,6 @@ class FinderTest < ActiveRecord::TestCase
end
end
- # http://dev.rubyonrails.org/ticket/6778
def test_find_ignores_previously_inserted_record
Post.create!(:title => 'test', :body => 'it out')
assert_equal [], Post.where(id: nil)
@@ -933,7 +988,7 @@ class FinderTest < ActiveRecord::TestCase
end
def test_select_values
- assert_equal ["1","2","3","4","5","6","7","8","9", "10", "11"], Company.connection.select_values("SELECT id FROM companies ORDER BY id").map! { |i| i.to_s }
+ assert_equal ["1","2","3","4","5","6","7","8","9", "10", "11"], Company.connection.select_values("SELECT id FROM companies ORDER BY id").map!(&:to_s)
assert_equal ["37signals","Summit","Microsoft", "Flamboyant Software", "Ex Nihilo", "RailsCore", "Leetsoft", "Jadedpixel", "Odegy", "Ex Nihilo Part Deux", "Apex"], Company.connection.select_values("SELECT name FROM companies ORDER BY id")
end
@@ -959,7 +1014,7 @@ class FinderTest < ActiveRecord::TestCase
where(client_of: [2, 1, nil],
name: ['37signals', 'Summit', 'Microsoft']).
order('client_of DESC').
- map { |x| x.client_of }
+ map(&:client_of)
assert client_of.include?(nil)
assert_equal [2, 1].sort, client_of.compact.sort
@@ -969,7 +1024,7 @@ class FinderTest < ActiveRecord::TestCase
client_of = Company.
where(client_of: [nil]).
order('client_of DESC').
- map { |x| x.client_of }
+ map(&:client_of)
assert_equal [], client_of.compact
end
@@ -1013,6 +1068,73 @@ class FinderTest < ActiveRecord::TestCase
assert_nothing_raised(ActiveRecord::StatementInvalid) { Topic.offset("3").to_a }
end
+ test "find_by with hash conditions returns the first matching record" do
+ assert_equal posts(:eager_other), Post.find_by(id: posts(:eager_other).id)
+ end
+
+ test "find_by with non-hash conditions returns the first matching record" do
+ assert_equal posts(:eager_other), Post.find_by("id = #{posts(:eager_other).id}")
+ end
+
+ test "find_by with multi-arg conditions returns the first matching record" do
+ assert_equal posts(:eager_other), Post.find_by('id = ?', posts(:eager_other).id)
+ end
+
+ test "find_by returns nil if the record is missing" do
+ assert_equal nil, Post.find_by("1 = 0")
+ end
+
+ 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
+ end
+
+ test "find_by doesn't have implicit ordering" do
+ assert_sql(/^((?!ORDER).)*$/) { Post.find_by(id: posts(:eager_other).id) }
+ end
+
+ test "find_by! with hash conditions returns the first matching record" do
+ assert_equal posts(:eager_other), Post.find_by!(id: posts(:eager_other).id)
+ end
+
+ test "find_by! with non-hash conditions returns the first matching record" do
+ assert_equal posts(:eager_other), Post.find_by!("id = #{posts(:eager_other).id}")
+ end
+
+ test "find_by! with multi-arg conditions returns the first matching record" do
+ assert_equal posts(:eager_other), Post.find_by!('id = ?', posts(:eager_other).id)
+ end
+
+ test "find_by! doesn't have implicit ordering" do
+ assert_sql(/^((?!ORDER).)*$/) { Post.find_by!(id: posts(:eager_other).id) }
+ end
+
+ test "find_by! raises RecordNotFound if the record is missing" do
+ assert_raises(ActiveRecord::RecordNotFound) do
+ Post.find_by!("1 = 0")
+ end
+ end
+
+ test "find on a scope does not perform statement caching" do
+ honda = cars(:honda)
+ zyke = cars(:zyke)
+ tyre = honda.tyres.create!
+ tyre2 = zyke.tyres.create!
+
+ assert_equal tyre, honda.tyres.custom_find(tyre.id)
+ assert_equal tyre2, zyke.tyres.custom_find(tyre2.id)
+ end
+
+ test "find_by on a scope does not perform statement caching" do
+ honda = cars(:honda)
+ zyke = cars(:zyke)
+ tyre = honda.tyres.create!
+ tyre2 = zyke.tyres.create!
+
+ assert_equal tyre, honda.tyres.custom_find_by(id: tyre.id)
+ assert_equal tyre2, zyke.tyres.custom_find_by(id: tyre2.id)
+ end
+
protected
def bind(statement, *vars)
if vars.first.is_a?(Hash)
@@ -1029,4 +1151,10 @@ class FinderTest < ActiveRecord::TestCase
end
end)
end
+
+ def assert_raises_with_message(exception_class, message, &block)
+ err = assert_raises(exception_class) { block.call }
+ assert_match message, err.message
+ end
+
end
diff --git a/activerecord/test/cases/fixture_set/file_test.rb b/activerecord/test/cases/fixture_set/file_test.rb
index 92efa8aca7..242e7a9bec 100644
--- a/activerecord/test/cases/fixture_set/file_test.rb
+++ b/activerecord/test/cases/fixture_set/file_test.rb
@@ -123,6 +123,18 @@ END
end
end
+ def test_removes_fixture_config_row
+ File.open(::File.join(FIXTURES_ROOT, 'other_posts.yml')) do |fh|
+ assert_equal(['second_welcome'], fh.each.map { |name, _| name })
+ end
+ end
+
+ def test_extracts_model_class_from_config_row
+ File.open(::File.join(FIXTURES_ROOT, 'other_posts.yml')) do |fh|
+ assert_equal 'Post', fh.model_class
+ end
+ end
+
private
def tmp_yaml(name, contents)
t = Tempfile.new name
diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb
index 042fdaf0bb..a0eaa66e94 100644
--- a/activerecord/test/cases/fixtures_test.rb
+++ b/activerecord/test/cases/fixtures_test.rb
@@ -5,11 +5,14 @@ require 'models/admin/randomly_named_c1'
require 'models/admin/user'
require 'models/binary'
require 'models/book'
+require 'models/bulb'
require 'models/category'
+require 'models/comment'
require 'models/company'
require 'models/computer'
require 'models/course'
require 'models/developer'
+require 'models/doubloon'
require 'models/joke'
require 'models/matey'
require 'models/parrot'
@@ -26,7 +29,7 @@ require 'tempfile'
class FixturesTest < ActiveRecord::TestCase
self.use_instantiated_fixtures = true
- self.use_transactional_fixtures = false
+ self.use_transactional_tests = false
# other_topics fixture should not be included here
fixtures :topics, :developers, :accounts, :tasks, :categories, :funny_jokes, :binaries, :traffic_lights
@@ -214,6 +217,13 @@ class FixturesTest < ActiveRecord::TestCase
end
end
+ def test_yaml_file_with_invalid_column
+ e = assert_raise(ActiveRecord::Fixture::FixtureError) do
+ ActiveRecord::FixtureSet.create_fixtures(FIXTURES_ROOT + "/naked/yml", "parrots")
+ end
+ assert_equal(%(table "parrots" has no column named "arrr".), e.message)
+ end
+
def test_omap_fixtures
assert_nothing_raised do
fixtures = ActiveRecord::FixtureSet.new(Account.connection, 'categories', Category, FIXTURES_ROOT + "/categories_ordered")
@@ -249,18 +259,19 @@ class FixturesTest < ActiveRecord::TestCase
def test_fixtures_are_set_up_with_database_env_variable
db_url_tmp = ENV['DATABASE_URL']
ENV['DATABASE_URL'] = "sqlite3::memory:"
- ActiveRecord::Base.stubs(:configurations).returns({})
- test_case = Class.new(ActiveRecord::TestCase) do
- fixtures :accounts
+ ActiveRecord::Base.stub(:configurations, {}) do
+ test_case = Class.new(ActiveRecord::TestCase) do
+ fixtures :accounts
- def test_fixtures
- assert accounts(:signals37)
+ def test_fixtures
+ assert accounts(:signals37)
+ end
end
- end
- result = test_case.new(:test_fixtures).run
+ result = test_case.new(:test_fixtures).run
- assert result.passed?, "Expected #{result.name} to pass:\n#{result}"
+ assert result.passed?, "Expected #{result.name} to pass:\n#{result}"
+ end
ensure
ENV['DATABASE_URL'] = db_url_tmp
end
@@ -271,16 +282,16 @@ class HasManyThroughFixture < ActiveSupport::TestCase
Class.new(ActiveRecord::Base) { define_singleton_method(:name) { name } }
end
- def test_has_many_through
+ def test_has_many_through_with_default_table_name
pt = make_model "ParrotTreasure"
parrot = make_model "Parrot"
treasure = make_model "Treasure"
pt.table_name = "parrots_treasures"
- pt.belongs_to :parrot, :class => parrot
- pt.belongs_to :treasure, :class => treasure
+ pt.belongs_to :parrot, :anonymous_class => parrot
+ pt.belongs_to :treasure, :anonymous_class => treasure
- parrot.has_many :parrot_treasures, :class => pt
+ parrot.has_many :parrot_treasures, :anonymous_class => pt
parrot.has_many :treasures, :through => :parrot_treasures
parrots = File.join FIXTURES_ROOT, 'parrots'
@@ -290,6 +301,24 @@ class HasManyThroughFixture < ActiveSupport::TestCase
assert_equal load_has_and_belongs_to_many['parrots_treasures'], rows['parrots_treasures']
end
+ def test_has_many_through_with_renamed_table
+ pt = make_model "ParrotTreasure"
+ parrot = make_model "Parrot"
+ treasure = make_model "Treasure"
+
+ pt.belongs_to :parrot, :anonymous_class => parrot
+ pt.belongs_to :treasure, :anonymous_class => treasure
+
+ parrot.has_many :parrot_treasures, :anonymous_class => pt
+ parrot.has_many :treasures, :through => :parrot_treasures
+
+ parrots = File.join FIXTURES_ROOT, 'parrots'
+
+ fs = ActiveRecord::FixtureSet.new parrot.connection, "parrots", parrot, parrots
+ rows = fs.table_rows
+ assert_equal load_has_and_belongs_to_many['parrots_treasures'], rows['parrot_treasures']
+ end
+
def load_has_and_belongs_to_many
parrot = make_model "Parrot"
parrot.has_and_belongs_to_many :treasures
@@ -307,7 +336,7 @@ if Account.connection.respond_to?(:reset_pk_sequence!)
fixtures :companies
def setup
- @instances = [Account.new(:credit_limit => 50), Company.new(:name => 'RoR Consulting')]
+ @instances = [Account.new(:credit_limit => 50), Company.new(:name => 'RoR Consulting'), Course.new(name: 'Test')]
ActiveRecord::FixtureSet.reset_cache # make sure tables get reinitialized
end
@@ -380,9 +409,11 @@ class FixturesWithoutInstantiationTest < ActiveRecord::TestCase
end
def test_reloading_fixtures_through_accessor_methods
+ topic = Struct.new(:title)
assert_equal "The First Topic", topics(:first).title
- @loaded_fixtures['topics']['first'].expects(:find).returns(stub(:title => "Fresh Topic!"))
- assert_equal "Fresh Topic!", topics(:first, true).title
+ assert_called(@loaded_fixtures['topics']['first'], :find, returns: topic.new("Fresh Topic!")) do
+ assert_equal "Fresh Topic!", topics(:first, true).title
+ end
end
end
@@ -399,7 +430,7 @@ end
class TransactionalFixturesTest < ActiveRecord::TestCase
self.use_instantiated_fixtures = true
- self.use_transactional_fixtures = true
+ self.use_transactional_tests = true
fixtures :topics
@@ -486,12 +517,44 @@ class OverRideFixtureMethodTest < ActiveRecord::TestCase
end
end
+class FixtureWithSetModelClassTest < ActiveRecord::TestCase
+ fixtures :other_posts, :other_comments
+
+ # Set to false to blow away fixtures cache and ensure our fixtures are loaded
+ # and thus takes into account the +set_model_class+.
+ self.use_transactional_tests = false
+
+ def test_uses_fixture_class_defined_in_yaml
+ assert_kind_of Post, other_posts(:second_welcome)
+ end
+
+ def test_loads_the_associations_to_fixtures_with_set_model_class
+ post = other_posts(:second_welcome)
+ comment = other_comments(:second_greetings)
+ assert_equal [comment], post.comments
+ assert_equal post, comment.post
+ end
+end
+
+class SetFixtureClassPrevailsTest < ActiveRecord::TestCase
+ set_fixture_class bad_posts: Post
+ fixtures :bad_posts
+
+ # Set to false to blow away fixtures cache and ensure our fixtures are loaded
+ # and thus takes into account the +set_model_class+.
+ self.use_transactional_tests = false
+
+ def test_uses_set_fixture_class
+ assert_kind_of Post, bad_posts(:bad_welcome)
+ end
+end
+
class CheckSetTableNameFixturesTest < ActiveRecord::TestCase
set_fixture_class :funny_jokes => Joke
fixtures :funny_jokes
# Set to false to blow away fixtures cache and ensure our fixtures are loaded
# and thus takes into account our set_fixture_class
- self.use_transactional_fixtures = false
+ self.use_transactional_tests = false
def test_table_method
assert_kind_of Joke, funny_jokes(:a_joke)
@@ -503,7 +566,7 @@ class FixtureNameIsNotTableNameFixturesTest < ActiveRecord::TestCase
fixtures :items
# Set to false to blow away fixtures cache and ensure our fixtures are loaded
# and thus takes into account our set_fixture_class
- self.use_transactional_fixtures = false
+ self.use_transactional_tests = false
def test_named_accessor
assert_kind_of Book, items(:dvd)
@@ -515,7 +578,7 @@ class FixtureNameIsNotTableNameMultipleFixturesTest < ActiveRecord::TestCase
fixtures :items, :funny_jokes
# Set to false to blow away fixtures cache and ensure our fixtures are loaded
# and thus takes into account our set_fixture_class
- self.use_transactional_fixtures = false
+ self.use_transactional_tests = false
def test_named_accessor_of_differently_named_fixture
assert_kind_of Book, items(:dvd)
@@ -529,7 +592,7 @@ end
class CustomConnectionFixturesTest < ActiveRecord::TestCase
set_fixture_class :courses => Course
fixtures :courses
- self.use_transactional_fixtures = false
+ self.use_transactional_tests = false
def test_leaky_destroy
assert_nothing_raised { courses(:ruby) }
@@ -544,7 +607,7 @@ end
class TransactionalFixturesOnCustomConnectionTest < ActiveRecord::TestCase
set_fixture_class :courses => Course
fixtures :courses
- self.use_transactional_fixtures = true
+ self.use_transactional_tests = true
def test_leaky_destroy
assert_nothing_raised { courses(:ruby) }
@@ -560,7 +623,7 @@ class InvalidTableNameFixturesTest < ActiveRecord::TestCase
fixtures :funny_jokes
# Set to false to blow away fixtures cache and ensure our fixtures are loaded
# and thus takes into account our lack of set_fixture_class
- self.use_transactional_fixtures = false
+ self.use_transactional_tests = false
def test_raises_error
assert_raise ActiveRecord::FixtureClassNotFound do
@@ -574,7 +637,7 @@ class CheckEscapedYamlFixturesTest < ActiveRecord::TestCase
fixtures :funny_jokes
# Set to false to blow away fixtures cache and ensure our fixtures are loaded
# and thus takes into account our set_fixture_class
- self.use_transactional_fixtures = false
+ self.use_transactional_tests = false
def test_proper_escaped_fixture
assert_equal "The \\n Aristocrats\nAte the candy\n", funny_jokes(:another_joke).name
@@ -644,6 +707,7 @@ class LoadAllFixturesWithPathnameTest < ActiveRecord::TestCase
end
class FasterFixturesTest < ActiveRecord::TestCase
+ self.use_transactional_tests = false
fixtures :categories, :authors
def load_extra_fixture(name)
@@ -669,12 +733,13 @@ class FasterFixturesTest < ActiveRecord::TestCase
end
class FoxyFixturesTest < ActiveRecord::TestCase
- fixtures :parrots, :parrots_pirates, :pirates, :treasures, :mateys, :ships, :computers, :developers, :"admin/accounts", :"admin/users"
+ fixtures :parrots, :parrots_pirates, :pirates, :treasures, :mateys, :ships, :computers,
+ :developers, :"admin/accounts", :"admin/users", :live_parrots, :dead_parrots, :books
if ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'
require 'models/uuid_parent'
require 'models/uuid_child'
- fixtures :uuid_parents, :uuid_children
+ fixtures :uuid_parents, :uuid_children
end
def test_identifies_strings
@@ -789,6 +854,10 @@ class FoxyFixturesTest < ActiveRecord::TestCase
assert_equal("X marks the spot!", pirates(:mark).catchphrase)
end
+ def test_supports_label_interpolation_for_fixnum_label
+ assert_equal("#1 pirate!", pirates(1).catchphrase)
+ end
+
def test_supports_polymorphic_belongs_to
assert_equal(pirates(:redbeard), treasures(:sapphire).looter)
assert_equal(parrots(:louis), treasures(:ruby).looter)
@@ -805,10 +874,23 @@ class FoxyFixturesTest < ActiveRecord::TestCase
assert_equal pirates(:blackbeard), parrots(:polly).killer
end
+ def test_supports_sti_with_respective_files
+ assert_kind_of LiveParrot, live_parrots(:dusty)
+ assert_kind_of DeadParrot, dead_parrots(:deadbird)
+ assert_equal pirates(:blackbeard), dead_parrots(:deadbird).killer
+ end
+
def test_namespaced_models
assert admin_accounts(:signals37).users.include?(admin_users(:david))
assert_equal 2, admin_accounts(:signals37).users.size
end
+
+ def test_resolves_enums
+ assert books(:awdr).published?
+ assert books(:awdr).read?
+ assert books(:rfr).proposed?
+ assert books(:ddd).published?
+ end
end
class ActiveSupportSubclassWithFixturesTest < ActiveRecord::TestCase
@@ -821,29 +903,15 @@ class ActiveSupportSubclassWithFixturesTest < ActiveRecord::TestCase
end
end
-class FixtureLoadingTest < ActiveRecord::TestCase
- def test_logs_message_for_failed_dependency_load
- ActiveRecord::TestCase.expects(:require_dependency).with(:does_not_exist).raises(LoadError)
- ActiveRecord::Base.logger.expects(:warn)
- ActiveRecord::TestCase.try_to_load_dependency(:does_not_exist)
- end
-
- def test_does_not_logs_message_for_successful_dependency_load
- ActiveRecord::TestCase.expects(:require_dependency).with(:works_out_fine)
- ActiveRecord::Base.logger.expects(:warn).never
- ActiveRecord::TestCase.try_to_load_dependency(:works_out_fine)
- end
-end
-
class CustomNameForFixtureOrModelTest < ActiveRecord::TestCase
ActiveRecord::FixtureSet.reset_cache
set_fixture_class :randomly_named_a9 =>
ClassNameThatDoesNotFollowCONVENTIONS,
:'admin/randomly_named_a9' =>
- Admin::ClassNameThatDoesNotFollowCONVENTIONS,
+ Admin::ClassNameThatDoesNotFollowCONVENTIONS1,
'admin/randomly_named_b0' =>
- Admin::ClassNameThatDoesNotFollowCONVENTIONS
+ Admin::ClassNameThatDoesNotFollowCONVENTIONS2
fixtures :randomly_named_a9, 'admin/randomly_named_a9',
:'admin/randomly_named_b0'
@@ -854,14 +922,36 @@ class CustomNameForFixtureOrModelTest < ActiveRecord::TestCase
end
def test_named_accessor_for_randomly_named_namespaced_fixture_and_class
- assert_kind_of Admin::ClassNameThatDoesNotFollowCONVENTIONS,
+ assert_kind_of Admin::ClassNameThatDoesNotFollowCONVENTIONS1,
admin_randomly_named_a9(:first_instance)
- assert_kind_of Admin::ClassNameThatDoesNotFollowCONVENTIONS,
+ assert_kind_of Admin::ClassNameThatDoesNotFollowCONVENTIONS2,
admin_randomly_named_b0(:second_instance)
end
def test_table_name_is_defined_in_the_model
- assert_equal 'randomly_named_table', ActiveRecord::FixtureSet::all_loaded_fixtures["admin/randomly_named_a9"].table_name
- assert_equal 'randomly_named_table', Admin::ClassNameThatDoesNotFollowCONVENTIONS.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
+
+class FixturesWithDefaultScopeTest < ActiveRecord::TestCase
+ fixtures :bulbs
+
+ test "inserts fixtures excluded by a default scope" do
+ assert_equal 1, Bulb.count
+ assert_equal 2, Bulb.unscoped.count
+ end
+
+ test "allows access to fixtures excluded by a default scope" do
+ assert_equal "special", bulbs(:special).name
+ end
+end
+
+class FixturesWithAbstractBelongsTo < ActiveRecord::TestCase
+ fixtures :pirates, :doubloons
+
+ test "creates fixtures with belongs_to associations defined in abstract base classes" do
+ assert_not_nil doubloons(:blackbeards_doubloon)
+ assert_equal pirates(:blackbeard), doubloons(:blackbeards_doubloon).pirate
end
end
diff --git a/activerecord/test/cases/forbidden_attributes_protection_test.rb b/activerecord/test/cases/forbidden_attributes_protection_test.rb
index 981a75faf6..f4e7646f03 100644
--- a/activerecord/test/cases/forbidden_attributes_protection_test.rb
+++ b/activerecord/test/cases/forbidden_attributes_protection_test.rb
@@ -66,4 +66,34 @@ class ForbiddenAttributesProtectionTest < ActiveRecord::TestCase
person = Person.new
assert_nil person.assign_attributes(ProtectedParams.new({}))
end
+
+ def test_create_with_checks_permitted
+ params = ProtectedParams.new(first_name: 'Guille', gender: 'm')
+
+ assert_raises(ActiveModel::ForbiddenAttributesError) do
+ Person.create_with(params).create!
+ end
+ end
+
+ def test_create_with_works_with_params_values
+ params = ProtectedParams.new(first_name: 'Guille')
+
+ person = Person.create_with(first_name: params[:first_name]).create!
+ assert_equal 'Guille', person.first_name
+ end
+
+ def test_where_checks_permitted
+ params = ProtectedParams.new(first_name: 'Guille', gender: 'm')
+
+ assert_raises(ActiveModel::ForbiddenAttributesError) do
+ Person.where(params).create!
+ end
+ end
+
+ def test_where_works_with_params_values
+ params = ProtectedParams.new(first_name: 'Guille')
+
+ person = Person.where(first_name: params[:first_name]).create!
+ assert_equal 'Guille', person.first_name
+ end
end
diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb
index 6a8aff4b69..d82a3040fc 100644
--- a/activerecord/test/cases/helper.rb
+++ b/activerecord/test/cases/helper.rb
@@ -3,6 +3,7 @@ require File.expand_path('../../../../load_paths', __FILE__)
require 'config'
require 'active_support/testing/autorun'
+require 'active_support/testing/method_call_assertions'
require 'stringio'
require 'active_record'
@@ -30,6 +31,9 @@ ARTest.connect
# Quote "type" if it's a reserved word for the current connection.
QUOTED_TYPE = ActiveRecord::Base.connection.quote_column_name('type')
+# FIXME: Remove this when the deprecation cycle on TZ aware types by default ends.
+ActiveRecord::Base.time_zone_aware_types << :time
+
def current_adapter?(*types)
types.any? do |type|
ActiveRecord::ConnectionAdapters.const_defined?(type) &&
@@ -42,9 +46,12 @@ def in_memory_db?
ActiveRecord::Base.connection_pool.spec.config[:database] == ":memory:"
end
-def mysql_56?
- current_adapter?(:Mysql2Adapter) &&
- ActiveRecord::Base.connection.send(:version).join(".") >= "5.6.0"
+def subsecond_precision_supported?
+ !current_adapter?(:MysqlAdapter, :Mysql2Adapter) || ActiveRecord::Base.connection.version >= '5.6.4'
+end
+
+def mysql_enforcing_gtid_consistency?
+ current_adapter?(:MysqlAdapter, :Mysql2Adapter) && 'ON' == ActiveRecord::Base.connection.show_variable('enforce_gtid_consistency')
end
def supports_savepoints?
@@ -88,7 +95,7 @@ EXPECTED_TIME_ZONE_AWARE_ATTRIBUTES = false
def verify_default_timezone_config
if Time.zone != EXPECTED_ZONE
$stderr.puts <<-MSG
-\n#{self.to_s}
+\n#{self}
Global state `Time.zone` was leaked.
Expected: #{EXPECTED_ZONE}
Got: #{Time.zone}
@@ -96,7 +103,7 @@ def verify_default_timezone_config
end
if ActiveRecord::Base.default_timezone != EXPECTED_DEFAULT_TIMEZONE
$stderr.puts <<-MSG
-\n#{self.to_s}
+\n#{self}
Global state `ActiveRecord::Base.default_timezone` was leaked.
Expected: #{EXPECTED_DEFAULT_TIMEZONE}
Got: #{ActiveRecord::Base.default_timezone}
@@ -104,7 +111,7 @@ def verify_default_timezone_config
end
if ActiveRecord::Base.time_zone_aware_attributes != EXPECTED_TIME_ZONE_AWARE_ATTRIBUTES
$stderr.puts <<-MSG
-\n#{self.to_s}
+\n#{self}
Global state `ActiveRecord::Base.time_zone_aware_attributes` was leaked.
Expected: #{EXPECTED_TIME_ZONE_AWARE_ATTRIBUTES}
Got: #{ActiveRecord::Base.time_zone_aware_attributes}
@@ -112,36 +119,32 @@ def verify_default_timezone_config
end
end
-def enable_uuid_ossp!(connection)
+def enable_extension!(extension, connection)
return false unless connection.supports_extensions?
- return true if connection.extension_enabled?('uuid-ossp')
+ return connection.reconnect! if connection.extension_enabled?(extension)
- connection.enable_extension 'uuid-ossp'
- connection.commit_db_transaction
+ connection.enable_extension extension
+ connection.commit_db_transaction if connection.transaction_open?
connection.reconnect!
end
-unless ENV['FIXTURE_DEBUG']
- module ActiveRecord::TestFixtures::ClassMethods
- def try_to_load_dependency_with_silence(*args)
- old = ActiveRecord::Base.logger.level
- ActiveRecord::Base.logger.level = ActiveSupport::Logger::ERROR
- try_to_load_dependency_without_silence(*args)
- ActiveRecord::Base.logger.level = old
- end
+def disable_extension!(extension, connection)
+ return false unless connection.supports_extensions?
+ return true unless connection.extension_enabled?(extension)
- alias_method_chain :try_to_load_dependency, :silence
- end
+ connection.disable_extension extension
+ connection.reconnect!
end
require "cases/validations_repair_helper"
class ActiveSupport::TestCase
include ActiveRecord::TestFixtures
include ActiveRecord::ValidationsRepairHelper
+ include ActiveSupport::Testing::MethodCallAssertions
self.fixture_path = FIXTURES_ROOT
self.use_instantiated_fixtures = false
- self.use_transactional_fixtures = true
+ self.use_transactional_tests = true
def create_fixtures(*fixture_set_names, &block)
ActiveRecord::FixtureSet.create_fixtures(ActiveSupport::TestCase.fixture_path, fixture_set_names, fixture_class_names, &block)
diff --git a/activerecord/test/cases/hot_compatibility_test.rb b/activerecord/test/cases/hot_compatibility_test.rb
index b4617cf6f9..5ba9a1029a 100644
--- a/activerecord/test/cases/hot_compatibility_test.rb
+++ b/activerecord/test/cases/hot_compatibility_test.rb
@@ -1,7 +1,7 @@
require 'cases/helper'
class HotCompatibilityTest < ActiveRecord::TestCase
- self.use_transactional_fixtures = false
+ self.use_transactional_tests = false
setup do
@klass = Class.new(ActiveRecord::Base) do
diff --git a/activerecord/test/cases/inheritance_test.rb b/activerecord/test/cases/inheritance_test.rb
index 792950d24d..f67d85603a 100644
--- a/activerecord/test/cases/inheritance_test.rb
+++ b/activerecord/test/cases/inheritance_test.rb
@@ -7,22 +7,39 @@ require 'models/subscriber'
require 'models/vegetables'
require 'models/shop'
+module InheritanceTestHelper
+ def with_store_full_sti_class(&block)
+ assign_store_full_sti_class true, &block
+ end
+
+ def without_store_full_sti_class(&block)
+ assign_store_full_sti_class false, &block
+ end
+
+ def assign_store_full_sti_class(flag)
+ old_store_full_sti_class = ActiveRecord::Base.store_full_sti_class
+ ActiveRecord::Base.store_full_sti_class = flag
+ yield
+ ensure
+ ActiveRecord::Base.store_full_sti_class = old_store_full_sti_class
+ end
+end
+
class InheritanceTest < ActiveRecord::TestCase
+ include InheritanceTestHelper
fixtures :companies, :projects, :subscribers, :accounts, :vegetables
def test_class_with_store_full_sti_class_returns_full_name
- old = ActiveRecord::Base.store_full_sti_class
- ActiveRecord::Base.store_full_sti_class = true
- assert_equal 'Namespaced::Company', Namespaced::Company.sti_name
- ensure
- ActiveRecord::Base.store_full_sti_class = old
+ with_store_full_sti_class do
+ assert_equal 'Namespaced::Company', Namespaced::Company.sti_name
+ end
end
def test_class_with_blank_sti_name
company = Company.first
company = company.dup
company.extend(Module.new {
- def read_attribute(name)
+ def _read_attribute(name)
return ' ' if name == 'type'
super
end
@@ -33,39 +50,31 @@ class InheritanceTest < ActiveRecord::TestCase
end
def test_class_without_store_full_sti_class_returns_demodulized_name
- old = ActiveRecord::Base.store_full_sti_class
- ActiveRecord::Base.store_full_sti_class = false
- assert_equal 'Company', Namespaced::Company.sti_name
- ensure
- ActiveRecord::Base.store_full_sti_class = old
+ without_store_full_sti_class do
+ assert_equal 'Company', Namespaced::Company.sti_name
+ end
end
def test_should_store_demodulized_class_name_with_store_full_sti_class_option_disabled
- old = ActiveRecord::Base.store_full_sti_class
- ActiveRecord::Base.store_full_sti_class = false
- item = Namespaced::Company.new
- assert_equal 'Company', item[:type]
- ensure
- ActiveRecord::Base.store_full_sti_class = old
+ without_store_full_sti_class do
+ item = Namespaced::Company.new
+ assert_equal 'Company', item[:type]
+ end
end
def test_should_store_full_class_name_with_store_full_sti_class_option_enabled
- old = ActiveRecord::Base.store_full_sti_class
- ActiveRecord::Base.store_full_sti_class = true
- item = Namespaced::Company.new
- assert_equal 'Namespaced::Company', item[:type]
- ensure
- ActiveRecord::Base.store_full_sti_class = old
+ with_store_full_sti_class do
+ item = Namespaced::Company.new
+ assert_equal 'Namespaced::Company', item[:type]
+ end
end
def test_different_namespace_subclass_should_load_correctly_with_store_full_sti_class_option
- old = ActiveRecord::Base.store_full_sti_class
- ActiveRecord::Base.store_full_sti_class = true
- item = Namespaced::Company.create :name => "Wolverine 2"
- assert_not_nil Company.find(item.id)
- assert_not_nil Namespaced::Company.find(item.id)
- ensure
- ActiveRecord::Base.store_full_sti_class = old
+ with_store_full_sti_class do
+ item = Namespaced::Company.create name: "Wolverine 2"
+ assert_not_nil Company.find(item.id)
+ assert_not_nil Namespaced::Company.find(item.id)
+ end
end
def test_company_descends_from_active_record
@@ -121,6 +130,12 @@ class InheritanceTest < ActiveRecord::TestCase
assert_kind_of Cabbage, cabbage
end
+ def test_becomes_and_change_tracking_for_inheritance_columns
+ cucumber = Vegetable.find(1)
+ cabbage = cucumber.becomes!(Cabbage)
+ assert_equal ['Cucumber', 'Cabbage'], cabbage.custom_type_change
+ end
+
def test_alt_becomes_bang_resets_inheritance_type_column
vegetable = Vegetable.create!(name: "Red Pepper")
assert_nil vegetable.custom_type
@@ -198,10 +213,28 @@ class InheritanceTest < ActiveRecord::TestCase
assert_raise(ActiveRecord::SubclassNotFound) { Company.new(:type => 'Account') }
end
+ def test_new_with_unrelated_namespaced_type
+ without_store_full_sti_class do
+ e = assert_raises ActiveRecord::SubclassNotFound do
+ Namespaced::Company.new(type: 'Firm')
+ end
+
+ assert_equal "Invalid single-table inheritance type: Namespaced::Firm is not a subclass of Namespaced::Company", e.message
+ end
+ end
+
+
def test_new_with_complex_inheritance
assert_nothing_raised { Client.new(type: 'VerySpecialClient') }
end
+ def test_new_without_storing_full_sti_class
+ without_store_full_sti_class do
+ item = Company.new(type: 'SpecialCo')
+ assert_instance_of Company::SpecialCo, item
+ end
+ end
+
def test_new_with_autoload_paths
path = File.expand_path('../../models/autoloadable', __FILE__)
ActiveSupport::Dependencies.autoload_paths << path
@@ -294,17 +327,17 @@ class InheritanceTest < ActiveRecord::TestCase
def test_eager_load_belongs_to_something_inherited
account = Account.all.merge!(:includes => :firm).find(1)
- assert account.association_cache.key?(:firm), "nil proves eager load failed"
+ assert account.association(:firm).loaded?, "association was not eager loaded"
end
def test_alt_eager_loading
cabbage = RedCabbage.all.merge!(:includes => :seller).find(4)
- assert cabbage.association_cache.key?(:seller), "nil proves eager load failed"
+ assert cabbage.association(:seller).loaded?, "association was not eager loaded"
end
def test_eager_load_belongs_to_primary_key_quoting
con = Account.connection
- assert_sql(/#{con.quote_table_name('companies')}.#{con.quote_column_name('id')} IN \(1\)/) do
+ assert_sql(/#{con.quote_table_name('companies')}.#{con.quote_column_name('id')} = 1/) do
Account.all.merge!(:includes => :firm).find(1)
end
end
@@ -325,6 +358,7 @@ class InheritanceTest < ActiveRecord::TestCase
end
class InheritanceComputeTypeTest < ActiveRecord::TestCase
+ include InheritanceTestHelper
fixtures :companies
def setup
@@ -338,27 +372,26 @@ class InheritanceComputeTypeTest < ActiveRecord::TestCase
end
def test_instantiation_doesnt_try_to_require_corresponding_file
- ActiveRecord::Base.store_full_sti_class = false
- foo = Firm.first.clone
- foo.type = 'FirmOnTheFly'
- foo.save!
+ without_store_full_sti_class do
+ foo = Firm.first.clone
+ foo.type = 'FirmOnTheFly'
+ foo.save!
- # Should fail without FirmOnTheFly in the type condition.
- assert_raise(ActiveRecord::RecordNotFound) { Firm.find(foo.id) }
+ # Should fail without FirmOnTheFly in the type condition.
+ assert_raise(ActiveRecord::RecordNotFound) { Firm.find(foo.id) }
- # Nest FirmOnTheFly in the test case where Dependencies won't see it.
- self.class.const_set :FirmOnTheFly, Class.new(Firm)
- assert_raise(ActiveRecord::SubclassNotFound) { Firm.find(foo.id) }
+ # Nest FirmOnTheFly in the test case where Dependencies won't see it.
+ self.class.const_set :FirmOnTheFly, Class.new(Firm)
+ assert_raise(ActiveRecord::SubclassNotFound) { Firm.find(foo.id) }
- # Nest FirmOnTheFly in Firm where Dependencies will see it.
- # This is analogous to nesting models in a migration.
- Firm.const_set :FirmOnTheFly, Class.new(Firm)
+ # Nest FirmOnTheFly in Firm where Dependencies will see it.
+ # This is analogous to nesting models in a migration.
+ Firm.const_set :FirmOnTheFly, Class.new(Firm)
- # And instantiate will find the existing constant rather than trying
- # to require firm_on_the_fly.
- assert_nothing_raised { assert_kind_of Firm::FirmOnTheFly, Firm.find(foo.id) }
- ensure
- ActiveRecord::Base.store_full_sti_class = true
+ # And instantiate will find the existing constant rather than trying
+ # to require firm_on_the_fly.
+ assert_nothing_raised { assert_kind_of Firm::FirmOnTheFly, Firm.find(foo.id) }
+ end
end
def test_sti_type_from_attributes_disabled_in_non_sti_class
diff --git a/activerecord/test/cases/integration_test.rb b/activerecord/test/cases/integration_test.rb
index dfb8a608cb..08a186ae07 100644
--- a/activerecord/test/cases/integration_test.rb
+++ b/activerecord/test/cases/integration_test.rb
@@ -1,8 +1,8 @@
-# encoding: utf-8
require 'cases/helper'
require 'models/company'
require 'models/developer'
+require 'models/computer'
require 'models/owner'
require 'models/pet'
@@ -81,7 +81,7 @@ class IntegrationTest < ActiveRecord::TestCase
def test_cache_key_format_for_existing_record_with_updated_at
dev = Developer.first
- assert_equal "developers/#{dev.id}-#{dev.updated_at.utc.to_s(:nsec)}", dev.cache_key
+ assert_equal "developers/#{dev.id}-#{dev.updated_at.utc.to_s(:usec)}", dev.cache_key
end
def test_cache_key_format_for_existing_record_with_updated_at_and_custom_cache_timestamp_format
@@ -96,7 +96,9 @@ class IntegrationTest < ActiveRecord::TestCase
owner.update_column :updated_at, Time.current
key = owner.cache_key
- assert pet.touch
+ travel(1.second) do
+ assert pet.touch
+ end
assert_not_equal key, owner.reload.cache_key
end
@@ -109,30 +111,39 @@ class IntegrationTest < ActiveRecord::TestCase
def test_cache_key_for_updated_on
dev = Developer.first
dev.updated_at = nil
- assert_equal "developers/#{dev.id}-#{dev.updated_on.utc.to_s(:nsec)}", dev.cache_key
+ assert_equal "developers/#{dev.id}-#{dev.updated_on.utc.to_s(:usec)}", dev.cache_key
end
def test_cache_key_for_newer_updated_at
dev = Developer.first
dev.updated_at += 3600
- assert_equal "developers/#{dev.id}-#{dev.updated_at.utc.to_s(:nsec)}", dev.cache_key
+ assert_equal "developers/#{dev.id}-#{dev.updated_at.utc.to_s(:usec)}", dev.cache_key
end
def test_cache_key_for_newer_updated_on
dev = Developer.first
dev.updated_on += 3600
- assert_equal "developers/#{dev.id}-#{dev.updated_on.utc.to_s(:nsec)}", dev.cache_key
+ assert_equal "developers/#{dev.id}-#{dev.updated_on.utc.to_s(:usec)}", dev.cache_key
end
def test_cache_key_format_is_precise_enough
+ skip("Subsecond precision is not supported") unless subsecond_precision_supported?
dev = Developer.first
key = dev.cache_key
dev.touch
assert_not_equal key, dev.cache_key
end
+ def test_cache_key_format_is_not_too_precise
+ skip("Subsecond precision is not supported") unless subsecond_precision_supported?
+ dev = Developer.first
+ dev.touch
+ key = dev.cache_key
+ assert_equal key, dev.reload.cache_key
+ end
+
def test_named_timestamps_for_cache_key
owner = owners(:blackbeard)
- assert_equal "owners/#{owner.id}-#{owner.happy_at.utc.to_s(:nsec)}", owner.cache_key(:updated_at, :happy_at)
+ assert_equal "owners/#{owner.id}-#{owner.happy_at.utc.to_s(:usec)}", owner.cache_key(:updated_at, :happy_at)
end
end
diff --git a/activerecord/test/cases/invalid_connection_test.rb b/activerecord/test/cases/invalid_connection_test.rb
index 8416c81f45..6523fc29fd 100644
--- a/activerecord/test/cases/invalid_connection_test.rb
+++ b/activerecord/test/cases/invalid_connection_test.rb
@@ -1,7 +1,7 @@
require "cases/helper"
class TestAdapterWithInvalidConnection < ActiveRecord::TestCase
- self.use_transactional_fixtures = false
+ self.use_transactional_tests = false
class Bird < ActiveRecord::Base
end
diff --git a/activerecord/test/cases/invertible_migration_test.rb b/activerecord/test/cases/invertible_migration_test.rb
index 285172d33e..84b0ff8fcb 100644
--- a/activerecord/test/cases/invertible_migration_test.rb
+++ b/activerecord/test/cases/invertible_migration_test.rb
@@ -1,5 +1,8 @@
require "cases/helper"
+class Horse < ActiveRecord::Base
+end
+
module ActiveRecord
class InvertibleMigrationTest < ActiveRecord::TestCase
class SilentMigration < ActiveRecord::Migration
@@ -76,6 +79,32 @@ module ActiveRecord
end
end
+ class ChangeColumnDefault1 < SilentMigration
+ def change
+ create_table("horses") do |t|
+ t.column :name, :string, default: "Sekitoba"
+ end
+ end
+ end
+
+ class ChangeColumnDefault2 < SilentMigration
+ def change
+ change_column_default :horses, :name, from: "Sekitoba", to: "Diomed"
+ end
+ end
+
+ class DisableExtension1 < SilentMigration
+ def change
+ enable_extension "hstore"
+ end
+ end
+
+ class DisableExtension2 < SilentMigration
+ def change
+ disable_extension "hstore"
+ end
+ end
+
class LegacyMigration < ActiveRecord::Migration
def self.up
create_table("horses") do |t|
@@ -122,12 +151,17 @@ module ActiveRecord
end
end
+ setup do
+ @verbose_was, ActiveRecord::Migration.verbose = ActiveRecord::Migration.verbose, false
+ end
+
teardown do
%w[horses new_horses].each do |table|
if ActiveRecord::Base.connection.table_exists?(table)
ActiveRecord::Base.connection.drop_table(table)
end
end
+ ActiveRecord::Migration.verbose = @verbose_was
end
def test_no_reverse
@@ -139,13 +173,17 @@ module ActiveRecord
end
def test_exception_on_removing_index_without_column_option
- RemoveIndexMigration1.new.migrate(:up)
- migration = RemoveIndexMigration2.new
- migration.migrate(:up)
+ index_definition = ["horses", [:name, :color]]
+ migration1 = RemoveIndexMigration1.new
+ migration1.migrate(:up)
+ assert migration1.connection.index_exists?(*index_definition)
- assert_raises(IrreversibleMigration) do
- migration.migrate(:down)
- end
+ migration2 = RemoveIndexMigration2.new
+ migration2.migrate(:up)
+ assert_not migration2.connection.index_exists?(*index_definition)
+
+ migration2.migrate(:down)
+ assert migration2.connection.index_exists?(*index_definition)
end
def test_migrate_up
@@ -214,6 +252,42 @@ module ActiveRecord
assert !revert.connection.table_exists?("horses")
end
+ def test_migrate_revert_change_column_default
+ migration1 = ChangeColumnDefault1.new
+ migration1.migrate(:up)
+ assert_equal "Sekitoba", Horse.new.name
+
+ migration2 = ChangeColumnDefault2.new
+ migration2.migrate(:up)
+ Horse.reset_column_information
+ assert_equal "Diomed", Horse.new.name
+
+ migration2.migrate(:down)
+ Horse.reset_column_information
+ assert_equal "Sekitoba", Horse.new.name
+ end
+
+ if current_adapter?(:PostgreSQLAdapter)
+ def test_migrate_enable_and_disable_extension
+ migration1 = InvertibleMigration.new
+ migration2 = DisableExtension1.new
+ migration3 = DisableExtension2.new
+
+ migration1.migrate(:up)
+ migration2.migrate(:up)
+ assert_equal true, Horse.connection.extension_enabled?('hstore')
+
+ migration3.migrate(:up)
+ assert_equal false, Horse.connection.extension_enabled?('hstore')
+
+ migration3.migrate(:down)
+ assert_equal true, Horse.connection.extension_enabled?('hstore')
+
+ migration2.migrate(:down)
+ assert_equal false, Horse.connection.extension_enabled?('hstore')
+ end
+ end
+
def test_revert_order
block = Proc.new{|t| t.string :name }
recorder = ActiveRecord::Migration::CommandRecorder.new(ActiveRecord::Base.connection)
diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb
index 0c9dff2c25..2e1363334d 100644
--- a/activerecord/test/cases/locking_test.rb
+++ b/activerecord/test/cases/locking_test.rb
@@ -5,6 +5,7 @@ require 'models/job'
require 'models/reader'
require 'models/ship'
require 'models/legacy_thing'
+require 'models/personal_legacy_thing'
require 'models/reference'
require 'models/string_key_object'
require 'models/car'
@@ -32,8 +33,6 @@ class OptimisticLockingTest < ActiveRecord::TestCase
p1 = Person.find(1)
assert_equal 0, p1.lock_version
- Person.expects(:quote_value).with(0, Person.columns_hash[Person.locking_column]).returns('0').once
-
p1.first_name = 'anika2'
p1.save!
@@ -178,6 +177,16 @@ class OptimisticLockingTest < ActiveRecord::TestCase
assert_equal 1, p1.lock_version
end
+ def test_touch_stale_object
+ person = Person.create!(first_name: 'Mehmet Emin')
+ stale_person = Person.find(person.id)
+ person.update_attribute(:gender, 'M')
+
+ assert_raises(ActiveRecord::StaleObjectError) do
+ stale_person.touch
+ end
+ end
+
def test_lock_column_name_existing
t1 = LegacyThing.find(1)
t2 = LegacyThing.find(1)
@@ -216,10 +225,12 @@ class OptimisticLockingTest < ActiveRecord::TestCase
def test_lock_with_custom_column_without_default_sets_version_to_zero
t1 = LockWithCustomColumnWithoutDefault.new
assert_equal 0, t1.custom_lock_version
+ assert_nil t1.custom_lock_version_before_type_cast
- t1.save
- t1 = LockWithCustomColumnWithoutDefault.find(t1.id)
+ t1.save!
+ t1.reload
assert_equal 0, t1.custom_lock_version
+ assert [0, "0"].include?(t1.custom_lock_version_before_type_cast)
end
def test_readonly_attributes
@@ -259,7 +270,7 @@ class OptimisticLockingTest < ActiveRecord::TestCase
car.wheels << Wheel.create!
end
assert_difference 'car.wheels.count', -1 do
- car.destroy
+ car.reload.destroy
end
assert car.destroyed?
end
@@ -284,10 +295,10 @@ end
class OptimisticLockingWithSchemaChangeTest < ActiveRecord::TestCase
fixtures :people, :legacy_things, :references
- # need to disable transactional fixtures, because otherwise the sqlite3
+ # need to disable transactional tests, because otherwise the sqlite3
# adapter (at least) chokes when we try and change the schema in the middle
# of a test (see test_increment_counter_*).
- self.use_transactional_fixtures = false
+ self.use_transactional_tests = false
{ :lock_version => Person, :custom_lock_version => LegacyThing }.each do |name, model|
define_method("test_increment_counter_updates_#{name}") do
@@ -311,30 +322,24 @@ class OptimisticLockingWithSchemaChangeTest < ActiveRecord::TestCase
# See Lighthouse ticket #1966
def test_destroy_dependents
- # Establish dependent relationship between People and LegacyThing
- add_counter_column_to(Person, 'legacy_things_count')
- LegacyThing.connection.add_column LegacyThing.table_name, 'person_id', :integer
- LegacyThing.reset_column_information
- LegacyThing.class_eval do
- belongs_to :person, :counter_cache => true
- end
- Person.class_eval do
- has_many :legacy_things, :dependent => :destroy
- end
+ # Establish dependent relationship between Person and PersonalLegacyThing
+ add_counter_column_to(Person, 'personal_legacy_things_count')
+ PersonalLegacyThing.reset_column_information
# Make sure that counter incrementing doesn't cause problems
p1 = Person.new(:first_name => 'fjord')
p1.save!
- t = LegacyThing.new(:person => p1)
+ t = PersonalLegacyThing.new(:person => p1)
t.save!
p1.reload
- assert_equal 1, p1.legacy_things_count
+ assert_equal 1, p1.personal_legacy_things_count
assert p1.destroy
assert_equal true, p1.frozen?
assert_raises(ActiveRecord::RecordNotFound) { Person.find(p1.id) }
- assert_raises(ActiveRecord::RecordNotFound) { LegacyThing.find(t.id) }
+ assert_raises(ActiveRecord::RecordNotFound) { PersonalLegacyThing.find(t.id) }
ensure
- remove_counter_column_from(Person, 'legacy_things_count')
+ remove_counter_column_from(Person, 'personal_legacy_things_count')
+ PersonalLegacyThing.reset_column_information
end
private
@@ -370,7 +375,7 @@ end
# (See exec vs. async_exec in the PostgreSQL adapter.)
unless in_memory_db?
class PessimisticLockingTest < ActiveRecord::TestCase
- self.use_transactional_fixtures = false
+ self.use_transactional_tests = false
fixtures :people, :readers
def setup
diff --git a/activerecord/test/cases/log_subscriber_test.rb b/activerecord/test/cases/log_subscriber_test.rb
index a578e81844..3846ba8e7f 100644
--- a/activerecord/test/cases/log_subscriber_test.rb
+++ b/activerecord/test/cases/log_subscriber_test.rb
@@ -7,6 +7,20 @@ require "active_support/log_subscriber/test_helper"
class LogSubscriberTest < ActiveRecord::TestCase
include ActiveSupport::LogSubscriber::TestHelper
include ActiveSupport::Logger::Severity
+ REGEXP_CLEAR = Regexp.escape(ActiveRecord::LogSubscriber::CLEAR)
+ REGEXP_BOLD = Regexp.escape(ActiveRecord::LogSubscriber::BOLD)
+ REGEXP_MAGENTA = Regexp.escape(ActiveRecord::LogSubscriber::MAGENTA)
+ REGEXP_CYAN = Regexp.escape(ActiveRecord::LogSubscriber::CYAN)
+ SQL_COLORINGS = {
+ SELECT: Regexp.escape(ActiveRecord::LogSubscriber::BLUE),
+ INSERT: Regexp.escape(ActiveRecord::LogSubscriber::GREEN),
+ UPDATE: Regexp.escape(ActiveRecord::LogSubscriber::YELLOW),
+ DELETE: Regexp.escape(ActiveRecord::LogSubscriber::RED),
+ LOCK: Regexp.escape(ActiveRecord::LogSubscriber::WHITE),
+ ROLLBACK: Regexp.escape(ActiveRecord::LogSubscriber::RED),
+ TRANSACTION: REGEXP_CYAN,
+ OTHER: REGEXP_MAGENTA
+ }
class TestDebugLogSubscriber < ActiveRecord::LogSubscriber
attr_reader :debugs
@@ -63,14 +77,6 @@ class LogSubscriberTest < ActiveRecord::TestCase
assert_match(/ruby rails/, logger.debugs.first)
end
- def test_ignore_binds_payload_with_nil_column
- event = Struct.new(:duration, :payload)
-
- logger = TestDebugLogSubscriber.new
- logger.sql(event.new(0, sql: 'hi mom!', binds: [[nil, 1]]))
- assert_equal 1, logger.debugs.length
- end
-
def test_basic_query_logging
Developer.all.load
wait
@@ -79,6 +85,90 @@ class LogSubscriberTest < ActiveRecord::TestCase
assert_match(/SELECT .*?FROM .?developers.?/i, @logger.logged(:debug).last)
end
+ def test_basic_query_logging_coloration
+ event = Struct.new(:duration, :payload)
+ logger = TestDebugLogSubscriber.new
+ logger.colorize_logging = true
+ SQL_COLORINGS.each do |verb, color_regex|
+ logger.sql(event.new(0, sql: verb.to_s))
+ assert_match(/#{REGEXP_BOLD}#{color_regex}#{verb}#{REGEXP_CLEAR}/i, logger.debugs.last)
+ end
+ end
+
+ def test_basic_payload_name_logging_coloration_generic_sql
+ event = Struct.new(:duration, :payload)
+ logger = TestDebugLogSubscriber.new
+ logger.colorize_logging = true
+ SQL_COLORINGS.each do |verb, _|
+ logger.sql(event.new(0, sql: verb.to_s))
+ assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA} \(0.0ms\)#{REGEXP_CLEAR}/i, logger.debugs.last)
+
+ logger.sql(event.new(0, {sql: verb.to_s, name: "SQL"}))
+ assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA}SQL \(0.0ms\)#{REGEXP_CLEAR}/i, logger.debugs.last)
+ end
+ end
+
+ def test_basic_payload_name_logging_coloration_named_sql
+ event = Struct.new(:duration, :payload)
+ logger = TestDebugLogSubscriber.new
+ logger.colorize_logging = true
+ SQL_COLORINGS.each do |verb, _|
+ logger.sql(event.new(0, {sql: verb.to_s, name: "Model Load"}))
+ assert_match(/#{REGEXP_BOLD}#{REGEXP_CYAN}Model Load \(0.0ms\)#{REGEXP_CLEAR}/i, logger.debugs.last)
+
+ logger.sql(event.new(0, {sql: verb.to_s, name: "Model Exists"}))
+ assert_match(/#{REGEXP_BOLD}#{REGEXP_CYAN}Model Exists \(0.0ms\)#{REGEXP_CLEAR}/i, logger.debugs.last)
+
+ logger.sql(event.new(0, {sql: verb.to_s, name: "ANY SPECIFIC NAME"}))
+ assert_match(/#{REGEXP_BOLD}#{REGEXP_CYAN}ANY SPECIFIC NAME \(0.0ms\)#{REGEXP_CLEAR}/i, logger.debugs.last)
+ end
+ end
+
+ def test_query_logging_coloration_with_nested_select
+ event = Struct.new(:duration, :payload)
+ logger = TestDebugLogSubscriber.new
+ logger.colorize_logging = true
+ SQL_COLORINGS.slice(:SELECT, :INSERT, :UPDATE, :DELETE).each do |verb, color_regex|
+ logger.sql(event.new(0, sql: "#{verb} WHERE ID IN SELECT"))
+ assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA} \(0.0ms\)#{REGEXP_CLEAR} #{REGEXP_BOLD}#{color_regex}#{verb} WHERE ID IN SELECT#{REGEXP_CLEAR}/i, logger.debugs.last)
+ end
+ end
+
+ def test_query_logging_coloration_with_multi_line_nested_select
+ event = Struct.new(:duration, :payload)
+ logger = TestDebugLogSubscriber.new
+ logger.colorize_logging = true
+ SQL_COLORINGS.slice(:SELECT, :INSERT, :UPDATE, :DELETE).each do |verb, color_regex|
+ sql = <<-EOS
+ #{verb}
+ WHERE ID IN (
+ SELECT ID FROM THINGS
+ )
+ EOS
+ logger.sql(event.new(0, sql: sql))
+ assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA} \(0.0ms\)#{REGEXP_CLEAR} #{REGEXP_BOLD}#{color_regex}.*#{verb}.*#{REGEXP_CLEAR}/mi, logger.debugs.last)
+ end
+ end
+
+ def test_query_logging_coloration_with_lock
+ event = Struct.new(:duration, :payload)
+ logger = TestDebugLogSubscriber.new
+ logger.colorize_logging = true
+ sql = <<-EOS
+ SELECT * FROM
+ (SELECT * FROM mytable FOR UPDATE) ss
+ WHERE col1 = 5;
+ EOS
+ logger.sql(event.new(0, sql: sql))
+ assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA} \(0.0ms\)#{REGEXP_CLEAR} #{REGEXP_BOLD}#{SQL_COLORINGS[:LOCK]}.*FOR UPDATE.*#{REGEXP_CLEAR}/mi, logger.debugs.last)
+
+ sql = <<-EOS
+ LOCK TABLE films IN SHARE MODE;
+ EOS
+ logger.sql(event.new(0, sql: sql))
+ assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA} \(0.0ms\)#{REGEXP_CLEAR} #{REGEXP_BOLD}#{SQL_COLORINGS[:LOCK]}.*LOCK TABLE.*#{REGEXP_CLEAR}/mi, logger.debugs.last)
+ end
+
def test_exists_query_logging
Developer.exists? 1
wait
@@ -125,12 +215,5 @@ class LogSubscriberTest < ActiveRecord::TestCase
wait
assert_match(/<16 bytes of binary data>/, @logger.logged(:debug).join)
end
-
- def test_nil_binary_data_is_logged
- binary = Binary.create(data: "")
- binary.update_attributes(data: nil)
- wait
- assert_match(/<NULL binary data>/, @logger.logged(:debug).join)
- end
end
end
diff --git a/activerecord/test/cases/migration/change_schema_test.rb b/activerecord/test/cases/migration/change_schema_test.rb
index c66eaf1ee1..83e50048ec 100644
--- a/activerecord/test/cases/migration/change_schema_test.rb
+++ b/activerecord/test/cases/migration/change_schema_test.rb
@@ -68,8 +68,8 @@ module ActiveRecord
five = columns.detect { |c| c.name == "five" } unless mysql
assert_equal "hello", one.default
- assert_equal true, two.type_cast_from_database(two.default)
- assert_equal false, three.type_cast_from_database(three.default)
+ assert_equal true, connection.lookup_cast_type_from_column(two).deserialize(two.default)
+ assert_equal false, connection.lookup_cast_type_from_column(three).deserialize(three.default)
assert_equal '1', four.default
assert_equal "hello", five.default unless mysql
end
@@ -82,7 +82,7 @@ module ActiveRecord
columns = connection.columns(:testings)
array_column = columns.detect { |c| c.name == "foo" }
- assert array_column.array
+ assert array_column.array?
end
def test_create_table_with_array_column
@@ -93,10 +93,29 @@ module ActiveRecord
columns = connection.columns(:testings)
array_column = columns.detect { |c| c.name == "foo" }
- assert array_column.array
+ assert array_column.array?
end
end
+ def test_create_table_with_bigint
+ connection.create_table :testings do |t|
+ t.bigint :eight_int
+ end
+ columns = connection.columns(:testings)
+ eight = columns.detect { |c| c.name == "eight_int" }
+
+ if current_adapter?(:OracleAdapter)
+ assert_equal 'NUMBER(19)', eight.sql_type
+ elsif current_adapter?(:SQLite3Adapter)
+ assert_equal 'bigint', eight.sql_type
+ else
+ assert_equal :integer, eight.type
+ assert_equal 8, eight.limit
+ end
+ ensure
+ connection.drop_table :testings
+ end
+
def test_create_table_with_limits
connection.create_table :testings do |t|
t.column :foo, :string, :limit => 255
@@ -184,21 +203,21 @@ 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 !created_at_column.null
+ assert !updated_at_column.null
end
def test_create_table_with_timestamps_should_create_datetime_columns_with_options
connection.create_table table_name do |t|
- t.timestamps :null => false
+ t.timestamps null: true
end
created_columns = connection.columns(table_name)
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 created_at_column.null
+ assert updated_at_column.null
end
def test_create_table_without_a_block
@@ -384,6 +403,17 @@ module ActiveRecord
end
end
+ def test_drop_table_if_exists
+ connection.create_table(:testings)
+ assert connection.table_exists?(:testings)
+ connection.drop_table(:testings, if_exists: true)
+ assert_not connection.table_exists?(:testings)
+ end
+
+ def test_drop_table_if_exists_nothing_raised
+ assert_nothing_raised { connection.drop_table(:nonexistent, if_exists: true) }
+ end
+
private
def testing_table_with_only_foo_attribute
connection.create_table :testings, :id => false do |t|
@@ -393,5 +423,36 @@ module ActiveRecord
yield
end
end
+
+ if ActiveRecord::Base.connection.supports_foreign_keys?
+ class ChangeSchemaWithDependentObjectsTest < ActiveRecord::TestCase
+ self.use_transactional_tests = false
+
+ setup do
+ @connection = ActiveRecord::Base.connection
+ @connection.create_table :trains
+ @connection.create_table(:wagons) { |t| t.references :train }
+ @connection.add_foreign_key :wagons, :trains
+ end
+
+ teardown do
+ [:wagons, :trains].each do |table|
+ @connection.drop_table table, if_exists: true
+ end
+ end
+
+ def test_create_table_with_force_cascade_drops_dependent_objects
+ skip "MySQL > 5.5 does not drop dependent objects with DROP TABLE CASCADE" if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
+ # can't re-create table referenced by foreign key
+ assert_raises(ActiveRecord::StatementInvalid) do
+ @connection.create_table :trains, force: true
+ end
+
+ # can recreate referenced table with force: :cascade
+ @connection.create_table :trains, force: :cascade
+ assert_equal [], @connection.foreign_keys(:wagons)
+ end
+ end
+ end
end
end
diff --git a/activerecord/test/cases/migration/change_table_test.rb b/activerecord/test/cases/migration/change_table_test.rb
index 3e9d957ed3..2f9c50141f 100644
--- a/activerecord/test/cases/migration/change_table_test.rb
+++ b/activerecord/test/cases/migration/change_table_test.rb
@@ -1,5 +1,4 @@
require "cases/migration/helper"
-require "minitest/mock"
module ActiveRecord
class Migration
@@ -13,7 +12,7 @@ module ActiveRecord
end
def with_change_table
- yield ConnectionAdapters::Table.new(:delete_me, @connection)
+ yield ActiveRecord::Base.connection.update_table_definition(:delete_me, @connection)
end
def test_references_column_type_adds_id
@@ -88,15 +87,22 @@ module ActiveRecord
def test_timestamps_creates_updated_at_and_created_at
with_change_table do |t|
- @connection.expect :add_timestamps, nil, [:delete_me]
- t.timestamps
+ @connection.expect :add_timestamps, nil, [:delete_me, null: true]
+ t.timestamps null: true
end
end
def test_remove_timestamps_creates_updated_at_and_created_at
with_change_table do |t|
- @connection.expect :remove_timestamps, nil, [:delete_me]
- t.remove_timestamps
+ @connection.expect :remove_timestamps, nil, [:delete_me, { null: true }]
+ t.remove_timestamps({ null: true })
+ end
+ end
+
+ def test_primary_key_creates_primary_key_column
+ with_change_table do |t|
+ @connection.expect :add_column, nil, [:delete_me, :id, :primary_key, primary_key: true, first: true]
+ t.primary_key :id, first: true
end
end
@@ -108,6 +114,14 @@ module ActiveRecord
end
end
+ def test_bigint_creates_bigint_column
+ with_change_table do |t|
+ @connection.expect :add_column, nil, [:delete_me, :foo, :bigint, {}]
+ @connection.expect :add_column, nil, [:delete_me, :bar, :bigint, {}]
+ t.bigint :foo, :bar
+ end
+ end
+
def test_string_creates_string_column
with_change_table do |t|
@connection.expect :add_column, nil, [:delete_me, :foo, :string, {}]
@@ -116,6 +130,24 @@ module ActiveRecord
end
end
+ if current_adapter?(:PostgreSQLAdapter)
+ def test_json_creates_json_column
+ with_change_table do |t|
+ @connection.expect :add_column, nil, [:delete_me, :foo, :json, {}]
+ @connection.expect :add_column, nil, [:delete_me, :bar, :json, {}]
+ t.json :foo, :bar
+ end
+ end
+
+ def test_xml_creates_xml_column
+ with_change_table do |t|
+ @connection.expect :add_column, nil, [:delete_me, :foo, :xml, {}]
+ @connection.expect :add_column, nil, [:delete_me, :bar, :xml, {}]
+ t.xml :foo, :bar
+ end
+ end
+ end
+
def test_column_creates_column
with_change_table do |t|
@connection.expect :add_column, nil, [:delete_me, :bar, :integer, {}]
@@ -213,6 +245,12 @@ module ActiveRecord
t.rename :bar, :baz
end
end
+
+ def test_table_name_set
+ with_change_table do |t|
+ assert_equal :delete_me, t.name
+ end
+ end
end
end
end
diff --git a/activerecord/test/cases/migration/column_attributes_test.rb b/activerecord/test/cases/migration/column_attributes_test.rb
index 763aa88f72..8d8e661aa5 100644
--- a/activerecord/test/cases/migration/column_attributes_test.rb
+++ b/activerecord/test/cases/migration/column_attributes_test.rb
@@ -5,7 +5,7 @@ module ActiveRecord
class ColumnAttributesTest < ActiveRecord::TestCase
include ActiveRecord::Migration::TestHelper
- self.use_transactional_fixtures = false
+ self.use_transactional_tests = false
def test_add_column_newline_default
string = "foo\nbar"
diff --git a/activerecord/test/cases/migration/column_positioning_test.rb b/activerecord/test/cases/migration/column_positioning_test.rb
index 77a752f050..4637970ce0 100644
--- a/activerecord/test/cases/migration/column_positioning_test.rb
+++ b/activerecord/test/cases/migration/column_positioning_test.rb
@@ -3,7 +3,7 @@ require 'cases/helper'
module ActiveRecord
class Migration
class ColumnPositioningTest < ActiveRecord::TestCase
- attr_reader :connection, :table_name
+ attr_reader :connection
alias :conn :connection
def setup
@@ -25,30 +25,30 @@ module ActiveRecord
if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
def test_column_positioning
- assert_equal %w(first second third), conn.columns(:testings).map {|c| c.name }
+ assert_equal %w(first second third), conn.columns(:testings).map(&:name)
end
def test_add_column_with_positioning
conn.add_column :testings, :new_col, :integer
- assert_equal %w(first second third new_col), conn.columns(:testings).map {|c| c.name }
+ assert_equal %w(first second third new_col), conn.columns(:testings).map(&:name)
end
def test_add_column_with_positioning_first
conn.add_column :testings, :new_col, :integer, :first => true
- assert_equal %w(new_col first second third), conn.columns(:testings).map {|c| c.name }
+ assert_equal %w(new_col first second third), conn.columns(:testings).map(&:name)
end
def test_add_column_with_positioning_after
conn.add_column :testings, :new_col, :integer, :after => :first
- assert_equal %w(first new_col second third), conn.columns(:testings).map {|c| c.name }
+ assert_equal %w(first new_col second third), conn.columns(:testings).map(&:name)
end
def test_change_column_with_positioning
conn.change_column :testings, :second, :integer, :first => true
- assert_equal %w(second first third), conn.columns(:testings).map {|c| c.name }
+ assert_equal %w(second first third), conn.columns(:testings).map(&:name)
conn.change_column :testings, :second, :integer, :after => :third
- assert_equal %w(first third second), conn.columns(:testings).map {|c| c.name }
+ assert_equal %w(first third second), conn.columns(:testings).map(&:name)
end
end
end
diff --git a/activerecord/test/cases/migration/columns_test.rb b/activerecord/test/cases/migration/columns_test.rb
index 4e6d7963aa..ab3f584350 100644
--- a/activerecord/test/cases/migration/columns_test.rb
+++ b/activerecord/test/cases/migration/columns_test.rb
@@ -5,7 +5,7 @@ module ActiveRecord
class ColumnsTest < ActiveRecord::TestCase
include ActiveRecord::Migration::TestHelper
- self.use_transactional_fixtures = false
+ self.use_transactional_tests = false
# FIXME: this is more of an integration test with AR::Base and the
# schema modifications. Maybe we should move this?
@@ -65,7 +65,10 @@ module ActiveRecord
if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
def test_mysql_rename_column_preserves_auto_increment
rename_column "test_models", "id", "id_test"
- assert_equal "auto_increment", connection.columns("test_models").find { |c| c.name == "id_test" }.extra
+ assert connection.columns("test_models").find { |c| c.name == "id_test" }.auto_increment?
+ TestModel.reset_column_information
+ ensure
+ rename_column "test_models", "id_test", "id"
end
end
@@ -193,7 +196,7 @@ module ActiveRecord
old_columns = connection.columns(TestModel.table_name)
assert old_columns.find { |c|
- default = c.type_cast_from_database(c.default)
+ default = connection.lookup_cast_type_from_column(c).deserialize(c.default)
c.name == 'approved' && c.type == :boolean && default == true
}
@@ -201,11 +204,11 @@ module ActiveRecord
new_columns = connection.columns(TestModel.table_name)
assert_not new_columns.find { |c|
- default = c.type_cast_from_database(c.default)
+ default = connection.lookup_cast_type_from_column(c).deserialize(c.default)
c.name == 'approved' and c.type == :boolean and default == true
}
assert new_columns.find { |c|
- default = c.type_cast_from_database(c.default)
+ default = connection.lookup_cast_type_from_column(c).deserialize(c.default)
c.name == 'approved' and c.type == :boolean and default == false
}
change_column :test_models, :approved, :boolean, :default => true
@@ -264,6 +267,13 @@ module ActiveRecord
assert_nil TestModel.new.first_name
end
+ def test_change_column_default_with_from_and_to
+ add_column "test_models", "first_name", :string
+ connection.change_column_default "test_models", "first_name", from: nil, to: "Tester"
+
+ assert_equal "Tester", TestModel.new.first_name
+ end
+
def test_remove_column_no_second_parameter_raises_exception
assert_raise(ArgumentError) { connection.remove_column("funny") }
end
diff --git a/activerecord/test/cases/migration/command_recorder_test.rb b/activerecord/test/cases/migration/command_recorder_test.rb
index e955beae1a..1e3529db54 100644
--- a/activerecord/test/cases/migration/command_recorder_test.rb
+++ b/activerecord/test/cases/migration/command_recorder_test.rb
@@ -31,7 +31,8 @@ module ActiveRecord
end
def test_unknown_commands_delegate
- recorder = CommandRecorder.new(stub(:foo => 'bar'))
+ recorder = Struct.new(:foo)
+ recorder = CommandRecorder.new(recorder.new('bar'))
assert_equal 'bar', recorder.foo
end
@@ -169,6 +170,16 @@ module ActiveRecord
end
end
+ def test_invert_change_column_default_with_from_and_to
+ change = @recorder.inverse_of :change_column_default, [:table, :column, from: "old_value", to: "new_value"]
+ assert_equal [:change_column_default, [:table, :column, from: "new_value", to: "old_value"]], change
+ end
+
+ def test_invert_change_column_default_with_from_and_to_with_boolean
+ change = @recorder.inverse_of :change_column_default, [:table, :column, from: true, to: false]
+ assert_equal [:change_column_default, [:table, :column, from: false, to: true]], change
+ end
+
def test_invert_change_column_null
add = @recorder.inverse_of :change_column_null, [:table, :column, true]
assert_equal [:change_column_null, [:table, :column, false]], add
@@ -206,6 +217,11 @@ module ActiveRecord
end
def test_invert_remove_index
+ add = @recorder.inverse_of :remove_index, [:table, :one]
+ assert_equal [:add_index, [:table, :one]], add
+ end
+
+ def test_invert_remove_index_with_column
add = @recorder.inverse_of :remove_index, [:table, {column: [:one, :two], options: true}]
assert_equal [:add_index, [:table, [:one, :two], options: true]], add
end
@@ -237,8 +253,8 @@ module ActiveRecord
end
def test_invert_remove_timestamps
- add = @recorder.inverse_of :remove_timestamps, [:table]
- assert_equal [:add_timestamps, [:table], nil], add
+ add = @recorder.inverse_of :remove_timestamps, [:table, { null: true }]
+ assert_equal [:add_timestamps, [:table, {null: true }], nil], add
end
def test_invert_add_reference
@@ -256,6 +272,11 @@ module ActiveRecord
assert_equal [:add_reference, [:table, :taggable, { polymorphic: true }], nil], add
end
+ def test_invert_remove_reference_with_index_and_foreign_key
+ add = @recorder.inverse_of :remove_reference, [:table, :taggable, { index: true, foreign_key: true }]
+ assert_equal [:add_reference, [:table, :taggable, { index: true, foreign_key: true }], nil], add
+ end
+
def test_invert_remove_belongs_to_alias
add = @recorder.inverse_of :remove_belongs_to, [:table, :user]
assert_equal [:add_reference, [:table, :user], nil], add
@@ -276,17 +297,42 @@ module ActiveRecord
assert_equal [:remove_foreign_key, [:dogs, :people]], enable
end
+ def test_invert_remove_foreign_key
+ enable = @recorder.inverse_of :remove_foreign_key, [:dogs, :people]
+ assert_equal [:add_foreign_key, [:dogs, :people]], enable
+ end
+
def test_invert_add_foreign_key_with_column
enable = @recorder.inverse_of :add_foreign_key, [:dogs, :people, column: "owner_id"]
assert_equal [:remove_foreign_key, [:dogs, column: "owner_id"]], enable
end
+ def test_invert_remove_foreign_key_with_column
+ enable = @recorder.inverse_of :remove_foreign_key, [:dogs, :people, column: "owner_id"]
+ assert_equal [:add_foreign_key, [:dogs, :people, column: "owner_id"]], enable
+ end
+
def test_invert_add_foreign_key_with_column_and_name
enable = @recorder.inverse_of :add_foreign_key, [:dogs, :people, column: "owner_id", name: "fk"]
assert_equal [:remove_foreign_key, [:dogs, name: "fk"]], enable
end
- def test_remove_foreign_key_is_irreversible
+ def test_invert_remove_foreign_key_with_column_and_name
+ enable = @recorder.inverse_of :remove_foreign_key, [:dogs, :people, column: "owner_id", name: "fk"]
+ assert_equal [:add_foreign_key, [:dogs, :people, column: "owner_id", name: "fk"]], enable
+ end
+
+ def test_invert_remove_foreign_key_with_primary_key
+ enable = @recorder.inverse_of :remove_foreign_key, [:dogs, :people, primary_key: "person_id"]
+ assert_equal [:add_foreign_key, [:dogs, :people, primary_key: "person_id"]], enable
+ end
+
+ def test_invert_remove_foreign_key_with_on_delete_on_update
+ enable = @recorder.inverse_of :remove_foreign_key, [:dogs, :people, on_delete: :nullify, on_update: :cascade]
+ assert_equal [:add_foreign_key, [:dogs, :people, on_delete: :nullify, on_update: :cascade]], enable
+ end
+
+ def test_invert_remove_foreign_key_is_irreversible_without_to_table
assert_raises ActiveRecord::IrreversibleMigration do
@recorder.inverse_of :remove_foreign_key, [:dogs, column: "owner_id"]
end
@@ -294,6 +340,10 @@ module ActiveRecord
assert_raises ActiveRecord::IrreversibleMigration do
@recorder.inverse_of :remove_foreign_key, [:dogs, name: "fk"]
end
+
+ assert_raises ActiveRecord::IrreversibleMigration do
+ @recorder.inverse_of :remove_foreign_key, [:dogs]
+ end
end
end
end
diff --git a/activerecord/test/cases/migration/create_join_table_test.rb b/activerecord/test/cases/migration/create_join_table_test.rb
index bea9d6b2c9..8fd08fe4ce 100644
--- a/activerecord/test/cases/migration/create_join_table_test.rb
+++ b/activerecord/test/cases/migration/create_join_table_test.rb
@@ -140,7 +140,7 @@ module ActiveRecord
tables_after = connection.tables - tables_before
tables_after.each do |table|
- connection.execute "DROP TABLE #{table}"
+ connection.drop_table table
end
end
end
diff --git a/activerecord/test/cases/migration/foreign_key_test.rb b/activerecord/test/cases/migration/foreign_key_test.rb
index c985092b4c..72f2fa95f1 100644
--- a/activerecord/test/cases/migration/foreign_key_test.rb
+++ b/activerecord/test/cases/migration/foreign_key_test.rb
@@ -8,6 +8,7 @@ module ActiveRecord
class ForeignKeyTest < ActiveRecord::TestCase
include DdlHelper
include SchemaDumpingHelper
+ include ActiveSupport::Testing::Stream
class Rocket < ActiveRecord::Base
end
@@ -17,11 +18,11 @@ module ActiveRecord
setup do
@connection = ActiveRecord::Base.connection
- @connection.create_table "rockets" do |t|
+ @connection.create_table "rockets", force: true do |t|
t.string :name
end
- @connection.create_table "astronauts" do |t|
+ @connection.create_table "astronauts", force: true do |t|
t.string :name
t.references :rocket
end
@@ -29,8 +30,8 @@ module ActiveRecord
teardown do
if defined?(@connection)
- @connection.execute "DROP TABLE IF EXISTS astronauts"
- @connection.execute "DROP TABLE IF EXISTS rockets"
+ @connection.drop_table "astronauts", if_exists: true
+ @connection.drop_table "rockets", if_exists: true
end
end
@@ -57,7 +58,7 @@ module ActiveRecord
assert_equal "rockets", fk.to_table
assert_equal "rocket_id", fk.column
assert_equal "id", fk.primary_key
- assert_match(/^fk_rails_.{10}$/, fk.name)
+ assert_equal("fk_rails_78146ddd2e", fk.name)
end
def test_add_foreign_key_with_column
@@ -71,7 +72,7 @@ module ActiveRecord
assert_equal "rockets", fk.to_table
assert_equal "rocket_id", fk.column
assert_equal "id", fk.primary_key
- assert_match(/^fk_rails_.{10}$/, fk.name)
+ assert_equal("fk_rails_78146ddd2e", fk.name)
end
def test_add_foreign_key_with_non_standard_primary_key
@@ -146,6 +147,27 @@ module ActiveRecord
assert_equal :nullify, fk.on_update
end
+ def test_foreign_key_exists
+ @connection.add_foreign_key :astronauts, :rockets
+
+ assert @connection.foreign_key_exists?(:astronauts, :rockets)
+ assert_not @connection.foreign_key_exists?(:astronauts, :stars)
+ end
+
+ def test_foreign_key_exists_by_column
+ @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id"
+
+ assert @connection.foreign_key_exists?(:astronauts, column: "rocket_id")
+ assert_not @connection.foreign_key_exists?(:astronauts, column: "star_id")
+ end
+
+ def test_foreign_key_exists_by_name
+ @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", name: "fancy_named_fk"
+
+ assert @connection.foreign_key_exists?(:astronauts, name: "fancy_named_fk")
+ assert_not @connection.foreign_key_exists?(:astronauts, name: "other_fancy_named_fk")
+ end
+
def test_remove_foreign_key_inferes_column
@connection.add_foreign_key :astronauts, :rockets
@@ -162,6 +184,14 @@ module ActiveRecord
assert_equal [], @connection.foreign_keys("astronauts")
end
+ def test_remove_foreign_key_by_symbol_column
+ @connection.add_foreign_key :astronauts, :rockets, column: :rocket_id
+
+ assert_equal 1, @connection.foreign_keys("astronauts").size
+ @connection.remove_foreign_key :astronauts, column: :rocket_id
+ assert_equal [], @connection.foreign_keys("astronauts")
+ end
+
def test_remove_foreign_key_by_name
@connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", name: "fancy_named_fk"
@@ -212,6 +242,38 @@ module ActiveRecord
ensure
silence_stream($stdout) { migration.migrate(:down) }
end
+
+ class CreateSchoolsAndClassesMigration < ActiveRecord::Migration
+ def change
+ create_table(:schools)
+
+ create_table(:classes) do |t|
+ t.column :school_id, :integer
+ end
+ add_foreign_key :classes, :schools
+ end
+ end
+
+ def test_add_foreign_key_with_prefix
+ ActiveRecord::Base.table_name_prefix = 'p_'
+ migration = CreateSchoolsAndClassesMigration.new
+ silence_stream($stdout) { migration.migrate(:up) }
+ assert_equal 1, @connection.foreign_keys("p_classes").size
+ ensure
+ silence_stream($stdout) { migration.migrate(:down) }
+ ActiveRecord::Base.table_name_prefix = nil
+ end
+
+ def test_add_foreign_key_with_suffix
+ ActiveRecord::Base.table_name_suffix = '_s'
+ migration = CreateSchoolsAndClassesMigration.new
+ silence_stream($stdout) { migration.migrate(:up) }
+ assert_equal 1, @connection.foreign_keys("classes_s").size
+ ensure
+ silence_stream($stdout) { migration.migrate(:down) }
+ ActiveRecord::Base.table_name_suffix = nil
+ end
+
end
end
end
diff --git a/activerecord/test/cases/migration/helper.rb b/activerecord/test/cases/migration/helper.rb
index e28feedcf9..ad85684c0b 100644
--- a/activerecord/test/cases/migration/helper.rb
+++ b/activerecord/test/cases/migration/helper.rb
@@ -5,10 +5,6 @@ module ActiveRecord
class << self; attr_accessor :message_count; end
self.message_count = 0
- def puts(text="")
- ActiveRecord::Migration.message_count += 1
- end
-
module TestHelper
attr_reader :connection, :table_name
@@ -22,7 +18,7 @@ module ActiveRecord
super
@connection = ActiveRecord::Base.connection
connection.create_table :test_models do |t|
- t.timestamps
+ t.timestamps null: true
end
TestModel.reset_column_information
@@ -32,7 +28,7 @@ module ActiveRecord
super
TestModel.reset_table_name
TestModel.reset_sequence_name
- connection.drop_table :test_models rescue nil
+ connection.drop_table :test_models, if_exists: true
end
private
diff --git a/activerecord/test/cases/migration/index_test.rb b/activerecord/test/cases/migration/index_test.rb
index 93c3bfae7a..b23b9a679f 100644
--- a/activerecord/test/cases/migration/index_test.rb
+++ b/activerecord/test/cases/migration/index_test.rb
@@ -36,6 +36,20 @@ module ActiveRecord
assert connection.index_name_exists?(table_name, 'new_idx', true)
end
+ def test_rename_index_too_long
+ too_long_index_name = good_index_name + 'x'
+ # keep the names short to make Oracle and similar behave
+ connection.add_index(table_name, [:foo], :name => 'old_idx')
+ e = assert_raises(ArgumentError) {
+ connection.rename_index(table_name, 'old_idx', too_long_index_name)
+ }
+ assert_match(/too long; the limit is #{connection.allowed_index_name_length} characters/, e.message)
+
+ # if the adapter doesn't support the indexes call, pick defaults that let the test pass
+ assert connection.index_name_exists?(table_name, 'old_idx', false)
+ end
+
+
def test_double_add_index
connection.add_index(table_name, [:foo], :name => 'some_idx')
assert_raises(ArgumentError) {
@@ -95,6 +109,12 @@ module ActiveRecord
assert connection.index_exists?(:testings, [:foo, :bar])
end
+ def test_index_exists_with_custom_name_checks_columns
+ connection.add_index :testings, [:foo, :bar], name: "my_index"
+ assert connection.index_exists?(:testings, [:foo, :bar], name: "my_index")
+ assert_not connection.index_exists?(:testings, [:foo], name: "my_index")
+ end
+
def test_valid_index_options
assert_raise ArgumentError do
connection.add_index :testings, :foo, unqiue: true
diff --git a/activerecord/test/cases/migration/logger_test.rb b/activerecord/test/cases/migration/logger_test.rb
index 319d3e1af3..bf6e684887 100644
--- a/activerecord/test/cases/migration/logger_test.rb
+++ b/activerecord/test/cases/migration/logger_test.rb
@@ -4,7 +4,7 @@ module ActiveRecord
class Migration
class LoggerTest < ActiveRecord::TestCase
# MySQL can't roll back ddl changes
- self.use_transactional_fixtures = false
+ self.use_transactional_tests = false
Migration = Struct.new(:name, :version) do
def disable_ddl_transaction; false end
diff --git a/activerecord/test/cases/migration/pending_migrations_test.rb b/activerecord/test/cases/migration/pending_migrations_test.rb
index 517ee695ce..4f5589f32a 100644
--- a/activerecord/test/cases/migration/pending_migrations_test.rb
+++ b/activerecord/test/cases/migration/pending_migrations_test.rb
@@ -1,13 +1,12 @@
require 'cases/helper'
-require "minitest/mock"
module ActiveRecord
class Migration
class PendingMigrationsTest < ActiveRecord::TestCase
def setup
super
- @connection = MiniTest::Mock.new
- @app = MiniTest::Mock.new
+ @connection = Minitest::Mock.new
+ @app = Minitest::Mock.new
conn = @connection
@pending = Class.new(CheckPending) {
define_method(:connection) { conn }
diff --git a/activerecord/test/cases/migration/postgresql_geometric_types_test.rb b/activerecord/test/cases/migration/postgresql_geometric_types_test.rb
new file mode 100644
index 0000000000..e4772905bb
--- /dev/null
+++ b/activerecord/test/cases/migration/postgresql_geometric_types_test.rb
@@ -0,0 +1,93 @@
+require 'cases/helper'
+
+module ActiveRecord
+ class Migration
+ class PostgreSQLGeometricTypesTest < ActiveRecord::TestCase
+ attr_reader :connection, :table_name
+
+ def setup
+ super
+ @connection = ActiveRecord::Base.connection
+ @table_name = :testings
+ end
+
+ if current_adapter?(:PostgreSQLAdapter)
+ def test_creating_column_with_point_type
+ connection.create_table(table_name) do |t|
+ t.point :foo_point
+ end
+
+ assert_column_exists(:foo_point)
+ assert_type_correct(:foo_point, :point)
+ end
+
+ def test_creating_column_with_line_type
+ connection.create_table(table_name) do |t|
+ t.line :foo_line
+ end
+
+ assert_column_exists(:foo_line)
+ assert_type_correct(:foo_line, :line)
+ end
+
+ def test_creating_column_with_lseg_type
+ connection.create_table(table_name) do |t|
+ t.lseg :foo_lseg
+ end
+
+ assert_column_exists(:foo_lseg)
+ assert_type_correct(:foo_lseg, :lseg)
+ end
+
+ def test_creating_column_with_box_type
+ connection.create_table(table_name) do |t|
+ t.box :foo_box
+ end
+
+ assert_column_exists(:foo_box)
+ assert_type_correct(:foo_box, :box)
+ end
+
+ def test_creating_column_with_path_type
+ connection.create_table(table_name) do |t|
+ t.path :foo_path
+ end
+
+ assert_column_exists(:foo_path)
+ assert_type_correct(:foo_path, :path)
+ end
+
+ def test_creating_column_with_polygon_type
+ connection.create_table(table_name) do |t|
+ t.polygon :foo_polygon
+ end
+
+ assert_column_exists(:foo_polygon)
+ assert_type_correct(:foo_polygon, :polygon)
+ end
+
+ def test_creating_column_with_circle_type
+ connection.create_table(table_name) do |t|
+ t.circle :foo_circle
+ end
+
+ assert_column_exists(:foo_circle)
+ assert_type_correct(:foo_circle, :circle)
+ end
+ end
+
+ private
+ def assert_column_exists(column_name)
+ columns = connection.columns(table_name)
+ assert columns.map(&:name).include?(column_name.to_s)
+ end
+
+ def assert_type_correct(column_name, type)
+ columns = connection.columns(table_name)
+ column = columns.select{ |c| c.name == column_name.to_s }.first
+ assert_equal type.to_s, column.sql_type
+ end
+
+ end
+ end
+end \ No newline at end of file
diff --git a/activerecord/test/cases/migration/references_foreign_key_test.rb b/activerecord/test/cases/migration/references_foreign_key_test.rb
new file mode 100644
index 0000000000..84ec657398
--- /dev/null
+++ b/activerecord/test/cases/migration/references_foreign_key_test.rb
@@ -0,0 +1,170 @@
+require 'cases/helper'
+
+if ActiveRecord::Base.connection.supports_foreign_keys?
+module ActiveRecord
+ class Migration
+ class ReferencesForeignKeyTest < ActiveRecord::TestCase
+ setup do
+ @connection = ActiveRecord::Base.connection
+ @connection.create_table(:testing_parents, force: true)
+ end
+
+ teardown do
+ @connection.drop_table "testings", if_exists: true
+ @connection.drop_table "testing_parents", if_exists: true
+ end
+
+ test "foreign keys can be created with the table" do
+ @connection.create_table :testings do |t|
+ t.references :testing_parent, foreign_key: true
+ end
+
+ fk = @connection.foreign_keys("testings").first
+ assert_equal "testings", fk.from_table
+ assert_equal "testing_parents", fk.to_table
+ end
+
+ test "no foreign key is created by default" do
+ @connection.create_table :testings do |t|
+ t.references :testing_parent
+ end
+
+ assert_equal [], @connection.foreign_keys("testings")
+ end
+
+ test "foreign keys can be created in one query" do
+ assert_queries(1) do
+ @connection.create_table :testings do |t|
+ t.references :testing_parent, foreign_key: true
+ end
+ end
+ end
+
+ test "options hash can be passed" do
+ @connection.change_table :testing_parents do |t|
+ t.integer :other_id
+ t.index :other_id, unique: true
+ end
+ @connection.create_table :testings do |t|
+ t.references :testing_parent, foreign_key: { primary_key: :other_id }
+ end
+
+ fk = @connection.foreign_keys("testings").find { |k| k.to_table == "testing_parents" }
+ assert_equal "other_id", fk.primary_key
+ end
+
+ test "to_table option can be passed" do
+ @connection.create_table :testings do |t|
+ t.references :parent, foreign_key: { to_table: :testing_parents }
+ end
+ fks = @connection.foreign_keys("testings")
+ assert_equal([["testings", "testing_parents", "parent_id"]],
+ fks.map {|fk| [fk.from_table, fk.to_table, fk.column] })
+ end
+
+ test "foreign keys cannot be added to polymorphic relations when creating the table" do
+ @connection.create_table :testings do |t|
+ assert_raises(ArgumentError) do
+ t.references :testing_parent, polymorphic: true, foreign_key: true
+ end
+ end
+ end
+
+ test "foreign keys can be created while changing the table" do
+ @connection.create_table :testings
+ @connection.change_table :testings do |t|
+ t.references :testing_parent, foreign_key: true
+ end
+
+ fk = @connection.foreign_keys("testings").first
+ assert_equal "testings", fk.from_table
+ assert_equal "testing_parents", fk.to_table
+ end
+
+ test "foreign keys are not added by default when changing the table" do
+ @connection.create_table :testings
+ @connection.change_table :testings do |t|
+ t.references :testing_parent
+ end
+
+ assert_equal [], @connection.foreign_keys("testings")
+ end
+
+ test "foreign keys accept options when changing the table" do
+ @connection.change_table :testing_parents do |t|
+ t.integer :other_id
+ t.index :other_id, unique: true
+ end
+ @connection.create_table :testings
+ @connection.change_table :testings do |t|
+ t.references :testing_parent, foreign_key: { primary_key: :other_id }
+ end
+
+ fk = @connection.foreign_keys("testings").find { |k| k.to_table == "testing_parents" }
+ assert_equal "other_id", fk.primary_key
+ end
+
+ test "foreign keys cannot be added to polymorphic relations when changing the table" do
+ @connection.create_table :testings
+ @connection.change_table :testings do |t|
+ assert_raises(ArgumentError) do
+ t.references :testing_parent, polymorphic: true, foreign_key: true
+ end
+ end
+ end
+
+ test "foreign key column can be removed" do
+ @connection.create_table :testings do |t|
+ t.references :testing_parent, index: true, foreign_key: true
+ end
+
+ assert_difference "@connection.foreign_keys('testings').size", -1 do
+ @connection.remove_reference :testings, :testing_parent, foreign_key: true
+ end
+ end
+
+ test "foreign key methods respect pluralize_table_names" do
+ begin
+ original_pluralize_table_names = ActiveRecord::Base.pluralize_table_names
+ ActiveRecord::Base.pluralize_table_names = false
+ @connection.create_table :testing
+ @connection.change_table :testing_parents do |t|
+ t.references :testing, foreign_key: true
+ end
+
+ fk = @connection.foreign_keys("testing_parents").first
+ assert_equal "testing_parents", fk.from_table
+ assert_equal "testing", fk.to_table
+
+ assert_difference "@connection.foreign_keys('testing_parents').size", -1 do
+ @connection.remove_reference :testing_parents, :testing, foreign_key: true
+ end
+ ensure
+ ActiveRecord::Base.pluralize_table_names = original_pluralize_table_names
+ @connection.drop_table "testing", if_exists: true
+ end
+ end
+ end
+ end
+end
+else
+class ReferencesWithoutForeignKeySupportTest < ActiveRecord::TestCase
+ setup do
+ @connection = ActiveRecord::Base.connection
+ @connection.create_table(:testing_parents, force: true)
+ end
+
+ teardown do
+ @connection.drop_table("testings", if_exists: true)
+ @connection.drop_table("testing_parents", if_exists: true)
+ end
+
+ test "ignores foreign keys defined with the table" do
+ @connection.create_table :testings do |t|
+ t.references :testing_parent, foreign_key: true
+ end
+
+ assert_includes @connection.tables, "testings"
+ end
+end
+end
diff --git a/activerecord/test/cases/migration/references_index_test.rb b/activerecord/test/cases/migration/references_index_test.rb
index 4485701a4e..ad6b828d0b 100644
--- a/activerecord/test/cases/migration/references_index_test.rb
+++ b/activerecord/test/cases/migration/references_index_test.rb
@@ -55,7 +55,7 @@ module ActiveRecord
t.references :foo, :polymorphic => true, :index => true
end
- assert connection.index_exists?(table_name, [:foo_id, :foo_type], :name => :index_testings_on_foo_id_and_foo_type)
+ assert connection.index_exists?(table_name, [:foo_type, :foo_id], name: :index_testings_on_foo_type_and_foo_id)
end
end
@@ -93,7 +93,7 @@ module ActiveRecord
t.references :foo, :polymorphic => true, :index => true
end
- assert connection.index_exists?(table_name, [:foo_id, :foo_type], :name => :index_testings_on_foo_id_and_foo_type)
+ assert connection.index_exists?(table_name, [:foo_type, :foo_id], name: :index_testings_on_foo_type_and_foo_id)
end
end
end
diff --git a/activerecord/test/cases/migration/references_statements_test.rb b/activerecord/test/cases/migration/references_statements_test.rb
index b8b4fa1135..f613fd66c3 100644
--- a/activerecord/test/cases/migration/references_statements_test.rb
+++ b/activerecord/test/cases/migration/references_statements_test.rb
@@ -5,7 +5,7 @@ module ActiveRecord
class ReferencesStatementsTest < ActiveRecord::TestCase
include ActiveRecord::Migration::TestHelper
- self.use_transactional_fixtures = false
+ self.use_transactional_tests = false
def setup
super
@@ -42,7 +42,7 @@ module ActiveRecord
def test_creates_polymorphic_index
add_reference table_name, :taggable, polymorphic: true, index: true
- assert index_exists?(table_name, [:taggable_id, :taggable_type])
+ assert index_exists?(table_name, [:taggable_type, :taggable_id])
end
def test_creates_reference_type_column_with_default
diff --git a/activerecord/test/cases/migration/rename_table_test.rb b/activerecord/test/cases/migration/rename_table_test.rb
index ba39fb1dec..6d742d3f2f 100644
--- a/activerecord/test/cases/migration/rename_table_test.rb
+++ b/activerecord/test/cases/migration/rename_table_test.rb
@@ -5,7 +5,7 @@ module ActiveRecord
class RenameTableTest < ActiveRecord::TestCase
include ActiveRecord::Migration::TestHelper
- self.use_transactional_fixtures = false
+ self.use_transactional_tests = false
def setup
super
@@ -39,33 +39,35 @@ module ActiveRecord
end
end
- def test_rename_table
- rename_table :test_models, :octopi
+ unless current_adapter?(:FbAdapter) # Firebird cannot rename tables
+ def test_rename_table
+ rename_table :test_models, :octopi
- connection.execute "INSERT INTO octopi (#{connection.quote_column_name('id')}, #{connection.quote_column_name('url')}) VALUES (1, 'http://www.foreverflying.com/octopus-black7.jpg')"
+ connection.execute "INSERT INTO octopi (#{connection.quote_column_name('id')}, #{connection.quote_column_name('url')}) VALUES (1, 'http://www.foreverflying.com/octopus-black7.jpg')"
- assert_equal 'http://www.foreverflying.com/octopus-black7.jpg', connection.select_value("SELECT url FROM octopi WHERE id=1")
- end
+ assert_equal 'http://www.foreverflying.com/octopus-black7.jpg', connection.select_value("SELECT url FROM octopi WHERE id=1")
+ end
- def test_rename_table_with_an_index
- add_index :test_models, :url
+ def test_rename_table_with_an_index
+ add_index :test_models, :url
- rename_table :test_models, :octopi
+ rename_table :test_models, :octopi
- connection.execute "INSERT INTO octopi (#{connection.quote_column_name('id')}, #{connection.quote_column_name('url')}) VALUES (1, 'http://www.foreverflying.com/octopus-black7.jpg')"
+ connection.execute "INSERT INTO octopi (#{connection.quote_column_name('id')}, #{connection.quote_column_name('url')}) VALUES (1, 'http://www.foreverflying.com/octopus-black7.jpg')"
- assert_equal 'http://www.foreverflying.com/octopus-black7.jpg', connection.select_value("SELECT url FROM octopi WHERE id=1")
- index = connection.indexes(:octopi).first
- assert index.columns.include?("url")
- assert_equal 'index_octopi_on_url', index.name
- end
+ assert_equal 'http://www.foreverflying.com/octopus-black7.jpg', connection.select_value("SELECT url FROM octopi WHERE id=1")
+ index = connection.indexes(:octopi).first
+ assert index.columns.include?("url")
+ assert_equal 'index_octopi_on_url', index.name
+ end
- def test_rename_table_does_not_rename_custom_named_index
- add_index :test_models, :url, name: 'special_url_idx'
+ def test_rename_table_does_not_rename_custom_named_index
+ add_index :test_models, :url, name: 'special_url_idx'
- rename_table :test_models, :octopi
+ rename_table :test_models, :octopi
- assert_equal ['special_url_idx'], connection.indexes(:octopi).map(&:name)
+ assert_equal ['special_url_idx'], connection.indexes(:octopi).map(&:name)
+ end
end
if current_adapter?(:PostgreSQLAdapter)
@@ -78,13 +80,14 @@ module ActiveRecord
end
def test_renaming_table_doesnt_attempt_to_rename_non_existent_sequences
- enable_uuid_ossp!(connection)
+ enable_extension!('uuid-ossp', connection)
connection.create_table :cats, id: :uuid
assert_nothing_raised { rename_table :cats, :felines }
assert connection.table_exists? :felines
ensure
- connection.drop_table :cats if connection.table_exists? :cats
- connection.drop_table :felines if connection.table_exists? :felines
+ disable_extension!('uuid-ossp', connection)
+ connection.drop_table :cats, if_exists: true
+ connection.drop_table :felines, if_exists: true
end
end
end
diff --git a/activerecord/test/cases/migration/table_and_index_test.rb b/activerecord/test/cases/migration/table_and_index_test.rb
index 8fd770abd1..24cba84a09 100644
--- a/activerecord/test/cases/migration/table_and_index_test.rb
+++ b/activerecord/test/cases/migration/table_and_index_test.rb
@@ -6,11 +6,11 @@ module ActiveRecord
def test_add_schema_info_respects_prefix_and_suffix
conn = ActiveRecord::Base.connection
- conn.drop_table(ActiveRecord::Migrator.schema_migrations_table_name) if conn.table_exists?(ActiveRecord::Migrator.schema_migrations_table_name)
+ conn.drop_table(ActiveRecord::Migrator.schema_migrations_table_name, if_exists: true)
# Use shorter prefix and suffix as in Oracle database identifier cannot be larger than 30 characters
ActiveRecord::Base.table_name_prefix = 'p_'
ActiveRecord::Base.table_name_suffix = '_s'
- conn.drop_table(ActiveRecord::Migrator.schema_migrations_table_name) if conn.table_exists?(ActiveRecord::Migrator.schema_migrations_table_name)
+ conn.drop_table(ActiveRecord::Migrator.schema_migrations_table_name, if_exists: true)
conn.initialize_schema_migrations_table
diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb
index ef3f073472..10f1c7216f 100644
--- a/activerecord/test/cases/migration_test.rb
+++ b/activerecord/test/cases/migration_test.rb
@@ -5,6 +5,7 @@ require 'bigdecimal/util'
require 'models/person'
require 'models/topic'
require 'models/developer'
+require 'models/computer'
require MIGRATIONS_ROOT + "/valid/2_we_need_reminders"
require MIGRATIONS_ROOT + "/rename/1_we_need_things"
@@ -13,10 +14,9 @@ require MIGRATIONS_ROOT + "/decimal/1_give_me_big_numbers"
class BigNumber < ActiveRecord::Base
unless current_adapter?(:PostgreSQLAdapter, :SQLite3Adapter)
- attribute :value_of_e, Type::Integer.new
+ attribute :value_of_e, :integer
end
- attribute :world_population, Type::Integer.new
- attribute :my_house_population, Type::Integer.new
+ attribute :my_house_population, :integer
end
class Reminder < ActiveRecord::Base; end
@@ -24,7 +24,7 @@ class Reminder < ActiveRecord::Base; end
class Thing < ActiveRecord::Base; end
class MigrationTest < ActiveRecord::TestCase
- self.use_transactional_fixtures = false
+ self.use_transactional_tests = false
fixtures :people
@@ -34,8 +34,7 @@ class MigrationTest < ActiveRecord::TestCase
Reminder.connection.drop_table(table) rescue nil
end
Reminder.reset_column_information
- ActiveRecord::Migration.verbose = true
- ActiveRecord::Migration.message_count = 0
+ @verbose_was, ActiveRecord::Migration.verbose = ActiveRecord::Migration.verbose, false
ActiveRecord::Base.connection.schema_cache.clear!
end
@@ -65,6 +64,8 @@ class MigrationTest < ActiveRecord::TestCase
Person.connection.remove_column("people", "middle_name") rescue nil
Person.connection.add_column("people", "first_name", :string)
Person.reset_column_information
+
+ ActiveRecord::Migration.verbose = @verbose_was
end
def test_migrator_versions
@@ -74,26 +75,48 @@ class MigrationTest < ActiveRecord::TestCase
ActiveRecord::Migrator.up(migrations_path)
assert_equal 3, ActiveRecord::Migrator.current_version
- assert_equal 3, ActiveRecord::Migrator.last_version
assert_equal false, ActiveRecord::Migrator.needs_migration?
ActiveRecord::Migrator.down(MIGRATIONS_ROOT + "/valid")
assert_equal 0, ActiveRecord::Migrator.current_version
- assert_equal 3, ActiveRecord::Migrator.last_version
+ assert_equal true, ActiveRecord::Migrator.needs_migration?
+
+ ActiveRecord::SchemaMigration.create!(version: 3)
assert_equal true, ActiveRecord::Migrator.needs_migration?
ensure
ActiveRecord::Migrator.migrations_paths = old_path
end
+ def test_migration_detection_without_schema_migration_table
+ ActiveRecord::Base.connection.drop_table 'schema_migrations', if_exists: true
+
+ migrations_path = MIGRATIONS_ROOT + "/valid"
+ old_path = ActiveRecord::Migrator.migrations_paths
+ ActiveRecord::Migrator.migrations_paths = migrations_path
+
+ assert_equal true, ActiveRecord::Migrator.needs_migration?
+ ensure
+ ActiveRecord::Migrator.migrations_paths = old_path
+ end
+
+ def test_any_migrations
+ old_path = ActiveRecord::Migrator.migrations_paths
+ ActiveRecord::Migrator.migrations_paths = MIGRATIONS_ROOT + "/valid"
+
+ assert ActiveRecord::Migrator.any_migrations?
+
+ ActiveRecord::Migrator.migrations_paths = MIGRATIONS_ROOT + "/empty"
+
+ assert_not ActiveRecord::Migrator.any_migrations?
+ ensure
+ ActiveRecord::Migrator.migrations_paths = old_path
+ end
+
def test_migration_version
- ActiveRecord::Migrator.run(:up, MIGRATIONS_ROOT + "/version_check", 20131219224947)
+ assert_nothing_raised { ActiveRecord::Migrator.run(:up, MIGRATIONS_ROOT + "/version_check", 20131219224947) }
end
def test_create_table_with_force_true_does_not_drop_nonexisting_table
- if Person.connection.table_exists?(:testings2)
- Person.connection.drop_table :testings2
- end
-
# using a copy as we need the drop_table method to
# continue to work for the ensure block of the test
temp_conn = Person.connection.dup
@@ -104,16 +127,12 @@ class MigrationTest < ActiveRecord::TestCase
t.column :foo, :string
end
ensure
- Person.connection.drop_table :testings2 rescue nil
- end
-
- def connection
- ActiveRecord::Base.connection
+ Person.connection.drop_table :testings2, if_exists: true
end
def test_migration_instance_has_connection
migration = Class.new(ActiveRecord::Migration).new
- assert_equal connection, migration.connection
+ assert_equal ActiveRecord::Base.connection, migration.connection
end
def test_method_missing_delegates_to_connection
@@ -133,6 +152,7 @@ class MigrationTest < ActiveRecord::TestCase
assert !BigNumber.table_exists?
GiveMeBigNumbers.up
+ BigNumber.reset_column_information
assert BigNumber.create(
:bank_balance => 1586.43,
@@ -368,6 +388,7 @@ class MigrationTest < ActiveRecord::TestCase
Thing.reset_table_name
Thing.reset_sequence_name
WeNeedThings.up
+ Thing.reset_column_information
assert Thing.create("content" => "hello world")
assert_equal "hello world", Thing.first.content
@@ -387,6 +408,7 @@ class MigrationTest < ActiveRecord::TestCase
ActiveRecord::Base.table_name_suffix = '_suffix'
Reminder.reset_table_name
Reminder.reset_sequence_name
+ Reminder.reset_column_information
WeNeedReminders.up
assert Reminder.create("content" => "hello world", "remind_at" => Time.now)
assert_equal "hello world", Reminder.first.content
@@ -398,8 +420,6 @@ class MigrationTest < ActiveRecord::TestCase
end
def test_create_table_with_binary_column
- Person.connection.drop_table :binary_testings rescue nil
-
assert_nothing_raised {
Person.connection.create_table :binary_testings do |t|
t.column "data", :binary, :null => false
@@ -411,33 +431,35 @@ class MigrationTest < ActiveRecord::TestCase
assert_nil data_column.default
- Person.connection.drop_table :binary_testings rescue nil
+ Person.connection.drop_table :binary_testings, if_exists: true
end
- def test_create_table_with_query
- Person.connection.drop_table :table_from_query_testings rescue nil
- Person.connection.create_table(:person, force: true)
+ unless mysql_enforcing_gtid_consistency?
+ def test_create_table_with_query
+ Person.connection.drop_table :table_from_query_testings rescue nil
+ Person.connection.create_table(:person, force: true)
- Person.connection.create_table :table_from_query_testings, as: "SELECT id FROM person"
+ Person.connection.create_table :table_from_query_testings, as: "SELECT id FROM person"
- columns = Person.connection.columns(:table_from_query_testings)
- assert_equal 1, columns.length
- assert_equal "id", columns.first.name
+ columns = Person.connection.columns(:table_from_query_testings)
+ assert_equal 1, columns.length
+ assert_equal "id", columns.first.name
- Person.connection.drop_table :table_from_query_testings rescue nil
- end
+ Person.connection.drop_table :table_from_query_testings rescue nil
+ end
- def test_create_table_with_query_from_relation
- Person.connection.drop_table :table_from_query_testings rescue nil
- Person.connection.create_table(:person, force: true)
+ def test_create_table_with_query_from_relation
+ Person.connection.drop_table :table_from_query_testings rescue nil
+ Person.connection.create_table(:person, force: true)
- Person.connection.create_table :table_from_query_testings, as: Person.select(:id)
+ Person.connection.create_table :table_from_query_testings, as: Person.select(:id)
- columns = Person.connection.columns(:table_from_query_testings)
- assert_equal 1, columns.length
- assert_equal "id", columns.first.name
+ columns = Person.connection.columns(:table_from_query_testings)
+ assert_equal 1, columns.length
+ assert_equal "id", columns.first.name
- Person.connection.drop_table :table_from_query_testings rescue nil
+ Person.connection.drop_table :table_from_query_testings rescue nil
+ end
end
if current_adapter? :OracleAdapter
@@ -480,12 +502,14 @@ class MigrationTest < ActiveRecord::TestCase
if current_adapter?(:MysqlAdapter, :Mysql2Adapter, :PostgreSQLAdapter)
def test_out_of_range_limit_should_raise
Person.connection.drop_table :test_limits rescue nil
- assert_raise(ActiveRecord::ActiveRecordError, "integer limit didn't raise") do
+ e = assert_raise(ActiveRecord::ActiveRecordError, "integer limit didn't raise") do
Person.connection.create_table :test_integer_limits, :force => true do |t|
t.column :bigone, :integer, :limit => 10
end
end
+ assert_match(/No integer type has byte size 10/, e.message)
+
unless current_adapter?(:PostgreSQLAdapter)
assert_raise(ActiveRecord::ActiveRecordError, "text limit didn't raise") do
Person.connection.create_table :test_text_limits, :force => true do |t|
@@ -561,7 +585,7 @@ if ActiveRecord::Base.connection.supports_bulk_alter?
t.string :qualification, :experience
t.integer :age, :default => 0
t.date :birthdate
- t.timestamps
+ t.timestamps null: true
end
end
@@ -687,6 +711,8 @@ if ActiveRecord::Base.connection.supports_bulk_alter?
end
class CopyMigrationsTest < ActiveRecord::TestCase
+ include ActiveSupport::Testing::Stream
+
def setup
end
@@ -896,13 +922,4 @@ class CopyMigrationsTest < ActiveRecord::TestCase
ActiveRecord::Base.logger = old
end
- private
-
- def quietly
- silence_stream(STDOUT) do
- silence_stream(STDERR) do
- yield
- end
- end
- end
end
diff --git a/activerecord/test/cases/migrator_test.rb b/activerecord/test/cases/migrator_test.rb
index 9568aa2217..2ff6938e7b 100644
--- a/activerecord/test/cases/migrator_test.rb
+++ b/activerecord/test/cases/migrator_test.rb
@@ -1,377 +1,388 @@
require "cases/helper"
require "cases/migration/helper"
-module ActiveRecord
- class MigratorTest < ActiveRecord::TestCase
- self.use_transactional_fixtures = false
+class MigratorTest < ActiveRecord::TestCase
+ self.use_transactional_tests = false
- # Use this class to sense if migrations have gone
- # up or down.
- class Sensor < ActiveRecord::Migration
- attr_reader :went_up, :went_down
+ # Use this class to sense if migrations have gone
+ # up or down.
+ class Sensor < ActiveRecord::Migration
+ attr_reader :went_up, :went_down
- def initialize name = self.class.name, version = nil
- super
- @went_up = false
- @went_down = false
- end
-
- def up; @went_up = true; end
- def down; @went_down = true; end
- end
-
- def setup
+ def initialize name = self.class.name, version = nil
super
- ActiveRecord::SchemaMigration.create_table
- ActiveRecord::SchemaMigration.delete_all rescue nil
+ @went_up = false
+ @went_down = false
end
- teardown do
- ActiveRecord::SchemaMigration.delete_all rescue nil
- ActiveRecord::Migration.verbose = true
- end
+ def up; @went_up = true; end
+ def down; @went_down = true; end
+ end
- def test_migrator_with_duplicate_names
- assert_raises(ActiveRecord::DuplicateMigrationNameError, "Multiple migrations have the name Chunky") do
- list = [Migration.new('Chunky'), Migration.new('Chunky')]
- ActiveRecord::Migrator.new(:up, list)
+ def setup
+ super
+ ActiveRecord::SchemaMigration.create_table
+ ActiveRecord::SchemaMigration.delete_all rescue nil
+ @verbose_was = ActiveRecord::Migration.verbose
+ ActiveRecord::Migration.message_count = 0
+ ActiveRecord::Migration.class_eval do
+ undef :puts
+ def puts(*)
+ ActiveRecord::Migration.message_count += 1
end
end
+ end
- def test_migrator_with_duplicate_versions
- assert_raises(ActiveRecord::DuplicateMigrationVersionError) do
- list = [Migration.new('Foo', 1), Migration.new('Bar', 1)]
- ActiveRecord::Migrator.new(:up, list)
+ teardown do
+ ActiveRecord::SchemaMigration.delete_all rescue nil
+ ActiveRecord::Migration.verbose = @verbose_was
+ ActiveRecord::Migration.class_eval do
+ undef :puts
+ def puts(*)
+ super
end
end
+ end
- def test_migrator_with_missing_version_numbers
- assert_raises(ActiveRecord::UnknownMigrationVersionError) do
- list = [Migration.new('Foo', 1), Migration.new('Bar', 2)]
- ActiveRecord::Migrator.new(:up, list, 3).run
- end
+ def test_migrator_with_duplicate_names
+ assert_raises(ActiveRecord::DuplicateMigrationNameError, "Multiple migrations have the name Chunky") do
+ list = [ActiveRecord::Migration.new('Chunky'), ActiveRecord::Migration.new('Chunky')]
+ ActiveRecord::Migrator.new(:up, list)
end
+ end
- def test_finds_migrations
- migrations = ActiveRecord::Migrator.migrations(MIGRATIONS_ROOT + "/valid")
+ def test_migrator_with_duplicate_versions
+ assert_raises(ActiveRecord::DuplicateMigrationVersionError) do
+ list = [ActiveRecord::Migration.new('Foo', 1), ActiveRecord::Migration.new('Bar', 1)]
+ ActiveRecord::Migrator.new(:up, list)
+ end
+ end
- [[1, 'ValidPeopleHaveLastNames'], [2, 'WeNeedReminders'], [3, 'InnocentJointable']].each_with_index do |pair, i|
- assert_equal migrations[i].version, pair.first
- assert_equal migrations[i].name, pair.last
- end
+ def test_migrator_with_missing_version_numbers
+ assert_raises(ActiveRecord::UnknownMigrationVersionError) do
+ list = [ActiveRecord::Migration.new('Foo', 1), ActiveRecord::Migration.new('Bar', 2)]
+ ActiveRecord::Migrator.new(:up, list, 3).run
end
+ end
- def test_finds_migrations_in_subdirectories
- migrations = ActiveRecord::Migrator.migrations(MIGRATIONS_ROOT + "/valid_with_subdirectories")
+ def test_finds_migrations
+ migrations = ActiveRecord::Migrator.migrations(MIGRATIONS_ROOT + "/valid")
- [[1, 'ValidPeopleHaveLastNames'], [2, 'WeNeedReminders'], [3, 'InnocentJointable']].each_with_index do |pair, i|
- assert_equal migrations[i].version, pair.first
- assert_equal migrations[i].name, pair.last
- end
+ [[1, 'ValidPeopleHaveLastNames'], [2, 'WeNeedReminders'], [3, 'InnocentJointable']].each_with_index do |pair, i|
+ assert_equal migrations[i].version, pair.first
+ assert_equal migrations[i].name, pair.last
end
+ end
- def test_finds_migrations_from_two_directories
- directories = [MIGRATIONS_ROOT + '/valid_with_timestamps', MIGRATIONS_ROOT + '/to_copy_with_timestamps']
- migrations = ActiveRecord::Migrator.migrations directories
-
- [[20090101010101, "PeopleHaveHobbies"],
- [20090101010202, "PeopleHaveDescriptions"],
- [20100101010101, "ValidWithTimestampsPeopleHaveLastNames"],
- [20100201010101, "ValidWithTimestampsWeNeedReminders"],
- [20100301010101, "ValidWithTimestampsInnocentJointable"]].each_with_index do |pair, i|
- assert_equal pair.first, migrations[i].version
- assert_equal pair.last, migrations[i].name
- end
- end
+ def test_finds_migrations_in_subdirectories
+ migrations = ActiveRecord::Migrator.migrations(MIGRATIONS_ROOT + "/valid_with_subdirectories")
- def test_finds_migrations_in_numbered_directory
- migrations = ActiveRecord::Migrator.migrations [MIGRATIONS_ROOT + '/10_urban']
- assert_equal 9, migrations[0].version
- assert_equal 'AddExpressions', migrations[0].name
+ [[1, 'ValidPeopleHaveLastNames'], [2, 'WeNeedReminders'], [3, 'InnocentJointable']].each_with_index do |pair, i|
+ assert_equal migrations[i].version, pair.first
+ assert_equal migrations[i].name, pair.last
end
+ end
- def test_relative_migrations
- list = Dir.chdir(MIGRATIONS_ROOT) do
- ActiveRecord::Migrator.migrations("valid")
- end
+ def test_finds_migrations_from_two_directories
+ directories = [MIGRATIONS_ROOT + '/valid_with_timestamps', MIGRATIONS_ROOT + '/to_copy_with_timestamps']
+ migrations = ActiveRecord::Migrator.migrations directories
+
+ [[20090101010101, "PeopleHaveHobbies"],
+ [20090101010202, "PeopleHaveDescriptions"],
+ [20100101010101, "ValidWithTimestampsPeopleHaveLastNames"],
+ [20100201010101, "ValidWithTimestampsWeNeedReminders"],
+ [20100301010101, "ValidWithTimestampsInnocentJointable"]].each_with_index do |pair, i|
+ assert_equal pair.first, migrations[i].version
+ assert_equal pair.last, migrations[i].name
+ end
+ end
- migration_proxy = list.find { |item|
- item.name == 'ValidPeopleHaveLastNames'
- }
- assert migration_proxy, 'should find pending migration'
+ def test_finds_migrations_in_numbered_directory
+ migrations = ActiveRecord::Migrator.migrations [MIGRATIONS_ROOT + '/10_urban']
+ 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")
end
- def test_finds_pending_migrations
- ActiveRecord::SchemaMigration.create!(:version => '1')
- migration_list = [ Migration.new('foo', 1), Migration.new('bar', 3) ]
- migrations = ActiveRecord::Migrator.new(:up, migration_list).pending_migrations
+ migration_proxy = list.find { |item|
+ item.name == 'ValidPeopleHaveLastNames'
+ }
+ assert migration_proxy, 'should find pending migration'
+ end
- assert_equal 1, migrations.size
- assert_equal migration_list.last, migrations.first
- end
+ def test_finds_pending_migrations
+ ActiveRecord::SchemaMigration.create!(:version => '1')
+ migration_list = [ActiveRecord::Migration.new('foo', 1), ActiveRecord::Migration.new('bar', 3)]
+ migrations = ActiveRecord::Migrator.new(:up, migration_list).pending_migrations
- def test_migrator_interleaved_migrations
- pass_one = [Sensor.new('One', 1)]
+ assert_equal 1, migrations.size
+ assert_equal migration_list.last, migrations.first
+ end
- ActiveRecord::Migrator.new(:up, pass_one).migrate
- assert pass_one.first.went_up
- assert_not pass_one.first.went_down
+ def test_migrator_interleaved_migrations
+ pass_one = [Sensor.new('One', 1)]
- pass_two = [Sensor.new('One', 1), Sensor.new('Three', 3)]
- ActiveRecord::Migrator.new(:up, pass_two).migrate
- assert_not pass_two[0].went_up
- assert pass_two[1].went_up
- assert pass_two.all? { |x| !x.went_down }
+ ActiveRecord::Migrator.new(:up, pass_one).migrate
+ assert pass_one.first.went_up
+ assert_not pass_one.first.went_down
- pass_three = [Sensor.new('One', 1),
- Sensor.new('Two', 2),
- Sensor.new('Three', 3)]
+ pass_two = [Sensor.new('One', 1), Sensor.new('Three', 3)]
+ ActiveRecord::Migrator.new(:up, pass_two).migrate
+ assert_not pass_two[0].went_up
+ assert pass_two[1].went_up
+ assert pass_two.all? { |x| !x.went_down }
- ActiveRecord::Migrator.new(:down, pass_three).migrate
- assert pass_three[0].went_down
- assert_not pass_three[1].went_down
- assert pass_three[2].went_down
- end
+ pass_three = [Sensor.new('One', 1),
+ Sensor.new('Two', 2),
+ Sensor.new('Three', 3)]
- def test_up_calls_up
- migrations = [Sensor.new(nil, 0), Sensor.new(nil, 1), Sensor.new(nil, 2)]
- ActiveRecord::Migrator.new(:up, migrations).migrate
- assert migrations.all? { |m| m.went_up }
- assert migrations.all? { |m| !m.went_down }
- assert_equal 2, ActiveRecord::Migrator.current_version
- end
+ ActiveRecord::Migrator.new(:down, pass_three).migrate
+ assert pass_three[0].went_down
+ assert_not pass_three[1].went_down
+ assert pass_three[2].went_down
+ end
- def test_down_calls_down
- test_up_calls_up
+ def test_up_calls_up
+ migrations = [Sensor.new(nil, 0), Sensor.new(nil, 1), Sensor.new(nil, 2)]
+ ActiveRecord::Migrator.new(:up, migrations).migrate
+ assert migrations.all?(&:went_up)
+ assert migrations.all? { |m| !m.went_down }
+ assert_equal 2, ActiveRecord::Migrator.current_version
+ end
- migrations = [Sensor.new(nil, 0), Sensor.new(nil, 1), Sensor.new(nil, 2)]
- ActiveRecord::Migrator.new(:down, migrations).migrate
- assert migrations.all? { |m| !m.went_up }
- assert migrations.all? { |m| m.went_down }
- assert_equal 0, ActiveRecord::Migrator.current_version
- end
+ def test_down_calls_down
+ test_up_calls_up
- def test_current_version
- ActiveRecord::SchemaMigration.create!(:version => '1000')
- assert_equal 1000, ActiveRecord::Migrator.current_version
- end
+ migrations = [Sensor.new(nil, 0), Sensor.new(nil, 1), Sensor.new(nil, 2)]
+ ActiveRecord::Migrator.new(:down, migrations).migrate
+ assert migrations.all? { |m| !m.went_up }
+ assert migrations.all?(&:went_down)
+ assert_equal 0, ActiveRecord::Migrator.current_version
+ end
- def test_migrator_one_up
- calls, migrations = sensors(3)
+ def test_current_version
+ ActiveRecord::SchemaMigration.create!(:version => '1000')
+ assert_equal 1000, ActiveRecord::Migrator.current_version
+ end
- ActiveRecord::Migrator.new(:up, migrations, 1).migrate
- assert_equal [[:up, 1]], calls
- calls.clear
+ def test_migrator_one_up
+ calls, migrations = sensors(3)
- ActiveRecord::Migrator.new(:up, migrations, 2).migrate
- assert_equal [[:up, 2]], calls
- end
+ ActiveRecord::Migrator.new(:up, migrations, 1).migrate
+ assert_equal [[:up, 1]], calls
+ calls.clear
- def test_migrator_one_down
- calls, migrations = sensors(3)
+ ActiveRecord::Migrator.new(:up, migrations, 2).migrate
+ assert_equal [[:up, 2]], calls
+ end
- ActiveRecord::Migrator.new(:up, migrations).migrate
- assert_equal [[:up, 1], [:up, 2], [:up, 3]], calls
- calls.clear
+ def test_migrator_one_down
+ calls, migrations = sensors(3)
- ActiveRecord::Migrator.new(:down, migrations, 1).migrate
+ ActiveRecord::Migrator.new(:up, migrations).migrate
+ assert_equal [[:up, 1], [:up, 2], [:up, 3]], calls
+ calls.clear
- assert_equal [[:down, 3], [:down, 2]], calls
- end
+ ActiveRecord::Migrator.new(:down, migrations, 1).migrate
- def test_migrator_one_up_one_down
- calls, migrations = sensors(3)
+ assert_equal [[:down, 3], [:down, 2]], calls
+ end
- ActiveRecord::Migrator.new(:up, migrations, 1).migrate
- assert_equal [[:up, 1]], calls
- calls.clear
+ def test_migrator_one_up_one_down
+ calls, migrations = sensors(3)
- ActiveRecord::Migrator.new(:down, migrations, 0).migrate
- assert_equal [[:down, 1]], calls
- end
+ ActiveRecord::Migrator.new(:up, migrations, 1).migrate
+ assert_equal [[:up, 1]], calls
+ calls.clear
- def test_migrator_double_up
- calls, migrations = sensors(3)
- assert_equal(0, ActiveRecord::Migrator.current_version)
+ ActiveRecord::Migrator.new(:down, migrations, 0).migrate
+ assert_equal [[:down, 1]], calls
+ end
- ActiveRecord::Migrator.new(:up, migrations, 1).migrate
- assert_equal [[:up, 1]], calls
- calls.clear
+ def test_migrator_double_up
+ calls, migrations = sensors(3)
+ assert_equal(0, ActiveRecord::Migrator.current_version)
- ActiveRecord::Migrator.new(:up, migrations, 1).migrate
- assert_equal [], calls
- end
+ ActiveRecord::Migrator.new(:up, migrations, 1).migrate
+ assert_equal [[:up, 1]], calls
+ calls.clear
- def test_migrator_double_down
- calls, migrations = sensors(3)
+ ActiveRecord::Migrator.new(:up, migrations, 1).migrate
+ assert_equal [], calls
+ end
- assert_equal(0, ActiveRecord::Migrator.current_version)
+ def test_migrator_double_down
+ calls, migrations = sensors(3)
- ActiveRecord::Migrator.new(:up, migrations, 1).run
- assert_equal [[:up, 1]], calls
- calls.clear
+ assert_equal(0, ActiveRecord::Migrator.current_version)
- ActiveRecord::Migrator.new(:down, migrations, 1).run
- assert_equal [[:down, 1]], calls
- calls.clear
+ ActiveRecord::Migrator.new(:up, migrations, 1).run
+ assert_equal [[:up, 1]], calls
+ calls.clear
- ActiveRecord::Migrator.new(:down, migrations, 1).run
- assert_equal [], calls
+ ActiveRecord::Migrator.new(:down, migrations, 1).run
+ assert_equal [[:down, 1]], calls
+ calls.clear
- assert_equal(0, ActiveRecord::Migrator.current_version)
- end
+ ActiveRecord::Migrator.new(:down, migrations, 1).run
+ assert_equal [], calls
- def test_migrator_verbosity
- _, migrations = sensors(3)
+ assert_equal(0, ActiveRecord::Migrator.current_version)
+ end
- ActiveRecord::Migrator.new(:up, migrations, 1).migrate
- assert_not_equal 0, ActiveRecord::Migration.message_count
+ def test_migrator_verbosity
+ _, migrations = sensors(3)
- ActiveRecord::Migration.message_count = 0
+ ActiveRecord::Migrator.new(:up, migrations, 1).migrate
+ assert_not_equal 0, ActiveRecord::Migration.message_count
- ActiveRecord::Migrator.new(:down, migrations, 0).migrate
- assert_not_equal 0, ActiveRecord::Migration.message_count
- ActiveRecord::Migration.message_count = 0
- end
+ ActiveRecord::Migration.message_count = 0
- def test_migrator_verbosity_off
- _, migrations = sensors(3)
+ ActiveRecord::Migrator.new(:down, migrations, 0).migrate
+ assert_not_equal 0, ActiveRecord::Migration.message_count
+ end
- ActiveRecord::Migration.message_count = 0
- ActiveRecord::Migration.verbose = false
- ActiveRecord::Migrator.new(:up, migrations, 1).migrate
- assert_equal 0, ActiveRecord::Migration.message_count
- ActiveRecord::Migrator.new(:down, migrations, 0).migrate
- assert_equal 0, ActiveRecord::Migration.message_count
- end
+ def test_migrator_verbosity_off
+ _, migrations = sensors(3)
- def test_target_version_zero_should_run_only_once
- calls, migrations = sensors(3)
+ ActiveRecord::Migration.message_count = 0
+ ActiveRecord::Migration.verbose = false
+ ActiveRecord::Migrator.new(:up, migrations, 1).migrate
+ assert_equal 0, ActiveRecord::Migration.message_count
+ ActiveRecord::Migrator.new(:down, migrations, 0).migrate
+ assert_equal 0, ActiveRecord::Migration.message_count
+ end
- # migrate up to 1
- ActiveRecord::Migrator.new(:up, migrations, 1).migrate
- assert_equal [[:up, 1]], calls
- calls.clear
+ def test_target_version_zero_should_run_only_once
+ calls, migrations = sensors(3)
- # migrate down to 0
- ActiveRecord::Migrator.new(:down, migrations, 0).migrate
- assert_equal [[:down, 1]], calls
- calls.clear
+ # migrate up to 1
+ ActiveRecord::Migrator.new(:up, migrations, 1).migrate
+ assert_equal [[:up, 1]], calls
+ calls.clear
- # migrate down to 0 again
- ActiveRecord::Migrator.new(:down, migrations, 0).migrate
- assert_equal [], calls
- end
+ # migrate down to 0
+ ActiveRecord::Migrator.new(:down, migrations, 0).migrate
+ assert_equal [[:down, 1]], calls
+ calls.clear
- def test_migrator_going_down_due_to_version_target
- calls, migrator = migrator_class(3)
+ # migrate down to 0 again
+ ActiveRecord::Migrator.new(:down, migrations, 0).migrate
+ assert_equal [], calls
+ end
- migrator.up("valid", 1)
- assert_equal [[:up, 1]], calls
- calls.clear
+ def test_migrator_going_down_due_to_version_target
+ calls, migrator = migrator_class(3)
- migrator.migrate("valid", 0)
- assert_equal [[:down, 1]], calls
- calls.clear
+ migrator.up("valid", 1)
+ assert_equal [[:up, 1]], calls
+ calls.clear
- migrator.migrate("valid")
- assert_equal [[:up, 1], [:up, 2], [:up, 3]], calls
- end
+ migrator.migrate("valid", 0)
+ assert_equal [[:down, 1]], calls
+ calls.clear
- def test_migrator_rollback
- _, migrator = migrator_class(3)
+ migrator.migrate("valid")
+ assert_equal [[:up, 1], [:up, 2], [:up, 3]], calls
+ end
- migrator.migrate("valid")
- assert_equal(3, ActiveRecord::Migrator.current_version)
+ def test_migrator_rollback
+ _, migrator = migrator_class(3)
- migrator.rollback("valid")
- assert_equal(2, ActiveRecord::Migrator.current_version)
+ migrator.migrate("valid")
+ assert_equal(3, ActiveRecord::Migrator.current_version)
- migrator.rollback("valid")
- assert_equal(1, ActiveRecord::Migrator.current_version)
+ migrator.rollback("valid")
+ assert_equal(2, ActiveRecord::Migrator.current_version)
- migrator.rollback("valid")
- assert_equal(0, ActiveRecord::Migrator.current_version)
+ migrator.rollback("valid")
+ assert_equal(1, ActiveRecord::Migrator.current_version)
- migrator.rollback("valid")
- assert_equal(0, ActiveRecord::Migrator.current_version)
- end
+ migrator.rollback("valid")
+ assert_equal(0, ActiveRecord::Migrator.current_version)
- def test_migrator_db_has_no_schema_migrations_table
- _, migrator = migrator_class(3)
+ migrator.rollback("valid")
+ assert_equal(0, ActiveRecord::Migrator.current_version)
+ end
- ActiveRecord::Base.connection.execute("DROP TABLE schema_migrations")
- assert_not ActiveRecord::Base.connection.table_exists?('schema_migrations')
- migrator.migrate("valid", 1)
- assert ActiveRecord::Base.connection.table_exists?('schema_migrations')
- end
+ def test_migrator_db_has_no_schema_migrations_table
+ _, migrator = migrator_class(3)
- def test_migrator_forward
- _, migrator = migrator_class(3)
- migrator.migrate("/valid", 1)
- assert_equal(1, ActiveRecord::Migrator.current_version)
+ ActiveRecord::Base.connection.drop_table "schema_migrations", if_exists: true
+ assert_not ActiveRecord::Base.connection.table_exists?('schema_migrations')
+ migrator.migrate("valid", 1)
+ assert ActiveRecord::Base.connection.table_exists?('schema_migrations')
+ end
- migrator.forward("/valid", 2)
- assert_equal(3, ActiveRecord::Migrator.current_version)
+ def test_migrator_forward
+ _, migrator = migrator_class(3)
+ migrator.migrate("/valid", 1)
+ assert_equal(1, ActiveRecord::Migrator.current_version)
- migrator.forward("/valid")
- assert_equal(3, ActiveRecord::Migrator.current_version)
- end
+ migrator.forward("/valid", 2)
+ assert_equal(3, ActiveRecord::Migrator.current_version)
- def test_only_loads_pending_migrations
- # migrate up to 1
- ActiveRecord::SchemaMigration.create!(:version => '1')
+ migrator.forward("/valid")
+ assert_equal(3, ActiveRecord::Migrator.current_version)
+ end
- calls, migrator = migrator_class(3)
- migrator.migrate("valid", nil)
+ def test_only_loads_pending_migrations
+ # migrate up to 1
+ ActiveRecord::SchemaMigration.create!(:version => '1')
- assert_equal [[:up, 2], [:up, 3]], calls
- end
+ calls, migrator = migrator_class(3)
+ migrator.migrate("valid", nil)
- def test_get_all_versions
- _, migrator = migrator_class(3)
+ assert_equal [[:up, 2], [:up, 3]], calls
+ end
- migrator.migrate("valid")
- assert_equal([1,2,3], ActiveRecord::Migrator.get_all_versions)
+ def test_get_all_versions
+ _, migrator = migrator_class(3)
- migrator.rollback("valid")
- assert_equal([1,2], ActiveRecord::Migrator.get_all_versions)
+ migrator.migrate("valid")
+ assert_equal([1,2,3], ActiveRecord::Migrator.get_all_versions)
- migrator.rollback("valid")
- assert_equal([1], ActiveRecord::Migrator.get_all_versions)
+ migrator.rollback("valid")
+ assert_equal([1,2], ActiveRecord::Migrator.get_all_versions)
- migrator.rollback("valid")
- assert_equal([], ActiveRecord::Migrator.get_all_versions)
- end
+ migrator.rollback("valid")
+ assert_equal([1], ActiveRecord::Migrator.get_all_versions)
- private
- def m(name, version, &block)
- x = Sensor.new name, version
- x.extend(Module.new {
- define_method(:up) { block.call(:up, x); super() }
- define_method(:down) { block.call(:down, x); super() }
- }) if block_given?
- end
+ migrator.rollback("valid")
+ assert_equal([], ActiveRecord::Migrator.get_all_versions)
+ end
+
+ private
+ def m(name, version)
+ x = Sensor.new name, version
+ x.extend(Module.new {
+ define_method(:up) { yield(:up, x); super() }
+ define_method(:down) { yield(:down, x); super() }
+ }) if block_given?
+ end
- def sensors(count)
- calls = []
- migrations = count.times.map { |i|
- m(nil, i + 1) { |c,migration|
- calls << [c, migration.version]
- }
+ def sensors(count)
+ calls = []
+ migrations = count.times.map { |i|
+ m(nil, i + 1) { |c,migration|
+ calls << [c, migration.version]
}
- [calls, migrations]
- end
+ }
+ [calls, migrations]
+ end
- def migrator_class(count)
- calls, migrations = sensors(count)
+ def migrator_class(count)
+ calls, migrations = sensors(count)
- migrator = Class.new(Migrator).extend(Module.new {
- define_method(:migrations) { |paths|
- migrations
- }
- })
- [calls, migrator]
- end
+ migrator = Class.new(ActiveRecord::Migrator).extend(Module.new {
+ define_method(:migrations) { |paths|
+ migrations
+ }
+ })
+ [calls, migrator]
end
end
diff --git a/activerecord/test/cases/mixin_test.rb b/activerecord/test/cases/mixin_test.rb
index 7ddb2bfee1..7ebdcac711 100644
--- a/activerecord/test/cases/mixin_test.rb
+++ b/activerecord/test/cases/mixin_test.rb
@@ -61,8 +61,6 @@ class TouchTest < ActiveRecord::TestCase
# Make sure Mixin.record_timestamps gets reset, even if this test fails,
# so that other tests do not fail because Mixin.record_timestamps == false
- rescue Exception => e
- raise e
ensure
Mixin.record_timestamps = true
end
diff --git a/activerecord/test/cases/modules_test.rb b/activerecord/test/cases/modules_test.rb
index e87773df94..7f31325f47 100644
--- a/activerecord/test/cases/modules_test.rb
+++ b/activerecord/test/cases/modules_test.rb
@@ -2,6 +2,7 @@ require "cases/helper"
require 'models/company_in_module'
require 'models/shop'
require 'models/developer'
+require 'models/computer'
class ModulesTest < ActiveRecord::TestCase
fixtures :accounts, :companies, :projects, :developers, :collections, :products, :variants
@@ -67,8 +68,7 @@ class ModulesTest < ActiveRecord::TestCase
end
end
- # need to add an eager loading condition to force the eager loading model into
- # the old join model, to test that. See http://dev.rubyonrails.org/ticket/9640
+ # An eager loading condition to force the eager loading model into the old join model.
def test_eager_loading_in_modules
clients = []
diff --git a/activerecord/test/cases/multiparameter_attributes_test.rb b/activerecord/test/cases/multiparameter_attributes_test.rb
index 14d4ef457d..ae18573126 100644
--- a/activerecord/test/cases/multiparameter_attributes_test.rb
+++ b/activerecord/test/cases/multiparameter_attributes_test.rb
@@ -199,6 +199,7 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase
def test_multiparameter_attributes_on_time_with_time_zone_aware_attributes
with_timezone_config default: :utc, aware_attributes: true, zone: -28800 do
+ Topic.reset_column_information
attributes = {
"written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24",
"written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00"
@@ -209,6 +210,8 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase
assert_equal Time.utc(2004, 6, 24, 16, 24, 0), topic.written_on.time
assert_equal Time.zone, topic.written_on.time_zone
end
+ ensure
+ Topic.reset_column_information
end
def test_multiparameter_attributes_on_time_with_time_zone_aware_attributes_false
@@ -227,6 +230,7 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase
def test_multiparameter_attributes_on_time_with_skip_time_zone_conversion_for_attributes
with_timezone_config default: :utc, aware_attributes: true, zone: -28800 do
Topic.skip_time_zone_conversion_for_attributes = [:written_on]
+ Topic.reset_column_information
attributes = {
"written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24",
"written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00"
@@ -238,21 +242,25 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase
end
ensure
Topic.skip_time_zone_conversion_for_attributes = []
+ Topic.reset_column_information
end
# Oracle does not have a TIME datatype.
unless current_adapter?(:OracleAdapter)
def test_multiparameter_attributes_on_time_only_column_with_time_zone_aware_attributes_does_not_do_time_zone_conversion
with_timezone_config default: :utc, aware_attributes: true, zone: -28800 do
+ Topic.reset_column_information
attributes = {
"bonus_time(1i)" => "2000", "bonus_time(2i)" => "1", "bonus_time(3i)" => "1",
"bonus_time(4i)" => "16", "bonus_time(5i)" => "24"
}
topic = Topic.find(1)
topic.attributes = attributes
- assert_equal Time.utc(2000, 1, 1, 16, 24, 0), topic.bonus_time
- assert topic.bonus_time.utc?
+ assert_equal Time.zone.local(2000, 1, 1, 16, 24, 0), topic.bonus_time
+ assert_not topic.bonus_time.utc?
end
+ ensure
+ Topic.reset_column_information
end
end
diff --git a/activerecord/test/cases/multiple_db_test.rb b/activerecord/test/cases/multiple_db_test.rb
index 3831de6ae3..39cdcf5403 100644
--- a/activerecord/test/cases/multiple_db_test.rb
+++ b/activerecord/test/cases/multiple_db_test.rb
@@ -4,7 +4,7 @@ require 'models/bird'
require 'models/course'
class MultipleDbTest < ActiveRecord::TestCase
- self.use_transactional_fixtures = false
+ self.use_transactional_tests = false
def setup
@courses = create_fixtures("courses") { Course.retrieve_connection }
@@ -94,6 +94,13 @@ class MultipleDbTest < ActiveRecord::TestCase
end
unless in_memory_db?
+ def test_count_on_custom_connection
+ ActiveRecord::Base.remove_connection
+ assert_equal 1, College.count
+ ensure
+ ActiveRecord::Base.establish_connection :arunit
+ end
+
def test_associations_should_work_when_model_has_no_connection
begin
ActiveRecord::Base.remove_connection
diff --git a/activerecord/test/cases/nested_attributes_test.rb b/activerecord/test/cases/nested_attributes_test.rb
index cf96c3fccf..93cb631a04 100644
--- a/activerecord/test/cases/nested_attributes_test.rb
+++ b/activerecord/test/cases/nested_attributes_test.rb
@@ -13,7 +13,7 @@ require 'active_support/hash_with_indifferent_access'
class TestNestedAttributesInGeneral < ActiveRecord::TestCase
teardown do
- Pirate.accepts_nested_attributes_for :ship, :allow_destroy => true, :reject_if => proc { |attributes| attributes.empty? }
+ Pirate.accepts_nested_attributes_for :ship, :allow_destroy => true, :reject_if => proc(&:empty?)
end
def test_base_should_have_an_empty_nested_attributes_options
@@ -273,10 +273,11 @@ class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase
end
def test_should_modify_an_existing_record_if_there_is_a_matching_composite_id
- @ship.stubs(:id).returns('ABC1X')
- @pirate.ship_attributes = { :id => @ship.id, :name => 'Davy Jones Gold Dagger' }
+ @ship.stub(:id, 'ABC1X') do
+ @pirate.ship_attributes = { :id => @ship.id, :name => 'Davy Jones Gold Dagger' }
- assert_equal 'Davy Jones Gold Dagger', @pirate.ship.name
+ assert_equal 'Davy Jones Gold Dagger', @pirate.ship.name
+ end
end
def test_should_destroy_an_existing_record_if_there_is_a_matching_id_and_destroy_is_truthy
@@ -300,13 +301,13 @@ class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase
end
def test_should_not_destroy_an_existing_record_if_allow_destroy_is_false
- Pirate.accepts_nested_attributes_for :ship, :allow_destroy => false, :reject_if => proc { |attributes| attributes.empty? }
+ Pirate.accepts_nested_attributes_for :ship, :allow_destroy => false, :reject_if => proc(&:empty?)
@pirate.update(ship_attributes: { id: @pirate.ship.id, _destroy: '1' })
assert_equal @ship, @pirate.reload.ship
- Pirate.accepts_nested_attributes_for :ship, :allow_destroy => true, :reject_if => proc { |attributes| attributes.empty? }
+ Pirate.accepts_nested_attributes_for :ship, :allow_destroy => true, :reject_if => proc(&:empty?)
end
def test_should_also_work_with_a_HashWithIndifferentAccess
@@ -457,10 +458,11 @@ class TestNestedAttributesOnABelongsToAssociation < ActiveRecord::TestCase
end
def test_should_modify_an_existing_record_if_there_is_a_matching_composite_id
- @pirate.stubs(:id).returns('ABC1X')
- @ship.pirate_attributes = { :id => @pirate.id, :catchphrase => 'Arr' }
+ @pirate.stub(:id, 'ABC1X') do
+ @ship.pirate_attributes = { :id => @pirate.id, :catchphrase => 'Arr' }
- assert_equal 'Arr', @ship.pirate.catchphrase
+ assert_equal 'Arr', @ship.pirate.catchphrase
+ end
end
def test_should_destroy_an_existing_record_if_there_is_a_matching_id_and_destroy_is_truthy
@@ -494,12 +496,12 @@ class TestNestedAttributesOnABelongsToAssociation < ActiveRecord::TestCase
end
def test_should_not_destroy_an_existing_record_if_allow_destroy_is_false
- Ship.accepts_nested_attributes_for :pirate, :allow_destroy => false, :reject_if => proc { |attributes| attributes.empty? }
+ Ship.accepts_nested_attributes_for :pirate, :allow_destroy => false, :reject_if => proc(&:empty?)
@ship.update(pirate_attributes: { id: @ship.pirate.id, _destroy: '1' })
assert_nothing_raised(ActiveRecord::RecordNotFound) { @ship.pirate.reload }
ensure
- Ship.accepts_nested_attributes_for :pirate, :allow_destroy => true, :reject_if => proc { |attributes| attributes.empty? }
+ Ship.accepts_nested_attributes_for :pirate, :allow_destroy => true, :reject_if => proc(&:empty?)
end
def test_should_work_with_update_as_well
@@ -638,17 +640,19 @@ module NestedAttributesOnACollectionAssociationTests
end
def test_should_take_a_hash_with_composite_id_keys_and_assign_the_attributes_to_the_associated_models
- @child_1.stubs(:id).returns('ABC1X')
- @child_2.stubs(:id).returns('ABC2X')
-
- @pirate.attributes = {
- association_getter => [
- { :id => @child_1.id, :name => 'Grace OMalley' },
- { :id => @child_2.id, :name => 'Privateers Greed' }
- ]
- }
+ @child_1.stub(:id, 'ABC1X') do
+ @child_2.stub(:id, 'ABC2X') do
- assert_equal ['Grace OMalley', 'Privateers Greed'], [@child_1.name, @child_2.name]
+ @pirate.attributes = {
+ association_getter => [
+ { :id => @child_1.id, :name => 'Grace OMalley' },
+ { :id => @child_2.id, :name => 'Privateers Greed' }
+ ]
+ }
+
+ assert_equal ['Grace OMalley', 'Privateers Greed'], [@child_1.name, @child_2.name]
+ end
+ end
end
def test_should_raise_RecordNotFound_if_an_id_is_given_but_doesnt_return_a_record
@@ -658,6 +662,16 @@ module NestedAttributesOnACollectionAssociationTests
assert_equal "Couldn't find #{@child_1.class.name} with ID=1234567890 for Pirate with ID=#{@pirate.id}", exception.message
end
+ def test_should_raise_RecordNotFound_if_an_id_belonging_to_a_different_record_is_given
+ other_pirate = Pirate.create! catchphrase: 'Ahoy!'
+ other_child = other_pirate.send(@association_name).create! name: 'Buccaneers Servant'
+
+ exception = assert_raise ActiveRecord::RecordNotFound do
+ @pirate.attributes = { association_getter => [{ id: other_child.id }] }
+ end
+ assert_equal "Couldn't find #{@child_1.class.name} with ID=#{other_child.id} for Pirate with ID=#{@pirate.id}", exception.message
+ end
+
def test_should_automatically_build_new_associated_models_for_each_entry_in_a_hash_where_the_id_is_missing
@pirate.send(@association_name).destroy_all
@pirate.reload.attributes = {
@@ -855,7 +869,7 @@ end
module NestedAttributesLimitTests
def teardown
- Pirate.accepts_nested_attributes_for :parrots, :allow_destroy => true, :reject_if => proc { |attributes| attributes.empty? }
+ Pirate.accepts_nested_attributes_for :parrots, :allow_destroy => true, :reject_if => proc(&:empty?)
end
def test_limit_with_less_records
@@ -943,7 +957,7 @@ class TestNestedAttributesWithNonStandardPrimaryKeys < ActiveRecord::TestCase
end
class TestHasOneAutosaveAssociationWhichItselfHasAutosaveAssociations < ActiveRecord::TestCase
- self.use_transactional_fixtures = false unless supports_savepoints?
+ self.use_transactional_tests = false unless supports_savepoints?
def setup
@pirate = Pirate.create!(:catchphrase => "My baby takes tha mornin' train!")
@@ -983,7 +997,7 @@ class TestHasOneAutosaveAssociationWhichItselfHasAutosaveAssociations < ActiveRe
end
class TestHasManyAutosaveAssociationWhichItselfHasAutosaveAssociations < ActiveRecord::TestCase
- self.use_transactional_fixtures = false unless supports_savepoints?
+ self.use_transactional_tests = false unless supports_savepoints?
def setup
@ship = Ship.create!(:name => "The good ship Dollypop")
@@ -1037,4 +1051,56 @@ class TestHasManyAutosaveAssociationWhichItselfHasAutosaveAssociations < ActiveR
ShipPart.create!(:ship => @ship, :name => "Stern")
assert_no_queries { @ship.valid? }
end
+
+ test "circular references do not perform unnecessary queries" do
+ ship = Ship.new(name: "The Black Rock")
+ part = ship.parts.build(name: "Stern")
+ ship.treasures.build(looter: part)
+
+ assert_queries 3 do
+ ship.save!
+ end
+ end
+
+ test "nested singular associations are validated" do
+ part = ShipPart.new(name: "Stern", ship_attributes: { name: nil })
+
+ assert_not part.valid?
+ assert_equal ["Ship name can't be blank"], part.errors.full_messages
+ end
+
+ class ProtectedParameters
+ def initialize(hash)
+ @hash = hash
+ end
+
+ def permitted?
+ true
+ end
+
+ def [](key)
+ @hash[key]
+ end
+
+ def to_h
+ @hash
+ end
+ end
+
+ test "strong params style objects can be assigned for singular associations" do
+ params = { name: "Stern", ship_attributes:
+ ProtectedParameters.new(name: "The Black Rock") }
+ part = ShipPart.new(params)
+
+ assert_equal "Stern", part.name
+ assert_equal "The Black Rock", part.ship.name
+ end
+
+ test "strong params style objects can be assigned for collection associations" do
+ params = { trinkets_attributes: ProtectedParameters.new("0" => ProtectedParameters.new(name: "Necklace"), "1" => ProtectedParameters.new(name: "Spoon")) }
+ part = ShipPart.new(params)
+
+ assert_equal "Necklace", part.trinkets[0].name
+ assert_equal "Spoon", part.trinkets[1].name
+ end
end
diff --git a/activerecord/test/cases/persistence_test.rb b/activerecord/test/cases/persistence_test.rb
index 2170fe6118..31686bde3f 100644
--- a/activerecord/test/cases/persistence_test.rb
+++ b/activerecord/test/cases/persistence_test.rb
@@ -8,6 +8,7 @@ require 'models/reply'
require 'models/category'
require 'models/company'
require 'models/developer'
+require 'models/computer'
require 'models/project'
require 'models/minimalistic'
require 'models/warehouse_thing'
@@ -16,6 +17,7 @@ require 'models/minivan'
require 'models/owner'
require 'models/person'
require 'models/pet'
+require 'models/ship'
require 'models/toy'
require 'rexml/document'
@@ -118,15 +120,24 @@ class PersistenceTest < ActiveRecord::TestCase
assert_equal 59, accounts(:signals37, :reload).credit_limit
end
+ def test_increment_updates_counter_in_db_using_offset
+ a1 = accounts(:signals37)
+ initial_credit = a1.credit_limit
+ a2 = Account.find(accounts(:signals37).id)
+ a1.increment!(:credit_limit)
+ a2.increment!(:credit_limit)
+ assert_equal initial_credit + 2, a1.reload.credit_limit
+ end
+
def test_destroy_all
conditions = "author_name = 'Mary'"
topics_by_mary = Topic.all.merge!(:where => conditions, :order => 'id').to_a
assert ! topics_by_mary.empty?
assert_difference('Topic.count', -topics_by_mary.size) do
- destroyed = Topic.destroy_all(conditions).sort_by(&:id)
+ destroyed = Topic.where(conditions).destroy_all.sort_by(&:id)
assert_equal topics_by_mary, destroyed
- assert destroyed.all? { |topic| topic.frozen? }, "destroyed topics should be frozen"
+ assert destroyed.all?(&:frozen?), "destroyed topics should be frozen"
end
end
@@ -136,7 +147,7 @@ class PersistenceTest < ActiveRecord::TestCase
assert_difference('Client.count', -2) do
destroyed = Client.destroy([2, 3]).sort_by(&:id)
assert_equal clients, destroyed
- assert destroyed.all? { |client| client.frozen? }, "destroyed clients should be frozen"
+ assert destroyed.all?(&:frozen?), "destroyed clients should be frozen"
end
end
@@ -251,8 +262,10 @@ class PersistenceTest < ActiveRecord::TestCase
def test_create_columns_not_equal_attributes
topic = Topic.instantiate(
- 'title' => 'Another New Topic',
- 'does_not_exist' => 'test'
+ 'attributes' => {
+ 'title' => 'Another New Topic',
+ 'does_not_exist' => 'test'
+ }
)
assert_nothing_raised { topic.save }
end
@@ -353,6 +366,22 @@ class PersistenceTest < ActiveRecord::TestCase
assert_equal("David", topic_reloaded.author_name)
end
+ def test_update_attribute_does_not_run_sql_if_attribute_is_not_changed
+ klass = Class.new(Topic) do
+ def self.name; 'Topic'; end
+ end
+ topic = klass.create(title: 'Another New Topic')
+ assert_queries(0) do
+ topic.update_attribute(:title, 'Another New Topic')
+ end
+ end
+
+ def test_update_does_not_run_sql_if_record_has_not_changed
+ topic = Topic.create(title: 'Another New Topic')
+ assert_queries(0) { topic.update(title: 'Another New Topic') }
+ assert_queries(0) { topic.update_attributes(title: 'Another New Topic') }
+ end
+
def test_delete
topic = Topic.find(1)
assert_equal topic, topic.delete, 'topic.delete did not return self'
@@ -877,4 +906,63 @@ class PersistenceTest < ActiveRecord::TestCase
assert_equal "Welcome to the weblog", post.title
assert_not post.new_record?
end
+
+ def test_reload_via_querycache
+ ActiveRecord::Base.connection.enable_query_cache!
+ ActiveRecord::Base.connection.clear_query_cache
+ assert ActiveRecord::Base.connection.query_cache_enabled, 'cache should be on'
+ parrot = Parrot.create(:name => 'Shane')
+
+ # populate the cache with the SELECT result
+ found_parrot = Parrot.find(parrot.id)
+ assert_equal parrot.id, found_parrot.id
+
+ # Manually update the 'name' attribute in the DB directly
+ assert_equal 1, ActiveRecord::Base.connection.query_cache.length
+ ActiveRecord::Base.uncached do
+ found_parrot.name = 'Mary'
+ found_parrot.save
+ end
+
+ # Now reload, and verify that it gets the DB version, and not the querycache version
+ found_parrot.reload
+ assert_equal 'Mary', found_parrot.name
+
+ found_parrot = Parrot.find(parrot.id)
+ assert_equal 'Mary', found_parrot.name
+ ensure
+ ActiveRecord::Base.connection.disable_query_cache!
+ end
+
+ class SaveTest < ActiveRecord::TestCase
+ self.use_transactional_tests = false
+
+ def test_save_touch_false
+ widget = Class.new(ActiveRecord::Base) do
+ connection.create_table :widgets, force: true do |t|
+ t.string :name
+ t.timestamps null: false
+ end
+
+ self.table_name = :widgets
+ end
+
+ instance = widget.create!({
+ name: 'Bob',
+ created_at: 1.day.ago,
+ updated_at: 1.day.ago
+ })
+
+ created_at = instance.created_at
+ updated_at = instance.updated_at
+
+ instance.name = 'Barb'
+ instance.save!(touch: false)
+ assert_equal instance.created_at, created_at
+ assert_equal instance.updated_at, updated_at
+ ensure
+ ActiveRecord::Base.connection.drop_table widget.table_name
+ widget.reset_column_information
+ end
+ end
end
diff --git a/activerecord/test/cases/pooled_connections_test.rb b/activerecord/test/cases/pooled_connections_test.rb
index 8eea10143f..daa3271777 100644
--- a/activerecord/test/cases/pooled_connections_test.rb
+++ b/activerecord/test/cases/pooled_connections_test.rb
@@ -3,7 +3,7 @@ require "models/project"
require "timeout"
class PooledConnectionsTest < ActiveRecord::TestCase
- self.use_transactional_fixtures = false
+ self.use_transactional_tests = false
def setup
@per_test_teardown = []
@@ -13,7 +13,7 @@ class PooledConnectionsTest < ActiveRecord::TestCase
teardown do
ActiveRecord::Base.clear_all_connections!
ActiveRecord::Base.establish_connection(@connection)
- @per_test_teardown.each {|td| td.call }
+ @per_test_teardown.each(&:call)
end
# Will deadlock due to lack of Monitor timeouts in 1.9
@@ -35,6 +35,22 @@ class PooledConnectionsTest < ActiveRecord::TestCase
end
end
+ def checkout_checkin_connections_loop(pool_size, loops)
+ ActiveRecord::Base.establish_connection(@connection.merge({:pool => pool_size, :checkout_timeout => 0.5}))
+ @connection_count = 0
+ @timed_out = 0
+ loops.times do
+ begin
+ conn = ActiveRecord::Base.connection_pool.checkout
+ ActiveRecord::Base.connection_pool.checkin conn
+ @connection_count += 1
+ ActiveRecord::Base.connection.tables
+ rescue ActiveRecord::ConnectionTimeoutError
+ @timed_out += 1
+ end
+ end
+ end
+
def test_pooled_connection_checkin_one
checkout_checkin_connections 1, 2
assert_equal 2, @connection_count
@@ -42,6 +58,20 @@ class PooledConnectionsTest < ActiveRecord::TestCase
assert_equal 1, ActiveRecord::Base.connection_pool.connections.size
end
+ def test_pooled_connection_checkin_two
+ checkout_checkin_connections_loop 2, 3
+ assert_equal 3, @connection_count
+ assert_equal 0, @timed_out
+ assert_equal 2, ActiveRecord::Base.connection_pool.connections.size
+ end
+
+ def test_pooled_connection_remove
+ ActiveRecord::Base.establish_connection(@connection.merge({:pool => 2, :checkout_timeout => 0.5}))
+ old_connection = ActiveRecord::Base.connection
+ extra_connection = ActiveRecord::Base.connection_pool.checkout
+ ActiveRecord::Base.connection_pool.remove(extra_connection)
+ assert_equal ActiveRecord::Base.connection, old_connection
+ end
private
diff --git a/activerecord/test/cases/primary_keys_test.rb b/activerecord/test/cases/primary_keys_test.rb
index b04df7ce43..5e4ba47988 100644
--- a/activerecord/test/cases/primary_keys_test.rb
+++ b/activerecord/test/cases/primary_keys_test.rb
@@ -1,10 +1,12 @@
require "cases/helper"
+require 'support/schema_dumping_helper'
require 'models/topic'
require 'models/reply'
require 'models/subscriber'
require 'models/movie'
require 'models/keyboard'
require 'models/mixed_case_monkey'
+require 'models/dashboard'
class PrimaryKeysTest < ActiveRecord::TestCase
fixtures :topics, :subscribers, :movies, :mixed_case_monkeys
@@ -164,10 +166,33 @@ class PrimaryKeysTest < ActiveRecord::TestCase
MixedCaseMonkey.reset_primary_key
assert_equal "monkeyID", MixedCaseMonkey.primary_key
end
+
+ def test_primary_key_update_with_custom_key_name
+ dashboard = Dashboard.create!(dashboard_id: '1')
+ dashboard.id = '2'
+ dashboard.save!
+
+ dashboard = Dashboard.first
+ assert_equal '2', dashboard.id
+ end
+
+ if current_adapter?(:PostgreSQLAdapter)
+ def test_serial_with_quoted_sequence_name
+ column = MixedCaseMonkey.columns_hash[MixedCaseMonkey.primary_key]
+ assert_equal "nextval('\"mixed_case_monkeys_monkeyID_seq\"'::regclass)", column.default_function
+ assert column.serial?
+ end
+
+ def test_serial_with_unquoted_sequence_name
+ column = Topic.columns_hash[Topic.primary_key]
+ assert_equal "nextval('topics_id_seq'::regclass)", column.default_function
+ assert column.serial?
+ end
+ end
end
class PrimaryKeyWithNoConnectionTest < ActiveRecord::TestCase
- self.use_transactional_fixtures = false
+ self.use_transactional_tests = false
unless in_memory_db?
def test_set_primary_key_with_no_connection
@@ -185,9 +210,67 @@ class PrimaryKeyWithNoConnectionTest < ActiveRecord::TestCase
end
end
+class PrimaryKeyAnyTypeTest < ActiveRecord::TestCase
+ include SchemaDumpingHelper
+
+ self.use_transactional_tests = false
+
+ class Barcode < ActiveRecord::Base
+ end
+
+ setup do
+ @connection = ActiveRecord::Base.connection
+ @connection.create_table(:barcodes, primary_key: "code", id: :string, limit: 42, force: true)
+ end
+
+ teardown do
+ @connection.drop_table(:barcodes) if @connection.table_exists? :barcodes
+ end
+
+ def test_any_type_primary_key
+ assert_equal "code", Barcode.primary_key
+
+ column_type = Barcode.type_for_attribute(Barcode.primary_key)
+ assert_equal :string, column_type.type
+ assert_equal 42, column_type.limit
+ end
+
+ test "schema dump primary key includes type and options" do
+ schema = dump_table_schema "barcodes"
+ assert_match %r{create_table "barcodes", primary_key: "code", id: :string, limit: 42}, schema
+ end
+end
+
+class CompositePrimaryKeyTest < ActiveRecord::TestCase
+ include SchemaDumpingHelper
+
+ self.use_transactional_tests = false
+
+ def setup
+ @connection = ActiveRecord::Base.connection
+ @connection.create_table(:barcodes, primary_key: ["region", "code"], force: true) do |t|
+ t.string :region
+ t.integer :code
+ end
+ end
+
+ def teardown
+ @connection.drop_table(:barcodes, if_exists: true)
+ end
+
+ def test_composite_primary_key
+ assert_equal ["region", "code"], @connection.primary_keys("barcodes")
+ end
+
+ def test_collectly_dump_composite_primary_key
+ schema = dump_table_schema "barcodes"
+ assert_match %r{create_table "barcodes", primary_key: \["region", "code"\]}, schema
+ end
+end
+
if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
class PrimaryKeyWithAnsiQuotesTest < ActiveRecord::TestCase
- self.use_transactional_fixtures = false
+ self.use_transactional_tests = false
def test_primary_key_method_with_ansi_quotes
con = ActiveRecord::Base.connection
@@ -199,28 +282,58 @@ if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
end
end
-if current_adapter?(:PostgreSQLAdapter)
+if current_adapter?(:PostgreSQLAdapter, :MysqlAdapter, :Mysql2Adapter)
class PrimaryKeyBigSerialTest < ActiveRecord::TestCase
- self.use_transactional_fixtures = false
+ include SchemaDumpingHelper
+
+ self.use_transactional_tests = false
class Widget < ActiveRecord::Base
end
setup do
@connection = ActiveRecord::Base.connection
- @connection.create_table(:widgets, id: :bigserial) { |t| }
+ if current_adapter?(:PostgreSQLAdapter)
+ @connection.create_table(:widgets, id: :bigserial, force: true)
+ else
+ @connection.create_table(:widgets, id: :bigint, force: true)
+ end
end
teardown do
- @connection.drop_table :widgets
+ @connection.drop_table :widgets, if_exists: true
+ Widget.reset_column_information
end
- def test_bigserial_primary_key
- assert_equal "id", Widget.primary_key
- assert_equal :integer, Widget.columns_hash[Widget.primary_key].type
+ test "primary key column type with bigserial" do
+ column_type = Widget.type_for_attribute(Widget.primary_key)
+ assert_equal :integer, column_type.type
+ assert_equal 8, column_type.limit
+ end
+ test "primary key with bigserial are automatically numbered" do
widget = Widget.create!
assert_not_nil widget.id
end
+
+ test "schema dump primary key with bigserial" do
+ schema = dump_table_schema "widgets"
+ if current_adapter?(:PostgreSQLAdapter)
+ assert_match %r{create_table "widgets", id: :bigserial}, schema
+ else
+ assert_match %r{create_table "widgets", id: :bigint}, schema
+ end
+ end
+
+ if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
+ test "primary key column type with options" do
+ @connection.create_table(:widgets, id: :primary_key, limit: 8, unsigned: true, force: true)
+ column = @connection.columns(:widgets).find { |c| c.name == 'id' }
+ assert column.auto_increment?
+ assert_equal :integer, column.type
+ assert_equal 8, column.limit
+ assert column.unsigned?
+ end
+ end
end
end
diff --git a/activerecord/test/cases/query_cache_test.rb b/activerecord/test/cases/query_cache_test.rb
index 9d89d6a1e8..d84653e4c9 100644
--- a/activerecord/test/cases/query_cache_test.rb
+++ b/activerecord/test/cases/query_cache_test.rb
@@ -184,7 +184,7 @@ class QueryCacheTest < ActiveRecord::TestCase
# Oracle adapter returns count() as Fixnum or Float
if current_adapter?(:OracleAdapter)
assert_kind_of Numeric, Task.connection.select_value("SELECT count(*) AS count_all FROM tasks")
- elsif current_adapter?(:SQLite3Adapter, :Mysql2Adapter)
+ elsif current_adapter?(:SQLite3Adapter, :Mysql2Adapter, :PostgreSQLAdapter)
# Future versions of the sqlite3 adapter will return numeric
assert_instance_of Fixnum,
Task.connection.select_value("SELECT count(*) AS count_all FROM tasks")
@@ -212,6 +212,38 @@ class QueryCacheTest < ActiveRecord::TestCase
ensure
ActiveRecord::Base.configurations = conf
end
+
+ def test_query_cache_doesnt_leak_cached_results_of_rolled_back_queries
+ ActiveRecord::Base.connection.enable_query_cache!
+ post = Post.first
+
+ Post.transaction do
+ post.update_attributes(title: 'rollback')
+ assert_equal 1, Post.where(title: 'rollback').to_a.count
+ raise ActiveRecord::Rollback
+ end
+
+ assert_equal 0, Post.where(title: 'rollback').to_a.count
+
+ ActiveRecord::Base.connection.uncached do
+ assert_equal 0, Post.where(title: 'rollback').to_a.count
+ end
+
+ begin
+ Post.transaction do
+ post.update_attributes(title: 'rollback')
+ assert_equal 1, Post.where(title: 'rollback').to_a.count
+ raise 'broken'
+ end
+ rescue Exception
+ end
+
+ assert_equal 0, Post.where(title: 'rollback').to_a.count
+
+ ActiveRecord::Base.connection.uncached do
+ assert_equal 0, Post.where(title: 'rollback').to_a.count
+ end
+ end
end
class QueryCacheExpiryTest < ActiveRecord::TestCase
@@ -230,61 +262,66 @@ class QueryCacheExpiryTest < ActiveRecord::TestCase
end
def test_find
- Task.connection.expects(:clear_query_cache).times(1)
+ assert_called(Task.connection, :clear_query_cache) do
+ assert !Task.connection.query_cache_enabled
+ Task.cache do
+ assert Task.connection.query_cache_enabled
+ Task.find(1)
- assert !Task.connection.query_cache_enabled
- Task.cache do
- assert Task.connection.query_cache_enabled
- Task.find(1)
+ Task.uncached do
+ assert !Task.connection.query_cache_enabled
+ Task.find(1)
+ end
- Task.uncached do
- assert !Task.connection.query_cache_enabled
- Task.find(1)
+ assert Task.connection.query_cache_enabled
end
-
- assert Task.connection.query_cache_enabled
+ assert !Task.connection.query_cache_enabled
end
- assert !Task.connection.query_cache_enabled
end
def test_update
- Task.connection.expects(:clear_query_cache).times(2)
- Task.cache do
- task = Task.find(1)
- task.starting = Time.now.utc
- task.save!
+ assert_called(Task.connection, :clear_query_cache, times: 2) do
+ Task.cache do
+ task = Task.find(1)
+ task.starting = Time.now.utc
+ task.save!
+ end
end
end
def test_destroy
- Task.connection.expects(:clear_query_cache).times(2)
- Task.cache do
- Task.find(1).destroy
+ assert_called(Task.connection, :clear_query_cache, times: 2) do
+ Task.cache do
+ Task.find(1).destroy
+ end
end
end
def test_insert
- ActiveRecord::Base.connection.expects(:clear_query_cache).times(2)
- Task.cache do
- Task.create!
+ assert_called(ActiveRecord::Base.connection, :clear_query_cache, times: 2) do
+ Task.cache do
+ Task.create!
+ end
end
end
def test_cache_is_expired_by_habtm_update
- ActiveRecord::Base.connection.expects(:clear_query_cache).times(2)
- ActiveRecord::Base.cache do
- c = Category.first
- p = Post.first
- p.categories << c
+ assert_called(ActiveRecord::Base.connection, :clear_query_cache, times: 2) do
+ ActiveRecord::Base.cache do
+ c = Category.first
+ p = Post.first
+ p.categories << c
+ end
end
end
def test_cache_is_expired_by_habtm_delete
- ActiveRecord::Base.connection.expects(:clear_query_cache).times(2)
- ActiveRecord::Base.cache do
- p = Post.find(1)
- assert p.categories.any?
- p.categories.delete_all
+ assert_called(ActiveRecord::Base.connection, :clear_query_cache, times: 2) do
+ ActiveRecord::Base.cache do
+ p = Post.find(1)
+ assert p.categories.any?
+ p.categories.delete_all
+ end
end
end
end
diff --git a/activerecord/test/cases/quoting_test.rb b/activerecord/test/cases/quoting_test.rb
index 1d6ae2f67f..6d91f96bf6 100644
--- a/activerecord/test/cases/quoting_test.rb
+++ b/activerecord/test/cases/quoting_test.rb
@@ -46,28 +46,28 @@ module ActiveRecord
def test_quoted_time_utc
with_timezone_config default: :utc do
- t = Time.now
+ t = Time.now.change(usec: 0)
assert_equal t.getutc.to_s(:db), @quoter.quoted_date(t)
end
end
def test_quoted_time_local
with_timezone_config default: :local do
- t = Time.now
+ t = Time.now.change(usec: 0)
assert_equal t.getlocal.to_s(:db), @quoter.quoted_date(t)
end
end
def test_quoted_time_crazy
with_timezone_config default: :asdfasdf do
- t = Time.now
+ t = Time.now.change(usec: 0)
assert_equal t.getlocal.to_s(:db), @quoter.quoted_date(t)
end
end
def test_quoted_datetime_utc
with_timezone_config default: :utc do
- t = DateTime.now
+ t = Time.now.change(usec: 0).to_datetime
assert_equal t.getutc.to_s(:db), @quoter.quoted_date(t)
end
end
@@ -76,7 +76,7 @@ module ActiveRecord
# DateTime doesn't define getlocal, so make sure it does nothing
def test_quoted_datetime_local
with_timezone_config default: :local do
- t = DateTime.now
+ t = Time.now.change(usec: 0).to_datetime
assert_equal t.to_s(:db), @quoter.quoted_date(t)
end
end
@@ -125,14 +125,11 @@ module ActiveRecord
end
def test_crazy_object
- crazy = Class.new.new
- expected = "'#{YAML.dump(crazy)}'"
- assert_equal expected, @quoter.quote(crazy, nil)
- end
-
- def test_crazy_object_calls_quote_string
- crazy = Class.new { def initialize; @lol = 'lo\l' end }.new
- assert_match "lo\\\\l", @quoter.quote(crazy, nil)
+ crazy = Object.new
+ e = assert_raises(TypeError) do
+ @quoter.quote(crazy, nil)
+ end
+ assert_equal "can't quote Object", e.message
end
def test_quote_string_no_column
diff --git a/activerecord/test/cases/readonly_test.rb b/activerecord/test/cases/readonly_test.rb
index 2afd25c989..5f6eb41240 100644
--- a/activerecord/test/cases/readonly_test.rb
+++ b/activerecord/test/cases/readonly_test.rb
@@ -3,9 +3,11 @@ require 'models/author'
require 'models/post'
require 'models/comment'
require 'models/developer'
+require 'models/computer'
require 'models/project'
require 'models/reader'
require 'models/person'
+require 'models/ship'
class ReadOnlyTest < ActiveRecord::TestCase
fixtures :authors, :posts, :comments, :developers, :projects, :developers_projects, :people, :readers
@@ -22,9 +24,15 @@ class ReadOnlyTest < ActiveRecord::TestCase
assert !dev.save
dev.name = 'Forbidden.'
end
- assert_raise(ActiveRecord::ReadOnlyRecord) { dev.save }
- assert_raise(ActiveRecord::ReadOnlyRecord) { dev.save! }
- assert_raise(ActiveRecord::ReadOnlyRecord) { dev.destroy }
+
+ e = assert_raise(ActiveRecord::ReadOnlyRecord) { dev.save }
+ assert_equal "Developer is marked as readonly", e.message
+
+ e = assert_raise(ActiveRecord::ReadOnlyRecord) { dev.save! }
+ assert_equal "Developer is marked as readonly", e.message
+
+ e = assert_raise(ActiveRecord::ReadOnlyRecord) { dev.destroy }
+ assert_equal "Developer is marked as readonly", e.message
end
diff --git a/activerecord/test/cases/reaper_test.rb b/activerecord/test/cases/reaper_test.rb
index f52fd22489..cccfc6774e 100644
--- a/activerecord/test/cases/reaper_test.rb
+++ b/activerecord/test/cases/reaper_test.rb
@@ -60,7 +60,7 @@ module ActiveRecord
def test_connection_pool_starts_reaper
spec = ActiveRecord::Base.connection_pool.spec.dup
- spec.config[:reaping_frequency] = 0.0001
+ spec.config[:reaping_frequency] = '0.0001'
pool = ConnectionPool.new spec
diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb
index 84abaf0291..9c04a41e69 100644
--- a/activerecord/test/cases/reflection_test.rb
+++ b/activerecord/test/cases/reflection_test.rb
@@ -23,6 +23,7 @@ require 'models/chef'
require 'models/department'
require 'models/cake_designer'
require 'models/drink_designer'
+require 'models/recipe'
class ReflectionTest < ActiveRecord::TestCase
include ActiveRecord::Reflection
@@ -50,13 +51,13 @@ class ReflectionTest < ActiveRecord::TestCase
end
def test_columns_are_returned_in_the_order_they_were_declared
- column_names = Topic.columns.map { |column| column.name }
+ column_names = Topic.columns.map(&:name)
assert_equal %w(id title author_name author_email_address written_on bonus_time last_read content important approved replies_count unique_replies_count parent_id parent_title type group created_at updated_at), column_names
end
def test_content_columns
content_columns = Topic.content_columns
- content_column_names = content_columns.map {|column| column.name}
+ content_column_names = content_columns.map(&:name)
assert_equal 13, content_columns.length
assert_equal %w(title author_name author_email_address written_on bonus_time last_read content important group approved parent_title created_at updated_at).sort, content_column_names.sort
end
@@ -80,10 +81,21 @@ class ReflectionTest < ActiveRecord::TestCase
assert_equal :integer, @first.column_for_attribute("id").type
end
- def test_non_existent_columns_return_nil
- assert_deprecated do
- assert_nil @first.column_for_attribute("attribute_that_doesnt_exist")
- end
+ def test_non_existent_columns_return_null_object
+ column = @first.column_for_attribute("attribute_that_doesnt_exist")
+ assert_instance_of ActiveRecord::ConnectionAdapters::NullColumn, column
+ assert_equal "attribute_that_doesnt_exist", column.name
+ assert_equal nil, column.sql_type
+ assert_equal nil, column.type
+ end
+
+ def test_non_existent_types_are_identity_types
+ type = @first.type_for_attribute("attribute_that_doesnt_exist")
+ object = Object.new
+
+ 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
@@ -209,6 +221,10 @@ class ReflectionTest < ActiveRecord::TestCase
assert_not_equal Object.new, Firm._reflections['clients']
end
+ def test_reflections_should_return_keys_as_strings
+ assert Category.reflections.keys.all? { |key| key.is_a? String }, "Model.reflections is expected to return string for keys"
+ end
+
def test_has_and_belongs_to_many_reflection
assert_equal :has_and_belongs_to_many, Category.reflections['posts'].macro
assert_equal :posts, Category.reflect_on_all_associations(:has_and_belongs_to_many).first.name
@@ -262,6 +278,22 @@ class ReflectionTest < ActiveRecord::TestCase
assert_equal 2, @hotel.chefs.size
end
+ def test_scope_chain_of_polymorphic_association_does_not_leak_into_other_hmt_associations
+ hotel = Hotel.create!
+ department = hotel.departments.create!
+ drink = department.chefs.create!(employable: DrinkDesigner.create!)
+ Recipe.create!(chef_id: drink.id, hotel_id: hotel.id)
+
+ expected_sql = capture_sql { hotel.recipes.to_a }
+
+ Hotel.reflect_on_association(:recipes).clear_association_scope_cache
+ hotel.reload
+ hotel.drink_designers.to_a
+ loaded_sql = capture_sql { hotel.recipes.to_a }
+
+ assert_equal expected_sql, loaded_sql
+ end
+
def test_nested?
assert !Author.reflect_on_association(:comments).nested?
assert Author.reflect_on_association(:tags).nested?
@@ -361,12 +393,14 @@ class ReflectionTest < ActiveRecord::TestCase
product = Struct.new(:table_name, :pluralize_table_names).new('products', true)
reflection = ActiveRecord::Reflection.create(:has_many, :categories, nil, {}, product)
- reflection.stubs(:klass).returns(category)
- assert_equal 'categories_products', reflection.join_table
+ reflection.stub(:klass, category) do
+ assert_equal 'categories_products', reflection.join_table
+ end
reflection = ActiveRecord::Reflection.create(:has_many, :products, nil, {}, category)
- reflection.stubs(:klass).returns(product)
- assert_equal 'categories_products', reflection.join_table
+ reflection.stub(:klass, product) do
+ assert_equal 'categories_products', reflection.join_table
+ end
end
def test_join_table_with_common_prefix
@@ -374,12 +408,14 @@ class ReflectionTest < ActiveRecord::TestCase
product = Struct.new(:table_name, :pluralize_table_names).new('catalog_products', true)
reflection = ActiveRecord::Reflection.create(:has_many, :categories, nil, {}, product)
- reflection.stubs(:klass).returns(category)
- assert_equal 'catalog_categories_products', reflection.join_table
+ reflection.stub(:klass, category) do
+ assert_equal 'catalog_categories_products', reflection.join_table
+ end
reflection = ActiveRecord::Reflection.create(:has_many, :products, nil, {}, category)
- reflection.stubs(:klass).returns(product)
- assert_equal 'catalog_categories_products', reflection.join_table
+ reflection.stub(:klass, product) do
+ assert_equal 'catalog_categories_products', reflection.join_table
+ end
end
def test_join_table_with_different_prefix
@@ -387,12 +423,14 @@ class ReflectionTest < ActiveRecord::TestCase
page = Struct.new(:table_name, :pluralize_table_names).new('content_pages', true)
reflection = ActiveRecord::Reflection.create(:has_many, :categories, nil, {}, page)
- reflection.stubs(:klass).returns(category)
- assert_equal 'catalog_categories_content_pages', reflection.join_table
+ reflection.stub(:klass, category) do
+ assert_equal 'catalog_categories_content_pages', reflection.join_table
+ end
reflection = ActiveRecord::Reflection.create(:has_many, :pages, nil, {}, category)
- reflection.stubs(:klass).returns(page)
- assert_equal 'catalog_categories_content_pages', reflection.join_table
+ reflection.stub(:klass, page) do
+ assert_equal 'catalog_categories_content_pages', reflection.join_table
+ end
end
def test_join_table_can_be_overridden
@@ -400,12 +438,14 @@ class ReflectionTest < ActiveRecord::TestCase
product = Struct.new(:table_name, :pluralize_table_names).new('products', true)
reflection = ActiveRecord::Reflection.create(:has_many, :categories, nil, { :join_table => 'product_categories' }, product)
- reflection.stubs(:klass).returns(category)
- assert_equal 'product_categories', reflection.join_table
+ reflection.stub(:klass, category) do
+ assert_equal 'product_categories', reflection.join_table
+ end
reflection = ActiveRecord::Reflection.create(:has_many, :products, nil, { :join_table => 'product_categories' }, category)
- reflection.stubs(:klass).returns(product)
- assert_equal 'product_categories', reflection.join_table
+ reflection.stub(:klass, product) do
+ assert_equal 'product_categories', reflection.join_table
+ end
end
def test_includes_accepts_symbols
diff --git a/activerecord/test/cases/relation/delegation_test.rb b/activerecord/test/cases/relation/delegation_test.rb
index 29c9d0e2af..989f4e1e5d 100644
--- a/activerecord/test/cases/relation/delegation_test.rb
+++ b/activerecord/test/cases/relation/delegation_test.rb
@@ -28,7 +28,7 @@ module ActiveRecord
module DelegationWhitelistBlacklistTests
ARRAY_DELEGATES = [
:+, :-, :|, :&, :[],
- :all?, :collect, :detect, :each, :each_cons, :each_with_index,
+ :all?, :collect, :compact, :detect, :each, :each_cons, :each_with_index,
:exclude?, :find_all, :flat_map, :group_by, :include?, :length,
:map, :none?, :one?, :partition, :reject, :reverse,
:sample, :second, :sort, :sort_by, :third,
diff --git a/activerecord/test/cases/relation/merging_test.rb b/activerecord/test/cases/relation/merging_test.rb
index 2b5c2fd5a4..0a2e874e4f 100644
--- a/activerecord/test/cases/relation/merging_test.rb
+++ b/activerecord/test/cases/relation/merging_test.rb
@@ -2,8 +2,10 @@ require 'cases/helper'
require 'models/author'
require 'models/comment'
require 'models/developer'
+require 'models/computer'
require 'models/post'
require 'models/project'
+require 'models/rating'
class RelationMergingTest < ActiveRecord::TestCase
fixtures :developers, :comments, :authors, :posts
@@ -80,26 +82,15 @@ class RelationMergingTest < ActiveRecord::TestCase
left = Post.where(title: "omg").where(comments_count: 1)
right = Post.where(title: "wtf").where(title: "bbq")
- expected = [left.bind_values[1]] + right.bind_values
+ expected = [left.bound_attributes[1]] + right.bound_attributes
merged = left.merge(right)
- assert_equal expected, merged.bind_values
+ assert_equal expected, merged.bound_attributes
assert !merged.to_sql.include?("omg")
assert merged.to_sql.include?("wtf")
assert merged.to_sql.include?("bbq")
end
- def test_merging_keeps_lhs_bind_parameters
- column = Post.columns_hash['id']
- binds = [[column, 20]]
-
- right = Post.where(id: 20)
- left = Post.where(id: 10)
-
- merged = left.merge(right)
- assert_equal binds, merged.bind_values
- end
-
def test_merging_reorders_bind_params
post = Post.first
right = Post.where(id: 1)
@@ -116,7 +107,7 @@ class RelationMergingTest < ActiveRecord::TestCase
end
class MergingDifferentRelationsTest < ActiveRecord::TestCase
- fixtures :posts, :authors
+ fixtures :posts, :authors, :developers
test "merging where relations" do
hello_by_bob = Post.where(body: "hello").joins(:author).
@@ -144,4 +135,16 @@ class MergingDifferentRelationsTest < ActiveRecord::TestCase
assert_equal ["Mary", "Mary", "Mary", "David"], posts_by_author_name
end
+
+ test "relation merging (using a proc argument)" do
+ dev = Developer.where(name: "Jamis").first
+
+ comment_1 = dev.comments.create!(body: "I'm Jamis", post: Post.first)
+ rating_1 = comment_1.ratings.create!
+
+ comment_2 = dev.comments.create!(body: "I'm John", post: Post.first)
+ comment_2.ratings.create!
+
+ assert_equal dev.ratings, [rating_1]
+ end
end
diff --git a/activerecord/test/cases/relation/mutation_test.rb b/activerecord/test/cases/relation/mutation_test.rb
index 1da5c36e1c..88d2dd55ab 100644
--- a/activerecord/test/cases/relation/mutation_test.rb
+++ b/activerecord/test/cases/relation/mutation_test.rb
@@ -18,10 +18,14 @@ module ActiveRecord
def attribute_alias?(name)
false
end
+
+ def sanitize_sql(sql)
+ sql
+ end
end
def relation
- @relation ||= Relation.new FakeKlass.new('posts'), Post.arel_table
+ @relation ||= Relation.new FakeKlass.new('posts'), Post.arel_table, Post.predicate_builder
end
(Relation::MULTI_VALUE_METHODS - [:references, :extending, :order, :unscope, :select]).each do |method|
@@ -51,9 +55,10 @@ module ActiveRecord
test '#order! on non-string does not attempt regexp match for references' do
obj = Object.new
- obj.expects(:=~).never
- assert relation.order!(obj)
- assert_equal [obj], relation.order_values
+ assert_not_called(obj, :=~) do
+ assert relation.order!(obj)
+ assert_equal [obj], relation.order_values
+ end
end
test '#references!' do
@@ -77,7 +82,7 @@ module ActiveRecord
assert_equal [], relation.extending_values
end
- (Relation::SINGLE_VALUE_METHODS - [:from, :lock, :reordering, :reverse_order, :create_with]).each do |method|
+ (Relation::SINGLE_VALUE_METHODS - [:lock, :reordering, :reverse_order, :create_with, :uniq]).each do |method|
test "##{method}!" do
assert relation.public_send("#{method}!", :foo).equal?(relation)
assert_equal :foo, relation.public_send("#{method}_value")
@@ -86,7 +91,7 @@ module ActiveRecord
test '#from!' do
assert relation.from!('foo').equal?(relation)
- assert_equal ['foo', nil], relation.from_value
+ assert_equal 'foo', relation.from_clause.value
end
test '#lock!' do
@@ -95,7 +100,7 @@ module ActiveRecord
end
test '#reorder!' do
- relation = self.relation.order('foo')
+ @relation = self.relation.order('foo')
assert relation.reorder!('bar').equal?(relation)
assert_equal ['bar'], relation.order_values
@@ -112,7 +117,7 @@ module ActiveRecord
end
test 'reverse_order!' do
- relation = Post.order('title ASC, comments_count DESC')
+ @relation = Post.order('title ASC, comments_count DESC')
relation.reverse_order!
@@ -132,12 +137,12 @@ module ActiveRecord
end
test 'test_merge!' do
- assert relation.merge!(where: :foo).equal?(relation)
- assert_equal [:foo], relation.where_values
+ assert relation.merge!(select: :foo).equal?(relation)
+ assert_equal [:foo], relation.select_values
end
test 'merge with a proc' do
- assert_equal [:foo], relation.merge(-> { where(:foo) }).where_values
+ assert_equal [:foo], relation.merge(-> { select(:foo) }).select_values
end
test 'none!' do
@@ -149,13 +154,22 @@ module ActiveRecord
test 'distinct!' do
relation.distinct! :foo
assert_equal :foo, relation.distinct_value
- assert_equal :foo, relation.uniq_value # deprecated access
+
+ assert_deprecated do
+ assert_equal :foo, relation.uniq_value # deprecated access
+ end
end
test 'uniq! was replaced by distinct!' do
- relation.uniq! :foo
+ assert_deprecated(/use distinct! instead/) do
+ relation.uniq! :foo
+ end
+
+ assert_deprecated(/use distinct_value instead/) do
+ assert_equal :foo, relation.uniq_value # deprecated access
+ end
+
assert_equal :foo, relation.distinct_value
- assert_equal :foo, relation.uniq_value # deprecated access
end
end
end
diff --git a/activerecord/test/cases/relation/or_test.rb b/activerecord/test/cases/relation/or_test.rb
new file mode 100644
index 0000000000..2006fc9611
--- /dev/null
+++ b/activerecord/test/cases/relation/or_test.rb
@@ -0,0 +1,84 @@
+require "cases/helper"
+require 'models/post'
+
+module ActiveRecord
+ class OrTest < ActiveRecord::TestCase
+ fixtures :posts
+
+ def test_or_with_relation
+ expected = Post.where('id = 1 or id = 2').to_a
+ assert_equal expected, Post.where('id = 1').or(Post.where('id = 2')).to_a
+ end
+
+ def test_or_identity
+ expected = Post.where('id = 1').to_a
+ assert_equal expected, Post.where('id = 1').or(Post.where('id = 1')).to_a
+ end
+
+ def test_or_with_null_left
+ expected = Post.where('id = 1').to_a
+ assert_equal expected, Post.none.or(Post.where('id = 1')).to_a
+ end
+
+ def test_or_with_null_right
+ expected = Post.where('id = 1').to_a
+ assert_equal expected, Post.where('id = 1').or(Post.none).to_a
+ end
+
+ def test_or_with_bind_params
+ assert_equal Post.find([1, 2]), Post.where(id: 1).or(Post.where(id: 2)).to_a
+ end
+
+ def test_or_with_null_both
+ expected = Post.none.to_a
+ assert_equal expected, Post.none.or(Post.none).to_a
+ end
+
+ def test_or_without_left_where
+ expected = Post.all
+ assert_equal expected, Post.or(Post.where('id = 1')).to_a
+ end
+
+ def test_or_without_right_where
+ expected = Post.all
+ assert_equal expected, Post.where('id = 1').or(Post.all).to_a
+ end
+
+ def test_or_preserves_other_querying_methods
+ expected = Post.where('id = 1 or id = 2 or id = 3').order('body asc').to_a
+ partial = Post.order('body asc')
+ assert_equal expected, partial.where('id = 1').or(partial.where(:id => [2, 3])).to_a
+ assert_equal expected, Post.order('body asc').where('id = 1').or(Post.order('body asc').where(:id => [2, 3])).to_a
+ end
+
+ def test_or_with_incompatible_relations
+ assert_raises ArgumentError do
+ Post.order('body asc').where('id = 1').or(Post.order('id desc').where(:id => [2, 3])).to_a
+ end
+ end
+
+ def test_or_when_grouping
+ groups = Post.where('id < 10').group('body').select('body, COUNT(*) AS c')
+ expected = groups.having("COUNT(*) > 1 OR body like 'Such%'").to_a.map {|o| [o.body, o.c] }
+ assert_equal expected, groups.having('COUNT(*) > 1').or(groups.having("body like 'Such%'")).to_a.map {|o| [o.body, o.c] }
+ end
+
+ def test_or_with_named_scope
+ expected = Post.where("id = 1 or body LIKE '\%a\%'").to_a
+ assert_equal expected, Post.where('id = 1').or(Post.containing_the_letter_a)
+ end
+
+ def test_or_inside_named_scope
+ expected = Post.where("body LIKE '\%a\%' OR title LIKE ?", "%'%").order('id DESC').to_a
+ assert_equal expected, Post.order(id: :desc).typographically_interesting
+ end
+
+ def test_or_on_loaded_relation
+ expected = Post.where('id = 1 or id = 2').to_a
+ p = Post.where('id = 1')
+ p.load
+ assert_equal p.loaded?, true
+ assert_equal expected, p.or(Post.where('id = 2')).to_a
+ end
+ end
+end
diff --git a/activerecord/test/cases/relation/predicate_builder_test.rb b/activerecord/test/cases/relation/predicate_builder_test.rb
index 4057835688..8f62014622 100644
--- a/activerecord/test/cases/relation/predicate_builder_test.rb
+++ b/activerecord/test/cases/relation/predicate_builder_test.rb
@@ -4,11 +4,13 @@ require 'models/topic'
module ActiveRecord
class PredicateBuilderTest < ActiveRecord::TestCase
def test_registering_new_handlers
- PredicateBuilder.register_handler(Regexp, proc do |column, value|
+ Topic.predicate_builder.register_handler(Regexp, proc do |column, value|
Arel::Nodes::InfixOperation.new('~', column, Arel.sql(value.source))
end)
- assert_match %r{["`]topics["`].["`]title["`] ~ rails}i, Topic.where(title: /rails/).to_sql
+ assert_match %r{["`]topics["`]\.["`]title["`] ~ rails}i, Topic.where(title: /rails/).to_sql
+ ensure
+ Topic.reset_column_information
end
end
end
diff --git a/activerecord/test/cases/relation/record_fetch_warning_test.rb b/activerecord/test/cases/relation/record_fetch_warning_test.rb
new file mode 100644
index 0000000000..62f0a7cc49
--- /dev/null
+++ b/activerecord/test/cases/relation/record_fetch_warning_test.rb
@@ -0,0 +1,28 @@
+require 'cases/helper'
+require 'models/post'
+
+module ActiveRecord
+ class RecordFetchWarningTest < ActiveRecord::TestCase
+ fixtures :posts
+
+ def test_warn_on_records_fetched_greater_than
+ original_logger = ActiveRecord::Base.logger
+ orginal_warn_on_records_fetched_greater_than = ActiveRecord::Base.warn_on_records_fetched_greater_than
+
+ log = StringIO.new
+ ActiveRecord::Base.logger = ActiveSupport::Logger.new(log)
+ ActiveRecord::Base.logger.level = Logger::WARN
+
+ require 'active_record/relation/record_fetch_warning'
+
+ ActiveRecord::Base.warn_on_records_fetched_greater_than = 1
+
+ Post.all.to_a
+
+ assert_match(/Query fetched/, log.string)
+ ensure
+ ActiveRecord::Base.logger = original_logger
+ ActiveRecord::Base.warn_on_records_fetched_greater_than = orginal_warn_on_records_fetched_greater_than
+ end
+ end
+end
diff --git a/activerecord/test/cases/relation/where_chain_test.rb b/activerecord/test/cases/relation/where_chain_test.rb
index b9e69bdb08..27bbd80f79 100644
--- a/activerecord/test/cases/relation/where_chain_test.rb
+++ b/activerecord/test/cases/relation/where_chain_test.rb
@@ -11,22 +11,11 @@ module ActiveRecord
@name = 'title'
end
- def test_not_eq
+ def test_not_inverts_where_clause
relation = Post.where.not(title: 'hello')
+ expected_where_clause = Post.where(title: 'hello').where_clause.invert
- assert_equal 1, relation.where_values.length
-
- value = relation.where_values.first
- bind = relation.bind_values.first
-
- assert_bound_ast value, Post.arel_table[@name], Arel::Nodes::NotEqual
- assert_equal 'hello', bind.last
- end
-
- def test_not_null
- expected = Post.arel_table[@name].not_eq(nil)
- relation = Post.where.not(title: nil)
- assert_equal([expected], relation.where_values)
+ assert_equal expected_where_clause, relation.where_clause
end
def test_not_with_nil
@@ -35,119 +24,82 @@ module ActiveRecord
end
end
- def test_not_in
- expected = Post.arel_table[@name].not_in(%w[hello goodbye])
- relation = Post.where.not(title: %w[hello goodbye])
- assert_equal([expected], relation.where_values)
- end
-
def test_association_not_eq
- expected = Comment.arel_table[@name].not_eq('hello')
+ expected = Arel::Nodes::Grouping.new(Comment.arel_table[@name].not_eq(Arel::Nodes::BindParam.new))
relation = Post.joins(:comments).where.not(comments: {title: 'hello'})
- assert_equal(expected.to_sql, relation.where_values.first.to_sql)
+ assert_equal(expected.to_sql, relation.where_clause.ast.to_sql)
end
def test_not_eq_with_preceding_where
relation = Post.where(title: 'hello').where.not(title: 'world')
+ expected_where_clause =
+ Post.where(title: 'hello').where_clause +
+ Post.where(title: 'world').where_clause.invert
- value = relation.where_values.first
- bind = relation.bind_values.first
- assert_bound_ast value, Post.arel_table[@name], Arel::Nodes::Equality
- assert_equal 'hello', bind.last
-
- value = relation.where_values.last
- bind = relation.bind_values.last
- assert_bound_ast value, Post.arel_table[@name], Arel::Nodes::NotEqual
- assert_equal 'world', bind.last
+ assert_equal expected_where_clause, relation.where_clause
end
def test_not_eq_with_succeeding_where
relation = Post.where.not(title: 'hello').where(title: 'world')
+ expected_where_clause =
+ Post.where(title: 'hello').where_clause.invert +
+ Post.where(title: 'world').where_clause
- value = relation.where_values.first
- bind = relation.bind_values.first
- assert_bound_ast value, Post.arel_table[@name], Arel::Nodes::NotEqual
- assert_equal 'hello', bind.last
-
- value = relation.where_values.last
- bind = relation.bind_values.last
- assert_bound_ast value, Post.arel_table[@name], Arel::Nodes::Equality
- assert_equal 'world', bind.last
- end
-
- def test_not_eq_with_string_parameter
- expected = Arel::Nodes::Not.new("title = 'hello'")
- relation = Post.where.not("title = 'hello'")
- assert_equal([expected], relation.where_values)
- end
-
- def test_not_eq_with_array_parameter
- expected = Arel::Nodes::Not.new("title = 'hello'")
- relation = Post.where.not(['title = ?', 'hello'])
- assert_equal([expected], relation.where_values)
+ assert_equal expected_where_clause, relation.where_clause
end
def test_chaining_multiple
relation = Post.where.not(author_id: [1, 2]).where.not(title: 'ruby on rails')
+ expected_where_clause =
+ Post.where(author_id: [1, 2]).where_clause.invert +
+ Post.where(title: 'ruby on rails').where_clause.invert
- expected = Post.arel_table['author_id'].not_in([1, 2])
- assert_equal(expected, relation.where_values[0])
-
- value = relation.where_values[1]
- bind = relation.bind_values.first
-
- assert_bound_ast value, Post.arel_table[@name], Arel::Nodes::NotEqual
- assert_equal 'ruby on rails', bind.last
+ assert_equal expected_where_clause, relation.where_clause
end
def test_rewhere_with_one_condition
relation = Post.where(title: 'hello').where(title: 'world').rewhere(title: 'alone')
+ expected = Post.where(title: 'alone')
- assert_equal 1, relation.where_values.size
- value = relation.where_values.first
- bind = relation.bind_values.first
- assert_bound_ast value, Post.arel_table[@name], Arel::Nodes::Equality
- assert_equal 'alone', bind.last
+ assert_equal expected.where_clause, relation.where_clause
end
def test_rewhere_with_multiple_overwriting_conditions
relation = Post.where(title: 'hello').where(body: 'world').rewhere(title: 'alone', body: 'again')
+ expected = Post.where(title: 'alone', body: 'again')
- assert_equal 2, relation.where_values.size
+ assert_equal expected.where_clause, relation.where_clause
+ end
- value = relation.where_values.first
- bind = relation.bind_values.first
- assert_bound_ast value, Post.arel_table['title'], Arel::Nodes::Equality
- assert_equal 'alone', bind.last
+ def test_rewhere_with_one_overwriting_condition_and_one_unrelated
+ relation = Post.where(title: 'hello').where(body: 'world').rewhere(title: 'alone')
+ expected = Post.where(body: 'world', title: 'alone')
- value = relation.where_values[1]
- bind = relation.bind_values[1]
- assert_bound_ast value, Post.arel_table['body'], Arel::Nodes::Equality
- assert_equal 'again', bind.last
+ assert_equal expected.where_clause, relation.where_clause
end
- def assert_bound_ast value, table, type
- assert_equal table, value.left
- assert_kind_of type, value
- assert_kind_of Arel::Nodes::BindParam, value.right
+ def test_rewhere_with_range
+ relation = Post.where(comments_count: 1..3).rewhere(comments_count: 3..5)
+
+ assert_equal Post.where(comments_count: 3..5), relation
end
- def test_rewhere_with_one_overwriting_condition_and_one_unrelated
- relation = Post.where(title: 'hello').where(body: 'world').rewhere(title: 'alone')
+ def test_rewhere_with_infinite_upper_bound_range
+ relation = Post.where(comments_count: 1..Float::INFINITY).rewhere(comments_count: 3..5)
- assert_equal 2, relation.where_values.size
+ assert_equal Post.where(comments_count: 3..5), relation
+ end
- value = relation.where_values.first
- bind = relation.bind_values.first
+ def test_rewhere_with_infinite_lower_bound_range
+ relation = Post.where(comments_count: -Float::INFINITY..1).rewhere(comments_count: 3..5)
- assert_bound_ast value, Post.arel_table['body'], Arel::Nodes::Equality
- assert_equal 'world', bind.last
+ assert_equal Post.where(comments_count: 3..5), relation
+ end
- value = relation.where_values.second
- bind = relation.bind_values.second
+ def test_rewhere_with_infinite_range
+ relation = Post.where(comments_count: -Float::INFINITY..Float::INFINITY).rewhere(comments_count: 3..5)
- assert_bound_ast value, Post.arel_table['title'], Arel::Nodes::Equality
- assert_equal 'alone', bind.last
+ assert_equal Post.where(comments_count: 3..5), relation
end
end
end
diff --git a/activerecord/test/cases/relation/where_clause_test.rb b/activerecord/test/cases/relation/where_clause_test.rb
new file mode 100644
index 0000000000..c20ed94d90
--- /dev/null
+++ b/activerecord/test/cases/relation/where_clause_test.rb
@@ -0,0 +1,182 @@
+require "cases/helper"
+
+class ActiveRecord::Relation
+ class WhereClauseTest < ActiveRecord::TestCase
+ test "+ combines two where clauses" do
+ first_clause = WhereClause.new([table["id"].eq(bind_param)], [["id", 1]])
+ second_clause = WhereClause.new([table["name"].eq(bind_param)], [["name", "Sean"]])
+ combined = WhereClause.new(
+ [table["id"].eq(bind_param), table["name"].eq(bind_param)],
+ [["id", 1], ["name", "Sean"]],
+ )
+
+ assert_equal combined, first_clause + second_clause
+ end
+
+ test "+ is associative, but not commutative" do
+ a = WhereClause.new(["a"], ["bind a"])
+ b = WhereClause.new(["b"], ["bind b"])
+ c = WhereClause.new(["c"], ["bind c"])
+
+ assert_equal a + (b + c), (a + b) + c
+ assert_not_equal a + b, b + a
+ end
+
+ test "an empty where clause is the identity value for +" do
+ clause = WhereClause.new([table["id"].eq(bind_param)], [["id", 1]])
+
+ assert_equal clause, clause + WhereClause.empty
+ end
+
+ test "merge combines two where clauses" do
+ a = WhereClause.new([table["id"].eq(1)], [])
+ b = WhereClause.new([table["name"].eq("Sean")], [])
+ expected = WhereClause.new([table["id"].eq(1), table["name"].eq("Sean")], [])
+
+ assert_equal expected, a.merge(b)
+ end
+
+ test "merge keeps the right side, when two equality clauses reference the same column" do
+ a = WhereClause.new([table["id"].eq(1), table["name"].eq("Sean")], [])
+ b = WhereClause.new([table["name"].eq("Jim")], [])
+ expected = WhereClause.new([table["id"].eq(1), table["name"].eq("Jim")], [])
+
+ assert_equal expected, a.merge(b)
+ end
+
+ test "merge removes bind parameters matching overlapping equality clauses" do
+ a = WhereClause.new(
+ [table["id"].eq(bind_param), table["name"].eq(bind_param)],
+ [attribute("id", 1), attribute("name", "Sean")],
+ )
+ b = WhereClause.new(
+ [table["name"].eq(bind_param)],
+ [attribute("name", "Jim")]
+ )
+ expected = WhereClause.new(
+ [table["id"].eq(bind_param), table["name"].eq(bind_param)],
+ [attribute("id", 1), attribute("name", "Jim")],
+ )
+
+ assert_equal expected, a.merge(b)
+ end
+
+ test "merge allows for columns with the same name from different tables" do
+ skip "This is not possible as of 4.2, and the binds do not yet contain sufficient information for this to happen"
+ # We might be able to change the implementation to remove conflicts by index, rather than column name
+ end
+
+ test "a clause knows if it is empty" do
+ assert WhereClause.empty.empty?
+ assert_not WhereClause.new(["anything"], []).empty?
+ end
+
+ test "invert cannot handle nil" do
+ where_clause = WhereClause.new([nil], [])
+
+ assert_raises ArgumentError do
+ where_clause.invert
+ end
+ end
+
+ test "invert replaces each part of the predicate with its inverse" do
+ random_object = Object.new
+ original = WhereClause.new([
+ table["id"].in([1, 2, 3]),
+ table["id"].eq(1),
+ "sql literal",
+ random_object
+ ], [])
+ expected = WhereClause.new([
+ table["id"].not_in([1, 2, 3]),
+ table["id"].not_eq(1),
+ Arel::Nodes::Not.new(Arel::Nodes::SqlLiteral.new("sql literal")),
+ Arel::Nodes::Not.new(random_object)
+ ], [])
+
+ assert_equal expected, original.invert
+ end
+
+ test "accept removes binary predicates referencing a given column" do
+ where_clause = WhereClause.new([
+ table["id"].in([1, 2, 3]),
+ table["name"].eq(bind_param),
+ table["age"].gteq(bind_param),
+ ], [
+ attribute("name", "Sean"),
+ attribute("age", 30),
+ ])
+ expected = WhereClause.new([table["age"].gteq(bind_param)], [attribute("age", 30)])
+
+ assert_equal expected, where_clause.except("id", "name")
+ end
+
+ test "ast groups its predicates with AND" do
+ predicates = [
+ table["id"].in([1, 2, 3]),
+ table["name"].eq(bind_param),
+ ]
+ where_clause = WhereClause.new(predicates, [])
+ expected = Arel::Nodes::And.new(predicates)
+
+ assert_equal expected, where_clause.ast
+ end
+
+ test "ast wraps any SQL literals in parenthesis" do
+ random_object = Object.new
+ where_clause = WhereClause.new([
+ table["id"].in([1, 2, 3]),
+ "foo = bar",
+ random_object,
+ ], [])
+ expected = Arel::Nodes::And.new([
+ table["id"].in([1, 2, 3]),
+ Arel::Nodes::Grouping.new(Arel.sql("foo = bar")),
+ Arel::Nodes::Grouping.new(random_object),
+ ])
+
+ assert_equal expected, where_clause.ast
+ end
+
+ test "ast removes any empty strings" do
+ where_clause = WhereClause.new([table["id"].in([1, 2, 3])], [])
+ where_clause_with_empty = WhereClause.new([table["id"].in([1, 2, 3]), ''], [])
+
+ assert_equal where_clause.ast, where_clause_with_empty.ast
+ end
+
+ test "or joins the two clauses using OR" do
+ where_clause = WhereClause.new([table["id"].eq(bind_param)], [attribute("id", 1)])
+ other_clause = WhereClause.new([table["name"].eq(bind_param)], [attribute("name", "Sean")])
+ expected_ast =
+ Arel::Nodes::Grouping.new(
+ Arel::Nodes::Or.new(table["id"].eq(bind_param), table["name"].eq(bind_param))
+ )
+ expected_binds = where_clause.binds + other_clause.binds
+
+ assert_equal expected_ast.to_sql, where_clause.or(other_clause).ast.to_sql
+ assert_equal expected_binds, where_clause.or(other_clause).binds
+ end
+
+ test "or returns an empty where clause when either side is empty" do
+ where_clause = WhereClause.new([table["id"].eq(bind_param)], [attribute("id", 1)])
+
+ assert_equal WhereClause.empty, where_clause.or(WhereClause.empty)
+ assert_equal WhereClause.empty, WhereClause.empty.or(where_clause)
+ end
+
+ private
+
+ def table
+ Arel::Table.new("table")
+ end
+
+ def bind_param
+ Arel::Nodes::BindParam.new
+ end
+
+ def attribute(name, value)
+ ActiveRecord::Attribute.with_cast_value(name, value, ActiveRecord::Type::Value.new)
+ end
+ end
+end
diff --git a/activerecord/test/cases/relation/where_test.rb b/activerecord/test/cases/relation/where_test.rb
index a6a36a6fd9..bc6378b90e 100644
--- a/activerecord/test/cases/relation/where_test.rb
+++ b/activerecord/test/cases/relation/where_test.rb
@@ -1,16 +1,20 @@
require "cases/helper"
-require 'models/author'
-require 'models/price_estimate'
-require 'models/treasure'
-require 'models/post'
-require 'models/comment'
-require 'models/edge'
-require 'models/topic'
-require 'models/binary'
+require "models/author"
+require "models/binary"
+require "models/cake_designer"
+require "models/chef"
+require "models/comment"
+require "models/edge"
+require "models/essay"
+require "models/post"
+require "models/price_estimate"
+require "models/topic"
+require "models/treasure"
+require "models/vertex"
module ActiveRecord
class WhereTest < ActiveRecord::TestCase
- fixtures :posts, :edges, :authors, :binaries
+ fixtures :posts, :edges, :authors, :binaries, :essays
def test_where_copies_bind_params
author = authors(:david)
@@ -25,6 +29,24 @@ module ActiveRecord
}
end
+ def test_where_copies_bind_params_in_the_right_order
+ author = authors(:david)
+ posts = author.posts.where.not(id: 1)
+ joined = Post.where(id: posts, title: posts.first.title)
+
+ assert_equal joined, [posts.first]
+ end
+
+ def test_where_copies_arel_bind_params
+ chef = Chef.create!
+ CakeDesigner.create!(chef: chef)
+
+ cake_designers = CakeDesigner.joins(:chef).where(chefs: { id: chef.id })
+ chefs = Chef.where(employable: cake_designers)
+
+ assert_equal [chef], chefs.to_a
+ end
+
def test_rewhere_on_root
assert_equal posts(:welcome), Post.rewhere(title: 'Welcome to the weblog').first
end
@@ -61,6 +83,15 @@ module ActiveRecord
assert_equal expected.to_sql, actual.to_sql
end
+ def test_belongs_to_nested_where_with_relation
+ author = authors(:david)
+
+ expected = Author.where(id: author ).joins(:posts)
+ actual = Author.where(posts: { author_id: Author.where(id: author.id) }).joins(:posts)
+
+ assert_equal expected.to_a, actual.to_a
+ end
+
def test_polymorphic_shallow_where
treasure = Treasure.new
treasure.id = 1
@@ -210,5 +241,70 @@ module ActiveRecord
count = Binary.where(:data => 0).count
assert_equal 0, count
end
+
+ def test_where_on_association_with_custom_primary_key
+ author = authors(:david)
+ essay = Essay.where(writer: author).first
+
+ assert_equal essays(:david_modest_proposal), essay
+ end
+
+ def test_where_on_association_with_custom_primary_key_with_relation
+ author = authors(:david)
+ essay = Essay.where(writer: Author.where(id: author.id)).first
+
+ assert_equal essays(:david_modest_proposal), essay
+ end
+
+ def test_where_on_association_with_relation_performs_subselect_not_two_queries
+ author = authors(:david)
+
+ assert_queries(1) do
+ Essay.where(writer: Author.where(id: author.id)).to_a
+ end
+ end
+
+ def test_where_on_association_with_custom_primary_key_with_array_of_base
+ author = authors(:david)
+ essay = Essay.where(writer: [author]).first
+
+ assert_equal essays(:david_modest_proposal), essay
+ end
+
+ def test_where_on_association_with_custom_primary_key_with_array_of_ids
+ essay = Essay.where(writer: ["David"]).first
+
+ assert_equal essays(:david_modest_proposal), essay
+ end
+
+ def test_where_with_strong_parameters
+ protected_params = Class.new do
+ attr_reader :permitted
+ alias :permitted? :permitted
+
+ def initialize(parameters)
+ @parameters = parameters
+ @permitted = false
+ end
+
+ def to_h
+ @parameters
+ end
+
+ def permit!
+ @permitted = true
+ self
+ end
+ end
+
+ author = authors(:david)
+ params = protected_params.new(name: author.name)
+ assert_raises(ActiveModel::ForbiddenAttributesError) { Author.where(params) }
+ assert_equal author, Author.where(params.permit!).first
+ end
+
+ def test_where_with_unsupported_arguments
+ assert_raises(ArgumentError) { Author.where(42) }
+ end
end
end
diff --git a/activerecord/test/cases/relation_test.rb b/activerecord/test/cases/relation_test.rb
index 3280945d09..675149556f 100644
--- a/activerecord/test/cases/relation_test.rb
+++ b/activerecord/test/cases/relation_test.rb
@@ -23,19 +23,19 @@ module ActiveRecord
end
def test_construction
- relation = Relation.new FakeKlass, :b
+ relation = Relation.new(FakeKlass, :b, nil)
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
+ relation = Relation.new(FakeKlass, :b, nil)
assert_equal FakeKlass, relation.model
end
def test_initialize_single_values
- relation = Relation.new FakeKlass, :b
+ relation = Relation.new(FakeKlass, :b, nil)
(Relation::SINGLE_VALUE_METHODS - [:create_with]).each do |method|
assert_nil relation.send("#{method}_value"), method.to_s
end
@@ -43,39 +43,37 @@ module ActiveRecord
end
def test_multi_value_initialize
- relation = Relation.new FakeKlass, :b
+ relation = Relation.new(FakeKlass, :b, nil)
Relation::MULTI_VALUE_METHODS.each do |method|
assert_equal [], relation.send("#{method}_values"), method.to_s
end
end
def test_extensions
- relation = Relation.new FakeKlass, :b
+ relation = Relation.new(FakeKlass, :b, nil)
assert_equal [], relation.extensions
end
def test_empty_where_values_hash
- relation = Relation.new FakeKlass, :b
- assert_equal({}, relation.where_values_hash)
-
- relation.where! :hello
+ relation = Relation.new(FakeKlass, :b, nil)
assert_equal({}, relation.where_values_hash)
end
def test_has_values
- relation = Relation.new Post, Post.arel_table
+ relation = Relation.new(Post, Post.arel_table, Post.predicate_builder)
relation.where! relation.table[:id].eq(10)
assert_equal({:id => 10}, relation.where_values_hash)
end
def test_values_wrong_table
- relation = Relation.new Post, Post.arel_table
+ relation = Relation.new(Post, Post.arel_table, Post.predicate_builder)
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
+ relation = Relation.new(Post, Post.arel_table, Post.predicate_builder)
+ # FIXME: Remove the Arel::Nodes::Quoted in Rails 5.1
left = relation.table[:id].eq(10)
right = relation.table[:id].eq(10)
combine = left.and right
@@ -84,24 +82,25 @@ module ActiveRecord
end
def test_table_name_delegates_to_klass
- relation = Relation.new FakeKlass.new('posts'), :b
+ relation = Relation.new(FakeKlass.new('posts'), :b, Post.predicate_builder)
assert_equal 'posts', relation.table_name
end
def test_scope_for_create
- relation = Relation.new FakeKlass, :b
+ relation = Relation.new(FakeKlass, :b, nil)
assert_equal({}, relation.scope_for_create)
end
def test_create_with_value
- relation = Relation.new Post, Post.arel_table
+ relation = Relation.new(Post, Post.arel_table, Post.predicate_builder)
hash = { :hello => 'world' }
relation.create_with_value = hash
assert_equal hash, relation.scope_for_create
end
def test_create_with_value_with_wheres
- relation = Relation.new Post, Post.arel_table
+ relation = Relation.new(Post, Post.arel_table, Post.predicate_builder)
+ # FIXME: Remove the Arel::Nodes::Quoted in Rails 5.1
relation.where! relation.table[:id].eq(10)
relation.create_with_value = {:hello => 'world'}
assert_equal({:hello => 'world', :id => 10}, relation.scope_for_create)
@@ -109,9 +108,10 @@ module ActiveRecord
# FIXME: is this really wanted or expected behavior?
def test_scope_for_create_is_cached
- relation = Relation.new Post, Post.arel_table
+ relation = Relation.new(Post, Post.arel_table, Post.predicate_builder)
assert_equal({}, relation.scope_for_create)
+ # FIXME: Remove the Arel::Nodes::Quoted in Rails 5.1
relation.where! relation.table[:id].eq(10)
assert_equal({}, relation.scope_for_create)
@@ -126,62 +126,72 @@ module ActiveRecord
end
def test_empty_eager_loading?
- relation = Relation.new FakeKlass, :b
+ relation = Relation.new(FakeKlass, :b, nil)
assert !relation.eager_loading?
end
def test_eager_load_values
- relation = Relation.new FakeKlass, :b
+ relation = Relation.new(FakeKlass, :b, nil)
relation.eager_load! :b
assert relation.eager_loading?
end
def test_references_values
- relation = Relation.new FakeKlass, :b
+ relation = Relation.new(FakeKlass, :b, nil)
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
+ relation = Relation.new(FakeKlass, :b, nil)
relation = relation.references(:foo).references(:foo)
assert_equal ['foo'], relation.references_values
end
test 'merging a hash into a relation' do
- relation = Relation.new FakeKlass, :b
- relation = relation.merge where: :lol, readonly: true
+ relation = Relation.new(Post, Post.arel_table, Post.predicate_builder)
+ relation = relation.merge where: {name: :lol}, readonly: true
- assert_equal [:lol], relation.where_values
+ assert_equal({"name"=>:lol}, relation.where_clause.to_h)
assert_equal true, relation.readonly_value
end
test 'merging an empty hash into a relation' do
- assert_equal [], Relation.new(FakeKlass, :b).merge({}).where_values
+ assert_equal Relation::WhereClause.empty, Relation.new(FakeKlass, :b, nil).merge({}).where_clause
end
test 'merging a hash with unknown keys raises' do
assert_raises(ArgumentError) { Relation::HashMerger.new(nil, omg: 'lol') }
end
+ test 'merging nil or false raises' do
+ relation = Relation.new(FakeKlass, :b, nil)
+
+ e = assert_raises(ArgumentError) do
+ relation = relation.merge nil
+ end
+
+ assert_equal 'invalid argument: nil.', e.message
+
+ e = assert_raises(ArgumentError) do
+ relation = relation.merge false
+ end
+
+ assert_equal 'invalid argument: false.', e.message
+ end
+
test '#values returns a dup of the values' do
- relation = Relation.new(FakeKlass, :b).where! :foo
+ relation = Relation.new(Post, Post.arel_table, Post.predicate_builder).where!(name: :foo)
values = relation.values
values[:where] = nil
- assert_not_nil relation.where_values
+ assert_not_nil relation.where_clause
end
test 'relations can be created with a values hash' do
- relation = Relation.new(FakeKlass, :b, where: [:foo])
- assert_equal [:foo], relation.where_values
- end
-
- test 'merging a single where value' do
- relation = Relation.new(FakeKlass, :b)
- relation.merge!(where: :foo)
- assert_equal [:foo], relation.where_values
+ relation = Relation.new(FakeKlass, :b, nil, select: [:foo])
+ assert_equal [:foo], relation.select_values
end
test 'merging a hash interpolates conditions' do
@@ -192,13 +202,13 @@ module ActiveRecord
end
end
- relation = Relation.new(klass, :b)
+ relation = Relation.new(klass, :b, nil)
relation.merge!(where: ['foo = ?', 'bar'])
- assert_equal ['foo = bar'], relation.where_values
+ assert_equal Relation::WhereClause.new(['foo = bar'], []), relation.where_clause
end
def test_merging_readonly_false
- relation = Relation.new FakeKlass, :b
+ relation = Relation.new(FakeKlass, :b, nil)
readonly_false_relation = relation.readonly(false)
# test merging in both directions
assert_equal false, relation.merge(readonly_false_relation).readonly_value
@@ -229,6 +239,24 @@ module ActiveRecord
assert_equal false, post.respond_to?(:title), "post should not respond_to?(:body) since invoking it raises exception"
end
+ def test_select_quotes_when_using_from_clause
+ skip_if_sqlite3_version_includes_quoting_bug
+ quoted_join = ActiveRecord::Base.connection.quote_table_name("join")
+ selected = Post.select(:join).from(Post.select("id as #{quoted_join}")).map(&:join)
+ assert_equal Post.pluck(:id), selected
+ end
+
+ def test_selecting_aliased_attribute_quotes_column_name_when_from_is_used
+ skip_if_sqlite3_version_includes_quoting_bug
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = :test_with_keyword_column_name
+ alias_attribute :description, :desc
+ end
+ klass.create!(description: "foo")
+
+ assert_equal ["foo"], klass.select(:description).from(klass.all).map(&:desc)
+ end
+
def test_relation_merging_with_merged_joins_as_strings
join_string = "LEFT OUTER JOIN #{Rating.quoted_table_name} ON #{SpecialComment.quoted_table_name}.id = #{Rating.quoted_table_name}.comment_id"
special_comments_with_ratings = SpecialComment.joins join_string
@@ -241,12 +269,12 @@ module ActiveRecord
:string
end
- def type_cast_from_database(value)
+ def deserialize(value)
raise value unless value == "type cast for database"
"type cast from database"
end
- def type_cast_for_database(value)
+ def serialize(value)
raise value unless value == "value from user"
"type cast for database"
end
@@ -263,5 +291,26 @@ module ActiveRecord
assert_equal "type cast from database", UpdateAllTestModel.first.body
end
+
+ private
+
+ def skip_if_sqlite3_version_includes_quoting_bug
+ if sqlite3_version_includes_quoting_bug?
+ skip <<-ERROR.squish
+ You are using an outdated version of SQLite3 which has a bug in
+ quoted column names. Please update SQLite3 and rebuild the sqlite3
+ ruby gem
+ ERROR
+ end
+ end
+
+ def sqlite3_version_includes_quoting_bug?
+ if current_adapter?(:SQLite3Adapter)
+ selected_quoted_column_names = ActiveRecord::Base.connection.exec_query(
+ 'SELECT "join" FROM (SELECT id AS "join" FROM posts) subquery'
+ ).columns
+ ["join"] != selected_quoted_column_names
+ end
+ end
end
end
diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb
index 88df997a2f..7521f0573a 100644
--- a/activerecord/test/cases/relations_test.rb
+++ b/activerecord/test/cases/relations_test.rb
@@ -7,6 +7,7 @@ require 'models/comment'
require 'models/author'
require 'models/entrant'
require 'models/developer'
+require 'models/computer'
require 'models/reply'
require 'models/company'
require 'models/bird'
@@ -15,12 +16,18 @@ require 'models/engine'
require 'models/tyre'
require 'models/minivan'
require 'models/aircraft'
-
+require "models/possession"
+require "models/reader"
class RelationTest < ActiveRecord::TestCase
fixtures :authors, :topics, :entrants, :developers, :companies, :developers_projects, :accounts, :categories, :categorizations, :posts, :comments,
:tags, :taggings, :cars, :minivans
+ class TopicWithCallbacks < ActiveRecord::Base
+ self.table_name = :topics
+ before_update { |topic| topic.author_name = 'David' if topic.author_name.blank? }
+ end
+
def test_do_not_double_quote_string_id
van = Minivan.last
assert van
@@ -33,15 +40,6 @@ class RelationTest < ActiveRecord::TestCase
assert_equal van, Minivan.where(:minivan_id => [van]).to_a.first
end
- def test_bind_values
- relation = Post.all
- assert_equal [], relation.bind_values
-
- relation2 = relation.bind 'foo'
- assert_equal %w{ foo }, relation2.bind_values
- assert_equal [], relation.bind_values
- end
-
def test_two_scopes_with_includes_should_not_drop_any_include
# heat habtm cache
car = Car.incl_engines.incl_tyres.first
@@ -159,6 +157,17 @@ class RelationTest < ActiveRecord::TestCase
end
end
+ def test_select_with_subquery_in_from_does_not_use_original_table_name
+ relation = Comment.group(:type).select('COUNT(post_id) AS post_count, type')
+ subquery = Comment.from(relation).select('type','post_count')
+ assert_equal(relation.map(&:post_count).sort,subquery.map(&:post_count).sort)
+ end
+
+ def test_group_with_subquery_in_from_does_not_use_original_table_name
+ relation = Comment.group(:type).select('COUNT(post_id) AS post_count,type')
+ subquery = Comment.from(relation).group('type').average("post_count")
+ assert_equal(relation.map(&:post_count).sort,subquery.values.sort)
+ end
def test_finding_with_conditions
assert_equal ["David"], Author.where(:name => 'David').map(&:name)
@@ -248,7 +257,7 @@ class RelationTest < ActiveRecord::TestCase
def test_finding_with_reorder
topics = Topic.order('author_name').order('title').reorder('id').to_a
- topics_titles = topics.map{ |t| t.title }
+ topics_titles = topics.map(&:title)
assert_equal ['The First Topic', 'The Second Topic of the day', 'The Third Topic of the day', 'The Fourth Topic of the day', 'The Fifth Topic of the day'], topics_titles
end
@@ -311,26 +320,26 @@ class RelationTest < ActiveRecord::TestCase
end
def test_none
- assert_no_queries do
+ assert_no_queries(ignore_none: false) do
assert_equal [], Developer.none
assert_equal [], Developer.all.none
end
end
def test_none_chainable
- assert_no_queries do
+ assert_no_queries(ignore_none: false) do
assert_equal [], Developer.none.where(:name => 'David')
end
end
def test_none_chainable_to_existing_scope_extension_method
- assert_no_queries do
+ assert_no_queries(ignore_none: false) do
assert_equal 1, Topic.anonymous_extension.none.one
end
end
def test_none_chained_to_methods_firing_queries_straight_to_db
- assert_no_queries do
+ assert_no_queries(ignore_none: false) do
assert_equal [], Developer.none.pluck(:id, :name)
assert_equal 0, Developer.none.delete_all
assert_equal 0, Developer.none.update_all(:name => 'David')
@@ -340,19 +349,21 @@ class RelationTest < ActiveRecord::TestCase
end
def test_null_relation_content_size_methods
- assert_no_queries do
+ assert_no_queries(ignore_none: false) do
assert_equal 0, Developer.none.size
assert_equal 0, Developer.none.count
assert_equal true, Developer.none.empty?
+ assert_equal true, Developer.none.none?
assert_equal false, Developer.none.any?
+ assert_equal false, Developer.none.one?
assert_equal false, Developer.none.many?
end
end
def test_null_relation_calculations_methods
- assert_no_queries do
+ assert_no_queries(ignore_none: false) do
assert_equal 0, Developer.none.count
- assert_equal 0, Developer.none.calculate(:count, nil, {})
+ assert_equal 0, Developer.none.calculate(:count, nil)
assert_equal nil, Developer.none.calculate(:average, 'salary')
end
end
@@ -420,6 +431,11 @@ class RelationTest < ActiveRecord::TestCase
assert_equal nil, ac.engines.maximum(:id)
end
+ def test_null_relation_in_where_condition
+ assert_operator Comment.count, :>, 0 # precondition, make sure there are comments.
+ assert_equal 0, Comment.where(post_id: Post.none).to_a.size
+ end
+
def test_joins_with_nil_argument
assert_nothing_raised { DependentFirm.joins(nil).first }
end
@@ -435,7 +451,7 @@ class RelationTest < ActiveRecord::TestCase
where('project_id=1').to_a
assert_equal 3, developers_on_project_one.length
- developer_names = developers_on_project_one.map { |d| d.name }
+ developer_names = developers_on_project_one.map(&:name)
assert developer_names.include?('David')
assert developer_names.include?('Jamis')
end
@@ -605,6 +621,51 @@ class RelationTest < ActiveRecord::TestCase
assert_equal 1, query.to_a.size
end
+ def test_preloading_with_associations_and_merges
+ post = Post.create! title: 'Uhuu', body: 'body'
+ reader = Reader.create! post_id: post.id, person_id: 1
+ comment = Comment.create! post_id: post.id, body: 'body'
+
+ assert !comment.respond_to?(:readers)
+
+ post_rel = Post.preload(:readers).joins(:readers).where(title: 'Uhuu')
+ result_comment = Comment.joins(:post).merge(post_rel).to_a.first
+ assert_equal comment, result_comment
+
+ assert_no_queries do
+ assert_equal post, result_comment.post
+ assert_equal [reader], result_comment.post.readers.to_a
+ end
+
+ post_rel = Post.includes(:readers).where(title: 'Uhuu')
+ result_comment = Comment.joins(:post).merge(post_rel).first
+ assert_equal comment, result_comment
+
+ assert_no_queries do
+ assert_equal post, result_comment.post
+ assert_equal [reader], result_comment.post.readers.to_a
+ end
+ end
+
+ def test_preloading_with_associations_default_scopes_and_merges
+ post = Post.create! title: 'Uhuu', body: 'body'
+ reader = Reader.create! post_id: post.id, person_id: 1
+
+ post_rel = PostWithPreloadDefaultScope.preload(:readers).joins(:readers).where(title: 'Uhuu')
+ result_post = PostWithPreloadDefaultScope.all.merge(post_rel).to_a.first
+
+ assert_no_queries do
+ assert_equal [reader], result_post.readers.to_a
+ end
+
+ post_rel = PostWithIncludesDefaultScope.includes(:readers).where(title: 'Uhuu')
+ result_post = PostWithIncludesDefaultScope.all.merge(post_rel).to_a.first
+
+ assert_no_queries do
+ assert_equal [reader], result_post.readers.to_a
+ end
+ end
+
def test_loading_with_one_association
posts = Post.preload(:comments)
post = posts.find { |p| p.id == 1 }
@@ -646,8 +707,8 @@ class RelationTest < ActiveRecord::TestCase
expected_taggings = taggings(:welcome_general, :thinking_general)
assert_no_queries do
- assert_equal expected_taggings, author.taggings.distinct.sort_by { |t| t.id }
- assert_equal expected_taggings, author.taggings.uniq.sort_by { |t| t.id }
+ assert_equal expected_taggings, author.taggings.distinct.sort_by(&:id)
+ assert_equal expected_taggings, author.taggings.uniq.sort_by(&:id)
end
authors = Author.all
@@ -706,7 +767,9 @@ class RelationTest < ActiveRecord::TestCase
def test_find_by_classname
Author.create!(:name => Mary.name)
- assert_equal 1, Author.where(:name => Mary).size
+ assert_deprecated do
+ assert_equal 1, Author.where(:name => Mary).size
+ end
end
def test_find_by_id_with_list_of_ar
@@ -844,6 +907,12 @@ class RelationTest < ActiveRecord::TestCase
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_last
authors = Author.all
assert_equal authors(:bob), authors.last
@@ -862,6 +931,12 @@ class RelationTest < ActiveRecord::TestCase
assert davids.loaded?
end
+ def test_destroy_all_with_conditions_is_deprecated
+ assert_deprecated do
+ assert_difference('Author.count', -1) { Author.destroy_all(name: 'David') }
+ end
+ end
+
def test_delete_all
davids = Author.where(:name => 'David')
@@ -869,6 +944,12 @@ class RelationTest < ActiveRecord::TestCase
assert ! davids.loaded?
end
+ def test_delete_all_with_conditions_is_deprecated
+ assert_deprecated do
+ assert_difference('Author.count', -1) { Author.delete_all(name: 'David') }
+ end
+ end
+
def test_delete_all_loaded
davids = Author.where(:name => 'David')
@@ -884,7 +965,7 @@ class RelationTest < ActiveRecord::TestCase
def test_delete_all_with_unpermitted_relation_raises_error
assert_raises(ActiveRecord::ActiveRecordError) { Author.limit(10).delete_all }
- assert_raises(ActiveRecord::ActiveRecordError) { Author.uniq.delete_all }
+ assert_raises(ActiveRecord::ActiveRecordError) { Author.distinct.delete_all }
assert_raises(ActiveRecord::ActiveRecordError) { Author.group(:name).delete_all }
assert_raises(ActiveRecord::ActiveRecordError) { Author.having('SUM(id) < 3').delete_all }
assert_raises(ActiveRecord::ActiveRecordError) { Author.offset(10).delete_all }
@@ -1091,6 +1172,38 @@ class RelationTest < ActiveRecord::TestCase
assert ! posts.limit(1).many?
end
+ def test_none?
+ posts = Post.all
+ assert_queries(1) do
+ assert ! posts.none? # Uses COUNT()
+ end
+
+ assert ! posts.loaded?
+
+ assert_queries(1) do
+ assert posts.none? {|p| p.id < 0 }
+ assert ! posts.none? {|p| p.id == 1 }
+ end
+
+ assert posts.loaded?
+ end
+
+ def test_one
+ posts = Post.all
+ assert_queries(1) do
+ assert ! posts.one? # Uses COUNT()
+ end
+
+ assert ! posts.loaded?
+
+ assert_queries(1) do
+ assert ! posts.one? {|p| p.id < 3 }
+ assert posts.one? {|p| p.id == 1 }
+ end
+
+ assert posts.loaded?
+ end
+
def test_build
posts = Post.all
@@ -1369,12 +1482,6 @@ class RelationTest < ActiveRecord::TestCase
assert_equal "id", Post.all.primary_key
end
- def test_disable_implicit_join_references_is_deprecated
- assert_deprecated do
- ActiveRecord::Base.disable_implicit_join_references = true
- end
- end
-
def test_ordering_with_extra_spaces
assert_equal authors(:david), Author.order('id DESC , name DESC').last
end
@@ -1421,6 +1528,26 @@ class RelationTest < ActiveRecord::TestCase
assert_equal posts(:welcome), comments(:greetings).post
end
+ def test_update_on_relation
+ topic1 = TopicWithCallbacks.create! title: 'arel', author_name: nil
+ topic2 = TopicWithCallbacks.create! title: 'activerecord', author_name: nil
+ topics = TopicWithCallbacks.where(id: [topic1.id, topic2.id])
+ topics.update(title: 'adequaterecord')
+
+ assert_equal 'adequaterecord', topic1.reload.title
+ assert_equal 'adequaterecord', topic2.reload.title
+ # Testing that the before_update callbacks have run
+ assert_equal 'David', topic1.reload.author_name
+ assert_equal 'David', topic2.reload.author_name
+ end
+
+ def test_update_on_relation_passing_active_record_object_is_deprecated
+ topic = Topic.create!(title: 'Foo', author_name: nil)
+ assert_deprecated(/update/) do
+ Topic.where(id: topic.id).update(topic, title: 'Bar')
+ end
+ end
+
def test_distinct
tag1 = Tag.create(:name => 'Foo')
tag2 = Tag.create(:name => 'Foo')
@@ -1430,22 +1557,46 @@ class RelationTest < ActiveRecord::TestCase
assert_equal ['Foo', 'Foo'], query.map(&:name)
assert_sql(/DISTINCT/) do
assert_equal ['Foo'], query.distinct.map(&:name)
- assert_equal ['Foo'], query.uniq.map(&:name)
+ assert_deprecated { assert_equal ['Foo'], query.uniq.map(&:name) }
end
assert_sql(/DISTINCT/) do
assert_equal ['Foo'], query.distinct(true).map(&:name)
- assert_equal ['Foo'], query.uniq(true).map(&:name)
+ assert_deprecated { assert_equal ['Foo'], query.uniq(true).map(&:name) }
end
assert_equal ['Foo', 'Foo'], query.distinct(true).distinct(false).map(&:name)
- assert_equal ['Foo', 'Foo'], query.uniq(true).uniq(false).map(&:name)
+
+ assert_deprecated do
+ assert_equal ['Foo', 'Foo'], query.uniq(true).uniq(false).map(&:name)
+ end
end
def test_doesnt_add_having_values_if_options_are_blank
scope = Post.having('')
- assert_equal [], scope.having_values
+ assert scope.having_clause.empty?
scope = Post.having([])
- assert_equal [], scope.having_values
+ assert scope.having_clause.empty?
+ end
+
+ def test_having_with_binds_for_both_where_and_having
+ post = Post.first
+ having_then_where = Post.having(id: post.id).where(title: post.title).group(:id)
+ where_then_having = Post.where(title: post.title).having(id: post.id).group(:id)
+
+ assert_equal [post], having_then_where
+ assert_equal [post], where_then_having
+ end
+
+ def test_multiple_where_and_having_clauses
+ post = Post.first
+ having_then_where = Post.having(id: post.id).where(title: post.title)
+ .having(id: post.id).where(title: post.title).group(:id)
+
+ assert_equal [post], having_then_where
+ end
+
+ def test_grouping_by_column_with_reserved_name
+ assert_equal [], Possession.select(:where).group(:where).to_a
end
def test_references_triggers_eager_loading
@@ -1570,7 +1721,11 @@ class RelationTest < ActiveRecord::TestCase
end
test "find_by doesn't have implicit ordering" do
- assert_sql(/^((?!ORDER).)*$/) { Post.find_by(author_id: 2) }
+ assert_sql(/^((?!ORDER).)*$/) { Post.all.find_by(author_id: 2) }
+ end
+
+ test "find_by requires at least one argument" do
+ assert_raises(ArgumentError) { Post.all.find_by }
end
test "find_by! with hash conditions returns the first matching record" do
@@ -1586,7 +1741,7 @@ class RelationTest < ActiveRecord::TestCase
end
test "find_by! doesn't have implicit ordering" do
- assert_sql(/^((?!ORDER).)*$/) { Post.find_by!(author_id: 2) }
+ assert_sql(/^((?!ORDER).)*$/) { Post.all.find_by!(author_id: 2) }
end
test "find_by! raises RecordNotFound if the record is missing" do
@@ -1595,6 +1750,10 @@ class RelationTest < ActiveRecord::TestCase
end
end
+ test "find_by! requires at least one argument" do
+ assert_raises(ArgumentError) { Post.all.find_by! }
+ end
+
test "loaded relations cannot be mutated by multi value methods" do
relation = Post.all
relation.to_a
@@ -1631,6 +1790,14 @@ class RelationTest < ActiveRecord::TestCase
end
end
+ test "relations with cached arel can't be mutated [internal API]" do
+ relation = Post.all
+ relation.count
+
+ assert_raises(ActiveRecord::ImmutableRelation) { relation.limit!(5) }
+ assert_raises(ActiveRecord::ImmutableRelation) { relation.where!("1 = 2") }
+ end
+
test "relations show the records in #inspect" do
relation = Post.limit(2)
assert_equal "#<ActiveRecord::Relation [#{Post.limit(2).map(&:inspect).join(', ')}]>", relation.inspect
@@ -1655,7 +1822,9 @@ class RelationTest < ActiveRecord::TestCase
test 'using a custom table affects the wheres' do
table_alias = Post.arel_table.alias('omg_posts')
- relation = ActiveRecord::Relation.new Post, table_alias
+ table_metadata = ActiveRecord::TableMetadata.new(Post, table_alias)
+ predicate_builder = ActiveRecord::PredicateBuilder.new(table_metadata)
+ relation = ActiveRecord::Relation.new(Post, table_alias, predicate_builder)
relation.where!(:foo => "bar")
node = relation.arel.constraints.first.grep(Arel::Attributes::Attribute).first
@@ -1697,7 +1866,7 @@ class RelationTest < ActiveRecord::TestCase
end
def test_unscope_removes_binds
- left = Post.where(id: Arel::Nodes::BindParam.new('?'))
+ left = Post.where(id: Arel::Nodes::BindParam.new)
column = Post.columns_hash['id']
left.bind_values += [[column, 20]]
@@ -1714,14 +1883,13 @@ class RelationTest < ActiveRecord::TestCase
end
def test_merging_keeps_lhs_bind_parameters
- column = Post.columns_hash['id']
- binds = [[column, 20]]
+ binds = [ActiveRecord::Relation::QueryAttribute.new("id", 20, Post.type_for_attribute("id"))]
right = Post.where(id: 20)
left = Post.where(id: 10)
merged = left.merge(right)
- assert_equal binds, merged.bind_values
+ assert_equal binds, merged.bound_attributes
end
def test_merging_reorders_bind_params
diff --git a/activerecord/test/cases/reload_models_test.rb b/activerecord/test/cases/reload_models_test.rb
index 0d16a3526f..431fbf1297 100644
--- a/activerecord/test/cases/reload_models_test.rb
+++ b/activerecord/test/cases/reload_models_test.rb
@@ -3,7 +3,7 @@ require 'models/owner'
require 'models/pet'
class ReloadModelsTest < ActiveRecord::TestCase
- fixtures :pets
+ fixtures :pets, :owners
def test_has_one_with_reload
pet = Pet.find_by_name('parrot')
diff --git a/activerecord/test/cases/result_test.rb b/activerecord/test/cases/result_test.rb
index d6decafad9..dec01dfa76 100644
--- a/activerecord/test/cases/result_test.rb
+++ b/activerecord/test/cases/result_test.rb
@@ -10,6 +10,10 @@ module ActiveRecord
])
end
+ test "length" do
+ assert_equal 3, result.length
+ end
+
test "to_hash returns row_hashes" do
assert_equal [
{'col_1' => 'row 1 col 1', 'col_2' => 'row 1 col 2'},
diff --git a/activerecord/test/cases/sanitize_test.rb b/activerecord/test/cases/sanitize_test.rb
index dca85fb5eb..14e392ac30 100644
--- a/activerecord/test/cases/sanitize_test.rb
+++ b/activerecord/test/cases/sanitize_test.rb
@@ -7,22 +7,13 @@ class SanitizeTest < ActiveRecord::TestCase
def setup
end
- def test_sanitize_sql_hash_handles_associations
- quoted_bambi = ActiveRecord::Base.connection.quote("Bambi")
- quoted_column_name = ActiveRecord::Base.connection.quote_column_name("name")
- quoted_table_name = ActiveRecord::Base.connection.quote_table_name("adorable_animals")
- expected_value = "#{quoted_table_name}.#{quoted_column_name} = #{quoted_bambi}"
-
- assert_equal expected_value, Binary.send(:sanitize_sql_hash, {adorable_animals: {name: 'Bambi'}})
- end
-
def test_sanitize_sql_array_handles_string_interpolation
quoted_bambi = ActiveRecord::Base.connection.quote_string("Bambi")
- assert_equal "name=#{quoted_bambi}", Binary.send(:sanitize_sql_array, ["name=%s", "Bambi"])
- assert_equal "name=#{quoted_bambi}", Binary.send(:sanitize_sql_array, ["name=%s", "Bambi".mb_chars])
+ assert_equal "name='#{quoted_bambi}'", Binary.send(:sanitize_sql_array, ["name='%s'", "Bambi"])
+ assert_equal "name='#{quoted_bambi}'", Binary.send(:sanitize_sql_array, ["name='%s'", "Bambi".mb_chars])
quoted_bambi_and_thumper = ActiveRecord::Base.connection.quote_string("Bambi\nand\nThumper")
- assert_equal "name=#{quoted_bambi_and_thumper}",Binary.send(:sanitize_sql_array, ["name=%s", "Bambi\nand\nThumper"])
- assert_equal "name=#{quoted_bambi_and_thumper}",Binary.send(:sanitize_sql_array, ["name=%s", "Bambi\nand\nThumper".mb_chars])
+ assert_equal "name='#{quoted_bambi_and_thumper}'",Binary.send(:sanitize_sql_array, ["name='%s'", "Bambi\nand\nThumper"])
+ assert_equal "name='#{quoted_bambi_and_thumper}'",Binary.send(:sanitize_sql_array, ["name='%s'", "Bambi\nand\nThumper".mb_chars])
end
def test_sanitize_sql_array_handles_bind_variables
diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb
index 4e71d04bc0..2a2c2bc8d0 100644
--- a/activerecord/test/cases/schema_dumper_test.rb
+++ b/activerecord/test/cases/schema_dumper_test.rb
@@ -2,30 +2,35 @@ require "cases/helper"
require 'support/schema_dumping_helper'
class SchemaDumperTest < ActiveRecord::TestCase
+ include SchemaDumpingHelper
+ self.use_transactional_tests = false
+
setup do
ActiveRecord::SchemaMigration.create_table
end
def standard_dump
- @stream = StringIO.new
- ActiveRecord::SchemaDumper.ignore_tables = []
- ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, @stream)
- @stream.string
+ @@standard_dump ||= perform_schema_dump
+ end
+
+ def perform_schema_dump
+ dump_all_table_schema []
end
def test_dump_schema_information_outputs_lexically_ordered_versions
versions = %w{ 20100101010101 20100201010101 20100301010101 }
- versions.reverse.each do |v|
+ versions.reverse_each do |v|
ActiveRecord::SchemaMigration.create!(:version => v)
end
schema_info = ActiveRecord::Base.connection.dump_schema_information
assert_match(/20100201010101.*20100301010101/m, schema_info)
+ ensure
+ ActiveRecord::SchemaMigration.delete_all
end
def test_magic_comment
- output = standard_dump
- assert_match "# encoding: #{@stream.external_encoding.name}", output
+ assert_match "# encoding: #{Encoding.default_external.name}", standard_dump
end
def test_schema_dump
@@ -35,6 +40,11 @@ class SchemaDumperTest < ActiveRecord::TestCase
assert_no_match %r{create_table "schema_migrations"}, output
end
+ def test_schema_dump_uses_force_cascade_on_create_table
+ output = dump_table_schema "authors"
+ assert_match %r{create_table "authors", force: :cascade}, output
+ end
+
def test_schema_dump_excludes_sqlite_sequence
output = standard_dump
assert_no_match %r{create_table "sqlite_sequence"}, output
@@ -63,10 +73,10 @@ class SchemaDumperTest < ActiveRecord::TestCase
next if column_set.empty?
lengths = column_set.map do |column|
- if match = column.match(/t\.(?:integer|decimal|float|datetime|timestamp|time|date|text|binary|string|boolean|uuid|point)\s+"/)
+ if match = column.match(/\bt\.\w+\s+"/)
match[0].length
end
- end
+ end.compact
assert_equal 1, lengths.uniq.length
end
@@ -86,22 +96,18 @@ class SchemaDumperTest < ActiveRecord::TestCase
end
def test_schema_dump_includes_not_null_columns
- stream = StringIO.new
-
- ActiveRecord::SchemaDumper.ignore_tables = [/^[^r]/]
- ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream)
- output = stream.string
+ output = dump_all_table_schema([/^[^r]/])
assert_match %r{null: false}, output
end
def test_schema_dump_includes_limit_constraint_for_integer_columns
- stream = StringIO.new
+ output = dump_all_table_schema([/^(?!integer_limits)/])
- ActiveRecord::SchemaDumper.ignore_tables = [/^(?!integer_limits)/]
- ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream)
- output = stream.string
+ assert_match %r{c_int_without_limit}, output
if current_adapter?(:PostgreSQLAdapter)
+ assert_no_match %r{c_int_without_limit.*limit:}, output
+
assert_match %r{c_int_1.*limit: 2}, output
assert_match %r{c_int_2.*limit: 2}, output
@@ -112,6 +118,8 @@ class SchemaDumperTest < ActiveRecord::TestCase
assert_match %r{c_int_4.*}, output
assert_no_match %r{c_int_4.*limit:}, output
elsif current_adapter?(:MysqlAdapter, :Mysql2Adapter)
+ assert_match %r{c_int_without_limit.*limit: 4}, output
+
assert_match %r{c_int_1.*limit: 1}, output
assert_match %r{c_int_2.*limit: 2}, output
assert_match %r{c_int_3.*limit: 3}, output
@@ -119,13 +127,13 @@ class SchemaDumperTest < ActiveRecord::TestCase
assert_match %r{c_int_4.*}, output
assert_no_match %r{c_int_4.*:limit}, output
elsif current_adapter?(:SQLite3Adapter)
+ assert_no_match %r{c_int_without_limit.*limit:}, output
+
assert_match %r{c_int_1.*limit: 1}, output
assert_match %r{c_int_2.*limit: 2}, output
assert_match %r{c_int_3.*limit: 3}, output
assert_match %r{c_int_4.*limit: 4}, output
end
- assert_match %r{c_int_without_limit.*}, output
- assert_no_match %r{c_int_without_limit.*limit:}, output
if current_adapter?(:SQLite3Adapter)
assert_match %r{c_int_5.*limit: 5}, output
@@ -146,54 +154,38 @@ class SchemaDumperTest < ActiveRecord::TestCase
end
def test_schema_dump_with_string_ignored_table
- stream = StringIO.new
-
- ActiveRecord::SchemaDumper.ignore_tables = ['accounts']
- ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream)
- output = stream.string
+ output = dump_all_table_schema(['accounts'])
assert_no_match %r{create_table "accounts"}, output
assert_match %r{create_table "authors"}, output
assert_no_match %r{create_table "schema_migrations"}, output
end
def test_schema_dump_with_regexp_ignored_table
- stream = StringIO.new
-
- ActiveRecord::SchemaDumper.ignore_tables = [/^account/]
- ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream)
- output = stream.string
+ output = dump_all_table_schema([/^account/])
assert_no_match %r{create_table "accounts"}, output
assert_match %r{create_table "authors"}, output
assert_no_match %r{create_table "schema_migrations"}, output
end
- def test_schema_dump_illegal_ignored_table_value
- stream = StringIO.new
- ActiveRecord::SchemaDumper.ignore_tables = [5]
- assert_raise(StandardError) do
- ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream)
- end
- end
-
def test_schema_dumps_index_columns_in_right_order
- index_definition = standard_dump.split(/\n/).grep(/add_index.*companies/).first.strip
+ index_definition = standard_dump.split(/\n/).grep(/t\.index.*company_index/).first.strip
if current_adapter?(:MysqlAdapter, :Mysql2Adapter, :PostgreSQLAdapter)
- assert_equal 'add_index "companies", ["firm_id", "type", "rating"], name: "company_index", using: :btree', index_definition
+ assert_equal 't.index ["firm_id", "type", "rating"], name: "company_index", using: :btree', index_definition
else
- assert_equal 'add_index "companies", ["firm_id", "type", "rating"], name: "company_index"', index_definition
+ assert_equal 't.index ["firm_id", "type", "rating"], name: "company_index"', index_definition
end
end
def test_schema_dumps_partial_indices
- index_definition = standard_dump.split(/\n/).grep(/add_index.*company_partial_index/).first.strip
+ index_definition = standard_dump.split(/\n/).grep(/t\.index.*company_partial_index/).first.strip
if current_adapter?(:PostgreSQLAdapter)
- assert_equal 'add_index "companies", ["firm_id", "type"], name: "company_partial_index", where: "(rating > 10)", using: :btree', index_definition
+ assert_equal 't.index ["firm_id", "type"], name: "company_partial_index", where: "(rating > 10)", using: :btree', index_definition
elsif current_adapter?(:MysqlAdapter, :Mysql2Adapter)
- assert_equal 'add_index "companies", ["firm_id", "type"], name: "company_partial_index", using: :btree', index_definition
+ assert_equal 't.index ["firm_id", "type"], name: "company_partial_index", using: :btree', index_definition
elsif current_adapter?(:SQLite3Adapter) && ActiveRecord::Base.connection.supports_partial_index?
- assert_equal 'add_index "companies", ["firm_id", "type"], name: "company_partial_index", where: "rating > 10"', index_definition
+ assert_equal 't.index ["firm_id", "type"], name: "company_partial_index", where: "rating > 10"', index_definition
else
- assert_equal 'add_index "companies", ["firm_id", "type"], name: "company_partial_index"', index_definition
+ assert_equal 't.index ["firm_id", "type"], name: "company_partial_index"', index_definition
end
end
@@ -210,48 +202,60 @@ class SchemaDumperTest < ActiveRecord::TestCase
end
if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
- def test_schema_dump_should_not_add_default_value_for_mysql_text_field
+ def test_schema_dump_should_add_default_value_for_mysql_text_field
output = standard_dump
- assert_match %r{t.text\s+"body",\s+null: false$}, output
+ assert_match %r{t\.text\s+"body",\s+limit: 65535,\s+null: false$}, output
end
def test_schema_dump_includes_length_for_mysql_binary_fields
output = standard_dump
- assert_match %r{t.binary\s+"var_binary",\s+limit: 255$}, output
- assert_match %r{t.binary\s+"var_binary_large",\s+limit: 4095$}, output
+ assert_match %r{t\.binary\s+"var_binary",\s+limit: 255$}, output
+ assert_match %r{t\.binary\s+"var_binary_large",\s+limit: 4095$}, output
end
def test_schema_dump_includes_length_for_mysql_blob_and_text_fields
output = standard_dump
- assert_match %r{t.binary\s+"tiny_blob",\s+limit: 255$}, output
- assert_match %r{t.binary\s+"normal_blob"$}, output
- assert_match %r{t.binary\s+"medium_blob",\s+limit: 16777215$}, output
- assert_match %r{t.binary\s+"long_blob",\s+limit: 2147483647$}, output
- assert_match %r{t.text\s+"tiny_text",\s+limit: 255$}, output
- assert_match %r{t.text\s+"normal_text"$}, output
- assert_match %r{t.text\s+"medium_text",\s+limit: 16777215$}, output
- assert_match %r{t.text\s+"long_text",\s+limit: 2147483647$}, output
+ assert_match %r{t\.blob\s+"tiny_blob",\s+limit: 255$}, output
+ assert_match %r{t\.binary\s+"normal_blob",\s+limit: 65535$}, output
+ assert_match %r{t\.binary\s+"medium_blob",\s+limit: 16777215$}, output
+ assert_match %r{t\.binary\s+"long_blob",\s+limit: 4294967295$}, output
+ assert_match %r{t\.text\s+"tiny_text",\s+limit: 255$}, output
+ assert_match %r{t\.text\s+"normal_text",\s+limit: 65535$}, output
+ assert_match %r{t\.text\s+"medium_text",\s+limit: 16777215$}, output
+ assert_match %r{t\.text\s+"long_text",\s+limit: 4294967295$}, output
+ end
+
+ def test_schema_does_not_include_limit_for_emulated_mysql_boolean_fields
+ output = standard_dump
+ assert_no_match %r{t\.boolean\s+"has_fun",.+limit: 1}, output
end
def test_schema_dumps_index_type
output = standard_dump
- assert_match %r{add_index "key_tests", \["awesome"\], name: "index_key_tests_on_awesome", type: :fulltext}, output
- assert_match %r{add_index "key_tests", \["pizza"\], name: "index_key_tests_on_pizza", using: :btree}, output
+ assert_match %r{t\.index \["awesome"\], name: "index_key_tests_on_awesome", type: :fulltext}, output
+ assert_match %r{t\.index \["pizza"\], name: "index_key_tests_on_pizza", using: :btree}, output
end
end
def test_schema_dump_includes_decimal_options
- stream = StringIO.new
- ActiveRecord::SchemaDumper.ignore_tables = [/^[^n]/]
- ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream)
- output = stream.string
- assert_match %r{precision: 3,[[:space:]]+scale: 2,[[:space:]]+default: 2.78}, output
+ output = dump_all_table_schema([/^[^n]/])
+ assert_match %r{precision: 3,[[:space:]]+scale: 2,[[:space:]]+default: "2\.78"}, output
end
if current_adapter?(:PostgreSQLAdapter)
def test_schema_dump_includes_bigint_default
output = standard_dump
- assert_match %r{t.integer\s+"bigint_default",\s+limit: 8,\s+default: 0}, output
+ assert_match %r{t\.integer\s+"bigint_default",\s+limit: 8,\s+default: 0}, output
+ end
+
+ def test_schema_dump_includes_limit_on_array_type
+ output = standard_dump
+ assert_match %r{t\.integer\s+"big_int_data_points\",\s+limit: 8,\s+array: true}, output
+ end
+
+ def test_schema_dump_allows_array_of_decimal_defaults
+ output = standard_dump
+ assert_match %r{t\.decimal\s+"decimal_array_default",\s+default: \["1.23", "3.45"\],\s+array: true}, output
end
if ActiveRecord::Base.connection.supports_extensions?
@@ -259,103 +263,27 @@ class SchemaDumperTest < ActiveRecord::TestCase
connection = ActiveRecord::Base.connection
connection.stubs(:extensions).returns(['hstore'])
- output = standard_dump
+ 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 = standard_dump
+ output = perform_schema_dump
assert_no_match "# These are extensions that must be enabled", output
assert_no_match %r{enable_extension}, output
end
end
-
- def test_schema_dump_includes_xml_shorthand_definition
- output = standard_dump
- if %r{create_table "postgresql_xml_data_type"} =~ output
- assert_match %r{t.xml "data"}, output
- end
- end
-
- def test_schema_dump_includes_json_shorthand_definition
- output = standard_dump
- if %r{create_table "postgresql_json_data_type"} =~ output
- assert_match %r|t.json "json_data", default: {}|, output
- end
- end
-
- def test_schema_dump_includes_inet_shorthand_definition
- output = standard_dump
- if %r{create_table "postgresql_network_addresses"} =~ output
- assert_match %r{t.inet\s+"inet_address",\s+default: "192.168.1.1"}, output
- end
- end
-
- def test_schema_dump_includes_cidr_shorthand_definition
- output = standard_dump
- if %r{create_table "postgresql_network_addresses"} =~ output
- assert_match %r{t.cidr\s+"cidr_address",\s+default: "192.168.1.0/24"}, output
- end
- end
-
- def test_schema_dump_includes_macaddr_shorthand_definition
- output = standard_dump
- if %r{create_table "postgresql_network_addresses"} =~ output
- assert_match %r{t.macaddr\s+"mac_address",\s+default: "ff:ff:ff:ff:ff:ff"}, output
- end
- end
-
- def test_schema_dump_includes_uuid_shorthand_definition
- output = standard_dump
- if %r{create_table "postgresql_uuids"} =~ output
- assert_match %r{t.uuid "guid"}, output
- end
- end
-
- def test_schema_dump_includes_hstores_shorthand_definition
- output = standard_dump
- if %r{create_table "postgresql_hstores"} =~ output
- assert_match %r[t.hstore "hash_store", default: {}], output
- end
- end
-
- def test_schema_dump_includes_citext_shorthand_definition
- output = standard_dump
- if %r{create_table "postgresql_citext"} =~ output
- assert_match %r[t.citext "text_citext"], output
- end
- end
-
- def test_schema_dump_includes_ltrees_shorthand_definition
- output = standard_dump
- if %r{create_table "postgresql_ltrees"} =~ output
- assert_match %r[t.ltree "path"], output
- end
- end
-
- def test_schema_dump_includes_arrays_shorthand_definition
- output = standard_dump
- if %r{create_table "postgresql_arrays"} =~ output
- assert_match %r[t.text\s+"nicknames",\s+array: true], output
- assert_match %r[t.integer\s+"commission_by_quarter",\s+array: true], output
- end
- end
-
- def test_schema_dump_includes_tsvector_shorthand_definition
- output = standard_dump
- if %r{create_table "postgresql_tsvectors"} =~ output
- assert_match %r{t.tsvector "text_vector"}, output
- end
- end
end
def test_schema_dump_keeps_large_precision_integer_columns_as_decimal
output = standard_dump
# Oracle supports precision up to 38 and it identifies decimals with scale 0 as integers
if current_adapter?(:OracleAdapter)
- assert_match %r{t.integer\s+"atoms_in_universe",\s+precision: 38}, output
+ assert_match %r{t\.integer\s+"atoms_in_universe",\s+precision: 38}, output
+ elsif current_adapter?(:FbAdapter)
+ assert_match %r{t\.integer\s+"atoms_in_universe",\s+precision: 18}, output
else
- assert_match %r{t.decimal\s+"atoms_in_universe",\s+precision: 55}, output
+ assert_match %r{t\.decimal\s+"atoms_in_universe",\s+precision: 55}, output
end
end
@@ -364,7 +292,7 @@ class SchemaDumperTest < ActiveRecord::TestCase
match = output.match(%r{create_table "goofy_string_id"(.*)do.*\n(.*)\n})
assert_not_nil(match, "goofy_string_id table not found")
assert_match %r(id: false), match[1], "no table id not preserved"
- assert_match %r{t.string[[:space:]]+"id",[[:space:]]+null: false$}, match[2], "non-primary key id column not preserved"
+ assert_match %r{t\.string\s+"id",.*?null: false$}, match[2], "non-primary key id column not preserved"
end
def test_schema_dump_keeps_id_false_when_id_is_false_and_unique_not_null_column_added
@@ -377,6 +305,11 @@ class SchemaDumperTest < ActiveRecord::TestCase
output = standard_dump
assert_match(/^\s+add_foreign_key "fk_test_has_fk"[^\n]+\n\s+add_foreign_key "lessons_students"/, output)
end
+
+ def test_do_not_dump_foreign_keys_for_ignored_tables
+ output = dump_table_schema "authors"
+ assert_equal ["authors"], output.scan(/^\s*add_foreign_key "([^"]+)".+$/).flatten
+ end
end
class CreateDogMigration < ActiveRecord::Migration
@@ -405,7 +338,7 @@ class SchemaDumperTest < ActiveRecord::TestCase
migration = CreateDogMigration.new
migration.migrate(:up)
- output = standard_dump
+ output = perform_schema_dump
assert_no_match %r{create_table "foo_.+_bar"}, output
assert_no_match %r{add_index "foo_.+_bar"}, output
assert_no_match %r{create_table "schema_migrations"}, output
@@ -437,13 +370,13 @@ class SchemaDumperDefaultsTest < ActiveRecord::TestCase
teardown do
return unless @connection
- @connection.execute 'DROP TABLE IF EXISTS defaults'
+ @connection.drop_table 'defaults', if_exists: true
end
def test_schema_dump_defaults_with_universally_supported_types
output = dump_table_schema('defaults')
- assert_match %r{t\.string\s+"string_with_default",\s+default: "Hello!"}, output
+ assert_match %r{t\.string\s+"string_with_default",.*?default: "Hello!"}, output
assert_match %r{t\.date\s+"date_with_default",\s+default: '2014-06-05'}, output
assert_match %r{t\.datetime\s+"datetime_with_default",\s+default: '2014-06-05 07:17:04'}, output
assert_match %r{t\.time\s+"time_with_default",\s+default: '2000-01-01 07:17:04'}, output
diff --git a/activerecord/test/cases/scoping/default_scoping_test.rb b/activerecord/test/cases/scoping/default_scoping_test.rb
index 9a4d8c6740..86316ab476 100644
--- a/activerecord/test/cases/scoping/default_scoping_test.rb
+++ b/activerecord/test/cases/scoping/default_scoping_test.rb
@@ -1,13 +1,16 @@
require 'cases/helper'
require 'models/post'
+require 'models/comment'
require 'models/developer'
+require 'models/computer'
+require 'models/vehicle'
class DefaultScopingTest < ActiveRecord::TestCase
- fixtures :developers, :posts
+ fixtures :developers, :posts, :comments
def test_default_scope
- expected = Developer.all.merge!(:order => 'salary DESC').to_a.collect { |dev| dev.salary }
- received = DeveloperOrderedBySalary.all.collect { |dev| dev.salary }
+ expected = Developer.all.merge!(:order => 'salary DESC').to_a.collect(&:salary)
+ received = DeveloperOrderedBySalary.all.collect(&:salary)
assert_equal expected, received
end
@@ -84,14 +87,14 @@ class DefaultScopingTest < ActiveRecord::TestCase
end
def test_scope_overwrites_default
- expected = Developer.all.merge!(order: 'salary DESC, name DESC').to_a.collect { |dev| dev.name }
- received = DeveloperOrderedBySalary.by_name.to_a.collect { |dev| dev.name }
+ expected = Developer.all.merge!(order: 'salary DESC, name DESC').to_a.collect(&:name)
+ received = DeveloperOrderedBySalary.by_name.to_a.collect(&:name)
assert_equal expected, received
end
def test_reorder_overrides_default_scope_order
- expected = Developer.order('name DESC').collect { |dev| dev.name }
- received = DeveloperOrderedBySalary.reorder('name DESC').collect { |dev| dev.name }
+ expected = Developer.order('name DESC').collect(&:name)
+ received = DeveloperOrderedBySalary.reorder('name DESC').collect(&:name)
assert_equal expected, received
end
@@ -141,37 +144,57 @@ class DefaultScopingTest < ActiveRecord::TestCase
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
+
+ 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
+
+ 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
+ end
+
+ def test_unscope_comparison_where_clauses
+ # unscoped for WHERE (`developers`.`id` <= 2)
+ expected = Developer.order('salary DESC').collect(&:name)
+ received = DeveloperOrderedBySalary.where(id: -Float::INFINITY..2).unscope(where: :id).collect { |dev| dev.name }
+ assert_equal expected, received
+
+ # unscoped for WHERE (`developers`.`id` < 2)
+ expected = Developer.order('salary DESC').collect(&:name)
+ received = DeveloperOrderedBySalary.where(id: -Float::INFINITY...2).unscope(where: :id).collect { |dev| dev.name }
+ assert_equal expected, received
end
def test_unscope_multiple_where_clauses
- expected = Developer.order('salary DESC').collect { |dev| dev.name }
- received = DeveloperOrderedBySalary.where(name: 'Jamis').where(id: 1).unscope(where: [:name, :id]).collect { |dev| dev.name }
+ 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
end
def test_unscope_string_where_clauses_involved
dev_relation = Developer.order('salary DESC').where("created_at > ?", 1.year.ago)
- expected = dev_relation.collect { |dev| dev.name }
+ expected = dev_relation.collect(&:name)
dev_ordered_relation = DeveloperOrderedBySalary.where(name: 'Jamis').where("created_at > ?", 1.year.ago)
- received = dev_ordered_relation.unscope(where: [:name]).collect { |dev| dev.name }
+ received = dev_ordered_relation.unscope(where: [:name]).collect(&:name)
assert_equal expected, received
end
def test_unscope_with_grouping_attributes
- expected = Developer.order('salary DESC').collect { |dev| dev.name }
- received = DeveloperOrderedBySalary.group(:name).unscope(:group).collect { |dev| dev.name }
+ expected = Developer.order('salary DESC').collect(&:name)
+ received = DeveloperOrderedBySalary.group(:name).unscope(:group).collect(&:name)
assert_equal expected, received
- expected_2 = Developer.order('salary DESC').collect { |dev| dev.name }
- received_2 = DeveloperOrderedBySalary.group("name").unscope(:group).collect { |dev| dev.name }
+ expected_2 = Developer.order('salary DESC').collect(&:name)
+ received_2 = DeveloperOrderedBySalary.group("name").unscope(:group).collect(&:name)
assert_equal expected_2, received_2
end
def test_unscope_with_limit_in_query
- expected = Developer.order('salary DESC').collect { |dev| dev.name }
- received = DeveloperOrderedBySalary.limit(1).unscope(:limit).collect { |dev| dev.name }
+ expected = Developer.order('salary DESC').collect(&:name)
+ received = DeveloperOrderedBySalary.limit(1).unscope(:limit).collect(&:name)
assert_equal expected, received
end
@@ -181,42 +204,42 @@ class DefaultScopingTest < ActiveRecord::TestCase
end
def test_unscope_reverse_order
- expected = Developer.all.collect { |dev| dev.name }
- received = Developer.order('salary DESC').reverse_order.unscope(:order).collect { |dev| dev.name }
+ expected = Developer.all.collect(&:name)
+ received = Developer.order('salary DESC').reverse_order.unscope(:order).collect(&:name)
assert_equal expected, received
end
def test_unscope_select
- expected = Developer.order('salary ASC').collect { |dev| dev.name }
- received = Developer.order('salary DESC').reverse_order.select(:name).unscope(:select).collect { |dev| dev.name }
+ expected = Developer.order('salary ASC').collect(&:name)
+ received = Developer.order('salary DESC').reverse_order.select(:name).unscope(:select).collect(&:name)
assert_equal expected, received
- expected_2 = Developer.all.collect { |dev| dev.id }
- received_2 = Developer.select(:name).unscope(:select).collect { |dev| dev.id }
+ expected_2 = Developer.all.collect(&:id)
+ received_2 = Developer.select(:name).unscope(:select).collect(&:id)
assert_equal expected_2, received_2
end
def test_unscope_offset
- expected = Developer.all.collect { |dev| dev.name }
- received = Developer.offset(5).unscope(:offset).collect { |dev| dev.name }
+ expected = Developer.all.collect(&:name)
+ received = Developer.offset(5).unscope(:offset).collect(&:name)
assert_equal expected, received
end
def test_unscope_joins_and_select_on_developers_projects
- expected = Developer.all.collect { |dev| dev.name }
- received = Developer.joins('JOIN developers_projects ON id = developer_id').select(:id).unscope(:joins, :select).collect { |dev| dev.name }
+ expected = Developer.all.collect(&:name)
+ received = Developer.joins('JOIN developers_projects ON id = developer_id').select(:id).unscope(:joins, :select).collect(&:name)
assert_equal expected, received
end
def test_unscope_includes
- expected = Developer.all.collect { |dev| dev.name }
- received = Developer.includes(:projects).select(:id).unscope(:includes, :select).collect { |dev| dev.name }
+ expected = Developer.all.collect(&:name)
+ received = Developer.includes(:projects).select(:id).unscope(:includes, :select).collect(&:name)
assert_equal expected, received
end
def test_unscope_having
- expected = DeveloperOrderedBySalary.all.collect { |dev| dev.name }
- received = DeveloperOrderedBySalary.having("name IN ('Jamis', 'David')").unscope(:having).collect { |dev| dev.name }
+ expected = DeveloperOrderedBySalary.all.collect(&:name)
+ received = DeveloperOrderedBySalary.having("name IN ('Jamis', 'David')").unscope(:having).collect(&:name)
assert_equal expected, received
end
@@ -274,13 +297,13 @@ class DefaultScopingTest < ActiveRecord::TestCase
def test_unscope_merging
merged = Developer.where(name: "Jamis").merge(Developer.unscope(:where))
- assert merged.where_values.empty?
- assert !merged.where(name: "Jon").where_values.empty?
+ assert merged.where_clause.empty?
+ assert !merged.where(name: "Jon").where_clause.empty?
end
def test_order_in_default_scope_should_not_prevail
- expected = Developer.all.merge!(order: 'salary desc').to_a.collect { |dev| dev.salary }
- received = DeveloperOrderedBySalary.all.merge!(order: 'salary').to_a.collect { |dev| dev.salary }
+ expected = Developer.all.merge!(order: 'salary desc').to_a.collect(&:salary)
+ received = DeveloperOrderedBySalary.all.merge!(order: 'salary').to_a.collect(&:salary)
assert_equal expected, received
end
@@ -378,6 +401,24 @@ class DefaultScopingTest < ActiveRecord::TestCase
assert_equal 1, DeveloperWithIncludes.where(:audit_logs => { :message => 'foo' }).count
end
+ def test_default_scope_with_references_works_through_collection_association
+ post = PostWithCommentWithDefaultScopeReferencesAssociation.create!(title: "Hello World", body: "Here we go.")
+ comment = post.comment_with_default_scope_references_associations.create!(body: "Great post.", developer_id: Developer.first.id)
+ assert_equal comment, post.comment_with_default_scope_references_associations.to_a.first
+ end
+
+ def test_default_scope_with_references_works_through_association
+ post = PostWithCommentWithDefaultScopeReferencesAssociation.create!(title: "Hello World", body: "Here we go.")
+ comment = post.comment_with_default_scope_references_associations.create!(body: "Great post.", developer_id: Developer.first.id)
+ assert_equal comment, post.first_comment
+ end
+
+ def test_default_scope_with_references_works_with_find_by
+ post = PostWithCommentWithDefaultScopeReferencesAssociation.create!(title: "Hello World", body: "Here we go.")
+ comment = post.comment_with_default_scope_references_associations.create!(body: "Great post.", developer_id: Developer.first.id)
+ assert_equal comment, CommentWithDefaultScopeReferencesAssociation.find_by(id: comment.id)
+ end
+
unless in_memory_db?
def test_default_scope_is_threadsafe
threads = []
@@ -398,19 +439,24 @@ class DefaultScopingTest < ActiveRecord::TestCase
test "additional conditions are ANDed with the default scope" do
scope = DeveloperCalledJamis.where(name: "David")
- assert_equal 2, scope.where_values.length
+ assert_equal 2, scope.where_clause.ast.children.length
assert_equal [], scope.to_a
end
test "additional conditions in a scope are ANDed with the default scope" do
scope = DeveloperCalledJamis.david
- assert_equal 2, scope.where_values.length
+ assert_equal 2, scope.where_clause.ast.children.length
assert_equal [], scope.to_a
end
test "a scope can remove the condition from the default scope" do
scope = DeveloperCalledJamis.david2
- assert_equal 1, scope.where_values.length
- assert_equal Developer.where(name: "David").map(&:id), scope.map(&:id)
+ assert_equal 1, scope.where_clause.ast.children.length
+ assert_equal Developer.where(name: "David"), scope
+ end
+
+ def test_with_abstract_class_where_clause_should_not_be_duplicated
+ scope = Bus.all
+ assert_equal scope.where_clause.ast.children.length, 1
end
end
diff --git a/activerecord/test/cases/scoping/named_scoping_test.rb b/activerecord/test/cases/scoping/named_scoping_test.rb
index 59ec2dd6a4..7a8eaeccb7 100644
--- a/activerecord/test/cases/scoping/named_scoping_test.rb
+++ b/activerecord/test/cases/scoping/named_scoping_test.rb
@@ -5,6 +5,7 @@ require 'models/comment'
require 'models/reply'
require 'models/author'
require 'models/developer'
+require 'models/computer'
class NamedScopingTest < ActiveRecord::TestCase
fixtures :posts, :authors, :topics, :comments, :author_addresses
@@ -132,6 +133,13 @@ class NamedScopingTest < ActiveRecord::TestCase
assert_equal Post.ranked_by_comments.limit_by(5), Post.top(5)
end
+ def test_scopes_body_is_a_callable
+ e = assert_raises ArgumentError do
+ Class.new(Post).class_eval { scope :containing_the_letter_z, where("body LIKE '%z%'") }
+ end
+ assert_equal "The scope body needs to be callable.", e.message
+ end
+
def test_active_records_have_scope_named__all__
assert !Topic.all.empty?
@@ -180,8 +188,9 @@ class NamedScopingTest < ActiveRecord::TestCase
def test_any_should_call_proxy_found_if_using_a_block
topics = Topic.base
assert_queries(1) do
- topics.expects(:empty?).never
- topics.any? { true }
+ assert_not_called(topics, :empty?) do
+ topics.any? { true }
+ end
end
end
@@ -209,8 +218,9 @@ class NamedScopingTest < ActiveRecord::TestCase
def test_many_should_call_proxy_found_if_using_a_block
topics = Topic.base
assert_queries(1) do
- topics.expects(:size).never
- topics.many? { true }
+ assert_not_called(topics, :size) do
+ topics.many? { true }
+ end
end
end
@@ -291,9 +301,12 @@ class NamedScopingTest < ActiveRecord::TestCase
:relation, # private class method on AR::Base
:new, # redefined class method on AR::Base
:all, # a default scope
- :public,
+ :public, # some imporant methods on Module and Class
:protected,
- :private
+ :private,
+ :name,
+ :parent,
+ :superclass
]
non_conflicts = [
@@ -306,13 +319,15 @@ class NamedScopingTest < ActiveRecord::TestCase
]
conflicts.each do |name|
- assert_raises(ArgumentError, "scope `#{name}` should not be allowed") do
+ e = assert_raises(ArgumentError, "scope `#{name}` should not be allowed") do
klass.class_eval { scope name, ->{ where(approved: true) } }
end
+ assert_match(/You tried to define a scope named \"#{name}\" on the model/, e.message)
- assert_raises(ArgumentError, "scope `#{name}` should not be allowed") do
+ e = assert_raises(ArgumentError, "scope `#{name}` should not be allowed") do
subklass.class_eval { scope name, ->{ where(approved: true) } }
end
+ assert_match(/You tried to define a scope named \"#{name}\" on the model/, e.message)
end
non_conflicts.each do |name|
@@ -369,8 +384,8 @@ class NamedScopingTest < ActiveRecord::TestCase
end
def test_should_not_duplicates_where_values
- where_values = Topic.where("1=1").scope_with_lambda.where_values
- assert_equal ["1=1"], where_values
+ relation = Topic.where("1=1")
+ assert_equal relation.where_clause, relation.scope_with_lambda.where_clause
end
def test_chaining_with_duplicate_joins
diff --git a/activerecord/test/cases/scoping/relation_scoping_test.rb b/activerecord/test/cases/scoping/relation_scoping_test.rb
index d8a467ec4d..4bfffbe9c6 100644
--- a/activerecord/test/cases/scoping/relation_scoping_test.rb
+++ b/activerecord/test/cases/scoping/relation_scoping_test.rb
@@ -2,6 +2,7 @@ require "cases/helper"
require 'models/post'
require 'models/author'
require 'models/developer'
+require 'models/computer'
require 'models/project'
require 'models/comment'
require 'models/category'
@@ -11,6 +12,30 @@ require 'models/reference'
class RelationScopingTest < ActiveRecord::TestCase
fixtures :authors, :developers, :projects, :comments, :posts, :developers_projects
+ setup do
+ developers(:david)
+ end
+
+ def test_unscoped_breaks_caching
+ author = authors :mary
+ assert_nil author.first_post
+ post = FirstPost.unscoped do
+ author.reload.first_post
+ end
+ assert post
+ end
+
+ def test_scope_breaks_caching_on_collections
+ author = authors :david
+ ids = author.reload.special_posts_with_default_scope.map(&:id)
+ assert_equal [1,5,6], ids.sort
+ scoped_posts = SpecialPostWithDefaultScope.unscoped do
+ author = authors :david
+ author.reload.special_posts_with_default_scope.to_a
+ end
+ assert_equal author.posts.map(&:id).sort, scoped_posts.map(&:id).sort
+ end
+
def test_reverse_order
assert_equal Developer.order("id DESC").to_a.reverse, Developer.order("id DESC").reverse_order
end
@@ -159,7 +184,7 @@ class RelationScopingTest < ActiveRecord::TestCase
rescue
end
- assert !Developer.all.where_values.include?("name = 'Jamis'")
+ assert_not Developer.all.to_sql.include?("name = 'Jamis'"), "scope was not restored"
end
def test_default_scope_filters_on_joins
@@ -183,6 +208,12 @@ class RelationScopingTest < ActiveRecord::TestCase
assert_equal [], DeveloperFilteredOnJoins.all
assert_not_equal [], Developer.all
end
+
+ def test_current_scope_does_not_pollute_other_subclasses
+ Post.none.scoping do
+ assert StiPost.all.any?
+ end
+ end
end
class NestedRelationScopingTest < ActiveRecord::TestCase
@@ -260,7 +291,7 @@ class NestedRelationScopingTest < ActiveRecord::TestCase
end
end
-class HasManyScopingTest< ActiveRecord::TestCase
+class HasManyScopingTest < ActiveRecord::TestCase
fixtures :comments, :posts, :people, :references
def setup
@@ -306,7 +337,7 @@ class HasManyScopingTest< ActiveRecord::TestCase
end
end
-class HasAndBelongsToManyScopingTest< ActiveRecord::TestCase
+class HasAndBelongsToManyScopingTest < ActiveRecord::TestCase
fixtures :posts, :categories, :categories_posts
def setup
diff --git a/activerecord/test/cases/secure_token_test.rb b/activerecord/test/cases/secure_token_test.rb
new file mode 100644
index 0000000000..e731443fc2
--- /dev/null
+++ b/activerecord/test/cases/secure_token_test.rb
@@ -0,0 +1,32 @@
+require 'cases/helper'
+require 'models/user'
+
+class SecureTokenTest < ActiveRecord::TestCase
+ setup do
+ @user = User.new
+ end
+
+ def test_token_values_are_generated_for_specified_attributes_and_persisted_on_save
+ @user.save
+ assert_not_nil @user.token
+ assert_not_nil @user.auth_token
+ end
+
+ def test_regenerating_the_secure_token
+ @user.save
+ old_token = @user.token
+ old_auth_token = @user.auth_token
+ @user.regenerate_token
+ @user.regenerate_auth_token
+
+ assert_not_equal @user.token, old_token
+ assert_not_equal @user.auth_token, old_auth_token
+ end
+
+ def test_token_value_not_overwritten_when_present
+ @user.token = "custom-secure-token"
+ @user.save
+
+ assert_equal @user.token, "custom-secure-token"
+ end
+end
diff --git a/activerecord/test/cases/serialization_test.rb b/activerecord/test/cases/serialization_test.rb
index 3f52e80e11..14b80f4df4 100644
--- a/activerecord/test/cases/serialization_test.rb
+++ b/activerecord/test/cases/serialization_test.rb
@@ -2,11 +2,13 @@ require "cases/helper"
require 'models/contact'
require 'models/topic'
require 'models/book'
+require 'models/author'
+require 'models/post'
class SerializationTest < ActiveRecord::TestCase
fixtures :books
- FORMATS = [ :xml, :json ]
+ FORMATS = [ :json ]
def setup
@contact_attributes = {
@@ -92,4 +94,11 @@ class SerializationTest < ActiveRecord::TestCase
book = klazz.find(books(:awdr).id)
assert_equal 'paperback', book.read_attribute_for_serialization(:format)
end
+
+ def test_find_records_by_serialized_attributes_through_join
+ author = Author.create!(name: "David")
+ author.serialized_posts.create!(title: "Hello")
+
+ assert_equal 1, Author.joins(:serialized_posts).where(name: "David", serialized_posts: { title: "Hello" }).length
+ end
end
diff --git a/activerecord/test/cases/serialized_attribute_test.rb b/activerecord/test/cases/serialized_attribute_test.rb
index f8d87a3661..6056156698 100644
--- a/activerecord/test/cases/serialized_attribute_test.rb
+++ b/activerecord/test/cases/serialized_attribute_test.rb
@@ -16,18 +16,12 @@ class SerializedAttributeTest < ActiveRecord::TestCase
end
def test_serialize_does_not_eagerly_load_columns
+ Topic.reset_column_information
assert_no_queries do
- Topic.reset_column_information
Topic.serialize(:content)
end
end
- def test_list_of_serialized_attributes
- assert_deprecated do
- assert_equal %w(content), Topic.serialized_attributes.keys
- end
- end
-
def test_serialized_attribute
Topic.serialize("content", MyObject)
@@ -140,11 +134,10 @@ class SerializedAttributeTest < ActiveRecord::TestCase
assert_equal 1, Topic.where(:content => nil).count
end
- def test_serialized_attribute_should_raise_exception_on_save_with_wrong_type
+ def test_serialized_attribute_should_raise_exception_on_assignment_with_wrong_type
Topic.serialize(:content, Hash)
assert_raise(ActiveRecord::SerializationTypeMismatch) do
- topic = Topic.new(content: 'string')
- topic.save
+ Topic.new(content: 'string')
end
end
@@ -244,8 +237,9 @@ class SerializedAttributeTest < ActiveRecord::TestCase
t = Topic.create(content: "first")
assert_equal("first", t.content)
- t.update_column(:content, Topic.type_for_attribute('content').type_cast_for_database("second"))
- assert_equal("second", t.content)
+ t.update_column(:content, ["second"])
+ assert_equal(["second"], t.content)
+ assert_equal(["second"], t.reload.content)
end
def test_serialized_column_should_unserialize_after_update_attribute
@@ -254,5 +248,51 @@ class SerializedAttributeTest < ActiveRecord::TestCase
t.update_attribute(:content, "second")
assert_equal("second", t.content)
+ assert_equal("second", t.reload.content)
+ end
+
+ def test_nil_is_not_changed_when_serialized_with_a_class
+ Topic.serialize(:content, Array)
+
+ topic = Topic.new(content: nil)
+
+ assert_not topic.content_changed?
+ end
+
+ def test_classes_without_no_arg_constructors_are_not_supported
+ assert_raises(ArgumentError) do
+ Topic.serialize(:content, Regexp)
+ end
+ end
+
+ def test_newly_emptied_serialized_hash_is_changed
+ Topic.serialize(:content, Hash)
+ topic = Topic.create(content: { "things" => "stuff" })
+ topic.content.delete("things")
+ topic.save!
+ topic.reload
+
+ assert_equal({}, topic.content)
+ end
+
+ def test_values_cast_from_nil_are_persisted_as_nil
+ # This is required to fulfil the following contract, which must be universally
+ # true in Active Record:
+ #
+ # model.attribute = value
+ # assert_equal model.attribute, model.tap(&:save).reload.attribute
+ Topic.serialize(:content, Hash)
+ topic = Topic.create!(content: {})
+ topic2 = Topic.create!(content: nil)
+
+ assert_equal [topic, topic2], Topic.where(content: nil)
+ end
+
+ def test_nil_is_always_persisted_as_null
+ Topic.serialize(:content, Hash)
+
+ topic = Topic.create!(content: { foo: "bar" })
+ topic.update_attribute :content, nil
+ assert_equal [topic], Topic.where(content: nil)
end
end
diff --git a/activerecord/test/cases/suppressor_test.rb b/activerecord/test/cases/suppressor_test.rb
new file mode 100644
index 0000000000..72c5c16555
--- /dev/null
+++ b/activerecord/test/cases/suppressor_test.rb
@@ -0,0 +1,52 @@
+require 'cases/helper'
+require 'models/notification'
+require 'models/user'
+
+class SuppressorTest < ActiveRecord::TestCase
+ def test_suppresses_create
+ assert_no_difference -> { Notification.count } do
+ Notification.suppress do
+ Notification.create
+ Notification.create!
+ Notification.new.save
+ Notification.new.save!
+ end
+ end
+ end
+
+ def test_suppresses_update
+ user = User.create! token: 'asdf'
+
+ User.suppress do
+ user.update token: 'ghjkl'
+ assert_equal 'asdf', user.reload.token
+
+ user.update! token: 'zxcvbnm'
+ assert_equal 'asdf', user.reload.token
+
+ user.token = 'qwerty'
+ user.save
+ assert_equal 'asdf', user.reload.token
+
+ user.token = 'uiop'
+ user.save!
+ assert_equal 'asdf', user.reload.token
+ end
+ end
+
+ def test_suppresses_create_in_callback
+ assert_difference -> { User.count } do
+ assert_no_difference -> { Notification.count } do
+ Notification.suppress { UserWithNotification.create! }
+ end
+ end
+ end
+
+ def test_resumes_saving_after_suppression_complete
+ Notification.suppress { UserWithNotification.create! }
+
+ assert_difference -> { Notification.count } do
+ Notification.create!
+ end
+ end
+end
diff --git a/activerecord/test/cases/tasks/database_tasks_test.rb b/activerecord/test/cases/tasks/database_tasks_test.rb
index 0f48c8d5fc..c8f4179313 100644
--- a/activerecord/test/cases/tasks/database_tasks_test.rb
+++ b/activerecord/test/cases/tasks/database_tasks_test.rb
@@ -273,6 +273,21 @@ module ActiveRecord
end
end
+ class DatabaseTasksMigrateTest < ActiveRecord::TestCase
+ def test_migrate_receives_correct_env_vars
+ verbose, version = ENV['VERBOSE'], ENV['VERSION']
+
+ ActiveRecord::Tasks::DatabaseTasks.migrations_paths = 'custom/path'
+ ENV['VERBOSE'] = 'false'
+ ENV['VERSION'] = '4'
+
+ ActiveRecord::Migrator.expects(:migrate).with('custom/path', 4)
+ ActiveRecord::Tasks::DatabaseTasks.migrate
+ ensure
+ ActiveRecord::Tasks::DatabaseTasks.migrations_paths = nil
+ ENV['VERBOSE'], ENV['VERSION'] = verbose, version
+ end
+ end
class DatabaseTasksPurgeTest < ActiveRecord::TestCase
include DatabaseTasksSetupper
@@ -296,6 +311,7 @@ module ActiveRecord
ActiveRecord::Tasks::DatabaseTasks.expects(:purge).
with('database' => 'prod-db')
+ ActiveRecord::Base.expects(:establish_connection).with(:production)
ActiveRecord::Tasks::DatabaseTasks.purge_current('production')
end
@@ -363,4 +379,20 @@ module ActiveRecord
ActiveRecord::Tasks::DatabaseTasks.check_schema_file("awesome-file.sql")
end
end
+
+ class DatabaseTasksCheckSchemaFileDefaultsTest < ActiveRecord::TestCase
+ def test_check_schema_file_defaults
+ ActiveRecord::Tasks::DatabaseTasks.stubs(:db_dir).returns('/tmp')
+ assert_equal '/tmp/schema.rb', ActiveRecord::Tasks::DatabaseTasks.schema_file
+ end
+ end
+
+ class DatabaseTasksCheckSchemaFileSpecifiedFormatsTest < ActiveRecord::TestCase
+ {ruby: 'schema.rb', sql: 'structure.sql'}.each_pair do |fmt, filename|
+ define_method("test_check_schema_file_for_#{fmt}_format") do
+ ActiveRecord::Tasks::DatabaseTasks.stubs(:db_dir).returns('/tmp')
+ assert_equal "/tmp/#{filename}", ActiveRecord::Tasks::DatabaseTasks.schema_file(fmt)
+ end
+ end
+ end
end
diff --git a/activerecord/test/cases/tasks/mysql_rake_test.rb b/activerecord/test/cases/tasks/mysql_rake_test.rb
index f58535f044..a93fa57257 100644
--- a/activerecord/test/cases/tasks/mysql_rake_test.rb
+++ b/activerecord/test/cases/tasks/mysql_rake_test.rb
@@ -265,30 +265,40 @@ module ActiveRecord
def test_structure_dump
filename = "awesome-file.sql"
- Kernel.expects(:system).with("mysqldump", "--result-file", filename, "--no-data", "test-db").returns(true)
+ Kernel.expects(:system).with("mysqldump", "--result-file", filename, "--no-data", "--routines", "test-db").returns(true)
ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename)
end
- def test_warn_when_external_structure_dump_fails
+ def test_warn_when_external_structure_dump_command_execution_fails
filename = "awesome-file.sql"
- Kernel.expects(:system).with("mysqldump", "--result-file", filename, "--no-data", "test-db").returns(false)
+ Kernel.expects(:system)
+ .with("mysqldump", "--result-file", filename, "--no-data", "--routines", "test-db")
+ .returns(false)
- warnings = capture(:stderr) do
+ e = assert_raise(RuntimeError) {
ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename)
- end
-
- assert_match(/Could not dump the database structure/, warnings)
+ }
+ assert_match(/^failed to execute: `mysqldump`$/, e.message)
end
def test_structure_dump_with_port_number
filename = "awesome-file.sql"
- Kernel.expects(:system).with("mysqldump", "--port", "10000", "--result-file", filename, "--no-data", "test-db").returns(true)
+ Kernel.expects(:system).with("mysqldump", "--port=10000", "--result-file", filename, "--no-data", "--routines", "test-db").returns(true)
ActiveRecord::Tasks::DatabaseTasks.structure_dump(
@configuration.merge('port' => 10000),
filename)
end
+
+ def test_structure_dump_with_ssl
+ filename = "awesome-file.sql"
+ Kernel.expects(:system).with("mysqldump", "--ssl-ca=ca.crt", "--result-file", filename, "--no-data", "--routines", "test-db").returns(true)
+
+ ActiveRecord::Tasks::DatabaseTasks.structure_dump(
+ @configuration.merge("sslca" => "ca.crt"),
+ filename)
+ end
end
class MySQLStructureLoadTest < ActiveRecord::TestCase
@@ -302,6 +312,7 @@ module ActiveRecord
def test_structure_load
filename = "awesome-file.sql"
Kernel.expects(:system).with('mysql', '--execute', %{SET FOREIGN_KEY_CHECKS = 0; SOURCE #{filename}; SET FOREIGN_KEY_CHECKS = 1}, "--database", "test-db")
+ .returns(true)
ActiveRecord::Tasks::DatabaseTasks.structure_load(@configuration, filename)
end
diff --git a/activerecord/test/cases/tasks/postgresql_rake_test.rb b/activerecord/test/cases/tasks/postgresql_rake_test.rb
index 0d574d071c..c31f94b2f2 100644
--- a/activerecord/test/cases/tasks/postgresql_rake_test.rb
+++ b/activerecord/test/cases/tasks/postgresql_rake_test.rb
@@ -60,7 +60,7 @@ module ActiveRecord
$stderr.expects(:puts).
with("Couldn't create database for #{@configuration.inspect}")
- ActiveRecord::Tasks::DatabaseTasks.create @configuration
+ assert_raises(Exception) { ActiveRecord::Tasks::DatabaseTasks.create @configuration }
end
def test_create_when_database_exists_outputs_info_to_stderr
@@ -195,21 +195,54 @@ module ActiveRecord
'adapter' => 'postgresql',
'database' => 'my-app-db'
}
+ @filename = "awesome-file.sql"
ActiveRecord::Base.stubs(:connection).returns(@connection)
ActiveRecord::Base.stubs(:establish_connection).returns(true)
Kernel.stubs(:system)
+ File.stubs(:open)
end
def test_structure_dump
- filename = "awesome-file.sql"
- Kernel.expects(:system).with("pg_dump -i -s -x -O -f #{filename} my-app-db").returns(true)
- @connection.expects(:schema_search_path).returns("foo")
+ Kernel.expects(:system).with('pg_dump', '-s', '-x', '-O', '-f', @filename, 'my-app-db').returns(true)
+
+ ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename)
+ 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)
+ 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)
+ 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)
+ end
+ end
+
+ private
- ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename)
- assert File.exist?(filename)
+ def with_dump_schemas(value, &block)
+ old_dump_schemas = ActiveRecord::Base.dump_schemas
+ ActiveRecord::Base.dump_schemas = value
+ yield
ensure
- FileUtils.rm(filename)
+ ActiveRecord::Base.dump_schemas = old_dump_schemas
end
end
@@ -228,14 +261,14 @@ module ActiveRecord
def test_structure_load
filename = "awesome-file.sql"
- Kernel.expects(:system).with("psql -q -f #{filename} my-app-db")
+ Kernel.expects(:system).with('psql', '-q', '-f', filename, @configuration['database']).returns(true)
ActiveRecord::Tasks::DatabaseTasks.structure_load(@configuration, filename)
end
def test_structure_load_accepts_path_with_spaces
filename = "awesome file.sql"
- Kernel.expects(:system).with("psql -q -f awesome\\ file.sql my-app-db")
+ Kernel.expects(:system).with('psql', '-q', '-f', filename, @configuration['database']).returns(true)
ActiveRecord::Tasks::DatabaseTasks.structure_load(@configuration, filename)
end
diff --git a/activerecord/test/cases/tasks/sqlite_rake_test.rb b/activerecord/test/cases/tasks/sqlite_rake_test.rb
index 750d5e42dc..0aea0c3b38 100644
--- a/activerecord/test/cases/tasks/sqlite_rake_test.rb
+++ b/activerecord/test/cases/tasks/sqlite_rake_test.rb
@@ -53,7 +53,7 @@ module ActiveRecord
$stderr.expects(:puts).
with("Couldn't create database for #{@configuration.inspect}")
- ActiveRecord::Tasks::DatabaseTasks.create @configuration, '/rails/root'
+ assert_raises(Exception) { ActiveRecord::Tasks::DatabaseTasks.create @configuration, '/rails/root' }
end
end
diff --git a/activerecord/test/cases/test_case.rb b/activerecord/test/cases/test_case.rb
index 23a170388e..47e664f4e7 100644
--- a/activerecord/test/cases/test_case.rb
+++ b/activerecord/test/cases/test_case.rb
@@ -1,10 +1,13 @@
require 'active_support/test_case'
+require 'active_support/testing/stream'
module ActiveRecord
# = Active Record Test Case
#
# Defines some test assertions to test against SQL queries.
class TestCase < ActiveSupport::TestCase #:nodoc:
+ include ActiveSupport::Testing::Stream
+
def teardown
SQLCounter.clear_log
end
@@ -13,23 +16,6 @@ module ActiveRecord
assert_equal expected.to_s, actual.to_s, message
end
- def capture(stream)
- stream = stream.to_s
- captured_stream = Tempfile.new(stream)
- stream_io = eval("$#{stream}")
- origin_stream = stream_io.dup
- stream_io.reopen(captured_stream)
-
- yield
-
- stream_io.rewind
- return captured_stream.read
- ensure
- captured_stream.close
- captured_stream.unlink
- stream_io.reopen(origin_stream)
- end
-
def capture_sql
SQLCounter.clear_log
yield
@@ -43,7 +29,7 @@ module ActiveRecord
patterns_to_match.each do |pattern|
failed_patterns << pattern unless SQLCounter.log_all.any?{ |sql| pattern === sql }
end
- assert failed_patterns.empty?, "Query pattern(s) #{failed_patterns.map{ |p| p.inspect }.join(', ')} not found.#{SQLCounter.log.size == 0 ? '' : "\nQueries:\n#{SQLCounter.log.join("\n")}"}"
+ assert failed_patterns.empty?, "Query pattern(s) #{failed_patterns.map(&:inspect).join(', ')} not found.#{SQLCounter.log.size == 0 ? '' : "\nQueries:\n#{SQLCounter.log.join("\n")}"}"
end
def assert_queries(num = 1, options = {})
@@ -79,6 +65,30 @@ module ActiveRecord
end
end
+ class PostgreSQLTestCase < TestCase
+ def self.run(*args)
+ super if current_adapter?(:PostgreSQLAdapter)
+ end
+ end
+
+ class Mysql2TestCase < TestCase
+ def self.run(*args)
+ super if current_adapter?(:Mysql2Adapter)
+ end
+ end
+
+ class MysqlTestCase < TestCase
+ def self.run(*args)
+ super if current_adapter?(:MysqlAdapter)
+ end
+ end
+
+ class SQLite3TestCase < TestCase
+ def self.run(*args)
+ super if current_adapter?(:SQLite3Adapter)
+ end
+ end
+
class SQLCounter
class << self
attr_accessor :ignored_sql, :log, :log_all
@@ -93,9 +103,9 @@ module ActiveRecord
# ignored SQL, or better yet, use a different notification for the queries
# instead examining the SQL content.
oracle_ignored = [/^select .*nextval/i, /^SAVEPOINT/, /^ROLLBACK TO/, /^\s*select .* from all_triggers/im, /^\s*select .* from all_constraints/im, /^\s*select .* from all_tab_cols/im]
- mysql_ignored = [/^SHOW TABLES/i, /^SHOW FULL FIELDS/, /^SHOW CREATE TABLE /i]
- postgresql_ignored = [/^\s*select\b.*\bfrom\b.*pg_namespace\b/im, /^\s*select\b.*\battname\b.*\bfrom\b.*\bpg_attribute\b/im, /^SHOW search_path/i]
- sqlite3_ignored = [/^\s*SELECT name\b.*\bFROM sqlite_master/im]
+ mysql_ignored = [/^SHOW FULL TABLES/i, /^SHOW FULL FIELDS/, /^SHOW CREATE TABLE /i, /^SHOW VARIABLES /, /^\s*SELECT (?:column_name|table_name)\b.*\bFROM information_schema\.(?:key_column_usage|tables)\b/im]
+ postgresql_ignored = [/^\s*select\b.*\bfrom\b.*pg_namespace\b/im, /^\s*select tablename\b.*from pg_tables\b/im, /^\s*select\b.*\battname\b.*\bfrom\b.*\bpg_attribute\b/im, /^SHOW search_path/i]
+ sqlite3_ignored = [/^\s*SELECT name\b.*\bFROM sqlite_master/im, /^\s*SELECT sql\b.*\bFROM sqlite_master/im]
[oracle_ignored, mysql_ignored, postgresql_ignored, sqlite3_ignored].each do |db_ignored_sql|
ignored_sql.concat db_ignored_sql
diff --git a/activerecord/test/cases/test_fixtures_test.rb b/activerecord/test/cases/test_fixtures_test.rb
new file mode 100644
index 0000000000..1970fe82d0
--- /dev/null
+++ b/activerecord/test/cases/test_fixtures_test.rb
@@ -0,0 +1,36 @@
+require 'cases/helper'
+
+class TestFixturesTest < ActiveRecord::TestCase
+ setup do
+ @klass = Class.new
+ @klass.send(:include, ActiveRecord::TestFixtures)
+ end
+
+ def test_deprecated_use_transactional_fixtures=
+ assert_deprecated 'use use_transactional_tests= instead' do
+ @klass.use_transactional_fixtures = true
+ end
+ end
+
+ def test_use_transactional_tests_prefers_use_transactional_fixtures
+ ActiveSupport::Deprecation.silence do
+ @klass.use_transactional_fixtures = false
+ end
+
+ assert_equal false, @klass.use_transactional_tests
+ end
+
+ def test_use_transactional_tests_defaults_to_true
+ ActiveSupport::Deprecation.silence do
+ @klass.use_transactional_fixtures = nil
+ end
+
+ assert_equal true, @klass.use_transactional_tests
+ end
+
+ def test_use_transactional_tests_can_be_overridden
+ @klass.use_transactional_tests = "foobar"
+
+ assert_equal "foobar", @klass.use_transactional_tests
+ end
+end
diff --git a/activerecord/test/cases/time_precision_test.rb b/activerecord/test/cases/time_precision_test.rb
new file mode 100644
index 0000000000..ff7a81fe60
--- /dev/null
+++ b/activerecord/test/cases/time_precision_test.rb
@@ -0,0 +1,108 @@
+require 'cases/helper'
+require 'support/schema_dumping_helper'
+
+if ActiveRecord::Base.connection.supports_datetime_with_precision?
+class TimePrecisionTest < ActiveRecord::TestCase
+ include SchemaDumpingHelper
+ self.use_transactional_tests = false
+
+ class Foo < ActiveRecord::Base; end
+
+ setup do
+ @connection = ActiveRecord::Base.connection
+ end
+
+ teardown do
+ @connection.drop_table :foos, if_exists: true
+ end
+
+ def test_time_data_type_with_precision
+ @connection.create_table(:foos, force: true)
+ @connection.add_column :foos, :start, :time, precision: 3
+ @connection.add_column :foos, :finish, :time, precision: 6
+ assert_equal 3, activerecord_column_option('foos', 'start', 'precision')
+ assert_equal 6, activerecord_column_option('foos', 'finish', 'precision')
+ end
+
+ def test_passing_precision_to_time_does_not_set_limit
+ @connection.create_table(:foos, force: true) do |t|
+ t.time :start, precision: 3
+ t.time :finish, precision: 6
+ end
+ assert_nil activerecord_column_option('foos', 'start', 'limit')
+ assert_nil activerecord_column_option('foos', 'finish', 'limit')
+ end
+
+ def test_invalid_time_precision_raises_error
+ assert_raises ActiveRecord::ActiveRecordError do
+ @connection.create_table(:foos, force: true) do |t|
+ t.time :start, precision: 7
+ t.time :finish, precision: 7
+ end
+ end
+ end
+
+ def test_database_agrees_with_activerecord_about_precision
+ @connection.create_table(:foos, force: true) do |t|
+ t.time :start, precision: 2
+ t.time :finish, precision: 4
+ end
+ assert_equal 2, database_datetime_precision('foos', 'start')
+ assert_equal 4, database_datetime_precision('foos', 'finish')
+ end
+
+ def test_formatting_time_according_to_precision
+ @connection.create_table(:foos, force: true) do |t|
+ t.time :start, precision: 0
+ t.time :finish, precision: 4
+ end
+ time = ::Time.utc(2000, 1, 1, 12, 30, 0, 999999)
+ Foo.create!(start: time, finish: time)
+ assert foo = Foo.find_by(start: time)
+ assert_equal 1, Foo.where(finish: time).count
+ assert_equal time.to_s, foo.start.to_s
+ assert_equal time.to_s, foo.finish.to_s
+ assert_equal 000000, foo.start.usec
+ assert_equal 999900, foo.finish.usec
+ end
+
+ def test_schema_dump_includes_time_precision
+ @connection.create_table(:foos, force: true) do |t|
+ t.time :start, precision: 4
+ t.time :finish, precision: 6
+ end
+ output = dump_table_schema("foos")
+ assert_match %r{t\.time\s+"start",\s+precision: 4$}, output
+ assert_match %r{t\.time\s+"finish",\s+precision: 6$}, output
+ end
+
+ if current_adapter?(:PostgreSQLAdapter)
+ def test_time_precision_with_zero_should_be_dumped
+ @connection.create_table(:foos, force: true) do |t|
+ t.time :start, precision: 0
+ t.time :finish, precision: 0
+ end
+ output = dump_table_schema("foos")
+ assert_match %r{t\.time\s+"start",\s+precision: 0$}, output
+ assert_match %r{t\.time\s+"finish",\s+precision: 0$}, output
+ end
+ end
+
+ private
+
+ def database_datetime_precision(table_name, column_name)
+ results = @connection.exec_query("SELECT column_name, datetime_precision FROM information_schema.columns WHERE table_name = '#{table_name}'")
+ result = results.find do |result_hash|
+ result_hash["column_name"] == column_name
+ end
+ result && result["datetime_precision"].to_i
+ end
+
+ def activerecord_column_option(tablename, column_name, option)
+ result = @connection.columns(tablename).find do |column|
+ column.name == column_name
+ end
+ result && result.send(option)
+ end
+end
+end
diff --git a/activerecord/test/cases/timestamp_test.rb b/activerecord/test/cases/timestamp_test.rb
index 0472246f71..970f6bcf4a 100644
--- a/activerecord/test/cases/timestamp_test.rb
+++ b/activerecord/test/cases/timestamp_test.rb
@@ -1,5 +1,7 @@
require 'cases/helper'
+require 'support/ddl_helper'
require 'models/developer'
+require 'models/computer'
require 'models/owner'
require 'models/pet'
require 'models/toy'
@@ -71,9 +73,20 @@ class TimestampTest < ActiveRecord::TestCase
assert_equal @previously_updated_at, @developer.updated_at
end
+ def test_touching_updates_timestamp_with_given_time
+ previously_updated_at = @developer.updated_at
+ new_time = Time.utc(2015, 2, 16, 0, 0, 0)
+ @developer.touch(time: new_time)
+
+ assert_not_equal previously_updated_at, @developer.updated_at
+ assert_equal new_time, @developer.updated_at
+ end
+
def test_touching_an_attribute_updates_timestamp
previously_created_at = @developer.created_at
- @developer.touch(:created_at)
+ travel(1.second) do
+ @developer.touch(:created_at)
+ end
assert !@developer.created_at_changed? , 'created_at should not be changed'
assert !@developer.changed?, 'record should not be changed'
@@ -89,6 +102,18 @@ class TimestampTest < ActiveRecord::TestCase
assert_in_delta Time.now, task.ending, 1
end
+ def test_touching_an_attribute_updates_timestamp_with_given_time
+ previously_updated_at = @developer.updated_at
+ previously_created_at = @developer.created_at
+ new_time = Time.utc(2015, 2, 16, 4, 54, 0)
+ @developer.touch(:created_at, time: new_time)
+
+ 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_touching_many_attributes_updates_them
task = Task.first
previous_starting = task.starting
@@ -176,8 +201,10 @@ class TimestampTest < ActiveRecord::TestCase
owner = pet.owner
previously_owner_updated_at = owner.updated_at
- pet.name = "Fluffy the Third"
- pet.save
+ travel(1.second) do
+ pet.name = "Fluffy the Third"
+ pet.save
+ end
assert_not_equal previously_owner_updated_at, pet.owner.updated_at
end
@@ -187,7 +214,9 @@ class TimestampTest < ActiveRecord::TestCase
owner = pet.owner
previously_owner_updated_at = owner.updated_at
- pet.destroy
+ travel(1.second) do
+ pet.destroy
+ end
assert_not_equal previously_owner_updated_at, pet.owner.updated_at
end
@@ -231,8 +260,10 @@ class TimestampTest < ActiveRecord::TestCase
owner.update_columns(happy_at: 3.days.ago)
previously_owner_updated_at = owner.updated_at
- pet.name = "I'm a parrot"
- pet.save
+ travel(1.second) do
+ pet.name = "I'm a parrot"
+ pet.save
+ end
assert_not_equal previously_owner_updated_at, pet.owner.updated_at
end
@@ -423,4 +454,33 @@ class TimestampTest < ActiveRecord::TestCase
toy = Toy.first
assert_equal [:created_at, :updated_at], toy.send(:all_timestamp_attributes_in_model)
end
+
+ def test_index_is_created_for_both_timestamps
+ ActiveRecord::Base.connection.create_table(:foos, force: true) do |t|
+ t.timestamps(:foos, null: true, index: true)
+ end
+
+ indexes = ActiveRecord::Base.connection.indexes('foos')
+ assert_equal ['created_at', 'updated_at'], indexes.flat_map(&:columns).sort
+ ensure
+ ActiveRecord::Base.connection.drop_table(:foos)
+ end
+end
+
+class TimestampsWithoutTransactionTest < ActiveRecord::TestCase
+ include DdlHelper
+ self.use_transactional_tests = false
+
+ class TimestampAttributePost < ActiveRecord::Base
+ attr_accessor :created_at, :updated_at
+ end
+
+ def test_do_not_write_timestamps_on_save_if_they_are_not_attributes
+ with_example_table ActiveRecord::Base.connection, "timestamp_attribute_posts", "id integer primary key" do
+ post = TimestampAttributePost.new(id: 1)
+ post.save! # should not try to assign and persist created_at, updated_at
+ assert_nil post.created_at
+ assert_nil post.updated_at
+ end
+ end
end
diff --git a/activerecord/test/cases/touch_later_test.rb b/activerecord/test/cases/touch_later_test.rb
new file mode 100644
index 0000000000..7058f4fbe2
--- /dev/null
+++ b/activerecord/test/cases/touch_later_test.rb
@@ -0,0 +1,114 @@
+require 'cases/helper'
+require 'models/invoice'
+require 'models/line_item'
+require 'models/topic'
+require 'models/node'
+require 'models/tree'
+
+class TouchLaterTest < ActiveRecord::TestCase
+ fixtures :nodes, :trees
+
+ def test_touch_laster_raise_if_non_persisted
+ invoice = Invoice.new
+ Invoice.transaction do
+ assert_not invoice.persisted?
+ assert_raises(ActiveRecord::ActiveRecordError) do
+ invoice.touch_later
+ end
+ end
+ end
+
+ def test_touch_later_dont_set_dirty_attributes
+ invoice = Invoice.create!
+ invoice.touch_later
+ assert_not invoice.changed?
+ end
+
+ def test_touch_later_update_the_attributes
+ time = Time.now.utc - 25.days
+ topic = Topic.create!(updated_at: time, created_at: time)
+ assert_equal time.to_i, topic.updated_at.to_i
+ assert_equal time.to_i, topic.created_at.to_i
+
+ Topic.transaction do
+ topic.touch_later(:created_at)
+ assert_not_equal time.to_i, topic.updated_at.to_i
+ assert_not_equal time.to_i, topic.created_at.to_i
+
+ assert_equal time.to_i, topic.reload.updated_at.to_i
+ assert_equal time.to_i, topic.reload.created_at.to_i
+ end
+ assert_not_equal time.to_i, topic.reload.updated_at.to_i
+ assert_not_equal time.to_i, topic.reload.created_at.to_i
+ end
+
+ def test_touch_touches_immediately
+ time = Time.now.utc - 25.days
+ topic = Topic.create!(updated_at: time, created_at: time)
+ assert_equal time.to_i, topic.updated_at.to_i
+ assert_equal time.to_i, topic.created_at.to_i
+
+ Topic.transaction do
+ topic.touch_later(:created_at)
+ topic.touch
+
+ assert_not_equal time, topic.reload.updated_at
+ assert_not_equal time, topic.reload.created_at
+ end
+ end
+
+ def test_touch_later_an_association_dont_autosave_parent
+ time = Time.now.utc - 25.days
+ line_item = LineItem.create!(amount: 1)
+ invoice = Invoice.create!(line_items: [line_item])
+ invoice.touch(time: time)
+
+ Invoice.transaction do
+ line_item.update(amount: 2)
+ assert_equal time.to_i, invoice.reload.updated_at.to_i
+ end
+
+ assert_not_equal time.to_i, invoice.updated_at.to_i
+ end
+
+ def test_touch_touches_immediately_with_a_custom_time
+ time = (Time.now.utc - 25.days).change(nsec: 0)
+ topic = Topic.create!(updated_at: time, created_at: time)
+ assert_equal time, topic.updated_at
+ assert_equal time, topic.created_at
+
+ Topic.transaction do
+ topic.touch_later(:created_at)
+ time = Time.now.utc - 2.days
+ topic.touch(time: time)
+
+ assert_equal time.to_i, topic.reload.updated_at.to_i
+ assert_equal time.to_i, topic.reload.created_at.to_i
+ end
+ end
+
+ def test_touch_later_dont_hit_the_db
+ invoice = Invoice.create!
+ assert_queries(0) do
+ invoice.touch_later
+ end
+ end
+
+ def test_touching_three_deep
+ skip "Pending from #19324"
+
+ previous_tree_updated_at = trees(:root).updated_at
+ previous_grandparent_updated_at = nodes(:grandparent).updated_at
+ previous_parent_updated_at = nodes(:parent_a).updated_at
+ previous_child_updated_at = nodes(:child_one_of_a).updated_at
+
+ travel 5.seconds
+
+ Node.create! parent: nodes(:child_one_of_a), tree: trees(:root)
+
+ assert_not_equal nodes(:child_one_of_a).reload.updated_at, previous_child_updated_at
+ assert_not_equal nodes(:parent_a).reload.updated_at, previous_parent_updated_at
+ assert_not_equal nodes(:grandparent).reload.updated_at, previous_grandparent_updated_at
+ assert_not_equal trees(:root).reload.updated_at, previous_tree_updated_at
+ end
+end
diff --git a/activerecord/test/cases/transaction_callbacks_test.rb b/activerecord/test/cases/transaction_callbacks_test.rb
index 3d64ecb464..f2229939c8 100644
--- a/activerecord/test/cases/transaction_callbacks_test.rb
+++ b/activerecord/test/cases/transaction_callbacks_test.rb
@@ -4,7 +4,6 @@ require 'models/pet'
require 'models/topic'
class TransactionCallbacksTest < ActiveRecord::TestCase
- self.use_transactional_fixtures = false
fixtures :topics, :owners, :pets
class ReplyWithCallbacks < ActiveRecord::Base
@@ -129,6 +128,19 @@ class TransactionCallbacksTest < ActiveRecord::TestCase
assert_equal [:commit_on_update], @first.history
end
+ def test_only_call_after_commit_on_top_level_transactions
+ @first.after_commit_block{|r| r.history << :after_commit}
+ assert @first.history.empty?
+
+ @first.transaction do
+ @first.transaction(requires_new: true) do
+ @first.touch
+ end
+ assert @first.history.empty?
+ end
+ assert_equal [:after_commit], @first.history
+ end
+
def test_call_after_rollback_after_transaction_rollsback
@first.after_commit_block{|r| r.history << :after_commit}
@first.after_rollback_block{|r| r.history << :after_rollback}
@@ -187,21 +199,21 @@ class TransactionCallbacksTest < ActiveRecord::TestCase
end
def test_call_after_rollback_when_commit_fails
- @first.class.connection.singleton_class.send(:alias_method, :real_method_commit_db_transaction, :commit_db_transaction)
- begin
- @first.class.connection.singleton_class.class_eval do
- def commit_db_transaction; raise "boom!"; end
- end
+ @first.after_commit_block { |r| r.history << :after_commit }
+ @first.after_rollback_block { |r| r.history << :after_rollback }
- @first.after_commit_block{|r| r.history << :after_commit}
- @first.after_rollback_block{|r| r.history << :after_rollback}
+ assert_raises RuntimeError do
+ @first.transaction do
+ tx = @first.class.connection.transaction_manager.current_transaction
+ def tx.commit
+ raise
+ end
- assert !@first.save rescue nil
- assert_equal [:after_rollback], @first.history
- ensure
- @first.class.connection.singleton_class.send(:remove_method, :commit_db_transaction)
- @first.class.connection.singleton_class.send(:alias_method, :commit_db_transaction, :real_method_commit_db_transaction)
+ @first.save
+ end
end
+
+ assert_equal [:after_rollback], @first.history
end
def test_only_call_after_rollback_on_records_rolled_back_to_a_savepoint
@@ -253,39 +265,78 @@ class TransactionCallbacksTest < ActiveRecord::TestCase
assert_equal 2, @first.rollbacks
end
- def test_after_transaction_callbacks_should_prevent_callbacks_from_being_called
- def @first.last_after_transaction_error=(e); @last_transaction_error = e; end
- def @first.last_after_transaction_error; @last_transaction_error; end
- @first.after_commit_block{|r| r.last_after_transaction_error = :commit; raise "fail!";}
- @first.after_rollback_block{|r| r.last_after_transaction_error = :rollback; raise "fail!";}
+ def test_after_commit_callback_should_not_swallow_errors
+ @first.after_commit_block{ fail "boom" }
+ assert_raises(RuntimeError) do
+ Topic.transaction do
+ @first.save!
+ end
+ end
+ end
- second = TopicWithCallbacks.find(3)
- second.after_commit_block{|r| r.history << :after_commit}
- second.after_rollback_block{|r| r.history << :after_rollback}
+ def test_after_commit_callback_when_raise_should_not_restore_state
+ first = TopicWithCallbacks.new
+ second = TopicWithCallbacks.new
+ first.after_commit_block{ fail "boom" }
+ second.after_commit_block{ fail "boom" }
- Topic.transaction do
- @first.save!
- second.save!
+ begin
+ Topic.transaction do
+ first.save!
+ assert_not_nil first.id
+ second.save!
+ assert_not_nil second.id
+ end
+ rescue
end
- assert_equal :commit, @first.last_after_transaction_error
- assert_equal [:after_commit], second.history
+ assert_not_nil first.id
+ assert_not_nil second.id
+ assert first.reload
+ end
- second.history.clear
- Topic.transaction do
- @first.save!
- second.save!
- raise ActiveRecord::Rollback
+ def test_after_rollback_callback_should_not_swallow_errors_when_set_to_raise
+ error_class = Class.new(StandardError)
+ @first.after_rollback_block{ raise error_class }
+ assert_raises(error_class) do
+ Topic.transaction do
+ @first.save!
+ raise ActiveRecord::Rollback
+ end
+ end
+ end
+
+ def test_after_rollback_callback_when_raise_should_restore_state
+ error_class = Class.new(StandardError)
+
+ first = TopicWithCallbacks.new
+ second = TopicWithCallbacks.new
+ first.after_rollback_block{ raise error_class }
+ second.after_rollback_block{ raise error_class }
+
+ begin
+ Topic.transaction do
+ first.save!
+ assert_not_nil first.id
+ second.save!
+ assert_not_nil second.id
+ raise ActiveRecord::Rollback
+ end
+ rescue error_class
end
- assert_equal :rollback, @first.last_after_transaction_error
- assert_equal [:after_rollback], second.history
+ assert_nil first.id
+ assert_nil second.id
end
def test_after_rollback_callbacks_should_validate_on_condition
assert_raise(ArgumentError) { Topic.after_rollback(on: :save) }
+ e = assert_raise(ArgumentError) { Topic.after_rollback(on: 'create') }
+ assert_match(/:on conditions for after_commit and after_rollback callbacks have to be one of \[:create, :destroy, :update\]/, e.message)
end
def test_after_commit_callbacks_should_validate_on_condition
assert_raise(ArgumentError) { Topic.after_commit(on: :save) }
+ e = assert_raise(ArgumentError) { Topic.after_commit(on: 'create') }
+ assert_match(/:on conditions for after_commit and after_rollback callbacks have to be one of \[:create, :destroy, :update\]/, e.message)
end
def test_saving_a_record_with_a_belongs_to_that_specifies_touching_the_parent_should_call_callbacks_on_the_parent_object
@@ -316,7 +367,7 @@ class TransactionCallbacksTest < ActiveRecord::TestCase
end
class CallbacksOnMultipleActionsTest < ActiveRecord::TestCase
- self.use_transactional_fixtures = false
+ self.use_transactional_tests = false
class TopicWithCallbacksOnMultipleActions < ActiveRecord::Base
self.table_name = :topics
@@ -325,6 +376,9 @@ class CallbacksOnMultipleActionsTest < ActiveRecord::TestCase
after_commit(on: [:create, :update]) { |record| record.history << :create_and_update }
after_commit(on: [:update, :destroy]) { |record| record.history << :update_and_destroy }
+ before_commit(if: :save_before_commit_history) { |record| record.history << :before_commit }
+ before_commit(if: :update_title) { |record| record.update(title: "before commit title") }
+
def clear_history
@history = []
end
@@ -332,6 +386,8 @@ class CallbacksOnMultipleActionsTest < ActiveRecord::TestCase
def history
@history ||= []
end
+
+ attr_accessor :save_before_commit_history, :update_title
end
def test_after_commit_on_multiple_actions
@@ -348,4 +404,81 @@ class CallbacksOnMultipleActionsTest < ActiveRecord::TestCase
topic.destroy
assert_equal [:update_and_destroy, :create_and_destroy], topic.history
end
+
+ def test_before_commit_actions
+ topic = TopicWithCallbacksOnMultipleActions.new
+ topic.save_before_commit_history = true
+ topic.save
+
+ assert_equal [:before_commit, :create_and_update, :create_and_destroy], topic.history
+ end
+
+ def test_before_commit_update_in_same_transaction
+ topic = TopicWithCallbacksOnMultipleActions.new
+ topic.update_title = true
+ topic.save
+
+ assert_equal "before commit title", topic.title
+ assert_equal "before commit title", topic.reload.title
+ end
+end
+
+
+class TransactionEnrollmentCallbacksTest < ActiveRecord::TestCase
+
+ class TopicWithoutTransactionalEnrollmentCallbacks < ActiveRecord::Base
+ self.table_name = :topics
+
+ before_commit_without_transaction_enrollment { |r| r.history << :before_commit }
+ after_commit_without_transaction_enrollment { |r| r.history << :after_commit }
+ after_rollback_without_transaction_enrollment { |r| r.history << :rollback }
+
+ def history
+ @history ||= []
+ end
+ end
+
+ def setup
+ @topic = TopicWithoutTransactionalEnrollmentCallbacks.create!
+ end
+
+ def test_commit_does_not_run_transactions_callbacks_without_enrollment
+ @topic.transaction do
+ @topic.content = 'foo'
+ @topic.save!
+ end
+ assert @topic.history.empty?
+ end
+
+ def test_commit_run_transactions_callbacks_with_explicit_enrollment
+ @topic.transaction do
+ 2.times do
+ @topic.content = 'foo'
+ @topic.save!
+ end
+ @topic.class.connection.add_transaction_record(@topic)
+ end
+ assert_equal [:before_commit, :after_commit], @topic.history
+ end
+
+ def test_rollback_does_not_run_transactions_callbacks_without_enrollment
+ @topic.transaction do
+ @topic.content = 'foo'
+ @topic.save!
+ raise ActiveRecord::Rollback
+ end
+ assert @topic.history.empty?
+ end
+
+ def test_rollback_run_transactions_callbacks_with_explicit_enrollment
+ @topic.transaction do
+ 2.times do
+ @topic.content = 'foo'
+ @topic.save!
+ end
+ @topic.class.connection.add_transaction_record(@topic)
+ raise ActiveRecord::Rollback
+ end
+ assert_equal [:rollback], @topic.history
+ end
end
diff --git a/activerecord/test/cases/transaction_isolation_test.rb b/activerecord/test/cases/transaction_isolation_test.rb
index f89c26532d..2f7d208ed2 100644
--- a/activerecord/test/cases/transaction_isolation_test.rb
+++ b/activerecord/test/cases/transaction_isolation_test.rb
@@ -2,7 +2,7 @@ require 'cases/helper'
unless ActiveRecord::Base.connection.supports_transaction_isolation?
class TransactionIsolationUnsupportedTest < ActiveRecord::TestCase
- self.use_transactional_fixtures = false
+ self.use_transactional_tests = false
class Tag < ActiveRecord::Base
end
@@ -17,7 +17,7 @@ end
if ActiveRecord::Base.connection.supports_transaction_isolation?
class TransactionIsolationTest < ActiveRecord::TestCase
- self.use_transactional_fixtures = false
+ self.use_transactional_tests = false
class Tag < ActiveRecord::Base
self.table_name = 'tags'
diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb
index f28a7b00e2..ec5bdfd725 100644
--- a/activerecord/test/cases/transactions_test.rb
+++ b/activerecord/test/cases/transactions_test.rb
@@ -2,17 +2,18 @@ require "cases/helper"
require 'models/topic'
require 'models/reply'
require 'models/developer'
+require 'models/computer'
require 'models/book'
require 'models/author'
require 'models/post'
require 'models/movie'
class TransactionTest < ActiveRecord::TestCase
- self.use_transactional_fixtures = false
+ self.use_transactional_tests = false
fixtures :topics, :developers, :authors, :posts
def setup
- @first, @second = Topic.find(1, 2).sort_by { |t| t.id }
+ @first, @second = Topic.find(1, 2).sort_by(&:id)
end
def test_persisted_in_a_model_with_custom_primary_key_after_failed_save
@@ -80,6 +81,30 @@ class TransactionTest < ActiveRecord::TestCase
end
end
+ def test_number_of_transactions_in_commit
+ num = nil
+
+ Topic.connection.class_eval do
+ alias :real_commit_db_transaction :commit_db_transaction
+ define_method(:commit_db_transaction) do
+ num = transaction_manager.open_transactions
+ real_commit_db_transaction
+ end
+ end
+
+ Topic.transaction do
+ @first.approved = true
+ @first.save!
+ end
+
+ assert_equal 0, num
+ ensure
+ Topic.connection.class_eval do
+ remove_method :commit_db_transaction
+ alias :commit_db_transaction :real_commit_db_transaction rescue nil
+ end
+ end
+
def test_successful_with_instance_method
@first.transaction do
@first.approved = true
@@ -150,13 +175,20 @@ class TransactionTest < ActiveRecord::TestCase
assert topic.new_record?, "#{topic.inspect} should be new record"
end
+ def test_transaction_state_is_cleared_when_record_is_persisted
+ author = Author.create! name: 'foo'
+ author.name = nil
+ assert_not author.save
+ assert_not author.new_record?
+ end
+
def test_update_should_rollback_on_failure
author = Author.find(1)
posts_count = author.posts.size
assert posts_count > 0
status = author.update(name: nil, post_ids: [])
assert !status
- assert_equal posts_count, author.posts(true).size
+ assert_equal posts_count, author.posts.reload.size
end
def test_update_should_rollback_on_failure!
@@ -166,7 +198,17 @@ class TransactionTest < ActiveRecord::TestCase
assert_raise(ActiveRecord::RecordInvalid) do
author.update!(name: nil, post_ids: [])
end
- assert_equal posts_count, author.posts(true).size
+ assert_equal posts_count, author.posts.reload.size
+ end
+
+ def test_cancellation_from_returning_false_in_before_filter
+ def @first.before_save_for_transaction
+ false
+ end
+
+ assert_deprecated do
+ @first.save
+ end
end
def test_cancellation_from_before_destroy_rollbacks_in_destroy
@@ -445,13 +487,17 @@ class TransactionTest < ActiveRecord::TestCase
end
def test_rollback_when_commit_raises
- Topic.connection.expects(:begin_db_transaction)
- Topic.connection.expects(:commit_db_transaction).raises('OH NOES')
- Topic.connection.expects(:rollback_db_transaction)
+ assert_called(Topic.connection, :begin_db_transaction) do
+ Topic.connection.stub(:commit_db_transaction, ->{ raise('OH NOES') }) do
+ assert_called(Topic.connection, :rollback_db_transaction) do
- assert_raise RuntimeError do
- Topic.transaction do
- # do nothing
+ e = assert_raise RuntimeError do
+ Topic.transaction do
+ # do nothing
+ end
+ end
+ assert_equal 'OH NOES', e.message
+ end
end
end
end
@@ -468,6 +514,34 @@ class TransactionTest < ActiveRecord::TestCase
assert topic.frozen?, 'not frozen'
end
+ def test_rollback_when_thread_killed
+ return if in_memory_db?
+
+ queue = Queue.new
+ thread = Thread.new do
+ Topic.transaction do
+ @first.approved = true
+ @second.approved = false
+ @first.save
+
+ queue.push nil
+ sleep
+
+ @second.save
+ end
+ end
+
+ queue.pop
+ thread.kill
+ thread.join
+
+ assert @first.approved?, "First should still be changed in the objects"
+ assert !@second.approved?, "Second should still be changed in the objects"
+
+ assert !Topic.find(1).approved?, "First shouldn't have been approved"
+ assert Topic.find(2).approved?, "Second should still be approved"
+ end
+
def test_restore_active_record_state_for_all_records_in_a_transaction
topic_without_callbacks = Class.new(ActiveRecord::Base) do
self.table_name = 'topics'
@@ -506,6 +580,39 @@ class TransactionTest < ActiveRecord::TestCase
assert !@second.destroyed?, 'not destroyed'
end
+ def test_restore_frozen_state_after_double_destroy
+ topic = Topic.create
+ reply = topic.replies.create
+
+ Topic.transaction do
+ topic.destroy # calls #destroy on reply (since dependent: destroy)
+ reply.destroy
+
+ raise ActiveRecord::Rollback
+ end
+
+ assert_not reply.frozen?
+ assert_not topic.frozen?
+ end
+
+ def test_rollback_of_frozen_records
+ topic = Topic.create.freeze
+ Topic.transaction do
+ topic.destroy
+ raise ActiveRecord::Rollback
+ end
+ assert topic.frozen?, 'frozen'
+ end
+
+ def test_rollback_for_freshly_persisted_records
+ topic = Topic.create
+ Topic.transaction do
+ topic.destroy
+ raise ActiveRecord::Rollback
+ end
+ assert topic.persisted?, 'persisted'
+ end
+
def test_sqlite_add_column_in_transaction
return true unless current_adapter?(:SQLite3Adapter)
@@ -546,13 +653,13 @@ class TransactionTest < ActiveRecord::TestCase
def test_transactions_state_from_rollback
connection = Topic.connection
- transaction = ActiveRecord::ConnectionAdapters::ClosedTransaction.new(connection).begin
+ transaction = ActiveRecord::ConnectionAdapters::TransactionManager.new(connection).begin_transaction
assert transaction.open?
assert !transaction.state.rolledback?
assert !transaction.state.committed?
- transaction.perform_rollback
+ transaction.rollback
assert transaction.state.rolledback?
assert !transaction.state.committed?
@@ -560,18 +667,39 @@ class TransactionTest < ActiveRecord::TestCase
def test_transactions_state_from_commit
connection = Topic.connection
- transaction = ActiveRecord::ConnectionAdapters::ClosedTransaction.new(connection).begin
+ transaction = ActiveRecord::ConnectionAdapters::TransactionManager.new(connection).begin_transaction
assert transaction.open?
assert !transaction.state.rolledback?
assert !transaction.state.committed?
- transaction.perform_commit
+ transaction.commit
assert !transaction.state.rolledback?
assert transaction.state.committed?
end
+ def test_transaction_rollback_with_primarykeyless_tables
+ connection = ActiveRecord::Base.connection
+ connection.create_table(:transaction_without_primary_keys, force: true, id: false) do |t|
+ t.integer :thing_id
+ end
+
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = 'transaction_without_primary_keys'
+ after_commit { } # necessary to trigger the has_transactional_callbacks branch
+ end
+
+ assert_no_difference(-> { klass.count }) do
+ ActiveRecord::Base.transaction do
+ klass.create!
+ raise ActiveRecord::Rollback
+ end
+ end
+ ensure
+ connection.drop_table 'transaction_without_primary_keys', if_exists: true
+ end
+
private
%w(validation save destroy).each do |filter|
@@ -579,14 +707,14 @@ class TransactionTest < ActiveRecord::TestCase
meta = class << topic; self; end
meta.send("define_method", "before_#{filter}_for_transaction") do
Book.create
- false
+ throw(:abort)
end
end
end
end
class TransactionsWithTransactionalFixturesTest < ActiveRecord::TestCase
- self.use_transactional_fixtures = true
+ self.use_transactional_tests = true
fixtures :topics
def test_automatic_savepoint_in_outer_transaction
@@ -643,7 +771,7 @@ if current_adapter?(:PostgreSQLAdapter)
end
end
- threads.each { |t| t.join }
+ threads.each(&:join)
end
end
@@ -691,7 +819,7 @@ if current_adapter?(:PostgreSQLAdapter)
Developer.connection.close
end
- threads.each { |t| t.join }
+ threads.each(&:join)
end
assert_equal original_salary, Developer.find(1).salary
diff --git a/activerecord/test/cases/type/adapter_specific_registry_test.rb b/activerecord/test/cases/type/adapter_specific_registry_test.rb
new file mode 100644
index 0000000000..8b836b4793
--- /dev/null
+++ b/activerecord/test/cases/type/adapter_specific_registry_test.rb
@@ -0,0 +1,133 @@
+require "cases/helper"
+
+module ActiveRecord
+ class AdapterSpecificRegistryTest < ActiveRecord::TestCase
+ test "a class can be registered for a symbol" do
+ registry = Type::AdapterSpecificRegistry.new
+ registry.register(:foo, ::String)
+ registry.register(:bar, ::Array)
+
+ assert_equal "", registry.lookup(:foo)
+ assert_equal [], registry.lookup(:bar)
+ end
+
+ test "a block can be registered" do
+ registry = Type::AdapterSpecificRegistry.new
+ registry.register(:foo) do |*args|
+ [*args, "block for foo"]
+ end
+ registry.register(:bar) do |*args|
+ [*args, "block for bar"]
+ end
+
+ assert_equal [:foo, 1, "block for foo"], registry.lookup(:foo, 1)
+ assert_equal [:foo, 2, "block for foo"], registry.lookup(:foo, 2)
+ assert_equal [:bar, 1, 2, 3, "block for bar"], registry.lookup(:bar, 1, 2, 3)
+ end
+
+ test "filtering by adapter" do
+ registry = Type::AdapterSpecificRegistry.new
+ registry.register(:foo, String, adapter: :sqlite3)
+ registry.register(:foo, Array, adapter: :postgresql)
+
+ assert_equal "", registry.lookup(:foo, adapter: :sqlite3)
+ assert_equal [], registry.lookup(:foo, adapter: :postgresql)
+ end
+
+ test "an error is raised if both a generic and adapter specific type match" do
+ registry = Type::AdapterSpecificRegistry.new
+ registry.register(:foo, String)
+ registry.register(:foo, Array, adapter: :postgresql)
+
+ assert_raises TypeConflictError do
+ registry.lookup(:foo, adapter: :postgresql)
+ end
+ assert_equal "", registry.lookup(:foo, adapter: :sqlite3)
+ end
+
+ test "a generic type can explicitly override an adapter specific type" do
+ registry = Type::AdapterSpecificRegistry.new
+ registry.register(:foo, String, override: true)
+ registry.register(:foo, Array, adapter: :postgresql)
+
+ assert_equal "", registry.lookup(:foo, adapter: :postgresql)
+ assert_equal "", registry.lookup(:foo, adapter: :sqlite3)
+ end
+
+ test "a generic type can explicitly allow an adapter type to be used instead" do
+ registry = Type::AdapterSpecificRegistry.new
+ registry.register(:foo, String, override: false)
+ registry.register(:foo, Array, adapter: :postgresql)
+
+ assert_equal [], registry.lookup(:foo, adapter: :postgresql)
+ assert_equal "", registry.lookup(:foo, adapter: :sqlite3)
+ end
+
+ test "a reasonable error is given when no type is found" do
+ registry = Type::AdapterSpecificRegistry.new
+
+ e = assert_raises(ArgumentError) do
+ registry.lookup(:foo)
+ end
+
+ assert_equal "Unknown type :foo", e.message
+ end
+
+ test "construct args are passed to the type" do
+ type = Struct.new(:args)
+ registry = Type::AdapterSpecificRegistry.new
+ registry.register(:foo, type)
+
+ assert_equal type.new, registry.lookup(:foo)
+ assert_equal type.new(:ordered_arg), registry.lookup(:foo, :ordered_arg)
+ assert_equal type.new(keyword: :arg), registry.lookup(:foo, keyword: :arg)
+ assert_equal type.new(keyword: :arg), registry.lookup(:foo, keyword: :arg, adapter: :postgresql)
+ end
+
+ test "registering a modifier" do
+ decoration = Struct.new(:value)
+ registry = Type::AdapterSpecificRegistry.new
+ registry.register(:foo, String)
+ registry.register(:bar, Hash)
+ registry.add_modifier({ array: true }, decoration)
+
+ assert_equal decoration.new(""), registry.lookup(:foo, array: true)
+ assert_equal decoration.new({}), registry.lookup(:bar, array: true)
+ assert_equal "", registry.lookup(:foo)
+ end
+
+ test "registering multiple modifiers" do
+ decoration = Struct.new(:value)
+ other_decoration = Struct.new(:value)
+ registry = Type::AdapterSpecificRegistry.new
+ registry.register(:foo, String)
+ registry.add_modifier({ array: true }, decoration)
+ registry.add_modifier({ range: true }, other_decoration)
+
+ assert_equal "", registry.lookup(:foo)
+ assert_equal decoration.new(""), registry.lookup(:foo, array: true)
+ assert_equal other_decoration.new(""), registry.lookup(:foo, range: true)
+ assert_equal(
+ decoration.new(other_decoration.new("")),
+ registry.lookup(:foo, array: true, range: true)
+ )
+ end
+
+ test "registering adapter specific modifiers" do
+ decoration = Struct.new(:value)
+ type = Struct.new(:args)
+ registry = Type::AdapterSpecificRegistry.new
+ registry.register(:foo, type)
+ registry.add_modifier({ array: true }, decoration, adapter: :postgresql)
+
+ assert_equal(
+ decoration.new(type.new(keyword: :arg)),
+ registry.lookup(:foo, array: true, adapter: :postgresql, keyword: :arg)
+ )
+ assert_equal(
+ type.new(array: true),
+ registry.lookup(:foo, array: true, adapter: :sqlite3)
+ )
+ end
+ end
+end
diff --git a/activerecord/test/cases/type/date_time_test.rb b/activerecord/test/cases/type/date_time_test.rb
new file mode 100644
index 0000000000..bc4900e1c2
--- /dev/null
+++ b/activerecord/test/cases/type/date_time_test.rb
@@ -0,0 +1,14 @@
+require "cases/helper"
+require "models/task"
+
+module ActiveRecord
+ module Type
+ class IntegerTest < ActiveRecord::TestCase
+ def test_datetime_seconds_precision_applied_to_timestamp
+ skip "This test is invalid if subsecond precision isn't supported" unless subsecond_precision_supported?
+ p = Task.create!(starting: ::Time.now)
+ assert_equal p.starting.usec, p.reload.starting.usec
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/type/decimal_test.rb b/activerecord/test/cases/type/decimal_test.rb
deleted file mode 100644
index 951cd879dd..0000000000
--- a/activerecord/test/cases/type/decimal_test.rb
+++ /dev/null
@@ -1,33 +0,0 @@
-require "cases/helper"
-
-module ActiveRecord
- module Type
- class DecimalTest < ActiveRecord::TestCase
- def test_type_cast_decimal
- type = Decimal.new
- assert_equal BigDecimal.new("0"), type.type_cast_from_user(BigDecimal.new("0"))
- assert_equal BigDecimal.new("123"), type.type_cast_from_user(123.0)
- assert_equal BigDecimal.new("1"), type.type_cast_from_user(:"1")
- end
-
- def test_type_cast_decimal_from_rational_with_precision
- type = Decimal.new(precision: 2)
- assert_equal BigDecimal("0.33"), type.type_cast_from_user(Rational(1, 3))
- end
-
- def test_type_cast_decimal_from_rational_without_precision_defaults_to_18_36
- type = Decimal.new
- assert_equal BigDecimal("0.333333333333333333E0"), type.type_cast_from_user(Rational(1, 3))
- end
-
- def test_type_cast_decimal_from_object_responding_to_d
- value = Object.new
- def value.to_d
- BigDecimal.new("1")
- end
- type = Decimal.new
- assert_equal BigDecimal("1"), type.type_cast_from_user(value)
- end
- end
- end
-end
diff --git a/activerecord/test/cases/type/integer_test.rb b/activerecord/test/cases/type/integer_test.rb
new file mode 100644
index 0000000000..c0932d5357
--- /dev/null
+++ b/activerecord/test/cases/type/integer_test.rb
@@ -0,0 +1,27 @@
+require "cases/helper"
+require "models/company"
+
+module ActiveRecord
+ module Type
+ class IntegerTest < ActiveRecord::TestCase
+ test "casting ActiveRecord models" do
+ type = Type::Integer.new
+ firm = Firm.create(:name => 'Apple')
+ assert_nil type.cast(firm)
+ end
+
+ test "values which are out of range can be re-assigned" do
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = 'posts'
+ attribute :foo, :integer
+ end
+ model = klass.new
+
+ model.foo = 2147483648
+ model.foo = 1
+
+ assert_equal 1, model.foo
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/type/string_test.rb b/activerecord/test/cases/type/string_test.rb
index 420177ed49..6fe6d46711 100644
--- a/activerecord/test/cases/type/string_test.rb
+++ b/activerecord/test/cases/type/string_test.rb
@@ -2,20 +2,6 @@ require 'cases/helper'
module ActiveRecord
class StringTypeTest < ActiveRecord::TestCase
- test "type casting" do
- type = Type::String.new
- assert_equal "1", type.type_cast_from_user(true)
- assert_equal "0", type.type_cast_from_user(false)
- assert_equal "123", type.type_cast_from_user(123)
- end
-
- test "values are duped coming out" do
- s = "foo"
- type = Type::String.new
- assert_not_same s, type.type_cast_from_user(s)
- assert_not_same s, type.type_cast_from_database(s)
- end
-
test "string mutations are detected" do
klass = Class.new(Base)
klass.table_name = 'authors'
diff --git a/activerecord/test/cases/type/type_map_test.rb b/activerecord/test/cases/type/type_map_test.rb
index 4e32f92dd0..172c6dfc4c 100644
--- a/activerecord/test/cases/type/type_map_test.rb
+++ b/activerecord/test/cases/type/type_map_test.rb
@@ -124,6 +124,53 @@ module ActiveRecord
assert_equal mapping.lookup(3), 'string'
assert_kind_of Type::Value, mapping.lookup(4)
end
+
+ def test_fetch
+ mapping = TypeMap.new
+ mapping.register_type(1, "string")
+
+ assert_equal "string", mapping.fetch(1) { "int" }
+ assert_equal "int", mapping.fetch(2) { "int" }
+ end
+
+ def test_fetch_yields_args
+ mapping = TypeMap.new
+
+ assert_equal "foo-1-2-3", mapping.fetch("foo", 1, 2, 3) { |*args| args.join("-") }
+ assert_equal "bar-1-2-3", mapping.fetch("bar", 1, 2, 3) { |*args| args.join("-") }
+ end
+
+ def test_fetch_memoizes
+ mapping = TypeMap.new
+
+ looked_up = false
+ mapping.register_type(1) do
+ fail if looked_up
+ looked_up = true
+ "string"
+ end
+
+ assert_equal "string", mapping.fetch(1)
+ assert_equal "string", mapping.fetch(1)
+ end
+
+ def test_fetch_memoizes_on_args
+ mapping = TypeMap.new
+ mapping.register_type("foo") { |*args| args.join("-") }
+
+ assert_equal "foo-1-2-3", mapping.fetch("foo", 1, 2, 3) { |*args| args.join("-") }
+ assert_equal "foo-2-3-4", mapping.fetch("foo", 2, 3, 4) { |*args| args.join("-") }
+ end
+
+ def test_register_clears_cache
+ mapping = TypeMap.new
+
+ mapping.register_type(1, "string")
+ mapping.lookup(1)
+ mapping.register_type(1, "int")
+
+ assert_equal "int", mapping.lookup(1)
+ end
end
end
end
diff --git a/activerecord/test/cases/type_test.rb b/activerecord/test/cases/type_test.rb
new file mode 100644
index 0000000000..d45a9b3141
--- /dev/null
+++ b/activerecord/test/cases/type_test.rb
@@ -0,0 +1,39 @@
+require "cases/helper"
+
+class TypeTest < ActiveRecord::TestCase
+ setup do
+ @old_registry = ActiveRecord::Type.registry
+ ActiveRecord::Type.registry = ActiveRecord::Type::AdapterSpecificRegistry.new
+ end
+
+ teardown do
+ ActiveRecord::Type.registry = @old_registry
+ end
+
+ test "registering a new type" do
+ type = Struct.new(:args)
+ ActiveRecord::Type.register(:foo, type)
+
+ assert_equal type.new(:arg), ActiveRecord::Type.lookup(:foo, :arg)
+ end
+
+ test "looking up a type for a specific adapter" do
+ type = Struct.new(:args)
+ pgtype = Struct.new(:args)
+ ActiveRecord::Type.register(:foo, type, override: false)
+ ActiveRecord::Type.register(:foo, pgtype, adapter: :postgresql)
+
+ assert_equal type.new, ActiveRecord::Type.lookup(:foo, adapter: :sqlite)
+ assert_equal pgtype.new, ActiveRecord::Type.lookup(:foo, adapter: :postgresql)
+ end
+
+ test "lookup defaults to the current adapter" do
+ current_adapter = ActiveRecord::Base.connection.adapter_name.downcase.to_sym
+ type = Struct.new(:args)
+ adapter_type = Struct.new(:args)
+ ActiveRecord::Type.register(:foo, type, override: false)
+ ActiveRecord::Type.register(:foo, adapter_type, adapter: current_adapter)
+
+ assert_equal adapter_type.new, ActiveRecord::Type.lookup(:foo)
+ end
+end
diff --git a/activerecord/test/cases/types_test.rb b/activerecord/test/cases/types_test.rb
index 5c54812f30..81fcf04a27 100644
--- a/activerecord/test/cases/types_test.rb
+++ b/activerecord/test/cases/types_test.rb
@@ -1,162 +1,23 @@
require "cases/helper"
-require 'models/company'
module ActiveRecord
module ConnectionAdapters
class TypesTest < ActiveRecord::TestCase
- def test_type_cast_boolean
- type = Type::Boolean.new
- assert type.type_cast_from_user('').nil?
- assert type.type_cast_from_user(nil).nil?
-
- assert type.type_cast_from_user(true)
- assert type.type_cast_from_user(1)
- assert type.type_cast_from_user('1')
- assert type.type_cast_from_user('t')
- assert type.type_cast_from_user('T')
- assert type.type_cast_from_user('true')
- assert type.type_cast_from_user('TRUE')
- assert type.type_cast_from_user('on')
- assert type.type_cast_from_user('ON')
-
- # explicitly check for false vs nil
- assert_equal false, type.type_cast_from_user(false)
- assert_equal false, type.type_cast_from_user(0)
- assert_equal false, type.type_cast_from_user('0')
- assert_equal false, type.type_cast_from_user('f')
- assert_equal false, type.type_cast_from_user('F')
- assert_equal false, type.type_cast_from_user('false')
- assert_equal false, type.type_cast_from_user('FALSE')
- assert_equal false, type.type_cast_from_user('off')
- assert_equal false, type.type_cast_from_user('OFF')
- assert_equal false, type.type_cast_from_user(' ')
- assert_equal false, type.type_cast_from_user("\u3000\r\n")
- assert_equal false, type.type_cast_from_user("\u0000")
- assert_equal false, type.type_cast_from_user('SOMETHING RANDOM')
- end
-
- def test_type_cast_integer
- type = Type::Integer.new
- assert_equal 1, type.type_cast_from_user(1)
- assert_equal 1, type.type_cast_from_user('1')
- assert_equal 1, type.type_cast_from_user('1ignore')
- assert_equal 0, type.type_cast_from_user('bad1')
- assert_equal 0, type.type_cast_from_user('bad')
- assert_equal 1, type.type_cast_from_user(1.7)
- assert_equal 0, type.type_cast_from_user(false)
- assert_equal 1, type.type_cast_from_user(true)
- assert_nil type.type_cast_from_user(nil)
- end
-
- def test_type_cast_non_integer_to_integer
- type = Type::Integer.new
- assert_nil type.type_cast_from_user([1,2])
- assert_nil type.type_cast_from_user({1 => 2})
- assert_nil type.type_cast_from_user((1..2))
- end
-
- def test_type_cast_activerecord_to_integer
- type = Type::Integer.new
- firm = Firm.create(:name => 'Apple')
- assert_nil type.type_cast_from_user(firm)
- end
-
- def test_type_cast_object_without_to_i_to_integer
- type = Type::Integer.new
- assert_nil type.type_cast_from_user(Object.new)
- end
-
- def test_type_cast_nan_and_infinity_to_integer
- type = Type::Integer.new
- assert_nil type.type_cast_from_user(Float::NAN)
- assert_nil type.type_cast_from_user(1.0/0.0)
- end
-
- def test_changing_integers
- type = Type::Integer.new
-
- assert type.changed?(5, 5, '5wibble')
- assert_not type.changed?(5, 5, '5')
- assert_not type.changed?(5, 5, '5.0')
- assert_not type.changed?(nil, nil, nil)
- end
-
- def test_type_cast_float
- type = Type::Float.new
- assert_equal 1.0, type.type_cast_from_user("1")
- end
-
- def test_changing_float
- type = Type::Float.new
-
- assert type.changed?(5.0, 5.0, '5wibble')
- assert_not type.changed?(5.0, 5.0, '5')
- assert_not type.changed?(5.0, 5.0, '5.0')
- assert_not type.changed?(nil, nil, nil)
- end
-
- def test_type_cast_binary
- type = Type::Binary.new
- assert_equal nil, type.type_cast_from_user(nil)
- assert_equal "1", type.type_cast_from_user("1")
- assert_equal 1, type.type_cast_from_user(1)
- end
-
- def test_type_cast_time
- type = Type::Time.new
- assert_equal nil, type.type_cast_from_user(nil)
- assert_equal nil, type.type_cast_from_user('')
- assert_equal nil, type.type_cast_from_user('ABC')
-
- time_string = Time.now.utc.strftime("%T")
- assert_equal time_string, type.type_cast_from_user(time_string).strftime("%T")
- end
-
- def test_type_cast_datetime_and_timestamp
- type = Type::DateTime.new
- assert_equal nil, type.type_cast_from_user(nil)
- assert_equal nil, type.type_cast_from_user('')
- assert_equal nil, type.type_cast_from_user(' ')
- assert_equal nil, type.type_cast_from_user('ABC')
-
- datetime_string = Time.now.utc.strftime("%FT%T")
- assert_equal datetime_string, type.type_cast_from_user(datetime_string).strftime("%FT%T")
- end
-
- def test_type_cast_date
- type = Type::Date.new
- assert_equal nil, type.type_cast_from_user(nil)
- assert_equal nil, type.type_cast_from_user('')
- assert_equal nil, type.type_cast_from_user(' ')
- assert_equal nil, type.type_cast_from_user('ABC')
-
- date_string = Time.now.utc.strftime("%F")
- assert_equal date_string, type.type_cast_from_user(date_string).strftime("%F")
- end
-
- def test_type_cast_duration_to_integer
- type = Type::Integer.new
- assert_equal 1800, type.type_cast_from_user(30.minutes)
- assert_equal 7200, type.type_cast_from_user(2.hours)
- end
-
- def test_string_to_time_with_timezone
- [:utc, :local].each do |zone|
- with_timezone_config default: zone do
- type = Type::DateTime.new
- assert_equal Time.utc(2013, 9, 4, 0, 0, 0), type.type_cast_from_user("Wed, 04 Sep 2013 03:00:00 EAT")
- end
+ def test_attributes_which_are_invalid_for_database_can_still_be_reassigned
+ type_which_cannot_go_to_the_database = Type::Value.new
+ def type_which_cannot_go_to_the_database.serialize(*)
+ raise
end
- end
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = 'posts'
+ attribute :foo, type_which_cannot_go_to_the_database
+ end
+ model = klass.new
- if current_adapter?(:SQLite3Adapter)
- def test_binary_encoding
- type = SQLite3Binary.new
- utf8_string = "a string".encode(Encoding::UTF_8)
- type_cast = type.type_cast_from_user(utf8_string)
+ model.foo = "foo"
+ model.foo = "bar"
- assert_equal Encoding::ASCII_8BIT, type_cast.encoding
- end
+ assert_equal "bar", model.foo
end
end
end
diff --git a/activerecord/test/cases/unconnected_test.rb b/activerecord/test/cases/unconnected_test.rb
index afb893a52c..b210584644 100644
--- a/activerecord/test/cases/unconnected_test.rb
+++ b/activerecord/test/cases/unconnected_test.rb
@@ -4,7 +4,7 @@ class TestRecord < ActiveRecord::Base
end
class TestUnconnectedAdapter < ActiveRecord::TestCase
- self.use_transactional_fixtures = false
+ self.use_transactional_tests = false
def setup
@underlying = ActiveRecord::Base.connection
diff --git a/activerecord/test/cases/validations/absence_validation_test.rb b/activerecord/test/cases/validations/absence_validation_test.rb
new file mode 100644
index 0000000000..dd43ee358c
--- /dev/null
+++ b/activerecord/test/cases/validations/absence_validation_test.rb
@@ -0,0 +1,75 @@
+require "cases/helper"
+require 'models/face'
+require 'models/interest'
+require 'models/man'
+require 'models/topic'
+
+class AbsenceValidationTest < ActiveRecord::TestCase
+ def test_non_association
+ boy_klass = Class.new(Man) do
+ def self.name; "Boy" end
+ validates_absence_of :name
+ end
+
+ assert boy_klass.new.valid?
+ assert_not boy_klass.new(name: "Alex").valid?
+ end
+
+ def test_has_one_marked_for_destruction
+ boy_klass = Class.new(Man) do
+ def self.name; "Boy" end
+ validates_absence_of :face
+ end
+
+ boy = boy_klass.new(face: Face.new)
+ assert_not boy.valid?, "should not be valid if has_one association is present"
+ assert_equal 1, boy.errors[:face].size, "should only add one error"
+
+ boy.face.mark_for_destruction
+ assert boy.valid?, "should be valid if association is marked for destruction"
+ end
+
+ def test_has_many_marked_for_destruction
+ boy_klass = Class.new(Man) do
+ def self.name; "Boy" end
+ validates_absence_of :interests
+ end
+ boy = boy_klass.new
+ boy.interests << [i1 = Interest.new, i2 = Interest.new]
+ assert_not boy.valid?, "should not be valid if has_many association is present"
+
+ i1.mark_for_destruction
+ assert_not boy.valid?, "should not be valid if has_many association is present"
+
+ i2.mark_for_destruction
+ assert boy.valid?
+ end
+
+ def test_does_not_call_to_a_on_associations
+ boy_klass = Class.new(Man) do
+ def self.name; "Boy" end
+ validates_absence_of :face
+ end
+
+ face_with_to_a = Face.new
+ def face_with_to_a.to_a; ['(/)', '(\)']; end
+
+ assert_nothing_raised { boy_klass.new(face: face_with_to_a).valid? }
+ end
+
+ def test_does_not_validate_if_parent_record_is_validate_false
+ repair_validations(Interest) do
+ Interest.validates_absence_of(:topic)
+ interest = Interest.new(topic: Topic.new(title: "Math"))
+ interest.save!(validate: false)
+ assert interest.persisted?
+
+ man = Man.new(interest_ids: [interest.id])
+ man.save!
+
+ assert_equal man.interests.size, 1
+ assert interest.valid?
+ assert man.valid?
+ end
+ end
+end
diff --git a/activerecord/test/cases/validations/association_validation_test.rb b/activerecord/test/cases/validations/association_validation_test.rb
index e4edc437e6..bff5ffa65e 100644
--- a/activerecord/test/cases/validations/association_validation_test.rb
+++ b/activerecord/test/cases/validations/association_validation_test.rb
@@ -50,7 +50,7 @@ class AssociationValidationTest < ActiveRecord::TestCase
Topic.validates_presence_of :content
r = Reply.create("title" => "A reply", "content" => "with content!")
r.topic = Topic.create("title" => "uhohuhoh")
- assert !r.valid?
+ assert_not_operator r, :valid?
assert_equal ["This string contains 'single' and \"double\" quotes"], r.errors[:topic]
end
@@ -82,5 +82,4 @@ class AssociationValidationTest < ActiveRecord::TestCase
assert interest.valid?, "Expected interest to be valid, but was not. Interest should have a man object associated"
end
end
-
end
diff --git a/activerecord/test/cases/validations/i18n_validation_test.rb b/activerecord/test/cases/validations/i18n_validation_test.rb
index 3db742c15b..981239c4d6 100644
--- a/activerecord/test/cases/validations/i18n_validation_test.rb
+++ b/activerecord/test/cases/validations/i18n_validation_test.rb
@@ -6,6 +6,7 @@ class I18nValidationTest < ActiveRecord::TestCase
repair_validations(Topic, Reply)
def setup
+ repair_validations(Topic, Reply)
Reply.validates_presence_of(:title)
@topic = Topic.new
@old_load_path, @old_backend = I18n.load_path.dup, I18n.backend
@@ -52,8 +53,9 @@ class I18nValidationTest < ActiveRecord::TestCase
test "validates_uniqueness_of on generated message #{name}" do
Topic.validates_uniqueness_of :title, validation_options
@topic.title = unique_topic.title
- @topic.errors.expects(:generate_message).with(:title, :taken, generate_message_options.merge(:value => 'unique!'))
- @topic.valid?
+ assert_called_with(@topic.errors, :generate_message, [:title, :taken, generate_message_options.merge(:value => 'unique!')]) do
+ @topic.valid?
+ end
end
end
@@ -62,8 +64,9 @@ class I18nValidationTest < ActiveRecord::TestCase
COMMON_CASES.each do |name, validation_options, generate_message_options|
test "validates_associated on generated message #{name}" do
Topic.validates_associated :replies, validation_options
- replied_topic.errors.expects(:generate_message).with(:replies, :invalid, generate_message_options.merge(:value => replied_topic.replies))
- replied_topic.save
+ assert_called_with(replied_topic.errors, :generate_message, [:replies, :invalid, generate_message_options.merge(:value => replied_topic.replies)]) do
+ replied_topic.save
+ end
end
end
diff --git a/activerecord/test/cases/validations/length_validation_test.rb b/activerecord/test/cases/validations/length_validation_test.rb
index 4a92da38ce..c5d8f8895c 100644
--- a/activerecord/test/cases/validations/length_validation_test.rb
+++ b/activerecord/test/cases/validations/length_validation_test.rb
@@ -1,47 +1,77 @@
-# -*- coding: utf-8 -*-
require "cases/helper"
require 'models/owner'
require 'models/pet'
+require 'models/person'
class LengthValidationTest < ActiveRecord::TestCase
fixtures :owners
- repair_validations(Owner)
- def test_validates_size_of_association
- repair_validations Owner do
- assert_nothing_raised { Owner.validates_size_of :pets, :minimum => 1 }
- o = Owner.new('name' => 'nopets')
- assert !o.save
- assert o.errors[:pets].any?
- o.pets.build('name' => 'apet')
- assert o.valid?
+ setup do
+ @owner = Class.new(Owner) do
+ def self.name; 'Owner'; end
end
end
+
+ 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?
+ o.pets.build('name' => 'apet')
+ assert o.valid?
+ end
+
def test_validates_size_of_association_using_within
- repair_validations Owner do
- assert_nothing_raised { Owner.validates_size_of :pets, :within => 1..2 }
- o = Owner.new('name' => 'nopets')
- assert !o.save
- assert o.errors[:pets].any?
-
- o.pets.build('name' => 'apet')
- assert o.valid?
-
- 2.times { o.pets.build('name' => 'apet') }
- assert !o.save
- assert o.errors[:pets].any?
- end
+ assert_nothing_raised { @owner.validates_size_of :pets, within: 1..2 }
+ o = @owner.new('name' => 'nopets')
+ assert !o.save
+ assert o.errors[:pets].any?
+
+ o.pets.build('name' => 'apet')
+ assert o.valid?
+
+ 2.times { o.pets.build('name' => 'apet') }
+ assert !o.save
+ assert o.errors[:pets].any?
end
def test_validates_size_of_association_utf8
- repair_validations Owner do
- assert_nothing_raised { Owner.validates_size_of :pets, :minimum => 1 }
- o = Owner.new('name' => 'あいうえおかきくけこ')
- assert !o.save
- assert o.errors[:pets].any?
- o.pets.build('name' => 'あいうえおかきくけこ')
- assert o.valid?
- end
+ @owner.validates_size_of :pets, minimum: 1
+ o = @owner.new('name' => 'あいうえおかきくけこ')
+ assert !o.save
+ assert o.errors[:pets].any?
+ o.pets.build('name' => 'あいうえおかきくけこ')
+ assert 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?
+ pet = owner.pets.build
+ assert 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_equal pet_count, Pet.count
+ end
+
+ def test_does_not_validate_length_of_if_parent_record_is_validate_false
+ @owner.validates_length_of :name, minimum: 1
+ owner = @owner.new
+ owner.save!(validate: false)
+ assert owner.persisted?
+
+ pet = Pet.new(owner_id: owner.id)
+ pet.save!
+
+ assert_equal owner.pets.size, 1
+ assert owner.valid?
+ assert pet.valid?
end
end
diff --git a/activerecord/test/cases/validations/presence_validation_test.rb b/activerecord/test/cases/validations/presence_validation_test.rb
index 4f38849131..6f8ad06ab6 100644
--- a/activerecord/test/cases/validations/presence_validation_test.rb
+++ b/activerecord/test/cases/validations/presence_validation_test.rb
@@ -1,4 +1,3 @@
-# encoding: utf-8
require "cases/helper"
require 'models/man'
require 'models/face'
@@ -65,4 +64,20 @@ class PresenceValidationTest < ActiveRecord::TestCase
assert_nothing_raised { s.valid? }
end
+
+ def test_does_not_validate_presence_of_if_parent_record_is_validate_false
+ repair_validations(Interest) do
+ Interest.validates_presence_of(:topic)
+ interest = Interest.new
+ interest.save!(validate: false)
+ assert interest.persisted?
+
+ man = Man.new(interest_ids: [interest.id])
+ man.save!
+
+ assert_equal man.interests.size, 1
+ assert interest.valid?
+ assert man.valid?
+ end
+ end
end
diff --git a/activerecord/test/cases/validations/uniqueness_validation_test.rb b/activerecord/test/cases/validations/uniqueness_validation_test.rb
index 18221cc73d..7502a55391 100644
--- a/activerecord/test/cases/validations/uniqueness_validation_test.rb
+++ b/activerecord/test/cases/validations/uniqueness_validation_test.rb
@@ -1,10 +1,10 @@
-# encoding: utf-8
require "cases/helper"
require 'models/topic'
require 'models/reply'
require 'models/warehouse_thing'
require 'models/guid'
require 'models/event'
+require 'models/dashboard'
class Wizard < ActiveRecord::Base
self.abstract_class = true
@@ -30,18 +30,28 @@ class ReplyWithTitleObject < Reply
def title; ReplyTitle.new; end
end
-class Employee < ActiveRecord::Base
- self.table_name = 'postgresql_arrays'
- validates_uniqueness_of :nicknames
-end
-
class TopicWithUniqEvent < Topic
belongs_to :event, foreign_key: :parent_id
validates :event, uniqueness: true
end
+class BigIntTest < ActiveRecord::Base
+ INT_MAX_VALUE = 2147483647
+ self.table_name = 'cars'
+ validates :engines_count, uniqueness: true, inclusion: { in: 0..INT_MAX_VALUE }
+end
+
+class BigIntReverseTest < ActiveRecord::Base
+ INT_MAX_VALUE = 2147483647
+ self.table_name = 'cars'
+ validates :engines_count, inclusion: { in: 0..INT_MAX_VALUE }
+ validates :engines_count, uniqueness: true
+end
+
class UniquenessValidationTest < ActiveRecord::TestCase
- fixtures :topics, 'warehouse-things', :developers
+ INT_MAX_VALUE = 2147483647
+
+ fixtures :topics, 'warehouse-things'
repair_validations(Topic, Reply)
@@ -92,6 +102,16 @@ class UniquenessValidationTest < ActiveRecord::TestCase
assert t2.errors[:title]
end
+ def test_validate_uniqueness_when_integer_out_of_range
+ entry = BigIntTest.create(engines_count: INT_MAX_VALUE + 1)
+ assert_equal entry.errors[:engines_count], ['is not included in the list']
+ end
+
+ def test_validate_uniqueness_when_integer_out_of_range_show_order_does_not_matter
+ entry = BigIntReverseTest.create(engines_count: INT_MAX_VALUE + 1)
+ assert_equal entry.errors[:engines_count], ['is not included in the list']
+ end
+
def test_validates_uniqueness_with_newline_chars
Topic.validates_uniqueness_of(:title, :case_sensitive => false)
@@ -378,18 +398,6 @@ class UniquenessValidationTest < ActiveRecord::TestCase
}
end
- if current_adapter? :PostgreSQLAdapter
- def test_validate_uniqueness_with_array_column
- e1 = Employee.create("nicknames" => ["john", "johnny"], "commission_by_quarter" => [1000, 1200])
- assert e1.persisted?, "Saving e1"
-
- e2 = Employee.create("nicknames" => ["john", "johnny"], "commission_by_quarter" => [2200])
- assert !e2.persisted?, "e2 shouldn't be valid"
- assert e2.errors[:nicknames].any?, "Should have errors for nicknames"
- assert_equal ["has already been taken"], e2.errors[:nicknames], "Should have uniqueness message for nicknames"
- end
- end
-
def test_validate_uniqueness_on_existing_relation
event = Event.create
assert TopicWithUniqEvent.create(event: event).valid?
@@ -403,4 +411,62 @@ class UniquenessValidationTest < ActiveRecord::TestCase
topic = TopicWithUniqEvent.new
assert topic.valid?
end
+
+ def test_does_not_validate_uniqueness_of_if_parent_record_is_validate_false
+ Reply.validates_uniqueness_of(:content)
+
+ Reply.create!(content: "Topic Title")
+
+ reply = Reply.new(content: "Topic Title")
+ reply.save!(validate: false)
+ assert reply.persisted?
+
+ topic = Topic.new(reply_ids: [reply.id])
+ topic.save!
+
+ assert_equal topic.replies.size, 1
+ assert reply.valid?
+ assert topic.valid?
+ end
+
+ def test_validate_uniqueness_of_custom_primary_key
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = "keyboards"
+ self.primary_key = :key_number
+
+ validates_uniqueness_of :key_number
+
+ def self.name
+ "Keyboard"
+ end
+ end
+
+ klass.create!(key_number: 10)
+ key2 = klass.create!(key_number: 11)
+
+ key2.key_number = 10
+ assert_not key2.valid?
+ end
+
+ def test_validate_uniqueness_without_primary_key
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = "dashboards"
+
+ validates_uniqueness_of :dashboard_id
+
+ def self.name; "Dashboard" end
+ end
+
+ abc = klass.create!(dashboard_id: "abc")
+ assert klass.new(dashboard_id: "xyz").valid?
+ assert_not klass.new(dashboard_id: "abc").valid?
+
+ abc.dashboard_id = "def"
+
+ e = assert_raises ActiveRecord::UnknownPrimaryKey do
+ abc.save!
+ end
+ assert_match(/\AUnknown primary key for table dashboards in model/, e.message)
+ assert_match(/Can not validate uniqueness for persisted record without primary key.\z/, e.message)
+ end
end
diff --git a/activerecord/test/cases/validations_repair_helper.rb b/activerecord/test/cases/validations_repair_helper.rb
index c02b3241cd..b30666d876 100644
--- a/activerecord/test/cases/validations_repair_helper.rb
+++ b/activerecord/test/cases/validations_repair_helper.rb
@@ -5,19 +5,15 @@ module ActiveRecord
module ClassMethods
def repair_validations(*model_classes)
teardown do
- model_classes.each do |k|
- k.clear_validators!
- end
+ model_classes.each(&:clear_validators!)
end
end
end
def repair_validations(*model_classes)
- yield
+ yield if block_given?
ensure
- model_classes.each do |k|
- k.clear_validators!
- end
+ model_classes.each(&:clear_validators!)
end
end
end
diff --git a/activerecord/test/cases/validations_test.rb b/activerecord/test/cases/validations_test.rb
index 55804f9576..d04f4f7ce7 100644
--- a/activerecord/test/cases/validations_test.rb
+++ b/activerecord/test/cases/validations_test.rb
@@ -1,9 +1,9 @@
-# encoding: utf-8
require "cases/helper"
require 'models/topic'
require 'models/reply'
require 'models/person'
require 'models/developer'
+require 'models/computer'
require 'models/parrot'
require 'models/company'
@@ -52,6 +52,13 @@ class ValidationsTest < ActiveRecord::TestCase
assert r.valid?(:special_case)
end
+ def test_invalid_using_multiple_contexts
+ r = WrongReply.new(:title => 'Wrong Create')
+ assert r.invalid?([:special_case, :create])
+ assert_equal "Invalid", r.errors[:author_name].join
+ assert_equal "is Wrong Create", r.errors[:title].join
+ end
+
def test_validate
r = WrongReply.new
@@ -148,4 +155,28 @@ class ValidationsTest < ActiveRecord::TestCase
assert_equal 1, Company.validators_on(:name).size
end
+ def test_numericality_validation_with_mutation
+ Topic.class_eval do
+ attribute :wibble, :string
+ validates_numericality_of :wibble, only_integer: true
+ end
+
+ topic = Topic.new(wibble: '123-4567')
+ topic.wibble.gsub!('-', '')
+
+ assert topic.valid?
+ ensure
+ Topic.reset_column_information
+ end
+
+ def test_acceptance_validator_doesnt_require_db_connection
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = 'posts'
+ end
+ klass.reset_column_information
+
+ assert_no_queries do
+ klass.validates_acceptance_of(:foo)
+ end
+ end
end
diff --git a/activerecord/test/cases/view_test.rb b/activerecord/test/cases/view_test.rb
new file mode 100644
index 0000000000..e80d8bd584
--- /dev/null
+++ b/activerecord/test/cases/view_test.rb
@@ -0,0 +1,216 @@
+require "cases/helper"
+require "models/book"
+require "support/schema_dumping_helper"
+
+module ViewBehavior
+ include SchemaDumpingHelper
+ extend ActiveSupport::Concern
+
+ included do
+ fixtures :books
+ end
+
+ class Ebook < ActiveRecord::Base
+ self.primary_key = "id"
+ end
+
+ def setup
+ super
+ @connection = ActiveRecord::Base.connection
+ create_view "ebooks", <<-SQL
+ SELECT id, name, status FROM books WHERE format = 'ebook'
+ SQL
+ end
+
+ def teardown
+ super
+ drop_view "ebooks"
+ end
+
+ def test_reading
+ books = Ebook.all
+ assert_equal [books(:rfr).id], books.map(&:id)
+ assert_equal ["Ruby for Rails"], books.map(&:name)
+ end
+
+ def test_views
+ assert_equal [Ebook.table_name], @connection.views
+ end
+
+ def test_view_exists
+ view_name = Ebook.table_name
+ assert @connection.view_exists?(view_name), "'#{view_name}' view should exist"
+ end
+
+ def test_table_exists
+ view_name = Ebook.table_name
+ # TODO: switch this assertion around once we changed #tables to not return views.
+ assert @connection.table_exists?(view_name), "'#{view_name}' table should exist"
+ end
+
+ def test_views_ara_valid_data_sources
+ view_name = Ebook.table_name
+ assert @connection.data_source_exists?(view_name), "'#{view_name}' should be a data source"
+ end
+
+ def test_column_definitions
+ assert_equal([["id", :integer],
+ ["name", :string],
+ ["status", :integer]], Ebook.columns.map { |c| [c.name, c.type] })
+ end
+
+ def test_attributes
+ assert_equal({"id" => 2, "name" => "Ruby for Rails", "status" => 0},
+ Ebook.first.attributes)
+ end
+
+ def test_does_not_assume_id_column_as_primary_key
+ model = Class.new(ActiveRecord::Base) do
+ self.table_name = "ebooks"
+ end
+ assert_nil model.primary_key
+ end
+
+ def test_does_not_dump_view_as_table
+ schema = dump_table_schema "ebooks"
+ assert_no_match %r{create_table "ebooks"}, schema
+ end
+end
+
+if ActiveRecord::Base.connection.supports_views?
+class ViewWithPrimaryKeyTest < ActiveRecord::TestCase
+ include ViewBehavior
+
+ private
+ def create_view(name, query)
+ @connection.execute "CREATE VIEW #{name} AS #{query}"
+ end
+
+ def drop_view(name)
+ @connection.execute "DROP VIEW #{name}" if @connection.table_exists? name
+ end
+end
+
+class ViewWithoutPrimaryKeyTest < ActiveRecord::TestCase
+ include SchemaDumpingHelper
+ fixtures :books
+
+ class Paperback < ActiveRecord::Base; end
+
+ setup do
+ @connection = ActiveRecord::Base.connection
+ @connection.execute <<-SQL
+ CREATE VIEW paperbacks
+ AS SELECT name, status FROM books WHERE format = 'paperback'
+ SQL
+ end
+
+ teardown do
+ @connection.execute "DROP VIEW paperbacks" if @connection.table_exists? "paperbacks"
+ end
+
+ def test_reading
+ books = Paperback.all
+ assert_equal ["Agile Web Development with Rails"], books.map(&:name)
+ end
+
+ def test_views
+ assert_equal [Paperback.table_name], @connection.views
+ end
+
+ def test_view_exists
+ view_name = Paperback.table_name
+ assert @connection.view_exists?(view_name), "'#{view_name}' view should exist"
+ end
+
+ def test_table_exists
+ view_name = Paperback.table_name
+ assert @connection.table_exists?(view_name), "'#{view_name}' table should exist"
+ end
+
+ def test_column_definitions
+ assert_equal([["name", :string],
+ ["status", :integer]], Paperback.columns.map { |c| [c.name, c.type] })
+ end
+
+ def test_attributes
+ assert_equal({"name" => "Agile Web Development with Rails", "status" => 2},
+ Paperback.first.attributes)
+ end
+
+ def test_does_not_have_a_primary_key
+ assert_nil Paperback.primary_key
+ end
+
+ def test_does_not_dump_view_as_table
+ schema = dump_table_schema "paperbacks"
+ assert_no_match %r{create_table "paperbacks"}, schema
+ end
+end
+
+# sqlite dose not support CREATE, INSERT, and DELETE for VIEW
+if current_adapter?(:MysqlAdapter, :Mysql2Adapter, :PostgreSQLAdapter)
+class UpdateableViewTest < ActiveRecord::TestCase
+ self.use_transactional_tests = false
+ fixtures :books
+
+ class PrintedBook < ActiveRecord::Base
+ self.primary_key = "id"
+ end
+
+ setup do
+ @connection = ActiveRecord::Base.connection
+ @connection.execute <<-SQL
+ CREATE VIEW printed_books
+ AS SELECT id, name, status, format FROM books WHERE format = 'paperback'
+ SQL
+ end
+
+ teardown do
+ @connection.execute "DROP VIEW printed_books" if @connection.table_exists? "printed_books"
+ end
+
+ def test_update_record
+ book = PrintedBook.first
+ book.name = "AWDwR"
+ book.save!
+ book.reload
+ assert_equal "AWDwR", book.name
+ end
+
+ def test_insert_record
+ PrintedBook.create! name: "Rails in Action", status: 0, format: "paperback"
+
+ new_book = PrintedBook.last
+ assert_equal "Rails in Action", new_book.name
+ end
+
+ def test_update_record_to_fail_view_conditions
+ book = PrintedBook.first
+ book.format = "ebook"
+ book.save!
+
+ assert_raises ActiveRecord::RecordNotFound do
+ book.reload
+ end
+ end
+end
+end # end fo `if current_adapter?(:MysqlAdapter, :Mysql2Adapter, :PostgreSQLAdapter)`
+end # end fo `if ActiveRecord::Base.connection.supports_views?`
+
+if ActiveRecord::Base.connection.respond_to?(:supports_materialized_views?) &&
+ ActiveRecord::Base.connection.supports_materialized_views?
+class MaterializedViewTest < ActiveRecord::PostgreSQLTestCase
+ include ViewBehavior
+
+ private
+ def create_view(name, query)
+ @connection.execute "CREATE MATERIALIZED VIEW #{name} AS #{query}"
+ end
+
+ def drop_view(name)
+ @connection.execute "DROP MATERIALIZED VIEW #{name}" if @connection.table_exists? name
+
+ end
+end
+end
diff --git a/activerecord/test/cases/xml_serialization_test.rb b/activerecord/test/cases/xml_serialization_test.rb
deleted file mode 100644
index c34e7d5a30..0000000000
--- a/activerecord/test/cases/xml_serialization_test.rb
+++ /dev/null
@@ -1,447 +0,0 @@
-require "cases/helper"
-require "rexml/document"
-require 'models/contact'
-require 'models/post'
-require 'models/author'
-require 'models/comment'
-require 'models/company_in_module'
-require 'models/toy'
-require 'models/topic'
-require 'models/reply'
-require 'models/company'
-
-class XmlSerializationTest < ActiveRecord::TestCase
- def test_should_serialize_default_root
- @xml = Contact.new.to_xml
- assert_match %r{^<contact>}, @xml
- assert_match %r{</contact>$}, @xml
- end
-
- def test_should_serialize_default_root_with_namespace
- @xml = Contact.new.to_xml :namespace=>"http://xml.rubyonrails.org/contact"
- assert_match %r{^<contact xmlns="http://xml.rubyonrails.org/contact">}, @xml
- assert_match %r{</contact>$}, @xml
- end
-
- def test_should_serialize_custom_root
- @xml = Contact.new.to_xml :root => 'xml_contact'
- assert_match %r{^<xml-contact>}, @xml
- assert_match %r{</xml-contact>$}, @xml
- end
-
- def test_should_allow_undasherized_tags
- @xml = Contact.new.to_xml :root => 'xml_contact', :dasherize => false
- assert_match %r{^<xml_contact>}, @xml
- assert_match %r{</xml_contact>$}, @xml
- assert_match %r{<created_at}, @xml
- end
-
- def test_should_allow_camelized_tags
- @xml = Contact.new.to_xml :root => 'xml_contact', :camelize => true
- assert_match %r{^<XmlContact>}, @xml
- assert_match %r{</XmlContact>$}, @xml
- assert_match %r{<CreatedAt}, @xml
- end
-
- def test_should_allow_skipped_types
- @xml = Contact.new(:age => 25).to_xml :skip_types => true
- assert %r{<age>25</age>}.match(@xml)
- end
-
- def test_should_include_yielded_additions
- @xml = Contact.new.to_xml do |xml|
- xml.creator "David"
- end
- assert_match %r{<creator>David</creator>}, @xml
- end
-
- def test_to_xml_with_block
- value = "Rockin' the block"
- xml = Contact.new.to_xml(:skip_instruct => true) do |_xml|
- _xml.tag! "arbitrary-element", value
- end
- assert_equal "<contact>", xml.first(9)
- assert xml.include?(%(<arbitrary-element>#{value}</arbitrary-element>))
- end
-
- def test_should_skip_instruct_for_included_records
- @contact = Contact.new
- @contact.alternative = Contact.new(:name => 'Copa Cabana')
- @xml = @contact.to_xml(:include => [ :alternative ])
- assert_equal @xml.index('<?xml '), 0
- assert_nil @xml.index('<?xml ', 1)
- end
-end
-
-class DefaultXmlSerializationTest < ActiveRecord::TestCase
- def setup
- @contact = Contact.new(
- :name => 'aaron stack',
- :age => 25,
- :avatar => 'binarydata',
- :created_at => Time.utc(2006, 8, 1),
- :awesome => false,
- :preferences => { :gem => 'ruby' }
- )
- end
-
- def test_should_serialize_string
- assert_match %r{<name>aaron stack</name>}, @contact.to_xml
- end
-
- def test_should_serialize_integer
- assert_match %r{<age type="integer">25</age>}, @contact.to_xml
- end
-
- def test_should_serialize_binary
- xml = @contact.to_xml
- assert_match %r{YmluYXJ5ZGF0YQ==\n</avatar>}, xml
- assert_match %r{<avatar(.*)(type="binary")}, xml
- assert_match %r{<avatar(.*)(encoding="base64")}, xml
- end
-
- def test_should_serialize_datetime
- assert_match %r{<created-at type=\"dateTime\">2006-08-01T00:00:00Z</created-at>}, @contact.to_xml
- end
-
- def test_should_serialize_boolean
- assert_match %r{<awesome type=\"boolean\">false</awesome>}, @contact.to_xml
- end
-
- def test_should_serialize_hash
- assert_match %r{<preferences>\s*<gem>ruby</gem>\s*</preferences>}m, @contact.to_xml
- end
-
- def test_uses_serializable_hash_with_only_option
- def @contact.serializable_hash(options=nil)
- super(only: %w(name))
- end
-
- xml = @contact.to_xml
- assert_match %r{<name>aaron stack</name>}, xml
- assert_no_match %r{age}, xml
- assert_no_match %r{awesome}, xml
- end
-
- def test_uses_serializable_hash_with_except_option
- def @contact.serializable_hash(options=nil)
- super(except: %w(age))
- end
-
- xml = @contact.to_xml
- assert_match %r{<name>aaron stack</name>}, xml
- assert_match %r{<awesome type=\"boolean\">false</awesome>}, xml
- assert_no_match %r{age}, xml
- end
-
- def test_does_not_include_inheritance_column_from_sti
- @contact = ContactSti.new(@contact.attributes)
- assert_equal 'ContactSti', @contact.type
-
- xml = @contact.to_xml
- assert_match %r{<name>aaron stack</name>}, xml
- assert_no_match %r{<type}, xml
- assert_no_match %r{ContactSti}, xml
- end
-
- def test_serializable_hash_with_default_except_option_and_excluding_inheritance_column_from_sti
- @contact = ContactSti.new(@contact.attributes)
- assert_equal 'ContactSti', @contact.type
-
- def @contact.serializable_hash(options={})
- super({ except: %w(age) }.merge!(options))
- end
-
- xml = @contact.to_xml
- assert_match %r{<name>aaron stack</name>}, xml
- assert_no_match %r{age}, xml
- assert_no_match %r{<type}, xml
- assert_no_match %r{ContactSti}, xml
- end
-end
-
-class DefaultXmlSerializationTimezoneTest < ActiveRecord::TestCase
- def test_should_serialize_datetime_with_timezone
- with_timezone_config zone: "Pacific Time (US & Canada)" do
- toy = Toy.create(:name => 'Mickey', :updated_at => Time.utc(2006, 8, 1))
- assert_match %r{<updated-at type=\"dateTime\">2006-07-31T17:00:00-07:00</updated-at>}, toy.to_xml
- end
- end
-
- def test_should_serialize_datetime_with_timezone_reloaded
- with_timezone_config zone: "Pacific Time (US & Canada)" do
- toy = Toy.create(:name => 'Minnie', :updated_at => Time.utc(2006, 8, 1)).reload
- assert_match %r{<updated-at type=\"dateTime\">2006-07-31T17:00:00-07:00</updated-at>}, toy.to_xml
- end
- end
-end
-
-class NilXmlSerializationTest < ActiveRecord::TestCase
- def setup
- @xml = Contact.new.to_xml(:root => 'xml_contact')
- end
-
- def test_should_serialize_string
- assert_match %r{<name nil="true"/>}, @xml
- end
-
- def test_should_serialize_integer
- assert %r{<age (.*)/>}.match(@xml)
- attributes = $1
- assert_match %r{nil="true"}, attributes
- assert_match %r{type="integer"}, attributes
- end
-
- def test_should_serialize_binary
- assert %r{<avatar (.*)/>}.match(@xml)
- attributes = $1
- assert_match %r{type="binary"}, attributes
- assert_match %r{encoding="base64"}, attributes
- assert_match %r{nil="true"}, attributes
- end
-
- def test_should_serialize_datetime
- assert %r{<created-at (.*)/>}.match(@xml)
- attributes = $1
- assert_match %r{nil="true"}, attributes
- assert_match %r{type="dateTime"}, attributes
- end
-
- def test_should_serialize_boolean
- assert %r{<awesome (.*)/>}.match(@xml)
- attributes = $1
- assert_match %r{type="boolean"}, attributes
- assert_match %r{nil="true"}, attributes
- end
-
- def test_should_serialize_yaml
- assert_match %r{<preferences nil=\"true\"/>}, @xml
- end
-end
-
-class DatabaseConnectedXmlSerializationTest < ActiveRecord::TestCase
- fixtures :topics, :companies, :accounts, :authors, :posts, :projects
-
- def test_to_xml
- xml = REXML::Document.new(topics(:first).to_xml(:indent => 0))
- bonus_time_in_current_timezone = topics(:first).bonus_time.xmlschema
- written_on_in_current_timezone = topics(:first).written_on.xmlschema
-
- assert_equal "topic", xml.root.name
- assert_equal "The First Topic" , xml.elements["//title"].text
- assert_equal "David" , xml.elements["//author-name"].text
- assert_match "Have a nice day", xml.elements["//content"].text
-
- assert_equal "1", xml.elements["//id"].text
- assert_equal "integer" , xml.elements["//id"].attributes['type']
-
- assert_equal "1", xml.elements["//replies-count"].text
- assert_equal "integer" , xml.elements["//replies-count"].attributes['type']
-
- assert_equal written_on_in_current_timezone, xml.elements["//written-on"].text
- assert_equal "dateTime" , xml.elements["//written-on"].attributes['type']
-
- assert_equal "david@loudthinking.com", xml.elements["//author-email-address"].text
-
- assert_equal nil, xml.elements["//parent-id"].text
- assert_equal "integer", xml.elements["//parent-id"].attributes['type']
- assert_equal "true", xml.elements["//parent-id"].attributes['nil']
-
- # Oracle enhanced adapter allows to define Date attributes in model class (see topic.rb)
- assert_equal "2004-04-15", xml.elements["//last-read"].text
- assert_equal "date" , xml.elements["//last-read"].attributes['type']
-
- # Oracle and DB2 don't have true boolean or time-only fields
- unless current_adapter?(:OracleAdapter, :DB2Adapter)
- assert_equal "false", xml.elements["//approved"].text
- assert_equal "boolean" , xml.elements["//approved"].attributes['type']
-
- assert_equal bonus_time_in_current_timezone, xml.elements["//bonus-time"].text
- assert_equal "dateTime" , xml.elements["//bonus-time"].attributes['type']
- end
- end
-
- def test_except_option
- xml = topics(:first).to_xml(:indent => 0, :skip_instruct => true, :except => [:title, :replies_count])
- assert_equal "<topic>", xml.first(7)
- assert !xml.include?(%(<title>The First Topic</title>))
- assert xml.include?(%(<author-name>David</author-name>))
-
- xml = topics(:first).to_xml(:indent => 0, :skip_instruct => true, :except => [:title, :author_name, :replies_count])
- assert !xml.include?(%(<title>The First Topic</title>))
- assert !xml.include?(%(<author-name>David</author-name>))
- end
-
- # to_xml used to mess with the hash the user provided which
- # caused the builder to be reused. This meant the document kept
- # getting appended to.
-
- def test_modules
- projects = MyApplication::Business::Project.all
- xml = projects.to_xml
- root = projects.first.class.to_s.underscore.pluralize.tr('/','_').dasherize
- assert_match "<#{root} type=\"array\">", xml
- assert_match "</#{root}>", xml
- end
-
- def test_passing_hash_shouldnt_reuse_builder
- options = {:include=>:posts}
- david = authors(:david)
- first_xml_size = david.to_xml(options).size
- second_xml_size = david.to_xml(options).size
- assert_equal first_xml_size, second_xml_size
- end
-
- def test_include_uses_association_name
- xml = authors(:david).to_xml :include=>:hello_posts, :indent => 0
- assert_match %r{<hello-posts type="array">}, xml
- assert_match %r{<hello-post type="Post">}, xml
- assert_match %r{<hello-post type="StiPost">}, xml
- end
-
- def test_included_associations_should_skip_types
- xml = authors(:david).to_xml :include=>:hello_posts, :indent => 0, :skip_types => true
- assert_match %r{<hello-posts>}, xml
- assert_match %r{<hello-post>}, xml
- assert_match %r{<hello-post>}, xml
- end
-
- def test_including_has_many_association
- xml = topics(:first).to_xml(:indent => 0, :skip_instruct => true, :include => :replies, :except => :replies_count)
- assert_equal "<topic>", xml.first(7)
- assert xml.include?(%(<replies type="array"><reply>))
- assert xml.include?(%(<title>The Second Topic of the day</title>))
- end
-
- def test_including_belongs_to_association
- xml = companies(:first_client).to_xml(:indent => 0, :skip_instruct => true, :include => :firm)
- assert !xml.include?("<firm>")
-
- xml = companies(:second_client).to_xml(:indent => 0, :skip_instruct => true, :include => :firm)
- assert xml.include?("<firm>")
- end
-
- def test_including_multiple_associations
- xml = companies(:first_firm).to_xml(:indent => 0, :skip_instruct => true, :include => [ :clients, :account ])
- assert_equal "<firm>", xml.first(6)
- assert xml.include?(%(<account>))
- assert xml.include?(%(<clients type="array"><client>))
- end
-
- def test_including_association_with_options
- xml = companies(:first_firm).to_xml(
- :indent => 0, :skip_instruct => true,
- :include => { :clients => { :only => :name } }
- )
-
- assert_equal "<firm>", xml.first(6)
- assert xml.include?(%(<client><name>Summit</name></client>))
- assert xml.include?(%(<clients type="array"><client>))
- end
-
- def test_methods_are_called_on_object
- xml = authors(:david).to_xml :methods => :label, :indent => 0
- assert_match %r{<label>.*</label>}, xml
- end
-
- def test_should_not_call_methods_on_associations_that_dont_respond
- xml = authors(:david).to_xml :include=>:hello_posts, :methods => :label, :indent => 2
- assert !authors(:david).hello_posts.first.respond_to?(:label)
- assert_match %r{^ <label>.*</label>}, xml
- assert_no_match %r{^ <label>}, xml
- end
-
- def test_procs_are_called_on_object
- proc = Proc.new { |options| options[:builder].tag!('nationality', 'Danish') }
- xml = authors(:david).to_xml(:procs => [ proc ])
- assert_match %r{<nationality>Danish</nationality>}, xml
- end
-
- def test_dual_arity_procs_are_called_on_object
- proc = Proc.new { |options, record| options[:builder].tag!('name-reverse', record.name.reverse) }
- xml = authors(:david).to_xml(:procs => [ proc ])
- assert_match %r{<name-reverse>divaD</name-reverse>}, xml
- end
-
- def test_top_level_procs_arent_applied_to_associations
- author_proc = Proc.new { |options| options[:builder].tag!('nationality', 'Danish') }
- xml = authors(:david).to_xml(:procs => [ author_proc ], :include => :posts, :indent => 2)
-
- assert_match %r{^ <nationality>Danish</nationality>}, xml
- assert_no_match %r{^ {6}<nationality>Danish</nationality>}, xml
- end
-
- def test_procs_on_included_associations_are_called
- posts_proc = Proc.new { |options| options[:builder].tag!('copyright', 'DHH') }
- xml = authors(:david).to_xml(
- :indent => 2,
- :include => {
- :posts => { :procs => [ posts_proc ] }
- }
- )
-
- assert_no_match %r{^ <copyright>DHH</copyright>}, xml
- assert_match %r{^ {6}<copyright>DHH</copyright>}, xml
- end
-
- def test_should_include_empty_has_many_as_empty_array
- authors(:david).posts.delete_all
- xml = authors(:david).to_xml :include=>:posts, :indent => 2
-
- assert_equal [], Hash.from_xml(xml)['author']['posts']
- assert_match %r{^ <posts type="array"/>}, xml
- end
-
- def test_should_has_many_array_elements_should_include_type_when_different_from_guessed_value
- xml = authors(:david).to_xml :include=>:posts_with_comments, :indent => 2
-
- assert Hash.from_xml(xml)
- assert_match %r{^ <posts-with-comments type="array">}, xml
- assert_match %r{^ <posts-with-comment type="Post">}, xml
- assert_match %r{^ <posts-with-comment type="StiPost">}, xml
-
- types = Hash.from_xml(xml)['author']['posts_with_comments'].collect {|t| t['type'] }
- assert types.include?('SpecialPost')
- assert types.include?('Post')
- assert types.include?('StiPost')
- end
-
- def test_should_produce_xml_for_methods_returning_array
- xml = authors(:david).to_xml(:methods => :social)
- array = Hash.from_xml(xml)['author']['social']
- assert_equal 2, array.size
- assert array.include? 'twitter'
- assert array.include? 'github'
- end
-
- def test_should_support_aliased_attributes
- xml = Author.select("name as firstname").to_xml
- Author.all.each do |author|
- assert xml.include?(%(<firstname>#{author.name}</firstname>)), xml
- end
- end
-
- def test_array_to_xml_including_has_many_association
- xml = [ topics(:first), topics(:second) ].to_xml(:indent => 0, :skip_instruct => true, :include => :replies)
- assert xml.include?(%(<replies type="array"><reply>))
- end
-
- def test_array_to_xml_including_methods
- xml = [ topics(:first), topics(:second) ].to_xml(:indent => 0, :skip_instruct => true, :methods => [ :topic_id ])
- assert xml.include?(%(<topic-id type="integer">#{topics(:first).topic_id}</topic-id>)), xml
- assert xml.include?(%(<topic-id type="integer">#{topics(:second).topic_id}</topic-id>)), xml
- end
-
- def test_array_to_xml_including_has_one_association
- xml = [ companies(:first_firm), companies(:rails_core) ].to_xml(:indent => 0, :skip_instruct => true, :include => :account)
- assert xml.include?(companies(:first_firm).account.to_xml(:indent => 0, :skip_instruct => true))
- assert xml.include?(companies(:rails_core).account.to_xml(:indent => 0, :skip_instruct => true))
- end
-
- def test_array_to_xml_including_belongs_to_association
- xml = [ companies(:first_client), companies(:second_client), companies(:another_client) ].to_xml(:indent => 0, :skip_instruct => true, :include => :firm)
- assert xml.include?(companies(:first_client).to_xml(:indent => 0, :skip_instruct => true))
- assert xml.include?(companies(:second_client).firm.to_xml(:indent => 0, :skip_instruct => true))
- assert xml.include?(companies(:another_client).firm.to_xml(:indent => 0, :skip_instruct => true))
- end
-end
diff --git a/activerecord/test/cases/yaml_serialization_test.rb b/activerecord/test/cases/yaml_serialization_test.rb
index bce59b4fcd..56909a8630 100644
--- a/activerecord/test/cases/yaml_serialization_test.rb
+++ b/activerecord/test/cases/yaml_serialization_test.rb
@@ -83,4 +83,39 @@ class YamlSerializationTest < ActiveRecord::TestCase
assert_equal 5, author.posts_count
assert_equal 5, dumped.posts_count
end
+
+ def test_a_yaml_version_is_provided_for_future_backwards_compat
+ coder = {}
+ Topic.first.encode_with(coder)
+
+ assert coder['active_record_yaml_version']
+ end
+
+ def test_deserializing_rails_41_yaml
+ topic = YAML.load(yaml_fixture("rails_4_1"))
+
+ assert topic.new_record?
+ assert_equal nil, topic.id
+ assert_equal "The First Topic", topic.title
+ assert_equal({ omg: :lol }, topic.content)
+ end
+
+ def test_deserializing_rails_4_2_0_yaml
+ topic = YAML.load(yaml_fixture("rails_4_2_0"))
+
+ assert_not topic.new_record?
+ assert_equal 1, topic.id
+ assert_equal "The First Topic", topic.title
+ assert_equal("Have a nice day", topic.content)
+ end
+
+ private
+
+ def yaml_fixture(file_name)
+ path = File.expand_path(
+ "../../support/yaml_compatibility_fixtures/#{file_name}.yml",
+ __FILE__
+ )
+ File.read(path)
+ end
end
diff --git a/activerecord/test/config.example.yml b/activerecord/test/config.example.yml
index a54914c372..e3b55d640e 100644
--- a/activerecord/test/config.example.yml
+++ b/activerecord/test/config.example.yml
@@ -55,6 +55,7 @@ connections:
arunit:
username: rails
encoding: utf8
+ collation: utf8_unicode_ci
arunit2:
username: rails
encoding: utf8
@@ -63,16 +64,11 @@ connections:
arunit:
username: rails
encoding: utf8
+ collation: utf8_unicode_ci
arunit2:
username: rails
encoding: utf8
- openbase:
- arunit:
- username: admin
- arunit2:
- username: admin
-
oracle:
arunit:
adapter: oracle_enhanced
diff --git a/activerecord/test/fixtures/bad_posts.yml b/activerecord/test/fixtures/bad_posts.yml
new file mode 100644
index 0000000000..addee8e3bf
--- /dev/null
+++ b/activerecord/test/fixtures/bad_posts.yml
@@ -0,0 +1,9 @@
+# Please do not use this fixture without `set_fixture_class` as Post
+
+_fixture:
+ model_class: BadPostModel
+
+bad_welcome:
+ author_id: 1
+ title: Welcome to the another weblog
+ body: It's really nice today
diff --git a/activerecord/test/fixtures/books.yml b/activerecord/test/fixtures/books.yml
index abe56752c6..a304fba399 100644
--- a/activerecord/test/fixtures/books.yml
+++ b/activerecord/test/fixtures/books.yml
@@ -3,9 +3,29 @@ awdr:
id: 1
name: "Agile Web Development with Rails"
format: "paperback"
+ status: :published
+ read_status: :read
+ language: :english
+ author_visibility: :visible
+ illustrator_visibility: :visible
+ font_size: :medium
rfr:
author_id: 1
id: 2
name: "Ruby for Rails"
format: "ebook"
+ status: "proposed"
+ read_status: "reading"
+
+ddd:
+ author_id: 1
+ id: 3
+ name: "Domain-Driven Design"
+ format: "hardcover"
+ status: 2
+
+tlg:
+ author_id: 1
+ id: 4
+ name: "Thoughtleadering"
diff --git a/activerecord/test/fixtures/bulbs.yml b/activerecord/test/fixtures/bulbs.yml
new file mode 100644
index 0000000000..e5ce2b796c
--- /dev/null
+++ b/activerecord/test/fixtures/bulbs.yml
@@ -0,0 +1,5 @@
+defaulty:
+ name: defaulty
+
+special:
+ name: special
diff --git a/activerecord/test/fixtures/computers.yml b/activerecord/test/fixtures/computers.yml
index 7281a4d768..ad5ae2ec71 100644
--- a/activerecord/test/fixtures/computers.yml
+++ b/activerecord/test/fixtures/computers.yml
@@ -3,3 +3,8 @@ workstation:
system: 'Linux'
developer: 1
extendedWarranty: 1
+
+laptop:
+ system: 'MacOS 1'
+ developer: 1
+ extendedWarranty: 1
diff --git a/activerecord/test/fixtures/content.yml b/activerecord/test/fixtures/content.yml
new file mode 100644
index 0000000000..0d12ee03dc
--- /dev/null
+++ b/activerecord/test/fixtures/content.yml
@@ -0,0 +1,3 @@
+content:
+ id: 1
+ title: How to use Rails
diff --git a/activerecord/test/fixtures/content_positions.yml b/activerecord/test/fixtures/content_positions.yml
new file mode 100644
index 0000000000..9e85773f8e
--- /dev/null
+++ b/activerecord/test/fixtures/content_positions.yml
@@ -0,0 +1,3 @@
+content_positions:
+ id: 1
+ content_id: 1
diff --git a/activerecord/test/fixtures/dead_parrots.yml b/activerecord/test/fixtures/dead_parrots.yml
new file mode 100644
index 0000000000..da5529dd27
--- /dev/null
+++ b/activerecord/test/fixtures/dead_parrots.yml
@@ -0,0 +1,5 @@
+deadbird:
+ name: "Dusty DeadBird"
+ treasures: [ruby, sapphire]
+ parrot_sti_class: DeadParrot
+ killer: blackbeard
diff --git a/activerecord/test/fixtures/developers.yml b/activerecord/test/fixtures/developers.yml
index 3656564f63..54b74e7a74 100644
--- a/activerecord/test/fixtures/developers.yml
+++ b/activerecord/test/fixtures/developers.yml
@@ -2,6 +2,7 @@ david:
id: 1
name: David
salary: 80000
+ shared_computers: laptop
jamis:
id: 2
@@ -18,4 +19,4 @@ dev_<%= digit %>:
poor_jamis:
id: 11
name: Jamis
- salary: 9000 \ No newline at end of file
+ salary: 9000
diff --git a/activerecord/test/fixtures/doubloons.yml b/activerecord/test/fixtures/doubloons.yml
new file mode 100644
index 0000000000..efd1643971
--- /dev/null
+++ b/activerecord/test/fixtures/doubloons.yml
@@ -0,0 +1,3 @@
+blackbeards_doubloon:
+ pirate: blackbeard
+ weight: 2
diff --git a/activerecord/test/fixtures/live_parrots.yml b/activerecord/test/fixtures/live_parrots.yml
new file mode 100644
index 0000000000..95b2078da7
--- /dev/null
+++ b/activerecord/test/fixtures/live_parrots.yml
@@ -0,0 +1,4 @@
+dusty:
+ name: "Dusty Bluebird"
+ treasures: [ruby, sapphire]
+ parrot_sti_class: LiveParrot
diff --git a/activerecord/test/fixtures/naked/csv/accounts.csv b/activerecord/test/fixtures/naked/csv/accounts.csv
deleted file mode 100644
index 8b13789179..0000000000
--- a/activerecord/test/fixtures/naked/csv/accounts.csv
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/activerecord/test/fixtures/naked/yml/parrots.yml b/activerecord/test/fixtures/naked/yml/parrots.yml
new file mode 100644
index 0000000000..3e10331105
--- /dev/null
+++ b/activerecord/test/fixtures/naked/yml/parrots.yml
@@ -0,0 +1,2 @@
+george:
+ arrr: "Curious George"
diff --git a/activerecord/test/fixtures/nodes.yml b/activerecord/test/fixtures/nodes.yml
new file mode 100644
index 0000000000..b8bb8216ee
--- /dev/null
+++ b/activerecord/test/fixtures/nodes.yml
@@ -0,0 +1,29 @@
+grandparent:
+ id: 1
+ tree_id: 1
+ name: Grand Parent
+
+parent_a:
+ id: 2
+ tree_id: 1
+ parent_id: 1
+ name: Parent A
+
+parent_b:
+ id: 3
+ tree_id: 1
+ parent_id: 1
+ name: Parent B
+
+child_one_of_a:
+ id: 4
+ tree_id: 1
+ parent_id: 2
+ name: Child one
+
+child_two_of_b:
+ id: 5
+ tree_id: 1
+ parent_id: 2
+ name: Child two
+
diff --git a/activerecord/test/fixtures/other_comments.yml b/activerecord/test/fixtures/other_comments.yml
new file mode 100644
index 0000000000..55e8216ec7
--- /dev/null
+++ b/activerecord/test/fixtures/other_comments.yml
@@ -0,0 +1,6 @@
+_fixture:
+ model_class: Comment
+
+second_greetings:
+ post: second_welcome
+ body: Thank you for the second welcome
diff --git a/activerecord/test/fixtures/other_posts.yml b/activerecord/test/fixtures/other_posts.yml
new file mode 100644
index 0000000000..39ff763547
--- /dev/null
+++ b/activerecord/test/fixtures/other_posts.yml
@@ -0,0 +1,7 @@
+_fixture:
+ model_class: Post
+
+second_welcome:
+ author_id: 1
+ title: Welcome to the another weblog
+ body: It's really nice today
diff --git a/activerecord/test/fixtures/pirates.yml b/activerecord/test/fixtures/pirates.yml
index 1bb3bf0051..0b1a785853 100644
--- a/activerecord/test/fixtures/pirates.yml
+++ b/activerecord/test/fixtures/pirates.yml
@@ -10,3 +10,6 @@ redbeard:
mark:
catchphrase: "X $LABELs the spot!"
+
+1:
+ catchphrase: "#$LABEL pirate!"
diff --git a/activerecord/test/fixtures/trees.yml b/activerecord/test/fixtures/trees.yml
new file mode 100644
index 0000000000..9e030b7632
--- /dev/null
+++ b/activerecord/test/fixtures/trees.yml
@@ -0,0 +1,3 @@
+root:
+ id: 1
+ name: The Root
diff --git a/activerecord/test/migrations/missing/1000_people_have_middle_names.rb b/activerecord/test/migrations/missing/1000_people_have_middle_names.rb
index 9fd495b97c..4b83d61beb 100644
--- a/activerecord/test/migrations/missing/1000_people_have_middle_names.rb
+++ b/activerecord/test/migrations/missing/1000_people_have_middle_names.rb
@@ -6,4 +6,4 @@ class PeopleHaveMiddleNames < ActiveRecord::Migration
def self.down
remove_column "people", "middle_name"
end
-end \ No newline at end of file
+end
diff --git a/activerecord/test/migrations/missing/1_people_have_last_names.rb b/activerecord/test/migrations/missing/1_people_have_last_names.rb
index 81af5fef5e..68209f3ce9 100644
--- a/activerecord/test/migrations/missing/1_people_have_last_names.rb
+++ b/activerecord/test/migrations/missing/1_people_have_last_names.rb
@@ -6,4 +6,4 @@ class PeopleHaveLastNames < ActiveRecord::Migration
def self.down
remove_column "people", "last_name"
end
-end \ No newline at end of file
+end
diff --git a/activerecord/test/migrations/missing/3_we_need_reminders.rb b/activerecord/test/migrations/missing/3_we_need_reminders.rb
index d5e71ce8ef..25bb49cb32 100644
--- a/activerecord/test/migrations/missing/3_we_need_reminders.rb
+++ b/activerecord/test/migrations/missing/3_we_need_reminders.rb
@@ -9,4 +9,4 @@ class WeNeedReminders < ActiveRecord::Migration
def self.down
drop_table "reminders"
end
-end \ No newline at end of file
+end
diff --git a/activerecord/test/migrations/missing/4_innocent_jointable.rb b/activerecord/test/migrations/missing/4_innocent_jointable.rb
index 21c9ca5328..002a1bf2a6 100644
--- a/activerecord/test/migrations/missing/4_innocent_jointable.rb
+++ b/activerecord/test/migrations/missing/4_innocent_jointable.rb
@@ -9,4 +9,4 @@ class InnocentJointable < ActiveRecord::Migration
def self.down
drop_table "people_reminders"
end
-end \ No newline at end of file
+end
diff --git a/activerecord/test/migrations/rename/1_we_need_things.rb b/activerecord/test/migrations/rename/1_we_need_things.rb
index cdbe0b1679..f5484ac54f 100644
--- a/activerecord/test/migrations/rename/1_we_need_things.rb
+++ b/activerecord/test/migrations/rename/1_we_need_things.rb
@@ -8,4 +8,4 @@ class WeNeedThings < ActiveRecord::Migration
def self.down
drop_table "things"
end
-end \ No newline at end of file
+end
diff --git a/activerecord/test/migrations/rename/2_rename_things.rb b/activerecord/test/migrations/rename/2_rename_things.rb
index d441b71fc9..533a113ea8 100644
--- a/activerecord/test/migrations/rename/2_rename_things.rb
+++ b/activerecord/test/migrations/rename/2_rename_things.rb
@@ -6,4 +6,4 @@ class RenameThings < ActiveRecord::Migration
def self.down
rename_table "awesome_things", "things"
end
-end \ No newline at end of file
+end
diff --git a/activerecord/test/migrations/valid/2_we_need_reminders.rb b/activerecord/test/migrations/valid/2_we_need_reminders.rb
index d5e71ce8ef..25bb49cb32 100644
--- a/activerecord/test/migrations/valid/2_we_need_reminders.rb
+++ b/activerecord/test/migrations/valid/2_we_need_reminders.rb
@@ -9,4 +9,4 @@ class WeNeedReminders < ActiveRecord::Migration
def self.down
drop_table "reminders"
end
-end \ No newline at end of file
+end
diff --git a/activerecord/test/migrations/valid/3_innocent_jointable.rb b/activerecord/test/migrations/valid/3_innocent_jointable.rb
index 21c9ca5328..002a1bf2a6 100644
--- a/activerecord/test/migrations/valid/3_innocent_jointable.rb
+++ b/activerecord/test/migrations/valid/3_innocent_jointable.rb
@@ -9,4 +9,4 @@ class InnocentJointable < ActiveRecord::Migration
def self.down
drop_table "people_reminders"
end
-end \ No newline at end of file
+end
diff --git a/activerecord/test/migrations/valid_with_subdirectories/sub/2_we_need_reminders.rb b/activerecord/test/migrations/valid_with_subdirectories/sub/2_we_need_reminders.rb
index d5e71ce8ef..25bb49cb32 100644
--- a/activerecord/test/migrations/valid_with_subdirectories/sub/2_we_need_reminders.rb
+++ b/activerecord/test/migrations/valid_with_subdirectories/sub/2_we_need_reminders.rb
@@ -9,4 +9,4 @@ class WeNeedReminders < ActiveRecord::Migration
def self.down
drop_table "reminders"
end
-end \ No newline at end of file
+end
diff --git a/activerecord/test/migrations/valid_with_subdirectories/sub1/3_innocent_jointable.rb b/activerecord/test/migrations/valid_with_subdirectories/sub1/3_innocent_jointable.rb
index 21c9ca5328..002a1bf2a6 100644
--- a/activerecord/test/migrations/valid_with_subdirectories/sub1/3_innocent_jointable.rb
+++ b/activerecord/test/migrations/valid_with_subdirectories/sub1/3_innocent_jointable.rb
@@ -9,4 +9,4 @@ class InnocentJointable < ActiveRecord::Migration
def self.down
drop_table "people_reminders"
end
-end \ No newline at end of file
+end
diff --git a/activerecord/test/models/admin.rb b/activerecord/test/models/admin.rb
index 00e69fbed8..a38e3f4846 100644
--- a/activerecord/test/models/admin.rb
+++ b/activerecord/test/models/admin.rb
@@ -2,4 +2,4 @@ module Admin
def self.table_name_prefix
'admin_'
end
-end \ No newline at end of file
+end
diff --git a/activerecord/test/models/admin/account.rb b/activerecord/test/models/admin/account.rb
index 46de28aae1..bd23192d20 100644
--- a/activerecord/test/models/admin/account.rb
+++ b/activerecord/test/models/admin/account.rb
@@ -1,3 +1,3 @@
class Admin::Account < ActiveRecord::Base
has_many :users
-end \ No newline at end of file
+end
diff --git a/activerecord/test/models/admin/randomly_named_c1.rb b/activerecord/test/models/admin/randomly_named_c1.rb
index 2f81d5b831..b64ae7fc41 100644
--- a/activerecord/test/models/admin/randomly_named_c1.rb
+++ b/activerecord/test/models/admin/randomly_named_c1.rb
@@ -1,3 +1,7 @@
-class Admin::ClassNameThatDoesNotFollowCONVENTIONS < ActiveRecord::Base
- self.table_name = :randomly_named_table
+class Admin::ClassNameThatDoesNotFollowCONVENTIONS1 < ActiveRecord::Base
+ self.table_name = :randomly_named_table2
+end
+
+class Admin::ClassNameThatDoesNotFollowCONVENTIONS2 < ActiveRecord::Base
+ self.table_name = :randomly_named_table3
end
diff --git a/activerecord/test/models/aircraft.rb b/activerecord/test/models/aircraft.rb
index 1f35ef45da..c4404a8094 100644
--- a/activerecord/test/models/aircraft.rb
+++ b/activerecord/test/models/aircraft.rb
@@ -1,4 +1,5 @@
class Aircraft < ActiveRecord::Base
self.pluralize_table_names = false
has_many :engines, :foreign_key => "car_id"
+ has_many :wheels, as: :wheelable
end
diff --git a/activerecord/test/models/author.rb b/activerecord/test/models/author.rb
index 8949cf5826..0d90cbb110 100644
--- a/activerecord/test/models/author.rb
+++ b/activerecord/test/models/author.rb
@@ -1,5 +1,6 @@
class Author < ActiveRecord::Base
has_many :posts
+ has_many :serialized_posts
has_one :post
has_many :very_special_comments, :through => :posts
has_many :posts_with_comments, -> { includes(:comments) }, :class_name => "Post"
@@ -44,13 +45,14 @@ class Author < ActiveRecord::Base
has_many :special_posts
has_many :special_post_comments, :through => :special_posts, :source => :comments
+ has_many :special_posts_with_default_scope, :class_name => 'SpecialPostWithDefaultScope'
has_many :sti_posts, :class_name => 'StiPost'
has_many :sti_post_comments, :through => :sti_posts, :source => :comments
- has_many :special_nonexistant_posts, -> { where("posts.body = 'nonexistant'") }, :class_name => "SpecialPost"
- has_many :special_nonexistant_post_comments, -> { where('comments.post_id' => 0) }, :through => :special_nonexistant_posts, :source => :comments
- has_many :nonexistant_comments, :through => :posts
+ has_many :special_nonexistent_posts, -> { where("posts.body = 'nonexistent'") }, :class_name => "SpecialPost"
+ has_many :special_nonexistent_post_comments, -> { where('comments.post_id' => 0) }, :through => :special_nonexistent_posts, :source => :comments
+ has_many :nonexistent_comments, :through => :posts
has_many :hello_posts, -> { where "posts.body = 'hello'" }, :class_name => "Post"
has_many :hello_post_comments, :through => :hello_posts, :source => :comments
@@ -142,9 +144,6 @@ class Author < ActiveRecord::Base
has_many :posts_with_signature, ->(record) { where("posts.title LIKE ?", "%by #{record.name.downcase}%") }, class_name: "Post"
- scope :relation_include_posts, -> { includes(:posts) }
- scope :relation_include_tags, -> { includes(:tags) }
-
attr_accessor :post_log
after_initialize :set_post_log
diff --git a/activerecord/test/models/binary.rb b/activerecord/test/models/binary.rb
index 950c459199..39b2f5090a 100644
--- a/activerecord/test/models/binary.rb
+++ b/activerecord/test/models/binary.rb
@@ -1,2 +1,2 @@
class Binary < ActiveRecord::Base
-end \ No newline at end of file
+end
diff --git a/activerecord/test/models/bird.rb b/activerecord/test/models/bird.rb
index dff099c1fb..2a51d903b8 100644
--- a/activerecord/test/models/bird.rb
+++ b/activerecord/test/models/bird.rb
@@ -7,6 +7,6 @@ class Bird < ActiveRecord::Base
attr_accessor :cancel_save_from_callback
before_save :cancel_save_callback_method, :if => :cancel_save_from_callback
def cancel_save_callback_method
- false
+ throw(:abort)
end
end
diff --git a/activerecord/test/models/book.rb b/activerecord/test/models/book.rb
index 2170018068..1927191393 100644
--- a/activerecord/test/models/book.rb
+++ b/activerecord/test/models/book.rb
@@ -10,6 +10,10 @@ class Book < ActiveRecord::Base
enum status: [:proposed, :written, :published]
enum read_status: {unread: 0, reading: 2, read: 3}
enum nullable_status: [:single, :married]
+ enum language: [:english, :spanish, :french], _prefix: :in
+ enum author_visibility: [:visible, :invisible], _prefix: true
+ enum illustrator_visibility: [:visible, :invisible], _prefix: true
+ enum font_size: [:small, :medium, :large], _prefix: :with, _suffix: true
def published!
super
diff --git a/activerecord/test/models/bulb.rb b/activerecord/test/models/bulb.rb
index 831a0d5387..a6e83fe353 100644
--- a/activerecord/test/models/bulb.rb
+++ b/activerecord/test/models/bulb.rb
@@ -46,6 +46,6 @@ end
class FailedBulb < Bulb
before_destroy do
- false
+ throw(:abort)
end
end
diff --git a/activerecord/test/models/car.rb b/activerecord/test/models/car.rb
index db0f93f63b..81263b79d1 100644
--- a/activerecord/test/models/car.rb
+++ b/activerecord/test/models/car.rb
@@ -8,7 +8,7 @@ class Car < ActiveRecord::Base
has_one :bulb
has_many :tyres
- has_many :engines, :dependent => :destroy
+ has_many :engines, :dependent => :destroy, inverse_of: :my_car
has_many :wheels, :as => :wheelable, :dependent => :destroy
scope :incl_tyres, -> { includes(:tyres) }
diff --git a/activerecord/test/models/carrier.rb b/activerecord/test/models/carrier.rb
new file mode 100644
index 0000000000..230be118c3
--- /dev/null
+++ b/activerecord/test/models/carrier.rb
@@ -0,0 +1,2 @@
+class Carrier < ActiveRecord::Base
+end
diff --git a/activerecord/test/models/categorization.rb b/activerecord/test/models/categorization.rb
index 6588531de6..4cd67c970a 100644
--- a/activerecord/test/models/categorization.rb
+++ b/activerecord/test/models/categorization.rb
@@ -1,6 +1,6 @@
class Categorization < ActiveRecord::Base
belongs_to :post
- belongs_to :category
+ belongs_to :category, counter_cache: true
belongs_to :named_category, :class_name => 'Category', :foreign_key => :named_category_name, :primary_key => :name
belongs_to :author
diff --git a/activerecord/test/models/chef.rb b/activerecord/test/models/chef.rb
index 67a4e54f06..698a52e045 100644
--- a/activerecord/test/models/chef.rb
+++ b/activerecord/test/models/chef.rb
@@ -1,3 +1,4 @@
class Chef < ActiveRecord::Base
belongs_to :employable, polymorphic: true
+ has_many :recipes
end
diff --git a/activerecord/test/models/comment.rb b/activerecord/test/models/comment.rb
index 15970758db..b38b17e90e 100644
--- a/activerecord/test/models/comment.rb
+++ b/activerecord/test/models/comment.rb
@@ -9,6 +9,7 @@ class Comment < ActiveRecord::Base
belongs_to :post, :counter_cache => true
belongs_to :author, polymorphic: true
belongs_to :resource, polymorphic: true
+ belongs_to :developer
has_many :ratings
@@ -51,3 +52,8 @@ class CommentThatAutomaticallyAltersPostBody < Comment
comment.post.update_attributes(body: "Automatically altered")
end
end
+
+class CommentWithDefaultScopeReferencesAssociation < Comment
+ default_scope ->{ includes(:developer).order('developers.name').references(:developer) }
+ belongs_to :developer
+end
diff --git a/activerecord/test/models/company.rb b/activerecord/test/models/company.rb
index 76411ecb37..1dcd9fc21e 100644
--- a/activerecord/test/models/company.rb
+++ b/activerecord/test/models/company.rb
@@ -25,6 +25,9 @@ class Company < AbstractCompany
def private_method
"I am Jack's innermost fears and aspirations"
end
+
+ class SpecialCo < Company
+ end
end
module Namespaced
@@ -71,6 +74,7 @@ class Firm < Company
# Oracle tests were failing because of that as the second fixture was selected
has_one :account_using_primary_key, -> { order('id') }, :primary_key => "firm_id", :class_name => "Account"
has_one :account_using_foreign_and_primary_keys, :foreign_key => "firm_name", :primary_key => "name", :class_name => "Account"
+ has_one :account_with_inexistent_foreign_key, class_name: 'Account', foreign_key: "inexistent"
has_one :deletable_account, :foreign_key => "firm_id", :class_name => "Account", :dependent => :delete
has_one :account_limit_500_with_hash_conditions, -> { where :credit_limit => 500 }, :foreign_key => "firm_id", :class_name => "Account"
@@ -81,6 +85,9 @@ class Firm < Company
has_many :association_with_references, -> { references(:foo) }, :class_name => 'Client'
+ has_one :lead_developer, class_name: "Developer"
+ has_many :projects
+
def log
@log ||= []
end
@@ -212,7 +219,7 @@ class Account < ActiveRecord::Base
protected
def check_empty_credit_limit
- errors.add_on_empty "credit_limit"
+ errors.add("credit_limit", :blank) if credit_limit.blank?
end
private
diff --git a/activerecord/test/models/company_in_module.rb b/activerecord/test/models/company_in_module.rb
index dae102d12b..bf0a0d1c3e 100644
--- a/activerecord/test/models/company_in_module.rb
+++ b/activerecord/test/models/company_in_module.rb
@@ -91,7 +91,7 @@ module MyApplication
protected
def check_empty_credit_limit
- errors.add_on_empty "credit_limit"
+ errors.add("credit_card", :blank) if credit_card.blank?
end
end
end
diff --git a/activerecord/test/models/contact.rb b/activerecord/test/models/contact.rb
index 3ea17c3abf..9f2f69e1ee 100644
--- a/activerecord/test/models/contact.rb
+++ b/activerecord/test/models/contact.rb
@@ -3,7 +3,7 @@ module ContactFakeColumns
base.class_eval do
establish_connection(:adapter => 'fake')
- connection.tables = [table_name]
+ connection.data_sources = [table_name]
connection.primary_keys = {
table_name => 'id'
}
diff --git a/activerecord/test/models/content.rb b/activerecord/test/models/content.rb
new file mode 100644
index 0000000000..140e1dfc78
--- /dev/null
+++ b/activerecord/test/models/content.rb
@@ -0,0 +1,40 @@
+class Content < ActiveRecord::Base
+ self.table_name = 'content'
+ has_one :content_position, dependent: :destroy
+
+ def self.destroyed_ids
+ @destroyed_ids ||= []
+ end
+
+ before_destroy do |object|
+ Content.destroyed_ids << object.id
+ end
+end
+
+class ContentWhichRequiresTwoDestroyCalls < ActiveRecord::Base
+ self.table_name = 'content'
+ has_one :content_position, foreign_key: 'content_id', dependent: :destroy
+
+ after_initialize do
+ @destroy_count = 0
+ end
+
+ before_destroy do
+ @destroy_count += 1
+ if @destroy_count == 1
+ throw :abort
+ end
+ end
+end
+
+class ContentPosition < ActiveRecord::Base
+ belongs_to :content, dependent: :destroy
+
+ def self.destroyed_ids
+ @destroyed_ids ||= []
+ end
+
+ before_destroy do |object|
+ ContentPosition.destroyed_ids << object.id
+ end
+end
diff --git a/activerecord/test/models/customer.rb b/activerecord/test/models/customer.rb
index 7e8e82542f..afe4b3d707 100644
--- a/activerecord/test/models/customer.rb
+++ b/activerecord/test/models/customer.rb
@@ -2,7 +2,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 { |balance| balance.to_money }
+ composed_of :balance, :class_name => "Money", :mapping => %w(balance amount), :converter => Proc.new(&:to_money)
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/customer_carrier.rb b/activerecord/test/models/customer_carrier.rb
new file mode 100644
index 0000000000..37186903ff
--- /dev/null
+++ b/activerecord/test/models/customer_carrier.rb
@@ -0,0 +1,14 @@
+class CustomerCarrier < ActiveRecord::Base
+ cattr_accessor :current_customer
+
+ belongs_to :customer
+ belongs_to :carrier
+
+ default_scope -> {
+ if current_customer
+ where(customer: current_customer)
+ else
+ all
+ end
+ }
+end
diff --git a/activerecord/test/models/developer.rb b/activerecord/test/models/developer.rb
index 5bd2f00129..9a907273f8 100644
--- a/activerecord/test/models/developer.rb
+++ b/activerecord/test/models/developer.rb
@@ -7,14 +7,20 @@ module DeveloperProjectsAssociationExtension2
end
class Developer < ActiveRecord::Base
+ self.ignored_columns = %w(first_name last_name)
+
has_and_belongs_to_many :projects do
def find_most_recent
order("id DESC").first
end
end
+ belongs_to :mentor
+
accepts_nested_attributes_for :projects
+ has_and_belongs_to_many :shared_computers, class_name: "Computer"
+
has_and_belongs_to_many :projects_extended_by_name,
-> { extending(DeveloperProjectsAssociationExtension) },
:class_name => "Project",
@@ -46,6 +52,12 @@ class Developer < ActiveRecord::Base
has_many :audit_logs
has_many :contracts
has_many :firms, :through => :contracts, :source => :firm
+ has_many :comments, ->(developer) { where(body: "I'm #{developer.name}") }
+ has_many :ratings, through: :comments
+ has_one :ship, dependent: :nullify
+
+ belongs_to :firm
+ has_many :contracted_projects, class_name: "Project"
scope :jamises, -> { where(:name => 'Jamis') }
@@ -56,6 +68,9 @@ class Developer < ActiveRecord::Base
developer.audit_logs.build :message => "Computer created"
end
+ attr_accessor :last_name
+ define_attribute_method 'last_name'
+
def log=(message)
audit_logs.build :message => message
end
diff --git a/activerecord/test/models/doubloon.rb b/activerecord/test/models/doubloon.rb
new file mode 100644
index 0000000000..2b11d128e2
--- /dev/null
+++ b/activerecord/test/models/doubloon.rb
@@ -0,0 +1,12 @@
+class AbstractDoubloon < ActiveRecord::Base
+ # This has functionality that might be shared by multiple classes.
+
+ self.abstract_class = true
+ belongs_to :pirate
+end
+
+class Doubloon < AbstractDoubloon
+ # This uses an abstract class that defines attributes and associations.
+
+ self.table_name = 'doubloons'
+end
diff --git a/activerecord/test/models/event.rb b/activerecord/test/models/event.rb
index 99fa0feeb7..365ab32b0b 100644
--- a/activerecord/test/models/event.rb
+++ b/activerecord/test/models/event.rb
@@ -1,3 +1,3 @@
class Event < ActiveRecord::Base
validates_uniqueness_of :title
-end \ No newline at end of file
+end
diff --git a/activerecord/test/models/face.rb b/activerecord/test/models/face.rb
index 91e46f83e5..af76fea52c 100644
--- a/activerecord/test/models/face.rb
+++ b/activerecord/test/models/face.rb
@@ -1,7 +1,7 @@
class Face < ActiveRecord::Base
belongs_to :man, :inverse_of => :face
belongs_to :polymorphic_man, :polymorphic => true, :inverse_of => :polymorphic_face
- # Oracle identifier lengh is limited to 30 bytes or less, `polymorphic` renamed `poly`
+ # 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
diff --git a/activerecord/test/models/guid.rb b/activerecord/test/models/guid.rb
index 9208dc28fa..05653ba498 100644
--- a/activerecord/test/models/guid.rb
+++ b/activerecord/test/models/guid.rb
@@ -1,2 +1,2 @@
class Guid < ActiveRecord::Base
-end \ No newline at end of file
+end
diff --git a/activerecord/test/models/guitar.rb b/activerecord/test/models/guitar.rb
new file mode 100644
index 0000000000..cd068ff53d
--- /dev/null
+++ b/activerecord/test/models/guitar.rb
@@ -0,0 +1,4 @@
+class Guitar < ActiveRecord::Base
+ has_many :tuning_pegs, index_errors: true
+ accepts_nested_attributes_for :tuning_pegs
+end
diff --git a/activerecord/test/models/hotel.rb b/activerecord/test/models/hotel.rb
index b352cd22f3..491f8dfde3 100644
--- a/activerecord/test/models/hotel.rb
+++ b/activerecord/test/models/hotel.rb
@@ -3,4 +3,5 @@ class Hotel < ActiveRecord::Base
has_many :chefs, through: :departments
has_many :cake_designers, source_type: 'CakeDesigner', source: :employable, through: :chefs
has_many :drink_designers, source_type: 'DrinkDesigner', source: :employable, through: :chefs
+ has_many :recipes, through: :chefs
end
diff --git a/activerecord/test/models/image.rb b/activerecord/test/models/image.rb
new file mode 100644
index 0000000000..7ae8e4a7f6
--- /dev/null
+++ b/activerecord/test/models/image.rb
@@ -0,0 +1,3 @@
+class Image < ActiveRecord::Base
+ belongs_to :imageable, foreign_key: :imageable_identifier, foreign_type: :imageable_class
+end
diff --git a/activerecord/test/models/member.rb b/activerecord/test/models/member.rb
index 72095f9236..7693c6e515 100644
--- a/activerecord/test/models/member.rb
+++ b/activerecord/test/models/member.rb
@@ -26,7 +26,13 @@ class Member < ActiveRecord::Base
has_many :current_memberships, -> { where :favourite => true }
has_many :clubs, :through => :current_memberships
+ has_many :tenant_memberships
+ has_many :tenant_clubs, through: :tenant_memberships, class_name: 'Club', source: :club
+
has_one :club_through_many, :through => :current_memberships, :source => :club
+
+ belongs_to :admittable, polymorphic: true
+ has_one :premium_club, through: :admittable
end
class SelfMember < ActiveRecord::Base
diff --git a/activerecord/test/models/member_detail.rb b/activerecord/test/models/member_detail.rb
index 9d253aa126..157130986c 100644
--- a/activerecord/test/models/member_detail.rb
+++ b/activerecord/test/models/member_detail.rb
@@ -1,7 +1,8 @@
class MemberDetail < ActiveRecord::Base
- belongs_to :member, :inverse_of => false
+ belongs_to :member, inverse_of: false
belongs_to :organization
- has_one :member_type, :through => :member
+ has_one :member_type, through: :member
+ has_one :membership, through: :member
- has_many :organization_member_details, :through => :organization, :source => :member_details
+ has_many :organization_member_details, through: :organization, source: :member_details
end
diff --git a/activerecord/test/models/membership.rb b/activerecord/test/models/membership.rb
index df7167ee93..e181ba1f11 100644
--- a/activerecord/test/models/membership.rb
+++ b/activerecord/test/models/membership.rb
@@ -18,3 +18,18 @@ class SelectedMembership < Membership
select("'1' as foo")
end
end
+
+class TenantMembership < Membership
+ cattr_accessor :current_member
+
+ belongs_to :member
+ belongs_to :club
+
+ default_scope -> {
+ if current_member
+ where(member: current_member)
+ else
+ all
+ end
+ }
+end
diff --git a/activerecord/test/models/mentor.rb b/activerecord/test/models/mentor.rb
new file mode 100644
index 0000000000..11f1e4bff8
--- /dev/null
+++ b/activerecord/test/models/mentor.rb
@@ -0,0 +1,3 @@
+class Mentor < ActiveRecord::Base
+ has_many :developers
+end \ No newline at end of file
diff --git a/activerecord/test/models/node.rb b/activerecord/test/models/node.rb
new file mode 100644
index 0000000000..07dd2dbccb
--- /dev/null
+++ b/activerecord/test/models/node.rb
@@ -0,0 +1,5 @@
+class Node < ActiveRecord::Base
+ belongs_to :tree, touch: true
+ belongs_to :parent, class_name: 'Node', touch: true, optional: true
+ has_many :children, class_name: 'Node', foreign_key: :parent_id, dependent: :destroy
+end
diff --git a/activerecord/test/models/notification.rb b/activerecord/test/models/notification.rb
new file mode 100644
index 0000000000..b4b4b8f1b6
--- /dev/null
+++ b/activerecord/test/models/notification.rb
@@ -0,0 +1,2 @@
+class Notification < ActiveRecord::Base
+end
diff --git a/activerecord/test/models/organization.rb b/activerecord/test/models/organization.rb
index 72e7bade68..f3e92f3067 100644
--- a/activerecord/test/models/organization.rb
+++ b/activerecord/test/models/organization.rb
@@ -8,5 +8,7 @@ class Organization < ActiveRecord::Base
has_one :author, :primary_key => :name
has_one :author_owned_essay_category, :through => :author, :source => :owned_essay_category
+ has_many :posts, :through => :author, :source => :posts
+
scope :clubs, -> { from('clubs') }
end
diff --git a/activerecord/test/models/owner.rb b/activerecord/test/models/owner.rb
index 2e3a9a3681..cedb774b10 100644
--- a/activerecord/test/models/owner.rb
+++ b/activerecord/test/models/owner.rb
@@ -17,6 +17,8 @@ class Owner < ActiveRecord::Base
after_commit :execute_blocks
+ accepts_nested_attributes_for :pets, allow_destroy: true
+
def blocks
@blocks ||= []
end
diff --git a/activerecord/test/models/parrot.rb b/activerecord/test/models/parrot.rb
index e76e83f314..ddc9dcaf29 100644
--- a/activerecord/test/models/parrot.rb
+++ b/activerecord/test/models/parrot.rb
@@ -11,7 +11,7 @@ class Parrot < ActiveRecord::Base
attr_accessor :cancel_save_from_callback
before_save :cancel_save_callback_method, :if => :cancel_save_from_callback
def cancel_save_callback_method
- false
+ throw(:abort)
end
end
@@ -19,11 +19,5 @@ class LiveParrot < Parrot
end
class DeadParrot < Parrot
- belongs_to :killer, :class_name => 'Pirate'
-end
-
-class FunkyParrot < Parrot
- before_destroy do
- raise "before_destroy was called"
- end
+ belongs_to :killer, :class_name => 'Pirate', foreign_key: :killer_id
end
diff --git a/activerecord/test/models/person.rb b/activerecord/test/models/person.rb
index c7e54e7b63..a4a9c6b0d4 100644
--- a/activerecord/test/models/person.rb
+++ b/activerecord/test/models/person.rb
@@ -30,12 +30,13 @@ class Person < ActiveRecord::Base
has_many :agents_of_agents, :through => :agents, :source => :agents
belongs_to :number1_fan, :class_name => 'Person'
+ has_many :personal_legacy_things, :dependent => :destroy
+
has_many :agents_posts, :through => :agents, :source => :posts
has_many :agents_posts_authors, :through => :agents_posts, :source => :author
has_many :essays, primary_key: "first_name", foreign_key: "writer_id"
scope :males, -> { where(:gender => 'M') }
- scope :females, -> { where(:gender => 'F') }
end
class PersonWithDependentDestroyJobs < ActiveRecord::Base
diff --git a/activerecord/test/models/personal_legacy_thing.rb b/activerecord/test/models/personal_legacy_thing.rb
new file mode 100644
index 0000000000..a7ee3a0bca
--- /dev/null
+++ b/activerecord/test/models/personal_legacy_thing.rb
@@ -0,0 +1,4 @@
+class PersonalLegacyThing < ActiveRecord::Base
+ self.locking_column = :version
+ belongs_to :person, :counter_cache => true
+end
diff --git a/activerecord/test/models/pirate.rb b/activerecord/test/models/pirate.rb
index 90a3c3ecee..30545bdcd7 100644
--- a/activerecord/test/models/pirate.rb
+++ b/activerecord/test/models/pirate.rb
@@ -36,8 +36,8 @@ class Pirate < ActiveRecord::Base
has_one :foo_bulb, -> { where :name => 'foo' }, :foreign_key => :car_id, :class_name => "Bulb"
- accepts_nested_attributes_for :parrots, :birds, :allow_destroy => true, :reject_if => proc { |attributes| attributes.empty? }
- accepts_nested_attributes_for :ship, :allow_destroy => true, :reject_if => proc { |attributes| attributes.empty? }
+ accepts_nested_attributes_for :parrots, :birds, :allow_destroy => true, :reject_if => proc(&:empty?)
+ accepts_nested_attributes_for :ship, :allow_destroy => true, :reject_if => proc(&:empty?)
accepts_nested_attributes_for :update_only_ship, :update_only => true
accepts_nested_attributes_for :parrots_with_method_callbacks, :parrots_with_proc_callbacks,
:birds_with_method_callbacks, :birds_with_proc_callbacks, :allow_destroy => true
@@ -56,7 +56,7 @@ class Pirate < ActiveRecord::Base
attr_accessor :cancel_save_from_callback, :parrots_limit
before_save :cancel_save_callback_method, :if => :cancel_save_from_callback
def cancel_save_callback_method
- false
+ throw(:abort)
end
private
@@ -89,4 +89,4 @@ class FamousPirate < ActiveRecord::Base
self.table_name = 'pirates'
has_many :famous_ships
validates_presence_of :catchphrase, on: :conference
-end \ No newline at end of file
+end
diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb
index a29858213b..23cebe2602 100644
--- a/activerecord/test/models/post.rb
+++ b/activerecord/test/models/post.rb
@@ -18,6 +18,7 @@ class Post < ActiveRecord::Base
end
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 :limit_by, lambda {|l| limit(l) }
@@ -41,6 +42,9 @@ class Post < ActiveRecord::Base
scope :with_tags, -> { preload(:taggings) }
scope :tagged_with, ->(id) { joins(:taggings).where(taggings: { tag_id: id }) }
+ scope :tagged_with_comment, ->(comment) { joins(:taggings).where(taggings: { comment: comment }) }
+
+ scope :typographically_interesting, -> { containing_the_letter_a.or(titled_with_an_apostrophe) }
has_many :comments do
def find_most_recent
@@ -71,14 +75,11 @@ class Post < ActiveRecord::Base
through: :author_with_address,
source: :author_address_extra
- has_many :comments_with_interpolated_conditions,
- ->(p) { where "#{"#{p.aliased_table_name}." rescue ""}body = ?", 'Thank you for the welcome' },
- :class_name => 'Comment'
-
has_one :very_special_comment
has_one :very_special_comment_with_post, -> { includes(:post) }, :class_name => "VerySpecialComment"
+ has_one :very_special_comment_with_post_with_joins, -> { joins(:post).order('posts.id') }, class_name: "VerySpecialComment"
has_many :special_comments
- has_many :nonexistant_comments, -> { where 'comments.id < 0' }, :class_name => 'Comment'
+ has_many :nonexistent_comments, -> { where 'comments.id < 0' }, :class_name => 'Comment'
has_many :special_comments_ratings, :through => :special_comments, :source => :ratings
has_many :special_comments_ratings_taggings, :through => :special_comments_ratings, :source => :taggings
@@ -97,11 +98,11 @@ class Post < ActiveRecord::Base
end
end
- has_many :taggings_with_delete_all, :class_name => 'Tagging', :as => :taggable, :dependent => :delete_all
- has_many :taggings_with_destroy, :class_name => 'Tagging', :as => :taggable, :dependent => :destroy
+ 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
- has_many :tags_with_destroy, :through => :taggings, :source => :tag, :dependent => :destroy
- has_many :tags_with_nullify, :through => :taggings, :source => :tag, :dependent => :nullify
+ has_many :tags_with_destroy, :through => :taggings, :source => :tag, :dependent => :destroy, counter_cache: :tags_with_destroy_count
+ has_many :tags_with_nullify, :through => :taggings, :source => :tag, :dependent => :nullify, counter_cache: :tags_with_nullify_count
has_many :misc_tags, -> { where :tags => { :name => 'Misc' } }, :through => :taggings, :source => :tag
has_many :funky_tags, :through => :taggings, :source => :tag
@@ -126,6 +127,9 @@ class Post < ActiveRecord::Base
has_many :taggings_using_author_id, :primary_key => :author_id, :as => :taggable, :class_name => 'Tagging'
has_many :tags_using_author_id, :through => :taggings_using_author_id, :source => :tag
+ has_many :images, :as => :imageable, :foreign_key => :imageable_identifier, :foreign_type => :imageable_class
+ has_one :main_image, :as => :imageable, :foreign_key => :imageable_identifier, :foreign_type => :imageable_class, :class_name => 'Image'
+
has_many :standard_categorizations, :class_name => 'Categorization', :foreign_key => :post_id
has_many :author_using_custom_pk, :through => :standard_categorizations
has_many :authors_using_custom_pk, :through => :standard_categorizations
@@ -181,6 +185,7 @@ class SubStiPost < StiPost
end
class FirstPost < ActiveRecord::Base
+ self.inheritance_column = :disabled
self.table_name = 'posts'
default_scope { where(:id => 1) }
@@ -189,6 +194,7 @@ class FirstPost < ActiveRecord::Base
end
class PostWithDefaultInclude < ActiveRecord::Base
+ self.inheritance_column = :disabled
self.table_name = 'posts'
default_scope { includes(:comments) }
has_many :comments, :foreign_key => :post_id
@@ -200,16 +206,35 @@ class PostWithSpecialCategorization < Post
end
class PostWithDefaultScope < ActiveRecord::Base
+ self.inheritance_column = :disabled
self.table_name = 'posts'
default_scope { order(:title) }
end
+class PostWithPreloadDefaultScope < ActiveRecord::Base
+ self.table_name = 'posts'
+
+ has_many :readers, foreign_key: 'post_id'
+
+ default_scope { preload(:readers) }
+end
+
+class PostWithIncludesDefaultScope < ActiveRecord::Base
+ self.table_name = 'posts'
+
+ has_many :readers, foreign_key: 'post_id'
+
+ default_scope { includes(:readers) }
+end
+
class SpecialPostWithDefaultScope < ActiveRecord::Base
+ self.inheritance_column = :disabled
self.table_name = 'posts'
default_scope { where(:id => [1, 5,6]) }
end
class PostThatLoadsCommentsInAnAfterSaveHook < ActiveRecord::Base
+ self.inheritance_column = :disabled
self.table_name = 'posts'
has_many :comments, class_name: "CommentThatAutomaticallyAltersPostBody", foreign_key: :post_id
@@ -217,3 +242,24 @@ class PostThatLoadsCommentsInAnAfterSaveHook < ActiveRecord::Base
post.comments.load
end
end
+
+class PostWithAfterCreateCallback < ActiveRecord::Base
+ self.inheritance_column = :disabled
+ self.table_name = 'posts'
+ has_many :comments, foreign_key: :post_id
+
+ after_create do |post|
+ update_attribute(:author_id, comments.first.id)
+ end
+end
+
+class PostWithCommentWithDefaultScopeReferencesAssociation < ActiveRecord::Base
+ self.inheritance_column = :disabled
+ self.table_name = 'posts'
+ has_many :comment_with_default_scope_references_associations, foreign_key: :post_id
+ has_one :first_comment, class_name: "CommentWithDefaultScopeReferencesAssociation", foreign_key: :post_id
+end
+
+class SerializedPost < ActiveRecord::Base
+ serialize :title
+end
diff --git a/activerecord/test/models/professor.rb b/activerecord/test/models/professor.rb
new file mode 100644
index 0000000000..7654eda0ef
--- /dev/null
+++ b/activerecord/test/models/professor.rb
@@ -0,0 +1,5 @@
+require_dependency 'models/arunit2_model'
+
+class Professor < ARUnit2Model
+ has_and_belongs_to_many :courses
+end
diff --git a/activerecord/test/models/project.rb b/activerecord/test/models/project.rb
index 7f42a4b1f8..b034e0e267 100644
--- a/activerecord/test/models/project.rb
+++ b/activerecord/test/models/project.rb
@@ -1,4 +1,5 @@
class Project < ActiveRecord::Base
+ belongs_to :mentor
has_and_belongs_to_many :developers, -> { distinct.order 'developers.name desc, developers.id desc' }
has_and_belongs_to_many :readonly_developers, -> { readonly }, :class_name => "Developer"
has_and_belongs_to_many :non_unique_developers, -> { order 'developers.name desc, developers.id desc' }, :class_name => 'Developer'
@@ -11,6 +12,8 @@ class Project < ActiveRecord::Base
:before_remove => Proc.new {|o, r| o.developers_log << "before_removing#{r.id}"},
:after_remove => Proc.new {|o, r| o.developers_log << "after_removing#{r.id}"}
has_and_belongs_to_many :well_payed_salary_groups, -> { group("developers.salary").having("SUM(salary) > 10000").select("SUM(salary) as salary") }, :class_name => "Developer"
+ belongs_to :firm
+ has_one :lead_developer, through: :firm, inverse_of: :contracted_projects
attr_accessor :developers_log
after_initialize :set_developers_log
diff --git a/activerecord/test/models/randomly_named_c1.rb b/activerecord/test/models/randomly_named_c1.rb
index 18a86c4989..d4be1e13b4 100644
--- a/activerecord/test/models/randomly_named_c1.rb
+++ b/activerecord/test/models/randomly_named_c1.rb
@@ -1,3 +1,3 @@
class ClassNameThatDoesNotFollowCONVENTIONS < ActiveRecord::Base
- self.table_name = :randomly_named_table
+ self.table_name = :randomly_named_table1
end
diff --git a/activerecord/test/models/recipe.rb b/activerecord/test/models/recipe.rb
new file mode 100644
index 0000000000..c387230603
--- /dev/null
+++ b/activerecord/test/models/recipe.rb
@@ -0,0 +1,3 @@
+class Recipe < ActiveRecord::Base
+ belongs_to :chef
+end
diff --git a/activerecord/test/models/ship.rb b/activerecord/test/models/ship.rb
index 77a4728d0b..e333b964ab 100644
--- a/activerecord/test/models/ship.rb
+++ b/activerecord/test/models/ship.rb
@@ -3,10 +3,12 @@ class Ship < ActiveRecord::Base
belongs_to :pirate
belongs_to :update_only_pirate, :class_name => 'Pirate'
+ belongs_to :developer, dependent: :destroy
has_many :parts, :class_name => 'ShipPart'
+ has_many :treasures
accepts_nested_attributes_for :parts, :allow_destroy => true
- accepts_nested_attributes_for :pirate, :allow_destroy => true, :reject_if => proc { |attributes| attributes.empty? }
+ accepts_nested_attributes_for :pirate, :allow_destroy => true, :reject_if => proc(&:empty?)
accepts_nested_attributes_for :update_only_pirate, :update_only => true
validates_presence_of :name
@@ -14,10 +16,22 @@ class Ship < ActiveRecord::Base
attr_accessor :cancel_save_from_callback
before_save :cancel_save_callback_method, :if => :cancel_save_from_callback
def cancel_save_callback_method
- false
+ throw(:abort)
end
end
+class ShipWithoutNestedAttributes < ActiveRecord::Base
+ self.table_name = "ships"
+ has_many :prisoners, inverse_of: :ship, foreign_key: :ship_id
+ has_many :parts, class_name: "ShipPart", foreign_key: :ship_id
+
+ validates :name, presence: true
+end
+
+class Prisoner < ActiveRecord::Base
+ belongs_to :ship, autosave: true, class_name: "ShipWithoutNestedAttributes", inverse_of: :prisoners
+end
+
class FamousShip < ActiveRecord::Base
self.table_name = 'ships'
belongs_to :famous_pirate
diff --git a/activerecord/test/models/ship_part.rb b/activerecord/test/models/ship_part.rb
index b6a8a506b4..05c65f8a4a 100644
--- a/activerecord/test/models/ship_part.rb
+++ b/activerecord/test/models/ship_part.rb
@@ -2,6 +2,7 @@ class ShipPart < ActiveRecord::Base
belongs_to :ship
has_many :trinkets, :class_name => "Treasure", :as => :looter
accepts_nested_attributes_for :trinkets, :allow_destroy => true
+ accepts_nested_attributes_for :ship
validates_presence_of :name
-end \ No newline at end of file
+end
diff --git a/activerecord/test/models/shop_account.rb b/activerecord/test/models/shop_account.rb
new file mode 100644
index 0000000000..1580e8b20c
--- /dev/null
+++ b/activerecord/test/models/shop_account.rb
@@ -0,0 +1,6 @@
+class ShopAccount < ActiveRecord::Base
+ belongs_to :customer
+ belongs_to :customer_carrier
+
+ has_one :carrier, through: :customer_carrier
+end
diff --git a/activerecord/test/models/topic.rb b/activerecord/test/models/topic.rb
index f81ffe1d90..176bc79dc7 100644
--- a/activerecord/test/models/topic.rb
+++ b/activerecord/test/models/topic.rb
@@ -32,7 +32,7 @@ class Topic < ActiveRecord::Base
end
end
- has_many :replies, :dependent => :destroy, :foreign_key => "parent_id"
+ has_many :replies, dependent: :destroy, foreign_key: "parent_id", autosave: true
has_many :approved_replies, -> { approved }, class_name: 'Reply', foreign_key: "parent_id", counter_cache: 'replies_count'
has_many :unique_replies, :dependent => :destroy, :foreign_key => "parent_id"
@@ -86,7 +86,7 @@ class Topic < ActiveRecord::Base
end
def destroy_children
- self.class.delete_all "parent_id = #{id}"
+ self.class.where("parent_id = #{id}").delete_all
end
def set_email_address
diff --git a/activerecord/test/models/treasure.rb b/activerecord/test/models/treasure.rb
index a69d3fd3df..63ff0c23ec 100644
--- a/activerecord/test/models/treasure.rb
+++ b/activerecord/test/models/treasure.rb
@@ -1,6 +1,8 @@
class Treasure < ActiveRecord::Base
has_and_belongs_to_many :parrots
belongs_to :looter, :polymorphic => true
+ # No counter_cache option given
+ belongs_to :ship
has_many :price_estimates, :as => :estimate_of
has_and_belongs_to_many :rich_people, join_table: 'peoples_treasures', validate: false
diff --git a/activerecord/test/models/tree.rb b/activerecord/test/models/tree.rb
new file mode 100644
index 0000000000..dc29cccc9c
--- /dev/null
+++ b/activerecord/test/models/tree.rb
@@ -0,0 +1,3 @@
+class Tree < ActiveRecord::Base
+ has_many :nodes, dependent: :destroy
+end
diff --git a/activerecord/test/models/tuning_peg.rb b/activerecord/test/models/tuning_peg.rb
new file mode 100644
index 0000000000..1252d6dc1d
--- /dev/null
+++ b/activerecord/test/models/tuning_peg.rb
@@ -0,0 +1,4 @@
+class TuningPeg < ActiveRecord::Base
+ belongs_to :guitar
+ validates_numericality_of :pitch
+end
diff --git a/activerecord/test/models/tyre.rb b/activerecord/test/models/tyre.rb
index bc3444aa7d..e50a21ca68 100644
--- a/activerecord/test/models/tyre.rb
+++ b/activerecord/test/models/tyre.rb
@@ -1,3 +1,11 @@
class Tyre < ActiveRecord::Base
belongs_to :car
+
+ def self.custom_find(id)
+ find(id)
+ end
+
+ def self.custom_find_by(*args)
+ find_by(*args)
+ end
end
diff --git a/activerecord/test/models/user.rb b/activerecord/test/models/user.rb
new file mode 100644
index 0000000000..f5dc93e994
--- /dev/null
+++ b/activerecord/test/models/user.rb
@@ -0,0 +1,8 @@
+class User < ActiveRecord::Base
+ has_secure_token
+ has_secure_token :auth_token
+end
+
+class UserWithNotification < User
+ after_create -> { Notification.create! message: "A new user has been created." }
+end
diff --git a/activerecord/test/models/vehicle.rb b/activerecord/test/models/vehicle.rb
new file mode 100644
index 0000000000..ef26170f1f
--- /dev/null
+++ b/activerecord/test/models/vehicle.rb
@@ -0,0 +1,7 @@
+class Vehicle < ActiveRecord::Base
+ self.abstract_class = true
+ default_scope -> { where("tires_count IS NOT NULL") }
+end
+
+class Bus < Vehicle
+end \ No newline at end of file
diff --git a/activerecord/test/schema/mysql2_specific_schema.rb b/activerecord/test/schema/mysql2_specific_schema.rb
index a9a6514c9d..92e0b197a7 100644
--- a/activerecord/test/schema/mysql2_specific_schema.rb
+++ b/activerecord/test/schema/mysql2_specific_schema.rb
@@ -2,7 +2,7 @@ ActiveRecord::Schema.define do
create_table :binary_fields, force: true do |t|
t.binary :var_binary, limit: 255
t.binary :var_binary_large, limit: 4095
- t.column :tiny_blob, 'tinyblob', limit: 255
+ t.blob :tiny_blob, limit: 255
t.binary :normal_blob, limit: 65535
t.binary :medium_blob, limit: 16777215
t.binary :long_blob, limit: 2147483647
@@ -24,6 +24,11 @@ ActiveRecord::Schema.define do
add_index :key_tests, :pizza, :using => :btree, :name => 'index_key_tests_on_pizza'
add_index :key_tests, :snacks, :name => 'index_key_tests_on_snack'
+ create_table :collation_tests, id: false, force: true do |t|
+ t.string :string_cs_column, limit: 1, collation: 'utf8_bin'
+ t.string :string_ci_column, limit: 1, collation: 'utf8_general_ci'
+ end
+
ActiveRecord::Base.connection.execute <<-SQL
DROP PROCEDURE IF EXISTS ten;
SQL
@@ -36,19 +41,17 @@ END
SQL
ActiveRecord::Base.connection.execute <<-SQL
-DROP TABLE IF EXISTS collation_tests;
+DROP PROCEDURE IF EXISTS topics;
SQL
ActiveRecord::Base.connection.execute <<-SQL
-CREATE TABLE collation_tests (
- string_cs_column VARCHAR(1) COLLATE utf8_bin,
- string_ci_column VARCHAR(1) COLLATE utf8_general_ci
-) CHARACTER SET utf8 COLLATE utf8_general_ci
+CREATE PROCEDURE topics(IN num INT) SQL SECURITY INVOKER
+BEGIN
+ select * from topics limit num;
+END
SQL
- ActiveRecord::Base.connection.execute <<-SQL
-DROP TABLE IF EXISTS enum_tests;
-SQL
+ ActiveRecord::Base.connection.drop_table "enum_tests", if_exists: true
ActiveRecord::Base.connection.execute <<-SQL
CREATE TABLE enum_tests (
diff --git a/activerecord/test/schema/mysql_specific_schema.rb b/activerecord/test/schema/mysql_specific_schema.rb
index f2cffca52c..553cb56103 100644
--- a/activerecord/test/schema/mysql_specific_schema.rb
+++ b/activerecord/test/schema/mysql_specific_schema.rb
@@ -2,7 +2,7 @@ ActiveRecord::Schema.define do
create_table :binary_fields, force: true do |t|
t.binary :var_binary, limit: 255
t.binary :var_binary_large, limit: 4095
- t.column :tiny_blob, 'tinyblob', limit: 255
+ t.blob :tiny_blob, limit: 255
t.binary :normal_blob, limit: 65535
t.binary :medium_blob, limit: 16777215
t.binary :long_blob, limit: 2147483647
@@ -24,6 +24,11 @@ ActiveRecord::Schema.define do
add_index :key_tests, :pizza, :using => :btree, :name => 'index_key_tests_on_pizza'
add_index :key_tests, :snacks, :name => 'index_key_tests_on_snack'
+ create_table :collation_tests, id: false, force: true do |t|
+ t.string :string_cs_column, limit: 1, collation: 'utf8_bin'
+ t.string :string_ci_column, limit: 1, collation: 'utf8_general_ci'
+ end
+
ActiveRecord::Base.connection.execute <<-SQL
DROP PROCEDURE IF EXISTS ten;
SQL
@@ -40,26 +45,13 @@ DROP PROCEDURE IF EXISTS topics;
SQL
ActiveRecord::Base.connection.execute <<-SQL
-CREATE PROCEDURE topics() SQL SECURITY INVOKER
+CREATE PROCEDURE topics(IN num INT) SQL SECURITY INVOKER
BEGIN
- select * from topics limit 1;
+ select * from topics limit num;
END
SQL
- ActiveRecord::Base.connection.execute <<-SQL
-DROP TABLE IF EXISTS collation_tests;
-SQL
-
- ActiveRecord::Base.connection.execute <<-SQL
-CREATE TABLE collation_tests (
- string_cs_column VARCHAR(1) COLLATE utf8_bin,
- string_ci_column VARCHAR(1) COLLATE utf8_general_ci
-) CHARACTER SET utf8 COLLATE utf8_general_ci
-SQL
-
- ActiveRecord::Base.connection.execute <<-SQL
-DROP TABLE IF EXISTS enum_tests;
-SQL
+ ActiveRecord::Base.connection.drop_table "enum_tests", if_exists: true
ActiveRecord::Base.connection.execute <<-SQL
CREATE TABLE enum_tests (
diff --git a/activerecord/test/schema/oracle_specific_schema.rb b/activerecord/test/schema/oracle_specific_schema.rb
index a7817772f4..264d9b8910 100644
--- a/activerecord/test/schema/oracle_specific_schema.rb
+++ b/activerecord/test/schema/oracle_specific_schema.rb
@@ -32,10 +32,7 @@ create sequence test_oracle_defaults_seq minvalue 10000
fixed_time date default TO_DATE('2004-01-01 00:00:00', 'YYYY-MM-DD HH24:MI:SS'),
char1 varchar2(1) default 'Y',
char2 varchar2(50) default 'a varchar field',
- char3 clob default 'a text field',
- positive_integer integer default 1,
- negative_integer integer default -1,
- decimal_number number(3,2) default 2.78
+ char3 clob default 'a text field'
)
SQL
execute "create sequence defaults_seq minvalue 10000"
diff --git a/activerecord/test/schema/postgresql_specific_schema.rb b/activerecord/test/schema/postgresql_specific_schema.rb
index e9294a11b9..df0362573b 100644
--- a/activerecord/test/schema/postgresql_specific_schema.rb
+++ b/activerecord/test/schema/postgresql_specific_schema.rb
@@ -1,8 +1,19 @@
ActiveRecord::Schema.define do
- %w(postgresql_tsvectors postgresql_hstores postgresql_arrays postgresql_moneys postgresql_numbers postgresql_times postgresql_network_addresses postgresql_uuids postgresql_ltrees
- postgresql_oids postgresql_xml_data_type defaults geometrics postgresql_timestamp_with_zones postgresql_partitioned_table postgresql_partitioned_table_parent postgresql_json_data_type postgresql_citext).each do |table_name|
- execute "DROP TABLE IF EXISTS #{quote_table_name table_name}"
+ enable_extension!('uuid-ossp', ActiveRecord::Base.connection)
+
+ create_table :uuid_parents, id: :uuid, force: true do |t|
+ t.string :name
+ end
+
+ create_table :uuid_children, id: :uuid, force: true do |t|
+ t.string :name
+ t.uuid :uuid_parent_id
+ end
+
+ %w(postgresql_times postgresql_oids defaults postgresql_timestamp_with_zones
+ postgresql_partitioned_table postgresql_partitioned_table_parent).each do |table_name|
+ drop_table table_name, if_exists: true
end
execute 'DROP SEQUENCE IF EXISTS companies_nonstd_seq CASCADE'
@@ -12,8 +23,6 @@ ActiveRecord::Schema.define do
execute 'DROP FUNCTION IF EXISTS partitioned_insert_trigger()'
- execute "DROP SCHEMA IF EXISTS schema_1 CASCADE"
-
%w(accounts_id_seq developers_id_seq projects_id_seq topics_id_seq customers_id_seq orders_id_seq).each do |seq_name|
execute "SELECT setval('#{seq_name}', 100)"
end
@@ -30,101 +39,13 @@ ActiveRecord::Schema.define do
char1 char(1) default 'Y',
char2 character varying(50) default 'a varchar field',
char3 text default 'a text field',
- positive_integer integer default 1,
- negative_integer integer default -1,
bigint_default bigint default 0::bigint,
- decimal_number decimal(3,2) default 2.78,
multiline_default text DEFAULT '--- []
'::text
);
_SQL
- execute "CREATE SCHEMA schema_1"
- execute "CREATE DOMAIN schema_1.text AS text"
- execute "CREATE DOMAIN schema_1.varchar AS varchar"
- execute "CREATE DOMAIN schema_1.bpchar AS bpchar"
-
- execute <<_SQL
- CREATE TABLE geometrics (
- id serial primary key,
- a_point point,
- -- a_line line, (the line type is currently not implemented in postgresql)
- a_line_segment lseg,
- a_box box,
- a_path path,
- a_polygon polygon,
- a_circle circle
- );
-_SQL
-
- execute <<_SQL
- CREATE TABLE postgresql_arrays (
- id SERIAL PRIMARY KEY,
- commission_by_quarter INTEGER[],
- nicknames TEXT[]
- );
-_SQL
-
- execute <<_SQL
- CREATE TABLE postgresql_uuids (
- id SERIAL PRIMARY KEY,
- guid uuid,
- compact_guid uuid
- );
-_SQL
-
- execute <<_SQL
- CREATE TABLE postgresql_tsvectors (
- id SERIAL PRIMARY KEY,
- text_vector tsvector
- );
-_SQL
-
- if 't' == select_value("select 'hstore'=ANY(select typname from pg_type)")
- execute <<_SQL
- CREATE TABLE postgresql_hstores (
- id SERIAL PRIMARY KEY,
- hash_store hstore default ''::hstore
- );
-_SQL
- end
-
- if 't' == select_value("select 'ltree'=ANY(select typname from pg_type)")
- execute <<_SQL
- CREATE TABLE postgresql_ltrees (
- id SERIAL PRIMARY KEY,
- path ltree
- );
-_SQL
- end
-
- if 't' == select_value("select 'citext'=ANY(select typname from pg_type)")
- execute <<_SQL
- CREATE TABLE postgresql_citext (
- id SERIAL PRIMARY KEY,
- text_citext citext default ''::citext
- );
-_SQL
- end
-
- if 't' == select_value("select 'json'=ANY(select typname from pg_type)")
- execute <<_SQL
- CREATE TABLE postgresql_json_data_type (
- id SERIAL PRIMARY KEY,
- json_data json default '{}'::json
- );
-_SQL
- end
-
- execute <<_SQL
- CREATE TABLE postgresql_numbers (
- id SERIAL PRIMARY KEY,
- single REAL,
- double DOUBLE PRECISION
- );
-_SQL
-
execute <<_SQL
CREATE TABLE postgresql_times (
id SERIAL PRIMARY KEY,
@@ -134,15 +55,6 @@ _SQL
_SQL
execute <<_SQL
- CREATE TABLE postgresql_network_addresses (
- id SERIAL PRIMARY KEY,
- cidr_address CIDR default '192.168.1.0/24',
- inet_address INET default '192.168.1.1',
- mac_address MACADDR default 'ff:ff:ff:ff:ff:ff'
- );
-_SQL
-
- execute <<_SQL
CREATE TABLE postgresql_oids (
id SERIAL PRIMARY KEY,
obj_id OID
@@ -187,19 +99,14 @@ _SQL
end
end
- begin
- execute <<_SQL
- CREATE TABLE postgresql_xml_data_type (
- id SERIAL PRIMARY KEY,
- data xml
- );
-_SQL
- rescue #This version of PostgreSQL either has no XML support or is was not compiled with XML support: skipping table
- end
-
# This table is to verify if the :limit option is being ignored for text and binary columns
create_table :limitless_fields, force: true do |t|
t.binary :binary, limit: 100_000
t.text :text, limit: 100_000
end
+
+ create_table :bigint_array, force: true do |t|
+ t.integer :big_int_data_points, limit: 8, array: true
+ t.decimal :decimal_array_default, array: true, default: [1.23, 3.45]
+ end
end
diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb
index a8b21904ac..66a1f5aa8a 100644
--- a/activerecord/test/schema/schema.rb
+++ b/activerecord/test/schema/schema.rb
@@ -1,5 +1,3 @@
-# encoding: utf-8
-
ActiveRecord::Schema.define do
def except(adapter_names_to_exclude)
unless [adapter_names_to_exclude].flatten.include?(adapter_name)
@@ -7,20 +5,6 @@ ActiveRecord::Schema.define do
end
end
- #put adapter specific setup here
- case adapter_name
- when "PostgreSQL"
- enable_uuid_ossp!(ActiveRecord::Base.connection)
- create_table :uuid_parents, id: :uuid, force: true do |t|
- t.string :name
- end
- create_table :uuid_children, id: :uuid, force: true do |t|
- t.string :name
- t.uuid :uuid_parent_id
- end
- end
-
-
# ------------------------------------------------------------------- #
# #
# Please keep these create table statements in alphabetical order #
@@ -52,6 +36,7 @@ ActiveRecord::Schema.define do
create_table :aircraft, force: true do |t|
t.string :name
+ t.integer :wheels_count, default: 0, null: false
end
create_table :articles, force: true do |t|
@@ -115,6 +100,10 @@ ActiveRecord::Schema.define do
t.column :status, :integer, default: 0
t.column :read_status, :integer, default: 0
t.column :nullable_status, :integer
+ t.column :language, :integer, default: 0
+ t.column :author_visibility, :integer, default: 0
+ t.column :illustrator_visibility, :integer, default: 0
+ t.column :font_size, :integer, default: 0
end
create_table :booleans, force: true do |t|
@@ -138,9 +127,11 @@ ActiveRecord::Schema.define do
t.integer :engines_count
t.integer :wheels_count
t.column :lock_version, :integer, null: false, default: 0
- t.timestamps
+ t.timestamps null: false
end
+ create_table :carriers, force: true
+
create_table :categories, force: true do |t|
t.string :name, null: false
t.string :type
@@ -198,6 +189,7 @@ ActiveRecord::Schema.define do
t.references :author, polymorphic: true
t.string :resource_id
t.string :resource_type
+ t.integer :developer_id
end
create_table :companies, force: true do |t|
@@ -215,6 +207,14 @@ ActiveRecord::Schema.define do
add_index :companies, [:firm_id, :type], name: "company_partial_index", where: "rating > 10"
add_index :companies, :name, name: 'company_name_index', using: :btree
+ create_table :content, force: true do |t|
+ t.string :title
+ end
+
+ create_table :content_positions, force: true do |t|
+ t.integer :content_id
+ end
+
create_table :vegetables, force: true do |t|
t.string :name
t.integer :seller_id
@@ -227,6 +227,11 @@ ActiveRecord::Schema.define do
t.integer :extendedWarranty, null: false
end
+ create_table :computers_developers, id: false, force: true do |t|
+ t.references :computer
+ t.references :developer
+ end
+
create_table :contracts, force: true do |t|
t.integer :developer_id
t.integer :company_id
@@ -241,6 +246,11 @@ ActiveRecord::Schema.define do
t.string :gps_location
end
+ create_table :customer_carriers, force: true do |t|
+ t.references :customer
+ t.references :carrier
+ end
+
create_table :dashboards, force: true, id: false do |t|
t.string :dashboard_id
t.string :name
@@ -248,11 +258,21 @@ ActiveRecord::Schema.define do
create_table :developers, force: true do |t|
t.string :name
+ t.string :first_name
t.integer :salary, default: 70000
- t.datetime :created_at
- t.datetime :updated_at
- t.datetime :created_on
- t.datetime :updated_on
+ t.integer :firm_id
+ t.integer :mentor_id
+ if subsecond_precision_supported?
+ t.datetime :created_at, precision: 6
+ t.datetime :updated_at, precision: 6
+ t.datetime :created_on, precision: 6
+ t.datetime :updated_on, precision: 6
+ else
+ t.datetime :created_at
+ t.datetime :updated_at
+ t.datetime :created_on
+ t.datetime :updated_on
+ end
end
create_table :developers_projects, force: true, id: false do |t|
@@ -275,6 +295,11 @@ ActiveRecord::Schema.define do
t.string :alias
end
+ create_table :doubloons, force: true do |t|
+ t.integer :pirate_id
+ t.integer :weight
+ end
+
create_table :edges, force: true, id: false do |t|
t.column :source_id, :integer, null: false
t.column :sink_id, :integer, null: false
@@ -310,7 +335,7 @@ ActiveRecord::Schema.define do
end
create_table :cold_jokes, force: true do |t|
- t.string :name
+ t.string :cold_name
end
create_table :friendships, force: true do |t|
@@ -331,6 +356,10 @@ ActiveRecord::Schema.define do
t.column :key, :string
end
+ create_table :guitar, force: true do |t|
+ t.string :color
+ end
+
create_table :inept_wizards, force: true do |t|
t.column :name, :string, null: false
t.column :city, :string, null: false
@@ -346,7 +375,11 @@ ActiveRecord::Schema.define do
create_table :invoices, force: true do |t|
t.integer :balance
- t.datetime :updated_at
+ if subsecond_precision_supported?
+ t.datetime :updated_at, precision: 6
+ else
+ t.datetime :updated_at
+ end
end
create_table :iris, force: true do |t|
@@ -432,6 +465,10 @@ ActiveRecord::Schema.define do
t.string :name
end
+ create_table :mentors, force: true do |t|
+ t.string :name
+ end
+
create_table :minivans, force: true, id: false do |t|
t.string :minivan_id
t.string :name
@@ -463,6 +500,10 @@ ActiveRecord::Schema.define do
t.string :name
end
+ create_table :notifications, force: true do |t|
+ t.string :message
+ end
+
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
@@ -473,6 +514,8 @@ ActiveRecord::Schema.define do
# Oracle/SQLServer supports precision up to 38
if current_adapter?(:OracleAdapter, :SQLServerAdapter)
t.decimal :atoms_in_universe, precision: 38, scale: 0
+ elsif current_adapter?(:FbAdapter)
+ t.decimal :atoms_in_universe, precision: 18, scale: 0
else
t.decimal :atoms_in_universe, precision: 55, scale: 0
end
@@ -490,7 +533,11 @@ ActiveRecord::Schema.define do
create_table :owners, primary_key: :owner_id, force: true do |t|
t.string :name
- t.column :updated_at, :datetime
+ if subsecond_precision_supported?
+ t.column :updated_at, :datetime, precision: 6
+ else
+ t.column :updated_at, :datetime
+ end
t.column :happy_at, :datetime
t.string :essay_id
end
@@ -508,10 +555,17 @@ ActiveRecord::Schema.define do
t.column :color, :string
t.column :parrot_sti_class, :string
t.column :killer_id, :integer
- t.column :created_at, :datetime
- t.column :created_on, :datetime
- t.column :updated_at, :datetime
- t.column :updated_on, :datetime
+ if subsecond_precision_supported?
+ t.column :created_at, :datetime, precision: 0
+ t.column :created_on, :datetime, precision: 0
+ t.column :updated_at, :datetime, precision: 0
+ t.column :updated_on, :datetime, precision: 0
+ else
+ t.column :created_at, :datetime
+ t.column :created_on, :datetime
+ t.column :updated_at, :datetime
+ t.column :updated_on, :datetime
+ end
end
create_table :parrots_pirates, id: false, force: true do |t|
@@ -537,7 +591,7 @@ ActiveRecord::Schema.define do
t.references :best_friend_of
t.integer :insures, null: false, default: 0
t.timestamp :born_at
- t.timestamps
+ t.timestamps null: false
end
create_table :peoples_treasures, id: false, force: true do |t|
@@ -545,18 +599,33 @@ ActiveRecord::Schema.define do
t.column :treasure_id, :integer
end
+ create_table :personal_legacy_things, force: true do |t|
+ t.integer :tps_report_number
+ t.integer :person_id
+ t.integer :version, null: false, default: 0
+ end
+
create_table :pets, primary_key: :pet_id, force: true do |t|
t.string :name
t.integer :owner_id, :integer
- t.timestamps
+ if subsecond_precision_supported?
+ t.timestamps null: false, precision: 6
+ else
+ t.timestamps null: false
+ end
end
create_table :pirates, force: true do |t|
t.column :catchphrase, :string
t.column :parrot_id, :integer
t.integer :non_validated_parrot_id
- t.column :created_on, :datetime
- t.column :updated_on, :datetime
+ if subsecond_precision_supported?
+ t.column :created_on, :datetime, precision: 6
+ t.column :updated_on, :datetime, precision: 6
+ else
+ t.column :created_on, :datetime
+ t.column :updated_on, :datetime
+ end
end
create_table :posts, force: true do |t|
@@ -578,6 +647,16 @@ ActiveRecord::Schema.define do
t.integer :tags_with_nullify_count, default: 0
end
+ create_table :serialized_posts, force: true do |t|
+ t.integer :author_id
+ t.string :title, null: false
+ end
+
+ create_table :images, force: true do |t|
+ t.integer :imageable_identifier
+ t.string :imageable_class
+ end
+
create_table :price_estimates, force: true do |t|
t.string :estimate_of_type
t.integer :estimate_of_id
@@ -597,9 +676,21 @@ ActiveRecord::Schema.define do
create_table :projects, force: true do |t|
t.string :name
t.string :type
+ t.integer :firm_id
+ t.integer :mentor_id
+ end
+
+ create_table :randomly_named_table1, force: true do |t|
+ t.string :some_attribute
+ t.integer :another_attribute
+ end
+
+ create_table :randomly_named_table2, force: true do |t|
+ t.string :some_attribute
+ t.integer :another_attribute
end
- create_table :randomly_named_table, force: true do |t|
+ create_table :randomly_named_table3, force: true do |t|
t.string :some_attribute
t.integer :another_attribute
end
@@ -633,7 +724,10 @@ ActiveRecord::Schema.define do
create_table :ships, force: true do |t|
t.string :name
t.integer :pirate_id
+ t.belongs_to :developer
t.integer :update_only_pirate_id
+ # Conventionally named column for counter_cache
+ t.integer :treasures_count, default: 0
t.datetime :created_at
t.datetime :created_on
t.datetime :updated_at
@@ -643,6 +737,20 @@ ActiveRecord::Schema.define do
create_table :ship_parts, force: true do |t|
t.string :name
t.integer :ship_id
+ if subsecond_precision_supported?
+ t.datetime :updated_at, precision: 6
+ else
+ t.datetime :updated_at
+ end
+ end
+
+ create_table :prisoners, force: true do |t|
+ t.belongs_to :ship
+ end
+
+ create_table :shop_accounts, force: true do |t|
+ t.references :customer
+ t.references :customer_carrier
end
create_table :speedometers, force: true, id: false do |t|
@@ -703,8 +811,8 @@ ActiveRecord::Schema.define do
t.string :title, limit: 250
t.string :author_name
t.string :author_email_address
- if mysql_56?
- t.datetime :written_on, limit: 6
+ if subsecond_precision_supported?
+ t.datetime :written_on, precision: 6
else
t.datetime :written_on
end
@@ -726,13 +834,17 @@ ActiveRecord::Schema.define do
t.string :parent_title
t.string :type
t.string :group
- t.timestamps
+ if subsecond_precision_supported?
+ t.timestamps null: true, precision: 6
+ else
+ t.timestamps null: true
+ end
end
create_table :toys, primary_key: :toy_id, force: true do |t|
t.string :name
t.integer :pet_id, :integer
- t.timestamps
+ t.timestamps null: false
end
create_table :traffic_lights, force: true do |t|
@@ -748,6 +860,12 @@ ActiveRecord::Schema.define do
t.column :type, :string
t.column :looter_id, :integer
t.column :looter_type, :string
+ t.belongs_to :ship
+ end
+
+ create_table :tuning_pegs, force: true do |t|
+ t.integer :guitar_id
+ t.float :pitch
end
create_table :tyres, force: true do |t|
@@ -833,6 +951,17 @@ ActiveRecord::Schema.define do
t.string 'from'
end
+ create_table :nodes, force: true do |t|
+ t.integer :tree_id
+ t.integer :parent_id
+ t.string :name
+ t.datetime :updated_at
+ end
+ create_table :trees, force: true do |t|
+ t.string :name
+ t.datetime :updated_at
+ end
+
create_table :hotels, force: true do |t|
end
create_table :departments, force: true do |t|
@@ -847,6 +976,10 @@ ActiveRecord::Schema.define do
t.string :employable_type
t.integer :department_id
end
+ create_table :recipes, force: true do |t|
+ t.integer :chef_id
+ t.integer :hotel_id
+ end
create_table :records, force: true do |t|
end
@@ -870,6 +1003,15 @@ ActiveRecord::Schema.define do
t.string :overloaded_string_with_limit, limit: 255
t.string :string_with_default, default: 'the original default'
end
+
+ create_table :users, force: true do |t|
+ t.string :token
+ t.string :auth_token
+ end
+
+ create_table :test_with_keyword_column_name, force: true do |t|
+ t.string :desc
+ end
end
Course.connection.create_table :courses, force: true do |t|
@@ -880,3 +1022,12 @@ end
College.connection.create_table :colleges, force: true do |t|
t.column :name, :string, null: false
end
+
+Professor.connection.create_table :professors, force: true do |t|
+ t.column :name, :string, null: false
+end
+
+Professor.connection.create_table :courses_professors, id: false, force: true do |t|
+ t.references :course
+ t.references :professor
+end
diff --git a/activerecord/test/support/connection.rb b/activerecord/test/support/connection.rb
index d11fd9cfc1..c5334e8596 100644
--- a/activerecord/test/support/connection.rb
+++ b/activerecord/test/support/connection.rb
@@ -1,6 +1,7 @@
require 'active_support/logger'
require 'models/college'
require 'models/course'
+require 'models/professor'
module ARTest
def self.connection_name
diff --git a/activerecord/test/support/schema_dumping_helper.rb b/activerecord/test/support/schema_dumping_helper.rb
index 2ae8d299e5..2d1651454d 100644
--- a/activerecord/test/support/schema_dumping_helper.rb
+++ b/activerecord/test/support/schema_dumping_helper.rb
@@ -8,4 +8,13 @@ module SchemaDumpingHelper
ensure
ActiveRecord::SchemaDumper.ignore_tables = old_ignore_tables
end
+
+ def dump_all_table_schema(ignore_tables)
+ old_ignore_tables, ActiveRecord::SchemaDumper.ignore_tables = ActiveRecord::SchemaDumper.ignore_tables, ignore_tables
+ stream = StringIO.new
+ ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream)
+ stream.string
+ ensure
+ ActiveRecord::SchemaDumper.ignore_tables = old_ignore_tables
+ end
end
diff --git a/activerecord/test/support/yaml_compatibility_fixtures/rails_4_1.yml b/activerecord/test/support/yaml_compatibility_fixtures/rails_4_1.yml
new file mode 100644
index 0000000000..20b128db9c
--- /dev/null
+++ b/activerecord/test/support/yaml_compatibility_fixtures/rails_4_1.yml
@@ -0,0 +1,22 @@
+--- !ruby/object:Topic
+ attributes:
+ id:
+ title: The First Topic
+ author_name: David
+ author_email_address: david@loudthinking.com
+ written_on: 2003-07-16 14:28:11.223300000 Z
+ bonus_time: 2000-01-01 14:28:00.000000000 Z
+ last_read: 2004-04-15
+ content: |
+ ---
+ :omg: :lol
+ important:
+ approved: false
+ replies_count: 1
+ unique_replies_count: 0
+ parent_id:
+ parent_title:
+ type:
+ group:
+ created_at: 2015-03-10 17:05:42.000000000 Z
+ updated_at: 2015-03-10 17:05:42.000000000 Z
diff --git a/activerecord/test/support/yaml_compatibility_fixtures/rails_4_2_0.yml b/activerecord/test/support/yaml_compatibility_fixtures/rails_4_2_0.yml
new file mode 100644
index 0000000000..b3d3b33141
--- /dev/null
+++ b/activerecord/test/support/yaml_compatibility_fixtures/rails_4_2_0.yml
@@ -0,0 +1,182 @@
+--- !ruby/object:Topic
+raw_attributes:
+ id: 1
+ title: The First Topic
+ author_name: David
+ author_email_address: david@loudthinking.com
+ written_on: '2003-07-16 14:28:11.223300'
+ bonus_time: '2005-01-30 14:28:00.000000'
+ last_read: '2004-04-15'
+ content: |
+ --- Have a nice day
+ ...
+ important:
+ approved: f
+ replies_count: 1
+ unique_replies_count: 0
+ parent_id:
+ parent_title:
+ type:
+ group:
+ created_at: '2015-03-10 17:44:41'
+ updated_at: '2015-03-10 17:44:41'
+attributes: !ruby/object:ActiveRecord::AttributeSet
+ attributes: !ruby/object:ActiveRecord::LazyAttributeHash
+ types:
+ id: &5 !ruby/object:ActiveRecord::Type::Integer
+ precision:
+ scale:
+ limit:
+ range: !ruby/range
+ begin: -2147483648
+ end: 2147483648
+ excl: true
+ title: &6 !ruby/object:ActiveRecord::Type::String
+ precision:
+ scale:
+ limit: 250
+ author_name: &1 !ruby/object:ActiveRecord::Type::String
+ precision:
+ scale:
+ limit:
+ author_email_address: *1
+ written_on: &4 !ruby/object:ActiveRecord::Type::DateTime
+ precision:
+ scale:
+ limit:
+ bonus_time: &7 !ruby/object:ActiveRecord::Type::Time
+ precision:
+ scale:
+ limit:
+ last_read: &8 !ruby/object:ActiveRecord::Type::Date
+ precision:
+ scale:
+ limit:
+ content: !ruby/object:ActiveRecord::Type::Serialized
+ coder: &9 !ruby/object:ActiveRecord::Coders::YAMLColumn
+ object_class: !ruby/class 'Object'
+ subtype: &2 !ruby/object:ActiveRecord::Type::Text
+ precision:
+ scale:
+ limit:
+ important: *2
+ approved: &10 !ruby/object:ActiveRecord::Type::Boolean
+ precision:
+ scale:
+ limit:
+ replies_count: &3 !ruby/object:ActiveRecord::Type::Integer
+ precision:
+ scale:
+ limit:
+ range: !ruby/range
+ begin: -2147483648
+ end: 2147483648
+ excl: true
+ unique_replies_count: *3
+ parent_id: *3
+ parent_title: *1
+ type: *1
+ group: *1
+ created_at: *4
+ updated_at: *4
+ values:
+ id: 1
+ title: The First Topic
+ author_name: David
+ author_email_address: david@loudthinking.com
+ written_on: '2003-07-16 14:28:11.223300'
+ bonus_time: '2005-01-30 14:28:00.000000'
+ last_read: '2004-04-15'
+ content: |
+ --- Have a nice day
+ ...
+ important:
+ approved: f
+ replies_count: 1
+ unique_replies_count: 0
+ parent_id:
+ parent_title:
+ type:
+ group:
+ created_at: '2015-03-10 17:44:41'
+ updated_at: '2015-03-10 17:44:41'
+ additional_types: {}
+ materialized: true
+ delegate_hash:
+ id: !ruby/object:ActiveRecord::Attribute::FromDatabase
+ name: id
+ value_before_type_cast: 1
+ type: *5
+ title: !ruby/object:ActiveRecord::Attribute::FromDatabase
+ name: title
+ value_before_type_cast: The First Topic
+ type: *6
+ author_name: !ruby/object:ActiveRecord::Attribute::FromDatabase
+ name: author_name
+ value_before_type_cast: David
+ type: *1
+ author_email_address: !ruby/object:ActiveRecord::Attribute::FromDatabase
+ name: author_email_address
+ value_before_type_cast: david@loudthinking.com
+ type: *1
+ written_on: !ruby/object:ActiveRecord::Attribute::FromDatabase
+ name: written_on
+ value_before_type_cast: '2003-07-16 14:28:11.223300'
+ type: *4
+ bonus_time: !ruby/object:ActiveRecord::Attribute::FromDatabase
+ name: bonus_time
+ value_before_type_cast: '2005-01-30 14:28:00.000000'
+ type: *7
+ last_read: !ruby/object:ActiveRecord::Attribute::FromDatabase
+ name: last_read
+ value_before_type_cast: '2004-04-15'
+ type: *8
+ content: !ruby/object:ActiveRecord::Attribute::FromDatabase
+ name: content
+ value_before_type_cast: |
+ --- Have a nice day
+ ...
+ type: !ruby/object:ActiveRecord::Type::Serialized
+ coder: *9
+ subtype: *2
+ important: !ruby/object:ActiveRecord::Attribute::FromDatabase
+ name: important
+ value_before_type_cast:
+ type: *2
+ approved: !ruby/object:ActiveRecord::Attribute::FromDatabase
+ name: approved
+ value_before_type_cast: f
+ type: *10
+ replies_count: !ruby/object:ActiveRecord::Attribute::FromDatabase
+ name: replies_count
+ value_before_type_cast: 1
+ type: *3
+ unique_replies_count: !ruby/object:ActiveRecord::Attribute::FromDatabase
+ name: unique_replies_count
+ value_before_type_cast: 0
+ type: *3
+ parent_id: !ruby/object:ActiveRecord::Attribute::FromDatabase
+ name: parent_id
+ value_before_type_cast:
+ type: *3
+ parent_title: !ruby/object:ActiveRecord::Attribute::FromDatabase
+ name: parent_title
+ value_before_type_cast:
+ type: *1
+ type: !ruby/object:ActiveRecord::Attribute::FromDatabase
+ name: type
+ value_before_type_cast:
+ type: *1
+ group: !ruby/object:ActiveRecord::Attribute::FromDatabase
+ name: group
+ value_before_type_cast:
+ type: *1
+ created_at: !ruby/object:ActiveRecord::Attribute::FromDatabase
+ name: created_at
+ value_before_type_cast: '2015-03-10 17:44:41'
+ type: *4
+ updated_at: !ruby/object:ActiveRecord::Attribute::FromDatabase
+ name: updated_at
+ value_before_type_cast: '2015-03-10 17:44:41'
+ type: *4
+new_record: false