aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord
diff options
context:
space:
mode:
Diffstat (limited to 'activerecord')
-rw-r--r--activerecord/CHANGELOG.md1377
-rw-r--r--activerecord/MIT-LICENSE2
-rw-r--r--activerecord/RUNNING_UNIT_TESTS.rdoc1
-rw-r--r--activerecord/Rakefile21
-rw-r--r--activerecord/activerecord.gemspec3
-rw-r--r--activerecord/examples/performance.rb48
-rw-r--r--activerecord/examples/simple.rb8
-rw-r--r--activerecord/lib/active_record.rb10
-rw-r--r--activerecord/lib/active_record/aggregations.rb3
-rw-r--r--activerecord/lib/active_record/associations.rb67
-rw-r--r--activerecord/lib/active_record/associations/association.rb39
-rw-r--r--activerecord/lib/active_record/associations/association_scope.rb30
-rw-r--r--activerecord/lib/active_record/associations/belongs_to_association.rb59
-rw-r--r--activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb7
-rw-r--r--activerecord/lib/active_record/associations/builder/association.rb123
-rw-r--r--activerecord/lib/active_record/associations/builder/belongs_to.rb144
-rw-r--r--activerecord/lib/active_record/associations/builder/collection_association.rb92
-rw-r--r--activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb132
-rw-r--r--activerecord/lib/active_record/associations/builder/has_many.rb6
-rw-r--r--activerecord/lib/active_record/associations/builder/has_one.rb12
-rw-r--r--activerecord/lib/active_record/associations/builder/singular_association.rb15
-rw-r--r--activerecord/lib/active_record/associations/collection_association.rb169
-rw-r--r--activerecord/lib/active_record/associations/collection_proxy.rb31
-rw-r--r--activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb65
-rw-r--r--activerecord/lib/active_record/associations/has_many_association.rb16
-rw-r--r--activerecord/lib/active_record/associations/has_many_through_association.rb18
-rw-r--r--activerecord/lib/active_record/associations/has_one_association.rb12
-rw-r--r--activerecord/lib/active_record/associations/join_dependency.rb374
-rw-r--r--activerecord/lib/active_record/associations/join_dependency/join_association.rb148
-rw-r--r--activerecord/lib/active_record/associations/join_dependency/join_base.rb14
-rw-r--r--activerecord/lib/active_record/associations/join_dependency/join_part.rb62
-rw-r--r--activerecord/lib/active_record/associations/join_helper.rb13
-rw-r--r--activerecord/lib/active_record/associations/preloader.rb119
-rw-r--r--activerecord/lib/active_record/associations/preloader/association.rb66
-rw-r--r--activerecord/lib/active_record/associations/preloader/collection_association.rb4
-rw-r--r--activerecord/lib/active_record/associations/preloader/has_and_belongs_to_many.rb60
-rw-r--r--activerecord/lib/active_record/associations/preloader/has_many_through.rb2
-rw-r--r--activerecord/lib/active_record/associations/preloader/singular_association.rb6
-rw-r--r--activerecord/lib/active_record/associations/preloader/through_association.rb84
-rw-r--r--activerecord/lib/active_record/associations/singular_association.rb5
-rw-r--r--activerecord/lib/active_record/associations/through_association.rb4
-rw-r--r--activerecord/lib/active_record/attribute_assignment.rb4
-rw-r--r--activerecord/lib/active_record/attribute_methods.rb119
-rw-r--r--activerecord/lib/active_record/attribute_methods/dirty.rb26
-rw-r--r--activerecord/lib/active_record/attribute_methods/read.rb95
-rw-r--r--activerecord/lib/active_record/attribute_methods/serialization.rb28
-rw-r--r--activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb2
-rw-r--r--activerecord/lib/active_record/attribute_methods/write.rb49
-rw-r--r--activerecord/lib/active_record/autosave_association.rb409
-rw-r--r--activerecord/lib/active_record/base.rb6
-rw-r--r--activerecord/lib/active_record/callbacks.rb9
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb9
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb33
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb25
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/quoting.rb6
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/savepoints.rb21
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb84
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb12
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb123
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_adapter.rb124
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb108
-rw-r--r--activerecord/lib/active_record/connection_adapters/column.rb73
-rw-r--r--activerecord/lib/active_record/connection_adapters/connection_specification.rb245
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb23
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql_adapter.rb49
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb37
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/cast.rb24
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb64
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid.rb33
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb31
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb9
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb160
-rw-r--r--activerecord/lib/active_record/connection_adapters/schema_cache.rb37
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb107
-rw-r--r--activerecord/lib/active_record/connection_handling.rb67
-rw-r--r--activerecord/lib/active_record/core.rb90
-rw-r--r--activerecord/lib/active_record/counter_cache.rb14
-rw-r--r--activerecord/lib/active_record/dynamic_matchers.rb11
-rw-r--r--activerecord/lib/active_record/enum.rb116
-rw-r--r--activerecord/lib/active_record/errors.rb18
-rw-r--r--activerecord/lib/active_record/fixture_set/file.rb3
-rw-r--r--activerecord/lib/active_record/fixtures.rb225
-rw-r--r--activerecord/lib/active_record/inheritance.rb13
-rw-r--r--activerecord/lib/active_record/integration.rb55
-rw-r--r--activerecord/lib/active_record/locking/optimistic.rb9
-rw-r--r--activerecord/lib/active_record/locking/pessimistic.rb8
-rw-r--r--activerecord/lib/active_record/log_subscriber.rb17
-rw-r--r--activerecord/lib/active_record/migration.rb151
-rw-r--r--activerecord/lib/active_record/migration/command_recorder.rb20
-rw-r--r--activerecord/lib/active_record/model_schema.rb65
-rw-r--r--activerecord/lib/active_record/nested_attributes.rb23
-rw-r--r--activerecord/lib/active_record/no_touching.rb52
-rw-r--r--activerecord/lib/active_record/null_relation.rb18
-rw-r--r--activerecord/lib/active_record/persistence.rb70
-rw-r--r--activerecord/lib/active_record/querying.rb19
-rw-r--r--activerecord/lib/active_record/railtie.rb79
-rw-r--r--activerecord/lib/active_record/railties/databases.rake71
-rw-r--r--activerecord/lib/active_record/readonly_attributes.rb6
-rw-r--r--activerecord/lib/active_record/reflection.rb251
-rw-r--r--activerecord/lib/active_record/relation.rb183
-rw-r--r--activerecord/lib/active_record/relation/batches.rb77
-rw-r--r--activerecord/lib/active_record/relation/calculations.rb86
-rw-r--r--activerecord/lib/active_record/relation/delegation.rb93
-rw-r--r--activerecord/lib/active_record/relation/finder_methods.rb161
-rw-r--r--activerecord/lib/active_record/relation/merger.rb36
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder.rb99
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder/array_handler.rb29
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder/relation_handler.rb13
-rw-r--r--activerecord/lib/active_record/relation/query_methods.rb221
-rw-r--r--activerecord/lib/active_record/relation/spawn_methods.rb3
-rw-r--r--activerecord/lib/active_record/result.rb67
-rw-r--r--activerecord/lib/active_record/runtime_registry.rb5
-rw-r--r--activerecord/lib/active_record/sanitization.rb28
-rw-r--r--activerecord/lib/active_record/schema_dumper.rb22
-rw-r--r--activerecord/lib/active_record/schema_migration.rb42
-rw-r--r--activerecord/lib/active_record/scoping/default.rb19
-rw-r--r--activerecord/lib/active_record/scoping/named.rb37
-rw-r--r--activerecord/lib/active_record/store.rb68
-rw-r--r--activerecord/lib/active_record/tasks/database_tasks.rb73
-rw-r--r--activerecord/lib/active_record/tasks/firebird_database_tasks.rb56
-rw-r--r--activerecord/lib/active_record/tasks/mysql_database_tasks.rb3
-rw-r--r--activerecord/lib/active_record/tasks/oracle_database_tasks.rb45
-rw-r--r--activerecord/lib/active_record/tasks/postgresql_database_tasks.rb2
-rw-r--r--activerecord/lib/active_record/tasks/sqlite_database_tasks.rb2
-rw-r--r--activerecord/lib/active_record/tasks/sqlserver_database_tasks.rb48
-rw-r--r--activerecord/lib/active_record/test_case.rb96
-rw-r--r--activerecord/lib/active_record/timestamp.rb8
-rw-r--r--activerecord/lib/active_record/transactions.rb20
-rw-r--r--activerecord/lib/active_record/validations/associated.rb2
-rw-r--r--activerecord/lib/active_record/validations/presence.rb2
-rw-r--r--activerecord/lib/active_record/validations/uniqueness.rb29
-rw-r--r--activerecord/lib/active_record/version.rb2
-rw-r--r--activerecord/lib/rails/generators/active_record.rb10
-rw-r--r--activerecord/lib/rails/generators/active_record/migration.rb18
-rw-r--r--activerecord/test/cases/adapter_test.rb41
-rw-r--r--activerecord/test/cases/adapters/firebird/connection_test.rb8
-rw-r--r--activerecord/test/cases/adapters/firebird/default_test.rb16
-rw-r--r--activerecord/test/cases/adapters/firebird/migration_test.rb124
-rw-r--r--activerecord/test/cases/adapters/mysql/connection_test.rb29
-rw-r--r--activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb26
-rw-r--r--activerecord/test/cases/adapters/mysql/reserved_word_test.rb2
-rw-r--r--activerecord/test/cases/adapters/mysql/statement_pool_test.rb24
-rw-r--r--activerecord/test/cases/adapters/mysql2/boolean_test.rb91
-rw-r--r--activerecord/test/cases/adapters/mysql2/connection_test.rb27
-rw-r--r--activerecord/test/cases/adapters/mysql2/explain_test.rb6
-rw-r--r--activerecord/test/cases/adapters/mysql2/reserved_word_test.rb2
-rw-r--r--activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb25
-rw-r--r--activerecord/test/cases/adapters/mysql2/schema_test.rb9
-rw-r--r--activerecord/test/cases/adapters/oracle/synonym_test.rb17
-rw-r--r--activerecord/test/cases/adapters/postgresql/active_schema_test.rb6
-rw-r--r--activerecord/test/cases/adapters/postgresql/array_test.rb89
-rw-r--r--activerecord/test/cases/adapters/postgresql/bytea_test.rb23
-rw-r--r--activerecord/test/cases/adapters/postgresql/connection_test.rb72
-rw-r--r--activerecord/test/cases/adapters/postgresql/datatype_test.rb411
-rw-r--r--activerecord/test/cases/adapters/postgresql/hstore_test.rb311
-rw-r--r--activerecord/test/cases/adapters/postgresql/json_test.rb34
-rw-r--r--activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb66
-rw-r--r--activerecord/test/cases/adapters/postgresql/quoting_test.rb7
-rw-r--r--activerecord/test/cases/adapters/postgresql/range_test.rb245
-rw-r--r--activerecord/test/cases/adapters/postgresql/statement_pool_test.rb56
-rw-r--r--activerecord/test/cases/adapters/postgresql/timestamp_test.rb12
-rw-r--r--activerecord/test/cases/adapters/postgresql/uuid_test.rb113
-rw-r--r--activerecord/test/cases/adapters/postgresql/xml_test.rb38
-rw-r--r--activerecord/test/cases/adapters/sqlite3/quoting_test.rb7
-rw-r--r--activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb63
-rw-r--r--activerecord/test/cases/adapters/sqlite3/sqlite3_create_folder_test.rb21
-rw-r--r--activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb23
-rw-r--r--activerecord/test/cases/associations/belongs_to_associations_test.rb82
-rw-r--r--activerecord/test/cases/associations/cascaded_eager_loading_test.rb21
-rw-r--r--activerecord/test/cases/associations/eager_test.rb49
-rw-r--r--activerecord/test/cases/associations/extension_test.rb13
-rw-r--r--activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb126
-rw-r--r--activerecord/test/cases/associations/has_many_associations_test.rb370
-rw-r--r--activerecord/test/cases/associations/has_many_through_associations_test.rb195
-rw-r--r--activerecord/test/cases/associations/has_one_associations_test.rb55
-rw-r--r--activerecord/test/cases/associations/has_one_through_associations_test.rb11
-rw-r--r--activerecord/test/cases/associations/inner_join_association_test.rb23
-rw-r--r--activerecord/test/cases/associations/inverse_associations_test.rb53
-rw-r--r--activerecord/test/cases/associations/join_dependency_test.rb8
-rw-r--r--activerecord/test/cases/associations/nested_through_associations_test.rb13
-rw-r--r--activerecord/test/cases/associations_test.rb4
-rw-r--r--activerecord/test/cases/attribute_methods/read_test.rb11
-rw-r--r--activerecord/test/cases/attribute_methods/serialization_test.rb29
-rw-r--r--activerecord/test/cases/attribute_methods_test.rb60
-rw-r--r--activerecord/test/cases/autosave_association_test.rb11
-rw-r--r--activerecord/test/cases/base_test.rb292
-rw-r--r--activerecord/test/cases/batches_test.rb28
-rw-r--r--activerecord/test/cases/bind_parameter_test.rb77
-rw-r--r--activerecord/test/cases/calculations_test.rb40
-rw-r--r--activerecord/test/cases/column_test.rb8
-rw-r--r--activerecord/test/cases/connection_adapters/connection_handler_test.rb127
-rw-r--r--activerecord/test/cases/connection_management_test.rb32
-rw-r--r--activerecord/test/cases/connection_pool_test.rb3
-rw-r--r--activerecord/test/cases/connection_specification/resolver_test.rb76
-rw-r--r--activerecord/test/cases/core_test.rb33
-rw-r--r--activerecord/test/cases/counter_cache_test.rb17
-rw-r--r--activerecord/test/cases/date_time_test.rb2
-rw-r--r--activerecord/test/cases/defaults_test.rb25
-rw-r--r--activerecord/test/cases/deprecated_dynamic_methods_test.rb592
-rw-r--r--activerecord/test/cases/dirty_test.rb64
-rw-r--r--activerecord/test/cases/disconnected_test.rb28
-rw-r--r--activerecord/test/cases/dup_test.rb2
-rw-r--r--activerecord/test/cases/enum_test.rb91
-rw-r--r--activerecord/test/cases/finder_respond_to_test.rb41
-rw-r--r--activerecord/test/cases/finder_test.rb271
-rw-r--r--activerecord/test/cases/fixture_set/file_test.rb55
-rw-r--r--activerecord/test/cases/fixtures_test.rb100
-rw-r--r--activerecord/test/cases/forbidden_attributes_protection_test.rb5
-rw-r--r--activerecord/test/cases/helper.rb93
-rw-r--r--activerecord/test/cases/inheritance_test.rb10
-rw-r--r--activerecord/test/cases/integration_test.rb67
-rw-r--r--activerecord/test/cases/invalid_connection_test.rb22
-rw-r--r--activerecord/test/cases/locking_test.rb20
-rw-r--r--activerecord/test/cases/log_subscriber_test.rb19
-rw-r--r--activerecord/test/cases/migration/change_schema_test.rb63
-rw-r--r--activerecord/test/cases/migration/change_table_test.rb2
-rw-r--r--activerecord/test/cases/migration/column_attributes_test.rb91
-rw-r--r--activerecord/test/cases/migration/column_positioning_test.rb47
-rw-r--r--activerecord/test/cases/migration/command_recorder_test.rb13
-rw-r--r--activerecord/test/cases/migration/index_test.rb66
-rw-r--r--activerecord/test/cases/migration/references_index_test.rb28
-rw-r--r--activerecord/test/cases/migration/rename_table_test.rb48
-rw-r--r--activerecord/test/cases/migration_test.rb357
-rw-r--r--activerecord/test/cases/migrator_test.rb6
-rw-r--r--activerecord/test/cases/mixin_test.rb44
-rw-r--r--activerecord/test/cases/modules_test.rb1
-rw-r--r--activerecord/test/cases/multiparameter_attributes_test.rb168
-rw-r--r--activerecord/test/cases/multiple_db_test.rb2
-rw-r--r--activerecord/test/cases/nested_attributes_test.rb20
-rw-r--r--activerecord/test/cases/nested_attributes_with_callbacks_test.rb144
-rw-r--r--activerecord/test/cases/persistence_test.rb42
-rw-r--r--activerecord/test/cases/pooled_connections_test.rb16
-rw-r--r--activerecord/test/cases/primary_keys_test.rb19
-rw-r--r--activerecord/test/cases/query_cache_test.rb9
-rw-r--r--activerecord/test/cases/quoting_test.rb69
-rw-r--r--activerecord/test/cases/readonly_test.rb18
-rw-r--r--activerecord/test/cases/reflection_test.rb29
-rw-r--r--activerecord/test/cases/relation/delegation_test.rb68
-rw-r--r--activerecord/test/cases/relation/merging_test.rb162
-rw-r--r--activerecord/test/cases/relation/mutation_test.rb144
-rw-r--r--activerecord/test/cases/relation/predicate_builder_test.rb14
-rw-r--r--activerecord/test/cases/relation/where_chain_test.rb36
-rw-r--r--activerecord/test/cases/relation/where_test.rb63
-rw-r--r--activerecord/test/cases/relation_test.rb165
-rw-r--r--activerecord/test/cases/relations_test.rb246
-rw-r--r--activerecord/test/cases/result_test.rb32
-rw-r--r--activerecord/test/cases/sanitize_test.rb17
-rw-r--r--activerecord/test/cases/schema_dumper_test.rb30
-rw-r--r--activerecord/test/cases/scoping/default_scoping_test.rb84
-rw-r--r--activerecord/test/cases/scoping/named_scoping_test.rb20
-rw-r--r--activerecord/test/cases/serialized_attribute_test.rb28
-rw-r--r--activerecord/test/cases/store_test.rb17
-rw-r--r--activerecord/test/cases/tasks/database_tasks_test.rb29
-rw-r--r--activerecord/test/cases/tasks/firebird_rake_test.rb100
-rw-r--r--activerecord/test/cases/tasks/mysql_rake_test.rb158
-rw-r--r--activerecord/test/cases/tasks/oracle_rake_test.rb93
-rw-r--r--activerecord/test/cases/tasks/postgresql_rake_test.rb9
-rw-r--r--activerecord/test/cases/tasks/sqlite_rake_test.rb6
-rw-r--r--activerecord/test/cases/tasks/sqlserver_rake_test.rb87
-rw-r--r--activerecord/test/cases/test_case.rb111
-rw-r--r--activerecord/test/cases/timestamp_test.rb118
-rw-r--r--activerecord/test/cases/transaction_callbacks_test.rb8
-rw-r--r--activerecord/test/cases/transaction_isolation_test.rb152
-rw-r--r--activerecord/test/cases/transactions_test.rb84
-rw-r--r--activerecord/test/cases/validations/association_validation_test.rb71
-rw-r--r--activerecord/test/cases/validations/i18n_generate_message_validation_test.rb24
-rw-r--r--activerecord/test/cases/validations/presence_validation_test.rb16
-rw-r--r--activerecord/test/cases/validations/uniqueness_validation_test.rb43
-rw-r--r--activerecord/test/cases/validations_test.rb10
-rw-r--r--activerecord/test/cases/xml_serialization_test.rb20
-rw-r--r--activerecord/test/cases/yaml_serialization_test.rb18
l---------activerecord/test/fixtures/all/admin1
-rw-r--r--activerecord/test/fixtures/owners.yml1
-rw-r--r--activerecord/test/fixtures/sponsors.yml2
-rw-r--r--activerecord/test/fixtures/tasks.yml2
-rw-r--r--activerecord/test/fixtures/to_be_linked/accounts.yml2
-rw-r--r--activerecord/test/fixtures/to_be_linked/users.yml10
-rw-r--r--activerecord/test/migrations/version_check/20131219224947_migration_version_check.rb8
-rw-r--r--activerecord/test/models/admin/user.rb1
-rw-r--r--activerecord/test/models/author.rb18
-rw-r--r--activerecord/test/models/auto_id.rb4
-rw-r--r--activerecord/test/models/book.rb12
-rw-r--r--activerecord/test/models/bulb.rb12
-rw-r--r--activerecord/test/models/cake_designer.rb3
-rw-r--r--activerecord/test/models/car.rb6
-rw-r--r--activerecord/test/models/chef.rb3
-rw-r--r--activerecord/test/models/citation.rb3
-rw-r--r--activerecord/test/models/club.rb3
-rw-r--r--activerecord/test/models/column_name.rb4
-rw-r--r--activerecord/test/models/company.rb45
-rw-r--r--activerecord/test/models/company_in_module.rb4
-rw-r--r--activerecord/test/models/contract.rb1
-rw-r--r--activerecord/test/models/department.rb4
-rw-r--r--activerecord/test/models/developer.rb12
-rw-r--r--activerecord/test/models/drink_designer.rb3
-rw-r--r--activerecord/test/models/hotel.rb6
-rw-r--r--activerecord/test/models/interest.rb2
-rw-r--r--activerecord/test/models/man.rb3
-rw-r--r--activerecord/test/models/member.rb3
-rw-r--r--activerecord/test/models/member_detail.rb2
-rw-r--r--activerecord/test/models/membership.rb5
-rw-r--r--activerecord/test/models/mixed_case_monkey.rb2
-rw-r--r--activerecord/test/models/movie.rb4
-rw-r--r--activerecord/test/models/parrot.rb6
-rw-r--r--activerecord/test/models/person.rb1
-rw-r--r--activerecord/test/models/post.rb12
-rw-r--r--activerecord/test/models/project.rb14
-rw-r--r--activerecord/test/models/topic.rb5
-rw-r--r--activerecord/test/schema/oracle_specific_schema.rb3
-rw-r--r--activerecord/test/schema/postgresql_specific_schema.rb15
-rw-r--r--activerecord/test/schema/schema.rb453
-rw-r--r--activerecord/test/schema/sqlite_specific_schema.rb7
-rw-r--r--activerecord/test/support/connection.rb4
313 files changed, 11040 insertions, 7505 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md
index 19d2195bca..d9f8ee7097 100644
--- a/activerecord/CHANGELOG.md
+++ b/activerecord/CHANGELOG.md
@@ -1,3 +1,1374 @@
+* Currently Active Record can be configured via the environment variable
+ `DATABASE_URL` or by manually injecting a hash of values which is what Rails does,
+ reading in `database.yml` and setting Active Record appropriately. Active Record
+ expects to be able to use `DATABASE_URL` without the use of Rails, and we cannot
+ rip out this functionality without deprecating. This presents a problem though
+ when both config is set, and a `DATABASE_URL` is present. Currently the
+ `DATABASE_URL` should "win" and none of the values in `database.yml` are
+ used. This is somewhat unexpected, if one were to set values such as
+ `pool` in the `production:` group of `database.yml` they are ignored.
+
+ There are many ways that Active Record initiates a connection today:
+
+ - Stand Alone (without rails)
+ - `rake db:<tasks>`
+ - `ActiveRecord.establish_connection`
+
+ - With Rails
+ - `rake db:<tasks>`
+ - `rails <server> | <console>`
+ - `rails dbconsole`
+
+ Now all of these behave exactly the same way. The best way to do
+ this is to put all of this logic in one place so it is guaranteed to be used.
+
+ Here is the matrix of how this behavior works:
+
+ ```
+ No database.yml
+ No DATABASE_URL
+ => Error
+ ```
+
+ ```
+ database.yml present
+ No DATABASE_URL
+ => Use database.yml configuration
+ ```
+
+ ```
+ No database.yml
+ DATABASE_URL present
+ => use DATABASE_URL configuration
+ ```
+
+ ```
+ database.yml present
+ DATABASE_URL present
+ => Merged into `url` sub key. If both specify `url` sub key, the `database.yml` `url`
+ sub key "wins". If other paramaters `adapter` or `database` are specified in YAML,
+ they are discarded as the `url` sub key "wins".
+ ```
+
+ Current implementation uses `ActiveRecord::Base.configurations` to resolve and merge
+ all connection information before returning. This is achieved through a utility
+ class: `ActiveRecord::ConnectionHandling::MergeAndResolveDefaultUrlConfig`.
+
+ To understand the exact behavior of this class, it is best to review the
+ behavior in `activerecord/test/cases/connection_adapters/connection_handler_test.rb`
+
+ *Richard Schneeman*
+
+* Make `change_column_null` revertable. Fixes #13576.
+
+ *Yves Senn*, *Nishant Modak*, *Prathamesh Sonpatki*
+
+* Don't create/drop the test database if RAILS_ENV is specified explicitly.
+
+ Previously, when the environment was development, we would always
+ create or drop both the test and development databases.
+
+ Now, if RAILS_ENV is explicitly defined as development, we don't create
+ the test database.
+
+ *Damien Mathieu*
+
+* Initialize version on Migration objects so that it can be used in a migration,
+ and it will be included in the announce message.
+
+ *Dylan Thacker-Smith*
+
+* `change_table` now uses the current adapter's `update_table_definition`
+ method to retrieve a specific table definition.
+ This ensures that `change_table` and `create_table` will use
+ similar objects.
+
+ Fixes #13577 and #13503.
+
+ *Nishant Modak*, *Prathamesh Sonpatki*, *Rafael Mendonça França*
+
+* Fixed ActiveRecord::Store nil conversion TypeError when using YAML coder.
+ In case the YAML passed as paramter is nil, uses an empty string.
+
+ Fixes #13570.
+
+ *Thales Oliveira*
+
+* Deprecate unused `ActiveRecord::Base.symbolized_base_class`
+ and `ActiveRecord::Base.symbolized_sti_name` without replacement.
+
+ *Yves Senn*
+
+* Since the `test_help.rb` file in Railties now automatically maintains
+ your test schema, the `rake db:test:*` tasks are deprecated. This
+ doesn't stop you manually running other tasks on your test database
+ if needed:
+
+ rake db:schema:load RAILS_ENV=test
+
+ *Jon Leighton*
+
+* Fix presence validator for association when the associated record responds to `to_a`.
+
+ *gmarik*
+
+* Fixed regression on preload/includes with multiple arguments failing in certain conditions,
+ raising a NoMethodError internally by calling `reflect_on_association` for `NilClass:Class`.
+
+ Fixes #13437.
+
+ *Vipul A M*, *khustochka*
+
+* Add the ability to nullify the `enum` column.
+
+ Example:
+
+ class Conversation < ActiveRecord::Base
+ enum gender: [:female, :male]
+ end
+
+ Conversation::GENDER # => { female: 0, male: 1 }
+
+ # conversation.update! gender: 0
+ conversation.female!
+ conversation.female? # => true
+ conversation.gender # => "female"
+
+ # conversation.update! gender: nil
+ conversation.gender = nil
+ conversation.gender.nil? # => true
+ conversation.gender # => nil
+
+ *Amr Tamimi*
+
+* Connection specification now accepts a "url" key. The value of this
+ key is expected to contain a database URL. The database URL will be
+ expanded into a hash and merged.
+
+ *Richard Schneeman*
+
+* An `ArgumentError` is now raised on a call to `Relation#where.not(nil)`.
+
+ Example:
+
+ User.where.not(nil)
+
+ # Before
+ # => 'SELECT `users`.* FROM `users` WHERE (NOT (NULL))'
+
+ # After
+ # => ArgumentError, 'Invalid argument for .where.not(), got nil.'
+
+ *Kuldeep Aggarwal*
+
+* Deprecated use of string argument as a configuration lookup in
+ `ActiveRecord::Base.establish_connection`. Instead, a symbol must be given.
+
+ *José Valim*
+
+* Fixed `update_column`, `update_columns`, and `update_all` to correctly serialize
+ values for `array`, `hstore` and `json` column types in PostgreSQL.
+
+ Fixes #12261.
+
+ *Tadas Tamosauskas*, *Carlos Antonio da Silva*
+
+* Do not consider PostgreSQL array columns as number or text columns.
+
+ The code uses these checks in several places to know what to do with a
+ particular column, for instance AR attribute query methods has a branch
+ like this:
+
+ if column.number?
+ !value.zero?
+ end
+
+ This should never be true for array columns, since it would be the same
+ as running [].zero?, which results in a NoMethodError exception.
+
+ Fixing this by ensuring that array columns in PostgreSQL never return
+ true for number?/text? checks.
+
+ *Carlos Antonio da Silva*
+
+* When connecting to a non-existant database, the error:
+ `ActiveRecord::NoDatabaseError` will now be raised. When being used with Rails
+ the error message will include information on how to create a database:
+ `rake db:create`. Supported adapters: postgresql, mysql, mysql2, sqlite3
+
+ *Richard Schneeman*
+
+* Do not raise `'cannot touch on a new record object'` exception on destroying
+ already destroyed `belongs_to` association with `touch: true` option.
+
+ Fixes #13445.
+
+ Example:
+
+ # Given Comment has belongs_to :post, touch: true
+ comment.post.destroy
+ comment.destroy # no longer raises an error
+
+ *Paul Nikitochkin*
+
+* Fix a bug when assigning an array containing string numbers to a
+ PostgreSQL integer array column.
+
+ Fixes #13444.
+
+ Example:
+
+ # Given Book#ratings is of type :integer, array: true
+ Book.new(ratings: [1, 2]) # worked before
+ Book.new(ratings: ['1', '2']) # now works as well
+
+ *Damien Mathieu*
+
+* Improve the default select when `from` is used.
+
+ Previously, if you did something like Topic.from(:temp_topics), it
+ would generate SQL like:
+
+ SELECT topics.* FROM temp_topics;
+
+ Which is will cause an error since there's not a topics table to select
+ from.
+
+ Now the default if you use from is just `*`:
+
+ SELECT * FROM temp_topics;
+
+ *Cody Cutrer*
+
+* Fix `PostgreSQL` insert to properly extract table name from multiline string SQL.
+
+ Previously, executing an insert SQL in `PostgreSQL` with a command like this:
+
+ insert into articles(
+ number)
+ values(
+ 5152
+ )
+
+ would not work because the adapter was unable to extract the correct `articles`
+ table name.
+
+ *Kuldeep Aggarwal*
+
+* `Relation` no longer has mutator methods like `#map!` and `#delete_if`. Convert
+ to an `Array` by calling `#to_a` before using these methods.
+
+ It intends to prevent odd bugs and confusion in code that call mutator
+ methods directly on the `Relation`.
+
+ Example:
+
+ # Instead of this
+ Author.where(name: 'Hank Moody').compact!
+
+ # Now you have to do this
+ authors = Author.where(name: 'Hank Moody').to_a
+ authors.compact!
+
+ *Lauro Caetano*
+
+* Better support for `where()` conditions that use a `belongs_to`
+ association name.
+
+ Using the name of an association in `where` previously worked only
+ if the value was a single `ActiveRecord::Base` object. e.g.
+
+ Post.where(author: Author.first)
+
+ Any other values, including `nil`, would cause invalid SQL to be
+ generated. This change supports arguments in the `where` query
+ conditions where the key is a `belongs_to` association name and the
+ value is `nil`, an `Array` of `ActiveRecord::Base` objects, or an
+ `ActiveRecord::Relation` object.
+
+ class Post < ActiveRecord::Base
+ belongs_to :author
+ end
+
+ `nil` value finds records where the association is not set:
+
+ Post.where(author: nil)
+ # SELECT "posts".* FROM "posts" WHERE "posts"."author_id" IS NULL
+
+ `Array` values find records where the association foreign key
+ matches the ids of the passed ActiveRecord models, resulting
+ in the same query as `Post.where(author_id: [1,2])`:
+
+ authors_array = [Author.find(1), Author.find(2)]
+ Post.where(author: authors_array)
+ # SELECT "posts".* FROM "posts" WHERE "posts"."author_id" IN (1, 2)
+
+ `ActiveRecord::Relation` values find records using the same
+ query as `Post.where(author_id: Author.where(last_name: "Emde"))`
+
+ Post.where(author: Author.where(last_name: "Emde"))
+ # SELECT "posts".* FROM "posts"
+ # WHERE "posts"."author_id" IN (
+ # SELECT "authors"."id" FROM "authors"
+ # WHERE "authors"."last_name" = 'Emde')
+
+ Polymorphic `belongs_to` associations will continue to be handled
+ appropriately, with the polymorphic `association_type` field added
+ to the query to match the base class of the value. This feature
+ previously only worked when the value was a single `ActveRecord::Base`.
+
+ class Post < ActiveRecord::Base
+ belongs_to :author, polymorphic: true
+ end
+
+ Post.where(author: Author.where(last_name: "Emde"))
+ # Generates a query similar to:
+ Post.where(author_id: Author.where(last_name: "Emde"), author_type: "Author")
+
+ *Martin Emde*
+
+* Respect temporary option when dropping tables with MySQL.
+
+ Normal DROP TABLE also works, but commits the transaction.
+
+ drop_table :temporary_table, temporary: true
+
+ *Cody Cutrer*
+
+* Add option to create tables from a query.
+
+ create_table(:long_query, temporary: true,
+ as: "SELECT * FROM orders INNER JOIN line_items ON order_id=orders.id")
+
+ Generates:
+
+ CREATE TEMPORARY TABLE long_query AS
+ SELECT * FROM orders INNER JOIN line_items ON order_id=orders.id
+
+ *Cody Cutrer*
+
+* `db:test:clone` and `db:test:prepare` must load Rails environment.
+
+ `db:test:clone` and `db:test:prepare` use `ActiveRecord::Base`. configurations,
+ so we need to load the Rails environment, otherwise the config wont be in place.
+
+ *arthurnn*
+
+* Use the right column to type cast grouped calculations with custom expressions.
+
+ Fixes #13230.
+
+ Example:
+
+ # Before
+ Account.group(:firm_name).sum('0.01 * credit_limit')
+ # => { '37signals' => '0.5' }
+
+ # After
+ Account.group(:firm_name).sum('0.01 * credit_limit')
+ # => { '37signals' => 0.5 }
+
+ *Paul Nikitochkin*
+
+* Polymorphic `belongs_to` associations with the `touch: true` option set update the timestamps of
+ the old and new owner correctly when moved between owners of different types.
+
+ Example:
+
+ class Rating < ActiveRecord::Base
+ belongs_to :rateable, polymorphic: true, touch: true
+ end
+
+ rating = Rating.create rateable: Song.find(1)
+ rating.update_attributes rateable: Book.find(2) # => timestamps of Song(1) and Book(2) are updated
+
+ *Severin Schoepke*
+
+* Improve formatting of migration exception messages: make them easier to read
+ with line breaks before/after, and improve the error for pending migrations.
+
+ *John Bachir*
+
+* Fix `last` with `offset` to return the proper record instead of always the last one.
+
+ Example:
+
+ Model.offset(4).last
+ # => returns the 4th record from the end.
+
+ Fixes #7441.
+
+ *kostya*, *Lauro Caetano*
+
+* `type_to_sql` returns a `String` for unmapped columns. This fixes an error
+ when using unmapped array types in PG
+
+ Example:
+
+ change_colum :table, :column, :bigint, array: true
+
+ Fixes #13146.
+
+ *Jens Fahnenbruck*, *Yves Senn*
+
+* Fix `QueryCache` to work with nested blocks, so that it will only clear the existing cache
+ after leaving the outer block instead of clearing it right after the inner block is finished.
+
+ *Vipul A M*
+
+* The ERB in fixture files is no longer evaluated in the context of the main
+ object. Helper methods used by multiple fixtures should be defined on the
+ class object returned by `ActiveRecord::FixtureSet.context_class`.
+
+ *Victor Costan*
+
+* Previously, the `has_one` macro incorrectly accepted the `counter_cache`
+ option, but never actually supported it. Now it will raise an `ArgumentError`
+ when using `has_one` with `counter_cache`.
+
+ *Godfrey Chan*
+
+* Implement `rename_index` natively for MySQL >= 5.7.
+
+ *Cody Cutrer*
+
+* Fix bug when validating the uniqueness of an aliased attribute.
+
+ Fixes #12402.
+
+ *Lauro Caetano*
+
+* Update counter cache on a `has_many` relationship regardless of default scope.
+
+ Fix #12952.
+
+ *Uku Taht*
+
+* `rename_index` adds the new index before removing the old one. This allows to
+ rename indexes on columns with a foreign key and prevents the following error:
+
+ Cannot drop index 'index_engines_on_car_id': needed in a foreign key constraint
+
+ *Cody Cutrer*, *Yves Senn*
+
+* Raise `ActiveRecord::RecordNotDestroyed` when a replaced child marked with `dependent: destroy` fails to be destroyed.
+
+ Fix #12812
+
+ *Brian Thomas Storti*
+
+* Fix validation on uniqueness of empty association.
+
+ *Evgeny Li*
+
+* Make `ActiveRecord::Relation#unscope` affect relations it is merged in to.
+
+ *Jon Leighton*
+
+* Use strings to represent non-string `order_values`.
+
+ *Yves Senn*
+
+* Checks to see if the record contains the foreign key to set the inverse automatically.
+
+ *Edo Balvers*
+
+* Added `ActiveRecord::Base.to_param` for convenient "pretty" URLs derived from a model's attribute or method.
+
+ Example:
+
+ class User < ActiveRecord::Base
+ to_param :name
+ end
+
+ user = User.find_by(name: 'Fancy Pants')
+ user.id # => 123
+ user.to_param # => "123-fancy-pants"
+
+ *Javan Makhmali*
+
+* Added `ActiveRecord::Base.no_touching`, which allows ignoring touch on models.
+
+ Example:
+
+ Post.no_touching do
+ Post.first.touch
+ end
+
+ *Sam Stephenson*, *Damien Mathieu*
+
+* Prevent the counter cache from being decremented twice when destroying
+ a record on a `has_many :through` association.
+
+ Fixes #11079.
+
+ *Dmitry Dedov*
+
+* Unify boolean type casting for `MysqlAdapter` and `Mysql2Adapter`.
+ `type_cast` will return `1` for `true` and `0` for `false`.
+
+ Fixes #11119.
+
+ *Adam Williams*, *Yves Senn*
+
+* Fix bug where `has_one` association record update result in crash, when replaced with itself.
+
+ Fixes #12834.
+
+ *Denis Redozubov*, *Sergio Cambra*
+
+* Log bind variables after they are type casted. This makes it more
+ transparent what values are actually sent to the database.
+
+ irb(main):002:0> Event.find("im-no-integer")
+ # Before: ... WHERE "events"."id" = $1 LIMIT 1 [["id", "im-no-integer"]]
+ # After: ... WHERE "events"."id" = $1 LIMIT 1 [["id", 0]]
+
+ *Yves Senn*
+
+* Fix uninitialized constant `TransactionState` error when `Marshall.load` is used on an Active Record result.
+
+ Fixes #12790.
+
+ *Jason Ayre*
+
+* `.unscope` now removes conditions specified in `default_scope`.
+
+ *Jon Leighton*
+
+* Added `ActiveRecord::QueryMethods#rewhere` which will overwrite an existing, named where condition.
+
+ Examples:
+
+ 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
+
+ *DHH*
+
+* Extend `ActiveRecord::Base#cache_key` to take an optional list of timestamp attributes of which the highest will be used.
+
+ Example:
+
+ # last_reviewed_at will be used, if that's more recent than updated_at, or vice versa
+ Person.find(5).cache_key(:updated_at, :last_reviewed_at)
+
+ *DHH*
+
+* Added `ActiveRecord::Base#enum` for declaring enum attributes where the values map to integers in the database, but can be queried by name.
+
+ Example:
+
+ class Conversation < ActiveRecord::Base
+ enum status: [:active, :archived]
+ end
+
+ Conversation::STATUS # => { active: 0, archived: 1 }
+
+ # conversation.update! status: 0
+ conversation.active!
+ conversation.active? # => true
+ conversation.status # => "active"
+
+ # conversation.update! status: 1
+ conversation.archived!
+ conversation.archived? # => true
+ conversation.status # => "archived"
+
+ # conversation.update! status: 1
+ conversation.status = :archived
+
+ *DHH*
+
+* `ActiveRecord::Base#attribute_for_inspect` now truncates long arrays (more than 10 elements).
+
+ *Jan Bernacki*
+
+* Allow for the name of the `schema_migrations` table to be configured.
+
+ *Jerad Phelps*
+
+* Do not add to scope includes values from through associations.
+ Fixed bug when providing `includes` in through association scope, and fetching targets.
+
+ Example:
+
+ class Vendor < ActiveRecord::Base
+ has_many :relationships, -> { includes(:user) }
+ has_many :users, through: :relationships
+ end
+
+ vendor = Vendor.first
+
+ # Before
+
+ vendor.users.to_a # => Raises exception: not found `:user` for `User`
+
+ # After
+
+ vendor.users.to_a # => No exception is raised
+
+ Fixes #12242, #9517, #10240.
+
+ *Paul Nikitochkin*
+
+* Type cast json values on write, so that the value is consistent
+ with reading from the database.
+
+ Example:
+
+ x = JsonDataType.new tags: {"string" => "foo", :symbol => :bar}
+
+ # Before:
+ x.tags # => {"string" => "foo", :symbol => :bar}
+
+ # After:
+ x.tags # => {"string" => "foo", "symbol" => "bar"}
+
+ *Severin Schoepke*
+
+* `ActiveRecord::Store` works together with PG `hstore` columns.
+
+ Fixes #12452.
+
+ *Yves Senn*
+
+* Fix bug where `ActiveRecord::Store` used a global `Hash` to keep track of
+ all registered `stored_attributes`. Now every subclass of
+ `ActiveRecord::Base` has it's own `Hash`.
+
+ *Yves Senn*
+
+* Save `has_one` association when primary key is manually set.
+
+ Fixes #12302.
+
+ *Lauro Caetano*
+
+* Allow any version of BCrypt when using `has_secure_password`.
+
+ *Mike Perham*
+
+* Sub-query generated for `Relation` passed as array condition did not take in account
+ bind values and have invalid syntax.
+
+ Generate sub-query with inline bind values.
+
+ Fixes #12586.
+
+ *Paul Nikitochkin*
+
+* Fix a bug where rake db:structure:load crashed when the path contained
+ spaces.
+
+ *Kevin Mook*
+
+* `ActiveRecord::QueryMethods#unscope` unscopes negative equality
+
+ Allows you to call `#unscope` on a relation with negative equality
+ operators, i.e. `Arel::Nodes::NotIn` and `Arel::Nodes::NotEqual` that have
+ been generated through the use of `where.not`.
+
+ *Eric Hankins*
+
+* Raise an exception when model without primary key calls `.find_with_ids`.
+
+ *Shimpei Makimoto*
+
+* Make `Relation#empty?` use `exists?` instead of `count`.
+
+ *Szymon Nowak*
+
+* `rake db:structure:dump` no longer crashes when the port was specified as `Fixnum`.
+
+ *Kenta Okamoto*
+
+* `NullRelation#pluck` takes a list of columns
+
+ The method signature in `NullRelation` was updated to mimic that in
+ `Calculations`.
+
+ *Derek Prior*
+
+* `scope_chain` should not be mutated for other reflections.
+
+ Currently `scope_chain` uses same array for building different
+ `scope_chain` for different associations. During processing
+ these arrays are sometimes mutated and because of in-place
+ mutation the changed `scope_chain` impacts other reflections.
+
+ Fix is to dup the value before adding to the `scope_chain`.
+
+ Fixes #3882.
+
+ *Neeraj Singh*
+
+* Prevent the inversed association from being reloaded on save.
+
+ Fixes #9499.
+
+ *Dmitry Polushkin*
+
+* Generate subquery for `Relation` if it passed as array condition for `where`
+ method.
+
+ Example:
+
+ # Before
+ Blog.where('id in (?)', Blog.where(id: 1))
+ # => SELECT "blogs".* FROM "blogs" WHERE "blogs"."id" = 1
+ # => SELECT "blogs".* FROM "blogs" WHERE (id IN (1))
+
+ # After
+ Blog.where('id in (?)', Blog.where(id: 1).select(:id))
+ # => SELECT "blogs".* FROM "blogs"
+ # WHERE "blogs"."id" IN (SELECT "blogs"."id" FROM "blogs" WHERE "blogs"."id" = 1)
+
+ Fixes #12415.
+
+ *Paul Nikitochkin*
+
+* For missed association exception message
+ which is raised in `ActiveRecord::Associations::Preloader` class
+ added owner record class name in order to simplify to find problem code.
+
+ *Paul Nikitochkin*
+
+* `has_and_belongs_to_many` is now transparently implemented in terms of
+ `has_many :through`. Behavior should remain the same, if not, it is a bug.
+
+* `create_savepoint`, `rollback_to_savepoint` and `release_savepoint` accept
+ a savepoint name.
+
+ *Yves Senn*
+
+* Make `next_migration_number` accessible for third party generators.
+
+ *Yves Senn*
+
+* Objects instantiated using a null relationship will now retain the
+ attributes of the where clause.
+
+ Fixes #11676, #11675, #11376.
+
+ *Paul Nikitochkin*, *Peter Brown*, *Nthalk*
+
+* Fixed `ActiveRecord::Associations::CollectionAssociation#find`
+ when using `has_many` association with `:inverse_of` and finding an array of one element,
+ it should return an array of one element too.
+
+ *arthurnn*
+
+* Callbacks on has_many should access the in memory parent if a inverse_of is set.
+
+ *arthurnn*
+
+* `ActiveRecord::ConnectionAdapters.string_to_time` respects
+ string with timezone (e.g. Wed, 04 Sep 2013 20:30:00 JST).
+
+ Fixes #12278.
+
+ *kennyj*
+
+* Calling `update_attributes` will now throw an `ArgumentError` whenever it
+ gets a `nil` argument. More specifically, it will throw an error if the
+ argument that it gets passed does not respond to to `stringify_keys`.
+
+ Example:
+
+ @my_comment.update_attributes(nil) # => raises ArgumentError
+
+ *John Wang*
+
+* Deprecate `quoted_locking_column` method, which isn't used anywhere.
+
+ *kennyj*
+
+* Migration dump UUID default functions to schema.rb.
+
+ Fixes #10751.
+
+ *kennyj*
+
+* Fixed a bug in `ActiveRecord::Associations::CollectionAssociation#find_by_scan`
+ when using `has_many` association with `:inverse_of` option and UUID primary key.
+
+ Fixes #10450.
+
+ *kennyj*
+
+* Fix: joins association, with defined in the scope block constraints by using several
+ where constraints and at least of them is not `Arel::Nodes::Equality`,
+ generates invalid SQL expression.
+
+ Fixes #11963.
+
+ *Paul Nikitochkin*
+
+* `CollectionAssociation#first`/`#last` (e.g. `has_many`) use a `LIMIT`ed
+ query to fetch results rather than loading the entire collection.
+
+ *Lann Martin*
+
+* Make possible to run SQLite rake tasks without the `Rails` constant defined.
+
+ *Damien Mathieu*
+
+* Allow Relation#from to accept other relations with bind values.
+
+ *Ryan Wallace*
+
+* Fix inserts with prepared statements disabled.
+
+ Fixes #12023.
+
+ *Rafael Mendonça França*
+
+* Setting a has_one association on a new record no longer causes an empty
+ transaction.
+
+ *Dylan Thacker-Smith*
+
+* Fix `AR::Relation#merge` sometimes failing to preserve `readonly(false)` flag.
+
+ *thedarkone*
+
+* Re-use `order` argument pre-processing for `reorder`.
+
+ *Paul Nikitochkin*
+
+* Fix PredicateBuilder so polymorphic association keys in `where` clause can
+ accept objects other than direct descendants of `ActiveRecord::Base` (decorated
+ models, for example).
+
+ *Mikhail Dieterle*
+
+* PostgreSQL adapter recognizes negative money values formatted with
+ parentheses (eg. `($1.25) # => -1.25`)).
+ Fixes #11899.
+
+ *Yves Senn*
+
+* Stop interpreting SQL 'string' columns as :string type because there is no
+ common STRING datatype in SQL.
+
+ *Ben Woosley*
+
+* `ActiveRecord::FinderMethods#exists?` returns `true`/`false` in all cases.
+
+ *Xavier Noria*
+
+* Assign inet/cidr attribute with `nil` value for invalid address.
+
+ Example:
+
+ record = User.new
+ record.logged_in_from_ip # is type of an inet or a cidr
+
+ # Before:
+ record.logged_in_from_ip = 'bad ip address' # raise exception
+
+ # After:
+ record.logged_in_from_ip = 'bad ip address' # do not raise exception
+ record.logged_in_from_ip # => nil
+ record.logged_in_from_ip_before_type_cast # => 'bad ip address'
+
+ *Paul Nikitochkin*
+
+* `add_to_target` now accepts a second optional `skip_callbacks` argument
+
+ If truthy, it will skip the :before_add and :after_add callbacks.
+
+ *Ben Woosley*
+
+* Fix interactions between `:before_add` callbacks and nested attributes
+ assignment of `has_many` associations, when the association was not
+ yet loaded:
+
+ - A `:before_add` callback was being called when a nested attributes
+ assignment assigned to an existing record.
+
+ - Nested Attributes assignment did not affect the record in the
+ association target when a `:before_add` callback triggered the
+ loading of the association
+
+ *Jörg Schray*
+
+* Allow enable_extension migration method to be revertible.
+
+ *Eric Tipton*
+
+* Type cast hstore values on write, so that the value is consistent
+ with reading from the database.
+
+ Example:
+
+ x = Hstore.new tags: {"bool" => true, "number" => 5}
+
+ # Before:
+ x.tags # => {"bool" => true, "number" => 5}
+
+ # After:
+ x.tags # => {"bool" => "true", "number" => "5"}
+
+ *Yves Senn* , *Severin Schoepke*
+
+* Fix multidimensional PG arrays containing non-string items.
+
+ *Yves Senn*
+
+* Fixes bug when using includes combined with select, the select statement was overwritten.
+
+ Fixes #11773.
+
+ *Edo Balvers*
+
+* Load fixtures from linked folders.
+
+ *Kassio Borges*
+
+* Create a directory for sqlite3 file if not present on the system.
+
+ *Richard Schneeman*
+
+* Removed redundant override of `xml` column definition for PG,
+ in order to use `xml` column type instead of `text`.
+
+ *Paul Nikitochkin*, *Michael Nikitochkin*
+
+* Revert `ActiveRecord::Relation#order` change that make new order
+ prepend the old one.
+
+ Before:
+
+ User.order("name asc").order("created_at desc")
+ # SELECT * FROM users ORDER BY created_at desc, name asc
+
+ After:
+
+ User.order("name asc").order("created_at desc")
+ # SELECT * FROM users ORDER BY name asc, created_at desc
+
+ This also affects order defined in `default_scope` or any kind of associations.
+
+* Add ability to define how a class is converted to Arel predicates.
+ For example, adding a very vendor specific regex implementation:
+
+ regex_handler = proc do |column, value|
+ Arel::Nodes::InfixOperation.new('~', column, value.source)
+ end
+ ActiveRecord::PredicateBuilder.register_handler(Regexp, regex_handler)
+
+ *Sean Griffin & @joannecheng*
+
+* Don't allow `quote_value` to be called without a column.
+
+ Some adapters require column information to do their job properly.
+ By enforcing the provision of the column for this internal method
+ we ensure that those using adapters that require column information
+ will always get the proper behavior.
+
+ *Ben Woosley*
+
+* When using optimistic locking, `update` was not passing the column to `quote_value`
+ to allow the connection adapter to properly determine how to quote the value. This was
+ affecting certain databases that use specific column types.
+
+ Fixes #6763.
+
+ *Alfred Wong*
+
+* rescue from all exceptions in `ConnectionManagement#call`
+
+ Fixes #11497.
+
+ As `ActiveRecord::ConnectionAdapters::ConnectionManagement` middleware does
+ not rescue from Exception (but only from StandardError), the Connection
+ Pool quickly runs out of connections when multiple erroneous Requests come
+ in right after each other.
+
+ Rescuing from all exceptions and not just StandardError, fixes this
+ behaviour.
+
+ *Vipul A M*
+
+* `change_column` for PostgreSQL adapter respects the `:array` option.
+
+ *Yves Senn*
+
+* Remove deprecation warning from `attribute_missing` for attributes that are columns.
+
+ *Arun Agrawal*
+
+* Remove extra decrement of transaction deep level.
+
+ Fixes #4566.
+
+ *Paul Nikitochkin*
+
+* Reset @column_defaults when assigning `locking_column`.
+ We had a potential problem. For example:
+
+ class Post < ActiveRecord::Base
+ self.column_defaults # if we call this unintentionally before setting locking_column ...
+ self.locking_column = 'my_locking_column'
+ end
+
+ Post.column_defaults["my_locking_column"]
+ => nil # expected value is 0 !
+
+ *kennyj*
+
+* Remove extra select and update queries on save/touch/destroy ActiveRecord model
+ with belongs to reflection with option `touch: true`.
+
+ Fixes #11288.
+
+ *Paul Nikitochkin*
+
+* Remove deprecated nil-passing to the following `SchemaCache` methods:
+ `primary_keys`, `tables`, `columns` and `columns_hash`.
+
+ *Yves Senn*
+
+* Remove deprecated block filter from `ActiveRecord::Migrator#migrate`.
+
+ *Yves Senn*
+
+* Remove deprecated String constructor from `ActiveRecord::Migrator`.
+
+ *Yves Senn*
+
+* Remove deprecated `scope` use without passing a callable object.
+
+ *Arun Agrawal*
+
+* Remove deprecated `transaction_joinable=` in favor of `begin_transaction`
+ with `:joinable` option.
+
+ *Arun Agrawal*
+
+* Remove deprecated `decrement_open_transactions`.
+
+ *Arun Agrawal*
+
+* Remove deprecated `increment_open_transactions`.
+
+ *Arun Agrawal*
+
+* Remove deprecated `PostgreSQLAdapter#outside_transaction?`
+ method. You can use `#transaction_open?` instead.
+
+ *Yves Senn*
+
+* Remove deprecated `ActiveRecord::Fixtures.find_table_name` in favor of
+ `ActiveRecord::Fixtures.default_fixture_model_name`.
+
+ *Vipul A M*
+
+* Removed deprecated `columns_for_remove` from `SchemaStatements`.
+
+ *Neeraj Singh*
+
+* Remove deprecated `SchemaStatements#distinct`.
+
+ *Francesco Rodriguez*
+
+* Move deprecated `ActiveRecord::TestCase` into the rails test
+ suite. The class is no longer public and is only used for internal
+ Rails tests.
+
+ *Yves Senn*
+
+* Removed support for deprecated option `:restrict` for `:dependent`
+ in associations.
+
+ *Neeraj Singh*
+
+* Removed support for deprecated `delete_sql` in associations.
+
+ *Neeraj Singh*
+
+* Removed support for deprecated `insert_sql` in associations.
+
+ *Neeraj Singh*
+
+* Removed support for deprecated `finder_sql` in associations.
+
+ *Neeraj Singh*
+
+* Support array as root element in JSON fields.
+
+ *Alexey Noskov & Francesco Rodriguez*
+
+* Removed support for deprecated `counter_sql` in associations.
+
+ *Neeraj Singh*
+
+* Do not invoke callbacks when `delete_all` is called on collection.
+
+ Method `delete_all` should not be invoking callbacks and this
+ feature was deprecated in Rails 4.0. This is being removed.
+ `delete_all` will continue to honor the `:dependent` option. However
+ if `:dependent` value is `:destroy` then the `:delete_all` deletion
+ strategy for that collection will be applied.
+
+ User can also force a deletion strategy by passing parameter to
+ `delete_all`. For example you can do `@post.comments.delete_all(:nullify)`.
+
+ *Neeraj Singh*
+
+* Calling default_scope without a proc will now raise `ArgumentError`.
+
+ *Neeraj Singh*
+
+* Removed deprecated method `type_cast_code` from Column.
+
+ *Neeraj Singh*
+
+* Removed deprecated options `delete_sql` and `insert_sql` from HABTM
+ association.
+
+ Removed deprecated options `finder_sql` and `counter_sql` from
+ collection association.
+
+ *Neeraj Singh*
+
+* Remove deprecated `ActiveRecord::Base#connection` method.
+ Make sure to access it via the class.
+
+ *Yves Senn*
+
+* Remove deprecation warning for `auto_explain_threshold_in_seconds`.
+
+ *Yves Senn*
+
+* Remove deprecated `:distinct` option from `Relation#count`.
+
+ *Yves Senn*
+
+* Removed deprecated methods `partial_updates`, `partial_updates?` and
+ `partial_updates=`.
+
+ *Neeraj Singh*
+
+* Removed deprecated method `scoped`.
+
+ *Neeraj Singh*
+
+* Removed deprecated method `default_scopes?`.
+
+ *Neeraj Singh*
+
+* Remove implicit join references that were deprecated in 4.0.
+
+ Example:
+
+ # before with implicit joins
+ Comment.where('posts.author_id' => 7)
+
+ # after
+ Comment.references(:posts).where('posts.author_id' => 7)
+
+ *Yves Senn*
+
+* Apply default scope when joining associations. For example:
+
+ class Post < ActiveRecord::Base
+ default_scope -> { where published: true }
+ end
+
+ class Comment
+ belongs_to :post
+ end
+
+ When calling `Comment.joins(:post)`, we expect to receive only
+ comments on published posts, since that is the default scope for
+ posts.
+
+ Before this change, the default scope from `Post` was not applied,
+ so we'd get comments on unpublished posts.
+
+ *Jon Leighton*
+
+* Remove `activerecord-deprecated_finders` as a dependency.
+
+ *Łukasz Strzałkowski*
+
+* Remove Oracle / Sqlserver / Firebird database tasks that were deprecated in 4.0.
+
+ *kennyj*
+
+* `find_each` now returns an `Enumerator` when called without a block, so that it
+ can be chained with other `Enumerable` methods.
+
+ *Ben Woosley*
+
+* `ActiveRecord::Result.each` now returns an `Enumerator` when called without
+ a block, so that it can be chained with other `Enumerable` methods.
+
+ *Ben Woosley*
+
+* Flatten merged join_values before building the joins.
+
+ While joining_values special treatment is given to string values.
+ By flattening the array it ensures that string values are detected
+ as strings and not arrays.
+
+ Fixes #10669.
+
+ *Neeraj Singh and iwiznia*
+
+* Do not load all child records for inverse case.
+
+ currently `post.comments.find(Comment.first.id)` would load all
+ comments for the given post to set the inverse association.
+
+ This has a huge performance penalty. Because if post has 100k
+ records and all these 100k records would be loaded in memory
+ even though the comment id was supplied.
+
+ Fix is to use in-memory records only if loaded? is true. Otherwise
+ load the records using full sql.
+
+ Fixes #10509.
+
+ *Neeraj Singh*
+
+* `inspect` on Active Record model classes does not initiate a
+ new connection. This means that calling `inspect`, when the
+ database is missing, will no longer raise an exception.
+ Fixes #10936.
+
+ Example:
+
+ Author.inspect # => "Author(no database connection)"
+
+ *Yves Senn*
+
+* Handle single quotes in PostgreSQL default column values.
+ Fixes #10881.
+
+ *Dylan Markow*
+
+* Log the sql that is actually sent to the database.
+
+ If I have a query that produces sql
+ `WHERE "users"."name" = 'a b'` then in the log all the
+ whitespace is being squeezed. So the sql that is printed in the
+ log is `WHERE "users"."name" = 'a b'`.
+
+ Do not squeeze whitespace out of sql queries. Fixes #10982.
+
+ *Neeraj Singh*
+
+* Fixture setup no longer depends on `ActiveRecord::Base.configurations`.
+ This is relevant when `ENV["DATABASE_URL"]` is used in place of a `database.yml`.
+
+ *Yves Senn*
+
+* Fix mysql2 adapter raises the correct exception when executing a query on a
+ closed connection.
+
+ *Yves Senn*
+
+* Ambiguous reflections are on :through relationships are no longer supported.
+ For example, you need to change this:
+
+ class Author < ActiveRecord::Base
+ has_many :posts
+ has_many :taggings, through: :posts
+ end
+
+ class Post < ActiveRecord::Base
+ has_one :tagging
+ has_many :taggings
+ end
+
+ class Tagging < ActiveRecord::Base
+ end
+
+ To this:
+
+ class Author < ActiveRecord::Base
+ has_many :posts
+ has_many :taggings, through: :posts, source: :tagging
+ end
+
+ class Post < ActiveRecord::Base
+ has_one :tagging
+ has_many :taggings
+ end
+
+ class Tagging < ActiveRecord::Base
+ end
+
+ *Aaron Patterson*
+
+* Remove column restrictions for `count`, let the database raise if the SQL is
+ invalid. The previous behavior was untested and surprising for the user.
+ Fixes #5554.
+
+ Example:
+
+ User.select("name, username").count
+ # Before => SELECT count(*) FROM users
+ # After => ActiveRecord::StatementInvalid
+
+ # you can still use `count(:all)` to perform a query unrelated to the
+ # selected columns
+ User.select("name, username").count(:all) # => SELECT count(*) FROM users
+
+ *Yves Senn*
+
+* Rails now automatically detects inverse associations. If you do not set the
+ `:inverse_of` option on the association, then Active Record will guess the
+ inverse association based on heuristics.
+
+ Note that automatic inverse detection only works on `has_many`, `has_one`,
+ and `belongs_to` associations. Extra options on the associations will
+ also prevent the association's inverse from being found automatically.
+
+ The automatic guessing of the inverse association uses a heuristic based
+ on the name of the class, so it may not work for all associations,
+ especially the ones with non-standard names.
+
+ You can turn off the automatic detection of inverse associations by setting
+ the `:inverse_of` option to `false` like so:
+
+ class Taggable < ActiveRecord::Base
+ belongs_to :tag, inverse_of: false
+ end
+
+ *John Wang*
+
+* Fix `add_column` with `array` option when using PostgreSQL. Fixes #10432.
+
+ *Adam Anderson*
+
+* Usage of `implicit_readonly` is being removed`. Please use `readonly` method
+ explicitly to mark records as `readonly.
+ Fixes #10615.
+
+ Example:
+
+ user = User.joins(:todos).select("users.*, todos.title as todos_title").readonly(true).first
+ user.todos_title = 'clean pet'
+ user.save! # will raise error
+
+ *Yves Senn*
+
+* Fix the `:primary_key` option for `has_many` associations.
+ Fixes #10693.
+
+ *Yves Senn*
+
+* Fix bug where tiny types are incorrectly coerced as boolean when the length is more than 1.
+
+ Fixes #10620.
+
+ *Aaron Patterson*
+
* Also support extensions in PostgreSQL 9.1. This feature has been supported since 9.1.
*kennyj*
@@ -5,7 +1376,7 @@
* Deprecate `ConnectionAdapters::SchemaStatements#distinct`,
as it is no longer used by internals.
- *Ben Woosley#
+ *Ben Woosley*
* Fix pending migrations error when loading schema and `ActiveRecord::Base.table_name_prefix`
is not blank.
@@ -75,8 +1446,8 @@
*Olek Janiszewski*
-* fixes bug introduced by #3329. Now, when autosaving associations,
- deletions happen before inserts and saves. This prevents a 'duplicate
+* fixes bug introduced by #3329. Now, when autosaving associations,
+ deletions happen before inserts and saves. This prevents a 'duplicate
unique value' database error that would occur if a record being created had
the same value on a unique indexed field as that of a record being destroyed.
diff --git a/activerecord/MIT-LICENSE b/activerecord/MIT-LICENSE
index 0d7fb865e2..2950f05b11 100644
--- a/activerecord/MIT-LICENSE
+++ b/activerecord/MIT-LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2004-2013 David Heinemeier Hansson
+Copyright (c) 2004-2014 David Heinemeier Hansson
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
diff --git a/activerecord/RUNNING_UNIT_TESTS.rdoc b/activerecord/RUNNING_UNIT_TESTS.rdoc
index c3ee34da55..ca1f2fd665 100644
--- a/activerecord/RUNNING_UNIT_TESTS.rdoc
+++ b/activerecord/RUNNING_UNIT_TESTS.rdoc
@@ -24,6 +24,7 @@ Simply executing <tt>bundle exec rake test</tt> is equivalent to the following:
$ bundle exec rake test_mysql2
$ bundle exec rake test_postgresql
$ bundle exec rake test_sqlite3
+ $ bundle exec rake test_sqlite3_mem
There should be tests available for each database backend listed in the {Config
File}[rdoc-label:label-Config+File]. (the exact set of available tests is
diff --git a/activerecord/Rakefile b/activerecord/Rakefile
index cd73489cbe..6f8948f987 100644
--- a/activerecord/Rakefile
+++ b/activerecord/Rakefile
@@ -1,5 +1,4 @@
require 'rake/testtask'
-require 'rake/packagetask'
require 'rubygems/package_task'
require File.expand_path(File.dirname(__FILE__)) + "/test/config"
@@ -40,8 +39,10 @@ namespace :test do
end
namespace :db do
- task :create => ['mysql:build_databases', 'postgresql:build_databases']
- task :drop => ['mysql:drop_databases', 'postgresql:drop_databases']
+ desc 'Build MySQL and PostgreSQL test databases'
+ task create: ['mysql:build_databases', 'postgresql:build_databases']
+ desc 'Drop MySQL and PostgreSQL test databases'
+ task drop: ['mysql:drop_databases', 'postgresql:drop_databases']
end
%w( mysql mysql2 postgresql sqlite3 sqlite3_mem firebird db2 oracle sybase openbase frontbase jdbcmysql jdbcpostgresql jdbcsqlite3 jdbcderby jdbch2 jdbchsqldb ).each do |adapter|
@@ -59,11 +60,10 @@ end
task "isolated_test_#{adapter}" do
adapter_short = adapter == 'db2' ? adapter : adapter[/^[a-z0-9]+/]
puts [adapter, adapter_short].inspect
- ruby = File.join(*RbConfig::CONFIG.values_at('bindir', 'RUBY_INSTALL_NAME'))
(Dir["test/cases/**/*_test.rb"].reject {
|x| x =~ /\/adapters\//
} + Dir["test/cases/adapters/#{adapter_short}/**/*_test.rb"]).all? do |file|
- sh(ruby, "-Itest", file)
+ sh(Gem.ruby, '-w' ,"-Itest", file)
end or raise "Failures"
end
@@ -120,12 +120,9 @@ namespace :postgresql do
%x( createdb -E UTF8 -T template0 #{config['arunit']['database']} )
%x( createdb -E UTF8 -T template0 #{config['arunit2']['database']} )
- # prepare hstore
- version = %x( createdb --version ).strip.gsub(/(.*)(\d\.\d\.\d)$/, "\\2")
- %w(arunit arunit2).each do |db|
- if version < "9.1.0"
- puts "Please prepare hstore data type. See http://www.postgresql.org/docs/9.0/static/hstore.html"
- end
+ # notify about preparing hstore
+ if %x( createdb --version ).strip.gsub(/(.*)(\d\.\d\.\d)$/, "\\2") < "9.1.0"
+ puts "Please prepare hstore data type. See http://www.postgresql.org/docs/9.0/static/hstore.html"
end
end
@@ -223,7 +220,7 @@ end
# Publishing ------------------------------------------------------
-desc "Release to gemcutter"
+desc "Release to rubygems"
task :release => :package do
require 'rake/gemcutter'
Rake::Gemcutter::Tasks.new(spec).define
diff --git a/activerecord/activerecord.gemspec b/activerecord/activerecord.gemspec
index 337106cb92..d397c9e016 100644
--- a/activerecord/activerecord.gemspec
+++ b/activerecord/activerecord.gemspec
@@ -24,6 +24,5 @@ Gem::Specification.new do |s|
s.add_dependency 'activesupport', version
s.add_dependency 'activemodel', version
- s.add_dependency 'arel', '~> 4.0.0'
- s.add_dependency 'activerecord-deprecated_finders', '~> 1.0.2'
+ s.add_dependency 'arel', '~> 5.0.0'
end
diff --git a/activerecord/examples/performance.rb b/activerecord/examples/performance.rb
index ad12f8597f..d3546ce948 100644
--- a/activerecord/examples/performance.rb
+++ b/activerecord/examples/performance.rb
@@ -5,12 +5,12 @@ require 'benchmark/ips'
TIME = (ENV['BENCHMARK_TIME'] || 20).to_i
RECORDS = (ENV['BENCHMARK_RECORDS'] || TIME*1000).to_i
-conn = { :adapter => 'sqlite3', :database => ':memory:' }
+conn = { adapter: 'sqlite3', database: ':memory:' }
ActiveRecord::Base.establish_connection(conn)
class User < ActiveRecord::Base
- connection.create_table :users, :force => true do |t|
+ connection.create_table :users, force: true do |t|
t.string :name, :email
t.timestamps
end
@@ -19,7 +19,7 @@ class User < ActiveRecord::Base
end
class Exhibit < ActiveRecord::Base
- connection.create_table :exhibits, :force => true do |t|
+ connection.create_table :exhibits, force: true do |t|
t.belongs_to :user
t.string :name
t.text :notes
@@ -43,6 +43,8 @@ class Exhibit < ActiveRecord::Base
def self.feel(exhibits) exhibits.each { |e| e.feel } end
end
+def progress_bar(int); print "." if (int%100).zero? ; end
+
puts 'Generating data...'
module ActiveRecord
@@ -75,30 +77,32 @@ notes = ActiveRecord::Faker::LOREM.join ' '
today = Date.today
puts "Inserting #{RECORDS} users and exhibits..."
-RECORDS.times do
+RECORDS.times do |record|
user = User.create(
- :created_at => today,
- :name => ActiveRecord::Faker.name,
- :email => ActiveRecord::Faker.email
+ created_at: today,
+ name: ActiveRecord::Faker.name,
+ email: ActiveRecord::Faker.email
)
Exhibit.create(
- :created_at => today,
- :name => ActiveRecord::Faker.name,
- :user => user,
- :notes => notes
+ created_at: today,
+ name: ActiveRecord::Faker.name,
+ user: user,
+ notes: notes
)
+ progress_bar(record)
end
+puts "Done!\n"
Benchmark.ips(TIME) do |x|
ar_obj = Exhibit.find(1)
- attrs = { :name => 'sam' }
- attrs_first = { :name => 'sam' }
- attrs_second = { :name => 'tom' }
+ attrs = { name: 'sam' }
+ attrs_first = { name: 'sam' }
+ attrs_second = { name: 'tom' }
exhibit = {
- :name => ActiveRecord::Faker.name,
- :notes => notes,
- :created_at => Date.today
+ name: ActiveRecord::Faker.name,
+ notes: notes,
+ created_at: Date.today
}
x.report("Model#id") do
@@ -117,10 +121,18 @@ Benchmark.ips(TIME) do |x|
Exhibit.first.look
end
+ x.report 'Model.take' do
+ Exhibit.take
+ end
+
x.report("Model.all limit(100)") do
Exhibit.look Exhibit.limit(100)
end
+ x.report("Model.all take(100)") do
+ Exhibit.look Exhibit.take(100)
+ end
+
x.report "Model.all limit(100) with relationship" do
Exhibit.feel Exhibit.limit(100).includes(:user)
end
@@ -167,6 +179,6 @@ Benchmark.ips(TIME) do |x|
end
x.report "AR.execute(query)" do
- ActiveRecord::Base.connection.execute("Select * from exhibits where id = #{(rand * 1000 + 1).to_i}")
+ ActiveRecord::Base.connection.execute("SELECT * FROM exhibits WHERE id = #{(rand * 1000 + 1).to_i}")
end
end
diff --git a/activerecord/examples/simple.rb b/activerecord/examples/simple.rb
index c12f746992..4ed5d80eb2 100644
--- a/activerecord/examples/simple.rb
+++ b/activerecord/examples/simple.rb
@@ -1,14 +1,14 @@
-$LOAD_PATH.unshift "#{File.dirname(__FILE__)}/../lib"
+require File.expand_path('../../../load_paths', __FILE__)
require 'active_record'
class Person < ActiveRecord::Base
- establish_connection :adapter => 'sqlite3', :database => 'foobar.db'
- connection.create_table table_name, :force => true do |t|
+ establish_connection adapter: 'sqlite3', database: 'foobar.db'
+ connection.create_table table_name, force: true do |t|
t.string :name
end
end
-bob = Person.create!(:name => 'bob')
+bob = Person.create!(name: 'bob')
puts Person.all.inspect
bob.destroy
puts Person.all.inspect
diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb
index 994cacb4f9..f856c482d6 100644
--- a/activerecord/lib/active_record.rb
+++ b/activerecord/lib/active_record.rb
@@ -1,5 +1,5 @@
#--
-# Copyright (c) 2004-2013 David Heinemeier Hansson
+# Copyright (c) 2004-2014 David Heinemeier Hansson
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
@@ -25,7 +25,6 @@ require 'active_support'
require 'active_support/rails'
require 'active_model'
require 'arel'
-require 'active_record/deprecated_finders'
require 'active_record/version'
@@ -38,6 +37,7 @@ module ActiveRecord
autoload :ConnectionHandling
autoload :CounterCache
autoload :DynamicMatchers
+ autoload :Enum
autoload :Explain
autoload :Inheritance
autoload :Integration
@@ -45,6 +45,7 @@ module ActiveRecord
autoload :Migrator, 'active_record/migration'
autoload :ModelSchema
autoload :NestedAttributes
+ autoload :NoTouching
autoload :Persistence
autoload :QueryCache
autoload :Querying
@@ -146,13 +147,8 @@ module ActiveRecord
autoload :MySQLDatabaseTasks, 'active_record/tasks/mysql_database_tasks'
autoload :PostgreSQLDatabaseTasks,
'active_record/tasks/postgresql_database_tasks'
-
- autoload :FirebirdDatabaseTasks, 'active_record/tasks/firebird_database_tasks'
- autoload :SqlserverDatabaseTasks, 'active_record/tasks/sqlserver_database_tasks'
- autoload :OracleDatabaseTasks, 'active_record/tasks/oracle_database_tasks'
end
- autoload :TestCase
autoload :TestFixtures, 'active_record/fixtures'
def self.eager_load!
diff --git a/activerecord/lib/active_record/aggregations.rb b/activerecord/lib/active_record/aggregations.rb
index 9d1c12ec62..0d5313956b 100644
--- a/activerecord/lib/active_record/aggregations.rb
+++ b/activerecord/lib/active_record/aggregations.rb
@@ -223,7 +223,8 @@ module ActiveRecord
reader_method(name, class_name, mapping, allow_nil, constructor)
writer_method(name, class_name, mapping, allow_nil, converter)
- create_reflection(:composed_of, part_id, nil, options, self)
+ reflection = ActiveRecord::Reflection.create(:composed_of, part_id, nil, options, self)
+ Reflection.add_aggregate_reflection self, part_id, reflection
end
private
diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb
index 3490057298..714f623af3 100644
--- a/activerecord/lib/active_record/associations.rb
+++ b/activerecord/lib/active_record/associations.rb
@@ -73,21 +73,15 @@ module ActiveRecord
end
end
- class HasAndBelongsToManyAssociationForeignKeyNeeded < ActiveRecordError #:nodoc:
- def initialize(reflection)
- super("Cannot create self referential has_and_belongs_to_many association on '#{reflection.class_name rescue nil}##{reflection.name rescue nil}'. :association_foreign_key cannot be the same as the :foreign_key.")
- end
- end
-
class EagerLoadPolymorphicError < ActiveRecordError #:nodoc:
def initialize(reflection)
- super("Can not eagerly load the polymorphic association #{reflection.name.inspect}")
+ super("Cannot eagerly load the polymorphic association #{reflection.name.inspect}")
end
end
class ReadOnlyAssociation < ActiveRecordError #:nodoc:
def initialize(reflection)
- super("Can not add to a has_many :through association. Try adding to #{reflection.through_reflection.name.inspect}.")
+ super("Cannot add to a has_many :through association. Try adding to #{reflection.through_reflection.name.inspect}.")
end
end
@@ -114,7 +108,6 @@ module ActiveRecord
autoload :BelongsToAssociation, 'active_record/associations/belongs_to_association'
autoload :BelongsToPolymorphicAssociation, 'active_record/associations/belongs_to_polymorphic_association'
- autoload :HasAndBelongsToManyAssociation, 'active_record/associations/has_and_belongs_to_many_association'
autoload :HasManyAssociation, 'active_record/associations/has_many_association'
autoload :HasManyThroughAssociation, 'active_record/associations/has_many_through_association'
autoload :HasOneAssociation, 'active_record/associations/has_one_association'
@@ -164,7 +157,7 @@ module ActiveRecord
private
# Returns the specified association instance if it responds to :loaded?, nil otherwise.
def association_instance_get(name)
- @association_cache[name.to_sym]
+ @association_cache[name]
end
# Set the specified association instance.
@@ -586,9 +579,10 @@ module ActiveRecord
# belongs_to :tag, inverse_of: :taggings
# end
#
- # If you do not set the +:inverse_of+ record, the association will do its
- # best to match itself up with the correct inverse. Automatic +:inverse_of+
- # detection only works on +has_many+, +has_one+, and +belongs_to+ associations.
+ # 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.
#
# Extra options on the associations, as defined in the
# <tt>AssociationReflection::INVALID_AUTOMATIC_INVERSE_OPTIONS</tt> constant, will
@@ -599,10 +593,10 @@ module ActiveRecord
# especially the ones with non-standard names.
#
# You can turn off the automatic detection of inverse associations by setting
- # the +:automatic_inverse_of+ option to +false+ like so:
+ # the <tt>:inverse_of</tt> option to <tt>false</tt> like so:
#
# class Taggable < ActiveRecord::Base
- # belongs_to :tag, automatic_inverse_of: false
+ # belongs_to :tag, inverse_of: false
# end
#
# == Nested \Associations
@@ -1204,7 +1198,8 @@ module ActiveRecord
# has_many :reports, -> { readonly }
# has_many :subscribers, through: :subscriptions, source: :user
def has_many(name, scope = nil, options = {}, &extension)
- Builder::HasMany.build(self, name, scope, options, &extension)
+ reflection = Builder::HasMany.build(self, name, scope, options, &extension)
+ Reflection.add_reflection self, name, reflection
end
# Specifies a one-to-one association with another class. This method should only be used
@@ -1307,7 +1302,8 @@ module ActiveRecord
# has_one :club, through: :membership
# has_one :primary_address, -> { where primary: true }, through: :addressables, source: :addressable
def has_one(name, scope = nil, options = {})
- Builder::HasOne.build(self, name, scope, options)
+ reflection = Builder::HasOne.build(self, name, scope, options)
+ Reflection.add_reflection self, name, reflection
end
# Specifies a one-to-one association with another class. This method should only be used
@@ -1419,7 +1415,8 @@ module ActiveRecord
# belongs_to :company, touch: true
# belongs_to :company, touch: :employees_last_updated_at
def belongs_to(name, scope = nil, options = {})
- Builder::BelongsTo.build(self, name, scope, options)
+ reflection = Builder::BelongsTo.build(self, name, scope, options)
+ Reflection.add_reflection self, name, reflection
end
# Specifies a many-to-many relationship with another class. This associates two classes via an
@@ -1556,7 +1553,39 @@ module ActiveRecord
# has_and_belongs_to_many :categories, join_table: "prods_cats"
# has_and_belongs_to_many :categories, -> { readonly }
def has_and_belongs_to_many(name, scope = nil, options = {}, &extension)
- Builder::HasAndBelongsToMany.build(self, name, scope, options, &extension)
+ if scope.is_a?(Hash)
+ options = scope
+ scope = nil
+ end
+
+ builder = Builder::HasAndBelongsToMany.new name, self, options
+
+ join_model = builder.through_model
+
+ middle_reflection = builder.middle_reflection join_model
+
+ Builder::HasMany.define_callbacks self, middle_reflection
+ Reflection.add_reflection self, middle_reflection.name, middle_reflection
+
+ include Module.new {
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
+ def destroy_associations
+ association(:#{middle_reflection.name}).delete_all(:delete_all)
+ association(:#{name}).reset
+ super
+ end
+ RUBY
+ }
+
+ hm_options = {}
+ hm_options[:through] = middle_reflection.name
+ hm_options[:source] = join_model.right_reflection.name
+
+ [:before_add, :after_add, :before_remove, :after_remove].each do |k|
+ hm_options[k] = options[k] if options.key? k
+ end
+
+ has_many name, scope, hm_options, &extension
end
end
end
diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb
index 608a6af16c..67ea489b22 100644
--- a/activerecord/lib/active_record/associations/association.rb
+++ b/activerecord/lib/active_record/associations/association.rb
@@ -13,11 +13,11 @@ module ActiveRecord
# BelongsToAssociation
# BelongsToPolymorphicAssociation
# CollectionAssociation
- # HasAndBelongsToManyAssociation
# HasManyAssociation
# HasManyThroughAssociation + ThroughAssociation
class Association #:nodoc:
attr_reader :owner, :target, :reflection
+ attr_accessor :inversed
delegate :options, :to => :reflection
@@ -43,6 +43,7 @@ module ActiveRecord
@loaded = false
@target = nil
@stale_state = nil
+ @inversed = false
end
# Reloads the \target and returns +self+ on success.
@@ -60,18 +61,19 @@ module ActiveRecord
# Asserts the \target has been loaded setting the \loaded flag to +true+.
def loaded!
- @loaded = true
+ @loaded = true
@stale_state = stale_state
+ @inversed = false
end
# The target is stale if the target no longer points to the record(s) that the
# relevant foreign_key(s) refers to. If stale, the association accessor method
# on the owner will reload the target. It's up to subclasses to implement the
- # state_state method if relevant.
+ # stale_state method if relevant.
#
# Note that if the target has not been loaded, it is not considered stale.
def stale_target?
- loaded? && @stale_state != stale_state
+ !inversed && loaded? && @stale_state != stale_state
end
# Sets the target of this association to <tt>\target</tt>, and the \loaded flag to +true+.
@@ -84,11 +86,6 @@ module ActiveRecord
target_scope.merge(association_scope)
end
- def scoped
- ActiveSupport::Deprecation.warn "#scoped is deprecated. use #scope instead."
- scope
- end
-
# The scope for this association.
#
# Note that the association_scope is merged into the target_scope only when the
@@ -107,10 +104,12 @@ module ActiveRecord
# Set the inverse association, if possible
def set_inverse_instance(record)
- if record && invertible_for?(record)
+ if invertible_for?(record)
inverse = record.association(inverse_reflection_for(record).name)
inverse.target = owner
+ inverse.inversed = true
end
+ record
end
# Returns the class of the target. belongs_to polymorphic overrides this to look at the
@@ -122,11 +121,7 @@ module ActiveRecord
# Can be overridden (i.e. in ThroughAssociation) to merge in other scopes (i.e. the
# through association's scope)
def target_scope
- all = klass.all
- scope = AssociationRelation.new(klass, klass.arel_table, self)
- scope.merge! all
- scope.default_scoped = all.default_scoped?
- scope
+ AssociationRelation.create(klass, klass.arel_table, self).merge!(klass.all)
end
# Loads the \target if needed and returns it.
@@ -200,13 +195,14 @@ module ActiveRecord
creation_attributes.each { |key, value| record[key] = value }
end
- # Should be true if there is a foreign key present on the owner which
+ # Returns true if there is a foreign key present on the owner which
# references the target. This is used to determine whether we can load
# the target if the owner is currently a new record (and therefore
- # without a key).
+ # without a key). If the owner is a new record then foreign_key must
+ # be present in order to load target.
#
# Currently implemented by belongs_to (vanilla and polymorphic) and
- # has_one/has_many :through associations which go through a belongs_to
+ # has_one/has_many :through associations which go through a belongs_to.
def foreign_key_present?
false
end
@@ -231,7 +227,12 @@ module ActiveRecord
# Returns true if inverse association on the given record needs to be set.
# This method is redefined by subclasses.
def invertible_for?(record)
- inverse_reflection_for(record)
+ foreign_key_for?(record) && inverse_reflection_for(record)
+ end
+
+ # Returns true if record contains the foreign_key
+ def foreign_key_for?(record)
+ record.attributes.has_key? reflection.foreign_key
end
# This should be implemented to return the values of the relevant key(s) on the owner,
diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb
index 21840ba74a..f455b6934e 100644
--- a/activerecord/lib/active_record/associations/association_scope.rb
+++ b/activerecord/lib/active_record/associations/association_scope.rb
@@ -44,18 +44,6 @@ module ActiveRecord
chain.each_with_index do |reflection, i|
table, foreign_table = tables.shift, tables.first
- if reflection.source_macro == :has_and_belongs_to_many
- join_table = tables.shift
-
- scope = scope.joins(join(
- join_table,
- table[reflection.association_primary_key].
- eq(join_table[reflection.association_foreign_key])
- ))
-
- table, foreign_table = join_table, tables.first
- end
-
if reflection.source_macro == :belongs_to
if reflection.options[:polymorphic]
key = reflection.association_primary_key(self.klass)
@@ -82,24 +70,30 @@ module ActiveRecord
constraint = table[key].eq(foreign_table[foreign_key])
if reflection.type
- type = chain[i + 1].klass.base_class.name
- constraint = constraint.and(table[reflection.type].eq(type))
+ value = chain[i + 1].klass.base_class.name
+ bind_val = bind scope, table.table_name, reflection.type.to_s, value
+ scope = scope.where(table[reflection.type].eq(bind_val))
end
scope = scope.joins(join(foreign_table, constraint))
end
+ is_first_chain = i == 0
+ klass = is_first_chain ? self.klass : reflection.klass
+
# Exclude the scope of the association itself, because that
# was already merged in the #scope method.
scope_chain[i].each do |scope_chain_item|
- klass = i == 0 ? self.klass : reflection.klass
item = eval_scope(klass, scope_chain_item)
if scope_chain_item == self.reflection.scope
- scope.merge! item.except(:where, :includes)
+ scope.merge! item.except(:where, :includes, :bind)
+ end
+
+ if is_first_chain
+ scope.includes! item.includes_values
end
- scope.includes! item.includes_values
scope.where_values += item.where_values
scope.bind_values += item.bind_values
scope.order_values |= item.order_values
@@ -120,7 +114,7 @@ module ActiveRecord
# the owner
klass.table_name
else
- reflection.table_name
+ super
end
end
diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb
index 8eec4f56af..8272a5584c 100644
--- a/activerecord/lib/active_record/associations/belongs_to_association.rb
+++ b/activerecord/lib/active_record/associations/belongs_to_association.rb
@@ -8,13 +8,16 @@ module ActiveRecord
end
def replace(record)
- raise_on_type_mismatch!(record) if record
-
- update_counters(record)
- replace_keys(record)
- set_inverse_instance(record)
-
- @updated = true if record
+ if record
+ raise_on_type_mismatch!(record)
+ update_counters(record)
+ replace_keys(record)
+ set_inverse_instance(record)
+ @updated = true
+ else
+ decrement_counters
+ remove_keys
+ end
self.target = record
end
@@ -34,35 +37,41 @@ module ActiveRecord
!loaded? && foreign_key_present? && klass
end
- def update_counters(record)
+ def with_cache_name
counter_cache_name = reflection.counter_cache_column
+ return unless counter_cache_name && owner.persisted?
+ yield counter_cache_name
+ 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
+ end
- if counter_cache_name && owner.persisted? && different_target?(record)
- if record
- record.class.increment_counter(counter_cache_name, record.id)
- end
+ def decrement_counters
+ with_cache_name { |name| decrement_counter name }
+ end
- if foreign_key_present?
- klass.decrement_counter(counter_cache_name, target_id)
- end
+ def decrement_counter counter_cache_name
+ if foreign_key_present?
+ klass.decrement_counter(counter_cache_name, target_id)
end
end
# Checks whether record is different to the current target, without loading it
def different_target?(record)
- if record.nil?
- owner[reflection.foreign_key]
- else
- record.id != owner[reflection.foreign_key]
- end
+ record.id != owner[reflection.foreign_key]
end
def replace_keys(record)
- if record
- owner[reflection.foreign_key] = record[reflection.association_primary_key(record.class)]
- else
- owner[reflection.foreign_key] = nil
- end
+ owner[reflection.foreign_key] = record[reflection.association_primary_key(record.class)]
+ end
+
+ def remove_keys
+ owner[reflection.foreign_key] = nil
end
def foreign_key_present?
diff --git a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb
index eae5eed3a1..b710cf6bdb 100644
--- a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb
+++ b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb
@@ -11,7 +11,12 @@ module ActiveRecord
def replace_keys(record)
super
- owner[reflection.foreign_type] = record && record.class.base_class.name
+ owner[reflection.foreign_type] = record.class.base_class.name
+ end
+
+ def remove_keys
+ super
+ owner[reflection.foreign_type] = nil
end
def different_target?(record)
diff --git a/activerecord/lib/active_record/associations/builder/association.rb b/activerecord/lib/active_record/associations/builder/association.rb
index 6a3658f328..3911d1b520 100644
--- a/activerecord/lib/active_record/associations/builder/association.rb
+++ b/activerecord/lib/active_record/associations/builder/association.rb
@@ -1,62 +1,66 @@
+require 'active_support/core_ext/module/attribute_accessors'
+
# This is the parent Association class which defines the variables
# used by all associations.
#
# The hierarchy is defined as follows:
-# Association
+# Association
# - SingularAssociation
# - BelongsToAssociation
# - HasOneAssociation
# - CollectionAssociation
# - HasManyAssociation
-# - HasAndBelongsToManyAssociation
module ActiveRecord::Associations::Builder
class Association #:nodoc:
class << self
+ attr_accessor :extensions
+ # TODO: This class accessor is needed to make activerecord-deprecated_finders work.
+ # We can move it to a constant in 5.0.
attr_accessor :valid_options
end
+ self.extensions = []
- self.valid_options = [:class_name, :foreign_key, :validate]
+ self.valid_options = [:class_name, :class, :foreign_key, :validate]
- attr_reader :model, :name, :scope, :options, :reflection
+ attr_reader :name, :scope, :options
- def self.build(*args, &block)
- new(*args, &block).build
+ def self.build(model, name, scope, options, &block)
+ builder = create_builder model, name, scope, options, &block
+ reflection = builder.build(model)
+ define_accessors model, reflection
+ define_callbacks model, reflection
+ builder.define_extensions model
+ reflection
end
- def initialize(model, name, scope, options)
+ def self.create_builder(model, name, scope, options, &block)
raise ArgumentError, "association names must be a Symbol" unless name.kind_of?(Symbol)
- @model = model
- @name = name
+ new(model, name, scope, options, &block)
+ end
+ def initialize(model, name, scope, options)
+ # TODO: Move this to create_builder as soon we drop support to activerecord-deprecated_finders.
if scope.is_a?(Hash)
- @scope = nil
- @options = scope
- else
- @scope = scope
- @options = options
+ options = scope
+ scope = nil
end
- if @scope && @scope.arity == 0
- prev_scope = @scope
- @scope = proc { instance_exec(&prev_scope) }
- end
- end
+ # TODO: Remove this model argument as soon we drop support to activerecord-deprecated_finders.
+ @name = name
+ @scope = scope
+ @options = options
- def mixin
- @model.generated_feature_methods
- end
+ validate_options
- include Module.new { def build; end }
+ if scope && scope.arity == 0
+ @scope = proc { instance_exec(&scope) }
+ end
+ end
- def build
- validate_options
- define_accessors
- configure_dependency if options[:dependent]
- @reflection = model.create_reflection(macro, name, scope, options, model)
- super # provides an extension point
- @reflection
+ def build(model)
+ ActiveRecord::Reflection.create(macro, name, scope, options, model)
end
def macro
@@ -64,26 +68,37 @@ module ActiveRecord::Associations::Builder
end
def valid_options
- Association.valid_options
+ Association.valid_options + Association.extensions.flat_map(&:valid_options)
end
def validate_options
options.assert_valid_keys(valid_options)
end
-
+
+ def define_extensions(model)
+ end
+
+ def self.define_callbacks(model, reflection)
+ add_before_destroy_callbacks(model, reflection) if reflection.options[:dependent]
+ Association.extensions.each do |extension|
+ extension.build model, reflection
+ end
+ end
+
# Defines the setter and getter methods for the association
# class Post < ActiveRecord::Base
# has_many :comments
# end
- #
+ #
# Post.first.comments and Post.first.comments= methods are defined by this method...
-
- def define_accessors
- define_readers
- define_writers
+ def self.define_accessors(model, reflection)
+ mixin = model.generated_association_methods
+ name = reflection.name
+ define_readers(mixin, name)
+ define_writers(mixin, name)
end
- def define_readers
+ def self.define_readers(mixin, name)
mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
def #{name}(*args)
association(:#{name}).reader(*args)
@@ -91,7 +106,7 @@ module ActiveRecord::Associations::Builder
CODE
end
- def define_writers
+ def self.define_writers(mixin, name)
mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
def #{name}=(value)
association(:#{name}).writer(value)
@@ -99,29 +114,19 @@ module ActiveRecord::Associations::Builder
CODE
end
- def configure_dependency
- unless valid_dependent_options.include? options[:dependent]
- raise ArgumentError, "The :dependent option must be one of #{valid_dependent_options}, but is :#{options[:dependent]}"
- end
-
- if options[:dependent] == :restrict
- ActiveSupport::Deprecation.warn(
- "The :restrict option is deprecated. Please use :restrict_with_exception instead, which " \
- "provides the same functionality."
- )
- end
+ def self.valid_dependent_options
+ raise NotImplementedError
+ end
- mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
- def #{macro}_dependent_for_#{name}
- association(:#{name}).handle_dependency
- end
- CODE
+ private
- model.before_destroy "#{macro}_dependent_for_#{name}"
- end
+ def self.add_before_destroy_callbacks(model, reflection)
+ unless valid_dependent_options.include? reflection.options[:dependent]
+ raise ArgumentError, "The :dependent option must be one of #{valid_dependent_options}, but is :#{reflection.options[:dependent]}"
+ end
- def valid_dependent_options
- raise NotImplementedError
+ name = reflection.name
+ model.before_destroy lambda { |o| o.association(name).handle_dependency }
end
end
end
diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb
index 63e9526436..5ccaa55a32 100644
--- a/activerecord/lib/active_record/associations/builder/belongs_to.rb
+++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb
@@ -5,96 +5,134 @@ module ActiveRecord::Associations::Builder
end
def valid_options
- super + [:foreign_type, :polymorphic, :touch]
+ super + [:foreign_type, :polymorphic, :touch, :counter_cache]
end
- def constructable?
- !options[:polymorphic]
+ def self.valid_dependent_options
+ [:destroy, :delete]
end
- def build
- reflection = super
- add_counter_cache_callbacks(reflection) if options[:counter_cache]
- add_touch_callbacks(reflection) if options[:touch]
- reflection
+ def self.define_callbacks(model, reflection)
+ super
+ add_counter_cache_callbacks(model, reflection) if reflection.options[:counter_cache]
+ add_touch_callbacks(model, reflection) if reflection.options[:touch]
end
- def add_counter_cache_callbacks(reflection)
- cache_column = reflection.counter_cache_column
- foreign_key = reflection.foreign_key
+ def self.define_accessors(mixin, reflection)
+ super
+ add_counter_cache_methods mixin
+ end
+
+ private
+
+ def self.add_counter_cache_methods(mixin)
+ return if mixin.method_defined? :belongs_to_counter_cache_after_create
- mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
- def belongs_to_counter_cache_after_create_for_#{name}
- if record = #{name}
- record.class.increment_counter(:#{cache_column}, record.id)
+ mixin.class_eval do
+ def belongs_to_counter_cache_after_create(reflection)
+ if record = send(reflection.name)
+ cache_column = reflection.counter_cache_column
+ record.class.increment_counter(cache_column, record.id)
@_after_create_counter_called = true
end
end
- def belongs_to_counter_cache_before_destroy_for_#{name}
- unless destroyed_by_association && destroyed_by_association.foreign_key.to_sym == #{foreign_key.to_sym.inspect}
- record = #{name}
+ def belongs_to_counter_cache_before_destroy(reflection)
+ foreign_key = reflection.foreign_key.to_sym
+ unless destroyed_by_association && destroyed_by_association.foreign_key.to_sym == foreign_key
+ record = send reflection.name
if record && !self.destroyed?
- record.class.decrement_counter(:#{cache_column}, record.id)
+ cache_column = reflection.counter_cache_column
+ record.class.decrement_counter(cache_column, record.id)
end
end
end
- def belongs_to_counter_cache_after_update_for_#{name}
+ def belongs_to_counter_cache_after_update(reflection)
+ foreign_key = reflection.foreign_key
+ cache_column = reflection.counter_cache_column
+
if (@_after_create_counter_called ||= false)
@_after_create_counter_called = false
- elsif self.#{foreign_key}_changed? && !new_record? && defined?(#{name.to_s.camelize})
- model = #{name.to_s.camelize}
- foreign_key_was = self.#{foreign_key}_was
- foreign_key = self.#{foreign_key}
+ elsif attribute_changed?(foreign_key) && !new_record? && reflection.constructable?
+ model = reflection.klass
+ foreign_key_was = attribute_was foreign_key
+ foreign_key = attribute foreign_key
if foreign_key && model.respond_to?(:increment_counter)
- model.increment_counter(:#{cache_column}, foreign_key)
+ model.increment_counter(cache_column, foreign_key)
end
if foreign_key_was && model.respond_to?(:decrement_counter)
- model.decrement_counter(:#{cache_column}, foreign_key_was)
+ model.decrement_counter(cache_column, foreign_key_was)
end
end
end
- CODE
+ end
+ end
+
+ def self.add_counter_cache_callbacks(model, reflection)
+ cache_column = reflection.counter_cache_column
+
+ model.after_create lambda { |record|
+ record.belongs_to_counter_cache_after_create(reflection)
+ }
- model.after_create "belongs_to_counter_cache_after_create_for_#{name}"
- model.before_destroy "belongs_to_counter_cache_before_destroy_for_#{name}"
- model.after_update "belongs_to_counter_cache_after_update_for_#{name}"
+ model.before_destroy lambda { |record|
+ record.belongs_to_counter_cache_before_destroy(reflection)
+ }
+
+ model.after_update lambda { |record|
+ record.belongs_to_counter_cache_after_update(reflection)
+ }
klass = reflection.class_name.safe_constantize
klass.attr_readonly cache_column if klass && klass.respond_to?(:attr_readonly)
end
- def add_touch_callbacks(reflection)
- mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
- def belongs_to_touch_after_save_or_destroy_for_#{name}
- foreign_key_field = #{reflection.foreign_key.inspect}
- old_foreign_id = attribute_was(foreign_key_field)
-
- if old_foreign_id
- klass = association(#{name.inspect}).klass
- old_record = klass.find_by(klass.primary_key => old_foreign_id)
+ def self.touch_record(o, foreign_key, name, touch) # :nodoc:
+ old_foreign_id = o.changed_attributes[foreign_key]
- if old_record
- old_record.touch #{options[:touch].inspect if options[:touch] != true}
- end
- end
+ if old_foreign_id
+ association = o.association(name)
+ reflection = association.reflection
+ if reflection.polymorphic?
+ klass = o.public_send("#{reflection.foreign_type}_was").constantize
+ else
+ klass = association.klass
+ end
+ old_record = klass.find_by(klass.primary_key => old_foreign_id)
- record = #{name}
- unless record.nil? || record.new_record?
- record.touch #{options[:touch].inspect if options[:touch] != true}
+ if old_record
+ if touch != true
+ old_record.touch touch
+ else
+ old_record.touch
end
end
- CODE
-
- model.after_save "belongs_to_touch_after_save_or_destroy_for_#{name}"
- model.after_touch "belongs_to_touch_after_save_or_destroy_for_#{name}"
- model.after_destroy "belongs_to_touch_after_save_or_destroy_for_#{name}"
+ end
+
+ record = o.send name
+ if record && record.persisted?
+ if touch != true
+ record.touch touch
+ else
+ record.touch
+ end
+ end
end
- def valid_dependent_options
- [:destroy, :delete]
+ def self.add_touch_callbacks(model, reflection)
+ foreign_key = reflection.foreign_key
+ n = reflection.name
+ touch = reflection.options[:touch]
+
+ callback = lambda { |record|
+ BelongsTo.touch_record(record, foreign_key, n, touch)
+ }
+
+ model.after_save callback
+ model.after_touch callback
+ model.after_destroy callback
end
end
end
diff --git a/activerecord/lib/active_record/associations/builder/collection_association.rb b/activerecord/lib/active_record/associations/builder/collection_association.rb
index 9c6690b721..bc15a49996 100644
--- a/activerecord/lib/active_record/associations/builder/collection_association.rb
+++ b/activerecord/lib/active_record/associations/builder/collection_association.rb
@@ -1,4 +1,4 @@
-# This class is inherited by the has_many and has_many_and_belongs_to_many association classes
+# This class is inherited by the has_many and has_many_and_belongs_to_many association classes
require 'active_record/associations'
@@ -8,69 +8,57 @@ module ActiveRecord::Associations::Builder
CALLBACKS = [:before_add, :after_add, :before_remove, :after_remove]
def valid_options
- super + [:table_name, :finder_sql, :counter_sql, :before_add,
+ super + [:table_name, :before_add,
:after_add, :before_remove, :after_remove, :extend]
end
- attr_reader :block_extension, :extension_module
+ attr_reader :block_extension
- def initialize(*args, &extension)
- super(*args)
- @block_extension = extension
- end
-
- def build
- show_deprecation_warnings
- wrap_block_extension
- reflection = super
- CALLBACKS.each { |callback_name| define_callback(callback_name) }
- reflection
+ def initialize(model, name, scope, options)
+ super
+ @mod = nil
+ if block_given?
+ @mod = Module.new(&Proc.new)
+ @scope = wrap_scope @scope, @mod
+ end
end
- def writable?
- true
+ def self.define_callbacks(model, reflection)
+ super
+ name = reflection.name
+ options = reflection.options
+ CALLBACKS.each { |callback_name|
+ define_callback(model, callback_name, name, options)
+ }
end
- def show_deprecation_warnings
- [:finder_sql, :counter_sql].each do |name|
- if options.include? name
- ActiveSupport::Deprecation.warn("The :#{name} association option is deprecated. Please find an alternative (such as using scopes).")
- end
+ def define_extensions(model)
+ if @mod
+ extension_module_name = "#{model.name.demodulize}#{name.to_s.camelize}AssociationExtension"
+ model.parent.const_set(extension_module_name, @mod)
end
end
- def wrap_block_extension
- if block_extension
- @extension_module = mod = Module.new(&block_extension)
- silence_warnings do
- model.parent.const_set(extension_module_name, mod)
- end
-
- prev_scope = @scope
+ def self.define_callback(model, callback_name, name, options)
+ full_callback_name = "#{callback_name}_for_#{name}"
- if prev_scope
- @scope = proc { |owner| instance_exec(owner, &prev_scope).extending(mod) }
+ # TODO : why do i need method_defined? I think its because of the inheritance chain
+ model.class_attribute full_callback_name unless model.method_defined?(full_callback_name)
+ callbacks = Array(options[callback_name.to_sym]).map do |callback|
+ case callback
+ when Symbol
+ ->(method, owner, record) { owner.send(callback, record) }
+ when Proc
+ ->(method, owner, record) { callback.call(owner, record) }
else
- @scope = proc { extending(mod) }
+ ->(method, owner, record) { callback.send(method, owner, record) }
end
end
- end
-
- def extension_module_name
- @extension_module_name ||= "#{model.name.demodulize}#{name.to_s.camelize}AssociationExtension"
- end
-
- def define_callback(callback_name)
- full_callback_name = "#{callback_name}_for_#{name}"
-
- # TODO : why do i need method_defined? I think its because of the inheritance chain
- model.class_attribute full_callback_name.to_sym unless model.method_defined?(full_callback_name)
- model.send("#{full_callback_name}=", Array(options[callback_name.to_sym]))
+ model.send "#{full_callback_name}=", callbacks
end
# Defines the setter and getter methods for the collection_singular_ids.
-
- def define_readers
+ def self.define_readers(mixin, name)
super
mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
@@ -80,7 +68,7 @@ module ActiveRecord::Associations::Builder
CODE
end
- def define_writers
+ def self.define_writers(mixin, name)
super
mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
@@ -89,5 +77,15 @@ module ActiveRecord::Associations::Builder
end
CODE
end
+
+ private
+
+ def wrap_scope(scope, mod)
+ if scope
+ proc { |owner| instance_exec(owner, &scope).extending(mod) }
+ else
+ proc { extending(mod) }
+ end
+ end
end
end
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 bdac02b5bf..e472277374 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,39 +1,121 @@
module ActiveRecord::Associations::Builder
- class HasAndBelongsToMany < CollectionAssociation #:nodoc:
- def macro
- :has_and_belongs_to_many
- end
+ class HasAndBelongsToMany # :nodoc:
+ class JoinTableResolver
+ KnownTable = Struct.new :join_table
+
+ class KnownClass
+ def initialize(lhs_class, rhs_class_name)
+ @lhs_class = lhs_class
+ @rhs_class_name = rhs_class_name
+ @join_table = nil
+ end
+
+ def join_table
+ @join_table ||= [@lhs_class.table_name, klass.table_name].sort.join("\0").gsub(/^(.*_)(.+)\0\1(.+)/, '\1\2_\3').gsub("\0", "_")
+ end
+
+ private
+ def klass; @rhs_class_name.constantize; end
+ end
- def valid_options
- super + [:join_table, :association_foreign_key, :delete_sql, :insert_sql]
+ def self.build(lhs_class, name, options)
+ if options[:join_table]
+ KnownTable.new options[:join_table].to_s
+ else
+ class_name = options.fetch(:class_name) {
+ name.to_s.camelize.singularize
+ }
+ KnownClass.new lhs_class, class_name
+ end
+ end
end
- def build
- reflection = super
- define_destroy_hook
- reflection
+ attr_reader :lhs_model, :association_name, :options
+
+ def initialize(association_name, lhs_model, options)
+ @association_name = association_name
+ @lhs_model = lhs_model
+ @options = options
end
- def show_deprecation_warnings
- super
+ def through_model
+ habtm = JoinTableResolver.build lhs_model, association_name, options
+
+ join_model = Class.new(ActiveRecord::Base) {
+ class << self;
+ attr_accessor :class_resolver
+ attr_accessor :name
+ attr_accessor :table_name_resolver
+ attr_accessor :left_reflection
+ attr_accessor :right_reflection
+ end
+
+ def self.table_name
+ table_name_resolver.join_table
+ end
+
+ def self.compute_type(class_name)
+ class_resolver.compute_type class_name
+ end
- [:delete_sql, :insert_sql].each do |name|
- if options.include? name
- ActiveSupport::Deprecation.warn("The :#{name} association option is deprecated. Please find an alternative (such as using has_many :through).")
+ def self.add_left_association(name, options)
+ belongs_to name, 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
+ self.right_reflection = reflect_on_association(rhs_name)
+ end
+
+ }
+
+ join_model.name = "HABTM_#{association_name.to_s.camelize}"
+ join_model.table_name_resolver = habtm
+ join_model.class_resolver = lhs_model
+
+ join_model.add_left_association :left_side, class: lhs_model
+ join_model.add_right_association association_name, belongs_to_options(options)
+ join_model
+ end
+
+ def middle_reflection(join_model)
+ middle_name = [lhs_model.name.downcase.pluralize,
+ association_name].join('_').gsub(/::/, '_').to_sym
+ middle_options = middle_options join_model
+ hm_builder = HasMany.create_builder(lhs_model,
+ middle_name,
+ nil,
+ middle_options)
+ hm_builder.build lhs_model
+ end
+
+ private
+
+ def middle_options(join_model)
+ middle_options = {}
+ middle_options[:class] = join_model
+ middle_options[:source] = join_model.left_reflection.name
+ if options.key? :foreign_key
+ middle_options[:foreign_key] = options[:foreign_key]
end
+ middle_options
end
- def define_destroy_hook
- name = self.name
- model.send(:include, Module.new {
- class_eval <<-RUBY, __FILE__, __LINE__ + 1
- def destroy_associations
- association(:#{name}).delete_all
- super
- end
- RUBY
- })
+ def belongs_to_options(options)
+ rhs_options = {}
+
+ if options.key? :class_name
+ rhs_options[:foreign_key] = options[:class_name].foreign_key
+ rhs_options[:class_name] = options[:class_name]
+ end
+
+ if options.key? :association_foreign_key
+ rhs_options[:foreign_key] = options[:association_foreign_key]
+ end
+
+ rhs_options
end
end
end
diff --git a/activerecord/lib/active_record/associations/builder/has_many.rb b/activerecord/lib/active_record/associations/builder/has_many.rb
index 429def5455..7909b93622 100644
--- a/activerecord/lib/active_record/associations/builder/has_many.rb
+++ b/activerecord/lib/active_record/associations/builder/has_many.rb
@@ -5,11 +5,11 @@ module ActiveRecord::Associations::Builder
end
def valid_options
- super + [:primary_key, :dependent, :as, :through, :source, :source_type, :inverse_of, :automatic_inverse_of, :counter_cache]
+ super + [:primary_key, :dependent, :as, :through, :source, :source_type, :inverse_of, :counter_cache]
end
- def valid_dependent_options
- [:destroy, :delete_all, :nullify, :restrict, :restrict_with_error, :restrict_with_exception]
+ def self.valid_dependent_options
+ [:destroy, :delete_all, :nullify, :restrict_with_error, :restrict_with_exception]
end
end
end
diff --git a/activerecord/lib/active_record/associations/builder/has_one.rb b/activerecord/lib/active_record/associations/builder/has_one.rb
index 0da564f402..f359efd496 100644
--- a/activerecord/lib/active_record/associations/builder/has_one.rb
+++ b/activerecord/lib/active_record/associations/builder/has_one.rb
@@ -10,16 +10,14 @@ module ActiveRecord::Associations::Builder
valid
end
- def constructable?
- !options[:through]
+ def self.valid_dependent_options
+ [:destroy, :delete, :nullify, :restrict_with_error, :restrict_with_exception]
end
- def configure_dependency
- super unless options[:through]
- end
+ private
- def valid_dependent_options
- [:destroy, :delete, :nullify, :restrict, :restrict_with_error, :restrict_with_exception]
+ def self.add_before_destroy_callbacks(model, reflection)
+ super unless reflection.options[:through]
end
end
end
diff --git a/activerecord/lib/active_record/associations/builder/singular_association.rb b/activerecord/lib/active_record/associations/builder/singular_association.rb
index 96ccbeb8a3..e655c389a6 100644
--- a/activerecord/lib/active_record/associations/builder/singular_association.rb
+++ b/activerecord/lib/active_record/associations/builder/singular_association.rb
@@ -1,23 +1,18 @@
-# This class is inherited by the has_one and belongs_to association classes
+# This class is inherited by the has_one and belongs_to association classes
module ActiveRecord::Associations::Builder
class SingularAssociation < Association #:nodoc:
def valid_options
- super + [:remote, :dependent, :counter_cache, :primary_key, :inverse_of, :automatic_inverse_of]
+ super + [:remote, :dependent, :primary_key, :inverse_of]
end
- def constructable?
- true
- end
-
- def define_accessors
+ def self.define_accessors(model, reflection)
super
- define_constructors if constructable?
+ define_constructors(model.generated_association_methods, reflection.name) if reflection.constructable?
end
# Defines the (build|create)_association methods for belongs_to or has_one association
-
- def define_constructors
+ def self.define_constructors(mixin, name)
mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
def build_#{name}(*args, &block)
association(:#{name}).build(*args, &block)
diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb
index efd7ecb97c..52531a3520 100644
--- a/activerecord/lib/active_record/associations/collection_association.rb
+++ b/activerecord/lib/active_record/associations/collection_association.rb
@@ -4,10 +4,9 @@ module ActiveRecord
#
# CollectionAssociation is an abstract class that provides common stuff to
# ease the implementation of association proxies that represent
- # collections. See the class hierarchy in AssociationProxy.
+ # collections. See the class hierarchy in Association.
#
# CollectionAssociation:
- # HasAndBelongsToManyAssociation => has_and_belongs_to_many
# HasManyAssociation => has_many
# HasManyThroughAssociation + ThroughAssociation => has_many :through
#
@@ -34,7 +33,7 @@ module ActiveRecord
reload
end
- @proxy ||= CollectionProxy.new(klass, self)
+ @proxy ||= CollectionProxy.create(klass, self)
end
# Implements the writer method, e.g. foo.items= for Foo.has_many :items
@@ -44,7 +43,7 @@ module ActiveRecord
# Implements the ids reader method, e.g. foo.item_ids for Foo.has_many :items
def ids_reader
- if loaded? || options[:finder_sql]
+ if loaded?
load_target.map do |record|
record.send(reflection.association_primary_key)
end
@@ -67,11 +66,11 @@ module ActiveRecord
@target = []
end
- def select(select = nil)
+ def select(*fields)
if block_given?
load_target.select.each { |e| yield e }
else
- scope.select(select)
+ scope.select(*fields)
end
end
@@ -79,17 +78,14 @@ module ActiveRecord
if block_given?
load_target.find(*args) { |*block_args| yield(*block_args) }
else
- if options[:finder_sql]
- find_by_scan(*args)
- elsif options[:inverse_of]
- args = args.flatten
- raise RecordNotFound, "Couldn't find #{scope.klass.name} without an ID" if args.blank?
-
+ if options[:inverse_of] && loaded?
+ args_flatten = args.flatten
+ raise RecordNotFound, "Couldn't find #{scope.klass.name} without an ID" if args_flatten.blank?
result = find_by_scan(*args)
result_size = Array(result).size
- if !result || result_size != args.size
- scope.raise_record_not_found_exception!(args, result_size, args.size)
+ if !result || result_size != args_flatten.size
+ scope.raise_record_not_found_exception!(args_flatten, result_size, args_flatten.size)
else
result
end
@@ -153,11 +149,33 @@ module ActiveRecord
end
end
- # Remove all records from this association.
+ # Removes all records from the association without calling callbacks
+ # on the associated records. It honors the `:dependent` option. However
+ # if the `:dependent` value is `:destroy` then in that case the `:delete_all`
+ # deletion strategy for the association is applied.
+ #
+ # You can force a particular deletion strategy by passing a parameter.
+ #
+ # Example:
+ #
+ # @author.books.delete_all(:nullify)
+ # @author.books.delete_all(:delete_all)
#
# See delete for more info.
- def delete_all
- delete(:all).tap do
+ def delete_all(dependent = nil)
+ if dependent.present? && ![:nullify, :delete_all].include?(dependent)
+ raise ArgumentError, "Valid values are :nullify or :delete_all"
+ end
+
+ dependent = if dependent.present?
+ dependent
+ elsif options[:dependent] == :destroy
+ :delete_all
+ else
+ options[:dependent]
+ end
+
+ delete(:all, dependent: dependent).tap do
reset
loaded!
end
@@ -173,36 +191,29 @@ module ActiveRecord
end
end
- # Count all records using SQL. If the +:counter_sql+ or +:finder_sql+ option is set for the
- # association, it will be used for the query. Otherwise, construct options and pass them with
+ # Count all records using SQL. Construct options and pass them with
# scope to the target class's +count+.
def count(column_name = nil, count_options = {})
+ # TODO: Remove count_options argument as soon we remove support to
+ # activerecord-deprecated_finders.
column_name, count_options = nil, column_name if column_name.is_a?(Hash)
- if options[:counter_sql] || options[:finder_sql]
- unless count_options.blank?
- raise ArgumentError, "If finder_sql/counter_sql is used then options cannot be passed"
- end
-
- reflection.klass.count_by_sql(custom_counter_sql)
- else
- relation = scope
- if association_scope.distinct_value
- # This is needed because 'SELECT count(DISTINCT *)..' is not valid SQL.
- column_name ||= reflection.klass.primary_key
- relation = relation.distinct
- end
+ relation = scope
+ if association_scope.distinct_value
+ # This is needed because 'SELECT count(DISTINCT *)..' is not valid SQL.
+ column_name ||= reflection.klass.primary_key
+ relation = relation.distinct
+ end
- value = relation.count(column_name)
+ value = relation.count(column_name)
- limit = options[:limit]
- offset = options[:offset]
+ limit = options[:limit]
+ offset = options[:offset]
- if limit || offset
- [ [value - offset.to_i, 0].max, limit.to_i ].min
- else
- value
- end
+ if limit || offset
+ [ [value - offset.to_i, 0].max, limit.to_i ].min
+ else
+ value
end
end
@@ -214,18 +225,10 @@ module ActiveRecord
# are actually removed from the database, that depends precisely on
# +delete_records+. They are in any case removed from the collection.
def delete(*records)
- dependent = options[:dependent]
+ _options = records.extract_options!
+ dependent = _options[:dependent] || options[:dependent]
if records.first == :all
-
- if dependent && dependent == :destroy
- message = 'In Rails 4.1 delete_all on associations would not fire callbacks. ' \
- 'It means if the :dependent option is :destroy then the associated ' \
- 'records would be deleted without loading and invoking callbacks.'
-
- ActiveRecord::Base.logger ? ActiveRecord::Base.logger.warn(message) : $stderr.puts(message)
- end
-
if loaded? || dependent == :destroy
delete_or_destroy(load_target, dependent)
else
@@ -285,14 +288,14 @@ module ActiveRecord
# Returns true if the collection is empty.
#
- # If the collection has been loaded or the <tt>:counter_sql</tt> option
- # is provided, it is equivalent to <tt>collection.size.zero?</tt>. If the
+ # If the collection has been loaded
+ # it is equivalent to <tt>collection.size.zero?</tt>. If the
# collection has not been loaded, it is equivalent to
# <tt>collection.exists?</tt>. If the collection has not already been
# loaded and you are going to fetch the records anyway it is better to
# check <tt>collection.length.zero?</tt>.
def empty?
- if loaded? || options[:counter_sql]
+ if loaded?
size.zero?
else
@target.blank? && !scope.exists?
@@ -345,7 +348,6 @@ module ActiveRecord
if record.new_record?
include_in_memory?(record)
else
- load_target if options[:finder_sql]
loaded? ? target.include?(record) : scope.exists?(record)
end
else
@@ -362,8 +364,8 @@ module ActiveRecord
target
end
- def add_to_target(record)
- callback(:before_add, record)
+ def add_to_target(record, skip_callbacks = false)
+ callback(:before_add, record) unless skip_callbacks
yield(record) if block_given?
if association_scope.distinct_value && index = @target.index(record)
@@ -372,7 +374,7 @@ module ActiveRecord
@target << record
end
- callback(:after_add, record)
+ callback(:after_add, record) unless skip_callbacks
set_inverse_instance(record)
record
@@ -390,31 +392,8 @@ module ActiveRecord
private
- def custom_counter_sql
- if options[:counter_sql]
- interpolate(options[:counter_sql])
- else
- # replace the SELECT clause with COUNT(SELECTS), preserving any hints within /* ... */
- interpolate(options[:finder_sql]).sub(/SELECT\b(\/\*.*?\*\/ )?(.*)\bFROM\b/im) do
- count_with = $2.to_s
- count_with = '*' if count_with.blank? || count_with =~ /,/ || count_with =~ /\.\*/
- "SELECT #{$1}COUNT(#{count_with}) FROM"
- end
- end
- end
-
- def custom_finder_sql
- interpolate(options[:finder_sql])
- end
-
def find_target
- records =
- if options[:finder_sql]
- reflection.klass.find_by_sql(custom_finder_sql)
- else
- scope.to_a
- end
-
+ records = scope.to_a
records.each { |record| set_inverse_instance(record) }
records
end
@@ -529,20 +508,13 @@ module ActiveRecord
def callback(method, record)
callbacks_for(method).each do |callback|
- case callback
- when Symbol
- owner.send(callback, record)
- when Proc
- callback.call(owner, record)
- else
- callback.send(method, owner, record)
- end
+ callback.call(method, owner, record)
end
end
def callbacks_for(callback_name)
full_callback_name = "#{callback_name}_for_#{reflection.name}"
- owner.class.send(full_callback_name.to_sym) || []
+ owner.class.send(full_callback_name)
end
# Should we deal with assoc.first or assoc.last by issuing an independent query to
@@ -553,24 +525,21 @@ module ActiveRecord
# Otherwise, go to the database only if none of the following are true:
# * target already loaded
# * owner is new record
- # * custom :finder_sql exists
# * target contains new or changed record(s)
- # * the first arg is an integer (which indicates the number of records to be returned)
def fetch_first_or_last_using_find?(args)
if args.first.is_a?(Hash)
true
else
!(loaded? ||
owner.new_record? ||
- options[:finder_sql] ||
- target.any? { |record| record.new_record? || record.changed? } ||
- args.first.kind_of?(Integer))
+ target.any? { |record| record.new_record? || record.changed? })
end
end
def include_in_memory?(record)
if reflection.is_a?(ActiveRecord::Reflection::ThroughReflection)
- owner.send(reflection.through_reflection.name).any? { |source|
+ assoc = owner.association(reflection.through_reflection.name)
+ assoc.reader.any? { |source|
target = source.send(reflection.source_reflection.name)
target.respond_to?(:include?) ? target.include?(record) : target == record
} || target.include?(record)
@@ -579,18 +548,18 @@ module ActiveRecord
end
end
- # If using a custom finder_sql or if the :inverse_of option has been
+ # If the :inverse_of option has been
# specified, then #find scans the entire collection.
def find_by_scan(*args)
expects_array = args.first.kind_of?(Array)
- ids = args.flatten.compact.map{ |arg| arg.to_i }.uniq
+ ids = args.flatten.compact.map{ |arg| arg.to_s }.uniq
if ids.size == 1
id = ids.first
- record = load_target.detect { |r| id == r.id }
+ record = load_target.detect { |r| id == r.id.to_s }
expects_array ? [ record ] : record
else
- load_target.select { |r| ids.include?(r.id) }
+ load_target.select { |r| ids.include?(r.id.to_s) }
end
end
diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb
index e82c195335..e3fc908444 100644
--- a/activerecord/lib/active_record/associations/collection_proxy.rb
+++ b/activerecord/lib/active_record/associations/collection_proxy.rb
@@ -33,7 +33,6 @@ module ActiveRecord
def initialize(klass, association) #:nodoc:
@association = association
super klass, klass.arel_table
- self.default_scoped = true
merge! association.scope(nullify: false)
end
@@ -76,7 +75,7 @@ module ActiveRecord
# # #<Pet id: nil, name: "Choo-Choo">
# # ]
#
- # person.pets.select([:id, :name])
+ # person.pets.select(:id, :name )
# # => [
# # #<Pet id: 1, name: "Fancy-Fancy">,
# # #<Pet id: 2, name: "Spook">,
@@ -107,13 +106,13 @@ module ActiveRecord
# # #<Pet id: 2, name: "Spook">,
# # #<Pet id: 3, name: "Choo-Choo">
# # ]
- def select(select = nil, &block)
- @association.select(select, &block)
+ def select(*fields, &block)
+ @association.select(*fields, &block)
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>
- # error if the object can not be found.
+ # error if the object cannot be found.
#
# class Person < ActiveRecord::Base
# has_many :pets
@@ -282,7 +281,7 @@ module ActiveRecord
# so method calls may be chained.
#
# class Person < ActiveRecord::Base
- # pets :has_many
+ # has_many :pets
# end
#
# person.pets.size # => 0
@@ -418,8 +417,8 @@ module ActiveRecord
#
# Pet.find(1, 2, 3)
# # => ActiveRecord::RecordNotFound
- def delete_all
- @association.delete_all
+ def delete_all(dependent = nil)
+ @association.delete_all(dependent)
end
# Deletes the records of the collection directly from the database
@@ -671,6 +670,8 @@ module ActiveRecord
# # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
# # ]
def count(column_name = nil, options = {})
+ # TODO: Remove options argument as soon we remove support to
+ # activerecord-deprecated_finders.
@association.count(column_name, options)
end
@@ -727,7 +728,7 @@ module ActiveRecord
end
# Returns +true+ if the collection is empty. If the collection has been
- # loaded or the <tt>:counter_sql</tt> option is provided, it is equivalent
+ # loaded it is equivalent
# to <tt>collection.size.zero?</tt>. If the collection has not been loaded,
# it is equivalent to <tt>collection.exists?</tt>. If the collection has
# not already been loaded and you are going to fetch the records anyway it
@@ -788,12 +789,12 @@ module ActiveRecord
# has_many :pets
# end
#
- # person.pets.count #=> 1
- # person.pets.many? #=> false
+ # person.pets.count # => 1
+ # person.pets.many? # => false
#
# person.pets << Pet.new(name: 'Snoopy')
- # person.pets.count #=> 2
- # person.pets.many? #=> true
+ # person.pets.count # => 2
+ # person.pets.many? # => true
#
# You can also pass a block to define criteria. The
# behavior is the same, it returns true if the collection
@@ -830,7 +831,7 @@ module ActiveRecord
# person.pets.include?(Pet.find(20)) # => true
# person.pets.include?(Pet.find(21)) # => false
def include?(record)
- @association.include?(record)
+ !!@association.include?(record)
end
def proxy_association
@@ -849,8 +850,6 @@ module ActiveRecord
def scope
@association.scope
end
-
- # :nodoc:
alias spawn scope
# Equivalent to <tt>Array#==</tt>. Returns +true+ if the two arrays
diff --git a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb
deleted file mode 100644
index bb3e3db379..0000000000
--- a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb
+++ /dev/null
@@ -1,65 +0,0 @@
-module ActiveRecord
- # = Active Record Has And Belongs To Many Association
- module Associations
- class HasAndBelongsToManyAssociation < CollectionAssociation #:nodoc:
- attr_reader :join_table
-
- def initialize(owner, reflection)
- @join_table = Arel::Table.new(reflection.join_table)
- super
- end
-
- def insert_record(record, validate = true, raise = false)
- if record.new_record?
- if raise
- record.save!(:validate => validate)
- else
- return unless record.save(:validate => validate)
- end
- end
-
- if options[:insert_sql]
- owner.connection.insert(interpolate(options[:insert_sql], record))
- else
- stmt = join_table.compile_insert(
- join_table[reflection.foreign_key] => owner.id,
- join_table[reflection.association_foreign_key] => record.id
- )
-
- owner.class.connection.insert stmt
- end
-
- record
- end
-
- private
-
- def count_records
- load_target.size
- end
-
- def delete_records(records, method)
- if sql = options[:delete_sql]
- records = load_target if records == :all
- records.each { |record| owner.class.connection.delete(interpolate(sql, record)) }
- else
- relation = join_table
- condition = relation[reflection.foreign_key].eq(owner.id)
-
- unless records == :all
- condition = condition.and(
- relation[reflection.association_foreign_key]
- .in(records.map { |x| x.id }.compact)
- )
- end
-
- owner.class.connection.delete(relation.where(condition).compile_delete)
- end
- end
-
- def invertible_for?(record)
- false
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb
index 29fae809da..72e0891702 100644
--- a/activerecord/lib/active_record/associations/has_many_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_association.rb
@@ -9,7 +9,7 @@ module ActiveRecord
def handle_dependency
case options[:dependent]
- when :restrict, :restrict_with_exception
+ when :restrict_with_exception
raise ActiveRecord::DeleteRestrictionError.new(reflection.name) unless empty?
when :restrict_with_error
@@ -32,6 +32,7 @@ module ActiveRecord
def insert_record(record, validate = true, raise = false)
set_owner_attributes(record)
+ set_inverse_instance(record)
if raise
record.save!(:validate => validate)
@@ -58,8 +59,6 @@ module ActiveRecord
def count_records
count = if has_cached_counter?
owner.send(:read_attribute, cached_counter_attribute_name)
- elsif options[:counter_sql] || options[:finder_sql]
- reflection.klass.count_by_sql(custom_counter_sql)
else
scope.count
end
@@ -109,14 +108,13 @@ module ActiveRecord
# Deletes the records according to the <tt>:dependent</tt> option.
def delete_records(records, method)
if method == :destroy
- records.each { |r| r.destroy }
+ records.each(&:destroy!)
update_counter(-records.length) unless inverse_updates_counter_cache?
else
if records == :all
scope = self.scope
else
- keys = records.map { |r| r[reflection.association_primary_key] }
- scope = self.scope.where(reflection.association_primary_key => keys)
+ scope = self.scope.where(reflection.klass.primary_key => records)
end
if method == :delete_all
@@ -128,7 +126,11 @@ module ActiveRecord
end
def foreign_key_present?
- owner.attribute_present?(reflection.association_primary_key)
+ if reflection.klass.primary_key
+ owner.attribute_present?(reflection.association_primary_key)
+ else
+ false
+ end
end
end
end
diff --git a/activerecord/lib/active_record/associations/has_many_through_association.rb b/activerecord/lib/active_record/associations/has_many_through_association.rb
index a74dd1cdab..31b8d27892 100644
--- a/activerecord/lib/active_record/associations/has_many_through_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_through_association.rb
@@ -140,7 +140,21 @@ module ActiveRecord
case method
when :destroy
- count = scope.destroy_all.length
+ if scope.klass.primary_key
+ count = scope.destroy_all.length
+ else
+ scope.to_a.each do |record|
+ record.run_callbacks :destroy
+ end
+
+ arel = scope.arel
+
+ stmt = Arel::DeleteManager.new arel.engine
+ stmt.from scope.klass.arel_table
+ stmt.wheres = arel.constraints
+
+ count = scope.klass.connection.delete(stmt, 'SQL', scope.bind_values)
+ end
when :nullify
count = scope.update_all(source_reflection.foreign_key => nil)
else
@@ -149,7 +163,7 @@ module ActiveRecord
delete_through_records(records)
- if source_reflection.options[:counter_cache]
+ if source_reflection.options[:counter_cache] && method != :destroy
counter = source_reflection.counter_cache_column
klass.decrement_counter counter, records.map(&:id)
end
diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb
index 920038a543..944caacab6 100644
--- a/activerecord/lib/active_record/associations/has_one_association.rb
+++ b/activerecord/lib/active_record/associations/has_one_association.rb
@@ -6,7 +6,7 @@ module ActiveRecord
def handle_dependency
case options[:dependent]
- when :restrict, :restrict_with_exception
+ when :restrict_with_exception
raise ActiveRecord::DeleteRestrictionError.new(reflection.name) if load_target
when :restrict_with_error
@@ -26,15 +26,19 @@ module ActiveRecord
load_target
return self.target if !(target || record)
- if (target != record) || record.changed?
+
+ assigning_another_record = target != record
+ if assigning_another_record || record.changed?
+ save &&= owner.persisted?
+
transaction_if(save) do
- remove_target!(options[:dependent]) if target && !target.destroyed?
+ remove_target!(options[:dependent]) if target && !target.destroyed? && assigning_another_record
if record
set_owner_attributes(record)
set_inverse_instance(record)
- if owner.persisted? && save && !record.save
+ if save && !record.save
nullify_owner_attributes(record)
set_owner_attributes(target) if target
raise RecordNotSaved, "Failed to save the new associated #{reflection.name}."
diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb
index 5b2f2d1902..295dccf34e 100644
--- a/activerecord/lib/active_record/associations/join_dependency.rb
+++ b/activerecord/lib/active_record/associations/join_dependency.rb
@@ -1,15 +1,79 @@
module ActiveRecord
module Associations
class JoinDependency # :nodoc:
- autoload :JoinPart, 'active_record/associations/join_dependency/join_part'
autoload :JoinBase, 'active_record/associations/join_dependency/join_base'
autoload :JoinAssociation, 'active_record/associations/join_dependency/join_association'
- attr_reader :join_parts, :reflections, :alias_tracker, :base_klass
+ class Aliases # :nodoc:
+ def initialize(tables)
+ @tables = tables
+ @alias_cache = tables.each_with_object({}) { |table,h|
+ h[table.node] = table.columns.each_with_object({}) { |column,i|
+ i[column.name] = column.alias
+ }
+ }
+ @name_and_alias_cache = tables.each_with_object({}) { |table,h|
+ h[table.node] = table.columns.map { |column|
+ [column.name, column.alias]
+ }
+ }
+ end
+
+ def columns
+ @tables.flat_map { |t| t.column_aliases }
+ end
+
+ # An array of [column_name, alias] pairs for the table
+ def column_aliases(node)
+ @name_and_alias_cache[node]
+ end
+
+ def column_alias(node, column)
+ @alias_cache[node][column]
+ end
+
+ class Table < Struct.new(:node, :columns)
+ def table
+ Arel::Nodes::TableAlias.new node.table, node.aliased_table_name
+ end
+
+ def column_aliases
+ t = table
+ columns.map { |column| t[column.name].as Arel.sql column.alias }
+ end
+ end
+ Column = Struct.new(:name, :alias)
+ end
+
+ attr_reader :alias_tracker, :base_klass, :join_root
+
+ def self.make_tree(associations)
+ hash = {}
+ walk_tree associations, hash
+ hash
+ end
+
+ def self.walk_tree(associations, hash)
+ case associations
+ when Symbol, String
+ hash[associations.to_sym] ||= {}
+ when Array
+ associations.each do |assoc|
+ walk_tree assoc, hash
+ end
+ when Hash
+ associations.each do |k,v|
+ cache = hash[k] ||= {}
+ walk_tree v, cache
+ end
+ else
+ raise ConfigurationError, associations.inspect
+ end
+ end
# base is the base class on which operation is taking place.
# associations is the list of associations which are joined using hash, symbol or array.
- # joins is the list of all string join commnads and arel nodes.
+ # joins is the list of all string join commands and arel nodes.
#
# Example :
#
@@ -19,223 +83,191 @@ module ActiveRecord
# end
#
# If I execute `@physician.patients.to_a` then
- # base #=> Physician
- # associations #=> []
- # joins #=> [#<Arel::Nodes::InnerJoin: ...]
+ # base # => Physician
+ # associations # => []
+ # joins # => [#<Arel::Nodes::InnerJoin: ...]
#
# However if I execute `Physician.joins(:appointments).to_a` then
- # base #=> Physician
- # associations #=> [:appointments]
- # joins #=> []
+ # base # => Physician
+ # associations # => [:appointments]
+ # joins # => []
#
def initialize(base, associations, joins)
- @base_klass = base
- @table_joins = joins
- @join_parts = [JoinBase.new(base)]
- @associations = {}
- @reflections = []
@alias_tracker = AliasTracker.new(base.connection, joins)
@alias_tracker.aliased_name_for(base.table_name) # Updates the count for base.table_name to 1
- build(associations)
+ tree = self.class.make_tree associations
+ @join_root = JoinBase.new base, build(tree, base)
+ @join_root.children.each { |child| construct_tables! @join_root, child }
end
- def graft(*associations)
- associations.each do |association|
- join_associations.detect {|a| association == a} ||
- build(association.reflection.name, association.find_parent_in(self) || join_base, association.join_type)
- end
- self
+ def reflections
+ join_root.drop(1).map!(&:reflection)
end
- def join_associations
- join_parts.last(join_parts.length - 1)
- end
+ def join_constraints(outer_joins)
+ joins = join_root.children.flat_map { |child|
+ make_inner_joins join_root, child
+ }
- def join_base
- join_parts.first
+ joins.concat outer_joins.flat_map { |oj|
+ if join_root.match? oj.join_root
+ walk join_root, oj.join_root
+ else
+ oj.join_root.children.flat_map { |child|
+ make_outer_joins oj.join_root, child
+ }
+ end
+ }
end
- def join_relation(relation)
- join_associations.each do |association|
- relation = association.join_relation(relation)
- end
- relation
+ def aliases
+ Aliases.new join_root.each_with_index.map { |join_part,i|
+ columns = join_part.column_names.each_with_index.map { |column_name,j|
+ Aliases::Column.new column_name, "t#{i}_r#{j}"
+ }
+ Aliases::Table.new(join_part, columns)
+ }
end
- def columns
- join_parts.collect { |join_part|
- table = join_part.aliased_table
- join_part.column_names_with_alias.collect{ |column_name, aliased_name|
- table[column_name].as Arel.sql(aliased_name)
+ def instantiate(result_set, aliases)
+ primary_key = aliases.column_alias(join_root, join_root.primary_key)
+ type_caster = result_set.column_type 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] = {} }
}
- }.flatten
- end
+ }
- def instantiate(rows)
- primary_key = join_base.aliased_primary_key
- parents = {}
+ model_cache = Hash.new { |h,klass| h[klass] = {} }
+ parents = model_cache[join_root]
+ column_aliases = aliases.column_aliases join_root
- records = rows.map { |model|
- primary_id = model[primary_key]
- parent = parents[primary_id] ||= join_base.instantiate(model)
- construct(parent, @associations, join_associations, model)
- parent
- }.uniq
+ result_set.each { |row_hash|
+ primary_id = type_caster.type_cast row_hash[primary_key]
+ parent = parents[primary_id] ||= join_root.instantiate(row_hash, column_aliases)
+ construct(parent, join_root, row_hash, result_set, seen, model_cache, aliases)
+ }
- remove_duplicate_results!(base_klass, records, @associations)
- records
+ parents.values
end
- protected
-
- def remove_duplicate_results!(base, records, associations)
- case associations
- when Symbol, String
- reflection = base.reflections[associations]
- remove_uniq_by_reflection(reflection, records)
- when Array
- associations.each do |association|
- remove_duplicate_results!(base, records, association)
- end
- when Hash
- associations.each_key do |name|
- reflection = base.reflections[name]
- remove_uniq_by_reflection(reflection, records)
-
- parent_records = []
- records.each do |record|
- if descendant = record.send(reflection.name)
- if reflection.collection?
- parent_records.concat descendant.target.uniq
- else
- parent_records << descendant
- end
- end
- end
+ private
- remove_duplicate_results!(reflection.klass, parent_records, associations[name]) unless parent_records.empty?
- end
- end
+ def make_constraints(parent, child, tables, join_type)
+ chain = child.reflection.chain
+ foreign_table = parent.table
+ foreign_klass = parent.base_klass
+ child.join_constraints(foreign_table, foreign_klass, child, join_type, tables, child.reflection.scope_chain, chain)
end
- def cache_joined_association(association)
- associations = []
- parent = association.parent
- while parent != join_base
- associations.unshift(parent.reflection.name)
- parent = parent.parent
- end
- ref = @associations
- associations.each do |key|
- ref = ref[key]
- end
- ref[association.reflection.name] ||= {}
- end
+ def make_outer_joins(parent, child)
+ tables = table_aliases_for(parent, child)
+ join_type = Arel::OuterJoin
+ joins = make_constraints parent, child, tables, join_type
- def build(associations, parent = nil, join_type = Arel::InnerJoin)
- parent ||= join_parts.last
- case associations
- when Symbol, String
- reflection = parent.reflections[associations.intern] or
- raise ConfigurationError, "Association named '#{ associations }' was not found on #{ parent.base_klass.name }; perhaps you misspelled it?"
- unless join_association = find_join_association(reflection, parent)
- @reflections << reflection
- join_association = build_join_association(reflection, parent)
- join_association.join_type = join_type
- @join_parts << join_association
- cache_joined_association(join_association)
- end
- join_association
- when Array
- associations.each do |association|
- build(association, parent, join_type)
- end
- when Hash
- associations.keys.sort_by { |a| a.to_s }.each do |name|
- join_association = build(name, parent, join_type)
- build(associations[name], join_association, join_type)
- end
- else
- raise ConfigurationError, associations.inspect
- end
+ joins.concat child.children.flat_map { |c| make_outer_joins(child, c) }
end
- def find_join_association(name_or_reflection, parent)
- if String === name_or_reflection
- name_or_reflection = name_or_reflection.to_sym
- end
+ def make_inner_joins(parent, child)
+ tables = child.tables
+ join_type = Arel::InnerJoin
+ joins = make_constraints parent, child, tables, join_type
- join_associations.detect { |j|
- j.reflection == name_or_reflection && j.parent == parent
+ joins.concat child.children.flat_map { |c| make_inner_joins(child, c) }
+ end
+
+ def table_aliases_for(parent, node)
+ node.reflection.chain.map { |reflection|
+ alias_tracker.aliased_table_for(
+ reflection.table_name,
+ table_alias_for(reflection, parent, reflection != node.reflection)
+ )
}
end
- def remove_uniq_by_reflection(reflection, records)
- if reflection && reflection.collection?
- records.each { |record| record.send(reflection.name).target.uniq! }
- end
+ def construct_tables!(parent, node)
+ node.tables = table_aliases_for(parent, node)
+ node.children.each { |child| construct_tables! node, child }
end
- def build_join_association(reflection, parent)
- JoinAssociation.new(reflection, self, parent)
+ def table_alias_for(reflection, parent, join)
+ name = "#{reflection.plural_name}_#{parent.table_name}"
+ name << "_join" if join
+ name
end
- def construct(parent, associations, join_parts, row)
- case associations
- when Symbol, String
- name = associations.to_s
+ def walk(left, right)
+ intersection, missing = right.children.map { |node1|
+ [left.children.find { |node2| node1.match? node2 }, node1]
+ }.partition(&:first)
- join_part = join_parts.detect { |j|
- j.reflection.name.to_s == name &&
- j.parent_table_name == parent.class.table_name }
+ ojs = missing.flat_map { |_,n| make_outer_joins left, n }
+ intersection.flat_map { |l,r| walk l, r }.concat ojs
+ end
- raise(ConfigurationError, "No such association") unless join_part
+ def find_reflection(klass, name)
+ klass.reflect_on_association(name) or
+ raise ConfigurationError, "Association named '#{ name }' was not found on #{ klass.name }; perhaps you misspelled it?"
+ end
- join_parts.delete(join_part)
- construct_association(parent, join_part, row)
- when Array
- associations.each do |association|
- construct(parent, association, join_parts, row)
- end
- when Hash
- associations.sort_by { |k,_| k.to_s }.each do |association_name, assoc|
- association = construct(parent, association_name, join_parts, row)
- construct(association, assoc, join_parts, row) if association
+ def build(associations, base_klass)
+ associations.map do |name, right|
+ reflection = find_reflection base_klass, name
+ reflection.check_validity!
+
+ if reflection.options[:polymorphic]
+ raise EagerLoadPolymorphicError.new(reflection)
end
- else
- raise ConfigurationError, associations.inspect
+
+ JoinAssociation.new reflection, build(right, reflection.klass)
end
end
- def construct_association(record, join_part, row)
- return if record.id.to_s != join_part.parent.record_id(row).to_s
+ def construct(ar_parent, parent, row, rs, seen, model_cache, aliases)
+ primary_id = ar_parent.id
- macro = join_part.reflection.macro
- if macro == :has_one
- return record.association(join_part.reflection.name).target if record.association_cache.key?(join_part.reflection.name)
- association = join_part.instantiate(row) unless row[join_part.aliased_primary_key].nil?
- set_target_and_inverse(join_part, association, record)
- else
- association = join_part.instantiate(row) unless row[join_part.aliased_primary_key].nil?
- case macro
- when :has_many, :has_and_belongs_to_many
- other = record.association(join_part.reflection.name)
+ parent.children.each do |node|
+ if node.reflection.collection?
+ other = ar_parent.association(node.reflection.name)
other.loaded!
- other.target.push(association) if association
- other.set_inverse_instance(association)
- when :belongs_to
- set_target_and_inverse(join_part, association, record)
else
- raise ConfigurationError, "unknown macro: #{join_part.reflection.macro}"
+ if ar_parent.association_cache.key?(node.reflection.name)
+ model = ar_parent.association(node.reflection.name).target
+ construct(model, node, row, rs, seen, model_cache, aliases)
+ next
+ end
+ end
+
+ key = aliases.column_alias(node, node.primary_key)
+ id = row[key]
+ next if id.nil?
+
+ model = seen[parent.base_klass][primary_id][node.base_klass][id]
+
+ if model
+ construct(model, node, row, rs, seen, model_cache, aliases)
+ else
+ model = construct_model(ar_parent, node, row, model_cache, id, aliases)
+ seen[parent.base_klass][primary_id][node.base_klass][id] = model
+ construct(model, node, row, rs, seen, model_cache, aliases)
end
end
- association
end
- def set_target_and_inverse(join_part, association, record)
- other = record.association(join_part.reflection.name)
- other.target = association
- other.set_inverse_instance(association)
+ def construct_model(record, node, row, model_cache, id, aliases)
+ model = model_cache[node][id] ||= node.instantiate(row,
+ aliases.column_aliases(node))
+ other = record.association(node.reflection.name)
+
+ if node.reflection.collection?
+ other.target.push(model)
+ else
+ other.target = model
+ end
+
+ other.set_inverse_instance(model)
+ model
end
end
end
diff --git a/activerecord/lib/active_record/associations/join_dependency/join_association.rb b/activerecord/lib/active_record/associations/join_dependency/join_association.rb
index 33b363339c..84e18684d8 100644
--- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb
+++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb
@@ -1,132 +1,80 @@
+require 'active_record/associations/join_dependency/join_part'
+
module ActiveRecord
module Associations
class JoinDependency # :nodoc:
class JoinAssociation < JoinPart # :nodoc:
- include JoinHelper
-
# The reflection of the association represented
attr_reader :reflection
- # The JoinDependency object which this JoinAssociation exists within. This is mainly
- # relevant for generating aliases which do not conflict with other joins which are
- # part of the query.
- attr_reader :join_dependency
-
- # A JoinBase instance representing the active record we are joining onto.
- # (So in Author.has_many :posts, the Author would be that base record.)
- attr_reader :parent
-
- # What type of join will be generated, either Arel::InnerJoin (default) or Arel::OuterJoin
- attr_accessor :join_type
-
- # These implement abstract methods from the superclass
- attr_reader :aliased_prefix
-
- attr_reader :tables
-
- delegate :options, :through_reflection, :source_reflection, :chain, :to => :reflection
- delegate :table, :table_name, :to => :parent, :prefix => :parent
- delegate :alias_tracker, :to => :join_dependency
-
- alias :alias_suffix :parent_table_name
+ attr_accessor :tables
- def initialize(reflection, join_dependency, parent = nil)
- reflection.check_validity!
-
- if reflection.options[:polymorphic]
- raise EagerLoadPolymorphicError.new(reflection)
- end
-
- super(reflection.klass)
+ def initialize(reflection, children)
+ super(reflection.klass, children)
@reflection = reflection
- @join_dependency = join_dependency
- @parent = parent
- @join_type = Arel::InnerJoin
- @aliased_prefix = "t#{ join_dependency.join_parts.size }"
- @tables = construct_tables.reverse
+ @tables = nil
end
- def ==(other)
- other.class == self.class &&
- other.reflection == reflection &&
- other.parent == parent
+ def match?(other)
+ return true if self == other
+ super && reflection == other.reflection
end
- def find_parent_in(other_join_dependency)
- other_join_dependency.join_parts.detect do |join_part|
- case parent
- when JoinBase
- parent.base_klass == join_part.base_klass
- else
- parent == join_part
- end
- end
- end
+ def join_constraints(foreign_table, foreign_klass, node, join_type, tables, scope_chain, chain)
+ joins = []
+ tables = tables.reverse
- def join_to(manager, relation)
- tables = @tables.dup
- foreign_table = parent_table
- foreign_klass = parent.base_klass
+ scope_chain_iter = scope_chain.reverse_each
# The chain starts with the target table, but we want to end with it here (makes
# more sense in this context), so we reverse
- chain.reverse.each_with_index do |reflection, i|
+ chain.reverse_each do |reflection|
table = tables.shift
+ klass = reflection.klass
case reflection.source_macro
when :belongs_to
key = reflection.association_primary_key
foreign_key = reflection.foreign_key
- when :has_and_belongs_to_many
- # Join the join table first...
- manager.from(join(
- table,
- table[reflection.foreign_key].
- eq(foreign_table[reflection.active_record_primary_key])
- ))
-
- foreign_table, table = table, tables.shift
-
- key = reflection.association_primary_key
- foreign_key = reflection.association_foreign_key
else
key = reflection.foreign_key
foreign_key = reflection.active_record_primary_key
end
- constraint = build_constraint(reflection, table, key, foreign_table, foreign_key)
+ constraint = build_constraint(klass, table, key, foreign_table, foreign_key)
- scope_chain_items = scope_chain[i]
+ scope_chain_items = scope_chain_iter.next.map do |item|
+ if item.is_a?(Relation)
+ item
+ else
+ ActiveRecord::Relation.create(klass, table).instance_exec(node, &item)
+ end
+ end
if reflection.type
- scope_chain_items += [
- ActiveRecord::Relation.new(reflection.klass, table)
+ scope_chain_items <<
+ ActiveRecord::Relation.create(klass, table)
.where(reflection.type => foreign_klass.base_class.name)
- ]
end
- constraint = scope_chain_items.inject(constraint) do |chain, item|
- unless item.is_a?(Relation)
- item = ActiveRecord::Relation.new(reflection.klass, table).instance_exec(self, &item)
- end
+ scope_chain_items.concat [klass.send(:build_default_scope)].compact
- relation.bind_values += item.bind_values
+ rel = scope_chain_items.inject(scope_chain_items.shift) do |left, right|
+ left.merge right
+ end
- if item.arel.constraints.empty?
- chain
- else
- chain.and(item.arel.constraints)
- end
+ if rel && !rel.arel.constraints.empty?
+ constraint = constraint.and rel.arel.constraints
end
- manager.from(join(table, constraint))
+ joins << table.create_join(table, table.create_on(constraint), join_type)
# The current table in this iteration becomes the foreign table in the next
- foreign_table, foreign_klass = table, reflection.klass
+ foreign_table, foreign_klass = table, klass
end
- manager
+ joins
end
# Builds equality condition.
@@ -138,42 +86,32 @@ module ActiveRecord
# end
#
# If I execute `Physician.joins(:appointments).to_a` then
- # reflection #=> #<ActiveRecord::Reflection::AssociationReflection @macro=:has_many ...>
- # table #=> #<Arel::Table @name="appointments" ...>
- # key #=> physician_id
- # foreign_table #=> #<Arel::Table @name="physicians" ...>
- # foreign_key #=> id
+ # reflection # => #<ActiveRecord::Reflection::AssociationReflection @macro=:has_many ...>
+ # table # => #<Arel::Table @name="appointments" ...>
+ # key # => physician_id
+ # foreign_table # => #<Arel::Table @name="physicians" ...>
+ # foreign_key # => id
#
- def build_constraint(reflection, table, key, foreign_table, foreign_key)
+ def build_constraint(klass, table, key, foreign_table, foreign_key)
constraint = table[key].eq(foreign_table[foreign_key])
- if reflection.klass.finder_needs_type_condition?
+ if klass.finder_needs_type_condition?
constraint = table.create_and([
constraint,
- reflection.klass.send(:type_condition, table)
+ klass.send(:type_condition, table)
])
end
constraint
end
- def join_relation(joining_relation)
- self.join_type = Arel::OuterJoin
- joining_relation.joins(self)
- end
-
def table
- tables.last
+ tables.first
end
def aliased_table_name
table.table_alias || table.name
end
-
- def scope_chain
- @scope_chain ||= reflection.scope_chain.reverse
- end
-
end
end
end
diff --git a/activerecord/lib/active_record/associations/join_dependency/join_base.rb b/activerecord/lib/active_record/associations/join_dependency/join_base.rb
index a7dacdbfd6..3a26c25737 100644
--- a/activerecord/lib/active_record/associations/join_dependency/join_base.rb
+++ b/activerecord/lib/active_record/associations/join_dependency/join_base.rb
@@ -1,18 +1,16 @@
+require 'active_record/associations/join_dependency/join_part'
+
module ActiveRecord
module Associations
class JoinDependency # :nodoc:
class JoinBase < JoinPart # :nodoc:
- def ==(other)
- other.class == self.class &&
- other.base_klass == base_klass
- end
-
- def aliased_prefix
- "t0"
+ def match?(other)
+ return true if self == other
+ super && base_klass == other.base_klass
end
def table
- Arel::Table.new(table_name, arel_engine)
+ base_klass.arel_table
end
def aliased_table_name
diff --git a/activerecord/lib/active_record/associations/join_dependency/join_part.rb b/activerecord/lib/active_record/associations/join_dependency/join_part.rb
index b534569063..91e1c6a9d7 100644
--- a/activerecord/lib/active_record/associations/join_dependency/join_part.rb
+++ b/activerecord/lib/active_record/associations/join_dependency/join_part.rb
@@ -8,34 +8,36 @@ module ActiveRecord
# operations (for example a has_and_belongs_to_many JoinAssociation would result in
# two; one for the join table and one for the target table).
class JoinPart # :nodoc:
+ include Enumerable
+
# The Active Record class which this join part is associated 'about'; for a JoinBase
# this is the actual base model, for a JoinAssociation this is the target model of the
# association.
- attr_reader :base_klass
+ attr_reader :base_klass, :children
- delegate :table_name, :column_names, :primary_key, :reflections, :arel_engine, :to => :base_klass
+ delegate :table_name, :column_names, :primary_key, :to => :base_klass
- def initialize(base_klass)
+ def initialize(base_klass, children)
@base_klass = base_klass
- @cached_record = {}
@column_names_with_alias = nil
+ @children = children
end
- def aliased_table
- Arel::Nodes::TableAlias.new table, aliased_table_name
+ def name
+ reflection.name
end
- def ==(other)
- raise NotImplementedError
+ def match?(other)
+ self.class == other.class
end
- # An Arel::Table for the active_record
- def table
- raise NotImplementedError
+ def each(&block)
+ yield self
+ children.each { |child| child.each(&block) }
end
- # The prefix to be used when aliasing columns in the active_record's table
- def aliased_prefix
+ # An Arel::Table for the active_record
+ def table
raise NotImplementedError
end
@@ -44,33 +46,25 @@ module ActiveRecord
raise NotImplementedError
end
- # The alias for the primary key of the active_record's table
- def aliased_primary_key
- "#{aliased_prefix}_r0"
- end
+ def extract_record(row, column_names_with_alias)
+ # This code is performance critical as it is called per row.
+ # see: https://github.com/rails/rails/pull/12185
+ hash = {}
- # An array of [column_name, alias] pairs for the table
- def column_names_with_alias
- unless @column_names_with_alias
- @column_names_with_alias = []
+ index = 0
+ length = column_names_with_alias.length
- ([primary_key] + (column_names - [primary_key])).compact.each_with_index do |column_name, i|
- @column_names_with_alias << [column_name, "#{aliased_prefix}_r#{i}"]
- end
+ while index < length
+ column_name, alias_name = column_names_with_alias[index]
+ hash[column_name] = row[alias_name]
+ index += 1
end
- @column_names_with_alias
- end
-
- def extract_record(row)
- Hash[column_names_with_alias.map{|cn, an| [cn, row[an]]}]
- end
- def record_id(row)
- row[aliased_primary_key]
+ hash
end
- def instantiate(row)
- @cached_record[record_id(row)] ||= base_klass.instantiate(extract_record(row))
+ def instantiate(row, aliases)
+ base_klass.instantiate(extract_record(row, aliases))
end
end
end
diff --git a/activerecord/lib/active_record/associations/join_helper.rb b/activerecord/lib/active_record/associations/join_helper.rb
index 5a41b40c8f..f345d16841 100644
--- a/activerecord/lib/active_record/associations/join_helper.rb
+++ b/activerecord/lib/active_record/associations/join_helper.rb
@@ -10,21 +10,12 @@ module ActiveRecord
private
def construct_tables
- tables = []
- chain.each do |reflection|
- tables << alias_tracker.aliased_table_for(
+ chain.map do |reflection|
+ alias_tracker.aliased_table_for(
table_name_for(reflection),
table_alias_for(reflection, reflection != self.reflection)
)
-
- if reflection.source_macro == :has_and_belongs_to_many
- tables << alias_tracker.aliased_table_for(
- (reflection.source_reflection || reflection).join_table,
- table_alias_for(reflection, true)
- )
- end
end
- tables
end
def table_name_for(reflection)
diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb
index 82bf426b22..e49fc5d5c4 100644
--- a/activerecord/lib/active_record/associations/preloader.rb
+++ b/activerecord/lib/active_record/associations/preloader.rb
@@ -42,12 +42,9 @@ module ActiveRecord
autoload :HasManyThrough, 'active_record/associations/preloader/has_many_through'
autoload :HasOne, 'active_record/associations/preloader/has_one'
autoload :HasOneThrough, 'active_record/associations/preloader/has_one_through'
- autoload :HasAndBelongsToMany, 'active_record/associations/preloader/has_and_belongs_to_many'
autoload :BelongsTo, 'active_record/associations/preloader/belongs_to'
end
- attr_reader :records, :associations, :preload_scope, :model
-
# Eager loads the named associations for the given Active Record record(s).
#
# In this description, 'association name' shall refer to the name passed
@@ -82,38 +79,47 @@ module ActiveRecord
# [ :books, :author ]
# { author: :avatar }
# [ :books, { author: :avatar } ]
- def initialize(records, associations, preload_scope = nil)
- @records = Array.wrap(records).compact.uniq
- @associations = Array.wrap(associations)
- @preload_scope = preload_scope || Relation.new(nil, nil)
- end
- def run
- unless records.empty?
- associations.each { |association| preload(association) }
+ NULL_RELATION = Struct.new(:values, :bind_values).new({}, [])
+
+ def preload(records, associations, preload_scope = nil)
+ records = Array.wrap(records).compact.uniq
+ associations = Array.wrap(associations)
+ preload_scope = preload_scope || NULL_RELATION
+
+ if records.empty?
+ []
+ else
+ associations.flat_map { |association|
+ preloaders_on association, records, preload_scope
+ }
end
end
private
- def preload(association)
+ def preloaders_on(association, records, scope)
case association
when Hash
- preload_hash(association)
+ preloaders_for_hash(association, records, scope)
when Symbol
- preload_one(association)
+ preloaders_for_one(association, records, scope)
when String
- preload_one(association.to_sym)
+ preloaders_for_one(association.to_sym, records, scope)
else
raise ArgumentError, "#{association.inspect} was not recognised for preload"
end
end
- def preload_hash(association)
- association.each do |parent, child|
- Preloader.new(records, parent, preload_scope).run
- Preloader.new(records.map { |record| record.send(parent) }.flatten, child).run
- end
+ def preloaders_for_hash(association, records, scope)
+ parent, child = association.to_a.first # hash should only be of length 1
+
+ loaders = preloaders_for_one parent, records, scope
+
+ recs = loaders.flat_map(&:preloaded_records).uniq
+ loaders.concat Array.wrap(child).flat_map { |assoc|
+ preloaders_on assoc, recs, scope
+ }
end
# Not all records have the same class, so group then preload group on the reflection
@@ -123,52 +129,81 @@ module ActiveRecord
# Additionally, polymorphic belongs_to associations can have multiple associated
# classes, depending on the polymorphic_type field. So we group by the classes as
# well.
- def preload_one(association)
- grouped_records(association).each do |reflection, klasses|
- klasses.each do |klass, records|
- preloader_for(reflection).new(klass, records, reflection, preload_scope).run
+ def preloaders_for_one(association, records, scope)
+ grouped_records(association, records).flat_map do |reflection, klasses|
+ klasses.map do |rhs_klass, rs|
+ loader = preloader_for(reflection, rs, rhs_klass).new(rhs_klass, rs, reflection, scope)
+ loader.run self
+ loader
end
end
end
- def grouped_records(association)
- Hash[
- records_by_reflection(association).map do |reflection, records|
- [reflection, records.group_by { |record| association_klass(reflection, record) }]
- end
- ]
+ def grouped_records(association, records)
+ reflection_records = records_by_reflection(association, records)
+
+ reflection_records.each_with_object({}) do |(reflection, r_records),h|
+ h[reflection] = r_records.group_by { |record|
+ association_klass(reflection, record)
+ }
+ end
end
- def records_by_reflection(association)
+ def records_by_reflection(association, records)
records.group_by do |record|
- reflection = record.class.reflections[association]
+ reflection = record.class.reflect_on_association(association)
- unless reflection
- raise ActiveRecord::ConfigurationError, "Association named '#{association}' was not found; " \
- "perhaps you misspelled it?"
- end
-
- reflection
+ reflection || raise_config_error(record, association)
end
end
+ def raise_config_error(record, association)
+ raise ActiveRecord::ConfigurationError,
+ "Association named '#{association}' was not found on #{record.class.name}; " \
+ "perhaps you misspelled it?"
+ end
+
def association_klass(reflection, record)
if reflection.macro == :belongs_to && reflection.options[:polymorphic]
- klass = record.send(reflection.foreign_type)
+ klass = record.read_attribute(reflection.foreign_type.to_s)
klass && klass.constantize
else
reflection.klass
end
end
- def preloader_for(reflection)
+ class AlreadyLoaded
+ attr_reader :owners, :reflection
+
+ def initialize(klass, owners, reflection, preload_scope)
+ @owners = owners
+ @reflection = reflection
+ end
+
+ def run(preloader); end
+
+ def preloaded_records
+ owners.flat_map { |owner| owner.association(reflection.name).target }
+ end
+ end
+
+ class NullPreloader
+ def self.new(klass, owners, reflection, preload_scope); self; end
+ def self.run(preloader); end
+ end
+
+ def preloader_for(reflection, owners, rhs_klass)
+ return NullPreloader unless rhs_klass
+
+ if owners.first.association(reflection.name).loaded?
+ return AlreadyLoaded
+ end
+
case reflection.macro
when :has_many
reflection.options[:through] ? HasManyThrough : HasMany
when :has_one
reflection.options[:through] ? HasOneThrough : HasOne
- when :has_and_belongs_to_many
- HasAndBelongsToMany
when :belongs_to
BelongsTo
end
diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb
index f55b4824e5..83c69586e6 100644
--- a/activerecord/lib/active_record/associations/preloader/association.rb
+++ b/activerecord/lib/active_record/associations/preloader/association.rb
@@ -3,6 +3,7 @@ module ActiveRecord
class Preloader
class Association #:nodoc:
attr_reader :owners, :reflection, :preload_scope, :model, :klass
+ attr_reader :preloaded_records
def initialize(klass, owners, reflection, preload_scope)
@klass = klass
@@ -12,15 +13,14 @@ module ActiveRecord
@model = owners.first && owners.first.class
@scope = nil
@owners_by_key = nil
+ @preloaded_records = []
end
- def run
- unless owners.first.association(reflection.name).loaded?
- preload
- end
+ def run(preloader)
+ preload(preloader)
end
- def preload
+ def preload(preloader)
raise NotImplementedError
end
@@ -29,6 +29,10 @@ module ActiveRecord
end
def records_for(ids)
+ query_scope(ids)
+ end
+
+ def query_scope(ids)
scope.where(association_key.in(ids))
end
@@ -52,12 +56,9 @@ module ActiveRecord
raise NotImplementedError
end
- # We're converting to a string here because postgres will return the aliased association
- # key in a habtm as a string (for whatever reason)
def owners_by_key
@owners_by_key ||= owners.group_by do |owner|
- key = owner[owner_key_name]
- key && key.to_s
+ owner[owner_key_name]
end
end
@@ -67,38 +68,47 @@ module ActiveRecord
private
- def associated_records_by_owner
+ def associated_records_by_owner(preloader)
owners_map = owners_by_key
owner_keys = owners_map.keys.compact
- if klass.nil? || owner_keys.empty?
- records = []
- else
+ # Each record may have multiple owners, and vice-versa
+ records_by_owner = owners.each_with_object({}) do |owner,h|
+ h[owner] = []
+ 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 = sliced.map { |slice| records_for(slice).to_a }.flatten
- end
-
- # Each record may have multiple owners, and vice-versa
- records_by_owner = Hash[owners.map { |owner| [owner, []] }]
- records.each do |record|
- owner_key = record[association_key_name].to_s
- owners_map[owner_key].each do |owner|
- records_by_owner[owner] << record
+ records = load_slices sliced
+ records.each do |record, owner_key|
+ owners_map[owner_key].each do |owner|
+ records_by_owner[owner] << record
+ end
end
end
+
records_by_owner
end
+ def load_slices(slices)
+ @preloaded_records = slices.flat_map { |slice|
+ records_for(slice)
+ }
+
+ @preloaded_records.map { |record|
+ [record, record[association_key_name]]
+ }
+ end
+
def reflection_scope
@reflection_scope ||= reflection.scope ? klass.unscoped.instance_exec(nil, &reflection.scope) : klass.unscoped
end
def build_scope
scope = klass.unscoped
- scope.default_scoped = true
values = reflection_scope.values
reflection_binds = reflection_scope.bind_values
@@ -112,11 +122,19 @@ module ActiveRecord
scope.select! preload_values[:select] || values[:select] || table[Arel.star]
scope.includes! preload_values[:includes] || values[:includes]
+ if preload_values.key? :order
+ scope.order! preload_values[:order]
+ else
+ if values.key? :order
+ scope.order! values[:order]
+ end
+ end
+
if options[:as]
scope.where!(klass.table_name => { reflection.type => model.base_class.sti_name })
end
- scope
+ klass.default_scoped.merge(scope)
end
end
end
diff --git a/activerecord/lib/active_record/associations/preloader/collection_association.rb b/activerecord/lib/active_record/associations/preloader/collection_association.rb
index e6cd35e7a1..5adffcd831 100644
--- a/activerecord/lib/active_record/associations/preloader/collection_association.rb
+++ b/activerecord/lib/active_record/associations/preloader/collection_association.rb
@@ -9,8 +9,8 @@ module ActiveRecord
super.order(preload_scope.values[:order] || reflection_scope.values[:order])
end
- def preload
- associated_records_by_owner.each do |owner, records|
+ def preload(preloader)
+ associated_records_by_owner(preloader).each do |owner, records|
association = owner.association(reflection.name)
association.loaded!
association.target.concat(records)
diff --git a/activerecord/lib/active_record/associations/preloader/has_and_belongs_to_many.rb b/activerecord/lib/active_record/associations/preloader/has_and_belongs_to_many.rb
deleted file mode 100644
index 9a3fada380..0000000000
--- a/activerecord/lib/active_record/associations/preloader/has_and_belongs_to_many.rb
+++ /dev/null
@@ -1,60 +0,0 @@
-module ActiveRecord
- module Associations
- class Preloader
- class HasAndBelongsToMany < CollectionAssociation #:nodoc:
- attr_reader :join_table
-
- def initialize(klass, records, reflection, preload_options)
- super
- @join_table = Arel::Table.new(reflection.join_table).alias('t0')
- end
-
- # Unlike the other associations, we want to get a raw array of rows so that we can
- # access the aliased column on the join table
- def records_for(ids)
- scope = super
- klass.connection.select_all(scope.arel, 'SQL', scope.bind_values)
- end
-
- def owner_key_name
- reflection.active_record_primary_key
- end
-
- def association_key_name
- 'ar_association_key_name'
- end
-
- def association_key
- join_table[reflection.foreign_key]
- end
-
- private
-
- # Once we have used the join table column (in super), we manually instantiate the
- # actual records, ensuring that we don't create more than one instances of the same
- # record
- def associated_records_by_owner
- records = {}
- super.each_value do |rows|
- rows.map! { |row| records[row[klass.primary_key]] ||= klass.instantiate(row) }
- end
- end
-
- def build_scope
- super.joins(join).select(join_select)
- end
-
- def join_select
- association_key.as(Arel.sql(association_key_name))
- end
-
- def join
- condition = table[reflection.association_primary_key].eq(
- join_table[reflection.association_foreign_key])
-
- table.create_join(join_table, table.create_on(condition))
- end
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/associations/preloader/has_many_through.rb b/activerecord/lib/active_record/associations/preloader/has_many_through.rb
index 157b627ad5..7b37b5942d 100644
--- a/activerecord/lib/active_record/associations/preloader/has_many_through.rb
+++ b/activerecord/lib/active_record/associations/preloader/has_many_through.rb
@@ -4,7 +4,7 @@ module ActiveRecord
class HasManyThrough < CollectionAssociation #:nodoc:
include ThroughAssociation
- def associated_records_by_owner
+ def associated_records_by_owner(preloader)
records_by_owner = super
if reflection_scope.distinct_value
diff --git a/activerecord/lib/active_record/associations/preloader/singular_association.rb b/activerecord/lib/active_record/associations/preloader/singular_association.rb
index 44e804d785..f60647a81e 100644
--- a/activerecord/lib/active_record/associations/preloader/singular_association.rb
+++ b/activerecord/lib/active_record/associations/preloader/singular_association.rb
@@ -5,13 +5,13 @@ module ActiveRecord
private
- def preload
- associated_records_by_owner.each do |owner, associated_records|
+ def preload(preloader)
+ associated_records_by_owner(preloader).each do |owner, associated_records|
record = associated_records.first
association = owner.association(reflection.name)
association.target = record
- association.set_inverse_instance(record)
+ association.set_inverse_instance(record) if record
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 c4b50ab306..2a8530af62 100644
--- a/activerecord/lib/active_record/associations/preloader/through_association.rb
+++ b/activerecord/lib/active_record/associations/preloader/through_association.rb
@@ -2,7 +2,6 @@ module ActiveRecord
module Associations
class Preloader
module ThroughAssociation #:nodoc:
-
def through_reflection
reflection.through_reflection
end
@@ -11,51 +10,84 @@ module ActiveRecord
reflection.source_reflection
end
- def associated_records_by_owner
- through_records = through_records_by_owner
+ def associated_records_by_owner(preloader)
+ preloader.preload(owners,
+ through_reflection.name,
+ through_scope)
- Preloader.new(through_records.values.flatten, source_reflection.name, reflection_scope).run
+ through_records = owners.map do |owner|
+ association = owner.association through_reflection.name
- through_records.each do |owner, records|
- records.map! { |r| r.send(source_reflection.name) }.flatten!
- records.compact!
+ [owner, Array(association.reader)]
end
- end
- private
+ reset_association owners, through_reflection.name
+
+ middle_records = through_records.map { |(_,rec)| rec }.flatten
+
+ preloaders = preloader.preload(middle_records,
+ source_reflection.name,
+ reflection_scope)
- def through_records_by_owner
- Preloader.new(owners, through_reflection.name, through_scope).run
+ @preloaded_records = preloaders.flat_map(&:preloaded_records)
+
+ middle_to_pl = preloaders.each_with_object({}) do |pl,h|
+ pl.owners.each { |middle|
+ h[middle] = pl
+ }
+ end
+
+ record_offset = {}
+ @preloaded_records.each_with_index do |record,i|
+ record_offset[record] = i
+ end
- Hash[owners.map do |owner|
- through_records = Array.wrap(owner.send(through_reflection.name))
+ through_records.each_with_object({}) { |(lhs,center),records_by_owner|
+ pl_to_middle = center.group_by { |record| middle_to_pl[record] }
- # Dont cache the association - we would only be caching a subset
- if (through_scope != through_reflection.klass.unscoped) ||
- (reflection.options[:source_type] && through_reflection.collection?)
- owner.association(through_reflection.name).reset
+ records_by_owner[lhs] = pl_to_middle.flat_map do |pl, middles|
+ rhs_records = middles.flat_map { |r|
+ association = r.association source_reflection.name
+
+ association.reader
+ }.compact
+
+ rhs_records.sort_by { |rhs| record_offset[rhs] }
end
+ }
+ end
+
+ private
+
+ def reset_association(owners, association_name)
+ should_reset = (through_scope != through_reflection.klass.unscoped) ||
+ (reflection.options[:source_type] && through_reflection.collection?)
- [owner, through_records]
- end]
+ # Dont cache the association - we would only be caching a subset
+ if should_reset
+ owners.each { |owner|
+ owner.association(association_name).reset
+ }
+ end
end
+
def through_scope
- through_scope = through_reflection.klass.unscoped
+ scope = through_reflection.klass.unscoped
if options[:source_type]
- through_scope.where! reflection.foreign_type => options[:source_type]
+ scope.where! reflection.foreign_type => options[:source_type]
else
unless reflection_scope.where_values.empty?
- through_scope.includes_values = Array(reflection_scope.values[:includes] || options[:source])
- through_scope.where_values = reflection_scope.values[:where]
+ scope.includes_values = Array(reflection_scope.values[:includes] || options[:source])
+ scope.where_values = reflection_scope.values[:where]
end
- through_scope.references! reflection_scope.values[:references]
- through_scope.order! reflection_scope.values[:order] if through_scope.eager_loading?
+ scope.references! reflection_scope.values[:references]
+ scope.order! reflection_scope.values[:order] if scope.eager_loading?
end
- through_scope
+ scope
end
end
end
diff --git a/activerecord/lib/active_record/associations/singular_association.rb b/activerecord/lib/active_record/associations/singular_association.rb
index 10238555f0..e4500af5b2 100644
--- a/activerecord/lib/active_record/associations/singular_association.rb
+++ b/activerecord/lib/active_record/associations/singular_association.rb
@@ -39,10 +39,11 @@ module ActiveRecord
end
def find_target
- scope.first.tap { |record| set_inverse_instance(record) }
+ if record = scope.first
+ set_inverse_instance record
+ end
end
- # Implemented by subclasses
def replace(record)
raise NotImplementedError, "Subclasses must implement a replace(record) method"
end
diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb
index 35f29b37a2..ba7d2a3782 100644
--- a/activerecord/lib/active_record/associations/through_association.rb
+++ b/activerecord/lib/active_record/associations/through_association.rb
@@ -13,9 +13,9 @@ module ActiveRecord
# 2. To get the type conditions for any STI models in the chain
def target_scope
scope = super
- chain[1..-1].each do |reflection|
+ chain.drop(1).each do |reflection|
scope.merge!(
- reflection.klass.all.with_default_scope.
+ reflection.klass.all.
except(:select, :create_with, :includes, :preload, :joins, :eager_load)
)
end
diff --git a/activerecord/lib/active_record/attribute_assignment.rb b/activerecord/lib/active_record/attribute_assignment.rb
index 75377bba57..30fa2c8ba5 100644
--- a/activerecord/lib/active_record/attribute_assignment.rb
+++ b/activerecord/lib/active_record/attribute_assignment.rb
@@ -3,7 +3,6 @@ require 'active_model/forbidden_attributes_protection'
module ActiveRecord
module AttributeAssignment
extend ActiveSupport::Concern
- include ActiveModel::DeprecatedMassAssignmentSecurity
include ActiveModel::ForbiddenAttributesProtection
# Allows you to set all the attributes by passing in a hash of attributes with
@@ -13,6 +12,9 @@ module ActiveRecord
# of this method is +false+ an <tt>ActiveModel::ForbiddenAttributesError</tt>
# exception is raised.
def assign_attributes(new_attributes)
+ if !new_attributes.respond_to?(:stringify_keys)
+ raise ArgumentError, "When assigning attributes, you must pass a hash as an argument."
+ end
return if new_attributes.blank?
attributes = new_attributes.stringify_keys
diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb
index 609c6e8cab..73761520f7 100644
--- a/activerecord/lib/active_record/attribute_methods.rb
+++ b/activerecord/lib/active_record/attribute_methods.rb
@@ -1,4 +1,6 @@
require 'active_support/core_ext/enumerable'
+require 'mutex_m'
+require 'thread_safe'
module ActiveRecord
# = Active Record Attribute Methods
@@ -7,6 +9,7 @@ module ActiveRecord
include ActiveModel::AttributeMethods
included do
+ initialize_generated_modules
include Read
include Write
include BeforeTypeCast
@@ -17,27 +20,66 @@ module ActiveRecord
include Serialization
end
+ AttrNames = Module.new {
+ def self.set_name_cache(name, value)
+ const_name = "ATTR_#{name}"
+ unless const_defined? const_name
+ const_set const_name, value.dup.freeze
+ end
+ end
+ }
+
+ class AttributeMethodCache
+ def initialize
+ @module = Module.new
+ @method_cache = ThreadSafe::Cache.new
+ end
+
+ def [](name)
+ @method_cache.compute_if_absent(name) do
+ safe_name = name.unpack('h*').first
+ temp_method = "__temp__#{safe_name}"
+ ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name
+ @module.module_eval method_body(temp_method, safe_name), __FILE__, __LINE__
+ @module.instance_method temp_method
+ end
+ end
+
+ private
+ def method_body; raise NotImplementedError; end
+ end
+
module ClassMethods
+ def inherited(child_class) #:nodoc:
+ child_class.initialize_generated_modules
+ super
+ end
+
+ def initialize_generated_modules # :nodoc:
+ @generated_attribute_methods = Module.new { extend Mutex_m }
+ @attribute_methods_generated = false
+ include @generated_attribute_methods
+ end
+
# Generates all the attribute related methods for columns in the database
# accessors, mutators and query methods.
def define_attribute_methods # :nodoc:
# Use a mutex; we don't want two thread simultaneously trying to define
# attribute methods.
- @attribute_methods_mutex.synchronize do
- return if attribute_methods_generated?
+ generated_attribute_methods.synchronize do
+ return false if @attribute_methods_generated
superclass.define_attribute_methods unless self == base_class
super(column_names)
@attribute_methods_generated = true
end
- end
-
- def attribute_methods_generated? # :nodoc:
- @attribute_methods_generated ||= false
+ true
end
def undefine_attribute_methods # :nodoc:
- super if attribute_methods_generated?
- @attribute_methods_generated = false
+ generated_attribute_methods.synchronize do
+ super if @attribute_methods_generated
+ @attribute_methods_generated = false
+ end
end
# Raises a <tt>ActiveRecord::DangerousAttributeError</tt> exception when an
@@ -86,6 +128,16 @@ module ActiveRecord
end
end
+ def find_generated_attribute_method(method_name) # :nodoc:
+ klass = self
+ until klass == Base
+ gen_methods = klass.generated_attribute_methods
+ return gen_methods.instance_method(method_name) if method_defined_within?(method_name, gen_methods, Object)
+ klass = klass.superclass
+ end
+ nil
+ end
+
# Returns +true+ if +attribute+ is an attribute method and table exists,
# +false+ otherwise.
#
@@ -119,33 +171,21 @@ module ActiveRecord
# If we haven't generated any methods yet, generate them, then
# see if we've created the method we're looking for.
def method_missing(method, *args, &block) # :nodoc:
- unless self.class.attribute_methods_generated?
- self.class.define_attribute_methods
-
- if respond_to_without_attributes?(method)
- send(method, *args, &block)
+ self.class.define_attribute_methods
+ if respond_to_without_attributes?(method)
+ # make sure to invoke the correct attribute method, as we might have gotten here via a `super`
+ # call in a overwritten attribute method
+ if attribute_method = self.class.find_generated_attribute_method(method)
+ # this is probably horribly slow, but should only happen at most once for a given AR class
+ attribute_method.bind(self).call(*args, &block)
else
- super
+ send(method, *args, &block)
end
else
super
end
end
- def attribute_missing(match, *args, &block) # :nodoc:
- if self.class.columns_hash[match.attr_name]
- ActiveSupport::Deprecation.warn(
- "The method `#{match.method_name}', matching the attribute `#{match.attr_name}' has " \
- "dispatched through method_missing. This shouldn't happen, because `#{match.attr_name}' " \
- "is a column of the table. If this error has happened through normal usage of Active " \
- "Record (rather than through your own code or external libraries), please report it as " \
- "a bug."
- )
- end
-
- super
- end
-
# A Person object with a name attribute can ask <tt>person.respond_to?(:name)</tt>,
# <tt>person.respond_to?(:name=)</tt>, and <tt>person.respond_to?(:name?)</tt>
# which will all return +true+. It also define the attribute methods if they have
@@ -164,7 +204,7 @@ module ActiveRecord
# person.respond_to(:nothing) # => false
def respond_to?(name, include_private = false)
name = name.to_s
- self.class.define_attribute_methods unless self.class.attribute_methods_generated?
+ self.class.define_attribute_methods
result = super
# If the result is false the answer is false.
@@ -174,7 +214,7 @@ module ActiveRecord
# For queries selecting a subset of columns, return false for unselected columns.
# We check defined?(@attributes) not to issue warnings if called on objects that
# have been allocated but not yet initialized.
- if defined?(@attributes) && @attributes.present? && self.class.column_names.include?(name)
+ if defined?(@attributes) && @attributes.any? && self.class.column_names.include?(name)
return has_attribute?(name)
end
@@ -222,24 +262,31 @@ module ActiveRecord
# Returns an <tt>#inspect</tt>-like string for the value of the
# attribute +attr_name+. String attributes are truncated upto 50
- # characters, and Date and Time attributes are returned in the
- # <tt>:db</tt> format. Other attributes return the value of
- # <tt>#inspect</tt> without modification.
+ # characters, Date and Time attributes are returned in the
+ # <tt>:db</tt> format, Array attributes are truncated upto 10 values.
+ # Other attributes return the value of <tt>#inspect</tt> without
+ # modification.
#
# person = Person.create!(name: 'David Heinemeier Hansson ' * 3)
#
# person.attribute_for_inspect(:name)
- # # => "\"David Heinemeier Hansson David Heinemeier Hansson D...\""
+ # # => "\"David Heinemeier Hansson David Heinemeier Hansson ...\""
#
# person.attribute_for_inspect(:created_at)
# # => "\"2012-10-22 00:15:07\""
+ #
+ # person.attribute_for_inspect(:tag_ids)
+ # # => "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]"
def attribute_for_inspect(attr_name)
value = read_attribute(attr_name)
if value.is_a?(String) && value.length > 50
- "#{value[0..50]}...".inspect
+ "#{value[0, 50]}...".inspect
elsif value.is_a?(Date) || value.is_a?(Time)
%("#{value.to_s(:db)}")
+ elsif value.is_a?(Array) && value.size > 10
+ inspected = value.first(10).inspect
+ %(#{inspected[0...-1]}, ...])
else
value.inspect
end
@@ -283,7 +330,7 @@ module ActiveRecord
end
# Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example,
- # "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)). It raises
+ # "2004-12-12" in a date column is cast to a date object, like Date.new(2004, 12, 12)). It raises
# <tt>ActiveModel::MissingAttributeError</tt> if the identified attribute is missing.
#
# Alias for the <tt>read_attribute</tt> method.
diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb
index 6315dd9549..19e81abba5 100644
--- a/activerecord/lib/active_record/attribute_methods/dirty.rb
+++ b/activerecord/lib/active_record/attribute_methods/dirty.rb
@@ -14,24 +14,12 @@ module ActiveRecord
class_attribute :partial_writes, instance_writer: false
self.partial_writes = true
-
- def self.partial_updates=(v); self.partial_writes = v; end
- def self.partial_updates?; partial_writes?; end
- def self.partial_updates; partial_writes; end
-
- ActiveSupport::Deprecation.deprecate_methods(
- singleton_class,
- :partial_updates= => :partial_writes=,
- :partial_updates? => :partial_writes?,
- :partial_updates => :partial_writes
- )
end
# Attempts to +save+ the record and clears changed attributes if successful.
def save(*)
if status = super
- @previously_changed = changes
- @changed_attributes.clear
+ changes_applied
end
status
end
@@ -39,16 +27,14 @@ module ActiveRecord
# Attempts to <tt>save!</tt> the record and clears changed attributes if successful.
def save!(*)
super.tap do
- @previously_changed = changes
- @changed_attributes.clear
+ changes_applied
end
end
# <tt>reload</tt> the record and clears changed attributes.
def reload(*)
super.tap do
- @previously_changed.clear
- @changed_attributes.clear
+ reset_changes
end
end
@@ -59,11 +45,11 @@ module ActiveRecord
# The attribute already has an unsaved change.
if attribute_changed?(attr)
- old = @changed_attributes[attr]
- @changed_attributes.delete(attr) unless _field_changed?(attr, old, value)
+ old = changed_attributes[attr]
+ changed_attributes.delete(attr) unless _field_changed?(attr, old, value)
else
old = clone_attribute_value(:read_attribute, attr)
- @changed_attributes[attr] = old if _field_changed?(attr, old, value)
+ changed_attributes[attr] = old if _field_changed?(attr, old, value)
end
# Carry on.
diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb
index 506f5d75f9..d01e9aea59 100644
--- a/activerecord/lib/active_record/attribute_methods/read.rb
+++ b/activerecord/lib/active_record/attribute_methods/read.rb
@@ -1,6 +1,38 @@
+require 'active_support/core_ext/module/method_transplanting'
+
module ActiveRecord
module AttributeMethods
module Read
+ ReaderMethodCache = Class.new(AttributeMethodCache) {
+ private
+ # We want to generate the methods via module_eval rather than
+ # define_method, because define_method is slower on dispatch.
+ # Evaluating many similar methods may use more memory as the instruction
+ # sequences are duplicated and cached (in MRI). define_method may
+ # be slower on dispatch, but if you're careful about the closure
+ # created, then define_method will consume much less memory.
+ #
+ # But sometimes the database might return columns with
+ # characters that are not allowed in normal method names (like
+ # 'my_column(omg)'. So to work around this we first define with
+ # the __temp__ identifier, and then use alias method to rename
+ # it to what we want.
+ #
+ # We are also defining a constant to hold the frozen string of
+ # the attribute name. Using a constant means that we do not have
+ # to allocate an object on each call to the attribute method.
+ # Making it frozen means that it doesn't get duped when used to
+ # key the @attributes_cache in read_attribute.
+ def method_body(method_name, const_name)
+ <<-EOMETHOD
+ def #{method_name}
+ name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{const_name}
+ read_attribute(name) { |n| missing_attribute(n, caller) }
+ end
+ EOMETHOD
+ end
+ }.new
+
extend ActiveSupport::Concern
ATTRIBUTE_TYPES_CACHED_BY_DEFAULT = [:datetime, :timestamp, :time, :date]
@@ -32,30 +64,30 @@ module ActiveRecord
protected
- # We want to generate the methods via module_eval rather than
- # define_method, because define_method is slower on dispatch and
- # uses more memory (because it creates a closure).
- #
- # But sometimes the database might return columns with
- # characters that are not allowed in normal method names (like
- # 'my_column(omg)'. So to work around this we first define with
- # the __temp__ identifier, and then use alias method to rename
- # it to what we want.
- #
- # We are also defining a constant to hold the frozen string of
- # the attribute name. Using a constant means that we do not have
- # to allocate an object on each call to the attribute method.
- # Making it frozen means that it doesn't get duped when used to
- # key the @attributes_cache in read_attribute.
- def define_method_attribute(name)
- safe_name = name.unpack('h*').first
- generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
- def __temp__#{safe_name}
- read_attribute(AttrNames::ATTR_#{safe_name}) { |n| missing_attribute(n, caller) }
+ if Module.methods_transplantable?
+ def define_method_attribute(name)
+ method = ReaderMethodCache[name]
+ generated_attribute_methods.module_eval { define_method name, method }
+ end
+ else
+ def define_method_attribute(name)
+ safe_name = name.unpack('h*').first
+ temp_method = "__temp__#{safe_name}"
+
+ ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name
+
+ generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
+ def #{temp_method}
+ name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{safe_name}
+ read_attribute(name) { |n| missing_attribute(n, caller) }
+ end
+ STR
+
+ generated_attribute_methods.module_eval do
+ alias_method name, temp_method
+ undef_method temp_method
end
- alias_method #{name.inspect}, :__temp__#{safe_name}
- undef_method :__temp__#{safe_name}
- STR
+ end
end
private
@@ -70,20 +102,21 @@ module ActiveRecord
end
# Returns the value of the attribute identified by <tt>attr_name</tt> after
- # it has been typecast (for example, "2004-12-12" in a data column is cast
+ # 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)
# If it's cached, just return it
# We use #[] first as a perf optimization for non-nil values. See https://gist.github.com/jonleighton/3552829.
name = attr_name.to_s
@attributes_cache[name] || @attributes_cache.fetch(name) {
- column = @columns_hash.fetch(name) {
- return @attributes.fetch(name) {
- if name == 'id' && self.class.primary_key != name
- read_attribute(self.class.primary_key)
- end
- }
- }
+ column = @column_types_override[name] if @column_types_override
+ column ||= @column_types[name]
+
+ return @attributes.fetch(name) {
+ if name == 'id' && self.class.primary_key != name
+ read_attribute(self.class.primary_key)
+ end
+ } unless column
value = @attributes.fetch(name) {
return block_given? ? yield(name) : nil
diff --git a/activerecord/lib/active_record/attribute_methods/serialization.rb b/activerecord/lib/active_record/attribute_methods/serialization.rb
index 7f1ebab4cd..d484659190 100644
--- a/activerecord/lib/active_record/attribute_methods/serialization.rb
+++ b/activerecord/lib/active_record/attribute_methods/serialization.rb
@@ -24,6 +24,9 @@ module ActiveRecord
# serialized object must be of that class on retrieval or
# <tt>SerializationTypeMismatch</tt> will be raised.
#
+ # A notable side effect of serialized attributes is that the model will
+ # be updated on every save, even if it is not dirty.
+ #
# ==== Parameters
#
# * +attr_name+ - The field name that should be serialized.
@@ -50,39 +53,40 @@ module ActiveRecord
end
end
- # *DEPRECATED*: Use ActiveRecord::AttributeMethods::Serialization::ClassMethods#serialized_attributes class level method instead.
- def serialized_attributes
- message = "Instance level serialized_attributes method is deprecated, please use class level method."
- ActiveSupport::Deprecation.warn message
- defined?(@serialized_attributes) ? @serialized_attributes : self.class.serialized_attributes
- end
-
class Type # :nodoc:
def initialize(column)
@column = column
end
def type_cast(value)
- value.unserialized_value
+ if value.state == :serialized
+ value.unserialized_value @column.type_cast value.value
+ else
+ value.unserialized_value
+ end
end
def type
@column.type
end
+
+ def accessor
+ ActiveRecord::Store::IndifferentHashAccessor
+ end
end
class Attribute < Struct.new(:coder, :value, :state) # :nodoc:
- def unserialized_value
- state == :serialized ? unserialize : value
+ def unserialized_value(v = value)
+ state == :serialized ? unserialize(v) : value
end
def serialized_value
state == :unserialized ? serialize : value
end
- def unserialize
+ def unserialize(v)
self.state = :unserialized
- self.value = coder.load(value)
+ self.value = coder.load(v)
end
def serialize
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 41b5a6e926..f168282ea3 100644
--- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
+++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
@@ -51,7 +51,7 @@ module ActiveRecord
def create_time_zone_conversion_attribute?(name, column)
time_zone_aware_attributes &&
!self.skip_time_zone_conversion_for_attributes.include?(name.to_sym) &&
- [:datetime, :timestamp].include?(column.type)
+ (:datetime == column.type || :timestamp == column.type)
end
end
end
diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb
index cd33494cc3..c853fc0917 100644
--- a/activerecord/lib/active_record/attribute_methods/write.rb
+++ b/activerecord/lib/active_record/attribute_methods/write.rb
@@ -1,6 +1,21 @@
+require 'active_support/core_ext/module/method_transplanting'
+
module ActiveRecord
module AttributeMethods
module Write
+ WriterMethodCache = Class.new(AttributeMethodCache) {
+ private
+
+ def method_body(method_name, const_name)
+ <<-EOMETHOD
+ def #{method_name}(value)
+ name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{const_name}
+ write_attribute(name, value)
+ end
+ EOMETHOD
+ end
+ }.new
+
extend ActiveSupport::Concern
included do
@@ -10,17 +25,29 @@ module ActiveRecord
module ClassMethods
protected
- # See define_method_attribute in read.rb for an explanation of
- # this code.
- def define_method_attribute=(name)
- safe_name = name.unpack('h*').first
- generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
- def __temp__#{safe_name}=(value)
- write_attribute(AttrNames::ATTR_#{safe_name}, value)
- end
- alias_method #{(name + '=').inspect}, :__temp__#{safe_name}=
- undef_method :__temp__#{safe_name}=
- STR
+ if Module.methods_transplantable?
+ # See define_method_attribute in read.rb for an explanation of
+ # this code.
+ def define_method_attribute=(name)
+ method = WriterMethodCache[name]
+ generated_attribute_methods.module_eval {
+ define_method "#{name}=", method
+ }
+ end
+ else
+ def define_method_attribute=(name)
+ safe_name = name.unpack('h*').first
+ ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name
+
+ generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
+ def __temp__#{safe_name}=(value)
+ name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{safe_name}
+ write_attribute(name, value)
+ end
+ alias_method #{(name + '=').inspect}, :__temp__#{safe_name}=
+ undef_method :__temp__#{safe_name}=
+ STR
+ end
end
end
diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb
index 87d4daa6d9..e9622ca0c1 100644
--- a/activerecord/lib/active_record/autosave_association.rb
+++ b/activerecord/lib/active_record/autosave_association.rb
@@ -127,87 +127,87 @@ module ActiveRecord
extend ActiveSupport::Concern
module AssociationBuilderExtension #:nodoc:
- def build
+ def self.build(model, reflection)
model.send(:add_autosave_association_callbacks, reflection)
- super
+ end
+
+ def self.valid_options
+ [ :autosave ]
end
end
included do
- Associations::Builder::Association.class_eval do
- self.valid_options << :autosave
- include AssociationBuilderExtension
- end
+ Associations::Builder::Association.extensions << AssociationBuilderExtension
end
module ClassMethods
private
- def define_non_cyclic_method(name, reflection, &block)
- define_method(name) do |*args|
- result = true; @_already_called ||= {}
- # Loop prevention for validation of associations
- unless @_already_called[[name, reflection.name]]
- begin
- @_already_called[[name, reflection.name]]=true
- result = instance_eval(&block)
- ensure
- @_already_called[[name, reflection.name]]=false
+ def define_non_cyclic_method(name, &block)
+ define_method(name) do |*args|
+ result = true; @_already_called ||= {}
+ # Loop prevention for validation of associations
+ unless @_already_called[name]
+ begin
+ @_already_called[name]=true
+ result = instance_eval(&block)
+ ensure
+ @_already_called[name]=false
+ end
end
- end
- result
+ result
+ end
end
- end
- # Adds validation and save callbacks for the association as specified by
- # the +reflection+.
- #
- # For performance reasons, we don't check whether to validate at runtime.
- # However the validation and callback methods are lazy and those methods
- # get created when they are invoked for the very first time. However,
- # this can change, for instance, when using nested attributes, which is
- # called _after_ the association has been defined. Since we don't want
- # the callbacks to get defined multiple times, there are guards that
- # check if the save or validation methods have already been defined
- # before actually defining them.
- def add_autosave_association_callbacks(reflection)
- save_method = :"autosave_associated_records_for_#{reflection.name}"
- validation_method = :"validate_associated_records_for_#{reflection.name}"
- collection = reflection.collection?
-
- unless method_defined?(save_method)
- if collection
- before_save :before_save_collection_association
-
- define_non_cyclic_method(save_method, reflection) { save_collection_association(reflection) }
- # Doesn't use after_save as that would save associations added in after_create/after_update twice
- after_create save_method
- after_update save_method
- elsif reflection.macro == :has_one
- define_method(save_method) { save_has_one_association(reflection) }
- # Configures two callbacks instead of a single after_save so that
- # the model may rely on their execution order relative to its
- # own callbacks.
- #
- # For example, given that after_creates run before after_saves, if
- # we configured instead an after_save there would be no way to fire
- # a custom after_create callback after the child association gets
- # created.
- after_create save_method
- after_update save_method
- else
- define_non_cyclic_method(save_method, reflection) { save_belongs_to_association(reflection) }
- before_save save_method
+ # Adds validation and save callbacks for the association as specified by
+ # the +reflection+.
+ #
+ # For performance reasons, we don't check whether to validate at runtime.
+ # However the validation and callback methods are lazy and those methods
+ # get created when they are invoked for the very first time. However,
+ # this can change, for instance, when using nested attributes, which is
+ # called _after_ the association has been defined. Since we don't want
+ # the callbacks to get defined multiple times, there are guards that
+ # check if the save or validation methods have already been defined
+ # before actually defining them.
+ def add_autosave_association_callbacks(reflection)
+ save_method = :"autosave_associated_records_for_#{reflection.name}"
+ validation_method = :"validate_associated_records_for_#{reflection.name}"
+ collection = reflection.collection?
+
+ unless method_defined?(save_method)
+ if collection
+ before_save :before_save_collection_association
+
+ define_non_cyclic_method(save_method) { save_collection_association(reflection) }
+ # Doesn't use after_save as that would save associations added in after_create/after_update twice
+ after_create save_method
+ after_update save_method
+ elsif reflection.macro == :has_one
+ define_method(save_method) { save_has_one_association(reflection) }
+ # Configures two callbacks instead of a single after_save so that
+ # the model may rely on their execution order relative to its
+ # own callbacks.
+ #
+ # For example, given that after_creates run before after_saves, if
+ # we configured instead an after_save there would be no way to fire
+ # a custom after_create callback after the child association gets
+ # created.
+ after_create save_method
+ after_update save_method
+ else
+ define_non_cyclic_method(save_method) { save_belongs_to_association(reflection) }
+ before_save save_method
+ end
end
- end
- if reflection.validate? && !method_defined?(validation_method)
- method = (collection ? :validate_collection_association : :validate_single_association)
- define_non_cyclic_method(validation_method, reflection) { send(method, reflection) }
- validate validation_method
+ if reflection.validate? && !method_defined?(validation_method)
+ method = (collection ? :validate_collection_association : :validate_single_association)
+ define_non_cyclic_method(validation_method) { send(method, reflection) }
+ validate validation_method
+ end
end
- end
end
# Reloads the attributes of the object as usual and clears <tt>marked_for_destruction</tt> flag.
@@ -254,172 +254,179 @@ module ActiveRecord
private
- # Returns the record for an association collection that should be validated
- # or saved. If +autosave+ is +false+ only new records will be returned,
- # unless the parent is/was a new record itself.
- def associated_records_to_validate_or_save(association, new_record, autosave)
- if new_record
- association && association.target
- elsif autosave
- association.target.find_all { |record| record.changed_for_autosave? }
- else
- association.target.find_all { |record| record.new_record? }
+ # Returns the record for an association collection that should be validated
+ # or saved. If +autosave+ is +false+ only new records will be returned,
+ # unless the parent is/was a new record itself.
+ def associated_records_to_validate_or_save(association, new_record, autosave)
+ if new_record
+ association && association.target
+ elsif autosave
+ association.target.find_all { |record| record.changed_for_autosave? }
+ else
+ association.target.find_all { |record| record.new_record? }
+ end
end
- end
- # go through nested autosave associations that are loaded in memory (without loading
- # any new ones), and return true if is changed for autosave
- def nested_records_changed_for_autosave?
- self.class.reflect_on_all_autosave_associations.any? do |reflection|
- association = association_instance_get(reflection.name)
- association && Array.wrap(association.target).any? { |a| a.changed_for_autosave? }
+ # go through nested autosave associations that are loaded in memory (without loading
+ # any new ones), and return true if is changed for autosave
+ def nested_records_changed_for_autosave?
+ self.class.reflect_on_all_autosave_associations.any? do |reflection|
+ association = association_instance_get(reflection.name)
+ association && Array.wrap(association.target).any? { |a| a.changed_for_autosave? }
+ end
end
- end
- # Validate the association if <tt>:validate</tt> or <tt>:autosave</tt> is
- # turned on for the association.
- def validate_single_association(reflection)
- association = association_instance_get(reflection.name)
- record = association && association.reader
- association_valid?(reflection, record) if record
- end
+ # Validate the association if <tt>:validate</tt> or <tt>:autosave</tt> is
+ # turned on for the association.
+ def validate_single_association(reflection)
+ association = association_instance_get(reflection.name)
+ record = association && association.reader
+ association_valid?(reflection, record) if record
+ end
- # Validate the associated records if <tt>:validate</tt> or
- # <tt>:autosave</tt> is turned on for the association specified by
- # +reflection+.
- 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) }
+ # Validate the associated records if <tt>:validate</tt> or
+ # <tt>:autosave</tt> is turned on for the association specified by
+ # +reflection+.
+ 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) }
+ end
end
end
- end
- # Returns whether or not the association is valid and applies any errors to
- # the parent, <tt>self</tt>, if it wasn't. Skips any <tt>:autosave</tt>
- # enabled records if they're marked_for_destruction? or destroyed.
- def association_valid?(reflection, record)
- return true if record.destroyed? || record.marked_for_destruction?
-
- unless valid = record.valid?(validation_context)
- if reflection.options[:autosave]
- record.errors.each do |attribute, message|
- attribute = "#{reflection.name}.#{attribute}"
- errors[attribute] << message
- errors[attribute].uniq!
+ # Returns whether or not the association is valid and applies any errors to
+ # the parent, <tt>self</tt>, if it wasn't. Skips any <tt>:autosave</tt>
+ # enabled records if they're marked_for_destruction? or destroyed.
+ def association_valid?(reflection, record)
+ return true if record.destroyed? || record.marked_for_destruction?
+
+ unless valid = record.valid?
+ if reflection.options[:autosave]
+ record.errors.each do |attribute, message|
+ attribute = "#{reflection.name}.#{attribute}"
+ errors[attribute] << message
+ errors[attribute].uniq!
+ end
+ else
+ errors.add(reflection.name)
end
- else
- errors.add(reflection.name)
end
+ valid
end
- valid
- end
- # Is used as a before_save callback to check while saving a collection
- # association whether or not the parent was a new record before saving.
- def before_save_collection_association
- @new_record_before_save = new_record?
- true
- end
+ # Is used as a before_save callback to check while saving a collection
+ # association whether or not the parent was a new record before saving.
+ def before_save_collection_association
+ @new_record_before_save = new_record?
+ true
+ end
- # Saves any new associated records, or all loaded autosave associations if
- # <tt>:autosave</tt> is enabled on the association.
- #
- # In addition, it destroys all children that were marked 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.
- def save_collection_association(reflection)
- if association = association_instance_get(reflection.name)
- autosave = reflection.options[:autosave]
-
- if records = associated_records_to_validate_or_save(association, @new_record_before_save, autosave)
-
- if autosave
- records_to_destroy = records.select(&:marked_for_destruction?)
- records_to_destroy.each { |record| association.destroy(record) }
- records -= records_to_destroy
- end
+ # Saves any new associated records, or all loaded autosave associations if
+ # <tt>:autosave</tt> is enabled on the association.
+ #
+ # In addition, it destroys all children that were marked 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.
+ def save_collection_association(reflection)
+ if association = association_instance_get(reflection.name)
+ autosave = reflection.options[:autosave]
+
+ if records = associated_records_to_validate_or_save(association, @new_record_before_save, autosave)
+
+ if autosave
+ records_to_destroy = records.select(&:marked_for_destruction?)
+ records_to_destroy.each { |record| association.destroy(record) }
+ records -= records_to_destroy
+ end
- records.each do |record|
+ records.each do |record|
+ next if record.destroyed?
- saved = true
+ saved = true
- if autosave != false && (@new_record_before_save || record.new_record?)
- if autosave
- saved = association.insert_record(record, false)
- else
- association.insert_record(record) unless reflection.nested?
+ if autosave != false && (@new_record_before_save || record.new_record?)
+ if autosave
+ saved = association.insert_record(record, false)
+ else
+ association.insert_record(record) unless reflection.nested?
+ end
+ elsif autosave
+ saved = record.save(:validate => false)
end
- elsif autosave
- saved = record.save(:validate => false)
- end
- raise ActiveRecord::Rollback unless saved
+ raise ActiveRecord::Rollback unless saved
+ end
end
- end
- # reconstruct the scope now that we know the owner's id
- association.reset_scope if association.respond_to?(:reset_scope)
+ # reconstruct the scope now that we know the owner's id
+ association.reset_scope if association.respond_to?(:reset_scope)
+ end
end
- end
- # Saves the associated record if it's new or <tt>:autosave</tt> is enabled
- # on the association.
- #
- # In addition, it will destroy the association if it was marked 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.
- def save_has_one_association(reflection)
- association = association_instance_get(reflection.name)
- record = association && association.load_target
- if record && !record.destroyed?
- autosave = reflection.options[:autosave]
-
- if autosave && record.marked_for_destruction?
- record.destroy
- else
- key = reflection.options[:primary_key] ? send(reflection.options[:primary_key]) : id
- if autosave != false && (new_record? || record.new_record? || record[reflection.foreign_key] != key || autosave)
- unless reflection.through_reflection
- record[reflection.foreign_key] = key
- end
+ # Saves the associated record if it's new or <tt>:autosave</tt> is enabled
+ # on the association.
+ #
+ # In addition, it will destroy the association if it was marked 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.
+ def save_has_one_association(reflection)
+ association = association_instance_get(reflection.name)
+ record = association && association.load_target
+ if record && !record.destroyed?
+ autosave = reflection.options[:autosave]
- saved = record.save(:validate => !autosave)
- raise ActiveRecord::Rollback if !saved && autosave
- saved
+ if autosave && record.marked_for_destruction?
+ record.destroy
+ else
+ key = reflection.options[:primary_key] ? send(reflection.options[:primary_key]) : id
+ if autosave != false && (autosave || new_record? || record_changed?(reflection, record, key))
+
+ unless reflection.through_reflection
+ record[reflection.foreign_key] = key
+ end
+
+ saved = record.save(:validate => !autosave)
+ raise ActiveRecord::Rollback if !saved && autosave
+ saved
+ end
end
end
end
- end
- # Saves the associated record if it's new or <tt>:autosave</tt> is enabled.
- #
- # In addition, it will destroy the association if it was marked for destruction.
- def save_belongs_to_association(reflection)
- association = association_instance_get(reflection.name)
- record = association && association.load_target
- if record && !record.destroyed?
- autosave = reflection.options[:autosave]
-
- if autosave && record.marked_for_destruction?
- self[reflection.foreign_key] = nil
- record.destroy
- elsif autosave != false
- saved = record.save(:validate => !autosave) if record.new_record? || (autosave && record.changed_for_autosave?)
-
- if association.updated?
- association_id = record.send(reflection.options[:primary_key] || :id)
- self[reflection.foreign_key] = association_id
- association.loaded!
- end
+ # If the record is new or it has changed, returns true.
+ def record_changed?(reflection, record, key)
+ record.new_record? || record[reflection.foreign_key] != key || record.attribute_changed?(reflection.foreign_key)
+ end
- saved if autosave
+ # Saves the associated record if it's new or <tt>:autosave</tt> is enabled.
+ #
+ # In addition, it will destroy the association if it was marked for destruction.
+ def save_belongs_to_association(reflection)
+ association = association_instance_get(reflection.name)
+ record = association && association.load_target
+ if record && !record.destroyed?
+ autosave = reflection.options[:autosave]
+
+ if autosave && record.marked_for_destruction?
+ self[reflection.foreign_key] = nil
+ record.destroy
+ elsif autosave != false
+ saved = record.save(:validate => !autosave) if record.new_record? || (autosave && record.changed_for_autosave?)
+
+ if association.updated?
+ association_id = record.send(reflection.options[:primary_key] || :id)
+ self[reflection.foreign_key] = association_id
+ association.loaded!
+ end
+
+ saved if autosave
+ end
end
end
- end
end
end
diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb
index b06add096f..1d3ec75aa1 100644
--- a/activerecord/lib/active_record/base.rb
+++ b/activerecord/lib/active_record/base.rb
@@ -4,7 +4,7 @@ require 'active_support/benchmarkable'
require 'active_support/dependencies'
require 'active_support/descendants_tracker'
require 'active_support/time'
-require 'active_support/core_ext/class/attribute_accessors'
+require 'active_support/core_ext/module/attribute_accessors'
require 'active_support/core_ext/class/delegating_attributes'
require 'active_support/core_ext/array/extract_options'
require 'active_support/core_ext/hash/deep_merge'
@@ -18,6 +18,7 @@ require 'arel'
require 'active_record/errors'
require 'active_record/log_subscriber'
require 'active_record/explain_subscriber'
+require 'active_record/relation/delegation'
module ActiveRecord #:nodoc:
# = Active Record
@@ -290,8 +291,11 @@ module ActiveRecord #:nodoc:
extend Translation
extend DynamicMatchers
extend Explain
+ extend Enum
+ extend Delegation::DelegateCache
include Persistence
+ include NoTouching
include ReadonlyAttributes
include ModelSchema
include Inheritance
diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb
index e4c484d64b..35f19f0bc0 100644
--- a/activerecord/lib/active_record/callbacks.rb
+++ b/activerecord/lib/active_record/callbacks.rb
@@ -23,11 +23,14 @@ module ActiveRecord
# Check out <tt>ActiveRecord::Transactions</tt> for more details about <tt>after_commit</tt> and
# <tt>after_rollback</tt>.
#
+ # Additionally, an <tt>after_touch</tt> callback is triggered whenever an
+ # object is touched.
+ #
# Lastly an <tt>after_find</tt> and <tt>after_initialize</tt> callback is triggered for each object that
# is found and instantiated by a finder, with <tt>after_initialize</tt> being triggered after new objects
# are instantiated as well.
#
- # That's a total of twelve callbacks, which gives you immense power to react and prepare for each state in the
+ # 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,
# except that each <tt>_create</tt> callback is replaced by the corresponding <tt>_update</tt> callback.
#
@@ -125,7 +128,7 @@ module ActiveRecord
# record.credit_card_number = decrypt(record.credit_card_number)
# end
#
- # alias_method :after_find, :after_save
+ # alias_method :after_initialize, :after_save
#
# private
# def encrypt(value)
@@ -160,7 +163,7 @@ module ActiveRecord
# record.send("#{@attribute}=", decrypt(record.send("#{@attribute}")))
# end
#
- # alias_method :after_find, :after_save
+ # alias_method :after_initialize, :after_save
#
# private
# def encrypt(value)
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 816b397fcf..cebe741daa 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
@@ -86,7 +86,7 @@ module ActiveRecord
end
end
- # Return the number of threads currently waiting on this
+ # Returns the number of threads currently waiting on this
# queue.
def num_waiting
synchronize do
@@ -332,11 +332,6 @@ module ActiveRecord
end
end
- def clear_stale_cached_connections! # :nodoc:
- reap
- end
- deprecate :clear_stale_cached_connections! => "Please use #reap instead"
-
# Check-out a database connection from the pool, indicating that you want
# to use it. You should call #checkin when you no longer need this.
#
@@ -629,7 +624,7 @@ module ActiveRecord
end
response
- rescue
+ rescue Exception
ActiveRecord::Base.clear_active_connections! unless testing
raise
end
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 c64b542286..c90915c509 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
@@ -18,8 +18,7 @@ module ActiveRecord
end
end
- # Returns an array of record hashes with the column names as keys and
- # column values as values.
+ # Returns an ActiveRecord::Result instance.
def select_all(arel, name = nil, binds = [])
select(to_sql(arel, binds), name, binds)
end
@@ -27,8 +26,7 @@ module ActiveRecord
# Returns a record hash with the column names as keys and column values
# as values.
def select_one(arel, name = nil, binds = [])
- result = select_all(arel, name, binds)
- result.first if result
+ select_all(arel, name, binds).first
end
# Returns a single value from a record
@@ -41,8 +39,8 @@ module ActiveRecord
# Returns an array of the values of the first column in a select:
# select_values("SELECT id FROM companies LIMIT 3") => [1,2,3]
def select_values(arel, name = nil)
- result = select_rows(to_sql(arel, []), name)
- result.map { |v| v[0] }
+ select_rows(to_sql(arel, []), name)
+ .map { |v| v[0] }
end
# Returns an array of arrays containing the field values.
@@ -303,10 +301,6 @@ module ActiveRecord
"DEFAULT VALUES"
end
- def case_sensitive_equality_operator
- "="
- end
-
def limited_update_conditions(where_sql, quoted_table_name, quoted_primary_key)
"WHERE #{quoted_primary_key} IN (SELECT #{quoted_primary_key} FROM #{quoted_table_name} #{where_sql})"
end
@@ -348,15 +342,14 @@ module ActiveRecord
protected
- # Return a subquery for the given key using the join information.
+ # Returns a subquery for the given key using the join information.
def subquery_for(key, select)
subselect = select.clone
subselect.projections = [key]
subselect
end
- # Returns an array of record hashes with the column names as keys and
- # column values as values.
+ # Returns an ActiveRecord::Result instance.
def select(sql, name = nil, binds = [])
end
undef_method :select
@@ -377,14 +370,14 @@ module ActiveRecord
update_sql(sql, name)
end
- def sql_for_insert(sql, pk, id_value, sequence_name, binds)
- [sql, binds]
- end
+ def sql_for_insert(sql, pk, id_value, sequence_name, binds)
+ [sql, binds]
+ end
- def last_inserted_id(result)
- row = result.rows.first
- row && row.first
- end
+ def last_inserted_id(result)
+ row = result.rows.first
+ row && row.first
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb
index 41e07fbda9..adc23a6674 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb
@@ -9,10 +9,10 @@ module ActiveRecord
def dirties_query_cache(base, *method_names)
method_names.each do |method_name|
base.class_eval <<-end_code, __FILE__, __LINE__ + 1
- def #{method_name}(*) # def update_with_query_dirty(*)
- clear_query_cache if @query_cache_enabled # clear_query_cache if @query_cache_enabled
- super # super
- end # end
+ def #{method_name}(*)
+ clear_query_cache if @query_cache_enabled
+ super
+ end
end_code
end
end
@@ -20,13 +20,19 @@ module ActiveRecord
attr_reader :query_cache, :query_cache_enabled
+ def initialize(*)
+ super
+ @query_cache = Hash.new { |h,sql| h[sql] = {} }
+ @query_cache_enabled = false
+ end
+
# Enable the query cache within the block.
def cache
old, @query_cache_enabled = @query_cache_enabled, true
yield
ensure
- clear_query_cache
@query_cache_enabled = old
+ clear_query_cache unless @query_cache_enabled
end
def enable_query_cache!
@@ -75,14 +81,7 @@ module ActiveRecord
else
@query_cache[sql][binds] = yield
end
-
- # FIXME: we should guarantee that all cached items are Result
- # objects. Then we can avoid this conditional
- if ActiveRecord::Result === result
- result.dup
- else
- result.collect { |row| row.dup }
- end
+ result.dup
end
# If arel is locked this is a SELECT ... FOR UPDATE or somesuch. Such
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
index d18b9c991f..75501852ed 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
@@ -15,7 +15,6 @@ module ActiveRecord
return "'#{quote_string(value)}'" unless column
case column.type
- when :binary then "'#{quote_string(column.string_to_binary(value))}'"
when :integer then value.to_i.to_s
when :float then value.to_f.to_s
else
@@ -44,7 +43,9 @@ module ActiveRecord
# SQLite does not understand dates, so this method will convert a Date
# to a String.
def type_cast(value, column)
- return value.id if value.respond_to?(:quoted_id)
+ if value.respond_to?(:quoted_id) && value.respond_to?(:id)
+ return value.id
+ end
case value
when String, ActiveSupport::Multibyte::Chars
@@ -52,7 +53,6 @@ module ActiveRecord
return value unless column
case column.type
- when :binary then value
when :integer then value.to_i
when :float then value.to_f
else
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/savepoints.rb b/activerecord/lib/active_record/connection_adapters/abstract/savepoints.rb
new file mode 100644
index 0000000000..25c17ce971
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/abstract/savepoints.rb
@@ -0,0 +1,21 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module Savepoints #:nodoc:
+ def supports_savepoints?
+ true
+ end
+
+ def create_savepoint(name = current_savepoint_name)
+ execute("SAVEPOINT #{name}")
+ end
+
+ def rollback_to_savepoint(name = current_savepoint_name)
+ execute("ROLLBACK TO SAVEPOINT #{name}")
+ end
+
+ def release_savepoint(name = current_savepoint_name)
+ execute("RELEASE SAVEPOINT #{name}")
+ end
+ end
+ end
+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
new file mode 100644
index 0000000000..a51691bfa8
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb
@@ -0,0 +1,84 @@
+module ActiveRecord
+ module ConnectionAdapters
+ class AbstractAdapter
+ class SchemaCreation # :nodoc:
+ def initialize(conn)
+ @conn = conn
+ @cache = {}
+ end
+
+ def accept(o)
+ m = @cache[o.class] ||= "visit_#{o.class.name.split('::').last}"
+ send m, o
+ end
+
+ def visit_AddColumn(o)
+ sql_type = type_to_sql(o.type.to_sym, o.limit, o.precision, o.scale)
+ sql = "ADD #{quote_column_name(o.name)} #{sql_type}"
+ add_column_options!(sql, column_options(o))
+ end
+
+ private
+
+ def visit_AlterTable(o)
+ sql = "ALTER TABLE #{quote_table_name(o.name)} "
+ sql << o.adds.map { |col| visit_AddColumn col }.join(' ')
+ end
+
+ def visit_ColumnDefinition(o)
+ sql_type = type_to_sql(o.type.to_sym, o.limit, o.precision, o.scale)
+ column_sql = "#{quote_column_name(o.name)} #{sql_type}"
+ add_column_options!(column_sql, column_options(o)) unless o.primary_key?
+ column_sql
+ 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 << "#{o.options}"
+ create_sql << " AS #{@conn.to_sql(o.as)}" if o.as
+ create_sql
+ end
+
+ def column_options(o)
+ column_options = {}
+ column_options[:null] = o.null unless o.null.nil?
+ column_options[:default] = o.default unless o.default.nil?
+ column_options[:column] = o
+ column_options[:first] = o.first
+ column_options[:after] = o.after
+ column_options
+ end
+
+ def quote_column_name(name)
+ @conn.quote_column_name name
+ end
+
+ def quote_table_name(name)
+ @conn.quote_table_name name
+ end
+
+ def type_to_sql(type, limit, precision, scale)
+ @conn.type_to_sql type.to_sym, limit, precision, scale
+ end
+
+ def add_column_options!(sql, options)
+ sql << " DEFAULT #{@conn.quote(options[:default], options[:column])}" if options_include_default?(options)
+ # must explicitly check for :null to allow change_column to work on migrations
+ if options[:null] == false
+ sql << " NOT NULL"
+ end
+ if options[:auto_increment] == true
+ sql << " AUTO_INCREMENT"
+ end
+ sql
+ end
+
+ def options_include_default?(options)
+ options.include?(:default) && !(options[:null] == false && options[:default].nil?)
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
index aabedf15e9..c39bf15e83 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
@@ -16,15 +16,15 @@ module ActiveRecord
# +columns+ attribute of said TableDefinition object, in order to be used
# for generating a number of table creation or table changing SQL statements.
class ColumnDefinition < Struct.new(:name, :type, :limit, :precision, :scale, :default, :null, :first, :after, :primary_key) #:nodoc:
- def string_to_binary(value)
- value
- end
def primary_key?
primary_key || type.to_sym == :primary_key
end
end
+ class ChangeColumnDefinition < Struct.new(:column, :type, :options) #:nodoc:
+ end
+
# Represents the schema of an SQL table in an abstract way. This class
# provides methods for manipulating the schema representation.
#
@@ -49,14 +49,15 @@ module ActiveRecord
# An array of ColumnDefinition objects, representing the column changes
# that have been defined.
attr_accessor :indexes
- attr_reader :name, :temporary, :options
+ attr_reader :name, :temporary, :options, :as
- def initialize(types, name, temporary, options)
+ def initialize(types, name, temporary, options, as = nil)
@columns_hash = {}
@indexes = {}
@native = types
@temporary = temporary
@options = options
+ @as = as
@name = name
end
@@ -269,6 +270,7 @@ module ActiveRecord
end
column.limit = limit
+ column.array = options[:array] if column.respond_to?(:array)
column.precision = options[:precision]
column.scale = options[:scale]
column.default = options[:default]
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 8ffe150de6..88bf15bc18 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
@@ -131,6 +131,9 @@ module ActiveRecord
# [<tt>:force</tt>]
# Set to true to drop the table before creating it.
# Defaults to false.
+ # [<tt>:as</tt>]
+ # SQL to use to generate the table. When this option is used, the block is
+ # ignored, as are the <tt>:id</tt> and <tt>:primary_key</tt> options.
#
# ====== Add a backend specific option to the generated SQL (MySQL)
#
@@ -169,19 +172,31 @@ module ActiveRecord
# supplier_id int
# )
#
+ # ====== Create a temporary table based on a query
+ #
+ # create_table(:long_query, temporary: true,
+ # as: "SELECT * FROM orders INNER JOIN line_items ON order_id=orders.id")
+ #
+ # generates:
+ #
+ # CREATE TEMPORARY TABLE long_query AS
+ # SELECT * FROM orders INNER JOIN line_items ON order_id=orders.id
+ #
# See also TableDefinition#column for details on how to create columns.
def create_table(table_name, options = {})
- td = create_table_definition table_name, options[:temporary], options[:options]
+ td = create_table_definition table_name, options[:temporary], options[:options], options[:as]
- unless options[:id] == false
- pk = options.fetch(:primary_key) {
- Base.get_primary_key table_name.to_s.singularize
- }
+ if !options[:as]
+ unless options[:id] == false
+ pk = options.fetch(:primary_key) {
+ Base.get_primary_key table_name.to_s.singularize
+ }
- td.primary_key pk, options.fetch(:id, :primary_key), options
- end
+ td.primary_key pk, options.fetch(:id, :primary_key), options
+ end
- yield td if block_given?
+ yield td if block_given?
+ end
if options[:force] && table_exists?(table_name)
drop_table(table_name, options)
@@ -214,8 +229,8 @@ module ActiveRecord
# its block form to do so yourself:
#
# create_join_table :products, :categories do |t|
- # t.index :products
- # t.index :categories
+ # t.index :product_id
+ # t.index :category_id
# end
#
# ====== Add a backend specific option to the generated SQL (MySQL)
@@ -558,8 +573,8 @@ module ActiveRecord
# this is a naive implementation; some DBs may support this more efficiently (Postgres, for instance)
old_index_def = indexes(table_name).detect { |i| i.name == old_name }
return unless old_index_def
- remove_index(table_name, :name => old_name)
- add_index(table_name, old_index_def.columns, :name => new_name, :unique => old_index_def.unique)
+ add_index(table_name, old_index_def.columns, name: new_name, unique: old_index_def.unique)
+ remove_index(table_name, name: old_name)
end
def index_name(table_name, options) #:nodoc:
@@ -606,7 +621,7 @@ module ActiveRecord
index_options = options.delete(:index)
add_column(table_name, "#{ref_name}_id", :integer, options)
add_column(table_name, "#{ref_name}_type", :string, polymorphic.is_a?(Hash) ? polymorphic : options) if polymorphic
- add_index(table_name, polymorphic ? %w[id type].map{ |t| "#{ref_name}_#{t}" } : "#{ref_name}_id", index_options.is_a?(Hash) ? index_options : nil) if index_options
+ add_index(table_name, polymorphic ? %w[id type].map{ |t| "#{ref_name}_#{t}" } : "#{ref_name}_id", index_options.is_a?(Hash) ? index_options : {}) if index_options
end
alias :add_belongs_to :add_reference
@@ -690,28 +705,8 @@ module ActiveRecord
column_type_sql
else
- type
- end
- end
-
- def add_column_options!(sql, options) #:nodoc:
- sql << " DEFAULT #{quote(options[:default], options[:column])}" if options_include_default?(options)
- # must explicitly check for :null to allow change_column to work on migrations
- if options[:null] == false
- sql << " NOT NULL"
+ type.to_s
end
- if options[:auto_increment] == true
- sql << " AUTO_INCREMENT"
- end
- end
-
- # SELECT DISTINCT clause for a given set of columns and a given ORDER BY clause.
- #
- # distinct("posts.id", ["posts.created_at desc"])
- #
- def distinct(columns, order_by)
- ActiveSupport::Deprecation.warn("#distinct is deprecated and shall be removed from future releases.")
- "DISTINCT #{columns_for_distinct(columns, order_by)}"
end
# Given a set of columns and an ORDER BY clause, returns the columns for a SELECT DISTINCT.
@@ -719,7 +714,7 @@ module ActiveRecord
# require the order columns appear in the SELECT.
#
# columns_for_distinct("posts.id", ["posts.created_at desc"])
- def columns_for_distinct(columns, orders) # :nodoc:
+ def columns_for_distinct(columns, orders) #:nodoc:
columns
end
@@ -741,6 +736,10 @@ module ActiveRecord
remove_column table_name, :created_at
end
+ def update_table_definition(table_name, base) #:nodoc:
+ Table.new(table_name, base)
+ end
+
protected
def add_index_sort_order(option_strings, column_names, options = {})
if options.is_a?(Hash) && order = options[:order]
@@ -775,37 +774,23 @@ module ActiveRecord
column_names = Array(column_name)
index_name = index_name(table_name, column: column_names)
- if Hash === options # legacy support, since this param was a string
- options.assert_valid_keys(:unique, :order, :name, :where, :length, :internal, :using, :algorithm, :type)
+ options.assert_valid_keys(:unique, :order, :name, :where, :length, :internal, :using, :algorithm, :type)
- index_type = options[:unique] ? "UNIQUE" : ""
- index_type = options[:type].to_s if options.key?(:type)
- index_name = options[:name].to_s if options.key?(:name)
- max_index_length = options.fetch(:internal, false) ? index_name_length : allowed_index_name_length
+ index_type = options[:unique] ? "UNIQUE" : ""
+ index_type = options[:type].to_s if options.key?(:type)
+ index_name = options[:name].to_s if options.key?(:name)
+ max_index_length = options.fetch(:internal, false) ? index_name_length : allowed_index_name_length
- if options.key?(:algorithm)
- algorithm = index_algorithms.fetch(options[:algorithm]) {
- raise ArgumentError.new("Algorithm must be one of the following: #{index_algorithms.keys.map(&:inspect).join(', ')}")
- }
- end
-
- using = "USING #{options[:using]}" if options[:using].present?
-
- if supports_partial_index?
- index_options = options[:where] ? " WHERE #{options[:where]}" : ""
- end
- else
- if options
- message = "Passing a string as third argument of `add_index` is deprecated and will" +
- " be removed in Rails 4.1." +
- " Use add_index(#{table_name.inspect}, #{column_name.inspect}, unique: true) instead"
+ if options.key?(:algorithm)
+ algorithm = index_algorithms.fetch(options[:algorithm]) {
+ raise ArgumentError.new("Algorithm must be one of the following: #{index_algorithms.keys.map(&:inspect).join(', ')}")
+ }
+ end
- ActiveSupport::Deprecation.warn message
- end
+ using = "USING #{options[:using]}" if options[:using].present?
- index_type = options
- max_index_length = allowed_index_name_length
- algorithm = using = nil
+ if supports_partial_index?
+ index_options = options[:where] ? " WHERE #{options[:where]}" : ""
end
if index_name.length > max_index_length
@@ -837,12 +822,6 @@ module ActiveRecord
index_name
end
- def columns_for_remove(table_name, *column_names)
- ActiveSupport::Deprecation.warn("columns_for_remove is deprecated and will be removed in the future")
- raise ArgumentError.new("You must specify at least one column name. Example: remove_columns(:people, :first_name)") if column_names.blank?
- column_names.map {|column_name| quote_column_name(column_name) }
- end
-
def rename_table_indexes(table_name, new_name)
indexes(new_name).each do |index|
generated_index_name = index_name(table_name, column: index.columns)
@@ -866,17 +845,13 @@ module ActiveRecord
end
private
- def create_table_definition(name, temporary, options)
- TableDefinition.new native_database_types, name, temporary, options
+ def create_table_definition(name, temporary, options, as = nil)
+ TableDefinition.new native_database_types, name, temporary, options, as
end
def create_alter_table(name)
AlterTable.new create_table_definition(name, false, {})
end
-
- def update_table_definition(table_name, base)
- Table.new(table_name, base)
- end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
index 26586f0974..8aa1ce5c04 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
@@ -4,6 +4,7 @@ require 'bigdecimal/util'
require 'active_support/core_ext/benchmark'
require 'active_record/connection_adapters/schema_cache'
require 'active_record/connection_adapters/abstract/schema_dumper'
+require 'active_record/connection_adapters/abstract/schema_creation'
require 'monitor'
module ActiveRecord
@@ -16,6 +17,7 @@ module ActiveRecord
autoload_at 'active_record/connection_adapters/abstract/schema_definitions' do
autoload :IndexDefinition
autoload :ColumnDefinition
+ autoload :ChangeColumnDefinition
autoload :TableDefinition
autoload :Table
autoload :AlterTable
@@ -33,12 +35,14 @@ module ActiveRecord
autoload :Quoting
autoload :ConnectionPool
autoload :QueryCache
+ autoload :Savepoints
end
autoload_at 'active_record/connection_adapters/abstract/transaction' do
autoload :ClosedTransaction
autoload :RealTransaction
autoload :SavepointTransaction
+ autoload :TransactionState
end
# Active Record supports multiple database systems. AbstractAdapter and
@@ -95,81 +99,15 @@ module ActiveRecord
@last_use = false
@logger = logger
@pool = pool
- @query_cache = Hash.new { |h,sql| h[sql] = {} }
- @query_cache_enabled = false
@schema_cache = SchemaCache.new self
@visitor = nil
+ @prepared_statements = false
end
def valid_type?(type)
true
end
- class SchemaCreation
- def initialize(conn)
- @conn = conn
- @cache = {}
- end
-
- def accept(o)
- m = @cache[o.class] ||= "visit_#{o.class.name.split('::').last}"
- send m, o
- end
-
- private
-
- def visit_AlterTable(o)
- sql = "ALTER TABLE #{quote_table_name(o.name)} "
- sql << o.adds.map { |col| visit_AddColumn col }.join(' ')
- end
-
- def visit_AddColumn(o)
- sql_type = type_to_sql(o.type.to_sym, o.limit, o.precision, o.scale)
- sql = "ADD #{quote_column_name(o.name)} #{sql_type}"
- add_column_options!(sql, column_options(o))
- end
-
- def visit_ColumnDefinition(o)
- sql_type = type_to_sql(o.type.to_sym, o.limit, o.precision, o.scale)
- column_sql = "#{quote_column_name(o.name)} #{sql_type}"
- add_column_options!(column_sql, column_options(o)) unless o.primary_key?
- column_sql
- 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(', ')
- create_sql << ") #{o.options}"
- create_sql
- end
-
- def column_options(o)
- column_options = {}
- column_options[:null] = o.null unless o.null.nil?
- column_options[:default] = o.default unless o.default.nil?
- column_options[:column] = o
- column_options
- end
-
- def quote_column_name(name)
- @conn.quote_column_name name
- end
-
- def quote_table_name(name)
- @conn.quote_table_name name
- end
-
- def type_to_sql(type, limit, precision, scale)
- @conn.type_to_sql type.to_sym, limit, precision, scale
- end
-
- def add_column_options!(column_sql, column_options)
- @conn.add_column_options! column_sql, column_options
- column_sql
- end
- end
-
def schema_creation
SchemaCreation.new self
end
@@ -197,10 +135,11 @@ module ActiveRecord
end
def unprepared_statement
- old, @visitor = @visitor, unprepared_visitor
+ old_prepared_statements, @prepared_statements = @prepared_statements, false
+ old_visitor, @visitor = @visitor, unprepared_visitor
yield
ensure
- @visitor = old
+ @visitor, @prepared_statements = old_visitor, old_prepared_statements
end
# Returns the human-readable name of the adapter. Use mixed case - one
@@ -280,6 +219,14 @@ module ActiveRecord
false
end
+ # This is meant to be implemented by the adapters that support extensions
+ def disable_extension(name)
+ end
+
+ # This is meant to be implemented by the adapters that support extensions
+ def enable_extension(name)
+ end
+
# A list of extensions, to be filled in by adapters that support them. At
# the moment only postgresql does.
def extensions
@@ -294,8 +241,8 @@ module ActiveRecord
# QUOTING ==================================================
- # Returns a bind substitution value given a +column+ and list of current
- # +binds+.
+ # Returns a bind substitution value given a bind +index+ and +column+
+ # NOTE: The column param is currently being used by the sqlserver-adapter
def substitute_at(column, index)
Arel::Nodes::BindParam.new '?'
end
@@ -374,27 +321,13 @@ module ActiveRecord
@transaction.number
end
- def increment_open_transactions
- ActiveSupport::Deprecation.warn "#increment_open_transactions is deprecated and has no effect"
- end
-
- def decrement_open_transactions
- ActiveSupport::Deprecation.warn "#decrement_open_transactions is deprecated and has no effect"
- end
-
- def transaction_joinable=(joinable)
- message = "#transaction_joinable= is deprecated. Please pass the :joinable option to #begin_transaction instead."
- ActiveSupport::Deprecation.warn message
- @transaction.joinable = joinable
+ def create_savepoint(name = nil)
end
- def create_savepoint
+ def rollback_to_savepoint(name = nil)
end
- def rollback_to_savepoint
- end
-
- def release_savepoint
+ def release_savepoint(name = nil)
end
def case_sensitive_modifier(node)
@@ -416,13 +349,14 @@ module ActiveRecord
protected
- def log(sql, name = "SQL", binds = [])
+ def log(sql, name = "SQL", binds = [], statement_name = nil)
@instrumenter.instrument(
"sql.active_record",
- :sql => sql,
- :name => name,
- :connection_id => object_id,
- :binds => binds) { yield }
+ :sql => sql,
+ :name => name,
+ :connection_id => object_id,
+ :statement_name => statement_name,
+ :binds => binds) { yield }
rescue => e
message = "#{e.class.name}: #{e.message}: #{sql}"
@logger.error message if @logger
@@ -435,6 +369,10 @@ module ActiveRecord
# override in derived class
ActiveRecord::StatementInvalid.new(message, exception)
end
+
+ def without_prepared_statement?(binds)
+ !@prepared_statements || binds.empty?
+ end
end
end
end
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 d098ded77c..7768c24967 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
@@ -3,18 +3,29 @@ require 'arel/visitors/bind_visitor'
module ActiveRecord
module ConnectionAdapters
class AbstractMysqlAdapter < AbstractAdapter
+ include Savepoints
+
class SchemaCreation < AbstractAdapter::SchemaCreation
- private
def visit_AddColumn(o)
- add_column_position!(super, o)
+ add_column_position!(super, column_options(o))
end
- def add_column_position!(sql, column)
- if column.first
+ private
+ def visit_ChangeColumnDefinition(o)
+ column = o.column
+ options = o.options
+ sql_type = type_to_sql(o.type, options[:limit], options[:precision], options[:scale])
+ change_column_sql = "CHANGE #{quote_column_name(column.name)} #{quote_column_name(options[:name])} #{sql_type}"
+ add_column_options!(change_column_sql, options)
+ add_column_position!(change_column_sql, options)
+ end
+
+ def add_column_position!(sql, options)
+ if options[:first]
sql << " FIRST"
- elsif column.after
- sql << " AFTER #{quote_column_name(column.after)}"
+ elsif options[:after]
+ sql << " AFTER #{quote_column_name(options[:after])}"
end
sql
end
@@ -137,7 +148,7 @@ module ActiveRecord
QUOTED_TRUE, QUOTED_FALSE = '1', '0'
NATIVE_DATABASE_TYPES = {
- :primary_key => "int(11) DEFAULT NULL auto_increment PRIMARY KEY",
+ :primary_key => "int(11) auto_increment PRIMARY KEY",
:string => { :name => "varchar", :limit => 255 },
:text => { :name => "text" },
:integer => { :name => "int", :limit => 4 },
@@ -165,6 +176,7 @@ module ActiveRecord
@quoted_column_names, @quoted_table_names = {}, {}
if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true })
+ @prepared_statements = true
@visitor = Arel::Visitors::MySQL.new self
else
@visitor = unprepared_visitor
@@ -184,11 +196,6 @@ module ActiveRecord
true
end
- # Returns true, since this connection adapter supports savepoints.
- def supports_savepoints?
- true
- end
-
def supports_bulk_alter? #:nodoc:
true
end
@@ -199,6 +206,17 @@ module ActiveRecord
true
end
+ def type_cast(value, column)
+ case value
+ when TrueClass
+ 1
+ when FalseClass
+ 0
+ else
+ super
+ end
+ end
+
# MySQL 4 technically support transaction isolation, but it is affected by a bug
# where the transaction level gets persisted for the whole session:
#
@@ -237,8 +255,8 @@ module ActiveRecord
# QUOTING ==================================================
def quote(value, column = nil)
- if value.kind_of?(String) && column && column.type == :binary && column.class.respond_to?(:string_to_binary)
- s = column.class.string_to_binary(value).unpack("H*")[0]
+ if value.kind_of?(String) && column && column.type == :binary
+ s = value.unpack("H*")[0]
"x'#{s}'"
elsif value.kind_of?(BigDecimal)
value.to_s("F")
@@ -265,7 +283,7 @@ module ActiveRecord
# REFERENTIAL INTEGRITY ====================================
- def disable_referential_integrity(&block) #:nodoc:
+ def disable_referential_integrity #:nodoc:
old = select_value("SELECT @@FOREIGN_KEY_CHECKS")
begin
@@ -285,12 +303,6 @@ module ActiveRecord
else
log(sql, name) { @connection.query(sql) }
end
- rescue ActiveRecord::StatementInvalid => exception
- if exception.message.split(":").first =~ /Packets out of order/
- raise ActiveRecord::StatementInvalid.new("'Packets out of order' error was received from the database. Please update your mysql bindings (gem install mysql) and read http://dev.mysql.com/doc/mysql/en/password-hashing.html for more information. If you're on Windows, use the Instant Rails installer to get the updated mysql bindings.", exception.original_exception)
- else
- raise
- end
end
# MysqlAdapter has to free a result after using it, so we use this method to write
@@ -307,39 +319,19 @@ module ActiveRecord
def begin_db_transaction
execute "BEGIN"
- rescue
- # Transactions aren't supported
end
def begin_isolated_db_transaction(isolation)
execute "SET TRANSACTION ISOLATION LEVEL #{transaction_isolation_levels.fetch(isolation)}"
begin_db_transaction
- rescue
- # Transactions aren't supported
end
def commit_db_transaction #:nodoc:
execute "COMMIT"
- rescue
- # Transactions aren't supported
end
def rollback_db_transaction #:nodoc:
execute "ROLLBACK"
- rescue
- # Transactions aren't supported
- end
-
- def create_savepoint
- execute("SAVEPOINT #{current_savepoint_name}")
- end
-
- def rollback_to_savepoint
- execute("ROLLBACK TO SAVEPOINT #{current_savepoint_name}")
- end
-
- def release_savepoint
- execute("RELEASE SAVEPOINT #{current_savepoint_name}")
end
# In the simple case, MySQL allows us to place JOINs directly into the UPDATE
@@ -460,7 +452,8 @@ module ActiveRecord
sql = "SHOW FULL FIELDS FROM #{quote_table_name(table_name)}"
execute_and_free(sql, 'SCHEMA') do |result|
each_hash(result).map do |field|
- new_column(field[:Field], field[:Default], field[:Type], field[:Null] == "YES", field[:Collation], field[:Extra])
+ field_name = set_field_encoding(field[:Field])
+ new_column(field_name, field[:Default], field[:Type], field[:Null] == "YES", field[:Collation], field[:Extra])
end
end
end
@@ -493,6 +486,18 @@ module ActiveRecord
rename_table_indexes(table_name, new_name)
end
+ def drop_table(table_name, options = {})
+ execute "DROP#{' TEMPORARY' if options[:temporary]} TABLE #{quote_table_name(table_name)}"
+ end
+
+ def rename_index(table_name, old_name, new_name)
+ if (version[0] == 5 && version[1] >= 7) || version[0] >= 6
+ execute "ALTER TABLE #{quote_table_name(table_name)} RENAME INDEX #{quote_table_name(old_name)} TO #{quote_table_name(new_name)}"
+ else
+ super
+ end
+ end
+
def change_column_default(table_name, column_name, default)
column = column_for(table_name, column_name)
change_column table_name, column_name, column.sql_type, :default => default
@@ -661,10 +666,9 @@ module ActiveRecord
end
def add_column_sql(table_name, column_name, type, options = {})
- add_column_sql = "ADD #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
- add_column_options!(add_column_sql, options)
- add_column_position!(add_column_sql, options)
- add_column_sql
+ td = create_table_definition table_name, options[:temporary], options[:options]
+ cd = td.new_column_definition(column_name, type, options)
+ schema_creation.visit_AddColumn cd
end
def change_column_sql(table_name, column_name, type, options = {})
@@ -678,14 +682,12 @@ module ActiveRecord
options[:null] = column.null
end
- change_column_sql = "CHANGE #{quote_column_name(column_name)} #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
- add_column_options!(change_column_sql, options)
- add_column_position!(change_column_sql, options)
- change_column_sql
+ options[:name] = column.name
+ schema_creation.accept ChangeColumnDefinition.new column, type, options
end
def rename_column_sql(table_name, column_name, new_column_name)
- options = {}
+ options = { name: new_column_name }
if column = columns(table_name).find { |c| c.name == column_name.to_s }
options[:default] = column.default
@@ -696,9 +698,7 @@ module ActiveRecord
end
current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'", 'SCHEMA')["Type"]
- rename_column_sql = "CHANGE #{quote_column_name(column_name)} #{quote_column_name(new_column_name)} #{current_type}"
- add_column_options!(rename_column_sql, options)
- rename_column_sql
+ schema_creation.accept ChangeColumnDefinition.new column, current_type, options
end
def remove_column_sql(table_name, column_name, type = nil, options = {})
diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb
index 609ccc2ed2..f2fbd5a8f2 100644
--- a/activerecord/lib/active_record/connection_adapters/column.rb
+++ b/activerecord/lib/active_record/connection_adapters/column.rb
@@ -13,7 +13,7 @@ module ActiveRecord
ISO_DATETIME = /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)(\.\d+)?\z/
end
- attr_reader :name, :default, :type, :limit, :null, :sql_type, :precision, :scale
+ attr_reader :name, :default, :type, :limit, :null, :sql_type, :precision, :scale, :default_function
attr_accessor :primary, :coder
alias :encoded? :coder
@@ -27,16 +27,17 @@ module ActiveRecord
# It will be mapped to one of the standard Rails SQL types in the <tt>type</tt> attribute.
# +null+ determines if this column allows +NULL+ values.
def initialize(name, default, sql_type = nil, null = true)
- @name = name
- @sql_type = sql_type
- @null = null
- @limit = extract_limit(sql_type)
- @precision = extract_precision(sql_type)
- @scale = extract_scale(sql_type)
- @type = simplified_type(sql_type)
- @default = extract_default(default)
- @primary = nil
- @coder = nil
+ @name = name
+ @sql_type = sql_type
+ @null = null
+ @limit = extract_limit(sql_type)
+ @precision = extract_precision(sql_type)
+ @scale = extract_scale(sql_type)
+ @type = simplified_type(sql_type)
+ @default = extract_default(default)
+ @default_function = nil
+ @primary = nil
+ @coder = nil
end
# Returns +true+ if the column is either of type string or text.
@@ -107,30 +108,6 @@ module ActiveRecord
end
end
- def type_cast_code(var_name)
- message = "Column#type_cast_code is deprecated in favor of using Column#type_cast only, " \
- "and it is going to be removed in future Rails versions."
- ActiveSupport::Deprecation.warn message
-
- klass = self.class.name
-
- case type
- when :string, :text then var_name
- when :integer then "#{klass}.value_to_integer(#{var_name})"
- when :float then "#{var_name}.to_f"
- when :decimal then "#{klass}.value_to_decimal(#{var_name})"
- when :datetime, :timestamp then "#{klass}.string_to_time(#{var_name})"
- when :time then "#{klass}.string_to_dummy_time(#{var_name})"
- when :date then "#{klass}.value_to_date(#{var_name})"
- when :binary then "#{klass}.binary_to_string(#{var_name})"
- when :boolean then "#{klass}.value_to_boolean(#{var_name})"
- when :hstore then "#{klass}.string_to_hstore(#{var_name})"
- when :inet, :cidr then "#{klass}.string_to_cidr(#{var_name})"
- when :json then "#{klass}.string_to_json(#{var_name})"
- else var_name
- end
- end
-
# Returns the human name of the column name.
#
# ===== Examples
@@ -143,17 +120,7 @@ module ActiveRecord
type_cast(default)
end
- # Used to convert from Strings to BLOBs
- def string_to_binary(value)
- self.class.string_to_binary(value)
- end
-
class << self
- # Used to convert from Strings to BLOBs
- def string_to_binary(value)
- value
- end
-
# Used to convert from BLOBs to Strings
def binary_to_string(value)
value
@@ -237,11 +204,19 @@ module ActiveRecord
end
end
- def new_time(year, mon, mday, hour, min, sec, microsec)
+ def new_time(year, mon, mday, hour, min, sec, microsec, offset = nil)
# Treat 0000-00-00 00:00:00 as nil.
return nil if year.nil? || (year == 0 && mon == 0 && mday == 0)
- Time.send(Base.default_timezone, year, mon, mday, hour, min, sec, microsec) rescue nil
+ if offset
+ time = Time.utc(year, mon, mday, hour, min, sec, microsec) rescue nil
+ return nil 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
def fast_string_to_date(string)
@@ -266,7 +241,7 @@ module ActiveRecord
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))
+ new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction, :offset))
end
end
@@ -306,7 +281,7 @@ module ActiveRecord
:text
when /blob/i, /binary/i
:binary
- when /char/i, /string/i
+ when /char/i
:string
when /boolean/i
:boolean
diff --git a/activerecord/lib/active_record/connection_adapters/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/connection_specification.rb
index 8bad7d0cf5..3f8b14bf67 100644
--- a/activerecord/lib/active_record/connection_adapters/connection_specification.rb
+++ b/activerecord/lib/active_record/connection_adapters/connection_specification.rb
@@ -13,41 +13,146 @@ module ActiveRecord
@config = original.config.dup
end
+ # Expands a connection string into a hash.
+ class ConnectionUrlResolver # :nodoc:
+
+ # == Example
+ #
+ # url = "postgresql://foo:bar@localhost:9000/foo_test?pool=5&timeout=3000"
+ # ConnectionUrlResolver.new(url).to_hash
+ # # => {
+ # "adapter" => "postgresql",
+ # "host" => "localhost",
+ # "port" => 9000,
+ # "database" => "foo_test",
+ # "username" => "foo",
+ # "password" => "bar",
+ # "pool" => "5",
+ # "timeout" => "3000"
+ # }
+ def initialize(url)
+ raise "Database URL cannot be empty" if url.blank?
+ @uri = URI.parse(url)
+ @adapter = @uri.scheme
+ @adapter = "postgresql" if @adapter == "postgres"
+ @query = @uri.query || ''
+ end
+
+ # Converts the given URL to a full connection hash.
+ def to_hash
+ config = raw_config.reject { |_,value| value.blank? }
+ config.map { |key,value| config[key] = uri_parser.unescape(value) if value.is_a? String }
+ config
+ end
+
+ private
+
+ def uri
+ @uri
+ end
+
+ def uri_parser
+ @uri_parser ||= URI::Parser.new
+ end
+
+ # Converts the query parameters of the URI into a hash.
+ #
+ # "localhost?pool=5&reap_frequency=2"
+ # # => { "pool" => "5", "reap_frequency" => "2" }
+ #
+ # returns empty hash if no query present.
+ #
+ # "localhost"
+ # # => {}
+ def query_hash
+ Hash[@query.split("&").map { |pair| pair.split("=") }]
+ end
+
+ def raw_config
+ query_hash.merge({
+ "adapter" => @adapter,
+ "username" => uri.user,
+ "password" => uri.password,
+ "port" => uri.port,
+ "database" => database,
+ "host" => uri.host })
+ end
+
+ # Returns name of the database.
+ # Sqlite3 expects this to be a full path or `:memory:`.
+ def database
+ if @adapter == 'sqlite3'
+ if '/:memory:' == uri.path
+ ':memory:'
+ else
+ uri.path
+ end
+ else
+ uri.path.sub(%r{^/},"")
+ end
+ end
+ end
+
##
- # Builds a ConnectionSpecification from user input
+ # Builds a ConnectionSpecification from user input.
class Resolver # :nodoc:
- attr_reader :config, :klass, :configurations
+ attr_reader :configurations
- def initialize(config, configurations)
- @config = config
+ # Accepts a hash two layers deep, keys on the first layer represent
+ # environments such as "production". Keys must be strings.
+ def initialize(configurations)
@configurations = configurations
end
- def spec
- case config
- when nil
- raise AdapterNotSpecified unless defined?(Rails.env)
- resolve_string_connection Rails.env
- when Symbol, String
- resolve_string_connection config.to_s
- when Hash
- resolve_hash_connection config
+ # Returns a hash with database connection information.
+ #
+ # == Examples
+ #
+ # Full hash Configuration.
+ #
+ # configurations = { "production" => { "host" => "localhost", "database" => "foo", "adapter" => "sqlite3" } }
+ # Resolver.new(configurations).resolve(:production)
+ # # => { "host" => "localhost", "database" => "foo", "adapter" => "sqlite3"}
+ #
+ # Initialized with URL configuration strings.
+ #
+ # configurations = { "production" => "postgresql://localhost/foo" }
+ # Resolver.new(configurations).resolve(:production)
+ # # => { "host" => "localhost", "database" => "foo", "adapter" => "postgresql" }
+ #
+ def resolve(config)
+ if config
+ resolve_connection config
+ elsif env = ActiveRecord::ConnectionHandling::RAILS_ENV.call
+ resolve_env_connection env.to_sym
+ else
+ raise AdapterNotSpecified
end
end
- private
- def resolve_string_connection(spec) # :nodoc:
- hash = configurations.fetch(spec) do |k|
- connection_url_to_hash(k)
+ # Expands each key in @configurations hash into fully resolved hash
+ def resolve_all
+ config = configurations.dup
+ config.each do |key, value|
+ config[key] = resolve(value) if value
end
-
- raise(AdapterNotSpecified, "#{spec} database is not configured") unless hash
-
- resolve_hash_connection hash
+ config
end
- def resolve_hash_connection(spec) # :nodoc:
- spec = spec.symbolize_keys
+ # Returns an instance of ConnectionSpecification for a given adapter.
+ # Accepts a hash one layer deep that contains all connection information.
+ #
+ # == Example
+ #
+ # config = { "production" => { "host" => "localhost", "database" => "foo", "adapter" => "sqlite3" } }
+ # spec = Resolver.new(config).spec(:production)
+ # spec.adapter_method
+ # # => "sqlite3"
+ # spec.config
+ # # => { "host" => "localhost", "database" => "foo", "adapter" => "sqlite3" }
+ #
+ def spec(config)
+ spec = resolve(config).symbolize_keys
raise(AdapterNotSpecified, "database configuration does not specify adapter") unless spec.key?(:adapter)
@@ -55,41 +160,93 @@ module ActiveRecord
begin
require path_to_adapter
rescue Gem::LoadError => e
- raise Gem::LoadError, "Specified '#{spec[:adapter]}' for database adapter, but the gem is not loaded. Add `gem '#{e.name}'` to your Gemfile."
+ raise Gem::LoadError, "Specified '#{spec[:adapter]}' for database adapter, but the gem is not loaded. Add `gem '#{e.name}'` to your Gemfile (and ensure its version is at the minimum required by ActiveRecord)."
rescue LoadError => e
raise LoadError, "Could not load '#{path_to_adapter}'. Make sure that the adapter in config/database.yml is valid. If you use an adapter other than 'mysql', 'mysql2', 'postgresql' or 'sqlite3' add the necessary adapter gem to the Gemfile.", e.backtrace
end
adapter_method = "#{spec[:adapter]}_connection"
-
ConnectionSpecification.new(spec, adapter_method)
end
- def connection_url_to_hash(url) # :nodoc:
- config = URI.parse url
- adapter = config.scheme
- adapter = "postgresql" if adapter == "postgres"
- spec = { :adapter => adapter,
- :username => config.user,
- :password => config.password,
- :port => config.port,
- :database => config.path.sub(%r{^/},""),
- :host => config.host }
-
- spec.reject!{ |_,value| value.blank? }
-
- uri_parser = URI::Parser.new
-
- spec.map { |key,value| spec[key] = uri_parser.unescape(value) if value.is_a?(String) }
+ private
- if config.query
- options = Hash[config.query.split("&").map{ |pair| pair.split("=") }].symbolize_keys
+ # Returns fully resolved connection, accepts hash, string or symbol.
+ # Always returns a hash.
+ #
+ # == Examples
+ #
+ # Symbol representing current environment.
+ #
+ # Resolver.new("production" => {}).resolve_connection(:production)
+ # # => {}
+ #
+ # One layer deep hash of connection values.
+ #
+ # Resolver.new({}).resolve_connection("adapter" => "sqlite3")
+ # # => { "adapter" => "sqlite3" }
+ #
+ # Connection URL.
+ #
+ # Resolver.new({}).resolve_connection("postgresql://localhost/foo")
+ # # => { "host" => "localhost", "database" => "foo", "adapter" => "postgresql" }
+ #
+ def resolve_connection(spec)
+ case spec
+ when Symbol, String
+ resolve_env_connection spec
+ when Hash
+ resolve_hash_connection spec
+ end
+ end
- spec.merge!(options)
+ # Takes the environment such as `:production` or `:development`.
+ # This requires that the @configurations was initialized with a key that
+ # matches.
+ #
+ #
+ # Resolver.new("production" => {}).resolve_env_connection(:production)
+ # # => {}
+ #
+ # Takes a connection URL.
+ #
+ # Resolver.new({}).resolve_env_connection("postgresql://localhost/foo")
+ # # => { "host" => "localhost", "database" => "foo", "adapter" => "postgresql" }
+ #
+ def resolve_env_connection(spec)
+ # Rails has historically accepted a string to mean either
+ # an environment key or a URL spec, so we have deprecated
+ # this ambiguous behaviour and in the future this function
+ # can be removed in favor of resolve_string_connection and
+ # resolve_symbol_connection.
+ if config = configurations[spec.to_s]
+ if spec.is_a?(String)
+ ActiveSupport::Deprecation.warn "Passing a string to ActiveRecord::Base.establish_connection " \
+ "for a configuration lookup is deprecated, please pass a symbol (#{spec.to_sym.inspect}) instead"
+ end
+ resolve_connection(config)
+ elsif spec.is_a?(String)
+ resolve_string_connection(spec)
+ else
+ raise(AdapterNotSpecified, "'#{spec}' database is not configured. Available configuration: #{configurations.inspect}")
end
+ end
+ # Accepts a hash. Expands the "url" key that contains a
+ # URL database connection to a full connection
+ # hash and merges with the rest of the hash.
+ # Connection details inside of the "url" key win any merge conflicts
+ def resolve_hash_connection(spec)
+ if url = spec.delete("url")
+ connection_hash = resolve_string_connection(url)
+ spec.merge!(connection_hash)
+ end
spec
end
+
+ def resolve_string_connection(url)
+ ConnectionUrlResolver.new(url).to_hash
+ 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 530a27d099..6d8e994654 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.10'
+gem 'mysql2', '~> 0.3.13'
require 'mysql2'
module ActiveRecord
@@ -18,6 +18,12 @@ module ActiveRecord
client = Mysql2::Client.new(config)
options = [config[:host], config[:username], config[:password], config[:database], config[:port], config[:socket], 0]
ConnectionAdapters::Mysql2Adapter.new(client, logger, options, config)
+ rescue Mysql2::Error => error
+ if error.message.include?("Unknown database")
+ raise ActiveRecord::NoDatabaseError.new(error.message)
+ else
+ raise error
+ end
end
end
@@ -213,9 +219,11 @@ module ActiveRecord
# Executes the SQL statement in the context of this connection.
def execute(sql, name = nil)
- # make sure we carry over any changes to ActiveRecord::Base.default_timezone that have been
- # made since we established the connection
- @connection.query_options[:database_timezone] = ActiveRecord::Base.default_timezone
+ if @connection
+ # make sure we carry over any changes to ActiveRecord::Base.default_timezone that have been
+ # made since we established the connection
+ @connection.query_options[:database_timezone] = ActiveRecord::Base.default_timezone
+ end
super
end
@@ -227,8 +235,7 @@ module ActiveRecord
alias exec_without_stmt exec_query
- # Returns an array of record hashes with the column names as keys and
- # column values as values.
+ # Returns an ActiveRecord::Result instance.
def select(sql, name = nil, binds = [])
exec_query(sql, name)
end
@@ -268,6 +275,10 @@ module ActiveRecord
def version
@version ||= @connection.info[:version].scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map { |v| v.to_i }
end
+
+ def set_field_encoding field_name
+ field_name
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
index 1826d88500..7dbaa272a3 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
@@ -34,6 +34,12 @@ module ActiveRecord
default_flags |= Mysql::CLIENT_FOUND_ROWS if Mysql.const_defined?(:CLIENT_FOUND_ROWS)
options = [host, username, password, database, port, socket, default_flags]
ConnectionAdapters::MysqlAdapter.new(mysql, logger, options, config)
+ rescue Mysql::Error => error
+ if error.message.include?("Unknown database")
+ raise ActiveRecord::NoDatabaseError.new(error.message)
+ else
+ raise error
+ end
end
end
@@ -160,12 +166,6 @@ module ActiveRecord
# QUOTING ==================================================
- def type_cast(value, column)
- return super unless value == true || value == false
-
- value ? 1 : 0
- end
-
def quote_string(string) #:nodoc:
@connection.quote(string)
end
@@ -279,11 +279,7 @@ module ActiveRecord
end
def exec_query(sql, name = 'SQL', binds = [])
- # If the configuration sets prepared_statements:false, binds will
- # always be empty, since the bind variables will have been already
- # substituted and removed from binds by BindVisitor, so this will
- # effectively disable prepared statement usage completely.
- if binds.empty?
+ if without_prepared_statement?(binds)
result_set, affected_rows = exec_without_stmt(sql, name)
else
result_set, affected_rows = exec_stmt(sql, name, binds)
@@ -429,14 +425,19 @@ module ActiveRecord
if result
types = {}
+ fields = []
result.fetch_fields.each { |field|
+ field_name = field.name
+ fields << field_name
+
if field.decimals > 0
- types[field.name] = Fields::Decimal.new
+ types[field_name] = Fields::Decimal.new
else
- types[field.name] = Fields.find_type field
+ types[field_name] = Fields.find_type field
end
}
- result_set = ActiveRecord::Result.new(types.keys, result.to_a, types)
+
+ result_set = ActiveRecord::Result.new(fields, result.to_a, types)
result.free
else
result_set = ActiveRecord::Result.new([], [])
@@ -472,15 +473,17 @@ module ActiveRecord
def begin_db_transaction #:nodoc:
exec_query "BEGIN"
- rescue Mysql::Error
- # Transactions aren't supported
end
private
def exec_stmt(sql, name, binds)
cache = {}
- log(sql, name, binds) do
+ type_casted_binds = binds.map { |col, val|
+ [col, type_cast(val, col)]
+ }
+
+ log(sql, name, type_casted_binds) do
if binds.empty?
stmt = @connection.prepare(sql)
else
@@ -491,7 +494,7 @@ module ActiveRecord
end
begin
- stmt.execute(*binds.map { |col, val| type_cast(val, col) })
+ stmt.execute(*type_casted_binds.map { |_, val| val })
rescue Mysql::Error => e
# Older versions of MySQL leave the prepared statement in a bad
# place when an error occurs. To support older mysql versions, we
@@ -507,12 +510,12 @@ module ActiveRecord
cols = cache[:cols] ||= metadata.fetch_fields.map { |field|
field.name
}
+ metadata.free
end
result_set = ActiveRecord::Result.new(cols, stmt.to_a) if cols
affected_rows = stmt.affected_rows
- stmt.result_metadata.free if cols
stmt.free_result
stmt.close if binds.empty?
@@ -559,6 +562,14 @@ module ActiveRecord
def version
@version ||= @connection.server_info.scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map { |v| v.to_i }
end
+
+ def set_field_encoding field_name
+ field_name.force_encoding(client_encoding)
+ if internal_enc = Encoding.default_internal
+ field_name = field_name.encode!(internal_enc)
+ end
+ field_name
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb b/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb
index b7d24f2bb3..20de8d1982 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb
@@ -2,6 +2,13 @@ module ActiveRecord
module ConnectionAdapters
class PostgreSQLColumn < Column
module ArrayParser
+
+ DOUBLE_QUOTE = '"'
+ BACKSLASH = "\\"
+ COMMA = ','
+ BRACKET_OPEN = '{'
+ BRACKET_CLOSE = '}'
+
private
# Loads pg_array_parser if available. String parsing can be
# performed quicker by a native extension, which will not create
@@ -12,18 +19,18 @@ module ActiveRecord
include PgArrayParser
rescue LoadError
def parse_pg_array(string)
- parse_data(string, 0)
+ parse_data(string)
end
end
- def parse_data(string, index)
- local_index = index
+ def parse_data(string)
+ local_index = 0
array = []
while(local_index < string.length)
case string[local_index]
- when '{'
+ when BRACKET_OPEN
local_index,array = parse_array_contents(array, string, local_index + 1)
- when '}'
+ when BRACKET_CLOSE
return array
end
local_index += 1
@@ -33,9 +40,9 @@ module ActiveRecord
end
def parse_array_contents(array, string, index)
- is_escaping = false
- is_quoted = false
- was_quoted = false
+ is_escaping = false
+ is_quoted = false
+ was_quoted = false
current_item = ''
local_index = index
@@ -47,29 +54,29 @@ module ActiveRecord
else
if is_quoted
case token
- when '"'
+ when DOUBLE_QUOTE
is_quoted = false
was_quoted = true
- when "\\"
+ when BACKSLASH
is_escaping = true
else
current_item << token
end
else
case token
- when "\\"
+ when BACKSLASH
is_escaping = true
- when ','
+ when COMMA
add_item_to_array(array, current_item, was_quoted)
current_item = ''
was_quoted = false
- when '"'
+ when DOUBLE_QUOTE
is_quoted = true
- when '{'
+ when BRACKET_OPEN
internal_items = []
local_index,internal_items = parse_array_contents(internal_items, string, local_index + 1)
array.push(internal_items)
- when '}'
+ when BRACKET_CLOSE
add_item_to_array(array, current_item, was_quoted)
return local_index,array
else
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb b/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb
index a73f0ac57f..35ce881302 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb
@@ -17,8 +17,8 @@ module ActiveRecord
return string unless String === string
case string
- when 'infinity'; 1.0 / 0.0
- when '-infinity'; -1.0 / 0.0
+ when 'infinity'; Float::INFINITY
+ when '-infinity'; -Float::INFINITY
when / BC$/
super("-" + string.sub(/ BC$/, ""))
else
@@ -67,7 +67,7 @@ module ActiveRecord
end
end
- def array_to_string(value, column, adapter, should_be_quoted = false)
+ def array_to_string(value, column, adapter)
casted_values = value.map do |val|
if String === val
if val == "NULL"
@@ -100,7 +100,11 @@ module ActiveRecord
if string.nil?
nil
elsif String === string
- IPAddr.new(string)
+ begin
+ IPAddr.new(string)
+ rescue ArgumentError
+ nil
+ end
else
string
end
@@ -115,7 +119,7 @@ module ActiveRecord
end
def string_to_array(string, oid)
- parse_pg_array(string).map{|val| oid.type_cast val}
+ parse_pg_array(string).map {|val| type_cast_array(oid, val)}
end
private
@@ -140,12 +144,20 @@ module ActiveRecord
def quote_and_escape(value)
case value
- when "NULL"
+ when "NULL", Numeric
value
else
"\"#{value.gsub(/"/,"\\\"")}\""
end
end
+
+ def type_cast_array(oid, value)
+ if ::Array === value
+ value.map {|item| type_cast_array(oid, item)}
+ else
+ oid.type_cast value
+ end
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb
index 9b5170f657..f349c37724 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb
@@ -134,34 +134,31 @@ module ActiveRecord
end
def exec_query(sql, name = 'SQL', binds = [])
- log(sql, name, binds) do
- result = binds.empty? ? exec_no_cache(sql, binds) :
- exec_cache(sql, binds)
-
- types = {}
- result.fields.each_with_index do |fname, i|
- ftype = result.ftype i
- fmod = result.fmod i
- types[fname] = OID::TYPE_MAP.fetch(ftype, fmod) { |oid, mod|
- warn "unknown OID: #{fname}(#{oid}) (#{sql})"
- OID::Identity.new
- }
- end
-
- ret = ActiveRecord::Result.new(result.fields, result.values, types)
- result.clear
- return ret
+ result = without_prepared_statement?(binds) ? exec_no_cache(sql, name, binds) :
+ exec_cache(sql, name, binds)
+
+ types = {}
+ fields = result.fields
+ fields.each_with_index do |fname, i|
+ ftype = result.ftype i
+ fmod = result.fmod i
+ types[fname] = type_map.fetch(ftype, fmod) { |oid, mod|
+ warn "unknown OID: #{fname}(#{oid}) (#{sql})"
+ OID::Identity.new
+ }
end
+
+ ret = ActiveRecord::Result.new(fields, result.values, types)
+ result.clear
+ return ret
end
def exec_delete(sql, name = 'SQL', binds = [])
- log(sql, name, binds) do
- result = binds.empty? ? exec_no_cache(sql, binds) :
- exec_cache(sql, binds)
- affected = result.cmd_tuples
- result.clear
- affected
- end
+ result = without_prepared_statement?(binds) ? exec_no_cache(sql, name, binds) :
+ exec_cache(sql, name, binds)
+ affected = result.cmd_tuples
+ result.clear
+ affected
end
alias :exec_update :exec_delete
@@ -217,25 +214,6 @@ module ActiveRecord
def rollback_db_transaction
execute "ROLLBACK"
end
-
- def outside_transaction?
- message = "#outside_transaction? is deprecated. This method was only really used " \
- "internally, but you can use #transaction_open? instead."
- ActiveSupport::Deprecation.warn message
- @connection.transaction_status == PGconn::PQTRANS_IDLE
- end
-
- def create_savepoint
- execute("SAVEPOINT #{current_savepoint_name}")
- end
-
- def rollback_to_savepoint
- execute("ROLLBACK TO SAVEPOINT #{current_savepoint_name}")
- end
-
- def release_savepoint
- execute("RELEASE SAVEPOINT #{current_savepoint_name}")
- end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb
index 1be116ce10..fae260a921 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb
@@ -6,10 +6,6 @@ module ActiveRecord
module OID
class Type
def type; end
-
- def type_cast_for_write(value)
- value
- end
end
class Identity < Type
@@ -38,12 +34,17 @@ module ActiveRecord
class Money < Type
def type_cast(value)
return if value.nil?
+ return value unless String === value
# Because money output is formatted according to the locale, there are two
# cases to consider (note the decimal separators):
# (1) $12,345,678.12
# (2) $12.345.678,12
+ # Negative values are represented as follows:
+ # (3) -$2.55
+ # (4) ($2.55)
+ value.sub!(/^\((.+)\)$/, '-\1') # (4)
case value
when /^-?\D+[\d,]+\.\d{2}$/ # (1)
value.gsub!(/[^-\d.]/, '')
@@ -224,11 +225,19 @@ module ActiveRecord
end
class Hstore < Type
+ def type_cast_for_write(value)
+ ConnectionAdapters::PostgreSQLColumn.hstore_to_string value
+ end
+
def type_cast(value)
return if value.nil?
ConnectionAdapters::PostgreSQLColumn.string_to_hstore value
end
+
+ def accessor
+ ActiveRecord::Store::StringKeyedHashAccessor
+ end
end
class Cidr < Type
@@ -240,11 +249,19 @@ module ActiveRecord
end
class Json < Type
+ def type_cast_for_write(value)
+ ConnectionAdapters::PostgreSQLColumn.json_to_string value
+ end
+
def type_cast(value)
return if value.nil?
ConnectionAdapters::PostgreSQLColumn.string_to_json value
end
+
+ def accessor
+ ActiveRecord::Store::StringKeyedHashAccessor
+ end
end
class TypeMap
@@ -284,17 +301,15 @@ module ActiveRecord
end
end
- TYPE_MAP = TypeMap.new # :nodoc:
-
- # When the PG adapter connects, the pg_type table is queried. The
+ # When the PG adapter connects, the pg_type table is queried. The
# key of this hash maps to the `typname` column from the table.
- # TYPE_MAP is then dynamically built with oids as the key and type
+ # type_map is then dynamically built with oids as the key and type
# objects as values.
NAMES = Hash.new { |h,k| # :nodoc:
h[k] = OID::Identity.new
}
- # Register an OID type named +name+ with a typcasting object in
+ # Register an OID type named +name+ with a typecasting object in
# +type+. +name+ should correspond to the `typname` column in
# the `pg_type` table.
def self.register_type(name, type)
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
index e9daa5d7ff..c1f978a081 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
@@ -86,8 +86,11 @@ module ActiveRecord
case value
when Range
- return super(value, column) unless /range$/ =~ column.sql_type
- PostgreSQLColumn.range_to_string(value)
+ if /range$/ =~ column.sql_type
+ PostgreSQLColumn.range_to_string(value)
+ else
+ super(value, column)
+ end
when NilClass
if column.array && array_member
'NULL'
@@ -101,12 +104,21 @@ module ActiveRecord
when 'point' then PostgreSQLColumn.point_to_string(value)
when 'json' then PostgreSQLColumn.json_to_string(value)
else
- return super(value, column) unless column.array
- PostgreSQLColumn.array_to_string(value, column, self)
+ if column.array
+ PostgreSQLColumn.array_to_string(value, column, self)
+ else
+ super(value, column)
+ end
end
when String
- return super(value, column) unless 'bytea' == column.sql_type
- { :value => value, :format => 1 }
+ if 'bytea' == column.sql_type
+ # Return a bind param hash with format as binary.
+ # See http://deveiate.org/code/pg/PGconn.html#method-i-exec_prepared-doc
+ # for more information
+ { value: value, format: 1 }
+ else
+ super(value, column)
+ end
when Hash
case column.sql_type
when 'hstore' then PostgreSQLColumn.hstore_to_string(value)
@@ -114,8 +126,11 @@ module ActiveRecord
else super(value, column)
end
when IPAddr
- return super(value, column) unless ['inet','cidr'].include? column.sql_type
- PostgreSQLColumn.cidr_to_string(value)
+ if %w(inet cidr).include? column.sql_type
+ PostgreSQLColumn.cidr_to_string(value)
+ else
+ super(value, column)
+ end
else
super(value, column)
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 a651b6c32e..571257f6dd 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
@@ -46,7 +46,7 @@ module ActiveRecord
end
# Create a new PostgreSQL database. Options include <tt>:owner</tt>, <tt>:template</tt>,
- # <tt>:encoding</tt>, <tt>:collation</tt>, <tt>:ctype</tt>,
+ # <tt>:encoding</tt> (defaults to utf8), <tt>:collation</tt>, <tt>:ctype</tt>,
# <tt>:tablespace</tt>, and <tt>:connection_limit</tt> (note that MySQL uses
# <tt>:charset</tt> while PostgreSQL uses <tt>:encoding</tt>).
#
@@ -172,7 +172,7 @@ module ActiveRecord
def columns(table_name)
# Limit, precision, and scale are all handled by the superclass.
column_definitions(table_name).map do |column_name, type, default, notnull, oid, fmod|
- oid = OID::TYPE_MAP.fetch(oid.to_i, fmod.to_i) {
+ oid = type_map.fetch(oid.to_i, fmod.to_i) {
OID::Identity.new
}
PostgreSQLColumn.new(column_name, default, oid, type, notnull == 'f')
@@ -384,8 +384,9 @@ module ActiveRecord
def change_column(table_name, column_name, type, options = {})
clear_cache!
quoted_table_name = quote_table_name(table_name)
-
- execute "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quote_column_name(column_name)} TYPE #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
+ sql_type = type_to_sql(type, options[:limit], options[:precision], options[:scale])
+ sql_type << "[]" if options[:array]
+ execute "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quote_column_name(column_name)} TYPE #{sql_type}"
change_column_default(table_name, column_name, options[:default]) if options_include_default?(options)
change_column_null(table_name, column_name, options[:null], options[:default]) if options.key?(:null)
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
index d5a603cadc..7e188907e1 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
@@ -46,16 +46,28 @@ module ActiveRecord
# PostgreSQL-specific extensions to column definitions in a table.
class PostgreSQLColumn < Column #:nodoc:
attr_accessor :array
- # Instantiates a new PostgreSQL column definition in a table.
+
def initialize(name, default, oid_type, sql_type = nil, null = true)
@oid_type = oid_type
+ default_value = self.class.extract_value_from_default(default)
+
if sql_type =~ /\[\]$/
@array = true
- super(name, self.class.extract_value_from_default(default), sql_type[0..sql_type.length - 3], null)
+ super(name, default_value, sql_type[0..sql_type.length - 3], null)
else
@array = false
- super(name, self.class.extract_value_from_default(default), sql_type, null)
+ super(name, default_value, sql_type, null)
end
+
+ @default_function = default if has_default_function?(default_value, default)
+ end
+
+ def number?
+ !array && super
+ end
+
+ def text?
+ !array && super
end
# :stopdoc:
@@ -84,7 +96,7 @@ module ActiveRecord
$1
# Character types
when /\A\(?'(.*)'::.*\b(?:character varying|bpchar|text)\z/m
- $1
+ $1.gsub(/''/, "'")
# Binary data types
when /\A'(.*)'::bytea\z/m
$1
@@ -129,6 +141,14 @@ module ActiveRecord
end
end
+ def type_cast_for_write(value)
+ if @oid_type.respond_to?(:type_cast_for_write)
+ @oid_type.type_cast_for_write(value)
+ else
+ super
+ end
+ end
+
def type_cast(value)
return if value.nil?
return super if encoded?
@@ -136,8 +156,16 @@ module ActiveRecord
@oid_type.type_cast value
end
+ def accessor
+ @oid_type.accessor
+ end
+
private
+ def has_default_function?(default_value, default)
+ !default_value && (%r{\w+\(.*\)} === default)
+ end
+
def extract_limit(sql_type)
case sql_type
when /^bigint/i; 8
@@ -373,15 +401,11 @@ module ActiveRecord
self
end
- def xml(options = {})
- column(args[0], :text, options)
- end
-
private
- def create_column_definition(name, type)
- ColumnDefinition.new name, type
- end
+ def create_column_definition(name, type)
+ ColumnDefinition.new name, type
+ end
end
class Table < ActiveRecord::ConnectionAdapters::Table
@@ -424,6 +448,7 @@ module ActiveRecord
include ReferentialIntegrity
include SchemaStatements
include DatabaseStatements
+ include Savepoints
# Returns 'PostgreSQL' as adapter name for identification purposes.
def adapter_name
@@ -435,6 +460,7 @@ module ActiveRecord
def prepare_column_options(column, types)
spec = super
spec[:array] = 'true' if column.respond_to?(:array) && column.array
+ spec[:default] = "\"#{column.default_function}\"" if column.default_function
spec
end
@@ -527,6 +553,7 @@ module ActiveRecord
super(connection, logger)
if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true })
+ @prepared_statements = true
@visitor = Arel::Visitors::PostgreSQL.new self
else
@visitor = unprepared_visitor
@@ -546,7 +573,8 @@ module ActiveRecord
raise "Your version of PostgreSQL (#{postgresql_version}) is too old, please upgrade!"
end
- initialize_type_map
+ @type_map = OID::TypeMap.new
+ initialize_type_map(type_map)
@local_tz = execute('SHOW TIME ZONE', 'SCHEMA').first["TimeZone"]
@use_insert_returning = @config.key?(:insert_returning) ? self.class.type_cast_config_to_boolean(@config[:insert_returning]) : true
end
@@ -612,12 +640,6 @@ module ActiveRecord
true
end
- # Returns true, since this connection adapter supports savepoints.
- def supports_savepoints?
- true
- end
-
- # Returns true.
def supports_explain?
true
end
@@ -699,6 +721,10 @@ module ActiveRecord
!native_database_types[type].nil?
end
+ def update_table_definition(table_name, base) #:nodoc:
+ Table.new(table_name, base)
+ end
+
protected
# Returns the version of the connected PostgreSQL server.
@@ -725,65 +751,85 @@ module ActiveRecord
private
+ def type_map
+ @type_map
+ end
+
def reload_type_map
- OID::TYPE_MAP.clear
- initialize_type_map
+ type_map.clear
+ initialize_type_map(type_map)
+ end
+
+ def add_oid(row, records_by_oid, type_map)
+ return type_map if type_map.key? row['type_elem'].to_i
+
+ if OID.registered_type? row['typname']
+ # this composite type is explicitly registered
+ vector = OID::NAMES[row['typname']]
+ else
+ # use the default for composite types
+ unless type_map.key? row['typelem'].to_i
+ add_oid records_by_oid[row['typelem']], records_by_oid, type_map
+ end
+
+ vector = OID::Vector.new row['typdelim'], type_map[row['typelem'].to_i]
+ end
+
+ type_map[row['oid'].to_i] = vector
+ type_map
end
- def initialize_type_map
+ def initialize_type_map(type_map)
result = execute('SELECT oid, typname, typelem, typdelim, typinput FROM pg_type', 'SCHEMA')
leaves, nodes = result.partition { |row| row['typelem'] == '0' }
# populate the leaf nodes
leaves.find_all { |row| OID.registered_type? row['typname'] }.each do |row|
- OID::TYPE_MAP[row['oid'].to_i] = OID::NAMES[row['typname']]
+ type_map[row['oid'].to_i] = OID::NAMES[row['typname']]
end
+ records_by_oid = result.group_by { |row| row['oid'] }
+
arrays, nodes = nodes.partition { |row| row['typinput'] == 'array_in' }
# populate composite types
- nodes.find_all { |row| OID::TYPE_MAP.key? row['typelem'].to_i }.each do |row|
- if OID.registered_type? row['typname']
- # this composite type is explicitly registered
- vector = OID::NAMES[row['typname']]
- else
- # use the default for composite types
- vector = OID::Vector.new row['typdelim'], OID::TYPE_MAP[row['typelem'].to_i]
- end
-
- OID::TYPE_MAP[row['oid'].to_i] = vector
+ nodes.each do |row|
+ add_oid row, records_by_oid, type_map
end
# populate array types
- arrays.find_all { |row| OID::TYPE_MAP.key? row['typelem'].to_i }.each do |row|
- array = OID::Array.new OID::TYPE_MAP[row['typelem'].to_i]
- OID::TYPE_MAP[row['oid'].to_i] = array
+ arrays.find_all { |row| type_map.key? row['typelem'].to_i }.each do |row|
+ array = OID::Array.new type_map[row['typelem'].to_i]
+ type_map[row['oid'].to_i] = array
end
end
- FEATURE_NOT_SUPPORTED = "0A000" # :nodoc:
+ FEATURE_NOT_SUPPORTED = "0A000" #:nodoc:
- def exec_no_cache(sql, binds)
- @connection.async_exec(sql)
+ def exec_no_cache(sql, name, binds)
+ log(sql, name, binds) { @connection.async_exec(sql) }
end
- def exec_cache(sql, binds)
+ def exec_cache(sql, name, binds)
stmt_key = prepare_statement(sql)
+ type_casted_binds = binds.map { |col, val|
+ [col, type_cast(val, col)]
+ }
+
+ log(sql, name, type_casted_binds, stmt_key) do
+ @connection.send_query_prepared(stmt_key, type_casted_binds.map { |_, val| val })
+ @connection.block
+ @connection.get_last_result
+ end
+ rescue ActiveRecord::StatementInvalid => e
+ pgerror = e.original_exception
- # Clear the queue
- @connection.get_last_result
- @connection.send_query_prepared(stmt_key, binds.map { |col, val|
- type_cast(val, col)
- })
- @connection.block
- @connection.get_last_result
- rescue PGError => e
# Get the PG code for the failure. Annoyingly, the code for
# prepared statements whose return value may have changed is
# FEATURE_NOT_SUPPORTED. Check here for more details:
# http://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/utils/cache/plancache.c#l573
begin
- code = e.result.result_error_field(PGresult::PG_DIAG_SQLSTATE)
+ code = pgerror.result.result_error_field(PGresult::PG_DIAG_SQLSTATE)
rescue
raise e
end
@@ -808,6 +854,8 @@ module ActiveRecord
unless @statements.key? sql_key
nextkey = @statements.next_key
@connection.prepare nextkey, sql
+ # Clear the queue
+ @connection.get_last_result
@statements[sql_key] = nextkey
end
@statements[sql_key]
@@ -829,6 +877,12 @@ module ActiveRecord
PostgreSQLColumn.money_precision = (postgresql_version >= 80300) ? 19 : 10
configure_connection
+ rescue ::PG::Error => error
+ if error.message.include?("does not exist")
+ raise ActiveRecord::NoDatabaseError.new(error.message)
+ else
+ raise error
+ end
end
# Configures the encoding, verbosity, schema search path, and time zone of the connection.
@@ -933,16 +987,12 @@ module ActiveRecord
end
def extract_table_ref_from_insert_sql(sql)
- sql[/into\s+([^\(]*).*values\s*\(/i]
+ sql[/into\s+([^\(]*).*values\s*\(/im]
$1.strip if $1
end
- def create_table_definition(name, temporary, options)
- TableDefinition.new native_database_types, name, temporary, options
- end
-
- def update_table_definition(table_name, base)
- Table.new(table_name, base)
+ def create_table_definition(name, temporary, options, as = nil)
+ TableDefinition.new native_database_types, name, temporary, options, as
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/schema_cache.rb b/activerecord/lib/active_record/connection_adapters/schema_cache.rb
index 1d7a22e831..e5c9f6f54a 100644
--- a/activerecord/lib/active_record/connection_adapters/schema_cache.rb
+++ b/activerecord/lib/active_record/connection_adapters/schema_cache.rb
@@ -1,4 +1,3 @@
-require 'active_support/deprecation/reporting'
module ActiveRecord
module ConnectionAdapters
@@ -16,13 +15,8 @@ module ActiveRecord
prepare_default_proc
end
- def primary_keys(table_name = nil)
- if table_name
- @primary_keys[table_name]
- else
- ActiveSupport::Deprecation.warn('call primary_keys with a table name!')
- @primary_keys.dup
- end
+ def primary_keys(table_name)
+ @primary_keys[table_name]
end
# A cached lookup for table existence.
@@ -41,34 +35,19 @@ module ActiveRecord
end
end
- def tables(name = nil)
- if name
- @tables[name]
- else
- ActiveSupport::Deprecation.warn('call tables with a name!')
- @tables.dup
- end
+ def tables(name)
+ @tables[name]
end
# Get the columns for a table
- def columns(table = nil)
- if table
- @columns[table]
- else
- ActiveSupport::Deprecation.warn('call columns with a table name!')
- @columns.dup
- end
+ def columns(table)
+ @columns[table]
end
# Get the columns for a table as a hash, key is the column name
# value is the column object.
- def columns_hash(table = nil)
- if table
- @columns_hash[table]
- else
- ActiveSupport::Deprecation.warn('call columns_hash with a table name!')
- @columns_hash.dup
- end
+ def columns_hash(table)
+ @columns_hash[table]
end
# Clears out internal caches
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
index 7d940fe1c9..92bb70ba53 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
@@ -17,18 +17,26 @@ module ActiveRecord
# Allow database path relative to Rails.root, but only if
# the database path is not the special path that tells
# Sqlite to build a database only in memory.
- if defined?(Rails.root) && ':memory:' != config[:database]
- config[:database] = File.expand_path(config[:database], Rails.root)
+ if ':memory:' != config[:database]
+ config[:database] = File.expand_path(config[:database], Rails.root) if defined?(Rails.root)
+ dirname = File.dirname(config[:database])
+ Dir.mkdir(dirname) unless File.directory?(dirname)
end
db = SQLite3::Database.new(
- config[:database],
+ config[:database].to_s,
:results_as_hash => true
)
db.busy_timeout(ConnectionAdapters::SQLite3Adapter.type_cast_config_to_integer(config[:timeout])) if config[:timeout]
ConnectionAdapters::SQLite3Adapter.new(db, logger, config)
+ rescue Errno::ENOENT => error
+ if error.message.include?("No such file or directory")
+ raise ActiveRecord::NoDatabaseError.new(error.message)
+ else
+ raise error
+ end
end
end
@@ -51,6 +59,23 @@ module ActiveRecord
#
# * <tt>:database</tt> - Path to the database file.
class SQLite3Adapter < AbstractAdapter
+ include Savepoints
+
+ NATIVE_DATABASE_TYPES = {
+ primary_key: 'INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL',
+ string: { name: "varchar", limit: 255 },
+ text: { name: "text" },
+ integer: { name: "integer" },
+ float: { name: "float" },
+ decimal: { name: "decimal" },
+ datetime: { name: "datetime" },
+ timestamp: { name: "datetime" },
+ time: { name: "time" },
+ date: { name: "date" },
+ binary: { name: "blob" },
+ boolean: { name: "boolean" }
+ }
+
class Version
include Comparable
@@ -111,6 +136,7 @@ module ActiveRecord
@config = config
if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true })
+ @prepared_statements = true
@visitor = Arel::Visitors::SQLite.new self
else
@visitor = unprepared_visitor
@@ -121,14 +147,12 @@ module ActiveRecord
'SQLite'
end
- # Returns true
def supports_ddl_transactions?
true
end
- # Returns true if SQLite version is '3.6.8' or greater, false otherwise.
def supports_savepoints?
- sqlite_version >= '3.6.8'
+ true
end
# Returns true, since this connection adapter supports prepared statement
@@ -142,7 +166,6 @@ module ActiveRecord
true
end
- # Returns true.
def supports_primary_key? #:nodoc:
true
end
@@ -151,7 +174,6 @@ module ActiveRecord
true
end
- # Returns true
def supports_add_column?
true
end
@@ -173,16 +195,6 @@ module ActiveRecord
@statements.clear
end
- # Returns true
- def supports_count_distinct? #:nodoc:
- true
- end
-
- # Returns true
- def supports_autoincrement? #:nodoc:
- true
- end
-
def supports_index_sort_order?
true
end
@@ -195,20 +207,7 @@ module ActiveRecord
end
def native_database_types #:nodoc:
- {
- :primary_key => default_primary_key_type,
- :string => { :name => "varchar", :limit => 255 },
- :text => { :name => "text" },
- :integer => { :name => "integer" },
- :float => { :name => "float" },
- :decimal => { :name => "decimal" },
- :datetime => { :name => "datetime" },
- :timestamp => { :name => "datetime" },
- :time => { :name => "time" },
- :date => { :name => "date" },
- :binary => { :name => "blob" },
- :boolean => { :name => "boolean" }
- }
+ NATIVE_DATABASE_TYPES
end
# Returns the current database encoding format as a string, eg: 'UTF-8'
@@ -216,7 +215,6 @@ module ActiveRecord
@connection.encoding.to_s
end
- # Returns true.
def supports_explain?
true
end
@@ -224,8 +222,8 @@ module ActiveRecord
# QUOTING ==================================================
def quote(value, column = nil)
- if value.kind_of?(String) && column && column.type == :binary && column.class.respond_to?(:string_to_binary)
- s = column.class.string_to_binary(value).unpack("H*")[0]
+ if value.kind_of?(String) && column && column.type == :binary
+ s = value.unpack("H*")[0]
"x'#{s}'"
else
super
@@ -289,10 +287,13 @@ module ActiveRecord
end
def exec_query(sql, name = nil, binds = [])
- log(sql, name, binds) do
+ type_casted_binds = binds.map { |col, val|
+ [col, type_cast(val, col)]
+ }
- # Don't cache statements without bind values
- if binds.empty?
+ log(sql, name, type_casted_binds) do
+ # Don't cache statements if they are not prepared
+ if without_prepared_statement?(binds)
stmt = @connection.prepare(sql)
cols = stmt.columns
records = stmt.to_a
@@ -305,9 +306,7 @@ module ActiveRecord
stmt = cache[:stmt]
cols = cache[:cols] ||= stmt.columns
stmt.reset!
- stmt.bind_params binds.map { |col, val|
- type_cast(val, col)
- }
+ stmt.bind_params type_casted_binds.map { |_, val| val }
end
ActiveRecord::Result.new(cols, stmt.to_a)
@@ -348,18 +347,6 @@ module ActiveRecord
exec_query(sql, name).rows
end
- def create_savepoint
- execute("SAVEPOINT #{current_savepoint_name}")
- end
-
- def rollback_to_savepoint
- execute("ROLLBACK TO SAVEPOINT #{current_savepoint_name}")
- end
-
- def release_savepoint
- execute("RELEASE SAVEPOINT #{current_savepoint_name}")
- end
-
def begin_db_transaction #:nodoc:
log('begin transaction',nil) { @connection.transaction }
end
@@ -603,17 +590,13 @@ module ActiveRecord
@sqlite_version ||= SQLite3Adapter::Version.new(select_value('select sqlite_version(*)'))
end
- def default_primary_key_type
- if supports_autoincrement?
- 'INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL'
- else
- 'INTEGER PRIMARY KEY NOT NULL'
- end
- end
-
def translate_exception(exception, message)
case exception.message
- when /column(s)? .* (is|are) not unique/
+ # SQLite 3.8.2 returns a newly formatted error message:
+ # UNIQUE constraint failed: *table_name*.*column_name*
+ # 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)
else
super
diff --git a/activerecord/lib/active_record/connection_handling.rb b/activerecord/lib/active_record/connection_handling.rb
index a1943dfcb0..11f6a47158 100644
--- a/activerecord/lib/active_record/connection_handling.rb
+++ b/activerecord/lib/active_record/connection_handling.rb
@@ -1,5 +1,8 @@
module ActiveRecord
module ConnectionHandling
+ RAILS_ENV = -> { Rails.env if defined?(Rails) }
+ DEFAULT_ENV = -> { RAILS_ENV.call || "default_env" }
+
# Establishes the connection to the database. Accepts a hash as input where
# the <tt>:adapter</tt> key must be specified with the name of a database adapter (in lower-case)
# example for regular databases (MySQL, Postgresql, etc):
@@ -32,11 +35,19 @@ 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),
+ # 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
# may be returned on an error.
- def establish_connection(spec = ENV["DATABASE_URL"])
- resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new spec, configurations
- spec = resolver.spec
+ def establish_connection(spec = nil)
+ spec ||= DEFAULT_ENV.call.to_sym
+ resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new configurations
+ spec = resolver.spec(spec)
unless respond_to?(spec.adapter_method)
raise AdapterNotFound, "database configuration specifies nonexistent #{spec.config[:adapter]} adapter"
@@ -46,6 +57,56 @@ module ActiveRecord
connection_handler.establish_connection self, spec
end
+ class MergeAndResolveDefaultUrlConfig # :nodoc:
+ def initialize(raw_configurations, url = ENV['DATABASE_URL'])
+ @raw_config = raw_configurations.dup
+ @url = url
+ end
+
+ # Returns fully resolved connection hashes.
+ # Merges connection information from `ENV['DATABASE_URL']` if available.
+ def resolve
+ ConnectionAdapters::ConnectionSpecification::Resolver.new(config).resolve_all
+ end
+
+ private
+ def config
+ if @url
+ raw_merged_into_default
+ else
+ @raw_config
+ end
+ end
+
+ def raw_merged_into_default
+ default = default_url_hash
+
+ @raw_config.each do |env, values|
+ default[env] = values || {}
+ default[env].merge!("url" => @url) { |h, v1, v2| v1 || v2 } if default[env].is_a?(Hash)
+ end
+ default
+ end
+
+ # When the raw configuration is not present and ENV['DATABASE_URL']
+ # is available we return a hash with the connection information in
+ # the connection URL. This hash responds to any string key with
+ # resolved connection information.
+ def default_url_hash
+ if @raw_config.blank?
+ Hash.new do |hash, key|
+ hash[key] = if key.is_a? String
+ ActiveRecord::ConnectionAdapters::ConnectionSpecification::ConnectionUrlResolver.new(@url).to_hash
+ else
+ nil
+ end
+ end
+ else
+ {}
+ end
+ end
+ end
+
# Returns the connection currently associated with the class. This can
# also be used to "borrow" the connection to do database work unrelated
# to any of the specific Active Records.
diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb
index ba053700f2..cd8690d500 100644
--- a/activerecord/lib/active_record/core.rb
+++ b/activerecord/lib/active_record/core.rb
@@ -42,9 +42,16 @@ module ActiveRecord
# 'database' => 'db/production.sqlite3'
# }
# }
- mattr_accessor :configurations, instance_writer: false
+ def self.configurations=(config)
+ @@configurations = ActiveRecord::ConnectionHandling::MergeAndResolveDefaultUrlConfig.new(config).resolve
+ end
self.configurations = {}
+ # Returns fully resolved configurations hash
+ def self.configurations
+ @@configurations
+ end
+
##
# :singleton-method:
# Determines whether to use Time.utc (using :utc) or Time.local (using :local) when pulling
@@ -69,13 +76,13 @@ module ActiveRecord
mattr_accessor :timestamped_migrations, instance_writer: false
self.timestamped_migrations = true
- ##
- # :singleton-method:
- # Disable implicit join references. This feature was deprecated with Rails 4.
- # If you don't make use of implicit references but still see deprecation warnings
- # you can disable the feature entirely. This will be the default with Rails 4.1.
- mattr_accessor :disable_implicit_join_references, instance_writer: false
- self.disable_implicit_join_references = false
+ # :nodoc:
+ mattr_accessor :maintain_test_schema, instance_accessor: false
+
+ def self.disable_implicit_join_references=(value)
+ ActiveSupport::Deprecation.warn("Implicit join references were removed with Rails 4.1." \
+ "Make sure to remove this configuration because it does nothing.")
+ end
class_attribute :default_connection_handler, instance_writer: false
@@ -91,27 +98,15 @@ module ActiveRecord
end
module ClassMethods
- def inherited(child_class) #:nodoc:
- child_class.initialize_generated_modules
- super
- end
-
def initialize_generated_modules
- @attribute_methods_mutex = Mutex.new
-
- # force attribute methods to be higher in inheritance hierarchy than other generated methods
- generated_attribute_methods.const_set(:AttrNames, Module.new {
- def self.const_missing(name)
- const_set(name, [name.to_s.sub(/ATTR_/, '')].pack('h*').freeze)
- end
- })
+ super
- generated_feature_methods
+ generated_association_methods
end
- def generated_feature_methods
- @generated_feature_methods ||= begin
- mod = const_set(:GeneratedFeatureMethods, Module.new)
+ def generated_association_methods
+ @generated_association_methods ||= begin
+ mod = const_set(:GeneratedAssociationMethods, Module.new)
include mod
mod
end
@@ -123,6 +118,8 @@ module ActiveRecord
super
elsif abstract_class?
"#{super}(abstract)"
+ elsif !connected?
+ "#{super} (call '#{super}.connection' to establish a connection)"
elsif table_exists?
attr_list = columns.map { |c| "#{c.name}: #{c.type}" } * ', '
"#{super}(#{attr_list})"
@@ -139,7 +136,7 @@ module ActiveRecord
# Returns an instance of <tt>Arel::Table</tt> loaded with the current table name.
#
# class Post < ActiveRecord::Base
- # scope :published_and_commented, published.and(self.arel_table[:comments_count].gt(0))
+ # scope :published_and_commented, -> { published.and(self.arel_table[:comments_count].gt(0)) }
# end
def arel_table
@arel_table ||= Arel::Table.new(table_name, arel_engine)
@@ -147,19 +144,18 @@ module ActiveRecord
# Returns the Arel engine.
def arel_engine
- @arel_engine ||= begin
+ @arel_engine ||=
if Base == self || connection_handler.retrieve_connection_pool(self)
self
else
superclass.arel_engine
end
- end
end
private
def relation #:nodoc:
- relation = Relation.new(self, arel_table)
+ relation = Relation.create(self, arel_table)
if finder_needs_type_condition?
relation.where(type_condition).create_with(inheritance_column.to_sym => sti_name)
@@ -177,19 +173,22 @@ module ActiveRecord
# ==== Example:
# # Instantiates a single new object
# User.new(first_name: 'Jamie')
- def initialize(attributes = nil)
+ def initialize(attributes = nil, options = {})
defaults = self.class.column_defaults.dup
defaults.each { |k, v| defaults[k] = v.dup if v.duplicable? }
@attributes = self.class.initialize_attributes(defaults)
- @columns_hash = self.class.column_types.dup
+ @column_types_override = nil
+ @column_types = self.class.column_types
init_internals
init_changed_attributes
ensure_proper_type
populate_with_current_scope_attributes
- assign_attributes(attributes) if attributes
+ # +options+ argument is only needed to make protected_attributes gem easier to hook.
+ # Remove it when we drop support to this gem.
+ init_attributes(attributes, options) if attributes
yield self if block_given?
run_callbacks :initialize unless _initialize_callbacks.empty?
@@ -207,7 +206,8 @@ module ActiveRecord
# post.title # => 'hello world'
def init_with(coder)
@attributes = self.class.initialize_attributes(coder['attributes'])
- @columns_hash = self.class.column_types.merge(coder['column_types'] || {})
+ @column_types_override = coder['column_types']
+ @column_types = self.class.column_types
init_internals
@@ -296,7 +296,7 @@ module ActiveRecord
def ==(comparison_object)
super ||
comparison_object.instance_of?(self.class) &&
- id.present? &&
+ id &&
comparison_object.id == id
end
alias :eql? :==
@@ -324,6 +324,8 @@ module ActiveRecord
def <=>(other_object)
if other_object.is_a?(self.class)
self.to_key <=> other_object.to_key
+ else
+ super
end
end
@@ -338,14 +340,6 @@ module ActiveRecord
@readonly = true
end
- # Returns the connection currently associated with the class. This can
- # also be used to "borrow" the connection to do database work that isn't
- # easily done without going straight to SQL.
- def connection
- ActiveSupport::Deprecation.warn("#connection is deprecated in favour of accessing it via the class")
- self.class.connection
- end
-
def connection_handler
self.class.connection_handler
end
@@ -368,7 +362,7 @@ module ActiveRecord
# Returns a hash of the given methods with their names as keys and returned values as values.
def slice(*methods)
- Hash[methods.map { |method| [method, public_send(method)] }].with_indifferent_access
+ Hash[methods.map! { |method| [method, public_send(method)] }].with_indifferent_access
end
def set_transaction_state(state) # :nodoc:
@@ -438,8 +432,6 @@ module ActiveRecord
@aggregation_cache = {}
@association_cache = {}
@attributes_cache = {}
- @previously_changed = {}
- @changed_attributes = {}
@readonly = false
@destroyed = false
@marked_for_destruction = false
@@ -456,8 +448,14 @@ module ActiveRecord
# optimistic locking) won't get written unless they get marked as changed
self.class.columns.each do |c|
attr, orig_value = c.name, c.default
- @changed_attributes[attr] = orig_value if _field_changed?(attr, orig_value, @attributes[attr])
+ changed_attributes[attr] = orig_value if _field_changed?(attr, orig_value, @attributes[attr])
end
end
+
+ # This method is needed to make protected_attributes gem easier to hook.
+ # Remove it when we drop support to this gem.
+ def init_attributes(attributes, options)
+ assign_attributes(attributes)
+ end
end
end
diff --git a/activerecord/lib/active_record/counter_cache.rb b/activerecord/lib/active_record/counter_cache.rb
index 81cca37939..dcbdf75627 100644
--- a/activerecord/lib/active_record/counter_cache.rb
+++ b/activerecord/lib/active_record/counter_cache.rb
@@ -35,7 +35,7 @@ module ActiveRecord
stmt = unscoped.where(arel_table[primary_key].eq(object.id)).arel.compile_update({
arel_table[counter_name] => object.send(association).count
- })
+ }, primary_key)
connection.update stmt
end
return true
@@ -50,7 +50,7 @@ module ActiveRecord
# ==== Parameters
#
# * +id+ - The id of the object you wish to update a counter on or an Array of ids.
- # * +counters+ - An Array of Hashes containing the names of the fields
+ # * +counters+ - A Hash containing the names of the fields
# to update as keys and the amount to update the field by as values.
#
# ==== Examples
@@ -77,15 +77,15 @@ module ActiveRecord
"#{quoted_column} = COALESCE(#{quoted_column}, 0) #{operator} #{value.abs}"
end
- where(primary_key => id).update_all updates.join(', ')
+ unscoped.where(primary_key => id).update_all updates.join(', ')
end
# Increment a numeric field by one, via a direct SQL update.
#
- # This method is used primarily for maintaining counter_cache columns 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.
+ # This method is used primarily for maintaining counter_cache columns that are
+ # 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
#
diff --git a/activerecord/lib/active_record/dynamic_matchers.rb b/activerecord/lib/active_record/dynamic_matchers.rb
index 3bac31c6aa..5caab09038 100644
--- a/activerecord/lib/active_record/dynamic_matchers.rb
+++ b/activerecord/lib/active_record/dynamic_matchers.rb
@@ -35,7 +35,7 @@ module ActiveRecord
end
def pattern
- /^#{prefix}_([_a-zA-Z]\w*)#{suffix}$/
+ @pattern ||= /\A#{prefix}_([_a-zA-Z]\w*)#{suffix}\Z/
end
def prefix
@@ -84,13 +84,18 @@ module ActiveRecord
"#{finder}(#{attributes_hash})"
end
+ # The parameters in the signature may have reserved Ruby words, in order
+ # to prevent errors, we start each param name with `_`.
+ #
# Extended in activerecord-deprecated_finders
def signature
- attribute_names.join(', ')
+ attribute_names.map { |name| "_#{name}" }.join(', ')
end
+ # Given that the parameters starts with `_`, the finder needs to use the
+ # same parameter name.
def attributes_hash
- "{" + attribute_names.map { |name| ":#{name} => #{name}" }.join(',') + "}"
+ "{" + attribute_names.map { |name| ":#{name} => _#{name}" }.join(',') + "}"
end
def finder
diff --git a/activerecord/lib/active_record/enum.rb b/activerecord/lib/active_record/enum.rb
new file mode 100644
index 0000000000..9ad718a02a
--- /dev/null
+++ b/activerecord/lib/active_record/enum.rb
@@ -0,0 +1,116 @@
+module ActiveRecord
+ # Declare an enum attribute where the values map to integers in the database,
+ # but can be queried by name. Example:
+ #
+ # class Conversation < ActiveRecord::Base
+ # enum status: [ :active, :archived ]
+ # end
+ #
+ # # conversation.update! status: 0
+ # conversation.active!
+ # conversation.active? # => true
+ # conversation.status # => "active"
+ #
+ # # conversation.update! status: 1
+ # conversation.archived!
+ # conversation.archived? # => true
+ # conversation.status # => "archived"
+ #
+ # # conversation.update! status: 1
+ # conversation.status = "archived"
+ #
+ # # conversation.update! status: nil
+ # conversation.status = nil
+ # conversation.status.nil? # => true
+ # conversation.status # => nil
+ #
+ # Scopes based on the allowed values of the enum field will be provided
+ # as well. With the above example, it will create an +active+ and +archived+
+ # scope.
+ #
+ # You can set the default value from the database declaration, like:
+ #
+ # create_table :conversations do |t|
+ # t.column :status, :integer, default: 0
+ # end
+ #
+ # 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+:
+ #
+ # 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
+ # 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
+ # database.
+ #
+ # 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.
+ #
+ # In rare circumstances you might need to access the mapping directly.
+ # The mappings are exposed through a constant with the attributes name:
+ #
+ # Conversation::STATUS # => { "active" => 0, "archived" => 1 }
+ #
+ # Use that constant when you need to know the ordinal value of an enum:
+ #
+ # Conversation.where("status <> ?", Conversation::STATUS[:archived])
+ module Enum
+ def enum(definitions)
+ klass = self
+ definitions.each do |name, values|
+ # STATUS = { }
+ enum_values = _enum_methods_module.const_set name.to_s.upcase, ActiveSupport::HashWithIndifferentAccess.new
+ name = name.to_sym
+
+ _enum_methods_module.module_eval do
+ # def status=(value) self[:status] = STATUS[value] end
+ define_method("#{name}=") { |value|
+ if enum_values.has_key?(value) || value.blank?
+ self[name] = enum_values[value]
+ elsif enum_values.has_value?(value)
+ # Assigning a value directly is not a end-user feature, hence it's not documented.
+ # This is used internally to make building objects from the generated scopes work
+ # as expected, i.e. +Conversation.archived.build.archived?+ should be true.
+ self[name] = value
+ else
+ raise ArgumentError, "'#{value}' is not a valid #{name}"
+ end
+ }
+
+ # def status() STATUS.key self[:status] end
+ define_method(name) { enum_values.key self[name] }
+
+ pairs = values.respond_to?(:each_pair) ? values.each_pair : values.each_with_index
+ pairs.each do |value, i|
+ enum_values[value] = i
+
+ # scope :active, -> { where status: 0 }
+ klass.scope value, -> { klass.where name => i }
+
+ # def active?() status == 0 end
+ define_method("#{value}?") { self[name] == i }
+
+ # def active!() update! status: :active end
+ define_method("#{value}!") { update! name => value }
+ end
+ end
+ end
+ end
+
+ private
+ def _enum_methods_module
+ @_enum_methods_module ||= begin
+ mod = Module.new
+ include mod
+ mod
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb
index 017b8bace6..7f6228131f 100644
--- a/activerecord/lib/active_record/errors.rb
+++ b/activerecord/lib/active_record/errors.rb
@@ -69,10 +69,6 @@ module ActiveRecord
end
end
- # Raised when SQL statement is invalid and the application gets a blank result.
- class ThrowResult < ActiveRecordError
- end
-
# Defunct wrapper class kept for compatibility.
# +StatementInvalid+ wraps the original exception now.
class WrappedDatabaseException < StatementInvalid
@@ -98,6 +94,18 @@ module ActiveRecord
class PreparedStatementInvalid < ActiveRecordError
end
+ # Raised when a given database does not exist
+ class NoDatabaseError < ActiveRecordError
+ def initialize(message)
+ super extend_message(message)
+ end
+
+ # can be over written to add additional error information.
+ def extend_message(message)
+ message
+ end
+ end
+
# Raised on attempt to save stale record. Record is stale when it's being saved in another query after
# instantiation, for example, when two users edit the same wiki page and one starts editing and saves
# the page before the other.
@@ -192,7 +200,7 @@ module ActiveRecord
end
end
- # Raised when a primary key is needed, but there is not one specified in the schema or model.
+ # Raised when a primary key is needed, but not specified in the schema or model.
class UnknownPrimaryKey < ActiveRecordError
attr_reader :model
diff --git a/activerecord/lib/active_record/fixture_set/file.rb b/activerecord/lib/active_record/fixture_set/file.rb
index fbd7a4d891..8132310c91 100644
--- a/activerecord/lib/active_record/fixture_set/file.rb
+++ b/activerecord/lib/active_record/fixture_set/file.rb
@@ -38,7 +38,8 @@ module ActiveRecord
end
def render(content)
- ERB.new(content).result
+ context = ActiveRecord::FixtureSet::RenderContext.create_subclass.new
+ ERB.new(content).result(context.get_binding)
end
# Validate our unmarshalled data.
diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb
index 45dc26f0ed..fee3f51d9e 100644
--- a/activerecord/lib/active_record/fixtures.rb
+++ b/activerecord/lib/active_record/fixtures.rb
@@ -119,6 +119,23 @@ module ActiveRecord
# perhaps you should reexamine whether your application is properly testable. Hence, dynamic values
# in fixtures are to be considered a code smell.
#
+ # 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>.
+ #
+ # - define a helper method in `test_helper.rb`
+ # class FixtureFileHelpers
+ # def file_sha(path)
+ # Digest::SHA2.hexdigest(File.read(Rails.root.join('test/fixtures', path)))
+ # end
+ # end
+ # ActiveRecord::FixtureSet.context_class.send :include, FixtureFileHelpers
+ #
+ # - use the helper method in a fixture
+ # photo:
+ # name: kitten.png
+ # sha: <%= file_sha 'files/kitten.png' %>
+ #
# = Transactional Fixtures
#
# Test cases can use begin+rollback to isolate their changes to the database instead of having to
@@ -379,22 +396,16 @@ module ActiveRecord
@@all_cached_fixtures = Hash.new { |h,k| h[k] = {} }
- def self.find_table_name(fixture_set_name) # :nodoc:
- ActiveSupport::Deprecation.warn(
- "ActiveRecord::Fixtures.find_table_name is deprecated and shall be removed from future releases. Use ActiveRecord::Fixtures.default_fixture_model_name instead.")
- default_fixture_model_name(fixture_set_name)
- end
-
- def self.default_fixture_model_name(fixture_set_name) # :nodoc:
- ActiveRecord::Base.pluralize_table_names ?
+ def self.default_fixture_model_name(fixture_set_name, config = ActiveRecord::Base) # :nodoc:
+ config.pluralize_table_names ?
fixture_set_name.singularize.camelize :
fixture_set_name.camelize
end
- def self.default_fixture_table_name(fixture_set_name) # :nodoc:
- "#{ ActiveRecord::Base.table_name_prefix }"\
+ def self.default_fixture_table_name(fixture_set_name, config = ActiveRecord::Base) # :nodoc:
+ "#{ config.table_name_prefix }"\
"#{ fixture_set_name.tr('/', '_') }"\
- "#{ ActiveRecord::Base.table_name_suffix }".to_sym
+ "#{ config.table_name_suffix }".to_sym
end
def self.reset_cache
@@ -442,9 +453,47 @@ module ActiveRecord
cattr_accessor :all_loaded_fixtures
self.all_loaded_fixtures = {}
- def self.create_fixtures(fixtures_directory, fixture_set_names, class_names = {})
+ class ClassCache
+ def initialize(class_names, config)
+ @class_names = class_names.stringify_keys
+ @config = config
+
+ # Remove string values that aren't constants or subclasses of AR
+ @class_names.delete_if { |k,klass|
+ unless klass.is_a? Class
+ klass = klass.safe_constantize
+ ActiveSupport::Deprecation.warn("The ability to pass in strings as a class name will be removed in Rails 4.2, consider using the class itself instead.")
+ end
+ !insert_class(@class_names, k, klass)
+ }
+ end
+
+ def [](fs_name)
+ @class_names.fetch(fs_name) {
+ klass = default_fixture_model(fs_name, @config).safe_constantize
+ insert_class(@class_names, fs_name, klass)
+ }
+ end
+
+ private
+
+ def insert_class(class_names, name, klass)
+ # We only want to deal with AR objects.
+ if klass && klass < ActiveRecord::Base
+ class_names[name] = klass
+ else
+ class_names[name] = nil
+ end
+ end
+
+ def default_fixture_model(fs_name, config)
+ ActiveRecord::FixtureSet.default_fixture_model_name(fs_name, config)
+ end
+ end
+
+ def self.create_fixtures(fixtures_directory, fixture_set_names, class_names = {}, config = ActiveRecord::Base)
fixture_set_names = Array(fixture_set_names).map(&:to_s)
- class_names = class_names.stringify_keys
+ class_names = ClassCache.new class_names, config
# FIXME: Apparently JK uses this.
connection = block_given? ? yield : ActiveRecord::Base.connection
@@ -458,10 +507,12 @@ module ActiveRecord
fixtures_map = {}
fixture_sets = files_to_read.map do |fs_name|
+ klass = class_names[fs_name]
+ conn = klass ? klass.connection : connection
fixtures_map[fs_name] = new( # ActiveRecord::FixtureSet.new
- connection,
+ conn,
fs_name,
- class_names[fs_name] || default_fixture_model_name(fs_name),
+ klass,
::File.join(fixtures_directory, fs_name))
end
@@ -503,27 +554,36 @@ module ActiveRecord
Zlib.crc32(label.to_s) % MAX_ID
end
- attr_reader :table_name, :name, :fixtures, :model_class
+ # Superclass for the evaluation contexts used by ERB fixtures.
+ def self.context_class
+ @context_class ||= Class.new
+ end
+
+ attr_reader :table_name, :name, :fixtures, :model_class, :config
- def initialize(connection, name, class_name, path)
- @fixtures = {} # Ordered hash
+ def initialize(connection, name, class_name, path, config = ActiveRecord::Base)
@name = name
@path = path
+ @config = config
+ @model_class = nil
+
+ if class_name.is_a?(String)
+ ActiveSupport::Deprecation.warn("The ability to pass in strings as a class name will be removed in Rails 4.2, consider using the class itself instead.")
+ end
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.constantize rescue nil
+ @model_class = class_name.safe_constantize if class_name
end
- @connection = ( model_class.respond_to?(:connection) ?
- model_class.connection : connection )
+ @connection = connection
@table_name = ( model_class.respond_to?(:table_name) ?
model_class.table_name :
- self.class.default_fixture_table_name(name) )
+ self.class.default_fixture_table_name(name, config) )
- read_fixture_files
+ @fixtures = read_fixture_files path, @model_class
end
def [](x)
@@ -542,10 +602,10 @@ module ActiveRecord
fixtures.size
end
- # Return a hash of rows to be inserted. The key is the table, the value is
+ # Returns a hash of rows to be inserted. The key is the table, the value is
# a list of rows to insert to that table.
def table_rows
- now = ActiveRecord::Base.default_timezone == :utc ? Time.now.utc : Time.now
+ now = config.default_timezone == :utc ? Time.now.utc : Time.now
now = now.to_s(:db)
# allow a standard key to be used for doing defaults in YAML
@@ -557,7 +617,7 @@ module ActiveRecord
rows[table_name] = fixtures.map do |label, fixture|
row = fixture.to_hash
- if model_class && model_class < ActiveRecord::Base
+ if model_class
# fill in timestamp columns if they aren't specified and the model is set to record_timestamps
if model_class.record_timestamps
timestamp_column_names.each do |c_name|
@@ -567,7 +627,7 @@ module ActiveRecord
# interpolate the fixture label
row.each do |key, value|
- row[key] = label if value == "$LABEL"
+ row[key] = label if "$LABEL" == value
end
# generate a primary key if necessary
@@ -597,14 +657,9 @@ module ActiveRecord
row[fk_name] = ActiveRecord::FixtureSet.identify(value)
end
- when :has_and_belongs_to_many
- if (targets = row.delete(association.name.to_s))
- targets = targets.is_a?(Array) ? targets : targets.split(/\s*,\s*/)
- table_name = association.join_table
- rows[table_name].concat targets.map { |target|
- { association.foreign_key => row[primary_key_name],
- association.association_foreign_key => ActiveRecord::FixtureSet.identify(target) }
- }
+ when :has_many
+ if association.options[:through]
+ add_join_records(rows, row, HasManyThroughProxy.new(association))
end
end
end
@@ -615,11 +670,50 @@ module ActiveRecord
rows
end
+ class ReflectionProxy # :nodoc:
+ def initialize(association)
+ @association = association
+ end
+
+ def join_table
+ @association.join_table
+ end
+
+ def name
+ @association.name
+ end
+ end
+
+ class HasManyThroughProxy < ReflectionProxy # :nodoc:
+ def rhs_key
+ @association.foreign_key
+ end
+
+ def lhs_key
+ @association.through_reflection.foreign_key
+ end
+ end
+
private
def primary_key_name
@primary_key_name ||= model_class && model_class.primary_key
end
+ def add_join_records(rows, row, association)
+ # This is the case when the join table has no fixtures file
+ if (targets = row.delete(association.name.to_s))
+ table_name = association.join_table
+ lhs_key = association.lhs_key
+ rhs_key = association.rhs_key
+
+ targets = targets.is_a?(Array) ? targets : targets.split(/\s*,\s*/)
+ rows[table_name].concat targets.map { |target|
+ { lhs_key => row[primary_key_name],
+ rhs_key => ActiveRecord::FixtureSet.identify(target) }
+ }
+ end
+ end
+
def has_primary_key_column?
@has_primary_key_column ||= primary_key_name &&
model_class.columns.any? { |c| c.name == primary_key_name }
@@ -638,12 +732,12 @@ module ActiveRecord
@column_names ||= @connection.columns(@table_name).collect { |c| c.name }
end
- def read_fixture_files
- yaml_files = Dir["#{@path}/**/*.yml"].select { |f|
+ def read_fixture_files(path, model_class)
+ yaml_files = Dir["#{path}/{**,*}/*.yml"].select { |f|
::File.file?(f)
- } + [yaml_file_path]
+ } + [yaml_file_path(path)]
- yaml_files.each do |file|
+ yaml_files.each_with_object({}) do |file, fixtures|
FixtureSet::File.open(file) do |fh|
fh.each do |fixture_name, row|
fixtures[fixture_name] = ActiveRecord::Fixture.new(row, model_class)
@@ -652,8 +746,8 @@ module ActiveRecord
end
end
- def yaml_file_path
- "#{@path}.yml"
+ def yaml_file_path(path)
+ "#{path}.yml"
end
end
@@ -725,14 +819,16 @@ module ActiveRecord
class_attribute :use_transactional_fixtures
class_attribute :use_instantiated_fixtures # true, false, or :no_instances
class_attribute :pre_loaded_fixtures
+ class_attribute :config
self.fixture_table_names = []
self.use_transactional_fixtures = true
self.use_instantiated_fixtures = false
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)
+ h[fixture_set_name] = ActiveRecord::FixtureSet.default_fixture_model_name(fixture_set_name, self.config)
end
end
@@ -745,27 +841,20 @@ module ActiveRecord
# 'namespaced/fixture' => Another::Model
#
# The keys must be the fixture names, that coincide with the short paths to the fixture files.
- #--
- # It is also possible to pass the class name instead of the class:
- # set_fixture_class 'some_fixture' => 'SomeModel'
- # I think this option is redundant, i propose to deprecate it.
- # Isn't it easier to always pass the class itself?
- # (2011-12-20 alexeymuranov)
- #++
def set_fixture_class(class_names = {})
self.fixture_class_names = self.fixture_class_names.merge(class_names.stringify_keys)
end
def fixtures(*fixture_set_names)
if fixture_set_names.first == :all
- fixture_set_names = Dir["#{fixture_path}/**/*.{yml}"]
+ fixture_set_names = Dir["#{fixture_path}/{**,*}/*.{yml}"]
fixture_set_names.map! { |f| f[(fixture_path.to_s.size + 1)..-5] }
else
fixture_set_names = fixture_set_names.flatten.map { |n| n.to_s }
end
self.fixture_table_names |= fixture_set_names
- require_fixture_classes(fixture_set_names)
+ require_fixture_classes(fixture_set_names, self.config)
setup_fixture_accessors(fixture_set_names)
end
@@ -780,7 +869,7 @@ module ActiveRecord
end
end
- def require_fixture_classes(fixture_set_names = nil)
+ def require_fixture_classes(fixture_set_names = nil, config = ActiveRecord::Base)
if fixture_set_names
fixture_set_names = fixture_set_names.map { |n| n.to_s }
else
@@ -788,7 +877,7 @@ module ActiveRecord
end
fixture_set_names.each do |file_name|
- file_name = file_name.singularize if ActiveRecord::Base.pluralize_table_names
+ file_name = file_name.singularize if config.pluralize_table_names
try_to_load_dependency(file_name)
end
end
@@ -840,9 +929,7 @@ module ActiveRecord
!self.class.uses_transaction?(method_name)
end
- def setup_fixtures
- return if ActiveRecord::Base.configurations.blank?
-
+ def setup_fixtures(config = ActiveRecord::Base)
if pre_loaded_fixtures && !use_transactional_fixtures
raise RuntimeError, 'pre_loaded_fixtures requires use_transactional_fixtures'
end
@@ -856,7 +943,7 @@ module ActiveRecord
if @@already_loaded_fixtures[self.class]
@loaded_fixtures = @@already_loaded_fixtures[self.class]
else
- @loaded_fixtures = load_fixtures
+ @loaded_fixtures = load_fixtures(config)
@@already_loaded_fixtures[self.class] = @loaded_fixtures
end
@fixture_connections = enlist_fixture_connections
@@ -867,16 +954,14 @@ module ActiveRecord
else
ActiveRecord::FixtureSet.reset_cache
@@already_loaded_fixtures[self.class] = nil
- @loaded_fixtures = load_fixtures
+ @loaded_fixtures = load_fixtures(config)
end
# Instantiate fixtures for every test if requested.
- instantiate_fixtures if use_instantiated_fixtures
+ instantiate_fixtures(config) if use_instantiated_fixtures
end
def teardown_fixtures
- return if ActiveRecord::Base.configurations.blank?
-
# Rollback changes if a transaction is active.
if run_in_transaction?
@fixture_connections.each do |connection|
@@ -895,19 +980,19 @@ module ActiveRecord
end
private
- def load_fixtures
- fixtures = ActiveRecord::FixtureSet.create_fixtures(fixture_path, fixture_table_names, fixture_class_names)
+ def load_fixtures(config)
+ fixtures = ActiveRecord::FixtureSet.create_fixtures(fixture_path, fixture_table_names, fixture_class_names, config)
Hash[fixtures.map { |f| [f.name, f] }]
end
# for pre_loaded_fixtures, only require the classes once. huge speed improvement
@@required_fixture_classes = false
- def instantiate_fixtures
+ def instantiate_fixtures(config)
if pre_loaded_fixtures
raise RuntimeError, 'Load fixtures before instantiating them.' if ActiveRecord::FixtureSet.all_loaded_fixtures.empty?
unless @@required_fixture_classes
- self.class.require_fixture_classes ActiveRecord::FixtureSet.all_loaded_fixtures.keys
+ self.class.require_fixture_classes ActiveRecord::FixtureSet.all_loaded_fixtures.keys, config
@@required_fixture_classes = true
end
ActiveRecord::FixtureSet.instantiate_all_loaded_fixtures(self, load_instances?)
@@ -924,3 +1009,13 @@ module ActiveRecord
end
end
end
+
+class ActiveRecord::FixtureSet::RenderContext # :nodoc:
+ def self.create_subclass
+ Class.new ActiveRecord::FixtureSet.context_class do
+ def get_binding
+ binding()
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb
index 40976bc29e..949e7678a5 100644
--- a/activerecord/lib/active_record/inheritance.rb
+++ b/activerecord/lib/active_record/inheritance.rb
@@ -5,7 +5,7 @@ module ActiveRecord
extend ActiveSupport::Concern
included do
- # Determine whether to store the full constant name including namespace when using STI
+ # Determines whether to store the full constant name including namespace when using STI.
class_attribute :store_full_sti_class, instance_writer: false
self.store_full_sti_class = true
end
@@ -13,10 +13,10 @@ module ActiveRecord
module ClassMethods
# Determines if one of the attributes passed in is the inheritance column,
# and if the inheritance column is attr accessible, it initializes an
- # instance of the given subclass instead of the base class
+ # instance of the given subclass instead of the base class.
def new(*args, &block)
if abstract_class? || self == Base
- raise NotImplementedError, "#{self} is an abstract class and can not be instantiated."
+ raise NotImplementedError, "#{self} is an abstract class and cannot be instantiated."
end
if (attrs = args.first).is_a?(Hash)
if subclass = subclass_from_attrs(attrs)
@@ -27,7 +27,8 @@ module ActiveRecord
super
end
- # True if this isn't a concrete subclass needing a STI type condition.
+ # Returns +true+ if this does not need STI type condition. Returns
+ # +false+ if STI type condition needs to be applied.
def descends_from_active_record?
if self == Base
false
@@ -44,10 +45,12 @@ module ActiveRecord
end
def symbolized_base_class
+ ActiveSupport::Deprecation.warn("ActiveRecord::Base.symbolized_base_class is deprecated and will be removed without replacement.")
@symbolized_base_class ||= base_class.to_s.to_sym
end
def symbolized_sti_name
+ ActiveSupport::Deprecation.warn("ActiveRecord::Base.symbolized_sti_name is deprecated and will be removed without replacement.")
@symbolized_sti_name ||= sti_name.present? ? sti_name.to_sym : symbolized_base_class
end
@@ -159,7 +162,7 @@ module ActiveRecord
end
def type_condition(table = arel_table)
- sti_column = table[inheritance_column.to_sym]
+ sti_column = table[inheritance_column]
sti_names = ([self] + descendants).map { |model| model.sti_name }
sti_column.in(sti_names)
diff --git a/activerecord/lib/active_record/integration.rb b/activerecord/lib/active_record/integration.rb
index 2589b2f3da..31e2518540 100644
--- a/activerecord/lib/active_record/integration.rb
+++ b/activerecord/lib/active_record/integration.rb
@@ -1,3 +1,5 @@
+require 'active_support/core_ext/string/filters'
+
module ActiveRecord
module Integration
extend ActiveSupport::Concern
@@ -45,10 +47,19 @@ module ActiveRecord
# Product.new.cache_key # => "products/new"
# Product.find(5).cache_key # => "products/5" (updated_at not available)
# Person.find(5).cache_key # => "people/5-20071224150000" (updated_at available)
- def cache_key
+ #
+ # You can also pass a list of named timestamps, and the newest in the list will be
+ # used to generate the key:
+ #
+ # Person.find(5).cache_key(:updated_at, :last_reviewed_at)
+ def cache_key(*timestamp_names)
case
when new_record?
"#{self.class.model_name.cache_key}/new"
+ when timestamp_names.any?
+ timestamp = max_updated_column_timestamp(timestamp_names)
+ timestamp = timestamp.utc.to_s(cache_timestamp_format)
+ "#{self.class.model_name.cache_key}/#{id}-#{timestamp}"
when timestamp = max_updated_column_timestamp
timestamp = timestamp.utc.to_s(cache_timestamp_format)
"#{self.class.model_name.cache_key}/#{id}-#{timestamp}"
@@ -56,5 +67,47 @@ module ActiveRecord
"#{self.class.model_name.cache_key}/#{id}"
end
end
+
+ module ClassMethods
+ # Defines your model's +to_param+ method to generate "pretty" URLs
+ # using +method_name+, which can be any attribute or method that
+ # responds to +to_s+.
+ #
+ # class User < ActiveRecord::Base
+ # to_param :name
+ # end
+ #
+ # user = User.find_by(name: 'Fancy Pants')
+ # user.id # => 123
+ # user_path(user) # => "/users/123-fancy-pants"
+ #
+ # Values longer than 20 characters will be truncated. The value
+ # is truncated word by word.
+ #
+ # user = User.find_by(name: 'David HeinemeierHansson')
+ # user.id # => 125
+ # user_path(user) # => "/users/125-david"
+ #
+ # Because the generated param begins with the record's +id+, it is
+ # suitable for passing to +find+. In a controller, for example:
+ #
+ # params[:id] # => "123-fancy-pants"
+ # User.find(params[:id]).id # => 123
+ def to_param(method_name = nil)
+ if method_name.nil?
+ super()
+ else
+ define_method :to_param do
+ if (default = super()) &&
+ (result = send(method_name).to_s).present? &&
+ (param = result.squish.truncate(20, separator: /\s/, omission: nil).parameterize).present?
+ "#{default}-#{param}"
+ else
+ default
+ end
+ end
+ end
+ end
+ end
end
end
diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb
index 209de78898..6f54729b3c 100644
--- a/activerecord/lib/active_record/locking/optimistic.rb
+++ b/activerecord/lib/active_record/locking/optimistic.rb
@@ -82,9 +82,12 @@ module ActiveRecord
stmt = relation.where(
relation.table[self.class.primary_key].eq(id).and(
- relation.table[lock_col].eq(self.class.quote_value(previous_lock_value))
+ relation.table[lock_col].eq(self.class.quote_value(previous_lock_value, column_for_attribute(lock_col)))
)
- ).arel.compile_update(arel_attributes_with_values_for_update(attribute_names))
+ ).arel.compile_update(
+ arel_attributes_with_values_for_update(attribute_names),
+ self.class.primary_key
+ )
affected_rows = self.class.connection.update stmt
@@ -138,6 +141,7 @@ module ActiveRecord
# Set the column to use for optimistic locking. Defaults to +lock_version+.
def locking_column=(value)
+ @column_defaults = nil
@locking_column = value.to_s
end
@@ -149,6 +153,7 @@ module ActiveRecord
# Quote the column name used for optimistic locking.
def quoted_locking_column
+ ActiveSupport::Deprecation.warn "ActiveRecord::Base.quoted_locking_column is deprecated and will be removed in Rails 4.2 or later."
connection.quote_column_name(locking_column)
end
diff --git a/activerecord/lib/active_record/locking/pessimistic.rb b/activerecord/lib/active_record/locking/pessimistic.rb
index 8e4ddcac82..ff7102d35b 100644
--- a/activerecord/lib/active_record/locking/pessimistic.rb
+++ b/activerecord/lib/active_record/locking/pessimistic.rb
@@ -3,12 +3,12 @@ module ActiveRecord
# Locking::Pessimistic provides support for row-level locking using
# SELECT ... FOR UPDATE and other lock types.
#
- # Pass <tt>lock: true</tt> to <tt>ActiveRecord::Base.find</tt> to obtain an exclusive
+ # Chain <tt>ActiveRecord::Base#find</tt> to <tt>ActiveRecord::QueryMethods#lock</tt> to obtain an exclusive
# lock on the selected rows:
# # select * from accounts where id=1 for update
- # Account.find(1, lock: true)
+ # Account.lock.find(1)
#
- # Pass <tt>lock: 'some locking clause'</tt> to give a database-specific locking clause
+ # Call <tt>lock('some locking clause')</tt> to use a database-specific locking clause
# of your own such as 'LOCK IN SHARE MODE' or 'FOR UPDATE NOWAIT'. Example:
#
# Account.transaction do
@@ -64,7 +64,7 @@ module ActiveRecord
end
# Wraps the passed block in a transaction, locking the object
- # before yielding. You pass can the SQL locking clause
+ # before yielding. You can pass the SQL locking clause
# as argument (see <tt>lock!</tt>).
def with_lock(lock = true)
transaction do
diff --git a/activerecord/lib/active_record/log_subscriber.rb b/activerecord/lib/active_record/log_subscriber.rb
index 61e5c120df..654ef21b07 100644
--- a/activerecord/lib/active_record/log_subscriber.rb
+++ b/activerecord/lib/active_record/log_subscriber.rb
@@ -17,12 +17,14 @@ module ActiveRecord
def initialize
super
- @odd_or_even = false
+ @odd = false
end
def render_bind(column, value)
if column
if column.binary?
+ # This specifically deals with the PG adapter that casts bytea columns into a Hash.
+ value = value[:value] if value.is_a?(Hash)
value = "<#{value.bytesize} bytes of binary data>"
end
@@ -41,7 +43,7 @@ module ActiveRecord
return if IGNORE_PAYLOAD_NAMES.include?(payload[:name])
name = "#{payload[:name]} (#{event.duration.round(1)}ms)"
- sql = payload[:sql].squeeze(' ')
+ sql = payload[:sql]
binds = nil
unless (payload[:binds] || []).empty?
@@ -60,17 +62,8 @@ module ActiveRecord
debug " #{name} #{sql}#{binds}"
end
- def identity(event)
- return unless logger.debug?
-
- name = color(event.payload[:name], odd? ? CYAN : MAGENTA, true)
- line = odd? ? color(event.payload[:line], nil, true) : event.payload[:line]
-
- debug " #{name} #{line}"
- end
-
def odd?
- @odd_or_even = !@odd_or_even
+ @odd = !@odd
end
def logger
diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb
index 511a1585a7..b57da73969 100644
--- a/activerecord/lib/active_record/migration.rb
+++ b/activerecord/lib/active_record/migration.rb
@@ -1,38 +1,49 @@
-require "active_support/core_ext/class/attribute_accessors"
+require "active_support/core_ext/module/attribute_accessors"
require 'set'
module ActiveRecord
+ class MigrationError < ActiveRecordError#:nodoc:
+ def initialize(message = nil)
+ message = "\n\n#{message}\n\n" if message
+ super
+ end
+ end
+
# Exception that can be raised to stop migrations from going backwards.
- class IrreversibleMigration < ActiveRecordError
+ class IrreversibleMigration < MigrationError
end
- class DuplicateMigrationVersionError < ActiveRecordError#:nodoc:
+ class DuplicateMigrationVersionError < MigrationError#:nodoc:
def initialize(version)
super("Multiple migrations have the version number #{version}")
end
end
- class DuplicateMigrationNameError < ActiveRecordError#:nodoc:
+ class DuplicateMigrationNameError < MigrationError#:nodoc:
def initialize(name)
super("Multiple migrations have the name #{name}")
end
end
- class UnknownMigrationVersionError < ActiveRecordError #:nodoc:
+ class UnknownMigrationVersionError < MigrationError #:nodoc:
def initialize(version)
super("No migration with version number #{version}")
end
end
- class IllegalMigrationNameError < ActiveRecordError#:nodoc:
+ class IllegalMigrationNameError < MigrationError#:nodoc:
def initialize(name)
super("Illegal name for migration file: #{name}\n\t(only lower case letters, numbers, and '_' allowed)")
end
end
- class PendingMigrationError < ActiveRecordError#:nodoc:
+ class PendingMigrationError < MigrationError#:nodoc:
def initialize
- super("Migrations are pending; run 'rake db:migrate RAILS_ENV=#{Rails.env}' to resolve this issue.")
+ if defined?(Rails)
+ super("Migrations are pending. To resolve this issue, run:\n\n\tbin/rake db:migrate RAILS_ENV=#{::Rails.env}")
+ else
+ super("Migrations are pending. To resolve this issue, run:\n\n\tbin/rake db:migrate")
+ end
end
end
@@ -120,8 +131,8 @@ module ActiveRecord
# 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_names)</tt>: Removes the column listed in
- # +column_names+ from the table called +table_name+.
+ # * <tt>remove_column(table_name, column_name, type, options)</tt>: Removes the column
+ # named +column_name+ from the table called +table_name+.
# * <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.
@@ -357,11 +368,14 @@ module ActiveRecord
class CheckPending
def initialize(app)
@app = app
+ @last_check = 0
end
def call(env)
- ActiveRecord::Base.logger.silence do
+ mtime = ActiveRecord::Migrator.last_migration.mtime.to_i
+ if @last_check < mtime
ActiveRecord::Migration.check_pending!
+ @last_check = mtime
end
@app.call(env)
end
@@ -370,23 +384,36 @@ module ActiveRecord
class << self
attr_accessor :delegate # :nodoc:
attr_accessor :disable_ddl_transaction # :nodoc:
- end
- def self.check_pending!
- raise ActiveRecord::PendingMigrationError if ActiveRecord::Migrator.needs_migration?
- end
+ def check_pending!
+ raise ActiveRecord::PendingMigrationError if ActiveRecord::Migrator.needs_migration?
+ end
- def self.method_missing(name, *args, &block) # :nodoc:
- (delegate || superclass.delegate).send(name, *args, &block)
- end
+ def load_schema_if_pending!
+ if ActiveRecord::Migrator.needs_migration?
+ ActiveRecord::Tasks::DatabaseTasks.load_schema
+ check_pending!
+ end
+ end
- def self.migrate(direction)
- new.migrate direction
- end
+ def maintain_test_schema! # :nodoc:
+ if ActiveRecord::Base.maintain_test_schema
+ suppress_messages { load_schema_if_pending! }
+ end
+ end
+
+ def method_missing(name, *args, &block) # :nodoc:
+ (delegate || superclass.delegate).send(name, *args, &block)
+ end
- # Disable DDL transactions for this migration.
- def self.disable_ddl_transaction!
- @disable_ddl_transaction = true
+ def migrate(direction)
+ new.migrate direction
+ end
+
+ # Disable DDL transactions for this migration.
+ def disable_ddl_transaction!
+ @disable_ddl_transaction = true
+ end
end
def disable_ddl_transaction # :nodoc:
@@ -614,8 +641,8 @@ module ActiveRecord
say_with_time "#{method}(#{arg_list})" do
unless @connection.respond_to? :revert
unless arguments.empty? || method == :execute
- arguments[0] = Migrator.proper_table_name(arguments.first)
- arguments[1] = Migrator.proper_table_name(arguments.second) if method == :rename_table
+ arguments[0] = proper_table_name(arguments.first, table_name_options)
+ arguments[1] = proper_table_name(arguments.second, table_name_options) if method == :rename_table
end
end
return super unless connection.respond_to?(method)
@@ -626,7 +653,7 @@ module ActiveRecord
def copy(destination, sources, options = {})
copied = []
- FileUtils.mkdir_p(destination) unless File.exists?(destination)
+ FileUtils.mkdir_p(destination) unless File.exist?(destination)
destination_migrations = ActiveRecord::Migrator.migrations(destination)
last = destination_migrations.last
@@ -668,6 +695,17 @@ module ActiveRecord
copied
end
+ # Finds the correct table name given an Active Record object.
+ # Uses the Active Record object's own table_name, or pre/suffix from the
+ # options passed in.
+ def proper_table_name(name, options = {})
+ if name.respond_to? :table_name
+ name.table_name
+ else
+ "#{options[:table_name_prefix]}#{name}#{options[:table_name_suffix]}"
+ end
+ end
+
# Determines the version number of the next migration.
def next_migration_number(number)
if ActiveRecord::Base.timestamped_migrations
@@ -677,6 +715,13 @@ module ActiveRecord
end
end
+ def table_name_options(config = ActiveRecord::Base)
+ {
+ table_name_prefix: config.table_name_prefix,
+ table_name_suffix: config.table_name_suffix
+ }
+ end
+
private
def execute_block
if connection.respond_to? :execute_block
@@ -700,6 +745,10 @@ module ActiveRecord
File.basename(filename)
end
+ def mtime
+ File.mtime filename
+ end
+
delegate :migrate, :announce, :write, :disable_ddl_transaction, to: :migration
private
@@ -710,11 +759,21 @@ module ActiveRecord
def load_migration
require(File.expand_path(filename))
- name.constantize.new
+ name.constantize.new(name, version)
end
end
+ class NullMigration < MigrationProxy #:nodoc:
+ def initialize
+ super(nil, 0, nil, nil)
+ end
+
+ def mtime
+ 0
+ end
+ end
+
class Migrator#:nodoc:
class << self
attr_writer :migrations_paths
@@ -785,15 +844,23 @@ module ActiveRecord
end
def last_version
- migrations(migrations_paths).last.try(:version)||0
+ last_migration.version
+ end
+
+ def last_migration #:nodoc:
+ migrations(migrations_paths).last || NullMigration.new
end
- def proper_table_name(name)
- # Use the Active Record objects own table_name, or pre/suffix from ActiveRecord::Base if name is a symbol/string
+ def proper_table_name(name, options = {})
+ ActiveSupport::Deprecation.warn "ActiveRecord::Migrator.proper_table_name is deprecated and will be removed in Rails 4.2. Use the proper_table_name instance method on ActiveRecord::Migration instead"
+ options = {
+ table_name_prefix: ActiveRecord::Base.table_name_prefix,
+ table_name_suffix: ActiveRecord::Base.table_name_suffix
+ }.merge(options)
if name.respond_to? :table_name
name.table_name
else
- "#{ActiveRecord::Base.table_name_prefix}#{name}#{ActiveRecord::Base.table_name_suffix}"
+ "#{options[:table_name_prefix]}#{name}#{options[:table_name_suffix]}"
end
end
@@ -845,17 +912,11 @@ module ActiveRecord
@direction = direction
@target_version = target_version
@migrated_versions = nil
-
- if Array(migrations).grep(String).empty?
- @migrations = migrations
- else
- ActiveSupport::Deprecation.warn "instantiate this class with a list of migrations"
- @migrations = self.class.migrations(migrations)
- end
+ @migrations = migrations
validate(@migrations)
- ActiveRecord::SchemaMigration.create_table
+ Base.connection.initialize_schema_migrations_table
end
def current_version
@@ -885,15 +946,7 @@ module ActiveRecord
raise UnknownMigrationVersionError.new(@target_version)
end
- running = runnable
-
- if block_given?
- message = "block argument to migrate is deprecated, please filter migrations before constructing the migrator"
- ActiveSupport::Deprecation.warn message
- running.select! { |m| yield m }
- end
-
- running.each do |migration|
+ runnable.each do |migration|
Base.logger.info "Migrating to #{migration.name} (#{migration.version})" if Base.logger
begin
diff --git a/activerecord/lib/active_record/migration/command_recorder.rb b/activerecord/lib/active_record/migration/command_recorder.rb
index 9782a48055..9139ad953c 100644
--- a/activerecord/lib/active_record/migration/command_recorder.rb
+++ b/activerecord/lib/active_record/migration/command_recorder.rb
@@ -73,8 +73,8 @@ module ActiveRecord
[: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,
- :change_column, :execute, :remove_columns, # irreversible methods need to be here too
+ :drop_join_table, :drop_table, :execute_block, :enable_extension,
+ :change_column, :execute, :remove_columns, :change_column_null # irreversible methods need to be here too
].each do |method|
class_eval <<-EOV, __FILE__, __LINE__ + 1
def #{method}(*args, &block) # def create_table(*args, &block)
@@ -86,7 +86,7 @@ module ActiveRecord
alias :remove_belongs_to :remove_reference
def change_table(table_name, options = {})
- yield ConnectionAdapters::Table.new(table_name, self)
+ yield delegate.update_table_definition(table_name, self)
end
private
@@ -100,6 +100,7 @@ module ActiveRecord
add_column: :remove_column,
add_timestamps: :remove_timestamps,
add_reference: :remove_reference,
+ enable_extension: :disable_extension
}.each do |cmd, inv|
[[inv, cmd], [cmd, inv]].uniq.each do |method, inverse|
class_eval <<-EOV, __FILE__, __LINE__ + 1
@@ -156,11 +157,18 @@ module ActiveRecord
alias :invert_add_belongs_to :invert_add_reference
alias :invert_remove_belongs_to :invert_remove_reference
+ def invert_change_column_null(args)
+ args[2] = !args[2]
+ [:change_column_null, args]
+ end
+
# Forwards any missing method call to the \target.
def method_missing(method, *args, &block)
- @delegate.send(method, *args, &block)
- rescue NoMethodError => e
- raise e, e.message.sub(/ for #<.*$/, " via proxy for #{@delegate}")
+ if @delegate.respond_to?(method)
+ @delegate.send(method, *args, &block)
+ else
+ super
+ end
end
end
end
diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb
index ac2d2f2712..dc5ff02882 100644
--- a/activerecord/lib/active_record/model_schema.rb
+++ b/activerecord/lib/active_record/model_schema.rb
@@ -34,6 +34,12 @@ module ActiveRecord
##
# :singleton-method:
+ # Accessor for the name of the schema migrations table. By default, the value is "schema_migrations"
+ class_attribute :schema_migrations_table_name, instance_accessor: false
+ self.schema_migrations_table_name = "schema_migrations"
+
+ ##
+ # :singleton-method:
# Indicates whether table names should be the pluralized versions of the corresponding class names.
# If true, the default table name for a Product class will be +products+. If false, it would just be +product+.
# See table_name for the full rules on table/class naming. This is true, by default.
@@ -124,7 +130,7 @@ module ActiveRecord
@quoted_table_name = nil
@arel_table = nil
@sequence_name = nil unless defined?(@explicit_sequence_name) && @explicit_sequence_name
- @relation = Relation.new(self, arel_table)
+ @relation = Relation.create(self, arel_table)
end
# Returns a quoted version of the table name, used to construct SQL statements.
@@ -224,13 +230,20 @@ module ActiveRecord
def decorate_columns(columns_hash) # :nodoc:
return if columns_hash.empty?
- columns_hash.each do |name, col|
- if serialized_attributes.key?(name)
- columns_hash[name] = AttributeMethods::Serialization::Type.new(col)
- end
- if create_time_zone_conversion_attribute?(name, col)
- columns_hash[name] = AttributeMethods::TimeZoneConversion::Type.new(col)
- end
+ @serialized_column_names ||= self.columns_hash.keys.find_all do |name|
+ serialized_attributes.key?(name)
+ end
+
+ @serialized_column_names.each do |name|
+ columns_hash[name] = AttributeMethods::Serialization::Type.new(columns_hash[name])
+ end
+
+ @time_zone_column_names ||= self.columns_hash.find_all do |name, col|
+ create_time_zone_conversion_attribute?(name, col)
+ end.map!(&:first)
+
+ @time_zone_column_names.each do |name|
+ columns_hash[name] = AttributeMethods::TimeZoneConversion::Type.new(columns_hash[name])
end
columns_hash
@@ -253,19 +266,6 @@ module ActiveRecord
@content_columns ||= columns.reject { |c| c.primary || c.name =~ /(_id|_count)$/ || c.name == inheritance_column }
end
- # Returns a hash of all the methods added to query each of the columns in the table with the name of the method as the key
- # and true as the value. This makes it possible to do O(1) lookups in respond_to? to check if a given method for attribute
- # is available.
- def column_methods_hash #:nodoc:
- @dynamic_methods_hash ||= column_names.each_with_object(Hash.new(false)) do |attr, methods|
- attr_name = attr.to_s
- methods[attr.to_sym] = attr_name
- methods["#{attr}=".to_sym] = attr_name
- methods["#{attr}?".to_sym] = attr_name
- methods["#{attr}_before_type_cast".to_sym] = attr_name
- end
- end
-
# Resets all the cached information about columns, which will cause them
# to be reloaded on the next request.
#
@@ -297,16 +297,19 @@ module ActiveRecord
undefine_attribute_methods
connection.schema_cache.clear_table_cache!(table_name) if table_exists?
- @arel_engine = nil
- @column_defaults = nil
- @column_names = nil
- @columns = nil
- @columns_hash = nil
- @column_types = nil
- @content_columns = nil
- @dynamic_methods_hash = nil
- @inheritance_column = nil unless defined?(@explicit_inheritance_column) && @explicit_inheritance_column
- @relation = nil
+ @arel_engine = nil
+ @column_defaults = nil
+ @column_names = nil
+ @columns = nil
+ @columns_hash = nil
+ @column_types = nil
+ @content_columns = nil
+ @dynamic_methods_hash = nil
+ @inheritance_column = nil unless defined?(@explicit_inheritance_column) && @explicit_inheritance_column
+ @relation = nil
+ @serialized_column_names = nil
+ @time_zone_column_names = nil
+ @cached_time_zone = nil
end
# This is a hook for use by modules that need to do extra stuff to
diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb
index 8bdaeef924..9d92e747d4 100644
--- a/activerecord/lib/active_record/nested_attributes.rb
+++ b/activerecord/lib/active_record/nested_attributes.rb
@@ -230,6 +230,10 @@ module ActiveRecord
# validates_presence_of :member
# end
#
+ # Note that if you do not specify the <tt>inverse_of</tt> option, then
+ # Active Record will try to automatically guess the inverse association
+ # based on heuristics.
+ #
# For one-to-one nested associations, if you build the new (in-memory)
# child object yourself before assignment, then this module will not
# overwrite it, e.g.:
@@ -302,14 +306,9 @@ module ActiveRecord
attr_names.each do |association_name|
if reflection = reflect_on_association(association_name)
- reflection.options[:autosave] = true
+ reflection.autosave = true
add_autosave_association_callbacks(reflection)
- # Clear cached values of any inverse associations found in the
- # reflection and prevent the reflection from finding inverses
- # automatically in the future.
- reflection.remove_automatic_inverse_of!
-
nested_attributes_options = self.nested_attributes_options.dup
nested_attributes_options[association_name.to_sym] = options
self.nested_attributes_options = nested_attributes_options
@@ -336,7 +335,7 @@ module ActiveRecord
# the helper methods defined below. Makes it seem like the nested
# associations are just regular associations.
def generate_association_writer(association_name, type)
- generated_feature_methods.module_eval <<-eoruby, __FILE__, __LINE__ + 1
+ generated_association_methods.module_eval <<-eoruby, __FILE__, __LINE__ + 1
if method_defined?(:#{association_name}_attributes=)
remove_method(:#{association_name}_attributes=)
end
@@ -466,19 +465,17 @@ module ActiveRecord
association.build(attributes.except(*UNASSIGNABLE_KEYS))
end
elsif existing_record = existing_records.detect { |record| record.id.to_s == attributes['id'].to_s }
- unless association.loaded? || call_reject_if(association_name, attributes)
+ unless call_reject_if(association_name, attributes)
# Make sure we are operating on the actual object which is in the association's
# proxy_target array (either by finding it, or adding it if not found)
- target_record = association.target.detect { |record| record == existing_record }
-
+ # Take into account that the proxy_target may have changed due to callbacks
+ target_record = association.target.detect { |record| record.id.to_s == attributes['id'].to_s }
if target_record
existing_record = target_record
else
- association.add_to_target(existing_record)
+ association.add_to_target(existing_record, :skip_callbacks)
end
- end
- if !call_reject_if(association_name, attributes)
assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy])
end
else
diff --git a/activerecord/lib/active_record/no_touching.rb b/activerecord/lib/active_record/no_touching.rb
new file mode 100644
index 0000000000..dbf4564ae5
--- /dev/null
+++ b/activerecord/lib/active_record/no_touching.rb
@@ -0,0 +1,52 @@
+module ActiveRecord
+ # = Active Record No Touching
+ module NoTouching
+ extend ActiveSupport::Concern
+
+ module ClassMethods
+ # Lets you selectively disable calls to `touch` for the
+ # duration of a block.
+ #
+ # ==== Examples
+ # ActiveRecord::Base.no_touching do
+ # Project.first.touch # does nothing
+ # Message.first.touch # does nothing
+ # end
+ #
+ # Project.no_touching do
+ # Project.first.touch # does nothing
+ # Message.first.touch # works, but does not touch the associated project
+ # end
+ #
+ def no_touching(&block)
+ NoTouching.apply_to(self, &block)
+ end
+ end
+
+ class << self
+ def apply_to(klass) #:nodoc:
+ klasses.push(klass)
+ yield
+ ensure
+ klasses.pop
+ end
+
+ def applied_to?(klass) #:nodoc:
+ klasses.any? { |k| k >= klass }
+ end
+
+ private
+ def klasses
+ Thread.current[:no_touching_classes] ||= []
+ end
+ end
+
+ def no_touching?
+ NoTouching.applied_to?(self.class)
+ end
+
+ def touch(*)
+ super unless no_touching?
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/null_relation.rb b/activerecord/lib/active_record/null_relation.rb
index 711fc8b883..5b255c3fe5 100644
--- a/activerecord/lib/active_record/null_relation.rb
+++ b/activerecord/lib/active_record/null_relation.rb
@@ -6,7 +6,7 @@ module ActiveRecord
@records = []
end
- def pluck(_column_name)
+ def pluck(*column_names)
[]
end
@@ -39,11 +39,7 @@ module ActiveRecord
end
def to_sql
- @to_sql ||= ""
- end
-
- def where_values_hash
- {}
+ ""
end
def count(*)
@@ -54,8 +50,14 @@ module ActiveRecord
0
end
- def calculate(_operation, _column_name, _options = {})
- nil
+ def calculate(operation, _column_name, _options = {})
+ # TODO: Remove _options argument as soon we remove support to
+ # activerecord-deprecated_finders.
+ if operation == :count
+ 0
+ else
+ nil
+ end
end
def exists?(_id = false)
diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb
index a8905ed739..4669c072a1 100644
--- a/activerecord/lib/active_record/persistence.rb
+++ b/activerecord/lib/active_record/persistence.rb
@@ -10,9 +10,6 @@ module ActiveRecord
# The +attributes+ parameter can be either a Hash or an Array of Hashes. These Hashes describe the
# attributes on the objects that are to be created.
#
- # +create+ respects mass-assignment security and accepts either +:as+ or +:without_protection+ options
- # in the +options+ parameter.
- #
# ==== Examples
# # Create a single new object
# User.create(first_name: 'Jamie')
@@ -184,6 +181,7 @@ module ActiveRecord
became = klass.new
became.instance_variable_set("@attributes", @attributes)
became.instance_variable_set("@attributes_cache", @attributes_cache)
+ became.instance_variable_set("@changed_attributes", @changed_attributes) if defined?(@changed_attributes)
became.instance_variable_set("@new_record", new_record?)
became.instance_variable_set("@destroyed", destroyed?)
became.instance_variable_set("@errors", errors)
@@ -267,7 +265,7 @@ module ActiveRecord
# 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, "can not update on a new record object" unless persisted?
+ raise ActiveRecordError, "cannot update on a new record object" unless persisted?
attributes.each_key do |key|
verify_readonly_attribute(key.to_s)
@@ -333,10 +331,54 @@ module ActiveRecord
toggle(attribute).update_attribute(attribute, self[attribute])
end
- # Reloads the attributes of this object from the database.
- # The optional options argument is passed to find when reloading so you
- # may do e.g. record.reload(lock: true) to reload the same record with
- # an exclusive row lock.
+ # Reloads the record from the database.
+ #
+ # This method finds record by its primary key (which could be assigned manually) and
+ # modifies the receiver in-place:
+ #
+ # account = Account.new
+ # # => #<Account id: nil, email: nil>
+ # account.id = 1
+ # account.reload
+ # # Account Load (1.2ms) SELECT "accounts".* FROM "accounts" WHERE "accounts"."id" = $1 LIMIT 1 [["id", 1]]
+ # # => #<Account id: 1, email: 'account@example.com'>
+ #
+ # Attributes are reloaded from the database, and caches busted, in
+ # particular the associations cache.
+ #
+ # If the record no longer exists in the database <tt>ActiveRecord::RecordNotFound</tt>
+ # is raised. Otherwise, in addition to the in-place modification the method
+ # returns +self+ for convenience.
+ #
+ # The optional <tt>:lock</tt> flag option allows you to lock the reloaded record:
+ #
+ # reload(lock: true) # reload with pessimistic locking
+ #
+ # Reloading is commonly used in test suites to test something is actually
+ # written to the database, or when some action modifies the corresponding
+ # row in the database but not the object in memory:
+ #
+ # assert account.deposit!(25)
+ # assert_equal 25, account.credit # check it is updated in memory
+ # assert_equal 25, account.reload.credit # check it is also persisted
+ #
+ # Another common use case is optimistic locking handling:
+ #
+ # def with_optimistic_retry
+ # begin
+ # yield
+ # rescue ActiveRecord::StaleObjectError
+ # begin
+ # # Reload lock_version in particular.
+ # reload
+ # rescue ActiveRecord::RecordNotFound
+ # # If the record is gone there is nothing to do.
+ # else
+ # retry
+ # end
+ # end
+ # end
+ #
def reload(options = nil)
clear_aggregation_cache
clear_association_cache
@@ -349,14 +391,16 @@ module ActiveRecord
end
@attributes.update(fresh_object.instance_variable_get('@attributes'))
- @columns_hash = fresh_object.instance_variable_get('@columns_hash')
- @attributes_cache = {}
+ @column_types = self.class.column_types
+ @column_types_override = fresh_object.instance_variable_get('@column_types_override')
+ @attributes_cache = {}
self
end
# Saves the record with the updated_at/on attributes set to the current time.
- # Please note that no validation is performed and no callbacks are executed.
+ # Please note that no validation is performed and only the +after_touch+
+ # callback is executed.
# If an attribute name is passed, that attribute is updated along with
# updated_at/on attributes.
#
@@ -383,7 +427,7 @@ module ActiveRecord
# ball.touch(:updated_at) # => raises ActiveRecordError
#
def touch(name = nil)
- raise ActiveRecordError, "can not touch on a new record object" unless persisted?
+ raise ActiveRecordError, "cannot touch on a new record object" unless persisted?
attributes = timestamp_attributes_for_update_in_model
attributes << name if name
@@ -399,7 +443,7 @@ module ActiveRecord
changes[self.class.locking_column] = increment_lock if locking_enabled?
- @changed_attributes.except!(*changes.keys)
+ changed_attributes.except!(*changes.keys)
primary_key = self.class.primary_key
self.class.unscoped.where(primary_key => self[primary_key]).update_all(changes) == 1
end
diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb
index 3d85898c41..fd4c973504 100644
--- a/activerecord/lib/active_record/querying.rb
+++ b/activerecord/lib/active_record/querying.rb
@@ -1,15 +1,16 @@
module ActiveRecord
module Querying
- delegate :find, :take, :take!, :first, :first!, :last, :last!, :exists?, :any?, :many?, :to => :all
- delegate :first_or_create, :first_or_create!, :first_or_initialize, :to => :all
- delegate :find_or_create_by, :find_or_create_by!, :find_or_initialize_by, :to => :all
- delegate :find_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 :find, :take, :take!, :first, :first!, :last, :last!, :exists?, :any?, :many?, to: :all
+ delegate :first_or_create, :first_or_create!, :first_or_initialize, to: :all
+ delegate :find_or_create_by, :find_or_create_by!, :find_or_initialize_by, to: :all
+ delegate :find_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,
- :where, :preload, :eager_load, :includes, :from, :lock, :readonly,
- :having, :create_with, :uniq, :distinct, :references, :none, :unscope, :to => :all
- delegate :count, :average, :minimum, :maximum, :sum, :calculate, :pluck, :ids, :to => :all
+ :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
+ delegate :pluck, :ids, to: :all
# Executes a custom SQL query against your database and returns all the results. The results will
# be returned as an array with columns requested encapsulated as attributes of the model you call
diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb
index 31a0ace864..11b564f8f9 100644
--- a/activerecord/lib/active_record/railtie.rb
+++ b/activerecord/lib/active_record/railtie.rb
@@ -31,22 +31,22 @@ module ActiveRecord
config.active_record.use_schema_cache_dump = true
+ config.active_record.maintain_test_schema = true
config.eager_load_namespaces << ActiveRecord
rake_tasks do
require "active_record/base"
- ActiveRecord::Tasks::DatabaseTasks.env = Rails.env
- ActiveRecord::Tasks::DatabaseTasks.db_dir = Rails.application.config.paths["db"].first
- ActiveRecord::Tasks::DatabaseTasks.seed_loader = Rails.application
- ActiveRecord::Tasks::DatabaseTasks.database_configuration = Rails.application.config.database_configuration
- ActiveRecord::Tasks::DatabaseTasks.migrations_paths = Rails.application.paths['db/migrate'].to_a
- ActiveRecord::Tasks::DatabaseTasks.fixtures_path = File.join Rails.root, 'test', 'fixtures'
+ namespace :db do
+ task :load_config do
+ ActiveRecord::Tasks::DatabaseTasks.database_configuration = Rails.application.config.database_configuration
- if defined?(APP_RAKEFILE) && engine = Rails::Engine.find(find_engine_path(APP_RAKEFILE))
- if engine.paths['db/migrate'].existent
- ActiveRecord::Tasks::DatabaseTasks.migrations_paths += engine.paths['db/migrate'].to_a
+ if defined?(ENGINE_PATH) && engine = Rails::Engine.find(ENGINE_PATH)
+ if engine.paths['db/migrate'].existent
+ ActiveRecord::Tasks::DatabaseTasks.migrations_paths += engine.paths['db/migrate'].to_a
+ end
+ end
end
end
@@ -106,56 +106,6 @@ module ActiveRecord
initializer "active_record.set_configs" do |app|
ActiveSupport.on_load(:active_record) do
- begin
- old_behavior, ActiveSupport::Deprecation.behavior = ActiveSupport::Deprecation.behavior, :stderr
- whitelist_attributes = app.config.active_record.delete(:whitelist_attributes)
-
- if respond_to?(:mass_assignment_sanitizer=)
- mass_assignment_sanitizer = nil
- else
- mass_assignment_sanitizer = app.config.active_record.delete(:mass_assignment_sanitizer)
- end
-
- unless whitelist_attributes.nil? && mass_assignment_sanitizer.nil?
- ActiveSupport::Deprecation.warn <<-EOF.strip_heredoc, []
- Model based mass assignment security has been extracted
- out of Rails into a gem. Please use the new recommended protection model for
- params or add `protected_attributes` to your Gemfile to use the old one.
-
- To disable this message remove the `whitelist_attributes` option from your
- `config/application.rb` file and any `mass_assignment_sanitizer` options
- from your `config/environments/*.rb` files.
-
- See http://guides.rubyonrails.org/security.html#mass-assignment for more information.
- EOF
- end
-
- unless app.config.active_record.delete(:auto_explain_threshold_in_seconds).nil?
- ActiveSupport::Deprecation.warn <<-EOF.strip_heredoc, []
- The Active Record auto explain feature has been removed.
-
- To disable this message remove the `active_record.auto_explain_threshold_in_seconds`
- option from the `config/environments/*.rb` config file.
-
- See http://guides.rubyonrails.org/4_0_release_notes.html for more information.
- EOF
- end
-
- unless app.config.active_record.delete(:observers).nil?
- ActiveSupport::Deprecation.warn <<-EOF.strip_heredoc, []
- Active Record Observers has been extracted out of Rails into a gem.
- Please use callbacks or add `rails-observers` to your Gemfile to use observers.
-
- To disable this message remove the `observers` option from your
- `config/application.rb` or from your initializers.
-
- See http://guides.rubyonrails.org/4_0_release_notes.html for more information.
- EOF
- end
- ensure
- ActiveSupport::Deprecation.behavior = old_behavior
- end
-
app.config.active_record.each do |k,v|
send "#{k}=", v
end
@@ -166,7 +116,16 @@ module ActiveRecord
# and then establishes the connection.
initializer "active_record.initialize_database" do |app|
ActiveSupport.on_load(:active_record) do
- self.configurations = app.config.database_configuration || {}
+
+ class ActiveRecord::NoDatabaseError
+ remove_possible_method :extend_message
+ def extend_message(message)
+ message << "Run `$ bin/rake db:create db:migrate` to create your database"
+ message
+ end
+ end
+
+ self.configurations = Rails.application.config.database_configuration
establish_connection
end
end
diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake
index f69e9b2217..561387a179 100644
--- a/activerecord/lib/active_record/railties/databases.rake
+++ b/activerecord/lib/active_record/railties/databases.rake
@@ -2,7 +2,7 @@ require 'active_record'
db_namespace = namespace :db do
task :load_config do
- ActiveRecord::Base.configurations = ActiveRecord::Tasks::DatabaseTasks.database_configuration || {}
+ ActiveRecord::Base.configurations = ActiveRecord::Tasks::DatabaseTasks.database_configuration || {}
ActiveRecord::Migrator.migrations_paths = ActiveRecord::Tasks::DatabaseTasks.migrations_paths
end
@@ -12,13 +12,9 @@ db_namespace = namespace :db do
end
end
- desc 'Create the database from DATABASE_URL or config/database.yml for the current Rails.env (use db:create:all to create all dbs in the config)'
+ desc 'Creates the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:create:all to create all databases in the config). Without RAILS_ENV it defaults to creating the development and test databases.'
task :create => [:load_config] do
- if ENV['DATABASE_URL']
- ActiveRecord::Tasks::DatabaseTasks.create_database_url
- else
- ActiveRecord::Tasks::DatabaseTasks.create_current
- end
+ ActiveRecord::Tasks::DatabaseTasks.create_current
end
namespace :drop do
@@ -27,13 +23,9 @@ db_namespace = namespace :db do
end
end
- desc 'Drops the database using DATABASE_URL or the current Rails.env (use db:drop:all to drop all databases)'
+ desc 'Drops the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:drop:all to drop all databases in the config). Without RAILS_ENV it defaults to dropping the development and test databases.'
task :drop => [:load_config] do
- if ENV['DATABASE_URL']
- ActiveRecord::Tasks::DatabaseTasks.drop_database_url
- else
- ActiveRecord::Tasks::DatabaseTasks.drop_current
- end
+ ActiveRecord::Tasks::DatabaseTasks.drop_current
end
desc "Migrate the database (options: VERSION=x, VERBOSE=false, SCOPE=blog)."
@@ -172,7 +164,7 @@ db_namespace = namespace :db do
end
end
- desc 'Create the database, load the schema, and initialize with the seed data (use db:reset to also drop the db first)'
+ desc 'Create the database, load the schema, and initialize with the seed data (use db:reset to also drop the database first)'
task :setup => ['db:schema:load_if_ruby', 'db:structure:load_if_sql', :seed]
desc 'Load the seed data from db/seeds.rb'
@@ -187,9 +179,6 @@ db_namespace = namespace :db do
require 'active_record/fixtures'
base_dir = if ENV['FIXTURES_PATH']
- STDERR.puts "Using FIXTURES_PATH env variable is deprecated, please use " +
- "ActiveRecord::Tasks::DatabaseTasks.fixtures_path = '/path/to/fixtures' " +
- "instead."
File.join [Rails.root, ENV['FIXTURES_PATH'] || %w{test fixtures}].flatten
else
ActiveRecord::Tasks::DatabaseTasks.fixtures_path
@@ -212,9 +201,6 @@ db_namespace = namespace :db do
puts %Q(The fixture ID for "#{label}" is #{ActiveRecord::FixtureSet.identify(label)}.) if label
base_dir = if ENV['FIXTURES_PATH']
- STDERR.puts "Using FIXTURES_PATH env variable is deprecated, please use " +
- "ActiveRecord::Tasks::DatabaseTasks.fixtures_path = '/path/to/fixtures' " +
- "instead."
File.join [Rails.root, ENV['FIXTURES_PATH'] || %w{test fixtures}].flatten
else
ActiveRecord::Tasks::DatabaseTasks.fixtures_path
@@ -236,7 +222,7 @@ db_namespace = namespace :db do
end
namespace :schema do
- desc 'Create a db/schema.rb file that can be portably used against any DB supported by AR'
+ desc 'Create a db/schema.rb file that is portable against any DB supported by AR'
task :dump => [:environment, :load_config] do
require 'active_record/schema_dumper'
filename = ENV['SCHEMA'] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, 'schema.rb')
@@ -248,9 +234,7 @@ db_namespace = namespace :db do
desc 'Load a schema.rb file into the database'
task :load => [:environment, :load_config] do
- file = ENV['SCHEMA'] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, 'schema.rb')
- ActiveRecord::Tasks::DatabaseTasks.check_schema_file(file)
- load(file)
+ ActiveRecord::Tasks::DatabaseTasks.load_schema(:ruby, ENV['SCHEMA'])
end
task :load_if_ruby => ['db:create', :environment] do
@@ -271,7 +255,7 @@ db_namespace = namespace :db do
desc 'Clear a db/schema_cache.dump file.'
task :clear => [:environment, :load_config] do
filename = File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "schema_cache.dump")
- FileUtils.rm(filename) if File.exists?(filename)
+ FileUtils.rm(filename) if File.exist?(filename)
end
end
@@ -287,6 +271,7 @@ db_namespace = namespace :db do
if ActiveRecord::Base.connection.supports_migrations?
File.open(filename, "a") do |f|
f.puts ActiveRecord::Base.connection.dump_schema_information
+ f.print "\n"
end
end
db_namespace['structure:dump'].reenable
@@ -294,10 +279,7 @@ db_namespace = namespace :db do
# desc "Recreate the databases from the structure.sql file"
task :load => [:environment, :load_config] do
- filename = ENV['DB_STRUCTURE'] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "structure.sql")
- ActiveRecord::Tasks::DatabaseTasks.check_schema_file(filename)
- current_config = ActiveRecord::Tasks::DatabaseTasks.current_config
- ActiveRecord::Tasks::DatabaseTasks.structure_load(current_config, filename)
+ ActiveRecord::Tasks::DatabaseTasks.load_schema(:sql, ENV['DB_STRUCTURE'])
end
task :load_if_sql => ['db:create', :environment] do
@@ -307,8 +289,15 @@ db_namespace = namespace :db do
namespace :test do
+ task :deprecated do
+ Rake.application.top_level_tasks.grep(/^db:test:/).each do |task|
+ $stderr.puts "WARNING: #{task} is deprecated. The Rails test helper now maintains " \
+ "your test schema automatically, see the release notes for details."
+ end
+ end
+
# desc "Recreate the test database from the current schema"
- task :load => 'db:test:purge' do
+ task :load => %w(db:test:deprecated db:test:purge) do
case ActiveRecord::Base.schema_format
when :ruby
db_namespace["test:load_schema"].invoke
@@ -318,18 +307,21 @@ db_namespace = namespace :db do
end
# desc "Recreate the test database from an existent schema.rb file"
- task :load_schema => 'db:test:purge' do
+ task :load_schema => %w(db:test:deprecated db:test:purge) do
begin
+ should_reconnect = ActiveRecord::Base.connection_pool.active_connection?
ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations['test'])
ActiveRecord::Schema.verbose = false
db_namespace["schema:load"].invoke
ensure
- ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations[Rails.env])
+ if should_reconnect
+ ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations[ActiveRecord::Tasks::DatabaseTasks.env])
+ end
end
end
# desc "Recreate the test database from an existent structure.sql file"
- task :load_structure => 'db:test:purge' do
+ task :load_structure => %w(db:test:deprecated db:test:purge) do
begin
ActiveRecord::Tasks::DatabaseTasks.current_config(:config => ActiveRecord::Base.configurations['test'])
db_namespace["structure:load"].invoke
@@ -339,7 +331,7 @@ db_namespace = namespace :db do
end
# desc "Recreate the test database from a fresh schema"
- task :clone do
+ task :clone => %w(db:test:deprecated environment) do
case ActiveRecord::Base.schema_format
when :ruby
db_namespace["test:clone_schema"].invoke
@@ -349,18 +341,18 @@ db_namespace = namespace :db do
end
# desc "Recreate the test database from a fresh schema.rb file"
- task :clone_schema => ["db:schema:dump", "db:test:load_schema"]
+ task :clone_schema => %w(db:test:deprecated db:schema:dump db:test:load_schema)
# desc "Recreate the test database from a fresh structure.sql file"
- task :clone_structure => [ "db:structure:dump", "db:test:load_structure" ]
+ task :clone_structure => %w(db:test:deprecated db:structure:dump db:test:load_structure)
# desc "Empty the test database"
- task :purge => [:environment, :load_config] do
+ task :purge => %w(db:test:deprecated environment load_config) do
ActiveRecord::Tasks::DatabaseTasks.purge ActiveRecord::Base.configurations['test']
end
# desc 'Check for pending migrations and load the test schema'
- task :prepare => :load_config do
+ task :prepare => %w(db:test:deprecated environment load_config) do
unless ActiveRecord::Base.configurations.blank?
db_namespace['test:load'].invoke
end
@@ -395,6 +387,3 @@ namespace :railties do
end
end
end
-
-task 'test:prepare' => ['db:test:prepare', 'db:test:load', 'db:abort_if_pending_migrations']
-
diff --git a/activerecord/lib/active_record/readonly_attributes.rb b/activerecord/lib/active_record/readonly_attributes.rb
index 8499bb16e7..b3ddfd63d4 100644
--- a/activerecord/lib/active_record/readonly_attributes.rb
+++ b/activerecord/lib/active_record/readonly_attributes.rb
@@ -20,11 +20,5 @@ module ActiveRecord
self._attr_readonly
end
end
-
- def _attr_readonly
- message = "Instance level _attr_readonly method is deprecated, please use class level method."
- ActiveSupport::Deprecation.warn message
- defined?(@_attr_readonly) ? @_attr_readonly : self.class._attr_readonly
- end
end
end
diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb
index 1f76adb367..bce7766501 100644
--- a/activerecord/lib/active_record/reflection.rb
+++ b/activerecord/lib/active_record/reflection.rb
@@ -5,7 +5,28 @@ module ActiveRecord
included do
class_attribute :reflections
+ class_attribute :aggregate_reflections
self.reflections = {}
+ self.aggregate_reflections = {}
+ end
+
+ def self.create(macro, name, scope, options, ar)
+ case macro
+ when :has_many, :belongs_to, :has_one
+ klass = options[:through] ? ThroughReflection : AssociationReflection
+ when :composed_of
+ klass = AggregateReflection
+ end
+
+ klass.new(macro, name, scope, options, ar)
+ end
+
+ def self.add_reflection(ar, name, reflection)
+ ar.reflections = ar.reflections.merge(name => reflection)
+ end
+
+ def self.add_aggregate_reflection(ar, name, reflection)
+ ar.aggregate_reflections = ar.aggregate_reflections.merge(name => reflection)
end
# \Reflection enables to interrogate Active Record classes and objects
@@ -17,22 +38,9 @@ module ActiveRecord
# MacroReflection class has info for AggregateReflection and AssociationReflection
# classes.
module ClassMethods
- def create_reflection(macro, name, scope, options, active_record)
- case macro
- when :has_many, :belongs_to, :has_one, :has_and_belongs_to_many
- klass = options[:through] ? ThroughReflection : AssociationReflection
- reflection = klass.new(macro, name, scope, options, active_record)
- when :composed_of
- reflection = AggregateReflection.new(macro, name, scope, options, active_record)
- end
-
- self.reflections = self.reflections.merge(name => reflection)
- reflection
- end
-
# Returns an array of AggregateReflection objects for all the aggregations in the class.
def reflect_on_all_aggregations
- reflections.values.grep(AggregateReflection)
+ aggregate_reflections.values
end
# Returns the AggregateReflection object for the named +aggregation+ (use the symbol).
@@ -40,8 +48,7 @@ module ActiveRecord
# Account.reflect_on_aggregation(:balance) # => the balance AggregateReflection
#
def reflect_on_aggregation(aggregation)
- reflection = reflections[aggregation]
- reflection if reflection.is_a?(AggregateReflection)
+ aggregate_reflections[aggregation]
end
# Returns an array of AssociationReflection objects for all the
@@ -55,7 +62,7 @@ module ActiveRecord
# Account.reflect_on_all_associations(:has_many) # returns an array of all has_many associations
#
def reflect_on_all_associations(macro = nil)
- association_reflections = reflections.values.grep(AssociationReflection)
+ association_reflections = reflections.values
macro ? association_reflections.select { |reflection| reflection.macro == macro } : association_reflections
end
@@ -65,8 +72,7 @@ module ActiveRecord
# Invoice.reflect_on_association(:line_items).macro # returns :has_many
#
def reflect_on_association(association)
- reflection = reflections[association]
- reflection if reflection.is_a?(AssociationReflection)
+ reflections[association]
end
# Returns an array of AssociationReflection objects for all associations which have <tt>:autosave</tt> enabled.
@@ -113,10 +119,16 @@ module ActiveRecord
@scope = scope
@options = options
@active_record = active_record
+ @klass = options[:class]
@plural_name = active_record.pluralize_table_names ?
name.to_s.pluralize : name.to_s
end
+ def autosave=(autosave)
+ @automatic_inverse_of = false
+ @options[:autosave] = autosave
+ end
+
# Returns the class for the macro.
#
# <tt>composed_of :balance, class_name: 'Money'</tt> returns the Money class
@@ -178,10 +190,15 @@ module ActiveRecord
@klass ||= active_record.send(:compute_type, class_name)
end
- def initialize(*args)
+ attr_reader :type, :foreign_type
+
+ def initialize(macro, name, scope, options, active_record)
super
- @collection = [:has_many, :has_and_belongs_to_many].include?(macro)
+ @collection = :has_many == macro
@automatic_inverse_of = nil
+ @type = options[:as] && "#{options[:as]}_type"
+ @foreign_type = options[:foreign_type] || "#{name}_type"
+ @constructable = calculate_constructable(macro, options)
end
# Returns a new, unsaved instance of the associated class. +attributes+ will
@@ -190,12 +207,16 @@ module ActiveRecord
klass.new(attributes, &block)
end
+ def constructable? # :nodoc:
+ @constructable
+ end
+
def table_name
- @table_name ||= klass.table_name
+ klass.table_name
end
def quoted_table_name
- @quoted_table_name ||= klass.quoted_table_name
+ klass.quoted_table_name
end
def join_table
@@ -206,16 +227,8 @@ module ActiveRecord
@foreign_key ||= options[:foreign_key] || derive_foreign_key
end
- def foreign_type
- @foreign_type ||= options[:foreign_type] || "#{name}_type"
- end
-
- def type
- @type ||= options[:as] && "#{options[:as]}_type"
- end
-
def primary_key_column
- @primary_key_column ||= klass.columns.find { |c| c.name == klass.primary_key }
+ klass.columns_hash[klass.primary_key]
end
def association_foreign_key
@@ -239,20 +252,8 @@ module ActiveRecord
end
end
- def columns(tbl_name)
- @columns ||= klass.connection.columns(tbl_name)
- end
-
- def reset_column_information
- @columns = nil
- end
-
def check_validity!
check_validity_of_inverse!
-
- if has_and_belongs_to_many? && association_foreign_key == foreign_key
- raise HasAndBelongsToManyAssociationForeignKeyNeeded.new(self)
- end
end
def check_validity_of_inverse!
@@ -268,7 +269,7 @@ module ActiveRecord
end
def source_reflection
- nil
+ self
end
# A chain of reflections from this one back to the owner. For more see the explanation in
@@ -290,30 +291,13 @@ module ActiveRecord
alias :source_macro :macro
def has_inverse?
- @options[:inverse_of] || find_inverse_of_automatically
+ inverse_name
end
def inverse_of
- @inverse_of ||= if options[:inverse_of]
- klass.reflect_on_association(options[:inverse_of])
- else
- find_inverse_of_automatically
- end
- end
+ return unless inverse_name
- # Clears the cached value of +@inverse_of+ on this object. This will
- # not remove the :inverse_of option however, so future calls on the
- # +inverse_of+ will have to recompute the inverse.
- def clear_inverse_of_cache!
- @inverse_of = nil
- end
-
- # Removes the cached inverse association that was found automatically
- # and prevents this object from finding the inverse association
- # automatically in the future.
- def remove_automatic_inverse_of!
- @automatic_inverse_of = nil
- options[:automatic_inverse_of] = false
+ @inverse_of ||= klass.reflect_on_association inverse_name
end
def polymorphic_inverse_of(associated_class)
@@ -351,10 +335,6 @@ module ActiveRecord
macro == :belongs_to
end
- def has_and_belongs_to_many?
- macro == :has_and_belongs_to_many
- end
-
def association_class
case macro
when :belongs_to
@@ -363,8 +343,6 @@ module ActiveRecord
else
Associations::BelongsToAssociation
end
- when :has_and_belongs_to_many
- Associations::HasAndBelongsToManyAssociation
when :has_many
if options[:through]
Associations::HasManyThroughAssociation
@@ -387,29 +365,42 @@ module ActiveRecord
VALID_AUTOMATIC_INVERSE_MACROS = [:has_many, :has_one, :belongs_to]
INVALID_AUTOMATIC_INVERSE_OPTIONS = [:conditions, :through, :polymorphic, :foreign_key]
+ protected
+
+ def actual_source_reflection # FIXME: this is a horrible name
+ self
+ end
+
private
- # Attempts to find the inverse association automatically.
- # If it cannot find a suitable inverse association, it returns
- # nil.
- def find_inverse_of_automatically
- if @automatic_inverse_of == false
- nil
- elsif @automatic_inverse_of.nil?
- set_automatic_inverse_of
+
+ def calculate_constructable(macro, options)
+ case macro
+ when :belongs_to
+ !options[:polymorphic]
+ when :has_one
+ !options[:through]
else
- klass.reflect_on_association(@automatic_inverse_of)
+ true
end
end
- # Sets the +@automatic_inverse_of+ instance variable, and returns
- # either nil or the inverse association that it finds.
- #
- # This method caches the inverse association that is found so that
- # future calls to +find_inverse_of_automatically+ have much less
- # overhead.
- def set_automatic_inverse_of
+ # Attempts to find the inverse association name automatically.
+ # If it cannot find a suitable inverse association name, it returns
+ # nil.
+ def inverse_name
+ options.fetch(:inverse_of) do
+ if @automatic_inverse_of == false
+ nil
+ else
+ @automatic_inverse_of ||= automatic_inverse_of
+ end
+ end
+ end
+
+ # returns either nil or the inverse association name that it finds.
+ def automatic_inverse_of
if can_find_inverse_of_automatically?(self)
- inverse_name = active_record.name.downcase.to_sym
+ inverse_name = ActiveSupport::Inflector.underscore(active_record.name).to_sym
begin
reflection = klass.reflect_on_association(inverse_name)
@@ -420,20 +411,15 @@ module ActiveRecord
end
if valid_inverse_reflection?(reflection)
- @automatic_inverse_of = inverse_name
- reflection
- else
- @automatic_inverse_of = false
- nil
+ return inverse_name
end
- else
- @automatic_inverse_of = false
- nil
end
+
+ false
end
# Checks if the inverse reflection that is returned from the
- # +set_automatic_inverse_of+ method is a valid reflection. We must
+ # +automatic_inverse_of+ method is a valid reflection. We must
# make sure that the reflection's active_record name matches up
# with the current reflection's klass name.
#
@@ -441,22 +427,22 @@ module ActiveRecord
# from calling +klass+, +reflection+ will already be set to false.
def valid_inverse_reflection?(reflection)
reflection &&
- klass.name == reflection.active_record.try(:name) &&
- klass.primary_key == reflection.active_record_primary_key &&
+ klass.name == reflection.active_record.name &&
can_find_inverse_of_automatically?(reflection)
end
# Checks to see if the reflection doesn't have any options that prevent
# us from being able to guess the inverse automatically. First, the
- # +automatic_inverse_of+ option cannot be set to false. Second, we must
- # have +has_many+, +has_one+, +belongs_to+ associations. Third, we must
- # not have options such as +:polymorphic+ or +:foreign_key+ which prevent us
- # from correctly guessing the inverse association.
+ # <tt>inverse_of</tt> option cannot be set to false. Second, we must
+ # have <tt>has_many</tt>, <tt>has_one</tt>, <tt>belongs_to</tt> associations.
+ # Third, we must not have options such as <tt>:polymorphic</tt> or
+ # <tt>:foreign_key</tt> which prevent us from correctly guessing the
+ # inverse association.
#
# Anything with a scope can additionally ruin our attempt at finding an
# inverse, so we exclude reflections with scopes.
def can_find_inverse_of_automatically?(reflection)
- reflection.options[:automatic_inverse_of] != false &&
+ reflection.options[:inverse_of] != false &&
VALID_AUTOMATIC_INVERSE_MACROS.include?(reflection.macro) &&
!INVALID_AUTOMATIC_INVERSE_OPTIONS.any? { |opt| reflection.options[opt] } &&
!reflection.scope
@@ -493,6 +479,11 @@ module ActiveRecord
delegate :foreign_key, :foreign_type, :association_foreign_key,
:active_record_primary_key, :type, :to => :source_reflection
+ def initialize(macro, name, scope, options, active_record)
+ super
+ @source_reflection_name = options[:source]
+ end
+
# Returns the source of the through reflection. It checks both a singularized
# and pluralized form for <tt>:belongs_to</tt> or <tt>:has_many</tt>.
#
@@ -511,7 +502,7 @@ module ActiveRecord
# # => <ActiveRecord::Reflection::AssociationReflection: @macro=:belongs_to, @name=:tag, @active_record=Tagging, @plural_name="tags">
#
def source_reflection
- @source_reflection ||= source_reflection_names.collect { |name| through_reflection.klass.reflect_on_association(name) }.compact.first
+ through_reflection.klass.reflect_on_association(source_reflection_name)
end
# Returns the AssociationReflection object specified in the <tt>:through</tt> option
@@ -527,7 +518,7 @@ module ActiveRecord
# # => <ActiveRecord::Reflection::AssociationReflection: @macro=:has_many, @name=:taggings, @active_record=Post, @plural_name="taggings">
#
def through_reflection
- @through_reflection ||= active_record.reflect_on_association(options[:through])
+ active_record.reflect_on_association(options[:through])
end
# Returns an array of reflections which are involved in this association. Each item in the
@@ -549,7 +540,9 @@ module ActiveRecord
#
def chain
@chain ||= begin
- chain = source_reflection.chain + through_reflection.chain
+ a = source_reflection.chain
+ b = through_reflection.chain
+ chain = a + b
chain[0] = self # Use self so we don't lose the information from :source_type
chain
end
@@ -581,7 +574,7 @@ module ActiveRecord
# Add to it the scope from this reflection (if any)
scope_chain.first << scope if scope
- through_scope_chain = through_reflection.scope_chain
+ through_scope_chain = through_reflection.scope_chain.map(&:dup)
if options[:source_type]
through_scope_chain.first <<
@@ -600,7 +593,7 @@ module ActiveRecord
# A through association is nested if there would be more than one join table
def nested?
- chain.length > 2 || through_reflection.macro == :has_and_belongs_to_many
+ chain.length > 2
end
# We want to use the klass from this reflection, rather than just delegate straight to
@@ -609,12 +602,7 @@ module ActiveRecord
def association_primary_key(klass = nil)
# Get the "actual" source reflection if the immediate source reflection has a
# source reflection itself
- source_reflection = self.source_reflection
- while source_reflection.source_reflection
- source_reflection = source_reflection.source_reflection
- end
-
- source_reflection.options[:primary_key] || primary_key(klass || self.klass)
+ actual_source_reflection.options[:primary_key] || primary_key(klass || self.klass)
end
# Gets an array of possible <tt>:through</tt> source reflection names in both singular and plural form.
@@ -629,7 +617,32 @@ module ActiveRecord
# # => [:tag, :tags]
#
def source_reflection_names
- @source_reflection_names ||= (options[:source] ? [options[:source]] : [name.to_s.singularize, name]).collect { |n| n.to_sym }
+ (options[:source] ? [options[:source]] : [name.to_s.singularize, name]).collect { |n| n.to_sym }.uniq
+ end
+
+ def source_reflection_name # :nodoc:
+ return @source_reflection_name if @source_reflection_name
+
+ names = [name.to_s.singularize, name].collect { |n| n.to_sym }.uniq
+ names = names.find_all { |n|
+ through_reflection.klass.reflect_on_association(n)
+ }
+
+ if names.length > 1
+ example_options = options.dup
+ example_options[:source] = source_reflection_names.first
+ ActiveSupport::Deprecation.warn <<-eowarn
+Ambiguous source reflection for through association. Please specify a :source
+directive on your declaration like:
+
+ class #{active_record.name} < ActiveRecord::Base
+ #{macro} :#{name}, #{example_options}
+ end
+
+ eowarn
+ end
+
+ @source_reflection_name = names.first
end
def source_options
@@ -668,6 +681,12 @@ module ActiveRecord
check_validity_of_inverse!
end
+ protected
+
+ def actual_source_reflection # FIXME: this is a horrible name
+ source_reflection.actual_source_reflection
+ 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 52936817f8..ea2c3f233d 100644
--- a/activerecord/lib/active_record/relation.rb
+++ b/activerecord/lib/active_record/relation.rb
@@ -7,7 +7,7 @@ module ActiveRecord
MULTI_VALUE_METHODS = [:includes, :eager_load, :preload, :select, :group,
:order, :joins, :where, :having, :bind, :references,
- :extending]
+ :extending, :unscope]
SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :from, :reordering,
:reverse_order, :distinct, :create_with, :uniq]
@@ -17,18 +17,14 @@ module ActiveRecord
include FinderMethods, Calculations, SpawnMethods, QueryMethods, Batches, Explain, Delegation
attr_reader :table, :klass, :loaded
- attr_accessor :default_scoped
alias :model :klass
alias :loaded? :loaded
- alias :default_scoped? :default_scoped
def initialize(klass, table, values = {})
- @klass = klass
- @table = table
- @values = values
- @implicit_readonly = nil
- @loaded = false
- @default_scoped = false
+ @klass = klass
+ @table = table
+ @values = values
+ @loaded = false
end
def initialize_copy(other)
@@ -75,11 +71,11 @@ module ActiveRecord
def update_record(values, id, id_was) # :nodoc:
substitutes, binds = substitute_values values
- um = @klass.unscoped.where(@klass.arel_table[@klass.primary_key].eq(id_was || id)).arel.compile_update(substitutes)
-
+ um = @klass.unscoped.where(@klass.arel_table[@klass.primary_key].eq(id_was || id)).arel.compile_update(substitutes, @klass.primary_key)
+
@klass.connection.update(
- um,
- 'SQL',
+ um,
+ 'SQL',
binds)
end
@@ -94,7 +90,7 @@ module ActiveRecord
end
[substitutes, binds]
- end
+ end
# Initializes new record from relation while maintaining the current
# scope.
@@ -155,34 +151,58 @@ module ActiveRecord
first || new(attributes, &block)
end
- # Finds the first record with the given attributes, or creates a record with the attributes
- # if one is not found.
+ # Finds the first record with the given attributes, or creates a record
+ # with the attributes if one is not found:
#
- # ==== Examples
- # # Find the first user named Penélope or create a new one.
+ # # Find the first user named "Penélope" or create a new one.
# User.find_or_create_by(first_name: 'Penélope')
- # # => <User id: 1, first_name: 'Penélope', last_name: nil>
+ # # => #<User id: 1, first_name: "Penélope", last_name: nil>
#
- # # Find the first user named Penélope or create a new one.
+ # # Find the first user named "Penélope" or create a new one.
# # We already have one so the existing record will be returned.
# User.find_or_create_by(first_name: 'Penélope')
- # # => <User id: 1, first_name: 'Penélope', last_name: nil>
+ # # => #<User id: 1, first_name: "Penélope", last_name: nil>
#
- # # Find the first user named Scarlett or create a new one with a particular last name.
+ # # Find the first user named "Scarlett" or create a new one with
+ # # a particular last name.
# User.create_with(last_name: 'Johansson').find_or_create_by(first_name: 'Scarlett')
- # # => <User id: 2, first_name: 'Scarlett', last_name: 'Johansson'>
+ # # => #<User id: 2, first_name: "Scarlett", last_name: "Johansson">
#
- # # Find the first user named Scarlett or create a new one with a different last name.
- # # We already have one so the existing record will be returned.
+ # 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
+ # # different last name.
# User.find_or_create_by(first_name: 'Scarlett') do |user|
- # user.last_name = "O'Hara"
+ # user.last_name = 'Johansson'
# end
- # # => <User id: 2, first_name: 'Scarlett', last_name: 'Johansson'>
+ # # => #<User id: 2, first_name: "Scarlett", last_name: "Johansson">
+ #
+ # 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.
+ #
+ # 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
+ # or processes there is a race condition between both calls and it could
+ # be the case that you end up with two similar records.
+ #
+ # Whether that is a problem or not depends on the logic of the
+ # application, but in the particular case in which rows have a UNIQUE
+ # constraint an exception may be raised, just retry:
+ #
+ # begin
+ # CreditAccount.find_or_create_by(user_id: user.id)
+ # rescue ActiveRecord::RecordNotUnique
+ # retry
+ # end
+ #
def find_or_create_by(attributes, &block)
find_by(attributes) || create(attributes, &block)
end
- # Like <tt>find_or_create_by</tt>, but calls <tt>create!</tt> so an exception is raised if the created record is invalid.
+ # Like <tt>find_or_create_by</tt>, but calls <tt>create!</tt> 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
@@ -225,8 +245,13 @@ module ActiveRecord
def empty?
return @records.empty? if loaded?
- c = count
- c.respond_to?(:zero?) ? c.zero? : c.empty?
+ if limit_value == 0
+ true
+ else
+ # FIXME: This count is not compatible with #select('authors.*') or other select narrows
+ c = count
+ c.respond_to?(:zero?) ? c.zero? : c.empty?
+ end
end
# Returns true if there are any records.
@@ -291,7 +316,7 @@ module ActiveRecord
stmt.table(table)
stmt.key = table[primary_key]
- if with_default_scope.joins_values.any?
+ if joins_values.any?
@klass.connection.join_to_update(stmt, arel)
else
stmt.take(arel.limit)
@@ -416,7 +441,7 @@ module ActiveRecord
stmt = Arel::DeleteManager.new(arel.engine)
stmt.from(table)
- if with_default_scope.joins_values.any?
+ if joins_values.any?
@klass.connection.join_to_delete(stmt, arel, table[primary_key])
else
stmt.wheres = arel.constraints
@@ -490,7 +515,21 @@ module ActiveRecord
# User.where(name: 'Oscar').to_sql
# # => SELECT "users".* FROM "users" WHERE "users"."name" = 'Oscar'
def to_sql
- @to_sql ||= klass.connection.to_sql(arel, bind_values.dup)
+ @to_sql ||= begin
+ relation = self
+ connection = klass.connection
+ visitor = connection.visitor
+
+ if eager_loading?
+ find_with_associations { |rel| relation = rel }
+ end
+
+ ast = relation.arel.ast
+ binds = relation.bind_values.dup
+ visitor.accept(ast) do
+ connection.quote(*binds.shift.reverse)
+ end
+ end
end
# Returns a hash of where conditions.
@@ -498,12 +537,11 @@ module ActiveRecord
# User.where(name: 'Oscar').where_values_hash
# # => {name: "Oscar"}
def where_values_hash
- scope = with_default_scope
- equalities = scope.where_values.grep(Arel::Nodes::Equality).find_all { |node|
+ equalities = where_values.grep(Arel::Nodes::Equality).find_all { |node|
node.left.relation.name == table_name
}
- binds = Hash[scope.bind_values.find_all(&:first).map { |column, v| [column.name, v] }]
+ binds = Hash[bind_values.find_all(&:first).map { |column, v| [column.name, v] }]
binds.merge!(Hash[bind_values.find_all(&:first).map { |column, v| [column.name, v] }])
Hash[equalities.map { |where|
@@ -551,16 +589,6 @@ module ActiveRecord
q.pp(self.to_a)
end
- def with_default_scope #:nodoc:
- if default_scoped? && default_scope = klass.send(:build_default_scope)
- default_scope = default_scope.merge(self)
- default_scope.default_scoped = false
- default_scope
- else
- self
- end
- end
-
# Returns true if relation is blank.
def blank?
to_a.blank?
@@ -580,34 +608,24 @@ module ActiveRecord
private
def exec_queries
- default_scoped = with_default_scope
-
- if default_scoped.equal?(self)
- @records = eager_loading? ? find_with_associations : @klass.find_by_sql(arel, bind_values)
+ @records = eager_loading? ? find_with_associations : @klass.find_by_sql(arel, bind_values)
- preload = preload_values
- preload += includes_values unless eager_loading?
- preload.each do |associations|
- ActiveRecord::Associations::Preloader.new(@records, associations).run
- end
-
- # @readonly_value is true only if set explicitly. @implicit_readonly is true if there
- # are JOINS and no explicit SELECT.
- readonly = readonly_value.nil? ? @implicit_readonly : readonly_value
- @records.each { |record| record.readonly! } if readonly
- else
- @records = default_scoped.to_a
+ preload = preload_values
+ preload += includes_values unless eager_loading?
+ preloader = ActiveRecord::Associations::Preloader.new
+ preload.each do |associations|
+ preloader.preload @records, associations
end
+ @records.each { |record| record.readonly! } if readonly_value
+
@loaded = true
@records
end
def references_eager_loaded_tables?
joined_tables = arel.join_sources.map do |join|
- if join.is_a?(Arel::Nodes::StringJoin)
- tables_in_string(join.left)
- else
+ unless join.is_a?(Arel::Nodes::StringJoin)
[join.left.table_name, join.left.table_alias]
end
end
@@ -616,41 +634,8 @@ module ActiveRecord
# always convert table names to downcase as in Oracle quoted table names are in uppercase
joined_tables = joined_tables.flatten.compact.map { |t| t.downcase }.uniq
- string_tables = tables_in_string(to_sql)
-
- if (references_values - joined_tables).any?
- true
- elsif !ActiveRecord::Base.disable_implicit_join_references &&
- (string_tables - joined_tables).any?
- ActiveSupport::Deprecation.warn(
- "It looks like you are eager loading table(s) (one of: #{string_tables.join(', ')}) " \
- "that are referenced in a string SQL snippet. For example: \n" \
- "\n" \
- " Post.includes(:comments).where(\"comments.title = 'foo'\")\n" \
- "\n" \
- "Currently, Active Record recognizes the table in the string, and knows to JOIN the " \
- "comments table to the query, rather than loading comments in a separate query. " \
- "However, doing this without writing a full-blown SQL parser is inherently flawed. " \
- "Since we don't want to write an SQL parser, we are removing this functionality. " \
- "From now on, you must explicitly tell Active Record when you are referencing a table " \
- "from a string:\n" \
- "\n" \
- " Post.includes(:comments).where(\"comments.title = 'foo'\").references(:comments)\n" \
- "\n" \
- "If you don't rely on implicit join references you can disable the feature entirely " \
- "by setting `config.active_record.disable_implicit_join_references = true`."
- )
- true
- else
- false
- end
- end
- def tables_in_string(string)
- return [] if string.blank?
- # always convert table names to downcase as in Oracle quoted table names are in uppercase
- # ignore raw_sql_ that is used by Oracle adapter as alias for limit/offset subqueries
- string.scan(/([a-zA-Z_][.\w]+).?\./).flatten.map{ |s| s.downcase }.uniq - ['raw_sql_']
+ (references_values - joined_tables).any?
end
end
end
diff --git a/activerecord/lib/active_record/relation/batches.rb b/activerecord/lib/active_record/relation/batches.rb
index b921f2eddb..49b01909c6 100644
--- a/activerecord/lib/active_record/relation/batches.rb
+++ b/activerecord/lib/active_record/relation/batches.rb
@@ -11,7 +11,7 @@ module ActiveRecord
# The #find_each method uses #find_in_batches with a batch size of 1000 (or as
# specified by the +:batch_size+ option).
#
- # Person.all.find_each do |person|
+ # Person.find_each do |person|
# person.do_awesome_stuff
# end
#
@@ -19,47 +19,78 @@ module ActiveRecord
# person.party_all_night!
# end
#
- # You can also pass the +:start+ option to specify
- # an offset to control the starting point.
- def find_each(options = {})
- find_in_batches(options) do |records|
- records.each { |record| yield record }
- end
- end
-
- # Yields each batch of records that was found by the find +options+ as
- # an array. The size of each batch is set by the +:batch_size+
- # option; the default is 1000.
+ # If you do not provide a block to #find_each, it will return an Enumerator
+ # for chaining with other methods:
+ #
+ # Person.find_each.with_index do |person, index|
+ # person.award_trophy(index + 1)
+ # end
+ #
+ # ==== Options
+ # * <tt>:batch_size</tt> - Specifies the size of the batch. Default to 1000.
+ # * <tt>:start</tt> - Specifies the starting point for the batch processing.
+ # This is especially useful if you want multiple workers dealing with
+ # the same processing queue. You can make worker 1 handle all the records
+ # between id 0 and 10,000 and worker 2 handle from 10,000 and beyond
+ # (by setting the +:start+ option on that worker).
#
- # You can control the starting point for the batch processing by
- # supplying the +:start+ option. This is especially useful if you
- # want multiple workers dealing with the same processing queue. You can
- # make worker 1 handle all the records between id 0 and 10,000 and
- # worker 2 handle from 10,000 and beyond (by setting the +:start+
- # option on that worker).
+ # # Let's process for a batch of 2000 records, skipping the first 2000 rows
+ # Person.find_each(start: 2000, batch_size: 2000) do |person|
+ # person.party_all_night!
+ # end
#
- # It's not possible to set the order. That is automatically set to
+ # NOTE: It's not possible to set the order. That is automatically set to
# ascending on the primary key ("id ASC") to make the batch ordering
# work. This also means that this method only works with integer-based
- # primary keys. You can't set the limit either, that's used to control
+ # primary keys.
+ #
+ # NOTE: You can't set the limit either, that's used to control
# the batch sizes.
+ def find_each(options = {})
+ if block_given?
+ find_in_batches(options) do |records|
+ records.each { |record| yield record }
+ end
+ else
+ enum_for :find_each, options
+ end
+ end
+
+ # Yields each batch of records that was found by the find +options+ as
+ # an array.
#
# Person.where("age > 21").find_in_batches do |group|
# sleep(50) # Make sure it doesn't get too crowded in there!
# group.each { |person| person.party_all_night! }
# end
#
+ # ==== Options
+ # * <tt>:batch_size</tt> - Specifies the size of the batch. Default to 1000.
+ # * <tt>:start</tt> - Specifies the starting point for the batch processing.
+ # This is especially useful if you want multiple workers dealing with
+ # the same processing queue. You can make worker 1 handle all the records
+ # between id 0 and 10,000 and worker 2 handle from 10,000 and beyond
+ # (by setting the +:start+ option on that worker).
+ #
# # Let's process the next 2000 records
- # Person.all.find_in_batches(start: 2000, batch_size: 2000) do |group|
+ # Person.find_in_batches(start: 2000, batch_size: 2000) do |group|
# group.each { |person| person.party_all_night! }
# end
+ #
+ # NOTE: It's not possible to set the order. That is automatically set to
+ # ascending on the primary key ("id ASC") to make the batch ordering
+ # work. This also means that this method only works with integer-based
+ # primary keys.
+ #
+ # NOTE: You can't set the limit either, that's used to control
+ # the batch sizes.
def find_in_batches(options = {})
options.assert_valid_keys(:start, :batch_size)
relation = self
- unless arel.orders.blank? && arel.taken.blank?
- ActiveRecord::Base.logger.warn("Scoped order and limit are ignored, it's forced to be batch order and batch size")
+ 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
start = options.delete(:start)
diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb
index 7239270c4d..45ffb99868 100644
--- a/activerecord/lib/active_record/relation/calculations.rb
+++ b/activerecord/lib/active_record/relation/calculations.rb
@@ -20,6 +20,8 @@ module ActiveRecord
# Person.group(:city).count
# # => { 'Rome' => 5, 'Paris' => 3 }
def count(column_name = nil, options = {})
+ # TODO: Remove options argument as soon we remove support to
+ # activerecord-deprecated_finders.
column_name, options = nil, column_name if column_name.is_a?(Hash)
calculate(:count, column_name, options)
end
@@ -29,6 +31,8 @@ module ActiveRecord
#
# Person.average(:age) # => 35.8
def average(column_name, options = {})
+ # TODO: Remove options argument as soon we remove support to
+ # activerecord-deprecated_finders.
calculate(:average, column_name, options)
end
@@ -38,6 +42,8 @@ module ActiveRecord
#
# Person.minimum(:age) # => 7
def minimum(column_name, options = {})
+ # TODO: Remove options argument as soon we remove support to
+ # activerecord-deprecated_finders.
calculate(:minimum, column_name, options)
end
@@ -47,6 +53,8 @@ module ActiveRecord
#
# Person.maximum(:age) # => 93
def maximum(column_name, options = {})
+ # TODO: Remove options argument as soon we remove support to
+ # activerecord-deprecated_finders.
calculate(:maximum, column_name, options)
end
@@ -56,15 +64,7 @@ module ActiveRecord
#
# Person.sum(:age) # => 4562
def sum(*args)
- if block_given?
- ActiveSupport::Deprecation.warn(
- "Calling #sum with a block is deprecated and will be removed in Rails 4.1. " \
- "If you want to perform sum calculation over the array of elements, use `to_a.sum(&block)`."
- )
- self.to_a.sum(*args) {|*block_args| yield(*block_args)}
- else
- calculate(:sum, *args)
- end
+ calculate(:sum, *args)
end
# This calculates aggregate values in the given column. Methods for count, sum, average,
@@ -99,23 +99,17 @@ module ActiveRecord
#
# Person.sum("2 * age")
def calculate(operation, column_name, options = {})
- relation = with_default_scope
-
- if column_name.is_a?(Symbol) && attribute_aliases.key?(column_name.to_s)
- column_name = attribute_aliases[column_name.to_s].to_sym
+ # TODO: Remove options argument as soon we remove support to
+ # activerecord-deprecated_finders.
+ if column_name.is_a?(Symbol) && attribute_alias?(column_name)
+ column_name = attribute_alias(column_name)
end
- if relation.equal?(self)
- if has_include?(column_name)
- construct_relation_for_association_calculations.calculate(operation, column_name, options)
- else
- perform_calculation(operation, column_name, options)
- end
+ if has_include?(column_name)
+ construct_relation_for_association_calculations.calculate(operation, column_name, options)
else
- relation.calculate(operation, column_name, options)
+ perform_calculation(operation, column_name, options)
end
- rescue ThrowResult
- 0
end
# Use <tt>pluck</tt> as a shortcut to select one or more attributes without
@@ -153,39 +147,31 @@ module ActiveRecord
#
def pluck(*column_names)
column_names.map! do |column_name|
- if column_name.is_a?(Symbol)
- if attribute_aliases.key?(column_name.to_s)
- column_name = attribute_aliases[column_name.to_s].to_sym
- end
-
- if self.columns_hash.key?(column_name.to_s)
- column_name = "#{connection.quote_table_name(table_name)}.#{connection.quote_column_name(column_name)}"
- end
+ if column_name.is_a?(Symbol) && attribute_alias?(column_name)
+ attribute_alias(column_name)
+ else
+ column_name.to_s
end
-
- column_name
end
if has_include?(column_names.first)
construct_relation_for_association_calculations.pluck(*column_names)
else
relation = spawn
- relation.select_values = column_names
+ relation.select_values = column_names.map { |cn|
+ columns_hash.key?(cn) ? arel_table[cn] : cn
+ }
result = klass.connection.select_all(relation.arel, nil, bind_values)
columns = result.columns.map do |key|
klass.column_types.fetch(key) {
- result.column_types.fetch(key) {
- Class.new { def type_cast(v); v; end }.new
- }
+ result.column_types.fetch(key) { result.identity_type }
}
end
result = result.map do |attributes|
values = klass.initialize_attributes(attributes).values
- columns.zip(values).map do |column, value|
- column.type_cast(value)
- end
+ columns.zip(values).map { |column, value| column.type_cast value }
end
columns.one? ? result.map!(&:first) : result
end
@@ -206,26 +192,22 @@ module ActiveRecord
end
def perform_calculation(operation, column_name, options = {})
+ # TODO: Remove options argument as soon we remove support to
+ # activerecord-deprecated_finders.
operation = operation.to_s.downcase
# If #count is used with #distinct / #uniq it is considered distinct. (eg. relation.distinct.count)
distinct = self.distinct_value
- if options.has_key?(:distinct)
- ActiveSupport::Deprecation.warn "The :distinct option for `Relation#count` is deprecated. " \
- "Please use `Relation#distinct` instead. (eg. `relation.distinct.count`)"
- distinct = options[:distinct]
- end
if operation == "count"
- column_name ||= (select_for_count || :all)
+ column_name ||= select_for_count
unless arel.ast.grep(Arel::Nodes::OuterJoin).empty?
distinct = true
end
column_name = primary_key if column_name == :all && distinct
-
- distinct = nil if column_name =~ /\s*DISTINCT\s+/i
+ distinct = nil if column_name =~ /\s*DISTINCT[\s(]+/i
end
if group_values.any?
@@ -342,7 +324,9 @@ module ActiveRecord
}
key = key.first if key.size == 1
key = key_records[key] if associated
- [key, type_cast_calculated_value(row[aggregate_alias], column_for(column_name), operation)]
+
+ column_type = calculated_data.column_types.fetch(aggregate_alias) { column_for(column_name) }
+ [key, type_cast_calculated_value(row[aggregate_alias], column_type, operation)]
end]
end
@@ -386,10 +370,12 @@ module ActiveRecord
column ? column.type_cast(value) : value
end
+ # TODO: refactor to allow non-string `select_values` (eg. Arel nodes).
def select_for_count
if select_values.present?
- select = select_values.join(", ")
- select if select !~ /[,*]/
+ select_values.join(", ")
+ else
+ :all
end
end
diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb
index 8d6740246c..21beed332f 100644
--- a/activerecord/lib/active_record/relation/delegation.rb
+++ b/activerecord/lib/active_record/relation/delegation.rb
@@ -1,8 +1,35 @@
-require 'thread'
-require 'thread_safe'
+require 'set'
+require 'active_support/concern'
+require 'active_support/deprecation'
module ActiveRecord
module Delegation # :nodoc:
+ module DelegateCache
+ def relation_delegate_class(klass) # :nodoc:
+ @relation_delegate_cache[klass]
+ end
+
+ def initialize_relation_delegate_cache # :nodoc:
+ @relation_delegate_cache = cache = {}
+ [
+ ActiveRecord::Relation,
+ ActiveRecord::Associations::CollectionProxy,
+ ActiveRecord::AssociationRelation
+ ].each do |klass|
+ delegate = Class.new(klass) {
+ include ClassSpecificRelation
+ }
+ const_set klass.name.gsub('::', '_'), delegate
+ cache[klass] = delegate
+ end
+ end
+
+ def inherited(child_class)
+ child_class.initialize_relation_delegate_cache
+ super
+ end
+ end
+
extend ActiveSupport::Concern
# This module creates compiled delegation methods dynamically at runtime, which makes
@@ -10,7 +37,14 @@ module ActiveRecord
# may vary depending on the klass of a relation, so we create a subclass of Relation
# for each different klass, and the delegations are compiled into that subclass only.
- delegate :to_xml, :to_yaml, :length, :collect, :map, :each, :all?, :include?, :to_ary, :to => :to_a
+ BLACKLISTED_ARRAY_METHODS = [
+ :compact!, :flatten!, :reject!, :reverse!, :rotate!, :map!,
+ :shuffle!, :slice!, :sort!, :sort_by!, :delete_if,
+ :keep_if, :pop, :shift, :delete_at, :compact
+ ].to_set # :nodoc:
+
+ delegate :to_xml, :to_yaml, :length, :collect, :map, :each, :all?, :include?, :to_ary, to: :to_a
+
delegate :table_name, :quoted_table_name, :primary_key, :quoted_primary_key,
:connection, :columns_hash, :to => :klass
@@ -38,7 +72,7 @@ module ActiveRecord
RUBY
else
define_method method do |*args, &block|
- scoping { @klass.send(method, *args, &block) }
+ scoping { @klass.public_send(method, *args, &block) }
end
end
end
@@ -57,13 +91,10 @@ module ActiveRecord
def method_missing(method, *args, &block)
if @klass.respond_to?(method)
self.class.delegate_to_scoped_klass(method)
- scoping { @klass.send(method, *args, &block) }
- elsif Array.method_defined?(method)
- self.class.delegate method, :to => :to_a
- to_a.send(method, *args, &block)
+ scoping { @klass.public_send(method, *args, &block) }
elsif arel.respond_to?(method)
self.class.delegate method, :to => :arel
- arel.send(method, *args, &block)
+ arel.public_send(method, *args, &block)
else
super
end
@@ -71,52 +102,36 @@ module ActiveRecord
end
module ClassMethods # :nodoc:
- @@subclasses = ThreadSafe::Cache.new(:initial_capacity => 2)
-
- def new(klass, *args)
- relation = relation_class_for(klass).allocate
- relation.__send__(:initialize, klass, *args)
- relation
- end
-
- # This doesn't have to be thread-safe. relation_class_for guarantees that this will only be
- # called exactly once for a given const name.
- def const_missing(name)
- const_set(name, Class.new(self) { include ClassSpecificRelation })
+ def create(klass, *args)
+ relation_class_for(klass).new(klass, *args)
end
private
- # Cache the constants in @@subclasses because looking them up via const_get
- # make instantiation significantly slower.
+
def relation_class_for(klass)
- if klass && (klass_name = klass.name)
- my_cache = @@subclasses.compute_if_absent(self) { ThreadSafe::Cache.new }
- # This hash is keyed by klass.name to avoid memory leaks in development mode
- my_cache.compute_if_absent(klass_name) do
- # Cache#compute_if_absent guarantees that the block will only executed once for the given klass_name
- const_get("#{name.gsub('::', '_')}_#{klass_name.gsub('::', '_')}", false)
- end
- else
- ActiveRecord::Relation
- end
+ klass.relation_delegate_class(self)
end
end
def respond_to?(method, include_private = false)
- super || Array.method_defined?(method) ||
- @klass.respond_to?(method, include_private) ||
+ super || @klass.respond_to?(method, include_private) ||
+ array_delegable?(method) ||
arel.respond_to?(method, include_private)
end
protected
+ def array_delegable?(method)
+ Array.method_defined?(method) && BLACKLISTED_ARRAY_METHODS.exclude?(method)
+ end
+
def method_missing(method, *args, &block)
if @klass.respond_to?(method)
- scoping { @klass.send(method, *args, &block) }
- elsif Array.method_defined?(method)
- to_a.send(method, *args, &block)
+ scoping { @klass.public_send(method, *args, &block) }
+ elsif array_delegable?(method)
+ to_a.public_send(method, *args, &block)
elsif arel.respond_to?(method)
- arel.send(method, *args, &block)
+ arel.public_send(method, *args, &block)
else
super
end
diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb
index ba222aac93..4984dbd277 100644
--- a/activerecord/lib/active_record/relation/finder_methods.rb
+++ b/activerecord/lib/active_record/relation/finder_methods.rb
@@ -1,19 +1,24 @@
module ActiveRecord
module FinderMethods
+ ONE_AS_ONE = '1 AS one'
+
# Find by id - This can either be a specific id (1), a list of ids (1, 5, 6), or an array of ids ([5, 6, 10]).
# If no record can be found for all of the listed ids, then RecordNotFound will be raised. If the primary key
# is an integer, find by id coerces its arguments using +to_i+.
#
- # Person.find(1) # returns the object for ID = 1
- # Person.find("1") # returns the object for ID = 1
- # Person.find(1, 2, 6) # returns an array for objects with IDs in (1, 2, 6)
- # Person.find([7, 17]) # returns an array for objects with IDs in (7, 17)
- # Person.find([1]) # returns an array for the object with ID = 1
+ # Person.find(1) # returns the object for ID = 1
+ # Person.find("1") # returns the object for ID = 1
+ # Person.find("31-sarah") # returns the object for ID = 31
+ # Person.find(1, 2, 6) # returns an array for objects with IDs in (1, 2, 6)
+ # Person.find([7, 17]) # returns an array for objects with IDs in (7, 17)
+ # Person.find([1]) # returns an array for the object with ID = 1
# Person.where("administrator = 1").order("created_on DESC").find(1)
#
- # Note that returned records may not be in the same order as the ids you
- # provide since database rows are unordered. Give an explicit <tt>order</tt>
- # to ensure the results are sorted.
+ # <tt>ActiveRecord::RecordNotFound</tt> will be raised if one or more ids are not found.
+ #
+ # NOTE: The returned records may not be in the same order as the ids you
+ # provide since database rows are unordered. You'd need to provide an explicit <tt>order</tt>
+ # option if you want the results are sorted.
#
# ==== Find with lock
#
@@ -28,6 +33,34 @@ module ActiveRecord
# person.visits += 1
# person.save!
# end
+ #
+ # ==== Variations of +find+
+ #
+ # Person.where(name: 'Spartacus', rating: 4)
+ # # returns a chainable list (which can be empty).
+ #
+ # Person.find_by(name: 'Spartacus', rating: 4)
+ # # returns the first item or nil.
+ #
+ # Person.where(name: 'Spartacus', rating: 4).first_or_initialize
+ # # returns the first item or returns a new instance (requires you call .save to persist against the database).
+ #
+ # Person.where(name: 'Spartacus', rating: 4).first_or_create
+ # # returns the first item or creates it and returns it, available since Rails 3.2.1.
+ #
+ # ==== Alternatives for +find+
+ #
+ # Person.where(name: 'Spartacus', rating: 4).exists?(conditions = :none)
+ # # returns a boolean indicating if any record with the given conditions exist.
+ #
+ # Person.where(name: 'Spartacus', rating: 4).select("field1, field2, field3")
+ # # returns a chainable list of instances with only the mentioned fields.
+ #
+ # Person.where(name: 'Spartacus', rating: 4).ids
+ # # returns an Array of ids, available since Rails 3.2.1.
+ #
+ # Person.where(name: 'Spartacus', rating: 4).pluck(:field1, :field2)
+ # # returns an Array of the required fields, available since Rails 3.1.
def find(*args)
if block_given?
to_a.find { |*block_args| yield(*block_args) }
@@ -79,13 +112,22 @@ module ActiveRecord
# Person.where(["user_name = :u", { u: user_name }]).first
# Person.order("created_on DESC").offset(5).first
# Person.first(3) # returns the first three objects fetched by SELECT * FROM people LIMIT 3
+ #
+ # ==== Rails 3
+ #
+ # Person.first # SELECT "people".* FROM "people" LIMIT 1
+ #
+ # NOTE: Rails 3 may not order this query by the primary key and the order
+ # will depend on the database implementation. In order to ensure that behavior,
+ # use <tt>User.order(:id).first</tt> instead.
+ #
+ # ==== Rails 4
+ #
+ # Person.first # SELECT "people".* FROM "people" ORDER BY "people"."id" ASC LIMIT 1
+ #
def first(limit = nil)
if limit
- if order_values.empty? && primary_key
- order(arel_table[primary_key].asc).limit(limit).to_a
- else
- limit(limit).to_a
- end
+ find_first_with_limit(limit)
else
find_first
end
@@ -130,21 +172,21 @@ module ActiveRecord
last or raise RecordNotFound
end
- # Returns truthy if a record exists in the table that matches the +id+ or
- # conditions given, or falsy 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
# string (such as <tt>'5'</tt>).
# * Array - Finds the record that matches these +find+-style conditions
- # (such as <tt>['color = ?', 'red']</tt>).
+ # (such as <tt>['name LIKE ?', "%#{query}%"]</tt>).
# * Hash - Finds the record that matches these +find+-style conditions
- # (such as <tt>{color: 'red'}</tt>).
+ # (such as <tt>{name: 'David'}</tt>).
# * +false+ - Returns always +false+.
# * 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 ActiveRecord::Base.
+ # For more information about specifying conditions as a hash or array,
+ # see the Conditions section in the introduction to <tt>ActiveRecord::Base</tt>.
#
# 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
@@ -153,6 +195,7 @@ module ActiveRecord
# Person.exists?(5)
# Person.exists?('5')
# Person.exists?(['name LIKE ?', "%#{query}%"])
+ # Person.exists?(id: [1, 4, 8])
# Person.exists?(name: 'David')
# Person.exists?(false)
# Person.exists?
@@ -160,9 +203,10 @@ module ActiveRecord
conditions = conditions.id if Base === conditions
return false if !conditions
- join_dependency = construct_join_dependency
- relation = construct_relation_for_association_find(join_dependency)
- relation = relation.except(:select, :order).select("1 AS one").limit(1)
+ relation = apply_join_dependency(self, construct_join_dependency)
+ return false if ActiveRecord::NullRelation === relation
+
+ relation = relation.except(:select, :order).select(ONE_AS_ONE).limit(1)
case conditions
when Array, Hash
@@ -171,9 +215,7 @@ module ActiveRecord
relation = relation.where(table[primary_key].eq(conditions)) if conditions != :none
end
- connection.select_value(relation, "#{name} Exists", relation.bind_values)
- rescue ThrowResult
- false
+ connection.select_value(relation, "#{name} Exists", relation.bind_values) ? true : false
end
# This method is called whenever no records are found with either a single
@@ -198,19 +240,29 @@ module ActiveRecord
raise RecordNotFound, error
end
- protected
+ private
def find_with_associations
join_dependency = construct_join_dependency
- relation = construct_relation_for_association_find(join_dependency)
- rows = connection.select_all(relation, 'SQL', relation.bind_values.dup)
- join_dependency.instantiate(rows)
- rescue ThrowResult
- []
+
+ aliases = join_dependency.aliases
+ relation = select aliases.columns
+ relation = apply_join_dependency(relation, join_dependency)
+
+ if block_given?
+ yield relation
+ else
+ if ActiveRecord::NullRelation === relation
+ []
+ else
+ rows = connection.select_all(relation.arel, 'SQL', relation.bind_values.dup)
+ join_dependency.instantiate(rows, aliases)
+ end
+ end
end
def construct_join_dependency(joins = [])
- including = (eager_load_values + includes_values).uniq
+ including = eager_load_values + includes_values
ActiveRecord::Associations::JoinDependency.new(@klass, including, joins)
end
@@ -218,36 +270,40 @@ module ActiveRecord
apply_join_dependency(self, construct_join_dependency(arel.froms.first))
end
- def construct_relation_for_association_find(join_dependency)
- relation = except(:select).select(join_dependency.columns)
- apply_join_dependency(relation, join_dependency)
- end
-
def apply_join_dependency(relation, join_dependency)
relation = relation.except(:includes, :eager_load, :preload)
- relation = join_dependency.join_relation(relation)
+ relation = relation.joins join_dependency
if using_limitable_reflections?(join_dependency.reflections)
relation
else
- relation.where!(construct_limited_ids_condition(relation)) if relation.limit_value
+ if relation.limit_value
+ limited_ids = limited_ids_for(relation)
+ limited_ids.empty? ? relation.none! : relation.where!(table[primary_key].in(limited_ids))
+ end
relation.except(:limit, :offset)
end
end
- def construct_limited_ids_condition(relation)
+ def limited_ids_for(relation)
values = @klass.connection.columns_for_distinct(
"#{quoted_table_name}.#{quoted_primary_key}", relation.order_values)
relation = relation.except(:select).select(values).distinct!
id_rows = @klass.connection.select_all(relation.arel, 'SQL', relation.bind_values)
- ids_array = id_rows.map {|row| row[primary_key]}
+ id_rows.map {|row| row[primary_key]}
+ end
- ids_array.empty? ? raise(ThrowResult) : table[primary_key].in(ids_array)
+ def using_limitable_reflections?(reflections)
+ reflections.none? { |r| r.collection? }
end
+ protected
+
def find_with_ids(*ids)
+ raise UnknownPrimaryKey.new(@klass) if primary_key.nil?
+
expects_array = ids.first.kind_of?(Array)
return ids.first if expects_array && ids.first.empty?
@@ -312,12 +368,15 @@ module ActiveRecord
if loaded?
@records.first
else
- @first ||=
- if with_default_scope.order_values.empty? && primary_key
- order(arel_table[primary_key].asc).limit(1).to_a.first
- else
- limit(1).to_a.first
- end
+ @first ||= find_first_with_limit(1).first
+ end
+ end
+
+ def find_first_with_limit(limit)
+ if order_values.empty? && primary_key
+ order(arel_table[primary_key].asc).limit(limit).to_a
+ else
+ limit(limit).to_a
end
end
@@ -326,16 +385,12 @@ module ActiveRecord
@records.last
else
@last ||=
- if offset_value || limit_value
+ if limit_value
to_a.last
else
reverse_order.limit(1).to_a.first
end
end
end
-
- def using_limitable_reflections?(reflections)
- reflections.none? { |r| r.collection? }
- end
end
end
diff --git a/activerecord/lib/active_record/relation/merger.rb b/activerecord/lib/active_record/relation/merger.rb
index c114ea0c0d..182b9ed89c 100644
--- a/activerecord/lib/active_record/relation/merger.rb
+++ b/activerecord/lib/active_record/relation/merger.rb
@@ -22,7 +22,7 @@ module ActiveRecord
# build a relation to merge in rather than directly merging
# the values.
def other
- other = Relation.new(relation.klass, relation.table)
+ other = Relation.create(relation.klass, relation.table)
hash.each { |k, v|
if k == :joins
if Hash === v
@@ -42,10 +42,6 @@ module ActiveRecord
attr_reader :relation, :values, :other
def initialize(relation, other)
- if other.default_scoped? && other.klass != relation.klass
- other = other.with_default_scope
- end
-
@relation = relation
@values = other.values
@other = other
@@ -62,7 +58,11 @@ module ActiveRecord
def merge
normal_values.each do |name|
value = values[name]
- relation.send("#{name}!", *value) unless value.blank?
+ # The unless clause is here mostly for performance reasons (since the `send` call might be moderately
+ # expensive), most of the time the value is going to be `nil` or `.blank?`, the only catch is that
+ # `false.blank?` returns `true`, so there needs to be an extra check so that explicit `false` values
+ # don't fall through the cracks.
+ relation.send("#{name}!", *value) unless value.nil? || (value.blank? && false != value)
end
merge_multi_values
@@ -94,26 +94,42 @@ module ActiveRecord
[])
relation.joins! rest
- @relation = join_dependency.join_relation(relation)
+ @relation = relation.joins join_dependency
end
end
def merge_multi_values
lhs_wheres = relation.where_values
rhs_wheres = values[:where] || []
+
lhs_binds = relation.bind_values
rhs_binds = values[:bind] || []
removed, kept = partition_overwrites(lhs_wheres, rhs_wheres)
- relation.where_values = kept + rhs_wheres
- relation.bind_values = filter_binds(lhs_binds, removed) + rhs_binds
+ where_values = kept + rhs_wheres
+ bind_values = filter_binds(lhs_binds, removed) + rhs_binds
+
+ conn = relation.klass.connection
+ bv_index = 0
+ where_values.map! do |node|
+ if Arel::Nodes::Equality === node && Arel::Nodes::BindParam === node.right
+ substitute = conn.substitute_at(bind_values[bv_index].first, bv_index)
+ bv_index += 1
+ Arel::Nodes::Equality.new(node.left, substitute)
+ else
+ node
+ end
+ end
+
+ relation.where_values = where_values
+ relation.bind_values = bind_values
if values[:reordering]
# override any order specified in the original relation
relation.reorder! values[:order]
elsif values[:order]
- # merge in order_values from r
+ # merge in order_values from relation
relation.order! values[:order]
end
diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb
index b7609c97b5..1252af7635 100644
--- a/activerecord/lib/active_record/relation/predicate_builder.rb
+++ b/activerecord/lib/active_record/relation/predicate_builder.rb
@@ -1,15 +1,26 @@
module ActiveRecord
class PredicateBuilder # :nodoc:
+ @handlers = []
+
+ autoload :RelationHandler, 'active_record/relation/predicate_builder/relation_handler'
+ autoload :ArrayHandler, 'active_record/relation/predicate_builder/array_handler'
+
+ def self.resolve_column_aliases(klass, hash)
+ hash = hash.dup
+ hash.keys.grep(Symbol) do |key|
+ if klass.attribute_alias? key
+ hash[klass.attribute_alias(key)] = hash.delete key
+ end
+ end
+ hash
+ end
+
def self.build_from_hash(klass, attributes, default_table)
queries = []
attributes.each do |column, value|
table = default_table
- if column.is_a?(Symbol) && klass.attribute_aliases.key?(column.to_s)
- column = klass.attribute_aliases[column.to_s]
- end
-
if value.is_a?(Hash)
if value.empty?
queries << '1=0'
@@ -44,9 +55,9 @@ module ActiveRecord
#
# For polymorphic relationships, find the foreign key and type:
# PriceEstimate.where(estimate_of: treasure)
- if klass && value.class < Base && reflection = klass.reflect_on_association(column.to_sym)
- if reflection.polymorphic?
- queries << build(table[reflection.foreign_type], value.class.base_class)
+ if klass && reflection = klass.reflect_on_association(column.to_sym)
+ if reflection.polymorphic? && base_class = polymorphic_base_class_from_value(value)
+ queries << build(table[reflection.foreign_type], base_class)
end
column = reflection.foreign_key
@@ -56,6 +67,18 @@ module ActiveRecord
queries
end
+ def self.polymorphic_base_class_from_value(value)
+ case value
+ when Relation
+ value.klass.base_class
+ when Array
+ val = value.compact.first
+ val.class.base_class if val.is_a?(Base)
+ when Base
+ value.class.base_class
+ end
+ end
+
def self.references(attributes)
attributes.map do |key, value|
if value.is_a?(Hash)
@@ -67,44 +90,36 @@ module ActiveRecord
end.compact
end
+ # Define how a class is converted to Arel nodes when passed to +where+.
+ # The handler can be any object that responds to +call+, and will be used
+ # for any value that +===+ the class given. For example:
+ #
+ # MyCustomDateRange = Struct.new(:start, :end)
+ # handler = proc do |column, range|
+ # Arel::Nodes::Between.new(column,
+ # Arel::Nodes::And.new([range.start, range.end])
+ # )
+ # end
+ # ActiveRecord::PredicateBuilder.register_handler(MyCustomDateRange, handler)
+ def self.register_handler(klass, handler)
+ @handlers.unshift([klass, handler])
+ end
+
+ register_handler(BasicObject, ->(attribute, value) { attribute.eq(value) })
+ # FIXME: I think we need to deprecate this behavior
+ register_handler(Class, ->(attribute, value) { attribute.eq(value.name) })
+ register_handler(Base, ->(attribute, value) { attribute.eq(value.id) })
+ register_handler(Range, ->(attribute, value) { attribute.in(value) })
+ register_handler(Relation, RelationHandler.new)
+ register_handler(Array, ArrayHandler.new)
+
private
def self.build(attribute, value)
- case value
- when Array
- values = value.to_a.map {|x| x.is_a?(Base) ? x.id : x}
- ranges, values = values.partition {|v| v.is_a?(Range)}
-
- values_predicate = if values.include?(nil)
- values = values.compact
-
- case values.length
- when 0
- attribute.eq(nil)
- when 1
- attribute.eq(values.first).or(attribute.eq(nil))
- else
- attribute.in(values).or(attribute.eq(nil))
- end
- else
- attribute.in(values)
- end
+ handler_for(value).call(attribute, value)
+ end
- array_predicates = ranges.map { |range| attribute.in(range) }
- array_predicates << values_predicate
- array_predicates.inject { |composite, predicate| composite.or(predicate) }
- when ActiveRecord::Relation
- value = value.select(value.klass.arel_table[value.klass.primary_key]) if value.select_values.empty?
- attribute.in(value.arel.ast)
- when Range
- attribute.in(value)
- when ActiveRecord::Base
- attribute.eq(value.id)
- when Class
- # FIXME: I think we need to deprecate this behavior
- attribute.eq(value.name)
- else
- attribute.eq(value)
- end
+ def self.handler_for(object)
+ @handlers.detect { |klass, _| klass === object }.last
end
end
end
diff --git a/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb
new file mode 100644
index 0000000000..2f6c34ac08
--- /dev/null
+++ b/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb
@@ -0,0 +1,29 @@
+module ActiveRecord
+ class PredicateBuilder
+ class ArrayHandler # :nodoc:
+ def call(attribute, value)
+ values = value.map { |x| x.is_a?(Base) ? x.id : x }
+ ranges, values = values.partition { |v| v.is_a?(Range) }
+
+ values_predicate = if values.include?(nil)
+ values = values.compact
+
+ case values.length
+ when 0
+ attribute.eq(nil)
+ when 1
+ attribute.eq(values.first).or(attribute.eq(nil))
+ else
+ attribute.in(values).or(attribute.eq(nil))
+ end
+ else
+ attribute.in(values)
+ end
+
+ array_predicates = ranges.map { |range| attribute.in(range) }
+ array_predicates << values_predicate
+ array_predicates.inject { |composite, predicate| composite.or(predicate) }
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/relation/predicate_builder/relation_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/relation_handler.rb
new file mode 100644
index 0000000000..618fa3cdd9
--- /dev/null
+++ b/activerecord/lib/active_record/relation/predicate_builder/relation_handler.rb
@@ -0,0 +1,13 @@
+module ActiveRecord
+ class PredicateBuilder
+ class RelationHandler # :nodoc:
+ def call(attribute, value)
+ if value.select_values.empty?
+ value = value.select(value.klass.arel_table[value.klass.primary_key])
+ end
+
+ attribute.in(value.arel.ast)
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb
index c27afb512f..fc653f467c 100644
--- a/activerecord/lib/active_record/relation/query_methods.rb
+++ b/activerecord/lib/active_record/relation/query_methods.rb
@@ -37,6 +37,8 @@ module ActiveRecord
def not(opts, *rest)
where_value = @scope.send(:build_where, opts, rest).map do |rel|
case rel
+ when NilClass
+ raise ArgumentError, 'Invalid argument for .where.not(), got nil.'
when Arel::Nodes::In
Arel::Nodes::NotIn.new(rel.left, rel.right)
when Arel::Nodes::Equality
@@ -100,6 +102,14 @@ module ActiveRecord
# firing an additional query. This will often result in a
# performance improvement over a simple +join+.
#
+ # You can also specify multiple relationships, like this:
+ #
+ # users = User.includes(:address, :friends)
+ #
+ # Loading nested relationships is possible using a Hash:
+ #
+ # users = User.includes(:address, friends: [:address, :followers])
+ #
# === conditions
#
# If you want to add conditions to your included models you'll have
@@ -111,14 +121,15 @@ module ActiveRecord
#
# User.includes(:posts).where('posts.name = ?', 'example').references(:posts)
def includes(*args)
- check_if_method_has_arguments!("includes", args)
+ check_if_method_has_arguments!(:includes, args)
spawn.includes!(*args)
end
def includes!(*args) # :nodoc:
- args.reject! {|a| a.blank? }
+ args.reject!(&:blank?)
+ args.flatten!
- self.includes_values = (includes_values + args).flatten.uniq
+ self.includes_values |= args
self
end
@@ -129,7 +140,7 @@ module ActiveRecord
# FROM "users" LEFT OUTER JOIN "posts" ON "posts"."user_id" =
# "users"."id"
def eager_load(*args)
- check_if_method_has_arguments!("eager_load", args)
+ check_if_method_has_arguments!(:eager_load, args)
spawn.eager_load!(*args)
end
@@ -143,7 +154,7 @@ module ActiveRecord
# User.preload(:posts)
# => SELECT "posts".* FROM "posts" WHERE "posts"."user_id" IN (1, 2, 3)
def preload(*args)
- check_if_method_has_arguments!("preload", args)
+ check_if_method_has_arguments!(:preload, args)
spawn.preload!(*args)
end
@@ -161,14 +172,15 @@ module ActiveRecord
# User.includes(:posts).where("posts.name = 'foo'").references(:posts)
# # => Query now knows the string references posts, so adds a JOIN
def references(*args)
- check_if_method_has_arguments!("references", args)
+ check_if_method_has_arguments!(:references, args)
spawn.references!(*args)
end
def references!(*args) # :nodoc:
args.flatten!
+ args.map!(&:to_s)
- self.references_values = (references_values + args.map!(&:to_s)).uniq
+ self.references_values |= args
self
end
@@ -221,7 +233,9 @@ module ActiveRecord
end
def select!(*fields) # :nodoc:
- self.select_values += fields.flatten
+ fields.flatten!
+
+ self.select_values += fields
self
end
@@ -241,7 +255,7 @@ module ActiveRecord
# 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, ...>]
def group(*args)
- check_if_method_has_arguments!("group", args)
+ check_if_method_has_arguments!(:group, args)
spawn.group!(*args)
end
@@ -272,24 +286,14 @@ module ActiveRecord
# User.order(:name, email: :desc)
# => SELECT "users".* FROM "users" ORDER BY "users"."name" ASC, "users"."email" DESC
def order(*args)
- check_if_method_has_arguments!("order", args)
+ check_if_method_has_arguments!(:order, args)
spawn.order!(*args)
end
def order!(*args) # :nodoc:
- args.flatten!
- validate_order_args args
+ preprocess_order_args(args)
- references = args.reject { |arg| Arel::Node === arg }
- references.map! { |arg| arg =~ /^([a-zA-Z]\w*)\.(\w+)/ && $1 }.compact!
- references!(references) if references.any?
-
- # if a symbol is given we prepend the quoted table name
- args = args.map { |arg|
- arg.is_a?(Symbol) ? "#{quoted_table_name}.#{arg} ASC" : arg
- }
-
- self.order_values = args + self.order_values
+ self.order_values += args
self
end
@@ -301,15 +305,14 @@ module ActiveRecord
#
# User.order('email DESC').reorder('id ASC').order('name ASC')
#
- # generates a query with 'ORDER BY name ASC, id ASC'.
+ # generates a query with 'ORDER BY id ASC, name ASC'.
def reorder(*args)
- check_if_method_has_arguments!("reorder", args)
+ check_if_method_has_arguments!(:reorder, args)
spawn.reorder!(*args)
end
def reorder!(*args) # :nodoc:
- args.flatten!
- validate_order_args args
+ preprocess_order_args(args)
self.reordering_value = true
self.order_values = args
@@ -340,23 +343,27 @@ module ActiveRecord
# User.where(name: "John", active: true).unscope(where: :name)
# == User.where(active: true)
#
- # This method is applied before the default_scope is applied. So the conditions
- # specified in default_scope will not be removed.
+ # This method is similar to <tt>except</tt>, but unlike
+ # <tt>except</tt>, it persists across merges:
#
- # Note that this method is more generalized than ActiveRecord::SpawnMethods#except
- # because #except will only affect a particular relation's values. It won't wipe
- # the order, grouping, etc. when that relation is merged. For example:
+ # User.order('email').merge(User.except(:order))
+ # == User.order('email')
#
- # Post.comments.except(:order)
+ # User.order('email').merge(User.unscope(:order))
+ # == User.all
+ #
+ # This means it can be used in association definitions:
+ #
+ # has_many :comments, -> { unscope where: :trashed }
#
- # will still have an order if it comes from the default_scope on Comment.
def unscope(*args)
- check_if_method_has_arguments!("unscope", args)
+ check_if_method_has_arguments!(:unscope, args)
spawn.unscope!(*args)
end
def unscope!(*args) # :nodoc:
args.flatten!
+ self.unscope_values += args
args.each do |scope|
case scope
@@ -393,8 +400,12 @@ module ActiveRecord
# 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
def joins(*args)
- check_if_method_has_arguments!("joins", args)
- spawn.joins!(*args.compact.flatten)
+ check_if_method_has_arguments!(:joins, args)
+
+ args.compact!
+ args.flatten!
+
+ spawn.joins!(*args)
end
def joins!(*args) # :nodoc:
@@ -421,7 +432,7 @@ module ActiveRecord
# === string
#
# A single string, without additional arguments, is passed to the query
- # constructor as a SQL fragment, and used in the where clause of the query.
+ # constructor as an SQL fragment, and used in the where clause of the query.
#
# Client.where("orders_count = '2'")
# # SELECT * from clients where orders_count = '2';
@@ -551,6 +562,18 @@ module ActiveRecord
end
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
+ #
+ # 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.
+ def rewhere(conditions)
+ unscope(where: conditions.keys).where(conditions)
+ end
+
# Allows to specify a HAVING clause. Note that you can't use HAVING
# without also specifying a GROUP clause.
#
@@ -613,12 +636,11 @@ module ActiveRecord
self
end
- # Returns a chainable relation with zero records, specifically an
- # instance of the <tt>ActiveRecord::NullRelation</tt> class.
+ # Returns a chainable relation with zero records.
#
- # The returned <tt>ActiveRecord::NullRelation</tt> inherits from Relation and implements the
- # Null Object pattern. It is an object with defined null behavior and always returns an empty
- # array of records without querying the database.
+ # The returned relation implements the Null Object pattern. It is an
+ # object with defined null behavior and always returns an empty array of
+ # records without querying the database.
#
# Any subsequent condition chained to the returned relation will continue
# generating an empty relation and will not fire any query to the database.
@@ -638,7 +660,7 @@ module ActiveRecord
# when 'Reviewer'
# Post.published
# when 'Bad User'
- # Post.none # => returning [] instead breaks the previous code
+ # Post.none # It can't be chained if [] is returned.
# end
# end
#
@@ -690,7 +712,7 @@ 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:
#
@@ -776,9 +798,10 @@ module ActiveRecord
end
def extending!(*modules, &block) # :nodoc:
- modules << Module.new(&block) if block_given?
+ modules << Module.new(&block) if block
+ modules.flatten!
- self.extending_values += modules.flatten
+ self.extending_values += modules
extend(*extending_values) if extending_values.any?
self
@@ -798,23 +821,23 @@ module ActiveRecord
# Returns the Arel object associated with the relation.
def arel
- @arel ||= with_default_scope.build_arel
+ @arel ||= build_arel
end
# Like #arel, but ignores the default scope of the model.
def build_arel
arel = Arel::SelectManager.new(table.engine, table)
- build_joins(arel, joins_values) unless joins_values.empty?
+ build_joins(arel, joins_values.flatten) unless joins_values.empty?
collapse_wheres(arel, (where_values - [''])) #TODO: Add uniq with real value comparison / ignore uniqs that have binds
- arel.having(*having_values.uniq.reject{|h| h.blank?}) unless having_values.empty?
+ arel.having(*having_values.uniq.reject(&:blank?)) unless having_values.empty?
arel.take(connection.sanitize_limit(limit_value)) if limit_value
arel.skip(offset_value.to_i) if offset_value
- arel.group(*group_values.uniq.reject{|g| g.blank?}) unless group_values.empty?
+ arel.group(*group_values.uniq.reject(&:blank?)) unless group_values.empty?
build_order(arel)
@@ -853,7 +876,7 @@ module ActiveRecord
where_values.reject! do |rel|
case rel
- when Arel::Nodes::In, Arel::Nodes::Equality
+ when Arel::Nodes::In, Arel::Nodes::NotIn, Arel::Nodes::Equality, Arel::Nodes::NotEqual
subrelation = (rel.left.kind_of?(Arel::Attributes::Attribute) ? rel.left : rel.right)
subrelation.name.to_sym == target_value_sym
else
@@ -863,13 +886,11 @@ module ActiveRecord
end
def custom_join_ast(table, joins)
- joins = joins.reject { |join| join.blank? }
+ joins = joins.reject(&:blank?)
return [] if joins.empty?
- @implicit_readonly = true
-
- joins.map do |join|
+ joins.map! do |join|
case join
when Array
join = Arel.sql(join.join(' ')) if array_of_strings?(join)
@@ -881,22 +902,30 @@ module ActiveRecord
end
def collapse_wheres(arel, wheres)
- equalities = wheres.grep(Arel::Nodes::Equality)
-
- arel.where(Arel::Nodes::And.new(equalities)) unless equalities.empty?
-
- (wheres - equalities).each do |where|
+ predicates = wheres.map do |where|
+ next where if ::Arel::Nodes::Equality === where
where = Arel.sql(where) if String === where
- arel.where(Arel::Nodes::Grouping.new(where))
+ Arel::Nodes::Grouping.new(where)
end
+
+ arel.where(Arel::Nodes::And.new(predicates)) if predicates.present?
end
def build_where(opts, other = [])
case opts
when String, Array
+ #TODO: Remove duplication with: /activerecord/lib/active_record/sanitization.rb:113
+ values = Hash === other.first ? other.first.values : other
+
+ values.grep(ActiveRecord::Relation) do |rel|
+ self.bind_values += rel.bind_values
+ end
+
[@klass.send(:sanitize_sql, other.empty? ? opts : ([opts] + other))]
when Hash
temp_opts = opts.dup
+ opts = PredicateBuilder.resolve_column_aliases(klass, opts)
+ attributes = @klass.send(:expand_hash_conditions_for_aggregates, opts)
create_binds(temp_opts)
temp_opts = substitute_opts(temp_opts)
@@ -941,6 +970,7 @@ module ActiveRecord
case opts
when Relation
name ||= 'subquery'
+ self.bind_values = opts.bind_values + self.bind_values
opts.arel.as(name.to_s)
else
opts
@@ -954,7 +984,7 @@ module ActiveRecord
:string_join
when Hash, Symbol, Array
:association_join
- when ActiveRecord::Associations::JoinDependency::JoinAssociation
+ when ActiveRecord::Associations::JoinDependency
:stashed_join
when Arel::Nodes::Join
:join_node
@@ -966,7 +996,7 @@ module ActiveRecord
association_joins = buckets[:association_join] || []
stashed_association_joins = buckets[:stashed_join] || []
join_nodes = (buckets[:join_node] || []).uniq
- string_joins = (buckets[:string_join] || []).map { |x| x.strip }.uniq
+ string_joins = (buckets[:string_join] || []).map(&:strip).uniq
join_list = join_nodes + custom_join_ast(manager, string_joins)
@@ -976,24 +1006,20 @@ module ActiveRecord
join_list
)
- join_dependency.graft(*stashed_association_joins)
+ joins = join_dependency.join_constraints stashed_association_joins
- @implicit_readonly = true unless association_joins.empty? && stashed_association_joins.empty?
+ joins.each { |join| manager.from(join) }
- # FIXME: refactor this to build an AST
- join_dependency.join_associations.each do |association|
- association.join_to(manager, self)
- end
-
- manager.join_sources.concat join_list
+ manager.join_sources.concat(join_list)
manager
end
def build_select(arel, selects)
- unless selects.empty?
- @implicit_readonly = false
+ if !selects.empty?
arel.project(*selects)
+ elsif from_value
+ arel.project(Arel.star)
else
arel.project(@klass.arel_table[Arel.star])
end
@@ -1007,16 +1033,10 @@ module ActiveRecord
when Arel::Nodes::Ordering
o.reverse
when String
- o.to_s.split(',').collect do |s|
+ o.to_s.split(',').map! do |s|
s.strip!
s.gsub!(/\sasc\Z/i, ' DESC') || s.gsub!(/\sdesc\Z/i, ' ASC') || s.concat(' DESC')
end
- when Symbol
- { o => :desc }
- when Hash
- o.each_with_object({}) do |(field, dir), memo|
- memo[field] = (dir == :asc ? :desc : :asc )
- end
else
o
end
@@ -1024,35 +1044,48 @@ module ActiveRecord
end
def array_of_strings?(o)
- o.is_a?(Array) && o.all?{|obj| obj.is_a?(String)}
+ o.is_a?(Array) && o.all? { |obj| obj.is_a?(String) }
end
def build_order(arel)
- orders = order_values
+ orders = order_values.uniq
+ orders.reject!(&:blank?)
orders = reverse_sql_order(orders) if reverse_order_value
- orders = orders.uniq.reject(&:blank?).flat_map do |order|
- case order
- when Symbol
- table[order].asc
- when Hash
- order.map { |field, dir| table[field].send(dir) }
- else
- order
- end
- end
-
arel.order(*orders) unless orders.empty?
end
def validate_order_args(args)
- args.select { |a| Hash === a }.each do |h|
+ args.grep(Hash) do |h|
unless (h.values - [:asc, :desc]).empty?
raise ArgumentError, 'Direction should be :asc or :desc'
end
end
end
+ def preprocess_order_args(order_args)
+ order_args.flatten!
+ validate_order_args(order_args)
+
+ references = order_args.grep(String)
+ references.map! { |arg| arg =~ /^([a-zA-Z]\w*)\.(\w+)/ && $1 }.compact!
+ references!(references) if references.any?
+
+ # if a symbol is given we prepend the quoted table name
+ order_args.map! do |arg|
+ case arg
+ when Symbol
+ table[arg].asc
+ when Hash
+ arg.map { |field, dir|
+ table[field].send(dir)
+ }
+ else
+ arg
+ end
+ end.flatten!
+ end
+
# Checks to make sure that the arguments are not blank. Note that if some
# blank-like object were initially passed into the query method, then this
# method will not raise an error.
diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb
index de784f9f57..2552cbd234 100644
--- a/activerecord/lib/active_record/relation/spawn_methods.rb
+++ b/activerecord/lib/active_record/relation/spawn_methods.rb
@@ -64,8 +64,7 @@ module ActiveRecord
private
def relation_with(values) # :nodoc:
- result = Relation.new(klass, table, values)
- result.default_scoped = default_scoped
+ result = Relation.create(klass, table, values)
result.extend(*extending_values) if extending_values.any?
result
end
diff --git a/activerecord/lib/active_record/result.rb b/activerecord/lib/active_record/result.rb
index bea195e9b8..469451e2f4 100644
--- a/activerecord/lib/active_record/result.rb
+++ b/activerecord/lib/active_record/result.rb
@@ -3,11 +3,36 @@ module ActiveRecord
# This class encapsulates a Result returned from calling +exec_query+ on any
# database connection adapter. For example:
#
- # x = ActiveRecord::Base.connection.exec_query('SELECT * FROM foo')
- # x # => #<ActiveRecord::Result:0xdeadbeef>
+ # result = ActiveRecord::Base.connection.exec_query('SELECT id, title, body FROM posts')
+ # result # => #<ActiveRecord::Result:0xdeadbeef>
+ #
+ # # Get the column names of the result:
+ # result.columns
+ # # => ["id", "title", "body"]
+ #
+ # # Get the record values of the result:
+ # result.rows
+ # # => [[1, "title_1", "body_1"],
+ # [2, "title_2", "body_2"],
+ # ...
+ # ]
+ #
+ # # Get an array of hashes representing the result (column => value):
+ # result.to_hash
+ # # => [{"id" => 1, "title" => "title_1", "body" => "body_1"},
+ # {"id" => 2, "title" => "title_2", "body" => "body_2"},
+ # ...
+ # ]
+ #
+ # # ActiveRecord::Result also includes Enumerable.
+ # result.each do |row|
+ # puts row['title'] + " " + row['body']
+ # end
class Result
include Enumerable
+ IDENTITY_TYPE = Class.new { def type_cast(v); v; end }.new # :nodoc:
+
attr_reader :columns, :rows, :column_types
def initialize(columns, rows, column_types = {})
@@ -17,8 +42,20 @@ module ActiveRecord
@column_types = column_types
end
+ def identity_type # :nodoc:
+ IDENTITY_TYPE
+ end
+
+ def column_type(name)
+ @column_types[name] || identity_type
+ end
+
def each
- hash_rows.each { |row| yield row }
+ if block_given?
+ hash_rows.each { |row| yield row }
+ else
+ hash_rows.to_enum
+ end
end
def to_hash
@@ -46,12 +83,14 @@ module ActiveRecord
end
def initialize_copy(other)
- @columns = columns.dup
- @rows = rows.dup
- @hash_rows = nil
+ @columns = columns.dup
+ @rows = rows.dup
+ @column_types = column_types.dup
+ @hash_rows = nil
end
private
+
def hash_rows
@hash_rows ||=
begin
@@ -59,7 +98,21 @@ module ActiveRecord
# used as keys in ActiveRecord::Base's @attributes hash
columns = @columns.map { |c| c.dup.freeze }
@rows.map { |row|
- Hash[columns.zip(row)]
+ # In the past we used Hash[columns.zip(row)]
+ # though elegant, the verbose way is much more efficient
+ # both time and memory wise cause it avoids a big array allocation
+ # this method is called a lot and needs to be micro optimised
+ hash = {}
+
+ index = 0
+ length = columns.length
+
+ while index < length
+ hash[columns[index]] = row[index]
+ index += 1
+ end
+
+ hash
}
end
end
diff --git a/activerecord/lib/active_record/runtime_registry.rb b/activerecord/lib/active_record/runtime_registry.rb
index 63e6738622..9d605b826a 100644
--- a/activerecord/lib/active_record/runtime_registry.rb
+++ b/activerecord/lib/active_record/runtime_registry.rb
@@ -13,5 +13,10 @@ module ActiveRecord
extend ActiveSupport::PerThreadRegistry
attr_accessor :connection_handler, :sql_runtime, :connection_id
+
+ [:connection_handler, :sql_runtime, :connection_id].each do |val|
+ class_eval %{ def self.#{val}; instance.#{val}; end }, __FILE__, __LINE__
+ class_eval %{ def self.#{val}=(x); instance.#{val}=x; end }, __FILE__, __LINE__
+ end
end
end
diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb
index 0ed97b66d6..dacaec26b7 100644
--- a/activerecord/lib/active_record/sanitization.rb
+++ b/activerecord/lib/active_record/sanitization.rb
@@ -3,8 +3,8 @@ module ActiveRecord
extend ActiveSupport::Concern
module ClassMethods
- def quote_value(value, column = nil) #:nodoc:
- connection.quote(value,column)
+ def quote_value(value, column) #:nodoc:
+ connection.quote(value, column)
end
# Used to sanitize objects before they're used in an SQL SELECT statement. Delegates to <tt>connection.quote</tt>.
@@ -86,6 +86,7 @@ module ActiveRecord
# { address: Address.new("123 abc st.", "chicago") }
# # => "address_street='123 abc st.' and address_city='chicago'"
def sanitize_sql_hash_for_conditions(attrs, default_table_name = self.table_name)
+ attrs = PredicateBuilder.resolve_column_aliases self, attrs
attrs = expand_hash_conditions_for_aggregates(attrs)
table = Arel::Table.new(table_name, arel_engine).alias(default_table_name)
@@ -99,8 +100,9 @@ module ActiveRecord
# { status: nil, group_id: 1 }
# # => "status = NULL , group_id = 1"
def sanitize_sql_hash_for_assignment(attrs, table)
+ c = connection
attrs.map do |attr, value|
- "#{connection.quote_table_name_for_assignment(table, attr)} = #{quote_bound_value(value)}"
+ "#{c.quote_table_name_for_assignment(table, attr)} = #{quote_bound_value(value, c, columns_hash[attr.to_s])}"
end.join(', ')
end
@@ -126,7 +128,17 @@ module ActiveRecord
raise_if_bind_arity_mismatch(statement, statement.count('?'), values.size)
bound = values.dup
c = connection
- statement.gsub('?') { quote_bound_value(bound.shift, c) }
+ statement.gsub('?') do
+ replace_bind_variable(bound.shift, c)
+ end
+ end
+
+ def replace_bind_variable(value, c = connection) #:nodoc:
+ if ActiveRecord::Relation === value
+ value.to_sql
+ else
+ quote_bound_value(value, c)
+ end
end
def replace_named_bind_variables(statement, bind_vars) #:nodoc:
@@ -134,15 +146,17 @@ module ActiveRecord
if $1 == ':' # skip postgresql casts
$& # return the whole match
elsif bind_vars.include?(match = $2.to_sym)
- quote_bound_value(bind_vars[match])
+ replace_bind_variable(bind_vars[match])
else
raise PreparedStatementInvalid, "missing value for :#{match} in #{statement}"
end
end
end
- def quote_bound_value(value, c = connection) #:nodoc:
- if value.respond_to?(:map) && !value.acts_like?(:string)
+ def quote_bound_value(value, c = connection, column = nil) #:nodoc:
+ if column
+ c.quote(value, column)
+ elsif value.respond_to?(:map) && !value.acts_like?(:string)
if value.respond_to?(:empty?) && value.empty?
c.quote(nil)
else
diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb
index 1181cc739e..e055d571ab 100644
--- a/activerecord/lib/active_record/schema_dumper.rb
+++ b/activerecord/lib/active_record/schema_dumper.rb
@@ -17,9 +17,19 @@ module ActiveRecord
cattr_accessor :ignore_tables
@@ignore_tables = []
- def self.dump(connection=ActiveRecord::Base.connection, stream=STDOUT)
- new(connection).dump(stream)
- stream
+ class << self
+ def dump(connection=ActiveRecord::Base.connection, stream=STDOUT, config = ActiveRecord::Base)
+ new(connection, generate_options(config)).dump(stream)
+ stream
+ end
+
+ private
+ def generate_options(config)
+ {
+ table_name_prefix: config.table_name_prefix,
+ table_name_suffix: config.table_name_suffix
+ }
+ end
end
def dump(stream)
@@ -32,10 +42,11 @@ module ActiveRecord
private
- def initialize(connection)
+ def initialize(connection, options = {})
@connection = connection
@types = @connection.native_database_types
@version = Migrator::current_version rescue nil
+ @options = options
end
def header(stream)
@@ -112,6 +123,7 @@ HEADER
tbl.print %Q(, primary_key: "#{pk}")
elsif pkcol.sql_type == 'uuid'
tbl.print ", id: :uuid"
+ tbl.print %Q(, default: "#{pkcol.default_function}") if pkcol.default_function
end
else
tbl.print ", id: false"
@@ -201,7 +213,7 @@ HEADER
end
def remove_prefix_and_suffix(table)
- table.gsub(/^(#{ActiveRecord::Base.table_name_prefix})(.+)(#{ActiveRecord::Base.table_name_suffix})$/, "\\2")
+ table.gsub(/^(#{@options[:table_name_prefix]})(.+)(#{@options[:table_name_suffix]})$/, "\\2")
end
end
end
diff --git a/activerecord/lib/active_record/schema_migration.rb b/activerecord/lib/active_record/schema_migration.rb
index 6077144265..a9d164e366 100644
--- a/activerecord/lib/active_record/schema_migration.rb
+++ b/activerecord/lib/active_record/schema_migration.rb
@@ -4,31 +4,37 @@ require 'active_record/base'
module ActiveRecord
class SchemaMigration < ActiveRecord::Base
+ class << self
- def self.table_name
- "#{Base.table_name_prefix}schema_migrations#{Base.table_name_suffix}"
- end
+ def table_name
+ "#{table_name_prefix}#{ActiveRecord::Base.schema_migrations_table_name}#{table_name_suffix}"
+ end
- def self.index_name
- "#{Base.table_name_prefix}unique_schema_migrations#{Base.table_name_suffix}"
- end
+ def index_name
+ "#{table_name_prefix}unique_#{ActiveRecord::Base.schema_migrations_table_name}#{table_name_suffix}"
+ end
- def self.create_table(limit=nil)
- unless connection.table_exists?(table_name)
- version_options = {null: false}
- version_options[:limit] = limit if limit
+ def table_exists?
+ connection.table_exists?(table_name)
+ end
+
+ def create_table(limit=nil)
+ unless table_exists?
+ version_options = {null: false}
+ version_options[:limit] = limit if limit
- connection.create_table(table_name, id: false) do |t|
- t.column :version, :string, version_options
+ connection.create_table(table_name, id: false) do |t|
+ t.column :version, :string, version_options
+ end
+ connection.add_index table_name, :version, unique: true, name: index_name
end
- connection.add_index table_name, :version, unique: true, name: index_name
end
- end
- def self.drop_table
- if connection.table_exists?(table_name)
- connection.remove_index table_name, name: index_name
- connection.drop_table(table_name)
+ def drop_table
+ if table_exists?
+ connection.remove_index table_name, name: index_name
+ connection.drop_table(table_name)
+ end
end
end
diff --git a/activerecord/lib/active_record/scoping/default.rb b/activerecord/lib/active_record/scoping/default.rb
index d37d33d552..01fec31544 100644
--- a/activerecord/lib/active_record/scoping/default.rb
+++ b/activerecord/lib/active_record/scoping/default.rb
@@ -8,14 +8,6 @@ module ActiveRecord
class_attribute :default_scopes, instance_writer: false, instance_predicate: false
self.default_scopes = []
-
- def self.default_scopes?
- ActiveSupport::Deprecation.warn(
- "#default_scopes? is deprecated. Do something like #default_scopes.empty? instead."
- )
-
- !!self.default_scopes
- end
end
module ClassMethods
@@ -91,12 +83,11 @@ module ActiveRecord
scope = Proc.new if block_given?
if scope.is_a?(Relation) || !scope.respond_to?(:call)
- ActiveSupport::Deprecation.warn(
- "Calling #default_scope without a block is deprecated. For example instead " \
+ raise ArgumentError,
+ "Support for calling #default_scope without a block is removed. For example instead " \
"of `default_scope where(color: 'red')`, please use " \
"`default_scope { where(color: 'red') }`. (Alternatively you can just redefine " \
"self.default_scope.)"
- )
end
self.default_scopes += [scope]
@@ -109,11 +100,7 @@ module ActiveRecord
elsif default_scopes.any?
evaluate_default_scope do
default_scopes.inject(relation) do |default_scope, scope|
- if !scope.is_a?(Relation) && scope.respond_to?(:call)
- default_scope.merge(unscoped { scope.call })
- else
- default_scope.merge(scope)
- end
+ default_scope.merge(unscoped { scope.call })
end
end
end
diff --git a/activerecord/lib/active_record/scoping/named.rb b/activerecord/lib/active_record/scoping/named.rb
index da73bead32..2a5718f388 100644
--- a/activerecord/lib/active_record/scoping/named.rb
+++ b/activerecord/lib/active_record/scoping/named.rb
@@ -25,22 +25,18 @@ module ActiveRecord
if current_scope
current_scope.clone
else
- scope = relation
- scope.default_scoped = true
- scope
+ default_scoped
end
end
+ def default_scoped # :nodoc:
+ relation.merge(build_default_scope)
+ end
+
# Collects attributes from scopes that should be applied when creating
# an AR instance for the particular class this is called on.
def scope_attributes # :nodoc:
- if current_scope
- current_scope.scope_for_create
- else
- scope = relation
- scope.default_scoped = true
- scope.scope_for_create
- end
+ all.scope_for_create
end
# Are there default attributes associated with this scope?
@@ -145,26 +141,9 @@ module ActiveRecord
def scope(name, body, &block)
extension = Module.new(&block) if block
- # Check body.is_a?(Relation) to prevent the relation actually being
- # loaded by respond_to?
- if body.is_a?(Relation) || !body.respond_to?(:call)
- ActiveSupport::Deprecation.warn(
- "Using #scope without passing a callable object is deprecated. For " \
- "example `scope :red, where(color: 'red')` should be changed to " \
- "`scope :red, -> { where(color: 'red') }`. There are numerous gotchas " \
- "in the former usage and it makes the implementation more complicated " \
- "and buggy. (If you prefer, you can just define a class method named " \
- "`self.red`.)"
- )
- end
-
singleton_class.send(:define_method, name) do |*args|
- if body.respond_to?(:call)
- scope = all.scoping { body.call(*args) }
- scope = scope.extending(extension) if extension
- else
- scope = body
- end
+ scope = all.scoping { body.call(*args) }
+ scope = scope.extending(extension) if extension
scope || all
end
diff --git a/activerecord/lib/active_record/store.rb b/activerecord/lib/active_record/store.rb
index a610f479f2..79a6ccbda0 100644
--- a/activerecord/lib/active_record/store.rb
+++ b/activerecord/lib/active_record/store.rb
@@ -15,6 +15,11 @@ module ActiveRecord
# You can set custom coder to encode/decode your serialized attributes to/from different formats.
# JSON, YAML, Marshal are supported out of the box. Generally it can be any wrapper that provides +load+ and +dump+.
#
+ # NOTE - If you are using PostgreSQL specific columns like +hstore+ or +json+ there is no need for
+ # the serialization provided by +store+. Simply use +store_accessor+ instead to generate
+ # the accessor methods. Be aware that these columns use a string keyed hash and do not allow access
+ # using a symbol.
+ #
# Examples:
#
# class User < ActiveRecord::Base
@@ -86,6 +91,9 @@ module ActiveRecord
end
end
+ # assign new store attribute and create new hash to ensure that each class in the hierarchy
+ # has its own hash of stored attributes.
+ self.stored_attributes = {} if self.stored_attributes.blank?
self.stored_attributes[store_attribute] ||= []
self.stored_attributes[store_attribute] |= keys
end
@@ -101,26 +109,58 @@ module ActiveRecord
protected
def read_store_attribute(store_attribute, key)
- attribute = initialize_store_attribute(store_attribute)
- attribute[key]
+ accessor = store_accessor_for(store_attribute)
+ accessor.read(self, store_attribute, key)
end
def write_store_attribute(store_attribute, key, value)
- attribute = initialize_store_attribute(store_attribute)
- if value != attribute[key]
- send :"#{store_attribute}_will_change!"
- attribute[key] = value
- end
+ accessor = store_accessor_for(store_attribute)
+ accessor.write(self, store_attribute, key, value)
end
private
- def initialize_store_attribute(store_attribute)
- attribute = send(store_attribute)
- unless attribute.is_a?(ActiveSupport::HashWithIndifferentAccess)
- attribute = IndifferentCoder.as_indifferent_hash(attribute)
- send :"#{store_attribute}=", attribute
+ def store_accessor_for(store_attribute)
+ @column_types[store_attribute.to_s].accessor
+ end
+
+ class HashAccessor
+ def self.read(object, attribute, key)
+ prepare(object, attribute)
+ object.public_send(attribute)[key]
+ end
+
+ def self.write(object, attribute, key, value)
+ prepare(object, attribute)
+ if value != read(object, attribute, key)
+ object.public_send :"#{attribute}_will_change!"
+ object.public_send(attribute)[key] = value
+ end
+ end
+
+ def self.prepare(object, attribute)
+ object.public_send :"#{attribute}=", {} unless object.send(attribute)
+ end
+ end
+
+ class StringKeyedHashAccessor < HashAccessor
+ def self.read(object, attribute, key)
+ super object, attribute, key.to_s
+ end
+
+ def self.write(object, attribute, key, value)
+ super object, attribute, key.to_s, value
+ end
+ end
+
+ class IndifferentHashAccessor < ActiveRecord::Store::HashAccessor
+ def self.prepare(object, store_attribute)
+ attribute = object.send(store_attribute)
+ unless attribute.is_a?(ActiveSupport::HashWithIndifferentAccess)
+ attribute = IndifferentCoder.as_indifferent_hash(attribute)
+ object.send :"#{store_attribute}=", attribute
+ end
+ attribute
end
- attribute
end
class IndifferentCoder # :nodoc:
@@ -138,7 +178,7 @@ module ActiveRecord
end
def load(yaml)
- self.class.as_indifferent_hash @coder.load(yaml)
+ self.class.as_indifferent_hash(@coder.load(yaml || ''))
end
def self.as_indifferent_hash(obj)
diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb
index 3e8b79c7a0..6ce0495f6f 100644
--- a/activerecord/lib/active_record/tasks/database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/database_tasks.rb
@@ -23,6 +23,7 @@ module ActiveRecord
# * +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:
#
@@ -35,9 +36,8 @@ module ActiveRecord
module DatabaseTasks
extend self
- attr_writer :current_config
- attr_accessor :database_configuration, :migrations_paths, :seed_loader, :db_dir,
- :fixtures_path, :env
+ attr_writer :current_config, :db_dir, :migrations_paths, :fixtures_path, :root, :env, :seed_loader
+ attr_accessor :database_configuration
LOCAL_HOSTS = ['127.0.0.1', 'localhost']
@@ -50,20 +50,36 @@ module ActiveRecord
register_task(/postgresql/, ActiveRecord::Tasks::PostgreSQLDatabaseTasks)
register_task(/sqlite/, ActiveRecord::Tasks::SQLiteDatabaseTasks)
- register_task(/firebird/, ActiveRecord::Tasks::FirebirdDatabaseTasks)
- register_task(/sqlserver/, ActiveRecord::Tasks::SqlserverDatabaseTasks)
- register_task(/(oci|oracle)/, ActiveRecord::Tasks::OracleDatabaseTasks)
+ def db_dir
+ @db_dir ||= Rails.application.config.paths["db"].first
+ end
+
+ def migrations_paths
+ @migrations_paths ||= Rails.application.paths['db/migrate'].to_a
+ end
+
+ def fixtures_path
+ @fixtures_path ||= File.join(root, 'test', 'fixtures')
+ end
+
+ def root
+ @root ||= Rails.root
+ end
+
+ def env
+ @env ||= Rails.env
+ end
+
+ def seed_loader
+ @seed_loader ||= Rails.application
+ end
def current_config(options = {})
options.reverse_merge! :env => env
if options.has_key?(:config)
@current_config = options[:config]
else
- @current_config ||= if ENV['DATABASE_URL']
- database_url_config
- else
- ActiveRecord::Base.configurations[options[:env]]
- end
+ @current_config ||= ActiveRecord::Base.configurations[options[:env]]
end
end
@@ -85,11 +101,7 @@ module ActiveRecord
each_current_configuration(environment) { |configuration|
create configuration
}
- ActiveRecord::Base.establish_connection environment
- end
-
- def create_database_url
- create database_url_config
+ ActiveRecord::Base.establish_connection(environment.to_sym)
end
def drop(*arguments)
@@ -110,10 +122,6 @@ module ActiveRecord
}
end
- def drop_database_url
- drop database_url_config
- end
-
def charset_current(environment = env)
charset ActiveRecord::Base.configurations[environment]
end
@@ -148,8 +156,23 @@ module ActiveRecord
class_for_adapter(configuration['adapter']).new(*arguments).structure_load(filename)
end
+ def load_schema(format = ActiveRecord::Base.schema_format, file = nil)
+ case format
+ when :ruby
+ file ||= File.join(db_dir, "schema.rb")
+ check_schema_file(file)
+ load(file)
+ when :sql
+ file ||= File.join(db_dir, "structure.sql")
+ check_schema_file(file)
+ structure_load(current_config, file)
+ else
+ raise ArgumentError, "unknown format #{format.inspect}"
+ end
+ end
+
def check_schema_file(filename)
- unless File.exists?(filename)
+ unless File.exist?(filename)
message = %{#{filename} doesn't exist yet. Run `rake db:migrate` to create it, then try again.}
message << %{ If you do not intend to use a database, you should instead alter #{Rails.root}/config/application.rb to limit the frameworks that will be loaded.} if defined?(::Rails)
Kernel.abort message
@@ -168,11 +191,6 @@ module ActiveRecord
private
- def database_url_config
- @database_url_config ||=
- ConnectionAdapters::ConnectionSpecification::Resolver.new(ENV["DATABASE_URL"], {}).spec.config.stringify_keys
- end
-
def class_for_adapter(adapter)
key = @tasks.keys.detect { |pattern| adapter[pattern] }
unless key
@@ -183,7 +201,8 @@ module ActiveRecord
def each_current_configuration(environment)
environments = [environment]
- environments << 'test' if environment == 'development'
+ # add test environment only if no RAILS_ENV was specified.
+ environments << 'test' if environment == 'development' && ENV['RAILS_ENV'].nil?
configurations = ActiveRecord::Base.configurations.values_at(*environments)
configurations.compact.each do |configuration|
diff --git a/activerecord/lib/active_record/tasks/firebird_database_tasks.rb b/activerecord/lib/active_record/tasks/firebird_database_tasks.rb
deleted file mode 100644
index 98014a38ea..0000000000
--- a/activerecord/lib/active_record/tasks/firebird_database_tasks.rb
+++ /dev/null
@@ -1,56 +0,0 @@
-module ActiveRecord
- module Tasks # :nodoc:
- class FirebirdDatabaseTasks # :nodoc:
- delegate :connection, :establish_connection, to: ActiveRecord::Base
-
- def initialize(configuration)
- ActiveSupport::Deprecation.warn "This database tasks were deprecated, because this tasks should be served by the 3rd party adapter."
- @configuration = configuration
- end
-
- def create
- $stderr.puts 'sorry, your database adapter is not supported yet, feel free to submit a patch'
- end
-
- def drop
- $stderr.puts 'sorry, your database adapter is not supported yet, feel free to submit a patch'
- end
-
- def purge
- establish_connection(:test)
- connection.recreate_database!
- end
-
- def charset
- $stderr.puts 'sorry, your database adapter is not supported yet, feel free to submit a patch'
- end
-
- def structure_dump(filename)
- set_firebird_env(configuration)
- db_string = firebird_db_string(configuration)
- Kernel.system "isql -a #{db_string} > #{filename}"
- end
-
- def structure_load(filename)
- set_firebird_env(configuration)
- db_string = firebird_db_string(configuration)
- Kernel.system "isql -i #{filename} #{db_string}"
- end
-
- private
-
- def set_firebird_env(config)
- ENV['ISC_USER'] = config['username'].to_s if config['username']
- ENV['ISC_PASSWORD'] = config['password'].to_s if config['password']
- end
-
- def firebird_db_string(config)
- FireRuby::Database.db_string_for(config.symbolize_keys)
- end
-
- def configuration
- @configuration
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb
index 50569d2462..c755831e6d 100644
--- a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb
@@ -134,8 +134,9 @@ IDENTIFIED BY '#{configuration['password']}' WITH GRANT OPTION;
args << "--password=#{configuration['password']}" if configuration['password']
args.concat(['--default-character-set', configuration['encoding']]) if configuration['encoding']
configuration.slice('host', 'port', 'socket').each do |k, v|
- args.concat([ "--#{k}", v ]) if v
+ args.concat([ "--#{k}", v.to_s ]) if v
end
+
args
end
end
diff --git a/activerecord/lib/active_record/tasks/oracle_database_tasks.rb b/activerecord/lib/active_record/tasks/oracle_database_tasks.rb
deleted file mode 100644
index de3aa50e5e..0000000000
--- a/activerecord/lib/active_record/tasks/oracle_database_tasks.rb
+++ /dev/null
@@ -1,45 +0,0 @@
-module ActiveRecord
- module Tasks # :nodoc:
- class OracleDatabaseTasks # :nodoc:
- delegate :connection, :establish_connection, to: ActiveRecord::Base
-
- def initialize(configuration)
- ActiveSupport::Deprecation.warn "This database tasks were deprecated, because this tasks should be served by the 3rd party adapter."
- @configuration = configuration
- end
-
- def create
- $stderr.puts 'sorry, your database adapter is not supported yet, feel free to submit a patch'
- end
-
- def drop
- $stderr.puts 'sorry, your database adapter is not supported yet, feel free to submit a patch'
- end
-
- def purge
- establish_connection(:test)
- connection.structure_drop.split(";\n\n").each { |ddl| connection.execute(ddl) }
- end
-
- def charset
- $stderr.puts 'sorry, your database adapter is not supported yet, feel free to submit a patch'
- end
-
- def structure_dump(filename)
- establish_connection(configuration)
- File.open(filename, "w:utf-8") { |f| f << connection.structure_dump }
- end
-
- def structure_load(filename)
- establish_connection(configuration)
- IO.read(filename).split(";\n\n").each { |ddl| connection.execute(ddl) }
- end
-
- private
-
- def configuration
- @configuration
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb
index 4413330fab..3d02ee07d0 100644
--- a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb
@@ -59,7 +59,7 @@ module ActiveRecord
def structure_load(filename)
set_psql_env
- Kernel.system("psql -q -f #{filename} #{configuration['database']}")
+ Kernel.system("psql -q -f #{Shellwords.escape(filename)} #{configuration['database']}")
end
private
diff --git a/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb b/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb
index de8b16627e..5688931db2 100644
--- a/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb
@@ -3,7 +3,7 @@ module ActiveRecord
class SQLiteDatabaseTasks # :nodoc:
delegate :connection, :establish_connection, to: ActiveRecord::Base
- def initialize(configuration, root = Rails.root)
+ def initialize(configuration, root = ActiveRecord::Tasks::DatabaseTasks.root)
@configuration, @root = configuration, root
end
diff --git a/activerecord/lib/active_record/tasks/sqlserver_database_tasks.rb b/activerecord/lib/active_record/tasks/sqlserver_database_tasks.rb
deleted file mode 100644
index c718ee03a8..0000000000
--- a/activerecord/lib/active_record/tasks/sqlserver_database_tasks.rb
+++ /dev/null
@@ -1,48 +0,0 @@
-require 'shellwords'
-
-module ActiveRecord
- module Tasks # :nodoc:
- class SqlserverDatabaseTasks # :nodoc:
- delegate :connection, :establish_connection, to: ActiveRecord::Base
-
- def initialize(configuration)
- ActiveSupport::Deprecation.warn "This database tasks were deprecated, because this tasks should be served by the 3rd party adapter."
- @configuration = configuration
- end
-
- def create
- $stderr.puts 'sorry, your database adapter is not supported yet, feel free to submit a patch'
- end
-
- def drop
- $stderr.puts 'sorry, your database adapter is not supported yet, feel free to submit a patch'
- end
-
- def purge
- test = configuration.deep_dup
- test_database = test['database']
- test['database'] = 'master'
- establish_connection(test)
- connection.recreate_database!(test_database)
- end
-
- def charset
- $stderr.puts 'sorry, your database adapter is not supported yet, feel free to submit a patch'
- end
-
- def structure_dump(filename)
- Kernel.system("smoscript -s #{configuration['host']} -d #{configuration['database']} -u #{configuration['username']} -p #{configuration['password']} -f #{filename} -A -U")
- end
-
- def structure_load(filename)
- Kernel.system("sqlcmd -S #{configuration['host']} -d #{configuration['database']} -U #{configuration['username']} -P #{configuration['password']} -i #{filename}")
- end
-
- private
-
- def configuration
- @configuration
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/test_case.rb b/activerecord/lib/active_record/test_case.rb
deleted file mode 100644
index 1b4c473bfc..0000000000
--- a/activerecord/lib/active_record/test_case.rb
+++ /dev/null
@@ -1,96 +0,0 @@
-require 'active_support/test_case'
-
-ActiveSupport::Deprecation.warn('ActiveRecord::TestCase is deprecated, please use ActiveSupport::TestCase')
-module ActiveRecord
- # = Active Record Test Case
- #
- # Defines some test assertions to test against SQL queries.
- class TestCase < ActiveSupport::TestCase #:nodoc:
- def teardown
- SQLCounter.clear_log
- end
-
- def assert_date_from_db(expected, actual, message = nil)
- # SybaseAdapter doesn't have a separate column type just for dates,
- # so the time is in the string and incorrectly formatted
- if current_adapter?(:SybaseAdapter)
- assert_equal expected.to_s, actual.to_date.to_s, message
- else
- assert_equal expected.to_s, actual.to_s, message
- end
- end
-
- def assert_sql(*patterns_to_match)
- SQLCounter.clear_log
- yield
- SQLCounter.log_all
- ensure
- failed_patterns = []
- patterns_to_match.each do |pattern|
- failed_patterns << pattern unless SQLCounter.log_all.any?{ |sql| pattern === sql }
- end
- assert failed_patterns.empty?, "Query pattern(s) #{failed_patterns.map{ |p| p.inspect }.join(', ')} not found.#{SQLCounter.log.size == 0 ? '' : "\nQueries:\n#{SQLCounter.log.join("\n")}"}"
- end
-
- def assert_queries(num = 1, options = {})
- ignore_none = options.fetch(:ignore_none) { num == :any }
- SQLCounter.clear_log
- x = yield
- the_log = ignore_none ? SQLCounter.log_all : SQLCounter.log
- if num == :any
- assert_operator the_log.size, :>=, 1, "1 or more queries expected, but none were executed."
- else
- mesg = "#{the_log.size} instead of #{num} queries were executed.#{the_log.size == 0 ? '' : "\nQueries:\n#{the_log.join("\n")}"}"
- assert_equal num, the_log.size, mesg
- end
- x
- end
-
- def assert_no_queries(&block)
- assert_queries(0, :ignore_none => true, &block)
- end
-
- end
-
- class SQLCounter
- class << self
- attr_accessor :ignored_sql, :log, :log_all
- def clear_log; self.log = []; self.log_all = []; end
- end
-
- self.clear_log
-
- self.ignored_sql = [/^PRAGMA/, /^SELECT currval/, /^SELECT CAST/, /^SELECT @@IDENTITY/, /^SELECT @@ROWCOUNT/, /^SAVEPOINT/, /^ROLLBACK TO SAVEPOINT/, /^RELEASE SAVEPOINT/, /^SHOW max_identifier_length/, /^BEGIN/, /^COMMIT/]
-
- # FIXME: this needs to be refactored so specific database can add their own
- # ignored SQL, or better yet, use a different notification for the queries
- # instead examining the SQL content.
- oracle_ignored = [/^select .*nextval/i, /^SAVEPOINT/, /^ROLLBACK TO/, /^\s*select .* from all_triggers/im]
- mysql_ignored = [/^SHOW TABLES/i, /^SHOW FULL FIELDS/]
- postgresql_ignored = [/^\s*select\b.*\bfrom\b.*pg_namespace\b/im, /^\s*select\b.*\battname\b.*\bfrom\b.*\bpg_attribute\b/im, /^SHOW search_path/i]
- sqlite3_ignored = [/^\s*SELECT name\b.*\bFROM sqlite_master/im]
-
- [oracle_ignored, mysql_ignored, postgresql_ignored, sqlite3_ignored].each do |db_ignored_sql|
- ignored_sql.concat db_ignored_sql
- end
-
- attr_reader :ignore
-
- def initialize(ignore = Regexp.union(self.class.ignored_sql))
- @ignore = ignore
- end
-
- def call(name, start, finish, message_id, values)
- sql = values[:sql]
-
- # FIXME: this seems bad. we should probably have a better way to indicate
- # the query was cached
- return if 'CACHE' == values[:name]
-
- self.class.log_all << sql
- self.class.log << sql unless ignore =~ sql
- end
- end
-
- ActiveSupport::Notifications.subscribe('sql.active_record', SQLCounter.new)
-end
diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb
index ae99cff35e..e0541b7681 100644
--- a/activerecord/lib/active_record/timestamp.rb
+++ b/activerecord/lib/active_record/timestamp.rb
@@ -10,9 +10,9 @@ module ActiveRecord
#
# config.active_record.record_timestamps = false
#
- # Timestamps are in the local timezone by default but you can use UTC by setting:
+ # Timestamps are in UTC by default but you can use the local timezone by setting:
#
- # config.active_record.default_timezone = :utc
+ # config.active_record.default_timezone = :local
#
# == Time Zone aware attributes
#
@@ -98,8 +98,8 @@ module ActiveRecord
timestamp_attributes_for_create + timestamp_attributes_for_update
end
- def max_updated_column_timestamp
- if (timestamps = timestamp_attributes_for_update.map { |attr| self[attr] }.compact).present?
+ def max_updated_column_timestamp(timestamp_names = timestamp_attributes_for_update)
+ if (timestamps = timestamp_names.map { |attr| self[attr] }.compact).present?
timestamps.map { |ts| ts.to_time }.max
end
end
diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb
index 77634b40bb..45313b5e75 100644
--- a/activerecord/lib/active_record/transactions.rb
+++ b/activerecord/lib/active_record/transactions.rb
@@ -220,8 +220,8 @@ module ActiveRecord
# after_commit :do_bar, on: :update
# after_commit :do_baz, on: :destroy
#
- # after_commit :do_foo_bar, :on [:create, :update]
- # after_commit :do_bar_baz, :on [:update, :destroy]
+ # after_commit :do_foo_bar, on: [:create, :update]
+ # after_commit :do_bar_baz, on: [:update, :destroy]
#
# Note that transactional fixtures do not play well with this feature. Please
# use the +test_after_commit+ gem to have these hooks fired in tests.
@@ -245,7 +245,7 @@ module ActiveRecord
if options.is_a?(Hash) && options[:on]
assert_valid_transaction_action(options[:on])
options[:if] = Array(options[:if])
- fire_on = Array(options[:on]).map(&:to_sym)
+ fire_on = Array(options[:on])
options[:if] << "transaction_include_any_action?(#{fire_on})"
end
end
@@ -288,25 +288,26 @@ module ActiveRecord
clear_transaction_record_state
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
+ # but call it after the commit of a destroyed object.
def committed! #:nodoc:
run_callbacks :commit if destroyed? || persisted?
ensure
clear_transaction_record_state
end
- # Call the after rollback callbacks. The restore_state argument indicates if the record
+ # Call the +after_rollback+ callbacks. The +force_restore_state+ argument indicates if the record
# state should be rolled back to the beginning or just to the last savepoint.
def rolledback!(force_restore_state = false) #:nodoc:
run_callbacks :rollback
ensure
restore_transaction_record_state(force_restore_state)
+ clear_transaction_record_state
end
- # Add the record to the current transaction so that the :after_rollback and :after_commit callbacks
+ # Add the record to the current transaction so that the +after_rollback+ and +after_commit+ callbacks
# can be called.
def add_to_transaction
if self.class.connection.add_transaction_record(self)
@@ -360,8 +361,8 @@ module ActiveRecord
# Restore the new record state and id of a record that was previously saved by a call to save_record_state.
def restore_transaction_record_state(force = false) #:nodoc:
unless @_start_transaction_state.empty?
- @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1
- if @_start_transaction_state[:level] < 1 || force
+ transaction_level = (@_start_transaction_state[:level] || 0) - 1
+ if transaction_level < 1 || force
restore_state = @_start_transaction_state
was_frozen = restore_state[:frozen?]
@attributes = @attributes.dup if @attributes.frozen?
@@ -374,7 +375,6 @@ module ActiveRecord
@attributes_cache.delete(self.class.primary_key)
end
@attributes.freeze if was_frozen
- @_start_transaction_state.clear
end
end
end
diff --git a/activerecord/lib/active_record/validations/associated.rb b/activerecord/lib/active_record/validations/associated.rb
index 744780d069..b4785d3ba4 100644
--- a/activerecord/lib/active_record/validations/associated.rb
+++ b/activerecord/lib/active_record/validations/associated.rb
@@ -2,7 +2,7 @@ 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?(record.validation_context) }.any?
+ if Array.wrap(value).reject {|r| r.marked_for_destruction? || r.valid?}.any?
record.errors.add(attribute, :invalid, options.merge(:value => value))
end
end
diff --git a/activerecord/lib/active_record/validations/presence.rb b/activerecord/lib/active_record/validations/presence.rb
index 6b14c39686..9a19483da3 100644
--- a/activerecord/lib/active_record/validations/presence.rb
+++ b/activerecord/lib/active_record/validations/presence.rb
@@ -5,7 +5,7 @@ module ActiveRecord
super
attributes.each do |attribute|
next unless record.class.reflect_on_association(attribute)
- associated_records = Array(record.send(attribute))
+ associated_records = Array.wrap(record.send(attribute))
# Superclass validates presence. Ensure present records aren't about to be destroyed.
if associated_records.present? && associated_records.all? { |r| r.marked_for_destruction? }
diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb
index a705d8c2c4..7ebe9dfec0 100644
--- a/activerecord/lib/active_record/validations/uniqueness.rb
+++ b/activerecord/lib/active_record/validations/uniqueness.rb
@@ -7,11 +7,7 @@ module ActiveRecord
"Pass a callable instead: `conditions: -> { where(approved: true) }`"
end
super({ case_sensitive: true }.merge!(options))
- end
-
- # Unfortunately, we have to tie Uniqueness validators to a class.
- def setup(klass)
- @klass = klass
+ @klass = options[:class]
end
def validate_each(record, attribute, value)
@@ -34,7 +30,6 @@ module ActiveRecord
end
protected
-
# The check for an existing value should be run from a class that
# isn't abstract. This means working down from the current class
# (self), to the first non-abstract class. Since classes don't know
@@ -53,10 +48,18 @@ module ActiveRecord
def build_relation(klass, table, attribute, value) #:nodoc:
if reflection = klass.reflect_on_association(attribute)
attribute = reflection.foreign_key
- value = value.attributes[reflection.primary_key_column.name]
+ value = value.attributes[reflection.primary_key_column.name] unless value.nil?
+ end
+
+ attribute_name = attribute.to_s
+
+ # the attribute may be an aliased attribute
+ if klass.attribute_aliases[attribute_name]
+ attribute = klass.attribute_aliases[attribute_name]
+ attribute_name = attribute.to_s
end
- column = klass.columns_hash[attribute.to_s]
+ column = klass.columns_hash[attribute_name]
value = klass.connection.type_cast(value, column)
value = value.to_s[0, column.limit] if value && column.limit && column.text?
@@ -171,11 +174,11 @@ module ActiveRecord
# WHERE title = 'My Post' |
# |
# | # User 2 does the same thing and also
- # | # infers that his title is unique.
+ # | # infers that their title is unique.
# | SELECT * FROM comments
# | WHERE title = 'My Post'
# |
- # # User 1 inserts his comment. |
+ # # User 1 inserts their comment. |
# INSERT INTO comments |
# (title, content) VALUES |
# ('My Post', 'hi!') |
@@ -201,9 +204,9 @@ module ActiveRecord
# 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
- # that the title already exists, and asking him to re-enter the title).
- # This technique is also known as optimistic concurrency control:
- # http://en.wikipedia.org/wiki/Optimistic_concurrency_control.
+ # that the title already exists, and asking them to re-enter the title).
+ # This technique is also known as
+ # {optimistic concurrency control}[http://en.wikipedia.org/wiki/Optimistic_concurrency_control].
#
# The bundled ActiveRecord::ConnectionAdapters distinguish unique index
# constraint errors from other types of database errors by throwing an
diff --git a/activerecord/lib/active_record/version.rb b/activerecord/lib/active_record/version.rb
index de5fd05468..863c3ebe4d 100644
--- a/activerecord/lib/active_record/version.rb
+++ b/activerecord/lib/active_record/version.rb
@@ -1,7 +1,7 @@
module ActiveRecord
# Returns the version of the currently loaded ActiveRecord as a Gem::Version
def self.version
- Gem::Version.new "4.1.0.beta"
+ Gem::Version.new "4.1.0.beta1"
end
module VERSION #:nodoc:
diff --git a/activerecord/lib/rails/generators/active_record.rb b/activerecord/lib/rails/generators/active_record.rb
index c8aa37f275..dc29213235 100644
--- a/activerecord/lib/rails/generators/active_record.rb
+++ b/activerecord/lib/rails/generators/active_record.rb
@@ -1,23 +1,17 @@
require 'rails/generators/named_base'
-require 'rails/generators/migration'
require 'rails/generators/active_model'
+require 'rails/generators/active_record/migration'
require 'active_record'
module ActiveRecord
module Generators # :nodoc:
class Base < Rails::Generators::NamedBase # :nodoc:
- include Rails::Generators::Migration
+ include ActiveRecord::Generators::Migration
# Set the current directory as base for the inherited generators.
def self.base_root
File.dirname(__FILE__)
end
-
- # Implement the required interface for Rails::Generators::Migration.
- def self.next_migration_number(dirname)
- next_migration_number = current_migration_number(dirname) + 1
- ActiveRecord::Migration.next_migration_number(next_migration_number)
- end
end
end
end
diff --git a/activerecord/lib/rails/generators/active_record/migration.rb b/activerecord/lib/rails/generators/active_record/migration.rb
new file mode 100644
index 0000000000..b7418cf42f
--- /dev/null
+++ b/activerecord/lib/rails/generators/active_record/migration.rb
@@ -0,0 +1,18 @@
+require 'rails/generators/migration'
+
+module ActiveRecord
+ module Generators # :nodoc:
+ module Migration
+ extend ActiveSupport::Concern
+ include Rails::Generators::Migration
+
+ module ClassMethods
+ # Implement the required interface for Rails::Generators::Migration.
+ def next_migration_number(dirname)
+ next_migration_number = current_migration_number(dirname) + 1
+ ActiveRecord::Migration.next_migration_number(next_migration_number)
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/adapter_test.rb b/activerecord/test/cases/adapter_test.rb
index e28bb7b6ca..b67e70ec7e 100644
--- a/activerecord/test/cases/adapter_test.rb
+++ b/activerecord/test/cases/adapter_test.rb
@@ -92,7 +92,7 @@ module ActiveRecord
)
end
ensure
- ActiveRecord::Base.establish_connection 'arunit'
+ ActiveRecord::Base.establish_connection :arunit
end
end
end
@@ -173,6 +173,15 @@ module ActiveRecord
end
end
end
+
+ def test_select_all_always_return_activerecord_result
+ result = @connection.select_all "SELECT * FROM posts"
+ assert result.is_a?(ActiveRecord::Result)
+ end
+
+ test "type_to_sql returns a String for unmapped types" do
+ assert_equal "special_db_type", @connection.type_to_sql(:special_db_type)
+ end
end
class AdapterTestWithoutTransaction < ActiveRecord::TestCase
@@ -182,7 +191,7 @@ module ActiveRecord
end
def setup
- Klass.establish_connection 'arunit'
+ Klass.establish_connection :arunit
@connection = Klass.connection
end
@@ -190,22 +199,20 @@ module ActiveRecord
Klass.remove_connection
end
- test "transaction state is reset after a reconnect" do
- skip "in-memory db doesn't allow reconnect" if in_memory_db?
-
- @connection.begin_transaction
- assert @connection.transaction_open?
- @connection.reconnect!
- assert !@connection.transaction_open?
- end
-
- test "transaction state is reset after a disconnect" do
- skip "in-memory db doesn't allow disconnect" if in_memory_db?
+ unless in_memory_db?
+ test "transaction state is reset after a reconnect" do
+ @connection.begin_transaction
+ assert @connection.transaction_open?
+ @connection.reconnect!
+ assert !@connection.transaction_open?
+ end
- @connection.begin_transaction
- assert @connection.transaction_open?
- @connection.disconnect!
- assert !@connection.transaction_open?
+ test "transaction state is reset after a disconnect" do
+ @connection.begin_transaction
+ assert @connection.transaction_open?
+ @connection.disconnect!
+ assert !@connection.transaction_open?
+ end
end
end
end
diff --git a/activerecord/test/cases/adapters/firebird/connection_test.rb b/activerecord/test/cases/adapters/firebird/connection_test.rb
deleted file mode 100644
index f57ea686a5..0000000000
--- a/activerecord/test/cases/adapters/firebird/connection_test.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-require "cases/helper"
-
-class FirebirdConnectionTest < ActiveRecord::TestCase
- def test_charset_properly_set
- fb_conn = ActiveRecord::Base.connection.instance_variable_get(:@connection)
- assert_equal 'UTF8', fb_conn.database.character_set
- end
-end
diff --git a/activerecord/test/cases/adapters/firebird/default_test.rb b/activerecord/test/cases/adapters/firebird/default_test.rb
deleted file mode 100644
index 713c7e11bf..0000000000
--- a/activerecord/test/cases/adapters/firebird/default_test.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-require "cases/helper"
-require 'models/default'
-
-class DefaultTest < ActiveRecord::TestCase
- def test_default_timestamp
- default = Default.new
- assert_instance_of(Time, default.default_timestamp)
- assert_equal(:datetime, default.column_for_attribute(:default_timestamp).type)
-
- # Variance should be small; increase if required -- e.g., if test db is on
- # remote host and clocks aren't synchronized.
- t1 = Time.new
- accepted_variance = 1.0
- assert_in_delta(t1.to_f, default.default_timestamp.to_f, accepted_variance)
- end
-end
diff --git a/activerecord/test/cases/adapters/firebird/migration_test.rb b/activerecord/test/cases/adapters/firebird/migration_test.rb
deleted file mode 100644
index 5c94593765..0000000000
--- a/activerecord/test/cases/adapters/firebird/migration_test.rb
+++ /dev/null
@@ -1,124 +0,0 @@
-require "cases/helper"
-require 'models/course'
-
-class FirebirdMigrationTest < ActiveRecord::TestCase
- self.use_transactional_fixtures = false
-
- def setup
- # using Course connection for tests -- need a db that doesn't already have a BOOLEAN domain
- @connection = Course.connection
- @fireruby_connection = @connection.instance_variable_get(:@connection)
- end
-
- def teardown
- @connection.drop_table :foo rescue nil
- @connection.execute("DROP DOMAIN D_BOOLEAN") rescue nil
- end
-
- def test_create_table_with_custom_sequence_name
- assert_nothing_raised do
- @connection.create_table(:foo, :sequence => 'foo_custom_seq') do |f|
- f.column :bar, :string
- end
- end
- assert !sequence_exists?('foo_seq')
- assert sequence_exists?('foo_custom_seq')
-
- assert_nothing_raised { @connection.drop_table(:foo) }
- assert !sequence_exists?('foo_custom_seq')
- ensure
- FireRuby::Generator.new('foo_custom_seq', @fireruby_connection).drop rescue nil
- end
-
- def test_create_table_without_sequence
- assert_nothing_raised do
- @connection.create_table(:foo, :sequence => false) do |f|
- f.column :bar, :string
- end
- end
- assert !sequence_exists?('foo_seq')
- assert_nothing_raised { @connection.drop_table :foo }
-
- assert_nothing_raised do
- @connection.create_table(:foo, :id => false) do |f|
- f.column :bar, :string
- end
- end
- assert !sequence_exists?('foo_seq')
- assert_nothing_raised { @connection.drop_table :foo }
- end
-
- def test_create_table_with_boolean_column
- assert !boolean_domain_exists?
- assert_nothing_raised do
- @connection.create_table :foo do |f|
- f.column :bar, :string
- f.column :baz, :boolean
- end
- end
- assert boolean_domain_exists?
- end
-
- def test_add_boolean_column
- assert !boolean_domain_exists?
- @connection.create_table :foo do |f|
- f.column :bar, :string
- end
-
- assert_nothing_raised { @connection.add_column :foo, :baz, :boolean }
- assert boolean_domain_exists?
- assert_equal :boolean, @connection.columns(:foo).find { |c| c.name == "baz" }.type
- end
-
- def test_change_column_to_boolean
- assert !boolean_domain_exists?
- # Manually create table with a SMALLINT column, which can be changed to a BOOLEAN
- @connection.execute "CREATE TABLE foo (bar SMALLINT)"
- assert_equal :integer, @connection.columns(:foo).find { |c| c.name == "bar" }.type
-
- assert_nothing_raised { @connection.change_column :foo, :bar, :boolean }
- assert boolean_domain_exists?
- assert_equal :boolean, @connection.columns(:foo).find { |c| c.name == "bar" }.type
- end
-
- def test_rename_table_with_data_and_index
- @connection.create_table :foo do |f|
- f.column :baz, :string, :limit => 50
- end
- 100.times { |i| @connection.execute "INSERT INTO foo VALUES (GEN_ID(foo_seq, 1), 'record #{i+1}')" }
- @connection.add_index :foo, :baz
-
- assert_nothing_raised { @connection.rename_table :foo, :bar }
- assert !@connection.tables.include?("foo")
- assert @connection.tables.include?("bar")
- assert_equal "index_bar_on_baz", @connection.indexes("bar").first.name
- assert_equal 100, FireRuby::Generator.new("bar_seq", @fireruby_connection).last
- assert_equal 100, @connection.select_one("SELECT COUNT(*) FROM bar")["count"]
- ensure
- @connection.drop_table :bar rescue nil
- end
-
- def test_renaming_table_with_fk_constraint_raises_error
- @connection.create_table :parent do |p|
- p.column :name, :string
- end
- @connection.create_table :child do |c|
- c.column :parent_id, :integer
- end
- @connection.execute "ALTER TABLE child ADD CONSTRAINT fk_child_parent FOREIGN KEY(parent_id) REFERENCES parent(id)"
- assert_raise(ActiveRecord::ActiveRecordError) { @connection.rename_table :child, :descendant }
- ensure
- @connection.drop_table :child rescue nil
- @connection.drop_table :descendant rescue nil
- @connection.drop_table :parent rescue nil
- end
-
- private
- def boolean_domain_exists?
- !@connection.select_one("SELECT 1 FROM rdb$fields WHERE rdb$field_name = 'D_BOOLEAN'").nil?
- end
-
- def sequence_exists?(sequence_name)
- FireRuby::Generator.exists?(sequence_name, @fireruby_connection)
- end
-end
diff --git a/activerecord/test/cases/adapters/mysql/connection_test.rb b/activerecord/test/cases/adapters/mysql/connection_test.rb
index 1844a2e0dc..5cd5d8ac5f 100644
--- a/activerecord/test/cases/adapters/mysql/connection_test.rb
+++ b/activerecord/test/cases/adapters/mysql/connection_test.rb
@@ -16,15 +16,15 @@ class MysqlConnectionTest < ActiveRecord::TestCase
end
end
- def test_connect_with_url
- run_without_connection do
- ar_config = ARTest.connection_config['arunit']
-
- skip "This test doesn't work with custom socket location" if ar_config['socket']
-
- url = "mysql://#{ar_config["username"]}@localhost/#{ar_config["database"]}"
- Klass.establish_connection(url)
- assert_equal ar_config['database'], Klass.connection.current_database
+ unless ARTest.connection_config['arunit']['socket']
+ def test_connect_with_url
+ run_without_connection do
+ ar_config = ARTest.connection_config['arunit']
+
+ url = "mysql://#{ar_config["username"]}@localhost/#{ar_config["database"]}"
+ Klass.establish_connection(url)
+ assert_equal ar_config['database'], Klass.connection.current_database
+ end
end
end
@@ -40,6 +40,11 @@ class MysqlConnectionTest < ActiveRecord::TestCase
@connection.update('set @@wait_timeout=1')
sleep 2
assert !@connection.active?
+
+ # Repair all fixture connections so other tests won't break.
+ @fixture_connections.each do |c|
+ c.verify!
+ end
end
def test_successful_reconnection_after_timeout_with_manual_reconnect
@@ -66,7 +71,7 @@ class MysqlConnectionTest < ActiveRecord::TestCase
def test_exec_no_binds
@connection.exec_query('drop table if exists ex')
@connection.exec_query(<<-eosql)
- CREATE TABLE `ex` (`id` int(11) DEFAULT NULL auto_increment PRIMARY KEY,
+ CREATE TABLE `ex` (`id` int(11) auto_increment PRIMARY KEY,
`data` varchar(255))
eosql
result = @connection.exec_query('SELECT id, data FROM ex')
@@ -88,7 +93,7 @@ class MysqlConnectionTest < ActiveRecord::TestCase
def test_exec_with_binds
@connection.exec_query('drop table if exists ex')
@connection.exec_query(<<-eosql)
- CREATE TABLE `ex` (`id` int(11) DEFAULT NULL auto_increment PRIMARY KEY,
+ CREATE TABLE `ex` (`id` int(11) auto_increment PRIMARY KEY,
`data` varchar(255))
eosql
@connection.exec_query('INSERT INTO ex (id, data) VALUES (1, "foo")')
@@ -104,7 +109,7 @@ class MysqlConnectionTest < ActiveRecord::TestCase
def test_exec_typecasts_bind_vals
@connection.exec_query('drop table if exists ex')
@connection.exec_query(<<-eosql)
- CREATE TABLE `ex` (`id` int(11) DEFAULT NULL auto_increment PRIMARY KEY,
+ CREATE TABLE `ex` (`id` int(11) auto_increment PRIMARY KEY,
`data` varchar(255))
eosql
@connection.exec_query('INSERT INTO ex (id, data) VALUES (1, "foo")')
diff --git a/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb b/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb
index 4a23287448..578f6301bd 100644
--- a/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb
+++ b/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb
@@ -10,12 +10,20 @@ module ActiveRecord
@conn.exec_query('drop table if exists ex')
@conn.exec_query(<<-eosql)
CREATE TABLE `ex` (
- `id` int(11) DEFAULT NULL auto_increment PRIMARY KEY,
+ `id` int(11) auto_increment PRIMARY KEY,
`number` integer,
`data` varchar(255))
eosql
end
+ def test_bad_connection_mysql
+ assert_raise ActiveRecord::NoDatabaseError do
+ configuration = ActiveRecord::Base.configurations['arunit'].merge(database: 'inexistent_activerecord_unittest')
+ connection = ActiveRecord::Base.mysql_connection(configuration)
+ connection.exec_query('drop table if exists ex')
+ end
+ end
+
def test_valid_column
column = @conn.columns('ex').find { |col| col.name == 'id' }
assert @conn.valid_type?(column.type)
@@ -75,7 +83,7 @@ module ActiveRecord
@conn.exec_query('drop table if exists ex_with_non_standard_pk')
@conn.exec_query(<<-eosql)
CREATE TABLE `ex_with_non_standard_pk` (
- `code` INT(11) DEFAULT NULL auto_increment,
+ `code` INT(11) auto_increment,
PRIMARY KEY (`code`))
eosql
pk, seq = @conn.pk_and_sequence_for('ex_with_non_standard_pk')
@@ -87,7 +95,7 @@ module ActiveRecord
@conn.exec_query('drop table if exists ex_with_custom_index_type_pk')
@conn.exec_query(<<-eosql)
CREATE TABLE `ex_with_custom_index_type_pk` (
- `id` INT(11) DEFAULT NULL auto_increment,
+ `id` INT(11) auto_increment,
PRIMARY KEY USING BTREE (`id`))
eosql
pk, seq = @conn.pk_and_sequence_for('ex_with_custom_index_type_pk')
@@ -108,6 +116,18 @@ module ActiveRecord
assert_equal 2, result.column_types['status'].type_cast(result.last['status'])
end
+ def test_supports_extensions
+ assert_not @conn.supports_extensions?, 'does not support extensions'
+ end
+
+ def test_respond_to_enable_extension
+ assert @conn.respond_to?(:enable_extension)
+ end
+
+ def test_respond_to_disable_extension
+ assert @conn.respond_to?(:disable_extension)
+ end
+
private
def insert(ctx, data, table='ex')
binds = data.map { |name, value|
diff --git a/activerecord/test/cases/adapters/mysql/reserved_word_test.rb b/activerecord/test/cases/adapters/mysql/reserved_word_test.rb
index 4cf4bc4c61..8eb9565963 100644
--- a/activerecord/test/cases/adapters/mysql/reserved_word_test.rb
+++ b/activerecord/test/cases/adapters/mysql/reserved_word_test.rb
@@ -2,7 +2,7 @@ require "cases/helper"
class Group < ActiveRecord::Base
Group.table_name = 'group'
- belongs_to :select, :class_name => 'Select'
+ belongs_to :select
has_one :values
end
diff --git a/activerecord/test/cases/adapters/mysql/statement_pool_test.rb b/activerecord/test/cases/adapters/mysql/statement_pool_test.rb
index 83de90f179..209a0cf464 100644
--- a/activerecord/test/cases/adapters/mysql/statement_pool_test.rb
+++ b/activerecord/test/cases/adapters/mysql/statement_pool_test.rb
@@ -3,20 +3,20 @@ require 'cases/helper'
module ActiveRecord::ConnectionAdapters
class MysqlAdapter
class StatementPoolTest < ActiveRecord::TestCase
- def test_cache_is_per_pid
- return skip('must support fork') unless Process.respond_to?(:fork)
+ if Process.respond_to?(:fork)
+ def test_cache_is_per_pid
+ cache = StatementPool.new nil, 10
+ cache['foo'] = 'bar'
+ assert_equal 'bar', cache['foo']
- cache = StatementPool.new nil, 10
- cache['foo'] = 'bar'
- assert_equal 'bar', cache['foo']
+ pid = fork {
+ lookup = cache['foo'];
+ exit!(!lookup)
+ }
- pid = fork {
- lookup = cache['foo'];
- exit!(!lookup)
- }
-
- Process.waitpid pid
- assert $?.success?, 'process should exit successfully'
+ Process.waitpid pid
+ assert $?.success?, 'process should exit successfully'
+ end
end
end
end
diff --git a/activerecord/test/cases/adapters/mysql2/boolean_test.rb b/activerecord/test/cases/adapters/mysql2/boolean_test.rb
new file mode 100644
index 0000000000..267aa232d9
--- /dev/null
+++ b/activerecord/test/cases/adapters/mysql2/boolean_test.rb
@@ -0,0 +1,91 @@
+require "cases/helper"
+
+class Mysql2BooleanTest < ActiveRecord::TestCase
+ self.use_transactional_fixtures = false
+
+ class BooleanType < ActiveRecord::Base
+ self.table_name = "mysql_booleans"
+ end
+
+ setup do
+ @connection = ActiveRecord::Base.connection
+ @connection.create_table("mysql_booleans") do |t|
+ t.boolean "archived"
+ t.string "published", limit: 1
+ end
+ BooleanType.reset_column_information
+
+ @emulate_booleans = ActiveRecord::ConnectionAdapters::Mysql2Adapter.emulate_booleans
+ end
+
+ teardown do
+ emulate_booleans @emulate_booleans
+ @connection.drop_table "mysql_booleans"
+ end
+
+ test "column type with emulated booleans" do
+ emulate_booleans true
+
+ assert_equal :boolean, boolean_column.type
+ assert_equal :string, string_column.type
+ end
+
+ test "column type without emulated booleans" do
+ emulate_booleans false
+
+ assert_equal :integer, boolean_column.type
+ assert_equal :string, string_column.type
+ end
+
+ test "test type casting with emulated booleans" do
+ emulate_booleans true
+
+ boolean = BooleanType.create!(archived: true, published: true)
+ attributes = boolean.reload.attributes_before_type_cast
+
+ assert_equal 1, attributes["archived"]
+ assert_equal "1", attributes["published"]
+
+ assert_equal 1, @connection.type_cast(true, boolean_column)
+ assert_equal 1, @connection.type_cast(true, string_column)
+ end
+
+ test "test type casting without emulated booleans" do
+ emulate_booleans false
+
+ boolean = BooleanType.create!(archived: true, published: true)
+ attributes = boolean.reload.attributes_before_type_cast
+
+ assert_equal 1, attributes["archived"]
+ assert_equal "1", attributes["published"]
+
+ assert_equal 1, @connection.type_cast(true, boolean_column)
+ assert_equal 1, @connection.type_cast(true, string_column)
+ end
+
+ test "with booleans stored as 1 and 0" do
+ @connection.execute "INSERT INTO mysql_booleans(archived, published) VALUES(1, '1')"
+ boolean = BooleanType.first
+ assert_equal true, boolean.archived
+ assert_equal "1", boolean.published
+ end
+
+ test "with booleans stored as t" do
+ @connection.execute "INSERT INTO mysql_booleans(published) VALUES('t')"
+ boolean = BooleanType.first
+ assert_equal "t", boolean.published
+ end
+
+ def boolean_column
+ BooleanType.columns.find { |c| c.name == 'archived' }
+ end
+
+ def string_column
+ BooleanType.columns.find { |c| c.name == 'published' }
+ end
+
+ def emulate_booleans(value)
+ ActiveRecord::ConnectionAdapters::Mysql2Adapter.emulate_booleans = value
+ BooleanType.reset_column_information
+ end
+end
diff --git a/activerecord/test/cases/adapters/mysql2/connection_test.rb b/activerecord/test/cases/adapters/mysql2/connection_test.rb
index fedd9f603c..9b7202c915 100644
--- a/activerecord/test/cases/adapters/mysql2/connection_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/connection_test.rb
@@ -3,14 +3,22 @@ require "cases/helper"
class MysqlConnectionTest < ActiveRecord::TestCase
def setup
super
+ @subscriber = SQLSubscriber.new
+ ActiveSupport::Notifications.subscribe('sql.active_record', @subscriber)
@connection = ActiveRecord::Base.connection
- @connection.extend(LogIntercepter)
- @connection.intercepted = true
end
def teardown
- @connection.intercepted = false
- @connection.logged = []
+ ActiveSupport::Notifications.unsubscribe(@subscriber)
+ super
+ end
+
+ def test_bad_connection
+ assert_raise ActiveRecord::NoDatabaseError do
+ configuration = ActiveRecord::Base.configurations['arunit'].merge(database: 'inexistent_activerecord_unittest')
+ connection = ActiveRecord::Base.mysql2_connection(configuration)
+ connection.exec_query('drop table if exists ex')
+ end
end
def test_no_automatic_reconnection_after_timeout
@@ -18,6 +26,11 @@ class MysqlConnectionTest < ActiveRecord::TestCase
@connection.update('set @@wait_timeout=1')
sleep 2
assert !@connection.active?
+
+ # Repair all fixture connections so other tests won't break.
+ @fixture_connections.each do |c|
+ c.verify!
+ end
end
def test_successful_reconnection_after_timeout_with_manual_reconnect
@@ -72,14 +85,14 @@ class MysqlConnectionTest < ActiveRecord::TestCase
def test_logs_name_show_variable
@connection.show_variable 'foo'
- assert_equal "SCHEMA", @connection.logged[0][1]
+ assert_equal "SCHEMA", @subscriber.logged[0][1]
end
def test_logs_name_rename_column_sql
@connection.execute "CREATE TABLE `bar_baz` (`foo` varchar(255))"
- @connection.logged = []
+ @subscriber.logged.clear
@connection.send(:rename_column_sql, 'bar_baz', 'foo', 'foo2')
- assert_equal "SCHEMA", @connection.logged[0][1]
+ assert_equal "SCHEMA", @subscriber.logged[0][1]
ensure
@connection.execute "DROP TABLE `bar_baz`"
end
diff --git a/activerecord/test/cases/adapters/mysql2/explain_test.rb b/activerecord/test/cases/adapters/mysql2/explain_test.rb
index 68ed361aeb..1cd356e868 100644
--- a/activerecord/test/cases/adapters/mysql2/explain_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/explain_test.rb
@@ -10,15 +10,15 @@ module ActiveRecord
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 %(developers | const), 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 %(developers | const), explain
+ assert_match %r(developers |.* const), explain
assert_match %(EXPLAIN for: SELECT `audit_logs`.* FROM `audit_logs` WHERE `audit_logs`.`developer_id` IN (1)), explain
- assert_match %(audit_logs | ALL), explain
+ assert_match %r(audit_logs |.* ALL), explain
end
end
end
diff --git a/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb b/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb
index e76617b845..1a82308176 100644
--- a/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb
@@ -2,7 +2,7 @@ require "cases/helper"
class Group < ActiveRecord::Base
Group.table_name = 'group'
- belongs_to :select, :class_name => 'Select'
+ belongs_to :select
has_one :values
end
diff --git a/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb b/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb
index 9ecd901eac..ec73ec35aa 100644
--- a/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb
@@ -4,22 +4,35 @@ module ActiveRecord
module ConnectionAdapters
class Mysql2Adapter
class SchemaMigrationsTest < ActiveRecord::TestCase
- def test_initializes_schema_migrations_for_encoding_utf8mb4
- conn = ActiveRecord::Base.connection
+ def test_renaming_index_on_foreign_key
+ connection.add_index "engines", "car_id"
+ connection.execute "ALTER TABLE engines ADD CONSTRAINT fk_engines_cars FOREIGN KEY (car_id) REFERENCES cars(id)"
+
+ connection.rename_index("engines", "index_engines_on_car_id", "idx_renamed")
+ assert_equal ["idx_renamed"], connection.indexes("engines").map(&:name)
+ ensure
+ connection.execute "ALTER TABLE engines DROP FOREIGN KEY fk_engines_cars"
+ end
+ def test_initializes_schema_migrations_for_encoding_utf8mb4
smtn = ActiveRecord::Migrator.schema_migrations_table_name
- conn.drop_table(smtn) if conn.table_exists?(smtn)
+ connection.drop_table(smtn) if connection.table_exists?(smtn)
- config = conn.instance_variable_get(:@config)
+ config = connection.instance_variable_get(:@config)
original_encoding = config[:encoding]
config[:encoding] = 'utf8mb4'
- conn.initialize_schema_migrations_table
+ connection.initialize_schema_migrations_table
- assert conn.column_exists?(smtn, :version, :string, limit: Mysql2Adapter::MAX_INDEX_LENGTH_FOR_UTF8MB4)
+ assert connection.column_exists?(smtn, :version, :string, limit: Mysql2Adapter::MAX_INDEX_LENGTH_FOR_UTF8MB4)
ensure
config[:encoding] = original_encoding
end
+
+ private
+ def connection
+ @connection ||= ActiveRecord::Base.connection
+ end
end
end
end
diff --git a/activerecord/test/cases/adapters/mysql2/schema_test.rb b/activerecord/test/cases/adapters/mysql2/schema_test.rb
index 5db60ff8a0..43c9116b5a 100644
--- a/activerecord/test/cases/adapters/mysql2/schema_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/schema_test.rb
@@ -65,6 +65,15 @@ module ActiveRecord
assert_nil index_c.using
assert_equal :fulltext, index_c.type
end
+
+ def test_drop_temporary_table
+ @connection.transaction do
+ @connection.create_table(:temp_table, temporary: true)
+ # if it doesn't properly say DROP TEMPORARY TABLE, the transaction commit
+ # will complain that no transaction is active
+ @connection.drop_table(:temp_table, temporary: true)
+ end
+ end
end
end
end
diff --git a/activerecord/test/cases/adapters/oracle/synonym_test.rb b/activerecord/test/cases/adapters/oracle/synonym_test.rb
deleted file mode 100644
index b9a422a6ca..0000000000
--- a/activerecord/test/cases/adapters/oracle/synonym_test.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-require "cases/helper"
-require 'models/topic'
-require 'models/subject'
-
-# confirm that synonyms work just like tables; in this case
-# the "subjects" table in Oracle (defined in oci.sql) is just
-# a synonym to the "topics" table
-
-class TestOracleSynonym < ActiveRecord::TestCase
-
- def test_oracle_synonym
- topic = Topic.new
- subject = Subject.new
- assert_equal(topic.attributes, subject.attributes)
- 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 16329689c0..22dd48e113 100644
--- a/activerecord/test/cases/adapters/postgresql/active_schema_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/active_schema_test.rb
@@ -25,9 +25,7 @@ class PostgresqlActiveSchemaTest < ActiveRecord::TestCase
def test_add_index
# add_index calls index_name_exists? which can't work since execute is stubbed
- ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.send(:define_method, :index_name_exists?) do |*|
- false
- end
+ ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.stubs(:index_name_exists?).returns(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'")
@@ -51,8 +49,6 @@ class PostgresqlActiveSchemaTest < ActiveRecord::TestCase
expected = %(CREATE UNIQUE INDEX "index_people_on_last_name" ON "people" USING gist ("last_name") WHERE state = 'active')
assert_equal expected, add_index(:people, :last_name, :unique => true, :where => "state = 'active'", :using => :gist)
-
- ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.send(:remove_method, :index_name_exists?)
end
private
diff --git a/activerecord/test/cases/adapters/postgresql/array_test.rb b/activerecord/test/cases/adapters/postgresql/array_test.rb
index 61a3a2ba0f..d71e2aa2bb 100644
--- a/activerecord/test/cases/adapters/postgresql/array_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/array_test.rb
@@ -10,11 +10,12 @@ class PostgresqlArrayTest < ActiveRecord::TestCase
def setup
@connection = ActiveRecord::Base.connection
- @connection.transaction do
- @connection.create_table('pg_arrays') do |t|
- t.string 'tags', :array => true
- end
+ @connection.transaction do
+ @connection.create_table('pg_arrays') do |t|
+ t.string 'tags', array: true
+ t.integer 'ratings', array: true
end
+ end
@column = PgArray.columns.find { |c| c.name == 'tags' }
end
@@ -25,11 +26,36 @@ class PostgresqlArrayTest < ActiveRecord::TestCase
def test_column
assert_equal :string, @column.type
assert @column.array
+ assert_not @column.text?
+
+ ratings_column = PgArray.columns_hash['ratings']
+ assert_equal :integer, ratings_column.type
+ assert ratings_column.array
+ assert_not ratings_column.number?
end
- def test_type_cast_array
- assert @column
+ def test_change_column_with_array
+ @connection.add_column :pg_arrays, :snippets, :string, array: true, default: []
+ @connection.change_column :pg_arrays, :snippets, :text, array: true, default: "{}"
+
+ PgArray.reset_column_information
+ column = PgArray.columns.find { |c| c.name == 'snippets' }
+
+ assert_equal :text, column.type
+ assert_equal [], column.default
+ assert column.array
+ end
+ def test_change_column_cant_make_non_array_column_to_array
+ @connection.add_column :pg_arrays, :a_string, :string
+ assert_raises ActiveRecord::StatementInvalid do
+ @connection.transaction do
+ @connection.change_column :pg_arrays, :a_string, :string, array: true
+ end
+ end
+ end
+
+ def test_type_cast_array
data = '{1,2,3}'
oid_type = @column.instance_variable_get('@oid_type').subtype
# we are getting the instance variable in this test, but in the
@@ -44,6 +70,12 @@ class PostgresqlArrayTest < ActiveRecord::TestCase
assert_equal([nil], @column.type_cast('{NULL}'))
end
+ def test_type_cast_integers
+ x = PgArray.new(ratings: ['1', '2'])
+ assert x.save!
+ assert_equal(['1', '2'], x.ratings)
+ end
+
def test_rewrite
@connection.execute "insert into pg_arrays (tags) VALUES ('{1,2,3}')"
x = PgArray.first
@@ -57,28 +89,32 @@ class PostgresqlArrayTest < ActiveRecord::TestCase
assert_equal(['1','2','3'], x.tags)
end
- def test_multi_dimensional
- assert_cycle([['1','2'],['2','3']])
+ def test_multi_dimensional_with_strings
+ assert_cycle(:tags, [[['1'], ['2']], [['2'], ['3']]])
+ end
+
+ def test_multi_dimensional_with_integers
+ assert_cycle(:ratings, [[[1], [7]], [[8], [10]]])
end
def test_strings_with_quotes
- assert_cycle(['this has','some "s that need to be escaped"'])
+ assert_cycle(:tags, ['this has','some "s that need to be escaped"'])
end
def test_strings_with_commas
- assert_cycle(['this,has','many,values'])
+ assert_cycle(:tags, ['this,has','many,values'])
end
def test_strings_with_array_delimiters
- assert_cycle(['{','}'])
+ assert_cycle(:tags, ['{','}'])
end
def test_strings_with_null_strings
- assert_cycle(['NULL','NULL'])
+ assert_cycle(:tags, ['NULL','NULL'])
end
def test_contains_nils
- assert_cycle(['1',nil,nil])
+ assert_cycle(:tags, ['1',nil,nil])
end
def test_insert_fixture
@@ -87,18 +123,33 @@ class PostgresqlArrayTest < ActiveRecord::TestCase
assert_equal(PgArray.last.tags, tag_values)
end
+ def test_attribute_for_inspect_for_array_field
+ record = PgArray.new { |a| a.ratings = (1..11).to_a }
+ assert_equal("[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]", record.attribute_for_inspect(:ratings))
+ end
+
+ def test_update_all
+ pg_array = PgArray.create! tags: ["one", "two", "three"]
+
+ PgArray.update_all tags: ["four", "five"]
+ assert_equal ["four", "five"], pg_array.reload.tags
+
+ PgArray.update_all tags: []
+ assert_equal [], pg_array.reload.tags
+ end
+
private
- def assert_cycle array
+ def assert_cycle field, array
# test creation
- x = PgArray.create!(:tags => array)
+ x = PgArray.create!(field => array)
x.reload
- assert_equal(array, x.tags)
+ assert_equal(array, x.public_send(field))
# test updating
- x = PgArray.create!(:tags => [])
- x.tags = array
+ x = PgArray.create!(field => [])
+ x.public_send("#{field}=", array)
x.save!
x.reload
- assert_equal(array, x.tags)
+ assert_equal(array, x.public_send(field))
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/bytea_test.rb b/activerecord/test/cases/adapters/postgresql/bytea_test.rb
index d7d77f96e2..b8dd35c4c5 100644
--- a/activerecord/test/cases/adapters/postgresql/bytea_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/bytea_test.rb
@@ -15,6 +15,7 @@ class PostgresqlByteaTest < ActiveRecord::TestCase
@connection.transaction do
@connection.create_table('bytea_data_type') do |t|
t.binary 'payload'
+ t.binary 'serialized'
end
end
end
@@ -65,7 +66,7 @@ class PostgresqlByteaTest < ActiveRecord::TestCase
def test_write_value
data = "\u001F"
record = ByteaDataType.create(payload: data)
- refute record.new_record?
+ assert_not record.new_record?
assert_equal(data, record.payload)
end
@@ -73,15 +74,31 @@ class PostgresqlByteaTest < ActiveRecord::TestCase
data = File.read(File.join(File.dirname(__FILE__), '..', '..', '..', 'assets', 'example.log'))
assert(data.size > 1)
record = ByteaDataType.create(payload: data)
- refute record.new_record?
+ assert_not record.new_record?
assert_equal(data, record.payload)
assert_equal(data, ByteaDataType.where(id: record.id).first.payload)
end
def test_write_nil
record = ByteaDataType.create(payload: nil)
- refute record.new_record?
+ assert_not record.new_record?
assert_equal(nil, record.payload)
assert_equal(nil, ByteaDataType.where(id: record.id).first.payload)
end
+
+ class Serializer
+ def load(str); str; end
+ def dump(str); str; end
+ end
+
+ def test_serialize
+ klass = Class.new(ByteaDataType) {
+ serialize :serialized, Serializer.new
+ }
+ obj = klass.new
+ obj.serialized = "hello world"
+ obj.save!
+ obj.reload
+ assert_equal "hello world", obj.serialized
+ end
end
diff --git a/activerecord/test/cases/adapters/postgresql/connection_test.rb b/activerecord/test/cases/adapters/postgresql/connection_test.rb
index 6b726ce875..90cca7d3e6 100644
--- a/activerecord/test/cases/adapters/postgresql/connection_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/connection_test.rb
@@ -7,14 +7,14 @@ module ActiveRecord
def setup
super
+ @subscriber = SQLSubscriber.new
+ ActiveSupport::Notifications.subscribe('sql.active_record', @subscriber)
@connection = ActiveRecord::Base.connection
- @connection.extend(LogIntercepter)
- @connection.intercepted = true
end
def teardown
- @connection.intercepted = false
- @connection.logged = []
+ ActiveSupport::Notifications.unsubscribe(@subscriber)
+ super
end
def test_encoding
@@ -47,38 +47,48 @@ module ActiveRecord
def test_tables_logs_name
@connection.tables('hello')
- assert_equal 'SCHEMA', @connection.logged[0][1]
+ assert_equal 'SCHEMA', @subscriber.logged[0][1]
end
def test_indexes_logs_name
@connection.indexes('items', 'hello')
- assert_equal 'SCHEMA', @connection.logged[0][1]
+ assert_equal 'SCHEMA', @subscriber.logged[0][1]
end
def test_table_exists_logs_name
@connection.table_exists?('items')
- assert_equal 'SCHEMA', @connection.logged[0][1]
+ assert_equal 'SCHEMA', @subscriber.logged[0][1]
end
def test_table_alias_length_logs_name
@connection.instance_variable_set("@table_alias_length", nil)
@connection.table_alias_length
- assert_equal 'SCHEMA', @connection.logged[0][1]
+ assert_equal 'SCHEMA', @subscriber.logged[0][1]
end
def test_current_database_logs_name
@connection.current_database
- assert_equal 'SCHEMA', @connection.logged[0][1]
+ assert_equal 'SCHEMA', @subscriber.logged[0][1]
end
def test_encoding_logs_name
@connection.encoding
- assert_equal 'SCHEMA', @connection.logged[0][1]
+ assert_equal 'SCHEMA', @subscriber.logged[0][1]
end
def test_schema_names_logs_name
@connection.schema_names
- assert_equal 'SCHEMA', @connection.logged[0][1]
+ assert_equal 'SCHEMA', @subscriber.logged[0][1]
+ end
+
+ def test_statement_key_is_logged
+ bindval = 1
+ @connection.exec_query('SELECT $1::integer', 'SQL', [[nil, bindval]])
+ name = @subscriber.payloads.last[:statement_name]
+ assert name
+ res = @connection.exec_query("EXPLAIN (FORMAT JSON) EXECUTE #{name}(#{bindval})")
+ plan = res.column_types['QUERY PLAN'].type_cast res.rows.first.first
+ assert_operator plan.length, :>, 0
end
# Must have with_manual_interventions set to true for this
@@ -88,33 +98,33 @@ module ActiveRecord
# you know the incantation to do that.
# To restart PostgreSQL 9.1 on OS X, installed via MacPorts, ...
# sudo su postgres -c "pg_ctl restart -D /opt/local/var/db/postgresql91/defaultdb/ -m fast"
- def test_reconnection_after_actual_disconnection_with_verify
- skip "with_manual_interventions is false in configuration" unless ARTest.config['with_manual_interventions']
-
- original_connection_pid = @connection.query('select pg_backend_pid()')
+ if ARTest.config['with_manual_interventions']
+ def test_reconnection_after_actual_disconnection_with_verify
+ original_connection_pid = @connection.query('select pg_backend_pid()')
- # Sanity check.
- assert @connection.active?
+ # Sanity check.
+ assert @connection.active?
- puts 'Kill the connection now (e.g. by restarting the PostgreSQL ' +
- 'server with the "-m fast" option) and then press enter.'
- $stdin.gets
+ puts 'Kill the connection now (e.g. by restarting the PostgreSQL ' +
+ 'server with the "-m fast" option) and then press enter.'
+ $stdin.gets
- @connection.verify!
+ @connection.verify!
- assert @connection.active?
+ assert @connection.active?
- # If we get no exception here, then either we re-connected successfully, or
- # we never actually got disconnected.
- new_connection_pid = @connection.query('select pg_backend_pid()')
+ # If we get no exception here, then either we re-connected successfully, or
+ # we never actually got disconnected.
+ new_connection_pid = @connection.query('select pg_backend_pid()')
- assert_not_equal original_connection_pid, new_connection_pid,
- "umm -- looks like you didn't break the connection, because we're still " +
- "successfully querying with the same connection pid."
+ assert_not_equal original_connection_pid, new_connection_pid,
+ "umm -- looks like you didn't break the connection, because we're still " +
+ "successfully querying with the same connection pid."
- # Repair all fixture connections so other tests won't break.
- @fixture_connections.each do |c|
- c.verify!
+ # Repair all fixture connections so other tests won't break.
+ @fixture_connections.each do |c|
+ c.verify!
+ end
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/datatype_test.rb b/activerecord/test/cases/adapters/postgresql/datatype_test.rb
index b5d7ea603e..04a458fbce 100644
--- a/activerecord/test/cases/adapters/postgresql/datatype_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/datatype_test.rb
@@ -3,9 +3,6 @@ require "cases/helper"
class PostgresqlArray < ActiveRecord::Base
end
-class PostgresqlRange < ActiveRecord::Base
-end
-
class PostgresqlTsvector < ActiveRecord::Base
end
@@ -46,104 +43,6 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase
@connection.execute("INSERT INTO postgresql_arrays (id, commission_by_quarter, nicknames) VALUES (1, '{35000,21000,18000,17000}', '{foo,bar,baz}')")
@first_array = PostgresqlArray.find(1)
- @connection.execute <<_SQL if @connection.supports_ranges?
- INSERT INTO postgresql_ranges (
- date_range,
- num_range,
- ts_range,
- tstz_range,
- int4_range,
- int8_range
- ) VALUES (
- '[''2012-01-02'', ''2012-01-04'']',
- '[0.1, 0.2]',
- '[''2010-01-01 14:30'', ''2011-01-01 14:30'']',
- '[''2010-01-01 14:30:00+05'', ''2011-01-01 14:30:00-03'']',
- '[1, 10]',
- '[10, 100]'
- )
-_SQL
-
- @connection.execute <<_SQL if @connection.supports_ranges?
- INSERT INTO postgresql_ranges (
- date_range,
- num_range,
- ts_range,
- tstz_range,
- int4_range,
- int8_range
- ) VALUES (
- '(''2012-01-02'', ''2012-01-04'')',
- '[0.1, 0.2)',
- '[''2010-01-01 14:30'', ''2011-01-01 14:30'')',
- '[''2010-01-01 14:30:00+05'', ''2011-01-01 14:30:00-03'')',
- '(1, 10)',
- '(10, 100)'
- )
-_SQL
-
- @connection.execute <<_SQL if @connection.supports_ranges?
- INSERT INTO postgresql_ranges (
- date_range,
- num_range,
- ts_range,
- tstz_range,
- int4_range,
- int8_range
- ) VALUES (
- '(''2012-01-02'',]',
- '[0.1,]',
- '[''2010-01-01 14:30'',]',
- '[''2010-01-01 14:30:00+05'',]',
- '(1,]',
- '(10,]'
- )
-_SQL
-
- @connection.execute <<_SQL if @connection.supports_ranges?
- INSERT INTO postgresql_ranges (
- date_range,
- num_range,
- ts_range,
- tstz_range,
- int4_range,
- int8_range
- ) VALUES (
- '[,]',
- '[,]',
- '[,]',
- '[,]',
- '[,]',
- '[,]'
- )
-_SQL
-
- @connection.execute <<_SQL if @connection.supports_ranges?
- INSERT INTO postgresql_ranges (
- date_range,
- num_range,
- ts_range,
- tstz_range,
- int4_range,
- int8_range
- ) VALUES (
- '(''2012-01-02'', ''2012-01-02'')',
- '(0.1, 0.1)',
- '(''2010-01-01 14:30'', ''2010-01-01 14:30'')',
- '(''2010-01-01 14:30:00+05'', ''2010-01-01 06:30:00-03'')',
- '(1, 1)',
- '(10, 10)'
- )
-_SQL
-
- if @connection.supports_ranges?
- @first_range = PostgresqlRange.find(1)
- @second_range = PostgresqlRange.find(2)
- @third_range = PostgresqlRange.find(3)
- @fourth_range = PostgresqlRange.find(4)
- @empty_range = PostgresqlRange.find(5)
- end
-
@connection.execute("INSERT INTO postgresql_tsvectors (id, text_vector) VALUES (1, ' ''text'' ''vector'' ')")
@first_tsvector = PostgresqlTsvector.find(1)
@@ -184,16 +83,6 @@ _SQL
assert_equal :text, @first_array.column_for_attribute(:nicknames).type
end
- def test_data_type_of_range_types
- skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges?
- assert_equal :daterange, @first_range.column_for_attribute(:date_range).type
- assert_equal :numrange, @first_range.column_for_attribute(:num_range).type
- assert_equal :tsrange, @first_range.column_for_attribute(:ts_range).type
- assert_equal :tstzrange, @first_range.column_for_attribute(:tstz_range).type
- assert_equal :int4range, @first_range.column_for_attribute(:int4_range).type
- assert_equal :int8range, @first_range.column_for_attribute(:int8_range).type
- end
-
def test_data_type_of_tsvector_types
assert_equal :tsvector, @first_tsvector.column_for_attribute(:text_vector).type
end
@@ -240,208 +129,28 @@ _SQL
assert_equal "'text' 'vector'", @first_tsvector.text_vector
end
- def test_int4range_values
- skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges?
- assert_equal 1...11, @first_range.int4_range
- assert_equal 2...10, @second_range.int4_range
- assert_equal 2...Float::INFINITY, @third_range.int4_range
- assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.int4_range)
- assert_equal nil, @empty_range.int4_range
- end
-
- def test_int8range_values
- skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges?
- assert_equal 10...101, @first_range.int8_range
- assert_equal 11...100, @second_range.int8_range
- assert_equal 11...Float::INFINITY, @third_range.int8_range
- assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.int8_range)
- assert_equal nil, @empty_range.int8_range
- end
-
- def test_daterange_values
- skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges?
- assert_equal Date.new(2012, 1, 2)...Date.new(2012, 1, 5), @first_range.date_range
- assert_equal Date.new(2012, 1, 3)...Date.new(2012, 1, 4), @second_range.date_range
- assert_equal Date.new(2012, 1, 3)...Float::INFINITY, @third_range.date_range
- assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.date_range)
- assert_equal nil, @empty_range.date_range
- end
-
- def test_numrange_values
- skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges?
- assert_equal BigDecimal.new('0.1')..BigDecimal.new('0.2'), @first_range.num_range
- assert_equal BigDecimal.new('0.1')...BigDecimal.new('0.2'), @second_range.num_range
- assert_equal BigDecimal.new('0.1')...BigDecimal.new('Infinity'), @third_range.num_range
- assert_equal BigDecimal.new('-Infinity')...BigDecimal.new('Infinity'), @fourth_range.num_range
- assert_equal nil, @empty_range.num_range
- end
-
- def test_tsrange_values
- skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges?
- tz = ::ActiveRecord::Base.default_timezone
- assert_equal Time.send(tz, 2010, 1, 1, 14, 30, 0)..Time.send(tz, 2011, 1, 1, 14, 30, 0), @first_range.ts_range
- assert_equal Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2011, 1, 1, 14, 30, 0), @second_range.ts_range
- assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.ts_range)
- assert_equal nil, @empty_range.ts_range
- end
-
- def test_tstzrange_values
- skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges?
- assert_equal Time.parse('2010-01-01 09:30:00 UTC')..Time.parse('2011-01-01 17:30:00 UTC'), @first_range.tstz_range
- assert_equal Time.parse('2010-01-01 09:30:00 UTC')...Time.parse('2011-01-01 17:30:00 UTC'), @second_range.tstz_range
- assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.tstz_range)
- assert_equal nil, @empty_range.tstz_range
- end
-
def test_money_values
assert_equal 567.89, @first_money.wealth
assert_equal(-567.89, @second_money.wealth)
end
- def test_create_tstzrange
- skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges?
- tstzrange = Time.parse('2010-01-01 14:30:00 +0100')...Time.parse('2011-02-02 14:30:00 CDT')
- range = PostgresqlRange.new(:tstz_range => tstzrange)
- assert range.save
- assert range.reload
- assert_equal range.tstz_range, tstzrange
- assert_equal range.tstz_range, Time.parse('2010-01-01 13:30:00 UTC')...Time.parse('2011-02-02 19:30:00 UTC')
- end
-
- def test_update_tstzrange
- skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges?
- new_tstzrange = Time.parse('2010-01-01 14:30:00 CDT')...Time.parse('2011-02-02 14:30:00 CET')
- assert @first_range.tstz_range = new_tstzrange
- assert @first_range.save
- assert @first_range.reload
- assert_equal @first_range.tstz_range, new_tstzrange
- assert @first_range.tstz_range = Time.parse('2010-01-01 14:30:00 +0100')...Time.parse('2010-01-01 13:30:00 +0000')
- assert @first_range.save
- assert @first_range.reload
- assert_equal @first_range.tstz_range, nil
- end
-
- def test_create_tsrange
- skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges?
- tz = ::ActiveRecord::Base.default_timezone
- tsrange = Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2011, 2, 2, 14, 30, 0)
- range = PostgresqlRange.new(:ts_range => tsrange)
- assert range.save
- assert range.reload
- assert_equal range.ts_range, tsrange
- end
-
- def test_update_tsrange
- skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges?
- tz = ::ActiveRecord::Base.default_timezone
- new_tsrange = Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2011, 2, 2, 14, 30, 0)
- assert @first_range.ts_range = new_tsrange
- assert @first_range.save
- assert @first_range.reload
- assert_equal @first_range.ts_range, new_tsrange
- assert @first_range.ts_range = Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2010, 1, 1, 14, 30, 0)
- assert @first_range.save
- assert @first_range.reload
- assert_equal @first_range.ts_range, nil
- end
-
- def test_create_numrange
- skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges?
- numrange = BigDecimal.new('0.5')...BigDecimal.new('1')
- range = PostgresqlRange.new(:num_range => numrange)
- assert range.save
- assert range.reload
- assert_equal range.num_range, numrange
- end
-
- def test_update_numrange
- skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges?
- new_numrange = BigDecimal.new('0.5')...BigDecimal.new('1')
- assert @first_range.num_range = new_numrange
- assert @first_range.save
- assert @first_range.reload
- assert_equal @first_range.num_range, new_numrange
- assert @first_range.num_range = BigDecimal.new('0.5')...BigDecimal.new('0.5')
- assert @first_range.save
- assert @first_range.reload
- assert_equal @first_range.num_range, nil
- end
-
- def test_create_daterange
- skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges?
- daterange = Range.new(Date.new(2012, 1, 1), Date.new(2013, 1, 1), true)
- range = PostgresqlRange.new(:date_range => daterange)
- assert range.save
- assert range.reload
- assert_equal range.date_range, daterange
- end
-
- def test_update_daterange
- skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges?
- new_daterange = Date.new(2012, 2, 3)...Date.new(2012, 2, 10)
- assert @first_range.date_range = new_daterange
- assert @first_range.save
- assert @first_range.reload
- assert_equal @first_range.date_range, new_daterange
- assert @first_range.date_range = Date.new(2012, 2, 3)...Date.new(2012, 2, 3)
- assert @first_range.save
- assert @first_range.reload
- assert_equal @first_range.date_range, nil
- end
-
- def test_create_int4range
- skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges?
- int4range = Range.new(3, 50, true)
- range = PostgresqlRange.new(:int4_range => int4range)
- assert range.save
- assert range.reload
- assert_equal range.int4_range, int4range
- end
-
- def test_update_int4range
- skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges?
- new_int4range = 6...10
- assert @first_range.int4_range = new_int4range
- assert @first_range.save
- assert @first_range.reload
- assert_equal @first_range.int4_range, new_int4range
- assert @first_range.int4_range = 3...3
- assert @first_range.save
- assert @first_range.reload
- assert_equal @first_range.int4_range, nil
- end
-
- def test_create_int8range
- skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges?
- int8range = Range.new(30, 50, true)
- range = PostgresqlRange.new(:int8_range => int8range)
- assert range.save
- assert range.reload
- assert_equal range.int8_range, int8range
- end
-
- def test_update_int8range
- skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges?
- new_int8range = 60000...10000000
- assert @first_range.int8_range = new_int8range
- assert @first_range.save
- assert @first_range.reload
- assert_equal @first_range.int8_range, new_int8range
- assert @first_range.int8_range = 39999...39999
- assert @first_range.save
- assert @first_range.reload
- assert_equal @first_range.int8_range, nil
+ def test_money_type_cast
+ column = PostgresqlMoney.columns.find { |c| c.name == 'wealth' }
+ assert_equal(12345678.12, column.type_cast("$12,345,678.12"))
+ assert_equal(12345678.12, column.type_cast("$12.345.678,12"))
+ assert_equal(-1.15, column.type_cast("-$1.15"))
+ assert_equal(-2.25, column.type_cast("($2.25)"))
end
def test_update_tsvector
new_text_vector = "'new' 'text' 'vector'"
- assert @first_tsvector.text_vector = new_text_vector
+ @first_tsvector.text_vector = new_text_vector
assert @first_tsvector.save
assert @first_tsvector.reload
- assert @first_tsvector.text_vector = new_text_vector
+ @first_tsvector.text_vector = new_text_vector
assert @first_tsvector.save
assert @first_tsvector.reload
- assert_equal @first_tsvector.text_vector, new_text_vector
+ assert_equal new_text_vector, @first_tsvector.text_vector
end
def test_number_values
@@ -479,31 +188,31 @@ _SQL
def test_update_integer_array
new_value = [32800,95000,29350,17000]
- assert @first_array.commission_by_quarter = new_value
+ @first_array.commission_by_quarter = new_value
assert @first_array.save
assert @first_array.reload
- assert_equal @first_array.commission_by_quarter, new_value
- assert @first_array.commission_by_quarter = new_value
+ assert_equal new_value, @first_array.commission_by_quarter
+ @first_array.commission_by_quarter = new_value
assert @first_array.save
assert @first_array.reload
- assert_equal @first_array.commission_by_quarter, new_value
+ assert_equal new_value, @first_array.commission_by_quarter
end
def test_update_text_array
new_value = ['robby','robert','rob','robbie']
- assert @first_array.nicknames = new_value
+ @first_array.nicknames = new_value
assert @first_array.save
assert @first_array.reload
- assert_equal @first_array.nicknames, new_value
- assert @first_array.nicknames = new_value
+ assert_equal new_value, @first_array.nicknames
+ @first_array.nicknames = new_value
assert @first_array.save
assert @first_array.reload
- assert_equal @first_array.nicknames, new_value
+ assert_equal new_value, @first_array.nicknames
end
def test_update_money
new_value = BigDecimal.new('123.45')
- assert @first_money.wealth = new_value
+ @first_money.wealth = new_value
assert @first_money.save
assert @first_money.reload
assert_equal new_value, @first_money.wealth
@@ -512,28 +221,28 @@ _SQL
def test_update_number
new_single = 789.012
new_double = 789012.345
- assert @first_number.single = new_single
- assert @first_number.double = new_double
+ @first_number.single = new_single
+ @first_number.double = new_double
assert @first_number.save
assert @first_number.reload
- assert_equal @first_number.single, new_single
- assert_equal @first_number.double, new_double
+ assert_equal new_single, @first_number.single
+ assert_equal new_double, @first_number.double
end
def test_update_time
- assert @first_time.time_interval = '2 years 3 minutes'
+ @first_time.time_interval = '2 years 3 minutes'
assert @first_time.save
assert @first_time.reload
- assert_equal @first_time.time_interval, '2 years 00:03:00'
+ assert_equal '2 years 00:03:00', @first_time.time_interval
end
def test_update_network_address
new_inet_address = '10.1.2.3/32'
new_cidr_address = '10.0.0.0/8'
new_mac_address = 'bc:de:f0:12:34:56'
- assert @first_network_address.cidr_address = new_cidr_address
- assert @first_network_address.inet_address = new_inet_address
- assert @first_network_address.mac_address = new_mac_address
+ @first_network_address.cidr_address = new_cidr_address
+ @first_network_address.inet_address = new_inet_address
+ @first_network_address.mac_address = new_mac_address
assert @first_network_address.save
assert @first_network_address.reload
assert_equal @first_network_address.cidr_address, new_cidr_address
@@ -544,61 +253,65 @@ _SQL
def test_update_bit_string
new_bit_string = '11111111'
new_bit_string_varying = '0xFF'
- assert @first_bit_string.bit_string = new_bit_string
- assert @first_bit_string.bit_string_varying = new_bit_string_varying
+ @first_bit_string.bit_string = new_bit_string
+ @first_bit_string.bit_string_varying = new_bit_string_varying
assert @first_bit_string.save
assert @first_bit_string.reload
- assert_equal @first_bit_string.bit_string, new_bit_string
+ assert_equal new_bit_string, @first_bit_string.bit_string
assert_equal @first_bit_string.bit_string, @first_bit_string.bit_string_varying
end
-
+
def test_invalid_hex_string
new_bit_string = 'FF'
@first_bit_string.bit_string = new_bit_string
assert_raise(ActiveRecord::StatementInvalid) { assert @first_bit_string.save }
end
+ def test_invalid_network_address
+ @first_network_address.cidr_address = 'invalid addr'
+ assert_nil @first_network_address.cidr_address
+ assert_equal 'invalid addr', @first_network_address.cidr_address_before_type_cast
+ assert @first_network_address.save
+
+ @first_network_address.reload
+
+ @first_network_address.inet_address = 'invalid addr'
+ assert_nil @first_network_address.inet_address
+ assert_equal 'invalid addr', @first_network_address.inet_address_before_type_cast
+ assert @first_network_address.save
+ end
+
def test_update_oid
new_value = 567890
- assert @first_oid.obj_id = new_value
+ @first_oid.obj_id = new_value
assert @first_oid.save
assert @first_oid.reload
- assert_equal @first_oid.obj_id, new_value
+ assert_equal new_value, @first_oid.obj_id
end
def test_timestamp_with_zone_values_with_rails_time_zone_support
- old_tz = ActiveRecord::Base.time_zone_aware_attributes
- old_default_tz = ActiveRecord::Base.default_timezone
-
- ActiveRecord::Base.time_zone_aware_attributes = true
- ActiveRecord::Base.default_timezone = :utc
+ with_timezone_config default: :utc, aware_attributes: true do
+ @connection.reconnect!
- @connection.reconnect!
-
- @first_timestamp_with_zone = PostgresqlTimestampWithZone.find(1)
- assert_equal Time.utc(2010,1,1, 11,0,0), @first_timestamp_with_zone.time
- assert_instance_of Time, @first_timestamp_with_zone.time
+ @first_timestamp_with_zone = PostgresqlTimestampWithZone.find(1)
+ assert_equal Time.utc(2010,1,1, 11,0,0), @first_timestamp_with_zone.time
+ assert_instance_of Time, @first_timestamp_with_zone.time
+ end
ensure
- ActiveRecord::Base.default_timezone = old_default_tz
- ActiveRecord::Base.time_zone_aware_attributes = old_tz
@connection.reconnect!
end
def test_timestamp_with_zone_values_without_rails_time_zone_support
- old_tz = ActiveRecord::Base.time_zone_aware_attributes
- old_default_tz = ActiveRecord::Base.default_timezone
-
- ActiveRecord::Base.time_zone_aware_attributes = false
- ActiveRecord::Base.default_timezone = :local
-
- @connection.reconnect!
-
- @first_timestamp_with_zone = PostgresqlTimestampWithZone.find(1)
- assert_equal Time.utc(2010,1,1, 11,0,0), @first_timestamp_with_zone.time
- assert_instance_of Time, @first_timestamp_with_zone.time
+ with_timezone_config default: :local, aware_attributes: false do
+ @connection.reconnect!
+ # make sure to use a non-UTC time zone
+ @connection.execute("SET time zone 'America/Jamaica'", 'SCHEMA')
+
+ @first_timestamp_with_zone = PostgresqlTimestampWithZone.find(1)
+ assert_equal Time.utc(2010,1,1, 11,0,0), @first_timestamp_with_zone.time
+ assert_instance_of Time, @first_timestamp_with_zone.time
+ end
ensure
- ActiveRecord::Base.default_timezone = old_default_tz
- ActiveRecord::Base.time_zone_aware_attributes = old_tz
@connection.reconnect!
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/hstore_test.rb b/activerecord/test/cases/adapters/postgresql/hstore_test.rb
index e434b4861c..d8782f5eaa 100644
--- a/activerecord/test/cases/adapters/postgresql/hstore_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/hstore_test.rb
@@ -7,15 +7,13 @@ require 'active_record/connection_adapters/postgresql_adapter'
class PostgresqlHstoreTest < ActiveRecord::TestCase
class Hstore < ActiveRecord::Base
self.table_name = 'hstores'
+
+ store_accessor :settings, :language, :timezone
end
def setup
@connection = ActiveRecord::Base.connection
- unless @connection.supports_extensions?
- return skip "do not test on PG without hstore"
- end
-
unless @connection.extension_enabled?('hstore')
@connection.enable_extension 'hstore'
@connection.commit_db_transaction
@@ -26,6 +24,7 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase
@connection.transaction do
@connection.create_table('hstores') do |t|
t.hstore 'tags', :default => ''
+ t.hstore 'settings'
end
end
@column = Hstore.columns.find { |c| c.name == 'tags' }
@@ -35,166 +34,220 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase
@connection.execute 'drop table if exists hstores'
end
- def test_hstore_included_in_extensions
- assert @connection.respond_to?(:extensions), "connection should have a list of extensions"
- assert @connection.extensions.include?('hstore'), "extension list should include hstore"
- end
+ if ActiveRecord::Base.connection.supports_extensions?
+ def test_hstore_included_in_extensions
+ assert @connection.respond_to?(:extensions), "connection should have a list of extensions"
+ assert @connection.extensions.include?('hstore'), "extension list should include hstore"
+ end
- def test_disable_enable_hstore
- assert @connection.extension_enabled?('hstore')
- @connection.disable_extension 'hstore'
- assert_not @connection.extension_enabled?('hstore')
- @connection.enable_extension 'hstore'
- assert @connection.extension_enabled?('hstore')
- ensure
- # Restore column(s) dropped by `drop extension hstore cascade;`
- load_schema
- end
+ def test_disable_enable_hstore
+ assert @connection.extension_enabled?('hstore')
+ @connection.disable_extension 'hstore'
+ assert_not @connection.extension_enabled?('hstore')
+ @connection.enable_extension 'hstore'
+ assert @connection.extension_enabled?('hstore')
+ ensure
+ # Restore column(s) dropped by `drop extension hstore cascade;`
+ load_schema
+ end
- def test_column
- assert_equal :hstore, @column.type
- end
+ def test_column
+ assert_equal :hstore, @column.type
+ end
- def test_change_table_supports_hstore
- @connection.transaction do
- @connection.change_table('hstores') do |t|
- t.hstore 'users', default: ''
+ def test_change_table_supports_hstore
+ @connection.transaction do
+ @connection.change_table('hstores') do |t|
+ t.hstore 'users', default: ''
+ end
+ Hstore.reset_column_information
+ column = Hstore.columns.find { |c| c.name == 'users' }
+ assert_equal :hstore, column.type
+
+ raise ActiveRecord::Rollback # reset the schema change
end
+ ensure
Hstore.reset_column_information
- column = Hstore.columns.find { |c| c.name == 'users' }
- assert_equal :hstore, column.type
+ end
+
+ def test_hstore_migration
+ hstore_migration = Class.new(ActiveRecord::Migration) do
+ def change
+ change_table("hstores") do |t|
+ t.hstore :keys
+ end
+ end
+ end
- raise ActiveRecord::Rollback # reset the schema change
+ hstore_migration.new.suppress_messages do
+ hstore_migration.migrate(:up)
+ assert_includes @connection.columns(:hstores).map(&:name), "keys"
+ hstore_migration.migrate(:down)
+ assert_not_includes @connection.columns(:hstores).map(&:name), "keys"
+ end
end
- ensure
- Hstore.reset_column_information
- end
- def test_type_cast_hstore
- assert @column
+ def test_cast_value_on_write
+ x = Hstore.new tags: {"bool" => true, "number" => 5}
+ assert_equal({"bool" => "true", "number" => "5"}, x.tags)
+ x.save
+ assert_equal({"bool" => "true", "number" => "5"}, x.reload.tags)
+ end
- data = "\"1\"=>\"2\""
- hash = @column.class.string_to_hstore data
- assert_equal({'1' => '2'}, hash)
- assert_equal({'1' => '2'}, @column.type_cast(data))
+ def test_type_cast_hstore
+ assert @column
- assert_equal({}, @column.type_cast(""))
- assert_equal({'key'=>nil}, @column.type_cast('key => NULL'))
- assert_equal({'c'=>'}','"a"'=>'b "a b'}, @column.type_cast(%q(c=>"}", "\"a\""=>"b \"a b")))
- end
+ data = "\"1\"=>\"2\""
+ hash = @column.class.string_to_hstore data
+ assert_equal({'1' => '2'}, hash)
+ assert_equal({'1' => '2'}, @column.type_cast(data))
- def test_gen1
- assert_equal(%q(" "=>""), @column.class.hstore_to_string({' '=>''}))
- end
+ assert_equal({}, @column.type_cast(""))
+ assert_equal({'key'=>nil}, @column.type_cast('key => NULL'))
+ assert_equal({'c'=>'}','"a"'=>'b "a b'}, @column.type_cast(%q(c=>"}", "\"a\""=>"b \"a b")))
+ end
- def test_gen2
- assert_equal(%q(","=>""), @column.class.hstore_to_string({','=>''}))
- end
+ def test_with_store_accessors
+ x = Hstore.new(language: "fr", timezone: "GMT")
+ assert_equal "fr", x.language
+ assert_equal "GMT", x.timezone
- def test_gen3
- assert_equal(%q("="=>""), @column.class.hstore_to_string({'='=>''}))
- end
+ x.save!
+ x = Hstore.first
+ assert_equal "fr", x.language
+ assert_equal "GMT", x.timezone
- def test_gen4
- assert_equal(%q(">"=>""), @column.class.hstore_to_string({'>'=>''}))
- end
+ x.language = "de"
+ x.save!
- def test_parse1
- assert_equal({'a'=>nil,'b'=>nil,'c'=>'NuLl','null'=>'c'}, @column.type_cast('a=>null,b=>NuLl,c=>"NuLl",null=>c'))
- end
+ x = Hstore.first
+ assert_equal "de", x.language
+ assert_equal "GMT", x.timezone
+ end
- def test_parse2
- assert_equal({" " => " "}, @column.type_cast("\\ =>\\ "))
- end
+ def test_gen1
+ assert_equal(%q(" "=>""), @column.class.hstore_to_string({' '=>''}))
+ end
- def test_parse3
- assert_equal({"=" => ">"}, @column.type_cast("==>>"))
- end
+ def test_gen2
+ assert_equal(%q(","=>""), @column.class.hstore_to_string({','=>''}))
+ end
- def test_parse4
- assert_equal({"=a"=>"q=w"}, @column.type_cast('\=a=>q=w'))
- end
+ def test_gen3
+ assert_equal(%q("="=>""), @column.class.hstore_to_string({'='=>''}))
+ end
- def test_parse5
- assert_equal({"=a"=>"q=w"}, @column.type_cast('"=a"=>q\=w'))
- end
+ def test_gen4
+ assert_equal(%q(">"=>""), @column.class.hstore_to_string({'>'=>''}))
+ end
- def test_parse6
- assert_equal({"\"a"=>"q>w"}, @column.type_cast('"\"a"=>q>w'))
- end
+ def test_parse1
+ assert_equal({'a'=>nil,'b'=>nil,'c'=>'NuLl','null'=>'c'}, @column.type_cast('a=>null,b=>NuLl,c=>"NuLl",null=>c'))
+ end
- def test_parse7
- assert_equal({"\"a"=>"q\"w"}, @column.type_cast('\"a=>q"w'))
- end
+ def test_parse2
+ assert_equal({" " => " "}, @column.type_cast("\\ =>\\ "))
+ end
- def test_rewrite
- @connection.execute "insert into hstores (tags) VALUES ('1=>2')"
- x = Hstore.first
- x.tags = { '"a\'' => 'b' }
- assert x.save!
- end
+ def test_parse3
+ assert_equal({"=" => ">"}, @column.type_cast("==>>"))
+ end
+ def test_parse4
+ assert_equal({"=a"=>"q=w"}, @column.type_cast('\=a=>q=w'))
+ end
- def test_select
- @connection.execute "insert into hstores (tags) VALUES ('1=>2')"
- x = Hstore.first
- assert_equal({'1' => '2'}, x.tags)
- end
+ def test_parse5
+ assert_equal({"=a"=>"q=w"}, @column.type_cast('"=a"=>q\=w'))
+ end
- def test_select_multikey
- @connection.execute "insert into hstores (tags) VALUES ('1=>2,2=>3')"
- x = Hstore.first
- assert_equal({'1' => '2', '2' => '3'}, x.tags)
- end
+ def test_parse6
+ assert_equal({"\"a"=>"q>w"}, @column.type_cast('"\"a"=>q>w'))
+ end
- def test_create
- assert_cycle('a' => 'b', '1' => '2')
- end
+ def test_parse7
+ assert_equal({"\"a"=>"q\"w"}, @column.type_cast('\"a=>q"w'))
+ end
- def test_nil
- assert_cycle('a' => nil)
- end
+ def test_rewrite
+ @connection.execute "insert into hstores (tags) VALUES ('1=>2')"
+ x = Hstore.first
+ x.tags = { '"a\'' => 'b' }
+ assert x.save!
+ end
- def test_quotes
- assert_cycle('a' => 'b"ar', '1"foo' => '2')
- end
+ def test_select
+ @connection.execute "insert into hstores (tags) VALUES ('1=>2')"
+ x = Hstore.first
+ assert_equal({'1' => '2'}, x.tags)
+ end
- def test_whitespace
- assert_cycle('a b' => 'b ar', '1"foo' => '2')
- end
+ def test_select_multikey
+ @connection.execute "insert into hstores (tags) VALUES ('1=>2,2=>3')"
+ x = Hstore.first
+ assert_equal({'1' => '2', '2' => '3'}, x.tags)
+ end
- def test_backslash
- assert_cycle('a\\b' => 'b\\ar', '1"foo' => '2')
- end
+ def test_create
+ assert_cycle('a' => 'b', '1' => '2')
+ end
- def test_comma
- assert_cycle('a, b' => 'bar', '1"foo' => '2')
- end
+ def test_nil
+ assert_cycle('a' => nil)
+ end
- def test_arrow
- assert_cycle('a=>b' => 'bar', '1"foo' => '2')
- end
+ def test_quotes
+ assert_cycle('a' => 'b"ar', '1"foo' => '2')
+ end
- def test_quoting_special_characters
- assert_cycle('ca' => 'cà', 'ac' => 'àc')
- end
+ def test_whitespace
+ assert_cycle('a b' => 'b ar', '1"foo' => '2')
+ end
+
+ def test_backslash
+ assert_cycle('a\\b' => 'b\\ar', '1"foo' => '2')
+ end
- def test_multiline
- assert_cycle("a\nb" => "c\nd")
+ def test_comma
+ assert_cycle('a, b' => 'bar', '1"foo' => '2')
+ end
+
+ def test_arrow
+ assert_cycle('a=>b' => 'bar', '1"foo' => '2')
+ end
+
+ def test_quoting_special_characters
+ assert_cycle('ca' => 'cà', 'ac' => 'àc')
+ end
+
+ def test_multiline
+ assert_cycle("a\nb" => "c\nd")
+ end
+
+ def test_update_all
+ hstore = Hstore.create! tags: { "one" => "two" }
+
+ Hstore.update_all tags: { "three" => "four" }
+ assert_equal({ "three" => "four" }, hstore.reload.tags)
+
+ Hstore.update_all tags: { }
+ assert_equal({ }, hstore.reload.tags)
+ end
end
private
- def assert_cycle hash
- # test creation
- x = Hstore.create!(:tags => hash)
- x.reload
- assert_equal(hash, x.tags)
-
- # test updating
- x = Hstore.create!(:tags => {})
- x.tags = hash
- x.save!
- x.reload
- assert_equal(hash, x.tags)
- end
+
+ def assert_cycle(hash)
+ # test creation
+ x = Hstore.create!(:tags => hash)
+ x.reload
+ assert_equal(hash, x.tags)
+
+ # test updating
+ x = Hstore.create!(:tags => {})
+ x.tags = hash
+ x.save!
+ x.reload
+ assert_equal(hash, x.tags)
+ end
end
diff --git a/activerecord/test/cases/adapters/postgresql/json_test.rb b/activerecord/test/cases/adapters/postgresql/json_test.rb
index f45c7afcc0..01e7334aad 100644
--- a/activerecord/test/cases/adapters/postgresql/json_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/json_test.rb
@@ -7,6 +7,8 @@ require 'active_record/connection_adapters/postgresql_adapter'
class PostgresqlJSONTest < ActiveRecord::TestCase
class JsonDataType < ActiveRecord::Base
self.table_name = 'json_data_type'
+
+ store_accessor :settings, :resolution
end
def setup
@@ -15,6 +17,7 @@ class PostgresqlJSONTest < ActiveRecord::TestCase
@connection.transaction do
@connection.create_table('json_data_type') do |t|
t.json 'payload', :default => {}
+ t.json 'settings'
end
end
rescue ActiveRecord::StatementInvalid
@@ -46,6 +49,13 @@ class PostgresqlJSONTest < ActiveRecord::TestCase
JsonDataType.reset_column_information
end
+ def test_cast_value_on_write
+ x = JsonDataType.new payload: {"string" => "foo", :symbol => :bar}
+ 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
assert @column
@@ -97,4 +107,28 @@ class PostgresqlJSONTest < ActiveRecord::TestCase
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_update_all
+ json = JsonDataType.create! payload: { "one" => "two" }
+
+ JsonDataType.update_all payload: { "three" => "four" }
+ assert_equal({ "three" => "four" }, json.reload.payload)
+
+ JsonDataType.update_all payload: { }
+ assert_equal({ }, json.reload.payload)
+ end
end
diff --git a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb
index fb88ab7c09..131080913c 100644
--- a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb
@@ -10,6 +10,14 @@ module ActiveRecord
@connection.exec_query('create table ex(id serial primary key, number integer, data character varying(255))')
end
+ def test_bad_connection
+ assert_raise ActiveRecord::NoDatabaseError do
+ configuration = ActiveRecord::Base.configurations['arunit'].merge(database: 'should_not_exist-cinco-dog-db')
+ connection = ActiveRecord::Base.postgresql_connection(configuration)
+ connection.exec_query('drop table if exists ex')
+ end
+ end
+
def test_valid_column
column = @connection.columns('ex').find { |col| col.name == 'id' }
assert @connection.valid_type?(column.type)
@@ -62,6 +70,18 @@ module ActiveRecord
assert_equal expect, id
end
+ def test_multiline_insert_sql
+ id = @connection.insert_sql(<<-SQL)
+ insert into ex(
+ number)
+ values(
+ 5152
+ )
+ SQL
+ expect = @connection.query('select max(id) from ex').first.first
+ assert_equal expect, id
+ end
+
def test_insert_sql_with_returning_disabled
connection = connection_without_insert_returning
id = connection.insert_sql("insert into postgresql_partitioned_table_parent (number) VALUES (1)")
@@ -225,65 +245,26 @@ module ActiveRecord
assert_equal "(number > 100)", index.where
end
- def test_distinct_zero_orders
- assert_deprecated do
- assert_equal "DISTINCT posts.id",
- @connection.distinct("posts.id", [])
- end
- end
-
def test_columns_for_distinct_zero_orders
assert_equal "posts.id",
@connection.columns_for_distinct("posts.id", [])
end
- def test_distinct_one_order
- assert_deprecated do
- assert_equal "DISTINCT posts.id, posts.created_at AS alias_0",
- @connection.distinct("posts.id", ["posts.created_at desc"])
- end
- end
-
def test_columns_for_distinct_one_order
assert_equal "posts.id, posts.created_at AS alias_0",
@connection.columns_for_distinct("posts.id", ["posts.created_at desc"])
end
- def test_distinct_few_orders
- assert_deprecated do
- assert_equal "DISTINCT posts.id, posts.created_at AS alias_0, posts.position AS alias_1",
- @connection.distinct("posts.id", ["posts.created_at desc", "posts.position asc"])
- end
- end
-
def test_columns_for_distinct_few_orders
assert_equal "posts.id, posts.created_at AS alias_0, posts.position AS alias_1",
@connection.columns_for_distinct("posts.id", ["posts.created_at desc", "posts.position asc"])
end
- def test_distinct_blank_not_nil_orders
- assert_deprecated do
- assert_equal "DISTINCT posts.id, posts.created_at AS alias_0",
- @connection.distinct("posts.id", ["posts.created_at desc", "", " "])
- end
- end
-
def test_columns_for_distinct_blank_not_nil_orders
assert_equal "posts.id, posts.created_at AS alias_0",
@connection.columns_for_distinct("posts.id", ["posts.created_at desc", "", " "])
end
- def test_distinct_with_arel_order
- order = Object.new
- def order.to_sql
- "posts.created_at desc"
- end
- assert_deprecated do
- assert_equal "DISTINCT posts.id, posts.created_at AS alias_0",
- @connection.distinct("posts.id", [order])
- end
- end
-
def test_columns_for_distinct_with_arel_order
order = Object.new
def order.to_sql
@@ -293,13 +274,6 @@ module ActiveRecord
@connection.columns_for_distinct("posts.id", [order])
end
- def test_distinct_with_nulls
- assert_deprecated do
- assert_equal "DISTINCT posts.title, posts.updater_id AS alias_0", @connection.distinct("posts.title", ["posts.updater_id desc nulls first"])
- assert_equal "DISTINCT posts.title, posts.updater_id AS alias_0", @connection.distinct("posts.title", ["posts.updater_id desc nulls last"])
- end
- end
-
def test_columns_for_distinct_with_nulls
assert_equal "posts.title, posts.updater_id AS alias_0", @connection.columns_for_distinct("posts.title", ["posts.updater_id desc nulls first"])
assert_equal "posts.title, posts.updater_id AS alias_0", @connection.columns_for_distinct("posts.title", ["posts.updater_id desc nulls last"])
diff --git a/activerecord/test/cases/adapters/postgresql/quoting_test.rb b/activerecord/test/cases/adapters/postgresql/quoting_test.rb
index b3429648ee..1122f8b9a1 100644
--- a/activerecord/test/cases/adapters/postgresql/quoting_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/quoting_test.rb
@@ -47,11 +47,16 @@ module ActiveRecord
def test_quote_cast_numeric
fixnum = 666
- c = Column.new(nil, nil, 'string')
+ c = Column.new(nil, nil, 'varchar')
assert_equal "'666'", @conn.quote(fixnum, c)
c = Column.new(nil, nil, 'text')
assert_equal "'666'", @conn.quote(fixnum, c)
end
+
+ def test_quote_time_usec
+ assert_equal "'1970-01-01 00:00:00.000000'", @conn.quote(Time.at(0))
+ assert_equal "'1970-01-01 00:00:00.000000'", @conn.quote(Time.at(0).to_datetime)
+ end
end
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/range_test.rb b/activerecord/test/cases/adapters/postgresql/range_test.rb
new file mode 100644
index 0000000000..a56b8ac791
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/range_test.rb
@@ -0,0 +1,245 @@
+require "cases/helper"
+require 'active_record/base'
+require 'active_record/connection_adapters/postgresql_adapter'
+
+if ActiveRecord::Base.connection.supports_ranges?
+ class PostgresqlRange < ActiveRecord::Base
+ self.table_name = "postgresql_ranges"
+ end
+
+ class PostgresqlRangeTest < ActiveRecord::TestCase
+ def teardown
+ @connection.execute 'DROP TABLE IF EXISTS postgresql_ranges'
+ end
+
+ def setup
+ @connection = ActiveRecord::Base.connection
+ begin
+ @connection.transaction do
+ @connection.create_table('postgresql_ranges') do |t|
+ t.daterange :date_range
+ t.numrange :num_range
+ t.tsrange :ts_range
+ t.tstzrange :tstz_range
+ t.int4range :int4_range
+ t.int8range :int8_range
+ end
+ end
+ rescue ActiveRecord::StatementInvalid
+ return skip "do not test on PG without range"
+ end
+
+ insert_range(id: 101,
+ date_range: "[''2012-01-02'', ''2012-01-04'']",
+ num_range: "[0.1, 0.2]",
+ ts_range: "[''2010-01-01 14:30'', ''2011-01-01 14:30'']",
+ tstz_range: "[''2010-01-01 14:30:00+05'', ''2011-01-01 14:30:00-03'']",
+ int4_range: "[1, 10]",
+ int8_range: "[10, 100]")
+
+ insert_range(id: 102,
+ date_range: "(''2012-01-02'', ''2012-01-04'')",
+ num_range: "[0.1, 0.2)",
+ ts_range: "[''2010-01-01 14:30'', ''2011-01-01 14:30'')",
+ tstz_range: "[''2010-01-01 14:30:00+05'', ''2011-01-01 14:30:00-03'')",
+ int4_range: "(1, 10)",
+ int8_range: "(10, 100)")
+
+ insert_range(id: 103,
+ date_range: "(''2012-01-02'',]",
+ num_range: "[0.1,]",
+ ts_range: "[''2010-01-01 14:30'',]",
+ tstz_range: "[''2010-01-01 14:30:00+05'',]",
+ int4_range: "(1,]",
+ int8_range: "(10,]")
+
+ insert_range(id: 104,
+ date_range: "[,]",
+ num_range: "[,]",
+ ts_range: "[,]",
+ tstz_range: "[,]",
+ int4_range: "[,]",
+ int8_range: "[,]")
+
+ insert_range(id: 105,
+ date_range: "(''2012-01-02'', ''2012-01-02'')",
+ num_range: "(0.1, 0.1)",
+ ts_range: "(''2010-01-01 14:30'', ''2010-01-01 14:30'')",
+ tstz_range: "(''2010-01-01 14:30:00+05'', ''2010-01-01 06:30:00-03'')",
+ int4_range: "(1, 1)",
+ int8_range: "(10, 10)")
+
+ @new_range = PostgresqlRange.new
+ @first_range = PostgresqlRange.find(101)
+ @second_range = PostgresqlRange.find(102)
+ @third_range = PostgresqlRange.find(103)
+ @fourth_range = PostgresqlRange.find(104)
+ @empty_range = PostgresqlRange.find(105)
+ end
+
+ def test_data_type_of_range_types
+ assert_equal :daterange, @first_range.column_for_attribute(:date_range).type
+ assert_equal :numrange, @first_range.column_for_attribute(:num_range).type
+ assert_equal :tsrange, @first_range.column_for_attribute(:ts_range).type
+ assert_equal :tstzrange, @first_range.column_for_attribute(:tstz_range).type
+ assert_equal :int4range, @first_range.column_for_attribute(:int4_range).type
+ assert_equal :int8range, @first_range.column_for_attribute(:int8_range).type
+ end
+
+ def test_int4range_values
+ assert_equal 1...11, @first_range.int4_range
+ assert_equal 2...10, @second_range.int4_range
+ assert_equal 2...Float::INFINITY, @third_range.int4_range
+ assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.int4_range)
+ assert_nil @empty_range.int4_range
+ end
+
+ def test_int8range_values
+ assert_equal 10...101, @first_range.int8_range
+ assert_equal 11...100, @second_range.int8_range
+ assert_equal 11...Float::INFINITY, @third_range.int8_range
+ assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.int8_range)
+ assert_nil @empty_range.int8_range
+ end
+
+ def test_daterange_values
+ assert_equal Date.new(2012, 1, 2)...Date.new(2012, 1, 5), @first_range.date_range
+ assert_equal Date.new(2012, 1, 3)...Date.new(2012, 1, 4), @second_range.date_range
+ assert_equal Date.new(2012, 1, 3)...Float::INFINITY, @third_range.date_range
+ assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.date_range)
+ assert_nil @empty_range.date_range
+ end
+
+ def test_numrange_values
+ assert_equal BigDecimal.new('0.1')..BigDecimal.new('0.2'), @first_range.num_range
+ assert_equal BigDecimal.new('0.1')...BigDecimal.new('0.2'), @second_range.num_range
+ assert_equal BigDecimal.new('0.1')...BigDecimal.new('Infinity'), @third_range.num_range
+ assert_equal BigDecimal.new('-Infinity')...BigDecimal.new('Infinity'), @fourth_range.num_range
+ assert_nil @empty_range.num_range
+ end
+
+ def test_tsrange_values
+ tz = ::ActiveRecord::Base.default_timezone
+ assert_equal Time.send(tz, 2010, 1, 1, 14, 30, 0)..Time.send(tz, 2011, 1, 1, 14, 30, 0), @first_range.ts_range
+ assert_equal Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2011, 1, 1, 14, 30, 0), @second_range.ts_range
+ assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.ts_range)
+ assert_nil @empty_range.ts_range
+ end
+
+ def test_tstzrange_values
+ assert_equal Time.parse('2010-01-01 09:30:00 UTC')..Time.parse('2011-01-01 17:30:00 UTC'), @first_range.tstz_range
+ assert_equal Time.parse('2010-01-01 09:30:00 UTC')...Time.parse('2011-01-01 17:30:00 UTC'), @second_range.tstz_range
+ assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.tstz_range)
+ assert_nil @empty_range.tstz_range
+ end
+
+ def test_create_tstzrange
+ tstzrange = Time.parse('2010-01-01 14:30:00 +0100')...Time.parse('2011-02-02 14:30:00 CDT')
+ round_trip(@new_range, :tstz_range, tstzrange)
+ assert_equal @new_range.tstz_range, tstzrange
+ assert_equal @new_range.tstz_range, Time.parse('2010-01-01 13:30:00 UTC')...Time.parse('2011-02-02 19:30:00 UTC')
+ end
+
+ def test_update_tstzrange
+ assert_equal_round_trip(@first_range, :tstz_range,
+ Time.parse('2010-01-01 14:30:00 CDT')...Time.parse('2011-02-02 14:30:00 CET'))
+ assert_nil_round_trip(@first_range, :tstz_range,
+ Time.parse('2010-01-01 14:30:00 +0100')...Time.parse('2010-01-01 13:30:00 +0000'))
+ end
+
+ def test_create_tsrange
+ tz = ::ActiveRecord::Base.default_timezone
+ assert_equal_round_trip(@new_range, :ts_range,
+ Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2011, 2, 2, 14, 30, 0))
+ end
+
+ def test_update_tsrange
+ tz = ::ActiveRecord::Base.default_timezone
+ assert_equal_round_trip(@first_range, :ts_range,
+ Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2011, 2, 2, 14, 30, 0))
+ assert_nil_round_trip(@first_range, :ts_range,
+ Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2010, 1, 1, 14, 30, 0))
+ end
+
+ def test_create_numrange
+ assert_equal_round_trip(@new_range, :num_range,
+ BigDecimal.new('0.5')...BigDecimal.new('1'))
+ end
+
+ def test_update_numrange
+ assert_equal_round_trip(@first_range, :num_range,
+ BigDecimal.new('0.5')...BigDecimal.new('1'))
+ assert_nil_round_trip(@first_range, :num_range,
+ BigDecimal.new('0.5')...BigDecimal.new('0.5'))
+ end
+
+ def test_create_daterange
+ assert_equal_round_trip(@new_range, :date_range,
+ Range.new(Date.new(2012, 1, 1), Date.new(2013, 1, 1), true))
+ end
+
+ def test_update_daterange
+ assert_equal_round_trip(@first_range, :date_range,
+ Date.new(2012, 2, 3)...Date.new(2012, 2, 10))
+ assert_nil_round_trip(@first_range, :date_range,
+ Date.new(2012, 2, 3)...Date.new(2012, 2, 3))
+ end
+
+ def test_create_int4range
+ assert_equal_round_trip(@new_range, :int4_range, Range.new(3, 50, true))
+ end
+
+ def test_update_int4range
+ assert_equal_round_trip(@first_range, :int4_range, 6...10)
+ assert_nil_round_trip(@first_range, :int4_range, 3...3)
+ end
+
+ def test_create_int8range
+ assert_equal_round_trip(@new_range, :int8_range, Range.new(30, 50, true))
+ end
+
+ def test_update_int8range
+ assert_equal_round_trip(@first_range, :int8_range, 60000...10000000)
+ assert_nil_round_trip(@first_range, :int8_range, 39999...39999)
+ end
+
+ private
+ def assert_equal_round_trip(range, attribute, value)
+ round_trip(range, attribute, value)
+ assert_equal value, range.public_send(attribute)
+ end
+
+ def assert_nil_round_trip(range, attribute, value)
+ round_trip(range, attribute, value)
+ assert_nil range.public_send(attribute)
+ end
+
+ def round_trip(range, attribute, value)
+ range.public_send "#{attribute}=", value
+ assert range.save
+ assert range.reload
+ end
+
+ def insert_range(values)
+ @connection.execute <<-SQL
+ INSERT INTO postgresql_ranges (
+ id,
+ date_range,
+ num_range,
+ ts_range,
+ tstz_range,
+ int4_range,
+ int8_range
+ ) VALUES (
+ #{values[:id]},
+ '#{values[:date_range]}',
+ '#{values[:num_range]}',
+ '#{values[:ts_range]}',
+ '#{values[:tstz_range]}',
+ '#{values[:int4_range]}',
+ '#{values[:int8_range]}'
+ )
+ SQL
+ end
+ end
+end
diff --git a/activerecord/test/cases/adapters/postgresql/statement_pool_test.rb b/activerecord/test/cases/adapters/postgresql/statement_pool_test.rb
index f1c4b85126..1497b0abc7 100644
--- a/activerecord/test/cases/adapters/postgresql/statement_pool_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/statement_pool_test.rb
@@ -1,38 +1,40 @@
require 'cases/helper'
-module ActiveRecord::ConnectionAdapters
- class PostgreSQLAdapter < AbstractAdapter
- class InactivePGconn
- def query(*args)
- raise PGError
- end
+module ActiveRecord
+ module ConnectionAdapters
+ class PostgreSQLAdapter < AbstractAdapter
+ class InactivePGconn
+ def query(*args)
+ raise PGError
+ end
- def status
- PGconn::CONNECTION_BAD
+ def status
+ PGconn::CONNECTION_BAD
+ end
end
- end
- class StatementPoolTest < ActiveRecord::TestCase
- def test_cache_is_per_pid
- return skip('must support fork') unless Process.respond_to?(:fork)
+ class StatementPoolTest < ActiveRecord::TestCase
+ if Process.respond_to?(:fork)
+ def test_cache_is_per_pid
+ cache = StatementPool.new nil, 10
+ cache['foo'] = 'bar'
+ assert_equal 'bar', cache['foo']
- cache = StatementPool.new nil, 10
- cache['foo'] = 'bar'
- assert_equal 'bar', cache['foo']
+ pid = fork {
+ lookup = cache['foo'];
+ exit!(!lookup)
+ }
- pid = fork {
- lookup = cache['foo'];
- exit!(!lookup)
- }
-
- Process.waitpid pid
- assert $?.success?, 'process should exit successfully'
- end
+ Process.waitpid pid
+ assert $?.success?, 'process should exit successfully'
+ end
+ end
- def test_dealloc_does_not_raise_on_inactive_connection
- cache = StatementPool.new InactivePGconn.new, 10
- cache['foo'] = 'bar'
- assert_nothing_raised { cache.clear }
+ def test_dealloc_does_not_raise_on_inactive_connection
+ cache = StatementPool.new InactivePGconn.new, 10
+ cache['foo'] = 'bar'
+ assert_nothing_raised { cache.clear }
+ end
end
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/timestamp_test.rb b/activerecord/test/cases/adapters/postgresql/timestamp_test.rb
index dbc69a529c..89210866f0 100644
--- a/activerecord/test/cases/adapters/postgresql/timestamp_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/timestamp_test.rb
@@ -12,10 +12,6 @@ class TimestampTest < ActiveRecord::TestCase
end
def test_load_infinity_and_beyond
- unless current_adapter?(:PostgreSQLAdapter)
- return skip("only tested on postgresql")
- end
-
d = Developer.find_by_sql("select 'infinity'::timestamp as updated_at")
assert d.first.updated_at.infinite?, 'timestamp should be infinite'
@@ -26,10 +22,6 @@ class TimestampTest < ActiveRecord::TestCase
end
def test_save_infinity_and_beyond
- unless current_adapter?(:PostgreSQLAdapter)
- return skip("only tested on postgresql")
- end
-
d = Developer.create!(:name => 'aaron', :updated_at => 1.0 / 0.0)
assert_equal(1.0 / 0.0, d.updated_at)
@@ -85,9 +77,6 @@ class TimestampTest < ActiveRecord::TestCase
end
def test_bc_timestamp
- unless current_adapter?(:PostgreSQLAdapter)
- return skip("only tested on postgresql")
- end
date = Date.new(0) - 1.second
Developer.create!(:name => "aaron", :updated_at => date)
assert_equal date, Developer.find_by_name("aaron").updated_at
@@ -109,5 +98,4 @@ class TimestampTest < ActiveRecord::TestCase
end
result && result.send(option)
end
-
end
diff --git a/activerecord/test/cases/adapters/postgresql/uuid_test.rb b/activerecord/test/cases/adapters/postgresql/uuid_test.rb
index b573d48807..3f5d981444 100644
--- a/activerecord/test/cases/adapters/postgresql/uuid_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/uuid_test.rb
@@ -12,10 +12,6 @@ class PostgresqlUUIDTest < ActiveRecord::TestCase
def setup
@connection = ActiveRecord::Base.connection
- unless @connection.supports_extensions?
- return skip "do not test on PG without uuid-ossp"
- end
-
unless @connection.extension_enabled?('uuid-ossp')
@connection.enable_extension 'uuid-ossp'
@connection.commit_db_transaction
@@ -24,7 +20,7 @@ class PostgresqlUUIDTest < ActiveRecord::TestCase
@connection.reconnect!
@connection.transaction do
- @connection.create_table('pg_uuids', id: :uuid) do |t|
+ @connection.create_table('pg_uuids', id: :uuid, default: 'uuid_generate_v1()') do |t|
t.string 'name'
t.uuid 'other_uuid', default: 'uuid_generate_v4()'
end
@@ -35,32 +31,35 @@ class PostgresqlUUIDTest < ActiveRecord::TestCase
@connection.execute 'drop table if exists pg_uuids'
end
- def test_id_is_uuid
- assert_equal :uuid, UUID.columns_hash['id'].type
- assert UUID.primary_key
- end
+ if ActiveRecord::Base.connection.supports_extensions?
+ def test_id_is_uuid
+ assert_equal :uuid, UUID.columns_hash['id'].type
+ assert UUID.primary_key
+ end
- def test_id_has_a_default
- u = UUID.create
- assert_not_nil u.id
- end
+ def test_id_has_a_default
+ u = UUID.create
+ assert_not_nil u.id
+ end
- def test_auto_create_uuid
- u = UUID.create
- u.reload
- assert_not_nil u.other_uuid
- end
+ def test_auto_create_uuid
+ u = UUID.create
+ u.reload
+ assert_not_nil u.other_uuid
+ end
- def test_pk_and_sequence_for_uuid_primary_key
- pk, seq = @connection.pk_and_sequence_for('pg_uuids')
- assert_equal 'id', pk
- assert_equal nil, seq
- end
+ def test_pk_and_sequence_for_uuid_primary_key
+ pk, seq = @connection.pk_and_sequence_for('pg_uuids')
+ assert_equal 'id', pk
+ assert_equal nil, seq
+ end
- def test_schema_dumper_for_uuid_primary_key
- schema = StringIO.new
- ActiveRecord::SchemaDumper.dump(@connection, schema)
- assert_match(/\bcreate_table "pg_uuids", id: :uuid\b/, schema.string)
+ def test_schema_dumper_for_uuid_primary_key
+ schema = StringIO.new
+ ActiveRecord::SchemaDumper.dump(@connection, schema)
+ assert_match(/\bcreate_table "pg_uuids", id: :uuid, default: "uuid_generate_v1\(\)"/, schema.string)
+ assert_match(/t\.uuid "other_uuid", default: "uuid_generate_v4\(\)"/, schema.string)
+ end
end
end
@@ -73,6 +72,11 @@ class PostgresqlUUIDTestNilDefault < ActiveRecord::TestCase
@connection = ActiveRecord::Base.connection
@connection.reconnect!
+ unless @connection.extension_enabled?('uuid-ossp')
+ @connection.enable_extension 'uuid-ossp'
+ @connection.commit_db_transaction
+ end
+
@connection.transaction do
@connection.create_table('pg_uuids', id: false) do |t|
t.primary_key :id, :uuid, default: nil
@@ -85,11 +89,60 @@ class PostgresqlUUIDTestNilDefault < ActiveRecord::TestCase
@connection.execute 'drop table if exists pg_uuids'
end
- def test_id_allows_default_override_via_nil
- col_desc = @connection.execute("SELECT pg_get_expr(d.adbin, d.adrelid) as default
+ if ActiveRecord::Base.connection.supports_extensions?
+ def test_id_allows_default_override_via_nil
+ col_desc = @connection.execute("SELECT pg_get_expr(d.adbin, d.adrelid) as default
FROM pg_attribute a
LEFT JOIN pg_attrdef d ON a.attrelid = d.adrelid AND a.attnum = d.adnum
WHERE a.attname='id' AND a.attrelid = 'pg_uuids'::regclass").first
- assert_nil col_desc["default"]
+ assert_nil col_desc["default"]
+ end
+ end
+end
+
+class PostgresqlUUIDTestInverseOf < ActiveRecord::TestCase
+ class UuidPost < ActiveRecord::Base
+ self.table_name = 'pg_uuid_posts'
+ has_many :uuid_comments, inverse_of: :uuid_post
+ end
+
+ class UuidComment < ActiveRecord::Base
+ self.table_name = 'pg_uuid_comments'
+ belongs_to :uuid_post
+ end
+
+ def setup
+ @connection = ActiveRecord::Base.connection
+ @connection.reconnect!
+
+ unless @connection.extension_enabled?('uuid-ossp')
+ @connection.enable_extension 'uuid-ossp'
+ @connection.commit_db_transaction
+ end
+
+ @connection.transaction do
+ @connection.create_table('pg_uuid_posts', id: :uuid) do |t|
+ t.string 'title'
+ end
+ @connection.create_table('pg_uuid_comments', id: :uuid) do |t|
+ t.uuid :uuid_post_id, default: 'uuid_generate_v4()'
+ t.string 'content'
+ end
+ end
+ end
+
+ def teardown
+ @connection.transaction do
+ @connection.execute 'drop table if exists pg_uuid_comments'
+ @connection.execute 'drop table if exists pg_uuid_posts'
+ end
+ end
+
+ if ActiveRecord::Base.connection.supports_extensions?
+ def test_collection_association_with_uuid
+ post = UuidPost.create!
+ comment = post.uuid_comments.create!
+ assert post.uuid_comments.find(comment.id)
+ end
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/xml_test.rb b/activerecord/test/cases/adapters/postgresql/xml_test.rb
new file mode 100644
index 0000000000..bf14b378d8
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/xml_test.rb
@@ -0,0 +1,38 @@
+# encoding: utf-8
+
+require 'cases/helper'
+require 'active_record/base'
+require 'active_record/connection_adapters/postgresql_adapter'
+
+class PostgresqlXMLTest < ActiveRecord::TestCase
+ class XmlDataType < ActiveRecord::Base
+ self.table_name = 'xml_data_type'
+ end
+
+ def setup
+ @connection = ActiveRecord::Base.connection
+ begin
+ @connection.transaction do
+ @connection.create_table('xml_data_type') do |t|
+ t.xml 'payload', default: {}
+ end
+ end
+ rescue ActiveRecord::StatementInvalid
+ return skip "do not test on PG without xml"
+ end
+ @column = XmlDataType.columns.find { |c| c.name == 'payload' }
+ end
+
+ def teardown
+ @connection.execute 'drop table if exists xml_data_type'
+ end
+
+ def test_column
+ assert_equal :xml, @column.type
+ end
+
+ def test_null_xml
+ @connection.execute %q|insert into xml_data_type (payload) VALUES(null)|
+ assert_nil XmlDataType.first.payload
+ end
+end
diff --git a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb
index a7b2764fc1..ba89487838 100644
--- a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb
+++ b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb
@@ -95,6 +95,13 @@ module ActiveRecord
end
}.new
assert_equal 10, @conn.type_cast(quoted_id_obj, nil)
+
+ quoted_id_obj = Class.new {
+ def quoted_id
+ "'zomg'"
+ end
+ }
+ assert_raise(TypeError) { @conn.type_cast(quoted_id_obj, nil) }
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 1c2d5cdaf2..b82dd7b04b 100644
--- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb
+++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb
@@ -1,6 +1,7 @@
# encoding: utf-8
require "cases/helper"
require 'models/owner'
+require 'tempfile'
module ActiveRecord
module ConnectionAdapters
@@ -21,8 +22,36 @@ module ActiveRecord
)
eosql
- @conn.extend(LogIntercepter)
- @conn.intercepted = true
+ @subscriber = SQLSubscriber.new
+ ActiveSupport::Notifications.subscribe('sql.active_record', @subscriber)
+ end
+
+ def test_bad_connection
+ assert_raise ActiveRecord::NoDatabaseError do
+ connection = ActiveRecord::Base.sqlite3_connection(adapter: "sqlite3", database: "/tmp/should/_not/_exist/-cinco-dog.db")
+ connection.exec_query('drop table if exists ex')
+ end
+ end
+
+ def test_connect_with_url
+ original_connection = ActiveRecord::Base.remove_connection
+ tf = Tempfile.open 'whatever'
+ url = "sqlite3://#{tf.path}"
+ ActiveRecord::Base.establish_connection(url)
+ assert ActiveRecord::Base.connection
+ ensure
+ tf.close
+ tf.unlink
+ ActiveRecord::Base.establish_connection(original_connection)
+ end
+
+ def test_connect_memory_with_url
+ original_connection = ActiveRecord::Base.remove_connection
+ url = "sqlite3:///:memory:"
+ ActiveRecord::Base.establish_connection(url)
+ assert ActiveRecord::Base.connection
+ ensure
+ ActiveRecord::Base.establish_connection(original_connection)
end
def test_valid_column
@@ -31,16 +60,16 @@ module ActiveRecord
end
# sqlite databases should be able to support any type and not
- # just the ones mentioned in the native_database_types.
- # Therefore test_invalid column should always return true
+ # just the ones mentioned in the native_database_types.
+ # Therefore test_invalid column should always return true
# even if the type is not valid.
def test_invalid_column
assert @conn.valid_type?(:foobar)
end
def teardown
- @conn.intercepted = false
- @conn.logged = []
+ ActiveSupport::Notifications.unsubscribe(@subscriber)
+ super
end
def test_column_types
@@ -255,7 +284,7 @@ module ActiveRecord
def test_tables_logs_name
assert_logged [['SCHEMA', []]] do
@conn.tables('hello')
- assert_not_nil @conn.logged.first.shift
+ assert_not_nil @subscriber.logged.first.shift
end
end
@@ -267,7 +296,7 @@ module ActiveRecord
def test_table_exists_logs_name
assert @conn.table_exists?('items')
- assert_equal 'SCHEMA', @conn.logged[0][1]
+ assert_equal 'SCHEMA', @subscriber.logged[0][1]
end
def test_columns
@@ -305,10 +334,10 @@ module ActiveRecord
end
def test_indexes_logs
- assert_difference('@conn.logged.length') do
+ assert_difference('@subscriber.logged.length') do
@conn.indexes('items')
end
- assert_match(/items/, @conn.logged.last.first)
+ assert_match(/items/, @subscriber.logged.last.first)
end
def test_no_indexes
@@ -353,11 +382,23 @@ module ActiveRecord
assert_nil @conn.primary_key('failboat')
end
+ def test_supports_extensions
+ assert_not @conn.supports_extensions?, 'does not support extensions'
+ end
+
+ def test_respond_to_enable_extension
+ assert @conn.respond_to?(:enable_extension)
+ end
+
+ def test_respond_to_disable_extension
+ assert @conn.respond_to?(:disable_extension)
+ end
+
private
def assert_logged logs
yield
- assert_equal logs, @conn.logged
+ assert_equal logs, @subscriber.logged
end
end
diff --git a/activerecord/test/cases/adapters/sqlite3/sqlite3_create_folder_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_create_folder_test.rb
new file mode 100644
index 0000000000..f545fc2011
--- /dev/null
+++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_create_folder_test.rb
@@ -0,0 +1,21 @@
+# encoding: utf-8
+require "cases/helper"
+require 'models/owner'
+
+module ActiveRecord
+ module ConnectionAdapters
+ class SQLite3CreateFolder < ActiveRecord::TestCase
+ def test_sqlite_creates_directory
+ Dir.mktmpdir do |dir|
+ dir = Pathname.new(dir)
+ @conn = Base.sqlite3_connection :database => dir.join("db/foo.sqlite3"),
+ :adapter => 'sqlite3',
+ :timeout => 100
+
+ assert Dir.exist? dir.join('db')
+ assert File.exist? dir.join('db/foo.sqlite3')
+ 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 2f04c60a9a..fd0044ac05 100644
--- a/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb
+++ b/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb
@@ -3,20 +3,21 @@ require 'cases/helper'
module ActiveRecord::ConnectionAdapters
class SQLite3Adapter
class StatementPoolTest < ActiveRecord::TestCase
- def test_cache_is_per_pid
- return skip('must support fork') unless Process.respond_to?(:fork)
+ if Process.respond_to?(:fork)
+ def test_cache_is_per_pid
- cache = StatementPool.new nil, 10
- cache['foo'] = 'bar'
- assert_equal 'bar', cache['foo']
+ cache = StatementPool.new nil, 10
+ cache['foo'] = 'bar'
+ assert_equal 'bar', cache['foo']
- pid = fork {
- lookup = cache['foo'];
- exit!(!lookup)
- }
+ pid = fork {
+ lookup = cache['foo'];
+ exit!(!lookup)
+ }
- Process.waitpid pid
- assert $?.success?, 'process should exit successfully'
+ Process.waitpid pid
+ assert $?.success?, 'process should exit successfully'
+ end
end
end
end
diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb
index 87af24cbe6..3205d0c28b 100644
--- a/activerecord/test/cases/associations/belongs_to_associations_test.rb
+++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb
@@ -1,4 +1,4 @@
-require "cases/helper"
+require 'cases/helper'
require 'models/developer'
require 'models/project'
require 'models/company'
@@ -14,6 +14,8 @@ require 'models/sponsor'
require 'models/member'
require 'models/essay'
require 'models/toy'
+require 'models/invoice'
+require 'models/line_item'
class BelongsToAssociationsTest < ActiveRecord::TestCase
fixtures :accounts, :companies, :developers, :projects, :topics,
@@ -324,6 +326,53 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
assert_equal 1, Topic.find(topic.id)[:replies_count]
end
+ def test_belongs_to_with_touch_option_on_touch
+ line_item = LineItem.create!
+ Invoice.create!(line_items: [line_item])
+
+ assert_queries(1) { line_item.touch }
+ end
+
+ def test_belongs_to_with_touch_option_on_touch_and_removed_parent
+ line_item = LineItem.create!
+ Invoice.create!(line_items: [line_item])
+
+ line_item.invoice = nil
+
+ assert_queries(2) { line_item.touch }
+ end
+
+ def test_belongs_to_with_touch_option_on_update
+ line_item = LineItem.create!
+ Invoice.create!(line_items: [line_item])
+
+ assert_queries(2) { line_item.update amount: 10 }
+ end
+
+ def test_belongs_to_with_touch_option_on_destroy
+ line_item = LineItem.create!
+ Invoice.create!(line_items: [line_item])
+
+ assert_queries(2) { line_item.destroy }
+ end
+
+ def test_belongs_to_with_touch_option_on_destroy_with_destroyed_parent
+ line_item = LineItem.create!
+ invoice = Invoice.create!(line_items: [line_item])
+ invoice.destroy
+
+ assert_queries(1) { line_item.destroy }
+ end
+
+ def test_belongs_to_with_touch_option_on_touch_and_reassigned_parent
+ line_item = LineItem.create!
+ Invoice.create!(line_items: [line_item])
+
+ line_item.invoice = Invoice.create!
+
+ assert_queries(3) { line_item.touch }
+ end
+
def test_belongs_to_counter_after_update
topic = Topic.create!(title: "37s")
topic.replies.create!(title: "re: 37s", content: "rails")
@@ -338,7 +387,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
topic.replies.create!(:title => "re: 37s", :content => "rails")
assert_equal 1, Topic.find(topic.id)[:replies_count]
- topic.update_columns(content: "rails is wonderfull")
+ topic.update_columns(content: "rails is wonderful")
assert_equal 1, Topic.find(topic.id)[:replies_count]
end
@@ -391,8 +440,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
def test_dont_find_target_when_foreign_key_is_null
tagging = taggings(:thinking_general)
- queries = assert_sql { tagging.super_tag }
- assert_equal 0, queries.length
+ assert_queries(0) { tagging.super_tag }
end
def test_field_name_same_as_foreign_key
@@ -538,6 +586,19 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
assert_nil essay.writer_id
end
+ def test_polymorphic_assignment_with_nil
+ essay = Essay.new
+ assert_nil essay.writer_id
+ assert_nil essay.writer_type
+
+ essay.writer_id = 1
+ essay.writer_type = 'Author'
+
+ essay.writer = nil
+ assert_nil essay.writer_id
+ assert_nil essay.writer_type
+ end
+
def test_belongs_to_proxy_should_not_respond_to_private_methods
assert_raise(NoMethodError) { companies(:first_firm).private_method }
assert_raise(NoMethodError) { companies(:second_client).firm.private_method }
@@ -565,6 +626,8 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
end
def test_dependent_delete_and_destroy_with_belongs_to
+ AuthorAddress.destroyed_author_address_ids.clear
+
author_address = author_addresses(:david_address)
author_address_extra = author_addresses(:david_address_extra)
assert_equal [], AuthorAddress.destroyed_author_address_ids
@@ -577,16 +640,11 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
assert_equal [author_address.id], AuthorAddress.destroyed_author_address_ids
end
- def test_invalid_belongs_to_dependent_option_nullify_raises_exception
- assert_raise ArgumentError do
+ def test_belongs_to_invalid_dependent_option_raises_exception
+ error = assert_raise ArgumentError do
Class.new(Author).belongs_to :special_author_address, :dependent => :nullify
end
- end
-
- def test_invalid_belongs_to_dependent_option_restrict_raises_exception
- assert_raise ArgumentError do
- Class.new(Author).belongs_to :special_author_address, :dependent => :restrict
- end
+ assert_equal error.message, 'The :dependent option must be one of [:destroy, :delete], but is :nullify'
end
def test_attributes_are_being_set_when_initialized_from_belongs_to_association_with_where_clause
diff --git a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb
index e693d34f99..71c0609df5 100644
--- a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb
+++ b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb
@@ -52,12 +52,10 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase
def test_cascaded_eager_association_loading_with_join_for_count
categories = Category.joins(:categorizations).includes([{:posts=>:comments}, :authors])
- assert_nothing_raised do
- assert_equal 4, categories.count
- assert_equal 4, categories.to_a.count
- assert_equal 3, categories.distinct.count
- assert_equal 3, categories.to_a.uniq.size # Must uniq since instantiating with inner joins will get dupes
- end
+ assert_equal 4, categories.count
+ assert_equal 4, categories.to_a.count
+ assert_equal 3, categories.distinct.count
+ assert_equal 3, categories.to_a.uniq.size # Must uniq since instantiating with inner joins will get dupes
end
def test_cascaded_eager_association_loading_with_duplicated_includes
@@ -176,4 +174,15 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase
sink = Vertex.all.merge!(:includes=>{:sources=>{:sources=>{:sources=>:sources}}}, :order => 'vertices.id DESC').first
assert_equal vertices(:vertex_1), assert_no_queries { sink.sources.first.sources.first.sources.first.sources.first }
end
+
+ def test_eager_association_loading_with_cascaded_interdependent_one_level_and_two_levels
+ authors_relation = Author.all.merge!(includes: [:comments, { posts: :categorizations }], order: "authors.id")
+ authors = authors_relation.to_a
+ assert_equal 3, authors.size
+ assert_equal 10, authors[0].comments.size
+ assert_equal 1, authors[1].comments.size
+ assert_equal 5, authors[0].posts.size
+ assert_equal 3, authors[1].posts.size
+ assert_equal 3, authors[0].posts.collect { |post| post.categorizations.size }.inject(0) { |sum, i| sum+i }
+ end
end
diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb
index 4aa6567d85..498a4e8144 100644
--- a/activerecord/test/cases/associations/eager_test.rb
+++ b/activerecord/test/cases/associations/eager_test.rb
@@ -4,6 +4,7 @@ require 'models/tagging'
require 'models/tag'
require 'models/comment'
require 'models/author'
+require 'models/essay'
require 'models/category'
require 'models/company'
require 'models/person'
@@ -24,7 +25,7 @@ require 'models/categorization'
require 'models/sponsor'
class EagerAssociationTest < ActiveRecord::TestCase
- fixtures :posts, :comments, :authors, :author_addresses, :categories, :categories_posts,
+ fixtures :posts, :comments, :authors, :essays, :author_addresses, :categories, :categories_posts,
:companies, :accounts, :tags, :taggings, :people, :readers, :categorizations,
:owners, :pets, :author_favorites, :jobs, :references, :subscribers, :subscriptions, :books,
:developers, :projects, :developers_projects, :members, :memberships, :clubs, :sponsors
@@ -250,7 +251,8 @@ class EagerAssociationTest < ActiveRecord::TestCase
assert_nil Post.all.merge!(:includes => :author).find(posts(:authorless).id).author
end
- def test_nested_loading_with_no_associations
+ # Regression test for 21c75e5
+ def test_nested_loading_does_not_raise_exception_when_association_does_not_exist
assert_nothing_raised do
Post.all.merge!(:includes => {:author => :author_addresss}).find(posts(:authorless).id)
end
@@ -345,9 +347,7 @@ class EagerAssociationTest < ActiveRecord::TestCase
def test_eager_association_loading_with_belongs_to_and_conditions_string_with_unquoted_table_name
assert_nothing_raised do
- ActiveSupport::Deprecation.silence do
- Comment.all.merge!(:includes => :post, :where => ['posts.id = ?',4]).to_a
- end
+ Comment.includes(:post).references(:posts).where('posts.id = ?', 4)
end
end
@@ -366,9 +366,7 @@ class EagerAssociationTest < ActiveRecord::TestCase
def test_eager_association_loading_with_belongs_to_and_conditions_string_with_quoted_table_name
quoted_posts_id= Comment.connection.quote_table_name('posts') + '.' + Comment.connection.quote_column_name('id')
assert_nothing_raised do
- ActiveSupport::Deprecation.silence do
- Comment.all.merge!(:includes => :post, :where => ["#{quoted_posts_id} = ?",4]).to_a
- end
+ Comment.includes(:post).references(:posts).where("#{quoted_posts_id} = ?", 4)
end
end
@@ -381,9 +379,7 @@ class EagerAssociationTest < ActiveRecord::TestCase
def test_eager_association_loading_with_belongs_to_and_order_string_with_quoted_table_name
quoted_posts_id= Comment.connection.quote_table_name('posts') + '.' + Comment.connection.quote_column_name('id')
assert_nothing_raised do
- ActiveSupport::Deprecation.silence do
- Comment.all.merge!(:includes => :post, :order => quoted_posts_id).to_a
- end
+ Comment.includes(:post).references(:posts).order(quoted_posts_id)
end
end
@@ -547,15 +543,11 @@ class EagerAssociationTest < ActiveRecord::TestCase
end
def test_eager_with_has_many_and_limit_and_conditions_array_on_the_eagers
- posts = ActiveSupport::Deprecation.silence do
- Post.all.merge!(:includes => [ :author, :comments ], :limit => 2, :where => [ "authors.name = ?", 'David' ]).to_a
- end
+ posts = Post.includes(:author, :comments).limit(2).references(:author).where("authors.name = ?", 'David')
assert_equal 2, posts.size
- count = ActiveSupport::Deprecation.silence do
- Post.count(:include => [ :author, :comments ], :limit => 2, :conditions => [ "authors.name = ?", 'David' ])
- end
- assert_equal count, posts.size
+ count = Post.includes(:author, :comments).limit(2).references(:author).where("authors.name = ?", 'David').count
+ assert_equal posts.size, count
end
def test_eager_with_has_many_and_limit_and_high_offset
@@ -756,6 +748,8 @@ class EagerAssociationTest < ActiveRecord::TestCase
end
def test_eager_with_default_scope_as_block
+ # warm up the habtm cache
+ EagerDeveloperWithBlockDefaultScope.where(:name => 'David').first.projects
developer = EagerDeveloperWithBlockDefaultScope.where(:name => 'David').first
projects = Project.order(:id).to_a
assert_no_queries do
@@ -1145,6 +1139,10 @@ class EagerAssociationTest < ActiveRecord::TestCase
end
def test_deep_including_through_habtm
+ # warm up habtm cache
+ posts = Post.all.merge!(:includes => {:categories => :categorizations}, :order => "posts.id").to_a
+ posts[0].categories[0].categorizations.length
+
posts = Post.all.merge!(:includes => {:categories => :categorizations}, :order => "posts.id").to_a
assert_no_queries { assert_equal 2, posts[0].categories[0].categorizations.length }
assert_no_queries { assert_equal 1, posts[0].categories[1].categorizations.length }
@@ -1181,8 +1179,19 @@ class EagerAssociationTest < ActiveRecord::TestCase
}
end
- test "works in combination with order(:symbol)" do
- author = Author.includes(:posts).references(:posts).order(:name).where('posts.title IS NOT NULL').first
+ test "works in combination with order(:symbol) and reorder(:symbol)" do
+ author = Author.includes(:posts).references(:posts).order(:name).find_by('posts.title IS NOT NULL')
assert_equal authors(:bob), author
+
+ author = Author.includes(:posts).references(:posts).reorder(:name).find_by('posts.title IS NOT NULL')
+ assert_equal authors(:bob), author
+ end
+
+ test "preloading with a polymorphic association and using the existential predicate" do
+ assert_equal authors(:david), authors(:david).essays.includes(:writer).first.writer
+
+ assert_nothing_raised do
+ authors(:david).essays.includes(:writer).any?
+ end
end
end
diff --git a/activerecord/test/cases/associations/extension_test.rb b/activerecord/test/cases/associations/extension_test.rb
index da767a2a7e..4c1fdfdd9a 100644
--- a/activerecord/test/cases/associations/extension_test.rb
+++ b/activerecord/test/cases/associations/extension_test.rb
@@ -59,9 +59,11 @@ class AssociationsExtensionsTest < ActiveRecord::TestCase
end
def test_extension_name
- assert_equal 'DeveloperAssociationNameAssociationExtension', extension_name(Developer)
- assert_equal 'MyApplication::Business::DeveloperAssociationNameAssociationExtension', extension_name(MyApplication::Business::Developer)
- assert_equal 'MyApplication::Business::DeveloperAssociationNameAssociationExtension', extension_name(MyApplication::Business::Developer)
+ extend!(Developer)
+ extend!(MyApplication::Business::Developer)
+
+ assert Object.const_get 'DeveloperAssociationNameAssociationExtension'
+ assert MyApplication::Business.const_get 'DeveloperAssociationNameAssociationExtension'
end
def test_proxy_association_after_scoped
@@ -72,9 +74,8 @@ class AssociationsExtensionsTest < ActiveRecord::TestCase
private
- def extension_name(model)
+ def extend!(model)
builder = ActiveRecord::Associations::Builder::HasMany.new(model, :association_name, nil, {}) { }
- builder.send(:wrap_block_extension)
- builder.extension_module.name
+ builder.define_extensions(model)
end
end
diff --git a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb
index 84bdca3a97..8aee7ff40e 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
@@ -65,19 +65,6 @@ class DeveloperWithSymbolsForKeys < ActiveRecord::Base
:foreign_key => "developer_id"
end
-class DeveloperWithCounterSQL < ActiveRecord::Base
- self.table_name = 'developers'
-
- ActiveSupport::Deprecation.silence do
- has_and_belongs_to_many :projects,
- :class_name => "DeveloperWithCounterSQL",
- :join_table => "developers_projects",
- :association_foreign_key => "project_id",
- :foreign_key => "developer_id",
- :counter_sql => proc { "SELECT COUNT(*) AS count_all FROM projects INNER JOIN developers_projects ON projects.id = developers_projects.project_id WHERE developers_projects.developer_id =#{id}" }
- end
-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
@@ -364,31 +351,6 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
assert_equal 0, david.projects(true).size
end
- def test_deleting_with_sql
- david = Developer.find(1)
- active_record = Project.find(1)
- active_record.developers.reload
- assert_equal 3, active_record.developers_by_sql.size
-
- active_record.developers_by_sql.delete(david)
- assert_equal 2, active_record.developers_by_sql(true).size
- end
-
- def test_deleting_array_with_sql
- active_record = Project.find(1)
- active_record.developers.reload
- assert_equal 3, active_record.developers_by_sql.size
-
- active_record.developers_by_sql.delete(Developer.all)
- assert_equal 0, active_record.developers_by_sql(true).size
- end
-
- def test_deleting_all_with_sql
- project = Project.find(1)
- project.developers_by_sql.delete_all
- assert_equal 0, project.developers_by_sql.size
- end
-
def test_deleting_all
david = Developer.find(1)
david.projects.reload
@@ -475,13 +437,6 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
assert george.treasures(true).empty?
end
- def test_deprecated_push_with_attributes_was_removed
- jamis = developers(:jamis)
- assert_raise(NoMethodError) do
- jamis.projects.push_with_attributes(projects(:action_controller), :joined_on => Date.today)
- end
- end
-
def test_associations_with_conditions
assert_equal 3, projects(:active_record).developers.size
assert_equal 1, projects(:active_record).developers_named_david.size
@@ -537,25 +492,6 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
assert ! project.developers.include?(developer)
end
- def test_find_in_association_with_custom_finder_sql
- assert_equal developers(:david), projects(:active_record).developers_with_finder_sql.find(developers(:david).id), "SQL find"
-
- active_record = projects(:active_record)
- active_record.developers_with_finder_sql.reload
- assert_equal developers(:david), active_record.developers_with_finder_sql.find(developers(:david).id), "Ruby find"
- end
-
- def test_find_in_association_with_custom_finder_sql_and_multiple_interpolations
- # interpolate once:
- assert_equal [developers(:david), developers(:jamis), developers(:poor_jamis)], projects(:active_record).developers_with_finder_sql, "first interpolation"
- # interpolate again, for a different project id
- assert_equal [developers(:david)], projects(:action_controller).developers_with_finder_sql, "second interpolation"
- end
-
- def test_find_in_association_with_custom_finder_sql_and_string_id
- assert_equal developers(:david), projects(:active_record).developers_with_finder_sql.find(developers(:david).id.to_s), "SQL find"
- end
-
def test_find_with_merged_options
assert_equal 1, projects(:active_record).limited_developers.size
assert_equal 1, projects(:active_record).limited_developers.to_a.size
@@ -570,9 +506,9 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
assert_equal high_id_jamis, projects(:active_record).developers.find_by_name('Jamis')
end
- def test_find_should_prepend_to_association_order
+ def test_find_should_append_to_association_order
ordered_developers = projects(:active_record).developers.order('projects.id')
- assert_equal ['projects.id', 'developers.name desc, developers.id desc'], ordered_developers.order_values
+ assert_equal ['developers.name desc, developers.id desc', 'projects.id'], ordered_developers.order_values
end
def test_dynamic_find_all_should_respect_readonly_access
@@ -634,6 +570,13 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
assert !developer.special_projects.include?(other_project)
end
+ def test_symbol_join_table
+ developer = Developer.first
+ sp = developer.sym_special_projects.create("name" => "omg")
+ developer.reload
+ assert_includes developer.sym_special_projects, sp
+ end
+
def test_update_attributes_after_push_without_duplicate_join_table_rows
developer = Developer.new("name" => "Kano")
project = SpecialProject.create("name" => "Special Project")
@@ -669,16 +612,24 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
end
def test_join_table_alias
+ # FIXME: `references` has no impact on the aliases generated for the join
+ # query. The fact that we pass `:developers_projects_join` to `references`
+ # and that the SQL string contains `developers_projects_join` is merely a
+ # coincidence.
assert_equal(
3,
Developer.references(:developers_projects_join).merge(
:includes => {:projects => :developers},
- :where => 'developers_projects_join.joined_on IS NOT NULL'
+ :where => 'projects_developers_projects_join.joined_on IS NOT NULL'
).to_a.size
)
end
def test_join_with_group
+ # FIXME: `references` has no impact on the aliases generated for the join
+ # query. The fact that we pass `:developers_projects_join` to `references`
+ # and that the SQL string contains `developers_projects_join` is merely a
+ # coincidence.
group = Developer.columns.inject([]) do |g, c|
g << "developers.#{c.name}"
g << "developers_projects_2.#{c.name}"
@@ -688,7 +639,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
assert_equal(
3,
Developer.references(:developers_projects_join).merge(
- :includes => {:projects => :developers}, :where => 'developers_projects_join.joined_on IS NOT NULL',
+ :includes => {:projects => :developers}, :where => 'projects_developers_projects_join.joined_on IS NOT NULL',
:group => group.join(",")
).to_a.size
)
@@ -702,12 +653,12 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
end
def test_find_scoped_grouped
- assert_equal 5, categories(:general).posts_grouped_by_title.size
- assert_equal 1, categories(:technology).posts_grouped_by_title.size
+ assert_equal 5, categories(:general).posts_grouped_by_title.to_a.size
+ assert_equal 1, categories(:technology).posts_grouped_by_title.to_a.size
end
def test_find_scoped_grouped_having
- assert_equal 2, projects(:active_record).well_payed_salary_groups.size
+ assert_equal 2, projects(:active_record).well_payed_salary_groups.to_a.size
assert projects(:active_record).well_payed_salary_groups.all? { |g| g.salary > 10000 }
end
@@ -774,12 +725,6 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
assert_equal project, developer.projects.first
end
- def test_self_referential_habtm_without_foreign_key_set_should_raise_exception
- assert_raise(ActiveRecord::HasAndBelongsToManyAssociationForeignKeyNeeded) {
- SelfMember.new.friends
- }
- end
-
def test_dynamic_find_should_respect_association_include
# SQL error in sort clause if :include is not included
# due to Unknown column 'authors.id'
@@ -791,21 +736,6 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
assert_equal 2, david.projects.count
end
- def test_count_with_counter_sql
- developer = DeveloperWithCounterSQL.create(:name => 'tekin')
- developer.project_ids = [projects(:active_record).id]
- developer.save
- developer.reload
- assert_equal 1, developer.projects.count
- end
-
- unless current_adapter?(:PostgreSQLAdapter)
- def test_count_with_finder_sql
- assert_equal 3, projects(:active_record).developers_with_finder_sql.count
- assert_equal 3, projects(:active_record).developers_with_multiline_finder_sql.count
- end
- end
-
def test_association_proxy_transaction_method_starts_transaction_in_association_class
Post.expects(:transaction)
Category.first.posts.transaction do
@@ -845,18 +775,6 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
assert project.developers.include?(developer)
end
- test ":insert_sql is deprecated" do
- klass = Class.new(ActiveRecord::Base)
- def klass.name; 'Foo'; end
- assert_deprecated { klass.has_and_belongs_to_many :posts, :insert_sql => 'lol' }
- end
-
- test ":delete_sql is deprecated" do
- klass = Class.new(ActiveRecord::Base)
- def klass.name; 'Foo'; end
- assert_deprecated { klass.has_and_belongs_to_many :posts, :delete_sql => 'lol' }
- end
-
test "has and belongs to many associations on new records use null relations" do
projects = Developer.new.projects
assert_no_queries do
diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb
index d85570236f..e45efb0161 100644
--- a/activerecord/test/cases/associations/has_many_associations_test.rb
+++ b/activerecord/test/cases/associations/has_many_associations_test.rb
@@ -23,79 +23,6 @@ require 'models/categorization'
require 'models/minivan'
require 'models/speedometer'
-class HasManyAssociationsTestForCountWithFinderSql < ActiveRecord::TestCase
- class Invoice < ActiveRecord::Base
- ActiveSupport::Deprecation.silence do
- has_many :custom_line_items, :class_name => 'LineItem', :finder_sql => "SELECT line_items.* from line_items"
- end
- end
- def test_should_fail
- assert_raise(ArgumentError) do
- Invoice.create.custom_line_items.count(:conditions => {:amount => 0})
- end
- end
-end
-
-class HasManyAssociationsTestForCountWithCountSql < ActiveRecord::TestCase
- class Invoice < ActiveRecord::Base
- ActiveSupport::Deprecation.silence do
- has_many :custom_line_items, :class_name => 'LineItem', :counter_sql => "SELECT COUNT(*) line_items.* from line_items"
- end
- end
- def test_should_fail
- assert_raise(ArgumentError) do
- Invoice.create.custom_line_items.count(:conditions => {:amount => 0})
- end
- end
-end
-
-class HasManyAssociationsTestForCountWithVariousFinderSqls < ActiveRecord::TestCase
- class Invoice < ActiveRecord::Base
- ActiveSupport::Deprecation.silence do
- has_many :custom_line_items, :class_name => 'LineItem', :finder_sql => "SELECT DISTINCT line_items.amount from line_items"
- has_many :custom_full_line_items, :class_name => 'LineItem', :finder_sql => "SELECT line_items.invoice_id, line_items.amount from line_items"
- has_many :custom_star_line_items, :class_name => 'LineItem', :finder_sql => "SELECT * from line_items"
- has_many :custom_qualified_star_line_items, :class_name => 'LineItem', :finder_sql => "SELECT line_items.* from line_items"
- end
- end
-
- def test_should_count_distinct_results
- invoice = Invoice.new
- invoice.custom_line_items << LineItem.new(:amount => 0)
- invoice.custom_line_items << LineItem.new(:amount => 0)
- invoice.save!
-
- assert_equal 1, invoice.custom_line_items.count
- end
-
- def test_should_count_results_with_multiple_fields
- invoice = Invoice.new
- invoice.custom_full_line_items << LineItem.new(:amount => 0)
- invoice.custom_full_line_items << LineItem.new(:amount => 0)
- invoice.save!
-
- assert_equal 2, invoice.custom_full_line_items.count
- end
-
- def test_should_count_results_with_star
- invoice = Invoice.new
- invoice.custom_star_line_items << LineItem.new(:amount => 0)
- invoice.custom_star_line_items << LineItem.new(:amount => 0)
- invoice.save!
-
- assert_equal 2, invoice.custom_star_line_items.count
- end
-
- def test_should_count_results_with_qualified_star
- invoice = Invoice.new
- invoice.custom_qualified_star_line_items << LineItem.new(:amount => 0)
- invoice.custom_qualified_star_line_items << LineItem.new(:amount => 0)
- invoice.save!
-
- assert_equal 2, invoice.custom_qualified_star_line_items.count
- end
-end
-
class HasManyAssociationsTestForReorderWithJoinDependency < ActiveRecord::TestCase
fixtures :authors, :posts, :comments
@@ -118,6 +45,24 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
Client.destroyed_client_ids.clear
end
+ def test_anonymous_has_many
+ developer = Class.new(ActiveRecord::Base) {
+ self.table_name = 'developers'
+ dev = self
+
+ developer_project = Class.new(ActiveRecord::Base) {
+ self.table_name = 'developers_projects'
+ belongs_to :developer, :class => dev
+ }
+ has_many :developer_projects, :class => developer_project, :foreign_key => 'developer_id'
+ }
+ dev = developer.first
+ named = Developer.find(dev.id)
+ assert_operator dev.developer_projects.count, :>, 0
+ assert_equal named.projects.map(&:id).sort,
+ dev.developer_projects.map(&:project_id).sort
+ end
+
def test_create_from_association_should_respect_default_scope
car = Car.create(:name => 'honda')
assert_equal 'honda', car.name
@@ -135,6 +80,13 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal 'exotic', bulb.name
end
+ def test_build_from_association_should_respect_scope
+ author = Author.new
+
+ post = author.thinking_posts.build
+ assert_equal 'So I was thinking', post.title
+ end
+
def test_create_from_association_with_nil_values_should_work
car = Car.create(:name => 'honda')
@@ -148,6 +100,13 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal 'defaulty', bulb.name
end
+ def test_do_not_call_callbacks_for_delete_all
+ car = Car.create(:name => 'honda')
+ car.funky_bulbs.create!
+ assert_nothing_raised { car.reload.funky_bulbs.delete_all }
+ assert_equal 0, Bulb.count, "bulbs should have been deleted using :delete_all strategy"
+ end
+
def test_building_the_associated_object_with_implicit_sti_base_class
firm = DependentFirm.new
company = firm.companies.build
@@ -176,6 +135,16 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_raise(ActiveRecord::SubclassNotFound) { firm.companies.build(:type => "Account") }
end
+ test "building the association with an array" do
+ speedometer = Speedometer.new(speedometer_id: "a")
+ data = [{name: "first"}, {name: "second"}]
+ speedometer.minivans.build(data)
+
+ assert_equal 2, speedometer.minivans.size
+ assert speedometer.save
+ assert_equal ["first", "second"], speedometer.reload.minivans.map(&:name)
+ end
+
def test_association_keys_bypass_attribute_protection
car = Car.create(:name => 'honda')
@@ -308,9 +277,9 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal 2, companies(:first_firm).limited_clients.limit(nil).to_a.size
end
- def test_find_should_prepend_to_association_order
+ def test_find_should_append_to_association_order
ordered_clients = companies(:first_firm).clients_sorted_desc.order('companies.id')
- assert_equal ['companies.id', 'id DESC'], ordered_clients.order_values
+ assert_equal ['id DESC', 'companies.id'], ordered_clients.order_values
end
def test_dynamic_find_should_respect_association_order
@@ -347,42 +316,9 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal "Summit", Firm.all.merge!(:order => "id").first.clients_using_primary_key.first.name
end
- def test_finding_using_sql
- firm = Firm.order("id").first
- first_client = firm.clients_using_sql.first
- assert_not_nil first_client
- assert_equal "Microsoft", first_client.name
- assert_equal 1, firm.clients_using_sql.size
- assert_equal 1, Firm.order("id").first.clients_using_sql.size
- end
-
- def test_finding_using_sql_take_into_account_only_uniq_ids
- firm = Firm.order("id").first
- client = firm.clients_using_sql.first
- assert_equal client, firm.clients_using_sql.find(client.id, client.id)
- assert_equal client, firm.clients_using_sql.find(client.id, client.id.to_s)
- end
-
- def test_counting_using_sql
- assert_equal 1, Firm.order("id").first.clients_using_counter_sql.size
- assert Firm.order("id").first.clients_using_counter_sql.any?
- assert_equal 0, Firm.order("id").first.clients_using_zero_counter_sql.size
- assert !Firm.order("id").first.clients_using_zero_counter_sql.any?
- end
-
- def test_counting_non_existant_items_using_sql
- assert_equal 0, Firm.order("id").first.no_clients_using_counter_sql.size
- end
-
- def test_counting_using_finder_sql
- assert_equal 2, Firm.find(4).clients_using_sql.count
- end
-
def test_belongs_to_sanity
c = Client.new
- assert_nil c.firm
-
- flunk "belongs_to failed if check" if c.firm
+ assert_nil c.firm, "belongs_to failed sanity check on new object"
end
def test_find_ids
@@ -405,20 +341,16 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_raise(ActiveRecord::RecordNotFound) { firm.clients.find(2, 99) }
end
- def test_find_string_ids_when_using_finder_sql
- firm = Firm.order("id").first
+ def test_find_ids_and_inverse_of
+ force_signal37_to_load_all_clients_of_firm
- client = firm.clients_using_finder_sql.find("2")
+ firm = companies(:first_firm)
+ client = firm.clients_of_firm.find(3)
assert_kind_of Client, client
- client_ary = firm.clients_using_finder_sql.find(["2"])
+ client_ary = firm.clients_of_firm.find([3])
assert_kind_of Array, client_ary
assert_equal client, client_ary.first
-
- client_ary = firm.clients_using_finder_sql.find("2", "3")
- assert_kind_of Array, client_ary
- assert_equal 2, client_ary.size
- assert client_ary.include?(client)
end
def test_find_all
@@ -523,9 +455,17 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
def test_select_query_method
- assert_equal ['id'], posts(:welcome).comments.select(:id).first.attributes.keys
+ assert_equal ['id', 'body'], posts(:welcome).comments.select(:id, :body).first.attributes.keys
end
+ def test_select_with_block
+ assert_equal [1], posts(:welcome).comments.select { |c| c.id == 1 }.map(&:id)
+ end
+
+ def test_select_without_foreign_key
+ assert_equal companies(:first_firm).accounts.first.credit_limit, companies(:first_firm).accounts.select(:credit_limit).first.credit_limit
+ end
+
def test_adding
force_signal37_to_load_all_clients_of_firm
natural = Client.new("name" => "Natural Company")
@@ -602,6 +542,13 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
end
+ def test_inverse_on_before_validate
+ firm = companies(:first_firm)
+ assert_queries(1) do
+ firm.clients_of_firm << Client.new("name" => "Natural Company")
+ end
+ end
+
def test_new_aliased_to_build
company = companies(:first_firm)
new_client = assert_no_queries { company.clients_of_firm.new("name" => "Another Client") }
@@ -840,13 +787,13 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_delete_all
force_signal37_to_load_all_clients_of_firm
- companies(:first_firm).clients_of_firm.create("name" => "Another Client")
- clients = companies(:first_firm).clients_of_firm.to_a
+ companies(:first_firm).dependent_clients_of_firm.create("name" => "Another Client")
+ clients = companies(:first_firm).dependent_clients_of_firm.to_a
assert_equal 2, clients.count
- deleted = companies(:first_firm).clients_of_firm.delete_all
- assert_equal clients.sort_by(&:id), deleted.sort_by(&:id)
- assert_equal 0, companies(:first_firm).clients_of_firm.size
- assert_equal 0, companies(:first_firm).clients_of_firm(true).size
+
+ assert_difference "Client.count", -(clients.count) do
+ companies(:first_firm).dependent_clients_of_firm.delete_all
+ end
end
def test_delete_all_with_not_yet_loaded_association_collection
@@ -920,18 +867,33 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
firm = companies(:first_firm)
client_id = firm.dependent_clients_of_firm.first.id
assert_equal 1, firm.dependent_clients_of_firm.size
+ assert_equal 1, Client.find_by_id(client_id).client_of
- # :dependent means destroy is called on each client
+ # :delete_all is called on each client since the dependent options is :destroy
firm.dependent_clients_of_firm.clear
assert_equal 0, firm.dependent_clients_of_firm.size
assert_equal 0, firm.dependent_clients_of_firm(true).size
- assert_equal [client_id], Client.destroyed_client_ids[firm.id]
+ assert_equal [], Client.destroyed_client_ids[firm.id]
# Should be destroyed since the association is dependent.
assert_nil Client.find_by_id(client_id)
end
+ def test_delete_all_with_option_delete_all
+ firm = companies(:first_firm)
+ client_id = firm.dependent_clients_of_firm.first.id
+ firm.dependent_clients_of_firm.delete_all(:delete_all)
+ assert_nil Client.find_by_id(client_id)
+ end
+
+ def test_delete_all_accepts_limited_parameters
+ firm = companies(:first_firm)
+ assert_raise(ArgumentError) do
+ firm.dependent_clients_of_firm.delete_all(:destroy)
+ end
+ end
+
def test_clearing_an_exclusively_dependent_association_collection
firm = companies(:first_firm)
client_id = firm.exclusively_dependent_clients_of_firm.first.id
@@ -1179,21 +1141,6 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal num_accounts, Account.count
end
- def test_restrict
- firm = RestrictedFirm.create!(:name => 'restrict')
- firm.companies.create(:name => 'child')
-
- assert !firm.companies.empty?
- assert_raise(ActiveRecord::DeleteRestrictionError) { firm.destroy }
- assert RestrictedFirm.exists?(:name => 'restrict')
- assert firm.companies.exists?(:name => 'child')
- end
-
- def test_restrict_is_deprecated
- klass = Class.new(ActiveRecord::Base)
- assert_deprecated { klass.has_many :posts, dependent: :restrict }
- end
-
def test_restrict_with_exception
firm = RestrictedWithExceptionFirm.create!(:name => 'restrict')
firm.companies.create(:name => 'child')
@@ -1220,14 +1167,14 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
def test_included_in_collection
- assert companies(:first_firm).clients.include?(Client.find(2))
+ assert_equal true, companies(:first_firm).clients.include?(Client.find(2))
end
def test_included_in_collection_for_new_records
client = Client.create(:name => 'Persisted')
assert_nil client.client_of
- assert !Firm.new.clients_of_firm.include?(client),
- 'includes a client that does not belong to any firm'
+ assert_equal false, Firm.new.clients_of_firm.include?(client),
+ 'includes a client that does not belong to any firm'
end
def test_adding_array_and_collection
@@ -1254,7 +1201,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
firm.save
firm.reload
assert_equal 2, firm.clients.length
- assert !firm.clients.include?(:first_client)
+ assert_equal false, firm.clients.include?(:first_client)
end
def test_replace_failure
@@ -1315,24 +1262,44 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal [readers(:michael_welcome).id], posts(:welcome).readers_with_person_ids
end
- def test_get_ids_for_unloaded_finder_sql_associations_loads_them
- company = companies(:first_firm)
- assert !company.clients_using_sql.loaded?
- assert_equal [companies(:second_client).id], company.clients_using_sql_ids
- assert company.clients_using_sql.loaded?
- end
-
def test_get_ids_for_ordered_association
assert_equal [companies(:second_client).id, companies(:first_client).id], companies(:first_firm).clients_ordered_by_name_ids
end
+ def test_get_ids_for_association_on_new_record_does_not_try_to_find_records
+ Company.columns # Load schema information so we don't query below
+ Contract.columns # if running just this test.
+
+ company = Company.new
+ assert_queries(0) do
+ company.contract_ids
+ end
+
+ assert_equal [], company.contract_ids
+ end
+
+ def test_set_ids_for_association_on_new_record_applies_association_correctly
+ contract_a = Contract.create!
+ contract_b = Contract.create!
+ Contract.create! # another contract
+ company = Company.new(:name => "Some Company")
+
+ company.contract_ids = [contract_a.id, contract_b.id]
+ assert_equal [contract_a.id, contract_b.id], company.contract_ids
+ assert_equal [contract_a, contract_b], company.contracts
+
+ company.save!
+ assert_equal company, contract_a.reload.company
+ assert_equal company, contract_b.reload.company
+ end
+
def test_assign_ids_ignoring_blanks
firm = Firm.create!(:name => 'Apple')
firm.client_ids = [companies(:first_client).id, nil, companies(:second_client).id, '']
firm.save!
assert_equal 2, firm.clients(true).size
- assert firm.clients.include?(companies(:second_client))
+ assert_equal true, firm.clients.include?(companies(:second_client))
end
def test_get_ids_for_through
@@ -1366,7 +1333,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_no_queries do
assert firm.clients.loaded?
- assert firm.clients.include?(client)
+ assert_equal true, firm.clients.include?(client)
end
end
@@ -1377,28 +1344,17 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
firm.reload
assert ! firm.clients.loaded?
assert_queries(1) do
- assert firm.clients.include?(client)
+ assert_equal true, firm.clients.include?(client)
end
assert ! firm.clients.loaded?
end
- def test_include_loads_collection_if_target_uses_finder_sql
- firm = companies(:first_firm)
- client = firm.clients_using_sql.first
-
- firm.reload
- assert ! firm.clients_using_sql.loaded?
- assert firm.clients_using_sql.include?(client)
- assert firm.clients_using_sql.loaded?
- end
-
-
def test_include_returns_false_for_non_matching_record_to_verify_scoping
firm = companies(:first_firm)
client = Client.create!(:name => 'Not Associated')
assert ! firm.clients.loaded?
- assert ! firm.clients.include?(client)
+ assert_equal false, firm.clients.include?(client)
end
def test_calling_first_or_last_on_association_should_not_load_association
@@ -1472,6 +1428,14 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal david.essays, Essay.where(writer_id: "David")
end
+ def test_has_many_assignment_with_custom_primary_key
+ david = people(:david)
+
+ assert_equal ["A Modest Proposal"], david.essays.map(&:name)
+ david.essays = [Essay.create!(name: "Remote Work" )]
+ assert_equal ["Remote Work"], david.essays.map(&:name)
+ end
+
def test_blank_custom_primary_key_on_new_record_should_not_run_queries
author = Author.new
assert !author.essays.loaded?
@@ -1481,15 +1445,17 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
end
- def test_calling_first_or_last_with_integer_on_association_should_load_association
+ def test_calling_first_or_last_with_integer_on_association_should_not_load_association
firm = companies(:first_firm)
+ firm.clients.create(:name => 'Foo')
+ assert !firm.clients.loaded?
- assert_queries 1 do
+ assert_queries 2 do
firm.clients.first(2)
firm.clients.last(2)
end
- assert firm.clients.loaded?
+ assert !firm.clients.loaded?
end
def test_calling_many_should_count_instead_of_loading_association
@@ -1605,7 +1571,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_include_method_in_has_many_association_should_return_true_for_instance_added_with_build
post = Post.new
comment = post.comments.build
- assert post.comments.include?(comment)
+ assert_equal true, post.comments.include?(comment)
end
def test_load_target_respects_protected_attributes
@@ -1675,6 +1641,22 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal car.id, bulb.attributes_after_initialize['car_id']
end
+ def test_attributes_are_set_when_initialized_from_has_many_null_relationship
+ car = Car.new name: 'honda'
+ bulb = car.bulbs.where(name: 'headlight').first_or_initialize
+ assert_equal 'headlight', bulb.name
+ end
+
+ def test_attributes_are_set_when_initialized_from_polymorphic_has_many_null_relationship
+ post = Post.new title: 'title', body: 'bar'
+ tag = Tag.create!(name: 'foo')
+
+ tagging = post.taggings.where(tag: tag).first_or_initialize
+
+ assert_equal tag.id, tagging.tag_id
+ assert_equal 'Post', tagging.taggable_type
+ end
+
def test_replace
car = Car.create(:name => 'honda')
bulb1 = car.bulbs.create
@@ -1729,22 +1711,6 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
end
- test ":finder_sql is deprecated" do
- klass = Class.new(ActiveRecord::Base)
- assert_deprecated { klass.has_many :foo, :finder_sql => 'lol' }
- end
-
- test ":counter_sql is deprecated" do
- klass = Class.new(ActiveRecord::Base)
- assert_deprecated { klass.has_many :foo, :counter_sql => 'lol' }
- end
-
- test "sum calculation with block for array compatibility is deprecated" do
- assert_deprecated do
- posts(:welcome).comments.sum { |c| c.id }
- end
- end
-
test "has many associations on new records use null relations" do
post = Post.new
@@ -1797,4 +1763,32 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal 1, speedometer.minivans.to_a.size, "Only one association should be present:\n#{speedometer.minivans.to_a}"
assert_equal 1, speedometer.reload.minivans.to_a.size
end
+
+ test "can unscope the default scope of the associated model" do
+ car = Car.create!
+ bulb1 = Bulb.create! name: "defaulty", car: car
+ bulb2 = Bulb.create! name: "other", car: car
+
+ assert_equal [bulb1], car.bulbs
+ assert_equal [bulb1, bulb2], car.all_bulbs.sort_by(&:id)
+ end
+
+ test "raises RecordNotDestroyed when replaced child can't be destroyed" do
+ car = Car.create!
+ original_child = FailedBulb.create!(car: car)
+
+ assert_raise(ActiveRecord::RecordNotDestroyed) do
+ car.failed_bulbs = [FailedBulb.create!]
+ end
+
+ assert_equal [original_child], car.reload.failed_bulbs
+ end
+
+ test 'updates counter cache when default scope is given' do
+ topic = DefaultRejectedTopic.create approved: true
+
+ assert_difference "topic.reload.replies_count", 1 do
+ topic.approved_replies.create!
+ end
+ 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 70c6b489aa..47592f312e 100644
--- a/activerecord/test/cases/associations/has_many_through_associations_test.rb
+++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb
@@ -5,6 +5,7 @@ require 'models/reference'
require 'models/job'
require 'models/reader'
require 'models/comment'
+require 'models/rating'
require 'models/tag'
require 'models/tagging'
require 'models/author'
@@ -27,7 +28,8 @@ require 'models/club'
class HasManyThroughAssociationsTest < ActiveRecord::TestCase
fixtures :posts, :readers, :people, :comments, :authors, :categories, :taggings, :tags,
:owners, :pets, :toys, :jobs, :references, :companies, :members, :author_addresses,
- :subscribers, :books, :subscriptions, :developers, :categorizations, :essays
+ :subscribers, :books, :subscriptions, :developers, :categorizations, :essays,
+ :categories_posts, :clubs, :memberships
# Dummies to force column loads so query counts are clean.
def setup
@@ -35,6 +37,136 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
Reader.create :person_id => 0, :post_id => 0
end
+ def test_preload_sti_rhs_class
+ developers = Developer.includes(:firms).all.to_a
+ assert_no_queries do
+ developers.each { |d| d.firms }
+ end
+ end
+
+ def test_preload_sti_middle_relation
+ club = Club.create!(name: 'Aaron cool banana club')
+ member1 = Member.create!(name: 'Aaron')
+ member2 = Member.create!(name: 'Cat')
+
+ SuperMembership.create! club: club, member: member1
+ CurrentMembership.create! club: club, member: member2
+
+ club1 = Club.includes(:members).find_by_id club.id
+ assert_equal [member1, member2].sort_by(&:id),
+ club1.members.sort_by(&:id)
+ end
+
+ def make_model(name)
+ Class.new(ActiveRecord::Base) { define_singleton_method(:name) { name } }
+ end
+
+ def test_ordered_habtm
+ person_prime = Class.new(ActiveRecord::Base) do
+ def self.name; 'Person'; end
+
+ has_many :readers
+ has_many :posts, -> { order('posts.id DESC') }, :through => :readers
+ end
+ posts = person_prime.includes(:posts).first.posts
+
+ assert_operator posts.length, :>, 1
+ posts.each_cons(2) do |left,right|
+ assert_operator left.id, :>, right.id
+ end
+ end
+
+ def test_singleton_has_many_through
+ book = make_model "Book"
+ subscription = make_model "Subscription"
+ subscriber = make_model "Subscriber"
+
+ subscriber.primary_key = 'nick'
+ subscription.belongs_to :book, class: book
+ subscription.belongs_to :subscriber, class: subscriber
+
+ book.has_many :subscriptions, class: subscription
+ book.has_many :subscribers, through: :subscriptions, class: subscriber
+
+ anonbook = book.first
+ namebook = Book.find anonbook.id
+
+ assert_operator anonbook.subscribers.count, :>, 0
+ anonbook.subscribers.each do |s|
+ assert_instance_of subscriber, s
+ end
+ assert_equal namebook.subscribers.map(&:id).sort,
+ anonbook.subscribers.map(&:id).sort
+ end
+
+ def test_no_pk_join_table_append
+ lesson, _, student = make_no_pk_hm_t
+
+ sicp = lesson.new(:name => "SICP")
+ ben = student.new(:name => "Ben Bitdiddle")
+ sicp.students << ben
+ assert sicp.save!
+ end
+
+ def test_no_pk_join_table_delete
+ lesson, lesson_student, student = make_no_pk_hm_t
+
+ sicp = lesson.new(:name => "SICP")
+ ben = student.new(:name => "Ben Bitdiddle")
+ louis = student.new(:name => "Louis Reasoner")
+ sicp.students << ben
+ sicp.students << louis
+ assert sicp.save!
+
+ sicp.students.reload
+ assert_operator lesson_student.count, :>=, 2
+ assert_no_difference('student.count') do
+ assert_difference('lesson_student.count', -2) do
+ sicp.students.destroy(*student.all.to_a)
+ end
+ end
+ end
+
+ def test_no_pk_join_model_callbacks
+ lesson, lesson_student, student = make_no_pk_hm_t
+
+ after_destroy_called = false
+ lesson_student.after_destroy do
+ after_destroy_called = true
+ end
+
+ sicp = lesson.new(:name => "SICP")
+ ben = student.new(:name => "Ben Bitdiddle")
+ sicp.students << ben
+ assert sicp.save!
+
+ sicp.students.reload
+ sicp.students.destroy(*student.all.to_a)
+ assert after_destroy_called, "after destroy should be called"
+ end
+
+ def make_no_pk_hm_t
+ lesson = make_model 'Lesson'
+ student = make_model 'Student'
+
+ lesson_student = make_model 'LessonStudent'
+ lesson_student.table_name = 'lessons_students'
+
+ lesson_student.belongs_to :lesson, :class => lesson
+ lesson_student.belongs_to :student, :class => student
+ lesson.has_many :lesson_students, :class => lesson_student
+ lesson.has_many :students, :through => :lesson_students, :class => student
+ [lesson, lesson_student, student]
+ end
+
+ def test_pk_is_not_required_for_join
+ post = Post.includes(:scategories).first
+ post2 = Post.includes(:categories).first
+
+ assert_operator post.categories.length, :>, 0
+ assert_equal post2.categories, post.categories
+ end
+
def test_include?
person = Person.new
post = Post.new
@@ -57,6 +189,47 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
assert post.reload.people(true).include?(person)
end
+ def test_delete_all_for_with_dependent_option_destroy
+ person = people(:david)
+ assert_equal 1, person.jobs_with_dependent_destroy.count
+
+ assert_no_difference 'Job.count' do
+ assert_difference 'Reference.count', -1 do
+ person.reload.jobs_with_dependent_destroy.delete_all
+ end
+ end
+ end
+
+ def test_delete_all_for_with_dependent_option_nullify
+ person = people(:david)
+ assert_equal 1, person.jobs_with_dependent_nullify.count
+
+ assert_no_difference 'Job.count' do
+ assert_no_difference 'Reference.count' do
+ person.reload.jobs_with_dependent_nullify.delete_all
+ end
+ end
+ end
+
+ def test_delete_all_for_with_dependent_option_delete_all
+ person = people(:david)
+ assert_equal 1, person.jobs_with_dependent_delete_all.count
+
+ assert_no_difference 'Job.count' do
+ assert_difference 'Reference.count', -1 do
+ person.reload.jobs_with_dependent_delete_all.delete_all
+ end
+ end
+ end
+
+ def test_concat
+ person = people(:david)
+ post = posts(:thinking)
+ post.people.concat [person]
+ assert_equal 1, post.people.size
+ assert_equal 1, post.people(true).size
+ end
+
def test_associate_existing_record_twice_should_add_to_target_twice
post = posts(:thinking)
person = people(:david)
@@ -341,6 +514,15 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
assert_equal(post.taggings.count, post.taggings_count)
end
+ def test_update_counter_caches_on_destroy
+ post = posts(:welcome)
+ tag = post.tags.create!(name: 'doomed')
+
+ assert_difference 'post.reload.taggings_count', -1 do
+ tag.tagged_posts.destroy(post)
+ end
+ end
+
def test_replace_association
assert_queries(4){posts(:welcome);people(:david);people(:michael); posts(:welcome).people(true)}
@@ -582,6 +764,11 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
assert_equal post.author.author_favorites, post.author_favorites
end
+ def test_merge_join_association_with_has_many_through_association_proxy
+ author = authors(:mary)
+ assert_nothing_raised { author.comments.ratings.to_sql }
+ end
+
def test_has_many_association_through_a_has_many_association_with_nonstandard_primary_keys
assert_equal 2, owners(:blackbeard).toys.count
end
@@ -607,7 +794,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
sarah = Person.create!(:first_name => 'Sarah', :primary_contact_id => people(:susan).id, :gender => 'F', :number1_fan_id => 1)
john = Person.create!(:first_name => 'John', :primary_contact_id => sarah.id, :gender => 'M', :number1_fan_id => 1)
assert_equal sarah.agents, [john]
- assert_equal people(:susan).agents.map(&:agents).flatten, people(:susan).agents_of_agents
+ assert_equal people(:susan).agents.flat_map(&:agents), people(:susan).agents_of_agents
end
def test_associate_existing_with_nonstandard_primary_key_on_belongs_to
@@ -907,4 +1094,8 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
readers(:michael_authorless).update(first_post_id: 1)
assert_equal [posts(:thinking)], person.reload.first_posts
end
+
+ def test_has_many_through_with_includes_in_through_association_scope
+ assert_not_empty posts(:welcome).author_address_extra_with_address
+ 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 0e48fbca9c..5a41461edf 100644
--- a/activerecord/test/cases/associations/has_one_associations_test.rb
+++ b/activerecord/test/cases/associations/has_one_associations_test.rb
@@ -158,22 +158,6 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
assert_nothing_raised { firm.destroy }
end
- def test_restrict
- firm = RestrictedFirm.create!(:name => 'restrict')
- firm.create_account(:credit_limit => 10)
-
- assert_not_nil firm.account
-
- assert_raise(ActiveRecord::DeleteRestrictionError) { firm.destroy }
- assert RestrictedFirm.exists?(:name => 'restrict')
- assert firm.account.present?
- end
-
- def test_restrict_is_deprecated
- klass = Class.new(ActiveRecord::Base)
- assert_deprecated { klass.has_one :post, dependent: :restrict }
- end
-
def test_restrict_with_exception
firm = RestrictedWithExceptionFirm.create!(:name => 'restrict')
firm.create_account(:credit_limit => 10)
@@ -521,21 +505,56 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
assert_no_queries { company.account = nil }
account = Account.find(2)
assert_queries { company.account = account }
+
+ assert_no_queries { Firm.new.account = account }
end
- def test_has_one_assignment_triggers_save_on_change
+ def test_has_one_assignment_dont_trigger_save_on_change_of_same_object
pirate = Pirate.create!(catchphrase: "Don' botharrr talkin' like one, savvy?")
ship = pirate.build_ship(name: 'old name')
ship.save!
ship.name = 'new name'
assert ship.changed?
+ assert_queries(1) do
+ # One query for updating name, not triggering query for updating pirate_id
+ pirate.ship = ship
+ end
+
+ assert_equal 'new name', pirate.ship.reload.name
+ end
+
+ def test_has_one_assignment_triggers_save_on_change_on_replacing_object
+ pirate = Pirate.create!(catchphrase: "Don' botharrr talkin' like one, savvy?")
+ ship = pirate.build_ship(name: 'old name')
+ ship.save!
+
+ new_ship = Ship.create(name: 'new name')
assert_queries(2) do
# One query for updating name and second query for updating pirate_id
- pirate.ship = ship
+ pirate.ship = new_ship
end
assert_equal 'new name', pirate.ship.reload.name
end
+ def test_has_one_autosave_with_primary_key_manually_set
+ post = Post.create(id: 1234, title: "Some title", body: 'Some content')
+ author = Author.new(id: 33, name: 'Hank Moody')
+
+ author.post = post
+ author.save
+ author.reload
+
+ assert_not_nil author.post
+ assert_equal author.post, post
+ end
+
+ def test_has_one_relationship_cannot_have_a_counter_cache
+ assert_raise(ArgumentError) do
+ Class.new(ActiveRecord::Base) do
+ has_one :thing, counter_cache: true
+ end
+ end
+ end
end
diff --git a/activerecord/test/cases/associations/has_one_through_associations_test.rb b/activerecord/test/cases/associations/has_one_through_associations_test.rb
index 90c557e886..a2725441b3 100644
--- a/activerecord/test/cases/associations/has_one_through_associations_test.rb
+++ b/activerecord/test/cases/associations/has_one_through_associations_test.rb
@@ -191,6 +191,7 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase
end
def test_preloading_has_one_through_on_belongs_to
+ MemberDetail.delete_all
assert_not_nil @member.member_type
@organization = organizations(:nsa)
@member_detail = MemberDetail.new
@@ -201,7 +202,7 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase
end
@new_detail = @member_details[0]
assert @new_detail.send(:association, :member_type).loaded?
- assert_not_nil assert_no_queries { @new_detail.member_type }
+ assert_no_queries { @new_detail.member_type }
end
def test_save_of_record_with_loaded_has_one_through
@@ -314,4 +315,12 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase
def test_has_one_through_with_custom_select_on_join_model_default_scope
assert_equal clubs(:boring_club), members(:groucho).selected_club
end
+
+ def test_has_one_through_relationship_cannot_have_a_counter_cache
+ assert_raise(ArgumentError) do
+ Class.new(ActiveRecord::Base) do
+ has_one :thing, through: :other_thing, counter_cache: true
+ end
+ end
+ end
end
diff --git a/activerecord/test/cases/associations/inner_join_association_test.rb b/activerecord/test/cases/associations/inner_join_association_test.rb
index 918783e8f1..a9efa6d86a 100644
--- a/activerecord/test/cases/associations/inner_join_association_test.rb
+++ b/activerecord/test/cases/associations/inner_join_association_test.rb
@@ -41,15 +41,20 @@ class InnerJoinAssociationTest < ActiveRecord::TestCase
assert_no_match(/WHERE/i, sql)
end
+ def test_join_association_conditions_support_string_and_arel_expressions
+ assert_equal 0, Author.joins(:welcome_posts_with_one_comment).count
+ assert_equal 1, Author.joins(:welcome_posts_with_comments).count
+ end
+
def test_join_conditions_allow_nil_associations
authors = Author.includes(:essays).where(:essays => {:id => nil})
assert_equal 2, authors.count
end
- def test_find_with_implicit_inner_joins_honors_readonly_without_select
- authors = Author.joins(:posts).to_a
- assert !authors.empty?, "expected authors to be non-empty"
- assert authors.all? {|a| a.readonly? }, "expected all authors to be readonly"
+ def test_find_with_implicit_inner_joins_without_select_does_not_imply_readonly
+ authors = Author.joins(:posts)
+ assert_not authors.empty?, "expected authors to be non-empty"
+ assert authors.none? {|a| a.readonly? }, "expected no authors to be readonly"
end
def test_find_with_implicit_inner_joins_honors_readonly_with_select
@@ -65,7 +70,7 @@ class InnerJoinAssociationTest < ActiveRecord::TestCase
end
def test_find_with_implicit_inner_joins_does_not_set_associations
- authors = Author.joins(:posts).select('authors.*')
+ authors = Author.joins(:posts).select('authors.*').to_a
assert !authors.empty?, "expected authors to be non-empty"
assert authors.all? { |a| !a.instance_variable_defined?(:@posts) }, "expected no authors to have the @posts association loaded"
end
@@ -104,4 +109,12 @@ class InnerJoinAssociationTest < ActiveRecord::TestCase
assert !posts(:welcome).tags.empty?
assert Post.joins(:misc_tags).where(:id => posts(:welcome).id).empty?
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).joins(:special_categorizations)
+ end
end
diff --git a/activerecord/test/cases/associations/inverse_associations_test.rb b/activerecord/test/cases/associations/inverse_associations_test.rb
index b1f0be3204..893030345f 100644
--- a/activerecord/test/cases/associations/inverse_associations_test.rb
+++ b/activerecord/test/cases/associations/inverse_associations_test.rb
@@ -9,10 +9,24 @@ require 'models/rating'
require 'models/comment'
require 'models/car'
require 'models/bulb'
+require 'models/mixed_case_monkey'
class AutomaticInverseFindingTests < ActiveRecord::TestCase
fixtures :ratings, :comments, :cars
+ def test_has_one_and_belongs_to_should_find_inverse_automatically_on_multiple_word_name
+ monkey_reflection = MixedCaseMonkey.reflect_on_association(:man)
+ man_reflection = Man.reflect_on_association(:mixed_case_monkey)
+
+ assert_respond_to monkey_reflection, :has_inverse?
+ assert monkey_reflection.has_inverse?, "The monkey reflection should have an inverse"
+ assert_equal man_reflection, monkey_reflection.inverse_of, "The monkey reflection's inverse should be the man reflection"
+
+ assert_respond_to man_reflection, :has_inverse?
+ assert man_reflection.has_inverse?, "The man reflection should have an inverse"
+ assert_equal monkey_reflection, man_reflection.inverse_of, "The man reflection's inverse should be the monkey reflection"
+ end
+
def test_has_one_and_belongs_to_should_find_inverse_automatically
car_reflection = Car.reflect_on_association(:bulb)
bulb_reflection = Bulb.reflect_on_association(:car)
@@ -401,10 +415,22 @@ class InverseHasManyTests < ActiveRecord::TestCase
assert_equal man.name, man.interests.find(interest.id).man.name, "The name of the man should match after the child name is changed"
end
+ def test_find_on_child_instance_with_id_should_not_load_all_child_records
+ man = Man.create!
+ interest = Interest.create!(man: man)
+
+ man.interests.find(interest.id)
+ assert_not man.interests.loaded?
+ end
+
def test_raise_record_not_found_error_when_invalid_ids_are_passed
+ # delete all interest records to ensure that hard coded invalid_id(s)
+ # are indeed invalid.
+ Interest.delete_all
+
man = Man.create!
- invalid_id = 2394823094892348920348523452345
+ invalid_id = 245324523
assert_raise(ActiveRecord::RecordNotFound) { man.interests.find(invalid_id) }
invalid_ids = [8432342, 2390102913, 2453245234523452]
@@ -420,6 +446,19 @@ class InverseHasManyTests < ActiveRecord::TestCase
def test_trying_to_use_inverses_that_dont_exist_should_raise_an_error
assert_raise(ActiveRecord::InverseOfAssociationNotFoundError) { Man.first.secret_interests }
end
+
+ def test_child_instance_should_point_to_parent_without_saving
+ man = Man.new
+ i = Interest.create(:topic => 'Industrial Revolution Re-enactment')
+
+ man.interests << i
+ assert_not_nil i.man
+
+ i.man.name = "Charles"
+ assert_equal i.man.name, man.name
+
+ assert !man.persisted?
+ end
end
class InverseBelongsToTests < ActiveRecord::TestCase
@@ -564,6 +603,18 @@ class InversePolymorphicBelongsToTests < ActiveRecord::TestCase
assert_equal face.description, new_man.polymorphic_face.description, "Description of face should be the same after changes to replaced-parent-owned instance"
end
+ def test_inversed_instance_should_not_be_reloaded_after_stale_state_changed
+ new_man = Man.new
+ face = Face.new
+ new_man.face = face
+
+ old_inversed_man = face.man
+ new_man.save!
+ new_inversed_man = face.man
+
+ assert_equal old_inversed_man.object_id, new_inversed_man.object_id
+ end
+
def test_should_not_try_to_set_inverse_instances_when_the_inverse_is_a_has_many
i = interests(:llama_wrangling)
m = i.polymorphic_man
diff --git a/activerecord/test/cases/associations/join_dependency_test.rb b/activerecord/test/cases/associations/join_dependency_test.rb
deleted file mode 100644
index 08c166dc33..0000000000
--- a/activerecord/test/cases/associations/join_dependency_test.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-require "cases/helper"
-require 'models/edge'
-
-class JoinDependencyTest < ActiveRecord::TestCase
- def test_column_names_with_alias_handles_nil_primary_key
- assert_equal Edge.column_names, ActiveRecord::Associations::JoinDependency::JoinBase.new(Edge).column_names_with_alias.map(&:first)
- end
-end \ No newline at end of file
diff --git a/activerecord/test/cases/associations/nested_through_associations_test.rb b/activerecord/test/cases/associations/nested_through_associations_test.rb
index e75d43bda8..8ef351cda8 100644
--- a/activerecord/test/cases/associations/nested_through_associations_test.rb
+++ b/activerecord/test/cases/associations/nested_through_associations_test.rb
@@ -186,7 +186,9 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase
members = assert_queries(4) { Member.includes(:organization_member_details_2).to_a.sort_by(&:id) }
groucho_details, other_details = member_details(:groucho), member_details(:some_other_guy)
- assert_no_queries do
+ # postgresql test if randomly executed then executes "SHOW max_identifier_length". Hence
+ # the need to ignore certain predefined sqls that deal with system calls.
+ assert_no_queries(ignore_none: false) do
assert_equal [groucho_details, other_details], members.first.organization_member_details_2.sort_by(&:id)
end
end
@@ -212,7 +214,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase
end
def test_has_many_through_has_many_with_has_and_belongs_to_many_source_reflection_preload
- authors = assert_queries(3) { Author.includes(:post_categories).to_a.sort_by(&:id) }
+ authors = assert_queries(4) { Author.includes(:post_categories).to_a.sort_by(&:id) }
general, cooking = categories(:general), categories(:cooking)
assert_no_queries do
@@ -240,7 +242,8 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase
end
def test_has_many_through_has_and_belongs_to_many_with_has_many_source_reflection_preload
- categories = assert_queries(3) { Category.includes(:post_comments).to_a.sort_by(&:id) }
+ Category.includes(:post_comments).to_a # preheat cache
+ categories = assert_queries(4) { Category.includes(:post_comments).to_a.sort_by(&:id) }
greetings, more = comments(:greetings), comments(:more_greetings)
assert_no_queries do
@@ -268,7 +271,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase
end
def test_has_many_through_has_many_with_has_many_through_habtm_source_reflection_preload
- authors = assert_queries(5) { Author.includes(:category_post_comments).to_a.sort_by(&:id) }
+ authors = assert_queries(6) { Author.includes(:category_post_comments).to_a.sort_by(&:id) }
greetings, more = comments(:greetings), comments(:more_greetings)
assert_no_queries do
@@ -369,7 +372,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase
prev_default_scope = Club.default_scopes
[:includes, :preload, :joins, :eager_load].each do |q|
- Club.default_scopes = [Club.send(q, :category)]
+ Club.default_scopes = [proc { Club.send(q, :category) }]
assert_equal categories(:general), members(:groucho).reload.club_category
end
ensure
diff --git a/activerecord/test/cases/associations_test.rb b/activerecord/test/cases/associations_test.rb
index c3b728296e..48e6fc5cd4 100644
--- a/activerecord/test/cases/associations_test.rb
+++ b/activerecord/test/cases/associations_test.rb
@@ -278,7 +278,7 @@ class OverridingAssociationsTest < ActiveRecord::TestCase
def test_habtm_association_redefinition_callbacks_should_differ_and_not_inherited
# redeclared association on AR descendant should not inherit callbacks from superclass
callbacks = PeopleList.before_add_for_has_and_belongs_to_many
- assert_equal([:enlist], callbacks)
+ assert_equal(1, callbacks.length)
callbacks = DifferentPeopleList.before_add_for_has_and_belongs_to_many
assert_equal([], callbacks)
end
@@ -286,7 +286,7 @@ class OverridingAssociationsTest < ActiveRecord::TestCase
def test_has_many_association_redefinition_callbacks_should_differ_and_not_inherited
# redeclared association on AR descendant should not inherit callbacks from superclass
callbacks = PeopleList.before_add_for_has_many
- assert_equal([:enlist], callbacks)
+ assert_equal(1, callbacks.length)
callbacks = DifferentPeopleList.before_add_for_has_many
assert_equal([], callbacks)
end
diff --git a/activerecord/test/cases/attribute_methods/read_test.rb b/activerecord/test/cases/attribute_methods/read_test.rb
index 8d8ff2f952..c0659fddef 100644
--- a/activerecord/test/cases/attribute_methods/read_test.rb
+++ b/activerecord/test/cases/attribute_methods/read_test.rb
@@ -15,13 +15,6 @@ module ActiveRecord
include ActiveRecord::AttributeMethods
- def self.define_attribute_methods
- # Created in the inherited/included hook for "proper" ARs
- @attribute_methods_mutex ||= Mutex.new
-
- super
- end
-
def self.column_names
%w{ one two three }
end
@@ -56,9 +49,9 @@ module ActiveRecord
end
def test_attribute_methods_generated?
- assert(!@klass.attribute_methods_generated?, 'attribute_methods_generated?')
+ assert_not @klass.method_defined?(:one)
@klass.define_attribute_methods
- assert(@klass.attribute_methods_generated?, 'attribute_methods_generated?')
+ assert @klass.method_defined?(:one)
end
end
end
diff --git a/activerecord/test/cases/attribute_methods/serialization_test.rb b/activerecord/test/cases/attribute_methods/serialization_test.rb
new file mode 100644
index 0000000000..75de773961
--- /dev/null
+++ b/activerecord/test/cases/attribute_methods/serialization_test.rb
@@ -0,0 +1,29 @@
+require "cases/helper"
+
+module ActiveRecord
+ module AttributeMethods
+ class SerializationTest < ActiveSupport::TestCase
+ class FakeColumn < Struct.new(:name)
+ def type; :integer; end
+ def type_cast(s); "#{s}!"; end
+ end
+
+ class NullCoder
+ def load(v); v; end
+ end
+
+ def test_type_cast_serialized_value
+ value = Serialization::Attribute.new(NullCoder.new, "Hello world", :serialized)
+ type = Serialization::Type.new(FakeColumn.new)
+ assert_equal "Hello world!", type.type_cast(value)
+ end
+
+ def test_type_cast_unserialized_value
+ value = Serialization::Attribute.new(nil, "Hello world", :unserialized)
+ type = Serialization::Type.new(FakeColumn.new)
+ type.type_cast(value)
+ assert_equal "Hello world", type.type_cast(value)
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb
index f10732ddda..6c581a432f 100644
--- a/activerecord/test/cases/attribute_methods_test.rb
+++ b/activerecord/test/cases/attribute_methods_test.rb
@@ -27,6 +27,14 @@ class AttributeMethodsTest < ActiveRecord::TestCase
ActiveRecord::Base.send(:attribute_method_matchers).concat(@old_matchers)
end
+ def test_attribute_for_inspect
+ t = topics(:first)
+ t.title = "The First Topic Now Has A Title With\nNewlines And More Than 50 Characters"
+
+ assert_equal %("#{t.written_on.to_s(:db)}"), t.attribute_for_inspect(:written_on)
+ assert_equal '"The First Topic Now Has A Title With\nNewlines And ..."', t.attribute_for_inspect(:title)
+ end
+
def test_attribute_present
t = Topic.new
t.title = "hello there!"
@@ -84,7 +92,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase
def test_set_attributes_without_hash
topic = Topic.new
- assert_nothing_raised { topic.attributes = '' }
+ assert_raise(ArgumentError) { topic.attributes = '' }
end
def test_integers_as_nil
@@ -751,21 +759,6 @@ class AttributeMethodsTest < ActiveRecord::TestCase
assert subklass.method_defined?(:id), "subklass is missing id method"
end
- def test_dispatching_column_attributes_through_method_missing_deprecated
- Topic.define_attribute_methods
-
- topic = Topic.new(:id => 5)
- topic.id = 5
-
- topic.method(:id).owner.send(:undef_method, :id)
-
- assert_deprecated do
- assert_equal 5, topic.id
- end
- ensure
- Topic.undefine_attribute_methods
- end
-
def test_read_attribute_with_nil_should_not_asplode
assert_equal nil, Topic.new.read_attribute(nil)
end
@@ -774,8 +767,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase
# that by defining a 'foo' method in the generated methods module for B.
# (That module will be inserted between the two, e.g. [B, <GeneratedAttributes>, A].)
def test_inherited_custom_accessors
- klass = Class.new(ActiveRecord::Base) do
- self.table_name = "topics"
+ klass = new_topic_like_ar_class do
self.abstract_class = true
def title; "omg"; end
def title=(val); self.author_name = val; end
@@ -790,8 +782,40 @@ class AttributeMethodsTest < ActiveRecord::TestCase
assert_equal "lol", topic.author_name
end
+ def test_on_the_fly_super_invokable_generated_attribute_methods_via_method_missing
+ klass = new_topic_like_ar_class do
+ def title
+ super + '!'
+ end
+ end
+
+ real_topic = topics(:first)
+ assert_equal real_topic.title + '!', klass.find(real_topic.id).title
+ end
+
+ def test_on_the_fly_super_invokable_generated_predicate_attribute_methods_via_method_missing
+ klass = new_topic_like_ar_class do
+ def title?
+ !super
+ end
+ end
+
+ real_topic = topics(:first)
+ assert_equal !real_topic.title?, klass.find(real_topic.id).title?
+ end
+
private
+ def new_topic_like_ar_class(&block)
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = 'topics'
+ class_eval(&block)
+ end
+
+ assert_empty klass.generated_attribute_methods.instance_methods(false)
+ klass
+ end
+
def cached_columns
Topic.columns.map(&:name) - Topic.serialized_attributes.keys
end
diff --git a/activerecord/test/cases/autosave_association_test.rb b/activerecord/test/cases/autosave_association_test.rb
index 580aa96ecd..517d2674a7 100644
--- a/activerecord/test/cases/autosave_association_test.rb
+++ b/activerecord/test/cases/autosave_association_test.rb
@@ -705,6 +705,13 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
ids.each { |id| assert_nil klass.find_by_id(id) }
end
+ def test_should_not_resave_destroyed_association
+ @pirate.birds.create!(name: :parrot)
+ @pirate.birds.first.destroy
+ @pirate.save!
+ assert @pirate.reload.birds.empty?
+ end
+
def test_should_skip_validation_on_has_many_if_marked_for_destruction
2.times { |i| @pirate.birds.create!(:name => "birds_#{i}") }
@@ -1433,10 +1440,6 @@ class TestAutosaveAssociationValidationMethodsGeneration < ActiveRecord::TestCas
test "should generate validation methods for HABTM associations with :validate => true" do
assert_respond_to @pirate, :validate_associated_records_for_parrots
end
-
- test "should not generate validation methods for HABTM associations without :validate => true" do
- assert !@pirate.respond_to?(:validate_associated_records_for_non_validated_parrots)
- end
end
class TestAutosaveAssociationWithTouch < ActiveRecord::TestCase
diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb
index 15d536daf5..cb8e564da1 100644
--- a/activerecord/test/cases/base_test.rb
+++ b/activerecord/test/cases/base_test.rb
@@ -1,4 +1,7 @@
+# encoding: utf-8
+
require "cases/helper"
+require 'active_support/concurrency/latch'
require 'models/post'
require 'models/author'
require 'models/topic'
@@ -75,22 +78,6 @@ end
class BasicsTest < ActiveRecord::TestCase
fixtures :topics, :companies, :developers, :projects, :computers, :accounts, :minimalistics, 'warehouse-things', :authors, :categorizations, :categories, :posts
- def setup
- ActiveRecord::Base.time_zone_aware_attributes = false
- ActiveRecord::Base.default_timezone = :local
- Time.zone = nil
- end
-
- def test_generated_methods_modules
- modules = Computer.ancestors
- assert modules.include?(Computer::GeneratedFeatureMethods)
- assert_equal(Computer::GeneratedFeatureMethods, Computer.generated_feature_methods)
- assert(modules.index(Computer.generated_attribute_methods) > modules.index(Computer.generated_feature_methods),
- "generated_attribute_methods must be higher in inheritance hierarchy than generated_feature_methods")
- assert_not_equal Computer.generated_feature_methods, Post.generated_feature_methods
- assert(modules.index(Computer.generated_attribute_methods) < modules.index(ActiveRecord::Base.ancestors[1]))
- end
-
def test_column_names_are_escaped
conn = ActiveRecord::Base.connection
classname = conn.class.name[/[^:]*$/]
@@ -134,6 +121,10 @@ class BasicsTest < ActiveRecord::TestCase
assert_equal 1, Topic.limit(1).to_a.length
end
+ def test_limit_should_take_value_from_latest_limit
+ assert_equal 1, Topic.limit(2).limit(1).to_a.length
+ end
+
def test_invalid_limit
assert_raises(ArgumentError) do
Topic.limit("asdfadf").to_a
@@ -231,7 +222,7 @@ class BasicsTest < ActiveRecord::TestCase
def test_preserving_time_objects_with_local_time_conversion_to_default_timezone_utc
with_env_tz 'America/New_York' do
- with_active_record_default_timezone :utc do
+ with_timezone_config default: :utc do
time = Time.local(2000)
topic = Topic.create('written_on' => time)
saved_time = Topic.find(topic.id).reload.written_on
@@ -244,7 +235,7 @@ class BasicsTest < ActiveRecord::TestCase
def test_preserving_time_objects_with_time_with_zone_conversion_to_default_timezone_utc
with_env_tz 'America/New_York' do
- with_active_record_default_timezone :utc do
+ with_timezone_config default: :utc do
Time.use_zone 'Central Time (US & Canada)' do
time = Time.zone.local(2000)
topic = Topic.create('written_on' => time)
@@ -259,18 +250,20 @@ class BasicsTest < ActiveRecord::TestCase
def test_preserving_time_objects_with_utc_time_conversion_to_default_timezone_local
with_env_tz 'America/New_York' do
- time = Time.utc(2000)
- topic = Topic.create('written_on' => time)
- saved_time = Topic.find(topic.id).reload.written_on
- assert_equal time, saved_time
- assert_equal [0, 0, 0, 1, 1, 2000, 6, 1, false, "UTC"], time.to_a
- assert_equal [0, 0, 19, 31, 12, 1999, 5, 365, false, "EST"], saved_time.to_a
+ with_timezone_config default: :local do
+ time = Time.utc(2000)
+ topic = Topic.create('written_on' => time)
+ saved_time = Topic.find(topic.id).reload.written_on
+ assert_equal time, saved_time
+ assert_equal [0, 0, 0, 1, 1, 2000, 6, 1, false, "UTC"], time.to_a
+ assert_equal [0, 0, 19, 31, 12, 1999, 5, 365, false, "EST"], saved_time.to_a
+ end
end
end
def test_preserving_time_objects_with_time_with_zone_conversion_to_default_timezone_local
with_env_tz 'America/New_York' do
- with_active_record_default_timezone :local do
+ with_timezone_config default: :local do
Time.use_zone 'Central Time (US & Canada)' do
time = Time.zone.local(2000)
topic = Topic.create('written_on' => time)
@@ -490,25 +483,25 @@ class BasicsTest < ActiveRecord::TestCase
# Oracle, and Sybase do not have a TIME datatype.
unless current_adapter?(:OracleAdapter, :SybaseAdapter)
def test_utc_as_time_zone
- Topic.default_timezone = :utc
- attributes = { "bonus_time" => "5:42:00AM" }
- topic = Topic.find(1)
- topic.attributes = attributes
- assert_equal Time.utc(2000, 1, 1, 5, 42, 0), topic.bonus_time
- Topic.default_timezone = :local
+ with_timezone_config default: :utc do
+ attributes = { "bonus_time" => "5:42:00AM" }
+ topic = Topic.find(1)
+ topic.attributes = attributes
+ assert_equal Time.utc(2000, 1, 1, 5, 42, 0), topic.bonus_time
+ end
end
def test_utc_as_time_zone_and_new
- Topic.default_timezone = :utc
- attributes = { "bonus_time(1i)"=>"2000",
- "bonus_time(2i)"=>"1",
- "bonus_time(3i)"=>"1",
- "bonus_time(4i)"=>"10",
- "bonus_time(5i)"=>"35",
- "bonus_time(6i)"=>"50" }
- topic = Topic.new(attributes)
- assert_equal Time.utc(2000, 1, 1, 10, 35, 50), topic.bonus_time
- Topic.default_timezone = :local
+ with_timezone_config default: :utc do
+ attributes = { "bonus_time(1i)"=>"2000",
+ "bonus_time(2i)"=>"1",
+ "bonus_time(3i)"=>"1",
+ "bonus_time(4i)"=>"10",
+ "bonus_time(5i)"=>"35",
+ "bonus_time(6i)"=>"50" }
+ topic = Topic.new(attributes)
+ assert_equal Time.utc(2000, 1, 1, 10, 35, 50), topic.bonus_time
+ end
end
end
@@ -555,19 +548,55 @@ class BasicsTest < ActiveRecord::TestCase
assert_equal [ Topic.find(1) ], [ Topic.find(2).topic ] & [ Topic.find(1) ]
end
- def test_comparison
+ def test_successful_comparison_of_like_class_records
topic_1 = Topic.create!
topic_2 = Topic.create!
assert_equal [topic_2, topic_1].sort, [topic_1, topic_2]
end
+ def test_failed_comparison_of_unlike_class_records
+ assert_raises ArgumentError do
+ [ topics(:first), posts(:welcome) ].sort
+ end
+ end
+
+ def test_create_without_prepared_statement
+ topic = Topic.connection.unprepared_statement do
+ Topic.create(:title => 'foo')
+ end
+
+ assert_equal topic, Topic.find(topic.id)
+ end
+
+ def test_destroy_without_prepared_statement
+ topic = Topic.create(title: 'foo')
+ Topic.connection.unprepared_statement do
+ Topic.find(topic.id).destroy
+ end
+
+ assert_equal nil, Topic.find_by_id(topic.id)
+ end
+
+ def test_blank_ids
+ one = Subscriber.new(:id => '')
+ two = Subscriber.new(:id => '')
+ assert_equal one, two
+ end
+
def test_comparison_with_different_objects
topic = Topic.create
category = Category.create(:name => "comparison")
assert_nil topic <=> category
end
+ def test_comparison_with_different_objects_in_array
+ topic = Topic.create
+ assert_raises(ArgumentError) do
+ [1, topic].sort
+ end
+ end
+
def test_readonly_attributes
assert_equal Set.new([ 'title' , 'comments_count' ]), ReadonlyTitlePost.readonly_attributes
@@ -581,10 +610,23 @@ class BasicsTest < ActiveRecord::TestCase
assert_equal "changed", post.body
end
- def test_attr_readonly_is_class_level_setting
- post = ReadonlyTitlePost.new
- assert_raise(NoMethodError) { post._attr_readonly = [:title] }
- assert_deprecated { post._attr_readonly }
+ def test_unicode_column_name
+ Weird.reset_column_information
+ weird = Weird.create(:なまえ => 'たこ焼き仮面')
+ assert_equal 'たこ焼き仮面', weird.なまえ
+ end
+
+ unless current_adapter?(:PostgreSQLAdapter)
+ def test_respect_internal_encoding
+ old_default_internal = Encoding.default_internal
+ silence_warnings { Encoding.default_internal = "EUC-JP" }
+
+ Weird.reset_column_information
+
+ assert_equal ["EUC-JP"], Weird.columns.map {|c| c.name.encoding.name }.uniq
+ ensure
+ silence_warnings { Encoding.default_internal = old_default_internal }
+ end
end
def test_non_valid_identifier_column_name
@@ -609,12 +651,14 @@ class BasicsTest < ActiveRecord::TestCase
# Oracle, and Sybase do not have a TIME datatype.
return true if current_adapter?(:OracleAdapter, :SybaseAdapter)
- attributes = {
- "bonus_time" => "5:42:00AM"
- }
- topic = Topic.find(1)
- topic.attributes = attributes
- assert_equal Time.local(2000, 1, 1, 5, 42, 0), topic.bonus_time
+ with_timezone_config default: :local do
+ attributes = {
+ "bonus_time" => "5:42:00AM"
+ }
+ topic = Topic.find(1)
+ topic.attributes = attributes
+ assert_equal Time.local(2000, 1, 1, 5, 42, 0), topic.bonus_time
+ end
end
def test_attributes_on_dummy_time_with_invalid_time
@@ -802,19 +846,18 @@ class BasicsTest < ActiveRecord::TestCase
# TODO: extend defaults tests to other databases!
if current_adapter?(:PostgreSQLAdapter)
def test_default
- tz = Default.default_timezone
- Default.default_timezone = :local
- default = Default.new
- Default.default_timezone = tz
-
- # fixed dates / times
- assert_equal Date.new(2004, 1, 1), default.fixed_date
- assert_equal Time.local(2004, 1,1,0,0,0,0), default.fixed_time
-
- # char types
- assert_equal 'Y', default.char1
- assert_equal 'a varchar field', default.char2
- assert_equal 'a text field', default.char3
+ with_timezone_config default: :local do
+ default = Default.new
+
+ # fixed dates / times
+ assert_equal Date.new(2004, 1, 1), default.fixed_date
+ assert_equal Time.local(2004, 1,1,0,0,0,0), default.fixed_time
+
+ # char types
+ assert_equal 'Y', default.char1
+ assert_equal 'a varchar field', default.char2
+ assert_equal 'a text field', default.char3
+ end
end
class Geometric < ActiveRecord::Base; end
@@ -1228,83 +1271,6 @@ class BasicsTest < ActiveRecord::TestCase
assert_no_queries { assert true }
end
- def test_inspect_class
- assert_equal 'ActiveRecord::Base', ActiveRecord::Base.inspect
- assert_equal 'LoosePerson(abstract)', LoosePerson.inspect
- assert_match(/^Topic\(id: integer, title: string/, Topic.inspect)
- end
-
- def test_inspect_instance
- topic = topics(:first)
- assert_equal %(#<Topic id: 1, title: "The First Topic", author_name: "David", author_email_address: "david@loudthinking.com", written_on: "#{topic.written_on.to_s(:db)}", bonus_time: "#{topic.bonus_time.to_s(:db)}", last_read: "#{topic.last_read.to_s(:db)}", content: "Have a nice day", important: nil, approved: false, replies_count: 1, unique_replies_count: 0, parent_id: nil, parent_title: nil, type: nil, group: nil, created_at: "#{topic.created_at.to_s(:db)}", updated_at: "#{topic.updated_at.to_s(:db)}">), topic.inspect
- end
-
- def test_inspect_new_instance
- assert_match(/Topic id: nil/, Topic.new.inspect)
- end
-
- def test_inspect_limited_select_instance
- assert_equal %(#<Topic id: 1>), Topic.all.merge!(:select => 'id', :where => 'id = 1').first.inspect
- assert_equal %(#<Topic id: 1, title: "The First Topic">), Topic.all.merge!(:select => 'id, title', :where => 'id = 1').first.inspect
- end
-
- def test_inspect_class_without_table
- assert_equal "NonExistentTable(Table doesn't exist)", NonExistentTable.inspect
- end
-
- def test_attribute_for_inspect
- t = topics(:first)
- t.title = "The First Topic Now Has A Title With\nNewlines And More Than 50 Characters"
-
- assert_equal %("#{t.written_on.to_s(:db)}"), t.attribute_for_inspect(:written_on)
- assert_equal '"The First Topic Now Has A Title With\nNewlines And M..."', t.attribute_for_inspect(:title)
- end
-
- def test_becomes
- assert_kind_of Reply, topics(:first).becomes(Reply)
- assert_equal "The First Topic", topics(:first).becomes(Reply).title
- end
-
- def test_becomes_includes_errors
- company = Company.new(:name => nil)
- assert !company.valid?
- original_errors = company.errors
- client = company.becomes(Client)
- assert_equal original_errors, client.errors
- end
-
- def test_silence_sets_log_level_to_error_in_block
- original_logger = ActiveRecord::Base.logger
-
- assert_deprecated do
- log = StringIO.new
- ActiveRecord::Base.logger = ActiveSupport::Logger.new(log)
- ActiveRecord::Base.logger.level = Logger::DEBUG
- ActiveRecord::Base.silence do
- ActiveRecord::Base.logger.warn "warn"
- ActiveRecord::Base.logger.error "error"
- end
- assert_equal "error\n", log.string
- end
- ensure
- ActiveRecord::Base.logger = original_logger
- end
-
- def test_silence_sets_log_level_back_to_level_before_yield
- original_logger = ActiveRecord::Base.logger
-
- assert_deprecated do
- log = StringIO.new
- ActiveRecord::Base.logger = ActiveSupport::Logger.new(log)
- ActiveRecord::Base.logger.level = Logger::WARN
- ActiveRecord::Base.silence do
- end
- assert_equal Logger::WARN, ActiveRecord::Base.logger.level
- end
- ensure
- ActiveRecord::Base.logger = original_logger
- end
-
def test_benchmark_with_log_level
original_logger = ActiveRecord::Base.logger
log = StringIO.new
@@ -1399,6 +1365,35 @@ class BasicsTest < ActiveRecord::TestCase
assert_equal 1, post.comments.length
end
+ if Process.respond_to?(:fork) && !in_memory_db?
+ def test_marshal_between_processes
+ # Define a new model to ensure there are no caches
+ if self.class.const_defined?("Post", false)
+ flunk "there should be no post constant"
+ end
+
+ self.class.const_set("Post", Class.new(ActiveRecord::Base) {
+ has_many :comments
+ })
+
+ rd, wr = IO.pipe
+
+ ActiveRecord::Base.connection_handler.clear_all_connections!
+
+ fork do
+ rd.close
+ post = Post.new
+ post.comments.build
+ wr.write Marshal.dump(post)
+ wr.close
+ end
+
+ wr.close
+ assert Marshal.load rd.read
+ rd.close
+ end
+ end
+
def test_marshalling_new_record_round_trip_with_associations
post = Post.new
post.comments.build
@@ -1541,21 +1536,20 @@ class BasicsTest < ActiveRecord::TestCase
orig_handler = klass.connection_handler
new_handler = ActiveRecord::ConnectionAdapters::ConnectionHandler.new
after_handler = nil
- is_set = false
+ latch1 = ActiveSupport::Concurrency::Latch.new
+ latch2 = ActiveSupport::Concurrency::Latch.new
t = Thread.new do
klass.connection_handler = new_handler
- is_set = true
- Thread.stop
+ latch1.release
+ latch2.await
after_handler = klass.connection_handler
end
- while(!is_set)
- Thread.pass
- end
+ latch1.await
klass.connection_handler = orig_handler
- t.wakeup
+ latch2.release
t.join
assert_equal after_handler, new_handler
diff --git a/activerecord/test/cases/batches_test.rb b/activerecord/test/cases/batches_test.rb
index e09fa95756..38c2560d69 100644
--- a/activerecord/test/cases/batches_test.rb
+++ b/activerecord/test/cases/batches_test.rb
@@ -26,6 +26,24 @@ class EachTest < ActiveRecord::TestCase
end
end
+ def test_each_should_return_an_enumerator_if_no_block_is_present
+ assert_queries(1) do
+ Post.find_each(:batch_size => 100000).with_index do |post, index|
+ assert_kind_of Post, post
+ assert_kind_of Integer, index
+ end
+ end
+ end
+
+ def test_each_enumerator_should_execute_one_query_per_batch
+ assert_queries(@total + 1) do
+ Post.find_each(:batch_size => 1).with_index do |post, index|
+ assert_kind_of Post, post
+ assert_kind_of Integer, index
+ end
+ end
+ end
+
def test_each_should_raise_if_select_is_set_without_id
assert_raise(RuntimeError) do
Post.select(:title).find_each(:batch_size => 1) { |post| post }
@@ -50,6 +68,16 @@ class EachTest < ActiveRecord::TestCase
Post.order("title").find_each { |post| post }
end
+ def test_logger_not_required
+ previous_logger = ActiveRecord::Base.logger
+ ActiveRecord::Base.logger = nil
+ assert_nothing_raised do
+ Post.limit(1).find_each { |post| post }
+ end
+ ensure
+ ActiveRecord::Base.logger = previous_logger
+ end
+
def test_find_in_batches_should_return_batches
assert_queries(@total + 1) do
Post.find_in_batches(:batch_size => 1) do |batch|
diff --git a/activerecord/test/cases/bind_parameter_test.rb b/activerecord/test/cases/bind_parameter_test.rb
index 03aa9fdb27..291751c435 100644
--- a/activerecord/test/cases/bind_parameter_test.rb
+++ b/activerecord/test/cases/bind_parameter_test.rb
@@ -23,46 +23,56 @@ module ActiveRecord
@listener = LogListener.new
@pk = Topic.columns.find { |c| c.primary }
ActiveSupport::Notifications.subscribe('sql.active_record', @listener)
-
- skip_if_prepared_statement_caching_is_not_supported
end
def teardown
ActiveSupport::Notifications.unsubscribe(@listener)
end
- def test_binds_are_logged
- sub = @connection.substitute_at(@pk, 0)
- binds = [[@pk, 1]]
- sql = "select * from topics where id = #{sub}"
+ if ActiveRecord::Base.connection.supports_statement_cache?
+ def test_binds_are_logged
+ sub = @connection.substitute_at(@pk, 0)
+ binds = [[@pk, 1]]
+ sql = "select * from topics where id = #{sub}"
- @connection.exec_query(sql, 'SQL', binds)
+ @connection.exec_query(sql, 'SQL', binds)
- message = @listener.calls.find { |args| args[4][:sql] == sql }
- assert_equal binds, message[4][:binds]
- end
+ message = @listener.calls.find { |args| args[4][:sql] == sql }
+ assert_equal binds, message[4][:binds]
+ end
- def test_find_one_uses_binds
- Topic.find(1)
- binds = [[@pk, 1]]
- message = @listener.calls.find { |args| args[4][:binds] == binds }
- assert message, 'expected a message with binds'
- end
+ def test_binds_are_logged_after_type_cast
+ sub = @connection.substitute_at(@pk, 0)
+ binds = [[@pk, "3"]]
+ sql = "select * from topics where id = #{sub}"
- def test_logs_bind_vars
- pk = Topic.columns.find { |x| x.primary }
-
- payload = {
- :name => 'SQL',
- :sql => 'select * from topics where id = ?',
- :binds => [[pk, 10]]
- }
- event = ActiveSupport::Notifications::Event.new(
- 'foo',
- Time.now,
- Time.now,
- 123,
- payload)
+ @connection.exec_query(sql, 'SQL', binds)
+
+ message = @listener.calls.find { |args| args[4][:sql] == sql }
+ assert_equal [[@pk, 3]], message[4][:binds]
+ end
+
+ def test_find_one_uses_binds
+ Topic.find(1)
+ binds = [[@pk, 1]]
+ message = @listener.calls.find { |args| args[4][:binds] == binds }
+ assert message, 'expected a message with binds'
+ end
+
+ def test_logs_bind_vars
+ pk = Topic.columns.find { |x| x.primary }
+
+ payload = {
+ :name => 'SQL',
+ :sql => 'select * from topics where id = ?',
+ :binds => [[pk, 10]]
+ }
+ event = ActiveSupport::Notifications::Event.new(
+ 'foo',
+ Time.now,
+ Time.now,
+ 123,
+ payload)
logger = Class.new(ActiveRecord::LogSubscriber) {
attr_reader :debugs
@@ -78,12 +88,7 @@ module ActiveRecord
logger.sql event
assert_match([[pk.name, 10]].inspect, logger.debugs.first)
- end
-
- private
-
- def skip_if_prepared_statement_caching_is_not_supported
- skip('prepared statement caching is not supported') unless @connection.supports_statement_cache?
+ end
end
end
end
diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb
index f49bef2346..2f6913167d 100644
--- a/activerecord/test/cases/calculations_test.rb
+++ b/activerecord/test/cases/calculations_test.rb
@@ -6,6 +6,7 @@ require 'models/edge'
require 'models/organization'
require 'models/possession'
require 'models/topic'
+require 'models/reply'
require 'models/minivan'
require 'models/speedometer'
require 'models/ship_part'
@@ -166,6 +167,15 @@ class CalculationsTest < ActiveRecord::TestCase
assert_no_match(/OFFSET/, queries.first)
end
+ def test_count_on_invalid_columns_raises
+ e = assert_raises(ActiveRecord::StatementInvalid) {
+ Account.select("credit_limit, firm_name").count
+ }
+
+ assert_match %r{accounts}i, e.message
+ assert_match "credit_limit, firm_name", e.message
+ end
+
def test_should_group_by_summed_field_having_condition
c = Account.group(:firm_id).having('sum(credit_limit) > 50').sum(:credit_limit)
assert_nil c[1]
@@ -201,6 +211,10 @@ class CalculationsTest < ActiveRecord::TestCase
assert_equal 19.83, NumericData.sum(:bank_balance)
end
+ def test_should_return_type_casted_values_with_group_and_expression
+ assert_equal 0.5, Account.group(:firm_name).sum('0.01 * credit_limit')['37signals']
+ end
+
def test_should_group_by_summed_field_with_conditions
c = Account.where('firm_id > 1').group(:firm_id).sum(:credit_limit)
assert_nil c[1]
@@ -341,16 +355,6 @@ class CalculationsTest < ActiveRecord::TestCase
assert_equal 5, Account.count(:firm_id)
end
- def test_count_distinct_option_is_deprecated
- assert_deprecated do
- assert_equal 4, Account.select(:credit_limit).count(distinct: true)
- end
-
- assert_deprecated do
- assert_equal 6, Account.select(:credit_limit).count(distinct: false)
- end
- end
-
def test_count_with_distinct
assert_equal 4, Account.select(:credit_limit).distinct.count
assert_equal 4, Account.select(:credit_limit).uniq.count
@@ -410,12 +414,6 @@ class CalculationsTest < ActiveRecord::TestCase
Account.where("credit_limit > 50").from('accounts').sum(:credit_limit)
end
- def test_sum_array_compatibility_deprecation
- assert_deprecated do
- assert_equal Account.sum(:credit_limit), Account.sum(&:credit_limit)
- end
- end
-
def test_average_with_from_option
assert_equal Account.average(:credit_limit), Account.from('accounts').average(:credit_limit)
assert_equal Account.where("credit_limit > 50").average(:credit_limit),
@@ -478,6 +476,11 @@ class CalculationsTest < ActiveRecord::TestCase
assert_equal [1,2,3,4], Topic.order(:id).pluck(:id)
end
+ def test_pluck_without_column_names
+ assert_equal [[1, "Firm", 1, nil, "37signals", nil, 1, nil, ""]],
+ Company.order(:id).limit(1).pluck
+ end
+
def test_pluck_type_cast
topic = topics(:first)
relation = Topic.where(:id => topic.id)
@@ -539,6 +542,11 @@ class CalculationsTest < ActiveRecord::TestCase
assert_equal Company.all.map(&:id).sort, Company.ids.sort
end
+ def test_pluck_with_includes_limit_and_empty_result
+ assert_equal [], Topic.includes(:replies).limit(0).pluck(:id)
+ assert_equal [], Topic.includes(:replies).limit(1).where('0 = 1').pluck(:id)
+ end
+
def test_pluck_not_auto_table_name_prefix_if_column_included
Company.create!(:name => "test", :contracts => [Contract.new(:developer_id => 7)])
ids = Company.includes(:contracts).pluck(:developer_id)
diff --git a/activerecord/test/cases/column_test.rb b/activerecord/test/cases/column_test.rb
index 3a4f414ae8..2a6d8cc2ab 100644
--- a/activerecord/test/cases/column_test.rb
+++ b/activerecord/test/cases/column_test.rb
@@ -110,6 +110,14 @@ module ActiveRecord
assert_equal 1800, column.type_cast(30.minutes)
assert_equal 7200, column.type_cast(2.hours)
end
+
+ def test_string_to_time_with_timezone
+ [:utc, :local].each do |zone|
+ with_timezone_config default: zone do
+ assert_equal Time.utc(2013, 9, 4, 0, 0, 0), Column.string_to_time("Wed, 04 Sep 2013 03:00:00 EAT")
+ end
+ end
+ end
end
end
end
diff --git a/activerecord/test/cases/connection_adapters/connection_handler_test.rb b/activerecord/test/cases/connection_adapters/connection_handler_test.rb
index 3e33b30144..318cc5a32c 100644
--- a/activerecord/test/cases/connection_adapters/connection_handler_test.rb
+++ b/activerecord/test/cases/connection_adapters/connection_handler_test.rb
@@ -2,6 +2,133 @@ require "cases/helper"
module ActiveRecord
module ConnectionAdapters
+
+ class MergeAndResolveDefaultUrlConfigTest < ActiveRecord::TestCase
+
+ def klass
+ ActiveRecord::ConnectionHandling::MergeAndResolveDefaultUrlConfig
+ end
+
+ def setup
+ @previous_database_url = ENV.delete("DATABASE_URL")
+ end
+
+ def teardown
+ ENV["DATABASE_URL"] = @previous_database_url
+ end
+
+ def test_string_connection
+ config = { "production" => "postgres://localhost/foo" }
+ actual = klass.new(config).resolve
+ expected = { "production" =>
+ { "adapter" => "postgresql",
+ "database" => "foo",
+ "host" => "localhost"
+ }
+ }
+ assert_equal expected, actual
+ end
+
+ def test_url_sub_key
+ config = { "production" => { "url" => "postgres://localhost/foo" } }
+ actual = klass.new(config).resolve
+ expected = { "production" =>
+ { "adapter" => "postgresql",
+ "database" => "foo",
+ "host" => "localhost"
+ }
+ }
+ assert_equal expected, actual
+ end
+
+ def test_hash
+ config = { "production" => { "adapter" => "postgres", "database" => "foo" } }
+ actual = klass.new(config).resolve
+ assert_equal config, actual
+ end
+
+ def test_blank
+ config = {}
+ actual = klass.new(config).resolve
+ assert_equal config, actual
+ end
+
+ def test_blank_with_database_url
+ ENV['DATABASE_URL'] = "postgres://localhost/foo"
+
+ config = {}
+ actual = klass.new(config).resolve
+ expected = { "adapter" => "postgresql",
+ "database" => "foo",
+ "host" => "localhost" }
+ assert_equal expected, actual["production"]
+ assert_equal expected, actual["development"]
+ assert_equal expected, actual["test"]
+ assert_equal nil, actual[:production]
+ assert_equal nil, actual[:development]
+ assert_equal nil, actual[:test]
+ end
+
+ def test_sting_with_database_url
+ ENV['DATABASE_URL'] = "NOT-POSTGRES://localhost/NOT_FOO"
+
+ config = { "production" => "postgres://localhost/foo" }
+ actual = klass.new(config).resolve
+
+ expected = { "production" =>
+ { "adapter" => "postgresql",
+ "database" => "foo",
+ "host" => "localhost"
+ }
+ }
+ assert_equal expected, actual
+ end
+
+ def test_url_sub_key_with_database_url
+ ENV['DATABASE_URL'] = "NOT-POSTGRES://localhost/NOT_FOO"
+
+ config = { "production" => { "url" => "postgres://localhost/foo" } }
+ actual = klass.new(config).resolve
+ expected = { "production" =>
+ { "adapter" => "postgresql",
+ "database" => "foo",
+ "host" => "localhost"
+ }
+ }
+ assert_equal expected, actual
+ end
+
+ def test_merge_no_conflicts_with_database_url
+ ENV['DATABASE_URL'] = "postgres://localhost/foo"
+
+ config = {"production" => { "pool" => "5" } }
+ actual = klass.new(config).resolve
+ expected = { "production" =>
+ { "adapter" => "postgresql",
+ "database" => "foo",
+ "host" => "localhost",
+ "pool" => "5"
+ }
+ }
+ assert_equal expected, actual
+ end
+
+ def test_merge_conflicts_with_database_url
+ ENV['DATABASE_URL'] = "postgres://localhost/foo"
+
+ config = {"production" => { "adapter" => "NOT-POSTGRES", "database" => "NOT-FOO", "pool" => "5" } }
+ actual = klass.new(config).resolve
+ expected = { "production" =>
+ { "adapter" => "postgresql",
+ "database" => "foo",
+ "host" => "localhost",
+ "pool" => "5"
+ }
+ }
+ assert_equal expected, actual
+ end
+ end
+
class ConnectionHandlerTest < ActiveRecord::TestCase
def setup
@klass = Class.new(Base) { def self.name; 'klass'; end }
diff --git a/activerecord/test/cases/connection_management_test.rb b/activerecord/test/cases/connection_management_test.rb
index fe1b40d884..00667cc52e 100644
--- a/activerecord/test/cases/connection_management_test.rb
+++ b/activerecord/test/cases/connection_management_test.rb
@@ -26,25 +26,25 @@ module ActiveRecord
assert ActiveRecord::Base.connection_handler.active_connections?
end
- def test_connection_pool_per_pid
- return skip('must support fork') unless Process.respond_to?(:fork)
+ if Process.respond_to?(:fork)
+ def test_connection_pool_per_pid
+ object_id = ActiveRecord::Base.connection.object_id
- object_id = ActiveRecord::Base.connection.object_id
+ rd, wr = IO.pipe
- rd, wr = IO.pipe
+ pid = fork {
+ rd.close
+ wr.write Marshal.dump ActiveRecord::Base.connection.object_id
+ wr.close
+ exit!
+ }
- pid = fork {
- rd.close
- wr.write Marshal.dump ActiveRecord::Base.connection.object_id
wr.close
- exit!
- }
-
- wr.close
- Process.waitpid pid
- assert_not_equal object_id, Marshal.load(rd.read)
- rd.close
+ Process.waitpid pid
+ assert_not_equal object_id, Marshal.load(rd.read)
+ rd.close
+ end
end
def test_app_delegation
@@ -80,9 +80,9 @@ module ActiveRecord
end
def test_connections_closed_if_exception
- app = Class.new(App) { def call(env); raise; end }.new
+ app = Class.new(App) { def call(env); raise NotImplementedError; end }.new
explosive = ConnectionManagement.new(app)
- assert_raises(RuntimeError) { explosive.call(@env) }
+ assert_raises(NotImplementedError) { explosive.call(@env) }
assert !ActiveRecord::Base.connection_handler.active_connections?
end
diff --git a/activerecord/test/cases/connection_pool_test.rb b/activerecord/test/cases/connection_pool_test.rb
index e6af29282c..2da51ea015 100644
--- a/activerecord/test/cases/connection_pool_test.rb
+++ b/activerecord/test/cases/connection_pool_test.rb
@@ -118,6 +118,7 @@ module ActiveRecord
connection = cs.first
@pool.remove connection
assert_respond_to t.join.value, :execute
+ connection.close
end
def test_reap_and_active
@@ -329,7 +330,7 @@ module ActiveRecord
end
# make sure exceptions are thrown when establish_connection
- # is called with a anonymous class
+ # is called with an anonymous class
def test_anonymous_class_exception
anonymous = Class.new(ActiveRecord::Base)
handler = ActiveRecord::Base.connection_handler
diff --git a/activerecord/test/cases/connection_specification/resolver_test.rb b/activerecord/test/cases/connection_specification/resolver_test.rb
index c8dfc3244b..fdd1914cba 100644
--- a/activerecord/test/cases/connection_specification/resolver_test.rb
+++ b/activerecord/test/cases/connection_specification/resolver_test.rb
@@ -4,58 +4,92 @@ module ActiveRecord
module ConnectionAdapters
class ConnectionSpecification
class ResolverTest < ActiveRecord::TestCase
- def resolve(spec)
- Resolver.new(spec, {}).spec.config
+ def resolve(spec, config={})
+ Resolver.new(config).resolve(spec)
+ end
+
+ def spec(spec, config={})
+ Resolver.new(config).spec(spec)
end
def test_url_invalid_adapter
- assert_raises(LoadError) do
- resolve 'ridiculous://foo?encoding=utf8'
+ error = assert_raises(LoadError) do
+ spec 'ridiculous://foo?encoding=utf8'
end
+
+ assert_match "Could not load 'active_record/connection_adapters/ridiculous_adapter'", error.message
end
# The abstract adapter is used simply to bypass the bit of code that
# checks that the adapter file can be required in.
+ def test_url_from_environment
+ spec = resolve :production, 'production' => 'abstract://foo?encoding=utf8'
+ assert_equal({
+ "adapter" => "abstract",
+ "host" => "foo",
+ "encoding" => "utf8" }, spec)
+ end
+
+ def test_url_sub_key
+ spec = resolve :production, 'production' => {"url" => 'abstract://foo?encoding=utf8'}
+ assert_equal({
+ "adapter" => "abstract",
+ "host" => "foo",
+ "encoding" => "utf8" }, spec)
+ end
+
+ def test_url_sub_key_merges_correctly
+ hash = {"url" => 'abstract://foo?encoding=utf8&', "adapter" => "sqlite3", "host" => "bar", "pool" => "3"}
+ spec = resolve :production, 'production' => hash
+ assert_equal({
+ "adapter" => "abstract",
+ "host" => "foo",
+ "encoding" => "utf8",
+ "pool" => "3" }, spec)
+ end
+
def test_url_host_no_db
spec = resolve 'abstract://foo?encoding=utf8'
assert_equal({
- adapter: "abstract",
- host: "foo",
- encoding: "utf8" }, spec)
+ "adapter" => "abstract",
+ "host" => "foo",
+ "encoding" => "utf8" }, spec)
end
def test_url_host_db
spec = resolve 'abstract://foo/bar?encoding=utf8'
assert_equal({
- adapter: "abstract",
- database: "bar",
- host: "foo",
- encoding: "utf8" }, spec)
+ "adapter" => "abstract",
+ "database" => "bar",
+ "host" => "foo",
+ "encoding" => "utf8" }, spec)
end
def test_url_port
spec = resolve 'abstract://foo:123?encoding=utf8'
assert_equal({
- adapter: "abstract",
- port: 123,
- host: "foo",
- encoding: "utf8" }, spec)
+ "adapter" => "abstract",
+ "port" => 123,
+ "host" => "foo",
+ "encoding" => "utf8" }, spec)
end
def test_encoded_password
password = 'am@z1ng_p@ssw0rd#!'
encoded_password = URI.encode_www_form_component(password)
spec = resolve "abstract://foo:#{encoded_password}@localhost/bar"
- assert_equal password, spec[:password]
+ assert_equal password, spec["password"]
end
- def test_descriptive_error_message_when_adapter_is_missing
- error = assert_raise(LoadError) do
- resolve(adapter: 'non-existing')
- end
+ def test_url_host_db_for_sqlite3
+ spec = resolve 'sqlite3://foo:bar@dburl:9000/foo_test'
+ assert_equal('/foo_test', spec["database"])
+ end
- assert_match "Could not load 'active_record/connection_adapters/non-existing_adapter'", error.message
+ def test_url_host_memory_db_for_sqlite3
+ spec = resolve 'sqlite3://foo:bar@dburl:9000/:memory:'
+ assert_equal(':memory:', spec["database"])
end
end
end
diff --git a/activerecord/test/cases/core_test.rb b/activerecord/test/cases/core_test.rb
new file mode 100644
index 0000000000..2a52bf574c
--- /dev/null
+++ b/activerecord/test/cases/core_test.rb
@@ -0,0 +1,33 @@
+require 'cases/helper'
+require 'models/person'
+require 'models/topic'
+
+class NonExistentTable < ActiveRecord::Base; end
+
+class CoreTest < ActiveRecord::TestCase
+ fixtures :topics
+
+ def test_inspect_class
+ assert_equal 'ActiveRecord::Base', ActiveRecord::Base.inspect
+ assert_equal 'LoosePerson(abstract)', LoosePerson.inspect
+ assert_match(/^Topic\(id: integer, title: string/, Topic.inspect)
+ end
+
+ def test_inspect_instance
+ topic = topics(:first)
+ assert_equal %(#<Topic id: 1, title: "The First Topic", author_name: "David", author_email_address: "david@loudthinking.com", written_on: "#{topic.written_on.to_s(:db)}", bonus_time: "#{topic.bonus_time.to_s(:db)}", last_read: "#{topic.last_read.to_s(:db)}", content: "Have a nice day", important: nil, approved: false, replies_count: 1, unique_replies_count: 0, parent_id: nil, parent_title: nil, type: nil, group: nil, created_at: "#{topic.created_at.to_s(:db)}", updated_at: "#{topic.updated_at.to_s(:db)}">), topic.inspect
+ end
+
+ def test_inspect_new_instance
+ assert_match(/Topic id: nil/, Topic.new.inspect)
+ end
+
+ def test_inspect_limited_select_instance
+ assert_equal %(#<Topic id: 1>), Topic.all.merge!(:select => 'id', :where => 'id = 1').first.inspect
+ assert_equal %(#<Topic id: 1, title: "The First Topic">), Topic.all.merge!(:select => 'id, title', :where => 'id = 1').first.inspect
+ end
+
+ def test_inspect_class_without_table
+ assert_equal "NonExistentTable(Table doesn't exist)", NonExistentTable.inspect
+ end
+end
diff --git a/activerecord/test/cases/counter_cache_test.rb b/activerecord/test/cases/counter_cache_test.rb
index 61f9d4cdae..ee3d8a81c2 100644
--- a/activerecord/test/cases/counter_cache_test.rb
+++ b/activerecord/test/cases/counter_cache_test.rb
@@ -51,14 +51,9 @@ class CounterCacheTest < ActiveRecord::TestCase
end
end
- test 'reset multiple association counters' do
- Topic.increment_counter(:replies_count, @topic.id)
- assert_difference '@topic.reload.replies_count', -1 do
- Topic.reset_counters(@topic.id, :replies, :unique_replies)
- end
-
- Topic.increment_counter(:unique_replies_count, @topic.id)
- assert_difference '@topic.reload.unique_replies_count', -1 do
+ test 'reset multiple counters' do
+ Topic.update_counters @topic.id, replies_count: 1, unique_replies_count: 1
+ assert_difference ['@topic.reload.replies_count', '@topic.reload.unique_replies_count'], -1 do
Topic.reset_counters(@topic.id, :replies, :unique_replies)
end
end
@@ -127,6 +122,12 @@ class CounterCacheTest < ActiveRecord::TestCase
end
end
+ test 'update multiple counters' do
+ assert_difference ['@topic.reload.replies_count', '@topic.reload.unique_replies_count'], 2 do
+ Topic.update_counters @topic.id, replies_count: 2, unique_replies_count: 2
+ end
+ end
+
test "update other counters on parent destroy" do
david, joanna = dog_lovers(:david, :joanna)
joanna = joanna # squelch a warning
diff --git a/activerecord/test/cases/date_time_test.rb b/activerecord/test/cases/date_time_test.rb
index 427076bd80..c0491bbee5 100644
--- a/activerecord/test/cases/date_time_test.rb
+++ b/activerecord/test/cases/date_time_test.rb
@@ -5,7 +5,7 @@ require 'models/task'
class DateTimeTest < ActiveRecord::TestCase
def test_saves_both_date_and_time
with_env_tz 'America/New_York' do
- with_active_record_default_timezone :utc do
+ with_timezone_config default: :utc do
time_values = [1807, 2, 10, 15, 30, 45]
# create DateTime value with local time zone offset
local_offset = Rational(Time.local(*time_values).utc_offset, 86400)
diff --git a/activerecord/test/cases/defaults_test.rb b/activerecord/test/cases/defaults_test.rb
index e0cf4adf13..7e3d91e08c 100644
--- a/activerecord/test/cases/defaults_test.rb
+++ b/activerecord/test/cases/defaults_test.rb
@@ -39,6 +39,31 @@ class DefaultTest < ActiveRecord::TestCase
end
end
+class DefaultStringsTest < ActiveRecord::TestCase
+ class DefaultString < ActiveRecord::Base; end
+
+ setup do
+ @connection = ActiveRecord::Base.connection
+ @connection.create_table :default_strings do |t|
+ t.string :string_col, default: "Smith"
+ t.string :string_col_with_quotes, default: "O'Connor"
+ end
+ DefaultString.reset_column_information
+ end
+
+ def test_default_strings
+ assert_equal "Smith", DefaultString.new.string_col
+ end
+
+ def test_default_strings_containing_single_quotes
+ assert_equal "O'Connor", DefaultString.new.string_col_with_quotes
+ end
+
+ teardown do
+ @connection.drop_table :default_strings
+ end
+end
+
if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
class DefaultsTestWithoutTransactionalFixtures < ActiveRecord::TestCase
# ActiveRecord::Base#create! (and #save and other related methods) will
diff --git a/activerecord/test/cases/deprecated_dynamic_methods_test.rb b/activerecord/test/cases/deprecated_dynamic_methods_test.rb
deleted file mode 100644
index 8e842d8758..0000000000
--- a/activerecord/test/cases/deprecated_dynamic_methods_test.rb
+++ /dev/null
@@ -1,592 +0,0 @@
-# This file should be deleted when activerecord-deprecated_finders is removed as
-# a dependency.
-#
-# It is kept for now as there is some fairly nuanced behavior in the dynamic
-# finders so it is useful to keep this around to guard against regressions if
-# we need to change the code.
-
-require 'cases/helper'
-require 'models/topic'
-require 'models/reply'
-require 'models/customer'
-require 'models/post'
-require 'models/company'
-require 'models/author'
-require 'models/category'
-require 'models/comment'
-require 'models/person'
-require 'models/reader'
-
-class DeprecatedDynamicMethodsTest < ActiveRecord::TestCase
- fixtures :topics, :customers, :companies, :accounts, :posts, :categories, :categories_posts, :authors, :people, :comments, :readers
-
- def setup
- @deprecation_behavior = ActiveSupport::Deprecation.behavior
- ActiveSupport::Deprecation.behavior = :silence
- end
-
- def teardown
- ActiveSupport::Deprecation.behavior = @deprecation_behavior
- end
-
- def test_find_all_by_one_attribute
- topics = Topic.find_all_by_content("Have a nice day")
- assert_equal 2, topics.size
- assert topics.include?(topics(:first))
-
- assert_equal [], Topic.find_all_by_title("The First Topic!!")
- end
-
- def test_find_all_by_one_attribute_which_is_a_symbol
- topics = Topic.find_all_by_content("Have a nice day".to_sym)
- assert_equal 2, topics.size
- assert topics.include?(topics(:first))
-
- assert_equal [], Topic.find_all_by_title("The First Topic!!")
- end
-
- def test_find_all_by_one_attribute_that_is_an_aggregate
- balance = customers(:david).balance
- assert_kind_of Money, balance
- found_customers = Customer.find_all_by_balance(balance)
- assert_equal 1, found_customers.size
- assert_equal customers(:david), found_customers.first
- end
-
- def test_find_all_by_two_attributes_that_are_both_aggregates
- balance = customers(:david).balance
- address = customers(:david).address
- assert_kind_of Money, balance
- assert_kind_of Address, address
- found_customers = Customer.find_all_by_balance_and_address(balance, address)
- assert_equal 1, found_customers.size
- assert_equal customers(:david), found_customers.first
- end
-
- def test_find_all_by_two_attributes_with_one_being_an_aggregate
- balance = customers(:david).balance
- assert_kind_of Money, balance
- found_customers = Customer.find_all_by_balance_and_name(balance, customers(:david).name)
- assert_equal 1, found_customers.size
- assert_equal customers(:david), found_customers.first
- end
-
- def test_find_all_by_one_attribute_with_options
- topics = Topic.find_all_by_content("Have a nice day", :order => "id DESC")
- assert_equal topics(:first), topics.last
-
- topics = Topic.find_all_by_content("Have a nice day", :order => "id")
- assert_equal topics(:first), topics.first
- end
-
- def test_find_all_by_array_attribute
- assert_equal 2, Topic.find_all_by_title(["The First Topic", "The Second Topic of the day"]).size
- end
-
- def test_find_all_by_boolean_attribute
- topics = Topic.find_all_by_approved(false)
- assert_equal 1, topics.size
- assert topics.include?(topics(:first))
-
- topics = Topic.find_all_by_approved(true)
- assert_equal 3, topics.size
- assert topics.include?(topics(:second))
- end
-
- def test_find_all_by_nil_and_not_nil_attributes
- topics = Topic.find_all_by_last_read_and_author_name nil, "Mary"
- assert_equal 1, topics.size
- assert_equal "Mary", topics[0].author_name
- end
-
- def test_find_or_create_from_one_attribute
- number_of_companies = Company.count
- sig38 = Company.find_or_create_by_name("38signals")
- assert_equal number_of_companies + 1, Company.count
- assert_equal sig38, Company.find_or_create_by_name("38signals")
- assert sig38.persisted?
- end
-
- def test_find_or_create_from_two_attributes
- number_of_topics = Topic.count
- another = Topic.find_or_create_by_title_and_author_name("Another topic","John")
- assert_equal number_of_topics + 1, Topic.count
- assert_equal another, Topic.find_or_create_by_title_and_author_name("Another topic", "John")
- assert another.persisted?
- end
-
- def test_find_or_create_from_one_attribute_bang
- number_of_companies = Company.count
- assert_raises(ActiveRecord::RecordInvalid) { Company.find_or_create_by_name!("") }
- assert_equal number_of_companies, Company.count
- sig38 = Company.find_or_create_by_name!("38signals")
- assert_equal number_of_companies + 1, Company.count
- assert_equal sig38, Company.find_or_create_by_name!("38signals")
- assert sig38.persisted?
- end
-
- def test_find_or_create_from_two_attributes_bang
- number_of_companies = Company.count
- assert_raises(ActiveRecord::RecordInvalid) { Company.find_or_create_by_name_and_firm_id!("", 17) }
- assert_equal number_of_companies, Company.count
- sig38 = Company.find_or_create_by_name_and_firm_id!("38signals", 17)
- assert_equal number_of_companies + 1, Company.count
- assert_equal sig38, Company.find_or_create_by_name_and_firm_id!("38signals", 17)
- assert sig38.persisted?
- assert_equal "38signals", sig38.name
- assert_equal 17, sig38.firm_id
- end
-
- def test_find_or_create_from_two_attributes_with_one_being_an_aggregate
- number_of_customers = Customer.count
- created_customer = Customer.find_or_create_by_balance_and_name(Money.new(123), "Elizabeth")
- assert_equal number_of_customers + 1, Customer.count
- assert_equal created_customer, Customer.find_or_create_by_balance(Money.new(123), "Elizabeth")
- assert created_customer.persisted?
- end
-
- def test_find_or_create_from_one_attribute_and_hash
- number_of_companies = Company.count
- sig38 = Company.find_or_create_by_name({:name => "38signals", :firm_id => 17, :client_of => 23})
- assert_equal number_of_companies + 1, Company.count
- assert_equal sig38, Company.find_or_create_by_name({:name => "38signals", :firm_id => 17, :client_of => 23})
- assert sig38.persisted?
- assert_equal "38signals", sig38.name
- assert_equal 17, sig38.firm_id
- assert_equal 23, sig38.client_of
- end
-
- def test_find_or_create_from_two_attributes_and_hash
- number_of_companies = Company.count
- sig38 = Company.find_or_create_by_name_and_firm_id({:name => "38signals", :firm_id => 17, :client_of => 23})
- assert_equal number_of_companies + 1, Company.count
- assert_equal sig38, Company.find_or_create_by_name_and_firm_id({:name => "38signals", :firm_id => 17, :client_of => 23})
- assert sig38.persisted?
- assert_equal "38signals", sig38.name
- assert_equal 17, sig38.firm_id
- assert_equal 23, sig38.client_of
- end
-
- def test_find_or_create_from_one_aggregate_attribute
- number_of_customers = Customer.count
- created_customer = Customer.find_or_create_by_balance(Money.new(123))
- assert_equal number_of_customers + 1, Customer.count
- assert_equal created_customer, Customer.find_or_create_by_balance(Money.new(123))
- assert created_customer.persisted?
- end
-
- def test_find_or_create_from_one_aggregate_attribute_and_hash
- number_of_customers = Customer.count
- balance = Money.new(123)
- name = "Elizabeth"
- created_customer = Customer.find_or_create_by_balance({:balance => balance, :name => name})
- assert_equal number_of_customers + 1, Customer.count
- assert_equal created_customer, Customer.find_or_create_by_balance({:balance => balance, :name => name})
- assert created_customer.persisted?
- assert_equal balance, created_customer.balance
- assert_equal name, created_customer.name
- end
-
- def test_find_or_initialize_from_one_attribute
- sig38 = Company.find_or_initialize_by_name("38signals")
- assert_equal "38signals", sig38.name
- assert !sig38.persisted?
- end
-
- def test_find_or_initialize_from_one_aggregate_attribute
- new_customer = Customer.find_or_initialize_by_balance(Money.new(123))
- assert_equal 123, new_customer.balance.amount
- assert !new_customer.persisted?
- end
-
- def test_find_or_initialize_from_one_attribute_should_set_attribute
- c = Company.find_or_initialize_by_name_and_rating("Fortune 1000", 1000)
- assert_equal "Fortune 1000", c.name
- assert_equal 1000, c.rating
- assert c.valid?
- assert !c.persisted?
- end
-
- def test_find_or_create_from_one_attribute_should_set_attribute
- c = Company.find_or_create_by_name_and_rating("Fortune 1000", 1000)
- assert_equal "Fortune 1000", c.name
- assert_equal 1000, c.rating
- assert c.valid?
- assert c.persisted?
- end
-
- def test_find_or_initialize_from_one_attribute_should_set_attribute_even_when_set_the_hash
- c = Company.find_or_initialize_by_rating(1000, {:name => "Fortune 1000"})
- assert_equal "Fortune 1000", c.name
- assert_equal 1000, c.rating
- assert c.valid?
- assert !c.persisted?
- end
-
- def test_find_or_create_from_one_attribute_should_set_attribute_even_when_set_the_hash
- c = Company.find_or_create_by_rating(1000, {:name => "Fortune 1000"})
- assert_equal "Fortune 1000", c.name
- assert_equal 1000, c.rating
- assert c.valid?
- assert c.persisted?
- end
-
- def test_find_or_initialize_should_set_attributes_if_given_as_block
- c = Company.find_or_initialize_by_name(:name => "Fortune 1000") { |f| f.rating = 1000 }
- assert_equal "Fortune 1000", c.name
- assert_equal 1000.to_f, c.rating.to_f
- assert c.valid?
- assert !c.persisted?
- end
-
- def test_find_or_create_should_set_attributes_if_given_as_block
- c = Company.find_or_create_by_name(:name => "Fortune 1000") { |f| f.rating = 1000 }
- assert_equal "Fortune 1000", c.name
- assert_equal 1000.to_f, c.rating.to_f
- assert c.valid?
- assert c.persisted?
- end
-
- def test_find_or_create_should_work_with_block_on_first_call
- class << Company
- undef_method(:find_or_create_by_name) if method_defined?(:find_or_create_by_name)
- end
- c = Company.find_or_create_by_name(:name => "Fortune 1000") { |f| f.rating = 1000 }
- assert_equal "Fortune 1000", c.name
- assert_equal 1000.to_f, c.rating.to_f
- assert c.valid?
- assert c.persisted?
- end
-
- def test_find_or_initialize_from_two_attributes
- another = Topic.find_or_initialize_by_title_and_author_name("Another topic","John")
- assert_equal "Another topic", another.title
- assert_equal "John", another.author_name
- assert !another.persisted?
- end
-
- def test_find_or_initialize_from_two_attributes_but_passing_only_one
- assert_raise(ArgumentError) { Topic.find_or_initialize_by_title_and_author_name("Another topic") }
- end
-
- def test_find_or_initialize_from_one_aggregate_attribute_and_one_not
- new_customer = Customer.find_or_initialize_by_balance_and_name(Money.new(123), "Elizabeth")
- assert_equal 123, new_customer.balance.amount
- assert_equal "Elizabeth", new_customer.name
- assert !new_customer.persisted?
- end
-
- def test_find_or_initialize_from_one_attribute_and_hash
- sig38 = Company.find_or_initialize_by_name({:name => "38signals", :firm_id => 17, :client_of => 23})
- assert_equal "38signals", sig38.name
- assert_equal 17, sig38.firm_id
- assert_equal 23, sig38.client_of
- assert !sig38.persisted?
- end
-
- def test_find_or_initialize_from_one_aggregate_attribute_and_hash
- balance = Money.new(123)
- name = "Elizabeth"
- new_customer = Customer.find_or_initialize_by_balance({:balance => balance, :name => name})
- assert_equal balance, new_customer.balance
- assert_equal name, new_customer.name
- assert !new_customer.persisted?
- end
-
- def test_find_last_by_one_attribute
- assert_equal Topic.last, Topic.find_last_by_title(Topic.last.title)
- assert_nil Topic.find_last_by_title("A title with no matches")
- end
-
- def test_find_last_by_invalid_method_syntax
- assert_raise(NoMethodError) { Topic.fail_to_find_last_by_title("The First Topic") }
- assert_raise(NoMethodError) { Topic.find_last_by_title?("The First Topic") }
- end
-
- def test_find_last_by_one_attribute_with_several_options
- assert_equal accounts(:signals37), Account.order('id DESC').where('id != ?', 3).find_last_by_credit_limit(50)
- end
-
- def test_find_last_by_one_missing_attribute
- assert_raise(NoMethodError) { Topic.find_last_by_undertitle("The Last Topic!") }
- end
-
- def test_find_last_by_two_attributes
- topic = Topic.last
- assert_equal topic, Topic.find_last_by_title_and_author_name(topic.title, topic.author_name)
- assert_nil Topic.find_last_by_title_and_author_name(topic.title, "Anonymous")
- end
-
- def test_find_last_with_limit_gives_same_result_when_loaded_and_unloaded
- scope = Topic.limit(2)
- unloaded_last = scope.last
- loaded_last = scope.to_a.last
- assert_equal loaded_last, unloaded_last
- end
-
- def test_find_last_with_limit_and_offset_gives_same_result_when_loaded_and_unloaded
- scope = Topic.offset(2).limit(2)
- unloaded_last = scope.last
- loaded_last = scope.to_a.last
- assert_equal loaded_last, unloaded_last
- end
-
- def test_find_last_with_offset_gives_same_result_when_loaded_and_unloaded
- scope = Topic.offset(3)
- unloaded_last = scope.last
- loaded_last = scope.to_a.last
- assert_equal loaded_last, unloaded_last
- end
-
- def test_find_all_by_nil_attribute
- topics = Topic.find_all_by_last_read nil
- assert_equal 3, topics.size
- assert topics.collect(&:last_read).all?(&:nil?)
- end
-
- def test_forwarding_to_dynamic_finders
- welcome = Post.find(1)
- assert_equal 4, Category.find_all_by_type('SpecialCategory').size
- assert_equal 0, welcome.categories.find_all_by_type('SpecialCategory').size
- assert_equal 2, welcome.categories.find_all_by_type('Category').size
- end
-
- def test_dynamic_find_all_should_respect_association_order
- assert_equal [companies(:second_client), companies(:first_client)], companies(:first_firm).clients_sorted_desc.where("type = 'Client'").to_a
- assert_equal [companies(:second_client), companies(:first_client)], companies(:first_firm).clients_sorted_desc.find_all_by_type('Client')
- end
-
- def test_dynamic_find_all_should_respect_association_limit
- assert_equal 1, companies(:first_firm).limited_clients.where("type = 'Client'").to_a.length
- assert_equal 1, companies(:first_firm).limited_clients.find_all_by_type('Client').length
- end
-
- def test_dynamic_find_all_limit_should_override_association_limit
- assert_equal 2, companies(:first_firm).limited_clients.where("type = 'Client'").limit(9_000).to_a.length
- assert_equal 2, companies(:first_firm).limited_clients.find_all_by_type('Client', :limit => 9_000).length
- end
-
- def test_dynamic_find_last_without_specified_order
- assert_equal companies(:second_client), companies(:first_firm).unsorted_clients.find_last_by_type('Client')
- end
-
- def test_dynamic_find_or_create_from_two_attributes_using_an_association
- author = authors(:david)
- number_of_posts = Post.count
- another = author.posts.find_or_create_by_title_and_body("Another Post", "This is the Body")
- assert_equal number_of_posts + 1, Post.count
- assert_equal another, author.posts.find_or_create_by_title_and_body("Another Post", "This is the Body")
- assert another.persisted?
- end
-
- def test_dynamic_find_all_should_respect_association_order_for_through
- assert_equal [Comment.find(10), Comment.find(7), Comment.find(6), Comment.find(3)], authors(:david).comments_desc.where("comments.type = 'SpecialComment'").to_a
- assert_equal [Comment.find(10), Comment.find(7), Comment.find(6), Comment.find(3)], authors(:david).comments_desc.find_all_by_type('SpecialComment')
- end
-
- def test_dynamic_find_all_should_respect_association_limit_for_through
- assert_equal 1, authors(:david).limited_comments.where("comments.type = 'SpecialComment'").to_a.length
- assert_equal 1, authors(:david).limited_comments.find_all_by_type('SpecialComment').length
- end
-
- def test_dynamic_find_all_order_should_override_association_limit_for_through
- assert_equal 4, authors(:david).limited_comments.where("comments.type = 'SpecialComment'").limit(9_000).to_a.length
- assert_equal 4, authors(:david).limited_comments.find_all_by_type('SpecialComment', :limit => 9_000).length
- end
-
- def test_find_all_include_over_the_same_table_for_through
- assert_equal 2, people(:michael).posts.includes(:people).to_a.length
- end
-
- def test_find_or_create_by_resets_cached_counters
- person = Person.create! :first_name => 'tenderlove'
- post = Post.first
-
- assert_equal [], person.readers
- assert_nil person.readers.find_by_post_id(post.id)
-
- person.readers.find_or_create_by_post_id(post.id)
-
- assert_equal 1, person.readers.count
- assert_equal 1, person.readers.length
- assert_equal post, person.readers.first.post
- assert_equal person, person.readers.first.person
- end
-
- def test_find_or_initialize
- the_client = companies(:first_firm).clients.find_or_initialize_by_name("Yet another client")
- assert_equal companies(:first_firm).id, the_client.firm_id
- assert_equal "Yet another client", the_client.name
- assert !the_client.persisted?
- end
-
- def test_find_or_create_updates_size
- number_of_clients = companies(:first_firm).clients.size
- the_client = companies(:first_firm).clients.find_or_create_by_name("Yet another client")
- assert_equal number_of_clients + 1, companies(:first_firm, :reload).clients.size
- assert_equal the_client, companies(:first_firm).clients.find_or_create_by_name("Yet another client")
- assert_equal number_of_clients + 1, companies(:first_firm, :reload).clients.size
- end
-
- def test_find_or_initialize_updates_collection_size
- number_of_clients = companies(:first_firm).clients_of_firm.size
- companies(:first_firm).clients_of_firm.find_or_initialize_by_name("name" => "Another Client")
- assert_equal number_of_clients + 1, companies(:first_firm).clients_of_firm.size
- end
-
- def test_find_or_initialize_returns_the_instantiated_object
- client = companies(:first_firm).clients_of_firm.find_or_initialize_by_name("name" => "Another Client")
- assert_equal client, companies(:first_firm).clients_of_firm[-1]
- end
-
- def test_find_or_initialize_only_instantiates_a_single_object
- number_of_clients = Client.count
- companies(:first_firm).clients_of_firm.find_or_initialize_by_name("name" => "Another Client").save!
- companies(:first_firm).save!
- assert_equal number_of_clients+1, Client.count
- end
-
- def test_find_or_create_with_hash
- post = authors(:david).posts.find_or_create_by_title(:title => 'Yet another post', :body => 'somebody')
- assert_equal post, authors(:david).posts.find_or_create_by_title(:title => 'Yet another post', :body => 'somebody')
- assert post.persisted?
- end
-
- def test_find_or_create_with_one_attribute_followed_by_hash
- post = authors(:david).posts.find_or_create_by_title('Yet another post', :body => 'somebody')
- assert_equal post, authors(:david).posts.find_or_create_by_title('Yet another post', :body => 'somebody')
- assert post.persisted?
- end
-
- def test_find_or_create_should_work_with_block
- post = authors(:david).posts.find_or_create_by_title('Yet another post') {|p| p.body = 'somebody'}
- assert_equal post, authors(:david).posts.find_or_create_by_title('Yet another post') {|p| p.body = 'somebody'}
- assert post.persisted?
- end
-
- def test_forwarding_to_dynamic_finders_2
- welcome = Post.find(1)
- assert_equal 4, Comment.find_all_by_type('Comment').size
- assert_equal 2, welcome.comments.find_all_by_type('Comment').size
- end
-
- def test_dynamic_find_all_by_attributes
- authors = Author.all
-
- davids = authors.find_all_by_name('David')
- assert_kind_of Array, davids
- assert_equal [authors(:david)], davids
- end
-
- def test_dynamic_find_or_initialize_by_attributes
- authors = Author.all
-
- lifo = authors.find_or_initialize_by_name('Lifo')
- assert_equal "Lifo", lifo.name
- assert !lifo.persisted?
-
- assert_equal authors(:david), authors.find_or_initialize_by_name(:name => 'David')
- end
-
- def test_dynamic_find_or_create_by_attributes
- authors = Author.all
-
- lifo = authors.find_or_create_by_name('Lifo')
- assert_equal "Lifo", lifo.name
- assert lifo.persisted?
-
- assert_equal authors(:david), authors.find_or_create_by_name(:name => 'David')
- end
-
- def test_dynamic_find_or_create_by_attributes_bang
- authors = Author.all
-
- assert_raises(ActiveRecord::RecordInvalid) { authors.find_or_create_by_name!('') }
-
- lifo = authors.find_or_create_by_name!('Lifo')
- assert_equal "Lifo", lifo.name
- assert lifo.persisted?
-
- assert_equal authors(:david), authors.find_or_create_by_name!(:name => 'David')
- end
-
- def test_finder_block
- t = Topic.first
- found = nil
- Topic.find_by_id(t.id) { |f| found = f }
- assert_equal t, found
- end
-
- def test_finder_block_nothing_found
- bad_id = Topic.maximum(:id) + 1
- assert_nil Topic.find_by_id(bad_id) { |f| raise }
- end
-
- def test_find_returns_block_value
- t = Topic.first
- x = Topic.find_by_id(t.id) { |f| "hi mom!" }
- assert_equal "hi mom!", x
- end
-
- def test_dynamic_finder_with_invalid_params
- assert_raise(ArgumentError) { Topic.find_by_title 'No Title', :join => "It should be `joins'" }
- end
-
- def test_find_by_one_attribute_with_order_option
- assert_equal accounts(:signals37), Account.find_by_credit_limit(50, :order => 'id')
- assert_equal accounts(:rails_core_account), Account.find_by_credit_limit(50, :order => 'id DESC')
- end
-
- def test_dynamic_find_by_attributes_should_yield_found_object
- david = authors(:david)
- yielded_value = nil
- Author.find_by_name(david.name) do |author|
- yielded_value = author
- end
- assert_equal david, yielded_value
- end
-end
-
-class DynamicScopeTest < ActiveRecord::TestCase
- fixtures :posts
-
- def setup
- @test_klass = Class.new(Post) do
- def self.name; "Post"; end
- end
- @deprecation_behavior = ActiveSupport::Deprecation.behavior
- ActiveSupport::Deprecation.behavior = :silence
- end
-
- def teardown
- ActiveSupport::Deprecation.behavior = @deprecation_behavior
- end
-
- def test_dynamic_scope
- assert_equal @test_klass.scoped_by_author_id(1).find(1), @test_klass.find(1)
- assert_equal @test_klass.scoped_by_author_id_and_title(1, "Welcome to the weblog").first, @test_klass.all.merge!(:where => { :author_id => 1, :title => "Welcome to the weblog"}).first
- end
-
- def test_dynamic_scope_should_create_methods_after_hitting_method_missing
- assert @test_klass.methods.grep(/scoped_by_type/).blank?
- @test_klass.scoped_by_type(nil)
- assert @test_klass.methods.grep(/scoped_by_type/).present?
- end
-
- def test_dynamic_scope_with_less_number_of_arguments
- assert_raise(ArgumentError){ @test_klass.scoped_by_author_id_and_title(1) }
- end
-end
-
-class DynamicScopeMatchTest < ActiveRecord::TestCase
- def test_scoped_by_no_match
- assert_nil ActiveRecord::DynamicMatchers::Method.match(nil, "not_scoped_at_all")
- end
-
- def test_scoped_by
- model = stub(attribute_aliases: {})
- match = ActiveRecord::DynamicMatchers::Method.match(model, "scoped_by_age_and_sex_and_location")
- assert_not_nil match
- assert_equal %w(age sex location), match.attribute_names
- end
-end
diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb
index 36b87033ae..df4183c065 100644
--- a/activerecord/test/cases/dirty_test.rb
+++ b/activerecord/test/cases/dirty_test.rb
@@ -125,30 +125,30 @@ class DirtyTest < ActiveRecord::TestCase
end
def test_time_attributes_changes_without_time_zone
- target = Class.new(ActiveRecord::Base)
- target.table_name = 'pirates'
-
- target.time_zone_aware_attributes = false
+ with_timezone_config aware_attributes: false do
+ target = Class.new(ActiveRecord::Base)
+ target.table_name = 'pirates'
- # New record - no changes.
- pirate = target.new
- assert !pirate.created_on_changed?
- assert_nil pirate.created_on_change
+ # New record - no changes.
+ pirate = target.new
+ assert !pirate.created_on_changed?
+ assert_nil pirate.created_on_change
- # Saved - no changes.
- pirate.catchphrase = 'arrrr, time zone!!'
- pirate.save!
- assert !pirate.created_on_changed?
- assert_nil pirate.created_on_change
+ # Saved - no changes.
+ pirate.catchphrase = 'arrrr, time zone!!'
+ pirate.save!
+ assert !pirate.created_on_changed?
+ assert_nil pirate.created_on_change
- # Change created_on.
- old_created_on = pirate.created_on
- pirate.created_on = Time.now + 1.day
- assert pirate.created_on_changed?
- # kind_of does not work because
- # ActiveSupport::TimeWithZone.name == 'Time'
- assert_instance_of Time, pirate.created_on_was
- assert_equal old_created_on, pirate.created_on_was
+ # Change created_on.
+ old_created_on = pirate.created_on
+ pirate.created_on = Time.now + 1.day
+ assert pirate.created_on_changed?
+ # kind_of does not work because
+ # ActiveSupport::TimeWithZone.name == 'Time'
+ assert_instance_of Time, pirate.created_on_was
+ assert_equal old_created_on, pirate.created_on_was
+ end
end
@@ -584,6 +584,14 @@ class DirtyTest < ActiveRecord::TestCase
end
end
+ def test_datetime_attribute_doesnt_change_if_zone_is_modified_in_string
+ time_in_paris = Time.utc(2014, 1, 1, 12, 0, 0).in_time_zone('Paris')
+ pirate = Pirate.create!(:catchphrase => 'rrrr', :created_on => time_in_paris)
+
+ pirate.created_on = pirate.created_on.in_time_zone('Tokyo').to_s
+ assert !pirate.created_on_changed?
+ end
+
test "partial insert" do
with_partial_writes Person do
jon = nil
@@ -608,20 +616,6 @@ class DirtyTest < ActiveRecord::TestCase
end
end
- test "partial_updates config attribute is deprecated" do
- klass = Class.new(ActiveRecord::Base)
-
- assert klass.partial_writes?
- assert_deprecated { assert klass.partial_updates? }
- assert_deprecated { assert klass.partial_updates }
-
- assert_deprecated { klass.partial_updates = false }
-
- assert !klass.partial_writes?
- assert_deprecated { assert !klass.partial_updates? }
- assert_deprecated { assert !klass.partial_updates }
- end
-
private
def with_partial_writes(klass, on = true)
old = klass.partial_writes?
diff --git a/activerecord/test/cases/disconnected_test.rb b/activerecord/test/cases/disconnected_test.rb
new file mode 100644
index 0000000000..9e268dad74
--- /dev/null
+++ b/activerecord/test/cases/disconnected_test.rb
@@ -0,0 +1,28 @@
+require "cases/helper"
+
+class TestRecord < ActiveRecord::Base
+end
+
+class TestDisconnectedAdapter < ActiveRecord::TestCase
+ self.use_transactional_fixtures = false
+
+ def setup
+ @connection = ActiveRecord::Base.connection
+ end
+
+ def teardown
+ return if in_memory_db?
+ spec = ActiveRecord::Base.connection_config
+ ActiveRecord::Base.establish_connection(spec)
+ end
+
+ unless in_memory_db?
+ test "can't execute statements while disconnected" do
+ @connection.execute "SELECT count(*) from products"
+ @connection.disconnect!
+ assert_raises(ActiveRecord::StatementInvalid) do
+ @connection.execute "SELECT count(*) from products"
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/dup_test.rb b/activerecord/test/cases/dup_test.rb
index f73e449610..1e6ccecfab 100644
--- a/activerecord/test/cases/dup_test.rb
+++ b/activerecord/test/cases/dup_test.rb
@@ -126,7 +126,7 @@ module ActiveRecord
def test_dup_with_default_scope
prev_default_scopes = Topic.default_scopes
- Topic.default_scopes = [Topic.where(:approved => true)]
+ Topic.default_scopes = [proc { Topic.where(:approved => true) }]
topic = Topic.new(:approved => false)
assert !topic.dup.approved?, "should not be overridden by default scopes"
ensure
diff --git a/activerecord/test/cases/enum_test.rb b/activerecord/test/cases/enum_test.rb
new file mode 100644
index 0000000000..c09e58fbf1
--- /dev/null
+++ b/activerecord/test/cases/enum_test.rb
@@ -0,0 +1,91 @@
+require 'cases/helper'
+require 'models/book'
+
+class EnumTest < ActiveRecord::TestCase
+ fixtures :books
+
+ setup do
+ @book = books(:awdr)
+ end
+
+ test "query state by predicate" do
+ assert @book.proposed?
+ assert_not @book.written?
+ assert_not @book.published?
+
+ assert @book.unread?
+ end
+
+ test "query state with strings" do
+ assert_equal "proposed", @book.status
+ assert_equal "unread", @book.read_status
+ end
+
+ test "find via scope" do
+ assert_equal @book, Book.proposed.first
+ assert_equal @book, Book.unread.first
+ end
+
+ test "update by declaration" do
+ @book.written!
+ assert @book.written?
+ end
+
+ test "update by setter" do
+ @book.update! status: :written
+ assert @book.written?
+ end
+
+ test "enum methods are overwritable" do
+ assert_equal "do publish work...", @book.published!
+ assert @book.published?
+ end
+
+ test "direct assignment" do
+ @book.status = :written
+ assert @book.written?
+ end
+
+ test "assign string value" do
+ @book.status = "written"
+ assert @book.written?
+ end
+
+ test "assign non existing value raises an error" do
+ e = assert_raises(ArgumentError) do
+ @book.status = :unknown
+ end
+ assert_equal "'unknown' is not a valid status", e.message
+ end
+
+ test "assign nil value" do
+ @book.status = nil
+ assert @book.status.nil?
+ end
+
+ test "assign empty string value" do
+ @book.status = ''
+ assert @book.status.nil?
+ end
+
+ test "assign long empty string value" do
+ @book.status = ' '
+ assert @book.status.nil?
+ end
+
+ test "constant to access the mapping" do
+ assert_equal 0, Book::STATUS[:proposed]
+ assert_equal 1, Book::STATUS["written"]
+ assert_equal 2, Book::STATUS[:published]
+ end
+
+ test "building new objects with enum scopes" do
+ assert Book.written.build.written?
+ assert Book.read.build.read?
+ end
+
+ test "creating new objects with enum scopes" do
+ assert Book.written.create.written?
+ assert Book.read.create.read?
+ end
+end
diff --git a/activerecord/test/cases/finder_respond_to_test.rb b/activerecord/test/cases/finder_respond_to_test.rb
index 9440cd429a..3ff22f222f 100644
--- a/activerecord/test/cases/finder_respond_to_test.rb
+++ b/activerecord/test/cases/finder_respond_to_test.rb
@@ -21,14 +21,9 @@ class FinderRespondToTest < ActiveRecord::TestCase
assert_respond_to Topic, :find_by_title
end
- def test_should_respond_to_find_all_by_one_attribute
- ensure_topic_method_is_not_cached(:find_all_by_title)
- assert_respond_to Topic, :find_all_by_title
- end
-
- def test_should_respond_to_find_all_by_two_attributes
- ensure_topic_method_is_not_cached(:find_all_by_title_and_author_name)
- assert_respond_to Topic, :find_all_by_title_and_author_name
+ def test_should_respond_to_find_by_with_bang
+ ensure_topic_method_is_not_cached(:find_by_title!)
+ assert_respond_to Topic, :find_by_title!
end
def test_should_respond_to_find_by_two_attributes
@@ -41,36 +36,6 @@ class FinderRespondToTest < ActiveRecord::TestCase
assert_respond_to Topic, :find_by_heading
end
- def test_should_respond_to_find_or_initialize_from_one_attribute
- ensure_topic_method_is_not_cached(:find_or_initialize_by_title)
- assert_respond_to Topic, :find_or_initialize_by_title
- end
-
- def test_should_respond_to_find_or_initialize_from_two_attributes
- ensure_topic_method_is_not_cached(:find_or_initialize_by_title_and_author_name)
- assert_respond_to Topic, :find_or_initialize_by_title_and_author_name
- end
-
- def test_should_respond_to_find_or_create_from_one_attribute
- ensure_topic_method_is_not_cached(:find_or_create_by_title)
- assert_respond_to Topic, :find_or_create_by_title
- end
-
- def test_should_respond_to_find_or_create_from_two_attributes
- ensure_topic_method_is_not_cached(:find_or_create_by_title_and_author_name)
- assert_respond_to Topic, :find_or_create_by_title_and_author_name
- end
-
- def test_should_respond_to_find_or_create_from_one_attribute_bang
- ensure_topic_method_is_not_cached(:find_or_create_by_title!)
- assert_respond_to Topic, :find_or_create_by_title!
- end
-
- def test_should_respond_to_find_or_create_from_two_attributes_bang
- ensure_topic_method_is_not_cached(:find_or_create_by_title_and_author_name!)
- assert_respond_to Topic, :find_or_create_by_title_and_author_name!
- end
-
def test_should_not_respond_to_find_by_one_missing_attribute
assert !Topic.respond_to?(:find_by_undertitle)
end
diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb
index 6f0de42aef..5125d5df2a 100644
--- a/activerecord/test/cases/finder_test.rb
+++ b/activerecord/test/cases/finder_test.rb
@@ -11,6 +11,8 @@ require 'models/project'
require 'models/developer'
require 'models/customer'
require 'models/toy'
+require 'models/matey'
+require 'models/dog'
class FinderTest < ActiveRecord::TestCase
fixtures :companies, :topics, :entrants, :developers, :developers_projects, :posts, :comments, :accounts, :authors, :customers, :categories, :categorizations
@@ -45,17 +47,19 @@ class FinderTest < ActiveRecord::TestCase
end
def test_exists
- assert Topic.exists?(1)
- assert Topic.exists?("1")
- assert Topic.exists?(title: "The First Topic")
- assert Topic.exists?(heading: "The First Topic")
- assert Topic.exists?(:author_name => "Mary", :approved => true)
- assert Topic.exists?(["parent_id = ?", 1])
- assert !Topic.exists?(45)
- assert !Topic.exists?(Topic.new)
+ assert_equal true, Topic.exists?(1)
+ assert_equal true, Topic.exists?("1")
+ assert_equal true, Topic.exists?(title: "The First Topic")
+ assert_equal true, Topic.exists?(heading: "The First Topic")
+ assert_equal true, Topic.exists?(:author_name => "Mary", :approved => true)
+ assert_equal true, Topic.exists?(["parent_id = ?", 1])
+ assert_equal true, Topic.exists?(id: [1, 9999])
+
+ assert_equal false, Topic.exists?(45)
+ assert_equal false, Topic.exists?(Topic.new)
begin
- assert !Topic.exists?("foo")
+ assert_equal false, Topic.exists?("foo")
rescue ActiveRecord::StatementInvalid
# PostgreSQL complains about string comparison with integer field
rescue Exception
@@ -72,61 +76,62 @@ class FinderTest < ActiveRecord::TestCase
end
def test_exists_returns_true_with_one_record_and_no_args
- assert Topic.exists?
+ assert_equal true, Topic.exists?
end
def test_exists_returns_false_with_false_arg
- assert !Topic.exists?(false)
+ assert_equal false, Topic.exists?(false)
end
# exists? should handle nil for id's that come from URLs and always return false
# (example: Topic.exists?(params[:id])) where params[:id] is nil
def test_exists_with_nil_arg
- assert !Topic.exists?(nil)
- assert Topic.exists?
- assert !Topic.first.replies.exists?(nil)
- assert Topic.first.replies.exists?
+ assert_equal false, Topic.exists?(nil)
+ assert_equal true, Topic.exists?
+
+ assert_equal false, Topic.first.replies.exists?(nil)
+ assert_equal true, Topic.first.replies.exists?
end
# ensures +exists?+ runs valid SQL by excluding order value
def test_exists_with_order
- assert Topic.order(:id).distinct.exists?
+ assert_equal true, Topic.order(:id).distinct.exists?
end
def test_exists_with_includes_limit_and_empty_result
- assert !Topic.includes(:replies).limit(0).exists?
- assert !Topic.includes(:replies).limit(1).where('0 = 1').exists?
+ assert_equal false, Topic.includes(:replies).limit(0).exists?
+ assert_equal false, Topic.includes(:replies).limit(1).where('0 = 1').exists?
end
def test_exists_with_distinct_association_includes_and_limit
author = Author.first
- assert !author.unique_categorized_posts.includes(:special_comments).limit(0).exists?
- assert author.unique_categorized_posts.includes(:special_comments).limit(1).exists?
+ assert_equal false, author.unique_categorized_posts.includes(:special_comments).limit(0).exists?
+ assert_equal true, author.unique_categorized_posts.includes(:special_comments).limit(1).exists?
end
def test_exists_with_distinct_association_includes_limit_and_order
author = Author.first
- assert !author.unique_categorized_posts.includes(:special_comments).order('comments.taggings_count DESC').limit(0).exists?
- assert author.unique_categorized_posts.includes(:special_comments).order('comments.taggings_count DESC').limit(1).exists?
+ assert_equal false, author.unique_categorized_posts.includes(:special_comments).order('comments.taggings_count DESC').limit(0).exists?
+ assert_equal true, author.unique_categorized_posts.includes(:special_comments).order('comments.taggings_count DESC').limit(1).exists?
end
def test_exists_with_empty_table_and_no_args_given
Topic.delete_all
- assert !Topic.exists?
+ assert_equal false, Topic.exists?
end
def test_exists_with_aggregate_having_three_mappings
existing_address = customers(:david).address
- assert Customer.exists?(:address => existing_address)
+ assert_equal true, Customer.exists?(:address => existing_address)
end
def test_exists_with_aggregate_having_three_mappings_with_one_difference
existing_address = customers(:david).address
- assert !Customer.exists?(:address =>
+ assert_equal false, Customer.exists?(:address =>
Address.new(existing_address.street, existing_address.city, existing_address.country + "1"))
- assert !Customer.exists?(:address =>
+ assert_equal false, Customer.exists?(:address =>
Address.new(existing_address.street, existing_address.city + "1", existing_address.country))
- assert !Customer.exists?(:address =>
+ assert_equal false, Customer.exists?(:address =>
Address.new(existing_address.street + "1", existing_address.city, existing_address.country))
end
@@ -146,14 +151,14 @@ class FinderTest < ActiveRecord::TestCase
end
def test_find_by_ids_with_limit_and_offset
- assert_equal 2, Entrant.all.merge!(:limit => 2).find([1,3,2]).size
- assert_equal 1, Entrant.all.merge!(:limit => 3, :offset => 2).find([1,3,2]).size
+ assert_equal 2, Entrant.limit(2).find([1,3,2]).size
+ assert_equal 1, Entrant.limit(3).offset(2).find([1,3,2]).size
# Also test an edge case: If you have 11 results, and you set a
# limit of 3 and offset of 9, then you should find that there
# will be only 2 results, regardless of the limit.
devs = Developer.all
- last_devs = Developer.all.merge!(:limit => 3, :offset => 9).find devs.map(&:id)
+ last_devs = Developer.limit(3).offset(9).find devs.map(&:id)
assert_equal 2, last_devs.size
end
@@ -306,7 +311,7 @@ class FinderTest < ActiveRecord::TestCase
end
def test_find_only_some_columns
- topic = Topic.all.merge!(:select => "author_name").find(1)
+ topic = Topic.select("author_name").find(1)
assert_raise(ActiveModel::MissingAttributeError) {topic.title}
assert_raise(ActiveModel::MissingAttributeError) {topic.title?}
assert_nil topic.read_attribute("title")
@@ -318,23 +323,23 @@ class FinderTest < ActiveRecord::TestCase
end
def test_find_on_array_conditions
- assert Topic.all.merge!(:where => ["approved = ?", false]).find(1)
- assert_raise(ActiveRecord::RecordNotFound) { Topic.all.merge!(:where => ["approved = ?", true]).find(1) }
+ assert Topic.where(["approved = ?", false]).find(1)
+ assert_raise(ActiveRecord::RecordNotFound) { Topic.where(["approved = ?", true]).find(1) }
end
def test_find_on_hash_conditions
- assert Topic.all.merge!(:where => { :approved => false }).find(1)
- assert_raise(ActiveRecord::RecordNotFound) { Topic.all.merge!(:where => { :approved => true }).find(1) }
+ assert Topic.where(approved: false).find(1)
+ assert_raise(ActiveRecord::RecordNotFound) { Topic.where(approved: true).find(1) }
end
def test_find_on_hash_conditions_with_explicit_table_name
- assert Topic.all.merge!(:where => { 'topics.approved' => false }).find(1)
- assert_raise(ActiveRecord::RecordNotFound) { Topic.all.merge!(:where => { 'topics.approved' => true }).find(1) }
+ assert Topic.where('topics.approved' => false).find(1)
+ assert_raise(ActiveRecord::RecordNotFound) { Topic.where('topics.approved' => true).find(1) }
end
def test_find_on_hash_conditions_with_hashed_table_name
- assert Topic.all.merge!(:where => {:topics => { :approved => false }}).find(1)
- assert_raise(ActiveRecord::RecordNotFound) { Topic.all.merge!(:where => {:topics => { :approved => true }}).find(1) }
+ assert Topic.where(topics: { approved: false }).find(1)
+ assert_raise(ActiveRecord::RecordNotFound) { Topic.where(topics: { approved: true }).find(1) }
end
def test_find_with_hash_conditions_on_joined_table
@@ -344,7 +349,7 @@ class FinderTest < ActiveRecord::TestCase
end
def test_find_with_hash_conditions_on_joined_table_and_with_range
- firms = DependentFirm.all.merge!(:joins => :account, :where => {:name => 'RailsCore', :accounts => { :credit_limit => 55..60 }})
+ firms = DependentFirm.joins(:account).where(name: 'RailsCore', accounts: { credit_limit: 55..60 })
assert_equal 1, firms.size
assert_equal companies(:rails_core), firms.first
end
@@ -362,71 +367,71 @@ class FinderTest < ActiveRecord::TestCase
end
def test_find_on_hash_conditions_with_range
- assert_equal [1,2], Topic.all.merge!(:where => { :id => 1..2 }).to_a.map(&:id).sort
- assert_raise(ActiveRecord::RecordNotFound) { Topic.all.merge!(:where => { :id => 2..3 }).find(1) }
+ assert_equal [1,2], Topic.where(id: 1..2).to_a.map(&:id).sort
+ assert_raise(ActiveRecord::RecordNotFound) { Topic.where(id: 2..3).find(1) }
end
def test_find_on_hash_conditions_with_end_exclusive_range
- assert_equal [1,2,3], Topic.all.merge!(:where => { :id => 1..3 }).to_a.map(&:id).sort
- assert_equal [1,2], Topic.all.merge!(:where => { :id => 1...3 }).to_a.map(&:id).sort
- assert_raise(ActiveRecord::RecordNotFound) { Topic.all.merge!(:where => { :id => 2...3 }).find(3) }
+ assert_equal [1,2,3], Topic.where(id: 1..3).to_a.map(&:id).sort
+ assert_equal [1,2], Topic.where(id: 1...3).to_a.map(&:id).sort
+ assert_raise(ActiveRecord::RecordNotFound) { Topic.where(id: 2...3).find(3) }
end
def test_find_on_hash_conditions_with_multiple_ranges
- assert_equal [1,2,3], Comment.all.merge!(:where => { :id => 1..3, :post_id => 1..2 }).to_a.map(&:id).sort
- assert_equal [1], Comment.all.merge!(:where => { :id => 1..1, :post_id => 1..10 }).to_a.map(&:id).sort
+ assert_equal [1,2,3], Comment.where(id: 1..3, post_id: 1..2).to_a.map(&:id).sort
+ assert_equal [1], Comment.where(id: 1..1, post_id: 1..10).to_a.map(&:id).sort
end
def test_find_on_hash_conditions_with_array_of_integers_and_ranges
- assert_equal [1,2,3,5,6,7,8,9], Comment.all.merge!(:where => {:id => [1..2, 3, 5, 6..8, 9]}).to_a.map(&:id).sort
+ assert_equal [1,2,3,5,6,7,8,9], Comment.where(id: [1..2, 3, 5, 6..8, 9]).to_a.map(&:id).sort
end
def test_find_on_multiple_hash_conditions
- assert Topic.all.merge!(:where => { :author_name => "David", :title => "The First Topic", :replies_count => 1, :approved => false }).find(1)
- assert_raise(ActiveRecord::RecordNotFound) { Topic.all.merge!(:where => { :author_name => "David", :title => "The First Topic", :replies_count => 1, :approved => true }).find(1) }
- assert_raise(ActiveRecord::RecordNotFound) { Topic.all.merge!(:where => { :author_name => "David", :title => "HHC", :replies_count => 1, :approved => false }).find(1) }
- assert_raise(ActiveRecord::RecordNotFound) { Topic.all.merge!(:where => { :author_name => "David", :title => "The First Topic", :replies_count => 1, :approved => true }).find(1) }
+ assert Topic.where(author_name: "David", title: "The First Topic", replies_count: 1, approved: false).find(1)
+ assert_raise(ActiveRecord::RecordNotFound) { Topic.where(author_name: "David", title: "The First Topic", replies_count: 1, approved: true).find(1) }
+ assert_raise(ActiveRecord::RecordNotFound) { Topic.where(author_name: "David", title: "HHC", replies_count: 1, approved: false).find(1) }
+ assert_raise(ActiveRecord::RecordNotFound) { Topic.where(author_name: "David", title: "The First Topic", replies_count: 1, approved: true).find(1) }
end
def test_condition_interpolation
assert_kind_of Firm, Company.where("name = '%s'", "37signals").first
- assert_nil Company.all.merge!(:where => ["name = '%s'", "37signals!"]).first
- assert_nil Company.all.merge!(:where => ["name = '%s'", "37signals!' OR 1=1"]).first
- assert_kind_of Time, Topic.all.merge!(:where => ["id = %d", 1]).first.written_on
+ assert_nil Company.where(["name = '%s'", "37signals!"]).first
+ assert_nil Company.where(["name = '%s'", "37signals!' OR 1=1"]).first
+ assert_kind_of Time, Topic.where(["id = %d", 1]).first.written_on
end
def test_condition_array_interpolation
- assert_kind_of Firm, Company.all.merge!(:where => ["name = '%s'", "37signals"]).first
- assert_nil Company.all.merge!(:where => ["name = '%s'", "37signals!"]).first
- assert_nil Company.all.merge!(:where => ["name = '%s'", "37signals!' OR 1=1"]).first
- assert_kind_of Time, Topic.all.merge!(:where => ["id = %d", 1]).first.written_on
+ assert_kind_of Firm, Company.where(["name = '%s'", "37signals"]).first
+ assert_nil Company.where(["name = '%s'", "37signals!"]).first
+ assert_nil Company.where(["name = '%s'", "37signals!' OR 1=1"]).first
+ assert_kind_of Time, Topic.where(["id = %d", 1]).first.written_on
end
def test_condition_hash_interpolation
- assert_kind_of Firm, Company.all.merge!(:where => { :name => "37signals"}).first
- assert_nil Company.all.merge!(:where => { :name => "37signals!"}).first
- assert_kind_of Time, Topic.all.merge!(:where => {:id => 1}).first.written_on
+ assert_kind_of Firm, Company.where(name: "37signals").first
+ assert_nil Company.where(name: "37signals!").first
+ assert_kind_of Time, Topic.where(id: 1).first.written_on
end
def test_hash_condition_find_malformed
assert_raise(ActiveRecord::StatementInvalid) {
- Company.all.merge!(:where => { :id => 2, :dhh => true }).first
+ Company.where(id: 2, dhh: true).first
}
end
def test_hash_condition_find_with_escaped_characters
Company.create("name" => "Ain't noth'n like' \#stuff")
- assert Company.all.merge!(:where => { :name => "Ain't noth'n like' \#stuff" }).first
+ assert Company.where(name: "Ain't noth'n like' \#stuff").first
end
def test_hash_condition_find_with_array
- p1, p2 = Post.all.merge!(:limit => 2, :order => 'id asc').to_a
- assert_equal [p1, p2], Post.all.merge!(:where => { :id => [p1, p2] }, :order => 'id asc').to_a
- assert_equal [p1, p2], Post.all.merge!(:where => { :id => [p1, p2.id] }, :order => 'id asc').to_a
+ p1, p2 = Post.limit(2).order('id asc').to_a
+ assert_equal [p1, p2], Post.where(id: [p1, p2]).order('id asc').to_a
+ assert_equal [p1, p2], Post.where(id: [p1, p2.id]).order('id asc').to_a
end
def test_hash_condition_find_with_nil
- topic = Topic.all.merge!(:where => { :last_read => nil } ).first
+ topic = Topic.where(last_read: nil).first
assert_not_nil topic
assert_nil topic.last_read
end
@@ -475,61 +480,61 @@ class FinderTest < ActiveRecord::TestCase
def test_condition_utc_time_interpolation_with_default_timezone_local
with_env_tz 'America/New_York' do
- with_active_record_default_timezone :local do
+ with_timezone_config default: :local do
topic = Topic.first
- assert_equal topic, Topic.all.merge!(:where => ['written_on = ?', topic.written_on.getutc]).first
+ assert_equal topic, Topic.where(['written_on = ?', topic.written_on.getutc]).first
end
end
end
def test_hash_condition_utc_time_interpolation_with_default_timezone_local
with_env_tz 'America/New_York' do
- with_active_record_default_timezone :local do
+ with_timezone_config default: :local do
topic = Topic.first
- assert_equal topic, Topic.all.merge!(:where => {:written_on => topic.written_on.getutc}).first
+ assert_equal topic, Topic.where(written_on: topic.written_on.getutc).first
end
end
end
def test_condition_local_time_interpolation_with_default_timezone_utc
with_env_tz 'America/New_York' do
- with_active_record_default_timezone :utc do
+ with_timezone_config default: :utc do
topic = Topic.first
- assert_equal topic, Topic.all.merge!(:where => ['written_on = ?', topic.written_on.getlocal]).first
+ assert_equal topic, Topic.where(['written_on = ?', topic.written_on.getlocal]).first
end
end
end
def test_hash_condition_local_time_interpolation_with_default_timezone_utc
with_env_tz 'America/New_York' do
- with_active_record_default_timezone :utc do
+ with_timezone_config default: :utc do
topic = Topic.first
- assert_equal topic, Topic.all.merge!(:where => {:written_on => topic.written_on.getlocal}).first
+ assert_equal topic, Topic.where(written_on: topic.written_on.getlocal).first
end
end
end
def test_bind_variables
- assert_kind_of Firm, Company.all.merge!(:where => ["name = ?", "37signals"]).first
- assert_nil Company.all.merge!(:where => ["name = ?", "37signals!"]).first
- assert_nil Company.all.merge!(:where => ["name = ?", "37signals!' OR 1=1"]).first
- assert_kind_of Time, Topic.all.merge!(:where => ["id = ?", 1]).first.written_on
+ assert_kind_of Firm, Company.where(["name = ?", "37signals"]).first
+ assert_nil Company.where(["name = ?", "37signals!"]).first
+ assert_nil Company.where(["name = ?", "37signals!' OR 1=1"]).first
+ assert_kind_of Time, Topic.where(["id = ?", 1]).first.written_on
assert_raise(ActiveRecord::PreparedStatementInvalid) {
- Company.all.merge!(:where => ["id=? AND name = ?", 2]).first
+ Company.where(["id=? AND name = ?", 2]).first
}
assert_raise(ActiveRecord::PreparedStatementInvalid) {
- Company.all.merge!(:where => ["id=?", 2, 3, 4]).first
+ Company.where(["id=?", 2, 3, 4]).first
}
end
def test_bind_variables_with_quotes
Company.create("name" => "37signals' go'es agains")
- assert Company.all.merge!(:where => ["name = ?", "37signals' go'es agains"]).first
+ assert Company.where(["name = ?", "37signals' go'es agains"]).first
end
def test_named_bind_variables_with_quotes
Company.create("name" => "37signals' go'es agains")
- assert Company.all.merge!(:where => ["name = :name", {:name => "37signals' go'es agains"}]).first
+ assert Company.where(["name = :name", {name: "37signals' go'es agains"}]).first
end
def test_bind_arity
@@ -547,10 +552,10 @@ class FinderTest < ActiveRecord::TestCase
assert_nothing_raised { bind("'+00:00'", :foo => "bar") }
- assert_kind_of Firm, Company.all.merge!(:where => ["name = :name", { :name => "37signals" }]).first
- assert_nil Company.all.merge!(:where => ["name = :name", { :name => "37signals!" }]).first
- assert_nil Company.all.merge!(:where => ["name = :name", { :name => "37signals!' OR 1=1" }]).first
- assert_kind_of Time, Topic.all.merge!(:where => ["id = :id", { :id => 1 }]).first.written_on
+ assert_kind_of Firm, Company.where(["name = :name", { name: "37signals" }]).first
+ assert_nil Company.where(["name = :name", { name: "37signals!" }]).first
+ assert_nil Company.where(["name = :name", { name: "37signals!' OR 1=1" }]).first
+ assert_kind_of Time, Topic.where(["id = :id", { id: 1 }]).first.written_on
end
class SimpleEnumerable
@@ -613,7 +618,7 @@ class FinderTest < ActiveRecord::TestCase
def test_named_bind_with_postgresql_type_casts
l = Proc.new { bind(":a::integer '2009-01-01'::date", :a => '10') }
assert_nothing_raised(&l)
- assert_equal "#{ActiveRecord::Base.quote_value('10')}::integer '2009-01-01'::date", l.call
+ assert_equal "#{ActiveRecord::Base.connection.quote('10')}::integer '2009-01-01'::date", l.call
end
def test_string_sanitation
@@ -637,6 +642,13 @@ class FinderTest < ActiveRecord::TestCase
assert_raise(ActiveRecord::RecordNotFound) { Topic.find_by_title!("The First Topic!") }
end
+ def test_find_by_on_attribute_that_is_a_reserved_word
+ dog_alias = 'Dog'
+ dog = Dog.create(alias: dog_alias)
+
+ assert_equal dog, Dog.find_by_alias(dog_alias)
+ end
+
def test_find_by_one_attribute_that_is_an_alias
assert_equal topics(:first), Topic.find_by_heading("The First Topic")
assert_nil Topic.find_by_heading("The First Topic!")
@@ -716,6 +728,15 @@ class FinderTest < ActiveRecord::TestCase
assert_raise(ArgumentError) { Topic.find_by_title_and_author_name("The First Topic") }
end
+ def test_find_last_with_offset
+ devs = Developer.order('id')
+
+ assert_equal devs[2], Developer.offset(2).first
+ assert_equal devs[-3], Developer.offset(2).last
+ assert_equal devs[-3], Developer.offset(2).last
+ assert_equal devs[-3], Developer.offset(2).order('id DESC').first
+ end
+
def test_find_by_nil_attribute
topic = Topic.find_by_last_read nil
assert_not_nil topic
@@ -732,10 +753,9 @@ class FinderTest < ActiveRecord::TestCase
end
def test_find_all_with_join
- developers_on_project_one = Developer.all.merge!(
- :joins => 'LEFT JOIN developers_projects ON developers.id = developers_projects.developer_id',
- :where => 'project_id=1'
- ).to_a
+ developers_on_project_one = Developer.
+ joins('LEFT JOIN developers_projects ON developers.id = developers_projects.developer_id').
+ where('project_id=1').to_a
assert_equal 3, developers_on_project_one.length
developer_names = developers_on_project_one.map { |d| d.name }
assert developer_names.include?('David')
@@ -743,20 +763,17 @@ class FinderTest < ActiveRecord::TestCase
end
def test_joins_dont_clobber_id
- first = Firm.all.merge!(
- :joins => 'INNER JOIN companies clients ON clients.firm_id = companies.id',
- :where => 'companies.id = 1'
- ).first
+ first = Firm.
+ joins('INNER JOIN companies clients ON clients.firm_id = companies.id').
+ where('companies.id = 1').first
assert_equal 1, first.id
end
def test_joins_with_string_array
- person_with_reader_and_post = Post.all.merge!(
- :joins => [
- "INNER JOIN categorizations ON categorizations.post_id = posts.id",
- "INNER JOIN categories ON categories.id = categorizations.category_id AND categories.type = 'SpecialCategory'"
- ]
- )
+ person_with_reader_and_post = Post.
+ joins(["INNER JOIN categorizations ON categorizations.post_id = posts.id",
+ "INNER JOIN categories ON categories.id = categorizations.category_id AND categories.type = 'SpecialCategory'"
+ ])
assert_equal 1, person_with_reader_and_post.size
end
@@ -781,9 +798,9 @@ class FinderTest < ActiveRecord::TestCase
end
def test_find_by_records
- p1, p2 = Post.all.merge!(:limit => 2, :order => 'id asc').to_a
- assert_equal [p1, p2], Post.all.merge!(:where => ['id in (?)', [p1, p2]], :order => 'id asc')
- assert_equal [p1, p2], Post.all.merge!(:where => ['id in (?)', [p1, p2.id]], :order => 'id asc')
+ p1, p2 = Post.limit(2).order('id asc').to_a
+ assert_equal [p1, p2], Post.where(['id in (?)', [p1, p2]]).order('id asc')
+ assert_equal [p1, p2], Post.where(['id in (?)', [p1, p2.id]]).order('id asc')
end
def test_select_value
@@ -810,36 +827,35 @@ class FinderTest < ActiveRecord::TestCase
end
def test_find_with_order_on_included_associations_with_construct_finder_sql_for_association_limiting_and_is_distinct
- assert_equal 2, Post.all.merge!(:includes => { :authors => :author_address }, :order => 'author_addresses.id DESC ', :limit => 2).to_a.size
+ assert_equal 2, Post.includes(authors: :author_address).order('author_addresses.id DESC ').limit(2).to_a.size
- assert_equal 3, Post.all.merge!(:includes => { :author => :author_address, :authors => :author_address},
- :order => 'author_addresses_authors.id DESC ', :limit => 3).to_a.size
+ assert_equal 3, Post.includes(author: :author_address, authors: :author_address).
+ order('author_addresses_authors.id DESC ').limit(3).to_a.size
end
def test_find_with_nil_inside_set_passed_for_one_attribute
- client_of = Company.all.merge!(
- :where => {
- :client_of => [2, 1, nil],
- :name => ['37signals', 'Summit', 'Microsoft'] },
- :order => 'client_of DESC'
- ).map { |x| x.client_of }
+ client_of = Company.
+ where(client_of: [2, 1, nil],
+ name: ['37signals', 'Summit', 'Microsoft']).
+ order('client_of DESC').
+ map { |x| x.client_of }
assert client_of.include?(nil)
assert_equal [2, 1].sort, client_of.compact.sort
end
def test_find_with_nil_inside_set_passed_for_attribute
- client_of = Company.all.merge!(
- :where => { :client_of => [nil] },
- :order => 'client_of DESC'
- ).map { |x| x.client_of }
+ client_of = Company.
+ where(client_of: [nil]).
+ order('client_of DESC').
+ map { |x| x.client_of }
assert_equal [], client_of.compact
end
def test_with_limiting_with_custom_select
posts = Post.references(:authors).merge(
- :includes => :author, :select => ' posts.*, authors.id as "author_id"',
+ :includes => :author, :select => 'posts.*, authors.id as "author_id"',
:limit => 3, :order => 'posts.id'
).to_a
assert_equal 3, posts.size
@@ -857,8 +873,14 @@ class FinderTest < ActiveRecord::TestCase
Toy.reset_primary_key
end
+ def test_find_without_primary_key
+ assert_raises(ActiveRecord::UnknownPrimaryKey) do
+ Matey.find(1)
+ end
+ end
+
def test_finder_with_offset_string
- assert_nothing_raised(ActiveRecord::StatementInvalid) { Topic.all.merge!(:offset => "3").to_a }
+ assert_nothing_raised(ActiveRecord::StatementInvalid) { Topic.offset("3").to_a }
end
protected
@@ -876,11 +898,4 @@ class FinderTest < ActiveRecord::TestCase
ensure
old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ')
end
-
- def with_active_record_default_timezone(zone)
- old_zone, ActiveRecord::Base.default_timezone = ActiveRecord::Base.default_timezone, zone
- yield
- ensure
- ActiveRecord::Base.default_timezone = old_zone
- end
end
diff --git a/activerecord/test/cases/fixture_set/file_test.rb b/activerecord/test/cases/fixture_set/file_test.rb
index a029fedbd3..92efa8aca7 100644
--- a/activerecord/test/cases/fixture_set/file_test.rb
+++ b/activerecord/test/cases/fixture_set/file_test.rb
@@ -68,6 +68,61 @@ module ActiveRecord
end
end
+ def test_render_context_helper
+ ActiveRecord::FixtureSet.context_class.class_eval do
+ def fixture_helper
+ "Fixture helper"
+ end
+ end
+ yaml = "one:\n name: <%= fixture_helper %>\n"
+ tmp_yaml ['curious', 'yml'], yaml do |t|
+ golden =
+ [["one", {"name" => "Fixture helper"}]]
+ assert_equal golden, File.open(t.path) { |fh| fh.to_a }
+ end
+ ActiveRecord::FixtureSet.context_class.class_eval do
+ remove_method :fixture_helper
+ end
+ end
+
+ def test_render_context_lookup_scope
+ yaml = <<END
+one:
+ ActiveRecord: <%= defined? ActiveRecord %>
+ ActiveRecord_FixtureSet: <%= defined? ActiveRecord::FixtureSet %>
+ FixtureSet: <%= defined? FixtureSet %>
+ ActiveRecord_FixtureSet_File: <%= defined? ActiveRecord::FixtureSet::File %>
+ File: <%= File.name %>
+END
+
+ golden = [['one', {
+ 'ActiveRecord' => 'constant',
+ 'ActiveRecord_FixtureSet' => 'constant',
+ 'FixtureSet' => nil,
+ 'ActiveRecord_FixtureSet_File' => 'constant',
+ 'File' => 'File'
+ }]]
+
+ tmp_yaml ['curious', 'yml'], yaml do |t|
+ assert_equal golden, File.open(t.path) { |fh| fh.to_a }
+ end
+ end
+
+ # Make sure that each fixture gets its own rendering context so that
+ # fixtures are independent.
+ def test_independent_render_contexts
+ yaml1 = "<% def leaked_method; 'leak'; end %>\n"
+ yaml2 = "one:\n name: <%= leaked_method %>\n"
+ tmp_yaml ['leaky', 'yml'], yaml1 do |t1|
+ tmp_yaml ['curious', 'yml'], yaml2 do |t2|
+ File.open(t1.path) { |fh| fh.to_a }
+ assert_raises(NameError) do
+ File.open(t2.path) { |fh| fh.to_a }
+ end
+ end
+ 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 f6cfee0cb8..f3a4887a85 100644
--- a/activerecord/test/cases/fixtures_test.rb
+++ b/activerecord/test/cases/fixtures_test.rb
@@ -84,6 +84,12 @@ class FixturesTest < ActiveRecord::TestCase
assert fixtures.detect { |f| f.name == 'collections' }, "no fixtures named 'collections' in #{fixtures.map(&:name).inspect}"
end
+ def test_create_symbol_fixtures_is_deprecated
+ assert_deprecated do
+ ActiveRecord::FixtureSet.create_fixtures(FIXTURES_ROOT, :collections, :collections => 'Course') { Course.connection }
+ end
+ end
+
def test_attributes
topics = create_fixtures("topics").first
assert_equal("The First Topic", topics["first"]["title"])
@@ -190,11 +196,11 @@ class FixturesTest < ActiveRecord::TestCase
end
def test_empty_yaml_fixture
- assert_not_nil ActiveRecord::FixtureSet.new( Account.connection, "accounts", 'Account', FIXTURES_ROOT + "/naked/yml/accounts")
+ assert_not_nil ActiveRecord::FixtureSet.new( Account.connection, "accounts", Account, FIXTURES_ROOT + "/naked/yml/accounts")
end
def test_empty_yaml_fixture_with_a_comment_in_it
- assert_not_nil ActiveRecord::FixtureSet.new( Account.connection, "companies", 'Company', FIXTURES_ROOT + "/naked/yml/companies")
+ assert_not_nil ActiveRecord::FixtureSet.new( Account.connection, "companies", Company, FIXTURES_ROOT + "/naked/yml/companies")
end
def test_nonexistent_fixture_file
@@ -204,19 +210,19 @@ class FixturesTest < ActiveRecord::TestCase
assert Dir[nonexistent_fixture_path+"*"].empty?
assert_raise(Errno::ENOENT) do
- ActiveRecord::FixtureSet.new( Account.connection, "companies", 'Company', nonexistent_fixture_path)
+ ActiveRecord::FixtureSet.new( Account.connection, "companies", Company, nonexistent_fixture_path)
end
end
def test_dirty_dirty_yaml_file
assert_raise(ActiveRecord::Fixture::FormatError) do
- ActiveRecord::FixtureSet.new( Account.connection, "courses", 'Course', FIXTURES_ROOT + "/naked/yml/courses")
+ ActiveRecord::FixtureSet.new( Account.connection, "courses", Course, FIXTURES_ROOT + "/naked/yml/courses")
end
end
def test_omap_fixtures
assert_nothing_raised do
- fixtures = ActiveRecord::FixtureSet.new(Account.connection, 'categories', 'Category', FIXTURES_ROOT + "/categories_ordered")
+ fixtures = ActiveRecord::FixtureSet.new(Account.connection, 'categories', Category, FIXTURES_ROOT + "/categories_ordered")
fixtures.each.with_index do |(name, fixture), i|
assert_equal "fixture_no_#{i}", name
@@ -245,6 +251,60 @@ class FixturesTest < ActiveRecord::TestCase
def test_serialized_fixtures
assert_equal ["Green", "Red", "Orange"], traffic_lights(:uk).state
end
+
+ 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
+
+ def test_fixtures
+ assert accounts(:signals37)
+ end
+ end
+
+ result = test_case.new(:test_fixtures).run
+
+ assert result.passed?, "Expected #{result.name} to pass:\n#{result}"
+ ensure
+ ENV['DATABASE_URL'] = db_url_tmp
+ end
+end
+
+class HasManyThroughFixture < ActiveSupport::TestCase
+ def make_model(name)
+ Class.new(ActiveRecord::Base) { define_singleton_method(:name) { name } }
+ end
+
+ def test_has_many_through
+ pt = make_model "ParrotTreasure"
+ parrot = make_model "Parrot"
+ treasure = make_model "Treasure"
+
+ pt.table_name = "parrots_treasures"
+ pt.belongs_to :parrot, :class => parrot
+ pt.belongs_to :treasure, :class => treasure
+
+ parrot.has_many :parrot_treasures, :class => pt
+ parrot.has_many :treasures, :through => :parrot_treasures
+
+ parrots = File.join FIXTURES_ROOT, 'parrots'
+
+ fs = ActiveRecord::FixtureSet.new parrot.connection, "parrots", parrot, parrots
+ rows = fs.table_rows
+ assert_equal load_has_and_belongs_to_many['parrots_treasures'], rows['parrots_treasures']
+ end
+
+ def load_has_and_belongs_to_many
+ parrot = make_model "Parrot"
+ parrot.has_and_belongs_to_many :treasures
+
+ parrots = File.join FIXTURES_ROOT, 'parrots'
+
+ fs = ActiveRecord::FixtureSet.new parrot.connection, "parrots", parrot, parrots
+ fs.table_rows
+ end
end
if Account.connection.respond_to?(:reset_pk_sequence!)
@@ -433,7 +493,7 @@ class OverRideFixtureMethodTest < ActiveRecord::TestCase
end
class CheckSetTableNameFixturesTest < ActiveRecord::TestCase
- set_fixture_class :funny_jokes => 'Joke'
+ set_fixture_class :funny_jokes => Joke
fixtures :funny_jokes
# Set to false to blow away fixtures cache and ensure our fixtures are loaded
# and thus takes into account our set_fixture_class
@@ -477,10 +537,6 @@ class CustomConnectionFixturesTest < ActiveRecord::TestCase
fixtures :courses
self.use_transactional_fixtures = false
- def test_connection_instance_method_deprecation
- assert_deprecated { courses(:ruby).connection }
- end
-
def test_leaky_destroy
assert_nothing_raised { courses(:ruby) }
courses(:ruby).destroy
@@ -520,7 +576,7 @@ class InvalidTableNameFixturesTest < ActiveRecord::TestCase
end
class CheckEscapedYamlFixturesTest < ActiveRecord::TestCase
- set_fixture_class :funny_jokes => 'Joke'
+ set_fixture_class :funny_jokes => Joke
fixtures :funny_jokes
# Set to false to blow away fixtures cache and ensure our fixtures are loaded
# and thus takes into account our set_fixture_class
@@ -562,26 +618,30 @@ class FixturesBrokenRollbackTest < ActiveRecord::TestCase
end
private
- def load_fixtures
+ def load_fixtures(config)
raise 'argh'
end
end
class LoadAllFixturesTest < ActiveRecord::TestCase
- self.fixture_path = FIXTURES_ROOT + "/all"
- fixtures :all
-
def test_all_there
- assert_equal %w(developers people tasks), fixture_table_names.sort
+ self.class.fixture_path = FIXTURES_ROOT + "/all"
+ self.class.fixtures :all
+
+ assert_equal %w(admin/accounts admin/users developers people tasks), fixture_table_names.sort
+ ensure
+ ActiveRecord::FixtureSet.reset_cache
end
end
class LoadAllFixturesWithPathnameTest < ActiveRecord::TestCase
- self.fixture_path = Pathname.new(FIXTURES_ROOT).join('all')
- fixtures :all
-
def test_all_there
- assert_equal %w(developers people tasks), fixture_table_names.sort
+ self.class.fixture_path = Pathname.new(FIXTURES_ROOT).join('all')
+ self.class.fixtures :all
+
+ assert_equal %w(admin/accounts admin/users developers people tasks), fixture_table_names.sort
+ ensure
+ ActiveRecord::FixtureSet.reset_cache
end
end
diff --git a/activerecord/test/cases/forbidden_attributes_protection_test.rb b/activerecord/test/cases/forbidden_attributes_protection_test.rb
index 490b599fb6..981a75faf6 100644
--- a/activerecord/test/cases/forbidden_attributes_protection_test.rb
+++ b/activerecord/test/cases/forbidden_attributes_protection_test.rb
@@ -61,4 +61,9 @@ class ForbiddenAttributesProtectionTest < ActiveRecord::TestCase
assert_equal 'Guille', person.first_name
assert_equal 'm', person.gender
end
+
+ def test_blank_attributes_should_not_raise
+ person = Person.new
+ assert_nil person.assign_attributes(ProtectedParams.new({}))
+ end
end
diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb
index 7dbb6616f8..3758224b0c 100644
--- a/activerecord/test/cases/helper.rb
+++ b/activerecord/test/cases/helper.rb
@@ -20,6 +20,9 @@ Thread.abort_on_exception = true
# Show backtraces for deprecated behavior for quicker cleanup.
ActiveSupport::Deprecation.debug = true
+# Disable available locale checks to avoid warnings running the test suite.
+I18n.enforce_available_locales = false
+
# Connect to the database
ARTest.connect
@@ -49,11 +52,58 @@ ensure
old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ')
end
-def with_active_record_default_timezone(zone)
- old_zone, ActiveRecord::Base.default_timezone = ActiveRecord::Base.default_timezone, zone
+def with_timezone_config(cfg)
+ verify_default_timezone_config
+
+ old_default_zone = ActiveRecord::Base.default_timezone
+ old_awareness = ActiveRecord::Base.time_zone_aware_attributes
+ old_zone = Time.zone
+
+ if cfg.has_key?(:default)
+ ActiveRecord::Base.default_timezone = cfg[:default]
+ end
+ if cfg.has_key?(:aware_attributes)
+ ActiveRecord::Base.time_zone_aware_attributes = cfg[:aware_attributes]
+ end
+ if cfg.has_key?(:zone)
+ Time.zone = cfg[:zone]
+ end
yield
ensure
- ActiveRecord::Base.default_timezone = old_zone
+ ActiveRecord::Base.default_timezone = old_default_zone
+ ActiveRecord::Base.time_zone_aware_attributes = old_awareness
+ Time.zone = old_zone
+end
+
+# This method makes sure that tests don't leak global state related to time zones.
+EXPECTED_ZONE = nil
+EXPECTED_DEFAULT_TIMEZONE = :utc
+EXPECTED_TIME_ZONE_AWARE_ATTRIBUTES = false
+def verify_default_timezone_config
+ if Time.zone != EXPECTED_ZONE
+ $stderr.puts <<-MSG
+\n#{self.to_s}
+ Global state `Time.zone` was leaked.
+ Expected: #{EXPECTED_ZONE}
+ Got: #{Time.zone}
+ MSG
+ end
+ if ActiveRecord::Base.default_timezone != EXPECTED_DEFAULT_TIMEZONE
+ $stderr.puts <<-MSG
+\n#{self.to_s}
+ Global state `ActiveRecord::Base.default_timezone` was leaked.
+ Expected: #{EXPECTED_DEFAULT_TIMEZONE}
+ Got: #{ActiveRecord::Base.default_timezone}
+ MSG
+ end
+ if ActiveRecord::Base.time_zone_aware_attributes != EXPECTED_TIME_ZONE_AWARE_ATTRIBUTES
+ $stderr.puts <<-MSG
+\n#{self.to_s}
+ Global state `ActiveRecord::Base.time_zone_aware_attributes` was leaked.
+ Expected: #{EXPECTED_TIME_ZONE_AWARE_ATTRIBUTES}
+ Got: #{ActiveRecord::Base.time_zone_aware_attributes}
+ MSG
+ end
end
unless ENV['FIXTURE_DEBUG']
@@ -93,7 +143,7 @@ def load_schema
load SCHEMA_ROOT + "/schema.rb"
- if File.exists?(adapter_specific_schema_file)
+ if File.exist?(adapter_specific_schema_file)
load adapter_specific_schema_file
end
ensure
@@ -102,36 +152,21 @@ end
load_schema
-class << Time
- unless method_defined? :now_before_time_travel
- alias_method :now_before_time_travel, :now
- end
+class SQLSubscriber
+ attr_reader :logged
+ attr_reader :payloads
- def now
- (@now ||= nil) || now_before_time_travel
+ def initialize
+ @logged = []
+ @payloads = []
end
- def travel_to(time, &block)
- @now = time
- block.call
- ensure
- @now = nil
+ def start(name, id, payload)
+ @payloads << payload
+ @logged << [payload[:sql], payload[:name], payload[:binds]]
end
-end
-module LogIntercepter
- attr_accessor :logged, :intercepted
- def self.extended(base)
- base.logged = []
- end
- def log(sql, name, binds = [], &block)
- if @intercepted
- @logged << [sql, name, binds]
- yield
- else
- super(sql, name,binds, &block)
- end
- end
+ def finish(name, id, payload); end
end
module InTimeZone
diff --git a/activerecord/test/cases/inheritance_test.rb b/activerecord/test/cases/inheritance_test.rb
index a9be132801..cb0d374ef7 100644
--- a/activerecord/test/cases/inheritance_test.rb
+++ b/activerecord/test/cases/inheritance_test.rb
@@ -176,14 +176,14 @@ class InheritanceTest < ActiveRecord::TestCase
e = assert_raises(NotImplementedError) do
AbstractCompany.new
end
- assert_equal("AbstractCompany is an abstract class and can not be instantiated.", e.message)
+ assert_equal("AbstractCompany is an abstract class and cannot be instantiated.", e.message)
end
def test_new_with_ar_base
e = assert_raises(NotImplementedError) do
ActiveRecord::Base.new
end
- assert_equal("ActiveRecord::Base is an abstract class and can not be instantiated.", e.message)
+ assert_equal("ActiveRecord::Base is an abstract class and cannot be instantiated.", e.message)
end
def test_new_with_invalid_type
@@ -313,8 +313,12 @@ class InheritanceTest < ActiveRecord::TestCase
assert_kind_of SpecialSubscriber, SpecialSubscriber.find("webster132")
assert_nothing_raised { s = SpecialSubscriber.new("name" => "And breaaaaathe!"); s.id = 'roger'; s.save }
end
-end
+ def test_scope_inherited_properly
+ assert_nothing_raised { Company.of_first_firm }
+ assert_nothing_raised { Client.of_first_firm }
+ end
+end
class InheritanceComputeTypeTest < ActiveRecord::TestCase
fixtures :companies
diff --git a/activerecord/test/cases/integration_test.rb b/activerecord/test/cases/integration_test.rb
index b0a7cda2f3..2e71b1a40d 100644
--- a/activerecord/test/cases/integration_test.rb
+++ b/activerecord/test/cases/integration_test.rb
@@ -1,11 +1,14 @@
+# encoding: utf-8
+
require 'cases/helper'
require 'models/company'
require 'models/developer'
require 'models/car'
require 'models/bulb'
+require 'models/owner'
class IntegrationTest < ActiveRecord::TestCase
- fixtures :companies, :developers
+ fixtures :companies, :developers, :owners
def test_to_param_should_return_string
assert_kind_of String, Client.first.to_param
@@ -22,16 +25,59 @@ class IntegrationTest < ActiveRecord::TestCase
assert_equal '1', client.to_param
end
- def test_cache_key_for_existing_record_is_not_timezone_dependent
- ActiveRecord::Base.time_zone_aware_attributes = true
+ def test_to_param_class_method
+ firm = Firm.find(4)
+ assert_equal '4-flamboyant-software', firm.to_param
+ end
- Time.zone = 'UTC'
- utc_key = Developer.first.cache_key
+ def test_to_param_class_method_truncates
+ firm = Firm.find(4)
+ firm.name = 'a ' * 100
+ assert_equal '4-a-a-a-a-a-a-a-a-a', firm.to_param
+ end
- Time.zone = 'EST'
- est_key = Developer.first.cache_key
+ def test_to_param_class_method_truncates_edge_case
+ firm = Firm.find(4)
+ firm.name = 'David HeinemeierHansson'
+ assert_equal '4-david', firm.to_param
+ end
- assert_equal utc_key, est_key
+ def test_to_param_class_method_squishes
+ firm = Firm.find(4)
+ firm.name = "ab \n" * 100
+ assert_equal '4-ab-ab-ab-ab-ab-ab', firm.to_param
+ end
+
+ def test_to_param_class_method_multibyte_character
+ firm = Firm.find(4)
+ firm.name = "戦場ヶ原 ひたぎ"
+ assert_equal '4', firm.to_param
+ end
+
+ def test_to_param_class_method_uses_default_if_blank
+ firm = Firm.find(4)
+ firm.name = nil
+ assert_equal '4', firm.to_param
+ firm.name = ' '
+ assert_equal '4', firm.to_param
+ end
+
+ def test_to_param_class_method_uses_default_if_not_persisted
+ firm = Firm.new(name: 'Fancy Shirts')
+ assert_equal nil, firm.to_param
+ end
+
+ def test_to_param_with_no_arguments
+ assert_equal 'Firm', Firm.to_param
+ end
+
+ def test_cache_key_for_existing_record_is_not_timezone_dependent
+ utc_key = Developer.first.cache_key
+
+ with_timezone_config zone: "EST" do
+ est_key = Developer.first.cache_key
+ assert_equal utc_key, est_key
+ end
end
def test_cache_key_format_for_existing_record_with_updated_at
@@ -84,4 +130,9 @@ class IntegrationTest < ActiveRecord::TestCase
dev.touch
assert_not_equal key, dev.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)
+ end
end
diff --git a/activerecord/test/cases/invalid_connection_test.rb b/activerecord/test/cases/invalid_connection_test.rb
new file mode 100644
index 0000000000..f6774d7ef4
--- /dev/null
+++ b/activerecord/test/cases/invalid_connection_test.rb
@@ -0,0 +1,22 @@
+require "cases/helper"
+
+class TestAdapterWithInvalidConnection < ActiveRecord::TestCase
+ self.use_transactional_fixtures = false
+
+ class Bird < ActiveRecord::Base
+ end
+
+ def setup
+ # Can't just use current adapter; sqlite3 will create a database
+ # file on the fly.
+ Bird.establish_connection adapter: 'mysql', database: 'i_do_not_exist'
+ end
+
+ def teardown
+ Bird.remove_connection
+ end
+
+ test "inspect on Model class does not raise" do
+ assert_equal "#{Bird.name} (call '#{Bird.name}.connection' to establish a connection)", Bird.inspect
+ end
+end
diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb
index 77891b9156..a16ed963fe 100644
--- a/activerecord/test/cases/locking_test.rb
+++ b/activerecord/test/cases/locking_test.rb
@@ -8,6 +8,7 @@ require 'models/legacy_thing'
require 'models/reference'
require 'models/string_key_object'
require 'models/car'
+require 'models/bulb'
require 'models/engine'
require 'models/wheel'
require 'models/treasure'
@@ -16,6 +17,7 @@ class LockWithoutDefault < ActiveRecord::Base; end
class LockWithCustomColumnWithoutDefault < ActiveRecord::Base
self.table_name = :lock_without_defaults_cust
+ self.column_defaults # to test @column_defaults caching.
self.locking_column = :custom_lock_version
end
@@ -26,6 +28,18 @@ end
class OptimisticLockingTest < ActiveRecord::TestCase
fixtures :people, :legacy_things, :references, :string_key_objects, :peoples_treasures
+ def test_quote_value_passed_lock_col
+ p1 = Person.find(1)
+ assert_equal 0, p1.lock_version
+
+ Person.expects(:quote_value).with(0, Person.columns_hash[Person.locking_column]).returns('0').once
+
+ p1.first_name = 'anika2'
+ p1.save!
+
+ assert_equal 1, p1.lock_version
+ end
+
def test_non_integer_lock_existing
s1 = StringKeyObject.find("record1")
s2 = StringKeyObject.find("record1")
@@ -242,7 +256,7 @@ class OptimisticLockingTest < ActiveRecord::TestCase
car = Car.create!
assert_difference 'car.wheels.count' do
- car.wheels << Wheel.create!
+ car.wheels << Wheel.create!
end
assert_difference 'car.wheels.count', -1 do
car.destroy
@@ -258,6 +272,10 @@ class OptimisticLockingTest < ActiveRecord::TestCase
assert p.treasures.empty?
assert RichPerson.connection.select_all("SELECT * FROM peoples_treasures WHERE rich_person_id = 1").empty?
end
+
+ def test_quoted_locking_column_is_deprecated
+ assert_deprecated { ActiveRecord::Base.quoted_locking_column }
+ end
end
class OptimisticLockingWithSchemaChangeTest < ActiveRecord::TestCase
diff --git a/activerecord/test/cases/log_subscriber_test.rb b/activerecord/test/cases/log_subscriber_test.rb
index 57eac0c175..97c0350911 100644
--- a/activerecord/test/cases/log_subscriber_test.rb
+++ b/activerecord/test/cases/log_subscriber_test.rb
@@ -56,6 +56,13 @@ class LogSubscriberTest < ActiveRecord::TestCase
assert_equal 2, logger.debugs.length
end
+ def test_sql_statements_are_not_squeezed
+ event = Struct.new(:duration, :payload)
+ logger = TestDebugLogSubscriber.new
+ logger.sql(event.new(0, sql: 'ruby rails'))
+ assert_match(/ruby rails/, logger.debugs.first)
+ end
+
def test_ignore_binds_payload_with_nil_column
event = Struct.new(:duration, :payload)
@@ -112,11 +119,11 @@ class LogSubscriberTest < ActiveRecord::TestCase
Thread.new { assert_equal 0, ActiveRecord::LogSubscriber.runtime }.join
end
- def test_binary_data_is_not_logged
- skip if current_adapter?(:Mysql2Adapter)
-
- Binary.create(data: 'some binary data')
- wait
- assert_match(/<16 bytes of binary data>/, @logger.logged(:debug).join)
+ unless current_adapter?(:Mysql2Adapter)
+ def test_binary_data_is_not_logged
+ Binary.create(data: 'some binary data')
+ wait
+ assert_match(/<16 bytes of binary data>/, @logger.logged(:debug).join)
+ end
end
end
diff --git a/activerecord/test/cases/migration/change_schema_test.rb b/activerecord/test/cases/migration/change_schema_test.rb
index 54fff8a0f5..294f2eb9fe 100644
--- a/activerecord/test/cases/migration/change_schema_test.rb
+++ b/activerecord/test/cases/migration/change_schema_test.rb
@@ -74,6 +74,29 @@ module ActiveRecord
assert_equal "hello", five.default unless mysql
end
+ if current_adapter?(:PostgreSQLAdapter)
+ def test_add_column_with_array
+ connection.create_table :testings
+ connection.add_column :testings, :foo, :string, :array => true
+
+ columns = connection.columns(:testings)
+ array_column = columns.detect { |c| c.name == "foo" }
+
+ assert array_column.array
+ end
+
+ def test_create_table_with_array_column
+ connection.create_table :testings do |t|
+ t.string :foo, :array => true
+ end
+
+ columns = connection.columns(:testings)
+ array_column = columns.detect { |c| c.name == "foo" }
+
+ assert array_column.array
+ end
+ end
+
def test_create_table_with_limits
connection.create_table :testings do |t|
t.column :foo, :string, :limit => 255
@@ -182,20 +205,18 @@ module ActiveRecord
connection.create_table table_name
end
- def test_add_column_not_null_without_default
- # Sybase, and SQLite3 will not allow you to add a NOT NULL
- # column to a table without a default value.
- if current_adapter?(:SybaseAdapter, :SQLite3Adapter)
- skip "not supported on #{connection.class}"
- end
-
- connection.create_table :testings do |t|
- t.column :foo, :string
- end
- connection.add_column :testings, :bar, :string, :null => false
+ # Sybase, and SQLite3 will not allow you to add a NOT NULL
+ # column to a table without a default value.
+ unless current_adapter?(:SybaseAdapter, :SQLite3Adapter)
+ def test_add_column_not_null_without_default
+ connection.create_table :testings do |t|
+ t.column :foo, :string
+ end
+ connection.add_column :testings, :bar, :string, :null => false
- assert_raise(ActiveRecord::StatementInvalid) do
- connection.execute "insert into testings (foo, bar) values ('hello', NULL)"
+ assert_raise(ActiveRecord::StatementInvalid) do
+ connection.execute "insert into testings (foo, bar) values ('hello', NULL)"
+ end
end
end
@@ -287,6 +308,22 @@ module ActiveRecord
assert_equal 2000, connection.select_values("SELECT money FROM testings").first.to_i
end
+ def test_change_column_null
+ testing_table_with_only_foo_attribute do
+ notnull_migration = Class.new(ActiveRecord::Migration) do
+ def change
+ change_column_null :testings, :foo, false
+ end
+ end
+ notnull_migration.new.suppress_messages do
+ notnull_migration.migrate(:up)
+ assert_equal false, connection.columns(:testings).find{ |c| c.name == "foo"}.null
+ notnull_migration.migrate(:down)
+ assert connection.columns(:testings).find{ |c| c.name == "foo"}.null
+ end
+ end
+ end
+
def test_column_exists
connection.create_table :testings do |t|
t.column :foo, :string
diff --git a/activerecord/test/cases/migration/change_table_test.rb b/activerecord/test/cases/migration/change_table_test.rb
index 8065541bfe..c1d7cd5874 100644
--- a/activerecord/test/cases/migration/change_table_test.rb
+++ b/activerecord/test/cases/migration/change_table_test.rb
@@ -5,7 +5,7 @@ module ActiveRecord
class Migration
class TableTest < ActiveRecord::TestCase
def setup
- @connection = MiniTest::Mock.new
+ @connection = Minitest::Mock.new
end
def teardown
diff --git a/activerecord/test/cases/migration/column_attributes_test.rb b/activerecord/test/cases/migration/column_attributes_test.rb
index ec2926632c..ccf19fb4d0 100644
--- a/activerecord/test/cases/migration/column_attributes_test.rb
+++ b/activerecord/test/cases/migration/column_attributes_test.rb
@@ -16,40 +16,31 @@ module ActiveRecord
end
def test_add_remove_single_field_using_string_arguments
- assert_not TestModel.column_methods_hash.key?(:last_name)
+ assert_no_column TestModel, :last_name
add_column 'test_models', 'last_name', :string
-
- TestModel.reset_column_information
-
- assert TestModel.column_methods_hash.key?(:last_name)
+ assert_column TestModel, :last_name
remove_column 'test_models', 'last_name'
-
- TestModel.reset_column_information
- assert_not TestModel.column_methods_hash.key?(:last_name)
+ assert_no_column TestModel, :last_name
end
def test_add_remove_single_field_using_symbol_arguments
- assert_not TestModel.column_methods_hash.key?(:last_name)
+ assert_no_column TestModel, :last_name
add_column :test_models, :last_name, :string
-
- TestModel.reset_column_information
- assert TestModel.column_methods_hash.key?(:last_name)
+ assert_column TestModel, :last_name
remove_column :test_models, :last_name
-
- TestModel.reset_column_information
- assert_not TestModel.column_methods_hash.key?(:last_name)
+ assert_no_column TestModel, :last_name
end
- def test_unabstracted_database_dependent_types
- skip "not supported" unless current_adapter?(:MysqlAdapter, :Mysql2Adapter)
-
- add_column :test_models, :intelligence_quotient, :tinyint
- TestModel.reset_column_information
- assert_match(/tinyint/, TestModel.columns_hash['intelligence_quotient'].sql_type)
+ if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
+ def test_unabstracted_database_dependent_types
+ add_column :test_models, :intelligence_quotient, :tinyint
+ TestModel.reset_column_information
+ assert_match(/tinyint/, TestModel.columns_hash['intelligence_quotient'].sql_type)
+ end
end
# We specifically do a manual INSERT here, and then test only the SELECT
@@ -104,22 +95,22 @@ module ActiveRecord
assert_equal 7, wealth_column.scale
end
- def test_change_column_preserve_other_column_precision_and_scale
- skip "only on sqlite3" unless current_adapter?(:SQLite3Adapter)
+ if current_adapter?(:SQLite3Adapter)
+ def test_change_column_preserve_other_column_precision_and_scale
+ connection.add_column 'test_models', 'last_name', :string
+ connection.add_column 'test_models', 'wealth', :decimal, :precision => 9, :scale => 7
- connection.add_column 'test_models', 'last_name', :string
- connection.add_column 'test_models', 'wealth', :decimal, :precision => 9, :scale => 7
+ wealth_column = TestModel.columns_hash['wealth']
+ assert_equal 9, wealth_column.precision
+ assert_equal 7, wealth_column.scale
- wealth_column = TestModel.columns_hash['wealth']
- assert_equal 9, wealth_column.precision
- assert_equal 7, wealth_column.scale
-
- connection.change_column 'test_models', 'last_name', :string, :null => false
- TestModel.reset_column_information
+ connection.change_column 'test_models', 'last_name', :string, :null => false
+ TestModel.reset_column_information
- wealth_column = TestModel.columns_hash['wealth']
- assert_equal 9, wealth_column.precision
- assert_equal 7, wealth_column.scale
+ wealth_column = TestModel.columns_hash['wealth']
+ assert_equal 9, wealth_column.precision
+ assert_equal 7, wealth_column.scale
+ end
end
def test_native_types
@@ -168,37 +159,17 @@ module ActiveRecord
assert_equal Date, bob.favorite_day.class
end
- # Oracle adapter stores Time or DateTime with timezone value already in _before_type_cast column
- # therefore no timezone change is done afterwards when default timezone is changed
- unless current_adapter?(:OracleAdapter)
- # Test DateTime column and defaults, including timezone.
- # FIXME: moment of truth may be Time on 64-bit platforms.
- if bob.moment_of_truth.is_a?(DateTime)
-
- with_env_tz 'US/Eastern' do
- bob.reload
- assert_equal DateTime.local_offset, bob.moment_of_truth.offset
- assert_not_equal 0, bob.moment_of_truth.offset
- assert_not_equal "Z", bob.moment_of_truth.zone
- # US/Eastern is -5 hours from GMT
- assert_equal Rational(-5, 24), bob.moment_of_truth.offset
- assert_match(/\A-05:00\Z/, bob.moment_of_truth.zone)
- assert_equal DateTime::ITALY, bob.moment_of_truth.start
- end
- end
- end
-
assert_instance_of TrueClass, bob.male?
assert_kind_of BigDecimal, bob.wealth
end
- def test_out_of_range_limit_should_raise
- skip("MySQL and PostgreSQL only") unless current_adapter?(:MysqlAdapter, :Mysql2Adapter, :PostgreSQLAdapter)
-
- assert_raise(ActiveRecordError) { add_column :test_models, :integer_too_big, :integer, :limit => 10 }
+ if current_adapter?(:MysqlAdapter, :Mysql2Adapter, :PostgreSQLAdapter)
+ def test_out_of_range_limit_should_raise
+ assert_raise(ActiveRecordError) { add_column :test_models, :integer_too_big, :integer, :limit => 10 }
- unless current_adapter?(:PostgreSQLAdapter)
- assert_raise(ActiveRecordError) { add_column :test_models, :text_too_big, :integer, :limit => 0xfffffffff }
+ unless current_adapter?(:PostgreSQLAdapter)
+ assert_raise(ActiveRecordError) { add_column :test_models, :text_too_big, :integer, :limit => 0xfffffffff }
+ end
end
end
end
diff --git a/activerecord/test/cases/migration/column_positioning_test.rb b/activerecord/test/cases/migration/column_positioning_test.rb
index 913d935f7c..87e29e41ba 100644
--- a/activerecord/test/cases/migration/column_positioning_test.rb
+++ b/activerecord/test/cases/migration/column_positioning_test.rb
@@ -9,10 +9,6 @@ module ActiveRecord
def setup
super
- unless current_adapter?(:MysqlAdapter, :Mysql2Adapter)
- skip "not supported on #{connection.class}"
- end
-
@connection = ActiveRecord::Base.connection
connection.create_table :testings, :id => false do |t|
@@ -28,33 +24,34 @@ module ActiveRecord
ActiveRecord::Base.primary_key_prefix_type = nil
end
- def test_column_positioning
- assert_equal %w(first second third), conn.columns(:testings).map {|c| c.name }
- end
+ if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
+ def test_column_positioning
+ assert_equal %w(first second third), conn.columns(:testings).map {|c| c.name }
+ end
- def test_add_column_with_positioning
- conn.add_column :testings, :new_col, :integer
- assert_equal %w(first second third new_col), conn.columns(:testings).map {|c| c.name }
- end
+ def test_add_column_with_positioning
+ conn.add_column :testings, :new_col, :integer
+ assert_equal %w(first second third new_col), conn.columns(:testings).map {|c| c.name }
+ end
- def test_add_column_with_positioning_first
- conn.add_column :testings, :new_col, :integer, :first => true
- assert_equal %w(new_col first second third), conn.columns(:testings).map {|c| c.name }
- end
+ def test_add_column_with_positioning_first
+ conn.add_column :testings, :new_col, :integer, :first => true
+ assert_equal %w(new_col first second third), conn.columns(:testings).map {|c| c.name }
+ end
- def test_add_column_with_positioning_after
- conn.add_column :testings, :new_col, :integer, :after => :first
- assert_equal %w(first new_col second third), conn.columns(:testings).map {|c| c.name }
- end
+ def test_add_column_with_positioning_after
+ conn.add_column :testings, :new_col, :integer, :after => :first
+ assert_equal %w(first new_col second third), conn.columns(:testings).map {|c| c.name }
+ end
- def test_change_column_with_positioning
- conn.change_column :testings, :second, :integer, :first => true
- assert_equal %w(second first third), conn.columns(:testings).map {|c| c.name }
+ def test_change_column_with_positioning
+ conn.change_column :testings, :second, :integer, :first => true
+ assert_equal %w(second first third), conn.columns(:testings).map {|c| c.name }
- conn.change_column :testings, :second, :integer, :after => :third
- assert_equal %w(first third second), conn.columns(:testings).map {|c| c.name }
+ conn.change_column :testings, :second, :integer, :after => :third
+ assert_equal %w(first third second), conn.columns(:testings).map {|c| c.name }
+ end
end
-
end
end
end
diff --git a/activerecord/test/cases/migration/command_recorder_test.rb b/activerecord/test/cases/migration/command_recorder_test.rb
index 2cad8a6d96..35b656ee43 100644
--- a/activerecord/test/cases/migration/command_recorder_test.rb
+++ b/activerecord/test/cases/migration/command_recorder_test.rb
@@ -4,7 +4,8 @@ module ActiveRecord
class Migration
class CommandRecorderTest < ActiveRecord::TestCase
def setup
- @recorder = CommandRecorder.new
+ connection = ActiveRecord::Base.connection
+ @recorder = CommandRecorder.new(connection)
end
def test_respond_to_delegates
@@ -242,6 +243,16 @@ module ActiveRecord
add = @recorder.inverse_of :remove_belongs_to, [:table, :user]
assert_equal [:add_reference, [:table, :user], nil], add
end
+
+ def test_invert_enable_extension
+ disable = @recorder.inverse_of :enable_extension, ['uuid-ossp']
+ assert_equal [:disable_extension, ['uuid-ossp'], nil], disable
+ end
+
+ def test_invert_disable_extension
+ enable = @recorder.inverse_of :disable_extension, ['uuid-ossp']
+ assert_equal [:enable_extension, ['uuid-ossp'], nil], enable
+ end
end
end
end
diff --git a/activerecord/test/cases/migration/index_test.rb b/activerecord/test/cases/migration/index_test.rb
index 0e375af6e8..8d1daa0a04 100644
--- a/activerecord/test/cases/migration/index_test.rb
+++ b/activerecord/test/cases/migration/index_test.rb
@@ -27,32 +27,28 @@ module ActiveRecord
ActiveRecord::Base.primary_key_prefix_type = nil
end
- def test_rename_index
- skip "not supported on openbase" if current_adapter?(:OpenBaseAdapter)
-
- # keep the names short to make Oracle and similar behave
- connection.add_index(table_name, [:foo], :name => 'old_idx')
- connection.rename_index(table_name, 'old_idx', 'new_idx')
-
- # if the adapter doesn't support the indexes call, pick defaults that let the test pass
- assert_not connection.index_name_exists?(table_name, 'old_idx', false)
- assert connection.index_name_exists?(table_name, 'new_idx', true)
- end
-
- def test_double_add_index
- skip "not supported on openbase" if current_adapter?(:OpenBaseAdapter)
+ unless current_adapter?(:OpenBaseAdapter)
+ def test_rename_index
+ # keep the names short to make Oracle and similar behave
+ connection.add_index(table_name, [:foo], :name => 'old_idx')
+ connection.rename_index(table_name, 'old_idx', 'new_idx')
+
+ # if the adapter doesn't support the indexes call, pick defaults that let the test pass
+ assert_not connection.index_name_exists?(table_name, 'old_idx', false)
+ assert connection.index_name_exists?(table_name, 'new_idx', true)
+ end
- connection.add_index(table_name, [:foo], :name => 'some_idx')
- assert_raises(ArgumentError) {
+ def test_double_add_index
connection.add_index(table_name, [:foo], :name => 'some_idx')
- }
- end
-
- def test_remove_nonexistent_index
- skip "not supported on openbase" if current_adapter?(:OpenBaseAdapter)
+ assert_raises(ArgumentError) {
+ connection.add_index(table_name, [:foo], :name => 'some_idx')
+ }
+ end
- # we do this by name, so OpenBase is a wash as noted above
- assert_raise(ArgumentError) { connection.remove_index(table_name, "no_such_index") }
+ def test_remove_nonexistent_index
+ # we do this by name, so OpenBase is a wash as noted above
+ assert_raise(ArgumentError) { connection.remove_index(table_name, "no_such_index") }
+ end
end
def test_add_index_works_with_long_index_names
@@ -109,16 +105,6 @@ module ActiveRecord
end
end
- def test_deprecated_type_argument
- message = "Passing a string as third argument of `add_index` is deprecated and will" +
- " be removed in Rails 4.1." +
- " Use add_index(:testings, [:foo, :bar], unique: true) instead"
-
- assert_deprecated message do
- connection.add_index :testings, [:foo, :bar], "UNIQUE"
- end
- end
-
def test_unique_index_exists
connection.add_index :testings, :foo, :unique => true
@@ -199,14 +185,14 @@ module ActiveRecord
end
end
- def test_add_partial_index
- skip 'only on pg' unless current_adapter?(:PostgreSQLAdapter)
+ if current_adapter?(:PostgreSQLAdapter)
+ def test_add_partial_index
+ connection.add_index("testings", "last_name", :where => "first_name = 'john doe'")
+ assert connection.index_exists?("testings", "last_name")
- connection.add_index("testings", "last_name", :where => "first_name = 'john doe'")
- assert connection.index_exists?("testings", "last_name")
-
- connection.remove_index("testings", "last_name")
- assert !connection.index_exists?("testings", "last_name")
+ connection.remove_index("testings", "last_name")
+ assert !connection.index_exists?("testings", "last_name")
+ end
end
private
diff --git a/activerecord/test/cases/migration/references_index_test.rb b/activerecord/test/cases/migration/references_index_test.rb
index 3ff89524fe..19eb7d3c9e 100644
--- a/activerecord/test/cases/migration/references_index_test.rb
+++ b/activerecord/test/cases/migration/references_index_test.rb
@@ -50,14 +50,14 @@ module ActiveRecord
assert connection.index_exists?(table_name, :bar_id, :name => :index_testings_on_bar_id, :unique => true)
end
- def test_creates_polymorphic_index
- return skip "Oracle Adapter does not support foreign keys if :polymorphic => true is used" if current_adapter? :OracleAdapter
+ unless current_adapter? :OracleAdapter
+ def test_creates_polymorphic_index
+ connection.create_table table_name do |t|
+ t.references :foo, :polymorphic => true, :index => true
+ end
- connection.create_table table_name do |t|
- t.references :foo, :polymorphic => true, :index => true
+ assert connection.index_exists?(table_name, [:foo_id, :foo_type], :name => :index_testings_on_foo_id_and_foo_type)
end
-
- assert connection.index_exists?(table_name, [:foo_id, :foo_type], :name => :index_testings_on_foo_id_and_foo_type)
end
def test_creates_index_for_existing_table
@@ -87,16 +87,16 @@ module ActiveRecord
assert_not connection.index_exists?(table_name, :foo_id, :name => :index_testings_on_foo_id)
end
- def test_creates_polymorphic_index_for_existing_table
- return skip "Oracle Adapter does not support foreign keys if :polymorphic => true is used" if current_adapter? :OracleAdapter
- connection.create_table table_name
- connection.change_table table_name do |t|
- t.references :foo, :polymorphic => true, :index => true
- end
+ unless current_adapter? :OracleAdapter
+ def test_creates_polymorphic_index_for_existing_table
+ connection.create_table table_name
+ connection.change_table table_name do |t|
+ t.references :foo, :polymorphic => true, :index => true
+ end
- assert connection.index_exists?(table_name, [:foo_id, :foo_type], :name => :index_testings_on_foo_id_and_foo_type)
+ assert connection.index_exists?(table_name, [:foo_id, :foo_type], :name => :index_testings_on_foo_id_and_foo_type)
+ end
end
-
end
end
end
diff --git a/activerecord/test/cases/migration/rename_table_test.rb b/activerecord/test/cases/migration/rename_table_test.rb
index 22dbd7c38b..2a7fafc559 100644
--- a/activerecord/test/cases/migration/rename_table_test.rb
+++ b/activerecord/test/cases/migration/rename_table_test.rb
@@ -19,24 +19,24 @@ module ActiveRecord
super
end
- def test_rename_table_for_sqlite_should_work_with_reserved_words
- renamed = false
-
- skip "not supported" unless current_adapter?(:SQLite3Adapter)
-
- add_column :test_models, :url, :string
- connection.rename_table :references, :old_references
- connection.rename_table :test_models, :references
-
- renamed = true
-
- # Using explicit id in insert for compatibility across all databases
- connection.execute "INSERT INTO 'references' (url, created_at, updated_at) VALUES ('http://rubyonrails.com', 0, 0)"
- assert_equal 'http://rubyonrails.com', connection.select_value("SELECT url FROM 'references' WHERE id=1")
- ensure
- return unless renamed
- connection.rename_table :references, :test_models
- connection.rename_table :old_references, :references
+ if current_adapter?(:SQLite3Adapter)
+ def test_rename_table_for_sqlite_should_work_with_reserved_words
+ renamed = false
+
+ add_column :test_models, :url, :string
+ connection.rename_table :references, :old_references
+ connection.rename_table :test_models, :references
+
+ renamed = true
+
+ # Using explicit id in insert for compatibility across all databases
+ connection.execute "INSERT INTO 'references' (url, created_at, updated_at) VALUES ('http://rubyonrails.com', 0, 0)"
+ assert_equal 'http://rubyonrails.com', connection.select_value("SELECT url FROM 'references' WHERE id=1")
+ ensure
+ return unless renamed
+ connection.rename_table :references, :test_models
+ connection.rename_table :old_references, :references
+ end
end
def test_rename_table
@@ -76,14 +76,14 @@ module ActiveRecord
assert_equal ['special_url_idx'], connection.indexes(:octopi).map(&:name)
end
- def test_rename_table_for_postgresql_should_also_rename_default_sequence
- skip 'not supported' unless current_adapter?(:PostgreSQLAdapter)
-
- rename_table :test_models, :octopi
+ if current_adapter?(:PostgreSQLAdapter)
+ def test_rename_table_for_postgresql_should_also_rename_default_sequence
+ rename_table :test_models, :octopi
- pk, seq = connection.pk_and_sequence_for('octopi')
+ pk, seq = connection.pk_and_sequence_for('octopi')
- assert_equal "octopi_#{pk}_seq", seq
+ assert_equal "octopi_#{pk}_seq", seq
+ end
end
end
end
diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb
index 193ffb26e3..0363bf1048 100644
--- a/activerecord/test/cases/migration_test.rb
+++ b/activerecord/test/cases/migration_test.rb
@@ -63,6 +63,7 @@ class MigrationTest < ActiveRecord::TestCase
def test_migrator_versions
migrations_path = MIGRATIONS_ROOT + "/valid"
+ old_path = ActiveRecord::Migrator.migrations_paths
ActiveRecord::Migrator.migrations_paths = migrations_path
ActiveRecord::Migrator.up(migrations_path)
@@ -74,6 +75,12 @@ class MigrationTest < ActiveRecord::TestCase
assert_equal 0, ActiveRecord::Migrator.current_version
assert_equal 3, ActiveRecord::Migrator.last_version
assert_equal true, ActiveRecord::Migrator.needs_migration?
+ ensure
+ ActiveRecord::Migrator.migrations_paths = old_path
+ end
+
+ def test_migration_version
+ ActiveRecord::Migrator.run(:up, MIGRATIONS_ROOT + "/version_check", 20131219224947)
end
def test_create_table_with_force_true_does_not_drop_nonexisting_table
@@ -177,20 +184,18 @@ class MigrationTest < ActiveRecord::TestCase
end
def test_filtering_migrations
- assert !Person.column_methods_hash.include?(:last_name)
+ assert_no_column Person, :last_name
assert !Reminder.table_exists?
name_filter = lambda { |migration| migration.name == "ValidPeopleHaveLastNames" }
ActiveRecord::Migrator.up(MIGRATIONS_ROOT + "/valid", &name_filter)
- Person.reset_column_information
- assert Person.column_methods_hash.include?(:last_name)
+ assert_column Person, :last_name
assert_raise(ActiveRecord::StatementInvalid) { Reminder.first }
ActiveRecord::Migrator.down(MIGRATIONS_ROOT + "/valid", &name_filter)
- Person.reset_column_information
- assert !Person.column_methods_hash.include?(:last_name)
+ assert_no_column Person, :last_name
assert_raise(ActiveRecord::StatementInvalid) { Reminder.first }
end
@@ -232,106 +237,110 @@ class MigrationTest < ActiveRecord::TestCase
assert migration.went_down, 'have not gone down'
end
- def test_migrator_one_up_with_exception_and_rollback
- unless ActiveRecord::Base.connection.supports_ddl_transactions?
- skip "not supported on #{ActiveRecord::Base.connection.class}"
- end
-
- assert_not Person.column_methods_hash.include?(:last_name)
+ if ActiveRecord::Base.connection.supports_ddl_transactions?
+ def test_migrator_one_up_with_exception_and_rollback
+ assert_no_column Person, :last_name
- migration = Class.new(ActiveRecord::Migration) {
- def version; 100 end
- def migrate(x)
- add_column "people", "last_name", :string
- raise 'Something broke'
- end
- }.new
+ migration = Class.new(ActiveRecord::Migration) {
+ def version; 100 end
+ def migrate(x)
+ add_column "people", "last_name", :string
+ raise 'Something broke'
+ end
+ }.new
- migrator = ActiveRecord::Migrator.new(:up, [migration], 100)
+ migrator = ActiveRecord::Migrator.new(:up, [migration], 100)
- e = assert_raise(StandardError) { migrator.migrate }
+ e = assert_raise(StandardError) { migrator.migrate }
- assert_equal "An error has occurred, this and all later migrations canceled:\n\nSomething broke", e.message
+ assert_equal "An error has occurred, this and all later migrations canceled:\n\nSomething broke", e.message
- Person.reset_column_information
- assert_not Person.column_methods_hash.include?(:last_name),
- "On error, the Migrator should revert schema changes but it did not."
- end
-
- def test_migrator_one_up_with_exception_and_rollback_using_run
- unless ActiveRecord::Base.connection.supports_ddl_transactions?
- skip "not supported on #{ActiveRecord::Base.connection.class}"
+ assert_no_column Person, :last_name,
+ "On error, the Migrator should revert schema changes but it did not."
end
- assert_not Person.column_methods_hash.include?(:last_name)
-
- migration = Class.new(ActiveRecord::Migration) {
- def version; 100 end
- def migrate(x)
- add_column "people", "last_name", :string
- raise 'Something broke'
- end
- }.new
+ def test_migrator_one_up_with_exception_and_rollback_using_run
+ assert_no_column Person, :last_name
- migrator = ActiveRecord::Migrator.new(:up, [migration], 100)
+ migration = Class.new(ActiveRecord::Migration) {
+ def version; 100 end
+ def migrate(x)
+ add_column "people", "last_name", :string
+ raise 'Something broke'
+ end
+ }.new
- e = assert_raise(StandardError) { migrator.run }
+ migrator = ActiveRecord::Migrator.new(:up, [migration], 100)
- assert_equal "An error has occurred, this migration was canceled:\n\nSomething broke", e.message
+ e = assert_raise(StandardError) { migrator.run }
- Person.reset_column_information
- assert_not Person.column_methods_hash.include?(:last_name),
- "On error, the Migrator should revert schema changes but it did not."
- end
+ assert_equal "An error has occurred, this migration was canceled:\n\nSomething broke", e.message
- def test_migration_without_transaction
- unless ActiveRecord::Base.connection.supports_ddl_transactions?
- skip "not supported on #{ActiveRecord::Base.connection.class}"
+ assert_no_column Person, :last_name,
+ "On error, the Migrator should revert schema changes but it did not."
end
- assert_not Person.column_methods_hash.include?(:last_name)
+ def test_migration_without_transaction
+ assert_no_column Person, :last_name
- migration = Class.new(ActiveRecord::Migration) {
- self.disable_ddl_transaction!
+ migration = Class.new(ActiveRecord::Migration) {
+ self.disable_ddl_transaction!
- def version; 101 end
- def migrate(x)
- add_column "people", "last_name", :string
- raise 'Something broke'
- end
- }.new
+ def version; 101 end
+ def migrate(x)
+ add_column "people", "last_name", :string
+ raise 'Something broke'
+ end
+ }.new
- migrator = ActiveRecord::Migrator.new(:up, [migration], 101)
- e = assert_raise(StandardError) { migrator.migrate }
- assert_equal "An error has occurred, all later migrations canceled:\n\nSomething broke", e.message
+ migrator = ActiveRecord::Migrator.new(:up, [migration], 101)
+ e = assert_raise(StandardError) { migrator.migrate }
+ assert_equal "An error has occurred, all later migrations canceled:\n\nSomething broke", e.message
- Person.reset_column_information
- assert Person.column_methods_hash.include?(:last_name),
- "without ddl transactions, the Migrator should not rollback on error but it did."
- ensure
- Person.reset_column_information
- if Person.column_methods_hash.include?(:last_name)
- Person.connection.remove_column('people', 'last_name')
+ assert_column Person, :last_name,
+ "without ddl transactions, the Migrator should not rollback on error but it did."
+ ensure
+ Person.reset_column_information
+ if Person.column_names.include?('last_name')
+ Person.connection.remove_column('people', 'last_name')
+ end
end
end
def test_schema_migrations_table_name
+ original_schema_migrations_table_name = ActiveRecord::Migrator.schema_migrations_table_name
+
+ assert_equal "schema_migrations", ActiveRecord::Migrator.schema_migrations_table_name
ActiveRecord::Base.table_name_prefix = "prefix_"
ActiveRecord::Base.table_name_suffix = "_suffix"
Reminder.reset_table_name
assert_equal "prefix_schema_migrations_suffix", ActiveRecord::Migrator.schema_migrations_table_name
+ ActiveRecord::Base.schema_migrations_table_name = "changed"
+ Reminder.reset_table_name
+ assert_equal "prefix_changed_suffix", ActiveRecord::Migrator.schema_migrations_table_name
ActiveRecord::Base.table_name_prefix = ""
ActiveRecord::Base.table_name_suffix = ""
Reminder.reset_table_name
- assert_equal "schema_migrations", ActiveRecord::Migrator.schema_migrations_table_name
+ assert_equal "changed", ActiveRecord::Migrator.schema_migrations_table_name
+ ensure
+ ActiveRecord::Base.schema_migrations_table_name = original_schema_migrations_table_name
+ Reminder.reset_table_name
end
- def test_proper_table_name
- assert_equal "table", ActiveRecord::Migrator.proper_table_name('table')
- assert_equal "table", ActiveRecord::Migrator.proper_table_name(:table)
- assert_equal "reminders", ActiveRecord::Migrator.proper_table_name(Reminder)
+ def test_proper_table_name_on_migrator
+ assert_deprecated do
+ assert_equal "table", ActiveRecord::Migrator.proper_table_name('table')
+ end
+ assert_deprecated do
+ assert_equal "table", ActiveRecord::Migrator.proper_table_name(:table)
+ end
+ assert_deprecated do
+ assert_equal "reminders", ActiveRecord::Migrator.proper_table_name(Reminder)
+ end
Reminder.reset_table_name
- assert_equal Reminder.table_name, ActiveRecord::Migrator.proper_table_name(Reminder)
+ assert_deprecated do
+ assert_equal Reminder.table_name, ActiveRecord::Migrator.proper_table_name(Reminder)
+ end
# Use the model's own prefix/suffix if a model is given
ActiveRecord::Base.table_name_prefix = "ARprefix_"
@@ -339,7 +348,9 @@ class MigrationTest < ActiveRecord::TestCase
Reminder.table_name_prefix = 'prefix_'
Reminder.table_name_suffix = '_suffix'
Reminder.reset_table_name
- assert_equal "prefix_reminders_suffix", ActiveRecord::Migrator.proper_table_name(Reminder)
+ assert_deprecated do
+ assert_equal "prefix_reminders_suffix", ActiveRecord::Migrator.proper_table_name(Reminder)
+ end
Reminder.table_name_prefix = ''
Reminder.table_name_suffix = ''
Reminder.reset_table_name
@@ -348,8 +359,39 @@ class MigrationTest < ActiveRecord::TestCase
ActiveRecord::Base.table_name_prefix = "prefix_"
ActiveRecord::Base.table_name_suffix = "_suffix"
Reminder.reset_table_name
- assert_equal "prefix_table_suffix", ActiveRecord::Migrator.proper_table_name('table')
- assert_equal "prefix_table_suffix", ActiveRecord::Migrator.proper_table_name(:table)
+ assert_deprecated do
+ assert_equal "prefix_table_suffix", ActiveRecord::Migrator.proper_table_name('table')
+ end
+ assert_deprecated do
+ assert_equal "prefix_table_suffix", ActiveRecord::Migrator.proper_table_name(:table)
+ end
+ end
+
+ def test_proper_table_name_on_migration
+ migration = ActiveRecord::Migration.new
+ assert_equal "table", migration.proper_table_name('table')
+ assert_equal "table", migration.proper_table_name(:table)
+ assert_equal "reminders", migration.proper_table_name(Reminder)
+ Reminder.reset_table_name
+ assert_equal Reminder.table_name, migration.proper_table_name(Reminder)
+
+ # Use the model's own prefix/suffix if a model is given
+ ActiveRecord::Base.table_name_prefix = "ARprefix_"
+ ActiveRecord::Base.table_name_suffix = "_ARsuffix"
+ Reminder.table_name_prefix = 'prefix_'
+ Reminder.table_name_suffix = '_suffix'
+ Reminder.reset_table_name
+ assert_equal "prefix_reminders_suffix", migration.proper_table_name(Reminder)
+ Reminder.table_name_prefix = ''
+ Reminder.table_name_suffix = ''
+ Reminder.reset_table_name
+
+ # Use AR::Base's prefix/suffix if string or symbol is given
+ ActiveRecord::Base.table_name_prefix = "prefix_"
+ ActiveRecord::Base.table_name_suffix = "_suffix"
+ Reminder.reset_table_name
+ assert_equal "prefix_table_suffix", migration.proper_table_name('table', migration.table_name_options)
+ assert_equal "prefix_table_suffix", migration.proper_table_name(:table, migration.table_name_options)
end
def test_rename_table_with_prefix_and_suffix
@@ -405,62 +447,88 @@ class MigrationTest < ActiveRecord::TestCase
Person.connection.drop_table :binary_testings rescue nil
end
- def test_create_table_with_custom_sequence_name
- skip "not supported" unless current_adapter? :OracleAdapter
+ def test_create_table_with_query
+ Person.connection.drop_table :table_from_query_testings rescue nil
+ Person.connection.create_table(:person, force: true)
- # table name is 29 chars, the standard sequence name will
- # be 33 chars and should be shortened
- assert_nothing_raised do
- begin
- Person.connection.create_table :table_with_name_thats_just_ok do |t|
- t.column :foo, :string, :null => false
+ Person.connection.create_table :table_from_query_testings, as: "SELECT id FROM person"
+
+ columns = Person.connection.columns(:table_from_query_testings)
+ assert_equal 1, columns.length
+ assert_equal "id", columns.first.name
+
+ Person.connection.drop_table :table_from_query_testings rescue nil
+ end
+
+ def test_create_table_with_query_from_relation
+ Person.connection.drop_table :table_from_query_testings rescue nil
+ Person.connection.create_table(:person, force: true)
+
+ Person.connection.create_table :table_from_query_testings, as: Person.select(:id)
+
+ columns = Person.connection.columns(:table_from_query_testings)
+ assert_equal 1, columns.length
+ assert_equal "id", columns.first.name
+
+ Person.connection.drop_table :table_from_query_testings rescue nil
+ end
+
+ if current_adapter? :OracleAdapter
+ def test_create_table_with_custom_sequence_name
+ # table name is 29 chars, the standard sequence name will
+ # be 33 chars and should be shortened
+ assert_nothing_raised do
+ begin
+ Person.connection.create_table :table_with_name_thats_just_ok do |t|
+ t.column :foo, :string, :null => false
+ end
+ ensure
+ Person.connection.drop_table :table_with_name_thats_just_ok rescue nil
end
- ensure
- Person.connection.drop_table :table_with_name_thats_just_ok rescue nil
end
- end
- # should be all good w/ a custom sequence name
- assert_nothing_raised do
- begin
- Person.connection.create_table :table_with_name_thats_just_ok,
- :sequence_name => 'suitably_short_seq' do |t|
- t.column :foo, :string, :null => false
- end
+ # should be all good w/ a custom sequence name
+ assert_nothing_raised do
+ begin
+ Person.connection.create_table :table_with_name_thats_just_ok,
+ :sequence_name => 'suitably_short_seq' do |t|
+ t.column :foo, :string, :null => false
+ end
- Person.connection.execute("select suitably_short_seq.nextval from dual")
+ Person.connection.execute("select suitably_short_seq.nextval from dual")
- ensure
- Person.connection.drop_table :table_with_name_thats_just_ok,
- :sequence_name => 'suitably_short_seq' rescue nil
+ ensure
+ Person.connection.drop_table :table_with_name_thats_just_ok,
+ :sequence_name => 'suitably_short_seq' rescue nil
+ end
end
- end
- # confirm the custom sequence got dropped
- assert_raise(ActiveRecord::StatementInvalid) do
- Person.connection.execute("select suitably_short_seq.nextval from dual")
+ # confirm the custom sequence got dropped
+ assert_raise(ActiveRecord::StatementInvalid) do
+ Person.connection.execute("select suitably_short_seq.nextval from dual")
+ end
end
end
- def test_out_of_range_limit_should_raise
- skip("MySQL and PostgreSQL only") unless current_adapter?(:MysqlAdapter, :Mysql2Adapter, :PostgreSQLAdapter)
-
- Person.connection.drop_table :test_limits rescue nil
- assert_raise(ActiveRecord::ActiveRecordError, "integer limit didn't raise") do
- Person.connection.create_table :test_integer_limits, :force => true do |t|
- t.column :bigone, :integer, :limit => 10
+ if current_adapter?(:MysqlAdapter, :Mysql2Adapter, :PostgreSQLAdapter)
+ def test_out_of_range_limit_should_raise
+ Person.connection.drop_table :test_limits rescue nil
+ assert_raise(ActiveRecord::ActiveRecordError, "integer limit didn't raise") do
+ Person.connection.create_table :test_integer_limits, :force => true do |t|
+ t.column :bigone, :integer, :limit => 10
+ end
end
- end
- unless current_adapter?(:PostgreSQLAdapter)
- assert_raise(ActiveRecord::ActiveRecordError, "text limit didn't raise") do
- Person.connection.create_table :test_text_limits, :force => true do |t|
- t.column :bigtext, :text, :limit => 0xfffffffff
+ unless current_adapter?(:PostgreSQLAdapter)
+ assert_raise(ActiveRecord::ActiveRecordError, "text limit didn't raise") do
+ Person.connection.create_table :test_text_limits, :force => true do |t|
+ t.column :bigtext, :text, :limit => 0xfffffffff
+ end
end
end
- end
- Person.connection.drop_table :test_limits rescue nil
+ Person.connection.drop_table :test_limits rescue nil
+ end
end
protected
@@ -665,8 +733,8 @@ class CopyMigrationsTest < ActiveRecord::TestCase
@existing_migrations = Dir[@migrations_path + "/*.rb"]
copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy"})
- assert File.exists?(@migrations_path + "/4_people_have_hobbies.bukkits.rb")
- assert File.exists?(@migrations_path + "/5_people_have_descriptions.bukkits.rb")
+ assert File.exist?(@migrations_path + "/4_people_have_hobbies.bukkits.rb")
+ assert File.exist?(@migrations_path + "/5_people_have_descriptions.bukkits.rb")
assert_equal [@migrations_path + "/4_people_have_hobbies.bukkits.rb", @migrations_path + "/5_people_have_descriptions.bukkits.rb"], copied.map(&:filename)
expected = "# This migration comes from bukkits (originally 1)"
@@ -689,10 +757,10 @@ class CopyMigrationsTest < ActiveRecord::TestCase
sources[:bukkits] = MIGRATIONS_ROOT + "/to_copy"
sources[:omg] = MIGRATIONS_ROOT + "/to_copy2"
ActiveRecord::Migration.copy(@migrations_path, sources)
- assert File.exists?(@migrations_path + "/4_people_have_hobbies.bukkits.rb")
- assert File.exists?(@migrations_path + "/5_people_have_descriptions.bukkits.rb")
- assert File.exists?(@migrations_path + "/6_create_articles.omg.rb")
- assert File.exists?(@migrations_path + "/7_create_comments.omg.rb")
+ assert File.exist?(@migrations_path + "/4_people_have_hobbies.bukkits.rb")
+ assert File.exist?(@migrations_path + "/5_people_have_descriptions.bukkits.rb")
+ assert File.exist?(@migrations_path + "/6_create_articles.omg.rb")
+ assert File.exist?(@migrations_path + "/7_create_comments.omg.rb")
files_count = Dir[@migrations_path + "/*.rb"].length
ActiveRecord::Migration.copy(@migrations_path, sources)
@@ -705,10 +773,10 @@ class CopyMigrationsTest < ActiveRecord::TestCase
@migrations_path = MIGRATIONS_ROOT + "/valid_with_timestamps"
@existing_migrations = Dir[@migrations_path + "/*.rb"]
- Time.travel_to(Time.utc(2010, 7, 26, 10, 10, 10)) do
+ travel_to(Time.utc(2010, 7, 26, 10, 10, 10)) do
copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy_with_timestamps"})
- assert File.exists?(@migrations_path + "/20100726101010_people_have_hobbies.bukkits.rb")
- assert File.exists?(@migrations_path + "/20100726101011_people_have_descriptions.bukkits.rb")
+ assert File.exist?(@migrations_path + "/20100726101010_people_have_hobbies.bukkits.rb")
+ assert File.exist?(@migrations_path + "/20100726101011_people_have_descriptions.bukkits.rb")
expected = [@migrations_path + "/20100726101010_people_have_hobbies.bukkits.rb",
@migrations_path + "/20100726101011_people_have_descriptions.bukkits.rb"]
assert_equal expected, copied.map(&:filename)
@@ -730,12 +798,12 @@ class CopyMigrationsTest < ActiveRecord::TestCase
sources[:bukkits] = MIGRATIONS_ROOT + "/to_copy_with_timestamps"
sources[:omg] = MIGRATIONS_ROOT + "/to_copy_with_timestamps2"
- Time.travel_to(Time.utc(2010, 7, 26, 10, 10, 10)) do
+ travel_to(Time.utc(2010, 7, 26, 10, 10, 10)) do
copied = ActiveRecord::Migration.copy(@migrations_path, sources)
- assert File.exists?(@migrations_path + "/20100726101010_people_have_hobbies.bukkits.rb")
- assert File.exists?(@migrations_path + "/20100726101011_people_have_descriptions.bukkits.rb")
- assert File.exists?(@migrations_path + "/20100726101012_create_articles.omg.rb")
- assert File.exists?(@migrations_path + "/20100726101013_create_comments.omg.rb")
+ assert File.exist?(@migrations_path + "/20100726101010_people_have_hobbies.bukkits.rb")
+ assert File.exist?(@migrations_path + "/20100726101011_people_have_descriptions.bukkits.rb")
+ assert File.exist?(@migrations_path + "/20100726101012_create_articles.omg.rb")
+ assert File.exist?(@migrations_path + "/20100726101013_create_comments.omg.rb")
assert_equal 4, copied.length
files_count = Dir[@migrations_path + "/*.rb"].length
@@ -750,10 +818,10 @@ class CopyMigrationsTest < ActiveRecord::TestCase
@migrations_path = MIGRATIONS_ROOT + "/valid_with_timestamps"
@existing_migrations = Dir[@migrations_path + "/*.rb"]
- Time.travel_to(Time.utc(2010, 2, 20, 10, 10, 10)) do
+ travel_to(Time.utc(2010, 2, 20, 10, 10, 10)) do
ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy_with_timestamps"})
- assert File.exists?(@migrations_path + "/20100301010102_people_have_hobbies.bukkits.rb")
- assert File.exists?(@migrations_path + "/20100301010103_people_have_descriptions.bukkits.rb")
+ assert File.exist?(@migrations_path + "/20100301010102_people_have_hobbies.bukkits.rb")
+ assert File.exist?(@migrations_path + "/20100301010103_people_have_descriptions.bukkits.rb")
files_count = Dir[@migrations_path + "/*.rb"].length
copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy_with_timestamps"})
@@ -770,7 +838,7 @@ class CopyMigrationsTest < ActiveRecord::TestCase
@existing_migrations = Dir[@migrations_path + "/*.rb"]
copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/magic"})
- assert File.exists?(@migrations_path + "/4_currencies_have_symbols.bukkits.rb")
+ assert File.exist?(@migrations_path + "/4_currencies_have_symbols.bukkits.rb")
assert_equal [@migrations_path + "/4_currencies_have_symbols.bukkits.rb"], copied.map(&:filename)
expected = "# coding: ISO-8859-15\n# This migration comes from bukkits (originally 1)"
@@ -825,10 +893,10 @@ class CopyMigrationsTest < ActiveRecord::TestCase
@migrations_path = MIGRATIONS_ROOT + "/non_existing"
@existing_migrations = []
- Time.travel_to(Time.utc(2010, 7, 26, 10, 10, 10)) do
+ travel_to(Time.utc(2010, 7, 26, 10, 10, 10)) do
copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy_with_timestamps"})
- assert File.exists?(@migrations_path + "/20100726101010_people_have_hobbies.bukkits.rb")
- assert File.exists?(@migrations_path + "/20100726101011_people_have_descriptions.bukkits.rb")
+ assert File.exist?(@migrations_path + "/20100726101010_people_have_hobbies.bukkits.rb")
+ assert File.exist?(@migrations_path + "/20100726101011_people_have_descriptions.bukkits.rb")
assert_equal 2, copied.length
end
ensure
@@ -840,13 +908,22 @@ class CopyMigrationsTest < ActiveRecord::TestCase
@migrations_path = MIGRATIONS_ROOT + "/empty"
@existing_migrations = []
- Time.travel_to(Time.utc(2010, 7, 26, 10, 10, 10)) do
+ travel_to(Time.utc(2010, 7, 26, 10, 10, 10)) do
copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy_with_timestamps"})
- assert File.exists?(@migrations_path + "/20100726101010_people_have_hobbies.bukkits.rb")
- assert File.exists?(@migrations_path + "/20100726101011_people_have_descriptions.bukkits.rb")
+ assert File.exist?(@migrations_path + "/20100726101010_people_have_hobbies.bukkits.rb")
+ assert File.exist?(@migrations_path + "/20100726101011_people_have_descriptions.bukkits.rb")
assert_equal 2, copied.length
end
ensure
clear
end
+
+ def test_check_pending_with_stdlib_logger
+ old, ActiveRecord::Base.logger = ActiveRecord::Base.logger, ::Logger.new($stdout)
+ quietly do
+ assert_nothing_raised { ActiveRecord::Migration::CheckPending.new(Proc.new {}).call({}) }
+ end
+ ensure
+ ActiveRecord::Base.logger = old
+ end
end
diff --git a/activerecord/test/cases/migrator_test.rb b/activerecord/test/cases/migrator_test.rb
index b5a69c4a92..3f9854200d 100644
--- a/activerecord/test/cases/migrator_test.rb
+++ b/activerecord/test/cases/migrator_test.rb
@@ -91,12 +91,6 @@ module ActiveRecord
assert_equal 'AddExpressions', migrations[0].name
end
- def test_deprecated_constructor
- assert_deprecated do
- ActiveRecord::Migrator.new(:up, MIGRATIONS_ROOT + "/valid")
- end
- end
-
def test_relative_migrations
list = Dir.chdir(MIGRATIONS_ROOT) do
ActiveRecord::Migrator.migrations("valid/")
diff --git a/activerecord/test/cases/mixin_test.rb b/activerecord/test/cases/mixin_test.rb
index f927c13979..ad0d5cce27 100644
--- a/activerecord/test/cases/mixin_test.rb
+++ b/activerecord/test/cases/mixin_test.rb
@@ -3,42 +3,11 @@ require "cases/helper"
class Mixin < ActiveRecord::Base
end
-# Let us control what Time.now returns for the TouchTest suite
-class Time
- @@forced_now_time = nil
- cattr_accessor :forced_now_time
-
- class << self
- def now_with_forcing
- if @@forced_now_time
- @@forced_now_time
- else
- now_without_forcing
- end
- end
- alias_method_chain :now, :forcing
- end
-end
-
-
class TouchTest < ActiveRecord::TestCase
fixtures :mixins
def setup
- Time.forced_now_time = Time.now
- end
-
- def teardown
- Time.forced_now_time = nil
- end
-
- def test_time_mocking
- five_minutes_ago = 5.minutes.ago
- Time.forced_now_time = five_minutes_ago
- assert_equal five_minutes_ago, Time.now
-
- Time.forced_now_time = nil
- assert_not_equal five_minutes_ago, Time.now
+ travel_to Time.now
end
def test_update
@@ -68,12 +37,13 @@ class TouchTest < ActiveRecord::TestCase
old_updated_at = stamped.updated_at
- Time.forced_now_time = 5.minutes.from_now
- stamped.lft_will_change!
- stamped.save
+ travel 5.minutes do
+ stamped.lft_will_change!
+ stamped.save
- assert_equal Time.now, stamped.updated_at
- assert_equal old_updated_at, stamped.created_at
+ assert_equal Time.now, stamped.updated_at
+ assert_equal old_updated_at, stamped.created_at
+ end
end
def test_create_turned_off
diff --git a/activerecord/test/cases/modules_test.rb b/activerecord/test/cases/modules_test.rb
index 08b3408665..9124105e6d 100644
--- a/activerecord/test/cases/modules_test.rb
+++ b/activerecord/test/cases/modules_test.rb
@@ -1,6 +1,7 @@
require "cases/helper"
require 'models/company_in_module'
require 'models/shop'
+require 'models/developer'
class ModulesTest < ActiveRecord::TestCase
fixtures :accounts, :companies, :projects, :developers, :collections, :products, :variants
diff --git a/activerecord/test/cases/multiparameter_attributes_test.rb b/activerecord/test/cases/multiparameter_attributes_test.rb
index 1209f5460f..c70a8f296f 100644
--- a/activerecord/test/cases/multiparameter_attributes_test.rb
+++ b/activerecord/test/cases/multiparameter_attributes_test.rb
@@ -5,12 +5,6 @@ require 'models/customer'
class MultiParameterAttributeTest < ActiveRecord::TestCase
fixtures :topics
- def setup
- ActiveRecord::Base.time_zone_aware_attributes = false
- ActiveRecord::Base.default_timezone = :local
- Time.zone = nil
- end
-
def test_multiparameter_attributes_on_date
attributes = { "last_read(1i)" => "2004", "last_read(2i)" => "6", "last_read(3i)" => "24" }
topic = Topic.find(1)
@@ -82,13 +76,15 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase
end
def test_multiparameter_attributes_on_time
- attributes = {
- "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24",
- "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00"
- }
- topic = Topic.find(1)
- topic.attributes = attributes
- assert_equal Time.local(2004, 6, 24, 16, 24, 0), topic.written_on
+ with_timezone_config default: :local do
+ attributes = {
+ "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24",
+ "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00"
+ }
+ topic = Topic.find(1)
+ topic.attributes = attributes
+ assert_equal Time.local(2004, 6, 24, 16, 24, 0), topic.written_on
+ end
end
def test_multiparameter_attributes_on_time_with_no_date
@@ -148,13 +144,15 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase
end
def test_multiparameter_attributes_on_time_will_ignore_hour_if_missing
- attributes = {
- "written_on(1i)" => "2004", "written_on(2i)" => "12", "written_on(3i)" => "12",
- "written_on(5i)" => "12", "written_on(6i)" => "02"
- }
- topic = Topic.find(1)
- topic.attributes = attributes
- assert_equal Time.local(2004, 12, 12, 0, 12, 2), topic.written_on
+ with_timezone_config default: :local do
+ attributes = {
+ "written_on(1i)" => "2004", "written_on(2i)" => "12", "written_on(3i)" => "12",
+ "written_on(5i)" => "12", "written_on(6i)" => "02"
+ }
+ topic = Topic.find(1)
+ topic.attributes = attributes
+ assert_equal Time.local(2004, 12, 12, 0, 12, 2), topic.written_on
+ end
end
def test_multiparameter_attributes_on_time_will_ignore_hour_if_blank
@@ -176,6 +174,7 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase
topic.attributes = attributes
assert_nil topic.written_on
end
+
def test_multiparameter_attributes_on_time_with_seconds_will_ignore_date_if_empty
attributes = {
"written_on(1i)" => "", "written_on(2i)" => "", "written_on(3i)" => "",
@@ -187,56 +186,56 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase
end
def test_multiparameter_attributes_on_time_with_utc
- ActiveRecord::Base.default_timezone = :utc
- attributes = {
- "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24",
- "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00"
- }
- topic = Topic.find(1)
- topic.attributes = attributes
- assert_equal Time.utc(2004, 6, 24, 16, 24, 0), topic.written_on
+ with_timezone_config default: :utc do
+ attributes = {
+ "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24",
+ "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00"
+ }
+ topic = Topic.find(1)
+ topic.attributes = attributes
+ assert_equal Time.utc(2004, 6, 24, 16, 24, 0), topic.written_on
+ end
end
def test_multiparameter_attributes_on_time_with_time_zone_aware_attributes
- ActiveRecord::Base.time_zone_aware_attributes = true
- ActiveRecord::Base.default_timezone = :utc
- Time.zone = ActiveSupport::TimeZone[-28800]
- attributes = {
- "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24",
- "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00"
- }
- topic = Topic.find(1)
- topic.attributes = attributes
- assert_equal Time.utc(2004, 6, 24, 23, 24, 0), topic.written_on
- assert_equal Time.utc(2004, 6, 24, 16, 24, 0), topic.written_on.time
- assert_equal Time.zone, topic.written_on.time_zone
+ with_timezone_config default: :utc, aware_attributes: true, zone: -28800 do
+ attributes = {
+ "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24",
+ "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00"
+ }
+ topic = Topic.find(1)
+ topic.attributes = attributes
+ assert_equal Time.utc(2004, 6, 24, 23, 24, 0), topic.written_on
+ assert_equal Time.utc(2004, 6, 24, 16, 24, 0), topic.written_on.time
+ assert_equal Time.zone, topic.written_on.time_zone
+ end
end
def test_multiparameter_attributes_on_time_with_time_zone_aware_attributes_false
- Time.zone = ActiveSupport::TimeZone[-28800]
- attributes = {
- "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24",
- "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00"
- }
- topic = Topic.find(1)
- topic.attributes = attributes
- assert_equal Time.local(2004, 6, 24, 16, 24, 0), topic.written_on
- assert_equal false, topic.written_on.respond_to?(:time_zone)
+ with_timezone_config default: :local, aware_attributes: false, zone: -28800 do
+ attributes = {
+ "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24",
+ "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00"
+ }
+ topic = Topic.find(1)
+ topic.attributes = attributes
+ assert_equal Time.local(2004, 6, 24, 16, 24, 0), topic.written_on
+ assert_equal false, topic.written_on.respond_to?(:time_zone)
+ end
end
def test_multiparameter_attributes_on_time_with_skip_time_zone_conversion_for_attributes
- ActiveRecord::Base.time_zone_aware_attributes = true
- ActiveRecord::Base.default_timezone = :utc
- Time.zone = ActiveSupport::TimeZone[-28800]
- Topic.skip_time_zone_conversion_for_attributes = [:written_on]
- attributes = {
- "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24",
- "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00"
- }
- topic = Topic.find(1)
- topic.attributes = attributes
- assert_equal Time.utc(2004, 6, 24, 16, 24, 0), topic.written_on
- assert_equal false, topic.written_on.respond_to?(:time_zone)
+ with_timezone_config default: :utc, aware_attributes: true, zone: -28800 do
+ Topic.skip_time_zone_conversion_for_attributes = [:written_on]
+ attributes = {
+ "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24",
+ "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00"
+ }
+ topic = Topic.find(1)
+ topic.attributes = attributes
+ assert_equal Time.utc(2004, 6, 24, 16, 24, 0), topic.written_on
+ assert_equal false, topic.written_on.respond_to?(:time_zone)
+ end
ensure
Topic.skip_time_zone_conversion_for_attributes = []
end
@@ -244,36 +243,37 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase
# Oracle, and Sybase do not have a TIME datatype.
unless current_adapter?(:OracleAdapter, :SybaseAdapter)
def test_multiparameter_attributes_on_time_only_column_with_time_zone_aware_attributes_does_not_do_time_zone_conversion
- ActiveRecord::Base.time_zone_aware_attributes = true
- ActiveRecord::Base.default_timezone = :utc
- Time.zone = ActiveSupport::TimeZone[-28800]
+ with_timezone_config default: :utc, aware_attributes: true, zone: -28800 do
+ attributes = {
+ "bonus_time(1i)" => "2000", "bonus_time(2i)" => "1", "bonus_time(3i)" => "1",
+ "bonus_time(4i)" => "16", "bonus_time(5i)" => "24"
+ }
+ topic = Topic.find(1)
+ topic.attributes = attributes
+ assert_equal Time.utc(2000, 1, 1, 16, 24, 0), topic.bonus_time
+ assert topic.bonus_time.utc?
+ end
+ end
+ end
+
+ def test_multiparameter_attributes_on_time_with_empty_seconds
+ with_timezone_config default: :local do
attributes = {
- "bonus_time(1i)" => "2000", "bonus_time(2i)" => "1", "bonus_time(3i)" => "1",
- "bonus_time(4i)" => "16", "bonus_time(5i)" => "24"
+ "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24",
+ "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => ""
}
topic = Topic.find(1)
topic.attributes = attributes
- assert_equal Time.utc(2000, 1, 1, 16, 24, 0), topic.bonus_time
- assert topic.bonus_time.utc?
+ assert_equal Time.local(2004, 6, 24, 16, 24, 0), topic.written_on
end
end
- def test_multiparameter_attributes_on_time_with_empty_seconds
- attributes = {
- "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24",
- "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => ""
- }
- topic = Topic.find(1)
- topic.attributes = attributes
- assert_equal Time.local(2004, 6, 24, 16, 24, 0), topic.written_on
- end
-
- def test_multiparameter_attributes_setting_time_attribute
- return skip "Oracle does not have TIME data type" if current_adapter? :OracleAdapter
-
- topic = Topic.new( "bonus_time(4i)"=> "01", "bonus_time(5i)" => "05" )
- assert_equal 1, topic.bonus_time.hour
- assert_equal 5, topic.bonus_time.min
+ unless current_adapter? :OracleAdapter
+ def test_multiparameter_attributes_setting_time_attribute
+ topic = Topic.new( "bonus_time(4i)"=> "01", "bonus_time(5i)" => "05" )
+ assert_equal 1, topic.bonus_time.hour
+ assert_equal 5, topic.bonus_time.min
+ end
end
def test_multiparameter_attributes_setting_date_attribute
diff --git a/activerecord/test/cases/multiple_db_test.rb b/activerecord/test/cases/multiple_db_test.rb
index 2e386a172a..3831de6ae3 100644
--- a/activerecord/test/cases/multiple_db_test.rb
+++ b/activerecord/test/cases/multiple_db_test.rb
@@ -101,7 +101,7 @@ class MultipleDbTest < ActiveRecord::TestCase
College.first.courses.first
end
ensure
- ActiveRecord::Base.establish_connection 'arunit'
+ ActiveRecord::Base.establish_connection :arunit
end
end
end
diff --git a/activerecord/test/cases/nested_attributes_test.rb b/activerecord/test/cases/nested_attributes_test.rb
index 6fe81e0d96..2f89699df7 100644
--- a/activerecord/test/cases/nested_attributes_test.rb
+++ b/activerecord/test/cases/nested_attributes_test.rb
@@ -797,26 +797,6 @@ module NestedAttributesOnACollectionAssociationTests
end
end
- def test_validate_presence_of_parent_fails_without_inverse_of
- Man.accepts_nested_attributes_for(:interests)
- Man.reflect_on_association(:interests).options.delete(:inverse_of)
- Man.reflect_on_association(:interests).clear_inverse_of_cache!
- Interest.reflect_on_association(:man).options.delete(:inverse_of)
- Interest.reflect_on_association(:man).clear_inverse_of_cache!
-
- repair_validations(Interest) do
- Interest.validates_presence_of(:man)
- assert_no_difference ['Man.count', 'Interest.count'] do
- man = Man.create(:name => 'John',
- :interests_attributes => [{:topic=>'Cars'}, {:topic=>'Sports'}])
- assert !man.errors[:"interests.man"].empty?
- end
- end
- ensure
- Man.reflect_on_association(:interests).options[:inverse_of] = :man
- Interest.reflect_on_association(:man).options[:inverse_of] = :interests
- end
-
def test_can_use_symbols_as_object_identifier
@pirate.attributes = { :parrots_attributes => { :foo => { :name => 'Lovely Day' }, :bar => { :name => 'Blown Away' } } }
assert_nothing_raised(NoMethodError) { @pirate.save! }
diff --git a/activerecord/test/cases/nested_attributes_with_callbacks_test.rb b/activerecord/test/cases/nested_attributes_with_callbacks_test.rb
new file mode 100644
index 0000000000..43a69928b6
--- /dev/null
+++ b/activerecord/test/cases/nested_attributes_with_callbacks_test.rb
@@ -0,0 +1,144 @@
+require "cases/helper"
+require "models/pirate"
+require "models/bird"
+
+class NestedAttributesWithCallbacksTest < ActiveRecord::TestCase
+ Pirate.has_many(:birds_with_add_load,
+ :class_name => "Bird",
+ :before_add => proc { |p,b|
+ @@add_callback_called << b
+ p.birds_with_add_load.to_a
+ })
+ Pirate.has_many(:birds_with_add,
+ :class_name => "Bird",
+ :before_add => proc { |p,b| @@add_callback_called << b })
+
+ Pirate.accepts_nested_attributes_for(:birds_with_add_load,
+ :birds_with_add,
+ :allow_destroy => true)
+
+ def setup
+ @@add_callback_called = []
+ @pirate = Pirate.new.tap do |pirate|
+ pirate.catchphrase = "Don't call me!"
+ pirate.birds_attributes = [{:name => 'Bird1'},{:name => 'Bird2'}]
+ pirate.save!
+ end
+ @birds = @pirate.birds.to_a
+ end
+
+ def bird_to_update
+ @birds[0]
+ end
+
+ def bird_to_destroy
+ @birds[1]
+ end
+
+ def existing_birds_attributes
+ @birds.map do |bird|
+ bird.attributes.slice("id","name")
+ end
+ end
+
+ def new_birds
+ @pirate.birds_with_add.to_a - @birds
+ end
+
+ def new_bird_attributes
+ [{'name' => "New Bird"}]
+ end
+
+ def destroy_bird_attributes
+ [{'id' => bird_to_destroy.id.to_s, "_destroy" => true}]
+ end
+
+ def update_new_and_destroy_bird_attributes
+ [{'id' => @birds[0].id.to_s, 'name' => 'New Name'},
+ {'name' => "New Bird"},
+ {'id' => bird_to_destroy.id.to_s, "_destroy" => true}]
+ end
+
+ # Characterizing when :before_add callback is called
+ test ":before_add called for new bird when not loaded" do
+ assert_not @pirate.birds_with_add.loaded?
+ @pirate.birds_with_add_attributes = new_bird_attributes
+ assert_new_bird_with_callback_called
+ end
+
+ test ":before_add called for new bird when loaded" do
+ @pirate.birds_with_add.load_target
+ @pirate.birds_with_add_attributes = new_bird_attributes
+ assert_new_bird_with_callback_called
+ end
+
+ def assert_new_bird_with_callback_called
+ assert_equal(1, new_birds.size)
+ assert_equal(new_birds, @@add_callback_called)
+ end
+
+ test ":before_add not called for identical assignment when not loaded" do
+ assert_not @pirate.birds_with_add.loaded?
+ @pirate.birds_with_add_attributes = existing_birds_attributes
+ assert_callbacks_not_called
+ end
+
+ test ":before_add not called for identical assignment when loaded" do
+ @pirate.birds_with_add.load_target
+ @pirate.birds_with_add_attributes = existing_birds_attributes
+ assert_callbacks_not_called
+ end
+
+ test ":before_add not called for destroy assignment when not loaded" do
+ assert_not @pirate.birds_with_add.loaded?
+ @pirate.birds_with_add_attributes = destroy_bird_attributes
+ assert_callbacks_not_called
+ end
+
+ test ":before_add not called for deletion assignment when loaded" do
+ @pirate.birds_with_add.load_target
+ @pirate.birds_with_add_attributes = destroy_bird_attributes
+ assert_callbacks_not_called
+ end
+
+ def assert_callbacks_not_called
+ assert_empty new_birds
+ assert_empty @@add_callback_called
+ end
+
+ # Ensuring that the records in the association target are updated,
+ # whether the association is loaded before or not
+ test "Assignment updates records in target when not loaded" do
+ assert_not @pirate.birds_with_add.loaded?
+ @pirate.birds_with_add_attributes = update_new_and_destroy_bird_attributes
+ assert_assignment_affects_records_in_target(:birds_with_add)
+ end
+
+ test "Assignment updates records in target when loaded" do
+ @pirate.birds_with_add.load_target
+ @pirate.birds_with_add_attributes = update_new_and_destroy_bird_attributes
+ assert_assignment_affects_records_in_target(:birds_with_add)
+ end
+
+ test("Assignment updates records in target when not loaded" +
+ " and callback loads target") do
+ assert_not @pirate.birds_with_add_load.loaded?
+ @pirate.birds_with_add_load_attributes = update_new_and_destroy_bird_attributes
+ assert_assignment_affects_records_in_target(:birds_with_add_load)
+ end
+
+ test("Assignment updates records in target when loaded" +
+ " and callback loads target") do
+ @pirate.birds_with_add_load.load_target
+ @pirate.birds_with_add_load_attributes = update_new_and_destroy_bird_attributes
+ assert_assignment_affects_records_in_target(:birds_with_add_load)
+ end
+
+ def assert_assignment_affects_records_in_target(association_name)
+ association = @pirate.send(association_name)
+ assert association.detect {|b| b == bird_to_update }.name_changed?,
+ 'Update record not updated'
+ assert association.detect {|b| b == bird_to_destroy }.marked_for_destruction?,
+ 'Destroy record not marked for destruction'
+ end
+end
diff --git a/activerecord/test/cases/persistence_test.rb b/activerecord/test/cases/persistence_test.rb
index c4a72ed3bc..6f1e518f45 100644
--- a/activerecord/test/cases/persistence_test.rb
+++ b/activerecord/test/cases/persistence_test.rb
@@ -139,6 +139,33 @@ class PersistenceTest < ActiveRecord::TestCase
end
end
+ def test_becomes
+ assert_kind_of Reply, topics(:first).becomes(Reply)
+ assert_equal "The First Topic", topics(:first).becomes(Reply).title
+ end
+
+ def test_becomes_includes_errors
+ company = Company.new(:name => nil)
+ assert !company.valid?
+ original_errors = company.errors
+ client = company.becomes(Client)
+ assert_equal original_errors, client.errors
+ end
+
+ def test_dupd_becomes_persists_changes_from_the_original
+ original = topics(:first)
+ copy = original.dup.becomes(Reply)
+ copy.save!
+ assert_equal "The First Topic", Topic.find(copy.id).title
+ end
+
+ def test_becomes_includes_changed_attributes
+ company = Company.new(name: "37signals")
+ client = company.becomes(Client)
+ assert_equal "37signals", client.name
+ assert_equal %w{name}, client.changed
+ end
+
def test_delete_many
original_count = Topic.count
Topic.delete(deleting = [1, 2])
@@ -406,10 +433,6 @@ class PersistenceTest < ActiveRecord::TestCase
assert !Topic.find(1).approved?
end
- def test_update_attribute_does_not_choke_on_nil
- assert Topic.find(1).update(nil)
- end
-
def test_update_attribute_for_readonly_attribute
minivan = Minivan.find('m1')
assert_raises(ActiveRecord::ActiveRecordError) { minivan.update_attribute(:color, 'black') }
@@ -688,6 +711,17 @@ class PersistenceTest < ActiveRecord::TestCase
assert_equal topic.title, Topic.find(1234).title
end
+ def test_update_attributes_parameters
+ topic = Topic.find(1)
+ assert_nothing_raised do
+ topic.update_attributes({})
+ end
+
+ assert_raises(ArgumentError) do
+ topic.update_attributes(nil)
+ end
+ end
+
def test_update!
Reply.validates_presence_of(:title)
reply = Reply.find(2)
diff --git a/activerecord/test/cases/pooled_connections_test.rb b/activerecord/test/cases/pooled_connections_test.rb
index a8a9b06ec4..626c6aeaf8 100644
--- a/activerecord/test/cases/pooled_connections_test.rb
+++ b/activerecord/test/cases/pooled_connections_test.rb
@@ -16,22 +16,6 @@ class PooledConnectionsTest < ActiveRecord::TestCase
@per_test_teardown.each {|td| td.call }
end
- def checkout_connections
- ActiveRecord::Base.establish_connection(@connection.merge({:pool => 2, :checkout_timeout => 0.3}))
- @connections = []
- @timed_out = 0
-
- 4.times do
- Thread.new do
- begin
- @connections << ActiveRecord::Base.connection_pool.checkout
- rescue ActiveRecord::ConnectionTimeoutError
- @timed_out += 1
- end
- end.join
- end
- end
-
# Will deadlock due to lack of Monitor timeouts in 1.9
def checkout_checkin_connections(pool_size, threads)
ActiveRecord::Base.establish_connection(@connection.merge({:pool => pool_size, :checkout_timeout => 0.5}))
diff --git a/activerecord/test/cases/primary_keys_test.rb b/activerecord/test/cases/primary_keys_test.rb
index aa125c70c5..1b915387be 100644
--- a/activerecord/test/cases/primary_keys_test.rb
+++ b/activerecord/test/cases/primary_keys_test.rb
@@ -185,19 +185,19 @@ end
class PrimaryKeyWithNoConnectionTest < ActiveRecord::TestCase
self.use_transactional_fixtures = false
- def test_set_primary_key_with_no_connection
- return skip("disconnect wipes in-memory db") if in_memory_db?
+ unless in_memory_db?
+ def test_set_primary_key_with_no_connection
+ connection = ActiveRecord::Base.remove_connection
- connection = ActiveRecord::Base.remove_connection
+ model = Class.new(ActiveRecord::Base)
+ model.primary_key = 'foo'
- model = Class.new(ActiveRecord::Base)
- model.primary_key = 'foo'
+ assert_equal 'foo', model.primary_key
- assert_equal 'foo', model.primary_key
+ ActiveRecord::Base.establish_connection(connection)
- ActiveRecord::Base.establish_connection(connection)
-
- assert_equal 'foo', model.primary_key
+ assert_equal 'foo', model.primary_key
+ end
end
end
@@ -212,7 +212,6 @@ if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
ensure
con.reconnect!
end
-
end
end
diff --git a/activerecord/test/cases/query_cache_test.rb b/activerecord/test/cases/query_cache_test.rb
index 136fda664c..5566563116 100644
--- a/activerecord/test/cases/query_cache_test.rb
+++ b/activerecord/test/cases/query_cache_test.rb
@@ -134,6 +134,15 @@ class QueryCacheTest < ActiveRecord::TestCase
end
end
+ def test_find_queries_with_multi_cache_blocks
+ Task.cache do
+ Task.cache do
+ assert_queries(2) { Task.find(1); Task.find(2) }
+ end
+ assert_queries(0) { Task.find(1); Task.find(1); Task.find(2) }
+ end
+ end
+
def test_count_queries_with_cache
Task.cache do
assert_queries(1) { Task.count; Task.count }
diff --git a/activerecord/test/cases/quoting_test.rb b/activerecord/test/cases/quoting_test.rb
index 3dd11ae89d..e2439b9a24 100644
--- a/activerecord/test/cases/quoting_test.rb
+++ b/activerecord/test/cases/quoting_test.rb
@@ -53,50 +53,40 @@ module ActiveRecord
end
def test_quoted_time_utc
- before = ActiveRecord::Base.default_timezone
- ActiveRecord::Base.default_timezone = :utc
- t = Time.now
- assert_equal t.getutc.to_s(:db), @quoter.quoted_date(t)
- ensure
- ActiveRecord::Base.default_timezone = before
+ with_timezone_config default: :utc do
+ t = Time.now
+ assert_equal t.getutc.to_s(:db), @quoter.quoted_date(t)
+ end
end
def test_quoted_time_local
- before = ActiveRecord::Base.default_timezone
- ActiveRecord::Base.default_timezone = :local
- t = Time.now
- assert_equal t.getlocal.to_s(:db), @quoter.quoted_date(t)
- ensure
- ActiveRecord::Base.default_timezone = before
+ with_timezone_config default: :local do
+ t = Time.now
+ assert_equal t.getlocal.to_s(:db), @quoter.quoted_date(t)
+ end
end
def test_quoted_time_crazy
- before = ActiveRecord::Base.default_timezone
- ActiveRecord::Base.default_timezone = :asdfasdf
- t = Time.now
- assert_equal t.getlocal.to_s(:db), @quoter.quoted_date(t)
- ensure
- ActiveRecord::Base.default_timezone = before
+ with_timezone_config default: :asdfasdf do
+ t = Time.now
+ assert_equal t.getlocal.to_s(:db), @quoter.quoted_date(t)
+ end
end
def test_quoted_datetime_utc
- before = ActiveRecord::Base.default_timezone
- ActiveRecord::Base.default_timezone = :utc
- t = DateTime.now
- assert_equal t.getutc.to_s(:db), @quoter.quoted_date(t)
- ensure
- ActiveRecord::Base.default_timezone = before
+ with_timezone_config default: :utc do
+ t = DateTime.now
+ assert_equal t.getutc.to_s(:db), @quoter.quoted_date(t)
+ end
end
###
# DateTime doesn't define getlocal, so make sure it does nothing
def test_quoted_datetime_local
- before = ActiveRecord::Base.default_timezone
- ActiveRecord::Base.default_timezone = :local
- t = DateTime.now
- assert_equal t.to_s(:db), @quoter.quoted_date(t)
- ensure
- ActiveRecord::Base.default_timezone = before
+ with_timezone_config default: :local do
+ t = DateTime.now
+ assert_equal t.to_s(:db), @quoter.quoted_date(t)
+ end
end
def test_quote_with_quoted_id
@@ -194,25 +184,6 @@ module ActiveRecord
assert_equal "'lo\\\\l'", @quoter.quote('lo\l', FakeColumn.new(:binary))
end
- def test_quote_binary_with_string_to_binary
- col = Class.new(FakeColumn) {
- def string_to_binary(value)
- 'foo'
- end
- }.new(:binary)
- assert_equal "'foo'", @quoter.quote('lo\l', col)
- end
-
- def test_quote_as_mb_chars_binary_column_with_string_to_binary
- col = Class.new(FakeColumn) {
- def string_to_binary(value)
- 'foo'
- end
- }.new(:binary)
- string = ActiveSupport::Multibyte::Chars.new('lo\l')
- assert_equal "'foo'", @quoter.quote(string, col)
- end
-
def test_string_with_crazy_column
assert_equal "'lo\\\\l'", @quoter.quote('lo\l', FakeColumn.new(:foo))
end
diff --git a/activerecord/test/cases/readonly_test.rb b/activerecord/test/cases/readonly_test.rb
index df076c97b4..2afd25c989 100644
--- a/activerecord/test/cases/readonly_test.rb
+++ b/activerecord/test/cases/readonly_test.rb
@@ -1,4 +1,5 @@
require "cases/helper"
+require 'models/author'
require 'models/post'
require 'models/comment'
require 'models/developer'
@@ -7,7 +8,7 @@ require 'models/reader'
require 'models/person'
class ReadOnlyTest < ActiveRecord::TestCase
- fixtures :posts, :comments, :developers, :projects, :developers_projects, :people, :readers
+ fixtures :authors, :posts, :comments, :developers, :projects, :developers_projects, :people, :readers
def test_cant_save_readonly_record
dev = Developer.find(1)
@@ -34,15 +35,12 @@ class ReadOnlyTest < ActiveRecord::TestCase
Developer.readonly.each { |d| assert d.readonly? }
end
+ def test_find_with_joins_option_does_not_imply_readonly
+ Developer.joins(' ').each { |d| assert_not d.readonly? }
+ Developer.joins(' ').readonly(true).each { |d| assert d.readonly? }
- def test_find_with_joins_option_implies_readonly
- # Blank joins don't count.
- Developer.joins(' ').each { |d| assert !d.readonly? }
- Developer.joins(' ').readonly(false).each { |d| assert !d.readonly? }
-
- # Others do.
- Developer.joins(', projects').each { |d| assert d.readonly? }
- Developer.joins(', projects').readonly(false).each { |d| assert !d.readonly? }
+ Developer.joins(', projects').each { |d| assert_not d.readonly? }
+ Developer.joins(', projects').readonly(true).each { |d| assert d.readonly? }
end
def test_has_many_find_readonly
@@ -87,7 +85,7 @@ class ReadOnlyTest < ActiveRecord::TestCase
# conflicting column names
unless current_adapter?(:OracleAdapter)
Post.joins(', developers').scoping do
- assert Post.find(1).readonly?
+ assert_not Post.find(1).readonly?
assert Post.readonly.find(1).readonly?
assert !Post.readonly(false).find(1).readonly?
end
diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb
index b5314bc9be..d7ad5ed29f 100644
--- a/activerecord/test/cases/reflection_test.rb
+++ b/activerecord/test/cases/reflection_test.rb
@@ -18,6 +18,11 @@ require 'models/subscription'
require 'models/tag'
require 'models/sponsor'
require 'models/edge'
+require 'models/hotel'
+require 'models/chef'
+require 'models/department'
+require 'models/cake_designer'
+require 'models/drink_designer'
class ReflectionTest < ActiveRecord::TestCase
include ActiveRecord::Reflection
@@ -186,14 +191,6 @@ class ReflectionTest < ActiveRecord::TestCase
ActiveRecord::Base.store_full_sti_class = true
end
- def test_reflection_of_all_associations
- # FIXME these assertions bust a lot
- assert_equal 39, Firm.reflect_on_all_associations.size
- assert_equal 29, Firm.reflect_on_all_associations(:has_many).size
- assert_equal 10, Firm.reflect_on_all_associations(:has_one).size
- assert_equal 0, Firm.reflect_on_all_associations(:belongs_to).size
- end
-
def test_reflection_should_not_raise_error_when_compared_to_other_object
assert_nothing_raised { Firm.reflections[:clients] == Object.new }
end
@@ -235,6 +232,17 @@ class ReflectionTest < ActiveRecord::TestCase
assert_equal expected, actual
end
+ def test_scope_chain_does_not_interfere_with_hmt_with_polymorphic_case
+ @hotel = Hotel.create!
+ @department = @hotel.departments.create!
+ @department.chefs.create!(employable: CakeDesigner.create!)
+ @department.chefs.create!(employable: DrinkDesigner.create!)
+
+ assert_equal 1, @hotel.cake_designers.size
+ assert_equal 1, @hotel.drink_designers.size
+ assert_equal 2, @hotel.chefs.size
+ end
+
def test_nested?
assert !Author.reflect_on_association(:comments).nested?
assert Author.reflect_on_association(:tags).nested?
@@ -260,8 +268,9 @@ class ReflectionTest < ActiveRecord::TestCase
reflection = ActiveRecord::Reflection::AssociationReflection.new(:fuu, :edge, nil, {}, Author)
assert_raises(ActiveRecord::UnknownPrimaryKey) { reflection.association_primary_key }
- through = ActiveRecord::Reflection::ThroughReflection.new(:fuu, :edge, nil, {}, Author)
- through.stubs(:source_reflection).returns(stub_everything(:options => {}, :class_name => 'Edge'))
+ through = Class.new(ActiveRecord::Reflection::ThroughReflection) {
+ define_method(:source_reflection) { reflection }
+ }.new(:fuu, :edge, nil, {}, Author)
assert_raises(ActiveRecord::UnknownPrimaryKey) { through.association_primary_key }
end
diff --git a/activerecord/test/cases/relation/delegation_test.rb b/activerecord/test/cases/relation/delegation_test.rb
new file mode 100644
index 0000000000..9b2bfed039
--- /dev/null
+++ b/activerecord/test/cases/relation/delegation_test.rb
@@ -0,0 +1,68 @@
+require 'cases/helper'
+require 'models/post'
+require 'models/comment'
+
+module ActiveRecord
+ class DelegationTest < ActiveRecord::TestCase
+ fixtures :posts
+
+ def call_method(target, method)
+ method_arity = target.to_a.method(method).arity
+
+ if method_arity.zero?
+ target.public_send(method)
+ elsif method_arity < 0
+ if method == :shuffle!
+ target.public_send(method)
+ else
+ target.public_send(method, 1)
+ end
+ elsif method_arity == 1
+ target.public_send(method, 1)
+ else
+ raise NotImplementedError
+ end
+ end
+ end
+
+ module DelegationWhitelistBlacklistTests
+ ARRAY_DELEGATES = [
+ :+, :-, :|, :&, :[],
+ :all?, :collect, :detect, :each, :each_cons, :each_with_index,
+ :exclude?, :find_all, :flat_map, :group_by, :include?, :length,
+ :map, :none?, :one?, :partition, :reject, :reverse,
+ :sample, :second, :sort, :sort_by, :third,
+ :to_ary, :to_set, :to_xml, :to_yaml
+ ]
+
+ ARRAY_DELEGATES.each do |method|
+ define_method "test_delegates_#{method}_to_Array" do
+ assert_respond_to target, method
+ end
+ end
+
+ ActiveRecord::Delegation::BLACKLISTED_ARRAY_METHODS.each do |method|
+ define_method "test_#{method}_is_not_delegated_to_Array" do
+ assert_raises(NoMethodError) { call_method(target, method) }
+ end
+ end
+ end
+
+ class DelegationAssociationTest < DelegationTest
+ include DelegationWhitelistBlacklistTests
+
+ def target
+ Post.first.comments
+ end
+ end
+
+ class DelegationRelationTest < DelegationTest
+ include DelegationWhitelistBlacklistTests
+
+ fixtures :comments
+
+ def target
+ Comment.all
+ end
+ end
+end
diff --git a/activerecord/test/cases/relation/merging_test.rb b/activerecord/test/cases/relation/merging_test.rb
new file mode 100644
index 0000000000..23500bf5d8
--- /dev/null
+++ b/activerecord/test/cases/relation/merging_test.rb
@@ -0,0 +1,162 @@
+require 'cases/helper'
+require 'models/author'
+require 'models/comment'
+require 'models/developer'
+require 'models/post'
+require 'models/project'
+
+class RelationMergingTest < ActiveRecord::TestCase
+ fixtures :developers, :comments, :authors, :posts
+
+ def test_relation_merging
+ devs = Developer.where("salary >= 80000").merge(Developer.limit(2)).merge(Developer.order('id ASC').where("id < 3"))
+ assert_equal [developers(:david), developers(:jamis)], devs.to_a
+
+ dev_with_count = Developer.limit(1).merge(Developer.order('id DESC')).merge(Developer.select('developers.*'))
+ assert_equal [developers(:poor_jamis)], dev_with_count.to_a
+ end
+
+ def test_relation_to_sql
+ sql = Post.connection.unprepared_statement do
+ Post.first.comments.to_sql
+ end
+ assert_no_match(/\?/, sql)
+ end
+
+ def test_relation_merging_with_arel_equalities_keeps_last_equality
+ devs = Developer.where(Developer.arel_table[:salary].eq(80000)).merge(
+ Developer.where(Developer.arel_table[:salary].eq(9000))
+ )
+ assert_equal [developers(:poor_jamis)], devs.to_a
+ end
+
+ def test_relation_merging_with_arel_equalities_keeps_last_equality_with_non_attribute_left_hand
+ salary_attr = Developer.arel_table[:salary]
+ devs = Developer.where(
+ Arel::Nodes::NamedFunction.new('abs', [salary_attr]).eq(80000)
+ ).merge(
+ Developer.where(
+ Arel::Nodes::NamedFunction.new('abs', [salary_attr]).eq(9000)
+ )
+ )
+ assert_equal [developers(:poor_jamis)], devs.to_a
+ end
+
+ def test_relation_merging_with_eager_load
+ relations = []
+ relations << Post.order('comments.id DESC').merge(Post.eager_load(:last_comment)).merge(Post.all)
+ relations << Post.eager_load(:last_comment).merge(Post.order('comments.id DESC')).merge(Post.all)
+
+ relations.each do |posts|
+ post = posts.find { |p| p.id == 1 }
+ assert_equal Post.find(1).last_comment, post.last_comment
+ end
+ end
+
+ def test_relation_merging_with_locks
+ devs = Developer.lock.where("salary >= 80000").order("id DESC").merge(Developer.limit(2))
+ assert devs.locked.present?
+ end
+
+ def test_relation_merging_with_preload
+ [Post.all.merge(Post.preload(:author)), Post.preload(:author).merge(Post.all)].each do |posts|
+ assert_queries(2) { assert posts.first.author }
+ end
+ end
+
+ def test_relation_merging_with_joins
+ comments = Comment.joins(:post).where(:body => 'Thank you for the welcome').merge(Post.where(:body => 'Such a lovely day'))
+ assert_equal 1, comments.count
+ end
+
+ def test_relation_merging_with_association
+ assert_queries(2) do # one for loading post, and another one merged query
+ post = Post.where(:body => 'Such a lovely day').first
+ comments = Comment.where(:body => 'Thank you for the welcome').merge(post.comments)
+ assert_equal 1, comments.count
+ end
+ end
+
+ test "merge collapses wheres from the LHS only" do
+ left = Post.where(title: "omg").where(comments_count: 1)
+ right = Post.where(title: "wtf").where(title: "bbq")
+
+ expected = [left.where_values[1]] + right.where_values
+ merged = left.merge(right)
+
+ assert_equal expected, merged.where_values
+ assert !merged.to_sql.include?("omg")
+ assert merged.to_sql.include?("wtf")
+ assert merged.to_sql.include?("bbq")
+ end
+
+ def test_merging_removes_rhs_bind_parameters
+ left = Post.where(id: Arel::Nodes::BindParam.new('?'))
+ column = Post.columns_hash['id']
+ left.bind_values += [[column, 20]]
+ right = Post.where(id: 10)
+
+ merged = left.merge(right)
+ assert_equal [], merged.bind_values
+ end
+
+ def test_merging_keeps_lhs_bind_parameters
+ column = Post.columns_hash['id']
+ binds = [[column, 20]]
+
+ right = Post.where(id: Arel::Nodes::BindParam.new('?'))
+ right.bind_values += binds
+ left = Post.where(id: 10)
+
+ merged = left.merge(right)
+ assert_equal binds, merged.bind_values
+ end
+
+ def test_merging_reorders_bind_params
+ post = Post.first
+ id_column = Post.columns_hash['id']
+ title_column = Post.columns_hash['title']
+
+ bv = Post.connection.substitute_at id_column, 0
+
+ right = Post.where(id: bv)
+ right.bind_values += [[id_column, post.id]]
+
+ left = Post.where(title: bv)
+ left.bind_values += [[title_column, post.title]]
+
+ merged = left.merge(right)
+ assert_equal post, merged.first
+ end
+end
+
+class MergingDifferentRelationsTest < ActiveRecord::TestCase
+ fixtures :posts, :authors
+
+ test "merging where relations" do
+ hello_by_bob = Post.where(body: "hello").joins(:author).
+ merge(Author.where(name: "Bob")).order("posts.id").pluck("posts.id")
+
+ assert_equal [posts(:misc_by_bob).id,
+ posts(:other_by_bob).id], hello_by_bob
+ end
+
+ test "merging order relations" do
+ posts_by_author_name = Post.limit(3).joins(:author).
+ merge(Author.order(:name)).pluck("authors.name")
+
+ assert_equal ["Bob", "Bob", "David"], posts_by_author_name
+
+ posts_by_author_name = Post.limit(3).joins(:author).
+ merge(Author.order("name")).pluck("authors.name")
+
+ assert_equal ["Bob", "Bob", "David"], posts_by_author_name
+ end
+
+ test "merging order relations (using a hash argument)" do
+ posts_by_author_name = Post.limit(4).joins(:author).
+ merge(Author.order(name: :desc)).pluck("authors.name")
+
+ assert_equal ["Mary", "Mary", "Mary", "David"], posts_by_author_name
+ end
+end
diff --git a/activerecord/test/cases/relation/mutation_test.rb b/activerecord/test/cases/relation/mutation_test.rb
new file mode 100644
index 0000000000..7cb2a19bee
--- /dev/null
+++ b/activerecord/test/cases/relation/mutation_test.rb
@@ -0,0 +1,144 @@
+require 'cases/helper'
+require 'models/post'
+
+module ActiveRecord
+ class RelationMutationTest < ActiveSupport::TestCase
+ class FakeKlass < Struct.new(:table_name, :name)
+ extend ActiveRecord::Delegation::DelegateCache
+ inherited self
+
+ def connection
+ Post.connection
+ end
+
+ def relation_delegate_class(klass)
+ self.class.relation_delegate_class(klass)
+ end
+ end
+
+ def relation
+ @relation ||= Relation.new FakeKlass.new('posts'), Post.arel_table
+ end
+
+ (Relation::MULTI_VALUE_METHODS - [:references, :extending, :order, :unscope]).each do |method|
+ test "##{method}!" do
+ assert relation.public_send("#{method}!", :foo).equal?(relation)
+ assert_equal [:foo], relation.public_send("#{method}_values")
+ end
+ end
+
+ test '#order!' do
+ assert relation.order!('name ASC').equal?(relation)
+ assert_equal ['name ASC'], relation.order_values
+ end
+
+ test '#order! with symbol prepends the table name' do
+ assert relation.order!(:name).equal?(relation)
+ node = relation.order_values.first
+ assert node.ascending?
+ assert_equal :name, node.expr.name
+ assert_equal "posts", node.expr.relation.name
+ end
+
+ 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
+ end
+
+ test '#references!' do
+ assert relation.references!(:foo).equal?(relation)
+ assert relation.references_values.include?('foo')
+ end
+
+ test 'extending!' do
+ mod, mod2 = Module.new, Module.new
+
+ assert relation.extending!(mod).equal?(relation)
+ assert_equal [mod], relation.extending_values
+ assert relation.is_a?(mod)
+
+ relation.extending!(mod2)
+ assert_equal [mod, mod2], relation.extending_values
+ end
+
+ test 'extending! with empty args' do
+ relation.extending!
+ assert_equal [], relation.extending_values
+ end
+
+ (Relation::SINGLE_VALUE_METHODS - [:from, :lock, :reordering, :reverse_order, :create_with]).each do |method|
+ test "##{method}!" do
+ assert relation.public_send("#{method}!", :foo).equal?(relation)
+ assert_equal :foo, relation.public_send("#{method}_value")
+ end
+ end
+
+ test '#from!' do
+ assert relation.from!('foo').equal?(relation)
+ assert_equal ['foo', nil], relation.from_value
+ end
+
+ test '#lock!' do
+ assert relation.lock!('foo').equal?(relation)
+ assert_equal 'foo', relation.lock_value
+ end
+
+ test '#reorder!' do
+ relation = self.relation.order('foo')
+
+ assert relation.reorder!('bar').equal?(relation)
+ assert_equal ['bar'], relation.order_values
+ assert relation.reordering_value
+ end
+
+ test '#reorder! with symbol prepends the table name' do
+ assert relation.reorder!(:name).equal?(relation)
+ node = relation.order_values.first
+
+ assert node.ascending?
+ assert_equal :name, node.expr.name
+ assert_equal "posts", node.expr.relation.name
+ end
+
+ test 'reverse_order!' do
+ assert relation.reverse_order!.equal?(relation)
+ assert relation.reverse_order_value
+ relation.reverse_order!
+ assert !relation.reverse_order_value
+ end
+
+ test 'create_with!' do
+ assert relation.create_with!(foo: 'bar').equal?(relation)
+ assert_equal({foo: 'bar'}, relation.create_with_value)
+ end
+
+ test 'test_merge!' do
+ assert relation.merge!(where: :foo).equal?(relation)
+ assert_equal [:foo], relation.where_values
+ end
+
+ test 'merge with a proc' do
+ assert_equal [:foo], relation.merge(-> { where(:foo) }).where_values
+ end
+
+ test 'none!' do
+ assert relation.none!.equal?(relation)
+ assert_equal [NullRelation], relation.extending_values
+ assert relation.is_a?(NullRelation)
+ end
+
+ test 'distinct!' do
+ relation.distinct! :foo
+ assert_equal :foo, relation.distinct_value
+ assert_equal :foo, relation.uniq_value # deprecated access
+ end
+
+ test 'uniq! was replaced by distinct!' do
+ relation.uniq! :foo
+ assert_equal :foo, relation.distinct_value
+ assert_equal :foo, relation.uniq_value # deprecated access
+ end
+ end
+end
diff --git a/activerecord/test/cases/relation/predicate_builder_test.rb b/activerecord/test/cases/relation/predicate_builder_test.rb
new file mode 100644
index 0000000000..14a8d97d36
--- /dev/null
+++ b/activerecord/test/cases/relation/predicate_builder_test.rb
@@ -0,0 +1,14 @@
+require "cases/helper"
+require 'models/topic'
+
+module ActiveRecord
+ class PredicateBuilderTest < ActiveRecord::TestCase
+ def test_registering_new_handlers
+ PredicateBuilder.register_handler(Regexp, proc do |column, value|
+ Arel::Nodes::InfixOperation.new('~', column, value.source)
+ end)
+
+ assert_match %r{["`]topics["`].["`]title["`] ~ 'rails'}i, Topic.where(title: /rails/).to_sql
+ end
+ end
+end
diff --git a/activerecord/test/cases/relation/where_chain_test.rb b/activerecord/test/cases/relation/where_chain_test.rb
index 92d1e013e8..fd2420cb88 100644
--- a/activerecord/test/cases/relation/where_chain_test.rb
+++ b/activerecord/test/cases/relation/where_chain_test.rb
@@ -23,6 +23,12 @@ module ActiveRecord
assert_equal([expected], relation.where_values)
end
+ def test_not_with_nil
+ assert_raise ArgumentError do
+ Post.where.not(nil)
+ end
+ end
+
def test_not_in
expected = Arel::Nodes::NotIn.new(Post.arel_table[@name], %w[hello goodbye])
relation = Post.where.not(title: %w[hello goodbye])
@@ -76,5 +82,35 @@ module ActiveRecord
expected = Arel::Nodes::NotEqual.new(Post.arel_table[@name], 'ruby on rails')
assert_equal(expected, relation.where_values[1])
end
+
+ def test_rewhere_with_one_condition
+ relation = Post.where(title: 'hello').where(title: 'world').rewhere(title: 'alone')
+
+ expected = Arel::Nodes::Equality.new(Post.arel_table[@name], 'alone')
+ assert_equal 1, relation.where_values.size
+ assert_equal expected, relation.where_values.first
+ end
+
+ def test_rewhere_with_multiple_overwriting_conditions
+ relation = Post.where(title: 'hello').where(body: 'world').rewhere(title: 'alone', body: 'again')
+
+ title_expected = Arel::Nodes::Equality.new(Post.arel_table['title'], 'alone')
+ body_expected = Arel::Nodes::Equality.new(Post.arel_table['body'], 'again')
+
+ assert_equal 2, relation.where_values.size
+ assert_equal title_expected, relation.where_values.first
+ assert_equal body_expected, relation.where_values.second
+ end
+
+ def test_rewhere_with_one_overwriting_condition_and_one_unrelated
+ relation = Post.where(title: 'hello').where(body: 'world').rewhere(title: 'alone')
+
+ title_expected = Arel::Nodes::Equality.new(Post.arel_table['title'], 'alone')
+ body_expected = Arel::Nodes::Equality.new(Post.arel_table['body'], 'world')
+
+ assert_equal 2, relation.where_values.size
+ assert_equal body_expected, relation.where_values.first
+ assert_equal title_expected, relation.where_values.second
+ end
end
end
diff --git a/activerecord/test/cases/relation/where_test.rb b/activerecord/test/cases/relation/where_test.rb
index d333be3560..937f226b1d 100644
--- a/activerecord/test/cases/relation/where_test.rb
+++ b/activerecord/test/cases/relation/where_test.rb
@@ -24,6 +24,10 @@ module ActiveRecord
}
end
+ def test_rewhere_on_root
+ assert_equal posts(:welcome), Post.rewhere(title: 'Welcome to the weblog').first
+ end
+
def test_belongs_to_shallow_where
author = Author.new
author.id = 1
@@ -31,6 +35,21 @@ module ActiveRecord
assert_equal Post.where(author_id: 1).to_sql, Post.where(author: author).to_sql
end
+ def test_belongs_to_nil_where
+ assert_equal Post.where(author_id: nil).to_sql, Post.where(author: nil).to_sql
+ end
+
+ def test_belongs_to_array_value_where
+ assert_equal Post.where(author_id: [1,2]).to_sql, Post.where(author: [1,2]).to_sql
+ end
+
+ def test_belongs_to_nested_relation_where
+ expected = Post.where(author_id: Author.where(id: [1,2])).to_sql
+ actual = Post.where(author: Author.where(id: [1,2])).to_sql
+
+ assert_equal expected, actual
+ end
+
def test_belongs_to_nested_where
parent = Comment.new
parent.id = 1
@@ -51,6 +70,25 @@ module ActiveRecord
assert_equal expected.to_sql, actual.to_sql
end
+ def test_polymorphic_nested_array_where
+ treasure = Treasure.new
+ treasure.id = 1
+ hidden = HiddenTreasure.new
+ hidden.id = 2
+
+ expected = PriceEstimate.where(estimate_of_type: 'Treasure', estimate_of_id: [treasure, hidden])
+ actual = PriceEstimate.where(estimate_of: [treasure, hidden])
+
+ assert_equal expected.to_sql, actual.to_sql
+ end
+
+ def test_polymorphic_nested_relation_where
+ expected = PriceEstimate.where(estimate_of_type: 'Treasure', estimate_of_id: Treasure.where(id: [1,2]))
+ actual = PriceEstimate.where(estimate_of: Treasure.where(id: [1,2]))
+
+ assert_equal expected.to_sql, actual.to_sql
+ end
+
def test_polymorphic_sti_shallow_where
treasure = HiddenTreasure.new
treasure.id = 1
@@ -81,6 +119,31 @@ module ActiveRecord
assert_equal expected.to_sql, actual.to_sql
end
+ def test_decorated_polymorphic_where
+ treasure_decorator = Struct.new(:model) do
+ def self.method_missing(method, *args, &block)
+ Treasure.send(method, *args, &block)
+ end
+
+ def is_a?(klass)
+ model.is_a?(klass)
+ end
+
+ def method_missing(method, *args, &block)
+ model.send(method, *args, &block)
+ end
+ end
+
+ treasure = Treasure.new
+ treasure.id = 1
+ decorated_treasure = treasure_decorator.new(treasure)
+
+ expected = PriceEstimate.where(estimate_of_type: 'Treasure', estimate_of_id: 1)
+ actual = PriceEstimate.where(estimate_of: decorated_treasure)
+
+ assert_equal expected.to_sql, actual.to_sql
+ end
+
def test_aliased_attribute
expected = Topic.where(heading: 'The First Topic')
actual = Topic.where(title: 'The First Topic')
diff --git a/activerecord/test/cases/relation_test.rb b/activerecord/test/cases/relation_test.rb
index 03c7009568..15611656fd 100644
--- a/activerecord/test/cases/relation_test.rb
+++ b/activerecord/test/cases/relation_test.rb
@@ -9,6 +9,13 @@ module ActiveRecord
fixtures :posts, :comments, :authors
class FakeKlass < Struct.new(:table_name, :name)
+ extend ActiveRecord::Delegation::DelegateCache
+
+ inherited self
+
+ def self.connection
+ Post.connection
+ end
end
def test_construction
@@ -73,8 +80,8 @@ module ActiveRecord
end
def test_table_name_delegates_to_klass
- relation = Relation.new FakeKlass.new('foo'), :b
- assert_equal 'foo', relation.table_name
+ relation = Relation.new FakeKlass.new('posts'), :b
+ assert_equal 'posts', relation.table_name
end
def test_scope_for_create
@@ -108,6 +115,12 @@ module ActiveRecord
assert_equal({}, relation.scope_for_create)
end
+ def test_bad_constants_raise_errors
+ assert_raises(NameError) do
+ ActiveRecord::Relation::HelloWorld
+ end
+ end
+
def test_empty_eager_loading?
relation = Relation.new FakeKlass, :b
assert !relation.eager_loading?
@@ -168,141 +181,55 @@ module ActiveRecord
end
test 'merging a hash interpolates conditions' do
- klass = stub_everything
- klass.stubs(:sanitize_sql).with(['foo = ?', 'bar']).returns('foo = bar')
+ klass = Class.new(FakeKlass) do
+ def self.sanitize_sql(args)
+ raise unless args == ['foo = ?', 'bar']
+ 'foo = bar'
+ end
+ end
relation = Relation.new(klass, :b)
relation.merge!(where: ['foo = ?', 'bar'])
assert_equal ['foo = bar'], relation.where_values
end
- def test_relation_merging_with_merged_joins
+ def test_merging_readonly_false
+ relation = Relation.new FakeKlass, :b
+ readonly_false_relation = relation.readonly(false)
+ # test merging in both directions
+ assert_equal false, relation.merge(readonly_false_relation).readonly_value
+ assert_equal false, readonly_false_relation.merge(relation).readonly_value
+ end
+
+ def test_relation_merging_with_merged_joins_as_symbols
special_comments_with_ratings = SpecialComment.joins(:ratings)
posts_with_special_comments_with_ratings = Post.group("posts.id").joins(:special_comments).merge(special_comments_with_ratings)
assert_equal 3, authors(:david).posts.merge(posts_with_special_comments_with_ratings).count.length
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"
-
- post = Post.select("'title' as post_title").first
- assert_equal false, post.respond_to?(:title), "post should not respond_to?(:body) since invoking it raises exception"
- end
-
- end
-
- class RelationMutationTest < ActiveSupport::TestCase
- class FakeKlass < Struct.new(:table_name, :name)
- def quoted_table_name
- %{"#{table_name}"}
- end
- end
-
- def relation
- @relation ||= Relation.new FakeKlass.new('posts'), :b
- end
-
- (Relation::MULTI_VALUE_METHODS - [:references, :extending, :order]).each do |method|
- test "##{method}!" do
- assert relation.public_send("#{method}!", :foo).equal?(relation)
- assert_equal [:foo], relation.public_send("#{method}_values")
- end
- end
-
- test "#order!" do
- assert relation.order!('name ASC').equal?(relation)
- assert_equal ['name ASC'], relation.order_values
- end
-
- test "#order! with symbol prepends the table name" do
- assert relation.order!(:name).equal?(relation)
- assert_equal ['"posts".name ASC'], relation.order_values
- end
-
- test '#references!' do
- assert relation.references!(:foo).equal?(relation)
- assert relation.references_values.include?('foo')
- end
-
- test 'extending!' do
- mod, mod2 = Module.new, Module.new
-
- assert relation.extending!(mod).equal?(relation)
- assert_equal [mod], relation.extending_values
- assert relation.is_a?(mod)
-
- relation.extending!(mod2)
- assert_equal [mod, mod2], relation.extending_values
- end
-
- test 'extending! with empty args' do
- relation.extending!
- assert_equal [], relation.extending_values
- end
-
- (Relation::SINGLE_VALUE_METHODS - [:from, :lock, :reordering, :reverse_order, :create_with]).each do |method|
- test "##{method}!" do
- assert relation.public_send("#{method}!", :foo).equal?(relation)
- assert_equal :foo, relation.public_send("#{method}_value")
- end
- end
-
- test '#from!' do
- assert relation.from!('foo').equal?(relation)
- assert_equal ['foo', nil], relation.from_value
- end
-
- test '#lock!' do
- assert relation.lock!('foo').equal?(relation)
- assert_equal 'foo', relation.lock_value
- end
+ def test_relation_merging_with_joins_as_join_dependency_pick_proper_parent
+ post = Post.create!(title: "haha", body: "huhu")
+ comment = post.comments.create!(body: "hu")
+ 3.times { comment.ratings.create! }
- test '#reorder!' do
- relation = self.relation.order('foo')
+ relation = Post.joins(:comments).merge Comment.joins(:ratings)
- assert relation.reorder!('bar').equal?(relation)
- assert_equal ['bar'], relation.order_values
- assert relation.reordering_value
+ assert_equal 3, relation.where(id: post.id).pluck(:id).size
end
- test 'reverse_order!' do
- assert relation.reverse_order!.equal?(relation)
- assert relation.reverse_order_value
- relation.reverse_order!
- assert !relation.reverse_order_value
- end
-
- test 'create_with!' do
- assert relation.create_with!(foo: 'bar').equal?(relation)
- assert_equal({foo: 'bar'}, relation.create_with_value)
- end
-
- test 'merge!' do
- assert relation.merge!(where: :foo).equal?(relation)
- assert_equal [:foo], relation.where_values
- end
-
- test 'merge with a proc' do
- assert_equal [:foo], relation.merge(-> { where(:foo) }).where_values
- end
-
- test 'none!' do
- assert relation.none!.equal?(relation)
- assert_equal [NullRelation], relation.extending_values
- assert relation.is_a?(NullRelation)
- 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"
- test "distinct!" do
- relation.distinct! :foo
- assert_equal :foo, relation.distinct_value
- assert_equal :foo, relation.uniq_value # deprecated access
+ silence_warnings { post = Post.select("'title' as post_title").first }
+ assert_equal false, post.respond_to?(:title), "post should not respond_to?(:body) since invoking it raises exception"
end
- test "uniq! was replaced by distinct!" do
- relation.uniq! :foo
- assert_equal :foo, relation.distinct_value
- assert_equal :foo, relation.uniq_value # deprecated access
+ 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
+ posts_with_special_comments_with_ratings = Post.group("posts.id").joins(:special_comments).merge(special_comments_with_ratings)
+ assert_equal 3, authors(:david).posts.merge(posts_with_special_comments_with_ratings).count.length
end
end
end
diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb
index b64ff13d29..031da8e6d6 100644
--- a/activerecord/test/cases/relations_test.rb
+++ b/activerecord/test/cases/relations_test.rb
@@ -42,6 +42,11 @@ class RelationTest < ActiveRecord::TestCase
end
def test_two_scopes_with_includes_should_not_drop_any_include
+ # heat habtm cache
+ car = Car.incl_engines.incl_tyres.first
+ car.tyres.length
+ car.engines.length
+
car = Car.incl_engines.incl_tyres.first
assert_no_queries { car.tyres.length }
assert_no_queries { car.engines.length }
@@ -139,6 +144,18 @@ class RelationTest < ActiveRecord::TestCase
assert_equal relation.to_a, Topic.select('a.*').from(relation, :a).to_a
end
+ def test_finding_with_subquery_with_binds
+ relation = Post.first.comments
+ assert_equal relation.to_a, Comment.select('*').from(relation).to_a
+ assert_equal relation.to_a, Comment.select('subquery.*').from(relation).to_a
+ assert_equal relation.to_a, Comment.select('a.*').from(relation, :a).to_a
+ end
+
+ def test_finding_with_subquery_without_select
+ relation = Topic.where(:approved => true)
+ assert_equal relation.to_a, Topic.from(relation).to_a
+ end
+
def test_finding_with_conditions
assert_equal ["David"], Author.where(:name => 'David').map(&:name)
assert_equal ['Mary'], Author.where(["name = ?", 'Mary']).map(&:name)
@@ -170,6 +187,10 @@ class RelationTest < ActiveRecord::TestCase
assert_equal topics(:fourth).title, topics.first.title
end
+ def test_order_with_hash_and_symbol_generates_the_same_sql
+ assert_equal Topic.order(:id).to_sql, Topic.order(:id => :asc).to_sql
+ end
+
def test_raising_exception_on_invalid_hash_params
assert_raise(ArgumentError) { Topic.order(:name, "id DESC", :id => :DeSc) }
end
@@ -180,7 +201,7 @@ class RelationTest < ActiveRecord::TestCase
end
def test_finding_with_order_concatenated
- topics = Topic.order('title').order('author_name')
+ topics = Topic.order('author_name').order('title')
assert_equal 4, topics.to_a.size
assert_equal topics(:fourth).title, topics.first.title
end
@@ -258,7 +279,7 @@ class RelationTest < ActiveRecord::TestCase
def test_none_chained_to_methods_firing_queries_straight_to_db
assert_no_queries do
- assert_equal [], Developer.none.pluck(:id) # => uses select_all
+ assert_equal [], Developer.none.pluck(:id, :name)
assert_equal 0, Developer.none.delete_all
assert_equal 0, Developer.none.update_all(:name => 'David')
assert_equal 0, Developer.none.delete(1)
@@ -278,8 +299,9 @@ class RelationTest < ActiveRecord::TestCase
def test_null_relation_calculations_methods
assert_no_queries do
- assert_equal 0, Developer.none.count
- assert_equal nil, Developer.none.calculate(:average, 'salary')
+ assert_equal 0, Developer.none.count
+ assert_equal 0, Developer.none.calculate(:count, nil, {})
+ assert_equal nil, Developer.none.calculate(:average, 'salary')
end
end
@@ -288,6 +310,10 @@ class RelationTest < ActiveRecord::TestCase
assert_equal({}, Developer.none.where_values_hash)
end
+ def test_null_relation_where_values_hash
+ assert_equal({ 'salary' => 100_000 }, Developer.none.where(salary: 100_000).where_values_hash)
+ end
+
def test_joins_with_nil_argument
assert_nothing_raised { DependentFirm.joins(nil).first }
end
@@ -367,7 +393,7 @@ class RelationTest < ActiveRecord::TestCase
def test_respond_to_dynamic_finders
relation = Topic.all
- ["find_by_title", "find_by_title_and_author_name", "find_or_create_by_title", "find_or_initialize_by_title_and_author_name"].each do |method|
+ ["find_by_title", "find_by_title_and_author_name"].each do |method|
assert_respond_to relation, method, "Topic.all should respond to #{method.inspect}"
end
end
@@ -465,6 +491,14 @@ class RelationTest < ActiveRecord::TestCase
assert_equal Developer.where(name: 'David').map(&:id).sort, developers
end
+ def test_includes_with_select
+ query = Post.select('comments_count AS ranking').order('ranking').includes(:comments)
+ .where(comments: { id: 1 })
+
+ assert_equal ['comments_count AS ranking'], query.select_values
+ assert_equal 1, query.to_a.size
+ end
+
def test_loading_with_one_association
posts = Post.preload(:comments)
post = posts.find { |p| p.id == 1 }
@@ -480,6 +514,14 @@ class RelationTest < ActiveRecord::TestCase
assert_equal Post.find(1).last_comment, post.last_comment
end
+ def test_to_sql_on_eager_join
+ expected = assert_sql {
+ Post.eager_load(:last_comment).order('comments.id DESC').to_a
+ }.first
+ actual = Post.eager_load(:last_comment).order('comments.id DESC').to_sql
+ assert_equal expected, actual
+ end
+
def test_loading_with_one_association_with_non_preload
posts = Post.eager_load(:last_comment).order('comments.id DESC')
post = posts.find { |p| p.id == 1 }
@@ -597,6 +639,36 @@ class RelationTest < ActiveRecord::TestCase
relation = Author.where(:id => Author.where(:id => david.id))
assert_equal [david], relation.to_a
}
+
+ assert_queries(1) {
+ relation = Author.where('id in (?)', Author.where(id: david).select(:id))
+ assert_equal [david], relation.to_a
+ }
+
+ assert_queries(1) do
+ relation = Author.where('id in (:author_ids)', author_ids: Author.where(id: david).select(:id))
+ assert_equal [david], relation.to_a
+ end
+ end
+
+ def test_find_all_using_where_with_relation_with_bound_values
+ david = authors(:david)
+ davids_posts = david.posts.order(:id).to_a
+
+ assert_queries(1) do
+ relation = Post.where(id: david.posts.select(:id))
+ assert_equal davids_posts, relation.order(:id).to_a
+ end
+
+ assert_queries(1) do
+ relation = Post.where('id in (?)', david.posts.select(:id))
+ assert_equal davids_posts, relation.order(:id).to_a, 'should process Relation as bind variables'
+ end
+
+ assert_queries(1) do
+ relation = Post.where('id in (:post_ids)', post_ids: david.posts.select(:id))
+ assert_equal davids_posts, relation.order(:id).to_a, 'should process Relation as named bind variables'
+ end
end
def test_find_all_using_where_with_relation_and_alternate_primary_key
@@ -707,75 +779,6 @@ class RelationTest < ActiveRecord::TestCase
assert_raises(ArgumentError) { Developer.select }
end
- def test_relation_merging
- devs = Developer.where("salary >= 80000").merge(Developer.limit(2)).merge(Developer.order('id ASC').where("id < 3"))
- assert_equal [developers(:david), developers(:jamis)], devs.to_a
-
- dev_with_count = Developer.limit(1).merge(Developer.order('id DESC')).merge(Developer.select('developers.*'))
- assert_equal [developers(:poor_jamis)], dev_with_count.to_a
- end
-
- def test_relation_to_sql
- sql = Post.connection.unprepared_statement do
- Post.first.comments.to_sql
- end
- assert_no_match(/\?/, sql)
- end
-
- def test_relation_merging_with_arel_equalities_keeps_last_equality
- devs = Developer.where(Developer.arel_table[:salary].eq(80000)).merge(
- Developer.where(Developer.arel_table[:salary].eq(9000))
- )
- assert_equal [developers(:poor_jamis)], devs.to_a
- end
-
- def test_relation_merging_with_arel_equalities_keeps_last_equality_with_non_attribute_left_hand
- salary_attr = Developer.arel_table[:salary]
- devs = Developer.where(
- Arel::Nodes::NamedFunction.new('abs', [salary_attr]).eq(80000)
- ).merge(
- Developer.where(
- Arel::Nodes::NamedFunction.new('abs', [salary_attr]).eq(9000)
- )
- )
- assert_equal [developers(:poor_jamis)], devs.to_a
- end
-
- def test_relation_merging_with_eager_load
- relations = []
- relations << Post.order('comments.id DESC').merge(Post.eager_load(:last_comment)).merge(Post.all)
- relations << Post.eager_load(:last_comment).merge(Post.order('comments.id DESC')).merge(Post.all)
-
- relations.each do |posts|
- post = posts.find { |p| p.id == 1 }
- assert_equal Post.find(1).last_comment, post.last_comment
- end
- end
-
- def test_relation_merging_with_locks
- devs = Developer.lock.where("salary >= 80000").order("id DESC").merge(Developer.limit(2))
- assert devs.locked.present?
- end
-
- def test_relation_merging_with_preload
- [Post.all.merge(Post.preload(:author)), Post.preload(:author).merge(Post.all)].each do |posts|
- assert_queries(2) { assert posts.first.author }
- end
- end
-
- def test_relation_merging_with_joins
- comments = Comment.joins(:post).where(:body => 'Thank you for the welcome').merge(Post.where(:body => 'Such a lovely day'))
- assert_equal 1, comments.count
- end
-
- def test_relation_merging_with_association
- assert_queries(2) do # one for loading post, and another one merged query
- post = Post.where(:body => 'Such a lovely day').first
- comments = Comment.where(:body => 'Thank you for the welcome').merge(post.comments)
- assert_equal 1, comments.count
- end
- end
-
def test_count
posts = Post.all
@@ -1174,20 +1177,20 @@ class RelationTest < ActiveRecord::TestCase
end
def test_default_scope_order_with_scope_order
- assert_equal 'honda', CoolCar.order_using_new_style.limit(1).first.name
- assert_equal 'honda', FastCar.order_using_new_style.limit(1).first.name
+ assert_equal 'zyke', CoolCar.order_using_new_style.limit(1).first.name
+ assert_equal 'zyke', FastCar.order_using_new_style.limit(1).first.name
end
def test_order_using_scoping
car1 = CoolCar.order('id DESC').scoping do
- CoolCar.all.merge!(:order => 'id asc').first
+ CoolCar.all.merge!(order: 'id asc').first
end
- assert_equal 'honda', car1.name
+ assert_equal 'zyke', car1.name
car2 = FastCar.order('id DESC').scoping do
- FastCar.all.merge!(:order => 'id asc').first
+ FastCar.all.merge!(order: 'id asc').first
end
- assert_equal 'honda', car2.name
+ assert_equal 'zyke', car2.name
end
def test_unscoped_block_style
@@ -1207,33 +1210,12 @@ class RelationTest < ActiveRecord::TestCase
assert_equal "id", Post.all.primary_key
end
- def test_eager_loading_with_conditions_on_joins
- scope = Post.includes(:comments)
-
- # This references the comments table, and so it should cause the comments to be eager
- # loaded via a JOIN, rather than by subsequent queries.
- scope = scope.joins(
- Post.arel_table.create_join(
- Post.arel_table,
- Post.arel_table.create_on(Comment.arel_table[:id].eq(3))
- )
- )
-
+ def test_disable_implicit_join_references_is_deprecated
assert_deprecated do
- assert scope.eager_loading?
+ ActiveRecord::Base.disable_implicit_join_references = true
end
end
- def test_turn_off_eager_loading_with_conditions_on_joins
- original_value = ActiveRecord::Base.disable_implicit_join_references
- ActiveRecord::Base.disable_implicit_join_references = true
-
- scope = Topic.where(author_email_address: 'my.example@gmail.com').includes(:replies)
- assert_not scope.eager_loading?
- ensure
- ActiveRecord::Base.disable_implicit_join_references = original_value
- end
-
def test_ordering_with_extra_spaces
assert_equal authors(:david), Author.order('id DESC , name DESC').last
end
@@ -1352,6 +1334,24 @@ class RelationTest < ActiveRecord::TestCase
assert_equal [], scope.references_values
end
+ def test_automatically_added_reorder_references
+ scope = Post.reorder('comments.body')
+ assert_equal %w(comments), scope.references_values
+
+ scope = Post.reorder('comments.body', 'yaks.body')
+ assert_equal %w(comments yaks), scope.references_values
+
+ # Don't infer yaks, let's not go down that road again...
+ scope = Post.reorder('comments.body, yaks.body')
+ assert_equal %w(comments), scope.references_values
+
+ scope = Post.reorder('comments.body asc')
+ assert_equal %w(comments), scope.references_values
+
+ scope = Post.reorder('foo(comments.body)')
+ assert_equal [], scope.references_values
+ end
+
def test_presence
topics = Topic.all
@@ -1511,29 +1511,6 @@ class RelationTest < ActiveRecord::TestCase
assert !Post.all.respond_to?(:by_lifo)
end
- class OMGTopic < ActiveRecord::Base
- self.table_name = 'topics'
-
- def self.__omg__
- "omgtopic"
- end
- end
-
- test "delegations do not clash across classes" do
- begin
- class ::Array
- def __omg__
- "array"
- end
- end
-
- assert_equal "array", Topic.all.__omg__
- assert_equal "omgtopic", OMGTopic.all.__omg__
- ensure
- Array.send(:remove_method, :__omg__)
- end
- end
-
test "merge collapses wheres from the LHS only" do
left = Post.where(title: "omg").where(comments_count: 1)
right = Post.where(title: "wtf").where(title: "bbq")
@@ -1568,4 +1545,21 @@ class RelationTest < ActiveRecord::TestCase
merged = left.merge(right)
assert_equal binds, merged.bind_values
end
+
+ def test_merging_reorders_bind_params
+ post = Post.first
+ id_column = Post.columns_hash['id']
+ title_column = Post.columns_hash['title']
+
+ bv = Post.connection.substitute_at id_column, 0
+
+ right = Post.where(id: bv)
+ right.bind_values += [[id_column, post.id]]
+
+ left = Post.where(title: bv)
+ left.bind_values += [[title_column, post.title]]
+
+ merged = left.merge(right)
+ assert_equal post, merged.first
+ end
end
diff --git a/activerecord/test/cases/result_test.rb b/activerecord/test/cases/result_test.rb
new file mode 100644
index 0000000000..b6c583dbf5
--- /dev/null
+++ b/activerecord/test/cases/result_test.rb
@@ -0,0 +1,32 @@
+require "cases/helper"
+
+module ActiveRecord
+ class ResultTest < ActiveRecord::TestCase
+ def result
+ Result.new(['col_1', 'col_2'], [
+ ['row 1 col 1', 'row 1 col 2'],
+ ['row 2 col 1', 'row 2 col 2']
+ ])
+ end
+
+ def test_to_hash_returns_row_hashes
+ assert_equal [
+ {'col_1' => 'row 1 col 1', 'col_2' => 'row 1 col 2'},
+ {'col_1' => 'row 2 col 1', 'col_2' => 'row 2 col 2'}
+ ], result.to_hash
+ end
+
+ def test_each_with_block_returns_row_hashes
+ result.each do |row|
+ assert_equal ['col_1', 'col_2'], row.keys
+ end
+ end
+
+ def test_each_without_block_returns_an_enumerator
+ result.each.with_index do |row, index|
+ assert_equal ['col_1', 'col_2'], row.keys
+ assert_kind_of Integer, index
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/sanitize_test.rb b/activerecord/test/cases/sanitize_test.rb
index 082570c55b..766b2ff2ef 100644
--- a/activerecord/test/cases/sanitize_test.rb
+++ b/activerecord/test/cases/sanitize_test.rb
@@ -1,5 +1,7 @@
require "cases/helper"
require 'models/binary'
+require 'models/author'
+require 'models/post'
class SanitizeTest < ActiveRecord::TestCase
def setup
@@ -9,7 +11,7 @@ class SanitizeTest < ActiveRecord::TestCase
quoted_bambi = ActiveRecord::Base.connection.quote("Bambi")
quoted_column_name = ActiveRecord::Base.connection.quote_column_name("name")
quoted_table_name = ActiveRecord::Base.connection.quote_table_name("adorable_animals")
- expected_value = "#{quoted_table_name}.#{quoted_column_name} = #{quoted_bambi}"
+ expected_value = "#{quoted_table_name}.#{quoted_column_name} = #{quoted_bambi}"
assert_equal expected_value, Binary.send(:sanitize_sql_hash, {adorable_animals: {name: 'Bambi'}})
end
@@ -31,4 +33,17 @@ class SanitizeTest < ActiveRecord::TestCase
assert_equal "name=#{quoted_bambi_and_thumper}", Binary.send(:sanitize_sql_array, ["name=?", "Bambi\nand\nThumper"])
assert_equal "name=#{quoted_bambi_and_thumper}", Binary.send(:sanitize_sql_array, ["name=?", "Bambi\nand\nThumper".mb_chars])
end
+
+ def test_sanitize_sql_array_handles_relations
+ david = Author.create!(name: 'David')
+ david_posts = david.posts.select(:id)
+
+ sub_query_pattern = /\(\bselect\b.*?\bwhere\b.*?\)/i
+
+ select_author_sql = Post.send(:sanitize_sql_array, ['id in (?)', david_posts])
+ assert_match(sub_query_pattern, select_author_sql, 'should sanitize `Relation` as subquery for bind variables')
+
+ select_author_sql = Post.send(:sanitize_sql_array, ['id in (:post_ids)', post_ids: david_posts])
+ assert_match(sub_query_pattern, select_author_sql, 'should sanitize `Relation` as subquery for named bind variables')
+ end
end
diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb
index a48ae1036f..741827446d 100644
--- a/activerecord/test/cases/schema_dumper_test.rb
+++ b/activerecord/test/cases/schema_dumper_test.rb
@@ -202,6 +202,11 @@ class SchemaDumperTest < ActiveRecord::TestCase
assert_match %r(primary_key: "movieid"), match[1], "non-standard primary key not preserved"
end
+ def test_schema_dump_should_use_false_as_default
+ output = standard_dump
+ assert_match %r{t\.boolean\s+"has_fun",.+default: false}, output
+ end
+
if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
def test_schema_dump_should_not_add_default_value_for_mysql_text_field
output = standard_dump
@@ -247,19 +252,20 @@ class SchemaDumperTest < ActiveRecord::TestCase
assert_match %r{t.integer\s+"bigint_default",\s+limit: 8,\s+default: 0}, output
end
- def test_schema_dump_includes_extensions
- connection = ActiveRecord::Base.connection
- skip unless connection.supports_extensions?
+ if ActiveRecord::Base.connection.supports_extensions?
+ def test_schema_dump_includes_extensions
+ connection = ActiveRecord::Base.connection
- connection.stubs(:extensions).returns(['hstore'])
- output = standard_dump
- assert_match "# These are extensions that must be enabled", output
- assert_match %r{enable_extension "hstore"}, output
+ connection.stubs(:extensions).returns(['hstore'])
+ output = standard_dump
+ assert_match "# These are extensions that must be enabled", output
+ assert_match %r{enable_extension "hstore"}, output
- connection.stubs(:extensions).returns([])
- output = standard_dump
- assert_no_match "# These are extensions that must be enabled", output
- assert_no_match %r{enable_extension}, output
+ connection.stubs(:extensions).returns([])
+ output = standard_dump
+ assert_no_match "# These are extensions that must be enabled", output
+ assert_no_match %r{enable_extension}, output
+ end
end
def test_schema_dump_includes_xml_shorthand_definition
@@ -299,7 +305,7 @@ class SchemaDumperTest < ActiveRecord::TestCase
def test_schema_dump_includes_uuid_shorthand_definition
output = standard_dump
- if %r{create_table "poistgresql_uuids"} =~ output
+ if %r{create_table "postgresql_uuids"} =~ output
assert_match %r{t.uuid "guid"}, output
end
end
diff --git a/activerecord/test/cases/scoping/default_scoping_test.rb b/activerecord/test/cases/scoping/default_scoping_test.rb
index 0f69443839..71754cf0a2 100644
--- a/activerecord/test/cases/scoping/default_scoping_test.rb
+++ b/activerecord/test/cases/scoping/default_scoping_test.rb
@@ -54,9 +54,14 @@ class DefaultScopingTest < ActiveRecord::TestCase
assert_equal 'Jamis', DeveloperCalledJamis.create!.name
end
- def test_default_scoping_with_threads
- 2.times do
- Thread.new { assert DeveloperOrderedBySalary.all.to_sql.include?('salary DESC') }.join
+ unless in_memory_db?
+ def test_default_scoping_with_threads
+ 2.times do
+ Thread.new {
+ assert DeveloperOrderedBySalary.all.to_sql.include?('salary DESC')
+ DeveloperOrderedBySalary.connection.close
+ }.join
+ end
end
end
@@ -79,7 +84,7 @@ class DefaultScopingTest < ActiveRecord::TestCase
end
def test_scope_overwrites_default
- expected = Developer.all.merge!(:order => ' name DESC, salary DESC').to_a.collect { |dev| dev.name }
+ expected = Developer.all.merge!(order: 'salary DESC, name DESC').to_a.collect { |dev| dev.name }
received = DeveloperOrderedBySalary.by_name.to_a.collect { |dev| dev.name }
assert_equal expected, received
end
@@ -91,14 +96,14 @@ class DefaultScopingTest < ActiveRecord::TestCase
end
def test_order_after_reorder_combines_orders
- expected = Developer.order('id DESC, name DESC').collect { |dev| [dev.name, dev.id] }
+ expected = Developer.order('name DESC, id DESC').collect { |dev| [dev.name, dev.id] }
received = Developer.order('name ASC').reorder('name DESC').order('id DESC').collect { |dev| [dev.name, dev.id] }
assert_equal expected, received
end
def test_unscope_overrides_default_scope
expected = Developer.all.collect { |dev| [dev.name, dev.id] }
- received = Developer.order('name ASC, id DESC').unscope(:order).collect { |dev| [dev.name, dev.id] }
+ received = DeveloperCalledJamis.unscope(:where).collect { |dev| [dev.name, dev.id] }
assert_equal expected, received
end
@@ -117,17 +122,25 @@ class DefaultScopingTest < ActiveRecord::TestCase
end
def test_unscope_with_where_attributes
- expected = Developer.order('salary DESC').collect { |dev| dev.name }
- received = DeveloperOrderedBySalary.where(name: 'David').unscope(where: :name).collect { |dev| dev.name }
+ expected = Developer.order('salary DESC').collect(&:name)
+ received = DeveloperOrderedBySalary.where(name: 'David').unscope(where: :name).collect(&:name)
assert_equal expected, received
- expected_2 = Developer.order('salary DESC').collect { |dev| dev.name }
- received_2 = DeveloperOrderedBySalary.select("id").where("name" => "Jamis").unscope({:where => :name}, :select).collect { |dev| dev.name }
+ expected_2 = Developer.order('salary DESC').collect(&:name)
+ received_2 = DeveloperOrderedBySalary.select("id").where("name" => "Jamis").unscope({:where => :name}, :select).collect(&:name)
assert_equal expected_2, received_2
- expected_3 = Developer.order('salary DESC').collect { |dev| dev.name }
- received_3 = DeveloperOrderedBySalary.select("id").where("name" => "Jamis").unscope(:select, :where).collect { |dev| dev.name }
+ expected_3 = Developer.order('salary DESC').collect(&:name)
+ received_3 = DeveloperOrderedBySalary.select("id").where("name" => "Jamis").unscope(:select, :where).collect(&:name)
assert_equal expected_3, received_3
+
+ expected_4 = Developer.order('salary DESC').collect(&:name)
+ received_4 = DeveloperOrderedBySalary.where.not("name" => "Jamis").unscope(where: :name).collect(&:name)
+ assert_equal expected_4, received_4
+
+ expected_5 = Developer.order('salary DESC').collect(&:name)
+ received_5 = DeveloperOrderedBySalary.where.not("name" => ["Jamis", "David"]).unscope(where: :name).collect(&:name)
+ assert_equal expected_5, received_5
end
def test_unscope_multiple_where_clauses
@@ -153,9 +166,8 @@ class DefaultScopingTest < ActiveRecord::TestCase
end
def test_order_to_unscope_reordering
- expected = DeveloperOrderedBySalary.all.collect { |dev| [dev.name, dev.id] }
- received = DeveloperOrderedBySalary.order('salary DESC, name ASC').reverse_order.unscope(:order).collect { |dev| [dev.name, dev.id] }
- assert_equal expected, received
+ scope = DeveloperOrderedBySalary.order('salary DESC, name ASC').reverse_order.unscope(:order)
+ assert !(scope.to_sql =~ /order/i)
end
def test_unscope_reverse_order
@@ -166,7 +178,7 @@ class DefaultScopingTest < ActiveRecord::TestCase
def test_unscope_select
expected = Developer.order('salary ASC').collect { |dev| dev.name }
- received = Developer.order('salary DESC').reverse_order.select(:name => "Jamis").unscope(:select).collect { |dev| dev.name }
+ received = Developer.order('salary DESC').reverse_order.select(:name).unscope(:select).collect { |dev| dev.name }
assert_equal expected, received
expected_2 = Developer.all.collect { |dev| dev.id }
@@ -250,9 +262,15 @@ class DefaultScopingTest < ActiveRecord::TestCase
end
end
+ def test_unscope_merging
+ merged = Developer.where(name: "Jamis").merge(Developer.unscope(:where))
+ assert merged.where_values.empty?
+ assert !merged.where(name: "Jon").where_values.empty?
+ end
+
def test_order_in_default_scope_should_not_prevail
- expected = Developer.all.merge!(:order => 'salary').to_a.collect { |dev| dev.salary }
- received = DeveloperOrderedBySalary.all.merge!(:order => 'salary').to_a.collect { |dev| dev.salary }
+ expected = Developer.all.merge!(order: 'salary desc').to_a.collect { |dev| dev.salary }
+ received = DeveloperOrderedBySalary.all.merge!(order: 'salary').to_a.collect { |dev| dev.salary }
assert_equal expected, received
end
@@ -350,21 +368,21 @@ class DefaultScopingTest < ActiveRecord::TestCase
assert_equal 1, DeveloperWithIncludes.where(:audit_logs => { :message => 'foo' }).count
end
- def test_default_scope_is_threadsafe
- if in_memory_db?
- skip "in memory db can't share a db between threads"
- end
-
- threads = []
- assert_not_equal 1, ThreadsafeDeveloper.unscoped.count
-
- threads << Thread.new do
- Thread.current[:long_default_scope] = true
- assert_equal 1, ThreadsafeDeveloper.all.to_a.count
- end
- threads << Thread.new do
- assert_equal 1, ThreadsafeDeveloper.all.to_a.count
+ unless in_memory_db?
+ def test_default_scope_is_threadsafe
+ threads = []
+ assert_not_equal 1, ThreadsafeDeveloper.unscoped.count
+
+ threads << Thread.new do
+ Thread.current[:long_default_scope] = true
+ assert_equal 1, ThreadsafeDeveloper.all.to_a.count
+ ThreadsafeDeveloper.connection.close
+ end
+ threads << Thread.new do
+ assert_equal 1, ThreadsafeDeveloper.all.to_a.count
+ ThreadsafeDeveloper.connection.close
+ end
+ threads.each(&:join)
end
- threads.each(&:join)
end
end
diff --git a/activerecord/test/cases/scoping/named_scoping_test.rb b/activerecord/test/cases/scoping/named_scoping_test.rb
index afe32af1d1..72c9787b84 100644
--- a/activerecord/test/cases/scoping/named_scoping_test.rb
+++ b/activerecord/test/cases/scoping/named_scoping_test.rb
@@ -60,11 +60,6 @@ class NamedScopingTest < ActiveRecord::TestCase
assert Topic.approved.respond_to?(:length)
end
- def test_respond_to_respects_include_private_parameter
- assert !Topic.approved.respond_to?(:tables_in_string)
- assert Topic.approved.respond_to?(:tables_in_string, true)
- end
-
def test_scopes_with_options_limit_finds_to_those_matching_the_criteria_specified
assert !Topic.all.merge!(:where => {:approved => true}).to_a.empty?
@@ -440,24 +435,13 @@ class NamedScopingTest < ActiveRecord::TestCase
end
end
- def test_eager_scopes_are_deprecated
- klass = Class.new(ActiveRecord::Base)
- klass.table_name = 'posts'
-
- assert_deprecated do
- klass.scope :welcome_2, klass.where(:id => posts(:welcome).id)
- end
- assert_equal [posts(:welcome).title], klass.welcome_2.map(&:title)
- end
-
- def test_eager_default_scope_relations_are_deprecated
+ def test_eager_default_scope_relations_are_remove
klass = Class.new(ActiveRecord::Base)
klass.table_name = 'posts'
- assert_deprecated do
+ assert_raises(ArgumentError) do
klass.send(:default_scope, klass.where(:id => posts(:welcome).id))
end
- assert_equal [posts(:welcome).title], klass.all.map(&:title)
end
def test_subclass_merges_scopes_properly
diff --git a/activerecord/test/cases/serialized_attribute_test.rb b/activerecord/test/cases/serialized_attribute_test.rb
index 765e92ccca..bc67da8d27 100644
--- a/activerecord/test/cases/serialized_attribute_test.rb
+++ b/activerecord/test/cases/serialized_attribute_test.rb
@@ -19,12 +19,6 @@ class SerializedAttributeTest < ActiveRecord::TestCase
assert_equal %w(content), Topic.serialized_attributes.keys
end
- def test_serialized_attributes_are_class_level_settings
- topic = Topic.new
- assert_raise(NoMethodError) { topic.serialized_attributes = [] }
- assert_deprecated { topic.serialized_attributes }
- end
-
def test_serialized_attribute
Topic.serialize("content", MyObject)
@@ -217,16 +211,15 @@ class SerializedAttributeTest < ActiveRecord::TestCase
end
def test_serialize_attribute_via_select_method_when_time_zone_available
- ActiveRecord::Base.time_zone_aware_attributes = true
- Topic.serialize(:content, MyObject)
+ with_timezone_config aware_attributes: true do
+ Topic.serialize(:content, MyObject)
- myobj = MyObject.new('value1', 'value2')
- topic = Topic.create(content: myobj)
+ myobj = MyObject.new('value1', 'value2')
+ topic = Topic.create(content: myobj)
- assert_equal(myobj, Topic.select(:content).find(topic.id).content)
- assert_raise(ActiveModel::MissingAttributeError) { Topic.select(:id).find(topic.id).content }
- ensure
- ActiveRecord::Base.time_zone_aware_attributes = false
+ assert_equal(myobj, Topic.select(:content).find(topic.id).content)
+ assert_raise(ActiveModel::MissingAttributeError) { Topic.select(:id).find(topic.id).content }
+ end
end
def test_serialize_attribute_can_be_serialized_in_an_integer_column
@@ -249,10 +242,7 @@ class SerializedAttributeTest < ActiveRecord::TestCase
myobj = MyObject.new('value1', 'value2')
Topic.create(content: myobj)
Topic.create(content: myobj)
-
- Topic.all.each do |topic|
- type = topic.instance_variable_get("@columns_hash")["content"]
- assert !type.instance_variable_get("@column").is_a?(ActiveRecord::AttributeMethods::Serialization::Type)
- end
+ type = Topic.column_types["content"]
+ assert !type.instance_variable_get("@column").is_a?(ActiveRecord::AttributeMethods::Serialization::Type)
end
end
diff --git a/activerecord/test/cases/store_test.rb b/activerecord/test/cases/store_test.rb
index 3e32d866ee..6f632b4d8d 100644
--- a/activerecord/test/cases/store_test.rb
+++ b/activerecord/test/cases/store_test.rb
@@ -151,8 +151,19 @@ class StoreTest < ActiveRecord::TestCase
assert_equal [:color, :homepage, :favorite_food], Admin::User.stored_attributes[:settings]
end
- test "stores_attributes are class level settings" do
- assert_raise(NoMethodError) { @john.stored_attributes = Hash.new }
- assert_raise(NoMethodError) { @john.stored_attributes }
+ test "stored_attributes are tracked per class" do
+ first_model = Class.new(ActiveRecord::Base) do
+ store_accessor :data, :color
+ end
+ second_model = Class.new(ActiveRecord::Base) do
+ store_accessor :data, :width, :height
+ end
+
+ assert_equal [:color], first_model.stored_attributes[:data]
+ assert_equal [:width, :height], second_model.stored_attributes[:data]
+ end
+
+ test "YAML coder initializes the store when a Nil value is given" do
+ assert_equal({}, @john.params)
end
end
diff --git a/activerecord/test/cases/tasks/database_tasks_test.rb b/activerecord/test/cases/tasks/database_tasks_test.rb
index e9000fef25..bf9e14fa4f 100644
--- a/activerecord/test/cases/tasks/database_tasks_test.rb
+++ b/activerecord/test/cases/tasks/database_tasks_test.rb
@@ -1,4 +1,5 @@
require 'cases/helper'
+require 'active_record/tasks/database_tasks'
module ActiveRecord
module DatabaseTasksSetupper
@@ -128,11 +129,22 @@ module ActiveRecord
)
end
- def test_creates_test_database_when_environment_is_database
+ def test_creates_test_and_development_databases_when_env_was_not_specified
ActiveRecord::Tasks::DatabaseTasks.expects(:create).
with('database' => 'dev-db')
ActiveRecord::Tasks::DatabaseTasks.expects(:create).
with('database' => 'test-db')
+ ENV.expects(:[]).with('RAILS_ENV').returns(nil)
+
+ ActiveRecord::Tasks::DatabaseTasks.create_current(
+ ActiveSupport::StringInquirer.new('development')
+ )
+ end
+
+ def test_creates_only_development_database_when_rails_env_is_development
+ ActiveRecord::Tasks::DatabaseTasks.expects(:create).
+ with('database' => 'dev-db')
+ ENV.expects(:[]).with('RAILS_ENV').returns('development')
ActiveRecord::Tasks::DatabaseTasks.create_current(
ActiveSupport::StringInquirer.new('development')
@@ -142,7 +154,7 @@ module ActiveRecord
def test_establishes_connection_for_the_given_environment
ActiveRecord::Tasks::DatabaseTasks.stubs(:create).returns true
- ActiveRecord::Base.expects(:establish_connection).with('development')
+ ActiveRecord::Base.expects(:establish_connection).with(:development)
ActiveRecord::Tasks::DatabaseTasks.create_current(
ActiveSupport::StringInquirer.new('development')
@@ -238,11 +250,22 @@ module ActiveRecord
)
end
- def test_creates_test_database_when_environment_is_database
+ def test_drops_test_and_development_databases_when_env_was_not_specified
ActiveRecord::Tasks::DatabaseTasks.expects(:drop).
with('database' => 'dev-db')
ActiveRecord::Tasks::DatabaseTasks.expects(:drop).
with('database' => 'test-db')
+ ENV.expects(:[]).with('RAILS_ENV').returns(nil)
+
+ ActiveRecord::Tasks::DatabaseTasks.drop_current(
+ ActiveSupport::StringInquirer.new('development')
+ )
+ end
+
+ def test_drops_only_development_database_when_rails_env_is_development
+ ActiveRecord::Tasks::DatabaseTasks.expects(:drop).
+ with('database' => 'dev-db')
+ ENV.expects(:[]).with('RAILS_ENV').returns('development')
ActiveRecord::Tasks::DatabaseTasks.drop_current(
ActiveSupport::StringInquirer.new('development')
diff --git a/activerecord/test/cases/tasks/firebird_rake_test.rb b/activerecord/test/cases/tasks/firebird_rake_test.rb
deleted file mode 100644
index c54989ae34..0000000000
--- a/activerecord/test/cases/tasks/firebird_rake_test.rb
+++ /dev/null
@@ -1,100 +0,0 @@
-require 'cases/helper'
-
-unless defined?(FireRuby::Database)
-module FireRuby
- module Database; end
-end
-end
-
-module ActiveRecord
- module FirebirdSetupper
- def setup
- @database = 'db.firebird'
- @connection = stub :connection
- @configuration = {
- 'adapter' => 'firebird',
- 'database' => @database
- }
- ActiveRecord::Base.stubs(:connection).returns(@connection)
- ActiveRecord::Base.stubs(:establish_connection).returns(true)
-
- @tasks = Class.new(ActiveRecord::Tasks::FirebirdDatabaseTasks) do
- def initialize(configuration)
- ActiveSupport::Deprecation.silence { super }
- end
- end
- ActiveRecord::Tasks::DatabaseTasks.stubs(:class_for_adapter).returns(@tasks) unless defined? ActiveRecord::ConnectionAdapters::FirebirdAdapter
- end
- end
-
- class FirebirdDBCreateTest < ActiveRecord::TestCase
- include FirebirdSetupper
-
- def test_db_retrieves_create
- message = capture(:stderr) do
- ActiveRecord::Tasks::DatabaseTasks.create @configuration
- end
- assert_match(/not supported/, message)
- end
- end
-
- class FirebirdDBDropTest < ActiveRecord::TestCase
- include FirebirdSetupper
-
- def test_db_retrieves_drop
- message = capture(:stderr) do
- ActiveRecord::Tasks::DatabaseTasks.drop @configuration
- end
- assert_match(/not supported/, message)
- end
- end
-
- class FirebirdDBCharsetAndCollationTest < ActiveRecord::TestCase
- include FirebirdSetupper
-
- def test_db_retrieves_collation
- assert_raise NoMethodError do
- ActiveRecord::Tasks::DatabaseTasks.collation @configuration
- end
- end
-
- def test_db_retrieves_charset
- message = capture(:stderr) do
- ActiveRecord::Tasks::DatabaseTasks.charset @configuration
- end
- assert_match(/not supported/, message)
- end
- end
-
- class FirebirdStructureDumpTest < ActiveRecord::TestCase
- include FirebirdSetupper
-
- def setup
- super
- FireRuby::Database.stubs(:db_string_for).returns(@database)
- end
-
- def test_structure_dump
- filename = "filebird.sql"
- Kernel.expects(:system).with("isql -a #{@database} > #{filename}")
-
- ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename)
- end
- end
-
- class FirebirdStructureLoadTest < ActiveRecord::TestCase
- include FirebirdSetupper
-
- def setup
- super
- FireRuby::Database.stubs(:db_string_for).returns(@database)
- end
-
- def test_structure_load
- filename = "firebird.sql"
- Kernel.expects(:system).with("isql -i #{filename} #{@database}")
-
- ActiveRecord::Tasks::DatabaseTasks.structure_load(@configuration, filename)
- end
- end
-end
diff --git a/activerecord/test/cases/tasks/mysql_rake_test.rb b/activerecord/test/cases/tasks/mysql_rake_test.rb
index 816bd62751..3e3a2828f3 100644
--- a/activerecord/test/cases/tasks/mysql_rake_test.rb
+++ b/activerecord/test/cases/tasks/mysql_rake_test.rb
@@ -65,99 +65,98 @@ module ActiveRecord
end
end
- class MysqlDBCreateAsRootTest < ActiveRecord::TestCase
- def setup
- unless current_adapter?(:MysqlAdapter)
- return skip("only tested on mysql")
+ if current_adapter?(:MysqlAdapter)
+ class MysqlDBCreateAsRootTest < ActiveRecord::TestCase
+ def setup
+ @connection = stub("Connection", create_database: true)
+ @error = Mysql::Error.new "Invalid permissions"
+ @configuration = {
+ 'adapter' => 'mysql',
+ 'database' => 'my-app-db',
+ 'username' => 'pat',
+ 'password' => 'wossname'
+ }
+
+ $stdin.stubs(:gets).returns("secret\n")
+ $stdout.stubs(:print).returns(nil)
+ @error.stubs(:errno).returns(1045)
+ ActiveRecord::Base.stubs(:connection).returns(@connection)
+ ActiveRecord::Base.stubs(:establish_connection).
+ raises(@error).
+ then.returns(true)
end
- @connection = stub("Connection", create_database: true)
- @error = Mysql::Error.new "Invalid permissions"
- @configuration = {
- 'adapter' => 'mysql',
- 'database' => 'my-app-db',
- 'username' => 'pat',
- 'password' => 'wossname'
- }
-
- $stdin.stubs(:gets).returns("secret\n")
- $stdout.stubs(:print).returns(nil)
- @error.stubs(:errno).returns(1045)
- ActiveRecord::Base.stubs(:connection).returns(@connection)
- ActiveRecord::Base.stubs(:establish_connection).
- raises(@error).
- then.returns(true)
- end
+ if defined?(::Mysql)
+ def test_root_password_is_requested
+ assert_permissions_granted_for "pat"
+ $stdin.expects(:gets).returns("secret\n")
- def test_root_password_is_requested
- assert_permissions_granted_for "pat"
- skip "only if mysql is available" unless defined?(::Mysql)
- $stdin.expects(:gets).returns("secret\n")
+ ActiveRecord::Tasks::DatabaseTasks.create @configuration
+ end
+ end
- ActiveRecord::Tasks::DatabaseTasks.create @configuration
- end
+ def test_connection_established_as_root
+ assert_permissions_granted_for "pat"
+ ActiveRecord::Base.expects(:establish_connection).with(
+ 'adapter' => 'mysql',
+ 'database' => nil,
+ 'username' => 'root',
+ 'password' => 'secret'
+ )
- def test_connection_established_as_root
- assert_permissions_granted_for "pat"
- ActiveRecord::Base.expects(:establish_connection).with(
- 'adapter' => 'mysql',
- 'database' => nil,
- 'username' => 'root',
- 'password' => 'secret'
- )
+ ActiveRecord::Tasks::DatabaseTasks.create @configuration
+ end
- ActiveRecord::Tasks::DatabaseTasks.create @configuration
- end
+ def test_database_created_by_root
+ assert_permissions_granted_for "pat"
+ @connection.expects(:create_database).
+ with('my-app-db', :charset => 'utf8', :collation => 'utf8_unicode_ci')
- 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')
+ ActiveRecord::Tasks::DatabaseTasks.create @configuration
+ end
- ActiveRecord::Tasks::DatabaseTasks.create @configuration
- end
+ def test_grant_privileges_for_normal_user
+ assert_permissions_granted_for "pat"
+ ActiveRecord::Tasks::DatabaseTasks.create @configuration
+ end
- def test_grant_privileges_for_normal_user
- assert_permissions_granted_for "pat"
- ActiveRecord::Tasks::DatabaseTasks.create @configuration
- end
+ def test_do_not_grant_privileges_for_root_user
+ @configuration['username'] = 'root'
+ @configuration['password'] = ''
+ ActiveRecord::Tasks::DatabaseTasks.create @configuration
+ end
- def test_do_not_grant_privileges_for_root_user
- @configuration['username'] = 'root'
- @configuration['password'] = ''
- ActiveRecord::Tasks::DatabaseTasks.create @configuration
- end
+ def test_connection_established_as_normal_user
+ assert_permissions_granted_for "pat"
+ ActiveRecord::Base.expects(:establish_connection).returns do
+ ActiveRecord::Base.expects(:establish_connection).with(
+ 'adapter' => 'mysql',
+ 'database' => 'my-app-db',
+ 'username' => 'pat',
+ 'password' => 'secret'
+ )
- def test_connection_established_as_normal_user
- assert_permissions_granted_for "pat"
- ActiveRecord::Base.expects(:establish_connection).returns do
- ActiveRecord::Base.expects(:establish_connection).with(
- 'adapter' => 'mysql',
- 'database' => 'my-app-db',
- 'username' => 'pat',
- 'password' => 'secret'
- )
+ raise @error
+ end
- raise @error
+ ActiveRecord::Tasks::DatabaseTasks.create @configuration
end
- ActiveRecord::Tasks::DatabaseTasks.create @configuration
- end
+ def test_sends_output_to_stderr_when_other_errors
+ @error.stubs(:errno).returns(42)
- def test_sends_output_to_stderr_when_other_errors
- @error.stubs(:errno).returns(42)
+ $stderr.expects(:puts).at_least_once.returns(nil)
- $stderr.expects(:puts).at_least_once.returns(nil)
+ ActiveRecord::Tasks::DatabaseTasks.create @configuration
+ end
- ActiveRecord::Tasks::DatabaseTasks.create @configuration
+ private
+ def assert_permissions_granted_for(db_user)
+ db_name = @configuration['database']
+ db_password = @configuration['password']
+ @connection.expects(:execute).with("GRANT ALL PRIVILEGES ON #{db_name}.* TO '#{db_user}'@'localhost' IDENTIFIED BY '#{db_password}' WITH GRANT OPTION;")
+ end
end
-
- private
- def assert_permissions_granted_for(db_user)
- db_name = @configuration['database']
- db_password = @configuration['password']
- @connection.expects(:execute).with("GRANT ALL PRIVILEGES ON #{db_name}.* TO '#{db_user}'@'localhost' IDENTIFIED BY '#{db_password}' WITH GRANT OPTION;")
- end
end
class MySQLDBDropTest < ActiveRecord::TestCase
@@ -280,6 +279,15 @@ module ActiveRecord
assert_match(/Could not dump the database structure/, warnings)
end
+
+ def test_structure_dump_with_port_number
+ filename = "awesome-file.sql"
+ Kernel.expects(:system).with("mysqldump", "--port", "10000", "--result-file", filename, "--no-data", "test-db").returns(true)
+
+ ActiveRecord::Tasks::DatabaseTasks.structure_dump(
+ @configuration.merge('port' => 10000),
+ filename)
+ end
end
class MySQLStructureLoadTest < ActiveRecord::TestCase
diff --git a/activerecord/test/cases/tasks/oracle_rake_test.rb b/activerecord/test/cases/tasks/oracle_rake_test.rb
deleted file mode 100644
index 5f840febbc..0000000000
--- a/activerecord/test/cases/tasks/oracle_rake_test.rb
+++ /dev/null
@@ -1,93 +0,0 @@
-require 'cases/helper'
-
-module ActiveRecord
- module OracleSetupper
- def setup
- @database = 'db.oracle'
- @connection = stub :connection
- @configuration = {
- 'adapter' => 'oracle',
- 'database' => @database
- }
- ActiveRecord::Base.stubs(:connection).returns(@connection)
- ActiveRecord::Base.stubs(:establish_connection).returns(true)
-
- @tasks = Class.new(ActiveRecord::Tasks::OracleDatabaseTasks) do
- def initialize(configuration)
- ActiveSupport::Deprecation.silence { super }
- end
- end
- ActiveRecord::Tasks::DatabaseTasks.stubs(:class_for_adapter).returns(@tasks) unless defined? ActiveRecord::ConnectionAdapters::OracleAdapter
- end
- end
-
- class OracleDBCreateTest < ActiveRecord::TestCase
- include OracleSetupper
-
- def test_db_retrieves_create
- message = capture(:stderr) do
- ActiveRecord::Tasks::DatabaseTasks.create @configuration
- end
- assert_match(/not supported/, message)
- end
- end
-
- class OracleDBDropTest < ActiveRecord::TestCase
- include OracleSetupper
-
- def test_db_retrieves_drop
- message = capture(:stderr) do
- ActiveRecord::Tasks::DatabaseTasks.drop @configuration
- end
- assert_match(/not supported/, message)
- end
- end
-
- class OracleDBCharsetAndCollationTest < ActiveRecord::TestCase
- include OracleSetupper
-
- def test_db_retrieves_collation
- assert_raise NoMethodError do
- ActiveRecord::Tasks::DatabaseTasks.collation @configuration
- end
- end
-
- def test_db_retrieves_charset
- message = capture(:stderr) do
- ActiveRecord::Tasks::DatabaseTasks.charset @configuration
- end
- assert_match(/not supported/, message)
- end
- end
-
- class OracleStructureDumpTest < ActiveRecord::TestCase
- include OracleSetupper
-
- def setup
- super
- @connection.stubs(:structure_dump).returns("select sysdate from dual;")
- end
-
- def test_structure_dump
- filename = "oracle.sql"
- ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename)
- assert File.exists?(filename)
- ensure
- FileUtils.rm_f(filename)
- end
- end
-
- class OracleStructureLoadTest < ActiveRecord::TestCase
- include OracleSetupper
-
- def test_structure_load
- filename = "oracle.sql"
-
- open(filename, 'w') { |f| f.puts("select sysdate from dual;") }
- @connection.stubs(:execute).with("select sysdate from dual;\n")
- ActiveRecord::Tasks::DatabaseTasks.structure_load(@configuration, filename)
- ensure
- FileUtils.rm_f(filename)
- end
- end
-end
diff --git a/activerecord/test/cases/tasks/postgresql_rake_test.rb b/activerecord/test/cases/tasks/postgresql_rake_test.rb
index f31896bc7f..6ea225178f 100644
--- a/activerecord/test/cases/tasks/postgresql_rake_test.rb
+++ b/activerecord/test/cases/tasks/postgresql_rake_test.rb
@@ -206,7 +206,7 @@ module ActiveRecord
@connection.expects(:schema_search_path).returns("foo")
ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename)
- assert File.exists?(filename)
+ assert File.exist?(filename)
ensure
FileUtils.rm(filename)
end
@@ -231,6 +231,13 @@ module ActiveRecord
ActiveRecord::Tasks::DatabaseTasks.structure_load(@configuration, filename)
end
+
+ def test_structure_load_accepts_path_with_spaces
+ filename = "awesome file.sql"
+ Kernel.expects(:system).with("psql -q -f awesome\\ file.sql my-app-db")
+
+ ActiveRecord::Tasks::DatabaseTasks.structure_load(@configuration, filename)
+ end
end
end
diff --git a/activerecord/test/cases/tasks/sqlite_rake_test.rb b/activerecord/test/cases/tasks/sqlite_rake_test.rb
index 7209c0f14d..da3471adf9 100644
--- a/activerecord/test/cases/tasks/sqlite_rake_test.rb
+++ b/activerecord/test/cases/tasks/sqlite_rake_test.rb
@@ -159,8 +159,8 @@ module ActiveRecord
filename = "awesome-file.sql"
ActiveRecord::Tasks::DatabaseTasks.structure_dump @configuration, filename, '/rails/root'
- assert File.exists?(dbfile)
- assert File.exists?(filename)
+ assert File.exist?(dbfile)
+ assert File.exist?(filename)
ensure
FileUtils.rm_f(filename)
FileUtils.rm_f(dbfile)
@@ -182,7 +182,7 @@ module ActiveRecord
open(filename, 'w') { |f| f.puts("select datetime('now', 'localtime');") }
ActiveRecord::Tasks::DatabaseTasks.structure_load @configuration, filename, '/rails/root'
- assert File.exists?(dbfile)
+ assert File.exist?(dbfile)
ensure
FileUtils.rm_f(filename)
FileUtils.rm_f(dbfile)
diff --git a/activerecord/test/cases/tasks/sqlserver_rake_test.rb b/activerecord/test/cases/tasks/sqlserver_rake_test.rb
deleted file mode 100644
index 0f1264b8ce..0000000000
--- a/activerecord/test/cases/tasks/sqlserver_rake_test.rb
+++ /dev/null
@@ -1,87 +0,0 @@
-require 'cases/helper'
-
-module ActiveRecord
- module SqlserverSetupper
- def setup
- @database = 'db.sqlserver'
- @connection = stub :connection
- @configuration = {
- 'adapter' => 'sqlserver',
- 'database' => @database,
- 'host' => 'localhost',
- 'username' => 'username',
- 'password' => 'password',
- }
- ActiveRecord::Base.stubs(:connection).returns(@connection)
- ActiveRecord::Base.stubs(:establish_connection).returns(true)
-
- @tasks = Class.new(ActiveRecord::Tasks::SqlserverDatabaseTasks) do
- def initialize(configuration)
- ActiveSupport::Deprecation.silence { super }
- end
- end
- ActiveRecord::Tasks::DatabaseTasks.stubs(:class_for_adapter).returns(@tasks) unless defined? ActiveRecord::ConnectionAdapters::SQLServerAdapter
- end
- end
-
- class SqlserverDBCreateTest < ActiveRecord::TestCase
- include SqlserverSetupper
-
- def test_db_retrieves_create
- message = capture(:stderr) do
- ActiveRecord::Tasks::DatabaseTasks.create @configuration
- end
- assert_match(/not supported/, message)
- end
- end
-
- class SqlserverDBDropTest < ActiveRecord::TestCase
- include SqlserverSetupper
-
- def test_db_retrieves_drop
- message = capture(:stderr) do
- ActiveRecord::Tasks::DatabaseTasks.drop @configuration
- end
- assert_match(/not supported/, message)
- end
- end
-
- class SqlserverDBCharsetAndCollationTest < ActiveRecord::TestCase
- include SqlserverSetupper
-
- def test_db_retrieves_collation
- assert_raise NoMethodError do
- ActiveRecord::Tasks::DatabaseTasks.collation @configuration
- end
- end
-
- def test_db_retrieves_charset
- message = capture(:stderr) do
- ActiveRecord::Tasks::DatabaseTasks.charset @configuration
- end
- assert_match(/not supported/, message)
- end
- end
-
- class SqlserverStructureDumpTest < ActiveRecord::TestCase
- include SqlserverSetupper
-
- def test_structure_dump
- filename = "sqlserver.sql"
- Kernel.expects(:system).with("smoscript -s localhost -d #{@database} -u username -p password -f #{filename} -A -U")
-
- ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename)
- end
- end
-
- class SqlserverStructureLoadTest < ActiveRecord::TestCase
- include SqlserverSetupper
-
- def test_structure_load
- filename = "sqlserver.sql"
- Kernel.expects(:system).with("sqlcmd -S localhost -d #{@database} -U username -P password -i #{filename}")
-
- ActiveRecord::Tasks::DatabaseTasks.structure_load(@configuration, filename)
- end
- end
-end
diff --git a/activerecord/test/cases/test_case.rb b/activerecord/test/cases/test_case.rb
index f3f7054794..4476ce3410 100644
--- a/activerecord/test/cases/test_case.rb
+++ b/activerecord/test/cases/test_case.rb
@@ -1,9 +1,108 @@
-ActiveSupport::Deprecation.silence do
- require 'active_record/test_case'
-end
+require 'active_support/test_case'
+
+module ActiveRecord
+ # = Active Record Test Case
+ #
+ # Defines some test assertions to test against SQL queries.
+ class TestCase < ActiveSupport::TestCase #:nodoc:
+ def teardown
+ SQLCounter.clear_log
+ end
+
+ def assert_date_from_db(expected, actual, message = nil)
+ # SybaseAdapter doesn't have a separate column type just for dates,
+ # so the time is in the string and incorrectly formatted
+ if current_adapter?(:SybaseAdapter)
+ assert_equal expected.to_s, actual.to_date.to_s, message
+ else
+ assert_equal expected.to_s, actual.to_s, message
+ end
+ end
+
+ def assert_sql(*patterns_to_match)
+ SQLCounter.clear_log
+ yield
+ SQLCounter.log_all
+ ensure
+ failed_patterns = []
+ patterns_to_match.each do |pattern|
+ failed_patterns << pattern unless SQLCounter.log_all.any?{ |sql| pattern === sql }
+ end
+ assert failed_patterns.empty?, "Query pattern(s) #{failed_patterns.map{ |p| p.inspect }.join(', ')} not found.#{SQLCounter.log.size == 0 ? '' : "\nQueries:\n#{SQLCounter.log.join("\n")}"}"
+ end
+
+ def assert_queries(num = 1, options = {})
+ ignore_none = options.fetch(:ignore_none) { num == :any }
+ SQLCounter.clear_log
+ x = yield
+ the_log = ignore_none ? SQLCounter.log_all : SQLCounter.log
+ if num == :any
+ assert_operator the_log.size, :>=, 1, "1 or more queries expected, but none were executed."
+ else
+ mesg = "#{the_log.size} instead of #{num} queries were executed.#{the_log.size == 0 ? '' : "\nQueries:\n#{the_log.join("\n")}"}"
+ assert_equal num, the_log.size, mesg
+ end
+ x
+ end
+
+ def assert_no_queries(options = {}, &block)
+ options.reverse_merge! ignore_none: true
+ assert_queries(0, options, &block)
+ end
+
+ def assert_column(model, column_name, msg=nil)
+ assert has_column?(model, column_name), msg
+ end
+
+ def assert_no_column(model, column_name, msg=nil)
+ assert_not has_column?(model, column_name), msg
+ end
+
+ def has_column?(model, column_name)
+ model.reset_column_information
+ model.column_names.include?(column_name.to_s)
+ end
+ end
-ActiveRecord::TestCase.class_eval do
- def sqlite3? connection
- connection.class.name.split('::').last == "SQLite3Adapter"
+ class SQLCounter
+ class << self
+ attr_accessor :ignored_sql, :log, :log_all
+ def clear_log; self.log = []; self.log_all = []; end
+ end
+
+ self.clear_log
+
+ self.ignored_sql = [/^PRAGMA/, /^SELECT currval/, /^SELECT CAST/, /^SELECT @@IDENTITY/, /^SELECT @@ROWCOUNT/, /^SAVEPOINT/, /^ROLLBACK TO SAVEPOINT/, /^RELEASE SAVEPOINT/, /^SHOW max_identifier_length/, /^BEGIN/, /^COMMIT/]
+
+ # FIXME: this needs to be refactored so specific database can add their own
+ # ignored SQL, or better yet, use a different notification for the queries
+ # instead examining the SQL content.
+ oracle_ignored = [/^select .*nextval/i, /^SAVEPOINT/, /^ROLLBACK TO/, /^\s*select .* from all_triggers/im, /^\s*select .* from all_constraints/im, /^\s*select .* from all_tab_cols/im]
+ mysql_ignored = [/^SHOW TABLES/i, /^SHOW FULL FIELDS/, /^SHOW CREATE TABLE /i]
+ postgresql_ignored = [/^\s*select\b.*\bfrom\b.*pg_namespace\b/im, /^\s*select\b.*\battname\b.*\bfrom\b.*\bpg_attribute\b/im, /^SHOW search_path/i]
+ sqlite3_ignored = [/^\s*SELECT name\b.*\bFROM sqlite_master/im]
+
+ [oracle_ignored, mysql_ignored, postgresql_ignored, sqlite3_ignored].each do |db_ignored_sql|
+ ignored_sql.concat db_ignored_sql
+ end
+
+ attr_reader :ignore
+
+ def initialize(ignore = Regexp.union(self.class.ignored_sql))
+ @ignore = ignore
+ end
+
+ def call(name, start, finish, message_id, values)
+ sql = values[:sql]
+
+ # FIXME: this seems bad. we should probably have a better way to indicate
+ # the query was cached
+ return if 'CACHE' == values[:name]
+
+ self.class.log_all << sql
+ self.class.log << sql unless ignore =~ sql
+ end
end
+
+ ActiveSupport::Notifications.subscribe('sql.active_record', SQLCounter.new)
end
diff --git a/activerecord/test/cases/timestamp_test.rb b/activerecord/test/cases/timestamp_test.rb
index ff1b01556d..717e0e1866 100644
--- a/activerecord/test/cases/timestamp_test.rb
+++ b/activerecord/test/cases/timestamp_test.rb
@@ -11,6 +11,7 @@ class TimestampTest < ActiveRecord::TestCase
def setup
@developer = Developer.first
+ @owner = Owner.first
@developer.update_columns(updated_at: Time.now.prev_month)
@previously_updated_at = @developer.updated_at
end
@@ -92,6 +93,53 @@ class TimestampTest < ActiveRecord::TestCase
assert_nothing_raised { Car.first.touch }
end
+ def test_touching_a_no_touching_object
+ Developer.no_touching do
+ assert @developer.no_touching?
+ assert !@owner.no_touching?
+ @developer.touch
+ end
+
+ assert !@developer.no_touching?
+ assert !@owner.no_touching?
+ assert_equal @previously_updated_at, @developer.updated_at
+ end
+
+ def test_touching_related_objects
+ @owner = Owner.first
+ @previously_updated_at = @owner.updated_at
+
+ Owner.no_touching do
+ @owner.pets.first.touch
+ end
+
+ assert_equal @previously_updated_at, @owner.updated_at
+ end
+
+ def test_global_no_touching
+ ActiveRecord::Base.no_touching do
+ assert @developer.no_touching?
+ assert @owner.no_touching?
+ @developer.touch
+ end
+
+ assert !@developer.no_touching?
+ assert !@owner.no_touching?
+ assert_equal @previously_updated_at, @developer.updated_at
+ end
+
+ def test_no_touching_threadsafe
+ Thread.new do
+ Developer.no_touching do
+ assert @developer.no_touching?
+
+ sleep(1)
+ end
+ end
+
+ assert !@developer.no_touching?
+ end
+
def test_saving_a_record_with_a_belongs_to_that_specifies_touching_the_parent_should_update_the_parent_updated_at
pet = Pet.first
owner = pet.owner
@@ -224,36 +272,62 @@ class TimestampTest < ActiveRecord::TestCase
assert_not_equal time, old_pet.updated_at
end
- def test_changing_parent_of_a_record_touches_both_new_and_old_polymorphic_parent_record
- klass = Class.new(ActiveRecord::Base) do
- def self.name; 'Toy'; end
+ def test_changing_parent_of_a_record_touches_both_new_and_old_polymorphic_parent_record_changes_within_same_class
+ car_class = Class.new(ActiveRecord::Base) do
+ def self.name; 'Car'; end
end
- wheel_klass = Class.new(ActiveRecord::Base) do
+ wheel_class = Class.new(ActiveRecord::Base) do
def self.name; 'Wheel'; end
belongs_to :wheelable, :polymorphic => true, :touch => true
end
- toy1 = klass.find(1)
- toy2 = klass.find(2)
+ car1 = car_class.find(1)
+ car2 = car_class.find(2)
- wheel = wheel_klass.new
- wheel.wheelable = toy1
- wheel.save!
+ wheel = wheel_class.create!(wheelable: car1)
time = 3.days.ago.at_beginning_of_hour
- toy1.update_columns(updated_at: time)
- toy2.update_columns(updated_at: time)
+ car1.update_columns(updated_at: time)
+ car2.update_columns(updated_at: time)
- wheel.wheelable = toy2
+ wheel.wheelable = car2
wheel.save!
- toy1.reload
- toy2.reload
+ assert_not_equal time, car1.reload.updated_at
+ assert_not_equal time, car2.reload.updated_at
+ end
+
+ def test_changing_parent_of_a_record_touches_both_new_and_old_polymorphic_parent_record_changes_with_other_class
+ car_class = Class.new(ActiveRecord::Base) do
+ def self.name; 'Car'; end
+ end
+
+ toy_class = Class.new(ActiveRecord::Base) do
+ def self.name; 'Toy'; end
+ end
+
+ wheel_class = Class.new(ActiveRecord::Base) do
+ def self.name; 'Wheel'; end
+ belongs_to :wheelable, :polymorphic => true, :touch => true
+ end
+
+ car = car_class.find(1)
+ toy = toy_class.find(3)
+
+ wheel = wheel_class.create!(wheelable: car)
+
+ time = 3.days.ago.at_beginning_of_hour
+
+ car.update_columns(updated_at: time)
+ toy.update_columns(updated_at: time)
+
+ wheel.wheelable = toy
+ wheel.save!
- assert_not_equal time, toy1.updated_at
- assert_not_equal time, toy2.updated_at
+ assert_not_equal time, car.reload.updated_at
+ assert_not_equal time, toy.reload.updated_at
end
def test_clearing_association_touches_the_old_record
@@ -278,31 +352,31 @@ class TimestampTest < ActiveRecord::TestCase
def test_timestamp_attributes_for_create
toy = Toy.first
- assert_equal toy.send(:timestamp_attributes_for_create), [:created_at, :created_on]
+ assert_equal [:created_at, :created_on], toy.send(:timestamp_attributes_for_create)
end
def test_timestamp_attributes_for_update
toy = Toy.first
- assert_equal toy.send(:timestamp_attributes_for_update), [:updated_at, :updated_on]
+ assert_equal [:updated_at, :updated_on], toy.send(:timestamp_attributes_for_update)
end
def test_all_timestamp_attributes
toy = Toy.first
- assert_equal toy.send(:all_timestamp_attributes), [:created_at, :created_on, :updated_at, :updated_on]
+ assert_equal [:created_at, :created_on, :updated_at, :updated_on], toy.send(:all_timestamp_attributes)
end
def test_timestamp_attributes_for_create_in_model
toy = Toy.first
- assert_equal toy.send(:timestamp_attributes_for_create_in_model), [:created_at]
+ assert_equal [:created_at], toy.send(:timestamp_attributes_for_create_in_model)
end
def test_timestamp_attributes_for_update_in_model
toy = Toy.first
- assert_equal toy.send(:timestamp_attributes_for_update_in_model), [:updated_at]
+ assert_equal [:updated_at], toy.send(:timestamp_attributes_for_update_in_model)
end
def test_all_timestamp_attributes_in_model
toy = Toy.first
- assert_equal toy.send(:all_timestamp_attributes_in_model), [:created_at, :updated_at]
+ assert_equal [:created_at, :updated_at], toy.send(:all_timestamp_attributes_in_model)
end
end
diff --git a/activerecord/test/cases/transaction_callbacks_test.rb b/activerecord/test/cases/transaction_callbacks_test.rb
index 9485de88a6..5644a35385 100644
--- a/activerecord/test/cases/transaction_callbacks_test.rb
+++ b/activerecord/test/cases/transaction_callbacks_test.rb
@@ -182,9 +182,9 @@ class TransactionCallbacksTest < ActiveRecord::TestCase
end
def test_call_after_rollback_when_commit_fails
- @first.class.connection.class.send(:alias_method, :real_method_commit_db_transaction, :commit_db_transaction)
+ @first.class.connection.singleton_class.send(:alias_method, :real_method_commit_db_transaction, :commit_db_transaction)
begin
- @first.class.connection.class.class_eval do
+ @first.class.connection.singleton_class.class_eval do
def commit_db_transaction; raise "boom!"; end
end
@@ -194,8 +194,8 @@ class TransactionCallbacksTest < ActiveRecord::TestCase
assert !@first.save rescue nil
assert_equal [:after_rollback], @first.history
ensure
- @first.class.connection.class.send(:remove_method, :commit_db_transaction)
- @first.class.connection.class.send(:alias_method, :commit_db_transaction, :real_method_commit_db_transaction)
+ @first.class.connection.singleton_class.send(:remove_method, :commit_db_transaction)
+ @first.class.connection.singleton_class.send(:alias_method, :commit_db_transaction, :real_method_commit_db_transaction)
end
end
diff --git a/activerecord/test/cases/transaction_isolation_test.rb b/activerecord/test/cases/transaction_isolation_test.rb
index 4f1cb99b68..f89c26532d 100644
--- a/activerecord/test/cases/transaction_isolation_test.rb
+++ b/activerecord/test/cases/transaction_isolation_test.rb
@@ -1,113 +1,105 @@
require 'cases/helper'
-class TransactionIsolationUnsupportedTest < ActiveRecord::TestCase
- self.use_transactional_fixtures = false
+unless ActiveRecord::Base.connection.supports_transaction_isolation?
+ class TransactionIsolationUnsupportedTest < ActiveRecord::TestCase
+ self.use_transactional_fixtures = false
- class Tag < ActiveRecord::Base
- end
-
- setup do
- if ActiveRecord::Base.connection.supports_transaction_isolation?
- skip "database supports transaction isolation; test is irrelevant"
+ class Tag < ActiveRecord::Base
end
- end
- test "setting the isolation level raises an error" do
- assert_raises(ActiveRecord::TransactionIsolationError) do
- Tag.transaction(isolation: :serializable) { }
+ test "setting the isolation level raises an error" do
+ assert_raises(ActiveRecord::TransactionIsolationError) do
+ Tag.transaction(isolation: :serializable) { }
+ end
end
end
end
-class TransactionIsolationTest < ActiveRecord::TestCase
- self.use_transactional_fixtures = false
-
- class Tag < ActiveRecord::Base
- self.table_name = 'tags'
- end
-
- class Tag2 < ActiveRecord::Base
- self.table_name = 'tags'
- end
+if ActiveRecord::Base.connection.supports_transaction_isolation?
+ class TransactionIsolationTest < ActiveRecord::TestCase
+ self.use_transactional_fixtures = false
- setup do
- unless ActiveRecord::Base.connection.supports_transaction_isolation?
- skip "database does not support setting transaction isolation"
+ class Tag < ActiveRecord::Base
+ self.table_name = 'tags'
end
- Tag.establish_connection 'arunit'
- Tag2.establish_connection 'arunit'
- Tag.destroy_all
- end
-
- # It is impossible to properly test read uncommitted. The SQL standard only
- # specifies what must not happen at a certain level, not what must happen. At
- # the read uncommitted level, there is nothing that must not happen.
- test "read uncommitted" do
- unless ActiveRecord::Base.connection.transaction_isolation_levels.include?(:read_uncommitted)
- skip "database does not support read uncommitted isolation level"
+ class Tag2 < ActiveRecord::Base
+ self.table_name = 'tags'
end
- Tag.transaction(isolation: :read_uncommitted) do
- assert_equal 0, Tag.count
- Tag2.create
- assert_equal 1, Tag.count
+
+ setup do
+ Tag.establish_connection :arunit
+ Tag2.establish_connection :arunit
+ Tag.destroy_all
end
- end
- # We are testing that a dirty read does not happen
- test "read committed" do
- Tag.transaction(isolation: :read_committed) do
- assert_equal 0, Tag.count
+ # It is impossible to properly test read uncommitted. The SQL standard only
+ # specifies what must not happen at a certain level, not what must happen. At
+ # the read uncommitted level, there is nothing that must not happen.
+ if ActiveRecord::Base.connection.transaction_isolation_levels.include?(:read_uncommitted)
+ test "read uncommitted" do
+ Tag.transaction(isolation: :read_uncommitted) do
+ assert_equal 0, Tag.count
+ Tag2.create
+ assert_equal 1, Tag.count
+ end
+ end
+ end
- Tag2.transaction do
- Tag2.create
+ # We are testing that a dirty read does not happen
+ test "read committed" do
+ Tag.transaction(isolation: :read_committed) do
assert_equal 0, Tag.count
+
+ Tag2.transaction do
+ Tag2.create
+ assert_equal 0, Tag.count
+ end
end
+
+ assert_equal 1, Tag.count
end
- assert_equal 1, Tag.count
- end
+ # We are testing that a nonrepeatable read does not happen
+ if ActiveRecord::Base.connection.transaction_isolation_levels.include?(:repeatable_read)
+ test "repeatable read" do
+ tag = Tag.create(name: 'jon')
- # We are testing that a nonrepeatable read does not happen
- test "repeatable read" do
- unless ActiveRecord::Base.connection.transaction_isolation_levels.include?(:repeatable_read)
- skip "database does not support repeatable read isolation level"
- end
- tag = Tag.create(name: 'jon')
+ Tag.transaction(isolation: :repeatable_read) do
+ tag.reload
+ Tag2.find(tag.id).update(name: 'emily')
- Tag.transaction(isolation: :repeatable_read) do
- tag.reload
- Tag2.find(tag.id).update(name: 'emily')
+ tag.reload
+ assert_equal 'jon', tag.name
+ end
- tag.reload
- assert_equal 'jon', tag.name
+ tag.reload
+ assert_equal 'emily', tag.name
+ end
end
- tag.reload
- assert_equal 'emily', tag.name
- end
-
- # We are only testing that there are no errors because it's too hard to
- # test serializable. Databases behave differently to enforce the serializability
- # constraint.
- test "serializable" do
- Tag.transaction(isolation: :serializable) do
- Tag.create
+ # We are only testing that there are no errors because it's too hard to
+ # test serializable. Databases behave differently to enforce the serializability
+ # constraint.
+ test "serializable" do
+ Tag.transaction(isolation: :serializable) do
+ Tag.create
+ end
end
- end
- test "setting isolation when joining a transaction raises an error" do
- Tag.transaction do
- assert_raises(ActiveRecord::TransactionIsolationError) do
- Tag.transaction(isolation: :serializable) { }
+ test "setting isolation when joining a transaction raises an error" do
+ Tag.transaction do
+ assert_raises(ActiveRecord::TransactionIsolationError) do
+ Tag.transaction(isolation: :serializable) { }
+ end
end
end
- end
- test "setting isolation when starting a nested transaction raises error" do
- Tag.transaction do
- assert_raises(ActiveRecord::TransactionIsolationError) do
- Tag.transaction(requires_new: true, isolation: :serializable) { }
+ test "setting isolation when starting a nested transaction raises error" do
+ Tag.transaction do
+ assert_raises(ActiveRecord::TransactionIsolationError) do
+ Tag.transaction(requires_new: true, isolation: :serializable) { }
+ end
end
end
end
diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb
index 6d66342fa5..89dab16975 100644
--- a/activerecord/test/cases/transactions_test.rb
+++ b/activerecord/test/cases/transactions_test.rb
@@ -117,6 +117,20 @@ class TransactionTest < ActiveRecord::TestCase
assert !Topic.find(1).approved?
end
+ def test_raising_exception_in_nested_transaction_restore_state_in_save
+ topic = Topic.new
+
+ def topic.after_save_for_transaction
+ raise 'Make the transaction rollback'
+ end
+
+ assert_raises(RuntimeError) do
+ Topic.transaction { topic.save }
+ end
+
+ assert topic.new_record?, "#{topic.inspect} should be new record"
+ end
+
def test_update_should_rollback_on_failure
author = Author.find(1)
posts_count = author.posts.size
@@ -361,6 +375,36 @@ class TransactionTest < ActiveRecord::TestCase
assert_equal "Three", @three
end if Topic.connection.supports_savepoints?
+ def test_using_named_savepoints
+ Topic.transaction do
+ @first.approved = true
+ @first.save!
+ Topic.connection.create_savepoint("first")
+
+ @first.approved = false
+ @first.save!
+ Topic.connection.rollback_to_savepoint("first")
+ assert @first.reload.approved?
+
+ @first.approved = false
+ @first.save!
+ Topic.connection.release_savepoint("first")
+ assert_not @first.reload.approved?
+ end
+ end if Topic.connection.supports_savepoints?
+
+ def test_releasing_named_savepoints
+ Topic.transaction do
+ Topic.connection.create_savepoint("another")
+ Topic.connection.release_savepoint("another")
+
+ # The savepoint is now gone and we can't remove it again.
+ assert_raises(ActiveRecord::StatementInvalid) do
+ Topic.connection.release_savepoint("another")
+ end
+ end
+ end
+
def test_rollback_when_commit_raises
Topic.connection.expects(:begin_db_transaction)
Topic.connection.expects(:commit_db_transaction).raises('OH NOES')
@@ -377,7 +421,9 @@ class TransactionTest < ActiveRecord::TestCase
topic = Topic.new(:title => 'test')
topic.freeze
e = assert_raise(RuntimeError) { topic.save }
- assert_equal "can't modify frozen Hash", e.message
+ assert_match(/frozen/i, e.message) # Not good enough, but we can't do much
+ # about it since there is no specific error
+ # for frozen objects.
assert !topic.persisted?, 'not persisted'
assert_nil topic.id
assert topic.frozen?, 'not frozen'
@@ -410,16 +456,6 @@ class TransactionTest < ActiveRecord::TestCase
assert !@second.destroyed?, 'not destroyed'
end
- if current_adapter?(:PostgreSQLAdapter) && defined?(PGconn::PQTRANS_IDLE)
- def test_outside_transaction_works
- assert assert_deprecated { Topic.connection.outside_transaction? }
- Topic.connection.begin_db_transaction
- assert assert_deprecated { !Topic.connection.outside_transaction? }
- Topic.connection.rollback_db_transaction
- assert assert_deprecated { Topic.connection.outside_transaction? }
- end
- end
-
def test_sqlite_add_column_in_transaction
return true unless current_adapter?(:SQLite3Adapter)
@@ -449,6 +485,13 @@ class TransactionTest < ActiveRecord::TestCase
raise ActiveRecord::Rollback
end
end
+ ensure
+ begin
+ Topic.connection.remove_column('topics', 'stuff')
+ rescue
+ ensure
+ Topic.reset_column_information
+ end
end
def test_transactions_state_from_rollback
@@ -535,16 +578,16 @@ if current_adapter?(:PostgreSQLAdapter)
class ConcurrentTransactionTest < TransactionTest
# This will cause transactions to overlap and fail unless they are performed on
# separate database connections.
- def test_transaction_per_thread
- assert_nothing_raised do
- threads = (1..3).map do
+ unless in_memory_db?
+ def test_transaction_per_thread
+ threads = 3.times.map do
Thread.new do
Topic.transaction do
topic = Topic.find(1)
topic.approved = !topic.approved?
- topic.save!
+ assert topic.save!
topic.approved = !topic.approved?
- topic.save!
+ assert topic.save!
end
Topic.connection.close
end
@@ -603,14 +646,5 @@ if current_adapter?(:PostgreSQLAdapter)
assert_equal original_salary, Developer.find(1).salary
end
-
- test "#transaction_joinable= is deprecated" do
- Developer.transaction do
- conn = Developer.connection
- assert conn.current_transaction.joinable?
- assert_deprecated { conn.transaction_joinable = false }
- assert !conn.current_transaction.joinable?
- end
- end
end
end
diff --git a/activerecord/test/cases/validations/association_validation_test.rb b/activerecord/test/cases/validations/association_validation_test.rb
index 7ac34bc71e..602f633c45 100644
--- a/activerecord/test/cases/validations/association_validation_test.rb
+++ b/activerecord/test/cases/validations/association_validation_test.rb
@@ -10,29 +10,33 @@ require 'models/interest'
class AssociationValidationTest < ActiveRecord::TestCase
fixtures :topics, :owners
- repair_validations(Topic, Reply, Owner)
+ repair_validations(Topic, Reply)
def test_validates_size_of_association
- assert_nothing_raised { Owner.validates_size_of :pets, :minimum => 1 }
- o = Owner.new('name' => 'nopets')
- assert !o.save
- assert o.errors[:pets].any?
- o.pets.build('name' => 'apet')
- assert o.valid?
+ repair_validations Owner do
+ assert_nothing_raised { Owner.validates_size_of :pets, :minimum => 1 }
+ o = Owner.new('name' => 'nopets')
+ assert !o.save
+ assert o.errors[:pets].any?
+ o.pets.build('name' => 'apet')
+ assert o.valid?
+ end
end
def test_validates_size_of_association_using_within
- assert_nothing_raised { Owner.validates_size_of :pets, :within => 1..2 }
- o = Owner.new('name' => 'nopets')
- assert !o.save
- assert o.errors[:pets].any?
-
- o.pets.build('name' => 'apet')
- assert o.valid?
-
- 2.times { o.pets.build('name' => 'apet') }
- assert !o.save
- assert o.errors[:pets].any?
+ repair_validations Owner do
+ assert_nothing_raised { Owner.validates_size_of :pets, :within => 1..2 }
+ o = Owner.new('name' => 'nopets')
+ assert !o.save
+ assert o.errors[:pets].any?
+
+ o.pets.build('name' => 'apet')
+ assert o.valid?
+
+ 2.times { o.pets.build('name' => 'apet') }
+ assert !o.save
+ assert o.errors[:pets].any?
+ end
end
def test_validates_associated_many
@@ -91,12 +95,14 @@ class AssociationValidationTest < ActiveRecord::TestCase
end
def test_validates_size_of_association_utf8
- assert_nothing_raised { Owner.validates_size_of :pets, :minimum => 1 }
- o = Owner.new('name' => 'あいうえおかきくけこ')
- assert !o.save
- assert o.errors[:pets].any?
- o.pets.build('name' => 'あいうえおかきくけこ')
- assert o.valid?
+ repair_validations Owner do
+ assert_nothing_raised { Owner.validates_size_of :pets, :minimum => 1 }
+ o = Owner.new('name' => 'あいうえおかきくけこ')
+ assert !o.save
+ assert o.errors[:pets].any?
+ o.pets.build('name' => 'あいうえおかきくけこ')
+ assert o.valid?
+ end
end
def test_validates_presence_of_belongs_to_association__parent_is_new_record
@@ -118,21 +124,4 @@ class AssociationValidationTest < ActiveRecord::TestCase
end
end
- def test_validates_associated_models_in_the_same_context
- Topic.validates_presence_of :title, :on => :custom_context
- Topic.validates_associated :replies
- Reply.validates_presence_of :title, :on => :custom_context
-
- t = Topic.new('title' => '')
- r = t.replies.new('title' => '')
-
- assert t.valid?
- assert !t.valid?(:custom_context)
-
- t.title = "Longer"
- assert !t.valid?(:custom_context), "Should NOT be valid if the associated object is not valid in the same context."
-
- r.title = "Longer"
- assert t.valid?(:custom_context), "Should be valid if the associated object is not valid in the same context."
- end
end
diff --git a/activerecord/test/cases/validations/i18n_generate_message_validation_test.rb b/activerecord/test/cases/validations/i18n_generate_message_validation_test.rb
index 32d2bf746f..a73c3bf1af 100644
--- a/activerecord/test/cases/validations/i18n_generate_message_validation_test.rb
+++ b/activerecord/test/cases/validations/i18n_generate_message_validation_test.rb
@@ -55,22 +55,30 @@ class I18nGenerateMessageValidationTest < ActiveRecord::TestCase
end
test "translation for 'taken' can be overridden" do
- I18n.backend.store_translations "en", {errors: {attributes: {title: {taken: "Custom taken message" }}}}
- assert_equal "Custom taken message", @topic.errors.generate_message(:title, :taken, :value => 'title')
+ reset_i18n_load_path do
+ I18n.backend.store_translations "en", {errors: {attributes: {title: {taken: "Custom taken message" }}}}
+ assert_equal "Custom taken message", @topic.errors.generate_message(:title, :taken, :value => 'title')
+ end
end
test "translation for 'taken' can be overridden in activerecord scope" do
- I18n.backend.store_translations "en", {activerecord: {errors: {messages: {taken: "Custom taken message" }}}}
- assert_equal "Custom taken message", @topic.errors.generate_message(:title, :taken, :value => 'title')
+ reset_i18n_load_path do
+ I18n.backend.store_translations "en", {activerecord: {errors: {messages: {taken: "Custom taken message" }}}}
+ assert_equal "Custom taken message", @topic.errors.generate_message(:title, :taken, :value => 'title')
+ end
end
test "translation for 'taken' can be overridden in activerecord model scope" do
- I18n.backend.store_translations "en", {activerecord: {errors: {models: {topic: {taken: "Custom taken message" }}}}}
- assert_equal "Custom taken message", @topic.errors.generate_message(:title, :taken, :value => 'title')
+ reset_i18n_load_path do
+ I18n.backend.store_translations "en", {activerecord: {errors: {models: {topic: {taken: "Custom taken message" }}}}}
+ assert_equal "Custom taken message", @topic.errors.generate_message(:title, :taken, :value => 'title')
+ end
end
test "translation for 'taken' can be overridden in activerecord attributes scope" do
- I18n.backend.store_translations "en", {activerecord: {errors: {models: {topic: {attributes: {title: {taken: "Custom taken message" }}}}}}}
- assert_equal "Custom taken message", @topic.errors.generate_message(:title, :taken, :value => 'title')
+ reset_i18n_load_path do
+ I18n.backend.store_translations "en", {activerecord: {errors: {models: {topic: {attributes: {title: {taken: "Custom taken message" }}}}}}}
+ assert_equal "Custom taken message", @topic.errors.generate_message(:title, :taken, :value => 'title')
+ end
end
end
diff --git a/activerecord/test/cases/validations/presence_validation_test.rb b/activerecord/test/cases/validations/presence_validation_test.rb
index 1de8934406..3790d3c8cf 100644
--- a/activerecord/test/cases/validations/presence_validation_test.rb
+++ b/activerecord/test/cases/validations/presence_validation_test.rb
@@ -3,6 +3,8 @@ require "cases/helper"
require 'models/man'
require 'models/face'
require 'models/interest'
+require 'models/speedometer'
+require 'models/dashboard'
class PresenceValidationTest < ActiveRecord::TestCase
class Boy < Man; end
@@ -48,4 +50,18 @@ class PresenceValidationTest < ActiveRecord::TestCase
i2.mark_for_destruction
assert b.invalid?
end
+
+ def test_validates_presence_doesnt_convert_to_array
+ Speedometer.validates_presence_of :dashboard
+
+ dash = Dashboard.new
+
+ # dashboard has to_a method
+ def dash.to_a; ['(/)', '(\)']; end
+
+ s = Speedometer.new
+ s.dashboard = dash
+
+ assert_nothing_raised { s.valid? }
+ end
end
diff --git a/activerecord/test/cases/validations/uniqueness_validation_test.rb b/activerecord/test/cases/validations/uniqueness_validation_test.rb
index 2b33f01783..74c696c858 100644
--- a/activerecord/test/cases/validations/uniqueness_validation_test.rb
+++ b/activerecord/test/cases/validations/uniqueness_validation_test.rb
@@ -35,6 +35,11 @@ class Employee < ActiveRecord::Base
validates_uniqueness_of :nicknames
end
+class TopicWithUniqEvent < Topic
+ belongs_to :event, foreign_key: :parent_id
+ validates :event, uniqueness: true
+end
+
class UniquenessValidationTest < ActiveRecord::TestCase
fixtures :topics, 'warehouse-things', :developers
@@ -58,6 +63,14 @@ class UniquenessValidationTest < ActiveRecord::TestCase
assert t2.save, "Should now save t2 as unique"
end
+ def test_validate_uniqueness_with_alias_attribute
+ Topic.alias_attribute :new_title, :title
+ Topic.validates_uniqueness_of(:new_title)
+
+ topic = Topic.new(new_title: 'abc')
+ assert topic.valid?
+ end
+
def test_validates_uniqueness_with_nil_value
Topic.validates_uniqueness_of(:title)
@@ -365,15 +378,29 @@ class UniquenessValidationTest < ActiveRecord::TestCase
}
end
- def test_validate_uniqueness_with_array_column
- return skip "Uniqueness on arrays has only been tested in PostgreSQL so far." if !current_adapter? :PostgreSQLAdapter
+ if current_adapter? :PostgreSQLAdapter
+ def test_validate_uniqueness_with_array_column
+ e1 = Employee.create("nicknames" => ["john", "johnny"], "commission_by_quarter" => [1000, 1200])
+ assert e1.persisted?, "Saving e1"
- e1 = Employee.create("nicknames" => ["john", "johnny"], "commission_by_quarter" => [1000, 1200])
- assert e1.persisted?, "Saving e1"
+ e2 = Employee.create("nicknames" => ["john", "johnny"], "commission_by_quarter" => [2200])
+ assert !e2.persisted?, "e2 shouldn't be valid"
+ assert e2.errors[:nicknames].any?, "Should have errors for nicknames"
+ assert_equal ["has already been taken"], e2.errors[:nicknames], "Should have uniqueness message for nicknames"
+ end
+ end
+
+ def test_validate_uniqueness_on_existing_relation
+ event = Event.create
+ assert TopicWithUniqEvent.create(event: event).valid?
+
+ topic = TopicWithUniqEvent.new(event: event)
+ assert_not topic.valid?
+ assert_equal ['has already been taken'], topic.errors[:event]
+ end
- e2 = Employee.create("nicknames" => ["john", "johnny"], "commission_by_quarter" => [2200])
- assert !e2.persisted?, "e2 shouldn't be valid"
- assert e2.errors[:nicknames].any?, "Should have errors for nicknames"
- assert_equal ["has already been taken"], e2.errors[:nicknames], "Should have uniqueness message for nicknames"
+ def test_validate_uniqueness_on_empty_relation
+ topic = TopicWithUniqEvent.new
+ assert topic.valid?
end
end
diff --git a/activerecord/test/cases/validations_test.rb b/activerecord/test/cases/validations_test.rb
index 3f587d177b..de618902aa 100644
--- a/activerecord/test/cases/validations_test.rb
+++ b/activerecord/test/cases/validations_test.rb
@@ -56,13 +56,11 @@ class ValidationsTest < ActiveRecord::TestCase
assert_raise(ActiveRecord::RecordInvalid) { WrongReply.create! }
assert_raise(ActiveRecord::RecordInvalid) { WrongReply.new.save! }
- begin
- r = WrongReply.new
+ r = WrongReply.new
+ invalid = assert_raise ActiveRecord::RecordInvalid do
r.save!
- flunk
- rescue ActiveRecord::RecordInvalid => invalid
- assert_equal r, invalid.record
end
+ assert_equal r, invalid.record
end
def test_exception_on_create_bang_many
@@ -93,7 +91,7 @@ class ValidationsTest < ActiveRecord::TestCase
assert reply.save(:validate => false)
end
- def test_validates_acceptance_of_with_non_existant_table
+ def test_validates_acceptance_of_with_non_existent_table
Object.const_set :IncorporealModel, Class.new(ActiveRecord::Base)
assert_nothing_raised ActiveRecord::StatementInvalid do
diff --git a/activerecord/test/cases/xml_serialization_test.rb b/activerecord/test/cases/xml_serialization_test.rb
index 68fa15de50..78fa2f935a 100644
--- a/activerecord/test/cases/xml_serialization_test.rb
+++ b/activerecord/test/cases/xml_serialization_test.rb
@@ -161,21 +161,17 @@ end
class DefaultXmlSerializationTimezoneTest < ActiveRecord::TestCase
def test_should_serialize_datetime_with_timezone
- timezone, Time.zone = Time.zone, "Pacific Time (US & Canada)"
-
- 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
- ensure
- Time.zone = 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
- timezone, Time.zone = Time.zone, "Pacific Time (US & Canada)"
-
- 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
- ensure
- Time.zone = timezone
+ 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
diff --git a/activerecord/test/cases/yaml_serialization_test.rb b/activerecord/test/cases/yaml_serialization_test.rb
index 302913e095..15815d56e4 100644
--- a/activerecord/test/cases/yaml_serialization_test.rb
+++ b/activerecord/test/cases/yaml_serialization_test.rb
@@ -5,16 +5,10 @@ class YamlSerializationTest < ActiveRecord::TestCase
fixtures :topics
def test_to_yaml_with_time_with_zone_should_not_raise_exception
- tz = Time.zone
- Time.zone = ActiveSupport::TimeZone["Pacific Time (US & Canada)"]
- ActiveRecord::Base.time_zone_aware_attributes = true
-
- topic = Topic.new(:written_on => DateTime.now)
- assert_nothing_raised { topic.to_yaml }
-
- ensure
- Time.zone = tz
- ActiveRecord::Base.time_zone_aware_attributes = false
+ with_timezone_config aware_attributes: true, zone: "Pacific Time (US & Canada)" do
+ topic = Topic.new(:written_on => DateTime.now)
+ assert_nothing_raised { topic.to_yaml }
+ end
end
def test_roundtrip
@@ -49,4 +43,8 @@ class YamlSerializationTest < ActiveRecord::TestCase
t = Psych.load Psych.dump topic
assert_equal topic.attributes, t.attributes
end
+
+ def test_active_record_relation_serialization
+ [Topic.all].to_yaml
+ end
end
diff --git a/activerecord/test/fixtures/all/admin b/activerecord/test/fixtures/all/admin
new file mode 120000
index 0000000000..984d12a043
--- /dev/null
+++ b/activerecord/test/fixtures/all/admin
@@ -0,0 +1 @@
+../to_be_linked/ \ No newline at end of file
diff --git a/activerecord/test/fixtures/owners.yml b/activerecord/test/fixtures/owners.yml
index 2d21ce433c..3b7b29bb34 100644
--- a/activerecord/test/fixtures/owners.yml
+++ b/activerecord/test/fixtures/owners.yml
@@ -2,6 +2,7 @@ blackbeard:
owner_id: 1
name: blackbeard
essay_id: A Modest Proposal
+ happy_at: '2150-10-10 16:00:00'
ashley:
owner_id: 2
diff --git a/activerecord/test/fixtures/sponsors.yml b/activerecord/test/fixtures/sponsors.yml
index bfc6b238b1..2da541c539 100644
--- a/activerecord/test/fixtures/sponsors.yml
+++ b/activerecord/test/fixtures/sponsors.yml
@@ -8,5 +8,5 @@ boring_club_sponsor_for_groucho:
sponsorable_type: Member
crazy_club_sponsor_for_groucho:
sponsor_club: crazy_club
- sponsorable_id: 2
+ sponsorable_id: 3
sponsorable_type: Member
diff --git a/activerecord/test/fixtures/tasks.yml b/activerecord/test/fixtures/tasks.yml
index 402ca85faf..c38b32b0e5 100644
--- a/activerecord/test/fixtures/tasks.yml
+++ b/activerecord/test/fixtures/tasks.yml
@@ -1,4 +1,4 @@
-# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/Fixtures.html
+# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
first_task:
id: 1
starting: 2005-03-30t06:30:00.00+01:00
diff --git a/activerecord/test/fixtures/to_be_linked/accounts.yml b/activerecord/test/fixtures/to_be_linked/accounts.yml
new file mode 100644
index 0000000000..9e341a15af
--- /dev/null
+++ b/activerecord/test/fixtures/to_be_linked/accounts.yml
@@ -0,0 +1,2 @@
+signals37:
+ name: 37signals
diff --git a/activerecord/test/fixtures/to_be_linked/users.yml b/activerecord/test/fixtures/to_be_linked/users.yml
new file mode 100644
index 0000000000..e2884beda5
--- /dev/null
+++ b/activerecord/test/fixtures/to_be_linked/users.yml
@@ -0,0 +1,10 @@
+david:
+ name: David
+ account: signals37
+
+jamis:
+ name: Jamis
+ account: signals37
+ settings:
+ :symbol: symbol
+ string: string
diff --git a/activerecord/test/migrations/version_check/20131219224947_migration_version_check.rb b/activerecord/test/migrations/version_check/20131219224947_migration_version_check.rb
new file mode 100644
index 0000000000..9d46485a31
--- /dev/null
+++ b/activerecord/test/migrations/version_check/20131219224947_migration_version_check.rb
@@ -0,0 +1,8 @@
+class MigrationVersionCheck < ActiveRecord::Migration
+ def self.up
+ raise "incorrect migration version" unless version == 20131219224947
+ end
+
+ def self.down
+ end
+end
diff --git a/activerecord/test/models/admin/user.rb b/activerecord/test/models/admin/user.rb
index 4c3b71e8f9..48a110bd23 100644
--- a/activerecord/test/models/admin/user.rb
+++ b/activerecord/test/models/admin/user.rb
@@ -14,6 +14,7 @@ class Admin::User < ActiveRecord::Base
end
belongs_to :account
+ store :params, accessors: [ :token ], coder: YAML
store :settings, :accessors => [ :color, :homepage ]
store_accessor :settings, :favorite_food
store :preferences, :accessors => [ :remember_login ]
diff --git a/activerecord/test/models/author.rb b/activerecord/test/models/author.rb
index a96899ae10..c197951c71 100644
--- a/activerecord/test/models/author.rb
+++ b/activerecord/test/models/author.rb
@@ -8,12 +8,14 @@ class Author < ActiveRecord::Base
has_many :posts_sorted_by_id_limited, -> { order('posts.id').limit(1) }, :class_name => "Post"
has_many :posts_with_categories, -> { includes(:categories) }, :class_name => "Post"
has_many :posts_with_comments_and_categories, -> { includes(:comments, :categories).order("posts.id") }, :class_name => "Post"
- has_many :posts_containing_the_letter_a, :class_name => "Post"
has_many :posts_with_special_categorizations, :class_name => 'PostWithSpecialCategorization'
- has_many :posts_with_extension, :class_name => "Post"
has_one :post_about_thinking, -> { where("posts.title like '%thinking%'") }, :class_name => 'Post'
has_one :post_about_thinking_with_last_comment, -> { where("posts.title like '%thinking%'").includes(:last_comment) }, :class_name => 'Post'
- has_many :comments, :through => :posts
+ has_many :comments, through: :posts do
+ def ratings
+ Rating.joins(:comment).merge(self)
+ end
+ end
has_many :comments_containing_the_letter_e, :through => :posts, :source => :comments
has_many :comments_with_order_and_conditions, -> { order('comments.body').where("comments.body like 'Thank%'") }, :through => :posts, :source => :comments
has_many :comments_with_include, -> { includes(:post) }, :through => :posts, :source => :comments
@@ -27,8 +29,14 @@ class Author < ActiveRecord::Base
has_many :thinking_posts, -> { where(:title => 'So I was thinking') }, :dependent => :delete_all, :class_name => 'Post'
has_many :welcome_posts, -> { where(:title => 'Welcome to the weblog') }, :class_name => 'Post'
+ has_many :welcome_posts_with_one_comment,
+ -> { where(title: 'Welcome to the weblog').where('comments_count = ?', 1) },
+ class_name: 'Post'
+ has_many :welcome_posts_with_comments,
+ -> { where(title: 'Welcome to the weblog').where(Post.arel_table[:comments_count].gt(0)) },
+ class_name: 'Post'
+
has_many :comments_desc, -> { order('comments.id DESC') }, :through => :posts, :source => :comments
- has_many :limited_comments, -> { limit(1) }, :through => :posts, :source => :comments
has_many :funky_comments, :through => :posts, :source => :comments
has_many :ordered_uniq_comments, -> { distinct.order('comments.id') }, :through => :posts, :source => :comments
has_many :ordered_uniq_comments_desc, -> { distinct.order('comments.id DESC') }, :through => :posts, :source => :comments
@@ -85,7 +93,7 @@ class Author < ActiveRecord::Base
has_many :author_favorites
has_many :favorite_authors, -> { order('name') }, :through => :author_favorites
- has_many :taggings, :through => :posts
+ has_many :taggings, :through => :posts, :source => :taggings
has_many :taggings_2, :through => :posts, :source => :tagging
has_many :tags, :through => :posts
has_many :post_categories, :through => :posts, :source => :categories
diff --git a/activerecord/test/models/auto_id.rb b/activerecord/test/models/auto_id.rb
index d720e2be5e..82c6544bd5 100644
--- a/activerecord/test/models/auto_id.rb
+++ b/activerecord/test/models/auto_id.rb
@@ -1,4 +1,4 @@
class AutoId < ActiveRecord::Base
- def self.table_name () "auto_id_tests" end
- def self.primary_key () "auto_id" end
+ self.table_name = "auto_id_tests"
+ self.primary_key = "auto_id"
end
diff --git a/activerecord/test/models/book.rb b/activerecord/test/models/book.rb
index 5458a28cc9..4cb2c7606b 100644
--- a/activerecord/test/models/book.rb
+++ b/activerecord/test/models/book.rb
@@ -2,8 +2,16 @@ class Book < ActiveRecord::Base
has_many :authors
has_many :citations, :foreign_key => 'book1_id'
- has_many :references, -> { distinct }, :through => :citations, :source => :reference_of
+ has_many :references, -> { distinct }, through: :citations, source: :reference_of
has_many :subscriptions
- has_many :subscribers, :through => :subscriptions
+ has_many :subscribers, through: :subscriptions
+
+ enum status: [:proposed, :written, :published]
+ enum read_status: {unread: 0, reading: 2, read: 3}
+
+ def published!
+ super
+ "do publish work..."
+ end
end
diff --git a/activerecord/test/models/bulb.rb b/activerecord/test/models/bulb.rb
index 0109ef4f83..831a0d5387 100644
--- a/activerecord/test/models/bulb.rb
+++ b/activerecord/test/models/bulb.rb
@@ -37,3 +37,15 @@ class CustomBulb < Bulb
self.frickinawesome = true if name == 'Dude'
end
end
+
+class FunkyBulb < Bulb
+ before_destroy do
+ raise "before_destroy was called"
+ end
+end
+
+class FailedBulb < Bulb
+ before_destroy do
+ false
+ end
+end
diff --git a/activerecord/test/models/cake_designer.rb b/activerecord/test/models/cake_designer.rb
new file mode 100644
index 0000000000..9c57ef573a
--- /dev/null
+++ b/activerecord/test/models/cake_designer.rb
@@ -0,0 +1,3 @@
+class CakeDesigner < ActiveRecord::Base
+ has_one :chef, as: :employable
+end
diff --git a/activerecord/test/models/car.rb b/activerecord/test/models/car.rb
index ac42f444e1..c4a15a79e2 100644
--- a/activerecord/test/models/car.rb
+++ b/activerecord/test/models/car.rb
@@ -1,11 +1,11 @@
class Car < ActiveRecord::Base
-
has_many :bulbs
+ has_many :all_bulbs, -> { unscope where: :name }, class_name: "Bulb"
+ 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 :frickinawesome_bulbs, -> { where :frickinawesome => true }, :class_name => "Bulb"
has_one :bulb
- has_one :frickinawesome_bulb, -> { where :frickinawesome => true }, :class_name => "Bulb"
has_many :tyres
has_many :engines, :dependent => :destroy
diff --git a/activerecord/test/models/chef.rb b/activerecord/test/models/chef.rb
new file mode 100644
index 0000000000..67a4e54f06
--- /dev/null
+++ b/activerecord/test/models/chef.rb
@@ -0,0 +1,3 @@
+class Chef < ActiveRecord::Base
+ belongs_to :employable, polymorphic: true
+end
diff --git a/activerecord/test/models/citation.rb b/activerecord/test/models/citation.rb
index 545aa8110d..3d87eb795c 100644
--- a/activerecord/test/models/citation.rb
+++ b/activerecord/test/models/citation.rb
@@ -1,6 +1,3 @@
class Citation < ActiveRecord::Base
belongs_to :reference_of, :class_name => "Book", :foreign_key => :book2_id
-
- belongs_to :book1, :class_name => "Book", :foreign_key => :book1_id
- belongs_to :book2, :class_name => "Book", :foreign_key => :book2_id
end
diff --git a/activerecord/test/models/club.rb b/activerecord/test/models/club.rb
index 7d7c205041..566e0873f1 100644
--- a/activerecord/test/models/club.rb
+++ b/activerecord/test/models/club.rb
@@ -1,8 +1,7 @@
class Club < ActiveRecord::Base
has_one :membership
- has_many :memberships, :automatic_inverse_of => false
+ has_many :memberships, :inverse_of => false
has_many :members, :through => :memberships
- has_many :current_memberships
has_one :sponsor
has_one :sponsored_member, :through => :sponsor, :source => :sponsorable, :source_type => "Member"
belongs_to :category
diff --git a/activerecord/test/models/column_name.rb b/activerecord/test/models/column_name.rb
index ec07205a3a..460eb4fe20 100644
--- a/activerecord/test/models/column_name.rb
+++ b/activerecord/test/models/column_name.rb
@@ -1,3 +1,3 @@
class ColumnName < ActiveRecord::Base
- def self.table_name () "colnametests" end
-end \ No newline at end of file
+ self.table_name = "colnametests"
+end
diff --git a/activerecord/test/models/company.rb b/activerecord/test/models/company.rb
index dcda62e71d..76411ecb37 100644
--- a/activerecord/test/models/company.rb
+++ b/activerecord/test/models/company.rb
@@ -11,6 +11,11 @@ class Company < AbstractCompany
has_many :contracts
has_many :developers, :through => :contracts
+ scope :of_first_firm, lambda {
+ joins(:account => :firm).
+ where('firms.id' => 1)
+ }
+
def arbitrary_method
"I am Jack's profound disappointment"
end
@@ -35,17 +40,13 @@ module Namespaced
end
class Firm < Company
- ActiveSupport::Deprecation.silence do
- has_many :clients, -> { order "id" }, :dependent => :destroy, :counter_sql =>
- "SELECT COUNT(*) FROM companies WHERE firm_id = 1 " +
- "AND (#{QUOTED_TYPE} = 'Client' OR #{QUOTED_TYPE} = 'SpecialClient' OR #{QUOTED_TYPE} = 'VerySpecialClient' )",
- :before_remove => :log_before_remove,
- :after_remove => :log_after_remove
- end
+ to_param :name
+
+ has_many :clients, -> { order "id" }, :dependent => :destroy, :before_remove => :log_before_remove, :after_remove => :log_after_remove
has_many :unsorted_clients, :class_name => "Client"
has_many :unsorted_clients_with_symbol, :class_name => :Client
has_many :clients_sorted_desc, -> { order "id DESC" }, :class_name => "Client"
- has_many :clients_of_firm, -> { order "id" }, :foreign_key => "client_of", :class_name => "Client"
+ has_many :clients_of_firm, -> { order "id" }, :foreign_key => "client_of", :class_name => "Client", :inverse_of => :firm
has_many :clients_ordered_by_name, -> { order "name" }, :class_name => "Client"
has_many :unvalidated_clients_of_firm, :foreign_key => "client_of", :class_name => "Client", :validate => false
has_many :dependent_clients_of_firm, -> { order "id" }, :foreign_key => "client_of", :class_name => "Client", :dependent => :destroy
@@ -54,21 +55,7 @@ class Firm < Company
has_many :clients_with_interpolated_conditions, ->(firm) { where "rating > #{firm.rating}" }, :class_name => "Client"
has_many :clients_like_ms, -> { where("name = 'Microsoft'").order("id") }, :class_name => "Client"
has_many :clients_like_ms_with_hash_conditions, -> { where(:name => 'Microsoft').order("id") }, :class_name => "Client"
- ActiveSupport::Deprecation.silence do
- has_many :clients_using_sql, :class_name => "Client", :finder_sql => proc { "SELECT * FROM companies WHERE client_of = #{id}" }
- has_many :clients_using_counter_sql, :class_name => "Client",
- :finder_sql => proc { "SELECT * FROM companies WHERE client_of = #{id} " },
- :counter_sql => proc { "SELECT COUNT(*) FROM companies WHERE client_of = #{id}" }
- has_many :clients_using_zero_counter_sql, :class_name => "Client",
- :finder_sql => proc { "SELECT * FROM companies WHERE client_of = #{id}" },
- :counter_sql => proc { "SELECT 0 FROM companies WHERE client_of = #{id}" }
- has_many :no_clients_using_counter_sql, :class_name => "Client",
- :finder_sql => 'SELECT * FROM companies WHERE client_of = 1000',
- :counter_sql => 'SELECT COUNT(*) FROM companies WHERE client_of = 1000'
- has_many :clients_using_finder_sql, :class_name => "Client", :finder_sql => 'SELECT * FROM companies WHERE 1=1'
- end
has_many :plain_clients, :class_name => 'Client'
- has_many :readonly_clients, -> { readonly }, :class_name => 'Client'
has_many :clients_using_primary_key, :class_name => 'Client',
:primary_key => 'name', :foreign_key => 'firm_name'
has_many :clients_using_primary_key_with_delete_all, :class_name => 'Client',
@@ -114,13 +101,6 @@ class DependentFirm < Company
has_one :company, :foreign_key => 'client_of', :dependent => :nullify
end
-class RestrictedFirm < Company
- ActiveSupport::Deprecation.silence do
- has_one :account, -> { order("id") }, :foreign_key => "firm_id", :dependent => :restrict
- has_many :companies, -> { order("id") }, :foreign_key => 'client_of', :dependent => :restrict
- end
-end
-
class RestrictedWithExceptionFirm < Company
has_one :account, -> { order("id") }, :foreign_key => "firm_id", :dependent => :restrict_with_exception
has_many :companies, -> { order("id") }, :foreign_key => 'client_of', :dependent => :restrict_with_exception
@@ -141,9 +121,13 @@ class Client < Company
belongs_to :firm_with_primary_key_symbols, :class_name => "Firm", :primary_key => :name, :foreign_key => :firm_name
belongs_to :readonly_firm, -> { readonly }, :class_name => "Firm", :foreign_key => "firm_id"
belongs_to :bob_firm, -> { where :name => "Bob" }, :class_name => "Firm", :foreign_key => "client_of"
- has_many :accounts, :through => :firm
+ has_many :accounts, :through => :firm, :source => :accounts
belongs_to :account
+ validate do
+ firm
+ end
+
class RaisedOnSave < RuntimeError; end
attr_accessor :raise_on_save
before_save do
@@ -193,7 +177,6 @@ class ExclusivelyDependentFirm < Company
has_one :account, :foreign_key => "firm_id", :dependent => :delete
has_many :dependent_sanitized_conditional_clients_of_firm, -> { order("id").where("name = 'BigShot Inc.'") }, :foreign_key => "client_of", :class_name => "Client", :dependent => :delete_all
has_many :dependent_conditional_clients_of_firm, -> { order("id").where("name = ?", 'BigShot Inc.') }, :foreign_key => "client_of", :class_name => "Client", :dependent => :delete_all
- has_many :dependent_hash_conditional_clients_of_firm, -> { order("id").where(:name => 'BigShot Inc.') }, :foreign_key => "client_of", :class_name => "Client", :dependent => :delete_all
end
class SpecialClient < Client
diff --git a/activerecord/test/models/company_in_module.rb b/activerecord/test/models/company_in_module.rb
index 461bb0de09..38b0b6aafa 100644
--- a/activerecord/test/models/company_in_module.rb
+++ b/activerecord/test/models/company_in_module.rb
@@ -10,10 +10,6 @@ module MyApplication
has_many :clients_sorted_desc, -> { order("id DESC") }, :class_name => "Client"
has_many :clients_of_firm, -> { order "id" }, :foreign_key => "client_of", :class_name => "Client"
has_many :clients_like_ms, -> { where("name = 'Microsoft'").order("id") }, :class_name => "Client"
- ActiveSupport::Deprecation.silence do
- has_many :clients_using_sql, :class_name => "Client", :finder_sql => 'SELECT * FROM companies WHERE client_of = #{id}'
- end
-
has_one :account, :class_name => 'MyApplication::Billing::Account', :dependent => :destroy
end
diff --git a/activerecord/test/models/contract.rb b/activerecord/test/models/contract.rb
index 2cf5aa7a85..cdf7b267b5 100644
--- a/activerecord/test/models/contract.rb
+++ b/activerecord/test/models/contract.rb
@@ -1,6 +1,7 @@
class Contract < ActiveRecord::Base
belongs_to :company
belongs_to :developer
+ belongs_to :firm, :foreign_key => 'company_id'
before_save :hi
after_save :bye
diff --git a/activerecord/test/models/department.rb b/activerecord/test/models/department.rb
new file mode 100644
index 0000000000..08004a0ed3
--- /dev/null
+++ b/activerecord/test/models/department.rb
@@ -0,0 +1,4 @@
+class Department < ActiveRecord::Base
+ has_many :chefs
+ belongs_to :hotel
+end
diff --git a/activerecord/test/models/developer.rb b/activerecord/test/models/developer.rb
index 81bc87bd42..2e2d8a0d37 100644
--- a/activerecord/test/models/developer.rb
+++ b/activerecord/test/models/developer.rb
@@ -1,11 +1,5 @@
require 'ostruct'
-module DeveloperProjectsAssociationExtension
- def find_most_recent
- order("id DESC").first
- end
-end
-
module DeveloperProjectsAssociationExtension2
def find_least_recent
order("id ASC").first
@@ -42,8 +36,14 @@ class Developer < ActiveRecord::Base
end
has_and_belongs_to_many :special_projects, :join_table => 'developers_projects', :association_foreign_key => 'project_id'
+ has_and_belongs_to_many :sym_special_projects,
+ :join_table => :developers_projects,
+ :association_foreign_key => 'project_id',
+ :class_name => 'SpecialProject'
has_many :audit_logs
+ has_many :contracts
+ has_many :firms, :through => :contracts, :source => :firm
scope :jamises, -> { where(:name => 'Jamis') }
diff --git a/activerecord/test/models/drink_designer.rb b/activerecord/test/models/drink_designer.rb
new file mode 100644
index 0000000000..2db968ef11
--- /dev/null
+++ b/activerecord/test/models/drink_designer.rb
@@ -0,0 +1,3 @@
+class DrinkDesigner < ActiveRecord::Base
+ has_one :chef, as: :employable
+end
diff --git a/activerecord/test/models/hotel.rb b/activerecord/test/models/hotel.rb
new file mode 100644
index 0000000000..b352cd22f3
--- /dev/null
+++ b/activerecord/test/models/hotel.rb
@@ -0,0 +1,6 @@
+class Hotel < ActiveRecord::Base
+ has_many :departments
+ has_many :chefs, through: :departments
+ has_many :cake_designers, source_type: 'CakeDesigner', source: :employable, through: :chefs
+ has_many :drink_designers, source_type: 'DrinkDesigner', source: :employable, through: :chefs
+end
diff --git a/activerecord/test/models/interest.rb b/activerecord/test/models/interest.rb
index f772bb1c7f..d5d9226204 100644
--- a/activerecord/test/models/interest.rb
+++ b/activerecord/test/models/interest.rb
@@ -1,5 +1,5 @@
class Interest < ActiveRecord::Base
- belongs_to :man, :inverse_of => :interests, :automatic_inverse_of => false
+ belongs_to :man, :inverse_of => :interests
belongs_to :polymorphic_man, :polymorphic => true, :inverse_of => :polymorphic_interests
belongs_to :zine, :inverse_of => :interests
end
diff --git a/activerecord/test/models/man.rb b/activerecord/test/models/man.rb
index 49f002aa9a..f4d127730c 100644
--- a/activerecord/test/models/man.rb
+++ b/activerecord/test/models/man.rb
@@ -1,9 +1,10 @@
class Man < ActiveRecord::Base
has_one :face, :inverse_of => :man
has_one :polymorphic_face, :class_name => 'Face', :as => :polymorphic_man, :inverse_of => :polymorphic_man
- has_many :interests, :inverse_of => :man, :automatic_inverse_of => false
+ has_many :interests, :inverse_of => :man
has_many :polymorphic_interests, :class_name => 'Interest', :as => :polymorphic_man, :inverse_of => :polymorphic_man
# These are "broken" inverse_of associations for the purposes of testing
has_one :dirty_face, :class_name => 'Face', :inverse_of => :dirty_man
has_many :secret_interests, :class_name => 'Interest', :inverse_of => :secret_man
+ has_one :mixed_case_monkey
end
diff --git a/activerecord/test/models/member.rb b/activerecord/test/models/member.rb
index b81304b8e0..72095f9236 100644
--- a/activerecord/test/models/member.rb
+++ b/activerecord/test/models/member.rb
@@ -2,14 +2,13 @@ class Member < ActiveRecord::Base
has_one :current_membership
has_one :selected_membership
has_one :membership
- has_many :fellow_members, :through => :club, :source => :members
has_one :club, :through => :current_membership
has_one :selected_club, :through => :selected_membership, :source => :club
has_one :favourite_club, -> { where "memberships.favourite = ?", true }, :through => :membership, :source => :club
has_one :hairy_club, -> { where :clubs => {:name => "Moustache and Eyebrow Fancier Club"} }, :through => :membership, :source => :club
has_one :sponsor, :as => :sponsorable
has_one :sponsor_club, :through => :sponsor
- has_one :member_detail, :automatic_inverse_of => false
+ has_one :member_detail, :inverse_of => false
has_one :organization, :through => :member_detail
belongs_to :member_type
diff --git a/activerecord/test/models/member_detail.rb b/activerecord/test/models/member_detail.rb
index a256c73c7e..9d253aa126 100644
--- a/activerecord/test/models/member_detail.rb
+++ b/activerecord/test/models/member_detail.rb
@@ -1,5 +1,5 @@
class MemberDetail < ActiveRecord::Base
- belongs_to :member, :automatic_inverse_of => false
+ belongs_to :member, :inverse_of => false
belongs_to :organization
has_one :member_type, :through => :member
diff --git a/activerecord/test/models/membership.rb b/activerecord/test/models/membership.rb
index bcbb7e42c5..df7167ee93 100644
--- a/activerecord/test/models/membership.rb
+++ b/activerecord/test/models/membership.rb
@@ -8,6 +8,11 @@ class CurrentMembership < Membership
belongs_to :club
end
+class SuperMembership < Membership
+ belongs_to :member, -> { order('members.id DESC') }
+ belongs_to :club
+end
+
class SelectedMembership < Membership
def self.default_scope
select("'1' as foo")
diff --git a/activerecord/test/models/mixed_case_monkey.rb b/activerecord/test/models/mixed_case_monkey.rb
index 763baefd91..4d37371777 100644
--- a/activerecord/test/models/mixed_case_monkey.rb
+++ b/activerecord/test/models/mixed_case_monkey.rb
@@ -1,3 +1,5 @@
class MixedCaseMonkey < ActiveRecord::Base
self.primary_key = 'monkeyID'
+
+ belongs_to :man
end
diff --git a/activerecord/test/models/movie.rb b/activerecord/test/models/movie.rb
index 6384b4c801..c441be2bef 100644
--- a/activerecord/test/models/movie.rb
+++ b/activerecord/test/models/movie.rb
@@ -1,5 +1,3 @@
class Movie < ActiveRecord::Base
- def self.primary_key
- "movieid"
- end
+ self.primary_key = "movieid"
end
diff --git a/activerecord/test/models/parrot.rb b/activerecord/test/models/parrot.rb
index c4ee2bd19d..e76e83f314 100644
--- a/activerecord/test/models/parrot.rb
+++ b/activerecord/test/models/parrot.rb
@@ -21,3 +21,9 @@ end
class DeadParrot < Parrot
belongs_to :killer, :class_name => 'Pirate'
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 2985160d28..1a282dbce4 100644
--- a/activerecord/test/models/person.rb
+++ b/activerecord/test/models/person.rb
@@ -32,6 +32,7 @@ class Person < ActiveRecord::Base
has_many :agents_posts, :through => :agents, :source => :posts
has_many :agents_posts_authors, :through => :agents_posts, :source => :author
+ has_many :essays, primary_key: "first_name", foreign_key: "writer_id"
scope :males, -> { where(:gender => 'M') }
scope :females, -> { where(:gender => 'F') }
diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb
index 93a7a2073c..faf539a562 100644
--- a/activerecord/test/models/post.rb
+++ b/activerecord/test/models/post.rb
@@ -1,4 +1,10 @@
class Post < ActiveRecord::Base
+ class CategoryPost < ActiveRecord::Base
+ self.table_name = "categories_posts"
+ belongs_to :category
+ belongs_to :post
+ end
+
module NamedExtension
def author
'lifo'
@@ -59,6 +65,9 @@ class Post < ActiveRecord::Base
has_many :author_favorites, :through => :author
has_many :author_categorizations, :through => :author, :source => :categorizations
has_many :author_addresses, :through => :author
+ has_many :author_address_extra_with_address,
+ through: :author_with_address,
+ source: :author_address_extra
has_many :comments_with_interpolated_conditions,
->(p) { where "#{"#{p.aliased_table_name}." rescue ""}body = ?", 'Thank you for the welcome' },
@@ -72,6 +81,8 @@ class Post < ActiveRecord::Base
has_many :special_comments_ratings, :through => :special_comments, :source => :ratings
has_many :special_comments_ratings_taggings, :through => :special_comments_ratings, :source => :taggings
+ has_many :category_posts, :class_name => 'CategoryPost'
+ has_many :scategories, through: :category_posts, source: :category
has_and_belongs_to_many :categories
has_and_belongs_to_many :special_categories, :join_table => "categories_posts", :association_foreign_key => 'category_id'
@@ -122,7 +133,6 @@ class Post < ActiveRecord::Base
has_many :secure_readers
has_many :readers_with_person, -> { includes(:person) }, :class_name => "Reader"
has_many :people, :through => :readers
- has_many :secure_people, :through => :secure_readers
has_many :single_people, :through => :readers
has_many :people_with_callbacks, :source=>:person, :through => :readers,
:before_add => lambda {|owner, reader| log(:added, :before, reader.first_name) },
diff --git a/activerecord/test/models/project.rb b/activerecord/test/models/project.rb
index f893754b9f..7f42a4b1f8 100644
--- a/activerecord/test/models/project.rb
+++ b/activerecord/test/models/project.rb
@@ -1,25 +1,11 @@
class Project < ActiveRecord::Base
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 :selected_developers, -> { distinct.select "developers.*" }, :class_name => "Developer"
has_and_belongs_to_many :non_unique_developers, -> { order 'developers.name desc, developers.id desc' }, :class_name => 'Developer'
has_and_belongs_to_many :limited_developers, -> { limit 1 }, :class_name => "Developer"
has_and_belongs_to_many :developers_named_david, -> { where("name = 'David'").distinct }, :class_name => "Developer"
has_and_belongs_to_many :developers_named_david_with_hash_conditions, -> { where(:name => 'David').distinct }, :class_name => "Developer"
has_and_belongs_to_many :salaried_developers, -> { where "salary > 0" }, :class_name => "Developer"
-
- ActiveSupport::Deprecation.silence do
- has_and_belongs_to_many :developers_with_finder_sql, :class_name => "Developer", :finder_sql => proc { "SELECT t.*, j.* FROM developers_projects j, developers t WHERE t.id = j.developer_id AND j.project_id = #{id} ORDER BY t.id" }
- has_and_belongs_to_many :developers_with_multiline_finder_sql, :class_name => "Developer", :finder_sql => proc {
- "SELECT
- t.*, j.*
- FROM
- developers_projects j,
- developers t WHERE t.id = j.developer_id AND j.project_id = #{id} ORDER BY t.id"
- }
- has_and_belongs_to_many :developers_by_sql, :class_name => "Developer", :delete_sql => proc { |record| "DELETE FROM developers_projects WHERE project_id = #{id} AND developer_id = #{record.id}" }
- end
-
has_and_belongs_to_many :developers_with_callbacks, :class_name => "Developer", :before_add => Proc.new {|o, r| o.developers_log << "before_adding#{r.id || '<new>'}"},
:after_add => Proc.new {|o, r| o.developers_log << "after_adding#{r.id || '<new>'}"},
:before_remove => Proc.new {|o, r| o.developers_log << "before_removing#{r.id}"},
diff --git a/activerecord/test/models/topic.rb b/activerecord/test/models/topic.rb
index 17035bf338..f81ffe1d90 100644
--- a/activerecord/test/models/topic.rb
+++ b/activerecord/test/models/topic.rb
@@ -34,7 +34,6 @@ class Topic < ActiveRecord::Base
has_many :replies, :dependent => :destroy, :foreign_key => "parent_id"
has_many :approved_replies, -> { approved }, class_name: 'Reply', foreign_key: "parent_id", counter_cache: 'replies_count'
- has_many :replies_with_primary_key, :class_name => "Reply", :dependent => :destroy, :primary_key => "title", :foreign_key => "parent_title"
has_many :unique_replies, :dependent => :destroy, :foreign_key => "parent_id"
has_many :silly_unique_replies, :dependent => :destroy, :foreign_key => "parent_id"
@@ -107,6 +106,10 @@ class ImportantTopic < Topic
serialize :important, Hash
end
+class DefaultRejectedTopic < Topic
+ default_scope -> { where(approved: false) }
+end
+
class BlankTopic < Topic
# declared here to make sure that dynamic finder with a bang can find a model that responds to `blank?`
def blank?
diff --git a/activerecord/test/schema/oracle_specific_schema.rb b/activerecord/test/schema/oracle_specific_schema.rb
index 3314687445..a7817772f4 100644
--- a/activerecord/test/schema/oracle_specific_schema.rb
+++ b/activerecord/test/schema/oracle_specific_schema.rb
@@ -3,7 +3,6 @@ ActiveRecord::Schema.define do
execute "drop table test_oracle_defaults" rescue nil
execute "drop sequence test_oracle_defaults_seq" rescue nil
execute "drop sequence companies_nonstd_seq" rescue nil
- execute "drop synonym subjects" rescue nil
execute "drop table defaults" rescue nil
execute "drop sequence defaults_seq" rescue nil
@@ -22,8 +21,6 @@ create sequence test_oracle_defaults_seq minvalue 10000
execute "create sequence companies_nonstd_seq minvalue 10000"
- execute "create synonym subjects for topics"
-
execute <<-SQL
CREATE TABLE defaults (
id integer not null,
diff --git a/activerecord/test/schema/postgresql_specific_schema.rb b/activerecord/test/schema/postgresql_specific_schema.rb
index 6b7012a172..a86a188bcf 100644
--- a/activerecord/test/schema/postgresql_specific_schema.rb
+++ b/activerecord/test/schema/postgresql_specific_schema.rb
@@ -1,6 +1,6 @@
ActiveRecord::Schema.define do
- %w(postgresql_ranges postgresql_tsvectors postgresql_hstores postgresql_arrays postgresql_moneys postgresql_numbers postgresql_times postgresql_network_addresses postgresql_bit_strings postgresql_uuids postgresql_ltrees
+ %w(postgresql_tsvectors postgresql_hstores postgresql_arrays postgresql_moneys postgresql_numbers postgresql_times postgresql_network_addresses postgresql_bit_strings postgresql_uuids postgresql_ltrees
postgresql_oids postgresql_xml_data_type defaults geometrics postgresql_timestamp_with_zones postgresql_partitioned_table postgresql_partitioned_table_parent postgresql_json_data_type).each do |table_name|
execute "DROP TABLE IF EXISTS #{quote_table_name table_name}"
end
@@ -74,18 +74,6 @@ _SQL
);
_SQL
- execute <<_SQL if supports_ranges?
- CREATE TABLE postgresql_ranges (
- id SERIAL PRIMARY KEY,
- date_range daterange,
- num_range numrange,
- ts_range tsrange,
- tstz_range tstzrange,
- int4_range int4range,
- int8_range int8range
- );
-_SQL
-
execute <<_SQL
CREATE TABLE postgresql_tsvectors (
id SERIAL PRIMARY KEY,
@@ -221,4 +209,3 @@ _SQL
t.text :text, limit: 100_000
end
end
-
diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb
index 188a3f0164..ddfc1ac0d6 100644
--- a/activerecord/test/schema/schema.rb
+++ b/activerecord/test/schema/schema.rb
@@ -1,3 +1,5 @@
+# encoding: utf-8
+
ActiveRecord::Schema.define do
def except(adapter_names_to_exclude)
unless [adapter_names_to_exclude].flatten.include?(adapter_name)
@@ -25,111 +27,114 @@ ActiveRecord::Schema.define do
# #
# ------------------------------------------------------------------- #
- create_table :accounts, :force => true do |t|
+ create_table :accounts, force: true do |t|
t.integer :firm_id
t.string :firm_name
t.integer :credit_limit
end
- create_table :admin_accounts, :force => true do |t|
+ create_table :admin_accounts, force: true do |t|
t.string :name
end
- create_table :admin_users, :force => true do |t|
+ create_table :admin_users, force: true do |t|
t.string :name
- t.string :settings, :null => true, :limit => 1024
+ t.string :settings, null: true, limit: 1024
# MySQL does not allow default values for blobs. Fake it out with a
# big varchar below.
- t.string :preferences, :null => true, :default => '', :limit => 1024
- t.string :json_data, :null => true, :limit => 1024
- t.string :json_data_empty, :null => true, :default => "", :limit => 1024
+ t.string :preferences, null: true, default: '', limit: 1024
+ t.string :json_data, null: true, limit: 1024
+ t.string :json_data_empty, null: true, default: "", limit: 1024
+ t.text :params
t.references :account
end
- create_table :aircraft, :force => true do |t|
+ create_table :aircraft, force: true do |t|
t.string :name
end
- create_table :audit_logs, :force => true do |t|
- t.column :message, :string, :null=>false
- t.column :developer_id, :integer, :null=>false
+ create_table :audit_logs, force: true do |t|
+ t.column :message, :string, null: false
+ t.column :developer_id, :integer, null: false
t.integer :unvalidated_developer_id
end
- create_table :authors, :force => true do |t|
- t.string :name, :null => false
+ create_table :authors, force: true do |t|
+ t.string :name, null: false
t.integer :author_address_id
t.integer :author_address_extra_id
t.string :organization_id
t.string :owned_essay_id
end
- create_table :author_addresses, :force => true do |t|
+ create_table :author_addresses, force: true do |t|
end
- create_table :author_favorites, :force => true do |t|
+ create_table :author_favorites, force: true do |t|
t.column :author_id, :integer
t.column :favorite_author_id, :integer
end
- create_table :auto_id_tests, :force => true, :id => false do |t|
+ create_table :auto_id_tests, force: true, id: false do |t|
t.primary_key :auto_id
t.integer :value
end
- create_table :binaries, :force => true do |t|
+ create_table :binaries, force: true do |t|
t.string :name
t.binary :data
- t.binary :short_data, :limit => 2048
+ t.binary :short_data, limit: 2048
end
- create_table :birds, :force => true do |t|
+ create_table :birds, force: true do |t|
t.string :name
t.string :color
t.integer :pirate_id
end
- create_table :books, :force => true do |t|
+ create_table :books, force: true do |t|
t.integer :author_id
t.column :name, :string
+ t.column :status, :integer, default: 0
+ t.column :read_status, :integer, default: 0
end
- create_table :booleans, :force => true do |t|
+ create_table :booleans, force: true do |t|
t.boolean :value
- t.boolean :has_fun, :null => false, :default => false
+ t.boolean :has_fun, null: false, default: false
end
- create_table :bulbs, :force => true do |t|
+ create_table :bulbs, force: true do |t|
t.integer :car_id
t.string :name
t.boolean :frickinawesome
t.string :color
end
- create_table "CamelCase", :force => true do |t|
+ create_table "CamelCase", force: true do |t|
t.string :name
end
- create_table :cars, :force => true do |t|
+ create_table :cars, force: true do |t|
t.string :name
t.integer :engines_count
t.integer :wheels_count
- t.column :lock_version, :integer, :null => false, :default => 0
+ t.column :lock_version, :integer, null: false, default: 0
t.timestamps
end
- create_table :categories, :force => true do |t|
- t.string :name, :null => false
+ create_table :categories, force: true do |t|
+ t.string :name, null: false
t.string :type
t.integer :categorizations_count
end
- create_table :categories_posts, :force => true, :id => false do |t|
- t.integer :category_id, :null => false
- t.integer :post_id, :null => false
+ create_table :categories_posts, force: true, id: false do |t|
+ t.integer :category_id, null: false
+ t.integer :post_id, null: false
end
- create_table :categorizations, :force => true do |t|
+ create_table :categorizations, force: true do |t|
t.column :category_id, :integer
t.string :named_category_name
t.column :post_id, :integer
@@ -137,98 +142,98 @@ ActiveRecord::Schema.define do
t.column :special, :boolean
end
- create_table :citations, :force => true do |t|
+ create_table :citations, force: true do |t|
t.column :book1_id, :integer
t.column :book2_id, :integer
end
- create_table :clubs, :force => true do |t|
+ create_table :clubs, force: true do |t|
t.string :name
t.integer :category_id
end
- create_table :collections, :force => true do |t|
+ create_table :collections, force: true do |t|
t.string :name
end
- create_table :colnametests, :force => true do |t|
- t.integer :references, :null => false
+ create_table :colnametests, force: true do |t|
+ t.integer :references, null: false
end
- create_table :comments, :force => true do |t|
- t.integer :post_id, :null => false
+ create_table :comments, force: true do |t|
+ t.integer :post_id, null: false
# use VARCHAR2(4000) instead of CLOB datatype as CLOB data type has many limitations in
# Oracle SELECT WHERE clause which causes many unit test failures
if current_adapter?(:OracleAdapter)
- t.string :body, :null => false, :limit => 4000
+ t.string :body, null: false, limit: 4000
else
- t.text :body, :null => false
+ t.text :body, null: false
end
t.string :type
- t.integer :taggings_count, :default => 0
- t.integer :children_count, :default => 0
+ t.integer :taggings_count, default: 0
+ t.integer :children_count, default: 0
t.integer :parent_id
end
- create_table :companies, :force => true do |t|
+ create_table :companies, force: true do |t|
t.string :type
t.integer :firm_id
t.string :firm_name
t.string :name
t.integer :client_of
- t.integer :rating, :default => 1
+ t.integer :rating, default: 1
t.integer :account_id
- t.string :description, :default => ""
+ t.string :description, default: ""
end
- add_index :companies, [:firm_id, :type, :rating], :name => "company_index"
- add_index :companies, [:firm_id, :type], :name => "company_partial_index", :where => "rating > 10"
- add_index :companies, :name, :name => 'company_name_index', :using => :btree
+ add_index :companies, [:firm_id, :type, :rating], name: "company_index"
+ 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 :vegetables, :force => true do |t|
+ create_table :vegetables, force: true do |t|
t.string :name
t.integer :seller_id
t.string :custom_type
end
- create_table :computers, :force => true do |t|
- t.integer :developer, :null => false
- t.integer :extendedWarranty, :null => false
+ create_table :computers, force: true do |t|
+ t.integer :developer, null: false
+ t.integer :extendedWarranty, null: false
end
- create_table :contracts, :force => true do |t|
+ create_table :contracts, force: true do |t|
t.integer :developer_id
t.integer :company_id
end
- create_table :customers, :force => true do |t|
+ create_table :customers, force: true do |t|
t.string :name
- t.integer :balance, :default => 0
+ t.integer :balance, default: 0
t.string :address_street
t.string :address_city
t.string :address_country
t.string :gps_location
end
- create_table :dashboards, :force => true, :id => false do |t|
+ create_table :dashboards, force: true, id: false do |t|
t.string :dashboard_id
t.string :name
end
- create_table :developers, :force => true do |t|
+ create_table :developers, force: true do |t|
t.string :name
- t.integer :salary, :default => 70000
+ t.integer :salary, default: 70000
t.datetime :created_at
t.datetime :updated_at
t.datetime :created_on
t.datetime :updated_on
end
- create_table :developers_projects, :force => true, :id => false do |t|
- t.integer :developer_id, :null => false
- t.integer :project_id, :null => false
+ create_table :developers_projects, force: true, id: false do |t|
+ t.integer :developer_id, null: false
+ t.integer :project_id, null: false
t.date :joined_on
- t.integer :access_level, :default => 1
+ t.integer :access_level, default: 1
end
create_table :dog_lovers, force: true do |t|
@@ -237,28 +242,29 @@ ActiveRecord::Schema.define do
t.integer :dogs_count, default: 0
end
- create_table :dogs, :force => true do |t|
+ create_table :dogs, force: true do |t|
t.integer :trainer_id
t.integer :breeder_id
t.integer :dog_lover_id
+ t.string :alias
end
- create_table :edges, :force => true, :id => false do |t|
- t.column :source_id, :integer, :null => false
- t.column :sink_id, :integer, :null => false
+ create_table :edges, force: true, id: false do |t|
+ t.column :source_id, :integer, null: false
+ t.column :sink_id, :integer, null: false
end
- add_index :edges, [:source_id, :sink_id], :unique => true, :name => 'unique_edge_index'
+ add_index :edges, [:source_id, :sink_id], unique: true, name: 'unique_edge_index'
- create_table :engines, :force => true do |t|
+ create_table :engines, force: true do |t|
t.integer :car_id
end
- create_table :entrants, :force => true do |t|
- t.string :name, :null => false
- t.integer :course_id, :null => false
+ create_table :entrants, force: true do |t|
+ t.string :name, null: false
+ t.integer :course_id, null: false
end
- create_table :essays, :force => true do |t|
+ create_table :essays, force: true do |t|
t.string :name
t.string :writer_id
t.string :writer_type
@@ -266,153 +272,153 @@ ActiveRecord::Schema.define do
t.string :author_id
end
- create_table :events, :force => true do |t|
- t.string :title, :limit => 5
+ create_table :events, force: true do |t|
+ t.string :title, limit: 5
end
- create_table :eyes, :force => true do |t|
+ create_table :eyes, force: true do |t|
end
- create_table :funny_jokes, :force => true do |t|
+ create_table :funny_jokes, force: true do |t|
t.string :name
end
- create_table :cold_jokes, :force => true do |t|
+ create_table :cold_jokes, force: true do |t|
t.string :name
end
- create_table :friendships, :force => true do |t|
+ create_table :friendships, force: true do |t|
t.integer :friend_id
t.integer :follower_id
end
- create_table :goofy_string_id, :force => true, :id => false do |t|
- t.string :id, :null => false
+ create_table :goofy_string_id, force: true, id: false do |t|
+ t.string :id, null: false
t.string :info
end
- create_table :having, :force => true do |t|
+ create_table :having, force: true do |t|
t.string :where
end
- create_table :guids, :force => true do |t|
+ create_table :guids, force: true do |t|
t.column :key, :string
end
- create_table :inept_wizards, :force => true do |t|
- t.column :name, :string, :null => false
- t.column :city, :string, :null => false
+ create_table :inept_wizards, force: true do |t|
+ t.column :name, :string, null: false
+ t.column :city, :string, null: false
t.column :type, :string
end
- create_table :integer_limits, :force => true do |t|
+ create_table :integer_limits, force: true do |t|
t.integer :"c_int_without_limit"
(1..8).each do |i|
- t.integer :"c_int_#{i}", :limit => i
+ t.integer :"c_int_#{i}", limit: i
end
end
- create_table :invoices, :force => true do |t|
+ create_table :invoices, force: true do |t|
t.integer :balance
t.datetime :updated_at
end
- create_table :iris, :force => true do |t|
+ create_table :iris, force: true do |t|
t.references :eye
t.string :color
end
- create_table :items, :force => true do |t|
+ create_table :items, force: true do |t|
t.column :name, :string
end
- create_table :jobs, :force => true do |t|
+ create_table :jobs, force: true do |t|
t.integer :ideal_reference_id
end
- create_table :keyboards, :force => true, :id => false do |t|
+ create_table :keyboards, force: true, id: false do |t|
t.primary_key :key_number
t.string :name
end
- create_table :legacy_things, :force => true do |t|
+ create_table :legacy_things, force: true do |t|
t.integer :tps_report_number
- t.integer :version, :null => false, :default => 0
+ t.integer :version, null: false, default: 0
end
- create_table :lessons, :force => true do |t|
+ create_table :lessons, force: true do |t|
t.string :name
end
- create_table :lessons_students, :id => false, :force => true do |t|
+ create_table :lessons_students, id: false, force: true do |t|
t.references :lesson
t.references :student
end
- create_table :lint_models, :force => true
+ create_table :lint_models, force: true
- create_table :line_items, :force => true do |t|
+ create_table :line_items, force: true do |t|
t.integer :invoice_id
t.integer :amount
end
- create_table :lock_without_defaults, :force => true do |t|
+ create_table :lock_without_defaults, force: true do |t|
t.column :lock_version, :integer
end
- create_table :lock_without_defaults_cust, :force => true do |t|
+ create_table :lock_without_defaults_cust, force: true do |t|
t.column :custom_lock_version, :integer
end
- create_table :mateys, :id => false, :force => true do |t|
+ create_table :mateys, id: false, force: true do |t|
t.column :pirate_id, :integer
t.column :target_id, :integer
t.column :weight, :integer
end
- create_table :members, :force => true do |t|
+ create_table :members, force: true do |t|
t.string :name
t.integer :member_type_id
end
- create_table :member_details, :force => true do |t|
+ create_table :member_details, force: true do |t|
t.integer :member_id
t.integer :organization_id
t.string :extra_data
end
- create_table :member_friends, :force => true, :id => false do |t|
+ create_table :member_friends, force: true, id: false do |t|
t.integer :member_id
t.integer :friend_id
end
- create_table :memberships, :force => true do |t|
+ create_table :memberships, force: true do |t|
t.datetime :joined_on
t.integer :club_id, :member_id
- t.boolean :favourite, :default => false
+ t.boolean :favourite, default: false
t.string :type
end
- create_table :member_types, :force => true do |t|
+ create_table :member_types, force: true do |t|
t.string :name
end
- create_table :minivans, :force => true, :id => false do |t|
+ create_table :minivans, force: true, id: false do |t|
t.string :minivan_id
t.string :name
t.string :speedometer_id
t.string :color
end
- create_table :minimalistics, :force => true do |t|
+ create_table :minimalistics, force: true do |t|
end
- create_table :mixed_case_monkeys, :force => true, :id => false do |t|
+ create_table :mixed_case_monkeys, force: true, id: false do |t|
t.primary_key :monkeyID
t.integer :fleaCount
end
- create_table :mixins, :force => true do |t|
+ create_table :mixins, force: true do |t|
t.integer :parent_id
t.integer :pos
t.datetime :created_at
@@ -423,52 +429,52 @@ ActiveRecord::Schema.define do
t.string :type
end
- create_table :movies, :force => true, :id => false do |t|
+ create_table :movies, force: true, id: false do |t|
t.primary_key :movieid
t.string :name
end
- create_table :numeric_data, :force => true do |t|
- t.decimal :bank_balance, :precision => 10, :scale => 2
- t.decimal :big_bank_balance, :precision => 15, :scale => 2
- t.decimal :world_population, :precision => 10, :scale => 0
- t.decimal :my_house_population, :precision => 2, :scale => 0
- t.decimal :decimal_number_with_default, :precision => 3, :scale => 2, :default => 2.78
+ create_table :numeric_data, force: true do |t|
+ t.decimal :bank_balance, precision: 10, scale: 2
+ t.decimal :big_bank_balance, precision: 15, scale: 2
+ t.decimal :world_population, precision: 10, scale: 0
+ t.decimal :my_house_population, precision: 2, scale: 0
+ t.decimal :decimal_number_with_default, precision: 3, scale: 2, default: 2.78
t.float :temperature
# Oracle/SQLServer supports precision up to 38
- if current_adapter?(:OracleAdapter,:SQLServerAdapter)
- t.decimal :atoms_in_universe, :precision => 38, :scale => 0
+ if current_adapter?(:OracleAdapter, :SQLServerAdapter)
+ t.decimal :atoms_in_universe, precision: 38, scale: 0
else
- t.decimal :atoms_in_universe, :precision => 55, :scale => 0
+ t.decimal :atoms_in_universe, precision: 55, scale: 0
end
end
- create_table :orders, :force => true do |t|
+ create_table :orders, force: true do |t|
t.string :name
t.integer :billing_customer_id
t.integer :shipping_customer_id
end
- create_table :organizations, :force => true do |t|
+ create_table :organizations, force: true do |t|
t.string :name
end
- create_table :owners, :primary_key => :owner_id ,:force => true do |t|
+ create_table :owners, primary_key: :owner_id, force: true do |t|
t.string :name
t.column :updated_at, :datetime
t.column :happy_at, :datetime
t.string :essay_id
end
- create_table :paint_colors, :force => true do |t|
+ create_table :paint_colors, force: true do |t|
t.integer :non_poly_one_id
end
- create_table :paint_textures, :force => true do |t|
+ create_table :paint_textures, force: true do |t|
t.integer :non_poly_two_id
end
- create_table :parrots, :force => true do |t|
+ create_table :parrots, force: true do |t|
t.column :name, :string
t.column :color, :string
t.column :parrot_sti_class, :string
@@ -479,43 +485,43 @@ ActiveRecord::Schema.define do
t.column :updated_on, :datetime
end
- create_table :parrots_pirates, :id => false, :force => true do |t|
+ create_table :parrots_pirates, id: false, force: true do |t|
t.column :parrot_id, :integer
t.column :pirate_id, :integer
end
- create_table :parrots_treasures, :id => false, :force => true do |t|
+ create_table :parrots_treasures, id: false, force: true do |t|
t.column :parrot_id, :integer
t.column :treasure_id, :integer
end
- create_table :people, :force => true do |t|
- t.string :first_name, :null => false
+ create_table :people, force: true do |t|
+ t.string :first_name, null: false
t.references :primary_contact
- t.string :gender, :limit => 1
+ t.string :gender, limit: 1
t.references :number1_fan
- t.integer :lock_version, :null => false, :default => 0
+ t.integer :lock_version, null: false, default: 0
t.string :comments
- t.integer :followers_count, :default => 0
- t.integer :friends_too_count, :default => 0
+ t.integer :followers_count, default: 0
+ t.integer :friends_too_count, default: 0
t.references :best_friend
t.references :best_friend_of
t.integer :insures, null: false, default: 0
t.timestamps
end
- create_table :peoples_treasures, :id => false, :force => true do |t|
+ create_table :peoples_treasures, id: false, force: true do |t|
t.column :rich_person_id, :integer
t.column :treasure_id, :integer
end
- create_table :pets, :primary_key => :pet_id ,:force => true do |t|
+ create_table :pets, primary_key: :pet_id, force: true do |t|
t.string :name
t.integer :owner_id, :integer
t.timestamps
end
- create_table :pirates, :force => true do |t|
+ create_table :pirates, force: true do |t|
t.column :catchphrase, :string
t.column :parrot_id, :integer
t.integer :non_validated_parrot_id
@@ -523,74 +529,74 @@ ActiveRecord::Schema.define do
t.column :updated_on, :datetime
end
- create_table :posts, :force => true do |t|
+ create_table :posts, force: true do |t|
t.integer :author_id
- t.string :title, :null => false
+ t.string :title, null: false
# use VARCHAR2(4000) instead of CLOB datatype as CLOB data type has many limitations in
# Oracle SELECT WHERE clause which causes many unit test failures
if current_adapter?(:OracleAdapter)
- t.string :body, :null => false, :limit => 4000
+ t.string :body, null: false, limit: 4000
else
- t.text :body, :null => false
+ t.text :body, null: false
end
t.string :type
- t.integer :comments_count, :default => 0
- t.integer :taggings_count, :default => 0
- t.integer :taggings_with_delete_all_count, :default => 0
- t.integer :taggings_with_destroy_count, :default => 0
- t.integer :tags_count, :default => 0
- t.integer :tags_with_destroy_count, :default => 0
- t.integer :tags_with_nullify_count, :default => 0
+ t.integer :comments_count, default: 0
+ t.integer :taggings_count, default: 0
+ t.integer :taggings_with_delete_all_count, default: 0
+ t.integer :taggings_with_destroy_count, default: 0
+ t.integer :tags_count, default: 0
+ t.integer :tags_with_destroy_count, default: 0
+ t.integer :tags_with_nullify_count, default: 0
end
- create_table :price_estimates, :force => true do |t|
+ create_table :price_estimates, force: true do |t|
t.string :estimate_of_type
t.integer :estimate_of_id
t.integer :price
end
- create_table :products, :force => true do |t|
+ create_table :products, force: true do |t|
t.references :collection
t.string :name
end
- create_table :projects, :force => true do |t|
+ create_table :projects, force: true do |t|
t.string :name
t.string :type
end
- create_table :randomly_named_table, :force => true do |t|
+ create_table :randomly_named_table, force: true do |t|
t.string :some_attribute
t.integer :another_attribute
end
- create_table :ratings, :force => true do |t|
+ create_table :ratings, force: true do |t|
t.integer :comment_id
t.integer :value
end
- create_table :readers, :force => true do |t|
- t.integer :post_id, :null => false
- t.integer :person_id, :null => false
- t.boolean :skimmer, :default => false
+ create_table :readers, force: true do |t|
+ t.integer :post_id, null: false
+ t.integer :person_id, null: false
+ t.boolean :skimmer, default: false
t.integer :first_post_id
end
- create_table :references, :force => true do |t|
+ create_table :references, force: true do |t|
t.integer :person_id
t.integer :job_id
t.boolean :favourite
- t.integer :lock_version, :default => 0
+ t.integer :lock_version, default: 0
end
- create_table :shape_expressions, :force => true do |t|
+ create_table :shape_expressions, force: true do |t|
t.string :paint_type
t.integer :paint_id
t.string :shape_type
t.integer :shape_id
end
- create_table :ships, :force => true do |t|
+ create_table :ships, force: true do |t|
t.string :name
t.integer :pirate_id
t.integer :update_only_pirate_id
@@ -600,51 +606,51 @@ ActiveRecord::Schema.define do
t.datetime :updated_on
end
- create_table :ship_parts, :force => true do |t|
+ create_table :ship_parts, force: true do |t|
t.string :name
t.integer :ship_id
end
- create_table :speedometers, :force => true, :id => false do |t|
+ create_table :speedometers, force: true, id: false do |t|
t.string :speedometer_id
t.string :name
t.string :dashboard_id
end
- create_table :sponsors, :force => true do |t|
+ create_table :sponsors, force: true do |t|
t.integer :club_id
t.integer :sponsorable_id
t.string :sponsorable_type
end
- create_table :string_key_objects, :id => false, :primary_key => :id, :force => true do |t|
+ create_table :string_key_objects, id: false, primary_key: :id, force: true do |t|
t.string :id
t.string :name
- t.integer :lock_version, :null => false, :default => 0
+ t.integer :lock_version, null: false, default: 0
end
- create_table :students, :force => true do |t|
+ create_table :students, force: true do |t|
t.string :name
end
- create_table :subscribers, :force => true, :id => false do |t|
- t.string :nick, :null => false
+ create_table :subscribers, force: true, id: false do |t|
+ t.string :nick, null: false
t.string :name
- t.column :books_count, :integer, :null => false, :default => 0
+ t.column :books_count, :integer, null: false, default: 0
end
- add_index :subscribers, :nick, :unique => true
+ add_index :subscribers, :nick, unique: true
- create_table :subscriptions, :force => true do |t|
+ create_table :subscriptions, force: true do |t|
t.string :subscriber_id
t.integer :book_id
end
- create_table :tags, :force => true do |t|
+ create_table :tags, force: true do |t|
t.column :name, :string
- t.column :taggings_count, :integer, :default => 0
+ t.column :taggings_count, :integer, default: 0
end
- create_table :taggings, :force => true do |t|
+ create_table :taggings, force: true do |t|
t.column :tag_id, :integer
t.column :super_tag_id, :integer
t.column :taggable_type, :string
@@ -652,12 +658,12 @@ ActiveRecord::Schema.define do
t.string :comment
end
- create_table :tasks, :force => true do |t|
+ create_table :tasks, force: true do |t|
t.datetime :starting
t.datetime :ending
end
- create_table :topics, :force => true do |t|
+ create_table :topics, force: true do |t|
t.string :title
t.string :author_name
t.string :author_email_address
@@ -667,15 +673,15 @@ ActiveRecord::Schema.define do
# use VARCHAR2(4000) instead of CLOB datatype as CLOB data type has many limitations in
# Oracle SELECT WHERE clause which causes many unit test failures
if current_adapter?(:OracleAdapter)
- t.string :content, :limit => 4000
- t.string :important, :limit => 4000
+ t.string :content, limit: 4000
+ t.string :important, limit: 4000
else
t.text :content
t.text :important
end
- t.boolean :approved, :default => true
- t.integer :replies_count, :default => 0
- t.integer :unique_replies_count, :default => 0
+ t.boolean :approved, default: true
+ t.integer :replies_count, default: 0
+ t.integer :unique_replies_count, default: 0
t.integer :parent_id
t.string :parent_title
t.string :type
@@ -683,54 +689,54 @@ ActiveRecord::Schema.define do
t.timestamps
end
- create_table :toys, :primary_key => :toy_id ,:force => true do |t|
+ create_table :toys, primary_key: :toy_id, force: true do |t|
t.string :name
t.integer :pet_id, :integer
t.timestamps
end
- create_table :traffic_lights, :force => true do |t|
+ create_table :traffic_lights, force: true do |t|
t.string :location
t.string :state
- t.text :long_state, :null => false
+ t.text :long_state, null: false
t.datetime :created_at
t.datetime :updated_at
end
- create_table :treasures, :force => true do |t|
+ create_table :treasures, force: true do |t|
t.column :name, :string
t.column :type, :string
t.column :looter_id, :integer
t.column :looter_type, :string
end
- create_table :tyres, :force => true do |t|
+ create_table :tyres, force: true do |t|
t.integer :car_id
end
- create_table :variants, :force => true do |t|
+ create_table :variants, force: true do |t|
t.references :product
t.string :name
end
- create_table :vertices, :force => true do |t|
+ create_table :vertices, force: true do |t|
t.column :label, :string
end
- create_table 'warehouse-things', :force => true do |t|
+ create_table 'warehouse-things', force: true do |t|
t.integer :value
end
[:circles, :squares, :triangles, :non_poly_ones, :non_poly_twos].each do |t|
- create_table(t, :force => true) { }
+ create_table(t, force: true) { }
end
# NOTE - the following 4 tables are used by models that have :inverse_of options on the associations
- create_table :men, :force => true do |t|
+ create_table :men, force: true do |t|
t.string :name
end
- create_table :faces, :force => true do |t|
+ create_table :faces, force: true do |t|
t.string :description
t.integer :man_id
t.integer :polymorphic_man_id
@@ -739,7 +745,7 @@ ActiveRecord::Schema.define do
t.string :horrible_polymorphic_man_type
end
- create_table :interests, :force => true do |t|
+ create_table :interests, force: true do |t|
t.string :topic
t.integer :man_id
t.integer :polymorphic_man_id
@@ -747,50 +753,67 @@ ActiveRecord::Schema.define do
t.integer :zine_id
end
- create_table :wheels, :force => true do |t|
- t.references :wheelable, :polymorphic => true
+ create_table :wheels, force: true do |t|
+ t.references :wheelable, polymorphic: true
end
- create_table :zines, :force => true do |t|
+ create_table :zines, force: true do |t|
t.string :title
end
- create_table :countries, :force => true, :id => false, :primary_key => 'country_id' do |t|
+ create_table :countries, force: true, id: false, primary_key: 'country_id' do |t|
t.string :country_id
t.string :name
end
- create_table :treaties, :force => true, :id => false, :primary_key => 'treaty_id' do |t|
+ create_table :treaties, force: true, id: false, primary_key: 'treaty_id' do |t|
t.string :treaty_id
t.string :name
end
- create_table :countries_treaties, :force => true, :id => false do |t|
- t.string :country_id, :null => false
- t.string :treaty_id, :null => false
+ create_table :countries_treaties, force: true, id: false do |t|
+ t.string :country_id, null: false
+ t.string :treaty_id, null: false
end
- create_table :liquid, :force => true do |t|
+ create_table :liquid, force: true do |t|
t.string :name
end
- create_table :molecules, :force => true do |t|
+ create_table :molecules, force: true do |t|
t.integer :liquid_id
t.string :name
end
- create_table :electrons, :force => true do |t|
+ create_table :electrons, force: true do |t|
t.integer :molecule_id
t.string :name
end
- create_table :weirds, :force => true do |t|
+ create_table :weirds, force: true do |t|
t.string 'a$b'
+ t.string 'なまえ'
t.string 'from'
end
+ create_table :hotels, force: true do |t|
+ end
+ create_table :departments, force: true do |t|
+ t.integer :hotel_id
+ end
+ create_table :cake_designers, force: true do |t|
+ end
+ create_table :drink_designers, force: true do |t|
+ end
+ create_table :chefs, force: true do |t|
+ t.integer :employable_id
+ t.string :employable_type
+ t.integer :department_id
+ end
+
+
except 'SQLite' do
# fk_test_has_fk should be before fk_test_has_pk
- create_table :fk_test_has_fk, :force => true do |t|
- t.integer :fk_id, :null => false
+ create_table :fk_test_has_fk, force: true do |t|
+ t.integer :fk_id, null: false
end
- create_table :fk_test_has_pk, :force => true do |t|
+ create_table :fk_test_has_pk, force: true do |t|
end
execute "ALTER TABLE fk_test_has_fk ADD CONSTRAINT fk_name FOREIGN KEY (#{quote_column_name 'fk_id'}) REFERENCES #{quote_table_name 'fk_test_has_pk'} (#{quote_column_name 'id'})"
@@ -799,11 +822,11 @@ ActiveRecord::Schema.define do
end
end
-Course.connection.create_table :courses, :force => true do |t|
- t.column :name, :string, :null => false
+Course.connection.create_table :courses, force: true do |t|
+ t.column :name, :string, null: false
t.column :college_id, :integer
end
-College.connection.create_table :colleges, :force => true do |t|
- t.column :name, :string, :null => false
+College.connection.create_table :colleges, force: true do |t|
+ t.column :name, :string, null: false
end
diff --git a/activerecord/test/schema/sqlite_specific_schema.rb b/activerecord/test/schema/sqlite_specific_schema.rb
index e9ddeb32cf..b7aff4f47d 100644
--- a/activerecord/test/schema/sqlite_specific_schema.rb
+++ b/activerecord/test/schema/sqlite_specific_schema.rb
@@ -1,9 +1,6 @@
ActiveRecord::Schema.define do
- # For sqlite 3.1.0+, make a table with an autoincrement column
- if supports_autoincrement?
- create_table :table_with_autoincrement, :force => true do |t|
- t.column :name, :string
- end
+ create_table :table_with_autoincrement, :force => true do |t|
+ t.column :name, :string
end
execute "DROP TABLE fk_test_has_fk" rescue nil
diff --git a/activerecord/test/support/connection.rb b/activerecord/test/support/connection.rb
index 196b3a9493..d11fd9cfc1 100644
--- a/activerecord/test/support/connection.rb
+++ b/activerecord/test/support/connection.rb
@@ -15,7 +15,7 @@ module ARTest
puts "Using #{connection_name}"
ActiveRecord::Base.logger = ActiveSupport::Logger.new("debug.log", 0, 100 * 1024 * 1024)
ActiveRecord::Base.configurations = connection_config
- ActiveRecord::Base.establish_connection 'arunit'
- ARUnit2Model.establish_connection 'arunit2'
+ ActiveRecord::Base.establish_connection :arunit
+ ARUnit2Model.establish_connection :arunit2
end
end