aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord
diff options
context:
space:
mode:
Diffstat (limited to 'activerecord')
-rw-r--r--activerecord/CHANGELOG.md549
-rw-r--r--activerecord/README.rdoc2
-rw-r--r--activerecord/Rakefile17
-rw-r--r--activerecord/lib/active_record.rb1
-rw-r--r--activerecord/lib/active_record/aggregations.rb24
-rw-r--r--activerecord/lib/active_record/associations.rb385
-rw-r--r--activerecord/lib/active_record/associations/alias_tracker.rb3
-rw-r--r--activerecord/lib/active_record/associations/association.rb17
-rw-r--r--activerecord/lib/active_record/associations/association_scope.rb2
-rw-r--r--activerecord/lib/active_record/associations/belongs_to_association.rb46
-rw-r--r--activerecord/lib/active_record/associations/builder/association.rb2
-rw-r--r--activerecord/lib/active_record/associations/builder/belongs_to.rb5
-rw-r--r--activerecord/lib/active_record/associations/builder/collection_association.rb2
-rw-r--r--activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb20
-rw-r--r--activerecord/lib/active_record/associations/builder/has_many.rb4
-rw-r--r--activerecord/lib/active_record/associations/builder/has_one.rb2
-rw-r--r--activerecord/lib/active_record/associations/builder/singular_association.rb2
-rw-r--r--activerecord/lib/active_record/associations/collection_association.rb15
-rw-r--r--activerecord/lib/active_record/associations/collection_proxy.rb65
-rw-r--r--activerecord/lib/active_record/associations/foreign_association.rb2
-rw-r--r--activerecord/lib/active_record/associations/has_many_association.rb72
-rw-r--r--activerecord/lib/active_record/associations/has_many_through_association.rb2
-rw-r--r--activerecord/lib/active_record/associations/has_one_association.rb6
-rw-r--r--activerecord/lib/active_record/associations/join_dependency.rb33
-rw-r--r--activerecord/lib/active_record/associations/preloader.rb17
-rw-r--r--activerecord/lib/active_record/associations/preloader/association.rb87
-rw-r--r--activerecord/lib/active_record/associations/preloader/collection_association.rb6
-rw-r--r--activerecord/lib/active_record/associations/preloader/has_one.rb8
-rw-r--r--activerecord/lib/active_record/associations/preloader/through_association.rb4
-rw-r--r--activerecord/lib/active_record/associations/singular_association.rb9
-rw-r--r--activerecord/lib/active_record/associations/through_association.rb12
-rw-r--r--activerecord/lib/active_record/attribute.rb55
-rw-r--r--activerecord/lib/active_record/attribute/user_provided_default.rb15
-rw-r--r--activerecord/lib/active_record/attribute_assignment.rb2
-rw-r--r--activerecord/lib/active_record/attribute_methods.rb48
-rw-r--r--activerecord/lib/active_record/attribute_methods/before_type_cast.rb2
-rw-r--r--activerecord/lib/active_record/attribute_methods/dirty.rb127
-rw-r--r--activerecord/lib/active_record/attribute_methods/primary_key.rb4
-rw-r--r--activerecord/lib/active_record/attribute_methods/query.rb2
-rw-r--r--activerecord/lib/active_record/attribute_methods/read.rb4
-rw-r--r--activerecord/lib/active_record/attribute_methods/serialization.rb6
-rw-r--r--activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb2
-rw-r--r--activerecord/lib/active_record/attribute_methods/write.rb2
-rw-r--r--activerecord/lib/active_record/attribute_mutation_tracker.rb70
-rw-r--r--activerecord/lib/active_record/attribute_set.rb17
-rw-r--r--activerecord/lib/active_record/attribute_set/builder.rb40
-rw-r--r--activerecord/lib/active_record/attributes.rb13
-rw-r--r--activerecord/lib/active_record/autosave_association.rb27
-rw-r--r--activerecord/lib/active_record/base.rb41
-rw-r--r--activerecord/lib/active_record/callbacks.rb47
-rw-r--r--activerecord/lib/active_record/coders/yaml_column.rb17
-rw-r--r--activerecord/lib/active_record/collection_cache_key.rb31
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb87
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb4
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb33
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/quoting.rb8
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb40
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb154
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb4
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb210
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_adapter.rb37
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb447
-rw-r--r--activerecord/lib/active_record/connection_adapters/column.rb11
-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.rb59
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb91
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql_adapter.rb33
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb6
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb18
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb27
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb5
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb24
-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.rb105
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb121
-rw-r--r--activerecord/lib/active_record/connection_adapters/schema_cache.rb51
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb84
-rw-r--r--activerecord/lib/active_record/connection_adapters/statement_pool.rb5
-rw-r--r--activerecord/lib/active_record/connection_handling.rb6
-rw-r--r--activerecord/lib/active_record/core.rb16
-rw-r--r--activerecord/lib/active_record/counter_cache.rb12
-rw-r--r--activerecord/lib/active_record/enum.rb81
-rw-r--r--activerecord/lib/active_record/errors.rb97
-rw-r--r--activerecord/lib/active_record/explain_registry.rb2
-rw-r--r--activerecord/lib/active_record/explain_subscriber.rb2
-rw-r--r--activerecord/lib/active_record/fixture_set/file.rb23
-rw-r--r--activerecord/lib/active_record/fixtures.rb51
-rw-r--r--activerecord/lib/active_record/inheritance.rb40
-rw-r--r--activerecord/lib/active_record/integration.rb8
-rw-r--r--activerecord/lib/active_record/locking/optimistic.rb4
-rw-r--r--activerecord/lib/active_record/locking/pessimistic.rb2
-rw-r--r--activerecord/lib/active_record/log_subscriber.rb34
-rw-r--r--activerecord/lib/active_record/migration.rb333
-rw-r--r--activerecord/lib/active_record/migration/command_recorder.rb41
-rw-r--r--activerecord/lib/active_record/model_schema.rb18
-rw-r--r--activerecord/lib/active_record/null_relation.rb2
-rw-r--r--activerecord/lib/active_record/persistence.rb60
-rw-r--r--activerecord/lib/active_record/querying.rb4
-rw-r--r--activerecord/lib/active_record/railtie.rb14
-rw-r--r--activerecord/lib/active_record/railties/databases.rake34
-rw-r--r--activerecord/lib/active_record/reflection.rb126
-rw-r--r--activerecord/lib/active_record/relation.rb143
-rw-r--r--activerecord/lib/active_record/relation/batches.rb102
-rw-r--r--activerecord/lib/active_record/relation/batches/batch_enumerator.rb67
-rw-r--r--activerecord/lib/active_record/relation/calculations.rb97
-rw-r--r--activerecord/lib/active_record/relation/delegation.rb6
-rw-r--r--activerecord/lib/active_record/relation/finder_methods.rb40
-rw-r--r--activerecord/lib/active_record/relation/from_clause.rb2
-rw-r--r--activerecord/lib/active_record/relation/merger.rb12
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder.rb6
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb4
-rw-r--r--activerecord/lib/active_record/relation/query_attribute.rb2
-rw-r--r--activerecord/lib/active_record/relation/query_methods.rb180
-rw-r--r--activerecord/lib/active_record/relation/record_fetch_warning.rb2
-rw-r--r--activerecord/lib/active_record/relation/spawn_methods.rb12
-rw-r--r--activerecord/lib/active_record/relation/where_clause_factory.rb7
-rw-r--r--activerecord/lib/active_record/result.rb5
-rw-r--r--activerecord/lib/active_record/runtime_registry.rb2
-rw-r--r--activerecord/lib/active_record/sanitization.rb100
-rw-r--r--activerecord/lib/active_record/schema.rb43
-rw-r--r--activerecord/lib/active_record/schema_dumper.rb23
-rw-r--r--activerecord/lib/active_record/schema_migration.rb8
-rw-r--r--activerecord/lib/active_record/scoping.rb8
-rw-r--r--activerecord/lib/active_record/scoping/default.rb21
-rw-r--r--activerecord/lib/active_record/scoping/named.rb33
-rw-r--r--activerecord/lib/active_record/secure_token.rb8
-rw-r--r--activerecord/lib/active_record/serialization.rb4
-rw-r--r--activerecord/lib/active_record/serializers/xml_serializer.rb193
-rw-r--r--activerecord/lib/active_record/statement_cache.rb8
-rw-r--r--activerecord/lib/active_record/store.rb5
-rw-r--r--activerecord/lib/active_record/table_metadata.rb12
-rw-r--r--activerecord/lib/active_record/tasks/database_tasks.rb32
-rw-r--r--activerecord/lib/active_record/tasks/mysql_database_tasks.rb37
-rw-r--r--activerecord/lib/active_record/tasks/postgresql_database_tasks.rb28
-rw-r--r--activerecord/lib/active_record/tasks/sqlite_database_tasks.rb6
-rw-r--r--activerecord/lib/active_record/timestamp.rb19
-rw-r--r--activerecord/lib/active_record/transactions.rb62
-rw-r--r--activerecord/lib/active_record/type.rb44
-rw-r--r--activerecord/lib/active_record/type/adapter_specific_registry.rb36
-rw-r--r--activerecord/lib/active_record/type/big_integer.rb13
-rw-r--r--activerecord/lib/active_record/type/binary.rb50
-rw-r--r--activerecord/lib/active_record/type/boolean.rb19
-rw-r--r--activerecord/lib/active_record/type/date.rb46
-rw-r--r--activerecord/lib/active_record/type/date_time.rb41
-rw-r--r--activerecord/lib/active_record/type/decimal.rb48
-rw-r--r--activerecord/lib/active_record/type/decimal_without_scale.rb11
-rw-r--r--activerecord/lib/active_record/type/float.rb25
-rw-r--r--activerecord/lib/active_record/type/helpers.rb4
-rw-r--r--activerecord/lib/active_record/type/helpers/accepts_multiparameter_time.rb30
-rw-r--r--activerecord/lib/active_record/type/helpers/mutable.rb18
-rw-r--r--activerecord/lib/active_record/type/helpers/numeric.rb34
-rw-r--r--activerecord/lib/active_record/type/helpers/time_value.rb58
-rw-r--r--activerecord/lib/active_record/type/integer.rb66
-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/serialized.rb10
-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.rb40
-rw-r--r--activerecord/lib/active_record/type/type_map.rb8
-rw-r--r--activerecord/lib/active_record/type/unsigned_integer.rb15
-rw-r--r--activerecord/lib/active_record/type/value.rb104
-rw-r--r--activerecord/lib/active_record/type_caster.rb2
-rw-r--r--activerecord/lib/active_record/type_caster/connection.rb4
-rw-r--r--activerecord/lib/active_record/type_caster/map.rb2
-rw-r--r--activerecord/lib/active_record/validations.rb47
-rw-r--r--activerecord/lib/active_record/validations/associated.rb13
-rw-r--r--activerecord/lib/active_record/validations/presence.rb7
-rw-r--r--activerecord/lib/active_record/validations/uniqueness.rb17
-rw-r--r--activerecord/lib/rails/generators/active_record/migration.rb7
-rw-r--r--activerecord/lib/rails/generators/active_record/migration/migration_generator.rb2
-rw-r--r--activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb2
-rw-r--r--activerecord/lib/rails/generators/active_record/model/model_generator.rb11
-rw-r--r--activerecord/test/active_record/connection_adapters/fake_adapter.rb6
-rw-r--r--activerecord/test/cases/adapter_test.rb58
-rw-r--r--activerecord/test/cases/adapters/mysql/active_schema_test.rb28
-rw-r--r--activerecord/test/cases/adapters/mysql/connection_test.rb41
-rw-r--r--activerecord/test/cases/adapters/mysql/explain_test.rb21
-rw-r--r--activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb37
-rw-r--r--activerecord/test/cases/adapters/mysql/quoting_test.rb6
-rw-r--r--activerecord/test/cases/adapters/mysql/schema_test.rb50
-rw-r--r--activerecord/test/cases/adapters/mysql/sp_test.rb29
-rw-r--r--activerecord/test/cases/adapters/mysql/sql_types_test.rb2
-rw-r--r--activerecord/test/cases/adapters/mysql/statement_pool_test.rb2
-rw-r--r--activerecord/test/cases/adapters/mysql/unsigned_type_test.rb39
-rw-r--r--activerecord/test/cases/adapters/mysql2/active_schema_test.rb28
-rw-r--r--activerecord/test/cases/adapters/mysql2/connection_test.rb28
-rw-r--r--activerecord/test/cases/adapters/mysql2/json_test.rb172
-rw-r--r--activerecord/test/cases/adapters/mysql2/quoting_test.rb6
-rw-r--r--activerecord/test/cases/adapters/mysql2/schema_test.rb48
-rw-r--r--activerecord/test/cases/adapters/mysql2/sp_test.rb30
-rw-r--r--activerecord/test/cases/adapters/mysql2/sql_types_test.rb2
-rw-r--r--activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb39
-rw-r--r--activerecord/test/cases/adapters/postgresql/active_schema_test.rb18
-rw-r--r--activerecord/test/cases/adapters/postgresql/connection_test.rb48
-rw-r--r--activerecord/test/cases/adapters/postgresql/json_test.rb1
-rw-r--r--activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb14
-rw-r--r--activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb2
-rw-r--r--activerecord/test/cases/adapters/postgresql/schema_test.rb119
-rw-r--r--activerecord/test/cases/adapters/postgresql/view_test.rb64
-rw-r--r--activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb34
-rw-r--r--activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb2
-rw-r--r--activerecord/test/cases/associations/belongs_to_associations_test.rb28
-rw-r--r--activerecord/test/cases/associations/bidirectional_destroy_dependencies_test.rb41
-rw-r--r--activerecord/test/cases/associations/eager_test.rb102
-rw-r--r--activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb65
-rw-r--r--activerecord/test/cases/associations/has_many_associations_test.rb117
-rw-r--r--activerecord/test/cases/associations/has_many_through_associations_test.rb44
-rw-r--r--activerecord/test/cases/associations/has_one_associations_test.rb26
-rw-r--r--activerecord/test/cases/associations/has_one_through_associations_test.rb34
-rw-r--r--activerecord/test/cases/associations/inverse_associations_test.rb21
-rw-r--r--activerecord/test/cases/associations/left_outer_join_association_test.rb79
-rw-r--r--activerecord/test/cases/associations/nested_through_associations_test.rb2
-rw-r--r--activerecord/test/cases/associations_test.rb2
-rw-r--r--activerecord/test/cases/attribute_methods_test.rb13
-rw-r--r--activerecord/test/cases/attribute_set_test.rb44
-rw-r--r--activerecord/test/cases/attribute_test.rb60
-rw-r--r--activerecord/test/cases/attributes_test.rb17
-rw-r--r--activerecord/test/cases/autosave_association_test.rb36
-rw-r--r--activerecord/test/cases/base_test.rb128
-rw-r--r--activerecord/test/cases/batches_test.rb259
-rw-r--r--activerecord/test/cases/cache_key_test.rb25
-rw-r--r--activerecord/test/cases/calculations_test.rb108
-rw-r--r--activerecord/test/cases/collection_cache_key_test.rb70
-rw-r--r--activerecord/test/cases/connection_adapters/mysql_type_lookup_test.rb4
-rw-r--r--activerecord/test/cases/connection_adapters/schema_cache_test.rb11
-rw-r--r--activerecord/test/cases/connection_management_test.rb9
-rw-r--r--activerecord/test/cases/connection_pool_test.rb2
-rw-r--r--activerecord/test/cases/dirty_test.rb21
-rw-r--r--activerecord/test/cases/enum_test.rb26
-rw-r--r--activerecord/test/cases/errors_test.rb16
-rw-r--r--activerecord/test/cases/explain_test.rb47
-rw-r--r--activerecord/test/cases/finder_test.rb23
-rw-r--r--activerecord/test/cases/fixture_set/file_test.rb12
-rw-r--r--activerecord/test/cases/fixtures_test.rb80
-rw-r--r--activerecord/test/cases/helper.rb8
-rw-r--r--activerecord/test/cases/inheritance_test.rb81
-rw-r--r--activerecord/test/cases/integration_test.rb23
-rw-r--r--activerecord/test/cases/invertible_migration_test.rb119
-rw-r--r--activerecord/test/cases/log_subscriber_test.rb100
-rw-r--r--activerecord/test/cases/migration/change_schema_test.rb4
-rw-r--r--activerecord/test/cases/migration/change_table_test.rb1
-rw-r--r--activerecord/test/cases/migration/command_recorder_test.rb3
-rw-r--r--activerecord/test/cases/migration/create_join_table_test.rb24
-rw-r--r--activerecord/test/cases/migration/helper.rb2
-rw-r--r--activerecord/test/cases/migration/pending_migrations_test.rb1
-rw-r--r--activerecord/test/cases/migration/postgresql_geometric_types_test.rb93
-rw-r--r--activerecord/test/cases/migration/references_foreign_key_test.rb19
-rw-r--r--activerecord/test/cases/migration/rename_table_test.rb4
-rw-r--r--activerecord/test/cases/migration_test.rb114
-rw-r--r--activerecord/test/cases/migrator_test.rb4
-rw-r--r--activerecord/test/cases/nested_attributes_test.rb36
-rw-r--r--activerecord/test/cases/persistence_test.rb28
-rw-r--r--activerecord/test/cases/pooled_connections_test.rb2
-rw-r--r--activerecord/test/cases/primary_keys_test.rb58
-rw-r--r--activerecord/test/cases/query_cache_test.rb69
-rw-r--r--activerecord/test/cases/readonly_test.rb1
-rw-r--r--activerecord/test/cases/reflection_test.rb40
-rw-r--r--activerecord/test/cases/relation/mutation_test.rb13
-rw-r--r--activerecord/test/cases/relation/where_test.rb30
-rw-r--r--activerecord/test/cases/relation_test.rb51
-rw-r--r--activerecord/test/cases/relations_test.rb24
-rw-r--r--activerecord/test/cases/sanitize_test.rb8
-rw-r--r--activerecord/test/cases/schema_dumper_test.rb34
-rw-r--r--activerecord/test/cases/scoping/default_scoping_test.rb6
-rw-r--r--activerecord/test/cases/scoping/named_scoping_test.rb10
-rw-r--r--activerecord/test/cases/serialization_test.rb2
-rw-r--r--activerecord/test/cases/tasks/database_tasks_test.rb4
-rw-r--r--activerecord/test/cases/tasks/mysql_rake_test.rb35
-rw-r--r--activerecord/test/cases/tasks/postgresql_rake_test.rb14
-rw-r--r--activerecord/test/cases/tasks/sqlite_rake_test.rb2
-rw-r--r--activerecord/test/cases/test_case.rb2
-rw-r--r--activerecord/test/cases/test_fixtures_test.rb2
-rw-r--r--activerecord/test/cases/timestamp_test.rb20
-rw-r--r--activerecord/test/cases/touch_later_test.rb6
-rw-r--r--activerecord/test/cases/transactions_test.rb16
-rw-r--r--activerecord/test/cases/type/date_time_test.rb14
-rw-r--r--activerecord/test/cases/type/decimal_test.rb51
-rw-r--r--activerecord/test/cases/type/integer_test.rb100
-rw-r--r--activerecord/test/cases/type/string_test.rb14
-rw-r--r--activerecord/test/cases/type/unsigned_integer_test.rb17
-rw-r--r--activerecord/test/cases/types_test.rb105
-rw-r--r--activerecord/test/cases/validations/association_validation_test.rb12
-rw-r--r--activerecord/test/cases/validations/i18n_validation_test.rb10
-rw-r--r--activerecord/test/cases/validations/length_validation_test.rb1
-rw-r--r--activerecord/test/cases/validations/uniqueness_validation_test.rb42
-rw-r--r--activerecord/test/cases/validations_test.rb18
-rw-r--r--activerecord/test/cases/view_test.rb111
-rw-r--r--activerecord/test/cases/xml_serialization_test.rb447
-rw-r--r--activerecord/test/fixtures/bad_posts.yml9
-rw-r--r--activerecord/test/fixtures/content.yml3
-rw-r--r--activerecord/test/fixtures/content_positions.yml3
-rw-r--r--activerecord/test/fixtures/naked/yml/parrots.yml2
-rw-r--r--activerecord/test/fixtures/other_comments.yml6
-rw-r--r--activerecord/test/fixtures/other_posts.yml7
-rw-r--r--activerecord/test/models/author.rb3
-rw-r--r--activerecord/test/models/book.rb8
-rw-r--r--activerecord/test/models/bulb.rb1
-rw-r--r--activerecord/test/models/car.rb1
-rw-r--r--activerecord/test/models/carrier.rb2
-rw-r--r--activerecord/test/models/categorization.rb2
-rw-r--r--activerecord/test/models/company.rb4
-rw-r--r--activerecord/test/models/contact.rb2
-rw-r--r--activerecord/test/models/content.rb40
-rw-r--r--activerecord/test/models/customer_carrier.rb14
-rw-r--r--activerecord/test/models/developer.rb11
-rw-r--r--activerecord/test/models/face.rb2
-rw-r--r--activerecord/test/models/guitar.rb4
-rw-r--r--activerecord/test/models/member.rb3
-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/parrot.rb6
-rw-r--r--activerecord/test/models/person.rb1
-rw-r--r--activerecord/test/models/post.rb15
-rw-r--r--activerecord/test/models/professor.rb5
-rw-r--r--activerecord/test/models/project.rb11
-rw-r--r--activerecord/test/models/ship.rb1
-rw-r--r--activerecord/test/models/shop_account.rb6
-rw-r--r--activerecord/test/models/topic.rb2
-rw-r--r--activerecord/test/models/treasure.rb1
-rw-r--r--activerecord/test/models/tuning_peg.rb4
-rw-r--r--activerecord/test/models/vehicle.rb7
-rw-r--r--activerecord/test/schema/mysql2_specific_schema.rb13
-rw-r--r--activerecord/test/schema/mysql_specific_schema.rb6
-rw-r--r--activerecord/test/schema/schema.rb128
-rw-r--r--activerecord/test/support/connection.rb1
-rw-r--r--activerecord/test/support/schema_dumping_helper.rb2
331 files changed, 7589 insertions, 4512 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md
index fb64156b78..2461dd517b 100644
--- a/activerecord/CHANGELOG.md
+++ b/activerecord/CHANGELOG.md
@@ -1,7 +1,528 @@
-* ActiveRecord::RecordNotFound modified to store model name, primary_key and
+* Except keys of `build_record`'s argument from `create_scope` in `initialize_attributes`.
+
+ Fixes #21893.
+
+ *Yuichiro Kaneko*
+
+* Deprecate `connection.tables` on the SQLite3 and MySQL adapters.
+ Also deprecate passing arguments to `#tables`.
+ And deprecate `table_exists?`.
+
+ The `#tables` method of some adapters (mysql, mysql2, sqlite3) would return
+ both tables and views while others (postgresql) just return tables. To make
+ their behavior consistent, `#tables` will return only tables in the future.
+
+ The `#table_exists?` method would check both tables and views. To make
+ their behavior consistent with `#tables`, `#table_exists?` will check only
+ tables in the future.
+
+ *Yuichiro Kaneko*
+
+* Improve support for non Active Record objects on `validates_associated`
+
+ Skipping `marked_for_destruction?` when the associated object does not responds
+ to it make easier to validate virtual associations built on top of Active Model
+ objects and/or serialized objects that implement a `valid?` instance method.
+
+ *Kassio Borges*, *Lucas Mazza*
+
+* Change connection management middleware to return a new response with
+ a body proxy, rather than mutating the original.
+
+ *Kevin Buchanan*
+
+* Make `db:migrate:status` to render `1_some.rb` format migrate files.
+
+ These files are in `db/migrate`:
+
+ * 1_valid_people_have_last_names.rb
+ * 20150819202140_irreversible_migration.rb
+ * 20150823202140_add_admin_flag_to_users.rb
+ * 20150823202141_migration_tests.rb
+ * 2_we_need_reminders.rb
+ * 3_innocent_jointable.rb
+
+ Before:
+
+ $ bundle exec rake db:migrate:status
+ ...
+
+ Status Migration ID Migration Name
+ --------------------------------------------------
+ up 001 ********** NO FILE **********
+ up 002 ********** NO FILE **********
+ up 003 ********** NO FILE **********
+ up 20150819202140 Irreversible migration
+ up 20150823202140 Add admin flag to users
+ up 20150823202141 Migration tests
+
+ After:
+
+ $ bundle exec rake db:migrate:status
+ ...
+
+ Status Migration ID Migration Name
+ --------------------------------------------------
+ up 001 Valid people have last names
+ up 002 We need reminders
+ up 003 Innocent jointable
+ up 20150819202140 Irreversible migration
+ up 20150823202140 Add admin flag to users
+ up 20150823202141 Migration tests
+
+ *Yuichiro Kaneko*
+
+* Define `ActiveRecord::Sanitization.sanitize_sql_for_order` and use it inside
+ `preprocess_order_args`.
+
+ *Yuichiro Kaneko*
+
+* Allow bigint with default nil for avoiding auto increment primary key.
+
+ *Ryuta Kamizono*
+
+* Remove `DEFAULT_CHARSET` and `DEFAULT_COLLATION` in `MySQLDatabaseTasks`.
+
+ We should omit the collation entirely rather than providing a default.
+ Then the choice is the responsibility of the server and MySQL distribution.
+
+ *Ryuta Kamizono*
+
+* Alias `ActiveRecord::Relation#left_joins` to
+ `ActiveRecord::Relation#left_outer_joins`.
+
+ *Takashi Kokubun*
+
+* Use advisory locking to raise a `ConcurrentMigrationError` instead of
+ attempting to migrate when another migration is currently running.
+
+ *Sam Davies*
+
+* Added `ActiveRecord::Relation#left_outer_joins`.
+
+ Example:
+
+ User.left_outer_joins(:posts)
+ # => SELECT "users".* FROM "users" LEFT OUTER JOIN "posts" ON
+ "posts"."user_id" = "users"."id"
+
+ *Florian Thomas*
+
+* Support passing an array to `order` for SQL parameter sanitization.
+
+ *Aaron Suggs*
+
+* Avoid disabling errors on the PostgreSQL connection when enabling the
+ `standard_conforming_strings` setting. Errors were previously disabled because
+ the setting wasn't writable in Postgres 8.1 and didn't exist in earlier
+ versions. Now Rails only supports Postgres 8.2+ we're fine to assume the
+ setting exists. Disabling errors caused problems when using a connection
+ pooling tool like PgBouncer because it's not guaranteed to have the same
+ connection between calls to `execute` and it could leave the connection
+ with errors disabled.
+
+ Fixes #22101.
+
+ *Harry Marr*
+
+* Set `scope.reordering_value` to `true` if `:reordering`-values are specified.
+
+ Fixes #21886.
+
+ *Hiroaki Izu*
+
+* Add support for bidirectional destroy dependencies.
+
+ Fixes #13609.
+
+ Example:
+
+ class Content < ActiveRecord::Base
+ has_one :position, dependent: :destroy
+ end
+
+ class Position < ActiveRecord::Base
+ belongs_to :content, dependent: :destroy
+ end
+
+ *Seb Jacobs*
+
+* 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.
+
+ Fixes #16032.
+
+ Examples:
+
+ before:
+
+ Project.first.salaried_developers.size # => 3
+ Project.includes(:salaried_developers).first.salaried_developers.size # => 1
+
+ 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:
+
+ class Guitar < ActiveRecord::Base
+ has_many :tuning_pegs
+ accepts_nested_attributes_for :tuning_pegs
+ end
+
+ class TuningPeg < ActiveRecord::Base
+ belongs_to :guitar
+ validates_numericality_of :pitch
+ end
+
+ # Old style
+ guitar.errors["tuning_pegs.pitch"] = ["is not a number"]
+
+ # New style (if defined globally, or set in has_many_relationship)
+ guitar.errors["tuning_pegs[1].pitch"] = ["is not a number"]
+
+ *Michael Probber*, *Terence Sun*
+
+* Exit with non-zero status for failed database rake tasks.
+
+ *Jay Hayes*
+
+* 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.
+
+ *Rafael Sales*
+
+* Add ability to default to `uuid` as primary key when generating database migrations.
+
+ Example:
+
+ config.generators do |g|
+ g.orm :active_record, primary_key_type: :uuid
+ end
+
+ *Jon McCartie*
+
+* Don't cache arguments in `#find_by` if they are an `ActiveRecord::Relation`.
+
+ Fixes #20817
+
+ *Hiroaki Izu*
+
+* Qualify column name inserted by `group` in calculation.
+
+ 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.
+
+ *Soutaro Matsumoto*
+
+* 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.
+
+ *Sean Griffin*
+
+* Fix `rewhere` in a `has_many` association.
+
+ Fixes #21955.
+
+ *Josh Branchaud*, *Kal*
+
+* `where` raises ArgumentError on unsupported types.
+
+ Fixes #20473.
+
+ *Jake Worth*
+
+* Add an immutable string type to help reduce memory usage for apps which do
+ not need mutation detection on strings.
+
+ *Sean Griffin*
+
+* Give `AcriveRecord::Relation#update` its own deprecation warning when
+ passed an `ActiveRecord::Base` instance.
+
+ Fixes #21945.
+
+ *Ted Johansson*
+
+* Make it possible to pass `:to_table` when adding a foreign key through
+ `add_reference`.
+
+ Fixes #21563.
+
+ *Yves Senn*
+
+* No longer pass deprecated option `-i` to `pg_dump`.
+
+ *Paul Sadauskas*
+
+* Concurrent `AR::Base#increment!` and `#decrement!` on the same record
+ are all reflected in the database rather than overwriting each other.
+
+ *Bogdan Gusiev*
+
+* Avoid leaking the first relation we call `first` on, per model.
+
+ Fixes #21921.
+
+ *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*
+
+* Correctly apply `unscope` when preloading through associations.
+
+ *Jimmy Bourassa*
+
+* Fixed taking precision into count when assigning a value to timestamp attribute.
+
+ 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.
+
+
+ m = Model.create!
+ m.created_at.usec == m.reload.created_at.usec # => false
+ # due to different precision in Time.now and database column
+
+ 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.
+
+ *Bogdan Gusiev*
+
+* 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).
+
+ Also deprecate `SchemaCache#tables`, `SchemaCache#table_exists?` and
+ `SchemaCache#clear_table_cache!` in favor of their new data source
+ counterparts.
+
+ *Yves Senn*, *Matthew Draper*
+
+* Add `ActiveRecord::Base.ignored_columns` to make some columns
+ invisible from Active Record.
+
+ *Jean Boussier*
+
+* `ActiveRecord::Tasks::MySQLDatabaseTasks` fails if shellout to
+ mysql commands (like `mysqldump`) is not successful.
+
+ *Steve Mitchell*
+
+* Ensure `select` quotes aliased attributes, even when using `from`.
+
+ 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:
+
+ 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.
+
+ See http://www.postgresql.org/docs/9.4/static/sql-dropindex.html for more
+ details.
+
+ *Grey Baker*
+
+* 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:
+
+ create_schema("my.schema")
+ # CREATE SCHEMA "my.schema";
+
+ *Yves Senn*
+
+* 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.
+
+ *Yves Senn*
+
+* Only try to nullify has_one target association if the record is persisted.
+
+ 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*
+
+* Descriptive error message when fixtures contain a missing column.
+
+ Fixes #21201.
+
+ *Yves Senn*
+
+* `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:
+
+ @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 #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*
+
+* `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. For example consider this simple
- example:
+ a better decision to what to do with it.
+
+ Example:
class SomeAbstractController < ActionController::Base
rescue_from ActiveRecord::RecordNotFound, with: :redirect_to_404
@@ -51,12 +572,12 @@
*Sean Griffin*
-* Fix a bug where counter_cache doesn't always work with polymorphic
+* Fix a bug where counter_cache doesn't always work with polymorphic
relations.
Fixes #16407.
- *Stefan Kanev & Sean Griffin*
+ *Stefan Kanev*, *Sean Griffin*
* Ensure that cyclic associations with autosave don't cause duplicate errors
to be added to the parent record.
@@ -170,7 +691,7 @@
*Aster Ryan*
-* Add `:enum_prefix`/`:enum_suffix` option to `enum` definition.
+* Add `:_prefix` and `:_suffix` options to `enum` definition.
Fixes #17511, #17415.
@@ -196,14 +717,12 @@
* Do not set `sql_mode` if `strict: :default` is specified.
- ```
- # database.yml
+ # config/database.yml
production:
adapter: mysql2
database: foo_prod
user: foo
strict: :default
- ```
*Ryuta Kamizono*
@@ -289,6 +808,10 @@
*Ryuta Kamizono*
+* Remove `ActiveRecord::Serialization::XmlSerializer` from core.
+
+ *Zachary Scott*
+
* Make `unscope` aware of "less than" and "greater than" conditions.
*TAKAHASHI Kazuaki*
@@ -876,7 +1399,7 @@
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
- `config.active_support.halt_callback_chains_on_return_false` option, will
+ `ActiveSupport.halt_callback_chains_on_return_false` option, will
either not work at all or display a deprecation warning.
*claudiob*
@@ -983,7 +1506,7 @@
* `eager_load` preserves readonly flag for associations.
- Closes #15853.
+ Fixes #15853.
*Takashi Kokubun*
@@ -1039,7 +1562,7 @@
* Fix bug with `ActiveRecord::Type::Numeric` that caused negative values to
be marked as having changed when set to the same negative value.
- Closes #18161.
+ Fixes #18161.
*Daniel Fox*
@@ -1054,7 +1577,7 @@
before loading the schema. This is left for the user to do.
`db:test:prepare` will still purge the database.
- Closes #17945.
+ Fixes #17945.
*Yves Senn*
diff --git a/activerecord/README.rdoc b/activerecord/README.rdoc
index 049c5d2b3b..3eac8cc422 100644
--- a/activerecord/README.rdoc
+++ b/activerecord/README.rdoc
@@ -26,7 +26,7 @@ The Product class is automatically mapped to the table named "products",
which might look like this:
CREATE TABLE products (
- id int(11) NOT NULL auto_increment,
+ id int NOT NULL auto_increment,
name varchar(255),
PRIMARY KEY (id)
);
diff --git a/activerecord/Rakefile b/activerecord/Rakefile
index a619204e6f..c93099a921 100644
--- a/activerecord/Rakefile
+++ b/activerecord/Rakefile
@@ -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'
diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb
index f5cf92db64..264f869c68 100644
--- a/activerecord/lib/active_record.rb
+++ b/activerecord/lib/active_record.rb
@@ -53,6 +53,7 @@ module ActiveRecord
autoload :Persistence
autoload :QueryCache
autoload :Querying
+ autoload :CollectionCacheKey
autoload :ReadonlyAttributes
autoload :RecordInvalid, 'active_record/validations'
autoload :Reflection
diff --git a/activerecord/lib/active_record/aggregations.rb b/activerecord/lib/active_record/aggregations.rb
index f7b50cd25a..be88c7c9e8 100644
--- a/activerecord/lib/active_record/aggregations.rb
+++ b/activerecord/lib/active_record/aggregations.rb
@@ -1,6 +1,6 @@
module ActiveRecord
- # = Active Record Aggregations
- module Aggregations # :nodoc:
+ # See ActiveRecord::Aggregations::ClassMethods for documentation
+ module Aggregations
extend ActiveSupport::Concern
def initialize_dup(*) # :nodoc:
@@ -24,8 +24,8 @@ module ActiveRecord
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]
+ # 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
@@ -120,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
@@ -134,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.rubydoc.info/gems/netaddr/1.5.0/NetAddr/CIDR).
+ # 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:
#
@@ -173,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":
#
@@ -186,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
diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb
index 82cb3fed59..b806a2f832 100644
--- a/activerecord/lib/active_record/associations.rb
+++ b/activerecord/lib/active_record/associations.rb
@@ -5,95 +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, 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}'.")
+ 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(&: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
@@ -101,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
@@ -222,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.
#
@@ -284,7 +363,7 @@ module ActiveRecord
# end
#
# If your model class is <tt>Project</tt>, the module is
- # named <tt>Project::GeneratedAssociationMethods</tt>. The GeneratedAssociationMethods 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.
#
@@ -292,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
@@ -308,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
@@ -321,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
@@ -337,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
@@ -349,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.
@@ -370,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)
# )
@@ -388,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).
@@ -425,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
#
@@ -441,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.
@@ -520,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,
@@ -533,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
@@ -550,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
@@ -570,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
@@ -590,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:
#
@@ -599,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
@@ -631,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
#
@@ -681,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
@@ -765,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.
@@ -776,7 +855,7 @@ module ActiveRecord
#
# This will load all comments with a single query. This reduces the total number of queries
# 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).
+ # named (except if some of the associations are polymorphic #belongs_to - see below).
#
# To include a deep hierarchy of associations, use a hash:
#
@@ -816,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)
@@ -848,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.
#
@@ -890,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 ...")
@@ -983,7 +1062,7 @@ module ActiveRecord
#
# * 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.
#
@@ -991,7 +1070,7 @@ module ActiveRecord
#
# === 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.
#
@@ -1012,22 +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.
+ # 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 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
+ # #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
@@ -1035,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>
@@ -1052,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
#
@@ -1119,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
@@ -1133,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
@@ -1185,11 +1264,11 @@ module ActiveRecord
# [: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
@@ -1213,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.
@@ -1237,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]
@@ -1253,10 +1332,11 @@ 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]
@@ -1265,10 +1345,10 @@ module ActiveRecord
# association objects.
#
# Option examples:
- # has_many :comments, -> { order "posted_on" }
- # has_many :comments, -> { includes :author }
+ # 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 :tracks, -> { order("position") }, dependent: :destroy
# has_many :comments, dependent: :nullify
# has_many :tags, as: :taggable
# has_many :reports, -> { readonly }
@@ -1280,8 +1360,8 @@ 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:
#
@@ -1303,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
@@ -1348,7 +1428,7 @@ module ActiveRecord
# 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
@@ -1359,20 +1439,20 @@ module ActiveRecord
# [: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]
@@ -1380,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]
@@ -1395,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 }
# 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)
@@ -1409,8 +1490,8 @@ 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:
@@ -1430,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
@@ -1477,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
@@ -1504,14 +1585,15 @@ 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]
@@ -1601,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.
@@ -1639,7 +1721,7 @@ module ActiveRecord
# query when you access the associated collection.
#
# Scope examples:
- # has_and_belongs_to_many :projects, -> { includes :milestones, :manager }
+ # has_and_belongs_to_many :projects, -> { includes(:milestones, :manager) }
# has_and_belongs_to_many :categories, ->(category) {
# where("default_category = ?", category.name)
# }
@@ -1668,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]
@@ -1689,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 }
diff --git a/activerecord/lib/active_record/associations/alias_tracker.rb b/activerecord/lib/active_record/associations/alias_tracker.rb
index 2b7e4f28c5..021bc32237 100644
--- a/activerecord/lib/active_record/associations/alias_tracker.rb
+++ b/activerecord/lib/active_record/associations/alias_tracker.rb
@@ -2,8 +2,7 @@ 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
diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb
index 7c729676a7..d64ab64c99 100644
--- a/activerecord/lib/active_record/associations/association.rb
+++ b/activerecord/lib/active_record/associations/association.rb
@@ -163,9 +163,12 @@ module ActiveRecord
@reflection = @owner.class._reflect_on_association(reflection_name)
end
- def initialize_attributes(record) #:nodoc:
+ def initialize_attributes(record, except_from_scope_attributes = nil) #:nodoc:
+ except_from_scope_attributes ||= {}
skip_assign = [reflection.foreign_key, reflection.type].compact
- attributes = create_scope.except(*(record.changed - skip_assign))
+ assigned_keys = record.changed
+ assigned_keys += except_from_scope_attributes.keys.map(&:to_s)
+ attributes = create_scope.except(*(assigned_keys - skip_assign))
record.assign_attributes(attributes)
set_inverse_instance(record)
end
@@ -248,9 +251,17 @@ module ActiveRecord
def build_record(attributes)
reflection.build_association(attributes) do |record|
- initialize_attributes(record)
+ initialize_attributes(record, attributes)
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 a140dc239c..48437a1c9e 100644
--- a/activerecord/lib/active_record/associations/association_scope.rb
+++ b/activerecord/lib/active_record/associations/association_scope.rb
@@ -147,9 +147,9 @@ module ActiveRecord
scope.includes! item.includes_values
end
+ scope.unscope!(*item.unscope_values)
scope.where_clause += item.where_clause
scope.order_values |= item.order_values
- scope.unscope!(*item.unscope_values)
end
reflection = reflection.next
diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb
index 260a0c6a2d..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,45 +32,37 @@ 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)
- if target && !stale_target?
- target.increment(counter_cache_name)
- end
+ def update_counters_on_replace(record)
+ if require_counter_update? && different_target?(record)
+ record.increment!(reflection.counter_cache_column)
+ decrement_counters
end
end
diff --git a/activerecord/lib/active_record/associations/builder/association.rb b/activerecord/lib/active_record/associations/builder/association.rb
index ba1b1814d1..d0534056d9 100644
--- a/activerecord/lib/active_record/associations/builder/association.rb
+++ b/activerecord/lib/active_record/associations/builder/association.rb
@@ -9,7 +9,7 @@
# - CollectionAssociation
# - HasManyAssociation
-module ActiveRecord::Associations::Builder
+module ActiveRecord::Associations::Builder # :nodoc:
class Association #:nodoc:
class << self
attr_accessor :extensions
diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb
index 6e4a53f7fb..81c535d962 100644
--- a/activerecord/lib/active_record/associations/builder/belongs_to.rb
+++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb
@@ -1,4 +1,4 @@
-module ActiveRecord::Associations::Builder
+module ActiveRecord::Associations::Builder # :nodoc:
class BelongsTo < SingularAssociation #:nodoc:
def self.macro
:belongs_to
@@ -116,8 +116,7 @@ module ActiveRecord::Associations::Builder
end
def self.add_destroy_callbacks(model, reflection)
- name = reflection.name
- model.after_destroy lambda { |o| o.association(name).handle_dependency }
+ model.after_destroy lambda { |o| o.association(reflection.name).handle_dependency }
end
def self.define_validations(model, reflection)
diff --git a/activerecord/lib/active_record/associations/builder/collection_association.rb b/activerecord/lib/active_record/associations/builder/collection_association.rb
index 2ff67f904d..56a8dc4e18 100644
--- a/activerecord/lib/active_record/associations/builder/collection_association.rb
+++ b/activerecord/lib/active_record/associations/builder/collection_association.rb
@@ -2,7 +2,7 @@
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]
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 ffd9c9d6fc..b888148841 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
@@ -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,25 +58,29 @@ 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)
- belongs_to name, options
+ belongs_to name, required: false, **options
self.left_reflection = _reflect_on_association(name)
end
def self.add_right_association(name, options)
rhs_name = name.to_s.singularize.to_sym
- belongs_to rhs_name, options
+ belongs_to rhs_name, required: false, **options
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, anonymous_class: lhs_model
join_model.add_right_association association_name, belongs_to_options(options)
diff --git a/activerecord/lib/active_record/associations/builder/has_many.rb b/activerecord/lib/active_record/associations/builder/has_many.rb
index 1c1b47bd56..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 self.macro
:has_many
end
def self.valid_options(options)
- super + [:primary_key, :dependent, :as, :through, :source, :source_type, :inverse_of, :counter_cache, :join_table, :foreign_type]
+ 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 a272d3c781..9d64ae877b 100644
--- a/activerecord/lib/active_record/associations/builder/has_one.rb
+++ b/activerecord/lib/active_record/associations/builder/has_one.rb
@@ -1,4 +1,4 @@
-module ActiveRecord::Associations::Builder
+module ActiveRecord::Associations::Builder # :nodoc:
class HasOne < SingularAssociation #:nodoc:
def self.macro
:has_one
diff --git a/activerecord/lib/active_record/associations/builder/singular_association.rb b/activerecord/lib/active_record/associations/builder/singular_association.rb
index 42542f188e..58a9c8ff24 100644
--- a/activerecord/lib/active_record/associations/builder/singular_association.rb
+++ b/activerecord/lib/active_record/associations/builder/singular_association.rb
@@ -1,6 +1,6 @@
# 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 self.valid_options(options)
super + [:dependent, :primary_key, :inverse_of, :required]
diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb
index 87576abd92..f32dddb8f0 100644
--- a/activerecord/lib/active_record/associations/collection_association.rb
+++ b/activerecord/lib/active_record/associations/collection_association.rb
@@ -1,5 +1,3 @@
-require "active_support/deprecation"
-
module ActiveRecord
module Associations
# = Active Record Association Collection
@@ -62,8 +60,10 @@ 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
@@ -440,12 +440,7 @@ module ActiveRecord
private
def get_records
- if reflection.scope_chain.any?(&:any?) ||
- scope.eager_loading? ||
- klass.scope_attributes?
-
- return scope.to_a
- end
+ return scope.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/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb
index b5a8c81fe4..fe693cfbb6 100644
--- a/activerecord/lib/active_record/associations/collection_proxy.rb
+++ b/activerecord/lib/active_record/associations/collection_proxy.rb
@@ -112,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
@@ -127,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>
@@ -171,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)
@@ -227,6 +227,31 @@ 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
@@ -290,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
@@ -307,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
@@ -364,7 +389,7 @@ module ActiveRecord
# specified by the +:dependent+ option. If no +:dependent+ option is given,
# then it will follow the default strategy.
#
- # For +has_many :through+ associations, the default deletion strategy is
+ # For <tt>has_many :through</tt> associations, the default deletion strategy is
# +:delete_all+.
#
# For +has_many+ associations, the default deletion strategy is +:nullify+.
@@ -399,7 +424,7 @@ module ActiveRecord
# # #<Pet id: 3, name: "Choo-Choo", person_id: nil>
# # ]
#
- # Both +has_many+ and +has_many :through+ dependencies default to the
+ # 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.
#
@@ -418,7 +443,7 @@ 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)
#
# If it is set to <tt>:delete_all</tt>, all the objects are deleted
# *without* calling their +destroy+ method.
@@ -438,7 +463,7 @@ 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
@@ -475,7 +500,7 @@ module ActiveRecord
# then it will follow the default strategy. Returns an array with the
# deleted records.
#
- # For +has_many :through+ associations, the default deletion strategy is
+ # For <tt>has_many :through</tt> associations, the default deletion strategy is
# +:delete_all+.
#
# For +has_many+ associations, the default deletion strategy is +:nullify+.
@@ -532,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.
@@ -560,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.
@@ -624,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.
@@ -656,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
@@ -856,7 +881,7 @@ module ActiveRecord
!!@association.include?(record)
end
- def arel
+ def arel #:nodoc:
scope.arel
end
diff --git a/activerecord/lib/active_record/associations/foreign_association.rb b/activerecord/lib/active_record/associations/foreign_association.rb
index fe48ecec29..3ceec0ee46 100644
--- a/activerecord/lib/active_record/associations/foreign_association.rb
+++ b/activerecord/lib/active_record/associations/foreign_association.rb
@@ -1,5 +1,5 @@
module ActiveRecord::Associations
- module ForeignAssociation
+ module ForeignAssociation # :nodoc:
def foreign_key_present?
if reflection.klass.primary_key
owner.attribute_present?(reflection.active_record_primary_key)
diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb
index 9f6c832c1b..a9f6aaafef 100644
--- a/activerecord/lib/active_record/associations/has_many_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_association.rb
@@ -15,7 +15,7 @@ module ActiveRecord
when :restrict_with_error
unless empty?
- record = klass.human_attribute_name(reflection.name).downcase
+ 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)
@@ -50,7 +50,7 @@ module ActiveRecord
end
def empty?
- if has_cached_counter?
+ if reflection.has_cached_counter?
size.zero?
else
super
@@ -73,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
@@ -87,70 +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())
- if reflection.options[:counter_cache]
- reflection.options[:counter_cache].to_s
- else
- "#{reflection.name}_count"
- end
- 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 counter_must_be_updated_by_has_many?(reflection)
- counter = cached_counter_attribute_name(reflection)
- owner[counter] += difference
- owner.send(:clear_attribute_changes, 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_which_updates_counter_cache(reflection = reflection())
- counter_name = cached_counter_attribute_name(reflection)
- inverse_which_updates_counter_named(counter_name, reflection)
- end
- alias inverse_updates_counter_cache? inverse_which_updates_counter_cache
-
- def inverse_which_updates_counter_named(counter_name, reflection)
- reflection.klass._reflections.values.find { |inverse_reflection|
- inverse_reflection.belongs_to? &&
- inverse_reflection.counter_cache_column == counter_name
- }
- end
-
- def inverse_updates_counter_in_memory?(reflection)
- inverse = inverse_which_updates_counter_cache(reflection)
- inverse && inverse == reflection.inverse_of
- end
-
- def counter_must_be_updated_by_has_many?(reflection)
- !inverse_updates_counter_in_memory?(reflection) && has_cached_counter?(reflection)
- end
-
def delete_count(method, scope)
if method == :delete_all
scope.delete_all
@@ -168,7 +118,7 @@ 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))
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 1aa6a2ca74..deb0f8c9f5 100644
--- a/activerecord/lib/active_record/associations/has_many_through_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_through_association.rb
@@ -110,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
diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb
index 5a92bc5e8a..0fe9b2e81b 100644
--- a/activerecord/lib/active_record/associations/has_one_association.rb
+++ b/activerecord/lib/active_record/associations/has_one_association.rb
@@ -1,5 +1,5 @@
module ActiveRecord
- # = Active Record Belongs To Has One Association
+ # = Active Record Has One Association
module Associations
class HasOneAssociation < SingularAssociation #:nodoc:
include ForeignAssociation
@@ -11,7 +11,7 @@ module ActiveRecord
when :restrict_with_error
if load_target
- record = klass.human_attribute_name(reflection.name).downcase
+ 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)
@@ -65,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 81eb5136a1..0e4e951269 100644
--- a/activerecord/lib/active_record/associations/join_dependency.rb
+++ b/activerecord/lib/active_record/associations/join_dependency.rb
@@ -32,7 +32,7 @@ module ActiveRecord
@alias_cache[node][column]
end
- class Table < Struct.new(:node, :columns)
+ class Table < Struct.new(:node, :columns) # :nodoc:
def table
Arel::Nodes::TableAlias.new node.table, node.aliased_table_name
end
@@ -103,9 +103,14 @@ module ActiveRecord
join_root.drop(1).map!(&:reflection)
end
- def join_constraints(outer_joins)
+ def join_constraints(outer_joins, join_type)
joins = join_root.children.flat_map { |child|
- make_inner_joins join_root, child
+
+ if join_type == Arel::Nodes::OuterJoin
+ make_left_outer_joins join_root, child
+ else
+ make_inner_joins join_root, child
+ end
}
joins.concat outer_joins.flat_map { |oj|
@@ -131,9 +136,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] = {}
}
}
@@ -150,7 +155,8 @@ module ActiveRecord
message_bus.instrument('instantiation.active_record', payload) do
result_set.each { |row_hash|
- parent = parents[row_hash[primary_key]] ||= join_root.instantiate(row_hash, column_aliases)
+ 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
@@ -175,6 +181,14 @@ module ActiveRecord
[info] + child.children.flat_map { |c| make_outer_joins(child, c) }
end
+ def make_left_outer_joins(parent, child)
+ tables = child.tables
+ join_type = Arel::Nodes::OuterJoin
+ info = make_constraints parent, child, tables, join_type
+
+ [info] + child.children.flat_map { |c| make_left_outer_joins(child, c) }
+ end
+
def make_inner_joins(parent, child)
tables = child.tables
join_type = Arel::Nodes::InnerJoin
@@ -233,7 +247,6 @@ module ActiveRecord
def construct(ar_parent, parent, row, rs, seen, model_cache, aliases)
return if ar_parent.nil?
- primary_id = ar_parent.id
parent.children.each do |node|
if node.reflection.collection?
@@ -253,14 +266,14 @@ module ActiveRecord
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)
model.readonly!
- seen[parent.base_klass][primary_id][node.base_klass][id] = model
+ 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/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb
index 6ecc741195..ecf6fb8643 100644
--- a/activerecord/lib/active_record/associations/preloader.rb
+++ b/activerecord/lib/active_record/associations/preloader.rb
@@ -54,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
@@ -88,9 +90,6 @@ module ActiveRecord
# [ :books, :author ]
# { author: :avatar }
# [ :books, { author: :avatar } ]
-
- NULL_RELATION = Struct.new(:values, :where_clause, :joins_values).new({}, Relation::WhereClause.empty, [])
-
def preload(records, associations, preload_scope = nil)
records = Array.wrap(records).compact.uniq
associations = Array.wrap(associations)
@@ -107,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
@@ -116,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
@@ -132,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
@@ -181,6 +186,10 @@ module ActiveRecord
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 1dc8bff193..e11a5cfb8a 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
@@ -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
@@ -137,14 +125,23 @@ module ActiveRecord
scope.where_clause = reflection_scope.where_clause + preload_scope.where_clause
scope.references_values = Array(values[:references]) + Array(preload_values[:references])
- 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_scope.joins_values.any?
scope.joins!(preload_scope.joins_values)
else
scope.joins!(reflection_scope.joins_values)
end
- scope.order! preload_values[:order] || values[:order]
+
+ if order_values = preload_values[:order] || values[:order]
+ scope.order!(order_values)
+ end
+
+ if preload_values[:reordering] || values[:reordering]
+ scope.reordering_value = true
+ end
if preload_values[:readonly] || values[:readonly]
scope.readonly!
@@ -154,7 +151,7 @@ module ActiveRecord
scope.where!(klass.table_name => { reflection.type => model.base_class.sti_name })
end
- scope.unscope_values = Array(values[:unscope])
+ 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/collection_association.rb b/activerecord/lib/active_record/associations/preloader/collection_association.rb
index 5adffcd831..9939280fa4 100644
--- a/activerecord/lib/active_record/associations/preloader/collection_association.rb
+++ b/activerecord/lib/active_record/associations/preloader/collection_association.rb
@@ -2,13 +2,8 @@ module ActiveRecord
module Associations
class Preloader
class CollectionAssociation < Association #:nodoc:
-
private
- def build_scope
- super.order(preload_scope.values[:order] || reflection_scope.values[:order])
- end
-
def preload(preloader)
associated_records_by_owner(preloader).each do |owner, records|
association = owner.association(reflection.name)
@@ -17,7 +12,6 @@ module ActiveRecord
records.each { |record| association.set_inverse_instance(record) }
end
end
-
end
end
end
diff --git a/activerecord/lib/active_record/associations/preloader/has_one.rb b/activerecord/lib/active_record/associations/preloader/has_one.rb
index 24728e9f01..c4add621ca 100644
--- a/activerecord/lib/active_record/associations/preloader/has_one.rb
+++ b/activerecord/lib/active_record/associations/preloader/has_one.rb
@@ -2,7 +2,6 @@ module ActiveRecord
module Associations
class Preloader
class HasOne < SingularAssociation #:nodoc:
-
def association_key_name
reflection.foreign_key
end
@@ -10,13 +9,6 @@ module ActiveRecord
def owner_key_name
reflection.active_record_primary_key
end
-
- private
-
- def build_scope
- super.order(preload_scope.values[:order] || reflection_scope.values[:order])
- end
-
end
end
end
diff --git a/activerecord/lib/active_record/associations/preloader/through_association.rb b/activerecord/lib/active_record/associations/preloader/through_association.rb
index 56aa23b173..24aa47bb51 100644
--- a/activerecord/lib/active_record/associations/preloader/through_association.rb
+++ b/activerecord/lib/active_record/associations/preloader/through_association.rb
@@ -84,7 +84,9 @@ module ActiveRecord
end
scope.references! reflection_scope.values[:references]
- scope = scope.order reflection_scope.values[:order] if scope.eager_loading?
+ if scope.eager_loading? && order_values = reflection_scope.values[:order]
+ scope = scope.order(order_values)
+ end
end
scope
diff --git a/activerecord/lib/active_record/associations/singular_association.rb b/activerecord/lib/active_record/associations/singular_association.rb
index 30c5d72482..c7cc48ba16 100644
--- a/activerecord/lib/active_record/associations/singular_association.rb
+++ b/activerecord/lib/active_record/associations/singular_association.rb
@@ -1,5 +1,3 @@
-require "active_support/deprecation"
-
module ActiveRecord
module Associations
class SingularAssociation < Association #:nodoc:
@@ -47,12 +45,7 @@ module ActiveRecord
end
def get_records
- if reflection.scope_chain.any?(&:any?) ||
- scope.eager_loading? ||
- klass.scope_attributes?
-
- return scope.limit(1).to_a
- end
+ 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 55ee9f04e0..d0ec3e8015 100644
--- a/activerecord/lib/active_record/associations/through_association.rb
+++ b/activerecord/lib/active_record/associations/through_association.rb
@@ -76,13 +76,21 @@ module ActiveRecord
def ensure_mutable
unless source_reflection.belongs_to?
- raise HasManyThroughCantAssociateThroughHasOneOrManyReflection.new(owner, reflection)
+ 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
diff --git a/activerecord/lib/active_record/attribute.rb b/activerecord/lib/active_record/attribute.rb
index 73dd3fa041..3c4c8f10ec 100644
--- a/activerecord/lib/active_record/attribute.rb
+++ b/activerecord/lib/active_record/attribute.rb
@@ -5,8 +5,8 @@ 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)
@@ -26,36 +26,46 @@ 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
# `defined?` is cheaper than `||=` when we get back falsy values
- @value = original_value unless defined?(@value)
+ @value = type_cast(value_before_type_cast) unless defined?(@value)
@value
end
def original_value
- type_cast(value_before_type_cast)
+ if assigned?
+ original_attribute.original_value
+ else
+ type_cast(value_before_type_cast)
+ end
end
def value_for_database
type.serialize(value)
end
- def changed_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?
+ has_been_read? && type.changed_in_place?(original_value_for_database, value)
end
- def changed_in_place_from?(old_value)
- has_been_read? && type.changed_in_place?(old_value, value)
+ 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)
@@ -67,7 +77,7 @@ module ActiveRecord
end
def with_type(type)
- self.class.new(name, value_before_type_cast, type)
+ self.class.new(name, value_before_type_cast, type, original_attribute)
end
def type_cast(*)
@@ -100,16 +110,39 @@ module ActiveRecord
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.deserialize(value)
end
+
+ def _original_value_for_database
+ value_before_type_cast
+ end
end
class FromUser < Attribute # :nodoc:
diff --git a/activerecord/lib/active_record/attribute/user_provided_default.rb b/activerecord/lib/active_record/attribute/user_provided_default.rb
index e0bee8c17e..6dbd92ce28 100644
--- a/activerecord/lib/active_record/attribute/user_provided_default.rb
+++ b/activerecord/lib/active_record/attribute/user_provided_default.rb
@@ -2,10 +2,9 @@ require 'active_record/attribute'
module ActiveRecord
class Attribute # :nodoc:
- class UserProvidedDefault < FromUser
+ class UserProvidedDefault < FromUser # :nodoc:
def initialize(name, value, type, database_default)
- super(name, value, type)
- @database_default = database_default
+ super(name, value, type, database_default)
end
def type_cast(value)
@@ -16,17 +15,9 @@ module ActiveRecord
end
end
- def changed_in_place_from?(old_value)
- super || changed_from?(database_default.value)
- end
-
def with_type(type)
- self.class.new(name, value_before_type_cast, type, database_default)
+ self.class.new(name, value_before_type_cast, type, original_attribute)
end
-
- protected
-
- attr_reader :database_default
end
end
end
diff --git a/activerecord/lib/active_record/attribute_assignment.rb b/activerecord/lib/active_record/attribute_assignment.rb
index 45fdcaa1cd..a6d81c82b4 100644
--- a/activerecord/lib/active_record/attribute_assignment.rb
+++ b/activerecord/lib/active_record/attribute_assignment.rb
@@ -5,7 +5,7 @@ module ActiveRecord
extend ActiveSupport::Concern
include ActiveModel::AttributeAssignment
- # Alias for `assign_attributes`. See +ActiveModel::AttributeAssignment+.
+ # Alias for ActiveModel::AttributeAssignment#assign_attributes. See ActiveModel::AttributeAssignment.
def attributes=(attributes)
assign_attributes(attributes)
end
diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb
index abe1d465a5..4ae585d3f5 100644
--- a/activerecord/lib/active_record/attribute_methods.rb
+++ b/activerecord/lib/active_record/attribute_methods.rb
@@ -1,7 +1,7 @@
require 'active_support/core_ext/enumerable'
require 'active_support/core_ext/string/filters'
require 'mutex_m'
-require 'thread_safe'
+require 'concurrent/map'
module ActiveRecord
# = Active Record Attribute Methods
@@ -37,7 +37,7 @@ module ActiveRecord
class AttributeMethodCache
def initialize
@module = Module.new
- @method_cache = ThreadSafe::Cache.new
+ @method_cache = Concurrent::Map.new
end
def [](name)
@@ -96,7 +96,7 @@ module ActiveRecord
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
@@ -106,7 +106,7 @@ 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
@@ -230,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.
@@ -338,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
@@ -356,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
@@ -377,27 +385,27 @@ module ActiveRecord
#
# For example:
#
- # class PostsController < ActionController::Base
- # after_action :print_accessed_fields, only: :index
+ # class PostsController < ActionController::Base
+ # after_action :print_accessed_fields, only: :index
#
- # def index
- # @posts = Post.all
- # end
+ # def index
+ # @posts = Post.all
+ # end
#
- # private
+ # private
#
- # def print_accessed_fields
- # p @posts.first.accessed_fields
+ # def print_accessed_fields
+ # p @posts.first.accessed_fields
+ # end
# 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)
+ # class PostsController < ActionController::Base
+ # def index
+ # @posts = Post.select(:id, :title, :author_id, :updated_at)
+ # end
# end
- # end
def accessed_fields
@attributes.accessed
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 56c1898551..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
diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb
index 0171ef3bdf..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,23 +35,43 @@ 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 changes_applied
- super
- store_original_raw_attributes
+ @previous_mutation_tracker = mutation_tracker
+ @changed_attributes = HashWithIndifferentAccess.new
+ store_original_attributes
end
def clear_changes_information
+ @previous_mutation_tracker = nil
+ @changed_attributes = HashWithIndifferentAccess.new
+ store_original_attributes
+ end
+
+ def raw_write_attribute(attr_name, *)
+ result = super
+ clear_attribute_change(attr_name)
+ result
+ end
+
+ def clear_attribute_changes(attr_names)
super
- original_raw_attributes.clear
+ attr_names.each do |attr_name|
+ clear_attribute_change(attr_name)
+ end
end
def changed_attributes
@@ -59,7 +80,7 @@ module ActiveRecord
if defined?(@cached_changed_attributes)
@cached_changed_attributes
else
- super.reverse_merge(attributes_changed_in_place).freeze
+ super.reverse_merge(mutation_tracker.changed_values).freeze
end
end
@@ -69,59 +90,29 @@ module ActiveRecord
end
end
+ def previous_changes
+ previous_mutation_tracker.changes
+ end
+
def attribute_changed_in_place?(attr_name)
- old_value = original_raw_attribute(attr_name)
- @attributes[attr_name].changed_in_place_from?(old_value)
+ mutation_tracker.changed_in_place?(attr_name)
end
private
- def changes_include?(attr_name)
- super || attribute_changed_in_place?(attr_name)
- end
-
- def calculate_changes_from_defaults
- @changed_attributes = nil
- self.class.column_defaults.each do |attr, orig_value|
- set_attribute_was(attr, orig_value) if _field_changed?(attr, orig_value)
+ def mutation_tracker
+ unless defined?(@mutation_tracker)
+ @mutation_tracker = nil
end
+ @mutation_tracker ||= AttributeMutationTracker.new(@attributes)
end
- # Wrap write_attribute to remember original attribute value.
- def write_attribute(attr, value)
- attr = attr.to_s
-
- old_value = old_attribute_value(attr)
-
- result = super
- store_original_raw_attribute(attr)
- save_changed_attribute(attr, old_value)
- result
- end
-
- def raw_write_attribute(attr, value)
- attr = attr.to_s
-
- result = super
- original_raw_attributes[attr] = value
- result
+ def changes_include?(attr_name)
+ super || mutation_tracker.changed?(attr_name)
end
- def save_changed_attribute(attr, old_value)
- clear_changed_attributes_cache
- if attribute_changed_by_setter?(attr)
- clear_attribute_changes(attr) unless _field_changed?(attr, old_value)
- else
- set_attribute_was(attr, old_value) if _field_changed?(attr, old_value)
- end
- 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(*)
@@ -136,41 +127,13 @@ module ActiveRecord
changed & self.class.column_names
end
- def _field_changed?(attr, old_value)
- @attributes[attr].changed_from?(old_value)
- end
-
- def attributes_changed_in_place
- changed_in_place.each_with_object({}) do |attr_name, h|
- orig = @attributes[attr_name].original_value
- h[attr_name] = orig
- end
- end
-
- def changed_in_place
- self.class.attribute_names.select do |attr_name|
- attribute_changed_in_place?(attr_name)
- end
- end
-
- def original_raw_attribute(attr_name)
- original_raw_attributes.fetch(attr_name) do
- read_attribute_before_type_cast(attr_name)
- end
+ def store_original_attributes
+ @attributes = @attributes.map(&:forgetting_assignment)
+ @mutation_tracker = nil
end
- def original_raw_attributes
- @original_raw_attributes ||= {}
- end
-
- def store_original_raw_attribute(attr_name)
- original_raw_attributes[attr_name] = @attributes[attr_name].value_for_database rescue nil
- end
-
- def store_original_raw_attributes
- attribute_names.each do |attr|
- store_original_raw_attribute(attr)
- end
+ def previous_mutation_tracker
+ @previous_mutation_tracker ||= NullMutationTracker.instance
end
def cache_changed_attributes
diff --git a/activerecord/lib/active_record/attribute_methods/primary_key.rb b/activerecord/lib/active_record/attribute_methods/primary_key.rb
index c28374e4ab..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
@@ -108,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
diff --git a/activerecord/lib/active_record/attribute_methods/query.rb b/activerecord/lib/active_record/attribute_methods/query.rb
index 553122a5fc..10498f4322 100644
--- a/activerecord/lib/active_record/attribute_methods/query.rb
+++ b/activerecord/lib/active_record/attribute_methods/query.rb
@@ -19,7 +19,7 @@ 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 value.respond_to?(:zero?)
diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb
index 2363cf7608..5197e21fa4 100644
--- a/activerecord/lib/active_record/attribute_methods/read.rb
+++ b/activerecord/lib/active_record/attribute_methods/read.rb
@@ -56,14 +56,12 @@ module ActiveRecord
end
end
- ID = 'id'.freeze
-
# Returns the value of the attribute identified by <tt>attr_name</tt> after
# it has been typecast (for example, "2004-12-12" in a date column is cast
# 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
+ name = self.class.primary_key if name == 'id'.freeze
_read_attribute(name, &block)
end
diff --git a/activerecord/lib/active_record/attribute_methods/serialization.rb b/activerecord/lib/active_record/attribute_methods/serialization.rb
index e03bf5945d..65978aea2a 100644
--- a/activerecord/lib/active_record/attribute_methods/serialization.rb
+++ b/activerecord/lib/active_record/attribute_methods/serialization.rb
@@ -9,15 +9,15 @@ module ActiveRecord
# 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 assignment and retrieval.
- # Otherwise <tt>SerializationTypeMismatch</tt> will be raised.
+ # Otherwise SerializationTypeMismatch will be raised.
#
- # Empty objects as +{}+, in the case of +Hash+, or +[]+, in the case of
+ # 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
+ # objects transparently. There is no need to use #serialize in this
# case.
#
# For more complex cases, such as conversion to or from your application
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 f9beb43e4b..9e693b6aee 100644
--- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
+++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
@@ -13,7 +13,7 @@ module ActiveRecord
set_time_zone_without_conversion(super)
elsif value.respond_to?(:in_time_zone)
begin
- user_input_in_time_zone(value) || super
+ super(user_input_in_time_zone(value)) || super
rescue ArgumentError
nil
end
diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb
index 07d5e7d38e..bbf2a51a0e 100644
--- a/activerecord/lib/active_record/attribute_methods/write.rb
+++ b/activerecord/lib/active_record/attribute_methods/write.rb
@@ -45,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
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 013a7d0e01..be581ac2a9 100644
--- a/activerecord/lib/active_record/attribute_set.rb
+++ b/activerecord/lib/active_record/attribute_set.rb
@@ -60,8 +60,14 @@ module ActiveRecord
super
end
+ def deep_dup
+ dup.tap do |copy|
+ copy.instance_variable_set(:@attributes, attributes.deep_dup)
+ end
+ end
+
def initialize_dup(_)
- @attributes = attributes.deep_dup
+ @attributes = attributes.dup
super
end
@@ -80,6 +86,15 @@ module ActiveRecord
attributes.select { |_, attr| attr.has_been_read? }.keys
end
+ def map(&block)
+ new_attributes = attributes.transform_values(&block)
+ AttributeSet.new(new_attributes)
+ end
+
+ def ==(other)
+ attributes == other.attributes
+ end
+
protected
attr_reader :attributes
diff --git a/activerecord/lib/active_record/attribute_set/builder.rb b/activerecord/lib/active_record/attribute_set/builder.rb
index e85777c335..3bd7c7997b 100644
--- a/activerecord/lib/active_record/attribute_set/builder.rb
+++ b/activerecord/lib/active_record/attribute_set/builder.rb
@@ -1,3 +1,5 @@
+require 'active_record/attribute'
+
module ActiveRecord
class AttributeSet # :nodoc:
class Builder # :nodoc:
@@ -45,8 +47,14 @@ module ActiveRecord
delegate_hash[key] = value
end
+ def deep_dup
+ dup.tap do |copy|
+ copy.instance_variable_set(:@delegate_hash, delegate_hash.transform_values(&:dup))
+ end
+ end
+
def initialize_dup(_)
- @delegate_hash = delegate_hash.transform_values(&:dup)
+ @delegate_hash = Hash[delegate_hash]
super
end
@@ -60,10 +68,29 @@ module ActiveRecord
end
end
+ def ==(other)
+ if other.is_a?(LazyAttributeHash)
+ materialize == other.materialize
+ else
+ materialize == other
+ end
+ end
+
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)
@@ -77,16 +104,5 @@ module ActiveRecord
delegate_hash[name] = Attribute.uninitialized(name, type)
end
end
-
- 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
end
end
diff --git a/activerecord/lib/active_record/attributes.rb b/activerecord/lib/active_record/attributes.rb
index 8b2c4c7170..5d0405c3be 100644
--- a/activerecord/lib/active_record/attributes.rb
+++ b/activerecord/lib/active_record/attributes.rb
@@ -5,9 +5,6 @@ module ActiveRecord
module Attributes
extend ActiveSupport::Concern
- # :nodoc:
- Type = ActiveRecord::Type
-
included do
class_attribute :attributes_to_define_after_schema_loads, instance_accessor: false # :internal:
self.attributes_to_define_after_schema_loads = {}
@@ -18,7 +15,7 @@ module ActiveRecord
# 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::QueryMethods#where. This will let you use
+ # {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.
#
@@ -90,7 +87,7 @@ module ActiveRecord
# 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.
+ # \Attributes do not need to be backed by a database column.
#
# class MyModel < ActiveRecord::Base
# attribute :my_string, :string
@@ -122,7 +119,7 @@ module ActiveRecord
#
# class MoneyType < ActiveRecord::Type::Integer
# def cast(value)
- # if value.include?('$')
+ # if !value.kind_of(Numeric) && value.include?('$')
# price_in_dollars = value.gsub(/\$/, '').to_f
# super(price_in_dollars * 100)
# else
@@ -147,9 +144,9 @@ module ActiveRecord
# to be referenced by a symbol, see ActiveRecord::Type.register. You can
# also pass a type object directly, in place of a symbol.
#
- # ==== Querying
+ # ==== \Querying
#
- # When ActiveRecord::QueryMethods#where is called, it will
+ # 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:
#
diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb
index dbb0e2fab2..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)
@@ -234,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.
#
@@ -243,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?
@@ -316,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
@@ -324,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)
+ 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
@@ -353,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.
@@ -396,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.
diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb
index c918e88590..9782e58299 100644
--- a/activerecord/lib/active_record/base.rb
+++ b/activerecord/lib/active_record/base.rb
@@ -1,5 +1,4 @@
require 'yaml'
-require 'set'
require 'active_support/benchmarkable'
require 'active_support/dependencies'
require 'active_support/descendants_tracker'
@@ -171,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_".
@@ -186,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.
#
@@ -226,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 <tt>save!</tt> and <tt>create!</tt> 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.
@@ -280,6 +288,7 @@ module ActiveRecord #:nodoc:
extend Explain
extend Enum
extend Delegation::DelegateCache
+ extend CollectionCacheKey
include Core
include Persistence
diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb
index 19f0dca5a6..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:
@@ -193,8 +193,9 @@ module ActiveRecord
# == <tt>before_validation*</tt> returning statements
#
# If the +before_validation+ callback throws +:abort+, the process will be
- # aborted and <tt>Base#save</tt> will return +false+. If Base#save! is called it will raise a
- # <tt>ActiveRecord::RecordInvalid</tt> exception. Nothing will be appended to the errors object.
+ # 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
#
@@ -206,7 +207,8 @@ module ActiveRecord
# == 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,7 +292,15 @@ module ActiveRecord
end
def destroy #:nodoc:
+ @_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:
diff --git a/activerecord/lib/active_record/coders/yaml_column.rb b/activerecord/lib/active_record/coders/yaml_column.rb
index 9ea22ed798..2456b8ad8c 100644
--- a/activerecord/lib/active_record/coders/yaml_column.rb
+++ b/activerecord/lib/active_record/coders/yaml_column.rb
@@ -14,10 +14,7 @@ module ActiveRecord
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
@@ -26,15 +23,19 @@ 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
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 282af220fb..486b7b6d25 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
@@ -1,5 +1,5 @@
require 'thread'
-require 'thread_safe'
+require 'concurrent/map'
require 'monitor'
module ActiveRecord
@@ -10,8 +10,9 @@ module ActiveRecord
end
# Raised when a pool was unable to get ahold of all its connections
- # to perform a "group" action such as +ConnectionPool#disconnect!+
- # or +ConnectionPool#clear_reloadable_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
@@ -37,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.
#
@@ -140,7 +142,7 @@ module ActiveRecord
# become available.
#
# Raises:
- # - ConnectionTimeoutError if +timeout+ is given and no element
+ # - ActiveRecord::ConnectionTimeoutError if +timeout+ is given and no element
# becomes available within +timeout+ seconds,
def poll(timeout = nil)
synchronize { internal_poll(timeout) }
@@ -331,18 +333,18 @@ module ActiveRecord
# 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 +thread => conn+,
+ # 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 = ThreadSafe::Cache.new(:initial_capacity => @size)
+ @thread_cached_conns = Concurrent::Map.new(:initial_capacity => @size)
@connections = []
@automatic_reconnect = true
- # Connection pool allows for concurrent (outside the main `synchronize` section)
+ # 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
@@ -364,7 +366,7 @@ module ActiveRecord
# Is there an open connection that is being used for the current thread?
#
- # This method only works for connections that have been abtained through
+ # 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?
@@ -406,9 +408,9 @@ module ActiveRecord
# Disconnects all connections in the pool, and clears the pool.
#
# Raises:
- # - +ExclusiveConnectionTimeoutError+ if unable to gain ownership of all
+ # - ActiveRecord::ExclusiveConnectionTimeoutError if unable to gain ownership of all
# connections in the pool within a timeout interval (default duration is
- # +spec.config[:checkout_timeout] * 2+ seconds).
+ # <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
@@ -426,8 +428,8 @@ module ActiveRecord
#
# The pool first tries to gain ownership of all connections, if unable to
# do so within a timeout interval (default duration is
- # +spec.config[:checkout_timeout] * 2+ seconds), the pool is forcefully
- # disconneted wihout any regard for other connection owning threads.
+ # <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
@@ -436,9 +438,9 @@ module ActiveRecord
# require reloading.
#
# Raises:
- # - +ExclusiveConnectionTimeoutError+ if unable to gain ownership of all
+ # - ActiveRecord::ExclusiveConnectionTimeoutError if unable to gain ownership of all
# connections in the pool within a timeout interval (default duration is
- # +spec.config[:checkout_timeout] * 2+ seconds).
+ # <tt>spec.config[:checkout_timeout] * 2</tt> seconds).
def clear_reloadable_connections(raise_on_acquisition_timeout = true)
num_new_conns_required = 0
@@ -474,7 +476,7 @@ module ActiveRecord
#
# The pool first tries to gain ownership of all connections, if unable to
# do so within a timeout interval (default duration is
- # +spec.config[:checkout_timeout] * 2+ seconds), the pool forcefully
+ # <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!
@@ -494,7 +496,7 @@ module ActiveRecord
# Returns: an AbstractAdapter object.
#
# Raises:
- # - ConnectionTimeoutError: no connection can be obtained from the pool.
+ # - ActiveRecord::ConnectionTimeoutError no connection can be obtained from the pool.
def checkout(checkout_timeout = @checkout_timeout)
checkout_and_verify(acquire_connection(checkout_timeout))
end
@@ -503,7 +505,7 @@ module ActiveRecord
# 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
remove_connection_from_thread_cache conn
@@ -516,7 +518,7 @@ module ActiveRecord
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
@@ -547,7 +549,7 @@ module ActiveRecord
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
@@ -587,7 +589,7 @@ module ActiveRecord
end
#--
- # From the discussion on Github:
+ # 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.
@@ -628,10 +630,10 @@ module ActiveRecord
end
end
rescue ExclusiveConnectionTimeoutError
- # `raise_on_acquisition_timeout == false` means we are directed to ignore any
+ # <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.
+ # +newly_checked_out+ connections.
if raise_on_acquisition_timeout
release_newly_checked_out = true
@@ -688,18 +690,18 @@ module ActiveRecord
# queue for a connection to become available.
#
# Raises:
- # - ConnectionTimeoutError if a connection could not be acquired
+ # - 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`
+ # 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
- # `synchronize { conn.lease }` 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.
+ # <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
else
@@ -824,11 +826,11 @@ module ActiveRecord
# 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
@@ -857,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
@@ -956,12 +960,11 @@ module ActiveRecord
def call(env)
testing = env['rack.test']
- response = @app.call(env)
- response[2] = ::Rack::BodyProxy.new(response[2]) do
+ status, headers, body = @app.call(env)
+ proxy = ::Rack::BodyProxy.new(body) do
ActiveRecord::Base.clear_active_connections! unless testing
end
-
- response
+ [status, headers, proxy]
rescue Exception
ActiveRecord::Base.clear_active_connections! unless testing
raise
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 30b2fca2ca..6711049588 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb
@@ -19,8 +19,8 @@ module ActiveRecord
# 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
+ # #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 38dd9578fe..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
@@ -67,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
@@ -137,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.6/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.
#
@@ -190,9 +200,9 @@ module ActiveRecord
# semantics of these different levels:
#
# * http://www.postgresql.org/docs/current/static/transaction-iso.html
- # * https://dev.mysql.com/doc/refman/5.6/en/set-transaction.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
@@ -289,8 +299,12 @@ module ActiveRecord
columns = schema_cache.columns_hash(table_name)
binds = fixture.map do |name, value|
- type = lookup_cast_type_from_column(columns[name])
- Relation::QueryAttribute.new(name, value, type)
+ 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|
@@ -354,9 +368,12 @@ module ActiveRecord
# Returns an ActiveRecord::Result instance.
def select(sql, name = nil, binds = [])
- exec_query(sql, name, binds)
+ exec_query(sql, name, binds, prepare: false)
end
+ def select_prepared(sql, name = nil, binds = [])
+ exec_query(sql, name, binds, prepare: true)
+ end
# Returns the last auto-generated ID from the affected table.
def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
index 2c7409b2dc..9ec0a67c8f 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
@@ -43,9 +43,9 @@ module ActiveRecord
# 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
- # `attribute`) or implicitly (via `serialize`,
- # `time_zone_aware_attributes`). In almost all cases, the sql type should
- # only be used to change quoting behavior, when the primitive to
+ # 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.
@@ -58,7 +58,7 @@ module ActiveRecord
end
end
- # See docs for +type_cast_from_column+
+ # See docs for #type_cast_from_column
def lookup_cast_type_from_column(column) # :nodoc:
lookup_cast_type(column.sql_type)
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 18d943f452..0ba4d94e3c 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb
@@ -14,8 +14,10 @@ module ActiveRecord
send m, o
end
- delegate :quote_column_name, :quote_table_name, :quote_default_expression, :type_to_sql, to: :@conn
- private :quote_column_name, :quote_table_name, :quote_default_expression, :type_to_sql
+ 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
@@ -38,17 +40,32 @@ module ActiveRecord
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
@@ -57,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
@@ -89,8 +110,9 @@ module ActiveRecord
sql
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)
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 d17e272ed1..abf0124562 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
@@ -23,6 +23,9 @@ module ActiveRecord
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:
def name
options[:name]
@@ -120,23 +123,29 @@ module ActiveRecord
end
def foreign_key_options
- as_options(foreign_key)
+ as_options(foreign_key).merge(column: column_name)
end
def columns
- result = [["#{name}_id", type, options]]
+ 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
- Base.pluralize_table_names ? name.to_s.pluralize : name
+ foreign_key_options.fetch(:to_table) do
+ Base.pluralize_table_names ? name.to_s.pluralize : name
+ end
end
end
@@ -178,7 +187,7 @@ module ActiveRecord
# 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
@@ -193,20 +202,17 @@ module ActiveRecord
# end
# end
#
- # The table definitions
- # 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, :foreign_keys
+ 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
@@ -214,6 +220,12 @@ module ActiveRecord
@name = name
end
+ 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+.
@@ -222,90 +234,23 @@ module ActiveRecord
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>: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 <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.
#
@@ -337,7 +282,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
@@ -369,6 +315,8 @@ 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
@@ -386,7 +334,7 @@ module ActiveRecord
end
# Appends <tt>:datetime</tt> columns <tt>:created_at</tt> and
- # <tt>:updated_at</tt> to the table. See SchemaStatements#add_timestamps
+ # <tt>:updated_at</tt> to the table. See {connection.add_timestamps}[rdoc-ref:SchemaStatements#add_timestamps]
#
# t.timestamps null: false
def timestamps(*args)
@@ -403,7 +351,7 @@ module ActiveRecord
# t.references(:user)
# t.belongs_to(:supplier, foreign_key: true)
#
- # See SchemaStatements#add_reference for details of the options you can use.
+ # 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|
ReferenceDefinition.new(col, **options).add_to(self)
@@ -436,10 +384,6 @@ module ActiveRecord
ColumnDefinition.new name, type
end
- def native
- @native
- end
-
def aliased_types(name, fallback)
'timestamp' == name ? :datetime : fallback
end
@@ -475,7 +419,7 @@ module ActiveRecord
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:
#
@@ -532,7 +476,7 @@ module ActiveRecord
#
# t.string(:name) unless t.column_exists?(:name, :string)
#
- # See SchemaStatements#column_exists?
+ # See {connection.column_exists?}[rdoc-ref:SchemaStatements#column_exists?]
def column_exists?(column_name, type = nil, options = {})
@base.column_exists?(name, column_name, type, options)
end
@@ -544,7 +488,7 @@ module ActiveRecord
# t.index([:branch_id, :party_id], unique: true)
# t.index([:branch_id, :party_id], unique: true, name: 'by_branch_party')
#
- # See SchemaStatements#add_index for details of the options you can use.
+ # 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(name, column_name, options)
end
@@ -555,7 +499,7 @@ module ActiveRecord
# t.index(:branch_id)
# end
#
- # See SchemaStatements#index_exists?
+ # See {connection.index_exists?}[rdoc-ref:SchemaStatements#index_exists?]
def index_exists?(column_name, options = {})
@base.index_exists?(name, column_name, options)
end
@@ -564,7 +508,7 @@ module ActiveRecord
#
# t.rename_index(:user_id, :account_id)
#
- # See SchemaStatements#rename_index
+ # See {connection.rename_index}[rdoc-ref:SchemaStatements#rename_index]
def rename_index(index_name, new_index_name)
@base.rename_index(name, index_name, new_index_name)
end
@@ -573,7 +517,7 @@ module ActiveRecord
#
# t.timestamps(null: false)
#
- # See SchemaStatements#add_timestamps
+ # See {connection.add_timestamps}[rdoc-ref:SchemaStatements#add_timestamps]
def timestamps(options = {})
@base.add_timestamps(name, options)
end
@@ -594,7 +538,7 @@ module ActiveRecord
# t.change_default(:authorized, 1)
# t.change_default(:status, from: nil, to: "draft")
#
- # See SchemaStatements#change_column_default
+ # 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
@@ -604,7 +548,7 @@ module ActiveRecord
# t.remove(:qualification)
# t.remove(:qualification, :experience)
#
- # See SchemaStatements#remove_columns
+ # See {connection.remove_columns}[rdoc-ref:SchemaStatements#remove_columns]
def remove(*column_names)
@base.remove_columns(name, *column_names)
end
@@ -615,7 +559,7 @@ module ActiveRecord
# t.remove_index(column: [:branch_id, :party_id])
# t.remove_index(name: :by_branch_party)
#
- # See SchemaStatements#remove_index
+ # See {connection.remove_index}[rdoc-ref:SchemaStatements#remove_index]
def remove_index(options = {})
@base.remove_index(name, options)
end
@@ -624,7 +568,7 @@ module ActiveRecord
#
# t.remove_timestamps
#
- # See SchemaStatements#remove_timestamps
+ # See {connection.remove_timestamps}[rdoc-ref:SchemaStatements#remove_timestamps]
def remove_timestamps(options = {})
@base.remove_timestamps(name, options)
end
@@ -633,7 +577,7 @@ module ActiveRecord
#
# t.rename(:description, :name)
#
- # See SchemaStatements#rename_column
+ # See {connection.rename_column}[rdoc-ref:SchemaStatements#rename_column]
def rename(column_name, new_column_name)
@base.rename_column(name, column_name, new_column_name)
end
@@ -643,7 +587,7 @@ module ActiveRecord
# t.references(:user)
# t.belongs_to(:supplier, foreign_key: true)
#
- # See SchemaStatements#add_reference for details of the options you can use.
+ # 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|
@@ -657,7 +601,7 @@ module ActiveRecord
# 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|
@@ -670,7 +614,7 @@ module ActiveRecord
#
# t.foreign_key(:authors)
#
- # See SchemaStatements#add_foreign_key
+ # See {connection.add_foreign_key}[rdoc-ref:SchemaStatements#add_foreign_key]
def foreign_key(*args) # :nodoc:
@base.add_foreign_key(name, *args)
end
@@ -679,7 +623,7 @@ module ActiveRecord
#
# t.foreign_key(:authors) unless t.foreign_key_exists?(:authors)
#
- # See SchemaStatements#foreign_key_exists?
+ # See {connection.foreign_key_exists?}[rdoc-ref:SchemaStatements#foreign_key_exists?]
def foreign_key_exists?(*args) # :nodoc:
@base.foreign_key_exists?(name, *args)
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 b944a8631c..e252ddb4cf 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb
@@ -18,9 +18,9 @@ module ActiveRecord
spec.merge!(prepare_column_options(column).delete_if { |key, _| [:name, :type].include?(key) })
end
- # This can be overridden on a Adapter level basis to support other
+ # This can be overridden on an Adapter level basis to support other
# extended datatypes (Example: Adding an array option in the
- # PostgreSQLAdapter)
+ # PostgreSQL::ColumnDumper)
def prepare_column_options(column)
spec = {}
spec[:name] = column.name.inspect
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 e3115abe66..5cacf6eddc 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
@@ -23,6 +23,25 @@ module ActiveRecord
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)
@@ -31,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
@@ -88,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
@@ -123,7 +161,7 @@ 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>]
@@ -131,7 +169,8 @@ module ActiveRecord
# 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>]
@@ -153,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
@@ -165,7 +204,7 @@ module ActiveRecord
# generates:
#
# CREATE TABLE objects (
- # guid int(11) DEFAULT NULL auto_increment PRIMARY KEY,
+ # guid int auto_increment PRIMARY KEY,
# name varchar(80)
# )
#
@@ -215,12 +254,16 @@ 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?
- if options[:force] && table_exists?(table_name)
+ if options[:force] && data_source_exists?(table_name)
drop_table(table_name, options)
end
@@ -232,10 +275,6 @@ module ActiveRecord
end
end
- td.foreign_keys.each_pair do |other_table_name, foreign_key_options|
- add_foreign_key(table_name, other_table_name, foreign_key_options)
- end
-
result
end
@@ -258,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|
@@ -293,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)
@@ -315,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.
#
@@ -402,17 +441,86 @@ module ActiveRecord
#
# 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.
+ # In that case, +options+ and the block will be used by #create_table.
def drop_table(table_name, options = {})
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.
- #
- # Note: Not all options will be available, generally this command should
- # ignore most of them. In favor of doing a low-level call to simply
- # create a column.
+ # 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)
@@ -469,7 +577,7 @@ module ActiveRecord
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)
@@ -481,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.
@@ -535,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:
@@ -607,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
@@ -657,7 +764,7 @@ module ActiveRecord
# 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.
- # <tt>add_reference</tt> and <tt>add_belongs_to</tt> are acceptable.
+ # #add_reference and #add_belongs_to are acceptable.
#
# The +options+ hash can include the following keys:
# [<tt>:type</tt>]
@@ -665,9 +772,11 @@ module ActiveRecord
# [<tt>:index</tt>]
# Add an appropriate index. Defaults to false.
# [<tt>:foreign_key</tt>]
- # Add an appropriate foreign key. Defaults to false.
+ # 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
#
@@ -685,13 +794,17 @@ module ActiveRecord
#
# add_reference(:products, :supplier, foreign_key: true)
#
+ # ====== Create a supplier_id column and a foreign key to the firms table
+ #
+ # 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> and <tt>remove_belongs_to</tt> are acceptable.
+ # #remove_reference and #remove_belongs_to are acceptable.
#
# ====== Remove the reference
#
@@ -717,7 +830,7 @@ module ActiveRecord
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
@@ -761,21 +874,13 @@ 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
@@ -843,6 +948,13 @@ module ActiveRecord
"#{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:
sm_table = ActiveRecord::Migrator.schema_migrations_table_name
@@ -857,7 +969,7 @@ 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)
@@ -976,7 +1088,7 @@ module ActiveRecord
if index_name.length > max_index_length
raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' is too long; the limit is #{max_index_length} characters"
end
- if table_exists?(table_name) && index_name_exists?(table_name, index_name, false)
+ if data_source_exists?(table_name) && index_name_exists?(table_name, index_name, false)
raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' already exists"
end
index_columns = quoted_columns_for_index(column_names, options).join(", ")
@@ -984,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]
@@ -1010,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)
diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
index 56227ddd80..4d4dc07b04 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
@@ -1,5 +1,6 @@
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'
@@ -51,15 +52,15 @@ 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
@@ -213,6 +214,11 @@ module ActiveRecord
false
end
+ # Does this adapter support application-enforced advisory locking?
+ def supports_advisory_locks?
+ false
+ end
+
# Should primary key values be selected from their corresponding
# sequence before the insert statement? If true, next_sequence_value
# is called before each insert to set the record's primary key.
@@ -266,6 +272,11 @@ module ActiveRecord
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
@@ -274,6 +285,20 @@ module ActiveRecord
def enable_extension(name)
end
+ # This is meant to be implemented by the adapters that support advisory
+ # locks
+ #
+ # Return true if we got the lock, otherwise false
+ def get_advisory_lock(lock_id) # :nodoc:
+ end
+
+ # This is meant to be implemented by the adapters that support advisory
+ # locks.
+ #
+ # Return true if we released the lock, otherwise false
+ def release_advisory_lock(lock_id) # :nodoc:
+ end
+
# A list of extensions, to be filled in by adapters that support them.
def extensions
[]
@@ -343,7 +368,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?
@@ -513,7 +538,7 @@ module ActiveRecord
def translate_exception(exception, message)
# override in derived class
- ActiveRecord::StatementInvalid.new(message, exception)
+ ActiveRecord::StatementInvalid.new(message)
end
def without_prepared_statement?(binds)
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 af156c9c78..deef246c37 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
@@ -1,146 +1,24 @@
+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
- module ColumnMethods
- def primary_key(name, type = :primary_key, **options)
- options[:auto_increment] = true if type == :bigint
- super
- end
- end
-
- class ColumnDefinition < ActiveRecord::ConnectionAdapters::ColumnDefinition
- attr_accessor :charset
- 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
- end
- column.charset = options[:charset]
- column
- end
-
- private
-
- def create_column_definition(name, type)
- ColumnDefinition.new(name, type)
- end
- end
-
- class Table < ActiveRecord::ConnectionAdapters::Table
- include ColumnMethods
- end
-
- class SchemaCreation < AbstractAdapter::SchemaCreation
- 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_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
-
def update_table_definition(table_name, base) # :nodoc:
- Table.new(table_name, base)
+ MySQL::Table.new(table_name, base)
end
def schema_creation
- SchemaCreation.new self
+ MySQL::SchemaCreation.new(self)
end
- def column_spec_for_primary_key(column)
- spec = {}
- if column.auto_increment?
- spec[:id] = ':bigint' if column.bigint?
- 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
-
- private
-
- 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)
- @collation_cache ||= {}
- @collation_cache[table_name] ||= select_one("SHOW TABLE STATUS LIKE '#{table_name}'")["Collation"]
- column.collation.inspect if column.collation != @collation_cache[table_name]
- end
- end
-
- public
-
class Column < ConnectionAdapters::Column # :nodoc:
delegate :strict, :extra, to: :sql_type_metadata, allow_nil: true
@@ -167,6 +45,10 @@ module ActiveRecord
sql_type =~ /blob/i || type == :text
end
+ def unsigned?
+ /unsigned/ === sql_type
+ end
+
def case_sensitive?
collation && !collation.match(/_ci$/)
end
@@ -242,17 +124,18 @@ 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" },
+ boolean: { name: "tinyint", limit: 1 },
+ json: { name: "json" },
}
INDEX_TYPES = [:fulltext, :spatial]
@@ -268,6 +151,7 @@ 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
@@ -283,6 +167,10 @@ module ActiveRecord
end
end
+ def version
+ @version ||= Version.new(full_version.match(/^\d+\.\d+\.\d+/)[0])
+ end
+
# Returns true, since this connection adapter supports migrations.
def supports_migrations?
true
@@ -310,6 +198,10 @@ module ActiveRecord
version >= '5.0.0'
end
+ def supports_explain?
+ true
+ end
+
def supports_indexes_in_create?
true
end
@@ -326,6 +218,20 @@ module ActiveRecord
version >= '5.6.4'
end
+ # 5.0.0 definitely supports it, possibly supported by earlier versions but
+ # not sure
+ def supports_advisory_locks?
+ version >= '5.0.0'
+ end
+
+ def get_advisory_lock(lock_name, timeout = 0) # :nodoc:
+ select_value("SELECT GET_LOCK('#{lock_name}', #{timeout});").to_s == '1'
+ end
+
+ def release_advisory_lock(lock_name) # :nodoc:
+ select_value("SELECT RELEASE_LOCK('#{lock_name}')").to_s == '1'
+ end
+
def native_database_types
NATIVE_DATABASE_TYPES
end
@@ -411,6 +317,80 @@ module ActiveRecord
# 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
reload_type_map
@@ -514,33 +494,69 @@ 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:
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ #tables currently returns both tables and views.
+ This behavior is deprecated and will be changed with Rails 5.1 to only return tables.
+ Use #data_sources instead.
+ MSG
- execute_and_free(sql, 'SCHEMA') do |result|
- result.collect(&:first)
+ if name
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ Passing arguments to #tables is deprecated without replacement.
+ MSG
end
+
+ data_sources
+ end
+
+ def data_sources
+ sql = "SELECT table_name FROM information_schema.tables "
+ sql << "WHERE table_schema = #{quote(@config[:database])}"
+
+ select_values(sql, 'SCHEMA')
end
def truncate(table_name, name = nil)
execute "TRUNCATE TABLE #{quote_table_name(table_name)}", name
end
- def table_exists?(name)
- return false unless name.present?
- return true if tables(nil, nil, name).any?
+ def table_exists?(table_name)
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ #table_exists? currently checks both tables and views.
+ This behavior is deprecated and will be changed with Rails 5.1 to only check tables.
+ Use #data_source_exists? instead.
+ MSG
- name = name.to_s
- schema, table = name.split('.', 2)
+ data_source_exists?(table_name)
+ end
- unless table # A table was provided without a schema
- table = schema
- schema = nil
- end
+ def data_source_exists?(table_name)
+ return false unless table_name.present?
+
+ schema, name = table_name.to_s.split('.', 2)
+ schema, name = @config[:database], schema unless name # A table was provided without a schema
- tables(nil, schema, table).any?
+ 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
+
+ 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.
@@ -679,7 +695,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 = {
@@ -696,7 +712,7 @@ module ActiveRecord
end
def table_options(table_name)
- create_table_info = select_one("SHOW CREATE TABLE #{quote_table_name(table_name)}")["Create Table"]
+ 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
@@ -706,42 +722,50 @@ module ActiveRecord
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'
- binary_to_sql(limit)
+ def type_to_sql(type, limit = nil, precision = nil, scale = nil, unsigned = nil)
+ sql = case type.to_s
when 'integer'
integer_to_sql(limit)
when 'text'
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
+
+ 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)
@@ -790,6 +814,7 @@ module ActiveRecord
m.register_type %r(longblob)i, Type::Binary.new(limit: 2**32 - 1)
m.register_type %r(^float)i, Type::Float.new(limit: 24)
m.register_type %r(^double)i, Type::Float.new(limit: 53)
+ m.register_type %r(^json)i, MysqlJson.new
register_integer_type m, %r(^bigint)i, limit: 8
register_integer_type m, %r(^int)i, limit: 4
@@ -798,7 +823,6 @@ module ActiveRecord
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'
@@ -807,6 +831,12 @@ module ActiveRecord
.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
def register_integer_type(mapping, key, options) # :nodoc:
@@ -859,9 +889,9 @@ module ActiveRecord
def translate_exception(exception, message)
case error_number(exception)
when 1062
- RecordNotUnique.new(message, exception)
+ RecordNotUnique.new(message)
when 1452
- InvalidForeignKey.new(message, exception)
+ InvalidForeignKey.new(message)
else
super
end
@@ -945,10 +975,6 @@ module ActiveRecord
subselect.from subsubselect.distinct.as('__active_record_temp')
end
- def version
- @version ||= Version.new(full_version.match(/^\d+\.\d+\.\d+/)[0])
- end
-
def mariadb?
full_version =~ /mariadb/i
end
@@ -971,14 +997,14 @@ module ActiveRecord
defaults = [':default', :default].to_set
# Make MySQL reject illegal values rather than truncating or blanking them, see
- # http://dev.mysql.com/doc/refman/5.6/en/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') || 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.6/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)
if @config[:encoding]
encoding = "NAMES #{@config[:encoding]}"
@@ -1009,17 +1035,13 @@ module ActiveRecord
end
end
- def create_table_definition(name, temporary = false, options = nil, as = nil) # :nodoc:
- TableDefinition.new(native_database_types, name, temporary, options, as)
+ 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 binary_to_sql(limit) # :nodoc:
- 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 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:
@@ -1027,8 +1049,9 @@ module ActiveRecord
when 1; 'tinyint'
when 2; 'smallint'
when 3; 'mediumint'
- when nil, 4, 11; 'int(11)' # compatibility with MySQL default
+ 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
@@ -1039,7 +1062,25 @@ module ActiveRecord
when nil, 0x100..0xffff; 'text'
when 0x10000..0xffffff; 'mediumtext'
when 0x1000000..0xffffffff; 'longtext'
- else raise(ActiveRecordError, "No text type has character length #{limit}")
+ 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
@@ -1063,8 +1104,12 @@ module ActiveRecord
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 4b95b0681d..81de7c03fb 100644
--- a/activerecord/lib/active_record/connection_adapters/column.rb
+++ b/activerecord/lib/active_record/connection_adapters/column.rb
@@ -5,25 +5,18 @@ module ActiveRecord
module ConnectionAdapters
# An abstract definition of a column in a table.
class Column
- FALSE_VALUES = [false, 0, '0', 'f', 'F', 'false', 'FALSE', 'off', 'OFF'].to_set
-
- 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, :null, :sql_type_metadata, :default, :default_function, :collation
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>.
# +sql_type_metadata+ is various information about the type of the column
# +null+ determines if this column allows +NULL+ values.
def initialize(name, default, sql_type_metadata = nil, null = true, default_function = nil, collation = nil)
- @name = name
+ @name = name.freeze
@sql_type_metadata = sql_type_metadata
@null = null
@default = default
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..ca7dfda80d
--- /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 && !options.key?(:default)
+ 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..9dee3172f4
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb
@@ -0,0 +1,59 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module MySQL
+ module ColumnDumper
+ def column_spec_for_primary_key(column)
+ spec = {}
+ if column.bigint?
+ spec[:id] = ':bigint'
+ spec[:default] = schema_default(column) || 'nil' unless column.auto_increment?
+ spec[:unsigned] = 'true' if column.unsigned?
+ elsif column.auto_increment?
+ 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 e97e82f056..773ecbe126 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.18'
+gem 'mysql2', '>= 0.3.18', '< 0.5'
require 'mysql2'
module ActiveRecord
@@ -20,7 +20,7 @@ module ActiveRecord
ConnectionAdapters::Mysql2Adapter.new(client, logger, options, config)
rescue Mysql2::Error => error
if error.message.include?("Unknown database")
- raise ActiveRecord::NoDatabaseError.new(error.message, error)
+ raise ActiveRecord::NoDatabaseError
else
raise
end
@@ -37,8 +37,8 @@ module ActiveRecord
configure_connection
end
- def supports_explain?
- true
+ def supports_json?
+ version >= '5.7.8'
end
# HELPER METHODS ===========================================
@@ -95,80 +95,6 @@ module ActiveRecord
# 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
#
# The overrides below perform much better than the originals in AbstractAdapter
@@ -200,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.
@@ -214,8 +142,9 @@ 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
@@ -254,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 2ae462d773..89d18ee14e 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
@@ -38,7 +38,7 @@ module ActiveRecord
ConnectionAdapters::MysqlAdapter.new(mysql, logger, options, config)
rescue Mysql::Error => error
if error.message.include?("Unknown database")
- raise ActiveRecord::NoDatabaseError.new(error.message, error)
+ raise ActiveRecord::NoDatabaseError
else
raise
end
@@ -58,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.6/en/auto-reconnect.html).
- # * <tt>:strict</tt> - Defaults to true. Enable STRICT_ALL_TABLES. (See MySQL documentation: http://dev.mysql.com/doc/refman/5.6/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.6/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.
@@ -80,8 +80,7 @@ module ActiveRecord
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
@@ -162,6 +161,14 @@ module ActiveRecord
# 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
rows = exec_query(sql, name, binds).rows
@@ -223,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?
@@ -371,12 +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 { |attr| type_cast(attr.value_for_database) }
log(sql, name, binds) do
- if binds.empty?
+ if !cache_stmt
stmt = @connection.prepare(sql)
else
cache = @statements[sql] ||= {
@@ -392,7 +399,7 @@ module ActiveRecord
# place when an error occurs. To support older MySQL versions, we
# need to close the statement and delete the statement from the
# cache.
- if binds.empty?
+ if !cache_stmt
stmt.close
else
@statements.delete sql
@@ -410,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
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 11d3f5301a..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,8 +156,8 @@ module ActiveRecord
end
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|
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 2c04c46131..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
@@ -4,18 +4,14 @@ module ActiveRecord
module OID # :nodoc:
class DateTime < Type::DateTime # :nodoc:
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/json.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb
index 8e1256baad..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::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
+ class Json < Type::Internal::AbstractJson
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 191c828e60..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
@@ -36,7 +36,7 @@ module ActiveRecord
WHERE
t.typname IN (%s)
OR t.typtype IN (%s)
- OR t.typinput::varchar = 'array_in'
+ OR t.typinput = 'array_in(cstring,oid,integer)'::regprocedure
OR t.typelem != 0
SQL
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
index f175730551..d5879ea7df 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
@@ -31,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
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 022dbdfa27..6399bddbee 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb
@@ -103,6 +103,30 @@ module ActiveRecord
args.each { |name| column(name, :point, options) }
end
+ def line(*args, **options)
+ args.each { |name| column(name, :line, options) }
+ end
+
+ def lseg(*args, **options)
+ args.each { |name| column(name, :lseg, options) }
+ end
+
+ def box(*args, **options)
+ args.each { |name| column(name, :box, options) }
+ end
+
+ def path(*args, **options)
+ args.each { |name| column(name, :path, options) }
+ end
+
+ def polygon(*args, **options)
+ args.each { |name| column(name, :polygon, options) }
+ end
+
+ def circle(*args, **options)
+ args.each { |name| column(name, :circle, options) }
+ end
+
def serial(*args, **options)
args.each { |name| column(name, :serial, options) }
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb
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 d114cad16b..a48d64f7bd 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
@@ -68,15 +68,41 @@ 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)
+ if name
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ Passing arguments to #tables is deprecated without replacement.
+ MSG
+ end
+
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
+
# Returns true if table exists.
# If the schema is not specified as part of +name+ then it will only find tables within
# the current schema search path (regardless of permissions to access tables in other schemas)
def table_exists?(name)
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ #table_exists? currently checks both tables and views.
+ This behavior is deprecated and will be changed with Rails 5.1 to only check tables.
+ Use #data_source_exists? instead.
+ MSG
+
+ data_source_exists?(name)
+ end
+
+ def data_source_exists?(name)
name = Utils.extract_schema_qualified_name(name.to_s)
return false unless name.identifier
@@ -90,6 +116,30 @@ module ActiveRecord
SQL
end
+ 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
@@ -101,15 +151,19 @@ module ActiveRecord
# Verifies existence of an index with a given name.
def index_name_exists?(table_name, index_name, default)
+ 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
@@ -210,12 +264,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.
@@ -349,17 +403,19 @@ module ActiveRecord
nil
end
- # Returns just a table's primary key
- def primary_key(table)
- pks = query(<<-end_sql, 'SCHEMA')
- SELECT attr.attname
- FROM pg_attribute attr
- INNER JOIN pg_constraint cons ON attr.attrelid = cons.conrelid AND attr.attnum = any(cons.conkey)
- WHERE cons.contype = 'p'
- AND cons.conrelid = '#{quote_table_name(table)}'::regclass
- end_sql
- return nil unless pks.count == 1
- pks[0][0]
+ 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.
@@ -376,7 +432,7 @@ module ActiveRecord
new_seq = "#{new_name}_#{pk}_seq"
idx = "#{table_name}_pkey"
new_idx = "#{new_name}_pkey"
- execute "ALTER TABLE #{quote_table_name(seq)} RENAME TO #{quote_table_name(new_seq)}"
+ 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
@@ -447,8 +503,15 @@ 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
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
index 2c43c46a3d..719592349b 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
@@ -9,6 +9,7 @@ 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"
@@ -18,12 +19,6 @@ require 'ipaddr'
module ActiveRecord
module ConnectionHandling # :nodoc:
- VALID_CONN_PARAMS = [:host, :hostaddr, :port, :dbname, :user, :password, :connect_timeout,
- :client_encoding, :options, :application_name, :fallback_application_name,
- :keepalives, :keepalives_idle, :keepalives_interval, :keepalives_count,
- :tty, :sslmode, :requiressl, :sslcompression, :sslcert, :sslkey,
- :sslrootcert, :sslcrl, :requirepeer, :krbsrvname, :gsslib, :service]
-
# Establishes a connection to the database that's used by all Active Record objects
def postgresql_connection(config)
conn_params = config.symbolize_keys
@@ -35,7 +30,8 @@ module ActiveRecord
conn_params[:dbname] = conn_params.delete(:database) if conn_params[:database]
# Forward only valid config params to PGconn.connect.
- conn_params.keep_if { |k, _| VALID_CONN_PARAMS.include?(k) }
+ valid_conn_param_keys = PGconn.conndefaults_hash.keys + [:requiressl]
+ conn_params.slice!(*valid_conn_param_keys)
# The postgres drivers don't allow the creation of an unconnected PGconn object,
# so just pass a nil connection object for the time being.
@@ -76,7 +72,6 @@ module ActiveRecord
NATIVE_DATABASE_TYPES = {
primary_key: "serial primary key",
- bigserial: "bigserial",
string: { name: "character varying" },
text: { name: "text" },
integer: { name: "integer" },
@@ -93,7 +88,6 @@ module ActiveRecord
int8range: { name: "int8range" },
binary: { name: "bytea" },
boolean: { name: "boolean" },
- bigint: { name: "bigint" },
xml: { name: "xml" },
tsvector: { name: "tsvector" },
hstore: { name: "hstore" },
@@ -117,61 +111,14 @@ module ActiveRecord
include PostgreSQL::ReferentialIntegrity
include PostgreSQL::SchemaStatements
include PostgreSQL::DatabaseStatements
+ include PostgreSQL::ColumnDumper
include Savepoints
def schema_creation # :nodoc:
PostgreSQL::SchemaCreation.new self
end
- 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 provided by the
- # AbstractAdapter
- def prepare_column_options(column) # :nodoc:
- spec = super
- spec[:array] = 'true' if column.array?
- spec
- end
-
- # Adds +:array+ as a valid migration key
- def migration_keys
- super + [:array]
- end
-
- def schema_type(column)
- return super unless column.serial?
-
- if column.bigint?
- 'bigserial'
- else
- 'serial'
- end
- end
- private :schema_type
-
- def schema_default(column)
- if column.default_function
- column.default_function.inspect unless column.serial?
- else
- super
- end
- end
- private :schema_default
-
- # Returns +true+, since this connection adapter supports prepared statement
+ # Returns true, since this connection adapter supports prepared statement
# caching.
def supports_statement_cache?
true
@@ -201,13 +148,18 @@ module ActiveRecord
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
end
@@ -239,6 +191,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
@@ -321,18 +274,18 @@ module ActiveRecord
true
end
- # Enable standard-conforming strings if available.
def set_standard_conforming_strings
- old, self.client_min_messages = client_min_messages, 'panic'
- execute('SET standard_conforming_strings = on', 'SCHEMA') rescue nil
- ensure
- self.client_min_messages = old
+ execute('SET standard_conforming_strings = on', 'SCHEMA')
end
def supports_ddl_transactions?
true
end
+ def supports_advisory_locks?
+ true
+ end
+
def supports_explain?
true
end
@@ -351,6 +304,20 @@ module ActiveRecord
postgresql_version >= 90300
end
+ def get_advisory_lock(lock_id) # :nodoc:
+ unless lock_id.is_a?(Integer) && lock_id.bit_length <= 63
+ raise(ArgumentError, "Postgres requires advisory lock ids to be a signed 64 bit integer")
+ end
+ select_value("SELECT pg_try_advisory_lock(#{lock_id});")
+ end
+
+ def release_advisory_lock(lock_id) # :nodoc:
+ unless lock_id.is_a?(Integer) && lock_id.bit_length <= 63
+ raise(ArgumentError, "Postgres requires advisory lock ids to be a signed 64 bit integer")
+ end
+ select_value("SELECT pg_advisory_unlock(#{lock_id})")
+ end
+
def enable_extension(name)
exec_query("CREATE EXTENSION IF NOT EXISTS \"#{name}\"").tap {
reload_type_map
@@ -433,9 +400,9 @@ module ActiveRecord
case exception.result.try(:error_field, PGresult::PG_DIAG_SQLSTATE)
when UNIQUE_VIOLATION
- RecordNotUnique.new(message, exception)
+ RecordNotUnique.new(message)
when FOREIGN_KEY_VIOLATION
- InvalidForeignKey.new(message, exception)
+ InvalidForeignKey.new(message)
else
super
end
@@ -594,16 +561,22 @@ module ActiveRecord
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)
@@ -614,7 +587,7 @@ module ActiveRecord
@connection.exec_prepared(stmt_key, type_casted_binds)
end
rescue ActiveRecord::StatementInvalid => e
- pgerror = e.original_exception
+ pgerror = e.cause
# Get the PG code for the failure. Annoyingly, the code for
# prepared statements whose return value may have changed is
@@ -670,7 +643,7 @@ module ActiveRecord
configure_connection
rescue ::PG::Error => error
if error.message.include?("does not exist")
- raise ActiveRecord::NoDatabaseError.new(error.message, error)
+ raise ActiveRecord::NoDatabaseError
else
raise
end
@@ -685,7 +658,7 @@ module ActiveRecord
self.client_min_messages = @config[:min_messages] || 'warning'
self.schema_search_path = @config[:schema_search_path] || @config[:schema_order]
- # Use standard-conforming strings if available so we don't have to do the E'...' dance.
+ # Use standard-conforming strings so we don't have to do the E'...' dance.
set_standard_conforming_strings
# If using Active Record's time zone support configure the connection to return
@@ -756,7 +729,7 @@ 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
diff --git a/activerecord/lib/active_record/connection_adapters/schema_cache.rb b/activerecord/lib/active_record/connection_adapters/schema_cache.rb
index 981d5d7a3c..eee142378c 100644
--- a/activerecord/lib/active_record/connection_adapters/schema_cache.rb
+++ b/activerecord/lib/active_record/connection_adapters/schema_cache.rb
@@ -10,7 +10,7 @@ module ActiveRecord
@columns = {}
@columns_hash = {}
@primary_keys = {}
- @tables = {}
+ @data_sources = {}
end
def initialize_dup(other)
@@ -18,33 +18,38 @@ module ActiveRecord
@columns = @columns.dup
@columns_hash = @columns_hash.dup
@primary_keys = @primary_keys.dup
- @tables = @tables.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)
- prepare_tables if @tables.empty?
- 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)
@@ -64,36 +69,38 @@ 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(&: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_tables
- connection.tables.each { |table| @tables[table] = true }
+ def prepare_data_sources
+ connection.data_sources.each { |source| @data_sources[source] = true }
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 358039723f..90df9b8825 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
@@ -33,7 +33,7 @@ module ActiveRecord
ConnectionAdapters::SQLite3Adapter.new(db, logger, nil, config)
rescue Errno::ENOENT => error
if error.message.include?("No such file or directory")
- raise ActiveRecord::NoDatabaseError.new(error.message, error)
+ raise ActiveRecord::NoDatabaseError
else
raise
end
@@ -81,8 +81,7 @@ module ActiveRecord
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
@@ -90,6 +89,7 @@ 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
@@ -219,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)
@@ -232,15 +232,18 @@ module ActiveRecord
end
end
- def exec_query(sql, name = nil, binds = [])
+ def exec_query(sql, name = nil, binds = [], prepare: false)
type_casted_binds = binds.map { |attr| type_cast(attr.value_for_database) }
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
@@ -253,7 +256,7 @@ module ActiveRecord
stmt = cache[:stmt]
cols = cache[:cols] ||= stmt.columns
stmt.reset!
- stmt.bind_params type_casted_binds
+ stmt.bind_params(type_casted_binds)
end
ActiveRecord::Result.new(cols, stmt.to_a)
@@ -308,21 +311,56 @@ module ActiveRecord
# SCHEMA STATEMENTS ========================================
- def tables(name = nil, table_name = nil) #:nodoc:
- sql = <<-SQL
- SELECT name
- FROM sqlite_master
- WHERE (type = 'table' OR type = 'view') 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']
+ def tables(name = nil) # :nodoc:
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ #tables currently returns both tables and views.
+ This behavior is deprecated and will be changed with Rails 5.1 to only return tables.
+ Use #data_sources instead.
+ MSG
+
+ if name
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ Passing arguments to #tables is deprecated without replacement.
+ MSG
end
+
+ data_sources
+ end
+
+ def data_sources
+ select_values("SELECT name FROM sqlite_master WHERE type IN ('table','view') AND name <> 'sqlite_sequence'", 'SCHEMA')
end
def table_exists?(table_name)
- table_name && tables(nil, table_name).any?
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ #table_exists? currently checks both tables and views.
+ This behavior is deprecated and will be changed with Rails 5.1 to only check tables.
+ Use #data_source_exists? instead.
+ MSG
+
+ data_source_exists?(table_name)
+ end
+
+ def data_source_exists?(table_name)
+ 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
+
+ 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+.
@@ -369,13 +407,13 @@ module ActiveRecord
end
end
- def primary_key(table_name) #:nodoc:
+ def primary_keys(table_name) # :nodoc:
pks = table_structure(table_name).select { |f| f['pk'] > 0 }
- return nil unless pks.count == 1
- pks[0]['name']
+ 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
@@ -545,7 +583,7 @@ module ActiveRecord
# Older versions of SQLite return:
# column *column_name* is not unique
when /column(s)? .* (is|are) not unique/, /UNIQUE constraint failed: .*/
- RecordNotUnique.new(message, exception)
+ RecordNotUnique.new(message)
else
super
end
diff --git a/activerecord/lib/active_record/connection_adapters/statement_pool.rb b/activerecord/lib/active_record/connection_adapters/statement_pool.rb
index 82e9ef3d3d..57463dd749 100644
--- a/activerecord/lib/active_record/connection_adapters/statement_pool.rb
+++ b/activerecord/lib/active_record/connection_adapters/statement_pool.rb
@@ -3,10 +3,9 @@ module ActiveRecord
class StatementPool
include Enumerable
- def initialize(connection, max = 1000)
+ def initialize(max = 1000)
@cache = Hash.new { |h,pid| h[pid] = {} }
- @connection = connection
- @max = max
+ @max = max
end
def each(&block)
diff --git a/activerecord/lib/active_record/connection_handling.rb b/activerecord/lib/active_record/connection_handling.rb
index d6b661ff76..aedef54928 100644
--- a/activerecord/lib/active_record/connection_handling.rb
+++ b/activerecord/lib/active_record/connection_handling.rb
@@ -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
diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb
index ffce2173ec..1250f8a3c3 100644
--- a/activerecord/lib/active_record/core.rb
+++ b/activerecord/lib/active_record/core.rb
@@ -177,7 +177,7 @@ module ActiveRecord
hash = args.first
return super if hash.values.any? { |v|
- v.nil? || Array === v || Hash === v
+ v.nil? || Array === v || Hash === v || Relation === v
}
# We can't cache Post.find_by(author: david) ...yet
@@ -193,8 +193,8 @@ module ActiveRecord
}
begin
statement.execute(hash.values, self, connection).first
- rescue TypeError => e
- raise ActiveRecord::StatementInvalid.new(e.message, e)
+ rescue TypeError
+ raise ActiveRecord::StatementInvalid
rescue RangeError
nil
end
@@ -296,7 +296,7 @@ module ActiveRecord
# # Instantiates a single new object
# User.new(first_name: 'Jamie')
def initialize(attributes = nil)
- @attributes = self.class._default_attributes.dup
+ @attributes = self.class._default_attributes.deep_dup
self.class.define_attribute_methods
init_internals
@@ -310,7 +310,7 @@ module ActiveRecord
# Initialize an empty model object from +coder+. +coder+ should be
# the result of previously encoding an Active Record model, using
- # `encode_with`
+ # #encode_with.
#
# class Post < ActiveRecord::Base
# end
@@ -366,7 +366,7 @@ module ActiveRecord
##
def initialize_dup(other) # :nodoc:
- @attributes = @attributes.dup
+ @attributes = @attributes.deep_dup
@attributes.reset(self.class.primary_key)
_run_initialize_callbacks
@@ -379,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:
@@ -477,7 +477,7 @@ 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?
diff --git a/activerecord/lib/active_record/counter_cache.rb b/activerecord/lib/active_record/counter_cache.rb
index 82596b63df..9e7d391c70 100644
--- a/activerecord/lib/active_record/counter_cache.rb
+++ b/activerecord/lib/active_record/counter_cache.rb
@@ -45,14 +45,14 @@ module ActiveRecord
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.
#
@@ -86,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
#
@@ -105,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
#
diff --git a/activerecord/lib/active_record/enum.rb b/activerecord/lib/active_record/enum.rb
index c0d9d9c1c8..7ded96f8fb 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
@@ -47,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
@@ -61,7 +60,7 @@ 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
@@ -75,22 +74,24 @@ module ActiveRecord
#
# Conversation.where("status <> ?", Conversation.statuses[:archived])
#
- # You can use the +:enum_prefix+ or +:enum_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.
+ # 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 Invoice < ActiveRecord::Base
- # enum verification: [:done, :fail], enum_prefix: true
+ # class Conversation < ActiveRecord::Base
+ # enum status: [:active, :archived], _suffix: true
+ # enum comments_status: [:active, :inactive], _prefix: :comments
# end
#
- # It is also possible to supply a custom prefix.
+ # With the above example, the bang and predicate methods along with the
+ # associated scopes are now prefixed and/or suffixed accordingly:
#
- # class Invoice < ActiveRecord::Base
- # enum verification: [:done, :fail], enum_prefix: :verification_status
- # end
+ # conversation.active_status!
+ # conversation.archived_status? # => false
#
- # Note that <tt>:enum_prefix</tt>/<tt>:enum_suffix</tt> are reserved keywords
- # and can not be used as an enum name.
+ # conversation.comments_inactive!
+ # conversation.comments_active? # => false
module Enum
def self.extended(base) # :nodoc:
@@ -103,7 +104,7 @@ module ActiveRecord
super
end
- class EnumType < Type::Value
+ class EnumType < Type::Value # :nodoc:
def initialize(name, mapping)
@name = name
@mapping = mapping
@@ -117,7 +118,7 @@ module ActiveRecord
elsif mapping.has_value?(value)
mapping.key(value)
else
- raise ArgumentError, "'#{value}' is not a valid #{name}"
+ assert_valid_value(value)
end
end
@@ -130,6 +131,12 @@ module ActiveRecord
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
@@ -137,8 +144,8 @@ module ActiveRecord
def enum(definitions)
klass = self
- enum_prefix = definitions.delete(:enum_prefix)
- enum_suffix = definitions.delete(:enum_suffix)
+ enum_prefix = definitions.delete(:_prefix)
+ enum_suffix = definitions.delete(:_suffix)
definitions.each do |name, values|
# statuses = { }
enum_values = ActiveSupport::HashWithIndifferentAccess.new
@@ -203,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 d589620f8a..1cd2c2ef8c 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,12 +42,13 @@ 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
@@ -58,18 +61,21 @@ module ActiveRecord
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, record = nil)
+ 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!
@@ -80,7 +86,7 @@ module ActiveRecord
class RecordNotDestroyed < ActiveRecordError
attr_reader :record
- def initialize(message, record = nil)
+ def initialize(message = nil, record = nil)
@record = record
super(message)
end
@@ -88,18 +94,26 @@ module ActiveRecord
# Superclass for all database execution errors.
#
- # Wraps the underlying database error as +original_exception+.
+ # Wraps the underlying database error as +cause+.
class StatementInvalid < ActiveRecordError
- attr_reader :original_exception
- def initialize(message, original_exception = nil)
- super(message)
- @original_exception = original_exception
+ def initialize(message = nil, original_exception = nil)
+ if original_exception
+ ActiveSupport::Deprecation.warn("Passing #original_exception is deprecated and has no effect. " \
+ "Exceptions will automatically capture the original exception.", caller)
+ end
+
+ super(message || $!.try(:message))
+ end
+
+ def original_exception
+ ActiveSupport::Deprecation.warn("#original_exception is deprecated. Use #cause instead.", caller)
+ cause
end
end
# Defunct wrapper class kept for compatibility.
- # +StatementInvalid+ wraps the original exception now.
+ # StatementInvalid wraps the original exception now.
class WrappedDatabaseException < StatementInvalid
end
@@ -112,8 +126,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:
#
@@ -134,16 +148,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
@@ -152,9 +172,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.
@@ -191,25 +212,26 @@ module ActiveRecord
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
@@ -218,11 +240,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 9adabd7819..90bcf5a205 100644
--- a/activerecord/lib/active_record/explain_subscriber.rb
+++ b/activerecord/lib/active_record/explain_subscriber.rb
@@ -14,7 +14,7 @@ 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.
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 d062dd9e34..deb7d7d06d 100644
--- a/activerecord/lib/active_record/fixtures.rb
+++ b/activerecord/lib/active_record/fixtures.rb
@@ -89,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
@@ -124,7 +124,7 @@ 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`
# module FixtureFileHelpers
@@ -395,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
@@ -578,21 +592,16 @@ module ActiveRecord
@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)
@@ -761,13 +770,25 @@ module ActiveRecord
@column_names ||= @connection.columns(@table_name).collect(&:name)
end
- def read_fixture_files(path, model_class)
+ 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
+
+ # 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
@@ -827,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
@@ -854,9 +875,7 @@ module ActiveRecord
self.pre_loaded_fixtures = false
self.config = ActiveRecord::Base
- 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
+ self.fixture_class_names = {}
silence_warnings do
define_singleton_method :use_transactional_tests do
diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb
index e613d157aa..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
@@ -82,7 +82,7 @@ module ActiveRecord
# 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
@@ -167,17 +167,23 @@ 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)
@@ -199,15 +205,7 @@ module ActiveRecord
subclass_name = attrs.with_indifferent_access[inheritance_column]
if subclass_name.present?
- subclass = find_sti_class(subclass_name)
-
- if subclass.name != self.name
- unless descendants.include?(subclass)
- raise ActiveRecord::SubclassNotFound.new("Invalid single-table inheritance type: #{subclass.name} is not a subclass of #{name}")
- end
-
- subclass
- end
+ 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 15b2f65dcb..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.
#
@@ -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/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb
index a09437b4b0..2336d23a1c 100644
--- a/activerecord/lib/active_record/locking/optimistic.rb
+++ b/activerecord/lib/active_record/locking/optimistic.rb
@@ -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.
diff --git a/activerecord/lib/active_record/locking/pessimistic.rb b/activerecord/lib/active_record/locking/pessimistic.rb
index 3d95c54ef3..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.6/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 4d597a0ab1..b63caa4473 100644
--- a/activerecord/lib/active_record/log_subscriber.rb
+++ b/activerecord/lib/active_record/log_subscriber.rb
@@ -47,20 +47,40 @@ module ActiveRecord
binds = " " + payload[:binds].map { |attr| render_bind(attr) }.inspect
end
- name = color(name, nil, true)
+ name = colorize_payload_name(name, payload[:name])
sql = color(sql, sql_color(sql), true)
debug " #{name} #{sql}#{binds}"
end
+ 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 /\s*\Ainsert/i then GREEN
- when /\s*\Aselect/i then BLUE
- when /\s*\Aupdate/i then YELLOW
- when /\s*\Adelete/i then RED
- when /transaction\s*\Z/i then CYAN
- else MAGENTA
+ 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
diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb
index 4cfda302ea..ca2537cdc3 100644
--- a/activerecord/lib/active_record/migration.rb
+++ b/activerecord/lib/active_record/migration.rb
@@ -9,44 +9,140 @@ 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.env)
- 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
+ class ConcurrentMigrationError < MigrationError #:nodoc:
+ DEFAULT_MESSAGE = "Cannot run migrations because another migration process is currently running.".freeze
+
+ def initialize(message = DEFAULT_MESSAGE)
+ super
+ end
+ end
+
# = Active Record Migrations
#
# Migrations can manage the evolution of a schema used by several physical
@@ -106,17 +202,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,24 +224,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>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>add_reference(:table_name, :reference_name)</tt>: Adds a new column
- # +reference_name_id+ by default a integer. See
- # ActiveRecord::ConnectionAdapters::SchemaStatements#add_reference for details.
+ # * <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
#
@@ -168,7 +300,7 @@ module ActiveRecord
#
# 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 change
# add_column :tablenames, :fieldname, :string
@@ -345,6 +477,7 @@ module ActiveRecord
class Migration
autoload :CommandRecorder, 'active_record/migration/command_recorder'
+ MigrationFilenameRegexp = /\A([0-9]+)_([_a-z0-9]*)\.?([_a-z0-9]*)?\.rb\z/ # :nodoc:
# This class is used to verify that all migrations have been run before
# loading a web page if config.active_record.migration_error is set to :page_load
@@ -376,6 +509,7 @@ 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
@@ -408,7 +542,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
@@ -455,7 +592,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
@@ -471,13 +608,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|
@@ -488,7 +625,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:
@@ -545,7 +682,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
@@ -637,7 +774,7 @@ module ActiveRecord
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) ||
@@ -716,7 +853,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
@@ -808,7 +947,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?
@@ -828,10 +967,12 @@ module ActiveRecord
end
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
- []
+ ActiveSupport::Deprecation.silence do
+ if connection.table_exists?(schema_migrations_table_name)
+ SchemaMigration.all.map { |x| x.version.to_i }.sort
+ else
+ []
+ end
end
end
@@ -847,22 +988,22 @@ module ActiveRecord
migrations(migrations_paths).any?
end
- def last_version
- last_migration.version
- end
-
def last_migration #:nodoc:
migrations(migrations_paths).last || NullMigration.new
end
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
+ def match_to_migration_filename?(filename) # :nodoc:
+ File.basename(filename) =~ Migration::MigrationFilenameRegexp
+ end
+
+ def parse_migration_filename(filename) # :nodoc:
+ File.basename(filename).scan(Migration::MigrationFilenameRegexp).first
end
def migrations(paths)
@@ -871,8 +1012,7 @@ module ActiveRecord
files = Dir[*paths.map { |p| "#{p}/**/[0-9]*_*.rb" }]
migrations = files.map do |file|
- version, name, scope = file.scan(/([0-9]+)_([_a-z0-9]*)\.?([_a-z0-9]*)?\.rb\z/).first
-
+ version, name, scope = parse_migration_filename(file)
raise IllegalMigrationNameError.new(file) unless version
version = version.to_i
name = name.camelize
@@ -920,32 +1060,18 @@ module ActiveRecord
alias :current :current_migration
def run
- migration = migrations.detect { |m| m.version == @target_version }
- raise UnknownMigrationVersionError.new(@target_version) if migration.nil?
- unless (up? && migrated.include?(migration.version.to_i)) || (down? && !migrated.include?(migration.version.to_i))
- begin
- execute_migration_in_transaction(migration, @direction)
- rescue => e
- canceled_msg = use_transaction?(migration) ? ", this migration was canceled" : ""
- raise StandardError, "An error has occurred#{canceled_msg}:\n\n#{e}", e.backtrace
- end
+ if use_advisory_lock?
+ with_advisory_lock { run_without_lock }
+ else
+ run_without_lock
end
end
def migrate
- if !target && @target_version && @target_version > 0
- raise UnknownMigrationVersionError.new(@target_version)
- end
-
- runnable.each do |migration|
- Base.logger.info "Migrating to #{migration.name} (#{migration.version})" if Base.logger
-
- begin
- execute_migration_in_transaction(migration, @direction)
- rescue => e
- canceled_msg = use_transaction?(migration) ? "this and " : ""
- raise StandardError, "An error has occurred, #{canceled_msg}all later migrations canceled:\n\n#{e}", e.backtrace
- end
+ if use_advisory_lock?
+ with_advisory_lock { migrate_without_lock }
+ else
+ migrate_without_lock
end
end
@@ -970,10 +1096,45 @@ module ActiveRecord
end
def migrated
- @migrated_versions ||= Set.new(self.class.get_all_versions)
+ @migrated_versions || load_migrated
+ end
+
+ def load_migrated
+ @migrated_versions = Set.new(self.class.get_all_versions)
end
private
+
+ def run_without_lock
+ migration = migrations.detect { |m| m.version == @target_version }
+ raise UnknownMigrationVersionError.new(@target_version) if migration.nil?
+ unless (up? && migrated.include?(migration.version.to_i)) || (down? && !migrated.include?(migration.version.to_i))
+ begin
+ execute_migration_in_transaction(migration, @direction)
+ rescue => e
+ canceled_msg = use_transaction?(migration) ? ", this migration was canceled" : ""
+ raise StandardError, "An error has occurred#{canceled_msg}:\n\n#{e}", e.backtrace
+ end
+ end
+ end
+
+ def migrate_without_lock
+ if !target && @target_version && @target_version > 0
+ raise UnknownMigrationVersionError.new(@target_version)
+ end
+
+ runnable.each do |migration|
+ Base.logger.info "Migrating to #{migration.name} (#{migration.version})" if Base.logger
+
+ begin
+ execute_migration_in_transaction(migration, @direction)
+ rescue => e
+ canceled_msg = use_transaction?(migration) ? "this and " : ""
+ raise StandardError, "An error has occurred, #{canceled_msg}all later migrations canceled:\n\n#{e}", e.backtrace
+ end
+ end
+ end
+
def ran?(migration)
migrated.include?(migration.version.to_i)
end
@@ -1035,5 +1196,25 @@ module ActiveRecord
def use_transaction?(migration)
!migration.disable_ddl_transaction && Base.connection.supports_ddl_transactions?
end
+
+ def use_advisory_lock?
+ Base.connection.supports_advisory_locks?
+ end
+
+ def with_advisory_lock
+ lock_id = generate_migrator_advisory_lock_id
+ got_lock = Base.connection.get_advisory_lock(lock_id)
+ raise ConcurrentMigrationError unless got_lock
+ load_migrated # reload schema_migrations to be sure it wasn't changed by another process before we got the lock
+ yield
+ ensure
+ Base.connection.release_advisory_lock(lock_id) if got_lock
+ end
+
+ MIGRATOR_SALT = 2053462845
+ def generate_migrator_advisory_lock_id
+ db_name_hash = Zlib.crc32(Base.connection.current_database)
+ MIGRATOR_SALT * db_name_hash
+ end
end
end
diff --git a/activerecord/lib/active_record/migration/command_recorder.rb b/activerecord/lib/active_record/migration/command_recorder.rb
index dcc2362397..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,
- :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)
diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb
index 5a6f42ba09..e3f304b0af 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
@@ -213,7 +220,7 @@ 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:
@@ -240,7 +247,7 @@ module ActiveRecord
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
load_schema
_default_attributes.to_hash
@@ -290,7 +297,7 @@ module ActiveRecord
def reset_column_information
connection.clear_cache!
undefine_attribute_methods
- connection.schema_cache.clear_table_cache!(table_name)
+ connection.schema_cache.clear_data_source_cache!(table_name)
reload_schema_from_cache
end
@@ -308,7 +315,7 @@ module ActiveRecord
end
def load_schema!
- @columns_hash = connection.schema_cache.columns_hash(table_name)
+ @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(
@@ -332,6 +339,9 @@ module ActiveRecord
@columns = nil
@columns_hash = nil
@attribute_names = nil
+ direct_descendants.each do |descendant|
+ descendant.send(:reload_schema_from_cache)
+ end
end
# Guesses the table name, but does not decorate it with prefix and suffix information.
diff --git a/activerecord/lib/active_record/null_relation.rb b/activerecord/lib/active_record/null_relation.rb
index 74894d0c37..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
diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb
index 6f708182ec..46c6d8c293 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
@@ -106,7 +106,7 @@ 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.
#
@@ -132,7 +132,7 @@ module ActiveRecord
# 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.
#
@@ -158,7 +158,7 @@ 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 <tt>readonly?</tt>.
+ # 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
@@ -193,7 +193,7 @@ module ActiveRecord
# and #destroy! raises ActiveRecord::RecordNotDestroyed.
# See ActiveRecord::Callbacks for further details.
def destroy!
- destroy || raise(RecordNotDestroyed.new("Failed to destroy the record", self))
+ destroy || _raise_record_not_destroyed
end
# Returns an instance of the specified +klass+ with the attributes of the
@@ -207,10 +207,11 @@ module ActiveRecord
# Note: The new instance will share a link to the same attributes as the original class.
# 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.
+ # 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("@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?)
@@ -218,7 +219,7 @@ module ActiveRecord
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.
#
@@ -238,14 +239,14 @@ 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)
@@ -267,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
@@ -294,11 +295,11 @@ 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 a new record" if new_record?
@@ -326,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
@@ -368,7 +371,7 @@ module ActiveRecord
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.
@@ -391,7 +394,7 @@ module ActiveRecord
# Attributes are reloaded from the database, and caches busted, in
# 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.
#
@@ -453,8 +456,8 @@ module ActiveRecord
# 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
@@ -556,5 +559,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 4e597590e9..1f429cfd94 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, :or,
+ delegate :find_each, :find_in_batches, :in_batches, to: :all
+ delegate :select, :group, :order, :except, :reorder, :limit, :offset, :joins, :left_joins, :left_outer_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
diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb
index da6b8447d3..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,
@@ -78,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
@@ -121,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
diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake
index 6a72d528b4..5b1ea16f0b 100644
--- a/activerecord/lib/active_record/railties/databases.rake
+++ b/activerecord/lib/active_record/railties/databases.rake
@@ -34,7 +34,7 @@ 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
@@ -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,13 +99,15 @@ db_namespace = namespace :db do
db_list = ActiveRecord::SchemaMigration.normalized_versions
file_list =
- ActiveRecord::Migrator.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)
+ ActiveRecord::Tasks::DatabaseTasks.migrations_paths.flat_map do |path|
+ Dir.foreach(path).map do |file|
+ next unless ActiveRecord::Migrator.match_to_migration_filename?(file)
+
+ version, name, scope = ActiveRecord::Migrator.parse_migration_filename(file)
+ version = ActiveRecord::SchemaMigration.normalize_migration_number(version)
status = db_list.delete(version) ? 'up' : 'down'
- [status, version, $2.humanize]
- end
+ [status, version, (name + scope).humanize]
+ end.compact
end
db_list.map! do |version|
@@ -125,14 +127,14 @@ 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
@@ -160,7 +162,7 @@ db_namespace = namespace :db do
# desc "Raises an error if there are pending migrations"
task :abort_if_pending_migrations => [:environment, :load_config] do
- pending_migrations = ActiveRecord::Migrator.open(ActiveRecord::Migrator.migrations_paths).pending_migrations
+ 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:'}"
@@ -229,7 +231,7 @@ db_namespace = namespace :db do
end
namespace :schema do
- desc 'Creates 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')
@@ -255,7 +257,7 @@ db_namespace = namespace :db do
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
@@ -353,7 +355,7 @@ db_namespace = namespace :db do
ActiveRecord::Tasks::DatabaseTasks.purge ActiveRecord::Base.configurations['test']
end
- # desc 'Check for pending migrations and load the test schema'
+ # desc 'Load the test schema'
task :prepare => %w(environment load_config) do
unless ActiveRecord::Base.configurations.blank?
db_namespace['test:load'].invoke
@@ -384,7 +386,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/reflection.rb b/activerecord/lib/active_record/reflection.rb
index 5360db6a19..a549b28f16 100644
--- a/activerecord/lib/active_record/reflection.rb
+++ b/activerecord/lib/active_record/reflection.rb
@@ -32,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
@@ -61,24 +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}
#
- # @api public
def reflections
- ref = {}
- _reflections.each do |name, reflection|
- parent_reflection = reflection.parent_reflection
+ @__reflections ||= begin
+ ref = {}
- if parent_reflection
- parent_name = parent_reflection.name
- ref[parent_name.to_s] = parent_reflection
- else
- ref[name] = reflection
+ _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
@@ -91,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).
@@ -102,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
@@ -159,6 +163,68 @@ module ActiveRecord
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
+ else
+ 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
+ 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
@@ -305,7 +371,7 @@ module ActiveRecord
end
def foreign_key
- @foreign_key ||= options[:foreign_key] || derive_foreign_key
+ @foreign_key ||= options[:foreign_key] || derive_foreign_key.freeze
end
def association_foreign_key
@@ -321,26 +387,10 @@ 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
@@ -392,12 +442,6 @@ module ActiveRecord
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])
@@ -880,6 +924,8 @@ module ActiveRecord
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
diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb
index e4df122af6..f100476374 100644
--- a/activerecord/lib/active_record/relation.rb
+++ b/activerecord/lib/active_record/relation.rb
@@ -1,11 +1,10 @@
-# -*- coding: utf-8 -*-
require "arel/collectors/bind"
module ActiveRecord
- # = Active Record Relation
+ # = Active Record \Relation
class Relation
MULTI_VALUE_METHODS = [:includes, :eager_load, :preload, :select, :group,
- :order, :joins, :references,
+ :order, :joins, :left_joins, :left_outer_joins, :references,
:extending, :unscope]
SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :reordering,
@@ -109,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>
@@ -127,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
@@ -182,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
@@ -194,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
@@ -217,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
@@ -298,6 +303,32 @@ module ActiveRecord
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.
#
# Comment.where(post_id: 1).scoping do
@@ -316,8 +347,8 @@ module ActiveRecord
# 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
- # ActiveRecord's type-casting behavior. It should receive only values that can be passed as-is to the SQL
+ # 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
@@ -375,27 +406,34 @@ module ActiveRecord
# people = Person.where(group: 'expert')
# people.update(group: 'masters')
#
- # Note: Updating a large number of records will run a
- # 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 <tt>update_all</tt> for updating all records using
- # a single query.
+ # 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
@@ -403,22 +441,17 @@ 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(&:destroy).tap { reset }
@@ -427,7 +460,7 @@ module ActiveRecord
# 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.
@@ -452,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
@@ -486,6 +518,10 @@ 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
@@ -512,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.
#
@@ -602,8 +638,10 @@ 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
@@ -641,6 +679,13 @@ module ActiveRecord
"#<#{self.class.name} [#{entries.join(', ')}]>"
end
+ protected
+
+ def load_records(records)
+ @records = records
+ @loaded = true
+ end
+
private
def exec_queries
diff --git a/activerecord/lib/active_record/relation/batches.rb b/activerecord/lib/active_record/relation/batches.rb
index e07580a563..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.
@@ -122,24 +124,102 @@ module ActiveRecord
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)
+ relation = relation.reorder(batch_order).limit(of)
relation = apply_limits(relation, begin_at, end_at)
- records = relation.to_a
+ batch_relation = relation
+
+ 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
- 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
+ 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
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 0f6015fa93..f45844a9ea 100644
--- a/activerecord/lib/active_record/relation/calculations.rb
+++ b/activerecord/lib/active_record/relation/calculations.rb
@@ -14,33 +14,34 @@ 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
+ # 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+.
+ # of each key would be the #count.
#
# Article.group(:status, :category).count
# # => {["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)
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)
@@ -49,7 +50,7 @@ module ActiveRecord
# 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)
@@ -58,7 +59,7 @@ module ActiveRecord
# 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)
@@ -66,46 +67,46 @@ module ActiveRecord
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)
- return super if block_given?
- 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")
+ # 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)
@@ -118,7 +119,7 @@ module ActiveRecord
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)
@@ -127,7 +128,7 @@ 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.
#
@@ -151,7 +152,7 @@ module ActiveRecord
# # SELECT DATEDIFF(updated_at, created_at) FROM people
# # => ['0', '27761', '173']
#
- # See also +ids+.
+ # See also #ids.
#
def pluck(*column_names)
column_names.map! do |column_name|
@@ -218,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
@@ -272,15 +275,10 @@ module ActiveRecord
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'
@@ -296,7 +294,7 @@ module ActiveRecord
]
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
@@ -305,7 +303,7 @@ 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, relation.bound_attributes)
@@ -338,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}"
@@ -367,9 +364,9 @@ module ActiveRecord
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
diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb
index d75ec72b1a..27de313d05 100644
--- a/activerecord/lib/active_record/relation/delegation.rb
+++ b/activerecord/lib/active_record/relation/delegation.rb
@@ -3,12 +3,12 @@ require 'active_support/concern'
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,
diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb
index 009b2bad57..435cef901b 100644
--- a/activerecord/lib/active_record/relation/finder_methods.rb
+++ b/activerecord/lib/active_record/relation/finder_methods.rb
@@ -17,7 +17,7 @@ module ActiveRecord
# Person.where("administrator = 1").order("created_on DESC").find(1)
#
# NOTE: The returned records may not be in the same order as the ids you
- # provide since database rows are unordered. You'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
@@ -34,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,7 +48,7 @@ module ActiveRecord
# Person.where(name: 'Spartacus', rating: 4).first_or_create
# # 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.
@@ -80,8 +80,8 @@ module ActiveRecord
nil
end
- # Like <tt>find_by</tt>, except that if no record is found, raises
- # an <tt>ActiveRecord::RecordNotFound</tt> error.
+ # 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
@@ -100,8 +100,8 @@ 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.new("Couldn't find #{@klass.name} with [#{arel.where_sql(@klass.arel_engine)}]")
end
@@ -123,8 +123,8 @@ 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!
find_nth! 0
end
@@ -156,8 +156,8 @@ 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.new("Couldn't find #{@klass.name} with [#{arel.where_sql(@klass.arel_engine)}]")
end
@@ -172,7 +172,7 @@ 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!
find_nth! 1
@@ -188,7 +188,7 @@ 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!
find_nth! 2
@@ -204,7 +204,7 @@ 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!
find_nth! 3
@@ -220,7 +220,7 @@ 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!
find_nth! 4
@@ -236,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!
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
@@ -256,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
@@ -298,7 +298,7 @@ module ActiveRecord
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
diff --git a/activerecord/lib/active_record/relation/from_clause.rb b/activerecord/lib/active_record/relation/from_clause.rb
index a93952fa30..92340216ed 100644
--- a/activerecord/lib/active_record/relation/from_clause.rb
+++ b/activerecord/lib/active_record/relation/from_clause.rb
@@ -1,6 +1,6 @@
module ActiveRecord
class Relation
- class FromClause
+ class FromClause # :nodoc:
attr_reader :value, :name
def initialize(value, name)
diff --git a/activerecord/lib/active_record/relation/merger.rb b/activerecord/lib/active_record/relation/merger.rb
index 0b38666ce9..cb971eb255 100644
--- a/activerecord/lib/active_record/relation/merger.rb
+++ b/activerecord/lib/active_record/relation/merger.rb
@@ -148,11 +148,15 @@ module ActiveRecord
end
end
+ CLAUSE_METHOD_NAMES = CLAUSE_METHODS.map do |name|
+ ["#{name}_clause", "#{name}_clause="]
+ end
+
def merge_clauses
- CLAUSE_METHODS.each do |name|
- clause = relation.send("#{name}_clause")
- other_clause = other.send("#{name}_clause")
- relation.send("#{name}_clause=", clause.merge(other_clause))
+ 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 d26db7d4cf..39e7b42629 100644
--- a/activerecord/lib/active_record/relation/predicate_builder.rb
+++ b/activerecord/lib/active_record/relation/predicate_builder.rb
@@ -24,12 +24,12 @@ module ActiveRecord
end
def build_from_hash(attributes)
- attributes = convert_dot_notation_to_hash(attributes.stringify_keys)
+ attributes = convert_dot_notation_to_hash(attributes)
expand_from_hash(attributes)
end
def create_binds(attributes)
- attributes = convert_dot_notation_to_hash(attributes.stringify_keys)
+ attributes = convert_dot_notation_to_hash(attributes)
create_binds_for_hash(attributes)
end
@@ -67,7 +67,7 @@ module ActiveRecord
# Arel::Nodes::And.new([range.start, range.end])
# )
# end
- # ActiveRecord::PredicateBuilder.register_handler(MyCustomDateRange, handler)
+ # ActiveRecord::PredicateBuilder.new("users").register_handler(MyCustomDateRange, handler)
def register_handler(klass, handler)
@handlers.unshift([klass, handler])
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
index 159889d3b8..e81be63cd3 100644
--- a/activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb
+++ b/activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb
@@ -10,10 +10,10 @@ module ActiveRecord
table = value.associated_table
if value.base_class
- queries[table.association_foreign_type] = value.base_class.name
+ queries[table.association_foreign_type.to_s] = value.base_class.name
end
- queries[table.association_foreign_key] = value.ids
+ queries[table.association_foreign_key.to_s] = value.ids
predicate_builder.build_from_hash(queries)
end
diff --git a/activerecord/lib/active_record/relation/query_attribute.rb b/activerecord/lib/active_record/relation/query_attribute.rb
index e69319b4de..7ba964e802 100644
--- a/activerecord/lib/active_record/relation/query_attribute.rb
+++ b/activerecord/lib/active_record/relation/query_attribute.rb
@@ -2,7 +2,7 @@ require 'active_record/attribute'
module ActiveRecord
class Relation
- class QueryAttribute < Attribute
+ class QueryAttribute < Attribute # :nodoc:
def type_cast(value)
value
end
diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb
index 706c99c245..2dc52982c9 100644
--- a/activerecord/lib/active_record/relation/query_methods.rb
+++ b/activerecord/lib/active_record/relation/query_methods.rb
@@ -3,7 +3,6 @@ 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'
-require 'active_support/core_ext/string/filters'
module ActiveRecord
module QueryMethods
@@ -21,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'")
@@ -113,7 +112,7 @@ module ActiveRecord
#
# 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:
#
@@ -134,7 +133,7 @@ module ActiveRecord
#
# 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)
@@ -152,9 +151,9 @@ module ActiveRecord
# 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)
@@ -165,10 +164,10 @@ module ActiveRecord
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)
@@ -181,14 +180,14 @@ module ActiveRecord
# 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)
@@ -204,12 +203,12 @@ module ActiveRecord
# 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:
@@ -237,7 +236,7 @@ module ActiveRecord
# # => "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
@@ -250,7 +249,7 @@ module ActiveRecord
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
@@ -259,22 +258,23 @@ module ActiveRecord
# 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)
@@ -290,22 +290,22 @@ module ActiveRecord
# 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)
@@ -357,15 +357,15 @@ module ActiveRecord
# 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')
@@ -375,7 +375,7 @@ module ActiveRecord
#
# 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)
@@ -410,12 +410,12 @@ module ActiveRecord
# 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)
spawn.joins!(*args)
@@ -428,6 +428,27 @@ module ActiveRecord
self
end
+ # Performs a left outer joins on +args+:
+ #
+ # User.left_outer_joins(:posts)
+ # => SELECT "users".* FROM "users" LEFT OUTER JOIN "posts" ON "posts"."user_id" = "users"."id"
+ #
+ def left_outer_joins(*args)
+ check_if_method_has_arguments!(:left_outer_joins, args)
+
+ args.compact!
+ args.flatten!
+
+ spawn.left_outer_joins!(*args)
+ end
+ alias :left_joins :left_outer_joins
+
+ def left_outer_joins!(*args) # :nodoc:
+ self.left_outer_joins_values += args
+ self
+ end
+ alias :left_joins! :left_outer_joins!
+
# Returns a new relation, which is the result of filtering the current relation
# according to the conditions in the arguments.
#
@@ -471,7 +492,7 @@ module ActiveRecord
# 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';
@@ -548,7 +569,7 @@ module ActiveRecord
# 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
@@ -558,23 +579,25 @@ module ActiveRecord
end
def where!(opts, *rest) # :nodoc:
- if Hash === opts
- opts = sanitize_forbidden_attributes(opts)
- references!(PredicateBuilder.references(opts))
- end
-
+ opts = sanitize_forbidden_attributes(opts)
+ references!(PredicateBuilder.references(opts)) if Hash === opts
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
+ #
+ # Post.where(trashed: true).rewhere(trashed: false)
+ # # WHERE `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(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
@@ -583,8 +606,8 @@ module ActiveRecord
# 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.
+ # 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'))
@@ -604,12 +627,6 @@ module ActiveRecord
self
end
- private def structurally_compatible_for_or?(other) # :nodoc:
- 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
-
# Allows to specify a HAVING clause. Note that you can't use HAVING
# without also specifying a GROUP clause.
#
@@ -619,6 +636,7 @@ module ActiveRecord
end
def having!(opts, *rest) # :nodoc:
+ opts = sanitize_forbidden_attributes(opts)
references!(PredicateBuilder.references(opts)) if Hash === opts
self.having_clause += having_clause_factory.build(opts, rest)
@@ -656,7 +674,7 @@ module ActiveRecord
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
@@ -687,7 +705,7 @@ module ActiveRecord
# 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
@@ -713,7 +731,7 @@ module ActiveRecord
#
# 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
@@ -732,7 +750,7 @@ module ActiveRecord
# 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'
@@ -754,15 +772,15 @@ module ActiveRecord
# 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)
@@ -776,13 +794,13 @@ module ActiveRecord
# 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
@@ -881,6 +899,7 @@ module ActiveRecord
arel = Arel::SelectManager.new(table)
build_joins(arel, joins_values.flatten) unless joins_values.empty?
+ build_left_outer_joins(arel, left_outer_joins_values.flatten) unless left_outer_joins_values.empty?
arel.where(where_clause.ast) unless where_clause.empty?
arel.having(having_clause.ast) unless having_clause.empty?
@@ -940,6 +959,19 @@ module ActiveRecord
end
end
+ def build_left_outer_joins(manager, outer_joins)
+ buckets = outer_joins.group_by do |join|
+ case join
+ when Hash, Symbol, Array
+ :association_join
+ else
+ raise ArgumentError, 'only Hash, Symbol and Array are allowed'
+ end
+ end
+
+ build_join_query(manager, buckets, Arel::Nodes::OuterJoin)
+ end
+
def build_joins(manager, joins)
buckets = joins.group_by do |join|
case join
@@ -955,6 +987,11 @@ module ActiveRecord
raise 'unknown class: %s' % join.class.name
end
end
+
+ build_join_query(manager, buckets, Arel::Nodes::InnerJoin)
+ end
+
+ def build_join_query(manager, buckets, join_type)
buckets.default = []
association_joins = buckets[:association_join]
@@ -970,7 +1007,7 @@ module ActiveRecord
join_list
)
- join_infos = join_dependency.join_constraints stashed_association_joins
+ join_infos = join_dependency.join_constraints stashed_association_joins, join_type
join_infos.each do |info|
info.joins.each { |join| manager.from(join) }
@@ -1048,6 +1085,9 @@ module ActiveRecord
end
def preprocess_order_args(order_args)
+ order_args.map! do |arg|
+ klass.send(:sanitize_sql_for_order, arg)
+ end
order_args.flatten!
validate_order_args(order_args)
@@ -1078,8 +1118,8 @@ module ActiveRecord
#
# 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:
@@ -1094,6 +1134,12 @@ module ActiveRecord
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
diff --git a/activerecord/lib/active_record/relation/record_fetch_warning.rb b/activerecord/lib/active_record/relation/record_fetch_warning.rb
index 14e1bf89fa..0a1814b3dd 100644
--- a/activerecord/lib/active_record/relation/record_fetch_warning.rb
+++ b/activerecord/lib/active_record/relation/record_fetch_warning.rb
@@ -23,11 +23,13 @@ module ActiveRecord
end
end
+ # :stopdoc:
ActiveSupport::Notifications.subscribe("sql.active_record") do |*args|
payload = args.last
QueryRegistry.queries << payload[:sql]
end
+ # :startdoc:
class QueryRegistry # :nodoc:
extend ActiveSupport::PerThreadRegistry
diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb
index 70da37fa84..67d7f83cb4 100644
--- a/activerecord/lib/active_record/relation/spawn_methods.rb
+++ b/activerecord/lib/active_record/relation/spawn_methods.rb
@@ -10,8 +10,9 @@ 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.
#
@@ -37,11 +38,14 @@ module ActiveRecord
end
def merge!(other) # :nodoc:
- if !other.is_a?(Relation) && other.respond_to?(:to_proc)
+ if other.is_a?(Hash)
+ Relation::HashMerger.new(self, other).merge
+ elsif other.is_a?(Relation)
+ Relation::Merger.new(self, other).merge
+ elsif other.respond_to?(:to_proc)
instance_exec(&other)
else
- klass = other.is_a?(Hash) ? Relation::HashMerger : Relation::Merger
- klass.new(self, other).merge
+ raise ArgumentError, "#{other.inspect} is not an ActiveRecord::Relation"
end
end
diff --git a/activerecord/lib/active_record/relation/where_clause_factory.rb b/activerecord/lib/active_record/relation/where_clause_factory.rb
index 0430922be3..a81ff98e49 100644
--- a/activerecord/lib/active_record/relation/where_clause_factory.rb
+++ b/activerecord/lib/active_record/relation/where_clause_factory.rb
@@ -1,6 +1,6 @@
module ActiveRecord
class Relation
- class WhereClauseFactory
+ class WhereClauseFactory # :nodoc:
def initialize(klass, predicate_builder)
@klass = klass
@predicate_builder = predicate_builder
@@ -15,12 +15,15 @@ module ActiveRecord
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)
- else
+ when Arel::Nodes::Node
parts = [opts]
+ else
+ raise ArgumentError, "Unsupported argument type: #{opts} (#{opts.class})"
end
WhereClause.new(parts, binds)
diff --git a/activerecord/lib/active_record/result.rb b/activerecord/lib/active_record/result.rb
index 500c478e65..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>
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 ba75ffa5a1..0c15f45db9 100644
--- a/activerecord/lib/active_record/sanitization.rb
+++ b/activerecord/lib/active_record/sanitization.rb
@@ -3,8 +3,9 @@ module ActiveRecord
extend ActiveSupport::Concern
module ClassMethods
- # 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
@@ -13,9 +14,16 @@ module ActiveRecord
# 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' 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
@@ -28,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)
@@ -37,17 +53,37 @@ module ActiveRecord
end
end
+ # Accepts an array, or string of SQL conditions and sanitizes
+ # them into a valid SQL fragment for a ORDER clause.
+ #
+ # sanitize_sql_for_order(["field(id, ?)", [1,3,2]])
+ # # => "field(id, 1,3,2)"
+ #
+ # sanitize_sql_for_order("id ASC")
+ # # => "id ASC"
+ def sanitize_sql_for_order(condition)
+ if condition.is_a?(Array) && condition.first.to_s.include?('?')
+ sanitize_sql_array(condition)
+ else
+ condition
+ end
+ 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|
@@ -68,8 +104,9 @@ module ActiveRecord
end
# 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|
@@ -79,7 +116,19 @@ module ActiveRecord
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 }
@@ -87,7 +136,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+/
@@ -101,7 +155,7 @@ 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
@@ -110,7 +164,7 @@ module ActiveRecord
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
@@ -118,7 +172,7 @@ module ActiveRecord
end
end
- def replace_named_bind_variables(statement, bind_vars) #:nodoc:
+ def replace_named_bind_variables(statement, bind_vars) # :nodoc:
statement.gsub(/(:?):([a-zA-Z]\w*)/) do |match|
if $1 == ':' # skip postgresql casts
match # return the whole match
@@ -130,7 +184,7 @@ module ActiveRecord
end
end
- def quote_bound_value(value, c = connection) #:nodoc:
+ 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)
@@ -142,7 +196,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
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 c5910fa1ad..2362dae9fc 100644
--- a/activerecord/lib/active_record/schema_dumper.rb
+++ b/activerecord/lib/active_record/schema_dumper.rb
@@ -89,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)
@@ -112,20 +112,27 @@ HEADER
tbl = StringIO.new
# first dump primary key column
- pk = @connection.primary_key(table)
+ 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}")
- end
+
+ 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
@@ -247,7 +254,7 @@ HEADER
end
def ignored?(table_name)
- ['schema_migrations', ignore_tables].flatten.any? do |ignored|
+ [ActiveRecord::Base.schema_migrations_table_name, ignore_tables].flatten.any? do |ignored|
ignored === remove_prefix_and_suffix(table_name)
end
end
diff --git a/activerecord/lib/active_record/schema_migration.rb b/activerecord/lib/active_record/schema_migration.rb
index cb47bf23f7..51b9b17395 100644
--- a/activerecord/lib/active_record/schema_migration.rb
+++ b/activerecord/lib/active_record/schema_migration.rb
@@ -2,7 +2,11 @@ require 'active_record/scoping/default'
require 'active_record/scoping/named'
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
@@ -17,7 +21,7 @@ module ActiveRecord
end
def table_exists?
- connection.table_exists?(table_name)
+ ActiveSupport::Deprecation.silence { connection.table_exists?(table_name) }
end
def create_table(limit=nil)
diff --git a/activerecord/lib/active_record/scoping.rb b/activerecord/lib/active_record/scoping.rb
index f049b658c4..e395970dc6 100644
--- a/activerecord/lib/active_record/scoping.rb
+++ b/activerecord/lib/active_record/scoping.rb
@@ -30,7 +30,7 @@ module ActiveRecord
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|
@@ -38,7 +38,7 @@ module ActiveRecord
end
end
- def initialize_internals_callback
+ def initialize_internals_callback # :nodoc:
super
populate_with_current_scope_attributes
end
@@ -59,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 3590b8846e..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
#
@@ -53,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
@@ -63,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
@@ -74,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
@@ -99,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 7b62626896..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
@@ -39,8 +39,13 @@ module ActiveRecord
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
@@ -48,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
@@ -66,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
@@ -84,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
@@ -93,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
diff --git a/activerecord/lib/active_record/secure_token.rb b/activerecord/lib/active_record/secure_token.rb
index ca11853da7..8abda2ac49 100644
--- a/activerecord/lib/active_record/secure_token.rb
+++ b/activerecord/lib/active_record/secure_token.rb
@@ -3,7 +3,7 @@ module ActiveRecord
extend ActiveSupport::Concern
module ClassMethods
- # Example using has_secure_token
+ # Example using #has_secure_token
#
# # Schema: User(token:string, auth_token:string)
# class User < ActiveRecord::Base
@@ -18,11 +18,11 @@ module ActiveRecord
# user.regenerate_token # => true
# user.regenerate_auth_token # => true
#
- # SecureRandom::base58 is used to generate the 24-character unique token, so collisions are highly unlikely.
+ # <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
- # <tt>validates_uniqueness_of</tt> can. You're encouraged to add a unique index in the database to deal
- # with this even more unlikely scenario.
+ # {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'
diff --git a/activerecord/lib/active_record/serialization.rb b/activerecord/lib/active_record/serialization.rb
index 48c12dcf9f..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
@@ -18,5 +18,3 @@ module ActiveRecord #:nodoc:
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 89b7e0be82..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
- cast_type = klass.type_for_attribute(name)
-
- type = ActiveSupport::XmlMini::TYPE_NAMES[value.class.name] || cast_type.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 95986c820c..f6b0efb88a 100644
--- a/activerecord/lib/active_record/statement_cache.rb
+++ b/activerecord/lib/active_record/statement_cache.rb
@@ -7,12 +7,14 @@ module ActiveRecord
# 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([], 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.
+ # 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.
diff --git a/activerecord/lib/active_record/store.rb b/activerecord/lib/active_record/store.rb
index 919bc58ba5..1b407f7702 100644
--- a/activerecord/lib/active_record/store.rb
+++ b/activerecord/lib/active_record/store.rb
@@ -16,7 +16,8 @@ module ActiveRecord
# 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
+ # 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.
#
@@ -43,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/table_metadata.rb b/activerecord/lib/active_record/table_metadata.rb
index 41f1c55c3c..f9bb1cf5e0 100644
--- a/activerecord/lib/active_record/table_metadata.rb
+++ b/activerecord/lib/active_record/table_metadata.rb
@@ -10,13 +10,15 @@ module ActiveRecord
end
def resolve_column_aliases(hash)
- hash = hash.dup
- hash.keys.grep(Symbol) do |key|
- if klass.attribute_alias? key
- hash[klass.attribute_alias(key)] = hash.delete key
+ # 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
- hash
+ new_hash
end
def arel_attribute(column_name)
diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb
index 683741768b..c0c29a618c 100644
--- a/activerecord/lib/active_record/tasks/database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/database_tasks.rb
@@ -5,7 +5,7 @@ module ActiveRecord
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.
@@ -18,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')
@@ -94,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
@@ -115,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
@@ -134,7 +136,7 @@ module ActiveRecord
version = ENV["VERSION"] ? ENV["VERSION"].to_i : nil
scope = ENV['SCOPE']
verbose_was, Migration.verbose = Migration.verbose, verbose
- Migrator.migrate(Migrator.migrations_paths, version) do |migration|
+ Migrator.migrate(migrations_paths, version) do |migration|
scope.blank? || scope == migration.scope
end
ensure
@@ -221,12 +223,6 @@ module ActiveRecord
end
end
- def load_schema_current_if_exists(format = ActiveRecord::Base.schema_format, file = nil, environment = env)
- if File.exist?(file || schema_file(format))
- load_schema_current(format, file, environment)
- 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
diff --git a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb
index 673386f0d9..100df9c6c6 100644
--- a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb
@@ -1,8 +1,6 @@
module ActiveRecord
module Tasks # :nodoc:
class MySQLDatabaseTasks # :nodoc:
- DEFAULT_CHARSET = ENV['CHARSET'] || 'utf8'
- DEFAULT_COLLATION = ENV['COLLATION'] || 'utf8_unicode_ci'
ACCESS_DENIED_ERROR = 1045
delegate :connection, :establish_connection, to: ActiveRecord::Base
@@ -23,7 +21,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'
@@ -56,22 +54,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
@@ -88,12 +85,6 @@ module ActiveRecord
Hash.new.tap do |options|
options[:charset] = configuration['encoding'] if configuration.include? 'encoding'
options[:collation] = configuration['collation'] if configuration.include? 'collation'
-
- # Set default charset only when collation isn't set.
- options[:charset] ||= DEFAULT_CHARSET unless options[:collation]
-
- # Set default collation only when charset is also default.
- options[:collation] ||= DEFAULT_COLLATION if options[:charset] == DEFAULT_CHARSET
end
end
@@ -130,7 +121,7 @@ IDENTIFIED BY '#{configuration['password']}' WITH GRANT OPTION;
$stdin.gets.strip
end
- def prepare_command_options(command)
+ def prepare_command_options
args = {
'host' => '--host',
'port' => '--port',
@@ -145,7 +136,17 @@ IDENTIFIED BY '#{configuration['password']}' WITH GRANT OPTION;
'sslkey' => '--ssl-key'
}.map { |opt, arg| "#{arg}=#{configuration[opt]}" if configuration[opt] }.compact
- [command, *args]
+ 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
diff --git a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb
index d7da95c8a9..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:
@@ -55,19 +53,22 @@ module ActiveRecord
when String
ActiveRecord::Base.dump_schemas
end
+
+ args = ['-s', '-x', '-O', '-f', filename]
unless search_path.blank?
- search_path = search_path.split(",").map{|search_path_part| "--schema=#{Shellwords.escape(search_path_part.strip)}" }.join(" ")
+ args << search_path.split(',').map do |part|
+ "--schema=#{part.strip}"
+ end.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)
-
+ 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 -X -q -f #{Shellwords.escape(filename)} #{configuration['database']}")
+ args = [ '-q', '-f', filename, configuration['database'] ]
+ run_cmd('psql', args, 'loading' )
end
private
@@ -93,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 9ab64d0325..9ec3c8a94a 100644
--- a/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb
@@ -19,11 +19,15 @@ 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
diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb
index 20e4235788..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]
diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb
index 887d7a5903..8de82feae3 100644
--- a/activerecord/lib/active_record/transactions.rb
+++ b/activerecord/lib/active_record/transactions.rb
@@ -11,15 +11,16 @@ module ActiveRecord
: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.
@@ -39,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
@@ -79,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
@@ -88,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
@@ -97,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
@@ -127,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:
#
@@ -143,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.
@@ -167,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
@@ -233,7 +235,7 @@ 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)
@@ -322,7 +324,7 @@ module ActiveRecord
_run_before_commit_callbacks
end
- # Call the +after_commit+ callbacks.
+ # 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.
@@ -335,7 +337,7 @@ module ActiveRecord
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, should_run_callbacks: true) #:nodoc:
if should_run_callbacks
@@ -347,7 +349,7 @@ module ActiveRecord
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 has_transactional_callbacks?
@@ -456,23 +458,23 @@ module ActiveRecord
!_rollback_callbacks.empty? || !_commit_callbacks.empty? || !_before_commit_callbacks.empty?
end
- # Updates the attributes on this particular ActiveRecord object so that
- # if it's associated with a transaction, then the state of the ActiveRecord
+ # 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
+ # 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 ActiveRecord object inside of a transaction carries that transaction's
+ # 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 ActiveRecord object
+ # the TransactionState, and rolls back or commits the Active Record object
# as appropriate.
#
- # Since ActiveRecord objects can be inside multiple transactions, this
+ # Since Active Record 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.
+ # 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
diff --git a/activerecord/lib/active_record/type.rb b/activerecord/lib/active_record/type.rb
index 2c0cda69d0..e210e94f00 100644
--- a/activerecord/lib/active_record/type.rb
+++ b/activerecord/lib/active_record/type.rb
@@ -1,22 +1,15 @@
-require 'active_record/type/helpers'
-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/big_integer'
-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/unsigned_integer'
+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'
@@ -29,13 +22,13 @@ module ActiveRecord
delegate :add_modifier, to: :registry
# Add a new type to the registry, allowing it to be referenced as a
- # symbol by ActiveRecord::Attributes::ClassMethods#attribute. If your
- # type is only meant to be used with a specific database adapter, you can
- # do so by passing +adapter: :postgresql+. If your type has the same
+ # 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. +override: true+ will
- # cause your type to be used instead of the native type. +override:
- # false+ will cause the native type to be used over yours if one exists.
+ # 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
@@ -51,6 +44,19 @@ module ActiveRecord
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)
diff --git a/activerecord/lib/active_record/type/adapter_specific_registry.rb b/activerecord/lib/active_record/type/adapter_specific_registry.rb
index 5f71b3cb94..d440eac619 100644
--- a/activerecord/lib/active_record/type/adapter_specific_registry.rb
+++ b/activerecord/lib/active_record/type/adapter_specific_registry.rb
@@ -1,35 +1,24 @@
+require 'active_model/type/registry'
+
module ActiveRecord
# :stopdoc:
module Type
- class AdapterSpecificRegistry
- def initialize
- @registrations = []
- end
-
- def register(type_name, klass = nil, **options, &block)
- block ||= proc { |_, *args| klass.new(*args) }
- registrations << Registration.new(type_name, block, **options)
+ class AdapterSpecificRegistry < ActiveModel::Type::Registry
+ def add_modifier(options, klass, **args)
+ registrations << DecorationRegistration.new(options, klass, **args)
end
- def lookup(symbol, *args)
- registration = registrations
- .select { |r| r.matches?(symbol, *args) }
- .max
+ private
- if registration
- registration.call(self, symbol, *args)
- else
- raise ArgumentError, "Unknown type #{symbol.inspect}"
- end
+ def registration_klass
+ Registration
end
- def add_modifier(options, klass, **args)
- registrations << DecorationRegistration.new(options, klass, **args)
+ def find_registration(symbol, *args)
+ registrations
+ .select { |registration| registration.matches?(symbol, *args) }
+ .max
end
-
- protected
-
- attr_reader :registrations
end
class Registration
@@ -137,6 +126,5 @@ module ActiveRecord
class TypeConflictError < StandardError
end
-
# :startdoc:
end
diff --git a/activerecord/lib/active_record/type/big_integer.rb b/activerecord/lib/active_record/type/big_integer.rb
deleted file mode 100644
index 0c72d8914f..0000000000
--- a/activerecord/lib/active_record/type/big_integer.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-require 'active_record/type/integer'
-
-module ActiveRecord
- module Type
- class BigInteger < Integer # :nodoc:
- private
-
- def max_value
- ::Float::INFINITY
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/type/binary.rb b/activerecord/lib/active_record/type/binary.rb
deleted file mode 100644
index 0baf8c63ad..0000000000
--- a/activerecord/lib/active_record/type/binary.rb
+++ /dev/null
@@ -1,50 +0,0 @@
-module ActiveRecord
- module Type
- class Binary < Value # :nodoc:
- def type
- :binary
- end
-
- def binary?
- true
- end
-
- def cast(value)
- if value.is_a?(Data)
- value.to_s
- else
- super
- end
- end
-
- def serialize(value)
- return if value.nil?
- Data.new(super)
- end
-
- def changed_in_place?(raw_old_value, value)
- old_value = deserialize(raw_old_value)
- old_value != value
- end
-
- class Data # :nodoc:
- def initialize(value)
- @value = value.to_s
- end
-
- def to_s
- @value
- end
- alias_method :to_str, :to_s
-
- def hex
- @value.unpack('H*')[0]
- end
-
- def ==(other)
- other == to_s || super
- 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 f6a75512fd..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::FALSE_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 3ceab59ebb..ccafed054e 100644
--- a/activerecord/lib/active_record/type/date.rb
+++ b/activerecord/lib/active_record/type/date.rb
@@ -1,49 +1,7 @@
module ActiveRecord
module Type
- class Date < Value # :nodoc:
- include Helpers::AcceptsMultiparameterTime.new
-
- def type
- :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
-
- def value_from_multiparameter_assignment(*)
- time = super
- time && time.to_date
- 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 a5199959b9..1fb9380ecd 100644
--- a/activerecord/lib/active_record/type/date_time.rb
+++ b/activerecord/lib/active_record/type/date_time.rb
@@ -1,44 +1,7 @@
module ActiveRecord
module Type
- class DateTime < Value # :nodoc:
- include Helpers::TimeValue
- include Helpers::AcceptsMultiparameterTime.new(
- defaults: { 4 => 0, 5 => 0 }
- )
-
- def type
- :datetime
- 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
-
- def value_from_multiparameter_assignment(values_hash)
- missing_parameter = (1..3).detect { |key| !values_hash.key?(key) }
- if missing_parameter
- raise ArgumentError, missing_parameter
- end
- super
- 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 f200a92d10..0000000000
--- a/activerecord/lib/active_record/type/decimal.rb
+++ /dev/null
@@ -1,48 +0,0 @@
-module ActiveRecord
- module Type
- class Decimal < Value # :nodoc:
- include Helpers::Numeric
-
- def type
- :decimal
- end
-
- def type_cast_for_schema(value)
- value.to_s.inspect
- end
-
- private
-
- def cast_value(value)
- case value
- when ::Float
- convert_float_to_big_decimal(value)
- when ::Numeric, ::String
- BigDecimal(value, precision.to_i)
- else
- if value.respond_to?(:to_d)
- value.to_d
- else
- cast_value(value.to_s)
- end
- end
- end
-
- def convert_float_to_big_decimal(value)
- if precision
- BigDecimal(value, float_precision)
- else
- value.to_d
- end
- end
-
- def float_precision
- if precision.to_i > ::Float::DIG + 1
- ::Float::DIG + 1
- else
- precision.to_i
- 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 ff5559e300..0000000000
--- a/activerecord/lib/active_record/type/decimal_without_scale.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-require 'active_record/type/big_integer'
-
-module ActiveRecord
- module Type
- class DecimalWithoutScale < BigInteger # :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 d88482b85d..0000000000
--- a/activerecord/lib/active_record/type/float.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-module ActiveRecord
- module Type
- class Float < Value # :nodoc:
- include Helpers::Numeric
-
- def type
- :float
- end
-
- alias serialize cast
-
- private
-
- 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
diff --git a/activerecord/lib/active_record/type/helpers.rb b/activerecord/lib/active_record/type/helpers.rb
deleted file mode 100644
index 634d417d13..0000000000
--- a/activerecord/lib/active_record/type/helpers.rb
+++ /dev/null
@@ -1,4 +0,0 @@
-require 'active_record/type/helpers/accepts_multiparameter_time'
-require 'active_record/type/helpers/numeric'
-require 'active_record/type/helpers/mutable'
-require 'active_record/type/helpers/time_value'
diff --git a/activerecord/lib/active_record/type/helpers/accepts_multiparameter_time.rb b/activerecord/lib/active_record/type/helpers/accepts_multiparameter_time.rb
deleted file mode 100644
index be571fc1c7..0000000000
--- a/activerecord/lib/active_record/type/helpers/accepts_multiparameter_time.rb
+++ /dev/null
@@ -1,30 +0,0 @@
-module ActiveRecord
- module Type
- module Helpers
- class AcceptsMultiparameterTime < Module # :nodoc:
- def initialize(defaults: {})
- define_method(:cast) do |value|
- if value.is_a?(Hash)
- value_from_multiparameter_assignment(value)
- else
- super(value)
- end
- end
-
- define_method(:value_from_multiparameter_assignment) do |values_hash|
- defaults.each do |k, v|
- values_hash[k] ||= v
- end
- return unless values_hash[1] && values_hash[2] && values_hash[3]
- values = values_hash.sort.map(&:last)
- ::Time.send(
- ActiveRecord::Base.default_timezone,
- *values
- )
- end
- private :value_from_multiparameter_assignment
- end
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/type/helpers/mutable.rb b/activerecord/lib/active_record/type/helpers/mutable.rb
deleted file mode 100644
index 88a9099277..0000000000
--- a/activerecord/lib/active_record/type/helpers/mutable.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-module ActiveRecord
- module Type
- module Helpers
- module Mutable # :nodoc:
- def cast(value)
- deserialize(serialize(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 != serialize(new_value)
- end
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/type/helpers/numeric.rb b/activerecord/lib/active_record/type/helpers/numeric.rb
deleted file mode 100644
index a755a02a59..0000000000
--- a/activerecord/lib/active_record/type/helpers/numeric.rb
+++ /dev/null
@@ -1,34 +0,0 @@
-module ActiveRecord
- module Type
- module Helpers
- module Numeric # :nodoc:
- def 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
-end
diff --git a/activerecord/lib/active_record/type/helpers/time_value.rb b/activerecord/lib/active_record/type/helpers/time_value.rb
deleted file mode 100644
index 7eb41557cb..0000000000
--- a/activerecord/lib/active_record/type/helpers/time_value.rb
+++ /dev/null
@@ -1,58 +0,0 @@
-module ActiveRecord
- module Type
- module Helpers
- module TimeValue # :nodoc:
- def serialize(value)
- if precision && value.respond_to?(:usec)
- number_of_insignificant_digits = 6 - precision
- round_power = 10 ** number_of_insignificant_digits
- value = value.change(usec: value.usec / round_power * round_power)
- end
-
- if value.acts_like?(:time)
- zone_conversion_method = ActiveRecord::Base.default_timezone == :utc ? :getutc : :getlocal
-
- if value.respond_to?(zone_conversion_method)
- value = value.send(zone_conversion_method)
- end
- end
-
- value
- end
-
- def type_cast_for_schema(value)
- "'#{value.to_s(:db)}'"
- end
-
- def user_input_in_time_zone(value)
- value.in_time_zone
- 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
-end
diff --git a/activerecord/lib/active_record/type/integer.rb b/activerecord/lib/active_record/type/integer.rb
deleted file mode 100644
index c5040c6d3b..0000000000
--- a/activerecord/lib/active_record/type/integer.rb
+++ /dev/null
@@ -1,66 +0,0 @@
-module ActiveRecord
- module Type
- class Integer < Value # :nodoc:
- include Helpers::Numeric
-
- # Column storage size in bytes.
- # 4 bytes means a MySQL int or Postgres integer as opposed to smallint etc.
- DEFAULT_LIMIT = 4
-
- def initialize(*)
- super
- @range = min_value...max_value
- end
-
- def type
- :integer
- end
-
- def deserialize(value)
- return if value.nil?
- value.to_i
- end
-
- def serialize(value)
- result = cast(value)
- if result
- ensure_in_range(result)
- end
- result
- end
-
- protected
-
- attr_reader :range
-
- private
-
- def cast_value(value)
- case value
- when true then 1
- when false then 0
- else
- value.to_i rescue nil
- end
- end
-
- def ensure_in_range(value)
- unless range.cover?(value)
- raise RangeError, "#{value} is out of range for #{self.class} with limit #{_limit}"
- end
- end
-
- def max_value
- 1 << (_limit * 8 - 1) # 8 bits per byte with one bit for sign
- end
-
- def min_value
- -max_value
- end
-
- def _limit
- self.limit || DEFAULT_LIMIT
- 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/serialized.rb b/activerecord/lib/active_record/type/serialized.rb
index ea3e0d6a45..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 < DelegateClass(Type::Value) # :nodoc:
- include Helpers::Mutable
+ class Serialized < DelegateClass(ActiveModel::Type::Value) # :nodoc:
+ include ActiveModel::Type::Helpers::Mutable
attr_reader :subtype, :coder
@@ -41,6 +41,12 @@ module ActiveRecord
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 default_value?(value)
diff --git a/activerecord/lib/active_record/type/string.rb b/activerecord/lib/active_record/type/string.rb
deleted file mode 100644
index 2662b7e874..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 serialize(value)
- case value
- when ::Numeric, ActiveSupport::Duration then value.to_s
- when ::String then ::String.new(value)
- when true then "t"
- when false then "f"
- else super
- end
- end
-
- private
-
- def cast_value(value)
- case value
- when true then "t"
- when false then "f"
- # 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 19a10021bc..70988d84ff 100644
--- a/activerecord/lib/active_record/type/time.rb
+++ b/activerecord/lib/active_record/type/time.rb
@@ -1,42 +1,8 @@
module ActiveRecord
module Type
- class Time < Value # :nodoc:
- include Helpers::TimeValue
- include Helpers::AcceptsMultiparameterTime.new(
- defaults: { 1 => 1970, 2 => 1, 3 => 1, 4 => 0, 5 => 0 }
- )
-
- def type
- :time
- end
-
- def user_input_in_time_zone(value)
- return unless value.present?
-
- case value
- when ::String
- value = "2000-01-01 #{value}"
- when ::Time
- value = value.change(year: 2000, day: 1, month: 1)
- end
-
- super(value)
- 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/type_map.rb b/activerecord/lib/active_record/type/type_map.rb
index 09f5ba6b74..850a7a4e09 100644
--- a/activerecord/lib/active_record/type/type_map.rb
+++ b/activerecord/lib/active_record/type/type_map.rb
@@ -1,12 +1,12 @@
-require 'thread_safe'
+require 'concurrent/map'
module ActiveRecord
module Type
class TypeMap # :nodoc:
def initialize
@mapping = {}
- @cache = ThreadSafe::Cache.new do |h, key|
- h.fetch_or_store(key, ThreadSafe::Cache.new)
+ @cache = Concurrent::Map.new do |h, key|
+ h.fetch_or_store(key, Concurrent::Map.new)
end
end
@@ -57,7 +57,7 @@ module ActiveRecord
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/unsigned_integer.rb b/activerecord/lib/active_record/type/unsigned_integer.rb
deleted file mode 100644
index ed3e527483..0000000000
--- a/activerecord/lib/active_record/type/unsigned_integer.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-module ActiveRecord
- module Type
- class UnsignedInteger < Integer # :nodoc:
- private
-
- def max_value
- super * 2
- end
-
- def min_value
- 0
- end
- 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 6b9d147ecc..0000000000
--- a/activerecord/lib/active_record/type/value.rb
+++ /dev/null
@@ -1,104 +0,0 @@
-module ActiveRecord
- module Type
- class Value
- attr_reader :precision, :scale, :limit
-
- def initialize(precision: nil, limit: nil, scale: nil)
- @precision = precision
- @scale = scale
- @limit = limit
- end
-
- def type # :nodoc:
- end
-
- # Converts a value from database input to the appropriate ruby type. The
- # return value of this method will be returned from
- # ActiveRecord::AttributeMethods::Read#read_attribute. The default
- # implementation just calls Value#cast.
- #
- # +value+ The raw input, as provided from the database.
- def deserialize(value)
- 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 a ruby object passed to a setter.
- # There is currently no way to differentiate between which source it came
- # from.
- #
- # The return value of this method will be returned from
- # ActiveRecord::AttributeMethods::Read#read_attribute. See also:
- # Value#cast_value.
- #
- # +value+ The raw input, as provided to the attribute setter.
- def cast(value)
- cast_value(value) unless value.nil?
- end
-
- # Casts 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 serialize(value)
- value
- end
-
- # Type casts 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 binary? # :nodoc:
- false
- 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. If your type returns an object
- # which could be mutated, you should override this method. You will need
- # to either:
- #
- # - pass +new_value+ to Value#serialize and compare it to
- # +raw_old_value+
- #
- # or
- #
- # - pass +raw_old_value+ to Value#deserialize and compare it to
- # +new_value+
- #
- # +raw_old_value+ The original value, before being passed to
- # +deserialize+.
- #
- # +new_value+ The current value, after type casting.
- def changed_in_place?(raw_old_value, new_value)
- false
- end
-
- def ==(other)
- self.class == other.class &&
- precision == other.precision &&
- scale == other.scale &&
- limit == other.limit
- end
-
- private
-
- # Convenience method for types which do not need separate type casting
- # behavior for user and database inputs. Called by Value#cast for
- # 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
index 63ba10c289..accc339d00 100644
--- a/activerecord/lib/active_record/type_caster.rb
+++ b/activerecord/lib/active_record/type_caster.rb
@@ -2,6 +2,6 @@ require 'active_record/type_caster/map'
require 'active_record/type_caster/connection'
module ActiveRecord
- module TypeCaster
+ module TypeCaster # :nodoc:
end
end
diff --git a/activerecord/lib/active_record/type_caster/connection.rb b/activerecord/lib/active_record/type_caster/connection.rb
index 3878270770..7ed8dcc313 100644
--- a/activerecord/lib/active_record/type_caster/connection.rb
+++ b/activerecord/lib/active_record/type_caster/connection.rb
@@ -1,6 +1,6 @@
module ActiveRecord
module TypeCaster
- class Connection
+ class Connection # :nodoc:
def initialize(klass, table_name)
@klass = klass
@table_name = table_name
@@ -20,7 +20,7 @@ module ActiveRecord
private
def column_for(attribute_name)
- if connection.schema_cache.table_exists?(table_name)
+ if connection.schema_cache.data_source_exists?(table_name)
connection.schema_cache.columns_hash(table_name)[attribute_name.to_s]
end
end
diff --git a/activerecord/lib/active_record/type_caster/map.rb b/activerecord/lib/active_record/type_caster/map.rb
index 4b1941351c..3a367b3999 100644
--- a/activerecord/lib/active_record/type_caster/map.rb
+++ b/activerecord/lib/active_record/type_caster/map.rb
@@ -1,6 +1,6 @@
module ActiveRecord
module TypeCaster
- class Map
+ class Map # :nodoc:
def initialize(types)
@types = types
end
diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb
index 34d96b19fe..6677e6dc5f 100644
--- a/activerecord/lib/active_record/validations.rb
+++ b/activerecord/lib/active_record/validations.rb
@@ -1,8 +1,9 @@
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_internally_calls_save!
@@ -12,33 +13,39 @@ module ActiveRecord
class RecordInvalid < ActiveRecordError
attr_reader :record
- def initialize(record)
- @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"))
+ 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
# 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_validation_error
end
@@ -46,15 +53,15 @@ module ActiveRecord
# 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
@@ -63,6 +70,10 @@ module ActiveRecord
protected
+ def default_validation_context
+ new_record? ? :create : :update
+ end
+
def raise_validation_error
raise(RecordInvalid.new(self))
end
diff --git a/activerecord/lib/active_record/validations/associated.rb b/activerecord/lib/active_record/validations/associated.rb
index 47ccef31a5..b14db85167 100644
--- a/activerecord/lib/active_record/validations/associated.rb
+++ b/activerecord/lib/active_record/validations/associated.rb
@@ -2,10 +2,16 @@ module ActiveRecord
module Validations
class AssociatedValidator < ActiveModel::EachValidator #:nodoc:
def validate_each(record, attribute, value)
- if Array.wrap(value).reject {|r| r.marked_for_destruction? || r.valid?}.any?
- record.errors.add(attribute, :invalid, options.merge(:value => value))
+ if Array(value).reject { |r| valid_object?(r) }.any?
+ record.errors.add(attribute, :invalid, options.merge(value: value))
end
end
+
+ private
+
+ def valid_object?(record)
+ (record.respond_to?(:marked_for_destruction?) && record.marked_for_destruction?) || record.valid?
+ end
end
module ClassMethods
@@ -24,7 +30,8 @@ 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:
#
diff --git a/activerecord/lib/active_record/validations/presence.rb b/activerecord/lib/active_record/validations/presence.rb
index 23a3985d35..7e85ed43ac 100644
--- a/activerecord/lib/active_record/validations/presence.rb
+++ b/activerecord/lib/active_record/validations/presence.rb
@@ -31,7 +31,7 @@ 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
@@ -39,7 +39,8 @@ module ActiveRecord
#
# 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+.
+ # 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").
@@ -57,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 5106f4e127..aa2794f120 100644
--- a/activerecord/lib/active_record/validations/uniqueness.rb
+++ b/activerecord/lib/active_record/validations/uniqueness.rb
@@ -17,7 +17,13 @@ module ActiveRecord
value = map_enum_attribute(finder_class, attribute, value)
relation = build_relation(finder_class, table, attribute, value)
- relation = relation.where.not(finder_class.primary_key => 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 = relation.merge(options[:conditions]) if options[:conditions]
@@ -166,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
@@ -203,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 0d57de4d65..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!
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 5b3e57dcf6..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,6 +1,6 @@
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 %>
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/test/active_record/connection_adapters/fake_adapter.rb b/activerecord/test/active_record/connection_adapters/fake_adapter.rb
index 49a68fb94c..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,7 +16,7 @@ module ActiveRecord
def initialize(connection, logger)
super
- @tables = []
+ @data_sources = []
@primary_keys = {}
@columns = self.class.columns
end
@@ -37,7 +37,7 @@ module ActiveRecord
@columns[table_name]
end
- def table_exists?(*)
+ def data_source_exists?(*)
true
end
diff --git a/activerecord/test/cases/adapter_test.rb b/activerecord/test/cases/adapter_test.rb
index 1712ff0ac6..abe1ea7c90 100644
--- a/activerecord/test/cases/adapter_test.rb
+++ b/activerecord/test/cases/adapter_test.rb
@@ -23,7 +23,8 @@ module ActiveRecord
end
def test_tables
- tables = @connection.tables
+ tables = nil
+ ActiveSupport::Deprecation.silence { tables = @connection.tables }
assert tables.include?("accounts")
assert tables.include?("authors")
assert tables.include?("tasks")
@@ -31,9 +32,30 @@ module ActiveRecord
end
def test_table_exists?
- assert @connection.table_exists?("accounts")
- assert !@connection.table_exists?("nonexistingtable")
- assert !@connection.table_exists?(nil)
+ ActiveSupport::Deprecation.silence do
+ assert @connection.table_exists?("accounts")
+ assert !@connection.table_exists?("nonexistingtable")
+ assert !@connection.table_exists?(nil)
+ end
+ end
+
+ def test_table_exists_checking_both_tables_and_views_is_deprecated
+ assert_deprecated { @connection.table_exists?("accounts") }
+ end
+
+ def test_data_sources
+ data_sources = @connection.data_sources
+ assert data_sources.include?("accounts")
+ assert data_sources.include?("authors")
+ assert data_sources.include?("tasks")
+ assert data_sources.include?("topics")
+ end
+
+ def test_data_source_exists?
+ assert @connection.data_source_exists?("accounts")
+ assert @connection.data_source_exists?(:accounts)
+ assert_not @connection.data_source_exists?("nonexistingtable")
+ assert_not @connection.data_source_exists?(nil)
end
def test_indexes
@@ -63,7 +85,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
@@ -136,14 +158,16 @@ module ActiveRecord
def test_uniqueness_violations_are_translated_to_specific_exception
@connection.execute "INSERT INTO subscribers(nick) VALUES('me')"
- assert_raises(ActiveRecord::RecordNotUnique) do
+ error = assert_raises(ActiveRecord::RecordNotUnique) do
@connection.execute "INSERT INTO subscribers(nick) VALUES('me')"
end
+
+ assert_not_nil error.cause
end
unless current_adapter?(:SQLite3Adapter)
def test_foreign_key_violations_are_translated_to_specific_exception
- assert_raises(ActiveRecord::InvalidForeignKey) do
+ error = assert_raises(ActiveRecord::InvalidForeignKey) do
# Oracle adapter uses prefetched primary key values from sequence and passes them to connection adapter insert method
if @connection.prefetch_primary_key?
id_value = @connection.next_sequence_value(@connection.default_sequence_name("fk_test_has_fk", "id"))
@@ -152,6 +176,8 @@ module ActiveRecord
@connection.execute "INSERT INTO fk_test_has_fk (fk_id) VALUES (0)"
end
end
+
+ assert_not_nil error.cause
end
def test_foreign_key_violations_are_translated_to_specific_exception_with_validate_false
@@ -159,11 +185,13 @@ module ActiveRecord
self.table_name = 'fk_test_has_fk'
end
- assert_raises(ActiveRecord::InvalidForeignKey) do
+ error = assert_raises(ActiveRecord::InvalidForeignKey) do
has_fk = klass_has_fk.new
has_fk.fk_id = 1231231231
has_fk.save(validate: false)
end
+
+ assert_not_nil error.cause
end
end
@@ -216,13 +244,25 @@ module ActiveRecord
unless current_adapter?(:PostgreSQLAdapter)
def test_log_invalid_encoding
- assert_raise ActiveRecord::StatementInvalid do
+ error = assert_raise ActiveRecord::StatementInvalid do
@connection.send :log, "SELECT 'Ñ‹' FROM DUAL" do
raise 'Ñ‹'.force_encoding(Encoding::ASCII_8BIT)
end
end
+
+ assert_not_nil error.cause
+ end
+ end
+
+ if current_adapter?(:MysqlAdapter, :Mysql2Adapter, :SQLite3Adapter)
+ def test_tables_returning_both_tables_and_views_is_deprecated
+ assert_deprecated { @connection.tables }
end
end
+
+ def test_passing_arguments_to_tables_is_deprecated
+ assert_deprecated { @connection.tables(:books) }
+ end
end
class AdapterTestWithoutTransaction < ActiveRecord::TestCase
diff --git a/activerecord/test/cases/adapters/mysql/active_schema_test.rb b/activerecord/test/cases/adapters/mysql/active_schema_test.rb
index f0fd95ac16..8a7a0bb25d 100644
--- a/activerecord/test/cases/adapters/mysql/active_schema_test.rb
+++ b/activerecord/test/cases/adapters/mysql/active_schema_test.rb
@@ -16,8 +16,8 @@ class MysqlActiveSchemaTest < ActiveRecord::MysqlTestCase
end
def test_add_index
- # add_index calls table_exists? and index_name_exists? which can't work since execute is stubbed
- def (ActiveRecord::Base.connection).table_exists?(*); true; end
+ # add_index calls data_source_exists? and index_name_exists? which can't work since execute is stubbed
+ def (ActiveRecord::Base.connection).data_source_exists?(*); true; end
def (ActiveRecord::Base.connection).index_name_exists?(*); false; end
expected = "CREATE INDEX `index_people_on_last_name` ON `people` (`last_name`) "
@@ -60,7 +60,7 @@ class MysqlActiveSchemaTest < ActiveRecord::MysqlTestCase
end
def test_index_in_create
- def (ActiveRecord::Base.connection).table_exists?(*); false; end
+ def (ActiveRecord::Base.connection).data_source_exists?(*); false; end
%w(SPATIAL FULLTEXT UNIQUE).each do |type|
expected = "CREATE TABLE `people` (#{type} INDEX `index_people_on_last_name` (`last_name`) ) ENGINE=InnoDB"
@@ -78,7 +78,7 @@ class MysqlActiveSchemaTest < ActiveRecord::MysqlTestCase
end
def test_index_in_bulk_change
- def (ActiveRecord::Base.connection).table_exists?(*); true; end
+ def (ActiveRecord::Base.connection).data_source_exists?(*); true; end
def (ActiveRecord::Base.connection).index_name_exists?(*); false; end
%w(SPATIAL FULLTEXT UNIQUE).each do |type|
@@ -100,17 +100,15 @@ class MysqlActiveSchemaTest < ActiveRecord::MysqlTestCase
assert_equal "DROP TABLE `people`", drop_table(:people)
end
- if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
- def test_create_mysql_database_with_encoding
- assert_equal "CREATE DATABASE `matt` DEFAULT CHARACTER SET `utf8`", create_database(:matt)
- assert_equal "CREATE DATABASE `aimonetti` DEFAULT CHARACTER SET `latin1`", create_database(:aimonetti, {:charset => 'latin1'})
- assert_equal "CREATE DATABASE `matt_aimonetti` DEFAULT CHARACTER SET `big5` COLLATE `big5_chinese_ci`", create_database(:matt_aimonetti, {:charset => :big5, :collation => :big5_chinese_ci})
- end
+ def test_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_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
@@ -154,7 +152,7 @@ class MysqlActiveSchemaTest < ActiveRecord::MysqlTestCase
end
def test_indexes_in_create
- ActiveRecord::Base.connection.stubs(:table_exists?).with(:temp).returns(false)
+ ActiveRecord::Base.connection.stubs(:data_source_exists?).with(:temp).returns(false)
ActiveRecord::Base.connection.stubs(:index_name_exists?).with(:index_temp_on_zip).returns(false)
expected = "CREATE TEMPORARY TABLE `temp` ( INDEX `index_temp_on_zip` (`zip`) ) ENGINE=InnoDB AS SELECT id, name, zip FROM a_really_complicated_query"
diff --git a/activerecord/test/cases/adapters/mysql/connection_test.rb b/activerecord/test/cases/adapters/mysql/connection_test.rb
index ddbc007b87..390dd15b92 100644
--- a/activerecord/test/cases/adapters/mysql/connection_test.rb
+++ b/activerecord/test/cases/adapters/mysql/connection_test.rb
@@ -26,7 +26,7 @@ class MysqlConnectionTest < ActiveRecord::MysqlTestCase
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
@@ -118,15 +118,6 @@ class MysqlConnectionTest < ActiveRecord::MysqlTestCase
end
end
- # Test that MySQL allows multiple results for stored procedures
- if defined?(Mysql) && Mysql.const_defined?(:CLIENT_MULTI_RESULTS)
- def test_multi_results
- rows = ActiveRecord::Base.connection.select_rows('CALL ten();')
- assert_equal 10, rows[0][0].to_i, "ten() did not return 10 as expected: #{rows.inspect}"
- assert @connection.active?, "Bad connection use by 'MysqlAdapter.select_rows'"
- end
- end
-
def test_mysql_connection_collation_is_configured
assert_equal 'utf8_unicode_ci', @connection.show_variable('collation_connection')
assert_equal 'utf8_general_ci', ARUnit2Model.connection.show_variable('collation_connection')
@@ -179,11 +170,39 @@ class MysqlConnectionTest < ActiveRecord::MysqlTestCase
end
end
+ def test_get_and_release_advisory_lock
+ lock_name = "test_lock_name"
+
+ got_lock = @connection.get_advisory_lock(lock_name)
+ assert got_lock, "get_advisory_lock should have returned true but it didn't"
+
+ assert_equal test_lock_free(lock_name), false,
+ "expected the test advisory lock to be held but it wasn't"
+
+ released_lock = @connection.release_advisory_lock(lock_name)
+ assert released_lock, "expected release_advisory_lock to return true but it didn't"
+
+ assert test_lock_free(lock_name), 'expected the test lock to be available after releasing'
+ end
+
+ def test_release_non_existent_advisory_lock
+ lock_name = "fake_lock_name"
+ released_non_existent_lock = @connection.release_advisory_lock(lock_name)
+ assert_equal released_non_existent_lock, false,
+ 'expected release_advisory_lock to return false when there was no lock to release'
+ end
+
+ protected
+
+ def test_lock_free(lock_name)
+ @connection.select_value("SELECT IS_FREE_LOCK('#{lock_name}');") == '1'
+ end
+
private
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/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 b804cb45b9..d2ce48fc00 100644
--- a/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb
+++ b/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb
@@ -1,4 +1,3 @@
-
require "cases/helper"
require 'support/ddl_helper'
@@ -66,40 +65,8 @@ 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
- end
- end
-
def test_composite_primary_key
- with_example_table '`id` INT(11), `number` INT(11), foo INT(11), PRIMARY KEY (`id`, `number`)' do
+ with_example_table '`id` INT, `number` INT, foo INT, PRIMARY KEY (`id`, `number`)' do
assert_nil @conn.primary_key('ex')
end
end
@@ -141,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 ca476947fc..2024aa36ab 100644
--- a/activerecord/test/cases/adapters/mysql/quoting_test.rb
+++ b/activerecord/test/cases/adapters/mysql/quoting_test.rb
@@ -15,15 +15,15 @@ class MysqlQuotingTest < ActiveRecord::MysqlTestCase
def test_quoted_date_precision_for_gte_564
@conn.stubs(:full_version).returns('5.6.4')
- @conn.remove_instance_variable(:@version)
+ @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_quoted_date_precision_for_lt_564
@conn.stubs(:full_version).returns('5.6.3')
- @conn.remove_instance_variable(:@version)
+ @conn.remove_instance_variable(:@version) if @conn.instance_variable_defined?(:@version)
t = Time.now.change(usec: 1)
- refute_match(/\.000001\z/, @conn.quoted_date(t))
+ assert_no_match(/\.000001\z/, @conn.quoted_date(t))
end
end
diff --git a/activerecord/test/cases/adapters/mysql/schema_test.rb b/activerecord/test/cases/adapters/mysql/schema_test.rb
index 2e18f609fd..14dbdd375b 100644
--- a/activerecord/test/cases/adapters/mysql/schema_test.rb
+++ b/activerecord/test/cases/adapters/mysql/schema_test.rb
@@ -14,38 +14,30 @@ 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
-
- @connection.create_table "mysql_doubles"
- end
-
- teardown do
- @connection.drop_table "mysql_doubles", if_exists: true
- end
-
- class MysqlDouble < ActiveRecord::Base
- self.table_name = "mysql_doubles"
end
def test_float_limits
- @connection.add_column :mysql_doubles, :float_no_limit, :float
- @connection.add_column :mysql_doubles, :float_short, :float, limit: 5
- @connection.add_column :mysql_doubles, :float_long, :float, limit: 53
-
- @connection.add_column :mysql_doubles, :float_23, :float, limit: 23
- @connection.add_column :mysql_doubles, :float_24, :float, limit: 24
- @connection.add_column :mysql_doubles, :float_25, :float, limit: 25
- MysqlDouble.reset_column_information
+ @connection.create_table :mysql_doubles do |t|
+ t.float :float_no_limit
+ t.float :float_short, limit: 5
+ t.float :float_long, limit: 53
+
+ t.float :float_23, limit: 23
+ t.float :float_24, limit: 24
+ t.float :float_25, limit: 25
+ end
- column_no_limit = MysqlDouble.columns.find { |c| c.name == 'float_no_limit' }
- column_short = MysqlDouble.columns.find { |c| c.name == 'float_short' }
- column_long = MysqlDouble.columns.find { |c| c.name == 'float_long' }
+ column_no_limit = @connection.columns(:mysql_doubles).find { |c| c.name == 'float_no_limit' }
+ column_short = @connection.columns(:mysql_doubles).find { |c| c.name == 'float_short' }
+ column_long = @connection.columns(:mysql_doubles).find { |c| c.name == 'float_long' }
- column_23 = MysqlDouble.columns.find { |c| c.name == 'float_23' }
- column_24 = MysqlDouble.columns.find { |c| c.name == 'float_24' }
- column_25 = MysqlDouble.columns.find { |c| c.name == 'float_25' }
+ column_23 = @connection.columns(:mysql_doubles).find { |c| c.name == 'float_23' }
+ column_24 = @connection.columns(:mysql_doubles).find { |c| c.name == 'float_24' }
+ column_25 = @connection.columns(:mysql_doubles).find { |c| c.name == 'float_25' }
# Mysql floats are precision 0..24, Mysql doubles are precision 25..53
assert_equal 24, column_no_limit.limit
@@ -55,6 +47,8 @@ module ActiveRecord
assert_equal 24, column_23.limit
assert_equal 24, column_24.limit
assert_equal 53, column_25.limit
+ ensure
+ @connection.drop_table "mysql_doubles", if_exists: true
end
def test_schema
@@ -65,13 +59,13 @@ module ActiveRecord
assert_equal 'id', @omgpost.primary_key
end
- def test_table_exists?
+ def test_data_source_exists?
name = @omgpost.table_name
- assert @connection.table_exists?(name), "#{name} table should exist"
+ assert @connection.data_source_exists?(name), "#{name} data_source should exist"
end
- def test_table_exists_wrong_schema
- assert(!@connection.table_exists?("#{@db_name}.zomg"), "table should not exist")
+ def test_data_source_exists_wrong_schema
+ assert(!@connection.data_source_exists?("#{@db_name}.zomg"), "data_source should not exist")
end
def test_dump_indexes
diff --git a/activerecord/test/cases/adapters/mysql/sp_test.rb b/activerecord/test/cases/adapters/mysql/sp_test.rb
index a3d5110032..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::MysqlTestCase
+class MysqlStoredProcedureTest < ActiveRecord::MysqlTestCase
fixtures :topics
- # Test that MySQL allows multiple results for stored procedures
- if defined?(Mysql) && 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 25b28de7f0..d18579f242 100644
--- a/activerecord/test/cases/adapters/mysql/sql_types_test.rb
+++ b/activerecord/test/cases/adapters/mysql/sql_types_test.rb
@@ -4,7 +4,7 @@ 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 6be36566de..0d1f968022 100644
--- a/activerecord/test/cases/adapters/mysql/statement_pool_test.rb
+++ b/activerecord/test/cases/adapters/mysql/statement_pool_test.rb
@@ -3,7 +3,7 @@ require 'cases/helper'
class MysqlStatementPoolTest < ActiveRecord::MysqlTestCase
if Process.respond_to?(:fork)
def test_cache_is_per_pid
- cache = ActiveRecord::ConnectionAdapters::MysqlAdapter::StatementPool.new nil, 10
+ cache = ActiveRecord::ConnectionAdapters::MysqlAdapter::StatementPool.new(10)
cache['foo'] = 'bar'
assert_equal 'bar', cache['foo']
diff --git a/activerecord/test/cases/adapters/mysql/unsigned_type_test.rb b/activerecord/test/cases/adapters/mysql/unsigned_type_test.rb
index ed9398a918..84c5394c2e 100644
--- a/activerecord/test/cases/adapters/mysql/unsigned_type_test.rb
+++ b/activerecord/test/cases/adapters/mysql/unsigned_type_test.rb
@@ -1,6 +1,8 @@
require "cases/helper"
+require "support/schema_dumping_helper"
class MysqlUnsignedTypeTest < ActiveRecord::MysqlTestCase
+ include SchemaDumpingHelper
self.use_transactional_tests = false
class UnsignedType < ActiveRecord::Base
@@ -9,12 +11,15 @@ class MysqlUnsignedTypeTest < ActiveRecord::MysqlTestCase
setup do
@connection = ActiveRecord::Base.connection
@connection.create_table("unsigned_types", force: true) do |t|
- t.column :unsigned_integer, "int unsigned"
+ t.integer :unsigned_integer, unsigned: true
+ t.bigint :unsigned_bigint, unsigned: true
+ t.float :unsigned_float, unsigned: true
+ t.decimal :unsigned_decimal, unsigned: true, precision: 10, scale: 2
end
end
teardown do
- @connection.drop_table "unsigned_types"
+ @connection.drop_table "unsigned_types", if_exists: true
end
test "unsigned int max value is in range" do
@@ -26,5 +31,35 @@ class MysqlUnsignedTypeTest < ActiveRecord::MysqlTestCase
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 6558d60aa1..99f97c7914 100644
--- a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb
@@ -16,8 +16,8 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase
end
def test_add_index
- # add_index calls table_exists? and index_name_exists? which can't work since execute is stubbed
- def (ActiveRecord::Base.connection).table_exists?(*); true; end
+ # add_index calls data_source_exists? and index_name_exists? which can't work since execute is stubbed
+ def (ActiveRecord::Base.connection).data_source_exists?(*); true; end
def (ActiveRecord::Base.connection).index_name_exists?(*); false; end
expected = "CREATE INDEX `index_people_on_last_name` ON `people` (`last_name`) "
@@ -60,7 +60,7 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase
end
def test_index_in_create
- def (ActiveRecord::Base.connection).table_exists?(*); false; end
+ def (ActiveRecord::Base.connection).data_source_exists?(*); false; end
%w(SPATIAL FULLTEXT UNIQUE).each do |type|
expected = "CREATE TABLE `people` (#{type} INDEX `index_people_on_last_name` (`last_name`) ) ENGINE=InnoDB"
@@ -78,7 +78,7 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase
end
def test_index_in_bulk_change
- def (ActiveRecord::Base.connection).table_exists?(*); true; end
+ def (ActiveRecord::Base.connection).data_source_exists?(*); true; end
def (ActiveRecord::Base.connection).index_name_exists?(*); false; end
%w(SPATIAL FULLTEXT UNIQUE).each do |type|
@@ -100,17 +100,15 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase
assert_equal "DROP TABLE `people`", drop_table(:people)
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})
- 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_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
@@ -154,7 +152,7 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase
end
def test_indexes_in_create
- ActiveRecord::Base.connection.stubs(:table_exists?).with(:temp).returns(false)
+ ActiveRecord::Base.connection.stubs(:data_source_exists?).with(:temp).returns(false)
ActiveRecord::Base.connection.stubs(:index_name_exists?).with(:index_temp_on_zip).returns(false)
expected = "CREATE TEMPORARY TABLE `temp` ( INDEX `index_temp_on_zip` (`zip`) ) ENGINE=InnoDB AS SELECT id, name, zip FROM a_really_complicated_query"
diff --git a/activerecord/test/cases/adapters/mysql2/connection_test.rb b/activerecord/test/cases/adapters/mysql2/connection_test.rb
index 000bcadebe..507d024bb6 100644
--- a/activerecord/test/cases/adapters/mysql2/connection_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/connection_test.rb
@@ -131,4 +131,32 @@ class Mysql2ConnectionTest < ActiveRecord::Mysql2TestCase
ensure
@connection.execute "DROP TABLE `bar_baz`"
end
+
+ def test_get_and_release_advisory_lock
+ lock_name = "test_lock_name"
+
+ got_lock = @connection.get_advisory_lock(lock_name)
+ assert got_lock, "get_advisory_lock should have returned true but it didn't"
+
+ assert_equal test_lock_free(lock_name), false,
+ "expected the test advisory lock to be held but it wasn't"
+
+ released_lock = @connection.release_advisory_lock(lock_name)
+ assert released_lock, "expected release_advisory_lock to return true but it didn't"
+
+ assert test_lock_free(lock_name), 'expected the test lock to be available after releasing'
+ end
+
+ def test_release_non_existent_advisory_lock
+ lock_name = "fake_lock_name"
+ released_non_existent_lock = @connection.release_advisory_lock(lock_name)
+ assert_equal released_non_existent_lock, false,
+ 'expected release_advisory_lock to return false when there was no lock to release'
+ end
+
+ protected
+
+ def test_lock_free(lock_name)
+ @connection.select_value("SELECT IS_FREE_LOCK('#{lock_name}');") == 1
+ 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
index a49cbba4b4..2de7e1b526 100644
--- a/activerecord/test/cases/adapters/mysql2/quoting_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/quoting_test.rb
@@ -7,15 +7,15 @@ class Mysql2QuotingTest < ActiveRecord::Mysql2TestCase
test 'quoted date precision for gte 5.6.4' do
@connection.stubs(:full_version).returns('5.6.4')
- @connection.remove_instance_variable(:@version)
+ @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)
+ @connection.remove_instance_variable(:@version) if @connection.instance_variable_defined?(:@version)
t = Time.now.change(usec: 1)
- refute_match(/\.000001\z/, @connection.quoted_date(t))
+ assert_no_match(/\.000001\z/, @connection.quoted_date(t))
end
end
diff --git a/activerecord/test/cases/adapters/mysql2/schema_test.rb b/activerecord/test/cases/adapters/mysql2/schema_test.rb
index 880a2123d2..43957791b1 100644
--- a/activerecord/test/cases/adapters/mysql2/schema_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/schema_test.rb
@@ -14,11 +14,43 @@ module ActiveRecord
@db_name = db
@omgpost = Class.new(ActiveRecord::Base) do
+ self.inheritance_column = :disabled
self.table_name = "#{db}.#{table}"
def self.name; 'Post'; end
end
end
+ def test_float_limits
+ @connection.create_table :mysql_doubles do |t|
+ t.float :float_no_limit
+ t.float :float_short, limit: 5
+ t.float :float_long, limit: 53
+
+ t.float :float_23, limit: 23
+ t.float :float_24, limit: 24
+ t.float :float_25, limit: 25
+ end
+
+ column_no_limit = @connection.columns(:mysql_doubles).find { |c| c.name == 'float_no_limit' }
+ column_short = @connection.columns(:mysql_doubles).find { |c| c.name == 'float_short' }
+ column_long = @connection.columns(:mysql_doubles).find { |c| c.name == 'float_long' }
+
+ column_23 = @connection.columns(:mysql_doubles).find { |c| c.name == 'float_23' }
+ column_24 = @connection.columns(:mysql_doubles).find { |c| c.name == 'float_24' }
+ column_25 = @connection.columns(:mysql_doubles).find { |c| c.name == 'float_25' }
+
+ # Mysql floats are precision 0..24, Mysql doubles are precision 25..53
+ assert_equal 24, column_no_limit.limit
+ assert_equal 24, column_short.limit
+ assert_equal 53, column_long.limit
+
+ assert_equal 24, column_23.limit
+ assert_equal 24, column_24.limit
+ assert_equal 53, column_25.limit
+ ensure
+ @connection.drop_table "mysql_doubles", if_exists: true
+ end
+
def test_schema
assert @omgpost.first
end
@@ -27,21 +59,13 @@ module ActiveRecord
assert_equal 'id', @omgpost.primary_key
end
- def test_table_exists?
+ def test_data_source_exists?
name = @omgpost.table_name
- assert @connection.table_exists?(name), "#{name} table should exist"
- end
-
- def test_table_exists_wrong_schema
- assert(!@connection.table_exists?("#{@db_name}.zomg"), "table should not exist")
+ assert @connection.data_source_exists?(name), "#{name} data_source should exist"
end
- def test_tables_quoting
- @connection.tables(nil, "foo-bar", nil)
- flunk
- rescue => e
- # assertion for *quoted* database properly
- assert_match(/database 'foo-bar'/, e.inspect)
+ def test_data_source_exists_wrong_schema
+ assert(!@connection.data_source_exists?("#{@db_name}.zomg"), "data_source should not exist")
end
def test_dump_indexes
diff --git a/activerecord/test/cases/adapters/mysql2/sp_test.rb b/activerecord/test/cases/adapters/mysql2/sp_test.rb
new file mode 100644
index 0000000000..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 ae505d29c9..4926bc2267 100644
--- a/activerecord/test/cases/adapters/mysql2/sql_types_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/sql_types_test.rb
@@ -4,7 +4,7 @@ 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/unsigned_type_test.rb b/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb
index 9e06db2519..a6f6dd21bb 100644
--- a/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb
@@ -1,6 +1,8 @@
require "cases/helper"
+require "support/schema_dumping_helper"
class Mysql2UnsignedTypeTest < ActiveRecord::Mysql2TestCase
+ include SchemaDumpingHelper
self.use_transactional_tests = false
class UnsignedType < ActiveRecord::Base
@@ -9,12 +11,15 @@ class Mysql2UnsignedTypeTest < ActiveRecord::Mysql2TestCase
setup do
@connection = ActiveRecord::Base.connection
@connection.create_table("unsigned_types", force: true) do |t|
- t.column :unsigned_integer, "int unsigned"
+ t.integer :unsigned_integer, unsigned: true
+ t.bigint :unsigned_bigint, unsigned: true
+ t.float :unsigned_float, unsigned: true
+ t.decimal :unsigned_decimal, unsigned: true, precision: 10, scale: 2
end
end
teardown do
- @connection.drop_table "unsigned_types"
+ @connection.drop_table "unsigned_types", if_exists: true
end
test "unsigned int max value is in range" do
@@ -26,5 +31,35 @@ class Mysql2UnsignedTypeTest < ActiveRecord::Mysql2TestCase
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 dc7ba314c6..24def31e36 100644
--- a/activerecord/test/cases/adapters/postgresql/active_schema_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/active_schema_test.rb
@@ -25,7 +25,7 @@ class PostgresqlActiveSchemaTest < ActiveRecord::PostgreSQLTestCase
def test_add_index
# add_index calls index_name_exists? which can't work since execute is stubbed
- ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.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::PostgreSQLTestCase
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/connection_test.rb b/activerecord/test/cases/adapters/postgresql/connection_test.rb
index 820d41e13b..d559de3e28 100644
--- a/activerecord/test/cases/adapters/postgresql/connection_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/connection_test.rb
@@ -90,7 +90,7 @@ module ActiveRecord
end
def test_tables_logs_name
- @connection.tables('hello')
+ ActiveSupport::Deprecation.silence { @connection.tables('hello') }
assert_equal 'SCHEMA', @subscriber.logged[0][1]
end
@@ -100,7 +100,7 @@ module ActiveRecord
end
def test_table_exists_logs_name
- @connection.table_exists?('items')
+ ActiveSupport::Deprecation.silence { @connection.table_exists?('items') }
assert_equal 'SCHEMA', @subscriber.logged[0][1]
end
@@ -127,7 +127,7 @@ module ActiveRecord
def test_statement_key_is_logged
bind = Relation::QueryAttribute.new(nil, 1, Type::Value.new)
- @connection.exec_query('SELECT $1::integer', 'SQL', [bind])
+ @connection.exec_query('SELECT $1::integer', 'SQL', [bind], prepare: true)
name = @subscriber.payloads.last[:statement_name]
assert name
res = @connection.exec_query("EXPLAIN (FORMAT JSON) EXECUTE #{name}(1)")
@@ -209,5 +209,47 @@ module ActiveRecord
ActiveRecord::Base.establish_connection(orig_connection.deep_merge({:variables => {:debug_print_plan => :default}}))
end
end
+
+ def test_get_and_release_advisory_lock
+ lock_id = 5295901941911233559
+ list_advisory_locks = <<-SQL
+ SELECT locktype,
+ (classid::bigint << 32) | objid::bigint AS lock_id
+ FROM pg_locks
+ WHERE locktype = 'advisory'
+ SQL
+
+ got_lock = @connection.get_advisory_lock(lock_id)
+ assert got_lock, "get_advisory_lock should have returned true but it didn't"
+
+ advisory_lock = @connection.query(list_advisory_locks).find {|l| l[1] == lock_id}
+ assert advisory_lock,
+ "expected to find an advisory lock with lock_id #{lock_id} but there wasn't one"
+
+ released_lock = @connection.release_advisory_lock(lock_id)
+ assert released_lock, "expected release_advisory_lock to return true but it didn't"
+
+ advisory_locks = @connection.query(list_advisory_locks).select {|l| l[1] == lock_id}
+ assert_empty advisory_locks,
+ "expected to have released advisory lock with lock_id #{lock_id} but it was still held"
+ end
+
+ def test_release_non_existent_advisory_lock
+ fake_lock_id = 2940075057017742022
+ with_warning_suppression do
+ released_non_existent_lock = @connection.release_advisory_lock(fake_lock_id)
+ assert_equal released_non_existent_lock, false,
+ 'expected release_advisory_lock to return false when there was no lock to release'
+ end
+ end
+
+ protected
+
+ def with_warning_suppression
+ log_level = @connection.client_min_messages
+ @connection.client_min_messages = 'error'
+ yield
+ @connection.client_min_messages = log_level
+ end
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/json_test.rb b/activerecord/test/cases/adapters/postgresql/json_test.rb
index f242f32496..b3b121b4fb 100644
--- a/activerecord/test/cases/adapters/postgresql/json_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/json_test.rb
@@ -1,4 +1,3 @@
-# -*- coding: utf-8 -*-
require "cases/helper"
require 'support/schema_dumping_helper'
diff --git a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb
index 6e6850c4a9..e361521155 100644
--- a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb
@@ -123,6 +123,20 @@ module ActiveRecord
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
connection = connection_without_insert_returning
result = connection.sql_for_insert('sql', nil, nil, nil, 'binds')
diff --git a/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb b/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb
index fa6584eae5..a0afd922b2 100644
--- a/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb
@@ -31,7 +31,7 @@ class SchemaAuthorizationTest < ActiveRecord::PostgreSQLTestCase
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
diff --git a/activerecord/test/cases/adapters/postgresql/schema_test.rb b/activerecord/test/cases/adapters/postgresql/schema_test.rb
index 35d5581aa7..7c9169f6e2 100644
--- a/activerecord/test/cases/adapters/postgresql/schema_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/schema_test.rb
@@ -2,7 +2,19 @@ require "cases/helper"
require 'models/default'
require 'support/schema_dumping_helper'
+module PGSchemaHelper
+ def with_schema_search_path(schema_search_path)
+ @connection.schema_search_path = schema_search_path
+ @connection.schema_cache.clear!
+ yield if block_given?
+ ensure
+ @connection.schema_search_path = "'$user', public"
+ @connection.schema_cache.clear!
+ end
+end
+
class SchemaTest < ActiveRecord::PostgreSQLTestCase
+ include PGSchemaHelper
self.use_transactional_tests = false
SCHEMA_NAME = 'test_schema'
@@ -84,8 +96,8 @@ class SchemaTest < ActiveRecord::PostgreSQLTestCase
end
teardown do
- @connection.execute "DROP SCHEMA #{SCHEMA2_NAME} CASCADE"
- @connection.execute "DROP SCHEMA #{SCHEMA_NAME} CASCADE"
+ @connection.drop_schema SCHEMA2_NAME, if_exists: true
+ @connection.drop_schema SCHEMA_NAME, if_exists: true
end
def test_schema_names
@@ -121,10 +133,17 @@ class SchemaTest < ActiveRecord::PostgreSQLTestCase
assert !@connection.schema_names.include?("test_schema3")
end
+ def test_drop_schema_if_exists
+ @connection.create_schema "some_schema"
+ assert_includes @connection.schema_names, "some_schema"
+ @connection.drop_schema "some_schema", if_exists: true
+ assert_not_includes @connection.schema_names, "some_schema"
+ end
+
def test_habtm_table_name_with_schema
+ ActiveRecord::Base.connection.drop_schema "music", if_exists: true
+ ActiveRecord::Base.connection.create_schema "music"
ActiveRecord::Base.connection.execute <<-SQL
- DROP SCHEMA IF EXISTS music CASCADE;
- CREATE SCHEMA music;
CREATE TABLE music.albums (id serial primary key);
CREATE TABLE music.songs (id serial primary key);
CREATE TABLE music.albums_songs (album_id integer, song_id integer);
@@ -134,18 +153,22 @@ class SchemaTest < ActiveRecord::PostgreSQLTestCase
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
@@ -161,42 +184,42 @@ class SchemaTest < ActiveRecord::PostgreSQLTestCase
@connection.exec_query("alter table developers drop column zomg", 'sql', []) if altered
end
- def test_table_exists?
+ def test_data_source_exists?
[Thing1, Thing2, Thing3, Thing4].each do |klass|
name = klass.table_name
- assert @connection.table_exists?(name), "'#{name}' table should exist"
+ assert @connection.data_source_exists?(name), "'#{name}' data_source should exist"
end
end
- def test_table_exists_when_on_schema_search_path
+ def test_data_source_exists_when_on_schema_search_path
with_schema_search_path(SCHEMA_NAME) do
- assert(@connection.table_exists?(TABLE_NAME), "table should exist and be found")
+ assert(@connection.data_source_exists?(TABLE_NAME), "data_source should exist and be found")
end
end
- def test_table_exists_when_not_on_schema_search_path
+ def test_data_source_exists_when_not_on_schema_search_path
with_schema_search_path('PUBLIC') do
- assert(!@connection.table_exists?(TABLE_NAME), "table exists but should not be found")
+ assert(!@connection.data_source_exists?(TABLE_NAME), "data_source exists but should not be found")
end
end
- def test_table_exists_wrong_schema
- assert(!@connection.table_exists?("foo.things"), "table should not exist")
+ def test_data_source_exists_wrong_schema
+ assert(!@connection.data_source_exists?("foo.things"), "data_source should not exist")
end
- def test_table_exists_quoted_names
+ def test_data_source_exists_quoted_names
[ %("#{SCHEMA_NAME}"."#{TABLE_NAME}"), %(#{SCHEMA_NAME}."#{TABLE_NAME}"), %(#{SCHEMA_NAME}."#{TABLE_NAME}")].each do |given|
- assert(@connection.table_exists?(given), "table should exist when specified as #{given}")
+ assert(@connection.data_source_exists?(given), "data_source should exist when specified as #{given}")
end
with_schema_search_path(SCHEMA_NAME) do
given = %("#{TABLE_NAME}")
- assert(@connection.table_exists?(given), "table should exist when specified as #{given}")
+ assert(@connection.data_source_exists?(given), "data_source should exist when specified as #{given}")
end
end
- def test_table_exists_quoted_table
+ def test_data_source_exists_quoted_table
with_schema_search_path(SCHEMA_NAME) do
- assert(@connection.table_exists?('"things.table"'), "table should exist")
+ assert(@connection.data_source_exists?('"things.table"'), "data_source should exist")
end
end
@@ -300,11 +323,11 @@ class SchemaTest < ActiveRecord::PostgreSQLTestCase
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
@@ -404,13 +427,6 @@ class SchemaTest < ActiveRecord::PostgreSQLTestCase
end
end
- def with_schema_search_path(schema_search_path)
- @connection.schema_search_path = schema_search_path
- yield if block_given?
- ensure
- @connection.schema_search_path = "'$user', public"
- end
-
def do_dump_index_tests_for_schema(this_schema_name, first_index_column_name, second_index_column_name, third_index_column_name, fourth_index_column_name)
with_schema_search_path(this_schema_name) do
indexes = @connection.indexes(TABLE_NAME).sort_by(&:name)
@@ -462,14 +478,14 @@ class SchemaForeignKeyTest < ActiveRecord::PostgreSQLTestCase
ensure
@connection.drop_table "wagons", if_exists: true
@connection.drop_table "my_schema.trains", if_exists: true
- @connection.execute "DROP SCHEMA IF EXISTS my_schema"
+ @connection.drop_schema "my_schema", if_exists: true
end
end
class DefaultsUsingMultipleSchemasAndDomainTest < ActiveRecord::PostgreSQLTestCase
setup do
@connection = ActiveRecord::Base.connection
- @connection.execute "DROP SCHEMA IF EXISTS schema_1 CASCADE"
+ @connection.drop_schema "schema_1", if_exists: true
@connection.execute "CREATE SCHEMA schema_1"
@connection.execute "CREATE DOMAIN schema_1.text AS text"
@connection.execute "CREATE DOMAIN schema_1.varchar AS varchar"
@@ -487,7 +503,7 @@ class DefaultsUsingMultipleSchemasAndDomainTest < ActiveRecord::PostgreSQLTestCa
teardown do
@connection.schema_search_path = @old_search_path
- @connection.execute "DROP SCHEMA IF EXISTS schema_1 CASCADE"
+ @connection.drop_schema "schema_1", if_exists: true
Default.reset_column_information
end
@@ -519,3 +535,40 @@ class DefaultsUsingMultipleSchemasAndDomainTest < ActiveRecord::PostgreSQLTestCa
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/view_test.rb b/activerecord/test/cases/adapters/postgresql/view_test.rb
deleted file mode 100644
index 2dd6ec5fe6..0000000000
--- a/activerecord/test/cases/adapters/postgresql/view_test.rb
+++ /dev/null
@@ -1,64 +0,0 @@
-require "cases/helper"
-require "cases/view_test"
-
-class UpdateableViewTest < ActiveRecord::PostgreSQLTestCase
- fixtures :books
-
- class PrintedBook < ActiveRecord::Base
- self.primary_key = "id"
- end
-
- setup do
- @connection = ActiveRecord::Base.connection
- @connection.execute <<-SQL
- CREATE VIEW printed_books
- AS SELECT id, name, status, format FROM books WHERE format = 'paperback'
- SQL
- end
-
- teardown do
- @connection.execute "DROP VIEW printed_books" if @connection.table_exists? "printed_books"
- end
-
- def test_update_record
- book = PrintedBook.first
- book.name = "AWDwR"
- book.save!
- book.reload
- assert_equal "AWDwR", book.name
- end
-
- def test_insert_record
- PrintedBook.create! name: "Rails in Action", status: 0, format: "paperback"
-
- new_book = PrintedBook.last
- assert_equal "Rails in Action", new_book.name
- end
-
- def test_update_record_to_fail_view_conditions
- book = PrintedBook.first
- book.format = "ebook"
- book.save!
-
- assert_raises ActiveRecord::RecordNotFound do
- book.reload
- end
- end
-end
-
-if ActiveRecord::Base.connection.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/adapters/sqlite3/sqlite3_adapter_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb
index 7996e7ad50..a2fd1177a6 100644
--- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb
+++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb
@@ -284,9 +284,9 @@ module ActiveRecord
def test_tables
with_example_table do
- assert_equal %w{ ex }, @conn.tables
+ ActiveSupport::Deprecation.silence { assert_equal %w{ ex }, @conn.tables }
with_example_table 'id integer PRIMARY KEY AUTOINCREMENT, number integer', 'people' do
- assert_equal %w{ ex people }.sort, @conn.tables.sort
+ ActiveSupport::Deprecation.silence { assert_equal %w{ ex people }.sort, @conn.tables.sort }
end
end
end
@@ -294,10 +294,12 @@ module ActiveRecord
def test_tables_logs_name
sql = <<-SQL
SELECT name FROM sqlite_master
- WHERE (type = 'table' OR type = 'view') AND NOT name = 'sqlite_sequence'
+ WHERE type IN ('table','view') AND name <> 'sqlite_sequence'
SQL
assert_logged [[sql.squish, 'SCHEMA', []]] do
- @conn.tables('hello')
+ ActiveSupport::Deprecation.silence do
+ @conn.tables('hello')
+ end
end
end
@@ -313,11 +315,12 @@ module ActiveRecord
with_example_table do
sql = <<-SQL
SELECT name FROM sqlite_master
- WHERE (type = 'table' OR type = 'view')
- AND NOT name = 'sqlite_sequence' AND name = \"ex\"
+ WHERE type IN ('table','view') AND name <> 'sqlite_sequence' AND name = 'ex'
SQL
assert_logged [[sql.squish, 'SCHEMA', []]] do
- assert @conn.table_exists?('ex')
+ ActiveSupport::Deprecation.silence do
+ assert @conn.table_exists?('ex')
+ end
end
end
end
@@ -425,13 +428,16 @@ module ActiveRecord
configurations['arunit']['database'])
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/statement_pool_test.rb b/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb
index ef324183a7..559b951109 100644
--- a/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb
+++ b/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb
@@ -6,7 +6,7 @@ module ActiveRecord::ConnectionAdapters
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']
diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb
index 73cde05504..938350627f 100644
--- a/activerecord/test/cases/associations/belongs_to_associations_test.rb
+++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb
@@ -1,6 +1,5 @@
require 'cases/helper'
require 'models/developer'
-require 'models/computer'
require 'models/project'
require 'models/company'
require 'models/topic'
@@ -21,6 +20,9 @@ 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,
@@ -91,7 +93,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
end
account = model.new
- refute account.valid?
+ assert_not account.valid?
assert_equal [{error: :blank}], account.errors.details[:company]
ensure
ActiveRecord::Base.belongs_to_required_by_default = original_value
@@ -108,7 +110,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
end
account = model.new
- refute account.valid?
+ assert_not account.valid?
assert_equal [{error: :blank}], account.errors.details[:company]
ensure
ActiveRecord::Base.belongs_to_required_by_default = original_value
@@ -347,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"
@@ -482,7 +500,9 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
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
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/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb
index ffbf60e390..0c09713971 100644
--- a/activerecord/test/cases/associations/eager_test.rb
+++ b/activerecord/test/cases/associations/eager_test.rb
@@ -24,6 +24,8 @@ require 'models/membership'
require 'models/club'
require 'models/categorization'
require 'models/sponsor'
+require 'models/mentor'
+require 'models/contract'
class EagerAssociationTest < ActiveRecord::TestCase
fixtures :posts, :comments, :authors, :essays, :author_addresses, :categories, :categories_posts,
@@ -108,53 +110,57 @@ class EagerAssociationTest < ActiveRecord::TestCase
end
def test_preloading_has_many_in_multiple_queries_with_more_ids_than_database_can_handle
- Comment.connection.expects(:in_clause_length).at_least_once.returns(5)
- posts = Post.all.merge!(:includes=>:comments).to_a
- assert_equal 11, posts.size
+ assert_called(Comment.connection, :in_clause_length, returns: 5) do
+ posts = Post.all.merge!(:includes=>:comments).to_a
+ assert_equal 11, posts.size
+ end
end
def test_preloading_has_many_in_one_queries_when_database_has_no_limit_on_ids_it_can_handle
- Comment.connection.expects(:in_clause_length).at_least_once.returns(nil)
- posts = Post.all.merge!(:includes=>:comments).to_a
- assert_equal 11, posts.size
+ assert_called(Comment.connection, :in_clause_length, returns: nil) do
+ posts = Post.all.merge!(:includes=>:comments).to_a
+ assert_equal 11, posts.size
+ end
end
def test_preloading_habtm_in_multiple_queries_with_more_ids_than_database_can_handle
- Comment.connection.expects(:in_clause_length).at_least_once.returns(5)
- posts = Post.all.merge!(:includes=>:categories).to_a
- assert_equal 11, posts.size
+ assert_called(Comment.connection, :in_clause_length, times: 2, returns: 5) do
+ posts = Post.all.merge!(:includes=>:categories).to_a
+ assert_equal 11, posts.size
+ end
end
def test_preloading_habtm_in_one_queries_when_database_has_no_limit_on_ids_it_can_handle
- Comment.connection.expects(:in_clause_length).at_least_once.returns(nil)
- posts = Post.all.merge!(:includes=>:categories).to_a
- assert_equal 11, posts.size
+ assert_called(Comment.connection, :in_clause_length, times: 2, returns: nil) do
+ posts = Post.all.merge!(:includes=>:categories).to_a
+ assert_equal 11, posts.size
+ end
end
def test_load_associated_records_in_one_query_when_adapter_has_no_limit
- Comment.connection.expects(:in_clause_length).at_least_once.returns(nil)
-
- post = posts(:welcome)
- assert_queries(2) do
- Post.includes(:comments).where(:id => post.id).to_a
+ assert_called(Comment.connection, :in_clause_length, returns: nil) do
+ post = posts(:welcome)
+ assert_queries(2) do
+ Post.includes(:comments).where(:id => post.id).to_a
+ end
end
end
def test_load_associated_records_in_several_queries_when_many_ids_passed
- Comment.connection.expects(:in_clause_length).at_least_once.returns(1)
-
- post1, post2 = posts(:welcome), posts(:thinking)
- assert_queries(3) do
- Post.includes(:comments).where(:id => [post1.id, post2.id]).to_a
+ assert_called(Comment.connection, :in_clause_length, returns: 1) do
+ post1, post2 = posts(:welcome), posts(:thinking)
+ assert_queries(3) do
+ Post.includes(:comments).where(:id => [post1.id, post2.id]).to_a
+ end
end
end
def test_load_associated_records_in_one_query_when_a_few_ids_passed
- Comment.connection.expects(:in_clause_length).at_least_once.returns(3)
-
- post = posts(:welcome)
- assert_queries(2) do
- Post.includes(:comments).where(:id => post.id).to_a
+ assert_called(Comment.connection, :in_clause_length, returns: 3) do
+ post = posts(:welcome)
+ assert_queries(2) do
+ Post.includes(:comments).where(:id => post.id).to_a
+ end
end
end
@@ -1173,6 +1179,24 @@ class EagerAssociationTest < ActiveRecord::TestCase
assert_equal 1, mary.unique_categorized_post_ids.length
end
+ def test_preloading_has_one_using_reorder
+ klass = Class.new(ActiveRecord::Base) do
+ def self.name; "TempAuthor"; end
+ self.table_name = "authors"
+ has_one :post, class_name: "PostWithDefaultScope", foreign_key: :author_id
+ has_one :reorderd_post, -> { reorder(title: :desc) }, class_name: "PostWithDefaultScope", foreign_key: :author_id
+ end
+
+ author = klass.first
+ # PRECONDITION: make sure ordering results in different results
+ assert_not_equal author.post, author.reorderd_post
+
+ preloaded_reorderd_post = klass.preload(:reorderd_post).first.reorderd_post
+
+ assert_equal author.reorderd_post, preloaded_reorderd_post
+ assert_equal Post.order(title: :desc).first.title, preloaded_reorderd_post.title
+ end
+
def test_preloading_polymorphic_with_custom_foreign_type
sponsor = sponsors(:moustache_club_sponsor_for_groucho)
groucho = members(:groucho)
@@ -1197,12 +1221,6 @@ class EagerAssociationTest < ActiveRecord::TestCase
end
end
- def test_join_eager_with_nil_order_should_generate_valid_sql
- assert_nothing_raised(ActiveRecord::StatementInvalid) do
- Post.includes(:comments).order(nil).where(:comments => {:body => "Thank you for the welcome"}).first
- end
- end
-
def test_deep_including_through_habtm
# warm up habtm cache
posts = Post.all.merge!(:includes => {:categories => :categorizations}, :order => "posts.id").to_a
@@ -1214,6 +1232,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
@@ -1325,6 +1353,14 @@ class EagerAssociationTest < ActiveRecord::TestCase
assert_match message, error.message
end
+ test "preload with invalid argument" do
+ exception = assert_raises(ArgumentError) do
+ Author.preload(10).to_a
+ end
+ assert_equal('10 was not recognized for preload', exception.message)
+ end
+
+
test "preloading readonly association" do
# has-one
firm = Firm.where(id: "1").preload(:readonly_account).first!
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 abbe37ab38..ccb062f991 100644
--- a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb
+++ b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb
@@ -3,6 +3,7 @@ require 'models/developer'
require 'models/computer'
require 'models/project'
require 'models/company'
+require 'models/course'
require 'models/customer'
require 'models/order'
require 'models/categorization'
@@ -14,6 +15,7 @@ require 'models/tagging'
require 'models/parrot'
require 'models/person'
require 'models/pirate'
+require 'models/professor'
require 'models/treasure'
require 'models/price_estimate'
require 'models/club'
@@ -93,6 +95,15 @@ class DeveloperWithExtendOption < Developer
has_and_belongs_to_many :projects, extend: NamedExtension
end
+class ProjectUnscopingDavidDefaultScope < ActiveRecord::Base
+ self.table_name = 'projects'
+ has_and_belongs_to_many :developers, -> { unscope(where: 'name') },
+ class_name: "LazyBlockDeveloperCalledDavid",
+ join_table: "developers_projects",
+ foreign_key: "project_id",
+ association_foreign_key: "developer_id"
+end
+
class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
fixtures :accounts, :companies, :categories, :posts, :categories_posts, :developers, :projects, :developers_projects,
:parrots, :pirates, :parrots_pirates, :treasures, :price_estimates, :tags, :taggings, :computers
@@ -794,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
@@ -923,4 +935,51 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
assert_deprecated { developer.projects(true) }
end
+
+ def test_alternate_database
+ professor = Professor.create(name: "Plum")
+ course = Course.create(name: "Forensics")
+ assert_equal 0, professor.courses.count
+ assert_nothing_raised do
+ professor.courses << course
+ end
+ assert_equal 1, professor.courses.count
+ end
+
+ def test_habtm_scope_can_unscope
+ project = ProjectUnscopingDavidDefaultScope.new
+ project.save!
+
+ developer = LazyBlockDeveloperCalledDavid.new(name: "Not David")
+ developer.save!
+ project.developers << developer
+
+ projects = ProjectUnscopingDavidDefaultScope.includes(:developers).where(id: project.id)
+ assert_equal 1, projects.first.developers.size
+ end
+
+ def test_preloaded_associations_size
+ assert_equal Project.first.salaried_developers.size,
+ Project.preload(:salaried_developers).first.salaried_developers.size
+
+ assert_equal Project.includes(:salaried_developers).references(:salaried_developers).first.salaried_developers.size,
+ Project.preload(:salaried_developers).first.salaried_developers.size
+
+ # Nested HATBM
+ first_project = Developer.first.projects.first
+ preloaded_first_project =
+ Developer.preload(projects: :salaried_developers).
+ first.
+ projects.
+ detect { |p| p.id == first_project.id }
+
+ assert preloaded_first_project.salaried_developers.loaded?, true
+ assert_equal first_project.salaried_developers.size, preloaded_first_project.salaried_developers.size
+ end
+
+ def test_has_and_belongs_to_many_is_useable_with_belongs_to_required_by_default
+ assert_difference "Project.first.developers_required_by_default.size", 1 do
+ Project.first.developers_required_by_default.create!(name: "Sean", salary: 50000)
+ end
+ end
end
diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb
index c2ef59d0f7..50ca6537cc 100644
--- a/activerecord/test/cases/associations/has_many_associations_test.rb
+++ b/activerecord/test/cases/associations/has_many_associations_test.rb
@@ -31,6 +31,8 @@ require 'models/student'
require 'models/pirate'
require 'models/ship'
require 'models/ship_part'
+require 'models/treasure'
+require 'models/parrot'
require 'models/tyre'
require 'models/subscriber'
require 'models/subscription'
@@ -168,7 +170,9 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
part = ShipPart.create(name: 'cockpit')
updated_at = part.updated_at
- ship.parts << part
+ travel(1.second) do
+ ship.parts << part
+ end
assert_equal part.ship, ship
assert_not_equal part.updated_at, updated_at
@@ -199,9 +203,22 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
bulb = car.bulbs.create
assert_equal 'defaulty', bulb.name
+ end
+
+ def test_build_and_create_from_association_should_respect_passed_attributes_over_default_scope
+ car = Car.create(name: 'honda')
+
+ bulb = car.bulbs.build(name: 'exotic')
+ assert_equal 'exotic', bulb.name
- bulb = car.bulbs.create(:name => 'exotic')
+ bulb = car.bulbs.create(name: 'exotic')
assert_equal 'exotic', bulb.name
+
+ bulb = car.awesome_bulbs.build(frickinawesome: false)
+ assert_equal false, bulb.frickinawesome
+
+ bulb = car.awesome_bulbs.create(frickinawesome: false)
+ assert_equal false, bulb.frickinawesome
end
def test_build_from_association_should_respect_scope
@@ -932,6 +949,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
@@ -1461,6 +1497,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
@@ -2139,6 +2194,26 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal [bulb1, bulb2], car.all_bulbs.sort_by(&:id)
end
+ test "can unscope and where the default scope of the associated model" do
+ Car.has_many :other_bulbs, -> { unscope(where: [:name]).where(name: 'other') }, class_name: "Bulb"
+ car = Car.create!
+ bulb1 = Bulb.create! name: "defaulty", car: car
+ bulb2 = Bulb.create! name: "other", car: car
+
+ assert_equal [bulb1], car.bulbs
+ assert_equal [bulb2], car.other_bulbs
+ end
+
+ test "can rewhere the default scope of the associated model" do
+ Car.has_many :old_bulbs, -> { rewhere(name: 'old') }, class_name: "Bulb"
+ car = Car.create!
+ bulb1 = Bulb.create! name: "defaulty", car: car
+ bulb2 = Bulb.create! name: "old", car: car
+
+ assert_equal [bulb1], car.bulbs
+ assert_equal [bulb2], car.old_bulbs
+ end
+
test 'unscopes the default scope of associated model when used with include' do
car = Car.create!
bulb = Bulb.create! name: "other", car: car
@@ -2278,4 +2353,42 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
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 ce2557339e..226ecf5447 100644
--- a/activerecord/test/cases/associations/has_many_through_associations_test.rb
+++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb
@@ -744,8 +744,9 @@ 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
@@ -765,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
@@ -1109,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
@@ -1171,4 +1173,32 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
assert_deprecated { post.people(true) }
end
+
+ def test_has_many_through_do_not_cache_association_reader_if_the_though_method_has_default_scopes
+ member = Member.create!
+ club = Club.create!
+ TenantMembership.create!(
+ member: member,
+ club: club
+ )
+
+ TenantMembership.current_member = member
+
+ tenant_clubs = member.tenant_clubs
+ assert_equal [club], tenant_clubs
+
+ TenantMembership.current_member = nil
+
+ other_member = Member.create!
+ other_club = Club.create!
+ TenantMembership.create!(
+ member: other_member,
+ club: other_club
+ )
+
+ tenant_clubs = other_member.tenant_clubs
+ assert_equal [other_club], tenant_clubs
+ ensure
+ TenantMembership.current_member = nil
+ end
end
diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb
index 5a8afaf4d2..c9d9e29f09 100644
--- a/activerecord/test/cases/associations/has_one_associations_test.rb
+++ b/activerecord/test/cases/associations/has_one_associations_test.rb
@@ -107,6 +107,14 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
assert_nil Account.find(old_account_id).firm_id
end
+ def test_nullification_on_destroyed_association
+ developer = Developer.create!(name: "Someone")
+ ship = Ship.create!(name: "Planet Caravan", developer: developer)
+ ship.destroy
+ assert !ship.persisted?
+ assert !developer.persisted?
+ end
+
def test_natural_assignment_to_nil_after_destroy
firm = companies(:rails_core)
old_account_id = firm.account.id
@@ -211,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
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 97fd458994..b2b46812b9 100644
--- a/activerecord/test/cases/associations/has_one_through_associations_test.rb
+++ b/activerecord/test/cases/associations/has_one_through_associations_test.rb
@@ -16,6 +16,10 @@ require 'models/owner'
require 'models/post'
require 'models/comment'
require 'models/categorization'
+require 'models/customer'
+require 'models/carrier'
+require 'models/shop_account'
+require 'models/customer_carrier'
class HasOneThroughAssociationsTest < ActiveRecord::TestCase
fixtures :member_types, :members, :clubs, :memberships, :sponsors, :organizations, :minivans,
@@ -347,9 +351,33 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase
end
end
- def test_association_force_reload_with_only_true_is_deprecated
- member = Member.find(1)
+ 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)
- assert_deprecated { member.club(true) }
+ CustomerCarrier.current_customer = customer
+
+ account_carrier = account.carrier
+ assert_equal carrier, account_carrier
+
+ CustomerCarrier.current_customer = nil
+
+ other_carrier = Carrier.create!
+ other_customer = Customer.create!
+ other_customer_carrier = CustomerCarrier.create!(
+ customer: other_customer,
+ carrier: other_carrier,
+ )
+ other_account = ShopAccount.create!(customer_carrier: other_customer_carrier)
+
+ account_carrier = other_account.carrier
+ assert_equal other_carrier, account_carrier
+ ensure
+ CustomerCarrier.current_customer = nil
end
end
diff --git a/activerecord/test/cases/associations/inverse_associations_test.rb b/activerecord/test/cases/associations/inverse_associations_test.rb
index 423b8238b1..57d1c8feda 100644
--- a/activerecord/test/cases/associations/inverse_associations_test.rb
+++ b/activerecord/test/cases/associations/inverse_associations_test.rb
@@ -13,6 +13,9 @@ require 'models/mixed_case_monkey'
require 'models/admin'
require 'models/admin/account'
require 'models/admin/user'
+require 'models/developer'
+require 'models/company'
+require 'models/project'
class AutomaticInverseFindingTests < ActiveRecord::TestCase
fixtures :ratings, :comments, :cars
@@ -80,10 +83,10 @@ class AutomaticInverseFindingTests < ActiveRecord::TestCase
assert_equal rating.comment, comment, "The Rating's comment should be the original Comment"
- rating.comment.body = "Brogramming is the act of programming, like a bro."
+ rating.comment.body = "Fennec foxes are the smallest of the foxes."
assert_equal rating.comment.body, comment.body, "Changing the Comment's body on the association should change the original Comment's body"
- comment.body = "Broseiden is the king of the sea of bros."
+ comment.body = "Kittens are adorable."
assert_equal comment.body, rating.comment.body, "Changing the original Comment's body should change the Comment's body on the association"
end
@@ -94,10 +97,10 @@ class AutomaticInverseFindingTests < ActiveRecord::TestCase
assert_equal rating.comment, comment, "The Rating's comment should be the original Comment"
- rating.comment.body = "Brogramming is the act of programming, like a bro."
+ rating.comment.body = "Fennec foxes are the smallest of the foxes."
assert_equal rating.comment.body, comment.body, "Changing the Comment's body on the association should change the original Comment's body"
- comment.body = "Broseiden is the king of the sea of bros."
+ comment.body = "Kittens are adorable."
assert_equal comment.body, rating.comment.body, "Changing the original Comment's body should change the Comment's body on the association"
end
@@ -198,6 +201,16 @@ class InverseAssociationTests < ActiveRecord::TestCase
belongs_to_ref = Sponsor.reflect_on_association(:sponsor_club)
assert_nil belongs_to_ref.inverse_of
end
+
+ def test_this_inverse_stuff
+ firm = Firm.create!(name: 'Adequate Holdings')
+ Project.create!(name: 'Project 1', firm: firm)
+ Developer.create!(name: 'Gorbypuff', firm: firm)
+
+ new_project = Project.last
+ assert Project.reflect_on_association(:lead_developer).inverse_of.present?, "Expected inverse of to be present"
+ assert new_project.lead_developer.present?, "Expected lead developer to be present on the project"
+ end
end
class InverseHasOneTests < ActiveRecord::TestCase
diff --git a/activerecord/test/cases/associations/left_outer_join_association_test.rb b/activerecord/test/cases/associations/left_outer_join_association_test.rb
new file mode 100644
index 0000000000..4af791b758
--- /dev/null
+++ b/activerecord/test/cases/associations/left_outer_join_association_test.rb
@@ -0,0 +1,79 @@
+require "cases/helper"
+require 'models/post'
+require 'models/comment'
+require 'models/author'
+require 'models/essay'
+require 'models/categorization'
+require 'models/person'
+
+class LeftOuterJoinAssociationTest < ActiveRecord::TestCase
+ fixtures :authors, :essays, :posts, :comments, :categorizations, :people
+
+ def test_construct_finder_sql_applies_aliases_tables_on_association_conditions
+ result = Author.left_outer_joins(:thinking_posts, :welcome_posts).to_a
+ assert_equal authors(:david), result.first
+ end
+
+ def test_construct_finder_sql_does_not_table_name_collide_on_duplicate_associations
+ assert_nothing_raised do
+ queries = capture_sql do
+ Person.left_outer_joins(:agents => {:agents => :agents})
+ .left_outer_joins(:agents => {:agents => {:primary_contact => :agents}}).to_a
+ end
+ assert queries.any? { |sql| /agents_people_4/i =~ sql }
+ end
+ end
+
+ def test_construct_finder_sql_executes_a_left_outer_join
+ assert_not_equal Author.count, Author.joins(:posts).count
+ assert_equal Author.count, Author.left_outer_joins(:posts).count
+ end
+
+ def test_left_outer_join_by_left_joins
+ assert_not_equal Author.count, Author.joins(:posts).count
+ assert_equal Author.count, Author.left_joins(:posts).count
+ end
+
+ def test_construct_finder_sql_ignores_empty_left_outer_joins_hash
+ queries = capture_sql { Author.left_outer_joins({}) }
+ assert queries.none? { |sql| /LEFT OUTER JOIN/i =~ sql }
+ end
+
+ def test_construct_finder_sql_ignores_empty_left_outer_joins_array
+ queries = capture_sql { Author.left_outer_joins([]) }
+ assert queries.none? { |sql| /LEFT OUTER JOIN/i =~ sql }
+ end
+
+ def test_left_outer_joins_forbids_to_use_string_as_argument
+ assert_raise(ArgumentError){ Author.left_outer_joins('LEFT OUTER JOIN "posts" ON "posts"."user_id" = "users"."id"').to_a }
+ end
+
+ def test_join_conditions_added_to_join_clause
+ queries = capture_sql { Author.left_outer_joins(:essays).to_a }
+ assert queries.any? { |sql| /writer_type.*?=.*?(Author|\?|\$1)/i =~ sql }
+ assert queries.none? { |sql| /WHERE/i =~ sql }
+ end
+
+ def test_find_with_sti_join
+ scope = Post.left_outer_joins(:special_comments).where(:id => posts(:sti_comments).id)
+
+ # The join should match SpecialComment and its subclasses only
+ assert scope.where("comments.type" => "Comment").empty?
+ assert !scope.where("comments.type" => "SpecialComment").empty?
+ assert !scope.where("comments.type" => "SubSpecialComment").empty?
+ end
+
+ def test_does_not_override_select
+ authors = Author.select("authors.name, #{%{(authors.author_address_id || ' ' || authors.author_address_extra_id) as addr_id}}").left_outer_joins(:posts)
+ assert authors.any?
+ assert authors.first.respond_to?(:addr_id)
+ end
+
+ test "the default scope of the target is applied when joining associations" do
+ author = Author.create! name: "Jon"
+ author.categorizations.create!
+ author.categorizations.create! special: true
+
+ assert_equal [author], Author.where(id: author).left_outer_joins(:special_categorizations)
+ end
+end
diff --git a/activerecord/test/cases/associations/nested_through_associations_test.rb b/activerecord/test/cases/associations/nested_through_associations_test.rb
index 31b68c940e..b040485d99 100644
--- a/activerecord/test/cases/associations/nested_through_associations_test.rb
+++ b/activerecord/test/cases/associations/nested_through_associations_test.rb
@@ -495,7 +495,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase
groucho = members(:groucho)
founding = member_types(:founding)
- assert_raises(ActiveRecord::HasManyThroughNestedAssociationsAreReadonly) do
+ assert_raises(ActiveRecord::HasOneThroughNestedAssociationsAreReadonly) do
groucho.nested_member_type = founding
end
end
diff --git a/activerecord/test/cases/associations_test.rb b/activerecord/test/cases/associations_test.rb
index 08f790e21b..01a058918a 100644
--- a/activerecord/test/cases/associations_test.rb
+++ b/activerecord/test/cases/associations_test.rb
@@ -1,7 +1,6 @@
require "cases/helper"
require 'models/computer'
require 'models/developer'
-require 'models/computer'
require 'models/project'
require 'models/company'
require 'models/categorization'
@@ -13,7 +12,6 @@ require 'models/tag'
require 'models/tagging'
require 'models/person'
require 'models/reader'
-require 'models/parrot'
require 'models/ship_part'
require 'models/ship'
require 'models/liquid'
diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb
index ea2b94cbf4..52d197718e 100644
--- a/activerecord/test/cases/attribute_methods_test.rb
+++ b/activerecord/test/cases/attribute_methods_test.rb
@@ -1,7 +1,6 @@
require "cases/helper"
require 'models/minimalistic'
require 'models/developer'
-require 'models/computer'
require 'models/auto_id'
require 'models/boolean'
require 'models/computer'
@@ -67,8 +66,9 @@ class AttributeMethodsTest < ActiveRecord::TestCase
def test_caching_nil_primary_key
klass = Class.new(Minimalistic)
- klass.expects(:reset_primary_key).returns(nil).once
- 2.times { klass.primary_key }
+ assert_called(klass, :reset_primary_key, returns: nil) do
+ 2.times { klass.primary_key }
+ end
end
def test_attribute_keys_on_new_instance
@@ -175,9 +175,9 @@ class AttributeMethodsTest < ActiveRecord::TestCase
assert_equal category_attrs , category.attributes_before_type_cast
end
- if current_adapter?(:MysqlAdapter)
+ if current_adapter?(: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"]
@@ -542,9 +542,6 @@ class AttributeMethodsTest < ActiveRecord::TestCase
developer.save!
- assert_equal "50000", developer.salary_before_type_cast
- assert_equal 1337, developer.name_before_type_cast
-
assert_equal 50000, developer.salary
assert_equal "1337", developer.name
end
diff --git a/activerecord/test/cases/attribute_set_test.rb b/activerecord/test/cases/attribute_set_test.rb
index 9d927481ec..7a24b85a36 100644
--- a/activerecord/test/cases/attribute_set_test.rb
+++ b/activerecord/test/cases/attribute_set_test.rb
@@ -29,7 +29,7 @@ module ActiveRecord
assert_equal :bar, attributes[:bar].name
end
- test "duping creates a new hash and dups each attribute" do
+ test "duping creates a new hash, but does not dup the attributes" do
builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::String.new)
attributes = builder.build_from_database(foo: 1, bar: 'foo')
@@ -43,6 +43,24 @@ module ActiveRecord
assert_equal 1, attributes[:foo].value
assert_equal 2, duped[:foo].value
+ assert_equal 'foobar', attributes[:bar].value
+ assert_equal 'foobar', duped[:bar].value
+ end
+
+ test "deep_duping creates a new hash and dups each attribute" do
+ builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::String.new)
+ attributes = builder.build_from_database(foo: 1, bar: 'foo')
+
+ # Ensure the type cast value is cached
+ attributes[:foo].value
+ attributes[:bar].value
+
+ duped = attributes.deep_dup
+ duped.write_from_database(:foo, 2)
+ duped[:bar].value << 'bar'
+
+ assert_equal 1, attributes[:foo].value
+ assert_equal 2, duped[:foo].value
assert_equal 'foo', attributes[:bar].value
assert_equal 'foobar', duped[:bar].value
end
@@ -160,6 +178,9 @@ module ActiveRecord
return if value.nil?
value + " from database"
end
+
+ def assert_valid_value(*)
+ end
end
test "write_from_database sets the attribute with database typecasting" do
@@ -207,5 +228,26 @@ module ActiveRecord
assert_equal [:foo], attributes.accessed
end
+
+ test "#map returns a new attribute set with the changes applied" do
+ builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Integer.new)
+ attributes = builder.build_from_database(foo: "1", bar: "2")
+ new_attributes = attributes.map do |attr|
+ attr.with_cast_value(attr.value + 1)
+ end
+
+ assert_equal 2, new_attributes.fetch_value(:foo)
+ assert_equal 3, new_attributes.fetch_value(:bar)
+ end
+
+ test "comparison for equality is correctly implemented" do
+ builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Integer.new)
+ attributes = builder.build_from_database(foo: "1", bar: "2")
+ attributes2 = builder.build_from_database(foo: "1", bar: "2")
+ attributes3 = builder.build_from_database(foo: "2", bar: "2")
+
+ assert_equal attributes, attributes2
+ assert_not_equal attributes2, attributes3
+ end
end
end
diff --git a/activerecord/test/cases/attribute_test.rb b/activerecord/test/cases/attribute_test.rb
index aa419c7a67..a24a4fc6a4 100644
--- a/activerecord/test/cases/attribute_test.rb
+++ b/activerecord/test/cases/attribute_test.rb
@@ -1,11 +1,9 @@
require 'cases/helper'
-require 'minitest/mock'
module ActiveRecord
class AttributeTest < ActiveRecord::TestCase
setup do
@type = Minitest::Mock.new
- @type.expect(:==, false, [false])
end
teardown do
@@ -109,6 +107,9 @@ module ActiveRecord
def deserialize(value)
value + " from database"
end
+
+ def assert_valid_value(*)
+ end
end
test "with_value_from_user returns a new attribute with the value from the user" do
@@ -181,12 +182,65 @@ module ActiveRecord
assert attribute.has_been_read?
end
+ test "an attribute is not changed if it hasn't been assigned or mutated" do
+ attribute = Attribute.from_database(:foo, 1, Type::Value.new)
+
+ refute attribute.changed?
+ end
+
+ test "an attribute is changed if it's been assigned a new value" do
+ attribute = Attribute.from_database(:foo, 1, Type::Value.new)
+ changed = attribute.with_value_from_user(2)
+
+ assert changed.changed?
+ end
+
+ test "an attribute is not changed if it's assigned the same value" do
+ attribute = Attribute.from_database(:foo, 1, Type::Value.new)
+ unchanged = attribute.with_value_from_user(1)
+
+ refute unchanged.changed?
+ end
+
test "an attribute can not be mutated if it has not been read,
and skips expensive calculations" do
type_which_raises_from_all_methods = Object.new
attribute = Attribute.from_database(:foo, "bar", type_which_raises_from_all_methods)
- assert_not attribute.changed_in_place_from?("bar")
+ assert_not attribute.changed_in_place?
+ end
+
+ test "an attribute is changed if it has been mutated" do
+ attribute = Attribute.from_database(:foo, "bar", Type::String.new)
+ attribute.value << "!"
+
+ assert attribute.changed_in_place?
+ assert attribute.changed?
+ end
+
+ test "an attribute can forget its changes" do
+ attribute = Attribute.from_database(:foo, "bar", Type::String.new)
+ changed = attribute.with_value_from_user("foo")
+ forgotten = changed.forgetting_assignment
+
+ assert changed.changed? # sanity check
+ refute forgotten.changed?
+ end
+
+ test "with_value_from_user validates the value" do
+ type = Type::Value.new
+ type.define_singleton_method(:assert_valid_value) do |value|
+ if value == 1
+ raise ArgumentError
+ end
+ end
+
+ attribute = Attribute.from_database(:foo, 1, type)
+ assert_equal 1, attribute.value
+ assert_equal 2, attribute.with_value_from_user(2).value
+ assert_raises ArgumentError do
+ attribute.with_value_from_user(1)
+ end
end
end
end
diff --git a/activerecord/test/cases/attributes_test.rb b/activerecord/test/cases/attributes_test.rb
index bd74821d87..2991ca8b76 100644
--- a/activerecord/test/cases/attributes_test.rb
+++ b/activerecord/test/cases/attributes_test.rb
@@ -152,7 +152,7 @@ module ActiveRecord
Type::String.new(limit: 50))
int_array = ConnectionAdapters::PostgreSQL::OID::Array.new(
Type::Integer.new)
- refute_equal string_array, int_array
+ assert_not_equal string_array, int_array
assert_equal string_array, klass.type_for_attribute("my_array")
assert_equal int_array, klass.type_for_attribute("my_int_array")
end
@@ -167,10 +167,23 @@ module ActiveRecord
Type::String.new(limit: 50))
int_range = ConnectionAdapters::PostgreSQL::OID::Range.new(
Type::Integer.new)
- refute_equal string_range, int_range
+ assert_not_equal string_range, int_range
assert_equal string_range, klass.type_for_attribute("my_range")
assert_equal int_range, klass.type_for_attribute("my_int_range")
end
end
+
+ test "attributes added after subclasses load are inherited" do
+ parent = Class.new(ActiveRecord::Base) do
+ self.table_name = "topics"
+ end
+
+ child = Class.new(parent)
+ child.new # => force a schema load
+
+ parent.attribute(:foo, Type::Value.new)
+
+ assert_equal(:bar, child.new(foo: :bar).foo)
+ end
end
end
diff --git a/activerecord/test/cases/autosave_association_test.rb b/activerecord/test/cases/autosave_association_test.rb
index 37ec3f1106..0df8f1f798 100644
--- a/activerecord/test/cases/autosave_association_test.rb
+++ b/activerecord/test/cases/autosave_association_test.rb
@@ -24,6 +24,8 @@ require 'models/molecule'
require 'models/member'
require 'models/member_detail'
require 'models/organization'
+require 'models/guitar'
+require 'models/tuning_peg'
class TestAutosaveAssociationsInGeneral < ActiveRecord::TestCase
def test_autosave_validation
@@ -397,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')
diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb
index 382adbbdc7..d961f4710e 100644
--- a/activerecord/test/cases/base_test.rb
+++ b/activerecord/test/cases/base_test.rb
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-
require "cases/helper"
require 'models/post'
require 'models/author'
@@ -28,7 +26,7 @@ require 'models/bird'
require 'models/car'
require 'models/bulb'
require 'rexml/document'
-require 'concurrent/atomics'
+require 'concurrent/atomic/count_down_latch'
class FirstAbstractClass < ActiveRecord::Base
self.abstract_class = true
@@ -206,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
@@ -946,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
@@ -1178,42 +1204,10 @@ class BasicsTest < ActiveRecord::TestCase
assert_equal last, Developer.all.merge!(:order => :salary).to_a.last
end
- def test_abstract_class
- assert !ActiveRecord::Base.abstract_class?
- assert LoosePerson.abstract_class?
- assert !LooseDescendant.abstract_class?
- end
-
def test_abstract_class_table_name
assert_nil AbstractCompany.table_name
end
- def test_descends_from_active_record
- assert !ActiveRecord::Base.descends_from_active_record?
-
- # Abstract subclass of AR::Base.
- assert LoosePerson.descends_from_active_record?
-
- # Concrete subclass of an abstract class.
- assert LooseDescendant.descends_from_active_record?
-
- # Concrete subclass of AR::Base.
- assert TightPerson.descends_from_active_record?
-
- # Concrete subclass of a concrete class but has no type column.
- assert TightDescendant.descends_from_active_record?
-
- # Concrete subclass of AR::Base.
- assert Post.descends_from_active_record?
-
- # Abstract subclass of a concrete class which has a type column.
- # This is pathological, as you'll never have Sub < Abstract < Concrete.
- assert !StiPost.descends_from_active_record?
-
- # Concrete subclasses an abstract class which has a type column.
- assert !SubStiPost.descends_from_active_record?
- end
-
def test_find_on_abstract_base_class_doesnt_use_type_condition
old_class = LooseDescendant
Object.send :remove_const, :LooseDescendant
@@ -1258,50 +1252,6 @@ class BasicsTest < ActiveRecord::TestCase
ActiveRecord::Base.logger = original_logger
end
- def test_compute_type_success
- assert_equal Author, ActiveRecord::Base.send(:compute_type, 'Author')
- end
-
- def test_compute_type_nonexistent_constant
- e = assert_raises NameError do
- ActiveRecord::Base.send :compute_type, 'NonexistentModel'
- end
- assert_equal 'uninitialized constant ActiveRecord::Base::NonexistentModel', e.message
- assert_equal 'ActiveRecord::Base::NonexistentModel', e.name
- end
-
- def test_compute_type_no_method_error
- ActiveSupport::Dependencies.stubs(:safe_constantize).raises(NoMethodError)
- assert_raises NoMethodError do
- ActiveRecord::Base.send :compute_type, 'InvalidModel'
- end
- end
-
- def test_compute_type_on_undefined_method
- error = nil
- begin
- Class.new(Author) do
- alias_method :foo, :bar
- end
- rescue => e
- error = e
- end
-
- ActiveSupport::Dependencies.stubs(:safe_constantize).raises(e)
-
- exception = assert_raises NameError do
- ActiveRecord::Base.send :compute_type, 'InvalidModel'
- end
- assert_equal error.message, exception.message
- end
-
- def test_compute_type_argument_error
- ActiveSupport::Dependencies.stubs(:safe_constantize).raises(ArgumentError)
- assert_raises ArgumentError do
- ActiveRecord::Base.send :compute_type, 'InvalidModel'
- end
- end
-
def test_clear_cache!
# preheat cache
c1 = Post.connection.schema_cache.columns('posts')
@@ -1542,4 +1492,22 @@ class BasicsTest < ActiveRecord::TestCase
assert_not topic.id_changed?
end
+
+ test "ignored columns are not present in columns_hash" do
+ cache_columns = Developer.connection.schema_cache.columns_hash(Developer.table_name)
+ assert_includes cache_columns.keys, 'first_name'
+ refute_includes Developer.columns_hash.keys, 'first_name'
+ end
+
+ test "ignored columns have no attribute methods" do
+ refute Developer.new.respond_to?(:first_name)
+ refute Developer.new.respond_to?(:first_name=)
+ refute Developer.new.respond_to?(:first_name?)
+ end
+
+ test "ignored columns don't prevent explicit declaration of attribute methods" do
+ assert Developer.new.respond_to?(:last_name)
+ assert Developer.new.respond_to?(:last_name=)
+ assert Developer.new.respond_to?(:last_name?)
+ end
end
diff --git a/activerecord/test/cases/batches_test.rb b/activerecord/test/cases/batches_test.rb
index 0791dde1f2..da65336305 100644
--- a/activerecord/test/cases/batches_test.rb
+++ b/activerecord/test/cases/batches_test.rb
@@ -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
@@ -137,14 +139,15 @@ class EachTest < ActiveRecord::TestCase
def test_find_in_batches_should_not_use_records_after_yielding_them_in_case_original_array_is_modified
not_a_post = "not a post"
- not_a_post.stubs(:id).raises(StandardError, "not_a_post had #id called on it")
-
- assert_nothing_raised do
- Post.find_in_batches(:batch_size => 1) do |batch|
- assert_kind_of Array, batch
- assert_kind_of Post, batch.first
+ def not_a_post.id; end
+ not_a_post.stub(:id, ->{ raise StandardError.new("not_a_post had #id called on it") }) do
+ assert_nothing_raised do
+ Post.find_in_batches(:batch_size => 1) do |batch|
+ assert_kind_of Array, batch
+ assert_kind_of Post, batch.first
- batch.map! { not_a_post }
+ batch.map! { not_a_post }
+ end
end
end
end
@@ -158,7 +161,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
@@ -199,7 +202,7 @@ class EachTest < ActiveRecord::TestCase
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
@@ -210,6 +213,234 @@ 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
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 aa10817527..4a0e6f497f 100644
--- a/activerecord/test/cases/calculations_test.rb
+++ b/activerecord/test/cases/calculations_test.rb
@@ -31,11 +31,20 @@ class CalculationsTest < ActiveRecord::TestCase
assert_equal 318, Account.sum(:credit_limit)
end
+ def test_should_sum_arel_attribute
+ assert_equal 318, Account.sum(Account.arel_table[:credit_limit])
+ end
+
def test_should_average_field
value = Account.average(:credit_limit)
assert_equal 53.0, value
end
+ def test_should_average_arel_attribute
+ value = Account.average(Account.arel_table[:credit_limit])
+ assert_equal 53.0, value
+ end
+
def test_should_resolve_aliased_attributes
assert_equal 318, Account.sum(:available_credit)
end
@@ -60,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|
@@ -102,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
@@ -131,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)
@@ -353,10 +401,23 @@ 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
@@ -378,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
@@ -681,4 +757,36 @@ class CalculationsTest < ActiveRecord::TestCase
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/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/connection_adapters/mysql_type_lookup_test.rb b/activerecord/test/cases/connection_adapters/mysql_type_lookup_test.rb
index 80244d1439..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,10 @@ module ActiveRecord
assert_lookup_type :string, "SET('one', 'two', 'three')"
end
+ def test_set_type_with_value_matching_other_type
+ assert_lookup_type :string, "SET('unicode', '8bit', 'none', 'time')"
+ end
+
def test_enum_type_with_value_matching_other_type
assert_lookup_type :string, "ENUM('unicode', '8bit', 'none')"
end
diff --git a/activerecord/test/cases/connection_adapters/schema_cache_test.rb b/activerecord/test/cases/connection_adapters/schema_cache_test.rb
index c7531f5418..db832fe55d 100644
--- a/activerecord/test/cases/connection_adapters/schema_cache_test.rb
+++ b/activerecord/test/cases/connection_adapters/schema_cache_test.rb
@@ -29,7 +29,7 @@ module ActiveRecord
def test_clearing
@cache.columns('posts')
@cache.columns_hash('posts')
- @cache.tables('posts')
+ @cache.data_sources('posts')
@cache.primary_keys('posts')
@cache.clear!
@@ -40,17 +40,22 @@ module ActiveRecord
def test_dump_and_load
@cache.columns('posts')
@cache.columns_hash('posts')
- @cache.tables('posts')
+ @cache.data_sources('posts')
@cache.primary_keys('posts')
@cache = Marshal.load(Marshal.dump(@cache))
assert_equal 11, @cache.columns('posts').size
assert_equal 11, @cache.columns_hash('posts').size
- assert @cache.tables('posts')
+ assert @cache.data_sources('posts')
assert_equal 'id', @cache.primary_keys('posts')
end
+ def test_table_methods_deprecation
+ assert_deprecated { assert @cache.table_exists?('posts') }
+ assert_deprecated { assert @cache.tables('posts') }
+ assert_deprecated { @cache.clear_table_cache!('posts') }
+ end
end
end
end
diff --git a/activerecord/test/cases/connection_management_test.rb b/activerecord/test/cases/connection_management_test.rb
index dff6ea0fb0..d43668e57c 100644
--- a/activerecord/test/cases/connection_management_test.rb
+++ b/activerecord/test/cases/connection_management_test.rb
@@ -92,7 +92,14 @@ module ActiveRecord
app = lambda { |_| [200, {}, body] }
response_body = ConnectionManagement.new(app).call(@env)[2]
assert response_body.respond_to?(:to_path)
- assert_equal response_body.to_path, "/path"
+ assert_equal "/path", response_body.to_path
+ end
+
+ test "doesn't mutate the original response" do
+ original_response = [200, {}, 'hi']
+ app = lambda { |_| original_response }
+ ConnectionManagement.new(app).call(@env)[2]
+ assert_equal 'hi', original_response.last
end
end
end
diff --git a/activerecord/test/cases/connection_pool_test.rb b/activerecord/test/cases/connection_pool_test.rb
index 7ef5c93a48..efa3e0455e 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 'concurrent/atomics'
+require 'concurrent/atomic/count_down_latch'
module ActiveRecord
module ConnectionAdapters
diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb
index f5aaf22e13..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
@@ -467,8 +467,10 @@ class DirtyTest < ActiveRecord::TestCase
topic.save!
updated_at = topic.updated_at
- topic.content[:hello] = 'world'
- topic.save!
+ travel(1.second) do
+ topic.content[:hello] = 'world'
+ topic.save!
+ end
assert_not_equal updated_at, topic.updated_at
assert_equal 'world', topic.content[:hello]
@@ -521,6 +523,9 @@ class DirtyTest < ActiveRecord::TestCase
assert_equal Hash.new, pirate.previous_changes
pirate = Pirate.find_by_catchphrase("arrr")
+
+ travel(1.second)
+
pirate.catchphrase = "Me Maties!"
pirate.save!
@@ -532,6 +537,9 @@ class DirtyTest < ActiveRecord::TestCase
assert !pirate.previous_changes.key?('created_on')
pirate = Pirate.find_by_catchphrase("Me Maties!")
+
+ travel(1.second)
+
pirate.catchphrase = "Thar She Blows!"
pirate.save
@@ -542,6 +550,8 @@ class DirtyTest < ActiveRecord::TestCase
assert !pirate.previous_changes.key?('parrot_id')
assert !pirate.previous_changes.key?('created_on')
+ travel(1.second)
+
pirate = Pirate.find_by_catchphrase("Thar She Blows!")
pirate.update(catchphrase: "Ahoy!")
@@ -552,6 +562,8 @@ class DirtyTest < ActiveRecord::TestCase
assert !pirate.previous_changes.key?('parrot_id')
assert !pirate.previous_changes.key?('created_on')
+ travel(1.second)
+
pirate = Pirate.find_by_catchphrase("Ahoy!")
pirate.update_attribute(:catchphrase, "Ninjas suck!")
@@ -561,6 +573,8 @@ class DirtyTest < ActiveRecord::TestCase
assert_not_nil pirate.previous_changes['updated_on'][1]
assert !pirate.previous_changes.key?('parrot_id')
assert !pirate.previous_changes.key?('created_on')
+ ensure
+ travel_back
end
if ActiveRecord::Base.connection.supports_migrations?
@@ -578,6 +592,7 @@ class DirtyTest < ActiveRecord::TestCase
end
def test_datetime_attribute_can_be_updated_with_fractional_seconds
+ skip "Fractional seconds are not supported" unless subsecond_precision_supported?
in_time_zone 'Paris' do
target = Class.new(ActiveRecord::Base)
target.table_name = 'topics'
diff --git a/activerecord/test/cases/enum_test.rb b/activerecord/test/cases/enum_test.rb
index ba23049a92..7c930de97b 100644
--- a/activerecord/test/cases/enum_test.rb
+++ b/activerecord/test/cases/enum_test.rb
@@ -40,43 +40,43 @@ class EnumTest < ActiveRecord::TestCase
published, written = Book.statuses[:published], Book.statuses[:written]
assert_equal @book, Book.where(status: published).first
- refute_equal @book, Book.where(status: written).first
+ assert_not_equal @book, Book.where(status: written).first
assert_equal @book, Book.where(status: [published]).first
- refute_equal @book, Book.where(status: [written]).first
- refute_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
- refute_equal @book, Book.where(status: :written).first
+ assert_not_equal @book, Book.where(status: :written).first
assert_equal @book, Book.where(status: [:published]).first
- refute_equal @book, Book.where(status: [:written]).first
- refute_equal @book, Book.where.not(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
- refute_equal @book, Book.where(status: "written").first
+ assert_not_equal @book, Book.where(status: "written").first
assert_equal @book, Book.where(status: ["published"]).first
- refute_equal @book, Book.where(status: ["written"]).first
- refute_equal @book, Book.where.not(status: "published").first
+ assert_not_equal @book, Book.where(status: ["written"]).first
+ assert_not_equal @book, Book.where.not(status: "published").first
assert_equal @book, Book.where.not(status: "written").first
end
test "build from scope" do
assert Book.written.build.written?
- refute Book.written.build.proposed?
+ assert_not Book.written.build.proposed?
end
test "build from where" do
assert Book.where(status: Book.statuses[:written]).build.written?
- refute Book.where(status: Book.statuses[:written]).build.proposed?
+ assert_not Book.where(status: Book.statuses[:written]).build.proposed?
assert Book.where(status: :written).build.written?
- refute Book.where(status: :written).build.proposed?
+ assert_not Book.where(status: :written).build.proposed?
assert Book.where(status: "written").build.written?
- refute Book.where(status: "written").build.proposed?
+ assert_not Book.where(status: "written").build.proposed?
end
test "update by declaration" do
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_test.rb b/activerecord/test/cases/explain_test.rb
index f1d5511bb8..64dfd86ce2 100644
--- a/activerecord/test/cases/explain_test.rb
+++ b/activerecord/test/cases/explain_test.rb
@@ -39,38 +39,49 @@ if ActiveRecord::Base.connection.supports_explain?
binds = [[], []]
queries = sqls.zip(binds)
- connection.stubs(:explain).returns('query plan foo', 'query plan bar')
- expected = sqls.map {|sql| "EXPLAIN for: #{sql}\nquery plan #{sql}"}.join("\n")
- assert_equal expected, base.exec_explain(queries)
+ stub_explain_for_query_plans do
+ expected = sqls.map {|sql| "EXPLAIN for: #{sql}\nquery plan #{sql}"}.join("\n")
+ assert_equal expected, base.exec_explain(queries)
+ end
end
def test_exec_explain_with_binds
- cols = [Object.new, Object.new]
- cols[0].expects(:name).returns('wadus')
- cols[1].expects(:name).returns('chaflan')
+ object = Struct.new(:name)
+ cols = [object.new('wadus'), object.new('chaflan')]
sqls = %w(foo bar)
binds = [[[cols[0], 1]], [[cols[1], 2]]]
queries = sqls.zip(binds)
- connection.stubs(:explain).returns("query plan foo\n", "query plan bar\n")
- expected = <<-SQL.strip_heredoc
- EXPLAIN for: #{sqls[0]} [["wadus", 1]]
- query plan foo
+ stub_explain_for_query_plans(["query plan foo\n", "query plan bar\n"]) do
+ expected = <<-SQL.strip_heredoc
+ EXPLAIN for: #{sqls[0]} [["wadus", 1]]
+ query plan foo
- EXPLAIN for: #{sqls[1]} [["chaflan", 2]]
- query plan bar
- SQL
- assert_equal expected, base.exec_explain(queries)
+ EXPLAIN for: #{sqls[1]} [["chaflan", 2]]
+ query plan bar
+ SQL
+ assert_equal expected, base.exec_explain(queries)
+ end
end
def test_unsupported_connection_adapter
- connection.stubs(:supports_explain?).returns(false)
+ connection.stub(:supports_explain?, false) do
+ assert_not_called(base.logger, :warn) do
+ Car.where(:name => 'honda').to_a
+ end
+ end
+ end
- base.logger.expects(:warn).never
+ private
- Car.where(:name => 'honda').to_a
- end
+ def stub_explain_for_query_plans(query_plans = ['query plan foo', 'query plan bar'])
+ explain_called = 0
+
+ connection.stub(:explain, proc{ explain_called += 1; query_plans[explain_called - 1] }) do
+ yield
+ end
+ end
end
end
diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb
index 4b819a82e8..6686ce012d 100644
--- a/activerecord/test/cases/finder_test.rb
+++ b/activerecord/test/cases/finder_test.rb
@@ -178,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
@@ -264,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
@@ -700,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
@@ -720,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
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 03a187ae92..f30ed4fcc8 100644
--- a/activerecord/test/cases/fixtures_test.rb
+++ b/activerecord/test/cases/fixtures_test.rb
@@ -7,16 +7,16 @@ require 'models/binary'
require 'models/book'
require 'models/bulb'
require 'models/category'
+require 'models/comment'
require 'models/company'
require 'models/computer'
require 'models/course'
require 'models/developer'
-require 'models/computer'
+require 'models/doubloon'
require 'models/joke'
require 'models/matey'
require 'models/parrot'
require 'models/pirate'
-require 'models/doubloon'
require 'models/post'
require 'models/randomly_named_c1'
require 'models/reply'
@@ -217,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")
@@ -252,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
@@ -401,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
@@ -507,6 +517,38 @@ class OverRideFixtureMethodTest < ActiveRecord::TestCase
end
end
+class FixtureWithSetModelClassTest < ActiveRecord::TestCase
+ fixtures :other_posts, :other_comments
+
+ # Set to false to blow away fixtures cache and ensure our fixtures are loaded
+ # and thus takes into account the +set_model_class+.
+ self.use_transactional_tests = false
+
+ def test_uses_fixture_class_defined_in_yaml
+ assert_kind_of Post, other_posts(:second_welcome)
+ end
+
+ def test_loads_the_associations_to_fixtures_with_set_model_class
+ post = other_posts(:second_welcome)
+ comment = other_comments(:second_greetings)
+ assert_equal [comment], post.comments
+ assert_equal post, comment.post
+ end
+end
+
+class SetFixtureClassPrevailsTest < ActiveRecord::TestCase
+ set_fixture_class bad_posts: Post
+ fixtures :bad_posts
+
+ # Set to false to blow away fixtures cache and ensure our fixtures are loaded
+ # and thus takes into account the +set_model_class+.
+ self.use_transactional_tests = false
+
+ def test_uses_set_fixture_class
+ assert_kind_of Post, bad_posts(:bad_welcome)
+ end
+end
+
class CheckSetTableNameFixturesTest < ActiveRecord::TestCase
set_fixture_class :funny_jokes => Joke
fixtures :funny_jokes
@@ -913,3 +955,17 @@ class FixturesWithAbstractBelongsTo < ActiveRecord::TestCase
assert_equal pirates(:blackbeard), doubloons(:blackbeards_doubloon).pirate
end
end
+
+class FixtureClassNamesTest < ActiveRecord::TestCase
+ def setup
+ @saved_cache = self.fixture_class_names.dup
+ end
+
+ def teardown
+ self.fixture_class_names.replace(@saved_cache)
+ end
+
+ test "fixture_class_names returns nil for unregistered identifier" do
+ assert_nil self.fixture_class_names['unregistered_identifier']
+ end
+end
diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb
index d9d0f929db..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'
@@ -45,10 +46,8 @@ def in_memory_db?
ActiveRecord::Base.connection_pool.spec.config[:database] == ":memory:"
end
-def mysql_56?
- current_adapter?(:MysqlAdapter, :Mysql2Adapter) &&
- ActiveRecord::Base.connection.send(:version) >= '5.6.0' &&
- ActiveRecord::Base.connection.send(:version) < '5.7.0'
+def subsecond_precision_supported?
+ !current_adapter?(:MysqlAdapter, :Mysql2Adapter) || ActiveRecord::Base.connection.version >= '5.6.4'
end
def mysql_enforcing_gtid_consistency?
@@ -141,6 +140,7 @@ require "cases/validations_repair_helper"
class ActiveSupport::TestCase
include ActiveRecord::TestFixtures
include ActiveRecord::ValidationsRepairHelper
+ include ActiveSupport::Testing::MethodCallAssertions
self.fixture_path = FIXTURES_ROOT
self.use_instantiated_fixtures = false
diff --git a/activerecord/test/cases/inheritance_test.rb b/activerecord/test/cases/inheritance_test.rb
index f67d85603a..52e3734dd0 100644
--- a/activerecord/test/cases/inheritance_test.rb
+++ b/activerecord/test/cases/inheritance_test.rb
@@ -1,4 +1,5 @@
require 'cases/helper'
+require 'models/author'
require 'models/company'
require 'models/person'
require 'models/post'
@@ -55,6 +56,53 @@ class InheritanceTest < ActiveRecord::TestCase
end
end
+ def test_compute_type_success
+ assert_equal Author, ActiveRecord::Base.send(:compute_type, 'Author')
+ end
+
+ def test_compute_type_nonexistent_constant
+ e = assert_raises NameError do
+ ActiveRecord::Base.send :compute_type, 'NonexistentModel'
+ end
+ assert_equal 'uninitialized constant ActiveRecord::Base::NonexistentModel', e.message
+ assert_equal 'ActiveRecord::Base::NonexistentModel', e.name
+ end
+
+ def test_compute_type_no_method_error
+ ActiveSupport::Dependencies.stub(:safe_constantize, proc{ raise NoMethodError }) do
+ assert_raises NoMethodError do
+ ActiveRecord::Base.send :compute_type, 'InvalidModel'
+ end
+ end
+ end
+
+ def test_compute_type_on_undefined_method
+ error = nil
+ begin
+ Class.new(Author) do
+ alias_method :foo, :bar
+ end
+ rescue => e
+ error = e
+ end
+
+ ActiveSupport::Dependencies.stub(:safe_constantize, proc{ raise e }) do
+
+ exception = assert_raises NameError do
+ ActiveRecord::Base.send :compute_type, 'InvalidModel'
+ end
+ assert_equal error.message, exception.message
+ end
+ end
+
+ def test_compute_type_argument_error
+ ActiveSupport::Dependencies.stub(:safe_constantize, proc{ raise ArgumentError }) do
+ assert_raises ArgumentError do
+ ActiveRecord::Base.send :compute_type, 'InvalidModel'
+ end
+ end
+ end
+
def test_should_store_demodulized_class_name_with_store_full_sti_class_option_disabled
without_store_full_sti_class do
item = Namespaced::Company.new
@@ -77,6 +125,32 @@ class InheritanceTest < ActiveRecord::TestCase
end
end
+ def test_descends_from_active_record
+ assert !ActiveRecord::Base.descends_from_active_record?
+
+ # Abstract subclass of AR::Base.
+ assert LoosePerson.descends_from_active_record?
+
+ # Concrete subclass of an abstract class.
+ assert LooseDescendant.descends_from_active_record?
+
+ # Concrete subclass of AR::Base.
+ assert TightPerson.descends_from_active_record?
+
+ # Concrete subclass of a concrete class but has no type column.
+ assert TightDescendant.descends_from_active_record?
+
+ # Concrete subclass of AR::Base.
+ assert Post.descends_from_active_record?
+
+ # Abstract subclass of a concrete class which has a type column.
+ # This is pathological, as you'll never have Sub < Abstract < Concrete.
+ assert !StiPost.descends_from_active_record?
+
+ # Concrete subclasses an abstract class which has a type column.
+ assert !SubStiPost.descends_from_active_record?
+ end
+
def test_company_descends_from_active_record
assert !ActiveRecord::Base.descends_from_active_record?
assert AbstractCompany.descends_from_active_record?, 'AbstractCompany should descend from ActiveRecord::Base'
@@ -84,6 +158,12 @@ class InheritanceTest < ActiveRecord::TestCase
assert !Class.new(Company).descends_from_active_record?, 'Company subclass should not descend from ActiveRecord::Base'
end
+ def test_abstract_class
+ assert !ActiveRecord::Base.abstract_class?
+ assert LoosePerson.abstract_class?
+ assert !LooseDescendant.abstract_class?
+ end
+
def test_inheritance_base_class
assert_equal Post, Post.base_class
assert_equal Post, SpecialPost.base_class
@@ -223,7 +303,6 @@ class InheritanceTest < ActiveRecord::TestCase
end
end
-
def test_new_with_complex_inheritance
assert_nothing_raised { Client.new(type: 'VerySpecialClient') }
end
diff --git a/activerecord/test/cases/integration_test.rb b/activerecord/test/cases/integration_test.rb
index 018b7b0d8f..08a186ae07 100644
--- a/activerecord/test/cases/integration_test.rb
+++ b/activerecord/test/cases/integration_test.rb
@@ -81,7 +81,7 @@ class IntegrationTest < ActiveRecord::TestCase
def test_cache_key_format_for_existing_record_with_updated_at
dev = Developer.first
- assert_equal "developers/#{dev.id}-#{dev.updated_at.utc.to_s(:nsec)}", dev.cache_key
+ assert_equal "developers/#{dev.id}-#{dev.updated_at.utc.to_s(:usec)}", dev.cache_key
end
def test_cache_key_format_for_existing_record_with_updated_at_and_custom_cache_timestamp_format
@@ -96,7 +96,9 @@ class IntegrationTest < ActiveRecord::TestCase
owner.update_column :updated_at, Time.current
key = owner.cache_key
- assert pet.touch
+ travel(1.second) do
+ assert pet.touch
+ end
assert_not_equal key, owner.reload.cache_key
end
@@ -109,30 +111,39 @@ class IntegrationTest < ActiveRecord::TestCase
def test_cache_key_for_updated_on
dev = Developer.first
dev.updated_at = nil
- assert_equal "developers/#{dev.id}-#{dev.updated_on.utc.to_s(:nsec)}", dev.cache_key
+ assert_equal "developers/#{dev.id}-#{dev.updated_on.utc.to_s(:usec)}", dev.cache_key
end
def test_cache_key_for_newer_updated_at
dev = Developer.first
dev.updated_at += 3600
- assert_equal "developers/#{dev.id}-#{dev.updated_at.utc.to_s(:nsec)}", dev.cache_key
+ assert_equal "developers/#{dev.id}-#{dev.updated_at.utc.to_s(:usec)}", dev.cache_key
end
def test_cache_key_for_newer_updated_on
dev = Developer.first
dev.updated_on += 3600
- assert_equal "developers/#{dev.id}-#{dev.updated_on.utc.to_s(:nsec)}", dev.cache_key
+ assert_equal "developers/#{dev.id}-#{dev.updated_on.utc.to_s(:usec)}", dev.cache_key
end
def test_cache_key_format_is_precise_enough
+ skip("Subsecond precision is not supported") unless subsecond_precision_supported?
dev = Developer.first
key = dev.cache_key
dev.touch
assert_not_equal key, dev.cache_key
end
+ def test_cache_key_format_is_not_too_precise
+ skip("Subsecond precision is not supported") unless subsecond_precision_supported?
+ dev = Developer.first
+ dev.touch
+ key = dev.cache_key
+ assert_equal key, dev.reload.cache_key
+ end
+
def test_named_timestamps_for_cache_key
owner = owners(:blackbeard)
- assert_equal "owners/#{owner.id}-#{owner.happy_at.utc.to_s(:nsec)}", owner.cache_key(:updated_at, :happy_at)
+ assert_equal "owners/#{owner.id}-#{owner.happy_at.utc.to_s(:usec)}", owner.cache_key(:updated_at, :happy_at)
end
end
diff --git a/activerecord/test/cases/invertible_migration_test.rb b/activerecord/test/cases/invertible_migration_test.rb
index 99230aa3d5..0da58040c8 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|
@@ -128,8 +157,10 @@ module ActiveRecord
teardown do
%w[horses new_horses].each do |table|
- if ActiveRecord::Base.connection.table_exists?(table)
- ActiveRecord::Base.connection.drop_table(table)
+ ActiveSupport::Deprecation.silence do
+ if ActiveRecord::Base.connection.table_exists?(table)
+ ActiveRecord::Base.connection.drop_table(table)
+ end
end
end
ActiveRecord::Migration.verbose = @verbose_was
@@ -160,14 +191,14 @@ module ActiveRecord
def test_migrate_up
migration = InvertibleMigration.new
migration.migrate(:up)
- assert migration.connection.table_exists?("horses"), "horses should exist"
+ ActiveSupport::Deprecation.silence { assert migration.connection.table_exists?("horses"), "horses should exist" }
end
def test_migrate_down
migration = InvertibleMigration.new
migration.migrate :up
migration.migrate :down
- assert !migration.connection.table_exists?("horses")
+ ActiveSupport::Deprecation.silence { assert !migration.connection.table_exists?("horses") }
end
def test_migrate_revert
@@ -175,11 +206,11 @@ module ActiveRecord
revert = InvertibleRevertMigration.new
migration.migrate :up
revert.migrate :up
- assert !migration.connection.table_exists?("horses")
+ ActiveSupport::Deprecation.silence { assert !migration.connection.table_exists?("horses") }
revert.migrate :down
- assert migration.connection.table_exists?("horses")
+ ActiveSupport::Deprecation.silence { assert migration.connection.table_exists?("horses") }
migration.migrate :down
- assert !migration.connection.table_exists?("horses")
+ ActiveSupport::Deprecation.silence { assert !migration.connection.table_exists?("horses") }
end
def test_migrate_revert_by_part
@@ -187,18 +218,24 @@ module ActiveRecord
received = []
migration = InvertibleByPartsMigration.new
migration.test = ->(dir){
- assert migration.connection.table_exists?("horses")
- assert migration.connection.table_exists?("new_horses")
+ ActiveSupport::Deprecation.silence do
+ assert migration.connection.table_exists?("horses")
+ assert migration.connection.table_exists?("new_horses")
+ end
received << dir
}
migration.migrate :up
assert_equal [:both, :up], received
- assert !migration.connection.table_exists?("horses")
- assert migration.connection.table_exists?("new_horses")
+ ActiveSupport::Deprecation.silence do
+ assert !migration.connection.table_exists?("horses")
+ assert migration.connection.table_exists?("new_horses")
+ end
migration.migrate :down
assert_equal [:both, :up, :both, :down], received
- assert migration.connection.table_exists?("horses")
- assert !migration.connection.table_exists?("new_horses")
+ ActiveSupport::Deprecation.silence do
+ assert migration.connection.table_exists?("horses")
+ assert !migration.connection.table_exists?("new_horses")
+ end
end
def test_migrate_revert_whole_migration
@@ -207,20 +244,56 @@ module ActiveRecord
revert = RevertWholeMigration.new(klass)
migration.migrate :up
revert.migrate :up
- assert !migration.connection.table_exists?("horses")
+ ActiveSupport::Deprecation.silence { assert !migration.connection.table_exists?("horses") }
revert.migrate :down
- assert migration.connection.table_exists?("horses")
+ ActiveSupport::Deprecation.silence { assert migration.connection.table_exists?("horses") }
migration.migrate :down
- assert !migration.connection.table_exists?("horses")
+ ActiveSupport::Deprecation.silence { assert !migration.connection.table_exists?("horses") }
end
end
def test_migrate_nested_revert_whole_migration
revert = NestedRevertWholeMigration.new(InvertibleRevertMigration)
revert.migrate :down
- assert revert.connection.table_exists?("horses")
+ ActiveSupport::Deprecation.silence { assert revert.connection.table_exists?("horses") }
revert.migrate :up
- assert !revert.connection.table_exists?("horses")
+ ActiveSupport::Deprecation.silence { assert !revert.connection.table_exists?("horses") }
+ end
+
+ def test_migrate_revert_change_column_default
+ migration1 = ChangeColumnDefault1.new
+ migration1.migrate(:up)
+ assert_equal "Sekitoba", Horse.new.name
+
+ migration2 = ChangeColumnDefault2.new
+ migration2.migrate(:up)
+ Horse.reset_column_information
+ assert_equal "Diomed", Horse.new.name
+
+ migration2.migrate(:down)
+ Horse.reset_column_information
+ assert_equal "Sekitoba", Horse.new.name
+ end
+
+ if current_adapter?(:PostgreSQLAdapter)
+ def test_migrate_enable_and_disable_extension
+ migration1 = InvertibleMigration.new
+ migration2 = DisableExtension1.new
+ migration3 = DisableExtension2.new
+
+ migration1.migrate(:up)
+ migration2.migrate(:up)
+ assert_equal true, Horse.connection.extension_enabled?('hstore')
+
+ migration3.migrate(:up)
+ assert_equal false, Horse.connection.extension_enabled?('hstore')
+
+ migration3.migrate(:down)
+ assert_equal true, Horse.connection.extension_enabled?('hstore')
+
+ migration2.migrate(:down)
+ assert_equal false, Horse.connection.extension_enabled?('hstore')
+ end
end
def test_revert_order
@@ -249,24 +322,24 @@ module ActiveRecord
def test_legacy_up
LegacyMigration.migrate :up
- assert ActiveRecord::Base.connection.table_exists?("horses"), "horses should exist"
+ ActiveSupport::Deprecation.silence { assert ActiveRecord::Base.connection.table_exists?("horses"), "horses should exist" }
end
def test_legacy_down
LegacyMigration.migrate :up
LegacyMigration.migrate :down
- assert !ActiveRecord::Base.connection.table_exists?("horses"), "horses should not exist"
+ ActiveSupport::Deprecation.silence { assert !ActiveRecord::Base.connection.table_exists?("horses"), "horses should not exist" }
end
def test_up
LegacyMigration.up
- assert ActiveRecord::Base.connection.table_exists?("horses"), "horses should exist"
+ ActiveSupport::Deprecation.silence { assert ActiveRecord::Base.connection.table_exists?("horses"), "horses should exist" }
end
def test_down
LegacyMigration.up
LegacyMigration.down
- assert !ActiveRecord::Base.connection.table_exists?("horses"), "horses should not exist"
+ ActiveSupport::Deprecation.silence { assert !ActiveRecord::Base.connection.table_exists?("horses"), "horses should not exist" }
end
def test_migrate_down_with_table_name_prefix
@@ -275,7 +348,7 @@ module ActiveRecord
migration = InvertibleMigration.new
migration.migrate(:up)
assert_nothing_raised { migration.migrate(:down) }
- assert !ActiveRecord::Base.connection.table_exists?("p_horses_s"), "p_horses_s should not exist"
+ ActiveSupport::Deprecation.silence { assert !ActiveRecord::Base.connection.table_exists?("p_horses_s"), "p_horses_s should not exist" }
ensure
ActiveRecord::Base.table_name_prefix = ActiveRecord::Base.table_name_suffix = ''
end
diff --git a/activerecord/test/cases/log_subscriber_test.rb b/activerecord/test/cases/log_subscriber_test.rb
index 4192d12ff4..707a2d1da1 100644
--- a/activerecord/test/cases/log_subscriber_test.rb
+++ b/activerecord/test/cases/log_subscriber_test.rb
@@ -7,6 +7,20 @@ require "active_support/log_subscriber/test_helper"
class LogSubscriberTest < ActiveRecord::TestCase
include ActiveSupport::LogSubscriber::TestHelper
include ActiveSupport::Logger::Severity
+ REGEXP_CLEAR = Regexp.escape(ActiveRecord::LogSubscriber::CLEAR)
+ REGEXP_BOLD = Regexp.escape(ActiveRecord::LogSubscriber::BOLD)
+ REGEXP_MAGENTA = Regexp.escape(ActiveRecord::LogSubscriber::MAGENTA)
+ REGEXP_CYAN = Regexp.escape(ActiveRecord::LogSubscriber::CYAN)
+ SQL_COLORINGS = {
+ SELECT: Regexp.escape(ActiveRecord::LogSubscriber::BLUE),
+ INSERT: Regexp.escape(ActiveRecord::LogSubscriber::GREEN),
+ UPDATE: Regexp.escape(ActiveRecord::LogSubscriber::YELLOW),
+ DELETE: Regexp.escape(ActiveRecord::LogSubscriber::RED),
+ LOCK: Regexp.escape(ActiveRecord::LogSubscriber::WHITE),
+ ROLLBACK: Regexp.escape(ActiveRecord::LogSubscriber::RED),
+ TRANSACTION: REGEXP_CYAN,
+ OTHER: REGEXP_MAGENTA
+ }
class TestDebugLogSubscriber < ActiveRecord::LogSubscriber
attr_reader :debugs
@@ -71,6 +85,90 @@ class LogSubscriberTest < ActiveRecord::TestCase
assert_match(/SELECT .*?FROM .?developers.?/i, @logger.logged(:debug).last)
end
+ def test_basic_query_logging_coloration
+ event = Struct.new(:duration, :payload)
+ logger = TestDebugLogSubscriber.new
+ logger.colorize_logging = true
+ SQL_COLORINGS.each do |verb, color_regex|
+ logger.sql(event.new(0, sql: verb.to_s))
+ assert_match(/#{REGEXP_BOLD}#{color_regex}#{verb}#{REGEXP_CLEAR}/i, logger.debugs.last)
+ end
+ end
+
+ def test_basic_payload_name_logging_coloration_generic_sql
+ event = Struct.new(:duration, :payload)
+ logger = TestDebugLogSubscriber.new
+ logger.colorize_logging = true
+ SQL_COLORINGS.each do |verb, _|
+ logger.sql(event.new(0, sql: verb.to_s))
+ assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA} \(0.0ms\)#{REGEXP_CLEAR}/i, logger.debugs.last)
+
+ logger.sql(event.new(0, {sql: verb.to_s, name: "SQL"}))
+ assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA}SQL \(0.0ms\)#{REGEXP_CLEAR}/i, logger.debugs.last)
+ end
+ end
+
+ def test_basic_payload_name_logging_coloration_named_sql
+ event = Struct.new(:duration, :payload)
+ logger = TestDebugLogSubscriber.new
+ logger.colorize_logging = true
+ SQL_COLORINGS.each do |verb, _|
+ logger.sql(event.new(0, {sql: verb.to_s, name: "Model Load"}))
+ assert_match(/#{REGEXP_BOLD}#{REGEXP_CYAN}Model Load \(0.0ms\)#{REGEXP_CLEAR}/i, logger.debugs.last)
+
+ logger.sql(event.new(0, {sql: verb.to_s, name: "Model Exists"}))
+ assert_match(/#{REGEXP_BOLD}#{REGEXP_CYAN}Model Exists \(0.0ms\)#{REGEXP_CLEAR}/i, logger.debugs.last)
+
+ logger.sql(event.new(0, {sql: verb.to_s, name: "ANY SPECIFIC NAME"}))
+ assert_match(/#{REGEXP_BOLD}#{REGEXP_CYAN}ANY SPECIFIC NAME \(0.0ms\)#{REGEXP_CLEAR}/i, logger.debugs.last)
+ end
+ end
+
+ def test_query_logging_coloration_with_nested_select
+ event = Struct.new(:duration, :payload)
+ logger = TestDebugLogSubscriber.new
+ logger.colorize_logging = true
+ SQL_COLORINGS.slice(:SELECT, :INSERT, :UPDATE, :DELETE).each do |verb, color_regex|
+ logger.sql(event.new(0, sql: "#{verb} WHERE ID IN SELECT"))
+ assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA} \(0.0ms\)#{REGEXP_CLEAR} #{REGEXP_BOLD}#{color_regex}#{verb} WHERE ID IN SELECT#{REGEXP_CLEAR}/i, logger.debugs.last)
+ end
+ end
+
+ def test_query_logging_coloration_with_multi_line_nested_select
+ event = Struct.new(:duration, :payload)
+ logger = TestDebugLogSubscriber.new
+ logger.colorize_logging = true
+ SQL_COLORINGS.slice(:SELECT, :INSERT, :UPDATE, :DELETE).each do |verb, color_regex|
+ sql = <<-EOS
+ #{verb}
+ WHERE ID IN (
+ SELECT ID FROM THINGS
+ )
+ EOS
+ logger.sql(event.new(0, sql: sql))
+ assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA} \(0.0ms\)#{REGEXP_CLEAR} #{REGEXP_BOLD}#{color_regex}.*#{verb}.*#{REGEXP_CLEAR}/mi, logger.debugs.last)
+ end
+ end
+
+ def test_query_logging_coloration_with_lock
+ event = Struct.new(:duration, :payload)
+ logger = TestDebugLogSubscriber.new
+ logger.colorize_logging = true
+ sql = <<-EOS
+ SELECT * FROM
+ (SELECT * FROM mytable FOR UPDATE) ss
+ WHERE col1 = 5;
+ EOS
+ logger.sql(event.new(0, sql: sql))
+ assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA} \(0.0ms\)#{REGEXP_CLEAR} #{REGEXP_BOLD}#{SQL_COLORINGS[:LOCK]}.*FOR UPDATE.*#{REGEXP_CLEAR}/mi, logger.debugs.last)
+
+ sql = <<-EOS
+ LOCK TABLE films IN SHARE MODE;
+ EOS
+ logger.sql(event.new(0, sql: sql))
+ assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA} \(0.0ms\)#{REGEXP_CLEAR} #{REGEXP_BOLD}#{SQL_COLORINGS[:LOCK]}.*LOCK TABLE.*#{REGEXP_CLEAR}/mi, logger.debugs.last)
+ end
+
def test_exists_query_logging
Developer.exists? 1
wait
@@ -111,7 +209,7 @@ class LogSubscriberTest < ActiveRecord::TestCase
Thread.new { assert_equal 0, ActiveRecord::LogSubscriber.runtime }.join
end
- unless current_adapter?(:Mysql2Adapter)
+ if ActiveRecord::Base.connection.prepared_statements
def test_binary_data_is_not_logged
Binary.create(data: 'some binary data')
wait
diff --git a/activerecord/test/cases/migration/change_schema_test.rb b/activerecord/test/cases/migration/change_schema_test.rb
index 83e50048ec..2ff9cf8cf5 100644
--- a/activerecord/test/cases/migration/change_schema_test.rb
+++ b/activerecord/test/cases/migration/change_schema_test.rb
@@ -405,9 +405,9 @@ module ActiveRecord
def test_drop_table_if_exists
connection.create_table(:testings)
- assert connection.table_exists?(:testings)
+ ActiveSupport::Deprecation.silence { assert connection.table_exists?(:testings) }
connection.drop_table(:testings, if_exists: true)
- assert_not connection.table_exists?(:testings)
+ ActiveSupport::Deprecation.silence { assert_not connection.table_exists?(:testings) }
end
def test_drop_table_if_exists_nothing_raised
diff --git a/activerecord/test/cases/migration/change_table_test.rb b/activerecord/test/cases/migration/change_table_test.rb
index 2ffe7a1b0d..2f9c50141f 100644
--- a/activerecord/test/cases/migration/change_table_test.rb
+++ b/activerecord/test/cases/migration/change_table_test.rb
@@ -1,5 +1,4 @@
require "cases/migration/helper"
-require "minitest/mock"
module ActiveRecord
class Migration
diff --git a/activerecord/test/cases/migration/command_recorder_test.rb b/activerecord/test/cases/migration/command_recorder_test.rb
index 99f1dc65b0..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
diff --git a/activerecord/test/cases/migration/create_join_table_test.rb b/activerecord/test/cases/migration/create_join_table_test.rb
index 8fd08fe4ce..0a7b57455c 100644
--- a/activerecord/test/cases/migration/create_join_table_test.rb
+++ b/activerecord/test/cases/migration/create_join_table_test.rb
@@ -12,7 +12,9 @@ module ActiveRecord
teardown do
%w(artists_musics musics_videos catalog).each do |table_name|
- connection.drop_table table_name if connection.tables.include?(table_name)
+ ActiveSupport::Deprecation.silence do
+ connection.drop_table table_name if connection.table_exists?(table_name)
+ end
end
end
@@ -82,62 +84,62 @@ module ActiveRecord
connection.create_join_table :artists, :musics
connection.drop_join_table :artists, :musics
- assert !connection.tables.include?('artists_musics')
+ ActiveSupport::Deprecation.silence { assert !connection.table_exists?('artists_musics') }
end
def test_drop_join_table_with_strings
connection.create_join_table :artists, :musics
connection.drop_join_table 'artists', 'musics'
- assert !connection.tables.include?('artists_musics')
+ ActiveSupport::Deprecation.silence { assert !connection.table_exists?('artists_musics') }
end
def test_drop_join_table_with_the_proper_order
connection.create_join_table :videos, :musics
connection.drop_join_table :videos, :musics
- assert !connection.tables.include?('musics_videos')
+ ActiveSupport::Deprecation.silence { assert !connection.table_exists?('musics_videos') }
end
def test_drop_join_table_with_the_table_name
connection.create_join_table :artists, :musics, table_name: :catalog
connection.drop_join_table :artists, :musics, table_name: :catalog
- assert !connection.tables.include?('catalog')
+ ActiveSupport::Deprecation.silence { assert !connection.table_exists?('catalog') }
end
def test_drop_join_table_with_the_table_name_as_string
connection.create_join_table :artists, :musics, table_name: 'catalog'
connection.drop_join_table :artists, :musics, table_name: 'catalog'
- assert !connection.tables.include?('catalog')
+ ActiveSupport::Deprecation.silence { assert !connection.table_exists?('catalog') }
end
def test_drop_join_table_with_column_options
connection.create_join_table :artists, :musics, column_options: {null: true}
connection.drop_join_table :artists, :musics, column_options: {null: true}
- assert !connection.tables.include?('artists_musics')
+ ActiveSupport::Deprecation.silence { assert !connection.table_exists?('artists_musics') }
end
def test_create_and_drop_join_table_with_common_prefix
with_table_cleanup do
connection.create_join_table 'audio_artists', 'audio_musics'
- assert_includes connection.tables, 'audio_artists_musics'
+ ActiveSupport::Deprecation.silence { assert connection.table_exists?('audio_artists_musics') }
connection.drop_join_table 'audio_artists', 'audio_musics'
- assert !connection.tables.include?('audio_artists_musics'), "Should have dropped join table, but didn't"
+ ActiveSupport::Deprecation.silence { assert !connection.table_exists?('audio_artists_musics'), "Should have dropped join table, but didn't" }
end
end
private
def with_table_cleanup
- tables_before = connection.tables
+ tables_before = connection.data_sources
yield
ensure
- tables_after = connection.tables - tables_before
+ tables_after = connection.data_sources - tables_before
tables_after.each do |table|
connection.drop_table table
diff --git a/activerecord/test/cases/migration/helper.rb b/activerecord/test/cases/migration/helper.rb
index 5bc0898f33..ad85684c0b 100644
--- a/activerecord/test/cases/migration/helper.rb
+++ b/activerecord/test/cases/migration/helper.rb
@@ -28,7 +28,7 @@ module ActiveRecord
super
TestModel.reset_table_name
TestModel.reset_sequence_name
- connection.drop_table :test_models rescue nil
+ connection.drop_table :test_models, if_exists: true
end
private
diff --git a/activerecord/test/cases/migration/pending_migrations_test.rb b/activerecord/test/cases/migration/pending_migrations_test.rb
index 7afac83bd2..4f5589f32a 100644
--- a/activerecord/test/cases/migration/pending_migrations_test.rb
+++ b/activerecord/test/cases/migration/pending_migrations_test.rb
@@ -1,5 +1,4 @@
require 'cases/helper'
-require "minitest/mock"
module ActiveRecord
class Migration
diff --git a/activerecord/test/cases/migration/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
index 1594f99852..edbc8abe4d 100644
--- a/activerecord/test/cases/migration/references_foreign_key_test.rb
+++ b/activerecord/test/cases/migration/references_foreign_key_test.rb
@@ -32,6 +32,14 @@ module ActiveRecord
assert_equal [], @connection.foreign_keys("testings")
end
+ test "foreign keys can be created in one query" 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
@@ -45,6 +53,15 @@ module ActiveRecord
assert_equal "other_id", fk.primary_key
end
+ test "to_table option can be passed" do
+ @connection.create_table :testings do |t|
+ t.references :parent, foreign_key: { to_table: :testing_parents }
+ end
+ fks = @connection.foreign_keys("testings")
+ assert_equal([["testings", "testing_parents", "parent_id"]],
+ fks.map {|fk| [fk.from_table, fk.to_table, fk.column] })
+ end
+
test "foreign keys cannot be added to polymorphic relations when creating the table" do
@connection.create_table :testings do |t|
assert_raises(ArgumentError) do
@@ -147,7 +164,7 @@ class ReferencesWithoutForeignKeySupportTest < ActiveRecord::TestCase
t.references :testing_parent, foreign_key: true
end
- assert_includes @connection.tables, "testings"
+ assert_includes @connection.data_sources, "testings"
end
end
end
diff --git a/activerecord/test/cases/migration/rename_table_test.rb b/activerecord/test/cases/migration/rename_table_test.rb
index 6d742d3f2f..8eb027d53f 100644
--- a/activerecord/test/cases/migration/rename_table_test.rb
+++ b/activerecord/test/cases/migration/rename_table_test.rb
@@ -15,7 +15,7 @@ module ActiveRecord
end
def teardown
- rename_table :octopi, :test_models if connection.table_exists? :octopi
+ ActiveSupport::Deprecation.silence { rename_table :octopi, :test_models if connection.table_exists? :octopi }
super
end
@@ -83,7 +83,7 @@ module ActiveRecord
enable_extension!('uuid-ossp', connection)
connection.create_table :cats, id: :uuid
assert_nothing_raised { rename_table :cats, :felines }
- assert connection.table_exists? :felines
+ ActiveSupport::Deprecation.silence { assert connection.table_exists? :felines }
ensure
disable_extension!('uuid-ossp', connection)
connection.drop_table :cats, if_exists: true
diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb
index b2f209fe97..c3c204cf9f 100644
--- a/activerecord/test/cases/migration_test.rb
+++ b/activerecord/test/cases/migration_test.rb
@@ -1,6 +1,7 @@
-require "cases/helper"
-require "cases/migration/helper"
+require 'cases/helper'
+require 'cases/migration/helper'
require 'bigdecimal/util'
+require 'concurrent/atomic/count_down_latch'
require 'models/person'
require 'models/topic'
@@ -75,15 +76,13 @@ class MigrationTest < ActiveRecord::TestCase
ActiveRecord::Migrator.up(migrations_path)
assert_equal 3, ActiveRecord::Migrator.current_version
- assert_equal 3, ActiveRecord::Migrator.last_version
assert_equal false, ActiveRecord::Migrator.needs_migration?
ActiveRecord::Migrator.down(MIGRATIONS_ROOT + "/valid")
assert_equal 0, ActiveRecord::Migrator.current_version
- assert_equal 3, ActiveRecord::Migrator.last_version
assert_equal true, ActiveRecord::Migrator.needs_migration?
- ActiveRecord::SchemaMigration.create!(:version => ActiveRecord::Migrator.last_version)
+ ActiveRecord::SchemaMigration.create!(version: 3)
assert_equal true, ActiveRecord::Migrator.needs_migration?
ensure
ActiveRecord::Migrator.migrations_paths = old_path
@@ -115,7 +114,7 @@ class MigrationTest < ActiveRecord::TestCase
end
def test_migration_version
- ActiveRecord::Migrator.run(:up, MIGRATIONS_ROOT + "/version_check", 20131219224947)
+ assert_nothing_raised { ActiveRecord::Migrator.run(:up, MIGRATIONS_ROOT + "/version_check", 20131219224947) }
end
def test_create_table_with_force_true_does_not_drop_nonexisting_table
@@ -132,13 +131,9 @@ class MigrationTest < ActiveRecord::TestCase
Person.connection.drop_table :testings2, if_exists: true
end
- def connection
- ActiveRecord::Base.connection
- end
-
def test_migration_instance_has_connection
migration = Class.new(ActiveRecord::Migration).new
- assert_equal connection, migration.connection
+ assert_equal ActiveRecord::Base.connection, migration.connection
end
def test_method_missing_delegates_to_connection
@@ -528,6 +523,79 @@ class MigrationTest < ActiveRecord::TestCase
end
end
+ if ActiveRecord::Base.connection.supports_advisory_locks?
+ def test_migrator_generates_valid_lock_id
+ migration = Class.new(ActiveRecord::Migration).new
+ migrator = ActiveRecord::Migrator.new(:up, [migration], 100)
+
+ lock_id = migrator.send(:generate_migrator_advisory_lock_id)
+
+ assert ActiveRecord::Base.connection.get_advisory_lock(lock_id),
+ "the Migrator should have generated a valid lock id, but it didn't"
+ assert ActiveRecord::Base.connection.release_advisory_lock(lock_id),
+ "the Migrator should have generated a valid lock id, but it didn't"
+ end
+
+ def test_generate_migrator_advisory_lock_id
+ # It is important we are consistent with how we generate this so that
+ # exclusive locking works across migrator versions
+ migration = Class.new(ActiveRecord::Migration).new
+ migrator = ActiveRecord::Migrator.new(:up, [migration], 100)
+
+ lock_id = migrator.send(:generate_migrator_advisory_lock_id)
+
+ current_database = ActiveRecord::Base.connection.current_database
+ salt = ActiveRecord::Migrator::MIGRATOR_SALT
+ expected_id = Zlib.crc32(current_database) * salt
+
+ assert lock_id == expected_id, "expected lock id generated by the migrator to be #{expected_id}, but it was #{lock_id} instead"
+ assert lock_id.is_a?(Fixnum), "expected lock id to be a Fixnum, but it wasn't"
+ assert lock_id.bit_length <= 63, "lock id must be a signed integer of max 63 bits magnitude"
+ end
+
+ def test_migrator_one_up_with_unavailable_lock
+ assert_no_column Person, :last_name
+
+ migration = Class.new(ActiveRecord::Migration) {
+ def version; 100 end
+ def migrate(x)
+ add_column "people", "last_name", :string
+ end
+ }.new
+
+ migrator = ActiveRecord::Migrator.new(:up, [migration], 100)
+ lock_id = migrator.send(:generate_migrator_advisory_lock_id)
+
+ with_another_process_holding_lock(lock_id) do
+ assert_raise(ActiveRecord::ConcurrentMigrationError) { migrator.migrate }
+ end
+
+ assert_no_column Person, :last_name,
+ "without an advisory lock, the Migrator should not make any changes, but it did."
+ end
+
+ def test_migrator_one_up_with_unavailable_lock_using_run
+ assert_no_column Person, :last_name
+
+ migration = Class.new(ActiveRecord::Migration) {
+ def version; 100 end
+ def migrate(x)
+ add_column "people", "last_name", :string
+ end
+ }.new
+
+ migrator = ActiveRecord::Migrator.new(:up, [migration], 100)
+ lock_id = migrator.send(:generate_migrator_advisory_lock_id)
+
+ with_another_process_holding_lock(lock_id) do
+ assert_raise(ActiveRecord::ConcurrentMigrationError) { migrator.run }
+ end
+
+ assert_no_column Person, :last_name,
+ "without an advisory lock, the Migrator should not make any changes, but it did."
+ end
+ end
+
protected
# This is needed to isolate class_attribute assignments like `table_name_prefix`
# for each test case.
@@ -537,6 +605,30 @@ class MigrationTest < ActiveRecord::TestCase
def self.base_class; self; end
}
end
+
+ def with_another_process_holding_lock(lock_id)
+ thread_lock = Concurrent::CountDownLatch.new
+ test_terminated = Concurrent::CountDownLatch.new
+
+ other_process = Thread.new do
+ begin
+ conn = ActiveRecord::Base.connection_pool.checkout
+ conn.get_advisory_lock(lock_id)
+ thread_lock.count_down
+ test_terminated.wait # hold the lock open until we tested everything
+ ensure
+ conn.release_advisory_lock(lock_id)
+ ActiveRecord::Base.connection_pool.checkin(conn)
+ end
+ end
+
+ thread_lock.wait # wait until the 'other process' has the lock
+
+ yield
+
+ test_terminated.count_down
+ other_process.join
+ end
end
class ReservedWordsMigrationTest < ActiveRecord::TestCase
diff --git a/activerecord/test/cases/migrator_test.rb b/activerecord/test/cases/migrator_test.rb
index 2ff6938e7b..dbf088f455 100644
--- a/activerecord/test/cases/migrator_test.rb
+++ b/activerecord/test/cases/migrator_test.rb
@@ -313,9 +313,9 @@ class MigratorTest < ActiveRecord::TestCase
_, migrator = migrator_class(3)
ActiveRecord::Base.connection.drop_table "schema_migrations", if_exists: true
- assert_not ActiveRecord::Base.connection.table_exists?('schema_migrations')
+ ActiveSupport::Deprecation.silence { assert_not ActiveRecord::Base.connection.table_exists?('schema_migrations') }
migrator.migrate("valid", 1)
- assert ActiveRecord::Base.connection.table_exists?('schema_migrations')
+ ActiveSupport::Deprecation.silence { assert ActiveRecord::Base.connection.table_exists?('schema_migrations') }
end
def test_migrator_forward
diff --git a/activerecord/test/cases/nested_attributes_test.rb b/activerecord/test/cases/nested_attributes_test.rb
index b8a0401fe3..93cb631a04 100644
--- a/activerecord/test/cases/nested_attributes_test.rb
+++ b/activerecord/test/cases/nested_attributes_test.rb
@@ -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
@@ -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
@@ -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
+
+ @pirate.attributes = {
+ association_getter => [
+ { :id => @child_1.id, :name => 'Grace OMalley' },
+ { :id => @child_2.id, :name => 'Privateers Greed' }
+ ]
+ }
- assert_equal ['Grace OMalley', 'Privateers Greed'], [@child_1.name, @child_2.name]
+ assert_equal ['Grace OMalley', 'Privateers Greed'], [@child_1.name, @child_2.name]
+ end
+ end
end
def test_should_raise_RecordNotFound_if_an_id_is_given_but_doesnt_return_a_record
diff --git a/activerecord/test/cases/persistence_test.rb b/activerecord/test/cases/persistence_test.rb
index 42e7507631..b2e59e3970 100644
--- a/activerecord/test/cases/persistence_test.rb
+++ b/activerecord/test/cases/persistence_test.rb
@@ -17,6 +17,7 @@ require 'models/minivan'
require 'models/owner'
require 'models/person'
require 'models/pet'
+require 'models/ship'
require 'models/toy'
require 'rexml/document'
@@ -119,13 +120,22 @@ class PersistenceTest < ActiveRecord::TestCase
assert_equal 59, accounts(:signals37, :reload).credit_limit
end
+ def test_increment_updates_counter_in_db_using_offset
+ a1 = accounts(:signals37)
+ initial_credit = a1.credit_limit
+ a2 = Account.find(accounts(:signals37).id)
+ a1.increment!(:credit_limit)
+ a2.increment!(:credit_limit)
+ assert_equal initial_credit + 2, a1.reload.credit_limit
+ end
+
def test_destroy_all
conditions = "author_name = 'Mary'"
topics_by_mary = Topic.all.merge!(:where => conditions, :order => 'id').to_a
assert ! topics_by_mary.empty?
assert_difference('Topic.count', -topics_by_mary.size) do
- destroyed = Topic.destroy_all(conditions).sort_by(&:id)
+ destroyed = Topic.where(conditions).destroy_all.sort_by(&:id)
assert_equal topics_by_mary, destroyed
assert destroyed.all?(&:frozen?), "destroyed topics should be frozen"
end
@@ -734,9 +744,10 @@ class PersistenceTest < ActiveRecord::TestCase
assert !topic.approved?
assert_equal "The First Topic", topic.title
- assert_raise(ActiveRecord::RecordNotUnique, ActiveRecord::StatementInvalid) do
+ error = assert_raise(ActiveRecord::RecordNotUnique, ActiveRecord::StatementInvalid) do
topic.update_attributes(id: 3, title: "Hm is it possible?")
end
+ assert_not_nil error.cause
assert_not_equal "Hm is it possible?", Topic.find(3).title
topic.update_attributes(id: 1234)
@@ -955,4 +966,17 @@ class PersistenceTest < ActiveRecord::TestCase
widget.reset_column_information
end
end
+
+ def test_reset_column_information_resets_children
+ child = Class.new(Topic)
+ child.new # force schema to load
+
+ ActiveRecord::Base.connection.add_column(:topics, :foo, :string)
+ Topic.reset_column_information
+
+ assert_equal "bar", child.new(foo: :bar).foo
+ ensure
+ ActiveRecord::Base.connection.remove_column(:topics, :foo)
+ Topic.reset_column_information
+ end
end
diff --git a/activerecord/test/cases/pooled_connections_test.rb b/activerecord/test/cases/pooled_connections_test.rb
index daa3271777..bca50dd008 100644
--- a/activerecord/test/cases/pooled_connections_test.rb
+++ b/activerecord/test/cases/pooled_connections_test.rb
@@ -44,7 +44,7 @@ class PooledConnectionsTest < ActiveRecord::TestCase
conn = ActiveRecord::Base.connection_pool.checkout
ActiveRecord::Base.connection_pool.checkin conn
@connection_count += 1
- ActiveRecord::Base.connection.tables
+ ActiveRecord::Base.connection.data_sources
rescue ActiveRecord::ConnectionTimeoutError
@timed_out += 1
end
diff --git a/activerecord/test/cases/primary_keys_test.rb b/activerecord/test/cases/primary_keys_test.rb
index 0745a37ee9..d883784553 100644
--- a/activerecord/test/cases/primary_keys_test.rb
+++ b/activerecord/test/cases/primary_keys_test.rb
@@ -224,7 +224,7 @@ class PrimaryKeyAnyTypeTest < ActiveRecord::TestCase
end
teardown do
- @connection.drop_table(:barcodes) if @connection.table_exists? :barcodes
+ @connection.drop_table(:barcodes, if_exists: true)
end
def test_any_type_primary_key
@@ -241,6 +241,33 @@ class PrimaryKeyAnyTypeTest < ActiveRecord::TestCase
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_tests = false
@@ -253,6 +280,32 @@ if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
con.reconnect!
end
end
+
+ class PrimaryKeyBigintNilDefaultTest < ActiveRecord::TestCase
+ include SchemaDumpingHelper
+
+ self.use_transactional_tests = false
+
+ def setup
+ @connection = ActiveRecord::Base.connection
+ @connection.create_table(:bigint_defaults, id: :bigint, default: nil, force: true)
+ end
+
+ def teardown
+ @connection.drop_table :bigint_defaults, if_exists: true
+ end
+
+ test "primary key with bigint allows default override via nil" do
+ column = @connection.columns(:bigint_defaults).find { |c| c.name == 'id' }
+ assert column.bigint?
+ assert_not column.auto_increment?
+ end
+
+ test "schema dump primary key with bigint default nil" do
+ schema = dump_table_schema "bigint_defaults"
+ assert_match %r{create_table "bigint_defaults", id: :bigint, default: nil}, schema
+ end
+ end
end
if current_adapter?(:PostgreSQLAdapter, :MysqlAdapter, :Mysql2Adapter)
@@ -300,11 +353,12 @@ if current_adapter?(:PostgreSQLAdapter, :MysqlAdapter, :Mysql2Adapter)
if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
test "primary key column type with options" do
- @connection.create_table(:widgets, id: :primary_key, limit: 8, force: true)
+ @connection.create_table(:widgets, id: :primary_key, limit: 8, unsigned: true, force: true)
column = @connection.columns(:widgets).find { |c| c.name == 'id' }
assert column.auto_increment?
assert_equal :integer, column.type
assert_equal 8, column.limit
+ assert column.unsigned?
end
end
end
diff --git a/activerecord/test/cases/query_cache_test.rb b/activerecord/test/cases/query_cache_test.rb
index 2f0b5df286..d84653e4c9 100644
--- a/activerecord/test/cases/query_cache_test.rb
+++ b/activerecord/test/cases/query_cache_test.rb
@@ -262,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/readonly_test.rb b/activerecord/test/cases/readonly_test.rb
index 1c919f0b57..5f6eb41240 100644
--- a/activerecord/test/cases/readonly_test.rb
+++ b/activerecord/test/cases/readonly_test.rb
@@ -7,6 +7,7 @@ require 'models/computer'
require 'models/project'
require 'models/reader'
require 'models/person'
+require 'models/ship'
class ReadOnlyTest < ActiveRecord::TestCase
fixtures :authors, :posts, :comments, :developers, :projects, :developers_projects, :people, :readers
diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb
index 7b47c80331..9c04a41e69 100644
--- a/activerecord/test/cases/reflection_test.rb
+++ b/activerecord/test/cases/reflection_test.rb
@@ -393,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
@@ -406,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
@@ -419,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
@@ -432,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/mutation_test.rb b/activerecord/test/cases/relation/mutation_test.rb
index ba4d9d2503..d0f60a84b5 100644
--- a/activerecord/test/cases/relation/mutation_test.rb
+++ b/activerecord/test/cases/relation/mutation_test.rb
@@ -22,13 +22,17 @@ module ActiveRecord
def sanitize_sql(sql)
sql
end
+
+ def sanitize_sql_for_order(sql)
+ sql
+ end
end
def relation
@relation ||= Relation.new FakeKlass.new('posts'), Post.arel_table, Post.predicate_builder
end
- (Relation::MULTI_VALUE_METHODS - [:references, :extending, :order, :unscope, :select]).each do |method|
+ (Relation::MULTI_VALUE_METHODS - [:references, :extending, :order, :unscope, :select, :left_joins]).each do |method|
test "##{method}!" do
assert relation.public_send("#{method}!", :foo).equal?(relation)
assert_equal [:foo], relation.public_send("#{method}_values")
@@ -55,9 +59,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
diff --git a/activerecord/test/cases/relation/where_test.rb b/activerecord/test/cases/relation/where_test.rb
index 6af31017d6..bc6378b90e 100644
--- a/activerecord/test/cases/relation/where_test.rb
+++ b/activerecord/test/cases/relation/where_test.rb
@@ -276,5 +276,35 @@ module ActiveRecord
assert_equal essays(:david_modest_proposal), essay
end
+
+ def test_where_with_strong_parameters
+ protected_params = Class.new do
+ attr_reader :permitted
+ alias :permitted? :permitted
+
+ def initialize(parameters)
+ @parameters = parameters
+ @permitted = false
+ end
+
+ def to_h
+ @parameters
+ end
+
+ def permit!
+ @permitted = true
+ self
+ end
+ end
+
+ author = authors(:david)
+ params = protected_params.new(name: author.name)
+ assert_raises(ActiveModel::ForbiddenAttributesError) { Author.where(params) }
+ assert_equal author, Author.where(params.permit!).first
+ end
+
+ def test_where_with_unsupported_arguments
+ assert_raises(ArgumentError) { Author.where(42) }
+ end
end
end
diff --git a/activerecord/test/cases/relation_test.rb b/activerecord/test/cases/relation_test.rb
index 37d3965022..f46d414b95 100644
--- a/activerecord/test/cases/relation_test.rb
+++ b/activerecord/test/cases/relation_test.rb
@@ -20,6 +20,10 @@ module ActiveRecord
def self.table_name
'fake_table'
end
+
+ def self.sanitize_sql_for_order(sql)
+ sql
+ end
end
def test_construction
@@ -57,9 +61,6 @@ module ActiveRecord
def test_empty_where_values_hash
relation = Relation.new(FakeKlass, :b, nil)
assert_equal({}, relation.where_values_hash)
-
- relation.where! :hello
- assert_equal({}, relation.where_values_hash)
end
def test_has_values
@@ -153,10 +154,10 @@ module ActiveRecord
end
test 'merging a hash into a relation' do
- relation = Relation.new(FakeKlass, :b, nil)
- relation = relation.merge where: :lol, readonly: true
+ relation = Relation.new(Post, Post.arel_table, Post.predicate_builder)
+ relation = relation.merge where: {name: :lol}, readonly: true
- assert_equal Relation::WhereClause.new([:lol], []), relation.where_clause
+ assert_equal({"name"=>:lol}, relation.where_clause.to_h)
assert_equal true, relation.readonly_value
end
@@ -185,7 +186,7 @@ module ActiveRecord
end
test '#values returns a dup of the values' do
- relation = Relation.new(FakeKlass, :b, nil).where! :foo
+ relation = Relation.new(Post, Post.arel_table, Post.predicate_builder).where!(name: :foo)
values = relation.values
values[:where] = nil
@@ -234,6 +235,13 @@ module ActiveRecord
assert_equal 3, relation.where(id: post.id).pluck(:id).size
end
+ def test_merge_raises_with_invalid_argument
+ assert_raises ArgumentError do
+ relation = Relation.new(FakeKlass, :b, nil)
+ relation.merge(true)
+ end
+ end
+
def test_respond_to_for_non_selected_element
post = Post.select(:title).first
assert_equal false, post.respond_to?(:body), "post should not respond_to?(:body) since invoking it raises exception"
@@ -243,18 +251,23 @@ module ActiveRecord
end
def test_select_quotes_when_using_from_clause
- 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
+ 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
@@ -292,6 +305,16 @@ module ActiveRecord
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(
diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb
index 5f48c2b40f..cd23c1b3e1 100644
--- a/activerecord/test/cases/relations_test.rb
+++ b/activerecord/test/cases/relations_test.rb
@@ -297,6 +297,11 @@ class RelationTest < ActiveRecord::TestCase
assert_equal 3, tags.length
end
+ def test_finding_with_sanitized_order
+ query = Tag.order(["field(id, ?)", [1,3,2]]).to_sql
+ assert_match(/field\(id, 1,3,2\)/, query)
+ end
+
def test_finding_with_order_limit_and_offset
entrants = Entrant.order("id ASC").limit(2).offset(1)
@@ -931,6 +936,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')
@@ -938,6 +949,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')
@@ -1529,6 +1546,13 @@ class RelationTest < ActiveRecord::TestCase
assert_equal 'David', topic2.reload.author_name
end
+ def test_update_on_relation_passing_active_record_object_is_deprecated
+ topic = Topic.create!(title: 'Foo', author_name: nil)
+ assert_deprecated(/update/) do
+ Topic.where(id: topic.id).update(topic, title: 'Bar')
+ end
+ end
+
def test_distinct
tag1 = Tag.create(:name => 'Foo')
tag2 = Tag.create(:name => 'Foo')
diff --git a/activerecord/test/cases/sanitize_test.rb b/activerecord/test/cases/sanitize_test.rb
index 262e0abc22..14e392ac30 100644
--- a/activerecord/test/cases/sanitize_test.rb
+++ b/activerecord/test/cases/sanitize_test.rb
@@ -9,11 +9,11 @@ class SanitizeTest < ActiveRecord::TestCase
def test_sanitize_sql_array_handles_string_interpolation
quoted_bambi = ActiveRecord::Base.connection.quote_string("Bambi")
- assert_equal "name=#{quoted_bambi}", Binary.send(:sanitize_sql_array, ["name=%s", "Bambi"])
- assert_equal "name=#{quoted_bambi}", Binary.send(:sanitize_sql_array, ["name=%s", "Bambi".mb_chars])
+ assert_equal "name='#{quoted_bambi}'", Binary.send(:sanitize_sql_array, ["name='%s'", "Bambi"])
+ assert_equal "name='#{quoted_bambi}'", Binary.send(:sanitize_sql_array, ["name='%s'", "Bambi".mb_chars])
quoted_bambi_and_thumper = ActiveRecord::Base.connection.quote_string("Bambi\nand\nThumper")
- assert_equal "name=#{quoted_bambi_and_thumper}",Binary.send(:sanitize_sql_array, ["name=%s", "Bambi\nand\nThumper"])
- assert_equal "name=#{quoted_bambi_and_thumper}",Binary.send(:sanitize_sql_array, ["name=%s", "Bambi\nand\nThumper".mb_chars])
+ assert_equal "name='#{quoted_bambi_and_thumper}'",Binary.send(:sanitize_sql_array, ["name='%s'", "Bambi\nand\nThumper"])
+ assert_equal "name='#{quoted_bambi_and_thumper}'",Binary.send(:sanitize_sql_array, ["name='%s'", "Bambi\nand\nThumper".mb_chars])
end
def test_sanitize_sql_array_handles_bind_variables
diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb
index feb1c29656..43f133b12d 100644
--- a/activerecord/test/cases/schema_dumper_test.rb
+++ b/activerecord/test/cases/schema_dumper_test.rb
@@ -215,7 +215,7 @@ class SchemaDumperTest < ActiveRecord::TestCase
def test_schema_dump_includes_length_for_mysql_blob_and_text_fields
output = standard_dump
- assert_match %r{t\.binary\s+"tiny_blob",\s+limit: 255$}, output
+ assert_match %r{t\.blob\s+"tiny_blob",\s+limit: 255$}, output
assert_match %r{t\.binary\s+"normal_blob",\s+limit: 65535$}, output
assert_match %r{t\.binary\s+"medium_blob",\s+limit: 16777215$}, output
assert_match %r{t\.binary\s+"long_blob",\s+limit: 4294967295$}, output
@@ -353,6 +353,38 @@ class SchemaDumperTest < ActiveRecord::TestCase
ActiveRecord::Base.table_name_suffix = ActiveRecord::Base.table_name_prefix = ''
$stdout = original
end
+
+ def test_schema_dump_with_table_name_prefix_and_ignoring_tables
+ original, $stdout = $stdout, StringIO.new
+
+ create_cat_migration = Class.new(ActiveRecord::Migration) do
+ def change
+ create_table("cats") do |t|
+ end
+ create_table("omg_cats") do |t|
+ end
+ end
+ end
+
+ original_table_name_prefix = ActiveRecord::Base.table_name_prefix
+ original_schema_dumper_ignore_tables = ActiveRecord::SchemaDumper.ignore_tables
+ ActiveRecord::Base.table_name_prefix = 'omg_'
+ ActiveRecord::SchemaDumper.ignore_tables = ["cats"]
+ migration = create_cat_migration.new
+ migration.migrate(:up)
+
+ stream = StringIO.new
+ output = ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream).string
+
+ assert_match %r{create_table "omg_cats"}, output
+ refute_match %r{create_table "cats"}, output
+ ensure
+ migration.migrate(:down)
+ ActiveRecord::Base.table_name_prefix = original_table_name_prefix
+ ActiveRecord::SchemaDumper.ignore_tables = original_schema_dumper_ignore_tables
+
+ $stdout = original
+ end
end
class SchemaDumperDefaultsTest < ActiveRecord::TestCase
diff --git a/activerecord/test/cases/scoping/default_scoping_test.rb b/activerecord/test/cases/scoping/default_scoping_test.rb
index 0dbc60940e..86316ab476 100644
--- a/activerecord/test/cases/scoping/default_scoping_test.rb
+++ b/activerecord/test/cases/scoping/default_scoping_test.rb
@@ -3,6 +3,7 @@ require 'models/post'
require 'models/comment'
require 'models/developer'
require 'models/computer'
+require 'models/vehicle'
class DefaultScopingTest < ActiveRecord::TestCase
fixtures :developers, :posts, :comments
@@ -453,4 +454,9 @@ class DefaultScopingTest < ActiveRecord::TestCase
assert_equal 1, scope.where_clause.ast.children.length
assert_equal Developer.where(name: "David"), scope
end
+
+ def test_with_abstract_class_where_clause_should_not_be_duplicated
+ scope = Bus.all
+ assert_equal scope.where_clause.ast.children.length, 1
+ end
end
diff --git a/activerecord/test/cases/scoping/named_scoping_test.rb b/activerecord/test/cases/scoping/named_scoping_test.rb
index e4cc533517..7a8eaeccb7 100644
--- a/activerecord/test/cases/scoping/named_scoping_test.rb
+++ b/activerecord/test/cases/scoping/named_scoping_test.rb
@@ -188,8 +188,9 @@ class NamedScopingTest < ActiveRecord::TestCase
def test_any_should_call_proxy_found_if_using_a_block
topics = Topic.base
assert_queries(1) do
- topics.expects(:empty?).never
- topics.any? { true }
+ assert_not_called(topics, :empty?) do
+ topics.any? { true }
+ end
end
end
@@ -217,8 +218,9 @@ class NamedScopingTest < ActiveRecord::TestCase
def test_many_should_call_proxy_found_if_using_a_block
topics = Topic.base
assert_queries(1) do
- topics.expects(:size).never
- topics.many? { true }
+ assert_not_called(topics, :size) do
+ topics.many? { true }
+ end
end
end
diff --git a/activerecord/test/cases/serialization_test.rb b/activerecord/test/cases/serialization_test.rb
index 35b13ea247..14b80f4df4 100644
--- a/activerecord/test/cases/serialization_test.rb
+++ b/activerecord/test/cases/serialization_test.rb
@@ -8,7 +8,7 @@ require 'models/post'
class SerializationTest < ActiveRecord::TestCase
fixtures :books
- FORMATS = [ :xml, :json ]
+ FORMATS = [ :json ]
def setup
@contact_attributes = {
diff --git a/activerecord/test/cases/tasks/database_tasks_test.rb b/activerecord/test/cases/tasks/database_tasks_test.rb
index 38164b2228..c8f4179313 100644
--- a/activerecord/test/cases/tasks/database_tasks_test.rb
+++ b/activerecord/test/cases/tasks/database_tasks_test.rb
@@ -277,12 +277,14 @@ module ActiveRecord
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(ActiveRecord::Migrator.migrations_paths, 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
diff --git a/activerecord/test/cases/tasks/mysql_rake_test.rb b/activerecord/test/cases/tasks/mysql_rake_test.rb
index d0deb4c273..723a7618ba 100644
--- a/activerecord/test/cases/tasks/mysql_rake_test.rb
+++ b/activerecord/test/cases/tasks/mysql_rake_test.rb
@@ -21,28 +21,21 @@ module ActiveRecord
ActiveRecord::Tasks::DatabaseTasks.create @configuration
end
- def test_creates_database_with_default_encoding_and_collation
+ def test_creates_database_with_no_default_options
@connection.expects(:create_database).
- with('my-app-db', charset: 'utf8', collation: 'utf8_unicode_ci')
+ with('my-app-db', {})
ActiveRecord::Tasks::DatabaseTasks.create @configuration
end
- def test_creates_database_with_given_encoding_and_default_collation
- @connection.expects(:create_database).
- with('my-app-db', charset: 'utf8', collation: 'utf8_unicode_ci')
-
- ActiveRecord::Tasks::DatabaseTasks.create @configuration.merge('encoding' => 'utf8')
- end
-
- def test_creates_database_with_given_encoding_and_no_collation
+ def test_creates_database_with_given_encoding
@connection.expects(:create_database).
with('my-app-db', charset: 'latin1')
ActiveRecord::Tasks::DatabaseTasks.create @configuration.merge('encoding' => 'latin1')
end
- def test_creates_database_with_given_collation_and_no_encoding
+ def test_creates_database_with_given_collation
@connection.expects(:create_database).
with('my-app-db', collation: 'latin1_swedish_ci')
@@ -111,7 +104,7 @@ module ActiveRecord
def test_database_created_by_root
assert_permissions_granted_for "pat"
@connection.expects(:create_database).
- with('my-app-db', :charset => 'utf8', :collation => 'utf8_unicode_ci')
+ with('my-app-db', {})
ActiveRecord::Tasks::DatabaseTasks.create @configuration
end
@@ -203,9 +196,9 @@ module ActiveRecord
ActiveRecord::Tasks::DatabaseTasks.purge @configuration
end
- def test_recreates_database_with_the_default_options
+ def test_recreates_database_with_no_default_options
@connection.expects(:recreate_database).
- with('test-db', charset: 'utf8', collation: 'utf8_unicode_ci')
+ with('test-db', {})
ActiveRecord::Tasks::DatabaseTasks.purge @configuration
end
@@ -270,15 +263,16 @@ module ActiveRecord
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", "--routines", "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
@@ -311,6 +305,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 084302cde5..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
@@ -204,7 +204,7 @@ module ActiveRecord
end
def test_structure_dump
- Kernel.expects(:system).with("pg_dump -i -s -x -O -f #{@filename} my-app-db").returns(true)
+ Kernel.expects(:system).with('pg_dump', '-s', '-x', '-O', '-f', @filename, 'my-app-db').returns(true)
ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename)
end
@@ -212,7 +212,7 @@ module ActiveRecord
def test_structure_dump_with_schema_search_path
@configuration['schema_search_path'] = 'foo,bar'
- Kernel.expects(:system).with("pg_dump -i -s -x -O -f #{@filename} --schema=foo --schema=bar my-app-db").returns(true)
+ Kernel.expects(:system).with('pg_dump', '-s', '-x', '-O', '-f', @filename, '--schema=foo --schema=bar', 'my-app-db').returns(true)
ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename)
end
@@ -220,7 +220,7 @@ module ActiveRecord
def test_structure_dump_with_schema_search_path_and_dump_schemas_all
@configuration['schema_search_path'] = 'foo,bar'
- Kernel.expects(:system).with("pg_dump -i -s -x -O -f #{@filename} my-app-db").returns(true)
+ Kernel.expects(:system).with("pg_dump", '-s', '-x', '-O', '-f', @filename, 'my-app-db').returns(true)
with_dump_schemas(:all) do
ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename)
@@ -228,7 +228,7 @@ module ActiveRecord
end
def test_structure_dump_with_dump_schemas_string
- Kernel.expects(:system).with("pg_dump -i -s -x -O -f #{@filename} --schema=foo --schema=bar my-app-db").returns(true)
+ Kernel.expects(:system).with("pg_dump", '-s', '-x', '-O', '-f', @filename, '--schema=foo --schema=bar', "my-app-db").returns(true)
with_dump_schemas('foo,bar') do
ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename)
@@ -261,14 +261,14 @@ module ActiveRecord
def test_structure_load
filename = "awesome-file.sql"
- Kernel.expects(:system).with("psql -X -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 -X -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 7761ea5612..47e664f4e7 100644
--- a/activerecord/test/cases/test_case.rb
+++ b/activerecord/test/cases/test_case.rb
@@ -103,7 +103,7 @@ module ActiveRecord
# ignored SQL, or better yet, use a different notification for the queries
# instead examining the SQL content.
oracle_ignored = [/^select .*nextval/i, /^SAVEPOINT/, /^ROLLBACK TO/, /^\s*select .* from all_triggers/im, /^\s*select .* from all_constraints/im, /^\s*select .* from all_tab_cols/im]
- mysql_ignored = [/^SHOW TABLES/i, /^SHOW FULL FIELDS/, /^SHOW CREATE TABLE /i, /^SHOW VARIABLES /]
+ mysql_ignored = [/^SHOW FULL TABLES/i, /^SHOW FULL FIELDS/, /^SHOW CREATE TABLE /i, /^SHOW VARIABLES /, /^\s*SELECT (?:column_name|table_name)\b.*\bFROM information_schema\.(?:key_column_usage|tables)\b/im]
postgresql_ignored = [/^\s*select\b.*\bfrom\b.*pg_namespace\b/im, /^\s*select tablename\b.*from pg_tables\b/im, /^\s*select\b.*\battname\b.*\bfrom\b.*\bpg_attribute\b/im, /^SHOW search_path/i]
sqlite3_ignored = [/^\s*SELECT name\b.*\bFROM sqlite_master/im, /^\s*SELECT sql\b.*\bFROM sqlite_master/im]
diff --git a/activerecord/test/cases/test_fixtures_test.rb b/activerecord/test/cases/test_fixtures_test.rb
index 3f4baf8378..1970fe82d0 100644
--- a/activerecord/test/cases/test_fixtures_test.rb
+++ b/activerecord/test/cases/test_fixtures_test.rb
@@ -28,7 +28,7 @@ class TestFixturesTest < ActiveRecord::TestCase
assert_equal true, @klass.use_transactional_tests
end
- def test_use_transactional_tests_can_be_overriden
+ def test_use_transactional_tests_can_be_overridden
@klass.use_transactional_tests = "foobar"
assert_equal "foobar", @klass.use_transactional_tests
diff --git a/activerecord/test/cases/timestamp_test.rb b/activerecord/test/cases/timestamp_test.rb
index 5dab32995c..970f6bcf4a 100644
--- a/activerecord/test/cases/timestamp_test.rb
+++ b/activerecord/test/cases/timestamp_test.rb
@@ -84,7 +84,9 @@ class TimestampTest < ActiveRecord::TestCase
def test_touching_an_attribute_updates_timestamp
previously_created_at = @developer.created_at
- @developer.touch(:created_at)
+ travel(1.second) do
+ @developer.touch(:created_at)
+ end
assert !@developer.created_at_changed? , 'created_at should not be changed'
assert !@developer.changed?, 'record should not be changed'
@@ -199,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
@@ -210,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
@@ -254,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
diff --git a/activerecord/test/cases/touch_later_test.rb b/activerecord/test/cases/touch_later_test.rb
index 49ada22529..7058f4fbe2 100644
--- a/activerecord/test/cases/touch_later_test.rb
+++ b/activerecord/test/cases/touch_later_test.rb
@@ -11,7 +11,7 @@ class TouchLaterTest < ActiveRecord::TestCase
def test_touch_laster_raise_if_non_persisted
invoice = Invoice.new
Invoice.transaction do
- refute invoice.persisted?
+ assert_not invoice.persisted?
assert_raises(ActiveRecord::ActiveRecordError) do
invoice.touch_later
end
@@ -21,7 +21,7 @@ class TouchLaterTest < ActiveRecord::TestCase
def test_touch_later_dont_set_dirty_attributes
invoice = Invoice.create!
invoice.touch_later
- refute invoice.changed?
+ assert_not invoice.changed?
end
def test_touch_later_update_the_attributes
@@ -72,7 +72,7 @@ class TouchLaterTest < ActiveRecord::TestCase
end
def test_touch_touches_immediately_with_a_custom_time
- time = Time.now.utc - 25.days
+ 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
diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb
index 29a6ec7522..ec5bdfd725 100644
--- a/activerecord/test/cases/transactions_test.rb
+++ b/activerecord/test/cases/transactions_test.rb
@@ -487,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
diff --git a/activerecord/test/cases/type/date_time_test.rb b/activerecord/test/cases/type/date_time_test.rb
new file mode 100644
index 0000000000..bc4900e1c2
--- /dev/null
+++ b/activerecord/test/cases/type/date_time_test.rb
@@ -0,0 +1,14 @@
+require "cases/helper"
+require "models/task"
+
+module ActiveRecord
+ module Type
+ class IntegerTest < ActiveRecord::TestCase
+ def test_datetime_seconds_precision_applied_to_timestamp
+ skip "This test is invalid if subsecond precision isn't supported" unless subsecond_precision_supported?
+ p = Task.create!(starting: ::Time.now)
+ assert_equal p.starting.usec, p.reload.starting.usec
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/type/decimal_test.rb b/activerecord/test/cases/type/decimal_test.rb
deleted file mode 100644
index fe49d0e79a..0000000000
--- a/activerecord/test/cases/type/decimal_test.rb
+++ /dev/null
@@ -1,51 +0,0 @@
-require "cases/helper"
-
-module ActiveRecord
- module Type
- class DecimalTest < ActiveRecord::TestCase
- def test_type_cast_decimal
- type = Decimal.new
- assert_equal BigDecimal.new("0"), type.cast(BigDecimal.new("0"))
- assert_equal BigDecimal.new("123"), type.cast(123.0)
- assert_equal BigDecimal.new("1"), type.cast(:"1")
- end
-
- def test_type_cast_decimal_from_float_with_large_precision
- type = Decimal.new(precision: ::Float::DIG + 2)
- assert_equal BigDecimal.new("123.0"), type.cast(123.0)
- end
-
- def test_type_cast_from_float_with_unspecified_precision
- type = Decimal.new
- assert_equal 22.68.to_d, type.cast(22.68)
- end
-
- def test_type_cast_decimal_from_rational_with_precision
- type = Decimal.new(precision: 2)
- assert_equal BigDecimal("0.33"), type.cast(Rational(1, 3))
- end
-
- def test_type_cast_decimal_from_rational_without_precision_defaults_to_18_36
- type = Decimal.new
- assert_equal BigDecimal("0.333333333333333333E0"), type.cast(Rational(1, 3))
- end
-
- def test_type_cast_decimal_from_object_responding_to_d
- value = Object.new
- def value.to_d
- BigDecimal.new("1")
- end
- type = Decimal.new
- assert_equal BigDecimal("1"), type.cast(value)
- end
-
- def test_changed?
- type = Decimal.new
-
- assert type.changed?(5.0, 5.0, '5.0wibble')
- assert_not type.changed?(5.0, 5.0, '5.0')
- assert_not type.changed?(-5.0, -5.0, '-5.0')
- end
- end
- end
-end
diff --git a/activerecord/test/cases/type/integer_test.rb b/activerecord/test/cases/type/integer_test.rb
index 0dcdbd0667..c0932d5357 100644
--- a/activerecord/test/cases/type/integer_test.rb
+++ b/activerecord/test/cases/type/integer_test.rb
@@ -4,112 +4,12 @@ require "models/company"
module ActiveRecord
module Type
class IntegerTest < ActiveRecord::TestCase
- test "simple values" do
- type = Type::Integer.new
- assert_equal 1, type.cast(1)
- assert_equal 1, type.cast('1')
- assert_equal 1, type.cast('1ignore')
- assert_equal 0, type.cast('bad1')
- assert_equal 0, type.cast('bad')
- assert_equal 1, type.cast(1.7)
- assert_equal 0, type.cast(false)
- assert_equal 1, type.cast(true)
- assert_nil type.cast(nil)
- end
-
- test "random objects cast to nil" do
- type = Type::Integer.new
- assert_nil type.cast([1,2])
- assert_nil type.cast({1 => 2})
- assert_nil type.cast(1..2)
- end
-
test "casting ActiveRecord models" do
type = Type::Integer.new
firm = Firm.create(:name => 'Apple')
assert_nil type.cast(firm)
end
- test "casting objects without to_i" do
- type = Type::Integer.new
- assert_nil type.cast(::Object.new)
- end
-
- test "casting nan and infinity" do
- type = Type::Integer.new
- assert_nil type.cast(::Float::NAN)
- assert_nil type.cast(1.0/0.0)
- end
-
- test "casting booleans for database" do
- type = Type::Integer.new
- assert_equal 1, type.serialize(true)
- assert_equal 0, type.serialize(false)
- end
-
- test "changed?" do
- type = Type::Integer.new
-
- assert type.changed?(5, 5, '5wibble')
- assert_not type.changed?(5, 5, '5')
- assert_not type.changed?(5, 5, '5.0')
- assert_not type.changed?(-5, -5, '-5')
- assert_not type.changed?(-5, -5, '-5.0')
- assert_not type.changed?(nil, nil, nil)
- end
-
- test "values below int min value are out of range" do
- assert_raises(::RangeError) do
- Integer.new.serialize(-2147483649)
- end
- end
-
- test "values above int max value are out of range" do
- assert_raises(::RangeError) do
- Integer.new.serialize(2147483648)
- end
- end
-
- test "very small numbers are out of range" do
- assert_raises(::RangeError) do
- Integer.new.serialize(-9999999999999999999999999999999)
- end
- end
-
- test "very large numbers are out of range" do
- assert_raises(::RangeError) do
- Integer.new.serialize(9999999999999999999999999999999)
- end
- end
-
- test "normal numbers are in range" do
- type = Integer.new
- assert_equal(0, type.serialize(0))
- assert_equal(-1, type.serialize(-1))
- assert_equal(1, type.serialize(1))
- end
-
- test "int max value is in range" do
- assert_equal(2147483647, Integer.new.serialize(2147483647))
- end
-
- test "int min value is in range" do
- assert_equal(-2147483648, Integer.new.serialize(-2147483648))
- end
-
- test "columns with a larger limit have larger ranges" do
- type = Integer.new(limit: 8)
-
- assert_equal(9223372036854775807, type.serialize(9223372036854775807))
- assert_equal(-9223372036854775808, type.serialize(-9223372036854775808))
- assert_raises(::RangeError) do
- type.serialize(-9999999999999999999999999999999)
- end
- assert_raises(::RangeError) do
- type.serialize(9999999999999999999999999999999)
- end
- end
-
test "values which are out of range can be re-assigned" do
klass = Class.new(ActiveRecord::Base) do
self.table_name = 'posts'
diff --git a/activerecord/test/cases/type/string_test.rb b/activerecord/test/cases/type/string_test.rb
index 56e9bf434d..6fe6d46711 100644
--- a/activerecord/test/cases/type/string_test.rb
+++ b/activerecord/test/cases/type/string_test.rb
@@ -2,20 +2,6 @@ require 'cases/helper'
module ActiveRecord
class StringTypeTest < ActiveRecord::TestCase
- test "type casting" do
- type = Type::String.new
- assert_equal "t", type.cast(true)
- assert_equal "f", type.cast(false)
- assert_equal "123", type.cast(123)
- end
-
- test "values are duped coming out" do
- s = "foo"
- type = Type::String.new
- assert_not_same s, type.cast(s)
- assert_not_same s, type.deserialize(s)
- end
-
test "string mutations are detected" do
klass = Class.new(Base)
klass.table_name = 'authors'
diff --git a/activerecord/test/cases/type/unsigned_integer_test.rb b/activerecord/test/cases/type/unsigned_integer_test.rb
deleted file mode 100644
index f2c910eade..0000000000
--- a/activerecord/test/cases/type/unsigned_integer_test.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-require "cases/helper"
-
-module ActiveRecord
- module Type
- class UnsignedIntegerTest < ActiveRecord::TestCase
- test "unsigned int max value is in range" do
- assert_equal(4294967295, UnsignedInteger.new.serialize(4294967295))
- end
-
- test "minus value is out of range" do
- assert_raises(::RangeError) do
- UnsignedInteger.new.serialize(-1)
- end
- end
- end
- end
-end
diff --git a/activerecord/test/cases/types_test.rb b/activerecord/test/cases/types_test.rb
index 9b1859c2ce..81fcf04a27 100644
--- a/activerecord/test/cases/types_test.rb
+++ b/activerecord/test/cases/types_test.rb
@@ -3,111 +3,6 @@ require "cases/helper"
module ActiveRecord
module ConnectionAdapters
class TypesTest < ActiveRecord::TestCase
- def test_type_cast_boolean
- type = Type::Boolean.new
- assert type.cast('').nil?
- assert type.cast(nil).nil?
-
- assert type.cast(true)
- assert type.cast(1)
- assert type.cast('1')
- assert type.cast('t')
- assert type.cast('T')
- assert type.cast('true')
- assert type.cast('TRUE')
- assert type.cast('on')
- assert type.cast('ON')
- assert type.cast(' ')
- assert type.cast("\u3000\r\n")
- assert type.cast("\u0000")
- assert type.cast('SOMETHING RANDOM')
-
- # explicitly check for false vs nil
- assert_equal false, type.cast(false)
- assert_equal false, type.cast(0)
- assert_equal false, type.cast('0')
- assert_equal false, type.cast('f')
- assert_equal false, type.cast('F')
- assert_equal false, type.cast('false')
- assert_equal false, type.cast('FALSE')
- assert_equal false, type.cast('off')
- assert_equal false, type.cast('OFF')
- end
-
- def test_type_cast_float
- type = Type::Float.new
- assert_equal 1.0, type.cast("1")
- end
-
- def test_changing_float
- type = Type::Float.new
-
- assert type.changed?(5.0, 5.0, '5wibble')
- assert_not type.changed?(5.0, 5.0, '5')
- assert_not type.changed?(5.0, 5.0, '5.0')
- assert_not type.changed?(nil, nil, nil)
- end
-
- def test_type_cast_binary
- type = Type::Binary.new
- assert_equal nil, type.cast(nil)
- assert_equal "1", type.cast("1")
- assert_equal 1, type.cast(1)
- end
-
- def test_type_cast_time
- type = Type::Time.new
- assert_equal nil, type.cast(nil)
- assert_equal nil, type.cast('')
- assert_equal nil, type.cast('ABC')
-
- time_string = Time.now.utc.strftime("%T")
- assert_equal time_string, type.cast(time_string).strftime("%T")
- end
-
- def test_type_cast_datetime_and_timestamp
- type = Type::DateTime.new
- assert_equal nil, type.cast(nil)
- assert_equal nil, type.cast('')
- assert_equal nil, type.cast(' ')
- assert_equal nil, type.cast('ABC')
-
- datetime_string = Time.now.utc.strftime("%FT%T")
- assert_equal datetime_string, type.cast(datetime_string).strftime("%FT%T")
- end
-
- def test_type_cast_date
- type = Type::Date.new
- assert_equal nil, type.cast(nil)
- assert_equal nil, type.cast('')
- assert_equal nil, type.cast(' ')
- assert_equal nil, type.cast('ABC')
-
- date_string = Time.now.utc.strftime("%F")
- assert_equal date_string, type.cast(date_string).strftime("%F")
- end
-
- def test_type_cast_duration_to_integer
- type = Type::Integer.new
- assert_equal 1800, type.cast(30.minutes)
- assert_equal 7200, type.cast(2.hours)
- end
-
- def test_string_to_time_with_timezone
- [:utc, :local].each do |zone|
- with_timezone_config default: zone do
- type = Type::DateTime.new
- assert_equal Time.utc(2013, 9, 4, 0, 0, 0), type.cast("Wed, 04 Sep 2013 03:00:00 EAT")
- end
- end
- end
-
- def test_type_equality
- assert_equal Type::Value.new, Type::Value.new
- assert_not_equal Type::Value.new, Type::Integer.new
- assert_not_equal Type::Value.new(precision: 1), Type::Value.new(precision: 2)
- end
-
def test_attributes_which_are_invalid_for_database_can_still_be_reassigned
type_which_cannot_go_to_the_database = Type::Value.new
def type_which_cannot_go_to_the_database.serialize(*)
diff --git a/activerecord/test/cases/validations/association_validation_test.rb b/activerecord/test/cases/validations/association_validation_test.rb
index bff5ffa65e..584a3dc0d8 100644
--- a/activerecord/test/cases/validations/association_validation_test.rb
+++ b/activerecord/test/cases/validations/association_validation_test.rb
@@ -45,6 +45,18 @@ class AssociationValidationTest < ActiveRecord::TestCase
assert t.valid?
end
+ def test_validates_associated_without_marked_for_destruction
+ reply = Class.new do
+ def valid?
+ true
+ end
+ end
+ Topic.validates_associated(:replies)
+ t = Topic.new
+ t.define_singleton_method(:replies) { [reply.new] }
+ assert t.valid?
+ end
+
def test_validates_associated_with_custom_message_using_quotes
Reply.validates_associated :topic, :message=> "This string contains 'single' and \"double\" quotes"
Topic.validates_presence_of :content
diff --git a/activerecord/test/cases/validations/i18n_validation_test.rb b/activerecord/test/cases/validations/i18n_validation_test.rb
index 268d7914b5..981239c4d6 100644
--- a/activerecord/test/cases/validations/i18n_validation_test.rb
+++ b/activerecord/test/cases/validations/i18n_validation_test.rb
@@ -53,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
@@ -63,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 f95f8f0b8f..c5d8f8895c 100644
--- a/activerecord/test/cases/validations/length_validation_test.rb
+++ b/activerecord/test/cases/validations/length_validation_test.rb
@@ -1,4 +1,3 @@
-# -*- coding: utf-8 -*-
require "cases/helper"
require 'models/owner'
require 'models/pet'
diff --git a/activerecord/test/cases/validations/uniqueness_validation_test.rb b/activerecord/test/cases/validations/uniqueness_validation_test.rb
index 2608c84be2..7502a55391 100644
--- a/activerecord/test/cases/validations/uniqueness_validation_test.rb
+++ b/activerecord/test/cases/validations/uniqueness_validation_test.rb
@@ -4,6 +4,7 @@ require 'models/reply'
require 'models/warehouse_thing'
require 'models/guid'
require 'models/event'
+require 'models/dashboard'
class Wizard < ActiveRecord::Base
self.abstract_class = true
@@ -427,4 +428,45 @@ class UniquenessValidationTest < ActiveRecord::TestCase
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_test.rb b/activerecord/test/cases/validations_test.rb
index f4f316f393..d04f4f7ce7 100644
--- a/activerecord/test/cases/validations_test.rb
+++ b/activerecord/test/cases/validations_test.rb
@@ -52,6 +52,13 @@ class ValidationsTest < ActiveRecord::TestCase
assert r.valid?(:special_case)
end
+ def test_invalid_using_multiple_contexts
+ r = WrongReply.new(:title => 'Wrong Create')
+ assert r.invalid?([:special_case, :create])
+ assert_equal "Invalid", r.errors[:author_name].join
+ assert_equal "is Wrong Create", r.errors[:title].join
+ end
+
def test_validate
r = WrongReply.new
@@ -161,4 +168,15 @@ class ValidationsTest < ActiveRecord::TestCase
ensure
Topic.reset_column_information
end
+
+ def test_acceptance_validator_doesnt_require_db_connection
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = 'posts'
+ end
+ klass.reset_column_information
+
+ assert_no_queries do
+ klass.validates_acceptance_of(:foo)
+ end
+ end
end
diff --git a/activerecord/test/cases/view_test.rb b/activerecord/test/cases/view_test.rb
index f9dca1e196..d50ae74e35 100644
--- a/activerecord/test/cases/view_test.rb
+++ b/activerecord/test/cases/view_test.rb
@@ -1,7 +1,9 @@
require "cases/helper"
require "models/book"
+require "support/schema_dumping_helper"
module ViewBehavior
+ include SchemaDumpingHelper
extend ActiveSupport::Concern
included do
@@ -31,9 +33,24 @@ module ViewBehavior
assert_equal ["Ruby for Rails"], books.map(&:name)
end
+ def test_views
+ assert_equal [Ebook.table_name], @connection.views
+ end
+
+ def test_view_exists
+ view_name = Ebook.table_name
+ assert @connection.view_exists?(view_name), "'#{view_name}' view should exist"
+ end
+
def test_table_exists
view_name = Ebook.table_name
- assert @connection.table_exists?(view_name), "'#{view_name}' table should exist"
+ # TODO: switch this assertion around once we changed #tables to not return views.
+ ActiveSupport::Deprecation.silence { assert @connection.table_exists?(view_name), "'#{view_name}' table should exist" }
+ end
+
+ def test_views_ara_valid_data_sources
+ view_name = Ebook.table_name
+ assert @connection.data_source_exists?(view_name), "'#{view_name}' should be a data source"
end
def test_column_definitions
@@ -53,6 +70,11 @@ module ViewBehavior
end
assert_nil model.primary_key
end
+
+ def test_does_not_dump_view_as_table
+ schema = dump_table_schema "ebooks"
+ assert_no_match %r{create_table "ebooks"}, schema
+ end
end
if ActiveRecord::Base.connection.supports_views?
@@ -65,11 +87,12 @@ class ViewWithPrimaryKeyTest < ActiveRecord::TestCase
end
def drop_view(name)
- @connection.execute "DROP VIEW #{name}" if @connection.table_exists? name
+ @connection.execute "DROP VIEW #{name}" if @connection.view_exists? name
end
end
class ViewWithoutPrimaryKeyTest < ActiveRecord::TestCase
+ include SchemaDumpingHelper
fixtures :books
class Paperback < ActiveRecord::Base; end
@@ -83,7 +106,7 @@ class ViewWithoutPrimaryKeyTest < ActiveRecord::TestCase
end
teardown do
- @connection.execute "DROP VIEW paperbacks" if @connection.table_exists? "paperbacks"
+ @connection.execute "DROP VIEW paperbacks" if @connection.view_exists? "paperbacks"
end
def test_reading
@@ -91,9 +114,19 @@ class ViewWithoutPrimaryKeyTest < ActiveRecord::TestCase
assert_equal ["Agile Web Development with Rails"], books.map(&:name)
end
+ def test_views
+ assert_equal [Paperback.table_name], @connection.views
+ end
+
+ def test_view_exists
+ view_name = Paperback.table_name
+ assert @connection.view_exists?(view_name), "'#{view_name}' view should exist"
+ end
+
def test_table_exists
view_name = Paperback.table_name
- assert @connection.table_exists?(view_name), "'#{view_name}' table should exist"
+ # TODO: switch this assertion around once we changed #tables to not return views.
+ ActiveSupport::Deprecation.silence { assert @connection.table_exists?(view_name), "'#{view_name}' table should exist" }
end
def test_column_definitions
@@ -109,5 +142,75 @@ class ViewWithoutPrimaryKeyTest < ActiveRecord::TestCase
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.view_exists? "printed_books"
+ end
+
+ def test_update_record
+ book = PrintedBook.first
+ book.name = "AWDwR"
+ book.save!
+ book.reload
+ assert_equal "AWDwR", book.name
+ end
+
+ def test_insert_record
+ PrintedBook.create! name: "Rails in Action", status: 0, format: "paperback"
+
+ new_book = PrintedBook.last
+ assert_equal "Rails in Action", new_book.name
+ end
+
+ def test_update_record_to_fail_view_conditions
+ book = PrintedBook.first
+ book.format = "ebook"
+ book.save!
+
+ assert_raises ActiveRecord::RecordNotFound do
+ book.reload
+ end
+ end
+end
+end # end fo `if current_adapter?(: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.view_exists? name
+ end
end
end
diff --git a/activerecord/test/cases/xml_serialization_test.rb b/activerecord/test/cases/xml_serialization_test.rb
deleted file mode 100644
index b30b50f597..0000000000
--- a/activerecord/test/cases/xml_serialization_test.rb
+++ /dev/null
@@ -1,447 +0,0 @@
-require "cases/helper"
-require "rexml/document"
-require 'models/contact'
-require 'models/post'
-require 'models/author'
-require 'models/comment'
-require 'models/company_in_module'
-require 'models/toy'
-require 'models/topic'
-require 'models/reply'
-require 'models/company'
-
-class XmlSerializationTest < ActiveRecord::TestCase
- def test_should_serialize_default_root
- @xml = Contact.new.to_xml
- assert_match %r{^<contact>}, @xml
- assert_match %r{</contact>$}, @xml
- end
-
- def test_should_serialize_default_root_with_namespace
- @xml = Contact.new.to_xml :namespace=>"http://xml.rubyonrails.org/contact"
- assert_match %r{^<contact xmlns="http://xml\.rubyonrails\.org/contact">}, @xml
- assert_match %r{</contact>$}, @xml
- end
-
- def test_should_serialize_custom_root
- @xml = Contact.new.to_xml :root => 'xml_contact'
- assert_match %r{^<xml-contact>}, @xml
- assert_match %r{</xml-contact>$}, @xml
- end
-
- def test_should_allow_undasherized_tags
- @xml = Contact.new.to_xml :root => 'xml_contact', :dasherize => false
- assert_match %r{^<xml_contact>}, @xml
- assert_match %r{</xml_contact>$}, @xml
- assert_match %r{<created_at}, @xml
- end
-
- def test_should_allow_camelized_tags
- @xml = Contact.new.to_xml :root => 'xml_contact', :camelize => true
- assert_match %r{^<XmlContact>}, @xml
- assert_match %r{</XmlContact>$}, @xml
- assert_match %r{<CreatedAt}, @xml
- end
-
- def test_should_allow_skipped_types
- @xml = Contact.new(:age => 25).to_xml :skip_types => true
- assert %r{<age>25</age>}.match(@xml)
- end
-
- def test_should_include_yielded_additions
- @xml = Contact.new.to_xml do |xml|
- xml.creator "David"
- end
- assert_match %r{<creator>David</creator>}, @xml
- end
-
- def test_to_xml_with_block
- value = "Rockin' the block"
- xml = Contact.new.to_xml(:skip_instruct => true) do |_xml|
- _xml.tag! "arbitrary-element", value
- end
- assert_equal "<contact>", xml.first(9)
- assert xml.include?(%(<arbitrary-element>#{value}</arbitrary-element>))
- end
-
- def test_should_skip_instruct_for_included_records
- @contact = Contact.new
- @contact.alternative = Contact.new(:name => 'Copa Cabana')
- @xml = @contact.to_xml(:include => [ :alternative ])
- assert_equal @xml.index('<?xml '), 0
- assert_nil @xml.index('<?xml ', 1)
- end
-end
-
-class DefaultXmlSerializationTest < ActiveRecord::TestCase
- def setup
- @contact = Contact.new(
- :name => 'aaron stack',
- :age => 25,
- :avatar => 'binarydata',
- :created_at => Time.utc(2006, 8, 1),
- :awesome => false,
- :preferences => { :gem => 'ruby' }
- )
- end
-
- def test_should_serialize_string
- assert_match %r{<name>aaron stack</name>}, @contact.to_xml
- end
-
- def test_should_serialize_integer
- assert_match %r{<age type="integer">25</age>}, @contact.to_xml
- end
-
- def test_should_serialize_binary
- xml = @contact.to_xml
- assert_match %r{YmluYXJ5ZGF0YQ==\n</avatar>}, xml
- assert_match %r{<avatar(.*)(type="binary")}, xml
- assert_match %r{<avatar(.*)(encoding="base64")}, xml
- end
-
- def test_should_serialize_datetime
- assert_match %r{<created-at type=\"dateTime\">2006-08-01T00:00:00Z</created-at>}, @contact.to_xml
- end
-
- def test_should_serialize_boolean
- assert_match %r{<awesome type=\"boolean\">false</awesome>}, @contact.to_xml
- end
-
- def test_should_serialize_hash
- assert_match %r{<preferences>\s*<gem>ruby</gem>\s*</preferences>}m, @contact.to_xml
- end
-
- def test_uses_serializable_hash_with_only_option
- def @contact.serializable_hash(options=nil)
- super(only: %w(name))
- end
-
- xml = @contact.to_xml
- assert_match %r{<name>aaron stack</name>}, xml
- assert_no_match %r{age}, xml
- assert_no_match %r{awesome}, xml
- end
-
- def test_uses_serializable_hash_with_except_option
- def @contact.serializable_hash(options=nil)
- super(except: %w(age))
- end
-
- xml = @contact.to_xml
- assert_match %r{<name>aaron stack</name>}, xml
- assert_match %r{<awesome type=\"boolean\">false</awesome>}, xml
- assert_no_match %r{age}, xml
- end
-
- def test_does_not_include_inheritance_column_from_sti
- @contact = ContactSti.new(@contact.attributes)
- assert_equal 'ContactSti', @contact.type
-
- xml = @contact.to_xml
- assert_match %r{<name>aaron stack</name>}, xml
- assert_no_match %r{<type}, xml
- assert_no_match %r{ContactSti}, xml
- end
-
- def test_serializable_hash_with_default_except_option_and_excluding_inheritance_column_from_sti
- @contact = ContactSti.new(@contact.attributes)
- assert_equal 'ContactSti', @contact.type
-
- def @contact.serializable_hash(options={})
- super({ except: %w(age) }.merge!(options))
- end
-
- xml = @contact.to_xml
- assert_match %r{<name>aaron stack</name>}, xml
- assert_no_match %r{age}, xml
- assert_no_match %r{<type}, xml
- assert_no_match %r{ContactSti}, xml
- end
-end
-
-class DefaultXmlSerializationTimezoneTest < ActiveRecord::TestCase
- def test_should_serialize_datetime_with_timezone
- with_timezone_config zone: "Pacific Time (US & Canada)" do
- toy = Toy.create(:name => 'Mickey', :updated_at => Time.utc(2006, 8, 1))
- assert_match %r{<updated-at type=\"dateTime\">2006-07-31T17:00:00-07:00</updated-at>}, toy.to_xml
- end
- end
-
- def test_should_serialize_datetime_with_timezone_reloaded
- with_timezone_config zone: "Pacific Time (US & Canada)" do
- toy = Toy.create(:name => 'Minnie', :updated_at => Time.utc(2006, 8, 1)).reload
- assert_match %r{<updated-at type=\"dateTime\">2006-07-31T17:00:00-07:00</updated-at>}, toy.to_xml
- end
- end
-end
-
-class NilXmlSerializationTest < ActiveRecord::TestCase
- def setup
- @xml = Contact.new.to_xml(:root => 'xml_contact')
- end
-
- def test_should_serialize_string
- assert_match %r{<name nil="true"/>}, @xml
- end
-
- def test_should_serialize_integer
- assert %r{<age (.*)/>}.match(@xml)
- attributes = $1
- assert_match %r{nil="true"}, attributes
- assert_match %r{type="integer"}, attributes
- end
-
- def test_should_serialize_binary
- assert %r{<avatar (.*)/>}.match(@xml)
- attributes = $1
- assert_match %r{type="binary"}, attributes
- assert_match %r{encoding="base64"}, attributes
- assert_match %r{nil="true"}, attributes
- end
-
- def test_should_serialize_datetime
- assert %r{<created-at (.*)/>}.match(@xml)
- attributes = $1
- assert_match %r{nil="true"}, attributes
- assert_match %r{type="dateTime"}, attributes
- end
-
- def test_should_serialize_boolean
- assert %r{<awesome (.*)/>}.match(@xml)
- attributes = $1
- assert_match %r{type="boolean"}, attributes
- assert_match %r{nil="true"}, attributes
- end
-
- def test_should_serialize_yaml
- assert_match %r{<preferences nil=\"true\"/>}, @xml
- end
-end
-
-class DatabaseConnectedXmlSerializationTest < ActiveRecord::TestCase
- fixtures :topics, :companies, :accounts, :authors, :posts, :projects
-
- def test_to_xml
- xml = REXML::Document.new(topics(:first).to_xml(:indent => 0))
- bonus_time_in_current_timezone = topics(:first).bonus_time.xmlschema
- written_on_in_current_timezone = topics(:first).written_on.xmlschema
-
- assert_equal "topic", xml.root.name
- assert_equal "The First Topic" , xml.elements["//title"].text
- assert_equal "David" , xml.elements["//author-name"].text
- assert_match "Have a nice day", xml.elements["//content"].text
-
- assert_equal "1", xml.elements["//id"].text
- assert_equal "integer" , xml.elements["//id"].attributes['type']
-
- assert_equal "1", xml.elements["//replies-count"].text
- assert_equal "integer" , xml.elements["//replies-count"].attributes['type']
-
- assert_equal written_on_in_current_timezone, xml.elements["//written-on"].text
- assert_equal "dateTime" , xml.elements["//written-on"].attributes['type']
-
- assert_equal "david@loudthinking.com", xml.elements["//author-email-address"].text
-
- assert_equal nil, xml.elements["//parent-id"].text
- assert_equal "integer", xml.elements["//parent-id"].attributes['type']
- assert_equal "true", xml.elements["//parent-id"].attributes['nil']
-
- # Oracle enhanced adapter allows to define Date attributes in model class (see topic.rb)
- assert_equal "2004-04-15", xml.elements["//last-read"].text
- assert_equal "date" , xml.elements["//last-read"].attributes['type']
-
- # Oracle and DB2 don't have true boolean or time-only fields
- unless current_adapter?(:OracleAdapter, :DB2Adapter)
- assert_equal "false", xml.elements["//approved"].text
- assert_equal "boolean" , xml.elements["//approved"].attributes['type']
-
- assert_equal bonus_time_in_current_timezone, xml.elements["//bonus-time"].text
- assert_equal "dateTime" , xml.elements["//bonus-time"].attributes['type']
- end
- end
-
- def test_except_option
- xml = topics(:first).to_xml(:indent => 0, :skip_instruct => true, :except => [:title, :replies_count])
- assert_equal "<topic>", xml.first(7)
- assert !xml.include?(%(<title>The First Topic</title>))
- assert xml.include?(%(<author-name>David</author-name>))
-
- xml = topics(:first).to_xml(:indent => 0, :skip_instruct => true, :except => [:title, :author_name, :replies_count])
- assert !xml.include?(%(<title>The First Topic</title>))
- assert !xml.include?(%(<author-name>David</author-name>))
- end
-
- # to_xml used to mess with the hash the user provided which
- # caused the builder to be reused. This meant the document kept
- # getting appended to.
-
- def test_modules
- projects = MyApplication::Business::Project.all
- xml = projects.to_xml
- root = projects.first.class.to_s.underscore.pluralize.tr('/','_').dasherize
- assert_match "<#{root} type=\"array\">", xml
- assert_match "</#{root}>", xml
- end
-
- def test_passing_hash_shouldnt_reuse_builder
- options = {:include=>:posts}
- david = authors(:david)
- first_xml_size = david.to_xml(options).size
- second_xml_size = david.to_xml(options).size
- assert_equal first_xml_size, second_xml_size
- end
-
- def test_include_uses_association_name
- xml = authors(:david).to_xml :include=>:hello_posts, :indent => 0
- assert_match %r{<hello-posts type="array">}, xml
- assert_match %r{<hello-post type="Post">}, xml
- assert_match %r{<hello-post type="StiPost">}, xml
- end
-
- def test_included_associations_should_skip_types
- xml = authors(:david).to_xml :include=>:hello_posts, :indent => 0, :skip_types => true
- assert_match %r{<hello-posts>}, xml
- assert_match %r{<hello-post>}, xml
- assert_match %r{<hello-post>}, xml
- end
-
- def test_including_has_many_association
- xml = topics(:first).to_xml(:indent => 0, :skip_instruct => true, :include => :replies, :except => :replies_count)
- assert_equal "<topic>", xml.first(7)
- assert xml.include?(%(<replies type="array"><reply>))
- assert xml.include?(%(<title>The Second Topic of the day</title>))
- end
-
- def test_including_belongs_to_association
- xml = companies(:first_client).to_xml(:indent => 0, :skip_instruct => true, :include => :firm)
- assert !xml.include?("<firm>")
-
- xml = companies(:second_client).to_xml(:indent => 0, :skip_instruct => true, :include => :firm)
- assert xml.include?("<firm>")
- end
-
- def test_including_multiple_associations
- xml = companies(:first_firm).to_xml(:indent => 0, :skip_instruct => true, :include => [ :clients, :account ])
- assert_equal "<firm>", xml.first(6)
- assert xml.include?(%(<account>))
- assert xml.include?(%(<clients type="array"><client>))
- end
-
- def test_including_association_with_options
- xml = companies(:first_firm).to_xml(
- :indent => 0, :skip_instruct => true,
- :include => { :clients => { :only => :name } }
- )
-
- assert_equal "<firm>", xml.first(6)
- assert xml.include?(%(<client><name>Summit</name></client>))
- assert xml.include?(%(<clients type="array"><client>))
- end
-
- def test_methods_are_called_on_object
- xml = authors(:david).to_xml :methods => :label, :indent => 0
- assert_match %r{<label>.*</label>}, xml
- end
-
- def test_should_not_call_methods_on_associations_that_dont_respond
- xml = authors(:david).to_xml :include=>:hello_posts, :methods => :label, :indent => 2
- assert !authors(:david).hello_posts.first.respond_to?(:label)
- assert_match %r{^ <label>.*</label>}, xml
- assert_no_match %r{^ <label>}, xml
- end
-
- def test_procs_are_called_on_object
- proc = Proc.new { |options| options[:builder].tag!('nationality', 'Danish') }
- xml = authors(:david).to_xml(:procs => [ proc ])
- assert_match %r{<nationality>Danish</nationality>}, xml
- end
-
- def test_dual_arity_procs_are_called_on_object
- proc = Proc.new { |options, record| options[:builder].tag!('name-reverse', record.name.reverse) }
- xml = authors(:david).to_xml(:procs => [ proc ])
- assert_match %r{<name-reverse>divaD</name-reverse>}, xml
- end
-
- def test_top_level_procs_arent_applied_to_associations
- author_proc = Proc.new { |options| options[:builder].tag!('nationality', 'Danish') }
- xml = authors(:david).to_xml(:procs => [ author_proc ], :include => :posts, :indent => 2)
-
- assert_match %r{^ <nationality>Danish</nationality>}, xml
- assert_no_match %r{^ {6}<nationality>Danish</nationality>}, xml
- end
-
- def test_procs_on_included_associations_are_called
- posts_proc = Proc.new { |options| options[:builder].tag!('copyright', 'DHH') }
- xml = authors(:david).to_xml(
- :indent => 2,
- :include => {
- :posts => { :procs => [ posts_proc ] }
- }
- )
-
- assert_no_match %r{^ <copyright>DHH</copyright>}, xml
- assert_match %r{^ {6}<copyright>DHH</copyright>}, xml
- end
-
- def test_should_include_empty_has_many_as_empty_array
- authors(:david).posts.delete_all
- xml = authors(:david).to_xml :include=>:posts, :indent => 2
-
- assert_equal [], Hash.from_xml(xml)['author']['posts']
- assert_match %r{^ <posts type="array"/>}, xml
- end
-
- def test_should_has_many_array_elements_should_include_type_when_different_from_guessed_value
- xml = authors(:david).to_xml :include=>:posts_with_comments, :indent => 2
-
- assert Hash.from_xml(xml)
- assert_match %r{^ <posts-with-comments type="array">}, xml
- assert_match %r{^ <posts-with-comment type="Post">}, xml
- assert_match %r{^ <posts-with-comment type="StiPost">}, xml
-
- types = Hash.from_xml(xml)['author']['posts_with_comments'].collect {|t| t['type'] }
- assert types.include?('SpecialPost')
- assert types.include?('Post')
- assert types.include?('StiPost')
- end
-
- def test_should_produce_xml_for_methods_returning_array
- xml = authors(:david).to_xml(:methods => :social)
- array = Hash.from_xml(xml)['author']['social']
- assert_equal 2, array.size
- assert array.include? 'twitter'
- assert array.include? 'github'
- end
-
- def test_should_support_aliased_attributes
- xml = Author.select("name as firstname").to_xml
- Author.all.each do |author|
- assert xml.include?(%(<firstname>#{author.name}</firstname>)), xml
- end
- end
-
- def test_array_to_xml_including_has_many_association
- xml = [ topics(:first), topics(:second) ].to_xml(:indent => 0, :skip_instruct => true, :include => :replies)
- assert xml.include?(%(<replies type="array"><reply>))
- end
-
- def test_array_to_xml_including_methods
- xml = [ topics(:first), topics(:second) ].to_xml(:indent => 0, :skip_instruct => true, :methods => [ :topic_id ])
- assert xml.include?(%(<topic-id type="integer">#{topics(:first).topic_id}</topic-id>)), xml
- assert xml.include?(%(<topic-id type="integer">#{topics(:second).topic_id}</topic-id>)), xml
- end
-
- def test_array_to_xml_including_has_one_association
- xml = [ companies(:first_firm), companies(:rails_core) ].to_xml(:indent => 0, :skip_instruct => true, :include => :account)
- assert xml.include?(companies(:first_firm).account.to_xml(:indent => 0, :skip_instruct => true))
- assert xml.include?(companies(:rails_core).account.to_xml(:indent => 0, :skip_instruct => true))
- end
-
- def test_array_to_xml_including_belongs_to_association
- xml = [ companies(:first_client), companies(:second_client), companies(:another_client) ].to_xml(:indent => 0, :skip_instruct => true, :include => :firm)
- assert xml.include?(companies(:first_client).to_xml(:indent => 0, :skip_instruct => true))
- assert xml.include?(companies(:second_client).firm.to_xml(:indent => 0, :skip_instruct => true))
- assert xml.include?(companies(:another_client).firm.to_xml(:indent => 0, :skip_instruct => true))
- end
-end
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/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/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/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/models/author.rb b/activerecord/test/models/author.rb
index 8c1f14bd36..0d90cbb110 100644
--- a/activerecord/test/models/author.rb
+++ b/activerecord/test/models/author.rb
@@ -144,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/book.rb b/activerecord/test/models/book.rb
index 24bfe47bbf..1927191393 100644
--- a/activerecord/test/models/book.rb
+++ b/activerecord/test/models/book.rb
@@ -10,10 +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], enum_prefix: :in
- enum author_visibility: [:visible, :invisible], enum_prefix: true
- enum illustrator_visibility: [:visible, :invisible], enum_prefix: true
- enum font_size: [:small, :medium, :large], enum_prefix: :with, enum_suffix: true
+ 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 a6e83fe353..c1e491e5c5 100644
--- a/activerecord/test/models/bulb.rb
+++ b/activerecord/test/models/bulb.rb
@@ -1,6 +1,7 @@
class Bulb < ActiveRecord::Base
default_scope { where(:name => 'defaulty') }
belongs_to :car, :touch => true
+ scope :awesome, -> { where(frickinawesome: true) }
attr_reader :scope_after_initialize, :attributes_after_initialize
diff --git a/activerecord/test/models/car.rb b/activerecord/test/models/car.rb
index 81263b79d1..778c22b1f6 100644
--- a/activerecord/test/models/car.rb
+++ b/activerecord/test/models/car.rb
@@ -4,6 +4,7 @@ class Car < ActiveRecord::Base
has_many :funky_bulbs, class_name: 'FunkyBulb', dependent: :destroy
has_many :failed_bulbs, class_name: 'FailedBulb', dependent: :destroy
has_many :foo_bulbs, -> { where(:name => 'foo') }, :class_name => "Bulb"
+ has_many :awesome_bulbs, -> { awesome }, class_name: "Bulb"
has_one :bulb
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/company.rb b/activerecord/test/models/company.rb
index 67936e8e5d..1dcd9fc21e 100644
--- a/activerecord/test/models/company.rb
+++ b/activerecord/test/models/company.rb
@@ -10,7 +10,6 @@ class Company < AbstractCompany
has_one :dummy_account, :foreign_key => "firm_id", :class_name => "Account"
has_many :contracts
has_many :developers, :through => :contracts
- has_many :accounts
scope :of_first_firm, lambda {
joins(:account => :firm).
@@ -86,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
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_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 d2a5a7fc49..9a907273f8 100644
--- a/activerecord/test/models/developer.rb
+++ b/activerecord/test/models/developer.rb
@@ -7,12 +7,16 @@ 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"
@@ -50,6 +54,10 @@ class Developer < ActiveRecord::Base
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') }
@@ -60,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/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/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/member.rb b/activerecord/test/models/member.rb
index dc0566d8a7..7693c6e515 100644
--- a/activerecord/test/models/member.rb
+++ b/activerecord/test/models/member.rb
@@ -26,6 +26,9 @@ 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
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/parrot.rb b/activerecord/test/models/parrot.rb
index b26035d944..ddc9dcaf29 100644
--- a/activerecord/test/models/parrot.rb
+++ b/activerecord/test/models/parrot.rb
@@ -21,9 +21,3 @@ end
class DeadParrot < Parrot
belongs_to :killer, :class_name => 'Pirate', foreign_key: :killer_id
end
-
-class FunkyParrot < Parrot
- before_destroy do
- raise "before_destroy was called"
- end
-end
diff --git a/activerecord/test/models/person.rb b/activerecord/test/models/person.rb
index ad12f00d42..a4a9c6b0d4 100644
--- a/activerecord/test/models/person.rb
+++ b/activerecord/test/models/person.rb
@@ -37,7 +37,6 @@ class Person < ActiveRecord::Base
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/post.rb b/activerecord/test/models/post.rb
index 10f13b67da..23cebe2602 100644
--- a/activerecord/test/models/post.rb
+++ b/activerecord/test/models/post.rb
@@ -98,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
@@ -185,6 +185,7 @@ class SubStiPost < StiPost
end
class FirstPost < ActiveRecord::Base
+ self.inheritance_column = :disabled
self.table_name = 'posts'
default_scope { where(:id => 1) }
@@ -193,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
@@ -204,6 +206,7 @@ class PostWithSpecialCategorization < Post
end
class PostWithDefaultScope < ActiveRecord::Base
+ self.inheritance_column = :disabled
self.table_name = 'posts'
default_scope { order(:title) }
end
@@ -225,11 +228,13 @@ class PostWithIncludesDefaultScope < ActiveRecord::Base
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
@@ -239,6 +244,7 @@ class PostThatLoadsCommentsInAnAfterSaveHook < ActiveRecord::Base
end
class PostWithAfterCreateCallback < ActiveRecord::Base
+ self.inheritance_column = :disabled
self.table_name = 'posts'
has_many :comments, foreign_key: :post_id
@@ -248,6 +254,7 @@ class PostWithAfterCreateCallback < ActiveRecord::Base
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
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..efa8246f1e 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,16 @@ 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
+
+ begin
+ previous_value, ActiveRecord::Base.belongs_to_required_by_default =
+ ActiveRecord::Base.belongs_to_required_by_default, true
+ has_and_belongs_to_many :developers_required_by_default, class_name: "Developer"
+ ensure
+ ActiveRecord::Base.belongs_to_required_by_default = previous_value
+ end
attr_accessor :developers_log
after_initialize :set_developers_log
diff --git a/activerecord/test/models/ship.rb b/activerecord/test/models/ship.rb
index 95172e4d3e..e333b964ab 100644
--- a/activerecord/test/models/ship.rb
+++ b/activerecord/test/models/ship.rb
@@ -3,6 +3,7 @@ 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
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 d17270021a..176bc79dc7 100644
--- a/activerecord/test/models/topic.rb
+++ b/activerecord/test/models/topic.rb
@@ -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 ffc65466d5..63ff0c23ec 100644
--- a/activerecord/test/models/treasure.rb
+++ b/activerecord/test/models/treasure.rb
@@ -1,6 +1,7 @@
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
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/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 52d3290c84..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
@@ -40,6 +40,17 @@ BEGIN
END
SQL
+ ActiveRecord::Base.connection.execute <<-SQL
+DROP PROCEDURE IF EXISTS topics;
+SQL
+
+ ActiveRecord::Base.connection.execute <<-SQL
+CREATE PROCEDURE topics(IN num INT) SQL SECURITY INVOKER
+BEGIN
+ select * from topics limit num;
+END
+SQL
+
ActiveRecord::Base.connection.drop_table "enum_tests", if_exists: true
ActiveRecord::Base.connection.execute <<-SQL
diff --git a/activerecord/test/schema/mysql_specific_schema.rb b/activerecord/test/schema/mysql_specific_schema.rb
index 90f5a60d7b..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
@@ -45,9 +45,9 @@ 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
diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb
index 7bab675b2a..99098017d7 100644
--- a/activerecord/test/schema/schema.rb
+++ b/activerecord/test/schema/schema.rb
@@ -1,4 +1,3 @@
-
ActiveRecord::Schema.define do
def except(adapter_names_to_exclude)
unless [adapter_names_to_exclude].flatten.include?(adapter_name)
@@ -115,7 +114,7 @@ ActiveRecord::Schema.define do
create_table :bulbs, force: true do |t|
t.integer :car_id
t.string :name
- t.boolean :frickinawesome
+ t.boolean :frickinawesome, default: false
t.string :color
end
@@ -131,6 +130,8 @@ ActiveRecord::Schema.define do
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
@@ -206,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
@@ -237,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
@@ -244,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|
@@ -332,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
@@ -347,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|
@@ -433,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
@@ -497,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
@@ -515,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|
@@ -561,15 +608,24 @@ ActiveRecord::Schema.define do
create_table :pets, primary_key: :pet_id, force: true do |t|
t.string :name
t.integer :owner_id, :integer
- t.timestamps null: false
+ 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|
@@ -620,6 +676,8 @@ 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|
@@ -666,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
@@ -676,13 +737,22 @@ ActiveRecord::Schema.define do
create_table :ship_parts, force: true do |t|
t.string :name
t.integer :ship_id
- t.datetime :updated_at
+ 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|
t.string :speedometer_id
t.string :name
@@ -741,7 +811,7 @@ ActiveRecord::Schema.define do
t.string :title, limit: 250
t.string :author_name
t.string :author_email_address
- if mysql_56?
+ if subsecond_precision_supported?
t.datetime :written_on, precision: 6
else
t.datetime :written_on
@@ -764,7 +834,11 @@ ActiveRecord::Schema.define do
t.string :parent_title
t.string :type
t.string :group
- t.timestamps null: true
+ 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|
@@ -789,6 +863,11 @@ ActiveRecord::Schema.define do
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|
t.integer :car_id
end
@@ -929,6 +1008,10 @@ ActiveRecord::Schema.define do
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|
@@ -939,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 2d1651454d..666c1b6a14 100644
--- a/activerecord/test/support/schema_dumping_helper.rb
+++ b/activerecord/test/support/schema_dumping_helper.rb
@@ -1,7 +1,7 @@
module SchemaDumpingHelper
def dump_table_schema(table, connection = ActiveRecord::Base.connection)
old_ignore_tables = ActiveRecord::SchemaDumper.ignore_tables
- ActiveRecord::SchemaDumper.ignore_tables = connection.tables - [table]
+ ActiveRecord::SchemaDumper.ignore_tables = connection.data_sources - [table]
stream = StringIO.new
ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream)
stream.string