aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord
diff options
context:
space:
mode:
Diffstat (limited to 'activerecord')
-rw-r--r--activerecord/CHANGELOG.md1197
-rw-r--r--activerecord/MIT-LICENSE2
-rw-r--r--activerecord/README.rdoc19
-rw-r--r--activerecord/RUNNING_UNIT_TESTS31
-rw-r--r--activerecord/RUNNING_UNIT_TESTS.rdoc44
-rw-r--r--activerecord/Rakefile22
-rw-r--r--activerecord/activerecord.gemspec20
-rw-r--r--activerecord/examples/associations.pngbin40623 -> 0 bytes
-rw-r--r--activerecord/examples/performance.rb50
-rw-r--r--activerecord/examples/simple.rb8
-rw-r--r--activerecord/lib/active_record.rb13
-rw-r--r--activerecord/lib/active_record/aggregations.rb41
-rw-r--r--activerecord/lib/active_record/association_relation.rb18
-rw-r--r--activerecord/lib/active_record/associations.rb267
-rw-r--r--activerecord/lib/active_record/associations/alias_tracker.rb2
-rw-r--r--activerecord/lib/active_record/associations/association.rb48
-rw-r--r--activerecord/lib/active_record/associations/association_scope.rb42
-rw-r--r--activerecord/lib/active_record/associations/belongs_to_association.rb11
-rw-r--r--activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb2
-rw-r--r--activerecord/lib/active_record/associations/builder/association.rb136
-rw-r--r--activerecord/lib/active_record/associations/builder/belongs_to.rb132
-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.rb10
-rw-r--r--activerecord/lib/active_record/associations/builder/has_one.rb16
-rw-r--r--activerecord/lib/active_record/associations/builder/singular_association.rb15
-rw-r--r--activerecord/lib/active_record/associations/collection_association.rb206
-rw-r--r--activerecord/lib/active_record/associations/collection_proxy.rb87
-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.rb23
-rw-r--r--activerecord/lib/active_record/associations/has_many_through_association.rb29
-rw-r--r--activerecord/lib/active_record/associations/has_one_association.rb23
-rw-r--r--activerecord/lib/active_record/associations/has_one_through_association.rb2
-rw-r--r--activerecord/lib/active_record/associations/join_dependency.rb372
-rw-r--r--activerecord/lib/active_record/associations/join_dependency/join_association.rb144
-rw-r--r--activerecord/lib/active_record/associations/join_dependency/join_base.rb16
-rw-r--r--activerecord/lib/active_record/associations/join_dependency/join_part.rb66
-rw-r--r--activerecord/lib/active_record/associations/join_helper.rb13
-rw-r--r--activerecord/lib/active_record/associations/preloader.rb127
-rw-r--r--activerecord/lib/active_record/associations/preloader/association.rb68
-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.rb10
-rw-r--r--activerecord/lib/active_record/associations/preloader/singular_association.rb4
-rw-r--r--activerecord/lib/active_record/associations/preloader/through_association.rb83
-rw-r--r--activerecord/lib/active_record/associations/singular_association.rb3
-rw-r--r--activerecord/lib/active_record/associations/through_association.rb8
-rw-r--r--activerecord/lib/active_record/attribute_assignment.rb24
-rw-r--r--activerecord/lib/active_record/attribute_methods.rb299
-rw-r--r--activerecord/lib/active_record/attribute_methods/before_type_cast.rb46
-rw-r--r--activerecord/lib/active_record/attribute_methods/dirty.rb49
-rw-r--r--activerecord/lib/active_record/attribute_methods/primary_key.rb9
-rw-r--r--activerecord/lib/active_record/attribute_methods/read.rb107
-rw-r--r--activerecord/lib/active_record/attribute_methods/serialization.rb61
-rw-r--r--activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb43
-rw-r--r--activerecord/lib/active_record/attribute_methods/write.rb49
-rw-r--r--activerecord/lib/active_record/autosave_association.rb463
-rw-r--r--activerecord/lib/active_record/base.rb127
-rw-r--r--activerecord/lib/active_record/callbacks.rb47
-rw-r--r--activerecord/lib/active_record/coders/yaml_column.rb21
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb97
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb9
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb34
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb25
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/quoting.rb14
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/savepoints.rb21
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb231
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb18
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb556
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/transaction.rb44
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_adapter.rb190
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb217
-rw-r--r--activerecord/lib/active_record/connection_adapters/column.rb106
-rw-r--r--activerecord/lib/active_record/connection_adapters/connection_specification.rb15
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb62
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql_adapter.rb61
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb37
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/cast.rb54
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb67
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid.rb126
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb60
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb16
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb123
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb314
-rw-r--r--activerecord/lib/active_record/connection_adapters/schema_cache.rb36
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb145
-rw-r--r--activerecord/lib/active_record/connection_handling.rb27
-rw-r--r--activerecord/lib/active_record/core.rb294
-rw-r--r--activerecord/lib/active_record/counter_cache.rb27
-rw-r--r--activerecord/lib/active_record/dynamic_matchers.rb2
-rw-r--r--activerecord/lib/active_record/errors.rb43
-rw-r--r--activerecord/lib/active_record/explain.rb68
-rw-r--r--activerecord/lib/active_record/explain_registry.rb30
-rw-r--r--activerecord/lib/active_record/explain_subscriber.rb7
-rw-r--r--activerecord/lib/active_record/fixture_set/file.rb3
-rw-r--r--activerecord/lib/active_record/fixtures.rb226
-rw-r--r--activerecord/lib/active_record/inheritance.rb135
-rw-r--r--activerecord/lib/active_record/integration.rb23
-rw-r--r--activerecord/lib/active_record/locale/en.yml6
-rw-r--r--activerecord/lib/active_record/locking/optimistic.rb18
-rw-r--r--activerecord/lib/active_record/locking/pessimistic.rb10
-rw-r--r--activerecord/lib/active_record/log_subscriber.rb35
-rw-r--r--activerecord/lib/active_record/migration.rb368
-rw-r--r--activerecord/lib/active_record/migration/command_recorder.rb127
-rw-r--r--activerecord/lib/active_record/migration/join_table.rb2
-rw-r--r--activerecord/lib/active_record/model.rb167
-rw-r--r--activerecord/lib/active_record/model_schema.rb113
-rw-r--r--activerecord/lib/active_record/nested_attributes.rb279
-rw-r--r--activerecord/lib/active_record/null_relation.rb18
-rw-r--r--activerecord/lib/active_record/observer.rb126
-rw-r--r--activerecord/lib/active_record/persistence.rb211
-rw-r--r--activerecord/lib/active_record/query_cache.rb14
-rw-r--r--activerecord/lib/active_record/querying.rb53
-rw-r--r--activerecord/lib/active_record/railtie.rb49
-rw-r--r--activerecord/lib/active_record/railties/console_sandbox.rb5
-rw-r--r--activerecord/lib/active_record/railties/controller_runtime.rb3
-rw-r--r--activerecord/lib/active_record/railties/databases.rake160
-rw-r--r--activerecord/lib/active_record/readonly_attributes.rb5
-rw-r--r--activerecord/lib/active_record/reflection.rb322
-rw-r--r--activerecord/lib/active_record/relation.rb303
-rw-r--r--activerecord/lib/active_record/relation/batches.rb81
-rw-r--r--activerecord/lib/active_record/relation/calculations.rb170
-rw-r--r--activerecord/lib/active_record/relation/delegation.rb129
-rw-r--r--activerecord/lib/active_record/relation/finder_methods.rb222
-rw-r--r--activerecord/lib/active_record/relation/merger.rb116
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder.rb95
-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.rb498
-rw-r--r--activerecord/lib/active_record/relation/spawn_methods.rb34
-rw-r--r--activerecord/lib/active_record/result.rb62
-rw-r--r--activerecord/lib/active_record/runtime_registry.rb17
-rw-r--r--activerecord/lib/active_record/sanitization.rb53
-rw-r--r--activerecord/lib/active_record/schema.rb9
-rw-r--r--activerecord/lib/active_record/schema_dumper.rb46
-rw-r--r--activerecord/lib/active_record/schema_migration.rb41
-rw-r--r--activerecord/lib/active_record/scoping.rb56
-rw-r--r--activerecord/lib/active_record/scoping/default.rb30
-rw-r--r--activerecord/lib/active_record/scoping/named.rb43
-rw-r--r--activerecord/lib/active_record/serialization.rb10
-rw-r--r--activerecord/lib/active_record/serializers/xml_serializer.rb14
-rw-r--r--activerecord/lib/active_record/statement_cache.rb26
-rw-r--r--activerecord/lib/active_record/store.rb100
-rw-r--r--activerecord/lib/active_record/tasks/database_tasks.rb78
-rw-r--r--activerecord/lib/active_record/tasks/mysql_database_tasks.rb48
-rw-r--r--activerecord/lib/active_record/tasks/postgresql_database_tasks.rb9
-rw-r--r--activerecord/lib/active_record/tasks/sqlite_database_tasks.rb8
-rw-r--r--activerecord/lib/active_record/test_case.rb95
-rw-r--r--activerecord/lib/active_record/timestamp.rb25
-rw-r--r--activerecord/lib/active_record/transactions.rb111
-rw-r--r--activerecord/lib/active_record/validations.rb3
-rw-r--r--activerecord/lib/active_record/validations/associated.rb6
-rw-r--r--activerecord/lib/active_record/validations/presence.rb6
-rw-r--r--activerecord/lib/active_record/validations/uniqueness.rb84
-rw-r--r--activerecord/lib/active_record/version.rb13
-rw-r--r--activerecord/lib/rails/generators/active_record.rb14
-rw-r--r--activerecord/lib/rails/generators/active_record/migration.rb18
-rw-r--r--activerecord/lib/rails/generators/active_record/migration/migration_generator.rb21
-rw-r--r--activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb (renamed from activerecord/lib/rails/generators/active_record/model/templates/migration.rb)4
-rw-r--r--activerecord/lib/rails/generators/active_record/migration/templates/migration.rb20
-rw-r--r--activerecord/lib/rails/generators/active_record/model/model_generator.rb10
-rw-r--r--activerecord/lib/rails/generators/active_record/model/templates/model.rb5
-rw-r--r--activerecord/lib/rails/generators/active_record/observer/observer_generator.rb15
-rw-r--r--activerecord/lib/rails/generators/active_record/observer/templates/observer.rb4
-rw-r--r--activerecord/test/cases/adapter_test.rb39
-rw-r--r--activerecord/test/cases/adapters/mysql/active_schema_test.rb64
-rw-r--r--activerecord/test/cases/adapters/mysql/connection_test.rb52
-rw-r--r--activerecord/test/cases/adapters/mysql/enum_test.rb2
-rw-r--r--activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb52
-rw-r--r--activerecord/test/cases/adapters/mysql/reserved_word_test.rb7
-rw-r--r--activerecord/test/cases/adapters/mysql/schema_test.rb22
-rw-r--r--activerecord/test/cases/adapters/mysql/sql_types_test.rb14
-rw-r--r--activerecord/test/cases/adapters/mysql2/active_schema_test.rb62
-rw-r--r--activerecord/test/cases/adapters/mysql2/connection_test.rb45
-rw-r--r--activerecord/test/cases/adapters/mysql2/enum_test.rb2
-rw-r--r--activerecord/test/cases/adapters/mysql2/reserved_word_test.rb8
-rw-r--r--activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb26
-rw-r--r--activerecord/test/cases/adapters/mysql2/schema_test.rb33
-rw-r--r--activerecord/test/cases/adapters/mysql2/sql_types_test.rb14
-rw-r--r--activerecord/test/cases/adapters/postgresql/active_schema_test.rb27
-rw-r--r--activerecord/test/cases/adapters/postgresql/array_test.rb65
-rw-r--r--activerecord/test/cases/adapters/postgresql/bytea_test.rb104
-rw-r--r--activerecord/test/cases/adapters/postgresql/connection_test.rb105
-rw-r--r--activerecord/test/cases/adapters/postgresql/datatype_test.rb450
-rw-r--r--activerecord/test/cases/adapters/postgresql/explain_test.rb7
-rw-r--r--activerecord/test/cases/adapters/postgresql/hstore_test.rb85
-rw-r--r--activerecord/test/cases/adapters/postgresql/json_test.rb53
-rw-r--r--activerecord/test/cases/adapters/postgresql/ltree_test.rb41
-rw-r--r--activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb50
-rw-r--r--activerecord/test/cases/adapters/postgresql/quoting_test.rb13
-rw-r--r--activerecord/test/cases/adapters/postgresql/schema_test.rb20
-rw-r--r--activerecord/test/cases/adapters/postgresql/sql_types_test.rb18
-rw-r--r--activerecord/test/cases/adapters/postgresql/statement_pool_test.rb54
-rw-r--r--activerecord/test/cases/adapters/postgresql/timestamp_test.rb18
-rw-r--r--activerecord/test/cases/adapters/postgresql/uuid_test.rb136
-rw-r--r--activerecord/test/cases/adapters/postgresql/xml_test.rb38
-rw-r--r--activerecord/test/cases/adapters/sqlite3/copy_table_test.rb21
-rw-r--r--activerecord/test/cases/adapters/sqlite3/quoting_test.rb2
-rw-r--r--activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb49
-rw-r--r--activerecord/test/cases/adapters/sqlite3/sqlite3_create_folder_test.rb21
-rw-r--r--activerecord/test/cases/aggregations_test.rb4
-rw-r--r--activerecord/test/cases/ar_schema_test.rb23
-rw-r--r--activerecord/test/cases/associations/belongs_to_associations_test.rb206
-rw-r--r--activerecord/test/cases/associations/cascaded_eager_loading_test.rb10
-rw-r--r--activerecord/test/cases/associations/eager_test.rb111
-rw-r--r--activerecord/test/cases/associations/extension_test.rb14
-rw-r--r--activerecord/test/cases/associations/habtm_join_table_test.rb35
-rw-r--r--activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb132
-rw-r--r--activerecord/test/cases/associations/has_many_associations_test.rb479
-rw-r--r--activerecord/test/cases/associations/has_many_through_associations_test.rb233
-rw-r--r--activerecord/test/cases/associations/has_one_associations_test.rb81
-rw-r--r--activerecord/test/cases/associations/has_one_through_associations_test.rb3
-rw-r--r--activerecord/test/cases/associations/inner_join_association_test.rb23
-rw-r--r--activerecord/test/cases/associations/inverse_associations_test.rb203
-rw-r--r--activerecord/test/cases/associations/join_dependency_test.rb8
-rw-r--r--activerecord/test/cases/associations/join_model_test.rb10
-rw-r--r--activerecord/test/cases/associations/nested_through_associations_test.rb24
-rw-r--r--activerecord/test/cases/associations_test.rb47
-rw-r--r--activerecord/test/cases/attribute_methods/read_test.rb13
-rw-r--r--activerecord/test/cases/attribute_methods/serialization_test.rb29
-rw-r--r--activerecord/test/cases/attribute_methods_test.rb87
-rw-r--r--activerecord/test/cases/autosave_association_test.rb47
-rw-r--r--activerecord/test/cases/base_test.rb478
-rw-r--r--activerecord/test/cases/batches_test.rb54
-rw-r--r--activerecord/test/cases/binary_test.rb2
-rw-r--r--activerecord/test/cases/calculations_test.rb151
-rw-r--r--activerecord/test/cases/callbacks_test.rb4
-rw-r--r--activerecord/test/cases/clone_test.rb7
-rw-r--r--activerecord/test/cases/coders/yaml_column_test.rb14
-rw-r--r--activerecord/test/cases/column_definition_test.rb13
-rw-r--r--activerecord/test/cases/column_test.rb76
-rw-r--r--activerecord/test/cases/connection_adapters/abstract_adapter_test.rb20
-rw-r--r--activerecord/test/cases/connection_adapters/connection_handler_test.rb20
-rw-r--r--activerecord/test/cases/connection_adapters/schema_cache_test.rb41
-rw-r--r--activerecord/test/cases/connection_management_test.rb4
-rw-r--r--activerecord/test/cases/connection_pool_test.rb14
-rw-r--r--activerecord/test/cases/connection_specification/resolver_test.rb55
-rw-r--r--activerecord/test/cases/core_test.rb33
-rw-r--r--activerecord/test/cases/counter_cache_test.rb31
-rw-r--r--activerecord/test/cases/date_time_test.rb8
-rw-r--r--activerecord/test/cases/defaults_test.rb99
-rw-r--r--activerecord/test/cases/deprecated_dynamic_methods_test.rb592
-rw-r--r--activerecord/test/cases/dirty_test.rb137
-rw-r--r--activerecord/test/cases/disconnected_test.rb27
-rw-r--r--activerecord/test/cases/dup_test.rb25
-rw-r--r--activerecord/test/cases/explain_subscriber_test.rb64
-rw-r--r--activerecord/test/cases/explain_test.rb73
-rw-r--r--activerecord/test/cases/finder_respond_to_test.rb41
-rw-r--r--activerecord/test/cases/finder_test.rb110
-rw-r--r--activerecord/test/cases/fixtures_test.rb92
-rw-r--r--activerecord/test/cases/forbidden_attributes_protection_test.rb20
-rw-r--r--activerecord/test/cases/helper.rb104
-rw-r--r--activerecord/test/cases/inclusion_test.rb133
-rw-r--r--activerecord/test/cases/inheritance_test.rb70
-rw-r--r--activerecord/test/cases/integration_test.rb84
-rw-r--r--activerecord/test/cases/invalid_connection_test.rb22
-rw-r--r--activerecord/test/cases/invertible_migration_test.rb159
-rw-r--r--activerecord/test/cases/json_serialization_test.rb88
-rw-r--r--activerecord/test/cases/lifecycle_test.rb256
-rw-r--r--activerecord/test/cases/locking_test.rb33
-rw-r--r--activerecord/test/cases/log_subscriber_test.rb56
-rw-r--r--activerecord/test/cases/migration/change_schema_test.rb75
-rw-r--r--activerecord/test/cases/migration/change_table_test.rb43
-rw-r--r--activerecord/test/cases/migration/column_attributes_test.rb41
-rw-r--r--activerecord/test/cases/migration/columns_test.rb (renamed from activerecord/test/cases/migration/rename_column_test.rb)111
-rw-r--r--activerecord/test/cases/migration/command_recorder_test.rb216
-rw-r--r--activerecord/test/cases/migration/create_join_table_test.rb48
-rw-r--r--activerecord/test/cases/migration/helper.rb6
-rw-r--r--activerecord/test/cases/migration/index_test.rb40
-rw-r--r--activerecord/test/cases/migration/logger_test.rb2
-rw-r--r--activerecord/test/cases/migration/references_index_test.rb8
-rw-r--r--activerecord/test/cases/migration/references_statements_test.rb20
-rw-r--r--activerecord/test/cases/migration/rename_table_test.rb12
-rw-r--r--activerecord/test/cases/migration_test.rb248
-rw-r--r--activerecord/test/cases/migrator_test.rb17
-rw-r--r--activerecord/test/cases/modules_test.rb1
-rw-r--r--activerecord/test/cases/multiparameter_attributes_test.rb156
-rw-r--r--activerecord/test/cases/multiple_db_test.rb4
-rw-r--r--activerecord/test/cases/nested_attributes_test.rb167
-rw-r--r--activerecord/test/cases/nested_attributes_with_callbacks_test.rb144
-rw-r--r--activerecord/test/cases/persistence_test.rb173
-rw-r--r--activerecord/test/cases/pooled_connections_test.rb32
-rw-r--r--activerecord/test/cases/primary_keys_test.rb12
-rw-r--r--activerecord/test/cases/query_cache_test.rb13
-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.rb35
-rw-r--r--activerecord/test/cases/relation/delegation_test.rb98
-rw-r--r--activerecord/test/cases/relation/mutation_test.rb148
-rw-r--r--activerecord/test/cases/relation/predicate_builder_test.rb14
-rw-r--r--activerecord/test/cases/relation/where_chain_test.rb80
-rw-r--r--activerecord/test/cases/relation/where_test.rb58
-rw-r--r--activerecord/test/cases/relation_test.rb160
-rw-r--r--activerecord/test/cases/relations_test.rb321
-rw-r--r--activerecord/test/cases/result_test.rb32
-rw-r--r--activerecord/test/cases/sanitize_test.rb24
-rw-r--r--activerecord/test/cases/schema_dumper_test.rb79
-rw-r--r--activerecord/test/cases/scoping/default_scoping_test.rb384
-rw-r--r--activerecord/test/cases/scoping/named_scoping_test.rb (renamed from activerecord/test/cases/named_scope_test.rb)48
-rw-r--r--activerecord/test/cases/scoping/relation_scoping_test.rb (renamed from activerecord/test/cases/relation_scoping_test.rb)235
-rw-r--r--activerecord/test/cases/serialization_test.rb20
-rw-r--r--activerecord/test/cases/serialized_attribute_test.rb63
-rw-r--r--activerecord/test/cases/statement_cache_test.rb64
-rw-r--r--activerecord/test/cases/store_test.rb32
-rw-r--r--activerecord/test/cases/tasks/database_tasks_test.rb27
-rw-r--r--activerecord/test/cases/tasks/mysql_rake_test.rb52
-rw-r--r--activerecord/test/cases/tasks/postgresql_rake_test.rb23
-rw-r--r--activerecord/test/cases/tasks/sqlite_rake_test.rb6
-rw-r--r--activerecord/test/cases/test_case.rb111
-rw-r--r--activerecord/test/cases/timestamp_test.rb112
-rw-r--r--activerecord/test/cases/transaction_callbacks_test.rb155
-rw-r--r--activerecord/test/cases/transaction_isolation_test.rb2
-rw-r--r--activerecord/test/cases/transactions_test.rb144
-rw-r--r--activerecord/test/cases/unconnected_test.rb6
-rw-r--r--activerecord/test/cases/validations/association_validation_test.rb71
-rw-r--r--activerecord/test/cases/validations/i18n_generate_message_validation_test.rb19
-rw-r--r--activerecord/test/cases/validations/presence_validation_test.rb7
-rw-r--r--activerecord/test/cases/validations/uniqueness_validation_test.rb35
-rw-r--r--activerecord/test/cases/validations_repair_helper.rb4
-rw-r--r--activerecord/test/cases/xml_serialization_test.rb20
-rw-r--r--activerecord/test/cases/yaml_serialization_test.rb14
l---------activerecord/test/fixtures/all/admin1
-rw-r--r--activerecord/test/fixtures/dog_lovers.yml3
-rw-r--r--activerecord/test/fixtures/dogs.yml1
-rw-r--r--activerecord/test/fixtures/friendships.yml4
-rw-r--r--activerecord/test/fixtures/people.yml3
-rw-r--r--activerecord/test/fixtures/pets.yml5
-rw-r--r--activerecord/test/fixtures/readers.yml4
-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/fixtures/toys.yml10
-rw-r--r--activerecord/test/fixtures/traffic_lights.yml4
-rw-r--r--activerecord/test/migrations/10_urban/9_add_expressions.rb11
-rw-r--r--activerecord/test/migrations/magic/1_currencies_have_symbols.rb12
-rw-r--r--activerecord/test/models/admin/user.rb27
-rw-r--r--activerecord/test/models/author.rb38
-rw-r--r--activerecord/test/models/auto_id.rb4
-rw-r--r--activerecord/test/models/autoloadable/extra_firm.rb2
-rw-r--r--activerecord/test/models/book.rb2
-rw-r--r--activerecord/test/models/bulb.rb8
-rw-r--r--activerecord/test/models/cake_designer.rb3
-rw-r--r--activerecord/test/models/car.rb4
-rw-r--r--activerecord/test/models/category.rb5
-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/comment.rb6
-rw-r--r--activerecord/test/models/company.rb46
-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.rb32
-rw-r--r--activerecord/test/models/dog.rb5
-rw-r--r--activerecord/test/models/dog_lover.rb5
-rw-r--r--activerecord/test/models/drink_designer.rb3
-rw-r--r--activerecord/test/models/friendship.rb4
-rw-r--r--activerecord/test/models/hotel.rb6
-rw-r--r--activerecord/test/models/liquid.rb3
-rw-r--r--activerecord/test/models/man.rb1
-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/owner.rb2
-rw-r--r--activerecord/test/models/parrot.rb6
-rw-r--r--activerecord/test/models/person.rb28
-rw-r--r--activerecord/test/models/pet.rb2
-rw-r--r--activerecord/test/models/post.rb52
-rw-r--r--activerecord/test/models/project.rb23
-rw-r--r--activerecord/test/models/reader.rb1
-rw-r--r--activerecord/test/models/reference.rb7
-rw-r--r--activerecord/test/models/reply.rb3
-rw-r--r--activerecord/test/models/speedometer.rb2
-rw-r--r--activerecord/test/models/teapot.rb35
-rw-r--r--activerecord/test/models/topic.rb9
-rw-r--r--activerecord/test/models/traffic_light.rb1
-rw-r--r--activerecord/test/schema/mysql2_specific_schema.rb34
-rw-r--r--activerecord/test/schema/mysql_specific_schema.rb34
-rw-r--r--activerecord/test/schema/postgresql_specific_schema.rb54
-rw-r--r--activerecord/test/schema/schema.rb45
-rw-r--r--activerecord/test/schema/sqlite_specific_schema.rb7
-rw-r--r--activerecord/test/support/connection.rb6
-rw-r--r--activerecord/test/support/mysql.rb11
387 files changed, 16067 insertions, 9517 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md
index f3362f81a3..7e6ef27964 100644
--- a/activerecord/CHANGELOG.md
+++ b/activerecord/CHANGELOG.md
@@ -1,908 +1,933 @@
-## Rails 4.0.0 (unreleased) ##
+* ActiveRecord::Base#attribute_for_inspect now truncates long arrays (more than 10 elements)
-* PostgreSQL adapter correctly fetches default values when using multiple schemas and domains in a db. Fixes #7914
+ *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
- *Arturo Pie*
+ vendor.users.to_a # => No exception is raised
-* Learn ActiveRecord::QueryMethods#order work with hash arguments
- When symbol or hash passed we convert it to Arel::Nodes::Ordering.
- If we pass invalid direction(like name: :DeSc) ActiveRecord::QueryMethods#order will raise an exception
-
- User.order(:name, email: :desc)
- # SELECT "users".* FROM "users" ORDER BY "users"."name" ASC, "users"."email" DESC
+ Fixes #12242, #9517, #10240.
- *Tima Maslyuchenko*
+ *Paul Nikitochkin*
-* Rename `ActiveRecord::Fixtures` class to `ActiveRecord::FixtureSet`.
- Instances of this class normally hold a collection of fixtures (records)
- loaded either from a single YAML file, or from a file and a folder
- with the same name. This change make the class name singular and makes
- the class easier to distinguish from the modules like
- `ActiveRecord::TestFixtures`, which operates on multiple fixture sets,
- or `DelegatingFixtures`, `::Fixtures`, etc.,
- and from the class `ActiveRecord::Fixture`, which corresponds to a single
- fixture.
+* Type cast json values on write, so that the value is consistent
+ with reading from the database.
- *Alexey Muranov*
+ Example:
+
+ x = JsonDataType.new tags: {"string" => "foo", :symbol => :bar}
+
+ # Before:
+ x.tags # => {"string" => "foo", :symbol => :bar}
-* The postgres adapter now supports tables with capital letters.
- Fix #5920
+ # After:
+ x.tags # => {"string" => "foo", "symbol" => "bar"}
+
+ *Severin Schoepke*
+
+* `ActiveRecord::Store` works together with PG `hstore` columns.
+ Fixes #12452.
*Yves Senn*
-* `CollectionAssociation#count` returns `0` without querying if the
- parent record is not persisted.
+* 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`.
- Before:
+ *Yves Senn*
- person.pets.count
- # SELECT COUNT(*) FROM "pets" WHERE "pets"."person_id" IS NULL
- # => 0
+* Save `has_one` association when primary key is manually set.
- After:
+ Fixes #12302.
- person.pets.count
- # fires without sql query
- # => 0
+ *Lauro Caetano*
- *Francesco Rodriguez*
+* Allow any version of BCrypt when using `has_secure_password`.
-* Fix `reset_counters` crashing on `has_many :through` associations.
- Fix #7822.
+ *Mike Perham*
- *lulalala*
+* Sub-query generated for `Relation` passed as array condition did not take in account
+ bind values and have invalid syntax.
-* Support for partial inserts.
+ Generate sub-query with inline bind values.
- When inserting new records, only the fields which have been changed
- from the defaults will actually be included in the INSERT statement.
- The other fields will be populated by the database.
+ Fixes #12586.
- This is more efficient, and also means that it will be safe to
- remove database columns without getting subsequent errors in running
- app processes (so long as the code in those processes doesn't
- contain any references to the removed column).
+ *Paul Nikitochkin*
- *Jon Leighton*
+* Fix a bug where rake db:structure:load crashed when the path contained
+ spaces.
-* Allow before and after validations to take an array of lifecycle events
+ *Kevin Mook*
- *John Foley*
+* `ActiveRecord::QueryMethods#unscope` unscopes negative equality
-* Support for specifying transaction isolation level
+ 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`.
- If your database supports setting the isolation level for a transaction, you can set
- it like so:
+ *Eric Hankins*
- Post.transaction(isolation: :serializable) do
- # ...
- end
+* Raise an exception when model without primary key calls `.find_with_ids`.
- Valid isolation levels are:
+ *Shimpei Makimoto*
- * `:read_uncommitted`
- * `:read_committed`
- * `:repeatable_read`
- * `:serializable`
+* Make `Relation#empty?` use `exists?` instead of `count`.
- You should consult the documentation for your database to understand the
- semantics of these different levels:
+ *Szymon Nowak*
- * http://www.postgresql.org/docs/9.1/static/transaction-iso.html
- * https://dev.mysql.com/doc/refman/5.0/en/set-transaction.html
+* `rake db:structure:dump` no longer crashes when the port was specified as `Fixnum`.
- An `ActiveRecord::TransactionIsolationError` will be raised if:
+ *Kenta Okamoto*
- * The adapter does not support setting the isolation level
- * You are joining an existing open transaction
- * You are creating a nested (savepoint) transaction
+* `NullRelation#pluck` takes a list of columns
- The mysql, mysql2 and postgresql adapters support setting the transaction
- isolation level. However, support is disabled for mysql versions below 5,
- because they are affected by a bug (http://bugs.mysql.com/bug.php?id=39170)
- which means the isolation level gets persisted outside the transaction.
+ The method signature in `NullRelation` was updated to mimic that in
+ `Calculations`.
- *Jon Leighton*
+ *Derek Prior*
-* `ActiveModel::ForbiddenAttributesProtection` is included by default
- in Active Record models. Check the docs of `ActiveModel::ForbiddenAttributesProtection`
- for more details.
+* `scope_chain` should not be mutated for other reflections.
- *Guillermo Iguaran*
+ 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.
-* Remove integration between Active Record and
- `ActiveModel::MassAssignmentSecurity`, `protected_attributes` gem
- should be added to use `attr_accessible`/`attr_protected`. Mass
- assignment options has been removed from all the AR methods that
- used it (ex. `AR::Base.new`, `AR::Base.create`, `AR::Base#update_attributes`, etc).
+ Fix is to dup the value before adding to the `scope_chain`.
- *Guillermo Iguaran*
+ Fixes #3882.
-* Fix the return of querying with an empty hash.
- Fix #6971.
+ *Neeraj Singh*
- User.where(token: {})
+* Prevent the inversed association from being reloaded on save.
- Before:
+ Fixes #9499.
- #=> SELECT * FROM users;
+ *Dmitry Polushkin*
- After:
+* Generate subquery for `Relation` if it passed as array condition for `where`
+ method.
- #=> SELECT * FROM users WHERE 1 = 2;
+ Example:
- *Damien Mathieu*
+ # 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))
-* Fix creation of through association models when using `collection=[]`
- on a `has_many :through` association from an unsaved model.
- Fix #7661.
+ # 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)
- *Ernie Miller*
+ Fixes #12415.
-* Explain only normal CRUD sql (select / update / insert / delete).
- Fix problem that explains unexplainable sql.
- Closes #7544 #6458.
+ *Paul Nikitochkin*
- *kennyj*
+* 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*
-* You can now override the generated accessor methods for stored attributes
- and reuse the original behavior with `read_store_attribute` and `write_store_attribute`,
- which are counterparts to `read_attribute` and `write_attribute`.
+* Make `next_migration_number` accessible for third party generators.
- *Matt Jones*
+ *Yves Senn*
+
+* Objects instantiated using a null relationship will now retain the
+ attributes of the where clause.
+
+ Fixes #11676, #11675, #11376.
-* Accept belongs_to (including polymorphic) association keys in queries.
+ *Paul Nikitochkin*, *Peter Brown*, *Nthalk*
- The following queries are now equivalent:
+* 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.
- Post.where(author: author)
- Post.where(author_id: author)
+ *arthurnn*
- PriceEstimate.where(estimate_of: treasure)
- PriceEstimate.where(estimate_of_type: 'Treasure', estimate_of_id: treasure)
+* Callbacks on has_many should access the in memory parent if a inverse_of is set.
- *Peter Brown*
+ *arthurnn*
-* Use native `mysqldump` command instead of `structure_dump` method
- when dumping the database structure to a sql file. Fixes #5547.
+* `ActiveRecord::ConnectionAdapters.string_to_time` respects
+ string with timezone (e.g. Wed, 04 Sep 2013 20:30:00 JST).
+
+ Fixes #12278.
*kennyj*
-* PostgreSQL inet and cidr types are converted to `IPAddr` objects.
+* 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`.
- *Dan McClain*
+ Example:
-* PostgreSQL array type support. Any datatype can be used to create an
- array column, with full migration and schema dumper support.
+ @my_comment.update_attributes(nil) # => raises ArgumentError
- To declare an array column, use the following syntax:
+ *John Wang*
- create_table :table_with_arrays do |t|
- t.integer :int_array, array: true
- # integer[]
- t.integer :int_array, array: true, length: 2
- # smallint[]
- t.string :string_array, array: true, length: 30
- # char varying(30)[]
- end
+* Deprecate `quoted_locking_column` method, which isn't used anywhere.
- This respects any other migration detail (limits, defaults, etc).
- Active Record will serialize and deserialize the array columns on
- their way to and from the database.
+ *kennyj*
- One thing to note: PostgreSQL does not enforce any limits on the
- number of elements, and any array can be multi-dimensional. Any
- array that is multi-dimensional must be rectangular (each sub array
- must have the same number of elements as its siblings).
+* Migration dump UUID default functions to schema.rb.
- If the `pg_array_parser` gem is available, it will be used when
- parsing PostgreSQL's array representation.
+ Fixes #10751.
- *Dan McClain*
+ *kennyj*
-* Attribute predicate methods, such as `article.title?`, will now raise
- `ActiveModel::MissingAttributeError` if the attribute being queried for
- truthiness was not read from the database, instead of just returning `false`.
+* Fixed a bug in `ActiveRecord::Associations::CollectionAssociation#find_by_scan`
+ when using `has_many` association with `:inverse_of` option and UUID primary key.
- *Ernie Miller*
+ Fixes #10450.
-* `ActiveRecord::SchemaDumper` uses Ruby 1.9 style hash, which means that the
- schema.rb file will be generated using this new syntax from now on.
+ *kennyj*
- *Konstantin Shabanov*
+* ActiveRecord::Base#<=> has been removed. Primary keys may not be in order,
+ or even be numbers, so sorting by id doesn't make sense. Please use `sort_by`
+ and specify the attribute you wish to sort with. For example, change:
-* Map interval with precision to string datatype in PostgreSQL. Fixes #7518.
+ Post.all.to_a.sort
- *Yves Senn*
+ to:
+
+ Post.all.to_a.sort_by(&:id)
-* Fix eagerly loading associations without primary keys. Fixes #4976.
+ *Aaron Patterson*
- *Kelley Reynolds*
+* 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.
-* Rails now raise an exception when you're trying to run a migration that has an invalid
- file name. Only lower case letters, numbers, and '_' are allowed in migration's file name.
- Please see #7419 for more details.
+ Fixes #11963.
- *Jan Bernacki*
+ *Paul Nikitochkin*
+
+* Deprecate the delegation of Array bang methods for associations.
+ To use them, instead first call `#to_a` on the association to access the
+ array to be acted on.
-* Fix bug when calling `store_accessor` multiple times.
- Fixes #7532.
+ *Ben Woosley*
- *Matt Jones*
+* `CollectionAssociation#first`/`#last` (e.g. `has_many`) use a `LIMIT`ed
+ query to fetch results rather than loading the entire collection.
-* Fix store attributes that show the changes incorrectly.
- Fixes #7532.
+ *Lann Martin*
+
+* Make possible to run SQLite rake tasks without the `Rails` constant defined.
+
+ *Damien Mathieu*
- *Matt Jones*
+* 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*
-* Fix `ActiveRecord::Relation#pluck` when columns or tables are reserved words.
+* Setting a has_one association on a new record no longer causes an empty
+ transaction.
- *Ian Lesperance*
+ *Dylan Thacker-Smith*
-* Allow JSON columns to be created in PostgreSQL and properly encoded/decoded.
- to/from database.
+* Fix `AR::Relation#merge` sometimes failing to preserve `readonly(false)` flag.
- *Dickson S. Guedes*
+ *thedarkone*
-* Fix time column type casting for invalid time string values to correctly return `nil`.
+* Re-use `order` argument pre-processing for `reorder`.
- *Adam Meehan*
+ *Paul Nikitochkin*
-* Allow to pass Symbol or Proc into `:limit` option of #accepts_nested_attributes_for.
+* 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*
-* ActiveRecord::SessionStore has been extracted from Active Record as `activerecord-session_store`
- gem. Please read the `README.md` file on the gem for the usage.
+* PostgreSQL adapter recognizes negative money values formatted with
+ parentheses (eg. `($1.25) # => -1.25`)).
+ Fixes #11899.
- *Prem Sichanugrist*
+ *Yves Senn*
-* Fix `reset_counters` when there are multiple `belongs_to` association with the
- same foreign key and one of them have a counter cache.
- Fixes #5200.
+* Stop interpreting SQL 'string' columns as :string type because there is no
+ common STRING datatype in SQL.
- *Dave Desrochers*
+ *Ben Woosley*
-* `serialized_attributes` and `_attr_readonly` become class method only. Instance reader methods are deprecated.
+* `ActiveRecord::FinderMethods#exists?` returns `true`/`false` in all cases.
- *kennyj*
+ *Xavier Noria*
-* Round usec when comparing timestamp attributes in the dirty tracking.
- Fixes #6975.
+* Assign inet/cidr attribute with `nil` value for invalid address.
- *kennyj*
+ Example:
-* Use inversed parent for first and last child of has_many association.
+ record = User.new
+ record.logged_in_from_ip # is type of an inet or a cidr
- *Ravil Bayramgalin*
+ # Before:
+ record.logged_in_from_ip = 'bad ip address' # raise exception
-* Fix Column.microseconds and Column.fast_string_to_date to avoid converting
- timestamp seconds to a float, since it occasionally results in inaccuracies
- with microsecond-precision times. Fixes #7352.
+ # 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'
- *Ari Pollak*
+ *Paul Nikitochkin*
-* Raise `ArgumentError` if list of attributes to change is empty in `update_all`.
+* `add_to_target` now accepts a second optional `skip_callbacks` argument
- *Roman Shatsov*
+ If truthy, it will skip the :before_add and :after_add callbacks.
-* Fix AR#create to return an unsaved record when AR::RecordInvalid is
- raised. Fixes #3217.
+ *Ben Woosley*
- *Dave Yeu*
+* Fix interactions between `:before_add` callbacks and nested attributes
+ assignment of `has_many` associations, when the association was not
+ yet loaded:
-* Fixed table name prefix that is generated in engines for namespaced models.
+ - A `:before_add` callback was being called when a nested attributes
+ assignment assigned to an existing record.
- *Wojciech Wnętrzak*
+ - Nested Attributes assignment did not affect the record in the
+ association target when a `:before_add` callback triggered the
+ loading of the association
-* Make sure `:environment` task is executed before `db:schema:load` or `db:structure:load`.
- Fixes #4772.
+ *Jörg Schray*
- *Seamus Abshere*
+* Allow enable_extension migration method to be revertible.
-* Allow Relation#merge to take a proc.
+ *Eric Tipton*
- This was requested by DHH to allow creating of one's own custom
- association macros.
+* Type cast hstore values on write, so that the value is consistent
+ with reading from the database.
- For example:
+ Example:
- module Commentable
- def has_many_comments(extra)
- has_many :comments, -> { where(:foo).merge(extra) }
- end
- end
+ x = Hstore.new tags: {"bool" => true, "number" => 5}
- class Post < ActiveRecord::Base
- extend Commentable
- has_many_comments -> { where(:bar) }
- end
+ # Before:
+ x.tags # => {"bool" => true, "number" => 5}
- *Jon Leighton*
+ # After:
+ x.tags # => {"bool" => "true", "number" => "5"}
+
+ *Yves Senn* , *Severin Schoepke*
-* Add CollectionProxy#scope.
+* Fix multidimensional PG arrays containing non-string items.
- This can be used to get a Relation from an association.
+ *Yves Senn*
- Previously we had a #scoped method, but we're deprecating that for
- AR::Base, so it doesn't make sense to have it here.
+* Fixes bug when using includes combined with select, the select statement was overwritten.
- This was requested by DHH, to facilitate code like this:
+ Fixes #11773.
- Project.scope.order('created_at DESC').page(current_page).tagged_with(@tag).limit(5).scoping do
- @topics = @project.topics.scope
- @todolists = @project.todolists.scope
- @attachments = @project.attachments.scope
- @documents = @project.documents.scope
- end
+ *Edo Balvers*
- *Jon Leighton*
+* Load fixtures from linked folders.
-* Add `Relation#load`.
+ *Kassio Borges*
- This method explicitly loads the records and then returns `self`.
+* Create a directory for sqlite3 file if not present on the system.
- Rather than deciding between "do I want an array or a relation?",
- most people are actually asking themselves "do I want to eager load
- or lazy load?" Therefore, this method provides a way to explicitly
- eager-load without having to switch from a `Relation` to an array.
+ *Richard Schneeman*
- Example:
+* Removed redundant override of `xml` column definition for PG,
+ in order to use `xml` column type instead of `text`.
- @posts = Post.where(published: true).load
+ *Paul Nikitochkin*, *Michael Nikitochkin*
- *Jon Leighton*
+* Revert `ActiveRecord::Relation#order` change that make new order
+ prepend the old one.
-* `Relation#order`: make new order prepend 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.
- *Bogdan Gusiev*
+* Add ability to define how a class is converted to Arel predicates.
+ For example, adding a very vendor specific regex implementation:
-* `Model.all` now returns an `ActiveRecord::Relation`, rather than an
- array of records. Use `Relation#to_a` if you really want an array.
+ regex_handler = proc do |column, value|
+ Arel::Nodes::InfixOperation.new('~', column, value.source)
+ end
+ ActiveRecord::PredicateBuilder.register_handler(Regexp, regex_handler)
- In some specific cases, this may cause breakage when upgrading.
- However in most cases the `ActiveRecord::Relation` will just act as a
- lazy-loaded array and there will be no problems.
+ *Sean Griffin & @joannecheng*
- Note that calling `Model.all` with options (e.g.
- `Model.all(conditions: '...')` was already deprecated, but it will
- still return an array in order to make the transition easier.
+* Don't allow `quote_value` to be called without a column.
- `Model.scoped` is deprecated in favour of `Model.all`.
+ 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.
- `Relation#all` still returns an array, but is deprecated (since it
- would serve no purpose if we made it return a `Relation`).
+ *Ben Woosley*
- *Jon Leighton*
+* 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.
-* `:finder_sql` and `:counter_sql` options on collection associations
- are deprecated. Please transition to using scopes.
+ Fixes #6763.
- *Jon Leighton*
+ *Alfred Wong*
-* `:insert_sql` and `:delete_sql` options on `has_and_belongs_to_many`
- associations are deprecated. Please transition to using `has_many
- :through`.
+* rescue from all exceptions in `ConnectionManagement#call`
- *Jon Leighton*
+ Fixes #11497.
-* Added `#update_columns` method which updates the attributes from
- the passed-in hash without calling save, hence skipping validations and
- callbacks. `ActiveRecordError` will be raised when called on new objects
- or when at least one of the attributes is marked as read only.
+ 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.
- post.attributes # => {"id"=>2, "title"=>"My title", "body"=>"My content", "author"=>"Peter"}
- post.update_columns(title: 'New title', author: 'Sebastian') # => true
- post.attributes # => {"id"=>2, "title"=>"New title", "body"=>"My content", "author"=>"Sebastian"}
+ Rescuing from all exceptions and not just StandardError, fixes this
+ behaviour.
- *Sebastian Martinez + Rafael Mendonça França*
+ *Vipul A M*
-* The migration generator now creates a join table with (commented) indexes every time
- the migration name contains the word `join_table`:
+* `change_column` for PostgreSQL adapter respects the `:array` option.
- rails g migration create_join_table_for_artists_and_musics artist_id:index music_id
+ *Yves Senn*
- *Aleksey Magusev*
+* Remove deprecation warning from `attribute_missing` for attributes that are columns.
-* Add `add_reference` and `remove_reference` schema statements. Aliases, `add_belongs_to`
- and `remove_belongs_to` are acceptable. References are reversible.
+ *Arun Agrawal*
- Examples:
+* Remove extra decrement of transaction deep level.
- # Create a user_id column
- add_reference(:products, :user)
- # Create a supplier_id, supplier_type columns and appropriate index
- add_reference(:products, :supplier, polymorphic: true, index: true)
- # Remove polymorphic reference
- remove_reference(:products, :supplier, polymorphic: true)
+ Fixes #4566.
- *Aleksey Magusev*
+ *Paul Nikitochkin*
-* Add `:default` and `:null` options to `column_exists?`.
+* Reset @column_defaults when assigning `locking_column`.
+ We had a potential problem. For example:
- column_exists?(:testings, :taggable_id, :integer, null: false)
- column_exists?(:testings, :taggable_type, :string, default: 'Photo')
+ class Post < ActiveRecord::Base
+ self.column_defaults # if we call this unintentionally before setting locking_column ...
+ self.locking_column = 'my_locking_column'
+ end
- *Aleksey Magusev*
+ Post.column_defaults["my_locking_column"]
+ => nil # expected value is 0 !
-* `ActiveRecord::Relation#inspect` now makes it clear that you are
- dealing with a `Relation` object rather than an array:.
+ *kennyj*
- User.where(age: 30).inspect
- # => <ActiveRecord::Relation [#<User ...>, #<User ...>, ...]>
+* Remove extra select and update queries on save/touch/destroy ActiveRecord model
+ with belongs to reflection with option `touch: true`.
- User.where(age: 30).to_a.inspect
- # => [#<User ...>, #<User ...>]
+ Fixes #11288.
- The number of records displayed will be limited to 10.
+ *Paul Nikitochkin*
- *Brian Cardarella, Jon Leighton & Damien Mathieu*
+* Remove deprecated nil-passing to the following `SchemaCache` methods:
+ `primary_keys`, `tables`, `columns` and `columns_hash`.
-* Add `collation` and `ctype` support to PostgreSQL. These are available for PostgreSQL 8.4 or later.
- Example:
+ *Yves Senn*
- development:
- adapter: postgresql
- host: localhost
- database: rails_development
- username: foo
- password: bar
- encoding: UTF8
- collation: ja_JP.UTF8
- ctype: ja_JP.UTF8
+* Remove deprecated block filter from `ActiveRecord::Migrator#migrate`.
- *kennyj*
+ *Yves Senn*
-* Changed validates_presence_of on an association so that children objects
- do not validate as being present if they are marked for destruction. This
- prevents you from saving the parent successfully and thus putting the parent
- in an invalid state.
+* Remove deprecated String constructor from `ActiveRecord::Migrator`.
- *Nick Monje & Brent Wheeldon*
+ *Yves Senn*
-* `FinderMethods#exists?` now returns `false` with the `false` argument.
+* Remove deprecated `scope` use without passing a callable object.
- *Egor Lynko*
+ *Arun Agrawal*
-* Added support for specifying the precision of a timestamp in the postgresql
- adapter. So, instead of having to incorrectly specify the precision using the
- `:limit` option, you may use `:precision`, as intended. For example, in a migration:
+* Remove deprecated `transaction_joinable=` in favor of `begin_transaction`
+ with `:joinable` option.
- def change
- create_table :foobars do |t|
- t.timestamps :precision => 0
- end
- end
+ *Arun Agrawal*
- *Tony Schneider*
+* Remove deprecated `decrement_open_transactions`.
-* Allow `ActiveRecord::Relation#pluck` to accept multiple columns. Returns an
- array of arrays containing the typecasted values:
+ *Arun Agrawal*
- Person.pluck(:id, :name)
- # SELECT people.id, people.name FROM people
- # [[1, 'David'], [2, 'Jeremy'], [3, 'Jose']]
+* Remove deprecated `increment_open_transactions`.
- *Jeroen van Ingen & Carlos Antonio da Silva*
+ *Arun Agrawal*
-* Improve the derivation of HABTM join table name to take account of nesting.
- It now takes the table names of the two models, sorts them lexically and
- then joins them, stripping any common prefix from the second table name.
+* Remove deprecated `PostgreSQLAdapter#outside_transaction?`
+ method. You can use `#transaction_open?` instead.
- Some examples:
+ *Yves Senn*
- Top level models (Category <=> Product)
- Old: categories_products
- New: categories_products
+* Remove deprecated `ActiveRecord::Fixtures.find_table_name` in favor of
+ `ActiveRecord::Fixtures.default_fixture_model_name`.
- Top level models with a global table_name_prefix (Category <=> Product)
- Old: site_categories_products
- New: site_categories_products
+ *Vipul A M*
- Nested models in a module without a table_name_prefix method (Admin::Category <=> Admin::Product)
- Old: categories_products
- New: categories_products
+* Removed deprecated `columns_for_remove` from `SchemaStatements`.
- Nested models in a module with a table_name_prefix method (Admin::Category <=> Admin::Product)
- Old: categories_products
- New: admin_categories_products
+ *Neeraj Singh*
- Nested models in a parent model (Catalog::Category <=> Catalog::Product)
- Old: categories_products
- New: catalog_categories_products
+* Remove deprecated `SchemaStatements#distinct`.
- Nested models in different parent models (Catalog::Category <=> Content::Page)
- Old: categories_pages
- New: catalog_categories_content_pages
+ *Francesco Rodriguez*
- *Andrew White*
+* Move deprecated `ActiveRecord::TestCase` into the rails test
+ suite. The class is no longer public and is only used for internal
+ Rails tests.
-* Move HABTM validity checks to `ActiveRecord::Reflection`. One side effect of
- this is to move when the exceptions are raised from the point of declaration
- to when the association is built. This is consistant with other association
- validity checks.
+ *Yves Senn*
- *Andrew White*
+* Removed support for deprecated option `:restrict` for `:dependent`
+ in associations.
-* Added `stored_attributes` hash which contains the attributes stored using
- `ActiveRecord::Store`. This allows you to retrieve the list of attributes
- you've defined.
+ *Neeraj Singh*
- class User < ActiveRecord::Base
- store :settings, accessors: [:color, :homepage]
- end
+* Removed support for deprecated `delete_sql` in associations.
- User.stored_attributes[:settings] # [:color, :homepage]
+ *Neeraj Singh*
- *Joost Baaij & Carlos Antonio da Silva*
+* Removed support for deprecated `insert_sql` in associations.
-* PostgreSQL default log level is now 'warning', to bypass the noisy notice
- messages. You can change the log level using the `min_messages` option
- available in your config/database.yml.
+ *Neeraj Singh*
- *kennyj*
+* Removed support for deprecated `finder_sql` in associations.
-* Add uuid datatype support to PostgreSQL adapter.
+ *Neeraj Singh*
- *Konstantin Shabanov*
+* Support array as root element in JSON fields.
-* Added `ActiveRecord::Migration.check_pending!` that raises an error if
- migrations are pending.
+ *Alexey Noskov & Francesco Rodriguez*
- *Richard Schneeman*
+* Removed support for deprecated `counter_sql` in associations.
-* Added `#destroy!` which acts like `#destroy` but will raise an
- `ActiveRecord::RecordNotDestroyed` exception instead of returning `false`.
+ *Neeraj Singh*
- *Marc-André Lafortune*
+* Do not invoke callbacks when `delete_all` is called on collection.
-* Allow blocks for `count` with `ActiveRecord::Relation`, to work similar as
- `Array#count`:
+ 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.
- Person.where("age > 26").count { |person| person.gender == 'female' }
+ User can also force a deletion strategy by passing parameter to
+ `delete_all`. For example you can do `@post.comments.delete_all(:nullify)`.
- *Chris Finne & Carlos Antonio da Silva*
+ *Neeraj Singh*
-* Added support to `CollectionAssociation#delete` for passing `fixnum`
- or `string` values as record ids. This finds the records responding
- to the `id` and executes delete on them.
+* Calling default_scope without a proc will now raise `ArgumentError`.
- class Person < ActiveRecord::Base
- has_many :pets
- end
+ *Neeraj Singh*
- person.pets.delete("1") # => [#<Pet id: 1>]
- person.pets.delete(2, 3) # => [#<Pet id: 2>, #<Pet id: 3>]
+* Removed deprecated method `type_cast_code` from Column.
- *Francesco Rodriguez*
+ *Neeraj Singh*
-* Deprecated most of the 'dynamic finder' methods. All dynamic methods
- except for `find_by_...` and `find_by_...!` are deprecated. Here's
- how you can rewrite the code:
+* Removed deprecated options `delete_sql` and `insert_sql` from HABTM
+ association.
- * `find_all_by_...` can be rewritten using `where(...)`
- * `find_last_by_...` can be rewritten using `where(...).last`
- * `scoped_by_...` can be rewritten using `where(...)`
- * `find_or_initialize_by_...` can be rewritten using
- `where(...).first_or_initialize`
- * `find_or_create_by_...` can be rewritten using
- `where(...).first_or_create`
- * `find_or_create_by_...!` can be rewritten using
- `where(...).first_or_create!`
+ Removed deprecated options `finder_sql` and `counter_sql` from
+ collection association.
- The implementation of the deprecated dynamic finders has been moved
- to the `activerecord-deprecated_finders` gem. See below for details.
+ *Neeraj Singh*
- *Jon Leighton*
+* 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*
-* Deprecated the old-style hash based finder API. This means that
- methods which previously accepted "finder options" no longer do. For
- example this:
+* Removed deprecated method `scoped`
- Post.find(:all, conditions: { comments_count: 10 }, limit: 5)
+ *Neeraj Singh*
- Should be rewritten in the new style which has existed since Rails 3:
+* Removed deprecated method `default_scopes?`
- Post.where(comments_count: 10).limit(5)
+ *Neeraj Singh*
- Note that as an interim step, it is possible to rewrite the above as:
+* Remove implicit join references that were deprecated in 4.0.
- Post.all.merge(where: { comments_count: 10 }, limit: 5)
+ Example:
+
+ # before with implicit joins
+ Comment.where('posts.author_id' => 7)
- This could save you a lot of work if there is a lot of old-style
- finder usage in your application.
+ # after
+ Comment.references(:posts).where('posts.author_id' => 7)
- `Relation#merge` now accepts a hash of
- options, but they must be identical to the names of the equivalent
- finder method. These are mostly identical to the old-style finder
- option names, except in the following cases:
+ *Yves Senn*
- * `:conditions` becomes `:where`.
- * `:include` becomes `:includes`.
- * `:extend` becomes `:extending`.
+* Apply default scope when joining associations. For example:
- The code to implement the deprecated features has been moved out to
- the `activerecord-deprecated_finders` gem. This gem is a dependency
- of Active Record in Rails 4.0. It will no longer be a dependency
- from Rails 4.1, but if your app relies on the deprecated features
- then you can add it to your own Gemfile. It will be maintained by
- the Rails core team until Rails 5.0 is released.
+ 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*
-* It's not possible anymore to destroy a model marked as read only.
+* Remove `activerecord-deprecated_finders` as a dependency
- *Johannes Barre*
+ *Łukasz Strzałkowski*
-* Added ability to ActiveRecord::Relation#from to accept other ActiveRecord::Relation objects.
+* Remove Oracle / Sqlserver / Firebird database tasks that were deprecated in 4.0.
- Record.from(subquery)
- Record.from(subquery, :a)
+ *kennyj*
- *Radoslav Stankov*
+* `find_each` now returns an `Enumerator` when called without a block, so that it
+ can be chained with other `Enumerable` methods.
-* Added custom coders support for ActiveRecord::Store. Now you can set
- your custom coder like this:
+ *Ben Woosley*
- store :settings, accessors: [ :color, :homepage ], coder: JSON
+* `ActiveRecord::Result.each` now returns an `Enumerator` when called without
+ a block, so that it can be chained with other `Enumerable` methods.
- *Andrey Voronkov*
+ *Ben Woosley*
-* `mysql` and `mysql2` connections will set `SQL_MODE=STRICT_ALL_TABLES` by
- default to avoid silent data loss. This can be disabled by specifying
- `strict: false` in your `database.yml`.
+* Flatten merged join_values before building the joins.
- *Michael Pearson*
+ 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.
-* Added default order to `first` to assure consistent results among
- different database engines. Introduced `take` as a replacement to
- the old behavior of `first`.
+ Fixes #10669.
- *Marcelo Silveira*
+ *Neeraj Singh and iwiznia*
-* Added an `:index` option to automatically create indexes for references
- and belongs_to statements in migrations.
+* Do not load all child records for inverse case.
- The `references` and `belongs_to` methods now support an `index`
- option that receives either a boolean value or an options hash
- that is identical to options available to the add_index method:
+ currently `post.comments.find(Comment.first.id)` would load all
+ comments for the given post to set the inverse association.
- create_table :messages do |t|
- t.references :person, index: true
- end
+ 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.
- Is the same as:
+ Fix is to use in-memory records only if loaded? is true. Otherwise
+ load the records using full sql.
- create_table :messages do |t|
- t.references :person
- end
- add_index :messages, :person_id
+ Fixes #10509.
- Generators have also been updated to use the new syntax.
+ *Neeraj Singh*
- *Joshua Wood*
+* `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.
-* Added bang methods for mutating `ActiveRecord::Relation` objects.
- For example, while `foo.where(:bar)` will return a new object
- leaving `foo` unchanged, `foo.where!(:bar)` will mutate the foo
- object
+ Example:
- *Jon Leighton*
+ Author.inspect # => "Author(no database connection)"
-* Added `#find_by` and `#find_by!` to mirror the functionality
- provided by dynamic finders in a way that allows dynamic input more
- easily:
+ *Yves Senn*
- Post.find_by name: 'Spartacus', rating: 4
- Post.find_by "published_at < ?", 2.weeks.ago
- Post.find_by! name: 'Spartacus'
+* Handle single quotes in PostgreSQL default column values.
+ Fixes #10881.
- *Jon Leighton*
+ *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*
-* Added ActiveRecord::Base#slice to return a hash of the given methods with
- their names as keys and returned values as values.
+* Fix mysql2 adapter raises the correct exception when executing a query on a
+ closed connection.
- *Guillermo Iguaran*
+ *Yves Senn*
-* Deprecate eager-evaluated scopes.
+* Ambiguous reflections are on :through relationships are no longer supported.
+ For example, you need to change this:
- Don't use this:
+ class Author < ActiveRecord::Base
+ has_many :posts
+ has_many :taggings, :through => :posts
+ end
- scope :red, where(color: 'red')
- default_scope where(color: 'red')
+ class Post < ActiveRecord::Base
+ has_one :tagging
+ has_many :taggings
+ end
- Use this:
+ class Tagging < ActiveRecord::Base
+ end
- scope :red, -> { where(color: 'red') }
- default_scope { where(color: 'red') }
+ To this:
- The former has numerous issues. It is a common newbie gotcha to do
- the following:
+ class Author < ActiveRecord::Base
+ has_many :posts
+ has_many :taggings, :through => :posts, :source => :tagging
+ end
- scope :recent, where(published_at: Time.now - 2.weeks)
+ class Post < ActiveRecord::Base
+ has_one :tagging
+ has_many :taggings
+ end
- Or a more subtle variant:
+ class Tagging < ActiveRecord::Base
+ end
- scope :recent, -> { where(published_at: Time.now - 2.weeks) }
- scope :recent_red, recent.where(color: 'red')
+ *Aaron Patterson*
- Eager scopes are also very complex to implement within Active
- Record, and there are still bugs. For example, the following does
- not do what you expect:
+* 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.
- scope :remove_conditions, except(:where)
- where(...).remove_conditions # => still has conditions
+ Example:
- *Jon Leighton*
+ User.select("name, username").count
+ # Before => SELECT count(*) FROM users
+ # After => ActiveRecord::StatementInvalid
-* Remove IdentityMap
+ # 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
- IdentityMap has never graduated to be an "enabled-by-default" feature, due
- to some inconsistencies with associations, as described in this commit:
+ *Yves Senn*
- https://github.com/rails/rails/commit/302c912bf6bcd0fa200d964ec2dc4a44abe328a6
+* 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.
- Hence the removal from the codebase, until such issues are fixed.
+ 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.
- *Carlos Antonio da Silva*
+ 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.
-* Added the schema cache dump feature.
+ You can turn off the automatic detection of inverse associations by setting
+ the `:inverse_of` option to `false` like so:
- `Schema cache dump` feature was implemetend. This feature can dump/load internal state of `SchemaCache` instance
- because we want to boot rails more quickly when we have many models.
+ class Taggable < ActiveRecord::Base
+ belongs_to :tag, inverse_of: false
+ end
- Usage notes:
+ *John Wang*
- 1) execute rake task.
- RAILS_ENV=production bundle exec rake db:schema:cache:dump
- => generate db/schema_cache.dump
+* Fix `add_column` with `array` option when using PostgreSQL. Fixes #10432
- 2) add config.active_record.use_schema_cache_dump = true in config/production.rb. BTW, true is default.
+ *Adam Anderson*
- 3) boot rails.
- RAILS_ENV=production bundle exec rails server
- => use db/schema_cache.dump
+* Usage of `implicit_readonly` is being removed`. Please use `readonly` method
+ explicitly to mark records as `readonly.
+ Fixes #10615.
- 4) If you remove clear dumped cache, execute rake task.
- RAILS_ENV=production bundle exec rake db:schema:cache:clear
- => remove db/schema_cache.dump
+ Example:
- *kennyj*
+ user = User.joins(:todos).select("users.*, todos.title as todos_title").readonly(true).first
+ user.todos_title = 'clean pet'
+ user.save! # will raise error
-* Added support for partial indices to PostgreSQL adapter.
+ *Yves Senn*
- The `add_index` method now supports a `where` option that receives a
- string with the partial index criteria.
+* Fix the `:primary_key` option for `has_many` associations.
+ Fixes #10693.
- add_index(:accounts, :code, where: 'active')
+ *Yves Senn*
- Generates
+* Fix bug where tiny types are incorrectly coerced as boolean when the length is more than 1.
- CREATE INDEX index_accounts_on_code ON accounts(code) WHERE active
+ Fixes #10620.
- *Marcelo Silveira*
+ *Aaron Patterson*
-* Implemented ActiveRecord::Relation#none method.
+* Also support extensions in PostgreSQL 9.1. This feature has been supported since 9.1.
- The `none` method returns a chainable relation with zero records
- (an instance of the NullRelation class).
+ *kennyj*
- Any subsequent condition chained to the returned relation will continue
- generating an empty relation and will not fire any query to the database.
+* Deprecate `ConnectionAdapters::SchemaStatements#distinct`,
+ as it is no longer used by internals.
- *Juanjo Bazán*
+ *Ben Woosley*
-* Added the `ActiveRecord::NullRelation` class implementing the null
- object pattern for the Relation class.
+* Fix pending migrations error when loading schema and `ActiveRecord::Base.table_name_prefix`
+ is not blank.
- *Juanjo Bazán*
+ Call `assume_migrated_upto_version` on connection to prevent it from first
+ being picked up in `method_missing`.
-* Added new `dependent: :restrict_with_error` option. This will add
- an error to the model, rather than raising an exception.
+ In the base class, `Migration`, `method_missing` expects the argument to be a
+ table name, and calls `proper_table_name` on the arguments before sending to
+ `connection`. If `table_name_prefix` or `table_name_suffix` is used, the schema
+ version changes to `prefix_version_suffix`, breaking `rake test:prepare`.
- The `:restrict` option is renamed to `:restrict_with_exception` to
- make this distinction explicit.
+ Fixes #10411.
- *Manoj Kumar & Jon Leighton*
+ *Kyle Stevens*
-* Added `create_join_table` migration helper to create HABTM join tables.
+* Method `read_attribute_before_type_cast` should accept input as symbol.
- create_join_table :products, :categories
- # =>
- # create_table :categories_products, id: false do |td|
- # td.integer :product_id, null: false
- # td.integer :category_id, null: false
- # end
+ *Neeraj Singh*
- *Rafael Mendonça França*
+* Confirm a record has not already been destroyed before decrementing counter cache.
-* The primary key is always initialized in the @attributes hash to `nil` (unless
- another value has been specified).
+ *Ben Tucker*
- *Aaron Paterson*
+* Fixed a bug in `ActiveRecord#sanitize_sql_hash_for_conditions` in which
+ `self.class` is an argument to `PredicateBuilder#build_from_hash`
+ causing `PredicateBuilder` to call non-existent method
+ `Class#reflect_on_association`.
-* In previous releases, the following would generate a single query with
- an `OUTER JOIN comments`, rather than two separate queries:
+ *Zach Ohlgren*
- Post.includes(:comments)
- .where("comments.name = 'foo'")
+* While removing index if column option is missing then raise IrreversibleMigration exception.
- This behaviour relies on matching SQL string, which is an inherently
- flawed idea unless we write an SQL parser, which we do not wish to
- do.
+ Following code should raise `IrreversibleMigration`. But the code was
+ failing since options is an array and not a hash.
- Therefore, it is now deprecated.
+ def change
+ change_table :users do |t|
+ t.remove_index [:name, :email]
+ end
+ end
- To avoid deprecation warnings and for future compatibility, you must
- explicitly state which tables you reference, when using SQL snippets:
+ Fix was to check if the options is a Hash before operating on it.
- Post.includes(:comments)
- .where("comments.name = 'foo'")
- .references(:comments)
+ Fixes #10419.
- Note that you do not need to explicitly specify references in the
- following cases, as they can be automatically inferred:
+ *Neeraj Singh*
- Post.where(comments: { name: 'foo' })
- Post.where('comments.name' => 'foo')
- Post.order('comments.name')
+* Do not overwrite manually built records during one-to-one nested attribute assignment
- You also do not need to worry about this unless you are doing eager
- loading. Basically, don't worry unless you see a deprecation warning
- or (in future releases) an SQL error due to a missing JOIN.
+ For one-to-one nested associations, if you build the new (in-memory)
+ child object yourself before assignment, then the NestedAttributes
+ module will not overwrite it, e.g.:
- *Jon Leighton*
+ class Member < ActiveRecord::Base
+ has_one :avatar
+ accepts_nested_attributes_for :avatar
-* Support for the `schema_info` table has been dropped. Please
- switch to `schema_migrations`.
+ def avatar
+ super || build_avatar(width: 200)
+ end
+ end
- *Aaron Patterson*
+ member = Member.new
+ member.avatar_attributes = {icon: 'sad'}
+ member.avatar.width # => 200
-* Connections *must* be closed at the end of a thread. If not, your
- connection pool can fill and an exception will be raised.
+ *Olek Janiszewski*
- *Aaron Patterson*
+* 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.
+
+ *Johnny Holton*
+
+* Handle aliased attributes in ActiveRecord::Relation.
+
+ When using symbol keys, ActiveRecord will now translate aliased attribute names to the actual column name used in the database:
-* Added the `ActiveRecord::Model` module which can be included in a
- class as an alternative to inheriting from `ActiveRecord::Base`:
+ With the model
- class Post
- include ActiveRecord::Model
+ class Topic
+ alias_attribute :heading, :title
end
- Please note:
+ The call
- * Up until now it has been safe to assume that all AR models are
- descendants of `ActiveRecord::Base`. This is no longer a safe
- assumption, but it may transpire that there are areas of the
- code which still make this assumption. So there may be
- 'teething difficulties' with this feature. (But please do try it
- and report bugs.)
+ Topic.where(heading: 'The First Topic')
- * Plugins & libraries etc that add methods to `ActiveRecord::Base`
- will not be compatible with `ActiveRecord::Model`. Those libraries
- should add to `ActiveRecord::Model` instead (which is included in
- `Base`), or better still, avoid monkey-patching AR and instead
- provide a module that users can include where they need it.
+ should yield the same result as
- * To minimise the risk of conflicts with other code, it is
- advisable to include `ActiveRecord::Model` early in your class
- definition.
+ Topic.where(title: 'The First Topic')
- *Jon Leighton*
+ This also applies to ActiveRecord::Relation::Calculations calls such as `Model.sum(:aliased)` and `Model.pluck(:aliased)`.
-* PostgreSQL hstore records can be created.
+ This will not work with SQL fragment strings like `Model.sum('DISTINCT aliased')`.
- *Aaron Patterson*
+ *Godfrey Chan*
-* PostgreSQL hstore types are automatically deserialized from the database.
+* Mute `psql` output when running rake db:schema:load.
- *Aaron Patterson*
+ *Godfrey Chan*
+
+* Trigger a save on `has_one association=(associate)` when the associate contents have changed.
+
+ Fix #8856.
+
+ *Chris Thompson*
+
+* Abort a rake task when missing db/structure.sql like `db:schema:load` task.
+
+ *kennyj*
+
+* rake:db:test:prepare falls back to original environment after execution.
+
+ *Slava Markevich*
-Please check [3-2-stable](https://github.com/rails/rails/blob/3-2-stable/activerecord/CHANGELOG.md) for previous changes.
+Please check [4-0-stable](https://github.com/rails/rails/blob/4-0-stable/activerecord/CHANGELOG.md) for previous changes.
diff --git a/activerecord/MIT-LICENSE b/activerecord/MIT-LICENSE
index 03bde18130..0d7fb865e2 100644
--- a/activerecord/MIT-LICENSE
+++ b/activerecord/MIT-LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2004-2012 David Heinemeier Hansson
+Copyright (c) 2004-2013 David Heinemeier Hansson
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
diff --git a/activerecord/README.rdoc b/activerecord/README.rdoc
index cc8942809c..e04abe9b37 100644
--- a/activerecord/README.rdoc
+++ b/activerecord/README.rdoc
@@ -80,17 +80,6 @@ A short rundown of some of the major features:
{Learn more}[link:classes/ActiveRecord/Callbacks.html]
-* Observers that react to changes in a model.
-
- class CommentObserver < ActiveRecord::Observer
- def after_create(comment) # is called just after Comment#save
- CommentMailer.new_comment_email('david@loudthinking.com', comment).deliver
- end
- end
-
- {Learn more}[link:classes/ActiveRecord/Observer.html]
-
-
* Inheritance hierarchies.
class Company < ActiveRecord::Base; end
@@ -141,7 +130,7 @@ A short rundown of some of the major features:
SQLite3[link:classes/ActiveRecord/ConnectionAdapters/SQLite3Adapter.html].
-* Logging support for Log4r[http://log4r.sourceforge.net] and Logger[http://www.ruby-doc.org/stdlib/libdoc/logger/rdoc].
+* Logging support for Log4r[http://log4r.rubyforge.org] and Logger[http://www.ruby-doc.org/stdlib/libdoc/logger/rdoc].
ActiveRecord::Base.logger = ActiveSupport::Logger.new(STDOUT)
ActiveRecord::Base.logger = Log4r::Logger.new('Application Log')
@@ -186,7 +175,7 @@ by relying on a number of conventions that make it easy for Active Record to inf
complex relations and structures from a minimal amount of explicit direction.
Convention over Configuration:
-* No XML-files!
+* No XML files!
* Lots of reflection and run-time extension
* Magic is not inherently a bad word
@@ -201,7 +190,7 @@ The latest version of Active Record can be installed with RubyGems:
% [sudo] gem install activerecord
-Source code can be downloaded as part of the Rails project on GitHub
+Source code can be downloaded as part of the Rails project on GitHub:
* https://github.com/rails/rails/tree/master/activerecord
@@ -215,7 +204,7 @@ Active Record is released under the MIT license:
== Support
-API documentation is at
+API documentation is at:
* http://api.rubyonrails.org
diff --git a/activerecord/RUNNING_UNIT_TESTS b/activerecord/RUNNING_UNIT_TESTS
deleted file mode 100644
index bdd8834dcb..0000000000
--- a/activerecord/RUNNING_UNIT_TESTS
+++ /dev/null
@@ -1,31 +0,0 @@
-== Setup
-
-If you don't have the environment set make sure to read
-
- http://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html#testing-active-record.
-
-== Running the Tests
-
-You can run a particular test file from the command line, e.g.
-
- $ ruby -Itest test/cases/base_test.rb
-
-To run a specific test:
-
- $ ruby -Itest test/cases/base_test.rb -n test_something_works
-
-You can run with a database other than the default you set in test/config.yml, using the ARCONN
-environment variable:
-
- $ ARCONN=postgresql ruby -Itest test/cases/base_test.rb
-
-You can run all the tests for a given database via rake:
-
- $ rake test_mysql
-
-The 'rake test' task will run all the tests for mysql, mysql2, sqlite3 and postgresql.
-
-== Custom Config file
-
-By default, the config file is expected to be at the path test/config.yml. You can specify a
-custom location with the ARCONFIG environment variable.
diff --git a/activerecord/RUNNING_UNIT_TESTS.rdoc b/activerecord/RUNNING_UNIT_TESTS.rdoc
new file mode 100644
index 0000000000..ca1f2fd665
--- /dev/null
+++ b/activerecord/RUNNING_UNIT_TESTS.rdoc
@@ -0,0 +1,44 @@
+== Setup
+
+If you don't have an environment for running tests, read
+http://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html#setting-up-a-development-environment
+
+== Running the Tests
+
+To run a specific test:
+
+ $ ruby -Itest test/cases/base_test.rb -n method_name
+
+To run a set of tests:
+
+ $ ruby -Itest test/cases/base_test.rb
+
+You can also run tests that depend upon a specific database backend. For
+example:
+
+ $ bundle exec rake test_sqlite3
+
+Simply executing <tt>bundle exec rake test</tt> is equivalent to the following:
+
+ $ bundle exec rake test_mysql
+ $ bundle exec rake test_mysql2
+ $ bundle exec rake test_postgresql
+ $ bundle exec rake test_sqlite3
+ $ bundle exec rake test_sqlite3_mem
+
+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
+defined in +Rakefile+)
+
+== Config File
+
+If +test/config.yml+ is present, it's parameters are obeyed. Otherwise, the
+parameters in +test/config.example.yml+ are obeyed.
+
+You can override the +connections:+ parameter in either file using the +ARCONN+
+(Active Record CONNection) environment variable:
+
+ $ ARCONN=postgresql ruby -Itest test/cases/base_test.rb
+
+You can specify a custom location for the config file using the +ARCONFIG+
+environment variable.
diff --git a/activerecord/Rakefile b/activerecord/Rakefile
index 53ddff420e..cee1dd5aeb 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"
@@ -39,6 +38,11 @@ namespace :test do
end
end
+namespace :db do
+ task :create => ['mysql:build_databases', 'postgresql:build_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|
Rake::TestTask.new("test_#{adapter}") { |t|
adapter_short = adapter == 'db2' ? adapter : adapter[/^[a-z0-9]+/]
@@ -54,11 +58,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
@@ -115,14 +118,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"
- else
- %x( psql #{config[db]['database']} -c "CREATE EXTENSION hstore;" )
- 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
@@ -220,7 +218,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 53791d96ef..9986ded904 100644
--- a/activerecord/activerecord.gemspec
+++ b/activerecord/activerecord.gemspec
@@ -1,4 +1,4 @@
-version = File.read(File.expand_path("../../RAILS_VERSION", __FILE__)).strip
+version = File.read(File.expand_path('../../RAILS_VERSION', __FILE__)).strip
Gem::Specification.new do |s|
s.platform = Gem::Platform::RUBY
@@ -8,21 +8,21 @@ Gem::Specification.new do |s|
s.description = 'Databases on Rails. Build a persistent domain model by mapping database tables to Ruby classes. Strong conventions for associations, validations, aggregations, migrations, and testing come baked-in.'
s.required_ruby_version = '>= 1.9.3'
- s.license = 'MIT'
- s.author = 'David Heinemeier Hansson'
- s.email = 'david@loudthinking.com'
- s.homepage = 'http://www.rubyonrails.org'
+ s.license = 'MIT'
+
+ s.author = 'David Heinemeier Hansson'
+ s.email = 'david@loudthinking.com'
+ s.homepage = 'http://www.rubyonrails.org'
s.files = Dir['CHANGELOG.md', 'MIT-LICENSE', 'README.rdoc', 'examples/**/*', 'lib/**/*']
s.require_path = 'lib'
- s.extra_rdoc_files = %w( README.rdoc )
+ s.extra_rdoc_files = %w(README.rdoc)
s.rdoc_options.concat ['--main', 'README.rdoc']
- s.add_dependency('activesupport', version)
- s.add_dependency('activemodel', version)
- s.add_dependency('arel', '~> 3.0.2')
+ s.add_dependency 'activesupport', version
+ s.add_dependency 'activemodel', version
- s.add_dependency('activerecord-deprecated_finders', '0.0.1')
+ s.add_dependency 'arel', '~> 4.0.0'
end
diff --git a/activerecord/examples/associations.png b/activerecord/examples/associations.png
deleted file mode 100644
index 661c7a8bbc..0000000000
--- a/activerecord/examples/associations.png
+++ /dev/null
Binary files differ
diff --git a/activerecord/examples/performance.rb b/activerecord/examples/performance.rb
index cd9825b50c..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
@@ -143,7 +155,7 @@ Benchmark.ips(TIME) do |x|
end
x.report 'Resource#update' do
- Exhibit.first.update_attributes(:name => 'bob')
+ Exhibit.first.update(name: 'bob')
end
x.report 'Resource#destroy' do
@@ -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 1675127ab0..f19f5ecdf9 100644
--- a/activerecord/lib/active_record.rb
+++ b/activerecord/lib/active_record.rb
@@ -1,5 +1,5 @@
#--
-# Copyright (c) 2004-2012 David Heinemeier Hansson
+# Copyright (c) 2004-2013 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'
@@ -35,29 +34,29 @@ module ActiveRecord
autoload :Base
autoload :Callbacks
autoload :Core
- autoload :CounterCache
autoload :ConnectionHandling
+ autoload :CounterCache
autoload :DynamicMatchers
autoload :Explain
autoload :Inheritance
autoload :Integration
autoload :Migration
autoload :Migrator, 'active_record/migration'
- autoload :Model
autoload :ModelSchema
autoload :NestedAttributes
- autoload :Observer
autoload :Persistence
autoload :QueryCache
autoload :Querying
autoload :ReadonlyAttributes
autoload :Reflection
+ autoload :RuntimeRegistry
autoload :Sanitization
autoload :Schema
autoload :SchemaDumper
autoload :SchemaMigration
autoload :Scoping
autoload :Serialization
+ autoload :StatementCache
autoload :Store
autoload :Timestamp
autoload :Transactions
@@ -71,11 +70,12 @@ module ActiveRecord
autoload :Aggregations
autoload :Associations
- autoload :AttributeMethods
autoload :AttributeAssignment
+ autoload :AttributeMethods
autoload :AutosaveAssociation
autoload :Relation
+ autoload :AssociationRelation
autoload :NullRelation
autoload_under 'relation' do
@@ -147,7 +147,6 @@ module ActiveRecord
'active_record/tasks/postgresql_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 3db8e0716b..0d5313956b 100644
--- a/activerecord/lib/active_record/aggregations.rb
+++ b/activerecord/lib/active_record/aggregations.rb
@@ -16,8 +16,8 @@ module ActiveRecord
# the database).
#
# class Customer < ActiveRecord::Base
- # composed_of :balance, :class_name => "Money", :mapping => %w(balance amount)
- # composed_of :address, :mapping => [ %w(address_street street), %w(address_city city) ]
+ # composed_of :balance, class_name: "Money", mapping: %w(balance amount)
+ # composed_of :address, mapping: [ %w(address_street street), %w(address_city city) ]
# end
#
# The customer class now has the following methods to manipulate the value objects:
@@ -113,7 +113,7 @@ module ActiveRecord
# other than the writer method.
#
# The immutable requirement is enforced by Active Record by freezing any object assigned as a value
- # object. Attempting to change it afterwards will result in a ActiveSupport::FrozenObjectError.
+ # object. Attempting to change it afterwards will result in a RuntimeError.
#
# Read more about value objects on http://c2.com/cgi/wiki?ValueObject and on the dangers of not
# keeping value objects immutable on http://c2.com/cgi/wiki?ValueObjectsShouldBeImmutable
@@ -138,15 +138,15 @@ module ActiveRecord
#
# class NetworkResource < ActiveRecord::Base
# composed_of :cidr,
- # :class_name => 'NetAddr::CIDR',
- # :mapping => [ %w(network_address network), %w(cidr_range bits) ],
- # :allow_nil => true,
- # :constructor => Proc.new { |network_address, cidr_range| NetAddr::CIDR.create("#{network_address}/#{cidr_range}") },
- # :converter => Proc.new { |value| NetAddr::CIDR.create(value.is_a?(Array) ? value.join('/') : value) }
+ # class_name: 'NetAddr::CIDR',
+ # mapping: [ %w(network_address network), %w(cidr_range bits) ],
+ # allow_nil: true,
+ # constructor: Proc.new { |network_address, cidr_range| NetAddr::CIDR.create("#{network_address}/#{cidr_range}") },
+ # converter: Proc.new { |value| NetAddr::CIDR.create(value.is_a?(Array) ? value.join('/') : value) }
# end
#
# # This calls the :constructor
- # network_resource = NetworkResource.new(:network_address => '192.168.0.1', :cidr_range => 24)
+ # network_resource = NetworkResource.new(network_address: '192.168.0.1', cidr_range: 24)
#
# # These assignments will both use the :converter
# network_resource.cidr = [ '192.168.2.1', 8 ]
@@ -165,7 +165,7 @@ module ActiveRecord
# by specifying an instance of the value object in the conditions hash. The following example
# finds all customers with +balance_amount+ equal to 20 and +balance_currency+ equal to "USD":
#
- # Customer.where(:balance => Money.new(20, "USD")).all
+ # Customer.where(balance: Money.new(20, "USD"))
#
module ClassMethods
# Adds reader and writer methods for manipulating a value object:
@@ -197,17 +197,17 @@ module ActiveRecord
# can return nil to skip the assignment.
#
# Option examples:
- # composed_of :temperature, :mapping => %w(reading celsius)
- # composed_of :balance, :class_name => "Money", :mapping => %w(balance amount),
- # :converter => Proc.new { |balance| balance.to_money }
- # composed_of :address, :mapping => [ %w(address_street street), %w(address_city city) ]
+ # composed_of :temperature, mapping: %w(reading celsius)
+ # composed_of :balance, class_name: "Money", mapping: %w(balance amount),
+ # converter: Proc.new { |balance| balance.to_money }
+ # composed_of :address, mapping: [ %w(address_street street), %w(address_city city) ]
# composed_of :gps_location
- # composed_of :gps_location, :allow_nil => true
+ # composed_of :gps_location, allow_nil: true
# composed_of :ip_address,
- # :class_name => 'IPAddr',
- # :mapping => %w(ip to_i),
- # :constructor => Proc.new { |ip| IPAddr.new(ip, Socket::AF_INET) },
- # :converter => Proc.new { |ip| ip.is_a?(Integer) ? IPAddr.new(ip, Socket::AF_INET) : IPAddr.new(ip.to_s) }
+ # class_name: 'IPAddr',
+ # mapping: %w(ip to_i),
+ # constructor: Proc.new { |ip| IPAddr.new(ip, Socket::AF_INET) },
+ # converter: Proc.new { |ip| ip.is_a?(Integer) ? IPAddr.new(ip, Socket::AF_INET) : IPAddr.new(ip.to_s) }
#
def composed_of(part_id, options = {})
options.assert_valid_keys(:class_name, :mapping, :allow_nil, :constructor, :converter)
@@ -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/association_relation.rb b/activerecord/lib/active_record/association_relation.rb
new file mode 100644
index 0000000000..20516bba0c
--- /dev/null
+++ b/activerecord/lib/active_record/association_relation.rb
@@ -0,0 +1,18 @@
+module ActiveRecord
+ class AssociationRelation < Relation
+ def initialize(klass, table, association)
+ super(klass, table)
+ @association = association
+ end
+
+ def proxy_association
+ @association
+ end
+
+ private
+
+ def exec_queries
+ super.each { |r| @association.set_inverse_instance r }
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb
index 69b95f814c..74e2774626 100644
--- a/activerecord/lib/active_record/associations.rb
+++ b/activerecord/lib/active_record/associations.rb
@@ -1,6 +1,7 @@
require 'active_support/core_ext/enumerable'
require 'active_support/core_ext/string/conversions'
require 'active_support/core_ext/module/remove_method'
+require 'active_record/errors'
module ActiveRecord
class InverseOfAssociationNotFoundError < ActiveRecordError #:nodoc:
@@ -72,12 +73,6 @@ 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}")
@@ -113,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'
@@ -163,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.
@@ -171,7 +165,7 @@ module ActiveRecord
@association_cache[name] = association
end
- # Associations are a set of macro-like class methods for tying objects together through
+ # \Associations are a set of macro-like class methods for tying objects together through
# foreign keys. They express relationships like "Project has one Project Manager"
# or "Project belongs to a Portfolio". Each macro adds a number of methods to the
# class which are specialized according to the collection or association symbol and the
@@ -190,8 +184,8 @@ module ActiveRecord
# * <tt>Project#portfolio, Project#portfolio=(portfolio), Project#portfolio.nil?</tt>
# * <tt>Project#project_manager, Project#project_manager=(project_manager), Project#project_manager.nil?,</tt>
# * <tt>Project#milestones.empty?, Project#milestones.size, Project#milestones, Project#milestones<<(milestone),</tt>
- # <tt>Project#milestones.delete(milestone), Project#milestones.destroy(mileston), Project#milestones.find(milestone_id),</tt>
- # <tt>Project#milestones.all(options), Project#milestones.build, Project#milestones.create</tt>
+ # <tt>Project#milestones.delete(milestone), Project#milestones.destroy(milestone), Project#milestones.find(milestone_id),</tt>
+ # <tt>Project#milestones.build, Project#milestones.create</tt>
# * <tt>Project#categories.empty?, Project#categories.size, Project#categories, Project#categories<<(category1),</tt>
# <tt>Project#categories.delete(category1), Project#categories.destroy(category1)</tt>
#
@@ -231,7 +225,7 @@ module ActiveRecord
# others.size | X | X | X
# others.length | X | X | X
# others.count | X | X | X
- # others.sum(args*,&block) | X | X | X
+ # others.sum(*args) | X | X | X
# others.empty? | X | X | X
# others.clear | X | X | X
# others.delete(other,other,...) | X | X | X
@@ -240,6 +234,7 @@ module ActiveRecord
# others.destroy_all | X | X | X
# others.find(*args) | X | X | X
# others.exists? | X | X | X
+ # others.distinct | X | X | X
# others.uniq | X | X | X
# others.reset | X | X | X
#
@@ -305,11 +300,11 @@ module ActiveRecord
# end
# class Programmer < ActiveRecord::Base
# has_many :assignments
- # has_many :projects, :through => :assignments
+ # has_many :projects, through: :assignments
# end
# class Project < ActiveRecord::Base
# has_many :assignments
- # has_many :programmers, :through => :assignments
+ # has_many :programmers, through: :assignments
# end
#
# For the second way, use +has_and_belongs_to_many+ in both models. This requires a join table
@@ -363,11 +358,11 @@ module ActiveRecord
# there is some special behavior you should be aware of, mostly involving the saving of
# associated objects.
#
- # You can set the :autosave option on a <tt>has_one</tt>, <tt>belongs_to</tt>,
+ # You can set the <tt>:autosave</tt> option on a <tt>has_one</tt>, <tt>belongs_to</tt>,
# <tt>has_many</tt>, or <tt>has_and_belongs_to_many</tt> association. Setting it
# to +true+ will _always_ save the members, whereas setting it to +false+ will
- # _never_ save the members. More details about :autosave option is available at
- # autosave_association.rb .
+ # _never_ save the members. More details about <tt>:autosave</tt> option is available at
+ # AutosaveAssociation.
#
# === One-to-one associations
#
@@ -400,7 +395,7 @@ module ActiveRecord
#
# == Customizing the query
#
- # Associations are built from <tt>Relation</tt>s, and you can use the <tt>Relation</tt> syntax
+ # \Associations are built from <tt>Relation</tt>s, and you can use the <tt>Relation</tt> syntax
# to customize them. For example, to add a condition:
#
# class Blog < ActiveRecord::Base
@@ -426,7 +421,7 @@ module ActiveRecord
# object from an association collection.
#
# class Project
- # has_and_belongs_to_many :developers, :after_add => :evaluate_velocity
+ # has_and_belongs_to_many :developers, after_add: :evaluate_velocity
#
# def evaluate_velocity(developer)
# ...
@@ -437,7 +432,7 @@ module ActiveRecord
#
# class Project
# has_and_belongs_to_many :developers,
- # :after_add => [:evaluate_velocity, Proc.new { |p, d| p.shipping_date = Time.now}]
+ # after_add: [:evaluate_velocity, Proc.new { |p, d| p.shipping_date = Time.now}]
# end
#
# Possible callbacks are: +before_add+, +after_add+, +before_remove+ and +after_remove+.
@@ -456,7 +451,7 @@ module ActiveRecord
# has_many :people do
# def find_or_create_by_name(name)
# first_name, last_name = name.split(" ", 2)
- # find_or_create_by_first_name_and_last_name(first_name, last_name)
+ # find_or_create_by(first_name: first_name, last_name: last_name)
# end
# end
# end
@@ -471,7 +466,7 @@ module ActiveRecord
# module FindOrCreateByNameExtension
# def find_or_create_by_name(name)
# first_name, last_name = name.split(" ", 2)
- # find_or_create_by_first_name_and_last_name(first_name, last_name)
+ # find_or_create_by(first_name: first_name, last_name: last_name)
# end
# end
#
@@ -507,7 +502,7 @@ module ActiveRecord
#
# class Author < ActiveRecord::Base
# has_many :authorships
- # has_many :books, :through => :authorships
+ # has_many :books, through: :authorships
# end
#
# class Authorship < ActiveRecord::Base
@@ -523,7 +518,7 @@ module ActiveRecord
#
# class Firm < ActiveRecord::Base
# has_many :clients
- # has_many :invoices, :through => :clients
+ # has_many :invoices, through: :clients
# end
#
# class Client < ActiveRecord::Base
@@ -543,7 +538,7 @@ module ActiveRecord
#
# class Group < ActiveRecord::Base
# has_many :users
- # has_many :avatars, :through => :users
+ # has_many :avatars, through: :users
# end
#
# class User < ActiveRecord::Base
@@ -566,12 +561,14 @@ module ActiveRecord
# @group.avatars << Avatar.new # this would work if User belonged_to Avatar rather than the other way around
# @group.avatars.delete(@group.avatars.last) # so would this
#
+ # == Setting Inverses
+ #
# If you are using a +belongs_to+ on the join model, it is a good idea to set the
# <tt>:inverse_of</tt> option on the +belongs_to+, which will mean that the following example
# works correctly (where <tt>tags</tt> is a +has_many+ <tt>:through</tt> association):
#
# @post = Post.first
- # @tag = @post.tags.build :name => "ruby"
+ # @tag = @post.tags.build name: "ruby"
# @tag.save
#
# The last line ought to save the through record (a <tt>Taggable</tt>). This will only work if the
@@ -579,18 +576,38 @@ module ActiveRecord
#
# class Taggable < ActiveRecord::Base
# belongs_to :post
- # belongs_to :tag, :inverse_of => :taggings
+ # belongs_to :tag, inverse_of: :taggings
+ # end
+ #
+ # If you do not set the <tt>:inverse_of</tt> record, the association will
+ # do its best to match itself up with the correct inverse. Automatic
+ # inverse detection only works on <tt>has_many</tt>, <tt>has_one</tt>, and
+ # <tt>belongs_to</tt> associations.
+ #
+ # Extra options on the associations, as defined in the
+ # <tt>AssociationReflection::INVALID_AUTOMATIC_INVERSE_OPTIONS</tt> constant, 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 <tt>:inverse_of</tt> option to <tt>false</tt> like so:
+ #
+ # class Taggable < ActiveRecord::Base
+ # belongs_to :tag, inverse_of: false
# end
#
- # == Nested Associations
+ # == Nested \Associations
#
# You can actually specify *any* association with the <tt>:through</tt> option, including an
# association which has a <tt>:through</tt> option itself. For example:
#
# class Author < ActiveRecord::Base
# has_many :posts
- # has_many :comments, :through => :posts
- # has_many :commenters, :through => :comments
+ # has_many :comments, through: :posts
+ # has_many :commenters, through: :comments
# end
#
# class Post < ActiveRecord::Base
@@ -608,12 +625,12 @@ module ActiveRecord
#
# class Author < ActiveRecord::Base
# has_many :posts
- # has_many :commenters, :through => :posts
+ # has_many :commenters, through: :posts
# end
#
# class Post < ActiveRecord::Base
# has_many :comments
- # has_many :commenters, :through => :comments
+ # has_many :commenters, through: :comments
# end
#
# class Comment < ActiveRecord::Base
@@ -625,18 +642,18 @@ module ActiveRecord
# add a <tt>Commenter</tt> in the example above, there would be no way to tell how to set up the
# intermediate <tt>Post</tt> and <tt>Comment</tt> objects.
#
- # == Polymorphic Associations
+ # == Polymorphic \Associations
#
# Polymorphic associations on models are not restricted on what types of models they
# can be associated with. Rather, they specify an interface that a +has_many+ association
# must adhere to.
#
# class Asset < ActiveRecord::Base
- # belongs_to :attachable, :polymorphic => true
+ # belongs_to :attachable, polymorphic: true
# end
#
# class Post < ActiveRecord::Base
- # has_many :assets, :as => :attachable # The :as option specifies the polymorphic interface to use.
+ # has_many :assets, as: :attachable # The :as option specifies the polymorphic interface to use.
# end
#
# @asset.attachable = @post
@@ -653,7 +670,7 @@ module ActiveRecord
# column in the posts table.
#
# class Asset < ActiveRecord::Base
- # belongs_to :attachable, :polymorphic => true
+ # belongs_to :attachable, polymorphic: true
#
# def attachable_type=(sType)
# super(sType.to_s.classify.constantize.base_class.to_s)
@@ -661,8 +678,8 @@ module ActiveRecord
# end
#
# class Post < ActiveRecord::Base
- # # because we store "Post" in attachable_type now :dependent => :destroy will work
- # has_many :assets, :as => :attachable, :dependent => :destroy
+ # # because we store "Post" in attachable_type now dependent: :destroy will work
+ # has_many :assets, as: :attachable, dependent: :destroy
# end
#
# class GuestPost < Post
@@ -724,7 +741,7 @@ module ActiveRecord
#
# To include a deep hierarchy of associations, use a hash:
#
- # Post.includes(:author, {:comments => {:author => :gravatar}}).each do |post|
+ # Post.includes(:author, {comments: {author: :gravatar}}).each do |post|
#
# That'll grab not only all the comments but all their authors and gravatar pictures.
# You can mix and match symbols, arrays and hashes in any combination to describe the
@@ -740,7 +757,7 @@ module ActiveRecord
# other than the main one. If this is the case Active Record falls back to the previously
# used LEFT OUTER JOIN based strategy. For example
#
- # Post.includes([:author, :comments]).where(['comments.approved = ?', true]).all
+ # Post.includes([:author, :comments]).where(['comments.approved = ?', true])
#
# This will result in a single SQL query with joins along the lines of:
# <tt>LEFT OUTER JOIN comments ON comments.post_id = posts.id</tt> and
@@ -749,13 +766,13 @@ module ActiveRecord
# In the above example posts with no approved comments are not returned at all, because
# the conditions apply to the SQL statement as a whole and not just to the association.
# You must disambiguate column references for this fallback to happen, for example
- # <tt>:order => "author.name DESC"</tt> will work but <tt>:order => "name DESC"</tt> will not.
+ # <tt>order: "author.name DESC"</tt> will work but <tt>order: "name DESC"</tt> will not.
#
# If you do want eager load only some members of an association it is usually more natural
# to include an association which has conditions defined on it:
#
# class Post < ActiveRecord::Base
- # has_many :approved_comments, -> { where approved: true }, :class_name => 'Comment'
+ # has_many :approved_comments, -> { where approved: true }, class_name: 'Comment'
# end
#
# Post.includes(:approved_comments)
@@ -767,7 +784,7 @@ module ActiveRecord
# returning all the associated objects:
#
# class Picture < ActiveRecord::Base
- # has_many :most_recent_comments, -> { order('id DESC').limit(10) }, :class_name => 'Comment'
+ # has_many :most_recent_comments, -> { order('id DESC').limit(10) }, class_name: 'Comment'
# end
#
# Picture.includes(:most_recent_comments).first.most_recent_comments # => returns all associated comments.
@@ -775,7 +792,7 @@ module ActiveRecord
# Eager loading is supported with polymorphic associations.
#
# class Address < ActiveRecord::Base
- # belongs_to :addressable, :polymorphic => true
+ # belongs_to :addressable, polymorphic: true
# end
#
# A call that tries to eager load the addressable model
@@ -787,7 +804,7 @@ module ActiveRecord
# For example if all the addressables are either of class Person or Company then a total
# of 3 queries will be executed. The list of addressable types to load is determined on
# the back of the addresses loaded. This is not supported if Active Record has to fallback
- # to the previous implementation of eager loading and will raise ActiveRecord::EagerLoadPolymorphicError.
+ # to the previous implementation of eager loading and will raise <tt>ActiveRecord::EagerLoadPolymorphicError</tt>.
# The reason is that the parent model's type is a column value so its corresponding table
# name cannot be put in the +FROM+/+JOIN+ clauses of that query.
#
@@ -809,10 +826,10 @@ module ActiveRecord
#
# TreeMixin.joins(:children)
# # => SELECT ... FROM mixins INNER JOIN mixins childrens_mixins ...
- # TreeMixin.joins(:children => :parent)
+ # TreeMixin.joins(children: :parent)
# # => SELECT ... FROM mixins INNER JOIN mixins childrens_mixins ...
# INNER JOIN parents_mixins ...
- # TreeMixin.joins(:children => {:parent => :children})
+ # TreeMixin.joins(children: {parent: :children})
# # => SELECT ... FROM mixins INNER JOIN mixins childrens_mixins ...
# INNER JOIN parents_mixins ...
# INNER JOIN mixins childrens_mixins_2
@@ -821,10 +838,10 @@ module ActiveRecord
#
# Post.joins(:categories)
# # => SELECT ... FROM posts INNER JOIN categories_posts ... INNER JOIN categories ...
- # Post.joins(:categories => :posts)
+ # Post.joins(categories: :posts)
# # => SELECT ... FROM posts INNER JOIN categories_posts ... INNER JOIN categories ...
# INNER JOIN categories_posts posts_categories_join INNER JOIN posts posts_categories
- # Post.joins(:categories => {:posts => :categories})
+ # Post.joins(categories: {posts: :categories})
# # => SELECT ... FROM posts INNER JOIN categories_posts ... INNER JOIN categories ...
# INNER JOIN categories_posts posts_categories_join INNER JOIN posts posts_categories
# INNER JOIN categories_posts categories_posts_join INNER JOIN categories categories_posts_2
@@ -868,7 +885,7 @@ module ActiveRecord
#
# module Billing
# class Account < ActiveRecord::Base
- # belongs_to :firm, :class_name => "MyApplication::Business::Firm"
+ # belongs_to :firm, class_name: "MyApplication::Business::Firm"
# end
# end
# end
@@ -910,16 +927,16 @@ module ActiveRecord
# example, if we changed our model definitions to:
#
# class Dungeon < ActiveRecord::Base
- # has_many :traps, :inverse_of => :dungeon
- # has_one :evil_wizard, :inverse_of => :dungeon
+ # has_many :traps, inverse_of: :dungeon
+ # has_one :evil_wizard, inverse_of: :dungeon
# end
#
# class Trap < ActiveRecord::Base
- # belongs_to :dungeon, :inverse_of => :traps
+ # belongs_to :dungeon, inverse_of: :traps
# end
#
# class EvilWizard < ActiveRecord::Base
- # belongs_to :dungeon, :inverse_of => :evil_wizard
+ # belongs_to :dungeon, inverse_of: :evil_wizard
# end
#
# Then, from our code snippet above, +d+ and <tt>t.dungeon</tt> are actually the same
@@ -942,13 +959,13 @@ module ActiveRecord
# For example:
#
# class Author
- # has_many :posts, :dependent => :destroy
+ # has_many :posts, dependent: :destroy
# end
# Author.find(1).destroy # => Will destroy all of the author's posts, too
#
# The <tt>:dependent</tt> option can have different values which specify how the deletion
# is done. For more information, see the documentation for this option on the different
- # specific association types. When no option is given, the behaviour is to do nothing
+ # specific association types. When no option is given, the behavior is to do nothing
# with the associated records when destroying a record.
#
# Note that <tt>:dependent</tt> is implemented using Rails' callback
@@ -964,8 +981,8 @@ module ActiveRecord
# For +has_and_belongs_to_many+, <tt>delete</tt> and <tt>destroy</tt> are the same: they
# cause the records in the join table to be removed.
#
- # For +has_many+, <tt>destroy</tt> will always call the <tt>destroy</tt> method of the
- # record(s) being removed so that callbacks are run. However <tt>delete</tt> will either
+ # For +has_many+, <tt>destroy</tt> and <tt>destroy_all</tt> will always call the <tt>destroy</tt> method of the
+ # record(s) being removed so that callbacks are run. However <tt>delete</tt> and <tt>delete_all</tt> will either
# do the deletion according to the strategy specified by the <tt>:dependent</tt> option, or
# if no <tt>:dependent</tt> option is given, then it will follow the default strategy.
# The default strategy is <tt>:nullify</tt> (set the foreign keys to <tt>nil</tt>), except for
@@ -986,7 +1003,7 @@ module ActiveRecord
# associated objects themselves. So with +has_and_belongs_to_many+ and +has_many+
# <tt>:through</tt>, the join records will be deleted, but the associated records won't.
#
- # This makes sense if you think about it: if you were to call <tt>post.tags.delete(Tag.find_by_name('food'))</tt>
+ # This makes sense if you think about it: if you were to call <tt>post.tags.delete(Tag.find_by(name: 'food'))</tt>
# you would want the 'food' tag to be unlinked from the post, rather than for the tag itself
# to be removed from the database.
#
@@ -1022,16 +1039,16 @@ module ActiveRecord
# An empty array is returned if none are found.
# [collection<<(object, ...)]
# Adds one or more objects to the collection by setting their foreign keys to the collection's primary key.
- # Note that this operation instantly fires update sql without waiting for the save or update call on the
- # parent object.
+ # Note that this operation instantly fires update SQL without waiting for the save or update call on the
+ # parent object, unless the parent object is a new record.
# [collection.delete(object, ...)]
# Removes one or more objects from the collection by setting their foreign keys to +NULL+.
- # Objects will be in addition destroyed if they're associated with <tt>:dependent => :destroy</tt>,
- # and deleted if they're associated with <tt>:dependent => :delete_all</tt>.
+ # Objects will be in addition destroyed if they're associated with <tt>dependent: :destroy</tt>,
+ # and deleted if they're associated with <tt>dependent: :delete_all</tt>.
#
# If the <tt>:through</tt> option is used, then the join records are deleted (rather than
- # nullified) by default, but you can specify <tt>:dependent => :destroy</tt> or
- # <tt>:dependent => :nullify</tt> to override this.
+ # nullified) by default, but you can specify <tt>dependent: :destroy</tt> or
+ # <tt>dependent: :nullify</tt> to override this.
# [collection.destroy(object, ...)]
# Removes one or more objects from the collection by running <tt>destroy</tt> on
# each record, regardless of any dependent option, ensuring callbacks are run.
@@ -1049,8 +1066,8 @@ module ActiveRecord
# method loads the models and calls <tt>collection=</tt>. See above.
# [collection.clear]
# Removes every object from the collection. This destroys the associated objects if they
- # are associated with <tt>:dependent => :destroy</tt>, deletes them directly from the
- # database if <tt>:dependent => :delete_all</tt>, otherwise sets their foreign keys to +NULL+.
+ # are associated with <tt>dependent: :destroy</tt>, deletes them directly from the
+ # database if <tt>dependent: :delete_all</tt>, otherwise sets their foreign keys to +NULL+.
# If the <tt>:through</tt> option is true no destroy callbacks are invoked on the join models.
# Join models are directly deleted.
# [collection.empty?]
@@ -1058,10 +1075,10 @@ module ActiveRecord
# [collection.size]
# Returns the number of associated objects.
# [collection.find(...)]
- # Finds an associated object according to the same rules as ActiveRecord::Base.find.
+ # Finds an associated object according to the same rules as <tt>ActiveRecord::Base.find</tt>.
# [collection.exists?(...)]
# Checks whether an associated object with the given conditions exists.
- # Uses the same rules as ActiveRecord::Base.exists?.
+ # Uses the same rules as <tt>ActiveRecord::Base.exists?</tt>.
# [collection.build(attributes = {}, ...)]
# Returns one or more new objects of the collection type that have been instantiated
# with +attributes+ and linked to this object through a foreign key, but have not yet
@@ -1071,14 +1088,17 @@ module ActiveRecord
# with +attributes+, linked to this object through a foreign key, and that has already
# been saved (if it passed the validation). *Note*: This only works if the base model
# already exists in the DB, not if it is a new (unsaved) record!
+ # [collection.create!(attributes = {})]
+ # Does the same as <tt>collection.create</tt>, but raises <tt>ActiveRecord::RecordInvalid</tt>
+ # if the record is invalid.
#
# (*Note*: +collection+ is replaced with the symbol passed as the first argument, so
# <tt>has_many :clients</tt> would add among others <tt>clients.empty?</tt>.)
#
# === Example
#
- # Example: A Firm class declares <tt>has_many :clients</tt>, which will add:
- # * <tt>Firm#clients</tt> (similar to <tt>Clients.all :conditions => ["firm_id = ?", id]</tt>)
+ # A <tt>Firm</tt> class declares <tt>has_many :clients</tt>, which will add:
+ # * <tt>Firm#clients</tt> (similar to <tt>Client.where(firm_id: id)</tt>)
# * <tt>Firm#clients<<</tt>
# * <tt>Firm#clients.delete</tt>
# * <tt>Firm#clients.destroy</tt>
@@ -1088,10 +1108,11 @@ module ActiveRecord
# * <tt>Firm#clients.clear</tt>
# * <tt>Firm#clients.empty?</tt> (similar to <tt>firm.clients.size == 0</tt>)
# * <tt>Firm#clients.size</tt> (similar to <tt>Client.count "firm_id = #{id}"</tt>)
- # * <tt>Firm#clients.find</tt> (similar to <tt>Client.find(id, :conditions => "firm_id = #{id}")</tt>)
- # * <tt>Firm#clients.exists?(:name => 'ACME')</tt> (similar to <tt>Client.exists?(:name => 'ACME', :firm_id => firm.id)</tt>)
+ # * <tt>Firm#clients.find</tt> (similar to <tt>Client.where(firm_id: id).find(id)</tt>)
+ # * <tt>Firm#clients.exists?(name: 'ACME')</tt> (similar to <tt>Client.exists?(name: 'ACME', firm_id: firm.id)</tt>)
# * <tt>Firm#clients.build</tt> (similar to <tt>Client.new("firm_id" => id)</tt>)
# * <tt>Firm#clients.create</tt> (similar to <tt>c = Client.new("firm_id" => id); c.save; c</tt>)
+ # * <tt>Firm#clients.create!</tt> (similar to <tt>c = Client.new("firm_id" => id); c.save!</tt>)
# The declaration can also include an options hash to specialize the behavior of the association.
#
# === Options
@@ -1110,18 +1131,21 @@ module ActiveRecord
# Controls what happens to the associated objects when
# their owner is destroyed. Note that these are implemented as
# callbacks, and Rails executes callbacks in order. Therefore, other
- # similar callbacks may affect the :dependent behavior, and the
- # :dependent behavior may affect other callbacks.
+ # similar callbacks may affect the <tt>:dependent</tt> behavior, and the
+ # <tt>:dependent</tt> behavior may affect other callbacks.
#
- # * <tt>:destroy</tt> causes all the associated objects to also be destroyed
- # * <tt>:delete_all</tt> causes all the asssociated objects to be deleted directly from the database (so callbacks will not execute)
+ # * <tt>:destroy</tt> causes all the associated objects to also be destroyed.
+ # * <tt>:delete_all</tt> causes all the associated objects to be deleted directly from the database (so callbacks will not be executed).
# * <tt>:nullify</tt> causes the foreign keys to be set to +NULL+. Callbacks are not executed.
- # * <tt>:restrict_with_exception</tt> causes an exception to be raised if there are any associated records
- # * <tt>:restrict_with_error</tt> causes an error to be added to the owner if there are any associated objects
+ # * <tt>:restrict_with_exception</tt> causes an exception to be raised if there are any associated records.
+ # * <tt>:restrict_with_error</tt> causes an error to be added to the owner if there are any associated objects.
#
# If using with the <tt>:through</tt> option, the association on the join model must be
# a +belongs_to+, and the records which get deleted are the join records, rather than
# the associated records.
+ # [:counter_cache]
+ # This option can be used to configure a custom named <tt>:counter_cache.</tt> You only need this option,
+ # when you customized the name of your <tt>:counter_cache</tt> on the <tt>belongs_to</tt> association.
# [:as]
# Specifies a polymorphic interface (See <tt>belongs_to</tt>).
# [:through]
@@ -1143,7 +1167,7 @@ module ActiveRecord
# [:source]
# Specifies the source association name used by <tt>has_many :through</tt> queries.
# Only use it if the name cannot be inferred from the association.
- # <tt>has_many :subscribers, :through => :subscriptions</tt> will look for either <tt>:subscribers</tt> or
+ # <tt>has_many :subscribers, through: :subscriptions</tt> will look for either <tt>:subscribers</tt> or
# <tt>:subscriber</tt> on Subscription, unless a <tt>:source</tt> is given.
# [:source_type]
# Specifies type of the source association used by <tt>has_many :through</tt> queries where the source
@@ -1154,8 +1178,8 @@ module ActiveRecord
# If true, always save the associated objects or destroy them if marked for destruction,
# when saving the parent object. If false, never save or destroy the associated objects.
# By default, only save associated objects that are new records. This option is implemented as a
- # before_save callback. Because callbacks are run in the order they are defined, associated objects
- # may need to be explicitly saved in any user-defined before_save callbacks.
+ # +before_save+ callback. Because callbacks are run in the order they are defined, associated objects
+ # may need to be explicitly saved in any user-defined +before_save+ callbacks.
#
# Note that <tt>accepts_nested_attributes_for</tt> sets <tt>:autosave</tt> to <tt>true</tt>.
# [:inverse_of]
@@ -1174,13 +1198,14 @@ 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
# if the other class contains the foreign key. If the current class contains the foreign key,
# then you should use +belongs_to+ instead. See also ActiveRecord::Associations::ClassMethods's overview
- # on when to use has_one and when to use belongs_to.
+ # on when to use +has_one+ and when to use +belongs_to+.
#
# The following methods for retrieval and query of a single associated object will be added:
#
@@ -1207,7 +1232,7 @@ module ActiveRecord
# === Example
#
# An Account class declares <tt>has_one :beneficiary</tt>, which will add:
- # * <tt>Account#beneficiary</tt> (similar to <tt>Beneficiary.first(:conditions => "account_id = #{id}")</tt>)
+ # * <tt>Account#beneficiary</tt> (similar to <tt>Beneficiary.where(account_id: id).first</tt>)
# * <tt>Account#beneficiary=(beneficiary)</tt> (similar to <tt>beneficiary.account_id = account.id; beneficiary.save</tt>)
# * <tt>Account#build_beneficiary</tt> (similar to <tt>Beneficiary.new("account_id" => id)</tt>)
# * <tt>Account#create_beneficiary</tt> (similar to <tt>b = Beneficiary.new("account_id" => id); b.save; b</tt>)
@@ -1227,7 +1252,7 @@ module ActiveRecord
# its owner is destroyed:
#
# * <tt>:destroy</tt> causes the associated object to also be destroyed
- # * <tt>:delete</tt> causes the asssociated object to be deleted directly from the database (so callbacks will not execute)
+ # * <tt>:delete</tt> causes the associated object to be deleted directly from the database (so callbacks will not execute)
# * <tt>:nullify</tt> causes the foreign key to be set to +NULL+. Callbacks are not executed.
# * <tt>:restrict_with_exception</tt> causes an exception to be raised if there is an associated record
# * <tt>:restrict_with_error</tt> causes an error to be added to the owner if there is an associated object
@@ -1247,7 +1272,7 @@ module ActiveRecord
# [:source]
# Specifies the source association name used by <tt>has_one :through</tt> queries.
# Only use it if the name cannot be inferred from the association.
- # <tt>has_one :favorite, :through => :favorites</tt> will look for a
+ # <tt>has_one :favorite, through: :favorites</tt> will look for a
# <tt>:favorite</tt> on Favorite, unless a <tt>:source</tt> is given.
# [:source_type]
# Specifies type of the source association used by <tt>has_one :through</tt> queries where the source
@@ -1267,17 +1292,18 @@ module ActiveRecord
# See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail.
#
# Option examples:
- # has_one :credit_card, :dependent => :destroy # destroys the associated credit card
- # has_one :credit_card, :dependent => :nullify # updates the associated records foreign
+ # has_one :credit_card, dependent: :destroy # destroys the associated credit card
+ # has_one :credit_card, dependent: :nullify # updates the associated records foreign
# # key value to NULL rather than destroying it
- # has_one :last_comment, -> { order 'posted_on' }, :class_name => "Comment"
- # has_one :project_manager, -> { where role: 'project_manager' }, :class_name => "Person"
+ # has_one :last_comment, -> { order 'posted_on' }, class_name: "Comment"
+ # has_one :project_manager, -> { where role: 'project_manager' }, class_name: "Person"
# has_one :attachment, as: :attachable
# has_one :boss, readonly: :true
# has_one :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
@@ -1326,12 +1352,12 @@ module ActiveRecord
# Specify the foreign key used for the association. By default this is guessed to be the name
# of the association with an "_id" suffix. So a class that defines a <tt>belongs_to :person</tt>
# association will use "person_id" as the default <tt>:foreign_key</tt>. Similarly,
- # <tt>belongs_to :favorite_person, :class_name => "Person"</tt> will use a foreign key
+ # <tt>belongs_to :favorite_person, class_name: "Person"</tt> will use a foreign key
# of "favorite_person_id".
# [:foreign_type]
# Specify the column used to store the associated object's type, if this is a polymorphic
# association. By default this is guessed to be the name of the association with a "_type"
- # suffix. So a class that defines a <tt>belongs_to :taggable, :polymorphic => true</tt>
+ # suffix. So a class that defines a <tt>belongs_to :taggable, polymorphic: true</tt>
# association will use "taggable_type" as the default <tt>:foreign_type</tt>.
# [:primary_key]
# Specify the method that returns the primary key of associated object used for the association.
@@ -1348,10 +1374,10 @@ module ActiveRecord
# class is created and decremented when it's destroyed. This requires that a column
# named <tt>#{table_name}_count</tt> (such as +comments_count+ for a belonging Comment class)
# is used on the associate class (such as a Post class) - that is the migration for
- # <tt>#{table_name}_count</tt> is created on the associate class (such that Post.comments_count will
+ # <tt>#{table_name}_count</tt> is created on the associate class (such that <tt>Post.comments_count</tt> will
# return the count cached, see note below). You can also specify a custom counter
# cache column by providing a column name instead of a +true+/+false+ value to this
- # option (e.g., <tt>:counter_cache => :my_custom_counter</tt>.)
+ # option (e.g., <tt>counter_cache: :my_custom_counter</tt>.)
# Note: Specifying a counter cache will add it to that model's list of readonly attributes
# using +attr_readonly+.
# [:polymorphic]
@@ -1389,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
@@ -1403,13 +1430,15 @@ module ActiveRecord
# to generate a join table name of "papers_paper_boxes" because of the length of the name "paper_boxes",
# but it in fact generates a join table name of "paper_boxes_papers". Be aware of this caveat, and use the
# custom <tt>:join_table</tt> option if you need to.
+ # If your tables share a common prefix, it will only appear once at the beginning. For example,
+ # the tables "catalog_categories" and "catalog_products" generate a join table name of "catalog_categories_products".
#
# The join table should not have a primary key or a model associated with it. You must manually generate the
# join table with a migration such as this:
#
# class CreateDevelopersProjectsJoinTable < ActiveRecord::Migration
# def change
- # create_table :developers_projects, :id => false do |t|
+ # create_table :developers_projects, id: false do |t|
# t.integer :developer_id
# t.integer :project_id
# end
@@ -1428,8 +1457,8 @@ module ActiveRecord
# [collection<<(object, ...)]
# Adds one or more objects to the collection by creating associations in the join table
# (<tt>collection.push</tt> and <tt>collection.concat</tt> are aliases to this method).
- # Note that this operation instantly fires update sql without waiting for the save or update call on the
- # parent object.
+ # Note that this operation instantly fires update SQL without waiting for the save or update call on the
+ # parent object, unless the parent object is a new record.
# [collection.delete(object, ...)]
# Removes one or more objects from the collection by removing their associations from the join table.
# This does not destroy the objects.
@@ -1451,10 +1480,10 @@ module ActiveRecord
# [collection.find(id)]
# Finds an associated object responding to the +id+ and that
# meets the condition that it has to be associated with this object.
- # Uses the same rules as ActiveRecord::Base.find.
+ # Uses the same rules as <tt>ActiveRecord::Base.find</tt>.
# [collection.exists?(...)]
# Checks whether an associated object with the given conditions exists.
- # Uses the same rules as ActiveRecord::Base.exists?.
+ # Uses the same rules as <tt>ActiveRecord::Base.exists?</tt>.
# [collection.build(attributes = {})]
# Returns a new object of the collection type that has been instantiated
# with +attributes+ and linked to this object through the join table, but has not yet been saved.
@@ -1524,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/alias_tracker.rb b/activerecord/lib/active_record/associations/alias_tracker.rb
index 84540a7000..0c23029981 100644
--- a/activerecord/lib/active_record/associations/alias_tracker.rb
+++ b/activerecord/lib/active_record/associations/alias_tracker.rb
@@ -8,7 +8,7 @@ module ActiveRecord
attr_reader :aliases, :table_joins, :connection
# table_joins is an array of arel joins which might conflict with the aliases we assign here
- def initialize(connection = ActiveRecord::Model.connection, table_joins = [])
+ def initialize(connection = Base.connection, table_joins = [])
@aliases = Hash.new { |h,k| h[k] = initial_count_for(k) }
@table_joins = table_joins
@connection = connection
diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb
index ba75c8be41..e6a45487d0 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
@@ -30,7 +30,7 @@ module ActiveRecord
reset_scope
end
- # Returns the name of the table of the related class:
+ # Returns the name of the table of the associated class:
#
# post.comments.aliased_table_name # => "comments"
#
@@ -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,8 +61,9 @@ 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
@@ -71,7 +73,7 @@ module ActiveRecord
#
# 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,15 +86,10 @@ 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
- # scoped method is called. This is because at that point the call may be surrounded
+ # scope method is called. This is because at that point the call may be surrounded
# by scope.scoping { ... } or with_scope { ... } etc, which affects the scope which
# actually gets built.
def association_scope
@@ -110,10 +107,11 @@ module ActiveRecord
if record && invertible_for?(record)
inverse = record.association(inverse_reflection_for(record).name)
inverse.target = owner
+ inverse.inversed = true
end
end
- # This class of the target. belongs_to polymorphic overrides this to look at the
+ # Returns the class of the target. belongs_to polymorphic overrides this to look at the
# polymorphic_type field on the owner.
def klass
reflection.klass
@@ -122,7 +120,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
- klass.all
+ AssociationRelation.create(klass, klass.arel_table, self).merge!(klass.all)
end
# Loads the \target if needed and returns it.
@@ -146,7 +144,7 @@ module ActiveRecord
def interpolate(sql, record = nil)
if sql.respond_to?(:to_proc)
- owner.send(:instance_exec, record, &sql)
+ owner.instance_exec(record, &sql)
else
sql
end
@@ -164,6 +162,13 @@ module ActiveRecord
@reflection = @owner.class.reflect_on_association(reflection_name)
end
+ def initialize_attributes(record) #:nodoc:
+ skip_assign = [reflection.foreign_key, reflection.type].compact
+ attributes = create_scope.except(*(record.changed - skip_assign))
+ record.assign_attributes(attributes)
+ set_inverse_instance(record)
+ end
+
private
def find_target?
@@ -189,13 +194,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
@@ -203,7 +209,7 @@ module ActiveRecord
# Raises ActiveRecord::AssociationTypeMismatch unless +record+ is of
# the kind of the class of the associated objects. Meant to be used as
# a sanity check when you are about to assign an associated record.
- def raise_on_type_mismatch(record)
+ def raise_on_type_mismatch!(record)
unless record.is_a?(reflection.klass) || record.is_a?(reflection.class_name.constantize)
message = "#{reflection.class_name}(##{reflection.klass.object_id}) expected, got #{record.class}(##{record.class.object_id})"
raise ActiveRecord::AssociationTypeMismatch, message
@@ -217,13 +223,14 @@ module ActiveRecord
reflection.inverse_of
end
- # Is this association invertible? Can be redefined by subclasses.
+ # 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)
end
# This should be implemented to return the values of the relevant key(s) on the owner,
- # so that when state_state is different from the value stored on the last find_target,
+ # so that when stale_state is different from the value stored on the last find_target,
# the target is stale.
#
# This is only relevant to certain associations, which is why it returns nil by default.
@@ -232,8 +239,7 @@ module ActiveRecord
def build_record(attributes)
reflection.build_association(attributes) do |record|
- attributes = create_scope.except(*(record.changed - [reflection.foreign_key]))
- record.assign_attributes(attributes)
+ initialize_attributes(record)
end
end
end
diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb
index 1303822868..17f056e764 100644
--- a/activerecord/lib/active_record/associations/association_scope.rb
+++ b/activerecord/lib/active_record/associations/association_scope.rb
@@ -15,14 +15,14 @@ module ActiveRecord
def scope
scope = klass.unscoped
- scope.merge! eval_scope(klass, reflection.scope) if reflection.scope
+ scope.extending! Array(options[:extend])
add_constraints(scope)
end
private
def column_for(table_name, column_name)
- columns = alias_tracker.connection.schema_cache.columns_hash[table_name]
+ columns = alias_tracker.connection.schema_cache.columns_hash(table_name)
columns[column_name]
end
@@ -44,21 +44,9 @@ 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(klass)
+ key = reflection.association_primary_key(self.klass)
else
key = reflection.association_primary_key
end
@@ -82,20 +70,32 @@ 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] - [self.reflection.scope]).each do |scope_chain_item|
- item = eval_scope(reflection.klass, scope_chain_item)
+ scope_chain[i].each do |scope_chain_item|
+ item = eval_scope(klass, scope_chain_item)
+
+ if scope_chain_item == self.reflection.scope
+ 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.order_values |= item.order_values
end
end
@@ -113,7 +113,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 75f72c1a46..e1fa5225b5 100644
--- a/activerecord/lib/active_record/associations/belongs_to_association.rb
+++ b/activerecord/lib/active_record/associations/belongs_to_association.rb
@@ -1,5 +1,5 @@
module ActiveRecord
- # = Active Record Belongs To Associations
+ # = Active Record Belongs To Association
module Associations
class BelongsToAssociation < SingularAssociation #:nodoc:
@@ -8,7 +8,7 @@ module ActiveRecord
end
def replace(record)
- raise_on_type_mismatch(record) if record
+ raise_on_type_mismatch!(record) if record
update_counters(record)
replace_keys(record)
@@ -50,8 +50,11 @@ module ActiveRecord
# Checks whether record is different to the current target, without loading it
def different_target?(record)
- record.nil? && owner[reflection.foreign_key] ||
- record && record.id != owner[reflection.foreign_key]
+ if record.nil?
+ owner[reflection.foreign_key]
+ else
+ record.id != owner[reflection.foreign_key]
+ end
end
def replace_keys(record)
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 88ce03a3cd..eae5eed3a1 100644
--- a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb
+++ b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb
@@ -22,7 +22,7 @@ module ActiveRecord
reflection.polymorphic_inverse_of(record.class)
end
- def raise_on_type_mismatch(record)
+ def raise_on_type_mismatch!(record)
# A polymorphic association cannot have a type mismatch, by definition
end
diff --git a/activerecord/lib/active_record/associations/builder/association.rb b/activerecord/lib/active_record/associations/builder/association.rb
index 1df876bf62..d8d68eb908 100644
--- a/activerecord/lib/active_record/associations/builder/association.rb
+++ b/activerecord/lib/active_record/associations/builder/association.rb
@@ -1,68 +1,100 @@
+# This is the parent Association class which defines the variables
+# used by all associations.
+#
+# The hierarchy is defined as follows:
+# Association
+# - SingularAssociation
+# - BelongsToAssociation
+# - HasOneAssociation
+# - CollectionAssociation
+# - HasManyAssociation
+
module ActiveRecord::Associations::Builder
class Association #:nodoc:
class << self
- attr_accessor :valid_options
+ attr_accessor :extensions
end
+ self.extensions = []
- self.valid_options = [:class_name, :foreign_key, :validate]
-
- attr_reader :model, :name, :scope, :options, :reflection
+ VALID_OPTIONS = [:class_name, :class, :foreign_key, :validate]
- def self.build(*args, &block)
- new(*args, &block).build
+ def self.build(model, name, scope, options, &block)
+ extension = define_extensions model, name, &block
+ reflection = create_reflection model, name, scope, options, extension
+ define_accessors model, reflection
+ define_callbacks model, reflection
+ reflection
end
- def initialize(model, name, scope, options)
- @model = model
- @name = name
+ def self.create_reflection(model, name, scope, options, extension = nil)
+ raise ArgumentError, "association names must be a Symbol" unless name.kind_of?(Symbol)
if scope.is_a?(Hash)
- @scope = nil
- @options = scope
- else
- @scope = scope
- @options = options
+ options = scope
+ scope = nil
+ end
+
+ validate_options(options)
+
+ scope = build_scope(scope, extension)
+
+ ActiveRecord::Reflection.create(macro, name, scope, options, model)
+ end
+
+ def self.build_scope(scope, extension)
+ new_scope = scope
+
+ if scope && scope.arity == 0
+ new_scope = proc { instance_exec(&scope) }
end
- if @scope && @scope.arity == 0
- prev_scope = @scope
- @scope = proc { instance_exec(&prev_scope) }
+ if extension
+ new_scope = wrap_scope new_scope, extension
end
+
+ new_scope
end
- def mixin
- @model.generated_feature_methods
+ def self.wrap_scope(scope, extension)
+ scope
end
- include Module.new { def build; end }
+ def self.macro
+ raise NotImplementedError
+ 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 self.valid_options(options)
+ VALID_OPTIONS + Association.extensions.flat_map(&:valid_options)
end
- def macro
- raise NotImplementedError
+ def self.validate_options(options)
+ options.assert_valid_keys(valid_options(options))
end
- def valid_options
- Association.valid_options
+ def self.define_extensions(model, name)
end
- def validate_options
- options.assert_valid_keys(valid_options)
+ 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
- def define_accessors
- define_readers
- define_writers
+ # 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 self.define_accessors(model, reflection)
+ mixin = model.generated_feature_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)
@@ -70,7 +102,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)
@@ -78,29 +110,17 @@ 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
+ def self.valid_dependent_options
+ raise NotImplementedError
+ end
- if options[:dependent] == :restrict
- ActiveSupport::Deprecation.warn(
- "The :restrict option is deprecated. Please use :restrict_with_exception instead, which " \
- "provides the same functionality."
- )
+ 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
- mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
- def #{macro}_dependent_for_#{name}
- association(:#{name}).handle_dependency
- end
- CODE
-
- model.before_destroy "#{macro}_dependent_for_#{name}"
- 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 2f2600b7fb..aa43c34d86 100644
--- a/activerecord/lib/active_record/associations/builder/belongs_to.rb
+++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb
@@ -1,66 +1,130 @@
module ActiveRecord::Associations::Builder
class BelongsTo < SingularAssociation #:nodoc:
- def macro
+ def self.macro
:belongs_to
end
- def valid_options
+ def self.valid_options(options)
super + [:foreign_type, :polymorphic, :touch]
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
+ def self.define_accessors(mixin, reflection)
+ super
+ add_counter_cache_methods mixin
+ end
+
+ def self.add_counter_cache_methods(mixin)
+ return if mixin.method_defined? :belongs_to_counter_cache_after_create
+
+ 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
- mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
- def belongs_to_counter_cache_after_create_for_#{name}
- record = #{name}
- record.class.increment_counter(:#{cache_column}, record.id) unless record.nil?
+ 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?
+ cache_column = reflection.counter_cache_column
+ record.class.decrement_counter(cache_column, record.id)
+ end
+ end
end
- def belongs_to_counter_cache_before_destroy_for_#{name}
- unless marked_for_destruction?
- record = #{name}
- record.class.decrement_counter(:#{cache_column}, record.id) unless record.nil?
+ 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 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)
+ end
+ if foreign_key_was && model.respond_to?(:decrement_counter)
+ 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.before_destroy lambda { |record|
+ record.belongs_to_counter_cache_before_destroy(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 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}
- record = #{name}
+ def self.touch_record(o, foreign_key, name, touch) # :nodoc:
+ old_foreign_id = o.changed_attributes[foreign_key]
- unless record.nil?
- record.touch #{options[:touch].inspect if options[:touch] != true}
+ if old_foreign_id
+ klass = o.association(name).klass
+ old_record = klass.find_by(klass.primary_key => old_foreign_id)
+
+ if old_record
+ if touch != true
+ old_record.touch touch
+ else
+ old_record.touch
end
end
- CODE
+ end
- 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}"
+ record = o.send name
+ unless record.nil? || record.new_record?
+ 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 1b382f7285..2ff67f904d 100644
--- a/activerecord/lib/active_record/associations/builder/collection_association.rb
+++ b/activerecord/lib/active_record/associations/builder/collection_association.rb
@@ -1,68 +1,54 @@
-module ActiveRecord::Associations::Builder
- class CollectionAssociation < Association #:nodoc:
- CALLBACKS = [:before_add, :after_add, :before_remove, :after_remove]
+# This class is inherited by the has_many and has_many_and_belongs_to_many association classes
- def valid_options
- super + [:table_name, :finder_sql, :counter_sql, :before_add, :after_add, :before_remove, :after_remove]
- end
+require 'active_record/associations'
- attr_reader :block_extension, :extension_module
+module ActiveRecord::Associations::Builder
+ class CollectionAssociation < Association #:nodoc:
- def initialize(*args, &extension)
- super(*args)
- @block_extension = extension
- end
+ CALLBACKS = [:before_add, :after_add, :before_remove, :after_remove]
- def build
- show_deprecation_warnings
- wrap_block_extension
- reflection = super
- CALLBACKS.each { |callback_name| define_callback(callback_name) }
- reflection
+ def self.valid_options(options)
+ super + [:table_name, :before_add,
+ :after_add, :before_remove, :after_remove, :extend]
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 self.define_extensions(model, name)
+ if block_given?
+ extension_module_name = "#{model.name.demodulize}#{name.to_s.camelize}AssociationExtension"
+ extension = Module.new(&Proc.new)
+ model.parent.const_set(extension_module_name, extension)
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
+ model.send "#{full_callback_name}=", callbacks
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]))
- end
-
- def define_readers
+ # Defines the setter and getter methods for the collection_singular_ids.
+ def self.define_readers(mixin, name)
super
mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
@@ -72,7 +58,7 @@ module ActiveRecord::Associations::Builder
CODE
end
- def define_writers
+ def self.define_writers(mixin, name)
super
mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
@@ -81,5 +67,13 @@ module ActiveRecord::Associations::Builder
end
CODE
end
+
+ def self.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..1c9c04b044 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]
+ 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
+
+ def self.add_left_association(name, options)
+ belongs_to name, options
+ self.left_reflection = reflect_on_association(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_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
+
+ HasMany.create_reflection(lhs_model,
+ middle_name,
+ nil,
+ middle_options)
+ end
+
+ private
+
+ def middle_options(join_model)
+ middle_options = {}
+ middle_options[:class] = join_model
+ middle_options[: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 ab8225460a..227184cd19 100644
--- a/activerecord/lib/active_record/associations/builder/has_many.rb
+++ b/activerecord/lib/active_record/associations/builder/has_many.rb
@@ -1,15 +1,15 @@
module ActiveRecord::Associations::Builder
class HasMany < CollectionAssociation #:nodoc:
- def macro
+ def self.macro
:has_many
end
- def valid_options
- super + [:primary_key, :dependent, :as, :through, :source, :source_type, :inverse_of]
+ def self.valid_options(options)
+ 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..064a3c8b51 100644
--- a/activerecord/lib/active_record/associations/builder/has_one.rb
+++ b/activerecord/lib/active_record/associations/builder/has_one.rb
@@ -1,25 +1,21 @@
module ActiveRecord::Associations::Builder
class HasOne < SingularAssociation #:nodoc:
- def macro
+ def self.macro
:has_one
end
- def valid_options
+ def self.valid_options(options)
valid = super + [:order, :as]
valid += [:through, :source, :source_type] if options[:through]
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
-
- 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 6a5830e57f..2a4b1c441f 100644
--- a/activerecord/lib/active_record/associations/builder/singular_association.rb
+++ b/activerecord/lib/active_record/associations/builder/singular_association.rb
@@ -1,19 +1,18 @@
+# This class is inherited by the has_one and belongs_to association classes
+
module ActiveRecord::Associations::Builder
class SingularAssociation < Association #:nodoc:
- def valid_options
+ def self.valid_options(options)
super + [:remote, :dependent, :counter_cache, :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_feature_methods, reflection.name) if reflection.constructable?
end
- def define_constructors
+ # Defines the (build|create)_association methods for belongs_to or has_one association
+ 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 7f39d3083e..62f23f54f9 100644
--- a/activerecord/lib/active_record/associations/collection_association.rb
+++ b/activerecord/lib/active_record/associations/collection_association.rb
@@ -7,7 +7,6 @@ module ActiveRecord
# collections. See the class hierarchy in AssociationProxy.
#
# CollectionAssociation:
- # HasAndBelongsToManyAssociation => has_and_belongs_to_many
# HasManyAssociation => has_many
# HasManyThroughAssociation + ThroughAssociation => has_many :through
#
@@ -34,7 +33,7 @@ module ActiveRecord
reload
end
- CollectionProxy.new(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
@@ -79,8 +78,17 @@ module ActiveRecord
if block_given?
load_target.find(*args) { |*block_args| yield(*block_args) }
else
- if options[:finder_sql]
- find_by_scan(*args)
+ if options[:inverse_of] && loaded?
+ args_flatten = args.flatten
+ raise RecordNotFound, "Couldn't find #{scope.klass.name} without an ID" if args_flatten.blank?
+ result = find_by_scan(*args)
+
+ result_size = Array(result).size
+ if !result || result_size != args_flatten.size
+ scope.raise_record_not_found_exception!(args_flatten, result_size, args_flatten.size)
+ else
+ result
+ end
else
scope.find(*args)
end
@@ -141,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
@@ -161,46 +191,25 @@ module ActiveRecord
end
end
- # Calculate sum using SQL, not Enumerable.
- def sum(*args)
- if block_given?
- scope.sum(*args) { |*block_args| yield(*block_args) }
- else
- scope.sum(*args)
- 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 = {})
- return 0 if owner.new_record?
+ def count(column_name = nil)
+ 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
- column_name, count_options = nil, column_name if column_name.is_a?(Hash)
+ value = relation.count(column_name)
- 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
+ limit = options[:limit]
+ offset = options[:offset]
- reflection.klass.count_by_sql(custom_counter_sql)
+ if limit || offset
+ [ [value - offset.to_i, 0].max, limit.to_i ].min
else
- if association_scope.uniq_value
- # This is needed because 'SELECT count(DISTINCT *)..' is not valid SQL.
- column_name ||= reflection.klass.primary_key
- count_options[:distinct] = true
- end
-
- value = scope.count(column_name, count_options)
-
- limit = options[:limit]
- offset = options[:offset]
-
- if limit || offset
- [ [value - offset.to_i, 0].max, limit.to_i ].min
- else
- value
- end
+ value
end
end
@@ -212,7 +221,8 @@ 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 loaded? || dependent == :destroy
@@ -226,11 +236,11 @@ module ActiveRecord
end
end
- # Destroy +records+ and remove them from this association calling
- # +before_remove+ and +after_remove+ callbacks.
+ # Deletes the +records+ and removes them from this association calling
+ # +before_remove+ , +after_remove+ , +before_destroy+ and +after_destroy+ callbacks.
#
- # Note that this method will _always_ remove records from the database
- # ignoring the +:dependent+ option.
+ # Note that this method removes records from the database ignoring the
+ # +:dependent+ option.
def destroy(*records)
records = find(records) if records.any? { |record| record.kind_of?(Fixnum) || record.kind_of?(String) }
delete_or_destroy(records, :destroy)
@@ -248,14 +258,14 @@ module ActiveRecord
# +count_records+, which is a method descendants have to provide.
def size
if !find_target? || loaded?
- if association_scope.uniq_value
+ if association_scope.distinct_value
target.uniq.size
else
target.size
end
elsif !loaded? && !association_scope.group_values.empty?
load_target.size
- elsif !loaded? && !association_scope.uniq_value && target.is_a?(Array)
+ elsif !loaded? && !association_scope.distinct_value && target.is_a?(Array)
unsaved_records = target.select { |r| r.new_record? }
unsaved_records.size + count_records
else
@@ -274,17 +284,17 @@ 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
- !scope.exists?
+ @target.blank? && !scope.exists?
end
end
@@ -308,17 +318,18 @@ module ActiveRecord
end
end
- def uniq
+ def distinct
seen = {}
load_target.find_all do |record|
seen[record.id] = true unless seen.key?(record.id)
end
end
+ alias uniq distinct
# Replace this collection with +other_array+. This will perform a diff
# and delete/add only records that have changed.
def replace(other_array)
- other_array.each { |val| raise_on_type_mismatch(val) }
+ other_array.each { |val| raise_on_type_mismatch!(val) }
original_target = load_target.dup
if owner.new_record?
@@ -333,7 +344,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
@@ -350,49 +360,36 @@ 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.uniq_value && index = @target.index(record)
+ if association_scope.distinct_value && index = @target.index(record)
@target[index] = record
else
@target << record
end
- callback(:after_add, record)
+ callback(:after_add, record) unless skip_callbacks
set_inverse_instance(record)
record
end
- private
+ def scope(opts = {})
+ scope = super()
+ scope.none! if opts.fetch(:nullify, true) && null_scope?
+ scope
+ end
- 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 null_scope?
+ owner.new_record? && !foreign_key_present?
+ end
- def custom_finder_sql
- interpolate(options[:finder_sql])
- end
+ private
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
@@ -455,7 +452,7 @@ module ActiveRecord
def delete_or_destroy(records, method)
records = records.flatten
- records.each { |record| raise_on_type_mismatch(record) }
+ records.each { |record| raise_on_type_mismatch!(record) }
existing_records = records.reject { |r| r.new_record? }
if existing_records.empty?
@@ -496,9 +493,9 @@ module ActiveRecord
result = true
records.flatten.each do |record|
- raise_on_type_mismatch(record)
- add_to_target(record) do |r|
- result &&= insert_record(record) unless owner.new_record?
+ raise_on_type_mismatch!(record)
+ add_to_target(record) do |rec|
+ result &&= insert_record(rec) unless owner.new_record?
end
end
@@ -507,20 +504,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
@@ -531,24 +521,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)
@@ -557,17 +544,18 @@ module ActiveRecord
end
end
- # If using a custom finder_sql, #find scans the entire collection.
+ # 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
@@ -576,7 +564,9 @@ module ActiveRecord
args.shift if args.first.is_a?(Hash) && args.first.empty?
collection = fetch_first_or_last_using_find?(args) ? scope : load_target
- collection.send(type, *args).tap {|it| set_inverse_instance it }
+ collection.send(type, *args).tap do |record|
+ set_inverse_instance record if record.is_a? ActiveRecord::Base
+ end
end
end
end
diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb
index e73f940334..2e70a07962 100644
--- a/activerecord/lib/active_record/associations/collection_proxy.rb
+++ b/activerecord/lib/active_record/associations/collection_proxy.rb
@@ -28,10 +28,12 @@ module ActiveRecord
# is computed directly through SQL and does not trigger by itself the
# instantiation of the actual post records.
class CollectionProxy < Relation
- def initialize(association) #:nodoc:
+ delegate(*(ActiveRecord::Calculations.public_instance_methods - [:count]), to: :scope)
+
+ def initialize(klass, association) #:nodoc:
@association = association
- super association.klass, association.klass.arel_table
- merge! association.scope
+ super klass, klass.arel_table
+ merge! association.scope(nullify: false)
end
def target
@@ -80,16 +82,16 @@ module ActiveRecord
# # #<Pet id: 3, name: "Choo-Choo">
# # ]
#
- # Be careful because this also means you’re initializing a model
- # object with only the fields that you’ve selected. If you attempt
- # to access a field that is not in the initialized record you’ll
+ # Be careful because this also means you're initializing a model
+ # object with only the fields that you've selected. If you attempt
+ # to access a field that is not in the initialized record you'll
# receive:
#
# person.pets.select(:name).first.person_id
# # => ActiveModel::MissingAttributeError: missing attribute: person_id
#
# *Second:* You can pass a block so it can be used just like Array#select.
- # This build an array of objects from the database for the scope,
+ # This builds an array of objects from the database for the scope,
# converting them into an array and iterating through them using
# Array#select.
#
@@ -99,7 +101,7 @@ module ActiveRecord
# # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
# # ]
#
- # person.pets.select(:name) { |pet| pet.name =~ /oo/ }
+ # person.pets.select(:name) { |pet| pet.name =~ /oo/ }
# # => [
# # #<Pet id: 2, name: "Spook">,
# # #<Pet id: 3, name: "Choo-Choo">
@@ -225,6 +227,7 @@ module ActiveRecord
def build(attributes = {}, &block)
@association.build(attributes, &block)
end
+ alias_method :new, :build
# Returns a new object of the collection type that has been instantiated with
# attributes, linked to this object and that has already been saved (if it
@@ -278,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
@@ -300,7 +303,7 @@ module ActiveRecord
@association.concat(*records)
end
- # Replace this collection with +other_array+. This will perform a diff
+ # Replaces this collection with +other_array+. This will perform a diff
# and delete/add only records that have changed.
#
# class Person < ActiveRecord::Base
@@ -414,13 +417,13 @@ 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.
- # This will _always_ remove the records ignoring the +:dependent+
- # option.
+ # Deletes the records of the collection directly from the database
+ # ignoring the +:dependent+ option. It invokes +before_remove+,
+ # +after_remove+ , +before_destroy+ and +after_destroy+ callbacks.
#
# class Person < ActiveRecord::Base
# has_many :pets
@@ -646,11 +649,12 @@ module ActiveRecord
# # #<Pet name: "Fancy-Fancy">
# # ]
#
- # person.pets.select(:name).uniq
+ # person.pets.select(:name).distinct
# # => [#<Pet name: "Fancy-Fancy">]
- def uniq
- @association.uniq
+ def distinct
+ @association.distinct
end
+ alias uniq distinct
# Count all records using SQL.
#
@@ -665,12 +669,16 @@ module ActiveRecord
# # #<Pet id: 2, name: "Spook", person_id: 1>,
# # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
# # ]
- def count(column_name = nil, options = {})
- @association.count(column_name, options)
+ def count(column_name = nil)
+ @association.count(column_name)
end
# Returns the size of the collection. If the collection hasn't been loaded,
- # it executes a <tt>SELECT COUNT(*)</tt> query.
+ # it executes a <tt>SELECT COUNT(*)</tt> query. Else it calls <tt>collection.size</tt>.
+ #
+ # If the collection has been already loaded +size+ and +length+ are
+ # equivalent. If not and you are going to need the records anyway
+ # +length+ will take one less query. Otherwise +size+ is more efficient.
#
# class Person < ActiveRecord::Base
# has_many :pets
@@ -695,7 +703,8 @@ module ActiveRecord
# Returns the size of the collection calling +size+ on the target.
# If the collection has been already loaded, +length+ and +size+ are
- # equivalent.
+ # equivalent. If not and you are going to need the records anyway this
+ # method will take one less query. Otherwise +size+ is more efficient.
#
# class Person < ActiveRecord::Base
# has_many :pets
@@ -716,7 +725,12 @@ module ActiveRecord
@association.length
end
- # Returns +true+ if the collection is empty.
+ # Returns +true+ if the collection is empty. 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>.
#
# class Person < ActiveRecord::Base
# has_many :pets
@@ -746,7 +760,7 @@ module ActiveRecord
# person.pets.count # => 0
# person.pets.any? # => true
#
- # You can also pass a block to define criteria. The behaviour
+ # You can also pass a block to define criteria. The behavior
# is the same, it returns true if the collection based on the
# criteria is not empty.
#
@@ -781,7 +795,7 @@ module ActiveRecord
# person.pets.many? #=> true
#
# You can also pass a block to define criteria. The
- # behaviour is the same, it returns true if the collection
+ # behavior is the same, it returns true if the collection
# based on the criteria has more than one record.
#
# person.pets
@@ -812,14 +826,12 @@ module ActiveRecord
#
# person.pets # => [#<Pet id: 20, name: "Snoop">]
#
- # person.pets.include?(Pet.find(20)) # => true
+ # person.pets.include?(Pet.find(20)) # => true
# person.pets.include?(Pet.find(21)) # => false
def include?(record)
- @association.include?(record)
+ !!@association.include?(record)
end
- alias_method :new, :build
-
def proxy_association
@association
end
@@ -834,14 +846,8 @@ module ActiveRecord
# Returns a <tt>Relation</tt> object for the records in this association
def scope
- association = @association
-
- @association.scope.extending! do
- define_method(:proxy_association) { association }
- end
+ @association.scope
end
-
- # :nodoc:
alias spawn scope
# Equivalent to <tt>Array#==</tt>. Returns +true+ if the two arrays
@@ -911,7 +917,7 @@ module ActiveRecord
alias_method :to_a, :to_ary
# Adds one or more +records+ to the collection by setting their foreign keys
- # to the association‘s primary key. Returns +self+, so several appends may be
+ # to the association's primary key. Returns +self+, so several appends may be
# chained together.
#
# class Person < ActiveRecord::Base
@@ -934,6 +940,11 @@ module ActiveRecord
proxy_association.concat(records) && self
end
alias_method :push, :<<
+ alias_method :append, :<<
+
+ def prepend(*args)
+ raise NoMethodError, "prepend on association is not defined. Please use << or append"
+ end
# Equivalent to +delete_all+. The difference is that returns +self+, instead
# of an array with the deleted objects, so methods can be chained. See
@@ -959,7 +970,7 @@ module ActiveRecord
# person.pets.reload # fetches pets from the database
# # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>]
#
- # person.pets(true)  # fetches pets from the database
+ # person.pets(true) # fetches pets from the database
# # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>]
def reload
proxy_association.reload
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 93618721bb..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.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.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.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 74864d271f..0a23109b9b 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
@@ -22,15 +22,17 @@ module ActiveRecord
else
if options[:dependent] == :destroy
# No point in executing the counter update since we're going to destroy the parent anyway
- load_target.each(&:mark_for_destruction)
+ load_target.each { |t| t.destroyed_by_association = reflection }
+ destroy_all
+ else
+ delete_all
end
-
- delete_all
end
end
def insert_record(record, validate = true, raise = false)
set_owner_attributes(record)
+ set_inverse_instance(record)
if raise
record.save!(:validate => validate)
@@ -57,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
@@ -76,7 +76,7 @@ module ActiveRecord
end
def cached_counter_attribute_name(reflection = reflection)
- "#{reflection.name}_count"
+ options[:counter_cache] || "#{reflection.name}_count"
end
def update_counter(difference, reflection = reflection)
@@ -114,8 +114,7 @@ module ActiveRecord
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
@@ -127,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 c7d8a84a7e..56331bbb0b 100644
--- a/activerecord/lib/active_record/associations/has_many_through_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_through_association.rb
@@ -29,7 +29,7 @@ module ActiveRecord
def concat(*records)
unless owner.new_record?
records.flatten.each do |record|
- raise_on_type_mismatch(record)
+ raise_on_type_mismatch!(record)
record.save! if record.new_record?
end
end
@@ -114,11 +114,7 @@ module ActiveRecord
end
def target_reflection_has_associated_record?
- if through_reflection.macro == :belongs_to && owner[through_reflection.foreign_key].blank?
- false
- else
- true
- end
+ !(through_reflection.macro == :belongs_to && owner[through_reflection.foreign_key].blank?)
end
def update_through_counter?(method)
@@ -144,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
@@ -153,6 +163,11 @@ module ActiveRecord
delete_through_records(records)
+ if source_reflection.options[:counter_cache]
+ counter = source_reflection.counter_cache_column
+ klass.decrement_counter counter, records.map(&:id)
+ end
+
if through_reflection.macro == :has_many && update_through_counter?(method)
update_counter(-count, through_reflection)
end
diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb
index 06bead41de..0008600418 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
@@ -22,20 +22,21 @@ module ActiveRecord
end
def replace(record, save = true)
- raise_on_type_mismatch(record) if record
+ raise_on_type_mismatch!(record) if record
load_target
- # If target and record are nil, or target is equal to record,
- # we don't need to have transaction.
- if (target || record) && target != record
- reflection.klass.transaction do
+ return self.target if !(target || record)
+ if (target != record) || record.changed?
+ save &&= owner.persisted?
+
+ transaction_if(save) do
remove_target!(options[:dependent]) if target && !target.destroyed?
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}."
@@ -90,6 +91,14 @@ module ActiveRecord
def nullify_owner_attributes(record)
record[reflection.foreign_key] = nil
end
+
+ def transaction_if(value)
+ if value
+ reflection.klass.transaction { yield }
+ else
+ yield
+ end
+ end
end
end
end
diff --git a/activerecord/lib/active_record/associations/has_one_through_association.rb b/activerecord/lib/active_record/associations/has_one_through_association.rb
index fdf8ae1453..08e0ec691f 100644
--- a/activerecord/lib/active_record/associations/has_one_through_association.rb
+++ b/activerecord/lib/active_record/associations/has_one_through_association.rb
@@ -23,7 +23,7 @@ module ActiveRecord
attributes = construct_join_attributes(record)
if through_record
- through_record.update_attributes(attributes)
+ through_record.update(attributes)
elsif owner.new_record?
through_proxy.build(attributes)
else
diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb
index cd366ac8b7..c3ac0680ea 100644
--- a/activerecord/lib/active_record/associations/join_dependency.rb
+++ b/activerecord/lib/active_record/associations/join_dependency.rb
@@ -1,213 +1,273 @@
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, :active_record
+ 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 initialize(base, associations, joins)
- @active_record = 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)
- end
+ def columns
+ @tables.flat_map { |t| t.column_aliases }
+ 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)
+ # An array of [column_name, alias] pairs for the table
+ def column_aliases(node)
+ @name_and_alias_cache[node]
end
- self
- end
- def join_associations
- join_parts.last(join_parts.length - 1)
- end
+ def column_alias(node, column)
+ @alias_cache[node][column]
+ end
- def join_base
- join_parts.first
- end
+ class Table < Struct.new(:node, :columns)
+ def table
+ Arel::Nodes::TableAlias.new node.table, node.aliased_table_name
+ 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)
- }
- }.flatten
+ def column_aliases
+ t = table
+ columns.map { |column| t[column.name].as Arel.sql column.alias }
+ end
+ end
+ Column = Struct.new(:name, :alias)
end
- def instantiate(rows)
- primary_key = join_base.aliased_primary_key
- parents = {}
-
- 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
+ attr_reader :alias_tracker, :base_klass, :join_root
- remove_duplicate_results!(active_record, records, @associations)
- records
+ def self.make_tree(associations)
+ hash = {}
+ walk_tree associations, hash
+ hash
end
- def remove_duplicate_results!(base, records, associations)
+ def self.walk_tree(associations, hash)
case associations
when Symbol, String
- reflection = base.reflections[associations]
- remove_uniq_by_reflection(reflection, records)
+ hash[associations.to_sym] ||= {}
when Array
- associations.each do |association|
- remove_duplicate_results!(base, records, association)
+ associations.each do |assoc|
+ walk_tree assoc, hash
end
when Hash
- associations.keys.each 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
-
- remove_duplicate_results!(reflection.klass, parent_records, associations[name]) unless parent_records.empty?
+ associations.each do |k,v|
+ cache = hash[k] ||= {}
+ walk_tree v, cache
end
+ else
+ raise ConfigurationError, associations.inspect
end
end
- protected
+ # 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.
+ #
+ # Example :
+ #
+ # class Physician < ActiveRecord::Base
+ # has_many :appointments
+ # has_many :patients, through: :appointments
+ # end
+ #
+ # If I execute `@physician.patients.to_a` then
+ # base #=> Physician
+ # associations #=> []
+ # joins #=> [#<Arel::Nodes::InnerJoin: ...]
+ #
+ # However if I execute `Physician.joins(:appointments).to_a` then
+ # base #=> Physician
+ # associations #=> [:appointments]
+ # joins #=> []
+ #
+ def initialize(base, associations, joins)
+ @alias_tracker = AliasTracker.new(base.connection, joins)
+ @alias_tracker.aliased_name_for(base.table_name) # Updates the count for base.table_name to 1
+ 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 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] ||= {}
+ def reflections
+ join_root.drop(1).map!(&:reflection)
end
- def build(associations, parent = nil, join_type = Arel::InnerJoin)
- parent ||= join_parts.last
- case associations
- when Symbol, String
- reflection = parent.reflections[associations.to_s.intern] or
- raise ConfigurationError, "Association named '#{ associations }' was not found; 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)
+ def join_constraints(outer_joins)
+ joins = join_root.children.flat_map { |child|
+ make_inner_joins join_root, child
+ }
+
+ 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 join_root, child
+ }
end
- else
- raise ConfigurationError, associations.inspect
- end
+ }
end
- def find_join_association(name_or_reflection, parent)
- if String === name_or_reflection
- name_or_reflection = name_or_reflection.to_sym
- end
+ 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 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] = {} }
+ }
+ }
+
+ model_cache = Hash.new { |h,klass| h[klass] = {} }
+ parents = model_cache[join_root]
+ column_aliases = aliases.column_aliases join_root
- join_associations.detect { |j|
- j.reflection == name_or_reflection && j.parent == parent
+ 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)
}
+
+ parents.values
end
- def remove_uniq_by_reflection(reflection, records)
- if reflection && reflection.collection?
- records.each { |record| record.send(reflection.name).target.uniq! }
- end
+ private
+
+ 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 build_join_association(reflection, parent)
- JoinAssociation.new(reflection, self, parent)
+ def make_outer_joins(parent, child)
+ tables = table_aliases_for(parent, child)
+ join_type = Arel::OuterJoin
+ joins = make_constraints parent, child, tables, join_type
+
+ joins.concat child.children.flat_map { |c| make_outer_joins(child, c) }
end
- def construct(parent, associations, join_parts, row)
- case associations
- when Symbol, String
- name = associations.to_s
+ def make_inner_joins(parent, child)
+ tables = child.tables
+ join_type = Arel::InnerJoin
+ joins = make_constraints parent, child, tables, join_type
- join_part = join_parts.detect { |j|
- j.reflection.name.to_s == name &&
- j.parent_table_name == parent.class.table_name }
+ joins.concat child.children.flat_map { |c| make_inner_joins(child, c) }
+ end
- raise(ConfigurationError, "No such association") unless join_part
+ 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
- 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 construct_tables!(parent, node)
+ node.tables = table_aliases_for(parent, node)
+ node.children.each { |child| construct_tables! node, child }
+ end
+
+ def table_alias_for(reflection, parent, join)
+ name = "#{reflection.plural_name}_#{parent.table_name}"
+ name << "_join" if join
+ name
+ end
+
+ def walk(left, right)
+ intersection, missing = right.children.map { |node1|
+ [left.children.find { |node2| node1.match? node2 }, node1]
+ }.partition(&:first)
+
+ ojs = missing.flat_map { |_,n| make_outer_joins left, n }
+ intersection.flat_map { |l,r| walk l, r }.concat ojs
+ end
+
+ 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
+
+ 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 0d3b4dbab1..191d430636 100644
--- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb
+++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb
@@ -1,153 +1,117 @@
+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
+ attr_accessor :tables
- alias :alias_suffix :parent_table_name
-
- 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|
- parent == join_part
- end
- end
+ def join_constraints(foreign_table, foreign_klass, node, join_type, tables, scope_chain, chain)
+ joins = []
+ tables = tables.reverse
- def join_to(relation)
- tables = @tables.dup
- foreign_table = parent_table
- foreign_klass = parent.active_record
+ 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...
- relation.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
- scope_chain_items.each do |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
- constraint = constraint.and(item.arel.constraints) unless item.arel.constraints.empty?
+ rel = scope_chain_items.inject(scope_chain_items.shift) do |left, right|
+ left.merge right
end
- relation.from(join(table, constraint))
+ if rel && !rel.arel.constraints.empty?
+ constraint = constraint.and rel.arel.constraints
+ end
+
+ 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
- relation
+ joins
end
- def build_constraint(reflection, table, key, foreign_table, foreign_key)
+ # Builds equality condition.
+ #
+ # Example:
+ #
+ # class Physician < ActiveRecord::Base
+ # has_many :appointments
+ # 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
+ #
+ 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 3920e84976..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,22 +1,20 @@
+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.active_record == active_record
- 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
- active_record.table_name
+ base_klass.table_name
end
end
end
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 711f7b3ce1..91e1c6a9d7 100644
--- a/activerecord/lib/active_record/associations/join_dependency/join_part.rb
+++ b/activerecord/lib/active_record/associations/join_dependency/join_part.rb
@@ -1,41 +1,43 @@
module ActiveRecord
module Associations
class JoinDependency # :nodoc:
- # A JoinPart represents a part of a JoinDependency. It is an abstract class, inherited
+ # A JoinPart represents a part of a JoinDependency. It is inherited
# by JoinBase and JoinAssociation. A JoinBase represents the Active Record which
# everything else is being joined onto. A JoinAssociation represents an association which
# is joining to the base. A JoinAssociation may result in more than one actual join
# 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 :active_record
+ attr_reader :base_klass, :children
- delegate :table_name, :column_names, :primary_key, :reflections, :arel_engine, :to => :active_record
+ delegate :table_name, :column_names, :primary_key, :to => :base_klass
- def initialize(active_record)
- @active_record = active_record
- @cached_record = {}
+ def initialize(base_klass, children)
+ @base_klass = base_klass
@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)] ||= active_record.send(: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 c1cd3a4ae3..2393667ac8 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
@@ -72,7 +69,7 @@ module ActiveRecord
# books.
# - a Hash which specifies multiple association names, as well as
# association names for the to-be-preloaded association objects. For
- # example, specifying <tt>{ :author => :avatar }</tt> will preload a
+ # example, specifying <tt>{ author: :avatar }</tt> will preload a
# book's author, as well as that author's avatar.
#
# +:associations+ has the same format as the +:include+ option for
@@ -80,38 +77,49 @@ module ActiveRecord
#
# :books
# [ :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
+ # { author: :avatar }
+ # [ :books, { author: :avatar } ]
+
+ NULL_RELATION = Struct.new(: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
- def run
- unless records.empty?
- associations.each { |association| preload(association) }
+ 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)
- when String, Symbol
- preload_one(association.to_sym)
+ preloaders_for_hash(association, records, scope)
+ when Symbol
+ preloaders_for_one(association, records, scope)
+ when String
+ 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
@@ -121,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.read_attribute reflection.name }
+ 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 cbf5e734ea..69b65982b3 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
- # 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(model.connection.in_clause_length || owner_keys.size)
- records = sliced.map { |slice| records_for(slice).to_a }.flatten
+ # Each record may have multiple owners, and vice-versa
+ records_by_owner = owners.each_with_object({}) do |owner,h|
+ h[owner] = []
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
+ 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)
- 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
preload_values = preload_scope.values
@@ -109,11 +119,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 8e8925f0a9..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 do |owner_key, 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 9a662d3f53..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,10 +4,14 @@ module ActiveRecord
class HasManyThrough < CollectionAssociation #:nodoc:
include ThroughAssociation
- def associated_records_by_owner
- super.each do |owner, records|
- records.uniq! if reflection_scope.uniq_value
+ def associated_records_by_owner(preloader)
+ records_by_owner = super
+
+ if reflection_scope.distinct_value
+ records_by_owner.each_value { |records| records.uniq! }
end
+
+ records_by_owner
end
end
end
diff --git a/activerecord/lib/active_record/associations/preloader/singular_association.rb b/activerecord/lib/active_record/associations/preloader/singular_association.rb
index 44e804d785..2b5cfda8ce 100644
--- a/activerecord/lib/active_record/associations/preloader/singular_association.rb
+++ b/activerecord/lib/active_record/associations/preloader/singular_association.rb
@@ -5,8 +5,8 @@ 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)
diff --git a/activerecord/lib/active_record/associations/preloader/through_association.rb b/activerecord/lib/active_record/associations/preloader/through_association.rb
index 1c1ba11c44..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,50 +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 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 = 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.order! reflection_scope.values[:order]
- through_scope.references! reflection_scope.values[:references]
+ 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 32f4557c28..02dc464536 100644
--- a/activerecord/lib/active_record/associations/singular_association.rb
+++ b/activerecord/lib/active_record/associations/singular_association.rb
@@ -12,7 +12,7 @@ module ActiveRecord
target
end
- # Implements the writer method, e.g. foo.items= for Foo.has_many :items
+ # Implements the writer method, e.g. foo.bar= for Foo.belongs_to :bar
def writer(record)
replace(record)
end
@@ -42,7 +42,6 @@ module ActiveRecord
scope.first.tap { |record| set_inverse_instance(record) }
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 b9e014735b..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|
- scope = scope.merge(
- reflection.klass.all.with_default_scope.
+ chain.drop(1).each do |reflection|
+ scope.merge!(
+ reflection.klass.all.
except(:select, :create_with, :includes, :preload, :joins, :eager_load)
)
end
@@ -28,7 +28,7 @@ module ActiveRecord
# methods which create and delete records on the association.
#
# We only support indirectly modifying through associations which has a belongs_to source.
- # This is the "has_many :tags, :through => :taggings" situation, where the join model
+ # This is the "has_many :tags, through: :taggings" situation, where the join model
# typically has a belongs_to on both side. In other words, associations which could also
# be represented as has_and_belongs_to_many associations.
#
diff --git a/activerecord/lib/active_record/attribute_assignment.rb b/activerecord/lib/active_record/attribute_assignment.rb
index af13b75a9d..30fa2c8ba5 100644
--- a/activerecord/lib/active_record/attribute_assignment.rb
+++ b/activerecord/lib/active_record/attribute_assignment.rb
@@ -1,8 +1,8 @@
+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
@@ -12,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
@@ -44,7 +47,7 @@ module ActiveRecord
if respond_to?("#{k}=")
raise
else
- raise UnknownAttributeError, "unknown attribute: #{k}"
+ raise UnknownAttributeError.new(self, k)
end
end
@@ -57,9 +60,8 @@ module ActiveRecord
# by calling new on the column type or aggregation type (through composed_of) object with these parameters.
# So having the pairs written_on(1) = "2004", written_on(2) = "6", written_on(3) = "24", will instantiate
# written_on (a date type) with Date.new("2004", "6", "24"). You can also specify a typecast character in the
- # parentheses to have the parameters typecasted before they're used in the constructor. Use i for Fixnum,
- # f for Float, s for String, and a for Array. If all the values for a given attribute are empty, the
- # attribute will be set to +nil+.
+ # parentheses to have the parameters typecasted before they're used in the constructor. Use i for Fixnum and
+ # f for Float. If all the values for a given attribute are empty, the attribute will be set to +nil+.
def assign_multiparameter_attributes(pairs)
execute_callstack_for_multiparameter_attributes(
extract_callstack_for_multiparameter_attributes(pairs)
@@ -82,7 +84,7 @@ module ActiveRecord
end
def extract_callstack_for_multiparameter_attributes(pairs)
- attributes = { }
+ attributes = {}
pairs.each do |(multiparameter_name, value)|
attribute_name = multiparameter_name.split("(").first
@@ -133,7 +135,7 @@ module ActiveRecord
if object.class.send(:create_time_zone_conversion_attribute?, name, column)
Time.zone.local(*set_values)
else
- Time.time_with_datetime_fallback(object.class.default_timezone, *set_values)
+ Time.send(object.class.default_timezone, *set_values)
end
end
@@ -147,7 +149,7 @@ module ActiveRecord
end
else
# else column is a timestamp, so if Date bits were not provided, error
- validate_missing_parameters!([1,2,3])
+ validate_required_parameters!([1,2,3])
# If Date bits were provided but blank, then return nil
return if blank_date_parameter?
@@ -173,14 +175,14 @@ module ActiveRecord
def read_other(klass)
max_position = extract_max_param
positions = (1..max_position)
- validate_missing_parameters!(positions)
+ validate_required_parameters!(positions)
set_values = values.values_at(*positions)
klass.new(*set_values)
end
# Checks whether some blank date parameter exists. Note that this is different
- # than the validate_missing_parameters! method, since it just checks for blank
+ # than the validate_required_parameters! method, since it just checks for blank
# positions instead of missing ones, and does not raise in case one blank position
# exists. The caller is responsible to handle the case of this returning true.
def blank_date_parameter?
@@ -188,7 +190,7 @@ module ActiveRecord
end
# If some position is not provided, it errors out a missing parameter exception.
- def validate_missing_parameters!(positions)
+ def validate_required_parameters!(positions)
if missing_parameter = positions.detect { |position| !values.key?(position) }
raise ArgumentError.new("Missing Parameter - #{name}(#{missing_parameter})")
end
diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb
index 0aff2562b8..3924eec872 100644
--- a/activerecord/lib/active_record/attribute_methods.rb
+++ b/activerecord/lib/active_record/attribute_methods.rb
@@ -1,12 +1,15 @@
require 'active_support/core_ext/enumerable'
+require 'mutex_m'
+require 'thread_safe'
module ActiveRecord
# = Active Record Attribute Methods
- module AttributeMethods #:nodoc:
+ module AttributeMethods
extend ActiveSupport::Concern
include ActiveModel::AttributeMethods
included do
+ initialize_generated_modules
include Read
include Write
include BeforeTypeCast
@@ -17,35 +20,88 @@ 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
- # Use a mutex; we don't want two thread simaltaneously trying to define
+ 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
+ true
end
- def attribute_methods_generated?
- @attribute_methods_generated ||= false
- end
-
- def undefine_attribute_methods
- super if attribute_methods_generated?
- @attribute_methods_generated = false
+ def undefine_attribute_methods # :nodoc:
+ 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
+ # \Active \Record method is defined in the model, otherwise +false+.
+ #
+ # class Person < ActiveRecord::Base
+ # def save
+ # 'already defined by Active Record'
+ # end
+ # end
+ #
+ # Person.instance_method_already_implemented?(:save)
+ # # => ActiveRecord::DangerousAttributeError: save is defined by ActiveRecord
+ #
+ # Person.instance_method_already_implemented?(:name)
+ # # => false
def instance_method_already_implemented?(method_name)
if dangerous_attribute_method?(method_name)
- raise DangerousAttributeError, "#{method_name} is defined by ActiveRecord"
+ raise DangerousAttributeError, "#{method_name} is defined by Active Record"
end
- if [Base, Model].include?(active_record_super)
+ if superclass == Base
super
else
# If B < A and A defines its own attribute method, then we don't want to overwrite that.
@@ -56,11 +112,11 @@ module ActiveRecord
# A method name is 'dangerous' if it is already defined by Active Record, but
# not by any ancestors. (So 'puts' is not dangerous but 'save' is.)
- def dangerous_attribute_method?(name)
+ def dangerous_attribute_method?(name) # :nodoc:
method_defined_within?(name, Base)
end
- def method_defined_within?(name, klass, sup = klass.superclass)
+ def method_defined_within?(name, klass, sup = klass.superclass) # :nodoc:
if klass.method_defined?(name) || klass.private_method_defined?(name)
if sup.method_defined?(name) || sup.private_method_defined?(name)
klass.instance_method(name).owner != sup.instance_method(name).owner
@@ -72,13 +128,27 @@ module ActiveRecord
end
end
+ # Returns +true+ if +attribute+ is an attribute method and table exists,
+ # +false+ otherwise.
+ #
+ # class Person < ActiveRecord::Base
+ # end
+ #
+ # Person.attribute_method?('name') # => true
+ # Person.attribute_method?(:age=) # => true
+ # Person.attribute_method?(:nothing) # => false
def attribute_method?(attribute)
super || (table_exists? && column_names.include?(attribute.to_s.sub(/=$/, '')))
end
- # Returns an array of column names as strings if it's not
- # an abstract class and table exists.
- # Otherwise it returns an empty array.
+ # Returns an array of column names as strings if it's not an abstract class and
+ # table exists. Otherwise it returns an empty array.
+ #
+ # class Person < ActiveRecord::Base
+ # end
+ #
+ # Person.attribute_names
+ # # => ["id", "created_at", "updated_at", "name", "age"]
def attribute_names
@attribute_names ||= if !abstract_class? && table_exists?
column_names
@@ -90,50 +160,83 @@ 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)
- unless self.class.attribute_methods_generated?
- self.class.define_attribute_methods
-
- if respond_to_without_attributes?(method)
- send(method, *args, &block)
- else
- super
- end
+ def method_missing(method, *args, &block) # :nodoc:
+ self.class.define_attribute_methods
+ if respond_to_without_attributes?(method)
+ send(method, *args, &block)
else
super
end
end
- def attribute_missing(match, *args, &block)
- 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."
- )
+ # 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
+ # not been generated.
+ #
+ # class Person < ActiveRecord::Base
+ # end
+ #
+ # person = Person.new
+ # person.respond_to(:name) # => true
+ # person.respond_to(:name=) # => true
+ # person.respond_to(:name?) # => true
+ # person.respond_to('age') # => true
+ # person.respond_to('age=') # => true
+ # person.respond_to('age?') # => true
+ # person.respond_to(:nothing) # => false
+ def respond_to?(name, include_private = false)
+ name = name.to_s
+ self.class.define_attribute_methods
+ result = super
+
+ # If the result is false the answer is false.
+ return false unless result
+
+ # If the result is true then check for the select case.
+ # 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.any? && self.class.column_names.include?(name)
+ return has_attribute?(name)
end
- super
+ return true
end
- def respond_to?(name, include_private = false)
- self.class.define_attribute_methods unless self.class.attribute_methods_generated?
- super
- end
-
- # Returns true if the given attribute is in the attributes hash
+ # Returns +true+ if the given attribute is in the attributes hash, otherwise +false+.
+ #
+ # class Person < ActiveRecord::Base
+ # end
+ #
+ # person = Person.new
+ # person.has_attribute?(:name) # => true
+ # person.has_attribute?('age') # => true
+ # person.has_attribute?(:nothing) # => false
def has_attribute?(attr_name)
@attributes.has_key?(attr_name.to_s)
end
# Returns an array of names for the attributes available on this object.
+ #
+ # class Person < ActiveRecord::Base
+ # end
+ #
+ # person = Person.new
+ # person.attribute_names
+ # # => ["id", "created_at", "updated_at", "name", "age"]
def attribute_names
@attributes.keys
end
# Returns a hash of all the attributes with their names as keys and the values of the attributes as values.
+ #
+ # class Person < ActiveRecord::Base
+ # end
+ #
+ # person = Person.create(name: 'Francesco', age: 22)
+ # person.attributes
+ # # => {"id"=>3, "created_at"=>Sun, 21 Oct 2012 04:53:04, "updated_at"=>Sun, 21 Oct 2012 04:53:04, "name"=>"Francesco", "age"=>22}
def attributes
attribute_names.each_with_object({}) { |name, attrs|
attrs[name] = read_attribute(name)
@@ -142,87 +245,141 @@ 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 = 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)
- # # => '"2009-01-12 04:48:57"'
+ # # => "\"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
end
- # Returns true if the specified +attribute+ has been set by the user or by a database load and is neither
- # nil nor empty? (the latter only applies to objects that respond to empty?, most notably Strings).
+ # Returns +true+ if the specified +attribute+ has been set by the user or by a
+ # database load and is neither +nil+ nor <tt>empty?</tt> (the latter only applies
+ # to objects that respond to <tt>empty?</tt>, most notably Strings). Otherwise, +false+.
+ # Note that it always returns +true+ with boolean attributes.
+ #
+ # class Task < ActiveRecord::Base
+ # end
+ #
+ # person = Task.new(title: '', is_done: false)
+ # person.attribute_present?(:title) # => false
+ # person.attribute_present?(:is_done) # => true
+ # person.name = 'Francesco'
+ # person.is_done = true
+ # person.attribute_present?(:title) # => true
+ # person.attribute_present?(:is_done) # => true
def attribute_present?(attribute)
value = read_attribute(attribute)
!value.nil? && !(value.respond_to?(:empty?) && value.empty?)
end
- # Returns the column object for the named attribute.
+ # Returns the column object for the named attribute. Returns +nil+ if the
+ # named attribute not exists.
+ #
+ # class Person < ActiveRecord::Base
+ # end
+ #
+ # person = Person.new
+ # person.column_for_attribute(:name) # the result depends on the ConnectionAdapter
+ # # => #<ActiveRecord::ConnectionAdapters::SQLite3Column:0x007ff4ab083980 @name="name", @sql_type="varchar(255)", @null=true, ...>
+ #
+ # person.column_for_attribute(:nothing)
+ # # => nil
def column_for_attribute(name)
# FIXME: should this return a null object for columns that don't exist?
self.class.columns_hash[name.to_s]
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)).
- # (Alias for the protected read_attribute method).
+ # "2004-12-12" in a data 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.
+ #
+ # class Person < ActiveRecord::Base
+ # belongs_to :organization
+ # end
+ #
+ # person = Person.new(name: 'Francesco', age: '22')
+ # person[:name] # => "Francesco"
+ # person[:age] # => 22
+ #
+ # person = Person.select('id').first
+ # person[:name] # => ActiveModel::MissingAttributeError: missing attribute: name
+ # person[:organization_id] # => ActiveModel::MissingAttributeError: missing attribute: organization_id
def [](attr_name)
- read_attribute(attr_name)
+ read_attribute(attr_name) { |n| missing_attribute(n, caller) }
end
# Updates the attribute identified by <tt>attr_name</tt> with the specified +value+.
- # (Alias for the protected write_attribute method).
+ # (Alias for the protected <tt>write_attribute</tt> method).
+ #
+ # class Person < ActiveRecord::Base
+ # end
+ #
+ # person = Person.new
+ # person[:age] = '22'
+ # person[:age] # => 22
+ # person[:age] # => Fixnum
def []=(attr_name, value)
write_attribute(attr_name, value)
end
protected
- def clone_attributes(reader_method = :read_attribute, attributes = {})
+ def clone_attributes(reader_method = :read_attribute, attributes = {}) # :nodoc:
attribute_names.each do |name|
attributes[name] = clone_attribute_value(reader_method, name)
end
attributes
end
- def clone_attribute_value(reader_method, attribute_name)
+ def clone_attribute_value(reader_method, attribute_name) # :nodoc:
value = send(reader_method, attribute_name)
value.duplicable? ? value.clone : value
rescue TypeError, NoMethodError
value
end
- def arel_attributes_with_values_for_create(attribute_names)
+ def arel_attributes_with_values_for_create(attribute_names) # :nodoc:
arel_attributes_with_values(attributes_for_create(attribute_names))
end
- def arel_attributes_with_values_for_update(attribute_names)
+ def arel_attributes_with_values_for_update(attribute_names) # :nodoc:
arel_attributes_with_values(attributes_for_update(attribute_names))
end
- def attribute_method?(attr_name)
+ def attribute_method?(attr_name) # :nodoc:
+ # We check defined? because Syck calls respond_to? before actually calling initialize.
defined?(@attributes) && @attributes.include?(attr_name)
end
private
# Returns a Hash of the Arel::Attributes and attribute values that have been
- # type casted for use in an Arel insert/update method.
+ # typecasted for use in an Arel insert/update method.
def arel_attributes_with_values(attribute_names)
attrs = {}
arel_table = self.class.arel_table
@@ -236,7 +393,7 @@ module ActiveRecord
# Filters the primary keys and readonly attributes from the attribute names.
def attributes_for_update(attribute_names)
attribute_names.select do |name|
- column_for_attribute(name) && !pk_attribute?(name) && !readonly_attribute?(name)
+ column_for_attribute(name) && !readonly_attribute?(name)
end
end
@@ -257,14 +414,10 @@ module ActiveRecord
end
def typecasted_attribute_value(name)
- if self.class.serialized_attributes.include?(name)
- @attributes[name].serialized_value
- else
- # FIXME: we need @attributes to be used consistently.
- # If the values stored in @attributes were already typecasted, this code
- # could be simplified
- read_attribute(name)
- end
+ # FIXME: we need @attributes to be used consistently.
+ # If the values stored in @attributes were already typecasted, this code
+ # could be simplified
+ read_attribute(name)
end
end
end
diff --git a/activerecord/lib/active_record/attribute_methods/before_type_cast.rb b/activerecord/lib/active_record/attribute_methods/before_type_cast.rb
index d4f529acbf..f596a8b02e 100644
--- a/activerecord/lib/active_record/attribute_methods/before_type_cast.rb
+++ b/activerecord/lib/active_record/attribute_methods/before_type_cast.rb
@@ -1,5 +1,28 @@
module ActiveRecord
module AttributeMethods
+ # = Active Record Attribute Methods Before Type Cast
+ #
+ # <tt>ActiveRecord::AttributeMethods::BeforeTypeCast</tt> provides a way to
+ # read the value of the attributes before typecasting and deserialization.
+ #
+ # class Task < ActiveRecord::Base
+ # end
+ #
+ # task = Task.new(id: '1', completed_on: '2012-10-21')
+ # task.id # => 1
+ # task.completed_on # => Sun, 21 Oct 2012
+ #
+ # task.attributes_before_type_cast
+ # # => {"id"=>"1", "completed_on"=>"2012-10-21", ... }
+ # task.read_attribute_before_type_cast('id') # => "1"
+ # task.read_attribute_before_type_cast('completed_on') # => "2012-10-21"
+ #
+ # In addition to #read_attribute_before_type_cast and #attributes_before_type_cast,
+ # it declares a method for all attributes with the <tt>*_before_type_cast</tt>
+ # suffix.
+ #
+ # task.id_before_type_cast # => "1"
+ # task.completed_on_before_type_cast # => "2012-10-21"
module BeforeTypeCast
extend ActiveSupport::Concern
@@ -7,11 +30,32 @@ module ActiveRecord
attribute_method_suffix "_before_type_cast"
end
+ # Returns the value of the attribute identified by +attr_name+ before
+ # typecasting and deserialization.
+ #
+ # class Task < ActiveRecord::Base
+ # end
+ #
+ # task = Task.new(id: '1', completed_on: '2012-10-21')
+ # task.read_attribute('id') # => 1
+ # task.read_attribute_before_type_cast('id') # => '1'
+ # task.read_attribute('completed_on') # => Sun, 21 Oct 2012
+ # task.read_attribute_before_type_cast('completed_on') # => "2012-10-21"
+ # task.read_attribute_before_type_cast(:completed_on) # => "2012-10-21"
def read_attribute_before_type_cast(attr_name)
- @attributes[attr_name]
+ @attributes[attr_name.to_s]
end
# Returns a hash of attributes before typecasting and deserialization.
+ #
+ # class Task < ActiveRecord::Base
+ # end
+ #
+ # task = Task.new(title: nil, is_done: true, completed_on: '2012-10-21')
+ # task.attributes
+ # # => {"id"=>nil, "title"=>nil, "is_done"=>true, "completed_on"=>Sun, 21 Oct 2012, "created_at"=>nil, "updated_at"=>nil}
+ # task.attributes_before_type_cast
+ # # => {"id"=>nil, "title"=>nil, "is_done"=>true, "completed_on"=>"2012-10-21", "created_at"=>nil, "updated_at"=>nil}
def attributes_before_type_cast
@attributes
end
diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb
index 7a5bb9e863..19e81abba5 100644
--- a/activerecord/lib/active_record/attribute_methods/dirty.rb
+++ b/activerecord/lib/active_record/attribute_methods/dirty.rb
@@ -1,11 +1,6 @@
require 'active_support/core_ext/module/attribute_accessors'
module ActiveRecord
- ActiveSupport.on_load(:active_record_config) do
- mattr_accessor :partial_updates, instance_accessor: false
- self.partial_updates = true
- end
-
module AttributeMethods
module Dirty # :nodoc:
extend ActiveSupport::Concern
@@ -17,14 +12,14 @@ module ActiveRecord
raise "You cannot include Dirty after Timestamp"
end
- config_attribute :partial_updates
+ class_attribute :partial_writes, instance_writer: false
+ self.partial_writes = true
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
@@ -32,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
@@ -52,38 +45,28 @@ 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.
super(attr, value)
end
- def update(*)
- partial_updates? ? super(keys_for_partial_update) : super
+ def update_record(*)
+ partial_writes? ? super(keys_for_partial_write) : super
end
- def create(*)
- if partial_updates?
- keys = keys_for_partial_update
-
- # This is an extremely bloody annoying necessity to work around mysql being crap.
- # See test_mysql_text_not_null_defaults
- keys.concat self.class.columns.select(&:explicit_default?).map(&:name)
-
- super keys
- else
- super
- end
+ def create_record(*)
+ partial_writes? ? super(keys_for_partial_write) : super
end
# Serialized attributes should always be written in case they've been
# changed in place.
- def keys_for_partial_update
+ def keys_for_partial_write
changed | (attributes.keys & self.class.serialized_attributes.keys)
end
@@ -110,7 +93,11 @@ module ActiveRecord
def changes_from_zero_to_string?(old, value)
# For columns with old 0 and value non-empty string
- old == 0 && value.is_a?(String) && value.present? && value != '0'
+ old == 0 && value.is_a?(String) && value.present? && non_zero?(value)
+ end
+
+ def non_zero?(value)
+ value !~ /\A0+(\.0+)?\z/
end
end
end
diff --git a/activerecord/lib/active_record/attribute_methods/primary_key.rb b/activerecord/lib/active_record/attribute_methods/primary_key.rb
index 0857b02c8e..931209b07b 100644
--- a/activerecord/lib/active_record/attribute_methods/primary_key.rb
+++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb
@@ -8,27 +8,32 @@ module ActiveRecord
# Returns this record's primary key value wrapped in an Array if one is
# available.
def to_key
+ sync_with_transaction_state
key = self.id
[key] if key
end
# Returns the primary key value.
def id
+ sync_with_transaction_state
read_attribute(self.class.primary_key)
end
# Sets the primary key value.
def id=(value)
+ sync_with_transaction_state
write_attribute(self.class.primary_key, value) if self.class.primary_key
end
# Queries the primary key value.
def id?
+ sync_with_transaction_state
query_attribute(self.class.primary_key)
end
# Returns the primary key value before type cast.
def id_before_type_cast
+ sync_with_transaction_state
read_attribute_before_type_cast(self.class.primary_key)
end
@@ -76,7 +81,7 @@ module ActiveRecord
end
def get_primary_key(base_name) #:nodoc:
- return 'id' unless base_name && !base_name.blank?
+ return 'id' if base_name.blank?
case primary_key_prefix_type
when :table_name
@@ -85,7 +90,7 @@ module ActiveRecord
base_name.foreign_key
else
if ActiveRecord::Base != self && table_exists?
- connection.schema_cache.primary_keys[table_name]
+ connection.schema_cache.primary_keys(table_name)
else
'id'
end
diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb
index 46fd6ebfb3..c152a246b5 100644
--- a/activerecord/lib/active_record/attribute_methods/read.rb
+++ b/activerecord/lib/active_record/attribute_methods/read.rb
@@ -1,16 +1,45 @@
-module ActiveRecord
- ActiveSupport.on_load(:active_record_config) do
- mattr_accessor :attribute_types_cached_by_default, instance_accessor: false
- end
+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]
included do
- config_attribute :attribute_types_cached_by_default
+ class_attribute :attribute_types_cached_by_default, instance_writer: false
+ self.attribute_types_cached_by_default = ATTRIBUTE_TYPES_CACHED_BY_DEFAULT
end
module ClassMethods
@@ -35,37 +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
- module AttrNames
- unless defined? ATTR_#{safe_name}
- ATTR_#{safe_name} = #{name.inspect}.freeze
+ 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
- end
+ STR
- def __temp__#{safe_name}
- read_attribute(AttrNames::ATTR_#{safe_name}) { |n| missing_attribute(n, caller) }
+ 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
@@ -79,23 +101,22 @@ module ActiveRecord
end
end
- ActiveRecord::Model.attribute_types_cached_by_default = ATTRIBUTE_TYPES_CACHED_BY_DEFAULT
-
# 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)).
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/3552829.
+ # 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 9994a81ede..5701804168 100644
--- a/activerecord/lib/active_record/attribute_methods/serialization.rb
+++ b/activerecord/lib/active_record/attribute_methods/serialization.rb
@@ -5,12 +5,18 @@ module ActiveRecord
included do
# Returns a hash of all the attributes that have been specified for
- # serialization as keys and their class restriction as values.
+ # serialization as keys and their class restriction as values.
class_attribute :serialized_attributes, instance_accessor: false
self.serialized_attributes = {}
end
module ClassMethods
+ ##
+ # :method: serialized_attributes
+ #
+ # Returns a hash of all the attributes that have been specified for
+ # serialization as keys and their class restriction as values.
+
# If you have an attribute that needs to be saved to the database as an
# object, and retrieved as the same object, then specify the name of that
# attribute using this method and it will be handled automatically. The
@@ -44,37 +50,40 @@ module ActiveRecord
end
end
- def serialized_attributes
- ActiveSupport::Deprecation.warn("Instance level serialized_attributes method is deprecated, please use class level method.")
- 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
@@ -85,10 +94,10 @@ module ActiveRecord
# This is only added to the model when serialize is called, which
# ensures we do not make things slower when serialization is not used.
- module Behavior #:nodoc:
+ module Behavior # :nodoc:
extend ActiveSupport::Concern
- module ClassMethods
+ module ClassMethods # :nodoc:
def initialize_attributes(attributes, options = {})
serialized = (options.delete(:serialized) { true }) ? :serialized : :unserialized
super(attributes, options)
@@ -111,6 +120,14 @@ module ActiveRecord
end
end
+ def _field_changed?(attr, old, value)
+ if self.class.serialized_attributes.include?(attr)
+ old != value
+ else
+ super
+ end
+ end
+
def read_attribute_before_type_cast(attr_name)
if self.class.serialized_attributes.include?(attr_name)
super.unserialized_value
@@ -118,6 +135,24 @@ module ActiveRecord
super
end
end
+
+ def attributes_before_type_cast
+ super.dup.tap do |attributes|
+ self.class.serialized_attributes.each_key do |key|
+ if attributes.key?(key)
+ attributes[key] = attributes[key].unserialized_value
+ end
+ end
+ end
+ end
+
+ def typecasted_attribute_value(name)
+ if self.class.serialized_attributes.include?(name)
+ @attributes[name].serialized_value
+ else
+ super
+ end
+ end
end
end
end
diff --git a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
index f36a5806a9..f168282ea3 100644
--- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
+++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
@@ -1,13 +1,4 @@
-
module ActiveRecord
- ActiveSupport.on_load(:active_record_config) do
- mattr_accessor :time_zone_aware_attributes, instance_accessor: false
- self.time_zone_aware_attributes = false
-
- mattr_accessor :skip_time_zone_conversion_for_attributes, instance_accessor: false
- self.skip_time_zone_conversion_for_attributes = []
- end
-
module AttributeMethods
module TimeZoneConversion
class Type # :nodoc:
@@ -28,8 +19,11 @@ module ActiveRecord
extend ActiveSupport::Concern
included do
- config_attribute :time_zone_aware_attributes, global: true
- config_attribute :skip_time_zone_conversion_for_attributes
+ mattr_accessor :time_zone_aware_attributes, instance_writer: false
+ self.time_zone_aware_attributes = false
+
+ class_attribute :skip_time_zone_conversion_for_attributes, instance_writer: false
+ self.skip_time_zone_conversion_for_attributes = []
end
module ClassMethods
@@ -39,19 +33,12 @@ module ActiveRecord
def define_method_attribute=(attr_name)
if create_time_zone_conversion_attribute?(attr_name, columns_hash[attr_name])
method_body, line = <<-EOV, __LINE__ + 1
- def #{attr_name}=(original_time)
- time = original_time
- unless time.acts_like?(:time)
- time = time.is_a?(String) ? Time.zone.parse(time) : time.to_time rescue time
- end
- zoned_time = time && time.in_time_zone rescue nil
- rounded_time = round_usec(zoned_time)
- rounded_value = round_usec(read_attribute("#{attr_name}"))
- if (rounded_value != rounded_time) || (!rounded_value && original_time)
- write_attribute("#{attr_name}", original_time)
- #{attr_name}_will_change!
- @attributes_cache["#{attr_name}"] = zoned_time
- end
+ def #{attr_name}=(time)
+ time_with_zone = time.respond_to?(:in_time_zone) ? time.in_time_zone : nil
+ previous_time = attribute_changed?("#{attr_name}") ? changed_attributes["#{attr_name}"] : read_attribute(:#{attr_name})
+ write_attribute(:#{attr_name}, time)
+ #{attr_name}_will_change! if previous_time != time_with_zone
+ @attributes_cache["#{attr_name}"] = time_with_zone
end
EOV
generated_attribute_methods.module_eval(method_body, __FILE__, line)
@@ -64,15 +51,9 @@ 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
-
- private
- def round_usec(value)
- return unless value
- value.change(:usec => 0)
- end
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 a30f888a7a..e9622ca0c1 100644
--- a/activerecord/lib/active_record/autosave_association.rb
+++ b/activerecord/lib/active_record/autosave_association.rb
@@ -16,8 +16,9 @@ module ActiveRecord
# Note that it also means that associations marked for destruction won't
# be destroyed directly. They will however still be marked for destruction.
#
- # Note that <tt>:autosave => false</tt> is not same as not declaring <tt>:autosave</tt>.
- # When the <tt>:autosave</tt> option is not present new associations are saved.
+ # Note that <tt>autosave: false</tt> is not same as not declaring <tt>:autosave</tt>.
+ # When the <tt>:autosave</tt> option is not present then new association records are
+ # saved but the updated association records are not saved.
#
# == Validation
#
@@ -32,12 +33,10 @@ module ActiveRecord
# autosave callbacks are executed. Placing your callbacks after
# associations is usually a good practice.
#
- # == Examples
- #
# === One-to-one Example
#
# class Post
- # has_one :author, :autosave => true
+ # has_one :author, autosave: true
# end
#
# Saving changes to the parent and its associated model can now be performed
@@ -64,14 +63,14 @@ module ActiveRecord
# Note that the model is _not_ yet removed from the database:
#
# id = post.author.id
- # Author.find_by_id(id).nil? # => false
+ # Author.find_by(id: id).nil? # => false
#
# post.save
# post.reload.author # => nil
#
# Now it _is_ removed from the database:
#
- # Author.find_by_id(id).nil? # => true
+ # Author.find_by(id: id).nil? # => true
#
# === One-to-many Example
#
@@ -81,27 +80,27 @@ module ActiveRecord
# has_many :comments # :autosave option is not declared
# end
#
- # post = Post.new(:title => 'ruby rocks')
- # post.comments.build(:body => 'hello world')
+ # post = Post.new(title: 'ruby rocks')
+ # post.comments.build(body: 'hello world')
# post.save # => saves both post and comment
#
- # post = Post.create(:title => 'ruby rocks')
- # post.comments.build(:body => 'hello world')
+ # post = Post.create(title: 'ruby rocks')
+ # post.comments.build(body: 'hello world')
# post.save # => saves both post and comment
#
- # post = Post.create(:title => 'ruby rocks')
- # post.comments.create(:body => 'hello world')
+ # post = Post.create(title: 'ruby rocks')
+ # post.comments.create(body: 'hello world')
# post.save # => saves both post and comment
#
# When <tt>:autosave</tt> is true all children are saved, no matter whether they
# are new records or not:
#
# class Post
- # has_many :comments, :autosave => true
+ # has_many :comments, autosave: true
# end
#
- # post = Post.create(:title => 'ruby rocks')
- # post.comments.create(:body => 'hello world')
+ # post = Post.create(title: 'ruby rocks')
+ # post.comments.create(body: 'hello world')
# post.comments[0].body = 'hi everyone'
# post.save # => saves both post and comment, with 'hi everyone' as body
#
@@ -115,105 +114,106 @@ module ActiveRecord
# Note that the model is _not_ yet removed from the database:
#
# id = post.comments.last.id
- # Comment.find_by_id(id).nil? # => false
+ # Comment.find_by(id: id).nil? # => false
#
# post.save
# post.reload.comments.length # => 1
#
# Now it _is_ removed from the database:
#
- # Comment.find_by_id(id).nil? # => true
+ # Comment.find_by(id: id).nil? # => true
module AutosaveAssociation
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.
def reload(options = nil)
@marked_for_destruction = false
+ @destroyed_by_association = nil
super
end
@@ -233,6 +233,19 @@ module ActiveRecord
@marked_for_destruction
end
+ # Records the association that is being destroyed and destroying this
+ # record in the process.
+ def destroyed_by_association=(reflection)
+ @destroyed_by_association = reflection
+ end
+
+ # Returns the association for the parent being destroyed.
+ #
+ # Used to avoid updating the counter cache unnecessarily.
+ def destroyed_by_association
+ @destroyed_by_association
+ end
+
# Returns whether or not this record has been changed in any way (including whether
# any of its nested autosave associations are likewise changed)
def changed_for_autosave?
@@ -241,173 +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)
- records_to_destroy = []
- records.each do |record|
- next if record.destroyed?
-
- saved = true
-
- if autosave && record.marked_for_destruction?
- records_to_destroy << record
- elsif 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)
+ # 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
- raise ActiveRecord::Rollback unless saved
- end
+ records.each do |record|
+ next if record.destroyed?
+
+ 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?
+ end
+ elsif autosave
+ saved = record.save(:validate => false)
+ end
- records_to_destroy.each do |record|
- association.destroy(record)
+ raise ActiveRecord::Rollback unless saved
+ end
end
- end
- # reconstruct the scope now that we know the owner's id
- association.send(: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
+
+ # 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
+ 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 a4705b24ca..04e3dd49e7 100644
--- a/activerecord/lib/active_record/base.rb
+++ b/activerecord/lib/active_record/base.rb
@@ -8,16 +8,17 @@ require 'active_support/core_ext/class/attribute_accessors'
require 'active_support/core_ext/class/delegating_attributes'
require 'active_support/core_ext/array/extract_options'
require 'active_support/core_ext/hash/deep_merge'
-require 'active_support/core_ext/hash/indifferent_access'
require 'active_support/core_ext/hash/slice'
require 'active_support/core_ext/string/behavior'
require 'active_support/core_ext/kernel/singleton_class'
require 'active_support/core_ext/module/introspection'
require 'active_support/core_ext/object/duplicable'
+require 'active_support/core_ext/class/subclasses'
require '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
@@ -36,7 +37,7 @@ module ActiveRecord #:nodoc:
# method is especially useful when you're receiving the data from somewhere else, like an
# HTTP request. It works like this:
#
- # user = User.new(:name => "David", :occupation => "Code Artist")
+ # user = User.new(name: "David", occupation: "Code Artist")
# user.name # => "David"
#
# You can also use block initialization:
@@ -69,7 +70,7 @@ module ActiveRecord #:nodoc:
# end
#
# def self.authenticate_safely_simply(user_name, password)
- # where(:user_name => user_name, :password => password).first
+ # where(user_name: user_name, password: password).first
# end
# end
#
@@ -87,27 +88,27 @@ module ActiveRecord #:nodoc:
#
# Company.where(
# "id = :id AND name = :name AND division = :division AND created_at > :accounting_date",
- # { :id => 3, :name => "37signals", :division => "First", :accounting_date => '2005-01-01' }
+ # { id: 3, name: "37signals", division: "First", accounting_date: '2005-01-01' }
# ).first
#
# Similarly, a simple hash without a statement will generate conditions based on equality with the SQL AND
# operator. For instance:
#
- # Student.where(:first_name => "Harvey", :status => 1)
+ # Student.where(first_name: "Harvey", status: 1)
# Student.where(params[:student])
#
# A range may be used in the hash to use the SQL BETWEEN operator:
#
- # Student.where(:grade => 9..12)
+ # Student.where(grade: 9..12)
#
# An array may be used in the hash to use the SQL IN operator:
#
- # Student.where(:grade => [9,11,12])
+ # Student.where(grade: [9,11,12])
#
# When joining tables, nested hashes or keys written in the form 'table_name.column_name'
# can be used to qualify the table name of a particular condition. For instance:
#
- # Student.joins(:schools).where(:schools => { :category => 'public' })
+ # Student.joins(:schools).where(schools: { category: 'public' })
# Student.joins(:schools).where('schools.category' => 'public' )
#
# == Overwriting default accessors
@@ -141,10 +142,10 @@ module ActiveRecord #:nodoc:
# For example, an Active Record User with the <tt>name</tt> attribute has a <tt>name?</tt> method that you can call
# to determine whether the user has a name:
#
- # user = User.new(:name => "David")
+ # user = User.new(name: "David")
# user.name? # => true
#
- # anonymous = User.new(:name => "")
+ # anonymous = User.new(name: "")
# anonymous.name? # => false
#
# == Accessing attributes before they have been typecasted
@@ -160,14 +161,11 @@ module ActiveRecord #:nodoc:
#
# == Dynamic attribute-based finders
#
- # Dynamic attribute-based finders are a cleaner way of getting (and/or creating) objects
+ # Dynamic attribute-based finders are a mildly deprecated way of getting (and/or creating) objects
# by simple queries without turning to SQL. They work by appending the name of an attribute
- # to <tt>find_by_</tt>, <tt>find_last_by_</tt>, or <tt>find_all_by_</tt> and thus produces finders
- # like <tt>Person.find_by_user_name</tt>, <tt>Person.find_all_by_last_name</tt>, and
- # <tt>Payment.find_by_transaction_id</tt>. Instead of writing
- # <tt>Person.where(:user_name => user_name).first</tt>, you just do <tt>Person.find_by_user_name(user_name)</tt>.
- # And instead of writing <tt>Person.where(:last_name => last_name).all</tt>, you just do
- # <tt>Person.find_all_by_last_name(last_name)</tt>.
+ # to <tt>find_by_</tt> like <tt>Person.find_by_user_name</tt>.
+ # Instead of writing <tt>Person.find_by(user_name: user_name)</tt>, you can use
+ # <tt>Person.find_by_user_name(user_name)</tt>.
#
# It's possible to add an exclamation point (!) on the end of the dynamic finders to get them to raise an
# <tt>ActiveRecord::RecordNotFound</tt> error if they do not return any records,
@@ -175,51 +173,12 @@ module ActiveRecord #:nodoc:
#
# It's also possible to use multiple attributes in the same find by separating them with "_and_".
#
- # Person.where(:user_name => user_name, :password => password).first
+ # Person.find_by(user_name: user_name, password: password)
# Person.find_by_user_name_and_password(user_name, password) # with dynamic finder
#
# It's even possible to call these dynamic finder methods on relations and named scopes.
#
- # Payment.order("created_on").find_all_by_amount(50)
- # Payment.pending.find_last_by_amount(100)
- #
- # The same dynamic finder style can be used to create the object if it doesn't already exist.
- # This dynamic finder is called with <tt>find_or_create_by_</tt> and will return the object if
- # it already exists and otherwise creates it, then returns it. Protected attributes won't be set
- # unless they are given in a block.
- #
- # # No 'Summer' tag exists
- # Tag.find_or_create_by_name("Summer") # equal to Tag.create(:name => "Summer")
- #
- # # Now the 'Summer' tag does exist
- # Tag.find_or_create_by_name("Summer") # equal to Tag.find_by_name("Summer")
- #
- # # Now 'Bob' exist and is an 'admin'
- # User.find_or_create_by_name('Bob', :age => 40) { |u| u.admin = true }
- #
- # Adding an exclamation point (!) on to the end of <tt>find_or_create_by_</tt> will
- # raise an <tt>ActiveRecord::RecordInvalid</tt> error if the new record is invalid.
- #
- # Use the <tt>find_or_initialize_by_</tt> finder if you want to return a new record without
- # saving it first. Protected attributes won't be set unless they are given in a block.
- #
- # # No 'Winter' tag exists
- # winter = Tag.find_or_initialize_by_name("Winter")
- # winter.persisted? # false
- #
- # To find by a subset of the attributes to be used for instantiating a new object, pass a hash instead of
- # a list of parameters.
- #
- # Tag.find_or_create_by_name(:name => "rails", :creator => current_user)
- #
- # That will either find an existing tag named "rails", or create a new one while setting the
- # user that created it.
- #
- # Just like <tt>find_by_*</tt>, you can also use <tt>scoped_by_*</tt> to retrieve data. The good thing about
- # using this feature is that the very first time result is returned using <tt>method_missing</tt> technique
- # but after that the method is declared on the class. Henceforth <tt>method_missing</tt> will not be hit.
- #
- # User.scoped_by_user_name('David')
+ # Payment.order("created_on").find_by_amount(50)
#
# == Saving arrays, hashes, and other non-mappable objects in text columns
#
@@ -232,7 +191,7 @@ module ActiveRecord #:nodoc:
# serialize :preferences
# end
#
- # user = User.create(:preferences => { "background" => "black", "display" => large })
+ # user = User.create(preferences: { "background" => "black", "display" => large })
# User.find(user.id).preferences # => { "background" => "black", "display" => large }
#
# You can also specify a class option as the second parameter that'll raise an exception
@@ -242,7 +201,7 @@ module ActiveRecord #:nodoc:
# serialize :preferences, Hash
# end
#
- # user = User.create(:preferences => %w( one two three ))
+ # user = User.create(preferences: %w( one two three ))
# User.find(user.id).preferences # raises SerializationTypeMismatch
#
# When you specify a class option, the default value for that attribute will be a new
@@ -267,9 +226,9 @@ module ActiveRecord #:nodoc:
# class Client < Company; end
# class PriorityClient < Client; end
#
- # When you do <tt>Firm.create(:name => "37signals")</tt>, this record will be saved in
+ # When you do <tt>Firm.create(name: "37signals")</tt>, this record will be saved in
# the companies table with type = "Firm". You can then fetch this row again using
- # <tt>Company.where(:name => '37signals').first</tt> and it will return a Firm object.
+ # <tt>Company.where(name: '37signals').first</tt> and it will return a Firm object.
#
# If you don't have a type column defined in your table, single-table inheritance won't
# be triggered. In that case, it'll work just like normal subclasses with no special magic
@@ -321,8 +280,46 @@ module ActiveRecord #:nodoc:
# So it's possible to assign a logger to the class through <tt>Base.logger=</tt> which will then be used by all
# instances in the current object space.
class Base
- include ActiveRecord::Model
+ extend ActiveModel::Naming
+
+ extend ActiveSupport::Benchmarkable
+ extend ActiveSupport::DescendantsTracker
+
+ extend ConnectionHandling
+ extend QueryCache::ClassMethods
+ extend Querying
+ extend Translation
+ extend DynamicMatchers
+ extend Explain
+ extend Delegation::DelegateCache
+
+ include Persistence
+ include ReadonlyAttributes
+ include ModelSchema
+ include Inheritance
+ include Scoping
+ include Sanitization
+ include AttributeAssignment
+ include ActiveModel::Conversion
+ include Integration
+ include Validations
+ include CounterCache
+ include Locking::Optimistic
+ include Locking::Pessimistic
+ include AttributeMethods
+ include Callbacks
+ include Timestamp
+ include Associations
+ include ActiveModel::SecurePassword
+ include AutosaveAssociation
+ include NestedAttributes
+ include Aggregations
+ include Transactions
+ include Reflection
+ include Serialization
+ include Store
+ include Core
end
-end
-ActiveSupport.run_load_hooks(:active_record, ActiveRecord::Model::DeprecationProxy.new)
+ ActiveSupport.run_load_hooks(:active_record, Base)
+end
diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb
index 111208d0b9..128a9377c1 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.
#
@@ -35,7 +38,7 @@ module ActiveRecord
# class CreditCard < ActiveRecord::Base
# # Strip everything but digits, so the user can specify "555 234 34" or
# # "5552-3434" and both will mean "55523434"
- # before_validation(:on => :create) do
+ # before_validation(on: :create) do
# self.number = number.gsub(/[^0-9]/, "") if attribute_present?("number")
# end
# end
@@ -83,7 +86,7 @@ module ActiveRecord
#
# In that case, <tt>Reply#destroy</tt> would only run +destroy_readers+ and _not_ +destroy_author+.
# So, use the callback macros when you want to ensure that a certain callback is called for the entire
- # hierarchy, and use the regular overwriteable methods when you want to leave it up to each descendant
+ # hierarchy, and use the regular overwritable methods when you want to leave it up to each descendant
# to decide whether they want to call +super+ and trigger the inherited callbacks.
#
# *IMPORTANT:* In order for inheritance to work for the callback queues, you must specify the
@@ -200,6 +203,40 @@ module ActiveRecord
# Callbacks are generally run in the order they are defined, with the exception of callbacks defined as
# methods on the model, which are called last.
#
+ # == Ordering callbacks
+ #
+ # Sometimes the code needs that the callbacks execute in a specific order. For example, a +before_destroy+
+ # callback (+log_children+ in this case) should be executed before the children get destroyed by the +dependent: destroy+ option.
+ #
+ # Let's look at the code below:
+ #
+ # class Topic < ActiveRecord::Base
+ # has_many :children, dependent: destroy
+ #
+ # before_destroy :log_children
+ #
+ # private
+ # def log_children
+ # # Child processing
+ # end
+ # end
+ #
+ # In this case, the problem is that when the +before_destroy+ callback is executed, the children are not available
+ # because the +destroy+ callback gets executed first. You can use the +prepend+ option on the +before_destroy+ callback to avoid this.
+ #
+ # class Topic < ActiveRecord::Base
+ # has_many :children, dependent: destroy
+ #
+ # before_destroy :log_children, prepend: true
+ #
+ # private
+ # def log_children
+ # # Child processing
+ # end
+ # end
+ #
+ # This way, the +before_destroy+ gets executed before the <tt>dependent: destroy</tt> is called, and the data is still available.
+ #
# == Transactions
#
# The entire callback chain of a +save+, <tt>save!</tt>, or +destroy+ call runs
@@ -265,11 +302,11 @@ module ActiveRecord
run_callbacks(:save) { super }
end
- def create #:nodoc:
+ def create_record #:nodoc:
run_callbacks(:create) { super }
end
- def update(*) #:nodoc:
+ def update_record(*) #:nodoc:
run_callbacks(:update) { super }
end
end
diff --git a/activerecord/lib/active_record/coders/yaml_column.rb b/activerecord/lib/active_record/coders/yaml_column.rb
index f6cdc67b4d..d3d7396c91 100644
--- a/activerecord/lib/active_record/coders/yaml_column.rb
+++ b/activerecord/lib/active_record/coders/yaml_column.rb
@@ -3,7 +3,6 @@ require 'yaml'
module ActiveRecord
module Coders # :nodoc:
class YAMLColumn # :nodoc:
- RESCUE_ERRORS = [ ArgumentError, Psych::SyntaxError ]
attr_accessor :object_class
@@ -24,19 +23,15 @@ module ActiveRecord
def load(yaml)
return object_class.new if object_class != Object && yaml.nil?
return yaml unless yaml.is_a?(String) && yaml =~ /^---/
- begin
- obj = YAML.load(yaml)
-
- unless obj.is_a?(object_class) || obj.nil?
- raise SerializationTypeMismatch,
- "Attribute was supposed to be a #{object_class}, but was a #{obj.class}"
- end
- obj ||= object_class.new if object_class != Object
-
- obj
- rescue *RESCUE_ERRORS
- yaml
+ obj = YAML.load(yaml)
+
+ unless obj.is_a?(object_class) || obj.nil?
+ raise SerializationTypeMismatch,
+ "Attribute was supposed to be a #{object_class}, but was a #{obj.class}"
end
+ obj ||= object_class.new if object_class != Object
+
+ obj
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
index 42bd16db80..cfdcae7f63 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
@@ -1,7 +1,7 @@
require 'thread'
+require 'thread_safe'
require 'monitor'
require 'set'
-require 'active_support/core_ext/module/deprecation'
module ActiveRecord
# Raised when a connection could not be obtained within the connection
@@ -236,38 +236,32 @@ module ActiveRecord
@spec = spec
- # The cache of reserved connections mapped to threads
- @reserved_connections = {}
-
@checkout_timeout = spec.config[:checkout_timeout] || 5
- @dead_connection_timeout = spec.config[:dead_connection_timeout]
+ @dead_connection_timeout = spec.config[:dead_connection_timeout] || 5
@reaper = Reaper.new self, spec.config[:reaping_frequency]
@reaper.run
# default max pool size to 5
@size = (spec.config[:pool] && spec.config[:pool].to_i) || 5
+ # The cache of reserved connections mapped to threads
+ @reserved_connections = ThreadSafe::Cache.new(:initial_capacity => @size)
+
@connections = []
@automatic_reconnect = true
@available = Queue.new self
end
- # Hack for tests to be able to add connections. Do not call outside of tests
- def insert_connection_for_test!(c) #:nodoc:
- synchronize do
- @connections << c
- @available.add c
- end
- end
-
# Retrieve the connection associated with the current thread, or call
# #checkout to obtain one if necessary.
#
# #connection can be called any number of times; the connection is
# held in a hash keyed by the thread id.
def connection
- synchronize do
+ # this is correctly done double-checked locking
+ # (ThreadSafe::Cache's lookups have volatile semantics)
+ @reserved_connections[current_connection_id] || synchronize do
@reserved_connections[current_connection_id] ||= checkout
end
end
@@ -310,7 +304,7 @@ module ActiveRecord
# Disconnects all connections in the pool, and clears the pool.
def disconnect!
synchronize do
- @reserved_connections = {}
+ @reserved_connections.clear
@connections.each do |conn|
checkin conn
conn.disconnect!
@@ -323,7 +317,7 @@ module ActiveRecord
# Clears the cache which maps classes.
def clear_reloadable_connections!
synchronize do
- @reserved_connections = {}
+ @reserved_connections.clear
@connections.each do |conn|
checkin conn
conn.disconnect! if conn.requires_reloading?
@@ -338,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.
#
@@ -404,7 +393,9 @@ module ActiveRecord
synchronize do
stale = Time.now - @dead_connection_timeout
connections.dup.each do |conn|
- remove conn if conn.in_use? && stale > conn.last_use && !conn.active?
+ if conn.in_use? && stale > conn.last_use && !conn.active?
+ remove conn
+ end
end
end
end
@@ -441,11 +432,11 @@ module ActiveRecord
end
def new_connection
- ActiveRecord::Model.send(spec.adapter_method, spec.config)
+ Base.send(spec.adapter_method, spec.config)
end
def current_connection_id #:nodoc:
- ActiveRecord::Model.connection_id ||= Thread.current.object_id
+ Base.connection_id ||= Thread.current.object_id
end
def checkout_new_connection
@@ -490,39 +481,55 @@ module ActiveRecord
# determine the connection pool that they should use.
class ConnectionHandler
def initialize
- @owner_to_pool = Hash.new { |h,k| h[k] = {} }
- @class_to_pool = Hash.new { |h,k| h[k] = {} }
+ # These caches are keyed by klass.name, NOT klass. Keying them by klass
+ # alone would lead to memory leaks in development mode as all previous
+ # instances of the class would stay in memory.
+ @owner_to_pool = ThreadSafe::Cache.new(:initial_capacity => 2) do |h,k|
+ h[k] = ThreadSafe::Cache.new(:initial_capacity => 2)
+ end
+ @class_to_pool = ThreadSafe::Cache.new(:initial_capacity => 2) do |h,k|
+ h[k] = ThreadSafe::Cache.new
+ end
end
- def connection_pools
+ def connection_pool_list
owner_to_pool.values.compact
end
+ def connection_pools
+ ActiveSupport::Deprecation.warn(
+ "In the next release, this will return the same as #connection_pool_list. " \
+ "(An array of pools, rather than a hash mapping specs to pools.)"
+ )
+ Hash[connection_pool_list.map { |pool| [pool.spec, pool] }]
+ end
+
def establish_connection(owner, spec)
@class_to_pool.clear
- owner_to_pool[owner] = ConnectionAdapters::ConnectionPool.new(spec)
+ raise RuntimeError, "Anonymous class is not allowed." unless owner.name
+ owner_to_pool[owner.name] = ConnectionAdapters::ConnectionPool.new(spec)
end
# Returns true if there are any active connections among the connection
# pools that the ConnectionHandler is managing.
def active_connections?
- connection_pools.any?(&:active_connection?)
+ connection_pool_list.any?(&:active_connection?)
end
# Returns any connections in use by the current thread back to the pool,
# and also returns connections to the pool cached by threads that are no
# longer alive.
def clear_active_connections!
- connection_pools.each(&:release_connection)
+ connection_pool_list.each(&:release_connection)
end
# Clears the cache which maps classes.
def clear_reloadable_connections!
- connection_pools.each(&:clear_reloadable_connections!)
+ connection_pool_list.each(&:clear_reloadable_connections!)
end
def clear_all_connections!
- connection_pools.each(&:disconnect!)
+ connection_pool_list.each(&:disconnect!)
end
# Locate the connection of the nearest super class. This can be an
@@ -546,7 +553,7 @@ module ActiveRecord
# can be used as an argument for establish_connection, for easily
# re-establishing the connection.
def remove_connection(owner)
- if pool = owner_to_pool.delete(owner)
+ if pool = owner_to_pool.delete(owner.name)
@class_to_pool.clear
pool.automatic_reconnect = false
pool.disconnect!
@@ -559,18 +566,18 @@ module ActiveRecord
# When a connection is established or removed, we invalidate the cache.
#
# Ideally we would use #fetch here, as class_to_pool[klass] may sometimes be nil.
- # However, benchmarking (https://gist.github.com/3552829) showed that #fetch is
- # significantly slower than #[]. So in the nil case, no caching will take place,
- # but that's ok since the nil case is not the common one that we wish to optimise
- # for.
+ # However, benchmarking (https://gist.github.com/jonleighton/3552829) showed that
+ # #fetch is significantly slower than #[]. So in the nil case, no caching will
+ # take place, but that's ok since the nil case is not the common one that we wish
+ # to optimise for.
def retrieve_connection_pool(klass)
- class_to_pool[klass] ||= begin
+ class_to_pool[klass.name] ||= begin
until pool = pool_for(klass)
klass = klass.superclass
- break unless klass < ActiveRecord::Tag
+ break unless klass <= Base
end
- class_to_pool[klass] = pool || pool_for(ActiveRecord::Model)
+ class_to_pool[klass.name] = pool
end
end
@@ -585,21 +592,21 @@ module ActiveRecord
end
def pool_for(owner)
- owner_to_pool.fetch(owner) {
+ owner_to_pool.fetch(owner.name) {
if ancestor_pool = pool_from_any_process_for(owner)
# A connection was established in an ancestor process that must have
# subsequently forked. We can't reuse the connection, but we can copy
# the specification and establish a new connection with it.
establish_connection owner, ancestor_pool.spec
else
- owner_to_pool[owner] = nil
+ owner_to_pool[owner.name] = nil
end
}
end
def pool_from_any_process_for(owner)
- owner_to_pool = @owner_to_pool.values.find { |v| v[owner] }
- owner_to_pool && owner_to_pool[owner]
+ owner_to_pool = @owner_to_pool.values.find { |v| v[owner.name] }
+ owner_to_pool && owner_to_pool[owner.name]
end
end
@@ -617,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_limits.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb
index 30ccb8f0a4..c0a2111571 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb
@@ -17,6 +17,15 @@ module ActiveRecord
64
end
+ # Returns the maximum allowed length for an index name. This
+ # limit is enforced by rails and Is less than or equal to
+ # <tt>index_name_length</tt>. The gap between
+ # <tt>index_name_length</tt> is to allow internal rails
+ # operations to use prefixes in temporary operations.
+ def allowed_index_name_length
+ index_name_length
+ end
+
# Returns the maximum length of an index name.
def index_name_length
64
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 0d7046a705..e1f29ea03a 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.
@@ -125,7 +123,8 @@ module ActiveRecord
# In order to get around this problem, #transaction will emulate the effect
# of nested transactions, by using savepoints:
# http://dev.mysql.com/doc/refman/5.0/en/savepoint.html
- # Savepoints are supported by MySQL and PostgreSQL, but not SQLite3.
+ # Savepoints are supported by MySQL and PostgreSQL. SQLite3 version >= '3.6.8'
+ # supports savepoints.
#
# It is safe to call this method if a database transaction is already open,
# i.e. if #transaction is called within another #transaction block. In case
@@ -150,7 +149,7 @@ module ActiveRecord
# already-automatically-released savepoints:
#
# Model.connection.transaction do # BEGIN
- # Model.connection.transaction(:requires_new => true) do # CREATE SAVEPOINT active_record_1
+ # Model.connection.transaction(requires_new: true) do # CREATE SAVEPOINT active_record_1
# Model.connection.create_table(...)
# # active_record_1 now automatically released
# end # RELEASE SAVEPOINT active_record_1 <--- BOOM! database error!
@@ -287,7 +286,7 @@ module ActiveRecord
# Inserts the given fixture into the table. Overridden in adapters that require
# something beyond a simple insert (eg. Oracle).
def insert_fixture(fixture, table_name)
- columns = Hash[columns(table_name).map { |c| [c.name, c] }]
+ columns = schema_cache.columns_hash(table_name)
key_list = []
value_list = fixture.map do |name, value|
@@ -354,8 +353,7 @@ module ActiveRecord
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
@@ -376,14 +374,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 be6fda95b4..8399232d73 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(*args)
- clear_query_cache if @query_cache_enabled # clear_query_cache if @query_cache_enabled
- super # update_without_query_dirty(*args)
- end # end
+ def #{method_name}(*)
+ clear_query_cache if @query_cache_enabled
+ super
+ end
end_code
end
end
@@ -20,6 +20,12 @@ 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
@@ -75,16 +81,11 @@ 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
+ # queries should not be cached.
def locked?(arel)
arel.respond_to?(:locked) && arel.locked
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
index 60a9eee7c7..552a22d28a 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
@@ -52,7 +51,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
@@ -93,6 +91,18 @@ module ActiveRecord
quote_column_name(table_name)
end
+ # Override to return the quoted table name for assignment. Defaults to
+ # table quoting.
+ #
+ # This works for mysql and mysql2 where table.column can be used to
+ # resolve ambiguity.
+ #
+ # We override this in the sqlite and postgresql adapters to use only
+ # the column name (as per syntax requirements).
+ def quote_table_name_for_assignment(table, attr)
+ quote_table_name("#{table}.#{attr}")
+ end
+
def quoted_true
"'t'"
end
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_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
index dca355aa93..063b19871a 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
@@ -8,44 +8,28 @@ module ActiveRecord
# Abstract representation of an index definition on a table. Instances of
# this type are typically created and returned by methods in database
# adapters. e.g. ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter#indexes
- class IndexDefinition < Struct.new(:table, :name, :unique, :columns, :lengths, :orders, :where) #:nodoc:
+ class IndexDefinition < Struct.new(:table, :name, :unique, :columns, :lengths, :orders, :where, :type, :using) #:nodoc:
end
# Abstract representation of a column definition. Instances of this type
# are typically created by methods in TableDefinition, and added to the
# +columns+ attribute of said TableDefinition object, in order to be used
# for generating a number of table creation or table changing SQL statements.
- class ColumnDefinition < Struct.new(:base, :name, :type, :limit, :precision, :scale, :default, :null) #:nodoc:
+ class ColumnDefinition < Struct.new(:name, :type, :limit, :precision, :scale, :default, :null, :first, :after, :primary_key) #:nodoc:
- def string_to_binary(value)
- value
+ def primary_key?
+ primary_key || type.to_sym == :primary_key
end
+ end
- def sql_type
- base.type_to_sql(type.to_sym, limit, precision, scale)
- end
-
- def to_sql
- column_sql = "#{base.quote_column_name(name)} #{sql_type}"
- column_options = {}
- column_options[:null] = null unless null.nil?
- column_options[:default] = default unless default.nil?
- add_column_options!(column_sql, column_options) unless type.to_sym == :primary_key
- column_sql
- end
-
- private
-
- def add_column_options!(sql, options)
- base.add_column_options!(sql, options.merge(:column => self))
- 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.
#
- # Inside migration files, the +t+ object in +create_table+ and
- # +change_table+ is actually of this type:
+ # Inside migration files, the +t+ object in +create_table+
+ # is actually of this type:
#
# class SomeMigration < ActiveRecord::Migration
# def up
@@ -64,28 +48,24 @@ module ActiveRecord
class TableDefinition
# An array of ColumnDefinition objects, representing the column changes
# that have been defined.
- attr_accessor :columns, :indexes
+ attr_accessor :indexes
+ attr_reader :name, :temporary, :options
- def initialize(base)
- @columns = []
+ def initialize(types, name, temporary, options)
@columns_hash = {}
@indexes = {}
- @base = base
+ @native = types
+ @temporary = temporary
+ @options = options
+ @name = name
end
- def xml(*args)
- raise NotImplementedError unless %w{
- sqlite mysql mysql2
- }.include? @base.adapter_name.downcase
-
- options = args.extract_options!
- column(args[0], :text, options)
- end
+ def columns; @columns_hash.values; end
# Appends a primary key definition to the table definition.
# Can be called multiple times, but this is probably not a good idea.
- def primary_key(name)
- column(name, :primary_key)
+ def primary_key(name, type = :primary_key, options = {})
+ column(name, type, options.merge(:primary_key => true))
end
# Returns a ColumnDefinition for the column with name +name+.
@@ -161,21 +141,21 @@ module ActiveRecord
# td.column(:granted, :boolean)
# # granted BOOLEAN
#
- # td.column(:picture, :binary, :limit => 2.megabytes)
+ # td.column(:picture, :binary, limit: 2.megabytes)
# # => picture BLOB(2097152)
#
- # td.column(:sales_stage, :string, :limit => 20, :default => 'new', :null => false)
+ # td.column(:sales_stage, :string, limit: 20, default: 'new', null: false)
# # => sales_stage VARCHAR(20) DEFAULT 'new' NOT NULL
#
- # td.column(:bill_gates_money, :decimal, :precision => 15, :scale => 2)
+ # td.column(:bill_gates_money, :decimal, precision: 15, scale: 2)
# # => bill_gates_money DECIMAL(15,2)
#
- # td.column(:sensor_reading, :decimal, :precision => 30, :scale => 20)
+ # td.column(:sensor_reading, :decimal, precision: 30, scale: 20)
# # => sensor_reading DECIMAL(30,20)
#
# # While <tt>:scale</tt> defaults to zero on most databases, it
# # probably wouldn't hurt to include it.
- # td.column(:huge_integer, :decimal, :precision => 30)
+ # td.column(:huge_integer, :decimal, precision: 30)
# # => huge_integer DECIMAL(30)
#
# # Defines a column with a database-specific type.
@@ -190,20 +170,20 @@ module ActiveRecord
#
# What can be written like this with the regular calls to column:
#
- # create_table "products", :force => true do |t|
- # t.column "shop_id", :integer
- # t.column "creator_id", :integer
- # t.column "name", :string, :default => "Untitled"
- # t.column "value", :string, :default => "Untitled"
- # t.column "created_at", :datetime
- # t.column "updated_at", :datetime
+ # create_table :products do |t|
+ # t.column :shop_id, :integer
+ # t.column :creator_id, :integer
+ # t.column :name, :string, default: "Untitled"
+ # t.column :value, :string, default: "Untitled"
+ # t.column :created_at, :datetime
+ # t.column :updated_at, :datetime
# end
#
- # Can also be written as follows using the short-hand:
+ # can also be written as follows using the short-hand:
#
# create_table :products do |t|
# t.integer :shop_id, :creator_id
- # t.string :name, :value, :default => "Untitled"
+ # t.string :name, :value, default: "Untitled"
# t.timestamps
# end
#
@@ -218,51 +198,46 @@ module ActiveRecord
# create_table :taggings do |t|
# t.integer :tag_id, :tagger_id, :taggable_id
# t.string :tagger_type
- # t.string :taggable_type, :default => 'Photo'
+ # t.string :taggable_type, default: 'Photo'
# end
- # add_index :taggings, :tag_id, :name => 'index_taggings_on_tag_id'
+ # add_index :taggings, :tag_id, name: 'index_taggings_on_tag_id'
# add_index :taggings, [:tagger_id, :tagger_type]
#
# Can also be written as follows using references:
#
# create_table :taggings do |t|
- # t.references :tag, :index => { :name => 'index_taggings_on_tag_id' }
- # t.references :tagger, :polymorphic => true, :index => true
- # t.references :taggable, :polymorphic => { :default => 'Photo' }
+ # t.references :tag, index: { name: 'index_taggings_on_tag_id' }
+ # t.references :tagger, polymorphic: true, index: true
+ # t.references :taggable, polymorphic: { default: 'Photo' }
# end
def column(name, type, options = {})
name = name.to_s
type = type.to_sym
- column = self[name] || new_column_definition(@base, name, type)
-
- limit = options.fetch(:limit) do
- native[type][:limit] if native[type].is_a?(Hash)
+ if primary_key_column_name == name
+ raise ArgumentError, "you can't redefine the primary key column '#{name}'. To define a custom primary key, pass { id: false } to create_table."
end
- column.limit = limit
- column.precision = options[:precision]
- column.scale = options[:scale]
- column.default = options[:default]
- column.null = options[:null]
+ @columns_hash[name] = new_column_definition(name, type, options)
self
end
- %w( string text integer float decimal datetime timestamp time date binary boolean ).each do |column_type|
- class_eval <<-EOV, __FILE__, __LINE__ + 1
- def #{column_type}(*args) # def string(*args)
- options = args.extract_options! # options = args.extract_options!
- column_names = args # column_names = args
- type = :'#{column_type}' # type = :string
- column_names.each { |name| column(name, type, options) } # column_names.each { |name| column(name, type, options) }
- end # end
- EOV
+ def remove_column(name)
+ @columns_hash.delete name.to_s
+ end
+
+ [:string, :text, :integer, :float, :decimal, :datetime, :timestamp, :time, :date, :binary, :boolean].each do |column_type|
+ define_method column_type do |*args|
+ options = args.extract_options!
+ column_names = args
+ column_names.each { |name| column(name, column_type, options) }
+ end
end
# Adds index options to the indexes hash, keyed by column name
# This is primarily used to track indexes that need to be created after the table
#
- # index(:account_id, :name => 'index_projects_on_account_id')
+ # index(:account_id, name: 'index_projects_on_account_id')
def index(column_name, options = {})
indexes[column_name] = options
end
@@ -282,28 +257,58 @@ module ActiveRecord
args.each do |col|
column("#{col}_id", :integer, options)
column("#{col}_type", :string, polymorphic.is_a?(Hash) ? polymorphic : options) if polymorphic
- index(polymorphic ? %w(id type).map { |t| "#{col}_#{t}" } : "#{col}_id", index_options.is_a?(Hash) ? index_options : nil) if index_options
+ index(polymorphic ? %w(id type).map { |t| "#{col}_#{t}" } : "#{col}_id", index_options.is_a?(Hash) ? index_options : {}) if index_options
end
end
alias :belongs_to :references
- # Returns a String whose contents are the column definitions
- # concatenated together. This string can then be prepended and appended to
- # to generate the final SQL to create the table.
- def to_sql
- @columns.map { |c| c.to_sql } * ', '
+ def new_column_definition(name, type, options) # :nodoc:
+ column = create_column_definition name, type
+ limit = options.fetch(:limit) do
+ native[type][:limit] if native[type].is_a?(Hash)
+ end
+
+ column.limit = limit
+ column.array = options[:array] if column.respond_to?(:array)
+ column.precision = options[:precision]
+ column.scale = options[:scale]
+ column.default = options[:default]
+ column.null = options[:null]
+ column.first = options[:first]
+ column.after = options[:after]
+ column.primary_key = type == :primary_key || options[:primary_key]
+ column
end
private
- def new_column_definition(base, name, type)
- definition = ColumnDefinition.new base, name, type
- @columns << definition
- @columns_hash[name] = definition
- definition
+ def create_column_definition(name, type)
+ ColumnDefinition.new name, type
+ end
+
+ def primary_key_column_name
+ primary_key_column = columns.detect { |c| c.primary_key? }
+ primary_key_column && primary_key_column.name
end
def native
- @base.native_database_types
+ @native
+ end
+ end
+
+ class AlterTable # :nodoc:
+ attr_reader :adds
+
+ def initialize(td)
+ @td = td
+ @adds = []
+ end
+
+ def name; @td.name; end
+
+ def add_column(name, type, options)
+ name = name.to_s
+ type = type.to_sym
+ @adds << @td.new_column_definition(name, type, options)
end
end
@@ -315,6 +320,7 @@ module ActiveRecord
# change_table :table do |t|
# t.column
# t.index
+ # t.rename_index
# t.timestamps
# t.change
# t.change_default
@@ -365,9 +371,9 @@ module ActiveRecord
# ====== Creating a simple index
# t.index(:name)
# ====== Creating a unique index
- # t.index([:branch_id, :party_id], :unique => true)
+ # t.index([:branch_id, :party_id], unique: true)
# ====== Creating a named index
- # t.index([:branch_id, :party_id], :unique => true, :name => 'by_branch_party')
+ # t.index([:branch_id, :party_id], unique: true, name: 'by_branch_party')
def index(column_name, options = {})
@base.add_index(@table_name, column_name, options)
end
@@ -377,6 +383,13 @@ module ActiveRecord
@base.index_exists?(@table_name, column_name, options)
end
+ # Renames the given index on the table.
+ #
+ # t.rename_index(:user_id, :account_id)
+ def rename_index(index_name, new_index_name)
+ @base.rename_index(@table_name, index_name, new_index_name)
+ end
+
# Adds timestamps (+created_at+ and +updated_at+) columns to the table. See SchemaStatements#add_timestamps
#
# t.timestamps
@@ -387,7 +400,7 @@ module ActiveRecord
# Changes the column's definition according to the new options.
# See TableDefinition#column for details of the options you can use.
#
- # t.change(:name, :string, :limit => 80)
+ # t.change(:name, :string, limit: 80)
# t.change(:description, :text)
def change(column_name, type, options = {})
@base.change_column(@table_name, column_name, type, options)
@@ -406,7 +419,7 @@ module ActiveRecord
# t.remove(:qualification)
# t.remove(:qualification, :experience)
def remove(*column_names)
- @base.remove_column(@table_name, *column_names)
+ @base.remove_columns(@table_name, *column_names)
end
# Removes the given index from the table.
@@ -414,11 +427,11 @@ module ActiveRecord
# ====== Remove the index_table_name_on_column in the table_name table
# t.remove_index :column
# ====== Remove the index named index_table_name_on_branch_id in the table_name table
- # t.remove_index :column => :branch_id
+ # t.remove_index column: :branch_id
# ====== Remove the index named index_table_name_on_branch_id_and_party_id in the table_name table
- # t.remove_index :column => [:branch_id, :party_id]
+ # t.remove_index column: [:branch_id, :party_id]
# ====== Remove the index named by_branch_party in the table_name table
- # t.remove_index :name => :by_branch_party
+ # t.remove_index name: :by_branch_party
def remove_index(options = {})
@base.remove_index(@table_name, options)
end
@@ -469,27 +482,13 @@ module ActiveRecord
#
# t.string(:goat)
# t.string(:goat, :sheep)
- %w( string text integer float decimal datetime timestamp time date binary boolean ).each do |column_type|
- class_eval <<-EOV, __FILE__, __LINE__ + 1
- def #{column_type}(*args) # def string(*args)
- options = args.extract_options! # options = args.extract_options!
- column_names = args # column_names = args
- type = :'#{column_type}' # type = :string
- column_names.each do |name| # column_names.each do |name|
- column = ColumnDefinition.new(@base, name.to_s, type) # column = ColumnDefinition.new(@base, name, type)
- if options[:limit] # if options[:limit]
- column.limit = options[:limit] # column.limit = options[:limit]
- elsif native[type].is_a?(Hash) # elsif native[type].is_a?(Hash)
- column.limit = native[type][:limit] # column.limit = native[type][:limit]
- end # end
- column.precision = options[:precision] # column.precision = options[:precision]
- column.scale = options[:scale] # column.scale = options[:scale]
- column.default = options[:default] # column.default = options[:default]
- column.null = options[:null] # column.null = options[:null]
- @base.add_column(@table_name, name, column.sql_type, options) # @base.add_column(@table_name, name, column.sql_type, options)
- end # end
- end # end
- EOV
+ [:string, :text, :integer, :float, :decimal, :datetime, :timestamp, :time, :date, :binary, :boolean].each do |column_type|
+ define_method column_type do |*args|
+ options = args.extract_options!
+ args.each do |name|
+ @base.add_column(@table_name, name, column_type, options)
+ end
+ end
end
private
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb
index 9d6111b51e..cdf0cbe218 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb
@@ -1,10 +1,12 @@
+require 'ipaddr'
+
module ActiveRecord
module ConnectionAdapters # :nodoc:
# The goal of this module is to move Adapter specific column
# definitions to the Adapter instead of having it in the schema
# dumper itself. This code represents the normal case.
- # We can then redefine how certain data types may be handled in the schema dumper on the
- # Adapter level by over-writing this code inside the database spececific adapters
+ # We can then redefine how certain data types may be handled in the schema dumper on the
+ # Adapter level by over-writing this code inside the database specific adapters
module ColumnDumper
def column_spec(column, types)
spec = prepare_column_options(column, types)
@@ -47,6 +49,18 @@ module ActiveRecord
value.to_s
when Date, DateTime, Time
"'#{value.to_s(:db)}'"
+ when Range
+ # infinity dumps as Infinity, which causes uninitialized constant error
+ value.inspect.gsub('Infinity', '::Float::INFINITY')
+ when IPAddr
+ subnet_mask = value.instance_variable_get(:@mask_addr)
+
+ # If the subnet mask is equal to /32, don't output it
+ if subnet_mask == (2**32 - 1)
+ "\"#{value.to_s}\""
+ else
+ "\"#{value.to_s}/#{subnet_mask.to_s(2).count('1')}\""
+ end
else
value.inspect
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
index 86d6266af9..4b425494d0 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
@@ -5,7 +5,7 @@ module ActiveRecord
module SchemaStatements
include ActiveRecord::Migration::JoinTable
- # Returns a Hash of mappings from the abstract data types to the native
+ # Returns a hash of mappings from the abstract data types to the native
# database types. See TableDefinition#column for details on the recognized
# abstract data types.
def native_database_types
@@ -20,6 +20,7 @@ module ActiveRecord
# Checks to see if the table +table_name+ exists on the database.
#
# table_exists?(:developers)
+ #
def table_exists?(table_name)
tables.include?(table_name.to_s)
end
@@ -29,17 +30,18 @@ module ActiveRecord
# Checks to see if an index exists on a table for a given index definition.
#
- # # Check an index exists
- # index_exists?(:suppliers, :company_id)
+ # # Check an index exists
+ # index_exists?(:suppliers, :company_id)
+ #
+ # # Check an index on multiple columns exists
+ # index_exists?(:suppliers, [:company_id, :company_type])
#
- # # Check an index on multiple columns exists
- # index_exists?(:suppliers, [:company_id, :company_type])
+ # # Check a unique index exists
+ # index_exists?(:suppliers, :company_id, unique: true)
#
- # # Check a unique index exists
- # index_exists?(:suppliers, :company_id, :unique => true)
+ # # Check an index with a custom name exists
+ # index_exists?(:suppliers, :company_id, name: "idx_company_id"
#
- # # Check an index with a custom name exists
- # index_exists?(:suppliers, :company_id, :name => "idx_company_id"
def index_exists?(table_name, column_name, options = {})
column_names = Array(column_name)
index_name = options.key?(:name) ? options[:name].to_s : index_name(table_name, :column => column_names)
@@ -56,17 +58,18 @@ module ActiveRecord
# Checks to see if a column exists in a given table.
#
- # # Check a column exists
- # column_exists?(:suppliers, :name)
+ # # Check a column exists
+ # column_exists?(:suppliers, :name)
+ #
+ # # Check a column exists of a particular type
+ # column_exists?(:suppliers, :name, :string)
#
- # # Check a column exists of a particular type
- # column_exists?(:suppliers, :name, :string)
+ # # Check a column exists with a specific definition
+ # column_exists?(:suppliers, :name, :string, limit: 100)
+ # column_exists?(:suppliers, :name, :string, default: 'default')
+ # column_exists?(:suppliers, :name, :string, null: false)
+ # column_exists?(:suppliers, :tax, :decimal, precision: 8, scale: 2)
#
- # # Check a column exists with a specific definition
- # column_exists?(:suppliers, :name, :string, limit: 100)
- # column_exists?(:suppliers, :name, :string, default: 'default')
- # column_exists?(:suppliers, :name, :string, null: false)
- # column_exists?(:suppliers, :tax, :decimal, precision: 8, scale: 2)
def column_exists?(table_name, column_name, type = nil, options = {})
columns(table_name).any?{ |c| c.name == column_name.to_s &&
(!type || c.type == type) &&
@@ -84,27 +87,30 @@ module ActiveRecord
# form or the regular form, like this:
#
# === Block form
- # # create_table() passes a TableDefinition object to the block.
- # # This form will not only create the table, but also columns for the
- # # table.
#
- # create_table(:suppliers) do |t|
- # t.column :name, :string, :limit => 60
- # # Other fields here
- # end
+ # # create_table() passes a TableDefinition object to the block.
+ # # This form will not only create the table, but also columns for the
+ # # table.
+ #
+ # create_table(:suppliers) do |t|
+ # t.column :name, :string, limit: 60
+ # # Other fields here
+ # end
#
# === Block form, with shorthand
- # # You can also use the column types as method calls, rather than calling the column method.
- # create_table(:suppliers) do |t|
- # t.string :name, :limit => 60
- # # Other fields here
- # end
+ #
+ # # You can also use the column types as method calls, rather than calling the column method.
+ # create_table(:suppliers) do |t|
+ # t.string :name, limit: 60
+ # # Other fields here
+ # end
#
# === Regular form
- # # Creates a table called 'suppliers' with no columns.
- # create_table(:suppliers)
- # # Add a column to 'suppliers'.
- # add_column(:suppliers, :name, :string, {:limit => 60})
+ #
+ # # Creates a table called 'suppliers' with no columns.
+ # create_table(:suppliers)
+ # # Add a column to 'suppliers'.
+ # add_column(:suppliers, :name, :string, {limit: 60})
#
# The +options+ hash can include the following keys:
# [<tt>:id</tt>]
@@ -127,37 +133,53 @@ module ActiveRecord
# Defaults to false.
#
# ====== Add a backend specific option to the generated SQL (MySQL)
- # create_table(:suppliers, :options => 'ENGINE=InnoDB DEFAULT CHARSET=utf8')
+ #
+ # create_table(:suppliers, options: 'ENGINE=InnoDB DEFAULT CHARSET=utf8')
+ #
# generates:
- # CREATE TABLE suppliers (
- # id int(11) DEFAULT NULL auto_increment PRIMARY KEY
- # ) ENGINE=InnoDB DEFAULT CHARSET=utf8
+ #
+ # CREATE TABLE suppliers (
+ # id int(11) DEFAULT NULL auto_increment PRIMARY KEY
+ # ) ENGINE=InnoDB DEFAULT CHARSET=utf8
#
# ====== Rename the primary key column
- # create_table(:objects, :primary_key => 'guid') do |t|
- # t.column :name, :string, :limit => 80
- # end
+ #
+ # create_table(:objects, primary_key: 'guid') do |t|
+ # t.column :name, :string, limit: 80
+ # end
+ #
# generates:
- # CREATE TABLE objects (
- # guid int(11) DEFAULT NULL auto_increment PRIMARY KEY,
- # name varchar(80)
- # )
+ #
+ # CREATE TABLE objects (
+ # guid int(11) DEFAULT NULL auto_increment PRIMARY KEY,
+ # name varchar(80)
+ # )
#
# ====== Do not add a primary key column
- # create_table(:categories_suppliers, :id => false) do |t|
- # t.column :category_id, :integer
- # t.column :supplier_id, :integer
- # end
+ #
+ # create_table(:categories_suppliers, id: false) do |t|
+ # t.column :category_id, :integer
+ # t.column :supplier_id, :integer
+ # end
+ #
# generates:
- # CREATE TABLE categories_suppliers (
- # category_id int,
- # supplier_id int
- # )
+ #
+ # CREATE TABLE categories_suppliers (
+ # category_id int,
+ # supplier_id int
+ # )
#
# See also TableDefinition#column for details on how to create columns.
def create_table(table_name, options = {})
- td = table_definition
- td.primary_key(options[:primary_key] || Base.get_primary_key(table_name.to_s.singularize)) unless options[:id] == false
+ td = create_table_definition table_name, options[:temporary], options[:options]
+
+ 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
yield td if block_given?
@@ -165,19 +187,15 @@ module ActiveRecord
drop_table(table_name, options)
end
- create_sql = "CREATE#{' TEMPORARY' if options[:temporary]} TABLE "
- create_sql << "#{quote_table_name(table_name)} ("
- create_sql << td.to_sql
- create_sql << ") #{options[:options]}"
- execute create_sql
+ execute schema_creation.accept td
td.indexes.each_pair { |c,o| add_index table_name, c, o }
end
# Creates a new join table with the name created using the lexical order of the first two
# arguments. These arguments can be a String or a Symbol.
#
- # # Creates a table called 'assemblies_parts' with no id.
- # create_join_table(:assemblies, :parts)
+ # # Creates a table called 'assemblies_parts' with no id.
+ # create_join_table(:assemblies, :parts)
#
# You can pass a +options+ hash can include the following keys:
# [<tt>:table_name</tt>]
@@ -192,13 +210,25 @@ module ActiveRecord
# Set to true to drop the table before creating it.
# Defaults to false.
#
+ # Note that +create_join_table+ does not create any indices by default; you can use
+ # its block form to do so yourself:
+ #
+ # create_join_table :products, :categories do |t|
+ # t.index :product_id
+ # t.index :category_id
+ # end
+ #
# ====== Add a backend specific option to the generated SQL (MySQL)
- # create_join_table(:assemblies, :parts, :options => 'ENGINE=InnoDB DEFAULT CHARSET=utf8')
+ #
+ # create_join_table(:assemblies, :parts, options: 'ENGINE=InnoDB DEFAULT CHARSET=utf8')
+ #
# generates:
- # CREATE TABLE assemblies_parts (
- # assembly_id int NOT NULL,
- # part_id int NOT NULL,
- # ) ENGINE=InnoDB DEFAULT CHARSET=utf8
+ #
+ # CREATE TABLE assemblies_parts (
+ # assembly_id int NOT NULL,
+ # part_id int NOT NULL,
+ # ) ENGINE=InnoDB DEFAULT CHARSET=utf8
+ #
def create_join_table(table_1, table_2, options = {})
join_table_name = find_join_table_name(table_1, table_2, options)
@@ -214,86 +244,110 @@ module ActiveRecord
end
end
+ # Drops the join table specified by the given arguments.
+ # See +create_join_table+ for details.
+ #
+ # Although this command ignores the block if one is given, it can be helpful
+ # to provide one in a migration's +change+ method so it can be reverted.
+ # In that case, the block will be used by create_join_table.
+ def drop_join_table(table_1, table_2, options = {})
+ join_table_name = find_join_table_name(table_1, table_2, options)
+ drop_table(join_table_name)
+ end
+
# A block for changing columns in +table+.
#
- # # change_table() yields a Table instance
- # change_table(:suppliers) do |t|
- # t.column :name, :string, :limit => 60
- # # Other column alterations here
- # end
+ # # change_table() yields a Table instance
+ # change_table(:suppliers) do |t|
+ # t.column :name, :string, limit: 60
+ # # Other column alterations here
+ # end
#
# The +options+ hash can include the following keys:
# [<tt>:bulk</tt>]
# Set this to true to make this a bulk alter query, such as
- # ALTER TABLE `users` ADD COLUMN age INT(11), ADD COLUMN birthdate DATETIME ...
+ #
+ # ALTER TABLE `users` ADD COLUMN age INT(11), ADD COLUMN birthdate DATETIME ...
#
# Defaults to false.
#
# ====== Add a column
- # change_table(:suppliers) do |t|
- # t.column :name, :string, :limit => 60
- # end
+ #
+ # change_table(:suppliers) do |t|
+ # t.column :name, :string, limit: 60
+ # end
#
# ====== Add 2 integer columns
- # change_table(:suppliers) do |t|
- # t.integer :width, :height, :null => false, :default => 0
- # end
+ #
+ # change_table(:suppliers) do |t|
+ # t.integer :width, :height, null: false, default: 0
+ # end
#
# ====== Add created_at/updated_at columns
- # change_table(:suppliers) do |t|
- # t.timestamps
- # end
+ #
+ # change_table(:suppliers) do |t|
+ # t.timestamps
+ # end
#
# ====== Add a foreign key column
- # change_table(:suppliers) do |t|
- # t.references :company
- # end
#
- # Creates a <tt>company_id(integer)</tt> column
+ # change_table(:suppliers) do |t|
+ # t.references :company
+ # end
+ #
+ # Creates a <tt>company_id(integer)</tt> column.
#
# ====== Add a polymorphic foreign key column
+ #
# change_table(:suppliers) do |t|
- # t.belongs_to :company, :polymorphic => true
+ # t.belongs_to :company, polymorphic: true
# end
#
- # Creates <tt>company_type(varchar)</tt> and <tt>company_id(integer)</tt> columns
+ # Creates <tt>company_type(varchar)</tt> and <tt>company_id(integer)</tt> columns.
#
# ====== Remove a column
+ #
# change_table(:suppliers) do |t|
# t.remove :company
# end
#
# ====== Remove several columns
+ #
# change_table(:suppliers) do |t|
# t.remove :company_id
# t.remove :width, :height
# end
#
# ====== Remove an index
+ #
# change_table(:suppliers) do |t|
# t.remove_index :company_id
# end
#
- # See also Table for details on
- # all of the various column transformation
+ # See also Table for details on all of the various column transformation.
def change_table(table_name, options = {})
if supports_bulk_alter? && options[:bulk]
recorder = ActiveRecord::Migration::CommandRecorder.new(self)
- yield Table.new(table_name, recorder)
+ yield update_table_definition(table_name, recorder)
bulk_change_table(table_name, recorder.commands)
else
- yield Table.new(table_name, self)
+ yield update_table_definition(table_name, self)
end
end
# Renames a table.
#
- # rename_table('octopuses', 'octopi')
+ # rename_table('octopuses', 'octopi')
+ #
def rename_table(table_name, new_name)
raise NotImplementedError, "rename_table is not implemented"
end
# Drops a table from the database.
+ #
+ # Although this command ignores +options+ and the block if one is given, it can be helpful
+ # to provide these in a migration's +change+ method so it can be reverted.
+ # In that case, +options+ and the block will be used by create_table.
def drop_table(table_name, options = {})
execute "DROP TABLE #{quote_table_name(table_name)}"
end
@@ -301,41 +355,80 @@ module ActiveRecord
# Adds a new column to the named table.
# See TableDefinition#column for details of the options you can use.
def add_column(table_name, column_name, type, options = {})
- add_column_sql = "ALTER TABLE #{quote_table_name(table_name)} ADD #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
- add_column_options!(add_column_sql, options)
- execute(add_column_sql)
+ at = create_alter_table table_name
+ at.add_column(column_name, type, options)
+ execute schema_creation.accept at
+ end
+
+ # Removes the given columns from the table definition.
+ #
+ # remove_columns(:suppliers, :qualification, :experience)
+ #
+ def remove_columns(table_name, *column_names)
+ raise ArgumentError.new("You must specify at least one column name. Example: remove_columns(:people, :first_name)") if column_names.empty?
+ column_names.each do |column_name|
+ remove_column(table_name, column_name)
+ end
end
- # Removes the column(s) from the table definition.
+ # Removes the column from the table definition.
+ #
+ # remove_column(:suppliers, :qualification)
#
- # remove_column(:suppliers, :qualification)
- # remove_columns(:suppliers, :qualification, :experience)
- def remove_column(table_name, *column_names)
- columns_for_remove(table_name, *column_names).each {|column_name| execute "ALTER TABLE #{quote_table_name(table_name)} DROP #{column_name}" }
+ # The +type+ and +options+ parameters will be ignored if present. It can be helpful
+ # to provide these in a migration's +change+ method so it can be reverted.
+ # In that case, +type+ and +options+ will be used by add_column.
+ def remove_column(table_name, column_name, type = nil, options = {})
+ execute "ALTER TABLE #{quote_table_name(table_name)} DROP #{quote_column_name(column_name)}"
end
- alias :remove_columns :remove_column
# Changes the column's definition according to the new options.
# See TableDefinition#column for details of the options you can use.
#
- # change_column(:suppliers, :name, :string, :limit => 80)
- # change_column(:accounts, :description, :text)
+ # change_column(:suppliers, :name, :string, limit: 80)
+ # change_column(:accounts, :description, :text)
+ #
def change_column(table_name, column_name, type, options = {})
raise NotImplementedError, "change_column is not implemented"
end
- # Sets a new default value for a column.
+ # Sets a new default value for a column:
+ #
+ # change_column_default(:suppliers, :qualification, 'new')
+ # change_column_default(:accounts, :authorized, 1)
+ #
+ # Setting the default to +nil+ effectively drops the default:
+ #
+ # change_column_default(:users, :email, nil)
#
- # change_column_default(:suppliers, :qualification, 'new')
- # change_column_default(:accounts, :authorized, 1)
- # change_column_default(:users, :email, nil)
def change_column_default(table_name, column_name, default)
raise NotImplementedError, "change_column_default is not implemented"
end
+ # Sets or removes a +NOT NULL+ constraint on a column. The +null+ flag
+ # indicates whether the value can be +NULL+. For example
+ #
+ # change_column_null(:users, :nickname, false)
+ #
+ # says nicknames cannot be +NULL+ (adds the constraint), whereas
+ #
+ # change_column_null(:users, :nickname, true)
+ #
+ # allows them to be +NULL+ (drops the constraint).
+ #
+ # The method accepts an optional fourth argument to replace existing
+ # +NULL+s with some other value. Use that one when enabling the
+ # constraint if needed, since otherwise those rows would not be valid.
+ #
+ # Please note the fourth argument does not set a column's default.
+ def change_column_null(table_name, column_name, null, default = nil)
+ raise NotImplementedError, "change_column_null is not implemented"
+ end
+
# Renames a column.
#
- # rename_column(:suppliers, :description, :name)
+ # rename_column(:suppliers, :description, :name)
+ #
def rename_column(table_name, column_name, new_column_name)
raise NotImplementedError, "rename_column is not implemented"
end
@@ -347,60 +440,106 @@ module ActiveRecord
# you pass <tt>:name</tt> as an option.
#
# ====== Creating a simple index
- # add_index(:suppliers, :name)
- # generates
- # CREATE INDEX suppliers_name_index ON suppliers(name)
+ #
+ # add_index(:suppliers, :name)
+ #
+ # generates:
+ #
+ # CREATE INDEX suppliers_name_index ON suppliers(name)
#
# ====== Creating a unique index
- # add_index(:accounts, [:branch_id, :party_id], :unique => true)
- # generates
- # CREATE UNIQUE INDEX accounts_branch_id_party_id_index ON accounts(branch_id, party_id)
+ #
+ # add_index(:accounts, [:branch_id, :party_id], unique: true)
+ #
+ # generates:
+ #
+ # CREATE UNIQUE INDEX accounts_branch_id_party_id_index ON accounts(branch_id, party_id)
#
# ====== Creating a named index
- # add_index(:accounts, [:branch_id, :party_id], :unique => true, :name => 'by_branch_party')
- # generates
+ #
+ # add_index(:accounts, [:branch_id, :party_id], unique: true, name: 'by_branch_party')
+ #
+ # generates:
+ #
# CREATE UNIQUE INDEX by_branch_party ON accounts(branch_id, party_id)
#
# ====== Creating an index with specific key length
- # add_index(:accounts, :name, :name => 'by_name', :length => 10)
- # generates
- # CREATE INDEX by_name ON accounts(name(10))
#
- # add_index(:accounts, [:name, :surname], :name => 'by_name_surname', :length => {:name => 10, :surname => 15})
- # generates
- # CREATE INDEX by_name_surname ON accounts(name(10), surname(15))
+ # add_index(:accounts, :name, name: 'by_name', length: 10)
+ #
+ # generates:
#
- # Note: SQLite doesn't support index length
+ # CREATE INDEX by_name ON accounts(name(10))
+ #
+ # add_index(:accounts, [:name, :surname], name: 'by_name_surname', length: {name: 10, surname: 15})
+ #
+ # generates:
+ #
+ # CREATE INDEX by_name_surname ON accounts(name(10), surname(15))
+ #
+ # Note: SQLite doesn't support index length.
#
# ====== Creating an index with a sort order (desc or asc, asc is the default)
- # add_index(:accounts, [:branch_id, :party_id, :surname], :order => {:branch_id => :desc, :party_id => :asc})
- # generates
- # CREATE INDEX by_branch_desc_party ON accounts(branch_id DESC, party_id ASC, surname)
#
- # Note: mysql doesn't yet support index order (it accepts the syntax but ignores it)
+ # add_index(:accounts, [:branch_id, :party_id, :surname], order: {branch_id: :desc, party_id: :asc})
+ #
+ # generates:
+ #
+ # CREATE INDEX by_branch_desc_party ON accounts(branch_id DESC, party_id ASC, surname)
+ #
+ # Note: MySQL doesn't yet support index order (it accepts the syntax but ignores it).
#
# ====== Creating a partial index
- # add_index(:accounts, [:branch_id, :party_id], :unique => true, :where => "active")
- # generates
- # CREATE UNIQUE INDEX index_accounts_on_branch_id_and_party_id ON accounts(branch_id, party_id) WHERE active
#
- # Note: only supported by PostgreSQL
+ # add_index(:accounts, [:branch_id, :party_id], unique: true, where: "active")
#
+ # generates:
+ #
+ # CREATE UNIQUE INDEX index_accounts_on_branch_id_and_party_id ON accounts(branch_id, party_id) WHERE active
+ #
+ # ====== Creating an index with a specific method
+ #
+ # add_index(:developers, :name, using: 'btree')
+ #
+ # generates:
+ #
+ # CREATE INDEX index_developers_on_name ON developers USING btree (name) -- PostgreSQL
+ # CREATE INDEX index_developers_on_name USING btree ON developers (name) -- MySQL
+ #
+ # Note: only supported by PostgreSQL and MySQL
+ #
+ # ====== Creating an index with a specific type
+ #
+ # add_index(:developers, :name, type: :fulltext)
+ #
+ # generates:
+ #
+ # CREATE FULLTEXT INDEX index_developers_on_name ON developers (name) -- MySQL
+ #
+ # Note: only supported by MySQL. Supported: <tt>:fulltext</tt> and <tt>:spatial</tt> on MyISAM tables.
def add_index(table_name, column_name, options = {})
index_name, index_type, index_columns, index_options = add_index_options(table_name, column_name, options)
execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} (#{index_columns})#{index_options}"
end
- # Remove the given index from the table.
+ # Removes the given index from the table.
+ #
+ # Removes the +index_accounts_on_column+ in the +accounts+ table.
#
- # Remove the index_accounts_on_column in the accounts table.
# remove_index :accounts, :column
- # Remove the index named index_accounts_on_branch_id in the accounts table.
- # remove_index :accounts, :column => :branch_id
- # Remove the index named index_accounts_on_branch_id_and_party_id in the accounts table.
- # remove_index :accounts, :column => [:branch_id, :party_id]
- # Remove the index named by_branch_party in the accounts table.
- # remove_index :accounts, :name => :by_branch_party
+ #
+ # Removes the index named +index_accounts_on_branch_id+ in the +accounts+ table.
+ #
+ # remove_index :accounts, column: :branch_id
+ #
+ # Removes the index named +index_accounts_on_branch_id_and_party_id+ in the +accounts+ table.
+ #
+ # remove_index :accounts, column: [:branch_id, :party_id]
+ #
+ # Removes the index named +by_branch_party+ in the +accounts+ table.
+ #
+ # remove_index :accounts, name: :by_branch_party
+ #
def remove_index(table_name, options = {})
remove_index!(table_name, index_name_for_remove(table_name, options))
end
@@ -409,10 +548,12 @@ module ActiveRecord
execute "DROP INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)}"
end
- # Rename an index.
+ # Renames an index.
+ #
+ # Rename the +index_people_on_last_name+ index to +index_users_on_last_name+:
#
- # Rename the index_people_on_last_name index to index_users_on_last_name
# rename_index :people, 'index_people_on_last_name', 'index_users_on_last_name'
+ #
def rename_index(table_name, old_name, new_name)
# this is a naive implementation; some DBs may support this more efficiently (Postgres, for instance)
old_index_def = indexes(table_name).detect { |i| i.name == old_name }
@@ -422,7 +563,7 @@ module ActiveRecord
end
def index_name(table_name, options) #:nodoc:
- if Hash === options # legacy support
+ if Hash === options
if options[:column]
"index_#{table_name}_on_#{Array(options[:column]) * '_and_'}"
elsif options[:name]
@@ -435,7 +576,7 @@ module ActiveRecord
end
end
- # Verify the existence of an index with a given name.
+ # Verifies the existence of an index with a given name.
#
# The default argument is returned if the underlying implementation does not define the indexes method,
# as there's no way to determine the correct answer in that case.
@@ -449,20 +590,23 @@ module ActiveRecord
# <tt>add_reference</tt> and <tt>add_belongs_to</tt> are acceptable.
#
# ====== Create a user_id column
- # add_reference(:products, :user)
+ #
+ # add_reference(:products, :user)
#
# ====== Create a supplier_id and supplier_type columns
- # add_belongs_to(:products, :supplier, polymorphic: true)
+ #
+ # add_belongs_to(:products, :supplier, polymorphic: true)
#
# ====== Create a supplier_id, supplier_type columns and appropriate index
- # add_reference(:products, :supplier, polymorphic: true, index: true)
+ #
+ # add_reference(:products, :supplier, polymorphic: true, index: true)
#
def add_reference(table_name, ref_name, options = {})
polymorphic = options.delete(:polymorphic)
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
@@ -470,10 +614,12 @@ module ActiveRecord
# <tt>remove_reference</tt>, <tt>remove_references</tt> and <tt>remove_belongs_to</tt> are acceptable.
#
# ====== Remove the reference
- # remove_reference(:products, :user, index: true)
+ #
+ # remove_reference(:products, :user, index: true)
#
# ====== Remove polymorphic reference
- # remove_reference(:products, :supplier, polymorphic: true)
+ #
+ # remove_reference(:products, :supplier, polymorphic: true)
#
def remove_reference(table_name, ref_name, options = {})
remove_column(table_name, "#{ref_name}_id")
@@ -481,11 +627,6 @@ module ActiveRecord
end
alias :remove_belongs_to :remove_reference
- # Returns a string of <tt>CREATE TABLE</tt> SQL statement(s) for recreating the
- # entire structure of the database.
- def structure_dump
- end
-
def dump_schema_information #:nodoc:
sm_table = ActiveRecord::Migrator.schema_migrations_table_name
@@ -540,7 +681,7 @@ module ActiveRecord
column_type_sql << "(#{precision})"
end
elsif scale
- raise ArgumentError, "Error adding decimal column: precision cannot be empty if scale if specified"
+ raise ArgumentError, "Error adding decimal column: precision cannot be empty if scale is specified"
end
elsif (type != :primary_key) && (limit ||= native.is_a?(Hash) && native[:limit])
@@ -553,33 +694,28 @@ module ActiveRecord
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"
- end
- end
-
- # SELECT DISTINCT clause for a given set of columns and a given ORDER BY clause.
- # Both PostgreSQL and Oracle overrides this for custom DISTINCT syntax.
+ # Given a set of columns and an ORDER BY clause, returns the columns for a SELECT DISTINCT.
+ # Both PostgreSQL and Oracle overrides this for custom DISTINCT syntax - they
+ # require the order columns appear in the SELECT.
#
- # distinct("posts.id", "posts.created_at desc")
- def distinct(columns, order_by)
- "DISTINCT #{columns}"
+ # columns_for_distinct("posts.id", ["posts.created_at desc"])
+ def columns_for_distinct(columns, orders) # :nodoc:
+ columns
end
- # Adds timestamps (created_at and updated_at) columns to the named table.
+ # Adds timestamps (+created_at+ and +updated_at+) columns to the named table.
+ #
+ # add_timestamps(:suppliers)
#
- # add_timestamps(:suppliers)
def add_timestamps(table_name)
add_column table_name, :created_at, :datetime
add_column table_name, :updated_at, :datetime
end
- # Removes the timestamp columns (created_at and updated_at) from the table definition.
+ # Removes the timestamp columns (+created_at+ and +updated_at+) from the table definition.
#
# remove_timestamps(:suppliers)
+ #
def remove_timestamps(table_name)
remove_column table_name, :updated_at
remove_column table_name, :created_at
@@ -617,47 +753,89 @@ module ActiveRecord
def add_index_options(table_name, column_name, options = {})
column_names = Array(column_name)
- index_name = index_name(table_name, :column => column_names)
+ index_name = index_name(table_name, column: column_names)
- if Hash === options # legacy support, since this param was a string
- index_type = options[:unique] ? "UNIQUE" : ""
- index_name = options[:name].to_s if options.key?(:name)
- if supports_partial_index?
- index_options = options[:where] ? " WHERE #{options[:where]}" : ""
- end
- else
- index_type = options
+ 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
+
+ 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
- if index_name.length > index_name_length
- raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' is too long; the limit is #{index_name_length} characters"
+ if index_name.length > max_index_length
+ raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' is too long; the limit is #{max_index_length} characters"
end
if index_name_exists?(table_name, index_name, false)
raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' already exists"
end
index_columns = quoted_columns_for_index(column_names, options).join(", ")
- [index_name, index_type, index_columns, index_options]
+ [index_name, index_type, index_columns, index_options, algorithm, using]
end
def index_name_for_remove(table_name, options = {})
index_name = index_name(table_name, options)
unless index_name_exists?(table_name, index_name, true)
+ if options.is_a?(Hash) && options.has_key?(:name)
+ options_without_column = options.dup
+ options_without_column.delete :column
+ index_name_without_column = index_name(table_name, options_without_column)
+
+ return index_name_without_column if index_name_exists?(table_name, index_name_without_column, false)
+ end
+
raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' does not exist"
end
index_name
end
- def columns_for_remove(table_name, *column_names)
- raise ArgumentError.new("You must specify at least one column name. Example: remove_column(:people, :first_name)") if column_names.blank?
- column_names.map {|column_name| quote_column_name(column_name) }
+ def rename_table_indexes(table_name, new_name)
+ indexes(new_name).each do |index|
+ generated_index_name = index_name(table_name, column: index.columns)
+ if generated_index_name == index.name
+ rename_index new_name, generated_index_name, index_name(new_name, column: index.columns)
+ end
+ end
+ end
+
+ def rename_column_indexes(table_name, column_name, new_column_name)
+ column_name, new_column_name = column_name.to_s, new_column_name.to_s
+ indexes(table_name).each do |index|
+ next unless index.columns.include?(new_column_name)
+ old_columns = index.columns.dup
+ old_columns[old_columns.index(new_column_name)] = column_name
+ generated_index_name = index_name(table_name, column: old_columns)
+ if generated_index_name == index.name
+ rename_index table_name, generated_index_name, index_name(table_name, column: index.columns)
+ end
+ end
end
private
- def table_definition
- TableDefinition.new(self)
+ def create_table_definition(name, temporary, options)
+ TableDefinition.new native_database_types, name, temporary, options
+ 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
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb
index 4cca94e40b..2b6685499a 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb
@@ -5,6 +5,37 @@ module ActiveRecord
def initialize(connection)
@connection = connection
+ @state = TransactionState.new
+ end
+
+ def state
+ @state
+ end
+ end
+
+ class TransactionState
+ attr_accessor :parent
+
+ VALID_STATES = Set.new([:committed, :rolledback, nil])
+
+ def initialize(state = nil)
+ @state = state
+ @parent = nil
+ end
+
+ def committed?
+ @state == :committed
+ end
+
+ def rolledback?
+ @state == :rolledback
+ end
+
+ def set_state(state)
+ if !VALID_STATES.include?(state)
+ raise ArgumentError, "Invalid transaction state: #{state}"
+ end
+ @state = state
end
end
@@ -47,7 +78,7 @@ module ActiveRecord
@joinable = options.fetch(:joinable, true)
end
- # This state is necesarry so that we correctly handle stuff that might
+ # This state is necessary so that we correctly handle stuff that might
# happen in a commit/rollback. But it's kinda distasteful. Maybe we can
# find a better way to structure it in the future.
def finishing?
@@ -87,10 +118,15 @@ module ActiveRecord
end
def add_record(record)
- records << record
+ if record.has_transactional_callbacks?
+ records << record
+ else
+ record.set_transaction_state(@state)
+ end
end
def rollback_records
+ @state.set_state(:rolledback)
records.uniq.each do |record|
begin
record.rolledback!(parent.closed?)
@@ -101,6 +137,7 @@ module ActiveRecord
end
def commit_records
+ @state.set_state(:committed)
records.uniq.each do |record|
begin
record.committed!
@@ -157,8 +194,9 @@ module ActiveRecord
end
def perform_commit
+ @state.set_state(:committed)
+ @state.parent = parent.state
connection.release_savepoint
- records.each { |r| parent.add_record(r) }
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 0cb219767b..cbe563676b 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
@@ -3,9 +3,8 @@ require 'bigdecimal'
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_dumper'
require 'monitor'
-require 'active_support/deprecation'
module ActiveRecord
module ConnectionAdapters # :nodoc:
@@ -19,6 +18,7 @@ module ActiveRecord
autoload :ColumnDefinition
autoload :TableDefinition
autoload :Table
+ autoload :AlterTable
end
autoload_at 'active_record/connection_adapters/abstract/connection_pool' do
@@ -33,6 +33,7 @@ module ActiveRecord
autoload :Quoting
autoload :ConnectionPool
autoload :QueryCache
+ autoload :Savepoints
end
autoload_at 'active_record/connection_adapters/abstract/transaction' do
@@ -62,12 +63,30 @@ module ActiveRecord
include MonitorMixin
include ColumnDumper
+ SIMPLE_INT = /\A\d+\z/
+
define_callbacks :checkout, :checkin
attr_accessor :visitor, :pool
attr_reader :schema_cache, :last_use, :in_use, :logger
alias :in_use? :in_use
+ def self.type_cast_config_to_integer(config)
+ if config =~ SIMPLE_INT
+ config.to_i
+ else
+ config
+ end
+ end
+
+ def self.type_cast_config_to_boolean(config)
+ if config == "false"
+ false
+ else
+ config
+ end
+ end
+
def initialize(connection, logger = nil, pool = nil) #:nodoc:
super()
@@ -77,10 +96,95 @@ 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
+
+ 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(', ')
+ 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[: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
+
+ def schema_creation
+ SchemaCreation.new self
end
def lease
@@ -101,6 +205,18 @@ module ActiveRecord
@in_use = false
end
+ def unprepared_visitor
+ self.class::BindSubstitution.new self
+ end
+
+ def unprepared_statement
+ old_prepared_statements, @prepared_statements = @prepared_statements, false
+ old_visitor, @visitor = @visitor, unprepared_visitor
+ yield
+ ensure
+ @visitor, @prepared_statements = old_visitor, old_prepared_statements
+ end
+
# Returns the human-readable name of the adapter. Use mixed case - one
# can always use downcase if needed.
def adapter_name
@@ -172,10 +288,36 @@ module ActiveRecord
false
end
+ # Does this adapter support database extensions? As of this writing only
+ # postgresql does.
+ def supports_extensions?
+ 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
+ []
+ end
+
+ # A list of index algorithms, to be filled by adapters that support them.
+ # MySQL and PostgreSQL have support for them right now.
+ def index_algorithms
+ {}
+ end
+
# 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
@@ -254,26 +396,13 @@ module ActiveRecord
@transaction.number
end
- def increment_open_transactions
- ActiveSupport::Deprecation.warn "#increment_open_transactions is deprecated and has no effect"
+ def create_savepoint(name = nil)
end
- def decrement_open_transactions
- ActiveSupport::Deprecation.warn "#decrement_open_transactions is deprecated and has no effect"
+ def rollback_to_savepoint(name = nil)
end
- def transaction_joinable=(joinable)
- ActiveSupport::Deprecation.warn "#transaction_joinable= is deprecated. Please pass the :joinable option to #begin_transaction instead."
- @transaction.joinable = joinable
- end
-
- def create_savepoint
- end
-
- def rollback_to_savepoint
- end
-
- def release_savepoint
+ def release_savepoint(name = nil)
end
def case_sensitive_modifier(node)
@@ -295,13 +424,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
@@ -312,7 +442,11 @@ module ActiveRecord
def translate_exception(exception, message)
# override in derived class
- ActiveRecord::StatementInvalid.new(message)
+ ActiveRecord::StatementInvalid.new(message, exception)
+ end
+
+ def without_prepared_statement?(binds)
+ !@prepared_statements || binds.empty?
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 8c83c4f5db..138ab811dc 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,52 @@ require 'arel/visitors/bind_visitor'
module ActiveRecord
module ConnectionAdapters
class AbstractMysqlAdapter < AbstractAdapter
+ include Savepoints
+
+ class SchemaCreation < AbstractAdapter::SchemaCreation
+
+ def visit_AddColumn(o)
+ add_column_position!(super, column_options(o))
+ end
+
+ 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 options[:after]
+ sql << " AFTER #{quote_column_name(options[:after])}"
+ end
+ sql
+ end
+ end
+
+ def schema_creation
+ SchemaCreation.new self
+ end
+
class Column < ConnectionAdapters::Column # :nodoc:
- attr_reader :collation
+ attr_reader :collation, :strict, :extra
- def initialize(name, default, sql_type = nil, null = true, collation = nil)
- super(name, default, sql_type, null)
+ def initialize(name, default, sql_type = nil, null = true, collation = nil, strict = false, extra = "")
+ @strict = strict
@collation = collation
+ @extra = extra
+ super(name, default, sql_type, null)
end
def extract_default(default)
- if sql_type =~ /blob/i || type == :text
+ if blob_or_text_column?
if default.blank?
- return null ? nil : ''
+ null || strict ? nil : ''
else
raise ArgumentError, "#{type} columns cannot have a default value: #{default.inspect}"
end
@@ -26,12 +60,12 @@ module ActiveRecord
end
def has_default?
- return false if sql_type =~ /blob/i || type == :text #mysql forbids defaults on blob and text columns
+ return false if blob_or_text_column? #mysql forbids defaults on blob and text columns
super
end
- def explicit_default?
- !null && (sql_type =~ /blob/i || type == :text)
+ def blob_or_text_column?
+ sql_type =~ /blob/i || type == :text
end
# Must return the relevant concrete adapter
@@ -59,6 +93,8 @@ module ActiveRecord
def extract_limit(sql_type)
case sql_type
+ when /^enum\((.+)\)/i
+ $1.split(',').map{|enum| enum.strip.length - 2}.max
when /blob|text/i
case sql_type
when /tiny/i
@@ -75,8 +111,6 @@ module ActiveRecord
when /^mediumint/i; 3
when /^smallint/i; 2
when /^tinyint/i; 1
- when /^enum\((.+)\)/i
- $1.split(',').map{|enum| enum.strip.length - 2}.max
else
super
end
@@ -128,6 +162,9 @@ module ActiveRecord
:boolean => { :name => "tinyint", :limit => 1 }
}
+ INDEX_TYPES = [:fulltext, :spatial]
+ INDEX_USINGS = [:btree, :hash]
+
class BindSubstitution < Arel::Visitors::MySQL # :nodoc:
include Arel::Visitors::BindVisitor
end
@@ -138,10 +175,11 @@ module ActiveRecord
@connection_options, @config = connection_options, config
@quoted_column_names, @quoted_table_names = {}, {}
- if config.fetch(:prepared_statements) { true }
+ if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true })
+ @prepared_statements = true
@visitor = Arel::Visitors::MySQL.new self
else
- @visitor = BindSubstitution.new self
+ @visitor = unprepared_visitor
end
end
@@ -158,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
@@ -185,6 +218,10 @@ module ActiveRecord
NATIVE_DATABASE_TYPES
end
+ def index_algorithms
+ { default: 'ALGORITHM = DEFAULT', copy: 'ALGORITHM = COPY', inplace: 'ALGORITHM = INPLACE' }
+ end
+
# HELPER METHODS ===========================================
# The two drivers have slightly different ways of yielding hashes of results, so
@@ -194,8 +231,8 @@ module ActiveRecord
end
# Overridden by the adapters to instantiate their specific Column type.
- def new_column(field, default, type, null, collation) # :nodoc:
- Column.new(field, default, type, null, collation)
+ def new_column(field, default, type, null, collation, extra = "") # :nodoc:
+ Column.new(field, default, type, null, collation, extra)
end
# Must return the Mysql error number from the exception, if the exception has an
@@ -207,8 +244,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")
@@ -257,7 +294,7 @@ module ActiveRecord
end
rescue ActiveRecord::StatementInvalid => exception
if exception.message.split(":").first =~ /Packets out of order/
- raise ActiveRecord::StatementInvalid, "'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."
+ 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
@@ -300,18 +337,6 @@ module ActiveRecord
# 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
# query. However, this does not allow for LIMIT, OFFSET and ORDER. To support
# these, we must use a subquery.
@@ -330,34 +355,22 @@ module ActiveRecord
# SCHEMA STATEMENTS ========================================
- def structure_dump #:nodoc:
- if supports_views?
- sql = "SHOW FULL TABLES WHERE Table_type = 'BASE TABLE'"
- else
- sql = "SHOW TABLES"
- end
-
- select_all(sql, 'SCHEMA').map { |table|
- table.delete('Table_type')
- sql = "SHOW CREATE TABLE #{quote_table_name(table.to_a.first.last)}"
- exec_query(sql, 'SCHEMA').first['Create Table'] + ";\n\n"
- }.join
- end
-
# Drops the database specified on the +name+ attribute
# and creates it again using the provided +options+.
def recreate_database(name, options = {})
drop_database(name)
- create_database(name, options)
+ sql = create_database(name, options)
+ reconnect!
+ sql
end
# Create a new MySQL database with optional <tt>:charset</tt> and <tt>:collation</tt>.
# Charset defaults to utf8.
#
# Example:
- # create_database 'charset_test', :charset => 'latin1', :collation => 'latin1_bin'
+ # create_database 'charset_test', charset: 'latin1', collation: 'latin1_bin'
# create_database 'matt_development'
- # create_database 'matt_development', :charset => :big5
+ # create_database 'matt_development', charset: :big5
def create_database(name, options = {})
if options[:collation]
execute "CREATE DATABASE `#{name}` DEFAULT CHARACTER SET `#{options[:charset] || 'utf8'}` COLLATE `#{options[:collation]}`"
@@ -422,7 +435,11 @@ module ActiveRecord
if current_index != row[:Key_name]
next if row[:Key_name] == 'PRIMARY' # skip the primary key
current_index = row[:Key_name]
- indexes << IndexDefinition.new(row[:Table], row[:Key_name], row[:Non_unique].to_i == 0, [], [])
+
+ mysql_index_type = row[:Index_type].downcase.to_sym
+ index_type = INDEX_TYPES.include?(mysql_index_type) ? mysql_index_type : nil
+ index_using = INDEX_USINGS.include?(mysql_index_type) ? mysql_index_type : nil
+ indexes << IndexDefinition.new(row[:Table], row[:Key_name], row[:Non_unique].to_i == 0, [], [], nil, nil, index_type, index_using)
end
indexes.last.columns << row[:Column_name]
@@ -438,7 +455,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_name = set_field_encoding(field[:Field])
+ new_column(field_name, field[:Default], field[:Type], field[:Null] == "YES", field[:Collation], field[:Extra])
end
end
end
@@ -468,10 +486,7 @@ module ActiveRecord
# rename_table('octopuses', 'octopi')
def rename_table(table_name, new_name)
execute "RENAME TABLE #{quote_table_name(table_name)} TO #{quote_table_name(new_name)}"
- end
-
- def add_column(table_name, column_name, type, options = {})
- execute("ALTER TABLE #{quote_table_name(table_name)} #{add_column_sql(table_name, column_name, type, options)}")
+ rename_table_indexes(table_name, new_name)
end
def change_column_default(table_name, column_name, default)
@@ -495,11 +510,24 @@ module ActiveRecord
def rename_column(table_name, column_name, new_column_name) #:nodoc:
execute("ALTER TABLE #{quote_table_name(table_name)} #{rename_column_sql(table_name, column_name, new_column_name)}")
+ rename_column_indexes(table_name, column_name, new_column_name)
+ end
+
+ def add_index(table_name, column_name, options = {}) #:nodoc:
+ index_name, index_type, index_columns, index_options, index_algorithm, index_using = add_index_options(table_name, column_name, options)
+ execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} #{index_using} ON #{quote_table_name(table_name)} (#{index_columns})#{index_options} #{index_algorithm}"
end
# Maps logical Rails types to MySQL-specific data types.
def type_to_sql(type, limit = nil, precision = nil, scale = nil)
case type.to_s
+ when 'binary'
+ case limit
+ when 0..0xfff; "varbinary(#{limit})"
+ when nil; "blob"
+ when 0x1000..0xffffffff; "blob(#{limit})"
+ else raise(ActiveRecordError, "No binary type has character length #{limit}")
+ end
when 'integer'
case limit
when 1; 'tinyint'
@@ -571,6 +599,14 @@ module ActiveRecord
where_sql
end
+ def strict_mode?
+ self.class.type_cast_config_to_boolean(@config.fetch(:strict, true))
+ end
+
+ def valid_type?(type)
+ !native_database_types[type].nil?
+ end
+
protected
# MySQL is too stupid to create a temporary table for use subquery, so we have
@@ -621,10 +657,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 = {})
@@ -638,32 +673,32 @@ 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
options[:null] = column.null
+ options[:auto_increment] = (column.extra == "auto_increment")
else
raise ActiveRecordError, "No such column: #{table_name}.#{column_name}"
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 = {})
+ "DROP #{quote_column_name(column_name)}"
end
- def remove_column_sql(table_name, *column_names)
- columns_for_remove(table_name, *column_names).map {|column_name| "DROP #{column_name}" }
+ def remove_columns_sql(table_name, *column_names)
+ column_names.map {|column_name| remove_column_sql(table_name, column_name) }
end
- alias :remove_columns_sql :remove_column
def add_index_sql(table_name, column_name, options = {})
index_name, index_type, index_columns = add_index_options(table_name, column_name, options)
@@ -695,6 +730,44 @@ module ActiveRecord
end
column
end
+
+ def configure_connection
+ variables = @config[:variables] || {}
+
+ # By default, MySQL 'where id is null' selects the last inserted id.
+ # Turn this off. http://dev.rubyonrails.org/ticket/6778
+ variables[:sql_auto_is_null] = 0
+
+ # Increase timeout so the server doesn't disconnect us.
+ wait_timeout = @config[:wait_timeout]
+ wait_timeout = 2147483 unless wait_timeout.is_a?(Fixnum)
+ variables[:wait_timeout] = self.class.type_cast_config_to_integer(wait_timeout)
+
+ # Make MySQL reject illegal values rather than truncating or blanking them, see
+ # http://dev.mysql.com/doc/refman/5.0/en/server-sql-mode.html#sqlmode_strict_all_tables
+ # If the user has provided another value for sql_mode, don't replace it.
+ if strict_mode? && !variables.has_key?(:sql_mode)
+ variables[:sql_mode] = 'STRICT_ALL_TABLES'
+ end
+
+ # NAMES does not have an equals sign, see
+ # http://dev.mysql.com/doc/refman/5.0/en/set-statement.html#id944430
+ # (trailing comma because variable_assignments will always have content)
+ encoding = "NAMES #{@config[:encoding]}, " if @config[:encoding]
+
+ # Gather up all of the SET variables...
+ variable_assignments = variables.map do |k, v|
+ if v == ':default' || v == :default
+ "@@SESSION.#{k.to_s} = DEFAULT" # Sets the value to the global or compile default
+ elsif !v.nil?
+ "@@SESSION.#{k.to_s} = #{quote(v)}"
+ end
+ # or else nil; compact to clear nils out
+ end.compact.join(', ')
+
+ # ...and send them all in one query
+ execute("SET #{encoding} #{variable_assignments}", :skip_logging)
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb
index 2028abf6f0..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.
@@ -53,10 +54,6 @@ module ActiveRecord
!default.nil?
end
- def explicit_default?
- false
- end
-
# Returns the Ruby class that corresponds to the abstract data type.
def klass
case type
@@ -78,12 +75,13 @@ module ActiveRecord
def type_cast_for_write(value)
return value unless number?
- if value == false
+ case value
+ when FalseClass
0
- elsif value == true
+ when TrueClass
1
- elsif value.is_a?(String) && value.blank?
- nil
+ when String
+ value.presence
else
value
end
@@ -98,7 +96,7 @@ module ActiveRecord
case type
when :string, :text then value
- when :integer then value.to_i
+ when :integer then klass.value_to_integer(value)
when :float then value.to_f
when :decimal then klass.value_to_decimal(value)
when :datetime, :timestamp then klass.string_to_time(value)
@@ -110,29 +108,6 @@ module ActiveRecord
end
end
- def type_cast_code(var_name)
- ActiveSupport::Deprecation.warn("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.")
-
- klass = self.class.name
-
- case type
- when :string, :text then var_name
- when :integer then "(#{var_name}.to_i)"
- 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
@@ -145,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
@@ -163,7 +128,7 @@ module ActiveRecord
def value_to_date(value)
if value.is_a?(String)
- return nil if value.blank?
+ return nil if value.empty?
fast_string_to_date(value) || fallback_string_to_date(value)
elsif value.respond_to?(:to_date)
value.to_date
@@ -174,14 +139,14 @@ module ActiveRecord
def string_to_time(string)
return string unless string.is_a?(String)
- return nil if string.blank?
+ return nil if string.empty?
fast_string_to_time(string) || fallback_string_to_time(string)
end
def string_to_dummy_time(string)
return string unless string.is_a?(String)
- return nil if string.blank?
+ return nil if string.empty?
dummy_time_string = "2000-01-01 #{string}"
@@ -194,13 +159,24 @@ module ActiveRecord
# convert something to a boolean
def value_to_boolean(value)
- if value.is_a?(String) && value.blank?
+ if value.is_a?(String) && value.empty?
nil
else
TRUE_VALUES.include?(value)
end
end
+ # Used to convert values to integer.
+ # handle the case when an integer column is used to store boolean values
+ def value_to_integer(value)
+ case value
+ when TrueClass, FalseClass
+ value ? 1 : 0
+ else
+ value.to_i rescue nil
+ end
+ end
+
# convert something to a BigDecimal
def value_to_decimal(value)
# Using .class is faster than .is_a? and
@@ -228,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.time_with_datetime_fallback(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)
@@ -257,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
@@ -297,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 b9a61f7d91..64fc9e95d8 100644
--- a/activerecord/lib/active_record/connection_adapters/connection_specification.rb
+++ b/activerecord/lib/active_record/connection_adapters/connection_specification.rb
@@ -51,10 +51,13 @@ module ActiveRecord
raise(AdapterNotSpecified, "database configuration does not specify adapter") unless spec.key?(:adapter)
+ path_to_adapter = "active_record/connection_adapters/#{spec[:adapter]}_adapter"
begin
- require "active_record/connection_adapters/#{spec[:adapter]}_adapter"
+ require path_to_adapter
+ rescue Gem::LoadError => e
+ raise Gem::LoadError, "Specified '#{spec[:adapter]}' for database adapter, but the gem is not loaded. Add `gem '#{e.name}'` to your Gemfile (and ensure its version is at the minimum required by ActiveRecord)."
rescue LoadError => e
- raise LoadError, "Please install the #{spec[:adapter]} adapter: `gem install activerecord-#{spec[:adapter]}-adapter` (#{e.message})", e.backtrace
+ 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"
@@ -72,11 +75,19 @@ module ActiveRecord
: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) }
+
if config.query
options = Hash[config.query.split("&").map{ |pair| pair.split("=") }].symbolize_keys
+
spec.merge!(options)
end
+
spec
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 328d080687..e790f731ea 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
@@ -1,19 +1,21 @@
require 'active_record/connection_adapters/abstract_mysql_adapter'
-gem 'mysql2', '~> 0.3.10'
+gem 'mysql2', '~> 0.3.13'
require 'mysql2'
module ActiveRecord
- module ConnectionHandling
+ module ConnectionHandling # :nodoc:
# Establishes a connection to the database that's used by all Active Record objects.
def mysql2_connection(config)
+ config = config.symbolize_keys
+
config[:username] = 'root' if config[:username].nil?
if Mysql2::Client.const_defined? :FOUND_ROWS
config[:flags] = Mysql2::Client::FOUND_ROWS
end
- client = Mysql2::Client.new(config.symbolize_keys)
+ 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)
end
@@ -36,6 +38,15 @@ module ActiveRecord
configure_connection
end
+ MAX_INDEX_LENGTH_FOR_UTF8MB4 = 191
+ def initialize_schema_migrations_table
+ if @config[:encoding] == 'utf8mb4'
+ ActiveRecord::SchemaMigration.create_table(MAX_INDEX_LENGTH_FOR_UTF8MB4)
+ else
+ ActiveRecord::SchemaMigration.create_table
+ end
+ end
+
def supports_explain?
true
end
@@ -52,8 +63,8 @@ module ActiveRecord
end
end
- def new_column(field, default, type, null, collation) # :nodoc:
- Column.new(field, default, type, null, collation)
+ def new_column(field, default, type, null, collation, extra = "") # :nodoc:
+ Column.new(field, default, type, null, collation, strict_mode?, extra)
end
def error_number(exception)
@@ -175,7 +186,7 @@ module ActiveRecord
# # as values.
# def select_one(sql, name = nil)
# result = execute(sql, name)
- # result.each(:as => :hash) do |r|
+ # result.each(as: :hash) do |r|
# return r
# end
# end
@@ -202,9 +213,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
@@ -216,8 +229,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
@@ -251,34 +263,16 @@ module ActiveRecord
def configure_connection
@connection.query_options.merge!(:as => :array)
-
- # By default, MySQL 'where id is null' selects the last inserted id.
- # Turn this off. http://dev.rubyonrails.org/ticket/6778
- variable_assignments = ['SQL_AUTO_IS_NULL=0']
-
- # Make MySQL reject illegal values rather than truncating or
- # blanking them. See
- # http://dev.mysql.com/doc/refman/5.5/en/server-sql-mode.html#sqlmode_strict_all_tables
- if @config.fetch(:strict, true)
- variable_assignments << "SQL_MODE='STRICT_ALL_TABLES'"
- end
-
- encoding = @config[:encoding]
-
- # make sure we set the encoding
- variable_assignments << "NAMES '#{encoding}'" if encoding
-
- # increase timeout so mysql server doesn't disconnect us
- wait_timeout = @config[:wait_timeout]
- wait_timeout = 2147483 unless wait_timeout.is_a?(Fixnum)
- variable_assignments << "@@wait_timeout = #{wait_timeout}"
-
- execute("SET #{variable_assignments.join(', ')}", :skip_logging)
+ super
end
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 0b936bbf39..88c9494fc6 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
@@ -2,7 +2,7 @@ require 'active_record/connection_adapters/abstract_mysql_adapter'
require 'active_record/connection_adapters/statement_pool'
require 'active_support/core_ext/hash/keys'
-gem 'mysql', '~> 2.8.1'
+gem 'mysql', '~> 2.9'
require 'mysql'
class Mysql
@@ -16,9 +16,9 @@ class Mysql
end
module ActiveRecord
- module ConnectionHandling
+ module ConnectionHandling # :nodoc:
# Establishes a connection to the database that's used by all Active Record objects.
- def mysql_connection(config) # :nodoc:
+ def mysql_connection(config)
config = config.symbolize_keys
host = config[:host]
port = config[:port]
@@ -51,7 +51,8 @@ module ActiveRecord
# * <tt>:database</tt> - The name of the database. No default, must be provided.
# * <tt>:encoding</tt> - (Optional) Sets the client encoding by executing "SET NAMES <encoding>" after connection.
# * <tt>:reconnect</tt> - Defaults to false (See MySQL documentation: http://dev.mysql.com/doc/refman/5.0/en/auto-reconnect.html).
- # * <tt>:strict</tt> - Defaults to true. Enable STRICT_ALL_TABLES. (See MySQL documentation: http://dev.mysql.com/doc/refman/5.5/en/server-sql-mode.html)
+ # * <tt>:strict</tt> - Defaults to true. Enable STRICT_ALL_TABLES. (See MySQL documentation: http://dev.mysql.com/doc/refman/5.0/en/server-sql-mode.html)
+ # * <tt>:variables</tt> - (Optional) A hash session variables to send as `SET @@SESSION.key = value` on each database connection. Use the value `:default` to set a variable to its DEFAULT value. (See MySQL documentation: http://dev.mysql.com/doc/refman/5.0/en/set-statement.html).
# * <tt>:sslca</tt> - Necessary to use MySQL with an SSL connection.
# * <tt>:sslkey</tt> - Necessary to use MySQL with an SSL connection.
# * <tt>:sslcert</tt> - Necessary to use MySQL with an SSL connection.
@@ -125,7 +126,7 @@ module ActiveRecord
def initialize(connection, logger, connection_options, config)
super
@statements = StatementPool.new(@connection,
- config.fetch(:statement_limit) { 1000 })
+ self.class.type_cast_config_to_integer(config.fetch(:statement_limit) { 1000 }))
@client_encoding = nil
connect
end
@@ -149,8 +150,8 @@ module ActiveRecord
end
end
- def new_column(field, default, type, null, collation) # :nodoc:
- Column.new(field, default, type, null, collation)
+ def new_column(field, default, type, null, collation, extra = "") # :nodoc:
+ Column.new(field, default, type, null, collation, strict_mode?, extra)
end
def error_number(exception) # :nodoc:
@@ -278,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)
@@ -382,7 +379,7 @@ module ActiveRecord
TYPES = {}
- # Register an MySQL +type_id+ with a typcasting object in
+ # Register an MySQL +type_id+ with a typecasting object in
# +type+.
def self.register_type(type_id, type)
TYPES[type_id] = type
@@ -392,6 +389,14 @@ module ActiveRecord
TYPES[new] = TYPES[old]
end
+ def self.find_type(field)
+ if field.type == Mysql::Field::TYPE_TINY && field.length > 1
+ TYPES[Mysql::Field::TYPE_LONG]
+ else
+ TYPES.fetch(field.type) { Fields::Identity.new }
+ end
+ end
+
register_type Mysql::Field::TYPE_TINY, Fields::Boolean.new
register_type Mysql::Field::TYPE_LONG, Fields::Integer.new
alias_type Mysql::Field::TYPE_LONGLONG, Mysql::Field::TYPE_LONG
@@ -424,9 +429,7 @@ module ActiveRecord
if field.decimals > 0
types[field.name] = Fields::Decimal.new
else
- types[field.name] = Fields::TYPES.fetch(field.type) {
- Fields::Identity.new
- }
+ types[field.name] = Fields.find_type field
end
}
result_set = ActiveRecord::Result.new(types.keys, result.to_a, types)
@@ -500,12 +503,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?
@@ -535,20 +538,10 @@ module ActiveRecord
configure_connection
end
+ # Many Rails applications monkey-patch a replacement of the configure_connection method
+ # and don't call 'super', so leave this here even though it looks superfluous.
def configure_connection
- encoding = @config[:encoding]
- execute("SET NAMES '#{encoding}'", :skip_logging) if encoding
-
- # By default, MySQL 'where id is null' selects the last inserted id.
- # Turn this off. http://dev.rubyonrails.org/ticket/6778
- execute("SET SQL_AUTO_IS_NULL=0", :skip_logging)
-
- # Make MySQL reject illegal values rather than truncating or
- # blanking them. See
- # http://dev.mysql.com/doc/refman/5.5/en/server-sql-mode.html#sqlmode_strict_all_tables
- if @config.fetch(:strict, true)
- execute("SET SQL_MODE='STRICT_ALL_TABLES'", :skip_logging)
- end
+ super
end
def select(sql, name = nil, binds = [])
@@ -562,6 +555,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 62d091357d..ea44e818e5 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb
@@ -2,17 +2,39 @@ module ActiveRecord
module ConnectionAdapters
class PostgreSQLColumn < Column
module Cast
+ def point_to_string(point)
+ "(#{point[0]},#{point[1]})"
+ end
+
+ def string_to_point(string)
+ if string[0] == '(' && string[-1] == ')'
+ string = string[1...-1]
+ end
+ string.split(',').map{ |v| Float(v) }
+ end
+
def string_to_time(string)
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
super
end
end
+ def string_to_bit(value)
+ case value
+ when /^0x/i
+ value[2..-1].hex.to_s(2) # Hexadecimal notation
+ else
+ value # Bit-string notation
+ end
+ end
+
def hstore_to_string(object)
if Hash === object
object.map { |k,v|
@@ -28,8 +50,8 @@ module ActiveRecord
nil
elsif String === string
Hash[string.scan(HstorePair).map { |k,v|
- v = v.upcase == 'NULL' ? nil : v.gsub(/^"(.*)"$/,'\1').gsub(/\\(.)/, '\1')
- k = k.gsub(/^"(.*)"$/,'\1').gsub(/\\(.)/, '\1')
+ v = v.upcase == 'NULL' ? nil : v.gsub(/\A"(.*)"\Z/m,'\1').gsub(/\\(.)/, '\1')
+ k = k.gsub(/\A"(.*)"\Z/m,'\1').gsub(/\\(.)/, '\1')
[k,v]
}]
else
@@ -38,7 +60,7 @@ module ActiveRecord
end
def json_to_string(object)
- if Hash === object
+ if Hash === object || Array === object
ActiveSupport::JSON.encode(object)
else
object
@@ -60,6 +82,12 @@ module ActiveRecord
"{#{casted_values.join(',')}}"
end
+ def range_to_string(object)
+ from = object.begin.respond_to?(:infinite?) && object.begin.infinite? ? '' : object.begin
+ to = object.end.respond_to?(:infinite?) && object.end.infinite? ? '' : object.end
+ "[#{from},#{to}#{object.exclude_end? ? ')' : ']'}"
+ end
+
def string_to_json(string)
if String === string
ActiveSupport::JSON.decode(string)
@@ -72,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
@@ -87,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
@@ -118,6 +150,14 @@ module ActiveRecord
"\"#{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 553985bd1e..fa173d13a2 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb
@@ -1,5 +1,3 @@
-require 'active_support/deprecation'
-
module ActiveRecord
module ConnectionAdapters
class PostgreSQLAdapter < AbstractAdapter
@@ -136,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] = OID::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
@@ -219,26 +214,6 @@ module ActiveRecord
def rollback_db_transaction
execute "ROLLBACK"
end
-
- def outside_transaction?
- ActiveSupport::Deprecation.warn(
- "#outside_transaction? is deprecated. This method was only really used " \
- "internally, but you can use #transaction_open? instead."
- )
- @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 52344f61c0..6c5792954f 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb
@@ -6,20 +6,27 @@ module ActiveRecord
module OID
class Type
def type; end
+ end
- def type_cast_for_write(value)
+ class Identity < Type
+ def type_cast(value)
value
end
end
- class Identity < Type
+ class Bit < Type
def type_cast(value)
- value
+ if String === value
+ ConnectionAdapters::PostgreSQLColumn.string_to_bit value
+ else
+ value
+ end
end
end
class Bytea < Type
def type_cast(value)
+ return if value.nil?
PGconn.unescape_bytea value
end
end
@@ -27,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.]/, '')
@@ -63,6 +75,16 @@ module ActiveRecord
end
end
+ class Point < Type
+ def type_cast(value)
+ if String === value
+ ConnectionAdapters::PostgreSQLColumn.string_to_point value
+ else
+ value
+ end
+ end
+ end
+
class Array < Type
attr_reader :subtype
def initialize(subtype)
@@ -78,11 +100,69 @@ module ActiveRecord
end
end
+ class Range < Type
+ attr_reader :subtype
+ def initialize(subtype)
+ @subtype = subtype
+ end
+
+ def extract_bounds(value)
+ from, to = value[1..-2].split(',')
+ {
+ from: (value[1] == ',' || from == '-infinity') ? infinity(:negative => true) : from,
+ to: (value[-2] == ',' || to == 'infinity') ? infinity : to,
+ exclude_start: (value[0] == '('),
+ exclude_end: (value[-1] == ')')
+ }
+ end
+
+ def infinity(options = {})
+ ::Float::INFINITY * (options[:negative] ? -1 : 1)
+ end
+
+ def infinity?(value)
+ value.respond_to?(:infinite?) && value.infinite?
+ end
+
+ def to_integer(value)
+ infinity?(value) ? value : value.to_i
+ end
+
+ def type_cast(value)
+ return if value.nil? || value == 'empty'
+ return value if value.is_a?(::Range)
+
+ extracted = extract_bounds(value)
+
+ case @subtype
+ when :date
+ from = ConnectionAdapters::Column.value_to_date(extracted[:from])
+ from -= 1.day if extracted[:exclude_start]
+ to = ConnectionAdapters::Column.value_to_date(extracted[:to])
+ when :decimal
+ from = BigDecimal.new(extracted[:from].to_s)
+ # FIXME: add exclude start for ::Range, same for timestamp ranges
+ to = BigDecimal.new(extracted[:to].to_s)
+ when :time
+ from = ConnectionAdapters::Column.string_to_time(extracted[:from])
+ to = ConnectionAdapters::Column.string_to_time(extracted[:to])
+ when :integer
+ from = to_integer(extracted[:from]) rescue value ? 1 : 0
+ from -= 1 if extracted[:exclude_start]
+ to = to_integer(extracted[:to]) rescue value ? 1 : 0
+ else
+ return value
+ end
+
+ ::Range.new(from, to, extracted[:exclude_end])
+ end
+ end
+
class Integer < Type
def type_cast(value)
return if value.nil?
- value.to_i rescue value ? 1 : 0
+ ConnectionAdapters::Column.value_to_integer value
end
end
@@ -145,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
@@ -161,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
@@ -181,6 +277,10 @@ module ActiveRecord
@mapping[oid]
end
+ def clear
+ @mapping.clear
+ end
+
def key?(oid)
@mapping.key? oid
end
@@ -233,6 +333,13 @@ module ActiveRecord
alias_type 'int8', 'int2'
alias_type 'oid', 'int2'
+ register_type 'daterange', OID::Range.new(:date)
+ register_type 'numrange', OID::Range.new(:decimal)
+ register_type 'tsrange', OID::Range.new(:time)
+ register_type 'int4range', OID::Range.new(:integer)
+ alias_type 'tstzrange', 'tsrange'
+ alias_type 'int8range', 'int4range'
+
register_type 'numeric', OID::Decimal.new
register_type 'text', OID::Identity.new
alias_type 'varchar', 'text'
@@ -243,31 +350,30 @@ module ActiveRecord
# FIXME: why are we keeping these types as strings?
alias_type 'tsvector', 'text'
alias_type 'interval', 'text'
- alias_type 'bit', 'text'
- alias_type 'varbit', 'text'
alias_type 'macaddr', 'text'
alias_type 'uuid', 'text'
- # FIXME: I don't think this is correct. We should probably be returning a parsed date,
- # but the tests pass with a string returned.
- register_type 'timestamptz', OID::Identity.new
-
register_type 'money', OID::Money.new
register_type 'bytea', OID::Bytea.new
register_type 'bool', OID::Boolean.new
+ register_type 'bit', OID::Bit.new
+ register_type 'varbit', OID::Bit.new
register_type 'float4', OID::Float.new
alias_type 'float8', 'float4'
register_type 'timestamp', OID::Timestamp.new
+ register_type 'timestamptz', OID::Timestamp.new
register_type 'date', OID::Date.new
register_type 'time', OID::Time.new
register_type 'path', OID::Identity.new
+ register_type 'point', OID::Point.new
register_type 'polygon', OID::Identity.new
register_type 'circle', OID::Identity.new
register_type 'hstore', OID::Hstore.new
register_type 'json', OID::Json.new
+ register_type 'ltree', OID::Identity.new
register_type 'cidr', OID::Cidr.new
alias_type 'inet', 'cidr'
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
index 9d3fa18e3a..e9daa5d7ff 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
@@ -18,21 +18,34 @@ module ActiveRecord
def quote(value, column = nil) #:nodoc:
return super unless column
+ sql_type = type_to_sql(column.type, column.limit, column.precision, column.scale)
+
case value
- when Array
- if column.array
- "'#{PostgreSQLColumn.array_to_string(value, column, self)}'"
+ when Range
+ if /range$/ =~ sql_type
+ "'#{PostgreSQLColumn.range_to_string(value)}'::#{sql_type}"
else
super
end
+ when Array
+ case sql_type
+ when 'point' then super(PostgreSQLColumn.point_to_string(value))
+ when 'json' then super(PostgreSQLColumn.json_to_string(value))
+ else
+ if column.array
+ "'#{PostgreSQLColumn.array_to_string(value, column, self).gsub(/'/, "''")}'"
+ else
+ super
+ end
+ end
when Hash
- case column.sql_type
+ case sql_type
when 'hstore' then super(PostgreSQLColumn.hstore_to_string(value), column)
when 'json' then super(PostgreSQLColumn.json_to_string(value), column)
else super
end
when IPAddr
- case column.sql_type
+ case sql_type
when 'inet', 'cidr' then super(PostgreSQLColumn.cidr_to_string(value), column)
else super
end
@@ -45,11 +58,14 @@ module ActiveRecord
super
end
when Numeric
- return super unless column.sql_type == 'money'
- # Not truly string input, so doesn't require (or allow) escape string syntax.
- "'#{value}'"
+ if sql_type == 'money' || [:string, :text].include?(column.type)
+ # Not truly string input, so doesn't require (or allow) escape string syntax.
+ "'#{value}'"
+ else
+ super
+ end
when String
- case column.sql_type
+ case sql_type
when 'bytea' then "'#{escape_bytea(value)}'"
when 'xml' then "xml '#{quote_string(value)}'"
when /^bit/
@@ -69,6 +85,9 @@ module ActiveRecord
return super(value, column) unless column
case value
+ when Range
+ return super(value, column) unless /range$/ =~ column.sql_type
+ PostgreSQLColumn.range_to_string(value)
when NilClass
if column.array && array_member
'NULL'
@@ -78,8 +97,13 @@ module ActiveRecord
super(value, column)
end
when Array
- return super(value, column) unless column.array
- PostgreSQLColumn.array_to_string(value, column, self)
+ case column.sql_type
+ 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)
+ end
when String
return super(value, column) unless 'bytea' == column.sql_type
{ :value => value, :format => 1 }
@@ -121,6 +145,10 @@ module ActiveRecord
end
end
+ def quote_table_name_for_assignment(table, attr)
+ quote_column_name(attr)
+ end
+
# Quotes column names for use in SQL queries.
def quote_column_name(name) #:nodoc:
PGconn.quote_ident(name.to_s)
@@ -129,11 +157,15 @@ module ActiveRecord
# Quote date/time values for use in SQL input. Includes microseconds
# if the value is a Time responding to usec.
def quoted_date(value) #:nodoc:
+ result = super
if value.acts_like?(:time) && value.respond_to?(:usec)
- "#{super}.#{sprintf("%06d", value.usec)}"
- else
- super
+ result = "#{result}.#{sprintf("%06d", value.usec)}"
+ end
+
+ if value.year < 0
+ result = result.sub(/^-/, "") + " BC"
end
+ result
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb b/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb
index 16da3ea732..bc775394a6 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb
@@ -7,13 +7,21 @@ module ActiveRecord
end
def disable_referential_integrity #:nodoc:
- if supports_disable_referential_integrity? then
- execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} DISABLE TRIGGER ALL" }.join(";"))
+ if supports_disable_referential_integrity?
+ begin
+ execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} DISABLE TRIGGER ALL" }.join(";"))
+ rescue
+ execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} DISABLE TRIGGER USER" }.join(";"))
+ end
end
yield
ensure
- if supports_disable_referential_integrity? then
- execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} ENABLE TRIGGER ALL" }.join(";"))
+ if supports_disable_referential_integrity?
+ begin
+ execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} ENABLE TRIGGER ALL" }.join(";"))
+ rescue
+ execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} ENABLE TRIGGER USER" }.join(";"))
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
index 7cad8f94cf..5dc70a5ad1 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
@@ -1,6 +1,42 @@
module ActiveRecord
module ConnectionAdapters
class PostgreSQLAdapter < AbstractAdapter
+ class SchemaCreation < AbstractAdapter::SchemaCreation
+ private
+
+ def visit_AddColumn(o)
+ sql_type = type_to_sql(o.type.to_sym, o.limit, o.precision, o.scale)
+ sql = "ADD COLUMN #{quote_column_name(o.name)} #{sql_type}"
+ add_column_options!(sql, column_options(o))
+ end
+
+ def visit_ColumnDefinition(o)
+ sql = super
+ if o.primary_key? && o.type == :uuid
+ sql << " PRIMARY KEY "
+ add_column_options!(sql, column_options(o))
+ end
+ sql
+ end
+
+ def add_column_options!(sql, options)
+ if options[:array] || options[:column].try(:array)
+ sql << '[]'
+ end
+
+ column = options.fetch(:column) { return super }
+ if column.type == :uuid && options[:default] =~ /\(\)/
+ sql << " DEFAULT #{options[:default]}"
+ else
+ super
+ end
+ end
+ end
+
+ def schema_creation
+ SchemaCreation.new self
+ end
+
module SchemaStatements
# Drops the database specified on the +name+ attribute
# and creates it again using the provided +options+.
@@ -10,17 +46,17 @@ 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>).
#
# Example:
# create_database config[:database], config
- # create_database 'foo_development', :encoding => 'unicode'
+ # create_database 'foo_development', encoding: 'unicode'
def create_database(name, options = {})
- options = options.reverse_merge(:encoding => "utf8")
+ options = { encoding: 'utf8' }.merge!(options.symbolize_keys)
- option_string = options.symbolize_keys.sum do |key, value|
+ option_string = options.sum do |key, value|
case key
when :owner
" OWNER = \"#{value}\""
@@ -120,12 +156,15 @@ module ActiveRecord
column_names = columns.values_at(*indkey).compact
- # add info on sort order for columns (only desc order is explicitly specified, asc is the default)
- desc_order_columns = inddef.scan(/(\w+) DESC/).flatten
- orders = desc_order_columns.any? ? Hash[desc_order_columns.map {|order_column| [order_column, :desc]}] : {}
- where = inddef.scan(/WHERE (.+)$/).flatten[0]
+ unless column_names.empty?
+ # add info on sort order for columns (only desc order is explicitly specified, asc is the default)
+ desc_order_columns = inddef.scan(/(\w+) DESC/).flatten
+ orders = desc_order_columns.any? ? Hash[desc_order_columns.map {|order_column| [order_column, :desc]}] : {}
+ where = inddef.scan(/WHERE (.+)$/).flatten[0]
+ using = inddef.scan(/USING (.+?) /).flatten[0].to_sym
- column_names.empty? ? nil : IndexDefinition.new(table_name, index_name, unique, column_names, [], orders, where)
+ IndexDefinition.new(table_name, index_name, unique, column_names, [], orders, where, nil, using)
+ end
end.compact
end
@@ -267,7 +306,6 @@ module ActiveRecord
FROM pg_class seq,
pg_attribute attr,
pg_depend dep,
- pg_namespace name,
pg_constraint cons
WHERE seq.oid = dep.objid
AND seq.relkind = 'S'
@@ -283,6 +321,7 @@ module ActiveRecord
result = query(<<-end_sql, 'SCHEMA')[0]
SELECT attr.attname,
CASE
+ WHEN pg_get_expr(def.adbin, def.adrelid) !~* 'nextval' THEN NULL
WHEN split_part(pg_get_expr(def.adbin, def.adrelid), '''', 2) ~ '.' THEN
substr(split_part(pg_get_expr(def.adbin, def.adrelid), '''', 2),
strpos(split_part(pg_get_expr(def.adbin, def.adrelid), '''', 2), '.')+1)
@@ -294,7 +333,7 @@ module ActiveRecord
JOIN pg_constraint cons ON (conrelid = adrelid AND adnum = conkey[1])
WHERE t.oid = '#{quote_table_name(table)}'::regclass
AND cons.contype = 'p'
- AND pg_get_expr(def.adbin, def.adrelid) ~* 'nextval'
+ AND pg_get_expr(def.adbin, def.adrelid) ~* 'nextval|uuid_generate'
end_sql
end
@@ -306,12 +345,11 @@ module ActiveRecord
# Returns just a table's primary key
def primary_key(table)
row = exec_query(<<-end_sql, 'SCHEMA').rows.first
- SELECT DISTINCT(attr.attname)
+ SELECT attr.attname
FROM pg_attribute attr
- INNER JOIN pg_depend dep ON attr.attrelid = dep.refobjid AND attr.attnum = dep.refobjsubid
INNER JOIN pg_constraint cons ON attr.attrelid = cons.conrelid AND attr.attnum = cons.conkey[1]
WHERE cons.contype = 'p'
- AND dep.refobjid = '#{quote_table_name(table)}'::regclass
+ AND cons.conrelid = '#{quote_table_name(table)}'::regclass
end_sql
row && row.first
@@ -323,32 +361,32 @@ module ActiveRecord
#
# Example:
# rename_table('octopuses', 'octopi')
- def rename_table(name, new_name)
+ def rename_table(table_name, new_name)
clear_cache!
- execute "ALTER TABLE #{quote_table_name(name)} RENAME TO #{quote_table_name(new_name)}"
+ execute "ALTER TABLE #{quote_table_name(table_name)} RENAME TO #{quote_table_name(new_name)}"
pk, seq = pk_and_sequence_for(new_name)
- if seq == "#{name}_#{pk}_seq"
+ if seq == "#{table_name}_#{pk}_seq"
new_seq = "#{new_name}_#{pk}_seq"
execute "ALTER TABLE #{quote_table_name(seq)} RENAME TO #{quote_table_name(new_seq)}"
end
+
+ rename_table_indexes(table_name, new_name)
end
# Adds a new column to the named table.
# See TableDefinition#column for details of the options you can use.
def add_column(table_name, column_name, type, options = {})
clear_cache!
- add_column_sql = "ALTER TABLE #{quote_table_name(table_name)} ADD COLUMN #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
- add_column_options!(add_column_sql, options)
-
- execute add_column_sql
+ super
end
# Changes the column of a table.
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)
@@ -372,6 +410,12 @@ module ActiveRecord
def rename_column(table_name, column_name, new_column_name)
clear_cache!
execute "ALTER TABLE #{quote_table_name(table_name)} RENAME COLUMN #{quote_column_name(column_name)} TO #{quote_column_name(new_column_name)}"
+ rename_column_indexes(table_name, column_name, new_column_name)
+ end
+
+ def add_index(table_name, column_name, options = {}) #:nodoc:
+ index_name, index_type, index_columns, index_options, index_algorithm, index_using = add_index_options(table_name, column_name, options)
+ execute "CREATE #{index_type} INDEX #{index_algorithm} #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} #{index_using} (#{index_columns})#{index_options}"
end
def remove_index!(table_name, index_name) #:nodoc:
@@ -396,6 +440,13 @@ module ActiveRecord
when nil, 0..0x3fffffff; super(type)
else raise(ActiveRecordError, "No binary type has byte size #{limit}.")
end
+ when 'text'
+ # PostgreSQL doesn't support limits on text columns.
+ # The hard limit is 1Gb, according to section 8.3 in the manual.
+ case limit
+ when nil, 0..0x3fffffff; super(type)
+ else raise(ActiveRecordError, "The limit on text can be at most 1GB - 1byte.")
+ end
when 'integer'
return 'integer' unless limit
@@ -417,25 +468,17 @@ module ActiveRecord
end
end
- # Returns a SELECT DISTINCT clause for a given set of columns and a given ORDER BY clause.
- #
# PostgreSQL requires the ORDER BY columns in the select list for distinct queries, and
# requires that the ORDER BY include the distinct column.
- #
- # distinct("posts.id", "posts.created_at desc")
- def distinct(columns, orders) #:nodoc:
- return "DISTINCT #{columns}" if orders.empty?
-
- # Construct a clean list of column names from the ORDER BY clause, removing
- # any ASC/DESC modifiers
- order_columns = orders.collect do |s|
- s = s.to_sql unless s.is_a?(String)
- s.gsub(/\s+(ASC|DESC)\s*(NULLS\s+(FIRST|LAST)\s*)?/i, '')
- end
- order_columns.delete_if { |c| c.blank? }
- order_columns = order_columns.zip((0...order_columns.size).to_a).map { |s,i| "#{s} AS alias_#{i}" }
-
- "DISTINCT #{columns}, #{order_columns * ', '}"
+ def columns_for_distinct(columns, orders) #:nodoc:
+ order_columns = orders.reject(&:blank?).map{ |s|
+ # Convert Arel node to string
+ s = s.to_sql unless s.is_a?(String)
+ # Remove any ASC/DESC modifiers
+ s.gsub(/\s+(ASC|DESC)\s*(NULLS\s+(FIRST|LAST)\s*)?/i, '')
+ }.reject(&:blank?).map.with_index { |column, i| "#{column} AS alias_#{i}" }
+
+ [super, *order_columns].join(', ')
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
index e18464fa35..3668aecd4b 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
@@ -16,23 +16,26 @@ require 'pg'
require 'ipaddr'
module ActiveRecord
- module ConnectionHandling
+ module ConnectionHandling # :nodoc:
+ VALID_CONN_PARAMS = [:host, :hostaddr, :port, :dbname, :user, :password, :connect_timeout,
+ :client_encoding, :options, :application_name, :fallback_application_name,
+ :keepalives, :keepalives_idle, :keepalives_interval, :keepalives_count,
+ :tty, :sslmode, :requiressl, :sslcompression, :sslcert, :sslkey,
+ :sslrootcert, :sslcrl, :requirepeer, :krbsrvname, :gsslib, :service]
+
# Establishes a connection to the database that's used by all Active Record objects
- def postgresql_connection(config) # :nodoc:
+ def postgresql_connection(config)
conn_params = config.symbolize_keys
- # Forward any unused config params to PGconn.connect.
- [:statement_limit, :encoding, :min_messages, :schema_search_path,
- :schema_order, :adapter, :pool, :checkout_timeout, :template,
- :reaping_frequency, :insert_returning].each do |key|
- conn_params.delete key
- end
- conn_params.delete_if { |k,v| v.nil? }
+ conn_params.delete_if { |_, v| v.nil? }
# Map ActiveRecords param names to PGs.
conn_params[:user] = conn_params.delete(:username) if conn_params[:username]
conn_params[:dbname] = conn_params.delete(:database) if conn_params[:database]
+ # Forward only valid config params to PGconn.connect.
+ conn_params.keep_if { |k, _| VALID_CONN_PARAMS.include?(k) }
+
# The postgres drivers don't allow the creation of an unconnected PGconn object,
# so just pass a nil connection object for the time being.
ConnectionAdapters::PostgreSQLAdapter.new(nil, logger, conn_params, config)
@@ -46,13 +49,17 @@ module ActiveRecord
# 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
# :stopdoc:
@@ -74,12 +81,14 @@ module ActiveRecord
return default unless default
case default
+ when /\A'(.*)'::(num|date|tstz|ts|int4|int8)range\z/m
+ $1
# Numeric types
- when /\A\(?(-?\d+(\.\d*)?\)?)\z/
+ when /\A\(?(-?\d+(\.\d*)?\)?(::bigint)?)\z/
$1
# Character types
when /\A\(?'(.*)'::.*\b(?:character varying|bpchar|text)\z/m
- $1
+ $1.gsub(/''/, "'")
# Binary data types
when /\A'(.*)'::bytea\z/m
$1
@@ -124,6 +133,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?
@@ -131,8 +148,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
@@ -170,6 +195,8 @@ module ActiveRecord
:decimal
when 'hstore'
:hstore
+ when 'ltree'
+ :ltree
# Network address types
when 'inet'
:inet
@@ -209,12 +236,14 @@ module ActiveRecord
# UUID type
when 'uuid'
:uuid
- # JSON type
- when 'json'
- :json
+ # JSON type
+ when 'json'
+ :json
# Small and big integer types
when /^(?:small|big)int$/
:integer
+ when /(num|date|tstz|ts|int4|int8)range$/
+ field_type.to_sym
# Pass through all types that are not specific to PostgreSQL.
else
super
@@ -238,6 +267,8 @@ module ActiveRecord
# <encoding></tt> call on the connection.
# * <tt>:min_messages</tt> - An optional client min messages that is used in a
# <tt>SET client_min_messages TO <min_messages></tt> call on the connection.
+ # * <tt>:variables</tt> - An optional hash of additional parameters that
+ # will be used in <tt>SET SESSION key = val</tt> calls on the connection.
# * <tt>:insert_returning</tt> - An optional boolean to control the use or <tt>RETURNING</tt> for <tt>INSERT</tt> statements
# defaults to true.
#
@@ -252,7 +283,7 @@ module ActiveRecord
attr_accessor :array
end
- class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition
+ module ColumnMethods
def xml(*args)
options = args.extract_options!
column(args[0], 'xml', options)
@@ -263,10 +294,38 @@ module ActiveRecord
column(args[0], 'tsvector', options)
end
+ def int4range(name, options = {})
+ column(name, 'int4range', options)
+ end
+
+ def int8range(name, options = {})
+ column(name, 'int8range', options)
+ end
+
+ def tsrange(name, options = {})
+ column(name, 'tsrange', options)
+ end
+
+ def tstzrange(name, options = {})
+ column(name, 'tstzrange', options)
+ end
+
+ def numrange(name, options = {})
+ column(name, 'numrange', options)
+ end
+
+ def daterange(name, options = {})
+ column(name, 'daterange', options)
+ end
+
def hstore(name, options = {})
column(name, 'hstore', options)
end
+ def ltree(name, options = {})
+ column(name, 'ltree', options)
+ end
+
def inet(name, options = {})
column(name, 'inet', options)
end
@@ -286,6 +345,45 @@ module ActiveRecord
def json(name, options = {})
column(name, 'json', options)
end
+ end
+
+ class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition
+ include ColumnMethods
+
+ # Defines the primary key field.
+ # Use of the native PostgreSQL UUID type is supported, and can be used
+ # by defining your tables as such:
+ #
+ # create_table :stuffs, id: :uuid do |t|
+ # t.string :content
+ # t.timestamps
+ # end
+ #
+ # By default, this will use the +uuid_generate_v4()+ function from the
+ # +uuid-ossp+ extension, which MUST be enabled on your database. To enable
+ # the +uuid-ossp+ extension, you can use the +enable_extension+ method in your
+ # migrations. To use a UUID primary key without +uuid-ossp+ enabled, you can
+ # set the +:default+ option to +nil+:
+ #
+ # create_table :stuffs, id: false do |t|
+ # t.primary_key :id, :uuid, default: nil
+ # t.uuid :foo_id
+ # t.timestamps
+ # end
+ #
+ # You may also pass a different UUID generation function from +uuid-ossp+
+ # or another library.
+ #
+ # Note that setting the UUID primary key default value to +nil+ will
+ # require you to assure that you always provide a UUID value before saving
+ # a record (as primary keys cannot be +nil+). This might be done via the
+ # +SecureRandom.uuid+ method and a +before_save+ callback, for instance.
+ def primary_key(name, type = :primary_key, options = {})
+ return super unless type == :uuid
+ options[:default] = options.fetch(:default, 'uuid_generate_v4()')
+ options[:primary_key] = true
+ column name, type, options
+ end
def column(name, type = nil, options = {})
super
@@ -297,12 +395,13 @@ module ActiveRecord
private
- def new_column_definition(base, name, type)
- definition = ColumnDefinition.new base, name, type
- @columns << definition
- @columns_hash[name] = definition
- definition
- end
+ def create_column_definition(name, type)
+ ColumnDefinition.new name, type
+ end
+ end
+
+ class Table < ActiveRecord::ConnectionAdapters::Table
+ include ColumnMethods
end
ADAPTER_NAME = 'PostgreSQL'
@@ -318,6 +417,12 @@ module ActiveRecord
timestamp: { name: "timestamp" },
time: { name: "time" },
date: { name: "date" },
+ daterange: { name: "daterange" },
+ numrange: { name: "numrange" },
+ tsrange: { name: "tsrange" },
+ tstzrange: { name: "tstzrange" },
+ int4range: { name: "int4range" },
+ int8range: { name: "int8range" },
binary: { name: "bytea" },
boolean: { name: "boolean" },
xml: { name: "xml" },
@@ -327,13 +432,15 @@ module ActiveRecord
cidr: { name: "cidr" },
macaddr: { name: "macaddr" },
uuid: { name: "uuid" },
- json: { name: "json" }
+ json: { name: "json" },
+ ltree: { name: "ltree" }
}
include Quoting
include ReferentialIntegrity
include SchemaStatements
include DatabaseStatements
+ include Savepoints
# Returns 'PostgreSQL' as adapter name for identification purposes.
def adapter_name
@@ -345,6 +452,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
@@ -371,6 +479,10 @@ module ActiveRecord
true
end
+ def index_algorithms
+ { concurrently: 'CONCURRENTLY' }
+ end
+
class StatementPool < ConnectionAdapters::StatementPool
def initialize(connection, max)
super
@@ -432,14 +544,13 @@ module ActiveRecord
def initialize(connection, logger, connection_parameters, config)
super(connection, logger)
- if config.fetch(:prepared_statements) { true }
+ if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true })
+ @prepared_statements = true
@visitor = Arel::Visitors::PostgreSQL.new self
else
- @visitor = BindSubstitution.new self
+ @visitor = unprepared_visitor
end
- connection_parameters.delete :prepared_statements
-
@connection_parameters, @config = connection_parameters, config
# @local_tz is initialized as nil to avoid warnings when connect tries to use it
@@ -448,7 +559,7 @@ module ActiveRecord
connect
@statements = StatementPool.new @connection,
- config.fetch(:statement_limit) { 1000 }
+ self.class.type_cast_config_to_integer(config.fetch(:statement_limit) { 1000 })
if postgresql_version < 80200
raise "Your version of PostgreSQL (#{postgresql_version}) is too old, please upgrade!"
@@ -456,7 +567,7 @@ module ActiveRecord
initialize_type_map
@local_tz = execute('SHOW TIME ZONE', 'SCHEMA').first["TimeZone"]
- @use_insert_returning = @config.key?(:insert_returning) ? @config[:insert_returning] : true
+ @use_insert_returning = @config.key?(:insert_returning) ? self.class.type_cast_config_to_boolean(@config[:insert_returning]) : true
end
# Clears the prepared statements cache.
@@ -466,8 +577,7 @@ module ActiveRecord
# Is this connection alive and ready for queries?
def active?
- @connection.query 'SELECT 1'
- true
+ @connection.connect_poll != PG::PGRES_POLLING_FAILED
rescue PGError
false
end
@@ -521,26 +631,53 @@ module ActiveRecord
true
end
- # Returns true, since this connection adapter supports savepoints.
- def supports_savepoints?
- true
- end
-
# Returns true.
def supports_explain?
true
end
- # Returns the configured supported identifier length supported by PostgreSQL
- def table_alias_length
- @table_alias_length ||= query('SHOW max_identifier_length', 'SCHEMA')[0][0].to_i
+ # Returns true if pg > 9.1
+ def supports_extensions?
+ postgresql_version >= 90100
end
- def add_column_options!(sql, options)
- if options[:array] || options[:column].try(:array)
- sql << '[]'
+ # Range datatypes weren't introduced until PostgreSQL 9.2
+ def supports_ranges?
+ postgresql_version >= 90200
+ end
+
+ def enable_extension(name)
+ exec_query("CREATE EXTENSION IF NOT EXISTS \"#{name}\"").tap {
+ reload_type_map
+ }
+ end
+
+ def disable_extension(name)
+ exec_query("DROP EXTENSION IF EXISTS \"#{name}\" CASCADE").tap {
+ reload_type_map
+ }
+ end
+
+ def extension_enabled?(name)
+ if supports_extensions?
+ res = exec_query "SELECT EXISTS(SELECT * FROM pg_available_extensions WHERE name = '#{name}' AND installed_version IS NOT NULL) as enabled",
+ 'SCHEMA'
+ res.column_types['enabled'].type_cast res.rows.first.first
end
- super
+ end
+
+ def extensions
+ if supports_extensions?
+ res = exec_query "SELECT extname from pg_extension", "SCHEMA"
+ res.rows.map { |r| res.column_types['extname'].type_cast r.first }
+ else
+ super
+ end
+ end
+
+ # Returns the configured supported identifier length supported by PostgreSQL
+ def table_alias_length
+ @table_alias_length ||= query('SHOW max_identifier_length', 'SCHEMA')[0][0].to_i
end
# Set the authorized user for this session
@@ -572,6 +709,10 @@ module ActiveRecord
@use_insert_returning
end
+ def valid_type?(type)
+ !native_database_types[type].nil?
+ end
+
protected
# Returns the version of the connected PostgreSQL server.
@@ -584,6 +725,8 @@ module ActiveRecord
UNIQUE_VIOLATION = "23505"
def translate_exception(exception, message)
+ return exception unless exception.respond_to?(:result)
+
case exception.result.try(:error_field, PGresult::PG_DIAG_SQLSTATE)
when UNIQUE_VIOLATION
RecordNotUnique.new(message, exception)
@@ -596,6 +739,11 @@ module ActiveRecord
private
+ def reload_type_map
+ OID::TYPE_MAP.clear
+ initialize_type_map
+ end
+
def initialize_type_map
result = execute('SELECT oid, typname, typelem, typdelim, typinput FROM pg_type', 'SCHEMA')
leaves, nodes = result.partition { |row| row['typelem'] == '0' }
@@ -609,7 +757,14 @@ module ActiveRecord
# populate composite types
nodes.find_all { |row| OID::TYPE_MAP.key? row['typelem'].to_i }.each do |row|
- vector = OID::Vector.new row['typdelim'], OID::TYPE_MAP[row['typelem'].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
+ vector = OID::Vector.new row['typdelim'], OID::TYPE_MAP[row['typelem'].to_i]
+ end
+
OID::TYPE_MAP[row['oid'].to_i] = vector
end
@@ -622,37 +777,37 @@ module ActiveRecord
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)
- begin
- stmt_key = prepare_statement sql
+ def exec_cache(sql, name, binds)
+ stmt_key = prepare_statement(sql)
- # Clear the queue
- @connection.get_last_result
+ log(sql, name, binds, stmt_key) do
@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)
- rescue
- raise e
- end
- if FEATURE_NOT_SUPPORTED == code
- @statements.delete sql_key(sql)
- retry
- else
- raise e
- end
+ end
+ rescue ActiveRecord::StatementInvalid => e
+ pgerror = e.original_exception
+
+ # 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 = pgerror.result.result_error_field(PGresult::PG_DIAG_SQLSTATE)
+ rescue
+ raise e
+ end
+ if FEATURE_NOT_SUPPORTED == code
+ @statements.delete sql_key(sql)
+ retry
+ else
+ raise e
end
end
@@ -669,6 +824,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]
@@ -706,11 +863,24 @@ module ActiveRecord
# If using Active Record's time zone support configure the connection to return
# TIMESTAMP WITH ZONE types in UTC.
+ # (SET TIME ZONE does not use an equals sign like other SET variables)
if ActiveRecord::Base.default_timezone == :utc
execute("SET time zone 'UTC'", 'SCHEMA')
elsif @local_tz
execute("SET time zone '#{@local_tz}'", 'SCHEMA')
end
+
+ # SET statements from :variables config hash
+ # http://www.postgresql.org/docs/8.3/static/sql-set.html
+ variables = @config[:variables] || {}
+ variables.map do |k, v|
+ if v == ':default' || v == :default
+ # Sets the value to the global or compile default
+ execute("SET SESSION #{k.to_s} TO DEFAULT", 'SCHEMA')
+ elsif !v.nil?
+ execute("SET SESSION #{k.to_s} TO #{quote(v)}", 'SCHEMA')
+ end
+ end
end
# Returns the current ID of a table's sequence.
@@ -785,8 +955,12 @@ module ActiveRecord
$1.strip if $1
end
- def table_definition
- TableDefinition.new(self)
+ 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)
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 aad1f9a7ef..e5c9f6f54a 100644
--- a/activerecord/lib/active_record/connection_adapters/schema_cache.rb
+++ b/activerecord/lib/active_record/connection_adapters/schema_cache.rb
@@ -1,7 +1,8 @@
+
module ActiveRecord
module ConnectionAdapters
class SchemaCache
- attr_reader :columns, :columns_hash, :primary_keys, :tables, :version
+ attr_reader :version
attr_accessor :connection
def initialize(conn)
@@ -14,6 +15,10 @@ module ActiveRecord
prepare_default_proc
end
+ def primary_keys(table_name)
+ @primary_keys[table_name]
+ end
+
# A cached lookup for table existence.
def table_exists?(name)
return @tables[name] if @tables.key? name
@@ -30,6 +35,21 @@ module ActiveRecord
end
end
+ def tables(name)
+ @tables[name]
+ end
+
+ # Get the columns for a table
+ 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)
+ @columns_hash[table]
+ end
+
# Clears out internal caches
def clear!
@columns.clear
@@ -39,6 +59,12 @@ module ActiveRecord
@version = nil
end
+ def size
+ [@columns, @columns_hash, @primary_keys, @tables].map { |x|
+ x.size
+ }.inject :+
+ end
+
# Clear out internal caches for table with +table_name+.
def clear_table_cache!(table_name)
@columns.delete table_name
@@ -50,9 +76,9 @@ module ActiveRecord
def marshal_dump
# if we get current version during initialization, it happens stack over flow.
@version = ActiveRecord::Migrator.current_version
- [@version] + [:@columns, :@columns_hash, :@primary_keys, :@tables].map do |val|
- self.instance_variable_get(val).inject({}) { |h, v| h[v[0]] = v[1]; h }
- end
+ [@version] + [@columns, @columns_hash, @primary_keys, @tables].map { |val|
+ Hash[val]
+ }
end
def marshal_load(array)
@@ -68,7 +94,7 @@ module ActiveRecord
end
@columns_hash.default_proc = Proc.new do |h, table_name|
- h[table_name] = Hash[columns[table_name].map { |col|
+ h[table_name] = Hash[columns(table_name).map { |col|
[col.name, col]
}]
end
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
index 4d5cb72c67..e5ad08b6b0 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
@@ -6,9 +6,9 @@ gem 'sqlite3', '~> 1.3.6'
require 'sqlite3'
module ActiveRecord
- module ConnectionHandling
+ module ConnectionHandling # :nodoc:
# sqlite3 adapter reuses sqlite_connection.
- def sqlite3_connection(config) # :nodoc:
+ def sqlite3_connection(config)
# Require database.
unless config[:database]
raise ArgumentError, "No database file specified. Missing argument: database"
@@ -17,16 +17,18 @@ 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(config[:timeout]) if config[:timeout]
+ db.busy_timeout(ConnectionAdapters::SQLite3Adapter.type_cast_config_to_integer(config[:timeout])) if config[:timeout]
ConnectionAdapters::SQLite3Adapter.new(db, logger, config)
end
@@ -51,6 +53,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
@@ -107,13 +126,14 @@ module ActiveRecord
@active = nil
@statements = StatementPool.new(@connection,
- config.fetch(:statement_limit) { 1000 })
+ self.class.type_cast_config_to_integer(config.fetch(:statement_limit) { 1000 }))
@config = config
- if config.fetch(:prepared_statements) { true }
+ if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true })
+ @prepared_statements = true
@visitor = Arel::Visitors::SQLite.new self
else
- @visitor = BindSubstitution.new self
+ @visitor = unprepared_visitor
end
end
@@ -178,30 +198,19 @@ module ActiveRecord
true
end
- # Returns true
- def supports_autoincrement? #:nodoc:
+ def supports_index_sort_order?
true
end
- def supports_index_sort_order?
- true
+ # Returns 62. SQLite supports index names up to 64
+ # characters. The rest is used by rails internally to perform
+ # temporary rename operations
+ def allowed_index_name_length
+ index_name_length - 2
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'
@@ -217,8 +226,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
@@ -229,6 +238,10 @@ module ActiveRecord
@connection.class.quote(s)
end
+ def quote_table_name_for_assignment(table, attr)
+ quote_column_name(attr)
+ end
+
def quote_column_name(name) #:nodoc:
%Q("#{name.to_s.gsub('"', '""')}")
end
@@ -251,7 +264,7 @@ module ActiveRecord
value = super
if column.type == :string && value.encoding == Encoding::ASCII_8BIT
logger.error "Binary data inserted for `string` type on column `#{column.name}`" if logger
- value.encode! 'utf-8'
+ value = value.encode Encoding::UTF_8
end
value
end
@@ -280,8 +293,8 @@ module ActiveRecord
def exec_query(sql, name = nil, binds = [])
log(sql, name, binds) do
- # Don't cache statements without bind values
- if binds.empty?
+ # 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
@@ -337,18 +350,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
@@ -424,8 +425,9 @@ module ActiveRecord
#
# Example:
# rename_table('octopuses', 'octopi')
- def rename_table(name, new_name)
- exec_query "ALTER TABLE #{quote_table_name(name)} RENAME TO #{quote_table_name(new_name)}"
+ def rename_table(table_name, new_name)
+ exec_query "ALTER TABLE #{quote_table_name(table_name)} RENAME TO #{quote_table_name(new_name)}"
+ rename_table_indexes(table_name, new_name)
end
# See: http://www.sqlite.org/lang_altertable.html
@@ -444,15 +446,11 @@ module ActiveRecord
end
end
- def remove_column(table_name, *column_names) #:nodoc:
- raise ArgumentError.new("You must specify at least one column name. Example: remove_column(:people, :first_name)") if column_names.empty?
- column_names.each do |column_name|
- alter_table(table_name) do |definition|
- definition.columns.delete(definition[column_name])
- end
+ def remove_column(table_name, column_name, type = nil, options = {}) #:nodoc:
+ alter_table(table_name) do |definition|
+ definition.remove_column column_name
end
end
- alias :remove_columns :remove_column
def change_column_default(table_name, column_name, default) #:nodoc:
alter_table(table_name) do |definition|
@@ -488,6 +486,7 @@ module ActiveRecord
raise ActiveRecord::ActiveRecordError, "Missing column #{table_name}.#{column_name}"
end
alter_table(table_name, :rename => {column_name.to_s => new_column_name.to_s})
+ rename_column_indexes(table_name, column_name, new_column_name)
end
protected
@@ -502,7 +501,7 @@ module ActiveRecord
end
def alter_table(table_name, options = {}) #:nodoc:
- altered_table_name = "altered_#{table_name}"
+ altered_table_name = "a#{table_name}"
caller = lambda {|definition| yield definition if block_given?}
transaction do
@@ -519,27 +518,24 @@ module ActiveRecord
def copy_table(from, to, options = {}) #:nodoc:
from_primary_key = primary_key(from)
- options[:primary_key] = from_primary_key if from_primary_key != 'id'
- unless options[:primary_key]
- options[:id] = columns(from).detect{|c| c.name == 'id'}.present? && from_primary_key == 'id'
- end
+ options[:id] = false
create_table(to, options) do |definition|
@definition = definition
+ @definition.primary_key(from_primary_key) if from_primary_key.present?
columns(from).each do |column|
column_name = options[:rename] ?
(options[:rename][column.name] ||
options[:rename][column.name.to_sym] ||
column.name) : column.name
+ next if column_name == from_primary_key
@definition.column(column_name, column.type,
:limit => column.limit, :default => column.default,
:precision => column.precision, :scale => column.scale,
:null => column.null)
end
- @definition.primary_key(from_primary_key) if from_primary_key
yield @definition if block_given?
end
-
copy_table_indexes(from, to, options[:rename] || {})
copy_table_contents(from, to,
@definition.columns.map {|column| column.name},
@@ -549,10 +545,10 @@ module ActiveRecord
def copy_table_indexes(from, to, rename = {}) #:nodoc:
indexes(from).each do |index|
name = index.name
- if to == "altered_#{from}"
- name = "temp_#{name}"
- elsif from == "altered_#{to}"
- name = name[5..-1]
+ if to == "a#{from}"
+ name = "t#{name}"
+ elsif from == "a#{to}"
+ name = name[1..-1]
end
to_column_names = columns(to).map { |c| c.name }
@@ -562,7 +558,7 @@ module ActiveRecord
unless columns.empty?
# index name can't be the same
- opts = { :name => name.gsub(/_(#{from})_/, "_#{to}_") }
+ opts = { name: name.gsub(/(^|_)(#{from})_/, "\\1#{to}_"), internal: true }
opts[:unique] = true if index.unique
add_index(to, columns, opts)
end
@@ -577,9 +573,17 @@ module ActiveRecord
quoted_columns = columns.map { |col| quote_column_name(col) } * ','
quoted_to = quote_table_name(to)
+
+ raw_column_mappings = Hash[columns(from).map { |c| [c.name, c] }]
+
exec_query("SELECT * FROM #{quote_table_name(from)}").each do |row|
sql = "INSERT INTO #{quoted_to} (#{quoted_columns}) VALUES ("
- sql << columns.map {|col| quote row[column_mappings[col]]} * ', '
+
+ column_values = columns.map do |col|
+ quote(row[column_mappings[col]], raw_column_mappings[col])
+ end
+
+ sql << column_values * ', '
sql << ')'
exec_query sql
end
@@ -589,14 +593,6 @@ 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/
@@ -605,7 +601,6 @@ module ActiveRecord
super
end
end
-
end
end
end
diff --git a/activerecord/lib/active_record/connection_handling.rb b/activerecord/lib/active_record/connection_handling.rb
index 3531be05bf..a1943dfcb0 100644
--- a/activerecord/lib/active_record/connection_handling.rb
+++ b/activerecord/lib/active_record/connection_handling.rb
@@ -1,4 +1,3 @@
-
module ActiveRecord
module ConnectionHandling
# Establishes the connection to the database. Accepts a hash as input where
@@ -6,25 +5,25 @@ module ActiveRecord
# example for regular databases (MySQL, Postgresql, etc):
#
# ActiveRecord::Base.establish_connection(
- # :adapter => "mysql",
- # :host => "localhost",
- # :username => "myuser",
- # :password => "mypass",
- # :database => "somedatabase"
+ # adapter: "mysql",
+ # host: "localhost",
+ # username: "myuser",
+ # password: "mypass",
+ # database: "somedatabase"
# )
#
# Example for SQLite database:
#
# ActiveRecord::Base.establish_connection(
- # :adapter => "sqlite",
- # :database => "path/to/dbfile"
+ # adapter: "sqlite",
+ # database: "path/to/dbfile"
# )
#
# Also accepts keys as strings (for parsing from YAML for example):
#
# ActiveRecord::Base.establish_connection(
- # "adapter" => "sqlite",
- # "database" => "path/to/dbfile"
+ # "adapter" => "sqlite",
+ # "database" => "path/to/dbfile"
# )
#
# Or a URL:
@@ -55,17 +54,17 @@ module ActiveRecord
end
def connection_id
- Thread.current['ActiveRecord::Base.connection_id']
+ ActiveRecord::RuntimeRegistry.connection_id
end
def connection_id=(connection_id)
- Thread.current['ActiveRecord::Base.connection_id'] = connection_id
+ ActiveRecord::RuntimeRegistry.connection_id = connection_id
end
# Returns the configuration of the associated connection as a hash:
#
# ActiveRecord::Base.connection_config
- # # => {:pool=>5, :timeout=>5000, :database=>"db/development.sqlite3", :adapter=>"sqlite3"}
+ # # => {pool: 5, timeout: 5000, database: "db/development.sqlite3", adapter: "sqlite3"}
#
# Please use only for reading.
def connection_config
@@ -80,7 +79,7 @@ module ActiveRecord
connection_handler.retrieve_connection(self)
end
- # Returns true if Active Record is connected.
+ # Returns +true+ if Active Record is connected.
def connected?
connection_handler.connected?(self)
end
diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb
index f97c363871..366ebde418 100644
--- a/activerecord/lib/active_record/core.rb
+++ b/activerecord/lib/active_record/core.rb
@@ -3,101 +3,94 @@ require 'active_support/core_ext/object/duplicable'
require 'thread'
module ActiveRecord
- ActiveSupport.on_load(:active_record_config) do
- ##
- # :singleton-method:
- #
- # Accepts a logger conforming to the interface of Log4r which is then
- # passed on to any new database connections made and which can be
- # retrieved on both a class and instance level by calling +logger+.
- mattr_accessor :logger, instance_accessor: false
+ module Core
+ extend ActiveSupport::Concern
- ##
- # :singleton-method:
- # Contains the database configuration - as is typically stored in config/database.yml -
- # as a Hash.
- #
- # For example, the following database.yml...
- #
- # development:
- # adapter: sqlite3
- # database: db/development.sqlite3
- #
- # production:
- # adapter: sqlite3
- # database: db/production.sqlite3
- #
- # ...would result in ActiveRecord::Base.configurations to look like this:
- #
- # {
- # 'development' => {
- # 'adapter' => 'sqlite3',
- # 'database' => 'db/development.sqlite3'
- # },
- # 'production' => {
- # 'adapter' => 'sqlite3',
- # 'database' => 'db/production.sqlite3'
- # }
- # }
- mattr_accessor :configurations, instance_accessor: false
- self.configurations = {}
+ included do
+ ##
+ # :singleton-method:
+ #
+ # Accepts a logger conforming to the interface of Log4r which is then
+ # passed on to any new database connections made and which can be
+ # retrieved on both a class and instance level by calling +logger+.
+ mattr_accessor :logger, instance_writer: false
- ##
- # :singleton-method:
- # Determines whether to use Time.utc (using :utc) or Time.local (using :local) when pulling
- # dates and times from the database. This is set to :utc by default.
- mattr_accessor :default_timezone, instance_accessor: false
- self.default_timezone = :utc
+ ##
+ # :singleton-method:
+ # Contains the database configuration - as is typically stored in config/database.yml -
+ # as a Hash.
+ #
+ # For example, the following database.yml...
+ #
+ # development:
+ # adapter: sqlite3
+ # database: db/development.sqlite3
+ #
+ # production:
+ # adapter: sqlite3
+ # database: db/production.sqlite3
+ #
+ # ...would result in ActiveRecord::Base.configurations to look like this:
+ #
+ # {
+ # 'development' => {
+ # 'adapter' => 'sqlite3',
+ # 'database' => 'db/development.sqlite3'
+ # },
+ # 'production' => {
+ # 'adapter' => 'sqlite3',
+ # 'database' => 'db/production.sqlite3'
+ # }
+ # }
+ mattr_accessor :configurations, instance_writer: false
+ self.configurations = {}
- ##
- # :singleton-method:
- # Specifies the format to use when dumping the database schema with Rails'
- # Rakefile. If :sql, the schema is dumped as (potentially database-
- # specific) SQL statements. If :ruby, the schema is dumped as an
- # ActiveRecord::Schema file which can be loaded into any database that
- # supports migrations. Use :ruby if you want to have different database
- # adapters for, e.g., your development and test environments.
- mattr_accessor :schema_format, instance_accessor: false
- self.schema_format = :ruby
+ ##
+ # :singleton-method:
+ # Determines whether to use Time.utc (using :utc) or Time.local (using :local) when pulling
+ # dates and times from the database. This is set to :utc by default.
+ mattr_accessor :default_timezone, instance_writer: false
+ self.default_timezone = :utc
- ##
- # :singleton-method:
- # Specify whether or not to use timestamps for migration versions
- mattr_accessor :timestamped_migrations, instance_accessor: false
- self.timestamped_migrations = true
+ ##
+ # :singleton-method:
+ # Specifies the format to use when dumping the database schema with Rails'
+ # Rakefile. If :sql, the schema is dumped as (potentially database-
+ # specific) SQL statements. If :ruby, the schema is dumped as an
+ # ActiveRecord::Schema file which can be loaded into any database that
+ # supports migrations. Use :ruby if you want to have different database
+ # adapters for, e.g., your development and test environments.
+ mattr_accessor :schema_format, instance_writer: false
+ self.schema_format = :ruby
- mattr_accessor :connection_handler, instance_accessor: false
- self.connection_handler = ConnectionAdapters::ConnectionHandler.new
+ ##
+ # :singleton-method:
+ # Specify whether or not to use timestamps for migration versions
+ mattr_accessor :timestamped_migrations, instance_writer: false
+ self.timestamped_migrations = true
- mattr_accessor :dependent_restrict_raises, instance_accessor: false
- self.dependent_restrict_raises = true
- end
+ 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
- module Core
- extend ActiveSupport::Concern
+ class_attribute :default_connection_handler, instance_writer: false
- included do
- ##
- # :singleton-method:
- # The connection handler
- config_attribute :connection_handler
+ def self.connection_handler
+ ActiveRecord::RuntimeRegistry.connection_handler || default_connection_handler
+ end
- %w(logger configurations default_timezone schema_format timestamped_migrations).each do |name|
- config_attribute name, global: true
+ def self.connection_handler=(handler)
+ ActiveRecord::RuntimeRegistry.connection_handler = handler
end
+
+ self.default_connection_handler = ConnectionAdapters::ConnectionHandler.new
end
module ClassMethods
- def inherited(child_class) #:nodoc:
- child_class.initialize_generated_modules
- super
- end
-
def initialize_generated_modules
- @attribute_methods_mutex = Mutex.new
+ super
- # force attribute methods to be higher in inheritance hierarchy than other generated methods
- generated_attribute_methods
generated_feature_methods
end
@@ -115,6 +108,8 @@ module ActiveRecord
super
elsif abstract_class?
"#{super}(abstract)"
+ elsif !connected?
+ "#{super}(no database connection)"
elsif table_exists?
attr_list = columns.map { |c| "#{c.name}: #{c.type}" } * ', '
"#{super}(#{attr_list})"
@@ -139,13 +134,18 @@ module ActiveRecord
# Returns the Arel engine.
def arel_engine
- @arel_engine ||= connection_handler.retrieve_connection_pool(self) ? self : active_record_super.arel_engine
+ @arel_engine ||=
+ if Base == self || connection_handler.retrieve_connection_pool(self)
+ self
+ else
+ superclass.arel_engine
+ 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)
@@ -162,19 +162,23 @@ module ActiveRecord
#
# ==== Example:
# # Instantiates a single new object
- # User.new(:first_name => 'Jamie')
- def initialize(attributes = nil)
+ # User.new(first_name: 'Jamie')
+ 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?
@@ -192,7 +196,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
@@ -241,9 +246,7 @@ module ActiveRecord
run_callbacks(:initialize) unless _initialize_callbacks.empty?
@changed_attributes = {}
- self.class.column_defaults.each do |attr, orig_value|
- @changed_attributes[attr] = orig_value if _field_changed?(attr, orig_value, @attributes[attr])
- end
+ init_changed_attributes
@aggregation_cache = {}
@association_cache = {}
@@ -252,7 +255,6 @@ module ActiveRecord
@new_record = true
ensure_proper_type
- populate_with_current_scope_attributes
super
end
@@ -284,7 +286,7 @@ module ActiveRecord
def ==(comparison_object)
super ||
comparison_object.instance_of?(self.class) &&
- id.present? &&
+ id &&
comparison_object.id == id
end
alias :eql? :==
@@ -295,9 +297,11 @@ module ActiveRecord
id.hash
end
- # Freeze the attributes hash such that associations are still accessible, even on destroyed records.
+ # Clone and freeze the attributes hash such that associations are still
+ # accessible, even on destroyed records, but cloned models will not be
+ # frozen.
def freeze
- @attributes.freeze
+ @attributes = @attributes.clone.freeze
self
end
@@ -306,13 +310,6 @@ module ActiveRecord
@attributes.frozen?
end
- # Allows sort on objects
- def <=>(other_object)
- if other_object.is_a?(self.class)
- self.to_key <=> other_object.to_key
- end
- end
-
# Returns +true+ if the record is read only. Records loaded through joins with piggy-back
# attributes will be marked as read only since they cannot be saved.
def readonly?
@@ -324,16 +321,15 @@ 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
- self.class.connection
+ def connection_handler
+ self.class.connection_handler
end
# Returns the contents of the record as a nicely formatted string.
def inspect
- inspection = if @attributes
+ # We check defined?(@attributes) not to issue warnings if the object is
+ # allocated but not initialized.
+ inspection = if defined?(@attributes) && @attributes
self.class.column_names.collect { |name|
if has_attribute?(name)
"#{name}: #{attribute_for_inspect(name)}"
@@ -347,11 +343,57 @@ 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:
+ @transaction_state = state
+ end
+
+ def has_transactional_callbacks? # :nodoc:
+ !_rollback_callbacks.empty? || !_commit_callbacks.empty? || !_create_callbacks.empty?
end
private
+ # Updates the attributes on this particular ActiveRecord object so that
+ # if it is associated with a transaction, then the state of the AR object
+ # will be updated to reflect the current state of the transaction
+ #
+ # The @transaction_state variable stores the states of the associated
+ # transaction. This relies on the fact that a transaction can only be in
+ # one rollback or commit (otherwise a list of states would be required)
+ # Each AR object inside of a transaction carries that transaction's
+ # TransactionState.
+ #
+ # This method checks to see if the ActiveRecord object's state reflects
+ # the TransactionState, and rolls back or commits the ActiveRecord object
+ # as appropriate.
+ #
+ # Since ActiveRecord objects can be inside multiple transactions, this
+ # method recursively goes through the parent of the TransactionState and
+ # checks if the ActiveRecord object reflects the state of the object.
+ def sync_with_transaction_state
+ update_attributes_from_transaction_state(@transaction_state, 0)
+ end
+
+ def update_attributes_from_transaction_state(transaction_state, depth)
+ if transaction_state && !has_transactional_callbacks?
+ unless @reflects_state[depth]
+ if transaction_state.committed?
+ committed!
+ elsif transaction_state.rolledback?
+ rolledback!
+ end
+ @reflects_state[depth] = true
+ end
+
+ if transaction_state.parent && !@reflects_state[depth+1]
+ update_attributes_from_transaction_state(transaction_state.parent, depth+1)
+ end
+ end
+ end
+
# Under Ruby 1.9, Array#flatten will call #to_ary (recursively) on each of the elements
# of the array, and then rescues from the possible NoMethodError. If those elements are
# ActiveRecord::Base's, then this triggers the various method_missing's that we have,
@@ -368,17 +410,33 @@ module ActiveRecord
pk = self.class.primary_key
@attributes[pk] = nil unless @attributes.key?(pk)
- @aggregation_cache = {}
- @association_cache = {}
- @attributes_cache = {}
- @previously_changed = {}
- @changed_attributes = {}
- @readonly = false
- @destroyed = false
- @marked_for_destruction = false
- @new_record = true
- @txn = nil
+ @aggregation_cache = {}
+ @association_cache = {}
+ @attributes_cache = {}
+ @readonly = false
+ @destroyed = false
+ @marked_for_destruction = false
+ @destroyed_by_association = nil
+ @new_record = true
+ @txn = nil
@_start_transaction_state = {}
+ @transaction_state = nil
+ @reflects_state = [false]
+ end
+
+ def init_changed_attributes
+ # Intentionally avoid using #column_defaults since overridden defaults (as is done in
+ # 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])
+ 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 57838ff984..e1faadf1ab 100644
--- a/activerecord/lib/active_record/counter_cache.rb
+++ b/activerecord/lib/active_record/counter_cache.rb
@@ -11,7 +11,7 @@ module ActiveRecord
# ==== Parameters
#
# * +id+ - The id of the object you wish to reset a counter on.
- # * +counters+ - One or more counter names to reset
+ # * +counters+ - One or more association counters to reset
#
# ==== Examples
#
@@ -21,6 +21,7 @@ module ActiveRecord
object = find(id)
counters.each do |association|
has_many_association = reflect_on_association(association.to_sym)
+ raise ArgumentError, "'#{self.name}' has no association called '#{association}'" unless has_many_association
if has_many_association.is_a? ActiveRecord::Reflection::ThroughReflection
has_many_association = has_many_association.through_reflection
@@ -49,14 +50,14 @@ 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
#
# # For the Post with id of 5, decrement the comment_count by 1, and
# # increment the action_count by 1
- # Post.update_counters 5, :comment_count => -1, :action_count => 1
+ # Post.update_counters 5, comment_count: -1, action_count: 1
# # Executes the following SQL:
# # UPDATE posts
# # SET comment_count = COALESCE(comment_count, 0) - 1,
@@ -64,7 +65,7 @@ module ActiveRecord
# # WHERE id = 5
#
# # For the Posts with id of 10 and 15, increment the comment_count by 1
- # Post.update_counters [10, 15], :comment_count => 1
+ # Post.update_counters [10, 15], comment_count: 1
# # Executes the following SQL:
# # UPDATE posts
# # SET comment_count = COALESCE(comment_count, 0) + 1
@@ -79,16 +80,17 @@ module ActiveRecord
where(primary_key => id).update_all updates.join(', ')
end
- # Increment a number field by one, usually representing a count.
+ # Increment a numeric field by one, via a direct SQL update.
#
- # This is used for caching aggregate values, so that they don't need to be computed every time.
- # For example, a DiscussionBoard may cache post_count and comment_count otherwise every time the board is
- # shown it would have to run an SQL query to find how many posts and comments there are.
+ # 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.
#
# ==== Parameters
#
# * +counter_name+ - The name of the field that should be incremented.
- # * +id+ - The id of the object that should be incremented.
+ # * +id+ - The id of the object that should be incremented or an Array of ids.
#
# ==== Examples
#
@@ -98,14 +100,15 @@ module ActiveRecord
update_counters(id, counter_name => 1)
end
- # Decrement a number field by one, usually representing a count.
+ # Decrement a numeric field by one, via a direct SQL update.
#
- # This works the same as increment_counter but reduces the column value by 1 instead of increasing it.
+ # This works the same as increment_counter but reduces the column value by
+ # 1 instead of increasing it.
#
# ==== Parameters
#
# * +counter_name+ - The name of the field that should be decremented.
- # * +id+ - The id of the object that should be decremented.
+ # * +id+ - The id of the object that should be decremented or an Array of ids.
#
# ==== Examples
#
diff --git a/activerecord/lib/active_record/dynamic_matchers.rb b/activerecord/lib/active_record/dynamic_matchers.rb
index 3bac31c6aa..e650ebcf64 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
diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb
index 0637dd58b6..7e38719811 100644
--- a/activerecord/lib/active_record/errors.rb
+++ b/activerecord/lib/active_record/errors.rb
@@ -22,7 +22,7 @@ module ActiveRecord
# end
#
# # Comments are not patches, this assignment raises AssociationTypeMismatch.
- # @ticket.patches << Comment.new(:content => "Please attach tests to your patch.")
+ # @ticket.patches << Comment.new(content: "Please attach tests to your patch.")
class AssociationTypeMismatch < ActiveRecordError
end
@@ -57,26 +57,23 @@ module ActiveRecord
class RecordNotDestroyed < ActiveRecordError
end
- # Raised when SQL statement cannot be executed by the database (for example, it's often the case for
- # MySQL when Ruby driver used is too old).
+ # Superclass for all database execution errors.
+ #
+ # Wraps the underlying database error as +original_exception+.
class StatementInvalid < ActiveRecordError
- end
-
- # Raised when SQL statement is invalid and the application gets a blank result.
- class ThrowResult < ActiveRecordError
- end
-
- # Parent class for all specific exceptions which wrap database driver exceptions
- # provides access to the original exception also.
- class WrappedDatabaseException < StatementInvalid
attr_reader :original_exception
- def initialize(message, original_exception)
+ def initialize(message, original_exception = nil)
super(message)
@original_exception = original_exception
end
end
+ # Defunct wrapper class kept for compatibility.
+ # +StatementInvalid+ wraps the original exception now.
+ class WrappedDatabaseException < StatementInvalid
+ end
+
# Raised when a record cannot be inserted because it would violate a uniqueness constraint.
class RecordNotUnique < WrappedDatabaseException
end
@@ -158,6 +155,15 @@ module ActiveRecord
# Raised when unknown attributes are supplied via mass assignment.
class UnknownAttributeError < NoMethodError
+
+ attr_reader :record, :attribute
+
+ def initialize(record, attribute)
+ @record = record
+ @attribute = attribute.to_s
+ super("unknown attribute: #{attribute}")
+ end
+
end
# Raised when an error occurred while doing a mass assignment to an attribute through the
@@ -193,6 +199,17 @@ module ActiveRecord
end
+ # Raised when a relation cannot be mutated because it's already loaded.
+ #
+ # class Task < ActiveRecord::Base
+ # end
+ #
+ # relation = Task.all
+ # relation.loaded? # => true
+ #
+ # # Methods which try to mutate a loaded relation fail.
+ # relation.where!(title: 'TODO') # => ActiveRecord::ImmutableRelation
+ # relation.limit!(5) # => ActiveRecord::ImmutableRelation
class ImmutableRelation < ActiveRecordError
end
diff --git a/activerecord/lib/active_record/explain.rb b/activerecord/lib/active_record/explain.rb
index 9e0390bed1..e65dab07ba 100644
--- a/activerecord/lib/active_record/explain.rb
+++ b/activerecord/lib/active_record/explain.rb
@@ -1,58 +1,22 @@
require 'active_support/lazy_load_hooks'
+require 'active_record/explain_registry'
module ActiveRecord
- ActiveSupport.on_load(:active_record_config) do
- mattr_accessor :auto_explain_threshold_in_seconds, instance_accessor: false
- end
-
module Explain
- delegate :auto_explain_threshold_in_seconds, :auto_explain_threshold_in_seconds=, to: 'ActiveRecord::Model'
-
- # If auto explain is enabled, this method triggers EXPLAIN logging for the
- # queries triggered by the block if it takes more than the threshold as a
- # whole. That is, the threshold is not checked against each individual
- # query, but against the duration of the entire block. This approach is
- # convenient for relations.
- #
- # The available_queries_for_explain thread variable collects the queries
- # to be explained. If the value is nil, it means queries are not being
- # currently collected. A false value indicates collecting is turned
- # off. Otherwise it is an array of queries.
- def logging_query_plan # :nodoc:
- return yield unless logger
-
- threshold = auto_explain_threshold_in_seconds
- current = Thread.current
- if threshold && current[:available_queries_for_explain].nil?
- begin
- queries = current[:available_queries_for_explain] = []
- start = Time.now
- result = yield
- logger.warn(exec_explain(queries)) if Time.now - start > threshold
- result
- ensure
- current[:available_queries_for_explain] = nil
- end
- else
- yield
- end
- end
-
- # Relation#explain needs to be able to collect the queries regardless of
- # whether auto explain is enabled. This method serves that purpose.
+ # Executes the block with the collect flag enabled. Queries are collected
+ # asynchronously by the subscriber and returned.
def collecting_queries_for_explain # :nodoc:
- current = Thread.current
- original, current[:available_queries_for_explain] = current[:available_queries_for_explain], []
- return yield, current[:available_queries_for_explain]
+ ExplainRegistry.collect = true
+ yield
+ ExplainRegistry.queries
ensure
- # Note that the return value above does not depend on this assigment.
- current[:available_queries_for_explain] = original
+ ExplainRegistry.reset
end
# Makes the adapter execute EXPLAIN for the tuples of queries and bindings.
# Returns a formatted string ready to be logged.
def exec_explain(queries) # :nodoc:
- str = queries && queries.map do |sql, bind|
+ str = queries.map do |sql, bind|
[].tap do |msg|
msg << "EXPLAIN for: #{sql}"
unless bind.empty?
@@ -67,22 +31,8 @@ module ActiveRecord
def str.inspect
self
end
- str
- end
- # Silences automatic EXPLAIN logging for the duration of the block.
- #
- # This has high priority, no EXPLAINs will be run even if downwards
- # the threshold is set to 0.
- #
- # As the name of the method suggests this only applies to automatic
- # EXPLAINs, manual calls to <tt>ActiveRecord::Relation#explain</tt> run.
- def silence_auto_explain
- current = Thread.current
- original, current[:available_queries_for_explain] = current[:available_queries_for_explain], false
- yield
- ensure
- current[:available_queries_for_explain] = original
+ str
end
end
end
diff --git a/activerecord/lib/active_record/explain_registry.rb b/activerecord/lib/active_record/explain_registry.rb
new file mode 100644
index 0000000000..f5cd57e075
--- /dev/null
+++ b/activerecord/lib/active_record/explain_registry.rb
@@ -0,0 +1,30 @@
+require 'active_support/per_thread_registry'
+
+module ActiveRecord
+ # This is a thread locals registry for EXPLAIN. For example
+ #
+ # ActiveRecord::ExplainRegistry.queries
+ #
+ # returns the collected queries local to the current thread.
+ #
+ # See the documentation of <tt>ActiveSupport::PerThreadRegistry</tt>
+ # for further details.
+ class ExplainRegistry # :nodoc:
+ extend ActiveSupport::PerThreadRegistry
+
+ attr_accessor :queries, :collect
+
+ def initialize
+ reset
+ end
+
+ def collect?
+ @collect
+ end
+
+ def reset
+ @collect = false
+ @queries = []
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/explain_subscriber.rb b/activerecord/lib/active_record/explain_subscriber.rb
index 0f927496fb..6a49936644 100644
--- a/activerecord/lib/active_record/explain_subscriber.rb
+++ b/activerecord/lib/active_record/explain_subscriber.rb
@@ -1,4 +1,5 @@
require 'active_support/notifications'
+require 'active_record/explain_registry'
module ActiveRecord
class ExplainSubscriber # :nodoc:
@@ -7,8 +8,8 @@ module ActiveRecord
end
def finish(name, id, payload)
- if queries = Thread.current[:available_queries_for_explain]
- queries << payload.values_at(:sql, :binds) unless ignore_payload?(payload)
+ if ExplainRegistry.collect? && !ignore_payload?(payload)
+ ExplainRegistry.queries << payload.values_at(:sql, :binds)
end
end
@@ -18,7 +19,7 @@ module ActiveRecord
# On the other hand, we want to monitor the performance of our real database
# queries, not the performance of the access to the query cache.
IGNORED_PAYLOADS = %w(SCHEMA EXPLAIN CACHE)
- EXPLAINED_SQLS = /\A\s*(select|update|delete|insert)/i
+ EXPLAINED_SQLS = /\A\s*(select|update|delete|insert)\b/i
def ignore_payload?(payload)
payload[:exception] || IGNORED_PAYLOADS.include?(payload[:name]) || payload[:sql] !~ EXPLAINED_SQLS
end
diff --git a/activerecord/lib/active_record/fixture_set/file.rb b/activerecord/lib/active_record/fixture_set/file.rb
index 11b53275e1..fbd7a4d891 100644
--- a/activerecord/lib/active_record/fixture_set/file.rb
+++ b/activerecord/lib/active_record/fixture_set/file.rb
@@ -24,7 +24,6 @@ module ActiveRecord
rows.each(&block)
end
- RESCUE_ERRORS = [ ArgumentError, Psych::SyntaxError ] # :nodoc:
private
def rows
@@ -32,7 +31,7 @@ module ActiveRecord
begin
data = YAML.load(render(IO.read(@file)))
- rescue *RESCUE_ERRORS => error
+ rescue ArgumentError, Psych::SyntaxError => error
raise Fixture::FormatError, "a YAML error occurred parsing #{@file}. Please note that YAML must be consistently indented using spaces. Tabs are not allowed. Please have a look at http://www.yaml.org/faq.html\nThe exact error was:\n #{error.class}: #{error}", error.backtrace
end
@rows = data ? validate(data).to_a : []
diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb
index 413bd147de..3bb3131bd1 100644
--- a/activerecord/lib/active_record/fixtures.rb
+++ b/activerecord/lib/active_record/fixtures.rb
@@ -5,8 +5,6 @@ require 'active_support/dependencies'
require 'active_record/fixture_set/file'
require 'active_record/errors'
-require 'active_support/deprecation' # temporary
-
module ActiveRecord
class FixtureClassNotFound < ActiveRecord::ActiveRecordError #:nodoc:
end
@@ -250,7 +248,7 @@ module ActiveRecord
#
# ### in fruit.rb
#
- # belongs_to :eater, :polymorphic => true
+ # belongs_to :eater, polymorphic: true
#
# ### in fruits.yml
#
@@ -365,11 +363,11 @@ module ActiveRecord
#
# first:
# name: Smurf
- # *DEFAULTS
+ # <<: *DEFAULTS
#
# second:
# name: Fraggle
- # *DEFAULTS
+ # <<: *DEFAULTS
#
# Any fixture labeled "DEFAULTS" is safely ignored.
class FixtureSet
@@ -381,16 +379,16 @@ module ActiveRecord
@@all_cached_fixtures = Hash.new { |h,k| h[k] = {} }
- 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
@@ -438,9 +436,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
@@ -454,10 +490,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
@@ -499,27 +537,31 @@ module ActiveRecord
Zlib.crc32(label.to_s) % MAX_ID
end
- attr_reader :table_name, :name, :fixtures, :model_class
+ 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)
@@ -541,7 +583,7 @@ module ActiveRecord
# Return 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
@@ -553,7 +595,7 @@ module ActiveRecord
rows[table_name] = fixtures.map do |label, fixture|
row = fixture.to_hash
- if model_class && model_class < ActiveRecord::Model
+ 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|
@@ -563,7 +605,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
@@ -593,14 +635,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
@@ -611,11 +648,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 }
@@ -634,12 +710,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)
@@ -648,8 +724,8 @@ module ActiveRecord
end
end
- def yaml_file_path
- "#{@path}.yml"
+ def yaml_file_path(path)
+ "#{path}.yml"
end
end
@@ -657,6 +733,7 @@ module ActiveRecord
#--
# Deprecate 'Fixtures' in favor of 'FixtureSet'.
#++
+ # :nodoc:
Fixtures = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('ActiveRecord::Fixtures', 'ActiveRecord::FixtureSet')
class Fixture #:nodoc:
@@ -703,24 +780,33 @@ module ActiveRecord
module TestFixtures
extend ActiveSupport::Concern
- included do
- setup :setup_fixtures
- teardown :teardown_fixtures
+ def before_setup
+ setup_fixtures
+ super
+ end
+
+ def after_teardown
+ super
+ teardown_fixtures
+ end
- class_attribute :fixture_path
+ included do
+ class_attribute :fixture_path, :instance_writer => false
class_attribute :fixture_table_names
class_attribute :fixture_class_names
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
@@ -729,40 +815,31 @@ module ActiveRecord
#
# Examples:
#
- # set_fixture_class :some_fixture => SomeModel,
+ # set_fixture_class some_fixture: SomeModel,
# '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"].map { |f|
- File.basename f, '.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
def try_to_load_dependency(file_name)
require_dependency file_name
rescue LoadError => e
- # Let's hope the developer has included it himself
-
+ # Let's hope the developer has included it
# Let's warn in case this is a subdependency, otherwise
# subdependency error messages are totally cryptic
if ActiveRecord::Base.logger
@@ -770,7 +847,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
@@ -778,7 +855,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
@@ -830,9 +907,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
@@ -846,7 +921,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
@@ -857,48 +932,45 @@ 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 unless defined?(ActiveRecord) && !ActiveRecord::Base.configurations.blank?
-
- unless run_in_transaction?
- ActiveRecord::FixtureSet.reset_cache
- end
-
# Rollback changes if a transaction is active.
if run_in_transaction?
@fixture_connections.each do |connection|
connection.rollback_transaction if connection.transaction_open?
end
@fixture_connections.clear
+ else
+ ActiveRecord::FixtureSet.reset_cache
end
+
ActiveRecord::Base.clear_active_connections!
end
def enlist_fixture_connections
- ActiveRecord::Base.connection_handler.connection_pools.map(&:connection)
+ ActiveRecord::Base.connection_handler.connection_pool_list.map(&:connection)
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?)
diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb
index 35273b0d81..7e1e120288 100644
--- a/activerecord/lib/active_record/inheritance.rb
+++ b/activerecord/lib/active_record/inheritance.rb
@@ -1,29 +1,41 @@
+require 'active_support/core_ext/hash/indifferent_access'
module ActiveRecord
- ActiveSupport.on_load(:active_record_config) do
- # Determine whether to store the full constant name including namespace when using STI
- mattr_accessor :store_full_sti_class, instance_accessor: false
- self.store_full_sti_class = true
- end
-
module Inheritance
extend ActiveSupport::Concern
included do
- config_attribute :store_full_sti_class
+ # 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
module ClassMethods
- # True if this isn't a concrete subclass needing a STI type condition.
- def descends_from_active_record?
- sup = active_record_super
+ # Determines if one of the attributes passed in is the inheritance column,
+ # and if the inheritance column is attr accessible, it initializes an
+ # instance of the given subclass instead of the base class.
+ def new(*args, &block)
+ if abstract_class? || self == Base
+ raise NotImplementedError, "#{self} is an abstract class and can not be instantiated."
+ end
+ if (attrs = args.first).is_a?(Hash)
+ if subclass = subclass_from_attrs(attrs)
+ return subclass.new(*args, &block)
+ end
+ end
+ # Delegate to the original .new
+ super
+ end
- if sup.abstract_class?
- sup.descends_from_active_record?
- elsif self == Base
+ # 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
+ elsif superclass.abstract_class?
+ superclass.descends_from_active_record?
else
- [Base, Model].include?(sup) || !columns_hash.include?(inheritance_column)
+ superclass == Base || !columns_hash.include?(inheritance_column)
end
end
@@ -40,9 +52,8 @@ module ActiveRecord
@symbolized_sti_name ||= sti_name.present? ? sti_name.to_sym : symbolized_base_class
end
- # Returns the class descending directly from ActiveRecord::Base (or
- # that includes ActiveRecord::Model), or an abstract class, if any, in
- # the inheritance hierarchy.
+ # Returns the class descending directly from ActiveRecord::Base, or
+ # an abstract class, if any, in the inheritance hierarchy.
#
# If A extends AR::Base, A.base_class will return A. If B descends from A
# through some arbitrarily deep hierarchy, B.base_class will return A.
@@ -50,15 +61,14 @@ module ActiveRecord
# If B < A and C < B and if A is an abstract_class then both B.base_class
# and C.base_class would return B as the answer since A is an abstract_class.
def base_class
- unless self < ActiveRecord::Tag
+ unless self < Base
raise ActiveRecordError, "#{name} doesn't belong in a hierarchy descending from ActiveRecord"
end
- sup = active_record_super
- if sup == Base || sup == Model || sup.abstract_class?
+ if superclass == Base || superclass.abstract_class?
self
else
- sup.base_class
+ superclass.base_class
end
end
@@ -88,23 +98,6 @@ module ActiveRecord
store_full_sti_class ? name : name.demodulize
end
- # Finder methods must instantiate through this method to work with the
- # single-table inheritance model that makes it possible to create
- # objects of different types from the same table.
- def instantiate(record, column_types = {})
- sti_class = find_sti_class(record[inheritance_column])
- column_types = sti_class.decorate_columns(column_types)
- sti_class.allocate.init_with('attributes' => record, 'column_types' => column_types)
- end
-
- # For internal use.
- #
- # If this class includes ActiveRecord::Model then it won't have a
- # superclass. So this provides a way to get to the 'root' (ActiveRecord::Model).
- def active_record_super #:nodoc:
- superclass < Model ? superclass : Model
- end
-
protected
# Returns the class type of the record using the current module as a prefix. So descendants of
@@ -124,9 +117,10 @@ module ActiveRecord
begin
constant = ActiveSupport::Dependencies.constantize(candidate)
return constant if candidate == constant.to_s
- rescue NameError => e
- # We don't want to swallow NoMethodError < NameError errors
- raise e unless e.instance_of?(NameError)
+ # We don't want to swallow NoMethodError < NameError errors
+ rescue NoMethodError
+ raise
+ rescue NameError
end
end
@@ -136,32 +130,59 @@ module ActiveRecord
private
+ # Called by +instantiate+ to decide which class to use for a new
+ # record instance. For single-table inheritance, we check the record
+ # for a +type+ column and return the corresponding class.
+ def discriminate_class_for_record(record)
+ if using_single_table_inheritance?(record)
+ find_sti_class(record[inheritance_column])
+ else
+ super
+ end
+ end
+
+ def using_single_table_inheritance?(record)
+ record[inheritance_column].present? && columns_hash.include?(inheritance_column)
+ end
+
def find_sti_class(type_name)
- if type_name.blank? || !columns_hash.include?(inheritance_column)
- self
+ if store_full_sti_class
+ ActiveSupport::Dependencies.constantize(type_name)
else
- begin
- if store_full_sti_class
- ActiveSupport::Dependencies.constantize(type_name)
- else
- compute_type(type_name)
- end
- rescue NameError
- raise SubclassNotFound,
- "The single-table inheritance mechanism failed to locate the subclass: '#{type_name}'. " +
- "This error is raised because the column '#{inheritance_column}' is reserved for storing the class in case of inheritance. " +
- "Please rename this column if you didn't intend it to be used for storing the inheritance class " +
- "or overwrite #{name}.inheritance_column to use another column for that information."
- end
+ compute_type(type_name)
end
+ rescue NameError
+ raise SubclassNotFound,
+ "The single-table inheritance mechanism failed to locate the subclass: '#{type_name}'. " +
+ "This error is raised because the column '#{inheritance_column}' is reserved for storing the class in case of inheritance. " +
+ "Please rename this column if you didn't intend it to be used for storing the inheritance class " +
+ "or overwrite #{name}.inheritance_column to use another column for that information."
end
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)
end
+
+ # Detect the subclass from the inheritance column of attrs. If the inheritance column value
+ # is not self or a valid subclass, raises ActiveRecord::SubclassNotFound
+ # If this is a StrongParameters hash, and access to inheritance_column is not permitted,
+ # this will ignore the inheritance column and return nil
+ def subclass_from_attrs(attrs)
+ subclass_name = attrs.with_indifferent_access[inheritance_column]
+
+ if subclass_name.present? && subclass_name != self.name
+ subclass = subclass_name.safe_constantize
+
+ unless descendants.include?(subclass)
+ raise ActiveRecord::SubclassNotFound.new("Invalid single-table inheritance type: #{subclass_name} is not a subclass of #{name}")
+ end
+
+ subclass
+ end
+ end
end
private
diff --git a/activerecord/lib/active_record/integration.rb b/activerecord/lib/active_record/integration.rb
index 23c272ef12..2589b2f3da 100644
--- a/activerecord/lib/active_record/integration.rb
+++ b/activerecord/lib/active_record/integration.rb
@@ -1,5 +1,18 @@
module ActiveRecord
module Integration
+ extend ActiveSupport::Concern
+
+ included do
+ ##
+ # :singleton-method:
+ # Indicates the format used to generate the timestamp in the cache key.
+ # Accepts any of the symbols in <tt>Time::DATE_FORMATS</tt>.
+ #
+ # This is +:nsec+, by default.
+ class_attribute :cache_timestamp_format, :instance_writer => false
+ self.cache_timestamp_format = :nsec
+ end
+
# Returns a String, which Action Pack uses for constructing an URL to this
# object. The default implementation returns this record's id as a String,
# or nil if this record's unsaved.
@@ -8,7 +21,7 @@ module ActiveRecord
# <tt>resources :users</tt> route. Normally, +user_path+ will
# construct a path with the user object's 'id' in it:
#
- # user = User.find_by_name('Phusion')
+ # user = User.find_by(name: 'Phusion')
# user_path(user) # => "/users/1"
#
# You can override +to_param+ in your model to make +user_path+ construct
@@ -20,7 +33,7 @@ module ActiveRecord
# end
# end
#
- # user = User.find_by_name('Phusion')
+ # user = User.find_by(name: 'Phusion')
# user_path(user) # => "/users/Phusion"
def to_param
# We can't use alias_method here, because method 'id' optimizes itself on the fly.
@@ -29,8 +42,6 @@ module ActiveRecord
# Returns a cache key that can be used to identify this record.
#
- # ==== Examples
- #
# 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)
@@ -38,8 +49,8 @@ module ActiveRecord
case
when new_record?
"#{self.class.model_name.cache_key}/new"
- when timestamp = self[:updated_at]
- timestamp = timestamp.utc.to_s(:nsec)
+ when timestamp = max_updated_column_timestamp
+ timestamp = timestamp.utc.to_s(cache_timestamp_format)
"#{self.class.model_name.cache_key}/#{id}-#{timestamp}"
else
"#{self.class.model_name.cache_key}/#{id}"
diff --git a/activerecord/lib/active_record/locale/en.yml b/activerecord/lib/active_record/locale/en.yml
index 896132d566..b1fbd38622 100644
--- a/activerecord/lib/active_record/locale/en.yml
+++ b/activerecord/lib/active_record/locale/en.yml
@@ -4,11 +4,15 @@ en:
#created_at: "Created at"
#updated_at: "Updated at"
+ # Default error messages
+ errors:
+ messages:
+ taken: "has already been taken"
+
# Active Record models configuration
activerecord:
errors:
messages:
- taken: "has already been taken"
record_invalid: "Validation failed: %{errors}"
restrict_dependent_destroy:
one: "Cannot delete record because a dependent %{record} exists"
diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb
index e96ed00f9c..55776a91c0 100644
--- a/activerecord/lib/active_record/locking/optimistic.rb
+++ b/activerecord/lib/active_record/locking/optimistic.rb
@@ -1,9 +1,4 @@
module ActiveRecord
- ActiveSupport.on_load(:active_record_config) do
- mattr_accessor :lock_optimistically, instance_accessor: false
- self.lock_optimistically = true
- end
-
module Locking
# == What is Optimistic Locking
#
@@ -56,7 +51,8 @@ module ActiveRecord
extend ActiveSupport::Concern
included do
- config_attribute :lock_optimistically
+ class_attribute :lock_optimistically, instance_writer: false
+ self.lock_optimistically = true
end
def locking_enabled? #:nodoc:
@@ -70,7 +66,7 @@ module ActiveRecord
send(lock_col + '=', previous_lock_value + 1)
end
- def update(attribute_names = @attributes.keys) #:nodoc:
+ def update_record(attribute_names = @attributes.keys) #:nodoc:
return super unless locking_enabled?
return 0 if attribute_names.empty?
@@ -86,11 +82,11 @@ 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))
- affected_rows = connection.update stmt
+ affected_rows = self.class.connection.update stmt
unless affected_rows == 1
raise ActiveRecord::StaleObjectError.new(self, "update")
@@ -121,7 +117,7 @@ module ActiveRecord
if locking_enabled?
column_name = self.class.locking_column
column = self.class.columns_hash[column_name]
- substitute = connection.substitute_at(column, relation.bind_values.length)
+ substitute = self.class.connection.substitute_at(column, relation.bind_values.length)
relation = relation.where(self.class.arel_table[column_name].eq(substitute))
relation.bind_values << [column, self[column_name].to_i]
@@ -142,6 +138,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
@@ -153,6 +150,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 58af92f0b1..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
@@ -26,7 +26,7 @@ module ActiveRecord
#
# Account.transaction do
# # select * from accounts where ...
- # accounts = Account.where(...).all
+ # accounts = Account.where(...)
# account1 = accounts.detect { |account| ... }
# account2 = accounts.detect { |account| ... }
# # select * from accounts where id=? for update
@@ -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 ca79950049..927fbab8f0 100644
--- a/activerecord/lib/active_record/log_subscriber.rb
+++ b/activerecord/lib/active_record/log_subscriber.rb
@@ -1,13 +1,13 @@
module ActiveRecord
class LogSubscriber < ActiveSupport::LogSubscriber
IGNORE_PAYLOAD_NAMES = ["SCHEMA", "EXPLAIN"]
-
+
def self.runtime=(value)
- Thread.current[:active_record_sql_runtime] = value
+ ActiveRecord::RuntimeRegistry.sql_runtime = value
end
def self.runtime
- Thread.current[:active_record_sql_runtime] ||= 0
+ ActiveRecord::RuntimeRegistry.sql_runtime ||= 0
end
def self.reset_runtime
@@ -17,7 +17,19 @@ module ActiveRecord
def initialize
super
- @odd_or_even = false
+ @odd = false
+ end
+
+ def render_bind(column, value)
+ if column
+ if column.binary?
+ value = "<#{value.bytesize} bytes of binary data>"
+ end
+
+ [column.name, value]
+ else
+ [nil, value]
+ end
end
def sql(event)
@@ -29,12 +41,12 @@ 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?
binds = " " + payload[:binds].map { |col,v|
- [col.name, v]
+ render_bind(col, v)
}.inspect
end
@@ -48,17 +60,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 d5ee98382d..5224a6b67c 100644
--- a/activerecord/lib/active_record/migration.rb
+++ b/activerecord/lib/active_record/migration.rb
@@ -32,7 +32,7 @@ module ActiveRecord
class PendingMigrationError < ActiveRecordError#:nodoc:
def initialize
- super("Migrations are pending run 'rake db:migrate RAILS_ENV=#{ENV['RAILS_ENV']}' to resolve the issue")
+ super("Migrations are pending; run 'bin/rake db:migrate RAILS_ENV=#{::Rails.env}' to resolve this issue.")
end
end
@@ -102,7 +102,7 @@ module ActiveRecord
# table definition.
# * <tt>drop_table(name)</tt>: Drops the table called +name+.
# * <tt>change_table(name, options)</tt>: Allows to make column alterations to
- # the table called +name+. It makes the table object availabe to a block that
+ # the table called +name+. It makes the table object available to a block that
# can then add/remove columns, indexes or foreign keys to it.
# * <tt>rename_table(old_name, new_name)</tt>: Renames the table called +old_name+
# to +new_name+.
@@ -120,8 +120,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.
@@ -330,6 +330,24 @@ module ActiveRecord
#
# For a list of commands that are reversible, please see
# <tt>ActiveRecord::Migration::CommandRecorder</tt>.
+ #
+ # == Transactional Migrations
+ #
+ # If the database adapter supports DDL transactions, all migrations will
+ # automatically be wrapped in a transaction. There are queries that you
+ # can't execute inside a transaction though, and for these situations
+ # you can turn the automatic transactions off.
+ #
+ # class ChangeEnum < ActiveRecord::Migration
+ # disable_ddl_transaction!
+ #
+ # def up
+ # execute "ALTER TYPE model_size ADD VALUE 'new_value'"
+ # end
+ # end
+ #
+ # Remember that you can still open your own transactions, even if you
+ # are in a Migration with <tt>self.disable_ddl_transaction!</tt>.
class Migration
autoload :CommandRecorder, 'active_record/migration/command_recorder'
@@ -339,54 +357,175 @@ module ActiveRecord
class CheckPending
def initialize(app)
@app = app
+ @last_check = 0
end
def call(env)
- ActiveRecord::Migration.check_pending!
+ mtime = ActiveRecord::Migrator.last_migration.mtime.to_i
+ if @last_check < mtime
+ ActiveRecord::Migration.check_pending!
+ @last_check = mtime
+ end
@app.call(env)
end
end
class << self
attr_accessor :delegate # :nodoc:
- end
+ attr_accessor :disable_ddl_transaction # :nodoc:
- 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)
+ def method_missing(name, *args, &block) # :nodoc:
+ (delegate || superclass.delegate).send(name, *args, &block)
+ end
+
+ def migrate(direction)
+ new.migrate direction
+ end
+
+ # Disable DDL transactions for this migration.
+ def disable_ddl_transaction!
+ @disable_ddl_transaction = true
+ end
end
- def self.migrate(direction)
- new.migrate direction
+ def disable_ddl_transaction # :nodoc:
+ self.class.disable_ddl_transaction
end
cattr_accessor :verbose
-
attr_accessor :name, :version
def initialize(name = self.class.name, version = nil)
@name = name
@version = version
@connection = nil
- @reverting = false
end
+ self.verbose = true
# instantiate the delegate object after initialize is defined
- self.verbose = true
self.delegate = new
- def revert
- @reverting = true
- yield
- ensure
- @reverting = false
+ # Reverses the migration commands for the given block and
+ # the given migrations.
+ #
+ # The following migration will remove the table 'horses'
+ # and create the table 'apples' on the way up, and the reverse
+ # on the way down.
+ #
+ # class FixTLMigration < ActiveRecord::Migration
+ # def change
+ # revert do
+ # create_table(:horses) do |t|
+ # t.text :content
+ # t.datetime :remind_at
+ # end
+ # end
+ # create_table(:apples) do |t|
+ # t.string :variety
+ # end
+ # end
+ # end
+ #
+ # Or equivalently, if +TenderloveMigration+ is defined as in the
+ # documentation for Migration:
+ #
+ # require_relative '2012121212_tenderlove_migration'
+ #
+ # class FixupTLMigration < ActiveRecord::Migration
+ # def change
+ # revert TenderloveMigration
+ #
+ # create_table(:apples) do |t|
+ # t.string :variety
+ # end
+ # end
+ # end
+ #
+ # This command can be nested.
+ def revert(*migration_classes)
+ run(*migration_classes.reverse, revert: true) unless migration_classes.empty?
+ if block_given?
+ if @connection.respond_to? :revert
+ @connection.revert { yield }
+ else
+ recorder = CommandRecorder.new(@connection)
+ @connection = recorder
+ suppress_messages do
+ @connection.revert { yield }
+ end
+ @connection = recorder.delegate
+ recorder.commands.each do |cmd, args, block|
+ send(cmd, *args, &block)
+ end
+ end
+ end
end
def reverting?
- @reverting
+ @connection.respond_to?(:reverting) && @connection.reverting
+ end
+
+ class ReversibleBlockHelper < Struct.new(:reverting) # :nodoc:
+ def up
+ yield unless reverting
+ end
+
+ def down
+ yield if reverting
+ end
+ end
+
+ # Used to specify an operation that can be run in one direction or another.
+ # Call the methods +up+ and +down+ of the yielded object to run a block
+ # only in one given direction.
+ # The whole block will be called in the right order within the migration.
+ #
+ # In the following example, the looping on users will always be done
+ # when the three columns 'first_name', 'last_name' and 'full_name' exist,
+ # even when migrating down:
+ #
+ # class SplitNameMigration < ActiveRecord::Migration
+ # def change
+ # add_column :users, :first_name, :string
+ # add_column :users, :last_name, :string
+ #
+ # reversible do |dir|
+ # User.reset_column_information
+ # User.all.each do |u|
+ # dir.up { u.first_name, u.last_name = u.full_name.split(' ') }
+ # dir.down { u.full_name = "#{u.first_name} #{u.last_name}" }
+ # u.save
+ # end
+ # end
+ #
+ # revert { add_column :users, :full_name, :string }
+ # end
+ # end
+ def reversible
+ helper = ReversibleBlockHelper.new(reverting?)
+ execute_block{ yield helper }
+ end
+
+ # Runs the given migration classes.
+ # Last argument can specify options:
+ # - :direction (default is :up)
+ # - :revert (default is false)
+ def run(*migration_classes)
+ opts = migration_classes.extract_options!
+ dir = opts[:direction] || :up
+ dir = (dir == :down ? :up : :down) if opts[:revert]
+ if reverting?
+ # If in revert and going :up, say, we want to execute :down without reverting, so
+ revert { run(*migration_classes, direction: dir, revert: true) }
+ else
+ migration_classes.each do |migration_class|
+ migration_class.new.exec_migration(@connection, dir)
+ end
+ end
end
def up
@@ -412,29 +551,9 @@ module ActiveRecord
time = nil
ActiveRecord::Base.connection_pool.with_connection do |conn|
- @connection = conn
- if respond_to?(:change)
- if direction == :down
- recorder = CommandRecorder.new(@connection)
- suppress_messages do
- @connection = recorder
- change
- end
- @connection = conn
- time = Benchmark.measure {
- self.revert {
- recorder.inverse.each do |cmd, args|
- send(cmd, *args)
- end
- }
- }
- else
- time = Benchmark.measure { change }
- end
- else
- time = Benchmark.measure { send(direction) }
+ time = Benchmark.measure do
+ exec_migration(conn, direction)
end
- @connection = nil
end
case direction
@@ -443,6 +562,21 @@ module ActiveRecord
end
end
+ def exec_migration(conn, direction)
+ @connection = conn
+ if respond_to?(:change)
+ if direction == :down
+ revert { change }
+ else
+ change
+ end
+ else
+ send(direction)
+ end
+ ensure
+ @connection = nil
+ end
+
def write(text="")
puts(text) if verbose
end
@@ -481,10 +615,10 @@ module ActiveRecord
arg_list = arguments.map{ |a| a.inspect } * ', '
say_with_time "#{method}(#{arg_list})" do
- unless reverting?
+ 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)
@@ -495,7 +629,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
@@ -503,8 +637,17 @@ module ActiveRecord
source_migrations = ActiveRecord::Migrator.migrations(path)
source_migrations.each do |migration|
- source = File.read(migration.filename)
- source = "# This migration comes from #{scope} (originally #{migration.version})\n#{source}"
+ source = File.binread(migration.filename)
+ inserted_comment = "# This migration comes from #{scope} (originally #{migration.version})\n"
+ if /\A#.*\b(?:en)?coding:\s*\S+/ =~ source
+ # If we have a magic comment in the original migration,
+ # insert our comment after the first newline(end of the magic comment line)
+ # so the magic keep working.
+ # Note that magic comments must be at the first line(except sh-bang).
+ source[/\n/] = "\n#{inserted_comment}"
+ else
+ source = "#{inserted_comment}#{source}"
+ end
if duplicate = destination_migrations.detect { |m| m.name == migration.name }
if options[:on_skip] && duplicate.scope != scope.to_s
@@ -518,7 +661,7 @@ module ActiveRecord
old_path, migration.filename = migration.filename, new_path
last = migration
- File.open(migration.filename, "w") { |f| f.write source }
+ File.binwrite(migration.filename, source)
copied << migration
options[:on_copy].call(scope, migration, old_path) if options[:on_copy]
destination_migrations << migration
@@ -528,6 +671,18 @@ 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
[Time.now.utc.strftime("%Y%m%d%H%M%S"), "%.14d" % number].max
@@ -535,6 +690,22 @@ module ActiveRecord
"%.3d" % number
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
+ super # use normal delegation to record the block
+ else
+ yield
+ end
+ end
end
# MigrationProxy is used to defer loading of the actual migration classes
@@ -550,7 +721,11 @@ module ActiveRecord
File.basename(filename)
end
- delegate :migrate, :announce, :write, :to => :migration
+ def mtime
+ File.mtime filename
+ end
+
+ delegate :migrate, :announce, :write, :disable_ddl_transaction, to: :migration
private
@@ -565,6 +740,16 @@ module ActiveRecord
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
@@ -635,12 +820,24 @@ module ActiveRecord
end
def last_version
- migrations(migrations_paths).last.try(:version)||0
+ last_migration.version
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
- name.table_name rescue "#{ActiveRecord::Base.table_name_prefix}#{name}#{ActiveRecord::Base.table_name_suffix}"
+ def last_migration #:nodoc:
+ migrations(migrations_paths).last || NullMigration.new
+ end
+
+ 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
+ "#{options[:table_name_prefix]}#{name}#{options[:table_name_suffix]}"
+ end
end
def migrations_paths
@@ -659,7 +856,7 @@ module ActiveRecord
files = Dir[*paths.map { |p| "#{p}/**/[0-9]*_*.rb" }]
migrations = files.map do |file|
- version, name, scope = file.scan(/([0-9]+)_([_a-z0-9]*)\.?([_a-z0-9]*)?.rb/).first
+ version, name, scope = file.scan(/([0-9]+)_([_a-z0-9]*)\.?([_a-z0-9]*)?\.rb\z/).first
raise IllegalMigrationNameError.new(file) unless version
version = version.to_i
@@ -691,13 +888,7 @@ 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)
@@ -705,7 +896,7 @@ module ActiveRecord
end
def current_version
- migrated.sort.last || 0
+ migrated.max || 0
end
def current_migration
@@ -714,11 +905,15 @@ module ActiveRecord
alias :current :current_migration
def run
- target = migrations.detect { |m| m.version == @target_version }
- raise UnknownMigrationVersionError.new(@target_version) if target.nil?
- unless (up? && migrated.include?(target.version.to_i)) || (down? && !migrated.include?(target.version.to_i))
- target.migrate(@direction)
- record_version_state_after_migrating(target.version)
+ migration = migrations.detect { |m| m.version == @target_version }
+ raise UnknownMigrationVersionError.new(@target_version) if migration.nil?
+ unless (up? && migrated.include?(migration.version.to_i)) || (down? && !migrated.include?(migration.version.to_i))
+ begin
+ execute_migration_in_transaction(migration, @direction)
+ rescue => e
+ canceled_msg = use_transaction?(migration) ? ", this migration was canceled" : ""
+ raise StandardError, "An error has occurred#{canceled_msg}:\n\n#{e}", e.backtrace
+ end
end
end
@@ -727,25 +922,13 @@ module ActiveRecord
raise UnknownMigrationVersionError.new(@target_version)
end
- running = runnable
-
- if block_given?
- ActiveSupport::Deprecation.warn(<<-eomsg)
-block argument to migrate is deprecated, please filter migrations before constructing the migrator
- eomsg
- 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
- ddl_transaction do
- migration.migrate(@direction)
- record_version_state_after_migrating(migration.version)
- end
+ execute_migration_in_transaction(migration, @direction)
rescue => e
- canceled_msg = Base.connection.supports_ddl_transactions? ? "this and " : ""
+ canceled_msg = use_transaction?(migration) ? "this and " : ""
raise StandardError, "An error has occurred, #{canceled_msg}all later migrations canceled:\n\n#{e}", e.backtrace
end
end
@@ -780,6 +963,13 @@ block argument to migrate is deprecated, please filter migrations before constru
migrated.include?(migration.version.to_i)
end
+ def execute_migration_in_transaction(migration, direction)
+ ddl_transaction(migration) do
+ migration.migrate(direction)
+ record_version_state_after_migrating(migration.version)
+ end
+ end
+
def target
migrations.detect { |m| m.version == @target_version }
end
@@ -819,12 +1009,16 @@ block argument to migrate is deprecated, please filter migrations before constru
end
# Wrap the migration in a transaction only if supported by the adapter.
- def ddl_transaction
- if Base.connection.supports_ddl_transactions?
+ def ddl_transaction(migration)
+ if use_transaction?(migration)
Base.transaction { yield }
else
yield
end
end
+
+ def use_transaction?(migration)
+ !migration.disable_ddl_transaction && Base.connection.supports_ddl_transactions?
+ end
end
end
diff --git a/activerecord/lib/active_record/migration/command_recorder.rb b/activerecord/lib/active_record/migration/command_recorder.rb
index 95f4360578..01c73be849 100644
--- a/activerecord/lib/active_record/migration/command_recorder.rb
+++ b/activerecord/lib/active_record/migration/command_recorder.rb
@@ -16,69 +16,118 @@ module ActiveRecord
class CommandRecorder
include JoinTable
- attr_accessor :commands, :delegate
+ attr_accessor :commands, :delegate, :reverting
def initialize(delegate = nil)
@commands = []
@delegate = delegate
+ @reverting = false
+ end
+
+ # While executing the given block, the recorded will be in reverting mode.
+ # All commands recorded will end up being recorded reverted
+ # and in reverse order.
+ # For example:
+ #
+ # recorder.revert{ recorder.record(:rename_table, [:old, :new]) }
+ # # same effect as recorder.record(:rename_table, [:new, :old])
+ def revert
+ @reverting = !@reverting
+ previous = @commands
+ @commands = []
+ yield
+ ensure
+ @commands = previous.concat(@commands.reverse)
+ @reverting = !@reverting
end
# record +command+. +command+ should be a method name and arguments.
# For example:
#
# recorder.record(:method_name, [:arg1, :arg2])
- def record(*command)
- @commands << command
+ def record(*command, &block)
+ if @reverting
+ @commands << inverse_of(*command, &block)
+ else
+ @commands << (command << block)
+ end
end
- # Returns a list that represents commands that are the inverse of the
- # commands stored in +commands+. For example:
+ # Returns the inverse of the given command. For example:
#
- # recorder.record(:rename_table, [:old, :new])
- # recorder.inverse # => [:rename_table, [:new, :old]]
+ # recorder.inverse_of(:rename_table, [:old, :new])
+ # # => [:rename_table, [:new, :old]]
#
# This method will raise an +IrreversibleMigration+ exception if it cannot
- # invert the +commands+.
- def inverse
- @commands.reverse.map { |name, args|
- method = :"invert_#{name}"
- raise IrreversibleMigration unless respond_to?(method, true)
- send(method, args)
- }
+ # invert the +command+.
+ def inverse_of(command, args, &block)
+ method = :"invert_#{command}"
+ raise IrreversibleMigration unless respond_to?(method, true)
+ send(method, args, &block)
end
def respond_to?(*args) # :nodoc:
super || delegate.respond_to?(*args)
end
- [:create_table, :create_join_table, :change_table, :rename_table, :add_column, :remove_column, :rename_index, :rename_column, :add_index, :remove_index, :add_timestamps, :remove_timestamps, :change_column, :change_column_default, :add_reference, :remove_reference].each do |method|
+ [:create_table, :create_join_table, :rename_table, :add_column, :remove_column,
+ :rename_index, :rename_column, :add_index, :remove_index, :add_timestamps, :remove_timestamps,
+ :change_column_default, :add_reference, :remove_reference, :transaction,
+ :drop_join_table, :drop_table, :execute_block, :enable_extension,
+ :change_column, :execute, :remove_columns, # irreversible methods need to be here too
+ ].each do |method|
class_eval <<-EOV, __FILE__, __LINE__ + 1
- def #{method}(*args) # def create_table(*args)
- record(:"#{method}", args) # record(:create_table, args)
- end # end
+ def #{method}(*args, &block) # def create_table(*args, &block)
+ record(:"#{method}", args, &block) # record(:create_table, args, &block)
+ end # end
EOV
end
alias :add_belongs_to :add_reference
alias :remove_belongs_to :remove_reference
- private
-
- def invert_create_table(args)
- [:drop_table, [args.first]]
+ def change_table(table_name, options = {})
+ yield ConnectionAdapters::Table.new(table_name, self)
end
- def invert_create_join_table(args)
- table_name = find_join_table_name(*args)
+ private
- [:drop_table, [table_name]]
+ module StraightReversions
+ private
+ { transaction: :transaction,
+ execute_block: :execute_block,
+ create_table: :drop_table,
+ create_join_table: :drop_join_table,
+ 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
+ def invert_#{method}(args, &block) # def invert_create_table(args, &block)
+ [:#{inverse}, args, block] # [:drop_table, args, block]
+ end # end
+ EOV
+ end
+ end
+ end
+
+ include StraightReversions
+
+ def invert_drop_table(args, &block)
+ if args.size == 1 && block == nil
+ raise ActiveRecord::IrreversibleMigration, "To avoid mistakes, drop_table is only reversible if given options or a block (can be empty)."
+ end
+ super
end
def invert_rename_table(args)
[:rename_table, args.reverse]
end
- def invert_add_column(args)
- [:remove_column, args.first(2)]
+ def invert_remove_column(args)
+ raise ActiveRecord::IrreversibleMigration, "remove_column is only reversible if given a type." if args.size <= 2
+ super
end
def invert_rename_index(args)
@@ -91,27 +140,21 @@ module ActiveRecord
def invert_add_index(args)
table, columns, options = *args
- index_name = options.try(:[], :name)
- options_hash = index_name ? {:name => index_name} : {:column => columns}
- [:remove_index, [table, options_hash]]
+ [:remove_index, [table, (options || {}).merge(column: columns)]]
end
- def invert_remove_timestamps(args)
- [:add_timestamps, args]
- end
+ def invert_remove_index(args)
+ table, options = *args
- def invert_add_timestamps(args)
- [:remove_timestamps, args]
- end
+ unless options && options.is_a?(Hash) && options[:column]
+ raise ActiveRecord::IrreversibleMigration, "remove_index is only reversible if given a :column option."
+ end
- def invert_add_reference(args)
- [:remove_reference, args]
+ options = options.dup
+ [:add_index, [table, options.delete(:column), options]]
end
- alias :invert_add_belongs_to :invert_add_reference
- def invert_remove_reference(args)
- [:add_reference, args]
- end
+ alias :invert_add_belongs_to :invert_add_reference
alias :invert_remove_belongs_to :invert_remove_reference
# Forwards any missing method call to the \target.
diff --git a/activerecord/lib/active_record/migration/join_table.rb b/activerecord/lib/active_record/migration/join_table.rb
index e880ae97bb..ebf64cbcdc 100644
--- a/activerecord/lib/active_record/migration/join_table.rb
+++ b/activerecord/lib/active_record/migration/join_table.rb
@@ -8,7 +8,7 @@ module ActiveRecord
end
def join_table_name(table_1, table_2)
- [table_1, table_2].sort.join("_").to_sym
+ [table_1.to_s, table_2.to_s].sort.join("_").to_sym
end
end
end
diff --git a/activerecord/lib/active_record/model.rb b/activerecord/lib/active_record/model.rb
deleted file mode 100644
index f059840f4d..0000000000
--- a/activerecord/lib/active_record/model.rb
+++ /dev/null
@@ -1,167 +0,0 @@
-require 'active_support/core_ext/module/attribute_accessors'
-
-module ActiveRecord
- module Configuration # :nodoc:
- # This just abstracts out how we define configuration options in AR. Essentially we
- # have mattr_accessors on the ActiveRecord:Model constant that define global defaults.
- # Classes that then use AR get class_attributes defined, which means that when they
- # are assigned the default will be overridden for that class and subclasses. (Except
- # when options[:global] == true, in which case there is one global value always.)
- def config_attribute(name, options = {})
- if options[:global]
- class_eval <<-CODE, __FILE__, __LINE__ + 1
- def self.#{name}; ActiveRecord::Model.#{name}; end
- def #{name}; ActiveRecord::Model.#{name}; end
- def self.#{name}=(val); ActiveRecord::Model.#{name} = val; end
- CODE
- else
- options[:instance_writer] ||= false
- class_attribute name, options
-
- singleton_class.class_eval <<-CODE, __FILE__, __LINE__ + 1
- remove_method :#{name}
- def #{name}; ActiveRecord::Model.#{name}; end
- CODE
- end
- end
- end
-
- # This allows us to detect an ActiveRecord::Model while it's in the process of
- # being included.
- module Tag; end
-
- # <tt>ActiveRecord::Model</tt> can be included into a class to add Active Record
- # persistence. This is an alternative to inheriting from <tt>ActiveRecord::Base</tt>.
- #
- # class Post
- # include ActiveRecord::Model
- # end
- module Model
- extend ActiveSupport::Concern
- extend ConnectionHandling
- extend ActiveModel::Observing::ClassMethods
-
- def self.append_features(base)
- base.class_eval do
- include Tag
- extend Configuration
- end
-
- super
- end
-
- included do
- extend ActiveModel::Naming
- extend ActiveSupport::Benchmarkable
- extend ActiveSupport::DescendantsTracker
-
- extend QueryCache::ClassMethods
- extend Querying
- extend Translation
- extend DynamicMatchers
- extend Explain
- extend ConnectionHandling
-
- initialize_generated_modules unless self == Base
- end
-
- include Persistence
- include ReadonlyAttributes
- include ModelSchema
- include Inheritance
- include Scoping
- include Sanitization
- include AttributeAssignment
- include ActiveModel::Conversion
- include Integration
- include Validations
- include CounterCache
- include Locking::Optimistic
- include Locking::Pessimistic
- include AttributeMethods
- include Callbacks
- include ActiveModel::Observing
- include Timestamp
- include Associations
- include ActiveModel::SecurePassword
- include AutosaveAssociation
- include NestedAttributes
- include Aggregations
- include Transactions
- include Reflection
- include Serialization
- include Store
- include Core
-
- class << self
- def arel_engine
- self
- end
-
- def abstract_class?
- false
- end
-
- # Defines the name of the table column which will store the class name on single-table
- # inheritance situations.
- #
- # The default inheritance column name is +type+, which means it's a
- # reserved word inside Active Record. To be able to use single-table
- # inheritance with another column name, or to use the column +type+ in
- # your own model for something else, you can set +inheritance_column+:
- #
- # self.inheritance_column = 'zoink'
- def inheritance_column
- 'type'
- end
- end
-
- class DeprecationProxy < BasicObject #:nodoc:
- def initialize(model = Model, base = Base)
- @model = model
- @base = base
- end
-
- def method_missing(name, *args, &block)
- if @model.respond_to?(name, true)
- @model.send(name, *args, &block)
- else
- ::ActiveSupport::Deprecation.warn(
- "The object passed to the active_record load hook was previously ActiveRecord::Base " \
- "(a Class). Now it is ActiveRecord::Model (a Module). You have called `#{name}' which " \
- "is only defined on ActiveRecord::Base. Please change your code so that it works with " \
- "a module rather than a class. (Model is included in Base, so anything added to Model " \
- "will be available on Base as well.)"
- )
- @base.send(name, *args, &block)
- end
- end
-
- alias send method_missing
-
- def extend(*mods)
- ::ActiveSupport::Deprecation.warn(
- "The object passed to the active_record load hook was previously ActiveRecord::Base " \
- "(a Class). Now it is ActiveRecord::Model (a Module). You have called `extend' which " \
- "would add singleton methods to Model. This is presumably not what you want, since the " \
- "methods would not be inherited down to Base. Rather than using extend, please use " \
- "ActiveSupport::Concern + include, which will ensure that your class methods are " \
- "inherited."
- )
- @base.extend(*mods)
- end
- end
- end
-
- # This hook is where config accessors on Model get defined.
- #
- # We don't want to just open the Model module and add stuff to it in other files, because
- # that would cause Model to load, which causes all sorts of loading order issues.
- #
- # We need this hook rather than just using the :active_record one, because users of the
- # :active_record hook may need to use config options.
- ActiveSupport.run_load_hooks(:active_record_config, Model)
-
- # Load Base at this point, because the active_record load hook is run in that file.
- Base
-end
diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb
index 99de16cd33..dc5ff02882 100644
--- a/activerecord/lib/active_record/model_schema.rb
+++ b/activerecord/lib/active_record/model_schema.rb
@@ -1,18 +1,4 @@
-
module ActiveRecord
- ActiveSupport.on_load(:active_record_config) do
- mattr_accessor :primary_key_prefix_type, instance_accessor: false
-
- mattr_accessor :table_name_prefix, instance_accessor: false
- self.table_name_prefix = ""
-
- mattr_accessor :table_name_suffix, instance_accessor: false
- self.table_name_suffix = ""
-
- mattr_accessor :pluralize_table_names, instance_accessor: false
- self.pluralize_table_names = true
- end
-
module ModelSchema
extend ActiveSupport::Concern
@@ -24,7 +10,7 @@ module ActiveRecord
# the Product class will look for "productid" instead of "id" as the primary column. If the
# latter is specified, the Product class will look for "product_id" instead of "id". Remember
# that this is a global setting for all Active Records.
- config_attribute :primary_key_prefix_type, global: true
+ mattr_accessor :primary_key_prefix_type, instance_writer: false
##
# :singleton-method:
@@ -36,20 +22,31 @@ module ActiveRecord
# If you are organising your models within modules you can add a prefix to the models within
# a namespace by defining a singleton method in the parent module called table_name_prefix which
# returns your chosen prefix.
- config_attribute :table_name_prefix
+ class_attribute :table_name_prefix, instance_writer: false
+ self.table_name_prefix = ""
##
# :singleton-method:
# Works like +table_name_prefix+, but appends instead of prepends (set to "_basecamp" gives "projects_basecamp",
# "people_basecamp"). By default, the suffix is the empty string.
- config_attribute :table_name_suffix
+ class_attribute :table_name_suffix, instance_writer: false
+ self.table_name_suffix = ""
+
+ ##
+ # :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.
- config_attribute :pluralize_table_names
+ class_attribute :pluralize_table_names, instance_writer: false
+ self.pluralize_table_names = true
+
+ self.inheritance_column = 'type'
end
module ClassMethods
@@ -133,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.
@@ -144,9 +141,9 @@ module ActiveRecord
# Computes the table name, (re)sets it internally, and returns it.
def reset_table_name #:nodoc:
self.table_name = if abstract_class?
- active_record_super == Base ? nil : active_record_super.table_name
- elsif active_record_super.abstract_class?
- active_record_super.table_name || compute_table_name
+ superclass == Base ? nil : superclass.table_name
+ elsif superclass.abstract_class?
+ superclass.table_name || compute_table_name
else
compute_table_name
end
@@ -156,9 +153,17 @@ module ActiveRecord
(parents.detect{ |p| p.respond_to?(:table_name_prefix) } || self).table_name_prefix
end
- # The name of the column containing the object's class when Single Table Inheritance is used
+ # Defines the name of the table column which will store the class name on single-table
+ # inheritance situations.
+ #
+ # The default inheritance column name is +type+, which means it's a
+ # reserved word inside Active Record. To be able to use single-table
+ # inheritance with another column name, or to use the column +type+ in
+ # your own model for something else, you can set +inheritance_column+:
+ #
+ # self.inheritance_column = 'zoink'
def inheritance_column
- (@inheritance_column ||= nil) || active_record_super.inheritance_column
+ (@inheritance_column ||= nil) || superclass.inheritance_column
end
# Sets the value of inheritance_column
@@ -206,7 +211,7 @@ module ActiveRecord
# Returns an array of column objects for the table associated with this class.
def columns
- @columns ||= connection.schema_cache.columns[table_name].map do |col|
+ @columns ||= connection.schema_cache.columns(table_name).map do |col|
col = col.dup
col.primary = (col.name == primary_key)
col
@@ -225,14 +230,20 @@ module ActiveRecord
def decorate_columns(columns_hash) # :nodoc:
return if columns_hash.empty?
- serialized_attributes.each_key do |key|
- columns_hash[key] = AttributeMethods::Serialization::Type.new(columns_hash[key])
+ @serialized_column_names ||= self.columns_hash.keys.find_all do |name|
+ serialized_attributes.key?(name)
end
- columns_hash.each do |name, col|
- if create_time_zone_conversion_attribute?(name, col)
- columns_hash[name] = AttributeMethods::TimeZoneConversion::Type.new(col)
- 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
@@ -255,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.
#
@@ -286,7 +284,7 @@ module ActiveRecord
#
# JobLevel.reset_column_information
# %w{assistant executive manager director}.each do |type|
- # JobLevel.create(:name => type)
+ # JobLevel.create(name: type)
# end
# end
#
@@ -299,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
@@ -331,7 +332,7 @@ module ActiveRecord
base = base_class
if self == base
# Nested classes are prefixed with singular parent table name.
- if parent < ActiveRecord::Model && !parent.abstract_class?
+ if parent < Base && !parent.abstract_class?
contained = parent.table_name
contained = contained.singularize if parent.pluralize_table_names
contained += '_'
diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb
index 2e7fb3bbb3..df28451bb7 100644
--- a/activerecord/lib/active_record/nested_attributes.rb
+++ b/activerecord/lib/active_record/nested_attributes.rb
@@ -3,11 +3,6 @@ require 'active_support/core_ext/object/try'
require 'active_support/core_ext/hash/indifferent_access'
module ActiveRecord
- ActiveSupport.on_load(:active_record_config) do
- mattr_accessor :nested_attributes_options, instance_accessor: false
- self.nested_attributes_options = {}
- end
-
module NestedAttributes #:nodoc:
class TooManyRecords < ActiveRecordError
end
@@ -15,7 +10,8 @@ module ActiveRecord
extend ActiveSupport::Concern
included do
- config_attribute :nested_attributes_options
+ class_attribute :nested_attributes_options, instance_writer: false
+ self.nested_attributes_options = {}
end
# = Active Record Nested Attributes
@@ -54,15 +50,15 @@ module ActiveRecord
# Enabling nested attributes on a one-to-one association allows you to
# create the member and avatar in one go:
#
- # params = { :member => { :name => 'Jack', :avatar_attributes => { :icon => 'smiling' } } }
+ # params = { member: { name: 'Jack', avatar_attributes: { icon: 'smiling' } } }
# member = Member.create(params[:member])
# member.avatar.id # => 2
# member.avatar.icon # => 'smiling'
#
# It also allows you to update the avatar through the member:
#
- # params = { :member => { :avatar_attributes => { :id => '2', :icon => 'sad' } } }
- # member.update_attributes params[:member]
+ # params = { member: { avatar_attributes: { id: '2', icon: 'sad' } } }
+ # member.update params[:member]
# member.avatar.icon # => 'sad'
#
# By default you will only be able to set and update attributes on the
@@ -72,13 +68,13 @@ module ActiveRecord
#
# class Member < ActiveRecord::Base
# has_one :avatar
- # accepts_nested_attributes_for :avatar, :allow_destroy => true
+ # accepts_nested_attributes_for :avatar, allow_destroy: true
# end
#
# Now, when you add the <tt>_destroy</tt> key to the attributes hash, with a
# value that evaluates to +true+, you will destroy the associated model:
#
- # member.avatar_attributes = { :id => '2', :_destroy => '1' }
+ # member.avatar_attributes = { id: '2', _destroy: '1' }
# member.avatar.marked_for_destruction? # => true
# member.save
# member.reload.avatar # => nil
@@ -94,22 +90,23 @@ module ActiveRecord
# accepts_nested_attributes_for :posts
# end
#
- # You can now set or update attributes on an associated post model through
- # the attribute hash.
+ # You can now set or update attributes on the associated posts through
+ # an attribute hash for a member: include the key +:posts_attributes+
+ # with an array of hashes of post attributes as a value.
#
# For each hash that does _not_ have an <tt>id</tt> key a new record will
# be instantiated, unless the hash also contains a <tt>_destroy</tt> key
# that evaluates to +true+.
#
- # params = { :member => {
- # :name => 'joe', :posts_attributes => [
- # { :title => 'Kari, the awesome Ruby documentation browser!' },
- # { :title => 'The egalitarian assumption of the modern citizen' },
- # { :title => '', :_destroy => '1' } # this will be ignored
+ # params = { member: {
+ # name: 'joe', posts_attributes: [
+ # { title: 'Kari, the awesome Ruby documentation browser!' },
+ # { title: 'The egalitarian assumption of the modern citizen' },
+ # { title: '', _destroy: '1' } # this will be ignored
# ]
# }}
#
- # member = Member.create(params['member'])
+ # member = Member.create(params[:member])
# member.posts.length # => 2
# member.posts.first.title # => 'Kari, the awesome Ruby documentation browser!'
# member.posts.second.title # => 'The egalitarian assumption of the modern citizen'
@@ -118,48 +115,48 @@ module ActiveRecord
# hashes if they fail to pass your criteria. For example, the previous
# example could be rewritten as:
#
- # class Member < ActiveRecord::Base
- # has_many :posts
- # accepts_nested_attributes_for :posts, :reject_if => proc { |attributes| attributes['title'].blank? }
- # end
+ # class Member < ActiveRecord::Base
+ # has_many :posts
+ # accepts_nested_attributes_for :posts, reject_if: proc { |attributes| attributes['title'].blank? }
+ # end
#
- # params = { :member => {
- # :name => 'joe', :posts_attributes => [
- # { :title => 'Kari, the awesome Ruby documentation browser!' },
- # { :title => 'The egalitarian assumption of the modern citizen' },
- # { :title => '' } # this will be ignored because of the :reject_if proc
+ # params = { member: {
+ # name: 'joe', posts_attributes: [
+ # { title: 'Kari, the awesome Ruby documentation browser!' },
+ # { title: 'The egalitarian assumption of the modern citizen' },
+ # { title: '' } # this will be ignored because of the :reject_if proc
# ]
# }}
#
- # member = Member.create(params['member'])
+ # member = Member.create(params[:member])
# member.posts.length # => 2
# member.posts.first.title # => 'Kari, the awesome Ruby documentation browser!'
# member.posts.second.title # => 'The egalitarian assumption of the modern citizen'
#
# Alternatively, :reject_if also accepts a symbol for using methods:
#
- # class Member < ActiveRecord::Base
- # has_many :posts
- # accepts_nested_attributes_for :posts, :reject_if => :new_record?
- # end
+ # class Member < ActiveRecord::Base
+ # has_many :posts
+ # accepts_nested_attributes_for :posts, reject_if: :new_record?
+ # end
#
- # class Member < ActiveRecord::Base
- # has_many :posts
- # accepts_nested_attributes_for :posts, :reject_if => :reject_posts
+ # class Member < ActiveRecord::Base
+ # has_many :posts
+ # accepts_nested_attributes_for :posts, reject_if: :reject_posts
#
- # def reject_posts(attributed)
- # attributed['title'].blank?
- # end
- # end
+ # def reject_posts(attributed)
+ # attributed['title'].blank?
+ # end
+ # end
#
# If the hash contains an <tt>id</tt> key that matches an already
# associated record, the matching record will be modified:
#
# member.attributes = {
- # :name => 'Joe',
- # :posts_attributes => [
- # { :id => 1, :title => '[UPDATED] An, as of yet, undisclosed awesome Ruby documentation browser!' },
- # { :id => 2, :title => '[UPDATED] other post' }
+ # name: 'Joe',
+ # posts_attributes: [
+ # { id: 1, title: '[UPDATED] An, as of yet, undisclosed awesome Ruby documentation browser!' },
+ # { id: 2, title: '[UPDATED] other post' }
# ]
# }
#
@@ -174,19 +171,42 @@ module ActiveRecord
#
# class Member < ActiveRecord::Base
# has_many :posts
- # accepts_nested_attributes_for :posts, :allow_destroy => true
+ # accepts_nested_attributes_for :posts, allow_destroy: true
# end
#
- # params = { :member => {
- # :posts_attributes => [{ :id => '2', :_destroy => '1' }]
+ # params = { member: {
+ # posts_attributes: [{ id: '2', _destroy: '1' }]
# }}
#
- # member.attributes = params['member']
+ # member.attributes = params[:member]
# member.posts.detect { |p| p.id == 2 }.marked_for_destruction? # => true
# member.posts.length # => 2
# member.save
# member.reload.posts.length # => 1
#
+ # Nested attributes for an associated collection can also be passed in
+ # the form of a hash of hashes instead of an array of hashes:
+ #
+ # Member.create(name: 'joe',
+ # posts_attributes: { first: { title: 'Foo' },
+ # second: { title: 'Bar' } })
+ #
+ # has the same effect as
+ #
+ # Member.create(name: 'joe',
+ # posts_attributes: [ { title: 'Foo' },
+ # { title: 'Bar' } ])
+ #
+ # The keys of the hash which is the value for +:posts_attributes+ are
+ # ignored in this case.
+ # However, it is not allowed to use +'id'+ or +:id+ for one of
+ # such keys, otherwise the hash will be wrapped in an array and
+ # interpreted as an attribute hash for a single post.
+ #
+ # Passing attributes for an associated collection in the form of a hash
+ # of hashes can be used with hashes generated from HTTP/HTML parameters,
+ # where there maybe no natural way to submit an array of hashes.
+ #
# === Saving
#
# All changes to models, including the destruction of those marked for
@@ -201,14 +221,35 @@ module ActiveRecord
# <tt>inverse_of</tt> as this example illustrates:
#
# class Member < ActiveRecord::Base
- # has_many :posts, :inverse_of => :member
+ # has_many :posts, inverse_of: :member
# accepts_nested_attributes_for :posts
# end
#
# class Post < ActiveRecord::Base
- # belongs_to :member, :inverse_of => :posts
+ # belongs_to :member, inverse_of: :posts
# 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.:
+ #
+ # class Member < ActiveRecord::Base
+ # has_one :avatar
+ # accepts_nested_attributes_for :avatar
+ #
+ # def avatar
+ # super || build_avatar(width: 200)
+ # end
+ # end
+ #
+ # member = Member.new
+ # member.avatar_attributes = {icon: 'sad'}
+ # member.avatar.width # => 200
module ClassMethods
REJECT_ALL_BLANK_PROC = proc { |attributes| attributes.all? { |key, value| key == '_destroy' || value.blank? } }
@@ -252,11 +293,11 @@ module ActiveRecord
#
# Examples:
# # creates avatar_attributes=
- # accepts_nested_attributes_for :avatar, :reject_if => proc { |attributes| attributes['name'].blank? }
+ # accepts_nested_attributes_for :avatar, reject_if: proc { |attributes| attributes['name'].blank? }
# # creates avatar_attributes=
- # accepts_nested_attributes_for :avatar, :reject_if => :all_blank
+ # accepts_nested_attributes_for :avatar, reject_if: :all_blank
# # creates avatar_attributes= and posts_attributes=
- # accepts_nested_attributes_for :avatar, :posts, :allow_destroy => true
+ # accepts_nested_attributes_for :avatar, :posts, allow_destroy: true
def accepts_nested_attributes_for(*attr_names)
options = { :allow_destroy => false, :update_only => false }
options.update(attr_names.extract_options!)
@@ -265,7 +306,7 @@ 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)
nested_attributes_options = self.nested_attributes_options.dup
@@ -273,23 +314,36 @@ module ActiveRecord
self.nested_attributes_options = nested_attributes_options
type = (reflection.collection? ? :collection : :one_to_one)
-
- # def pirate_attributes=(attributes)
- # assign_nested_attributes_for_one_to_one_association(:pirate, attributes, mass_assignment_options)
- # end
- generated_feature_methods.module_eval <<-eoruby, __FILE__, __LINE__ + 1
- if method_defined?(:#{association_name}_attributes=)
- remove_method(:#{association_name}_attributes=)
- end
- def #{association_name}_attributes=(attributes)
- assign_nested_attributes_for_#{type}_association(:#{association_name}, attributes)
- end
- eoruby
+ generate_association_writer(association_name, type)
else
raise ArgumentError, "No association found for name `#{association_name}'. Has it been defined yet?"
end
end
end
+
+ private
+
+ # Generates a writer method for this association. Serves as a point for
+ # accessing the objects in the association. For example, this method
+ # could generate the following:
+ #
+ # def pirate_attributes=(attributes)
+ # assign_nested_attributes_for_one_to_one_association(:pirate, attributes)
+ # end
+ #
+ # This redirects the attempts to write objects in an association through
+ # 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
+ if method_defined?(:#{association_name}_attributes=)
+ remove_method(:#{association_name}_attributes=)
+ end
+ def #{association_name}_attributes=(attributes)
+ assign_nested_attributes_for_#{type}_association(:#{association_name}, attributes)
+ end
+ eoruby
+ end
end
# Returns ActiveRecord::AutosaveAssociation::marked_for_destruction? It's
@@ -323,20 +377,28 @@ module ActiveRecord
def assign_nested_attributes_for_one_to_one_association(association_name, attributes)
options = self.nested_attributes_options[association_name]
attributes = attributes.with_indifferent_access
+ existing_record = send(association_name)
- if (options[:update_only] || !attributes['id'].blank?) && (record = send(association_name)) &&
- (options[:update_only] || record.id.to_s == attributes['id'].to_s)
- assign_to_or_mark_for_destruction(record, attributes, options[:allow_destroy]) unless call_reject_if(association_name, attributes)
+ if (options[:update_only] || !attributes['id'].blank?) && existing_record &&
+ (options[:update_only] || existing_record.id.to_s == attributes['id'].to_s)
+ assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy]) unless call_reject_if(association_name, attributes)
elsif attributes['id'].present?
- raise_nested_attributes_record_not_found(association_name, attributes['id'])
+ raise_nested_attributes_record_not_found!(association_name, attributes['id'])
elsif !reject_new_record?(association_name, attributes)
- method = "build_#{association_name}"
- if respond_to?(method)
- send(method, attributes.except(*UNASSIGNABLE_KEYS))
+ assignable_attributes = attributes.except(*UNASSIGNABLE_KEYS)
+
+ if existing_record && existing_record.new_record?
+ existing_record.assign_attributes(assignable_attributes)
+ association(association_name).initialize_attributes(existing_record)
else
- raise ArgumentError, "Cannot build association `#{association_name}'. Are you trying to build a polymorphic one-to-one association?"
+ method = "build_#{association_name}"
+ if respond_to?(method)
+ send(method, assignable_attributes)
+ else
+ raise ArgumentError, "Cannot build association `#{association_name}'. Are you trying to build a polymorphic one-to-one association?"
+ end
end
end
end
@@ -352,9 +414,9 @@ module ActiveRecord
# For example:
#
# assign_nested_attributes_for_collection_association(:people, {
- # '1' => { :id => '1', :name => 'Peter' },
- # '2' => { :name => 'John' },
- # '3' => { :id => '2', :_destroy => true }
+ # '1' => { id: '1', name: 'Peter' },
+ # '2' => { name: 'John' },
+ # '3' => { id: '2', _destroy: true }
# })
#
# Will update the name of the Person with ID 1, build a new associated
@@ -364,9 +426,9 @@ module ActiveRecord
# Also accepts an Array of attribute hashes:
#
# assign_nested_attributes_for_collection_association(:people, [
- # { :id => '1', :name => 'Peter' },
- # { :name => 'John' },
- # { :id => '2', :_destroy => true }
+ # { id: '1', name: 'Peter' },
+ # { name: 'John' },
+ # { id: '2', _destroy: true }
# ])
def assign_nested_attributes_for_collection_association(association_name, attributes_collection)
options = self.nested_attributes_options[association_name]
@@ -375,20 +437,7 @@ module ActiveRecord
raise ArgumentError, "Hash or Array expected, got #{attributes_collection.class.name} (#{attributes_collection.inspect})"
end
- if limit = options[:limit]
- limit = case limit
- when Symbol
- send(limit)
- when Proc
- limit.call
- else
- limit
- end
-
- if limit && attributes_collection.size > limit
- raise TooManyRecords, "Maximum #{limit} records are allowed. Got #{attributes_collection.size} records instead."
- end
- end
+ check_record_limit!(options[:limit], attributes_collection)
if attributes_collection.is_a? Hash
keys = attributes_collection.keys
@@ -416,23 +465,44 @@ 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
- raise_nested_attributes_record_not_found(association_name, attributes['id'])
+ raise_nested_attributes_record_not_found!(association_name, attributes['id'])
+ end
+ end
+ end
+
+ # Takes in a limit and checks if the attributes_collection has too many
+ # records. The method will take limits in the form of symbols, procs, and
+ # number-like objects (anything that can be compared with an integer).
+ #
+ # Will raise an TooManyRecords error if the attributes_collection is
+ # larger than the limit.
+ def check_record_limit!(limit, attributes_collection)
+ if limit
+ limit = case limit
+ when Symbol
+ send(limit)
+ when Proc
+ limit.call
+ else
+ limit
+ end
+
+ if limit && attributes_collection.size > limit
+ raise TooManyRecords, "Maximum #{limit} records are allowed. Got #{attributes_collection.size} records instead."
end
end
end
@@ -456,6 +526,11 @@ module ActiveRecord
has_destroy_flag?(attributes) || call_reject_if(association_name, attributes)
end
+ # Determines if a record with the particular +attributes+ should be
+ # rejected by calling the reject_if Symbol or Proc (if defined).
+ # The reject_if option is defined by +accepts_nested_attributes_for+.
+ #
+ # Returns false if there is a +destroy_flag+ on the attributes.
def call_reject_if(association_name, attributes)
return false if has_destroy_flag?(attributes)
case callback = self.nested_attributes_options[association_name][:reject_if]
@@ -466,7 +541,7 @@ module ActiveRecord
end
end
- def raise_nested_attributes_record_not_found(association_name, record_id)
+ def raise_nested_attributes_record_not_found!(association_name, record_id)
raise RecordNotFound, "Couldn't find #{self.class.reflect_on_association(association_name).klass.name} with ID=#{record_id} for #{self.class.name} with ID=#{id}"
end
end
diff --git a/activerecord/lib/active_record/null_relation.rb b/activerecord/lib/active_record/null_relation.rb
index 4c1c91e3df..080b20134d 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,19 +39,23 @@ module ActiveRecord
end
def to_sql
- @to_sql ||= ""
+ ""
end
- def where_values_hash
- {}
+ def count(*)
+ 0
end
- def count
+ def sum(*)
0
end
- def calculate(_operation, _column_name, _options = {})
- nil
+ def calculate(_operation, _column_name)
+ if _operation == :count
+ 0
+ else
+ nil
+ end
end
def exists?(_id = false)
diff --git a/activerecord/lib/active_record/observer.rb b/activerecord/lib/active_record/observer.rb
deleted file mode 100644
index 6b2f6f98a5..0000000000
--- a/activerecord/lib/active_record/observer.rb
+++ /dev/null
@@ -1,126 +0,0 @@
-
-module ActiveRecord
- # = Active Record Observer
- #
- # Observer classes respond to life cycle callbacks to implement trigger-like
- # behavior outside the original class. This is a great way to reduce the
- # clutter that normally comes when the model class is burdened with
- # functionality that doesn't pertain to the core responsibility of the
- # class. Example:
- #
- # class CommentObserver < ActiveRecord::Observer
- # def after_save(comment)
- # Notifications.comment("admin@do.com", "New comment was posted", comment).deliver
- # end
- # end
- #
- # This Observer sends an email when a Comment#save is finished.
- #
- # class ContactObserver < ActiveRecord::Observer
- # def after_create(contact)
- # contact.logger.info('New contact added!')
- # end
- #
- # def after_destroy(contact)
- # contact.logger.warn("Contact with an id of #{contact.id} was destroyed!")
- # end
- # end
- #
- # This Observer uses logger to log when specific callbacks are triggered.
- #
- # == Observing a class that can't be inferred
- #
- # Observers will by default be mapped to the class with which they share a name. So CommentObserver will
- # be tied to observing Comment, ProductManagerObserver to ProductManager, and so on. If you want to name your observer
- # differently than the class you're interested in observing, you can use the Observer.observe class method which takes
- # either the concrete class (Product) or a symbol for that class (:product):
- #
- # class AuditObserver < ActiveRecord::Observer
- # observe :account
- #
- # def after_update(account)
- # AuditTrail.new(account, "UPDATED")
- # end
- # end
- #
- # If the audit observer needs to watch more than one kind of object, this can be specified with multiple arguments:
- #
- # class AuditObserver < ActiveRecord::Observer
- # observe :account, :balance
- #
- # def after_update(record)
- # AuditTrail.new(record, "UPDATED")
- # end
- # end
- #
- # The AuditObserver will now act on both updates to Account and Balance by treating them both as records.
- #
- # == Available callback methods
- #
- # The observer can implement callback methods for each of the methods described in the Callbacks module.
- #
- # == Storing Observers in Rails
- #
- # If you're using Active Record within Rails, observer classes are usually stored in app/models with the
- # naming convention of app/models/audit_observer.rb.
- #
- # == Configuration
- #
- # In order to activate an observer, list it in the <tt>config.active_record.observers</tt> configuration
- # setting in your <tt>config/application.rb</tt> file.
- #
- # config.active_record.observers = :comment_observer, :signup_observer
- #
- # Observers will not be invoked unless you define these in your application configuration.
- #
- # If you are using Active Record outside Rails, activate the observers explicitly in a configuration or
- # environment file:
- #
- # ActiveRecord::Base.add_observer CommentObserver.instance
- # ActiveRecord::Base.add_observer SignupObserver.instance
- #
- # == Loading
- #
- # Observers register themselves in the model class they observe, since it is the class that
- # notifies them of events when they occur. As a side-effect, when an observer is loaded its
- # corresponding model class is loaded.
- #
- # Up to (and including) Rails 2.0.2 observers were instantiated between plugins and
- # application initializers. Now observers are loaded after application initializers,
- # so observed models can make use of extensions.
- #
- # If by any chance you are using observed models in the initialization you can still
- # load their observers by calling <tt>ModelObserver.instance</tt> before. Observers are
- # singletons and that call instantiates and registers them.
- #
- class Observer < ActiveModel::Observer
-
- protected
-
- def observed_classes
- klasses = super
- klasses + klasses.map { |klass| klass.descendants }.flatten
- end
-
- def add_observer!(klass)
- super
- define_callbacks klass
- end
-
- def define_callbacks(klass)
- observer = self
- observer_name = observer.class.name.underscore.gsub('/', '__')
-
- ActiveRecord::Callbacks::CALLBACKS.each do |callback|
- next unless respond_to?(callback)
- callback_meth = :"_notify_#{observer_name}_for_#{callback}"
- unless klass.respond_to?(callback_meth)
- klass.send(:define_method, callback_meth) do |&block|
- observer.update(callback, self, &block)
- end
- klass.send(callback, callback_meth)
- end
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb
index 611d3d97c3..a73a140ef1 100644
--- a/activerecord/lib/active_record/persistence.rb
+++ b/activerecord/lib/active_record/persistence.rb
@@ -15,18 +15,18 @@ module ActiveRecord
#
# ==== Examples
# # Create a single new object
- # User.create(:first_name => 'Jamie')
+ # User.create(first_name: 'Jamie')
#
# # Create an Array of new objects
- # User.create([{ :first_name => 'Jamie' }, { :first_name => 'Jeremy' }])
+ # User.create([{ first_name: 'Jamie' }, { first_name: 'Jeremy' }])
#
# # Create a single object and pass it into a block to set other attributes.
- # User.create(:first_name => 'Jamie') do |u|
+ # User.create(first_name: 'Jamie') do |u|
# u.is_admin = false
# end
#
# # Creating an Array of new objects using a block, where the block is executed for each object:
- # User.create([{ :first_name => 'Jamie' }, { :first_name => 'Jeremy' }]) do |u|
+ # User.create([{ first_name: 'Jamie' }, { first_name: 'Jeremy' }]) do |u|
# u.is_admin = false
# end
def create(attributes = nil, &block)
@@ -38,16 +38,44 @@ module ActiveRecord
object
end
end
+
+ # Given an attributes hash, +instantiate+ returns a new instance of
+ # the appropriate class.
+ #
+ # For example, +Post.all+ may return Comments, Messages, and Emails
+ # by storing the record's subclass in a +type+ attribute. By calling
+ # +instantiate+ instead of +new+, finder methods ensure they get new
+ # instances of the appropriate class for each record.
+ #
+ # See +ActiveRecord::Inheritance#discriminate_class_for_record+ to see
+ # how this "single-table" inheritance mapping is implemented.
+ def instantiate(record, column_types = {})
+ klass = discriminate_class_for_record(record)
+ column_types = klass.decorate_columns(column_types.dup)
+ klass.allocate.init_with('attributes' => record, 'column_types' => column_types)
+ end
+
+ private
+ # Called by +instantiate+ to decide which class to use for a new
+ # record instance.
+ #
+ # See +ActiveRecord::Inheritance#discriminate_class_for_record+ for
+ # the single-table inheritance discriminator.
+ def discriminate_class_for_record(record)
+ self
+ end
end
# Returns true if this object hasn't been saved yet -- that is, a record
# for the object doesn't exist in the data store yet; otherwise, returns false.
def new_record?
+ sync_with_transaction_state
@new_record
end
# Returns true if this object has been destroyed, otherwise returns false.
def destroyed?
+ sync_with_transaction_state
@destroyed
end
@@ -64,19 +92,20 @@ module ActiveRecord
#
# By default, save always run validations. If any of them fail the action
# is cancelled and +save+ returns +false+. However, if you supply
- # :validate => false, validations are bypassed altogether. See
+ # validate: false, validations are bypassed altogether. See
# ActiveRecord::Validations for more information.
#
# There's a series of callbacks associated with +save+. If any of the
# <tt>before_*</tt> callbacks return +false+ the action is cancelled and
# +save+ returns +false+. See ActiveRecord::Callbacks for further
# details.
+ #
+ # Attributes marked as readonly are silently ignored if the record is
+ # being updated.
def save(*)
- begin
- create_or_update
- rescue ActiveRecord::RecordInvalid
- false
- end
+ create_or_update
+ rescue ActiveRecord::RecordInvalid
+ false
end
# Saves the model.
@@ -92,6 +121,9 @@ module ActiveRecord
# the <tt>before_*</tt> callbacks return +false+ the action is cancelled
# and <tt>save!</tt> raises ActiveRecord::RecordNotSaved. See
# ActiveRecord::Callbacks for further details.
+ #
+ # Attributes marked as readonly are silently ignored if the record is
+ # being updated.
def save!(*)
create_or_update || raise(RecordNotSaved)
end
@@ -104,7 +136,7 @@ module ActiveRecord
# record's primary key, and no callbacks are executed.
#
# To enforce the object's +before_destroy+ and +after_destroy+
- # callbacks, Observer methods, or any <tt>:dependent</tt> association
+ # callbacks or any <tt>:dependent</tt> association
# options, use <tt>#destroy</tt>.
def delete
self.class.delete(id) if persisted?
@@ -143,7 +175,7 @@ module ActiveRecord
# inheritance structures where you want a subclass to appear as the
# superclass. This can be used along with record identification in
# Action Pack to allow, say, <tt>Client < Company</tt> to do something
- # like render <tt>:partial => @client.becomes(Company)</tt> to render that
+ # like render <tt>partial: @client.becomes(Company)</tt> to render that
# instance using the companies/company partial instead of clients/client.
#
# Note: The new instance will share a link to the same attributes as the original class.
@@ -155,7 +187,18 @@ module ActiveRecord
became.instance_variable_set("@new_record", new_record?)
became.instance_variable_set("@destroyed", destroyed?)
became.instance_variable_set("@errors", errors)
- became.public_send("#{klass.inheritance_column}=", klass.name) unless self.class.descends_from_active_record?
+ became
+ end
+
+ # Wrapper around +becomes+ that also changes the instance's sti column value.
+ # This is especially useful if you want to persist the changed class in your
+ # database.
+ #
+ # Note: The old instance's sti column value will be changed too, as both objects
+ # share the same set of attributes.
+ def becomes!(klass)
+ became = becomes(klass)
+ became.public_send("#{klass.inheritance_column}=", klass.sti_name) unless self.class.descends_from_active_record?
became
end
@@ -167,17 +210,19 @@ module ActiveRecord
# * updated_at/updated_on column is updated if that column is available.
# * Updates all the attributes that are dirty in this object.
#
+ # This method raises an +ActiveRecord::ActiveRecordError+ if the
+ # attribute is marked as readonly.
def update_attribute(name, value)
name = name.to_s
verify_readonly_attribute(name)
send("#{name}=", value)
- save(:validate => false)
+ save(validate: false)
end
# Updates the attributes of the model from the passed-in hash and saves the
# record, all wrapped in a transaction. If the object is invalid, the saving
# will fail and false will be returned.
- def update_attributes(attributes)
+ def update(attributes)
# The following transaction covers any possible database side-effects of the
# attributes assignment. For example, setting the IDs of a child collection.
with_transaction_returning_status do
@@ -186,9 +231,11 @@ module ActiveRecord
end
end
- # Updates its receiver just like +update_attributes+ but calls <tt>save!</tt> instead
+ alias update_attributes update
+
+ # Updates its receiver just like +update+ but calls <tt>save!</tt> instead
# of +save+, so an exception is raised if the record is invalid.
- def update_attributes!(attributes)
+ def update!(attributes)
# The following transaction covers any possible database side-effects of the
# attributes assignment. For example, setting the IDs of a child collection.
with_transaction_returning_status do
@@ -197,26 +244,28 @@ module ActiveRecord
end
end
- # Updates a single attribute of an object, without having to explicitly call save on that object.
- #
- # * Validation is skipped.
- # * Callbacks are skipped.
- # * updated_at/updated_on column is not updated if that column is available.
- #
- # Raises an +ActiveRecordError+ when called on new objects, or when the +name+
- # attribute is marked as readonly.
+ alias update_attributes! update!
+
+ # Equivalent to <code>update_columns(name => value)</code>.
def update_column(name, value)
update_columns(name => value)
end
- # Updates the attributes from the passed-in hash, without having to explicitly call save on that object.
+ # Updates the attributes directly in the database issuing an UPDATE SQL
+ # statement and sets them in the receiver:
#
- # * Validation is skipped.
+ # user.update_columns(last_request_at: Time.current)
+ #
+ # This is the fastest way to update attributes because it goes straight to
+ # the database, but take into account that in consequence the regular update
+ # procedures are totally bypassed. In particular:
+ #
+ # * Validations are skipped.
# * Callbacks are skipped.
- # * updated_at/updated_on column is not updated if that column is available.
+ # * +updated_at+/+updated_on+ are not updated.
#
- # Raises an +ActiveRecordError+ when called on new objects, or when at least
- # one of the attributes is marked as readonly.
+ # 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?
@@ -224,11 +273,13 @@ module ActiveRecord
verify_readonly_attribute(key.to_s)
end
- attributes.each do |k,v|
- raw_write_attribute(k,v)
+ updated_count = self.class.unscoped.where(self.class.primary_key => id).update_all(attributes)
+
+ attributes.each do |k, v|
+ raw_write_attribute(k, v)
end
- self.class.where(self.class.primary_key => id).update_all(attributes) == 1
+ updated_count == 1
end
# Initializes +attribute+ to zero if +nil+ and adds the value passed as +by+ (default is 1).
@@ -282,10 +333,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 commom 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
@@ -298,14 +393,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.
#
@@ -315,16 +412,25 @@ module ActiveRecord
# If used along with +belongs_to+ then +touch+ will invoke +touch+ method on associated object.
#
# class Brake < ActiveRecord::Base
- # belongs_to :car, :touch => true
+ # belongs_to :car, touch: true
# end
#
# class Car < ActiveRecord::Base
- # belongs_to :corporation, :touch => true
+ # belongs_to :corporation, touch: true
# end
#
# # triggers @brake.car.touch and @brake.car.corporation.touch
# @brake.touch
+ #
+ # Note that +touch+ must be used on a persisted object, or else an
+ # ActiveRecordError will be thrown. For example:
+ #
+ # ball = Ball.new
+ # ball.touch(:updated_at) # => raises ActiveRecordError
+ #
def touch(name = nil)
+ raise ActiveRecordError, "can not touch on a new record object" unless persisted?
+
attributes = timestamp_attributes_for_update_in_model
attributes << name if name
@@ -339,7 +445,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
@@ -358,7 +464,7 @@ module ActiveRecord
def relation_for_destroy
pk = self.class.primary_key
column = self.class.columns_hash[pk]
- substitute = connection.substitute_at(column, 0)
+ substitute = self.class.connection.substitute_at(column, 0)
relation = self.class.unscoped.where(
self.class.arel_table[pk].eq(substitute))
@@ -369,23 +475,24 @@ module ActiveRecord
def create_or_update
raise ReadOnlyRecord if readonly?
- result = new_record? ? create : update
+ result = new_record? ? create_record : update_record
result != false
end
# Updates the associated record with values matching those of the instance attributes.
# Returns the number of affected rows.
- def update(attribute_names = @attributes.keys)
- attributes_with_values = arel_attributes_with_values_for_update(attribute_names)
- return 0 if attributes_with_values.empty?
- klass = self.class
- stmt = klass.unscoped.where(klass.arel_table[klass.primary_key].eq(id)).arel.compile_update(attributes_with_values)
- klass.connection.update stmt
+ def update_record(attribute_names = @attributes.keys)
+ attributes_values = arel_attributes_with_values_for_update(attribute_names)
+ if attributes_values.empty?
+ 0
+ else
+ self.class.unscoped.update_record attributes_values, id, id_was
+ end
end
# Creates a record with values matching those of the instance attributes
# and returns its id.
- def create(attribute_names = @attributes.keys)
+ def create_record(attribute_names = @attributes.keys)
attributes_values = arel_attributes_with_values_for_create(attribute_names)
new_id = self.class.unscoped.insert attributes_values
diff --git a/activerecord/lib/active_record/query_cache.rb b/activerecord/lib/active_record/query_cache.rb
index 2bd8ecda20..df8654e5c1 100644
--- a/activerecord/lib/active_record/query_cache.rb
+++ b/activerecord/lib/active_record/query_cache.rb
@@ -4,20 +4,22 @@ module ActiveRecord
class QueryCache
module ClassMethods
# Enable the query cache within the block if Active Record is configured.
+ # If it's not, it will execute the given block.
def cache(&block)
- if ActiveRecord::Base.configurations.blank?
- yield
- else
+ if ActiveRecord::Base.connected?
connection.cache(&block)
+ else
+ yield
end
end
# Disable the query cache within the block if Active Record is configured.
+ # If it's not, it will execute the given block.
def uncached(&block)
- if ActiveRecord::Base.configurations.blank?
- yield
- else
+ if ActiveRecord::Base.connected?
connection.uncached(&block)
+ else
+ yield
end
end
end
diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb
index 13e09eda53..6bee4f38e7 100644
--- a/activerecord/lib/active_record/querying.rb
+++ b/activerecord/lib/active_record/querying.rb
@@ -1,20 +1,21 @@
-
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_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, :references, :none, :to => :all
- delegate :count, :average, :minimum, :maximum, :sum, :calculate, :pluck, :ids, :to => :all
+ :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
# this method from. If you call <tt>Product.find_by_sql</tt> then the results will be returned in
- # a Product object with the attributes you specified in the SQL query.
+ # a +Product+ object with the attributes you specified in the SQL query.
#
# If you call a complicated SQL query which spans multiple tables the columns specified by the
# SELECT will be attributes of the model, whether or not they are columns of the corresponding
@@ -25,27 +26,25 @@ module ActiveRecord
# MySQL specific terms will lock you to using that particular database engine or require you to
# change your call if you switch engines.
#
- # ==== Examples
# # A simple SQL query spanning multiple tables
# Post.find_by_sql "SELECT p.title, c.author FROM posts p, comments c WHERE p.id = c.post_id"
- # > [#<Post:0x36bff9c @attributes={"title"=>"Ruby Meetup", "first_name"=>"Quentin"}>, ...]
+ # # => [#<Post:0x36bff9c @attributes={"title"=>"Ruby Meetup", "first_name"=>"Quentin"}>, ...]
+ #
+ # You can use the same string replacement techniques as you can with <tt>ActiveRecord::QueryMethods#where</tt>:
#
- # # You can use the same string replacement techniques as you can with ActiveRecord#find
# Post.find_by_sql ["SELECT title FROM posts WHERE author = ? AND created > ?", author_id, start_date]
- # > [#<Post:0x36bff9c @attributes={"title"=>"The Cheap Man Buys Twice"}>, ...]
+ # Post.find_by_sql ["SELECT body FROM comments WHERE author = :user_id OR approved_by = :user_id", { :user_id => user_id }]
def find_by_sql(sql, binds = [])
- logging_query_plan do
- result_set = connection.select_all(sanitize_sql(sql), "#{name} Load", binds)
- column_types = {}
-
- if result_set.respond_to? :column_types
- column_types = result_set.column_types
- else
- ActiveSupport::Deprecation.warn "the object returned from `select_all` must respond to `column_types`"
- end
+ result_set = connection.select_all(sanitize_sql(sql), "#{name} Load", binds)
+ column_types = {}
- result_set.map { |record| instantiate(record, column_types) }
+ if result_set.respond_to? :column_types
+ column_types = result_set.column_types
+ else
+ ActiveSupport::Deprecation.warn "the object returned from `select_all` must respond to `column_types`"
end
+
+ result_set.map { |record| instantiate(record, column_types) }
end
# Returns the result of an SQL statement that should only include a COUNT(*) in the SELECT part.
@@ -56,14 +55,10 @@ module ActiveRecord
#
# * +sql+ - An SQL statement which should return a count query from the database, see the example below.
#
- # ==== Examples
- #
# Product.count_by_sql "SELECT COUNT(*) FROM sales s, customers c WHERE s.customer_id = c.id"
def count_by_sql(sql)
- logging_query_plan do
- sql = sanitize_conditions(sql)
- connection.select_value(sql, "#{name} Count").to_i
- end
+ sql = sanitize_conditions(sql)
+ connection.select_value(sql, "#{name} Count").to_i
end
end
end
diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb
index d7e35fb771..eef08aea88 100644
--- a/activerecord/lib/active_record/railtie.rb
+++ b/activerecord/lib/active_record/railtie.rb
@@ -36,6 +36,26 @@ module ActiveRecord
rake_tasks do
require "active_record/base"
+
+ ActiveRecord::Tasks::DatabaseTasks.seed_loader = Rails.application
+ ActiveRecord::Tasks::DatabaseTasks.env = Rails.env
+
+ namespace :db do
+ task :load_config do
+ ActiveRecord::Tasks::DatabaseTasks.db_dir = Rails.application.config.paths["db"].first
+ 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'
+ ActiveRecord::Tasks::DatabaseTasks.root = Rails.root
+
+ 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
+
load "active_record/railties/databases.rake"
end
@@ -49,7 +69,7 @@ module ActiveRecord
Rails.logger.extend ActiveSupport::Logger.broadcast console
end
- runner do |app|
+ runner do
require "active_record/base"
end
@@ -64,7 +84,7 @@ module ActiveRecord
ActiveSupport.on_load(:active_record) { self.logger ||= ::Rails.logger }
end
- initializer "active_record.migration_error" do |app|
+ initializer "active_record.migration_error" do
if config.active_record.delete(:migration_error) == :page_load
config.app_middleware.insert_after "::ActionDispatch::Callbacks",
"ActiveRecord::Migration::CheckPending"
@@ -80,7 +100,7 @@ module ActiveRecord
if File.file?(filename)
cache = Marshal.load File.binread filename
if cache.version == ActiveRecord::Migrator.current_version
- ActiveRecord::Model.connection.schema_cache = cache
+ self.connection.schema_cache = cache
else
warn "Ignoring db/schema_cache.dump because it has expired. The current schema version is #{ActiveRecord::Migrator.current_version}, but the one in the cache is #{cache.version}."
end
@@ -102,15 +122,13 @@ module ActiveRecord
# and then establishes the connection.
initializer "active_record.initialize_database" do |app|
ActiveSupport.on_load(:active_record) do
- unless ENV['DATABASE_URL']
- self.configurations = app.config.database_configuration
- end
+ self.configurations = app.config.database_configuration || {}
establish_connection
end
end
# Expose database runtime to controller for logging.
- initializer "active_record.log_runtime" do |app|
+ initializer "active_record.log_runtime" do
require "active_record/railties/controller_runtime"
ActiveSupport.on_load(:action_controller) do
include ActiveRecord::Railties::ControllerRuntime
@@ -122,8 +140,10 @@ module ActiveRecord
ActiveSupport.on_load(:active_record) do
ActionDispatch::Reloader.send(hook) do
- ActiveRecord::Model.clear_reloadable_connections!
- ActiveRecord::Model.clear_cache!
+ if ActiveRecord::Base.connected?
+ ActiveRecord::Base.clear_reloadable_connections!
+ ActiveRecord::Base.clear_cache!
+ end
end
end
end
@@ -132,16 +152,5 @@ module ActiveRecord
path = app.paths["db"].first
config.watchable_files.concat ["#{path}/schema.rb", "#{path}/structure.sql"]
end
-
- config.after_initialize do |app|
- ActiveSupport.on_load(:active_record) do
- ActiveRecord::Model.instantiate_observers
-
- ActionDispatch::Reloader.to_prepare do
- ActiveRecord::Model.instantiate_observers
- end
- end
-
- end
end
end
diff --git a/activerecord/lib/active_record/railties/console_sandbox.rb b/activerecord/lib/active_record/railties/console_sandbox.rb
index 90b462fad6..604a220303 100644
--- a/activerecord/lib/active_record/railties/console_sandbox.rb
+++ b/activerecord/lib/active_record/railties/console_sandbox.rb
@@ -1,4 +1,5 @@
-ActiveRecord::Base.connection.begin_db_transaction
+ActiveRecord::Base.connection.begin_transaction(joinable: false)
+
at_exit do
- ActiveRecord::Base.connection.rollback_db_transaction
+ ActiveRecord::Base.connection.rollback_transaction
end
diff --git a/activerecord/lib/active_record/railties/controller_runtime.rb b/activerecord/lib/active_record/railties/controller_runtime.rb
index 7695eacbff..af4840476c 100644
--- a/activerecord/lib/active_record/railties/controller_runtime.rb
+++ b/activerecord/lib/active_record/railties/controller_runtime.rb
@@ -21,9 +21,10 @@ module ActiveRecord
def cleanup_view_runtime
if ActiveRecord::Base.connected?
db_rt_before_render = ActiveRecord::LogSubscriber.reset_runtime
+ self.db_runtime = (db_runtime || 0) + db_rt_before_render
runtime = super
db_rt_after_render = ActiveRecord::LogSubscriber.reset_runtime
- self.db_runtime = db_rt_before_render + db_rt_after_render
+ self.db_runtime += db_rt_after_render
runtime - db_rt_after_render
else
super
diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake
index 69a9526fcc..52b3d3e5e6 100644
--- a/activerecord/lib/active_record/railties/databases.rake
+++ b/activerecord/lib/active_record/railties/databases.rake
@@ -2,14 +2,8 @@ require 'active_record'
db_namespace = namespace :db do
task :load_config do
- ActiveRecord::Base.configurations = Rails.application.config.database_configuration
- ActiveRecord::Migrator.migrations_paths = Rails.application.paths['db/migrate'].to_a
-
- if defined?(ENGINE_PATH) && engine = Rails::Engine.find(ENGINE_PATH)
- if engine.paths['db/migrate'].existent
- ActiveRecord::Migrator.migrations_paths += engine.paths['db/migrate'].to_a
- end
- end
+ ActiveRecord::Base.configurations = ActiveRecord::Tasks::DatabaseTasks.database_configuration || {}
+ ActiveRecord::Migrator.migrations_paths = ActiveRecord::Tasks::DatabaseTasks.migrations_paths
end
namespace :create do
@@ -18,7 +12,7 @@ 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 'Create 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)'
task :create => [:load_config] do
if ENV['DATABASE_URL']
ActiveRecord::Tasks::DatabaseTasks.create_database_url
@@ -156,7 +150,7 @@ db_namespace = namespace :db do
begin
puts ActiveRecord::Tasks::DatabaseTasks.collation_current
rescue NoMethodError
- $stderr.puts 'Sorry, your database adapter is not supported yet, feel free to submit a patch'
+ $stderr.puts 'Sorry, your database adapter is not supported yet. Feel free to submit a patch.'
end
end
@@ -166,11 +160,11 @@ db_namespace = namespace :db do
end
# desc "Raises an error if there are pending migrations"
- task :abort_if_pending_migrations => [:environment, :load_config] do
- pending_migrations = ActiveRecord::Migrator.new(:up, ActiveRecord::Migrator.migrations_paths).pending_migrations
+ task :abort_if_pending_migrations => :environment do
+ pending_migrations = ActiveRecord::Migrator.open(ActiveRecord::Migrator.migrations_paths).pending_migrations
if pending_migrations.any?
- puts "You have #{pending_migrations.size} pending migrations:"
+ puts "You have #{pending_migrations.size} pending #{pending_migrations.size > 1 ? 'migrations:' : 'migration:'}"
pending_migrations.each do |pending_migration|
puts ' %4d %s' % [pending_migration.version, pending_migration.name]
end
@@ -178,13 +172,13 @@ 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'
task :seed do
db_namespace['abort_if_pending_migrations'].invoke
- Rails.application.load_seed
+ ActiveRecord::Tasks::DatabaseTasks.load_seed
end
namespace :fixtures do
@@ -192,7 +186,15 @@ db_namespace = namespace :db do
task :load => [:environment, :load_config] do
require 'active_record/fixtures'
- base_dir = File.join [Rails.root, ENV['FIXTURES_PATH'] || %w{test fixtures}].flatten
+ 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
+ end
+
fixtures_dir = File.join [base_dir, ENV['FIXTURES_DIR']].compact
(ENV['FIXTURES'] ? ENV['FIXTURES'].split(/,/) : Dir["#{fixtures_dir}/**/*.yml"].map {|f| f[(fixtures_dir.size + 1)..-5] }).each do |fixture_file|
@@ -209,7 +211,16 @@ db_namespace = namespace :db do
puts %Q(The fixture ID for "#{label}" is #{ActiveRecord::FixtureSet.identify(label)}.) if label
- base_dir = ENV['FIXTURES_PATH'] ? File.join(Rails.root, ENV['FIXTURES_PATH']) : File.join(Rails.root, 'test', '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
+ end
+
+
Dir["#{base_dir}/**/*.yml"].each do |file|
if data = YAML::load(ERB.new(IO.read(file)).result)
data.keys.each do |key|
@@ -225,10 +236,10 @@ 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'] || "#{Rails.root}/db/schema.rb"
+ filename = ENV['SCHEMA'] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, 'schema.rb')
File.open(filename, "w:utf-8") do |file|
ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, file)
end
@@ -237,12 +248,9 @@ db_namespace = namespace :db do
desc 'Load a schema.rb file into the database'
task :load => [:environment, :load_config] do
- file = ENV['SCHEMA'] || "#{Rails.root}/db/schema.rb"
- if File.exists?(file)
- load(file)
- else
- abort %{#{file} doesn't exist yet. Run `rake db:migrate` to create it then try again. 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}
- end
+ file = ENV['SCHEMA'] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, 'schema.rb')
+ ActiveRecord::Tasks::DatabaseTasks.check_schema_file(file)
+ load(file)
end
task :load_if_ruby => ['db:create', :environment] do
@@ -253,7 +261,7 @@ db_namespace = namespace :db do
desc 'Create a db/schema_cache.dump file.'
task :dump => [:environment, :load_config] do
con = ActiveRecord::Base.connection
- filename = File.join(Rails.application.config.paths["db"].first, "schema_cache.dump")
+ filename = File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "schema_cache.dump")
con.schema_cache.clear!
con.tables.each { |table| con.schema_cache.add(table) }
@@ -262,70 +270,35 @@ db_namespace = namespace :db do
desc 'Clear a db/schema_cache.dump file.'
task :clear => [:environment, :load_config] do
- filename = File.join(Rails.application.config.paths["db"].first, "schema_cache.dump")
- FileUtils.rm(filename) if File.exists?(filename)
+ filename = File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "schema_cache.dump")
+ FileUtils.rm(filename) if File.exist?(filename)
end
end
end
namespace :structure do
- 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
-
desc 'Dump the database structure to db/structure.sql. Specify another file with DB_STRUCTURE=db/my_structure.sql'
task :dump => [:environment, :load_config] do
- filename = ENV['DB_STRUCTURE'] || File.join(Rails.root, "db", "structure.sql")
+ filename = ENV['DB_STRUCTURE'] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "structure.sql")
current_config = ActiveRecord::Tasks::DatabaseTasks.current_config
- case current_config['adapter']
- when /mysql/, /postgresql/, /sqlite/
- ActiveRecord::Tasks::DatabaseTasks.structure_dump(current_config, filename)
- when 'oci', 'oracle'
- ActiveRecord::Base.establish_connection(current_config)
- File.open(filename, "w:utf-8") { |f| f << ActiveRecord::Base.connection.structure_dump }
- when 'sqlserver'
- `smoscript -s #{current_config['host']} -d #{current_config['database']} -u #{current_config['username']} -p #{current_config['password']} -f #{filename} -A -U`
- when "firebird"
- set_firebird_env(current_config)
- db_string = firebird_db_string(current_config)
- sh "isql -a #{db_string} > #{filename}"
- else
- raise "Task not supported by '#{current_config["adapter"]}'"
- end
+ ActiveRecord::Tasks::DatabaseTasks.structure_dump(current_config, filename)
if ActiveRecord::Base.connection.supports_migrations?
- File.open(filename, "a") { |f| f << ActiveRecord::Base.connection.dump_schema_information }
+ File.open(filename, "a") do |f|
+ f.puts ActiveRecord::Base.connection.dump_schema_information
+ f.print "\n"
+ end
end
db_namespace['structure:dump'].reenable
end
# desc "Recreate the databases from the structure.sql file"
task :load => [:environment, :load_config] do
- current_config = ActiveRecord::Tasks::DatabaseTasks.current_config(:env => (ENV['RAILS_ENV'] || 'test'))
- filename = ENV['DB_STRUCTURE'] || File.join(Rails.root, "db", "structure.sql")
- case current_config['adapter']
- when /mysql/, /postgresql/, /sqlite/
- ActiveRecord::Tasks::DatabaseTasks.structure_load(current_config, filename)
- when 'sqlserver'
- `sqlcmd -S #{current_config['host']} -d #{current_config['database']} -U #{current_config['username']} -P #{current_config['password']} -i #{filename}`
- when 'oci', 'oracle'
- ActiveRecord::Base.establish_connection(current_config)
- IO.read(filename).split(";\n\n").each do |ddl|
- ActiveRecord::Base.connection.execute(ddl)
- end
- when 'firebird'
- set_firebird_env(current_config)
- db_string = firebird_db_string(current_config)
- sh "isql -i #{filename} #{db_string}"
- else
- raise "Task not supported by '#{current_config['adapter']}'"
- end
+ 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)
end
task :load_if_sql => ['db:create', :environment] do
@@ -347,9 +320,16 @@ db_namespace = namespace :db do
# desc "Recreate the test database from an existent schema.rb file"
task :load_schema => 'db:test:purge' do
- ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations['test'])
- ActiveRecord::Schema.verbose = false
- db_namespace["schema:load"].invoke
+ 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
+ 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"
@@ -380,31 +360,11 @@ db_namespace = namespace :db do
# desc "Empty the test database"
task :purge => [:environment, :load_config] do
- abcs = ActiveRecord::Base.configurations
- case abcs['test']['adapter']
- when /mysql/, /postgresql/, /sqlite/
- ActiveRecord::Tasks::DatabaseTasks.purge abcs['test']
- when 'sqlserver'
- test = abcs.deep_dup['test']
- test_database = test['database']
- test['database'] = 'master'
- ActiveRecord::Base.establish_connection(test)
- ActiveRecord::Base.connection.recreate_database!(test_database)
- when "oci", "oracle"
- ActiveRecord::Base.establish_connection(:test)
- ActiveRecord::Base.connection.structure_drop.split(";\n\n").each do |ddl|
- ActiveRecord::Base.connection.execute(ddl)
- end
- when 'firebird'
- ActiveRecord::Base.establish_connection(:test)
- ActiveRecord::Base.connection.recreate_database!
- else
- raise "Task not supported by '#{abcs['test']['adapter']}'"
- end
+ ActiveRecord::Tasks::DatabaseTasks.purge ActiveRecord::Base.configurations['test']
end
# desc 'Check for pending migrations and load the test schema'
- task :prepare => 'db:abort_if_pending_migrations' do
+ task :prepare => :load_config do
unless ActiveRecord::Base.configurations.blank?
db_namespace['test:load'].invoke
end
@@ -430,7 +390,7 @@ namespace :railties do
puts "NOTE: Migration #{migration.basename} from #{name} has been skipped. Migration with the same name already exists."
end
- on_copy = Proc.new do |name, migration, old_path|
+ on_copy = Proc.new do |name, migration|
puts "Copied migration #{migration.basename} from #{name}"
end
@@ -440,5 +400,5 @@ namespace :railties do
end
end
-task 'test:prepare' => 'db:test:prepare'
+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 b3c20c4aff..b3ddfd63d4 100644
--- a/activerecord/lib/active_record/readonly_attributes.rb
+++ b/activerecord/lib/active_record/readonly_attributes.rb
@@ -20,10 +20,5 @@ module ActiveRecord
self._attr_readonly
end
end
-
- def _attr_readonly
- ActiveSupport::Deprecation.warn("Instance level _attr_readonly method is deprecated, please use class level method.")
- 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 f322b96f79..bce7766501 100644
--- a/activerecord/lib/active_record/reflection.rb
+++ b/activerecord/lib/active_record/reflection.rb
@@ -1,4 +1,3 @@
-
module ActiveRecord
# = Active Record Reflection
module Reflection # :nodoc:
@@ -6,10 +5,31 @@ 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
+ # \Reflection enables to interrogate Active Record classes and objects
# about their associations and aggregations. This information can,
# for example, be used in a form builder that takes an Active Record object
# and creates input fields for all of the attributes depending on their type
@@ -18,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).
@@ -41,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
@@ -56,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
@@ -66,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.
@@ -76,18 +81,23 @@ module ActiveRecord
end
end
- # Abstract base class for AggregateReflection and AssociationReflection. Objects of
+ # Base class for AggregateReflection and AssociationReflection. Objects of
# AggregateReflection and AssociationReflection are returned by the Reflection::ClassMethods.
+ #
+ # MacroReflection
+ # AggregateReflection
+ # AssociationReflection
+ # ThroughReflection
class MacroReflection
# Returns the name of the macro.
#
- # <tt>composed_of :balance, :class_name => 'Money'</tt> returns <tt>:balance</tt>
+ # <tt>composed_of :balance, class_name: 'Money'</tt> returns <tt>:balance</tt>
# <tt>has_many :clients</tt> returns <tt>:clients</tt>
attr_reader :name
# Returns the macro type.
#
- # <tt>composed_of :balance, :class_name => 'Money'</tt> returns <tt>:composed_of</tt>
+ # <tt>composed_of :balance, class_name: 'Money'</tt> returns <tt>:composed_of</tt>
# <tt>has_many :clients</tt> returns <tt>:has_many</tt>
attr_reader :macro
@@ -95,8 +105,8 @@ module ActiveRecord
# Returns the hash of options used for the macro.
#
- # <tt>composed_of :balance, :class_name => 'Money'</tt> returns <tt>{ :class_name => "Money" }</tt>
- # <tt>has_many :clients</tt> returns +{}+
+ # <tt>composed_of :balance, class_name: 'Money'</tt> returns <tt>{ class_name: "Money" }</tt>
+ # <tt>has_many :clients</tt> returns <tt>{}</tt>
attr_reader :options
attr_reader :active_record
@@ -109,13 +119,19 @@ 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
+ # <tt>composed_of :balance, class_name: 'Money'</tt> returns the Money class
# <tt>has_many :clients</tt> returns the Client class
def klass
@klass ||= class_name.constantize
@@ -123,7 +139,7 @@ module ActiveRecord
# Returns the class name for the macro.
#
- # <tt>composed_of :balance, :class_name => 'Money'</tt> returns <tt>'Money'</tt>
+ # <tt>composed_of :balance, class_name: 'Money'</tt> returns <tt>'Money'</tt>
# <tt>has_many :clients</tt> returns <tt>'Client'</tt>
def class_name
@class_name ||= (options[:class_name] || derive_class_name).to_s
@@ -174,23 +190,33 @@ 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. +options+ will
+ # Returns a new, unsaved instance of the associated class. +attributes+ will
# be passed to the class's constructor.
def build_association(attributes, &block)
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
@@ -201,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
@@ -234,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!
@@ -263,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
@@ -285,13 +291,13 @@ module ActiveRecord
alias :source_macro :macro
def has_inverse?
- @options[:inverse_of]
+ inverse_name
end
def inverse_of
- if has_inverse?
- @inverse_of ||= klass.reflect_on_association(options[:inverse_of])
- end
+ return unless inverse_name
+
+ @inverse_of ||= klass.reflect_on_association inverse_name
end
def polymorphic_inverse_of(associated_class)
@@ -315,10 +321,10 @@ module ActiveRecord
# the parent's validation.
#
# Unless you explicitly disable validation with
- # <tt>:validate => false</tt>, validation will take place when:
+ # <tt>validate: false</tt>, validation will take place when:
#
- # * you explicitly enable validation; <tt>:validate => true</tt>
- # * you use autosave; <tt>:autosave => true</tt>
+ # * you explicitly enable validation; <tt>validate: true</tt>
+ # * you use autosave; <tt>autosave: true</tt>
# * the association is a +has_many+ association
def validate?
!options[:validate].nil? ? options[:validate] : (options[:autosave] == true || macro == :has_many)
@@ -329,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
@@ -341,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
@@ -362,7 +362,92 @@ module ActiveRecord
options.key? :polymorphic
end
+ 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
+
+ def calculate_constructable(macro, options)
+ case macro
+ when :belongs_to
+ !options[:polymorphic]
+ when :has_one
+ !options[:through]
+ else
+ true
+ end
+ end
+
+ # 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 = ActiveSupport::Inflector.underscore(active_record.name).to_sym
+
+ begin
+ reflection = klass.reflect_on_association(inverse_name)
+ rescue NameError
+ # Give up: we couldn't compute the klass type so we won't be able
+ # to find any associations either.
+ reflection = false
+ end
+
+ if valid_inverse_reflection?(reflection)
+ return inverse_name
+ end
+ end
+
+ false
+ end
+
+ # Checks if the inverse reflection that is returned from the
+ # +automatic_inverse_of+ method is a valid reflection. We must
+ # make sure that the reflection's active_record name matches up
+ # with the current reflection's klass name.
+ #
+ # Note: klass will always be valid because when there's a NameError
+ # from calling +klass+, +reflection+ will already be set to false.
+ def valid_inverse_reflection?(reflection)
+ reflection &&
+ klass.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
+ # <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[:inverse_of] != false &&
+ VALID_AUTOMATIC_INVERSE_MACROS.include?(reflection.macro) &&
+ !INVALID_AUTOMATIC_INVERSE_OPTIONS.any? { |opt| reflection.options[opt] } &&
+ !reflection.scope
+ end
+
def derive_class_name
class_name = name.to_s.camelize
class_name = class_name.singularize if collection?
@@ -394,16 +479,30 @@ module ActiveRecord
delegate :foreign_key, :foreign_type, :association_foreign_key,
:active_record_primary_key, :type, :to => :source_reflection
- # Gets the source of the through reflection. It checks both a singularized
+ 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>.
#
# class Post < ActiveRecord::Base
# has_many :taggings
- # has_many :tags, :through => :taggings
+ # has_many :tags, through: :taggings
# end
#
+ # class Tagging < ActiveRecord::Base
+ # belongs_to :post
+ # belongs_to :tag
+ # end
+ #
+ # tags_reflection = Post.reflect_on_association(:tags)
+ # tags_reflection.source_reflection
+ # # => <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
@@ -411,14 +510,15 @@ module ActiveRecord
#
# class Post < ActiveRecord::Base
# has_many :taggings
- # has_many :tags, :through => :taggings
+ # has_many :tags, through: :taggings
# end
#
# tags_reflection = Post.reflect_on_association(:tags)
- # taggings_reflection = tags_reflection.through_reflection
+ # tags_reflection.through_reflection
+ # # => <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
@@ -427,9 +527,22 @@ module ActiveRecord
# The chain is built by recursively calling #chain on the source reflection and the through
# reflection. The base case for the recursion is a normal association, which just returns
# [self] as its #chain.
+ #
+ # class Post < ActiveRecord::Base
+ # has_many :taggings
+ # has_many :tags, through: :taggings
+ # end
+ #
+ # tags_reflection = Post.reflect_on_association(:tags)
+ # tags_reflection.chain
+ # # => [<ActiveRecord::Reflection::ThroughReflection: @macro=:has_many, @name=:tags, @options={:through=>:taggings}, @active_record=Post>,
+ # <ActiveRecord::Reflection::AssociationReflection: @macro=:has_many, @name=:taggings, @options={}, @active_record=Post>]
+ #
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
@@ -439,12 +552,12 @@ module ActiveRecord
#
# class Person
# has_many :articles
- # has_many :comment_tags, :through => :articles
+ # has_many :comment_tags, through: :articles
# end
#
# class Article
# has_many :comments
- # has_many :comment_tags, :through => :comments, :source => :tags
+ # has_many :comment_tags, through: :comments, source: :tags
# end
#
# class Comment
@@ -461,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 <<
@@ -480,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
@@ -489,20 +602,47 @@ 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:
+ # Gets an array of possible <tt>:through</tt> source reflection names in both singular and plural form.
#
- # [:singularized, :pluralized]
+ # class Post < ActiveRecord::Base
+ # has_many :taggings
+ # has_many :tags, through: :taggings
+ # end
+ #
+ # tags_reflection = Post.reflect_on_association(:tags)
+ # tags_reflection.source_reflection_names
+ # # => [: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
@@ -541,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 ed80422336..60f2726a6e 100644
--- a/activerecord/lib/active_record/relation.rb
+++ b/activerecord/lib/active_record/relation.rb
@@ -10,28 +10,32 @@ module ActiveRecord
:extending]
SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :from, :reordering,
- :reverse_order, :uniq, :create_with]
+ :reverse_order, :distinct, :create_with, :uniq]
VALUE_METHODS = MULTI_VALUE_METHODS + SINGLE_VALUE_METHODS
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 insert(values)
+ def initialize_copy(other)
+ # This method is a hot spot, so for now, use Hash[] to dup the hash.
+ # https://bugs.ruby-lang.org/issues/7166
+ @values = Hash[@values]
+ @values[:bind] = @values[:bind].dup if @values.key? :bind
+ reset
+ end
+
+ def insert(values) # :nodoc:
primary_key_value = nil
if primary_key && Hash === values
@@ -48,16 +52,7 @@ module ActiveRecord
im = arel.create_insert
im.into @table
- conn = @klass.connection
-
- substitutes = values.sort_by { |arel_attr,_| arel_attr.name }
- binds = substitutes.map do |arel_attr, value|
- [@klass.columns_hash[arel_attr.name], value]
- end
-
- substitutes.each_with_index do |tuple, i|
- tuple[1] = conn.substitute_at(binds[i][0], i)
- end
+ substitutes, binds = substitute_values values
if values.empty? # empty insert
im.values = Arel.sql(connection.empty_insert_statement_value)
@@ -65,7 +60,7 @@ module ActiveRecord
im.insert substitutes
end
- conn.insert(
+ @klass.connection.insert(
im,
'SQL',
primary_key,
@@ -74,6 +69,29 @@ module ActiveRecord
binds)
end
+ 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)
+
+ @klass.connection.update(
+ um,
+ 'SQL',
+ binds)
+ end
+
+ def substitute_values(values) # :nodoc:
+ substitutes = values.sort_by { |arel_attr,_| arel_attr.name }
+ binds = substitutes.map do |arel_attr, value|
+ [@klass.columns_hash[arel_attr.name], value]
+ end
+
+ substitutes.each_with_index do |tuple, i|
+ tuple[1] = @klass.connection.substitute_at(binds[i][0], i)
+ end
+
+ [substitutes, binds]
+ end
+
# Initializes new record from relation while maintaining the current
# scope.
#
@@ -90,12 +108,6 @@ module ActiveRecord
scoping { @klass.new(*args, &block) }
end
- def initialize_copy(other)
- @values = @values.dup
- @values[:bind] = @values[:bind].dup if @values[:bind]
- reset
- end
-
alias build new
# Tries to create a new record with the same scoped attributes
@@ -127,46 +139,77 @@ module ActiveRecord
scoping { @klass.create!(*args, &block) }
end
- # Tries to load the first record; if it fails, then <tt>create</tt> is called with the same arguments as this method.
- #
- # Expects arguments in the same format as +Base.create+.
+ def first_or_create(attributes = nil, &block) # :nodoc:
+ first || create(attributes, &block)
+ end
+
+ def first_or_create!(attributes = nil, &block) # :nodoc:
+ first || create!(attributes, &block)
+ end
+
+ def first_or_initialize(attributes = nil, &block) # :nodoc:
+ 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:
#
- # ==== Examples
- # # Find the first user named Penélope or create a new one.
- # User.where(:first_name => 'Penélope').first_or_create
- # # => <User id: 1, first_name: 'Penélope', last_name: nil>
+ # # 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>
#
- # # 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.where(:first_name => 'Penélope').first_or_create
- # # => <User id: 1, first_name: 'Penélope', last_name: nil>
+ # User.find_or_create_by(first_name: 'Penélope')
+ # # => #<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.
- # User.where(:first_name => 'Scarlett').first_or_create(: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 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">
#
- # # 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.
- # User.where(:first_name => 'Scarlett').first_or_create do |user|
- # user.last_name = "O'Hara"
+ # 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 = 'Johansson'
# end
- # # => <User id: 2, first_name: 'Scarlett', last_name: 'Johansson'>
- def first_or_create(attributes = nil, &block)
- first || create(attributes, &block)
+ # # => #<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>first_or_create</tt> but calls <tt>create!</tt> so an exception is raised if the created record is invalid.
- #
- # Expects arguments in the same format as <tt>Base.create!</tt>.
- def first_or_create!(attributes = nil, &block)
- first || create!(attributes, &block)
+ # 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
- # Like <tt>first_or_create</tt> but calls <tt>new</tt> instead of <tt>create</tt>.
- #
- # Expects arguments in the same format as <tt>Base.new</tt>.
- def first_or_initialize(attributes = nil, &block)
- first || new(attributes, &block)
+ # Like <tt>find_or_create_by</tt>, but calls <tt>new</tt> instead of <tt>create</tt>.
+ def find_or_initialize_by(attributes, &block)
+ find_by(attributes) || new(attributes, &block)
end
# Runs EXPLAIN on the query or queries triggered by this relation and
@@ -179,8 +222,7 @@ module ActiveRecord
# Please see further details in the
# {Active Record Query Interface guide}[http://guides.rubyonrails.org/active_record_querying.html#running-explain].
def explain
- _, queries = collecting_queries_for_explain { exec_queries }
- exec_explain(queries)
+ exec_explain(collecting_queries_for_explain { exec_queries })
end
# Converts relation objects to Array.
@@ -202,8 +244,7 @@ module ActiveRecord
def empty?
return @records.empty? if loaded?
- c = count
- c.respond_to?(:zero?) ? c.zero? : c.empty?
+ limit_value == 0 ? true : !exists?
end
# Returns true if there are any records.
@@ -226,9 +267,10 @@ module ActiveRecord
# Scope all queries to the current scope.
#
- # Comment.where(:post_id => 1).scoping do
- # Comment.first # SELECT * FROM comments WHERE post_id = 1
+ # Comment.where(post_id: 1).scoping do
+ # Comment.first
# end
+ # # => SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = 1 ORDER BY "comments"."id" ASC LIMIT 1
#
# Please check unscoped if you want to remove all previous scopes (including
# the default_scope) during the execution of a block.
@@ -257,7 +299,7 @@ module ActiveRecord
# Book.where('title LIKE ?', '%Rails%').update_all(author: 'David')
#
# # Update all books that match conditions, but limit it to 5 ordered by date
- # Book.where('title LIKE ?', '%Rails%').order(:created_at).limit(5).update_all(:author => 'David')
+ # Book.where('title LIKE ?', '%Rails%').order(:created_at).limit(5).update_all(author: 'David')
def update_all(updates)
raise ArgumentError, "Empty list of attributes to change" if updates.blank?
@@ -299,18 +341,16 @@ module ActiveRecord
id.map.with_index { |one_id, idx| update(one_id, attributes[idx]) }
else
object = find(id)
- object.update_attributes(attributes)
+ object.update(attributes)
object
end
end
# Destroys the records matching +conditions+ by instantiating each
# record and calling its +destroy+ method. Each object's callbacks are
- # executed (including <tt>:dependent</tt> association options and
- # +before_destroy+/+after_destroy+ Observer methods). Returns the
+ # executed (including <tt>:dependent</tt> association options). Returns the
# collection of objects that were destroyed; each will be frozen, to
- # reflect that no changes should be made (since they can't be
- # persisted).
+ # reflect that no changes should be made (since they can't be persisted).
#
# Note: Instantiation, callback execution, and deletion of each
# record can be time consuming when you're removing many records at
@@ -330,7 +370,7 @@ module ActiveRecord
#
# Person.destroy_all("last_login < '2004-04-04'")
# Person.destroy_all(status: "inactive")
- # Person.where(:age => 0..18).destroy_all
+ # Person.where(age: 0..18).destroy_all
def destroy_all(conditions = nil)
if conditions
where(conditions).destroy_all
@@ -375,7 +415,7 @@ module ActiveRecord
#
# Post.delete_all("person_id = 5 AND (category = 'Something' OR category = 'Else')")
# Post.delete_all(["person_id = ? AND (category = ? OR category = ?)", 5, 'Something', 'Else'])
- # Post.where(:person_id => 5).where(:category => ['Something', 'Else']).delete_all
+ # Post.where(person_id: 5).where(category: ['Something', 'Else']).delete_all
#
# Both calls delete the affected posts all at once with a single DELETE statement.
# If you need to destroy dependent associations or call your <tt>before_*</tt> or
@@ -410,8 +450,7 @@ module ActiveRecord
# Deletes the row with a primary key matching the +id+ argument, using a
# SQL +DELETE+ statement, and returns the number of rows deleted. Active
# Record objects are not instantiated, so the object's callbacks are not
- # executed, including any <tt>:dependent</tt> association options or
- # Observer methods.
+ # executed, including any <tt>:dependent</tt> association options.
#
# You can delete multiple rows at once by passing an Array of <tt>id</tt>s.
#
@@ -438,17 +477,7 @@ module ActiveRecord
#
# Post.where(published: true).load # => #<ActiveRecord::Relation>
def load
- unless loaded?
- # We monitor here the entire execution rather than individual SELECTs
- # because from the point of view of the user fetching the records of a
- # relation is a single unit of work. You want to know if this call takes
- # too long, not if the individual queries take too long.
- #
- # It could be the case that none of the queries involved surpass the
- # threshold, and at the same time the sum of them all does. The user
- # should get a query plan logged in that case.
- logging_query_plan { exec_queries }
- end
+ exec_queries unless loaded?
self
end
@@ -468,22 +497,37 @@ module ActiveRecord
# Returns sql statement for the relation.
#
- # Users.where(name: 'Oscar').to_sql
+ # 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
+ # Returns a hash of where conditions.
#
- # Users.where(name: 'Oscar').where_values_hash
- # # => {:name=>"oscar"}
+ # User.where(name: 'Oscar').where_values_hash
+ # # => {name: "Oscar"}
def where_values_hash
- equalities = with_default_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[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|
name = where.left.name
@@ -505,11 +549,17 @@ module ActiveRecord
# Joins that are also marked for preloading. In which case we should just eager load them.
# Note that this is a naive implementation because we could have strings and symbols which
# represent the same association, but that aren't matched by this. Also, we could have
- # nested hashes which partially match, e.g. { :a => :b } & { :a => [:b, :c] }
+ # nested hashes which partially match, e.g. { a: :b } & { a: [:b, :c] }
def joined_includes_values
includes_values & joins_values
end
+ # +uniq+ and +uniq!+ are silently deprecated. +uniq_value+ delegates to +distinct_value+
+ # to maintain backwards compatibility. Use +distinct_value+ instead.
+ def uniq_value
+ distinct_value
+ end
+
# Compares two relations for equality.
def ==(other)
case other
@@ -524,23 +574,13 @@ 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?
end
def values
- @values.dup
+ Hash[@values]
end
def inspect
@@ -553,34 +593,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
@@ -589,37 +619,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 (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 recognises 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"
- )
- 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 8af0c6a8ef..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,54 +19,85 @@ 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) || 0
+ start = options.delete(:start)
batch_size = options.delete(:batch_size) || 1000
relation = relation.reorder(batch_order).limit(batch_size)
- records = relation.where(table[primary_key].gteq(start)).to_a
+ records = start ? relation.where(table[primary_key].gteq(start)).to_a : relation.to_a
while records.any?
records_size = records.size
diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb
index 7c43d844d0..2d267183ce 100644
--- a/activerecord/lib/active_record/relation/calculations.rb
+++ b/activerecord/lib/active_record/relation/calculations.rb
@@ -1,5 +1,3 @@
-require 'active_support/core_ext/object/try'
-
module ActiveRecord
module Calculations
# Count the records.
@@ -13,61 +11,51 @@ module ActiveRecord
# Person.count(:all)
# # => performs a COUNT(*) (:all is an alias for '*')
#
- # Person.count(:age, distinct: true)
+ # Person.distinct.count(:age)
# # => counts the number of different age values
#
- # Person.where("age > 26").count { |person| person.gender == 'female' }
- # # => queries people where "age > 26" then count the loaded results filtering by gender
- def count(column_name = nil, options = {})
- if block_given?
- self.to_a.count { |item| yield item }
- else
- column_name, options = nil, column_name if column_name.is_a?(Hash)
- calculate(:count, column_name, options)
- end
+ # If +count+ is used with +group+, it returns a Hash whose keys represent the aggregated column,
+ # and the values are the respective amounts:
+ #
+ # Person.group(:city).count
+ # # => { 'Rome' => 5, 'Paris' => 3 }
+ def count(column_name = nil)
+ calculate(:count, column_name)
end
# Calculates the average value on a given column. Returns +nil+ if there's
# no row. See +calculate+ for examples with options.
#
- # Person.average('age') # => 35.8
- def average(column_name, options = {})
- calculate(:average, column_name, options)
+ # Person.average(:age) # => 35.8
+ def average(column_name)
+ calculate(:average, column_name)
end
# Calculates the minimum value on a given column. The value is returned
# with the same data type of the column, or +nil+ if there's no row. See
# +calculate+ for examples with options.
#
- # Person.minimum('age') # => 7
- def minimum(column_name, options = {})
- calculate(:minimum, column_name, options)
+ # Person.minimum(:age) # => 7
+ def minimum(column_name)
+ calculate(:minimum, column_name)
end
# Calculates the maximum value on a given column. The value is returned
# with the same data type of the column, or +nil+ if there's no row. See
# +calculate+ for examples with options.
#
- # Person.maximum('age') # => 93
- def maximum(column_name, options = {})
- calculate(:maximum, column_name, options)
+ # Person.maximum(:age) # => 93
+ def maximum(column_name)
+ calculate(:maximum, column_name)
end
# Calculates the sum of values on a given column. The value is returned
# with the same data type of the column, 0 if there's no row. See
# +calculate+ for examples with options.
#
- # Person.sum('age') # => 4562
- # # => returns the total sum of all people's age
- #
- # Person.where('age > 100').sum { |person| person.age - 100 }
- # # queries people where "age > 100" then perform a sum calculation with the block returns
+ # Person.sum(:age) # => 4562
def sum(*args)
- if block_given?
- self.to_a.sum(*args) { |item| yield item }
- else
- calculate(:sum, *args)
- end
+ calculate(:sum, *args)
end
# This calculates aggregate values in the given column. Methods for count, sum, average,
@@ -83,18 +71,17 @@ module ActiveRecord
#
# values = Person.group('last_name').maximum(:age)
# puts values["Drake"]
- # => 43
+ # # => 43
#
- # drake = Family.find_by_last_name('Drake')
+ # drake = Family.find_by(last_name: 'Drake')
# values = Person.group(:family).maximum(:age) # Person belongs_to :family
# puts values[drake]
- # => 43
+ # # => 43
#
# values.each do |family, max_age|
# ...
# end
#
- # Examples:
# Person.calculate(:count, :all) # The same as Person.count
# Person.average(:age) # SELECT AVG(age) FROM people...
#
@@ -102,24 +89,20 @@ module ActiveRecord
# Person.group(:last_name).having("min(age) > 17").minimum(:age)
#
# Person.sum("2 * age")
- def calculate(operation, column_name, options = {})
- relation = with_default_scope
+ def calculate(operation, column_name)
+ 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)
else
- relation.calculate(operation, column_name, options)
+ perform_calculation(operation, column_name)
end
- rescue ThrowResult
- 0
end
- # Use <tt>pluck</tt> as a shortcut to select a single attribute without
- # loading a bunch of records just to grab one attribute you want.
+ # Use <tt>pluck</tt> as a shortcut to select one or more attributes without
+ # loading a bunch of records just to grab the attributes you want.
#
# Person.pluck(:name)
#
@@ -128,11 +111,9 @@ module ActiveRecord
# Person.all.map(&:name)
#
# Pluck returns an <tt>Array</tt> of attribute values type-casted to match
- # the plucked column name, if it can be deduced. Plucking an SQL fragment
+ # the plucked column names, if they can be deduced. Plucking an SQL fragment
# returns String values by default.
#
- # Examples:
- #
# Person.pluck(:id)
# # SELECT people.id FROM people
# # => [1, 2, 3]
@@ -141,11 +122,11 @@ module ActiveRecord
# # SELECT people.id, people.name FROM people
# # => [[1, 'David'], [2, 'Jeremy'], [3, 'Jose']]
#
- # Person.uniq.pluck(:role)
+ # Person.pluck('DISTINCT role')
# # SELECT DISTINCT role FROM people
# # => ['admin', 'member', 'guest']
#
- # Person.where(:age => 21).limit(5).pluck(:id)
+ # Person.where(age: 21).limit(5).pluck(:id)
# # SELECT people.id FROM people WHERE people.age = 21 LIMIT 5
# # => [2, 3]
#
@@ -155,31 +136,31 @@ module ActiveRecord
#
def pluck(*column_names)
column_names.map! do |column_name|
- if column_name.is_a?(Symbol) && self.column_names.include?(column_name.to_s)
- "#{connection.quote_table_name(table_name)}.#{connection.quote_column_name(column_name)}"
+ if column_name.is_a?(Symbol) && attribute_alias?(column_name)
+ attribute_alias(column_name)
else
- column_name
+ column_name.to_s
end
end
if has_include?(column_names.first)
construct_relation_for_association_calculations.pluck(*column_names)
else
- result = klass.connection.select_all(select(column_names).arel, nil, bind_values)
+ relation = spawn
+ 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
@@ -187,8 +168,6 @@ module ActiveRecord
# Pluck all the ID's for the relation using the table's primary key
#
- # Examples:
- #
# Person.ids # SELECT people.id FROM people
# Person.joins(:companies).ids # SELECT people.id FROM people INNER JOIN companies ON companies.person_id = people.id
def ids
@@ -201,21 +180,21 @@ module ActiveRecord
eager_loading? || (includes_values.present? && (column_name || references_eager_loaded_tables?))
end
- def perform_calculation(operation, column_name, options = {})
+ def perform_calculation(operation, column_name)
operation = operation.to_s.downcase
- distinct = options[:distinct]
+ # If #count is used with #distinct / #uniq it is considered distinct. (eg. relation.distinct.count)
+ distinct = self.distinct_value
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?
@@ -241,6 +220,8 @@ module ActiveRecord
# Postgresql doesn't like ORDER BY when there are no GROUP BY
relation = reorder(nil)
+ column_alias = column_name
+
if operation == "count" && (relation.limit_value || relation.offset_value)
# Shortcut when limit is zero.
return 0 if relation.limit_value == 0
@@ -251,13 +232,20 @@ module ActiveRecord
select_value = operation_over_aggregate_column(column, operation, distinct)
+ column_alias = select_value.alias
relation.select_values = [select_value]
query_builder = relation.arel
end
- result = @klass.connection.select_value(query_builder, nil, relation.bind_values)
- type_cast_calculated_value(result, column_for(column_name), operation)
+ result = @klass.connection.select_all(query_builder, nil, relation.bind_values)
+ row = result.first
+ value = row && row.values.first
+ column = result.column_types.fetch(column_alias) do
+ column_for(column_name)
+ end
+
+ type_cast_calculated_value(value, column, operation)
end
def execute_grouped_calculation(operation, column_name, distinct) #:nodoc:
@@ -271,17 +259,19 @@ module ActiveRecord
group_fields = group_attrs
end
- group_aliases = group_fields.map { |field| column_alias_for(field) }
+ group_aliases = group_fields.map { |field|
+ column_alias_for(field)
+ }
group_columns = group_aliases.zip(group_fields).map { |aliaz,field|
- [aliaz, column_for(field)]
+ [aliaz, field]
}
- group = @klass.connection.adapter_name == 'FrontBase' ? group_aliases : group_fields
+ group = group_fields
if operation == 'count' && column_name == :all
aggregate_alias = 'count_all'
else
- aggregate_alias = column_alias_for(operation, column_name)
+ aggregate_alias = column_alias_for([operation, column_name].join(' '))
end
select_values = [
@@ -300,7 +290,8 @@ module ActiveRecord
end
}
- relation = except(:group).group(group)
+ relation = except(:group)
+ relation.group_values = group
relation.select_values = select_values
calculated_data = @klass.connection.select_all(relation, nil, bind_values)
@@ -312,7 +303,10 @@ module ActiveRecord
end
Hash[calculated_data.map do |row|
- key = group_columns.map { |aliaz, column|
+ key = group_columns.map { |aliaz, col_name|
+ column = calculated_data.column_types.fetch(aliaz) do
+ column_for(col_name)
+ end
type_cast_calculated_value(row[aliaz], column)
}
key = key.first if key.size == 1
@@ -329,10 +323,12 @@ module ActiveRecord
# column_alias_for("count(distinct users.id)") # => "count_distinct_users_id"
# column_alias_for("count(*)") # => "count_all"
# column_alias_for("count", "id") # => "count_id"
- def column_alias_for(*keys)
- keys.map! {|k| k.respond_to?(:to_sql) ? k.to_sql : k}
- table_name = keys.join(' ')
- table_name.downcase!
+ def column_alias_for(keys)
+ if keys.respond_to? :name
+ keys = "#{keys.relation.name}.#{keys.name}"
+ end
+
+ table_name = keys.to_s.downcase
table_name.gsub!(/\*/, 'all')
table_name.gsub!(/\W+/, ' ')
table_name.strip!
@@ -343,13 +339,13 @@ module ActiveRecord
def column_for(field)
field_name = field.respond_to?(:name) ? field.name.to_s : field.to_s.split('.').last
- @klass.columns.detect { |c| c.name.to_s == field_name }
+ @klass.columns_hash[field_name]
end
def type_cast_calculated_value(value, column, operation = nil)
case operation
when 'count' then value.to_i
- when 'sum' then type_cast_using_column(value || '0', column)
+ when 'sum' then type_cast_using_column(value || 0, column)
when 'average' then value.respond_to?(:to_d) ? value.to_d : value
else type_cast_using_column(value, column)
end
@@ -359,10 +355,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 ab8b36c8ab..1e15bddcdf 100644
--- a/activerecord/lib/active_record/relation/delegation.rb
+++ b/activerecord/lib/active_record/relation/delegation.rb
@@ -1,44 +1,137 @@
+require 'active_support/concern'
+require 'active_support/deprecation'
module ActiveRecord
module Delegation # :nodoc:
- # Set up common delegations for performance (avoids method_missing)
+ 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
+ # subsequent calls to that method faster by avoiding method_missing. The delegations
+ # 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
delegate :table_name, :quoted_table_name, :primary_key, :quoted_primary_key,
- :connection, :columns_hash, :auto_explain_threshold_in_seconds, :to => :klass
+ :connection, :columns_hash, :to => :klass
+
+ module ClassSpecificRelation # :nodoc:
+ extend ActiveSupport::Concern
+
+ included do
+ @delegation_mutex = Mutex.new
+ end
+
+ module ClassMethods # :nodoc:
+ def name
+ superclass.name
+ end
+
+ def delegate_to_scoped_klass(method)
+ @delegation_mutex.synchronize do
+ return if method_defined?(method)
- def self.delegate_to_scoped_klass(method)
- if method.to_s =~ /\A[a-zA-Z_]\w*[!?]?\z/
- module_eval <<-RUBY, __FILE__, __LINE__ + 1
- def #{method}(*args, &block)
- scoping { @klass.#{method}(*args, &block) }
+ if method.to_s =~ /\A[a-zA-Z_]\w*[!?]?\z/
+ module_eval <<-RUBY, __FILE__, __LINE__ + 1
+ def #{method}(*args, &block)
+ scoping { @klass.#{method}(*args, &block) }
+ end
+ RUBY
+ else
+ define_method method do |*args, &block|
+ scoping { @klass.send(method, *args, &block) }
+ end
+ end
end
- RUBY
- else
- module_eval <<-RUBY, __FILE__, __LINE__ + 1
- def #{method}(*args, &block)
- scoping { @klass.send(#{method.inspect}, *args, &block) }
+ end
+
+ def delegate(method, opts = {})
+ @delegation_mutex.synchronize do
+ return if method_defined?(method)
+ super
end
- RUBY
+ end
+ end
+
+ protected
+
+ 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_delegable?(method)
+ self.class.delegate method, :to => :to_a
+ to_a.send(method, *args, &block)
+ elsif arel.respond_to?(method)
+ self.class.delegate method, :to => :arel
+ arel.send(method, *args, &block)
+ else
+ super
+ end
+ end
+ end
+
+ module ClassMethods # :nodoc:
+ def create(klass, *args)
+ relation_class_for(klass).new(klass, *args)
+ end
+
+ private
+
+ def relation_class_for(klass)
+ klass.relation_delegate_class(self)
end
end
def respond_to?(method, include_private = false)
- super || Array.method_defined?(method) ||
+ super || array_delegable?(method) ||
@klass.respond_to?(method, include_private) ||
arel.respond_to?(method, include_private)
end
protected
+ def array_delegable?(method)
+ defined = Array.method_defined?(method)
+ if defined && method.to_s.ends_with?('!')
+ ActiveSupport::Deprecation.warn(
+ "Association will no longer delegate #{method} to #to_a as of Rails 4.2. You instead must first call #to_a on the association to expose the array to be acted on."
+ )
+ end
+ defined
+ end
+
def method_missing(method, *args, &block)
if @klass.respond_to?(method)
- ::ActiveRecord::Delegation.delegate_to_scoped_klass(method)
scoping { @klass.send(method, *args, &block) }
- elsif Array.method_defined?(method)
- ::ActiveRecord::Delegation.delegate method, :to => :to_a
+ elsif array_delegable?(method)
to_a.send(method, *args, &block)
elsif arel.respond_to?(method)
- ::ActiveRecord::Delegation.delegate method, :to => :arel
arel.send(method, *args, &block)
else
super
diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb
index 84aaa39fed..3a02bf90e9 100644
--- a/activerecord/lib/active_record/relation/finder_methods.rb
+++ b/activerecord/lib/active_record/relation/finder_methods.rb
@@ -1,7 +1,7 @@
-require 'active_support/core_ext/hash/indifferent_access'
-
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+.
@@ -13,9 +13,11 @@ module ActiveRecord
# Person.find([1]) # returns an array for the object with ID = 1
# Person.where("administrator = 1").order("created_on DESC").find(1)
#
- # Note 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
#
@@ -30,6 +32,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) }
@@ -39,7 +69,7 @@ module ActiveRecord
end
# Finds the first record matching the specified conditions. There
- # is no implied ording so if order matters, you should specify it
+ # is no implied ordering so if order matters, you should specify it
# yourself.
#
# If no record is found, returns <tt>nil</tt>.
@@ -60,7 +90,7 @@ module ActiveRecord
# order. The order will depend on the database implementation.
# If an order is supplied it will be respected.
#
- # Person.take # returns an object fetched by SELECT * FROM people
+ # Person.take # returns an object fetched by SELECT * FROM people LIMIT 1
# Person.take(5) # returns 5 objects fetched by SELECT * FROM people LIMIT 5
# Person.where(["name LIKE '%?'", name]).take
def take(limit = nil)
@@ -78,16 +108,25 @@ module ActiveRecord
#
# Person.first # returns the first object fetched by SELECT * FROM people
# Person.where(["user_name = ?", user_name]).first
- # Person.where(["user_name = :u", { :u => user_name }]).first
+ # Person.where(["user_name = :u", { u: user_name }]).first
# Person.order("created_on DESC").offset(5).first
# Person.first(3) # returns the first three objects fetched by SELECT * FROM people LIMIT 3
+ #
+ # ==== Rails 3
+ #
+ # Person.first # SELECT "people".* FROM "people" LIMIT 1
+ #
+ # NOTE: Rails 3 may not order this query by the primary key and the order
+ # will depend on the database implementation. In order to ensure that behavior,
+ # use <tt>User.order(:id).first</tt> instead.
+ #
+ # ==== Rails 4
+ #
+ # Person.first # SELECT "people".* FROM "people" ORDER BY "people"."id" ASC LIMIT 1
+ #
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
@@ -139,14 +178,14 @@ module ActiveRecord
# * 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
@@ -159,12 +198,13 @@ module ActiveRecord
# Person.exists?(false)
# Person.exists?
def exists?(conditions = :none)
- conditions = conditions.id if ActiveRecord::Model === conditions
+ conditions = conditions.id if Base === conditions
return false if !conditions
- join_dependency = construct_join_dependency_for_association_find
- 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
@@ -173,68 +213,94 @@ 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
- protected
+ # This method is called whenever no records are found with either a single
+ # id or multiple ids and raises a +ActiveRecord::RecordNotFound+ exception.
+ #
+ # The error message is different depending on whether a single id or
+ # multiple ids are provided. If multiple ids are provided, then the number
+ # of results obtained should be provided in the +result_size+ argument and
+ # the expected number of results should be provided in the +expected_size+
+ # argument.
+ def raise_record_not_found_exception!(ids, result_size, expected_size) #:nodoc:
+ conditions = arel.where_sql
+ conditions = " [#{conditions}]" if conditions
+
+ if Array(ids).size == 1
+ error = "Couldn't find #{@klass.name} with #{primary_key}=#{ids}#{conditions}"
+ else
+ error = "Couldn't find all #{@klass.name.pluralize} with IDs "
+ error << "(#{ids.join(", ")})#{conditions} (found #{result_size} results, but was looking for #{expected_size})"
+ end
- def find_with_associations
- join_dependency = construct_join_dependency_for_association_find
- relation = construct_relation_for_association_find(join_dependency)
- rows = connection.select_all(relation, 'SQL', relation.bind_values.dup)
- join_dependency.instantiate(rows)
- rescue ThrowResult
- []
+ raise RecordNotFound, error
end
- def construct_join_dependency_for_association_find
- including = (eager_load_values + includes_values).uniq
- ActiveRecord::Associations::JoinDependency.new(@klass, including, [])
+ private
+
+ def find_with_associations
+ join_dependency = construct_join_dependency
+
+ 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_relation_for_association_calculations
- including = (eager_load_values + includes_values).uniq
- join_dependency = ActiveRecord::Associations::JoinDependency.new(@klass, including, arel.froms.first)
- relation = except(:includes, :eager_load, :preload)
- apply_join_dependency(relation, join_dependency)
+ def construct_join_dependency(joins = [])
+ including = eager_load_values + includes_values
+ ActiveRecord::Associations::JoinDependency.new(@klass, including, joins)
end
- def construct_relation_for_association_find(join_dependency)
- relation = except(:includes, :eager_load, :preload, :select).select(join_dependency.columns)
- apply_join_dependency(relation, join_dependency)
+ def construct_relation_for_association_calculations
+ apply_join_dependency(self, construct_join_dependency(arel.froms.first))
end
def apply_join_dependency(relation, join_dependency)
- join_dependency.join_associations.each do |association|
- relation = association.join_relation(relation)
- end
+ relation = relation.except(:includes, :eager_load, :preload)
+ relation = relation.joins join_dependency
- limitable_reflections = using_limitable_reflections?(join_dependency.reflections)
-
- if !limitable_reflections && relation.limit_value
- limited_id_condition = construct_limited_ids_condition(relation.except(:select))
- relation = relation.where(limited_id_condition)
+ if using_limitable_reflections?(join_dependency.reflections)
+ relation
+ else
+ 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
-
- relation = relation.except(:limit, :offset) unless limitable_reflections
-
- relation
end
- def construct_limited_ids_condition(relation)
- orders = relation.order_values.map { |val| val.presence }.compact
- values = @klass.connection.distinct("#{@klass.connection.quote_table_name table_name}.#{primary_key}", orders)
+ 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!
- relation = relation.dup
+ id_rows = @klass.connection.select_all(relation.arel, 'SQL', relation.bind_values)
+ id_rows.map {|row| row[primary_key]}
+ end
- ids_array = relation.select(values).collect {|row| row[primary_key]}
- 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)
- return to_a.find { |*block_args| yield(*block_args) } if block_given?
+ raise UnknownPrimaryKey.new(@klass) if primary_key.nil?
expects_array = ids.first.kind_of?(Array)
return ids.first if expects_array && ids.first.empty?
@@ -261,11 +327,7 @@ module ActiveRecord
relation.bind_values += [[column, id]]
record = relation.take
- unless record
- conditions = arel.where_sql
- conditions = " [#{conditions}]" if conditions
- raise RecordNotFound, "Couldn't find #{@klass.name} with #{primary_key}=#{id}#{conditions}"
- end
+ raise_record_not_found_exception!(id, 0, 1) unless record
record
end
@@ -288,12 +350,7 @@ module ActiveRecord
if result.size == expected_size
result
else
- conditions = arel.where_sql
- conditions = " [#{conditions}]" if conditions
-
- error = "Couldn't find all #{@klass.name.pluralize} with IDs "
- error << "(#{ids.join(", ")})#{conditions} (found #{result.size} results, but was looking for #{expected_size})"
- raise RecordNotFound, error
+ raise_record_not_found_exception!(ids, result.size, expected_size)
end
end
@@ -309,12 +366,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
@@ -330,9 +390,5 @@ module ActiveRecord
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 59226d316e..182b9ed89c 100644
--- a/activerecord/lib/active_record/relation/merger.rb
+++ b/activerecord/lib/active_record/relation/merger.rb
@@ -1,4 +1,5 @@
require 'active_support/core_ext/hash/keys'
+require "set"
module ActiveRecord
class Relation
@@ -21,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
@@ -38,20 +39,17 @@ module ActiveRecord
end
class Merger # :nodoc:
- attr_reader :relation, :values
+ 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
end
NORMAL_VALUES = Relation::SINGLE_VALUE_METHODS +
Relation::MULTI_VALUE_METHODS -
- [:where, :order, :bind, :reverse_order, :lock, :create_with, :reordering, :from] # :nodoc:
+ [:joins, :where, :order, :bind, :reverse_order, :lock, :create_with, :reordering, :from] # :nodoc:
def normal_values
NORMAL_VALUES
@@ -60,26 +58,78 @@ 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
merge_single_values
+ merge_joins
relation
end
private
+ def merge_joins
+ return if values[:joins].blank?
+
+ if other.klass == relation.klass
+ relation.joins!(*values[:joins])
+ else
+ joins_dependency, rest = values[:joins].partition do |join|
+ case join
+ when Hash, Symbol, Array
+ true
+ else
+ false
+ end
+ end
+
+ join_dependency = ActiveRecord::Associations::JoinDependency.new(other.klass,
+ joins_dependency,
+ [])
+ relation.joins! rest
+
+ @relation = relation.joins join_dependency
+ end
+ end
+
def merge_multi_values
- relation.where_values = merged_wheres
- relation.bind_values = merged_binds
+ lhs_wheres = relation.where_values
+ rhs_wheres = values[:where] || []
+
+ lhs_binds = relation.bind_values
+ rhs_binds = values[:bind] || []
+
+ removed, kept = partition_overwrites(lhs_wheres, rhs_wheres)
+
+ where_values = kept + rhs_wheres
+ bind_values = filter_binds(lhs_binds, removed) + rhs_binds
+
+ conn = relation.klass.connection
+ bv_index = 0
+ where_values.map! do |node|
+ if Arel::Nodes::Equality === node && Arel::Nodes::BindParam === node.right
+ substitute = conn.substitute_at(bind_values[bv_index].first, bv_index)
+ bv_index += 1
+ Arel::Nodes::Equality.new(node.left, substitute)
+ else
+ node
+ end
+ end
+
+ relation.where_values = where_values
+ relation.bind_values = bind_values
if values[:reordering]
# 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
@@ -96,34 +146,28 @@ module ActiveRecord
end
end
- def merged_binds
- if values[:bind]
- (relation.bind_values + values[:bind]).uniq(&:first)
- else
- relation.bind_values
- end
+ def filter_binds(lhs_binds, removed_wheres)
+ return lhs_binds if removed_wheres.empty?
+
+ set = Set.new removed_wheres.map { |x| x.left.name }
+ lhs_binds.dup.delete_if { |col,_| set.include? col.name }
end
- def merged_wheres
- if values[:where]
- merged_wheres = relation.where_values + values[:where]
-
- unless relation.where_values.empty?
- # Remove equalities with duplicated left-hand. Last one wins.
- seen = {}
- merged_wheres = merged_wheres.reverse.reject { |w|
- nuke = false
- if w.respond_to?(:operator) && w.operator == :==
- nuke = seen[w.left]
- seen[w.left] = true
- end
- nuke
- }.reverse
- end
+ # Remove equalities from the existing relation with a LHS which is
+ # present in the relation being merged in.
+ # returns [things_to_remove, things_to_keep]
+ def partition_overwrites(lhs_wheres, rhs_wheres)
+ if lhs_wheres.empty? || rhs_wheres.empty?
+ return [[], lhs_wheres]
+ end
- merged_wheres
- else
- relation.where_values
+ nodes = rhs_wheres.find_all do |w|
+ w.respond_to?(:operator) && w.operator == :==
+ end
+ seen = Set.new(nodes) { |node| node.left }
+
+ lhs_wheres.partition do |w|
+ w.respond_to?(:operator) && w.operator == :== && seen.include?(w.left)
end
end
end
diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb
index 71030cb5d7..c60cd27a83 100644
--- a/activerecord/lib/active_record/relation/predicate_builder.rb
+++ b/activerecord/lib/active_record/relation/predicate_builder.rb
@@ -1,5 +1,20 @@
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 = []
@@ -7,12 +22,12 @@ module ActiveRecord
table = default_table
if value.is_a?(Hash)
- table = Arel::Table.new(column, default_table.engine)
- association = klass.reflect_on_association(column.to_sym)
-
if value.empty?
- queries.concat ['1 = 2']
+ queries << '1=0'
else
+ table = Arel::Table.new(column, default_table.engine)
+ association = klass.reflect_on_association(column.to_sym)
+
value.each do |k, v|
queries.concat expand(association && association.klass, table, k, v)
end
@@ -36,11 +51,11 @@ module ActiveRecord
queries = []
# Find the foreign key when using queries such as:
- # Post.where(:author => author)
+ # Post.where(author: author)
#
# For polymorphic relationships, find the foreign key and type:
- # PriceEstimate.where(:estimate_of => treasure)
- if klass && value.class < ActiveRecord::Tag && reflection = klass.reflect_on_association(column.to_sym)
+ # PriceEstimate.where(estimate_of: treasure)
+ if klass && value.is_a?(Base) && reflection = klass.reflect_on_association(column.to_sym)
if reflection.polymorphic?
queries << build(table[reflection.foreign_type], value.class.base_class)
end
@@ -48,7 +63,7 @@ module ActiveRecord
column = reflection.foreign_key
end
- queries << build(table[column.to_sym], value)
+ queries << build(table[column], value)
queries
end
@@ -58,49 +73,41 @@ module ActiveRecord
key
else
key = key.to_s
- key.split('.').first.to_sym if key.include?('.')
+ key.split('.').first if key.include?('.')
end
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, ActiveRecord::Associations::CollectionProxy
- values = value.to_a.map {|x| x.is_a?(ActiveRecord::Model) ? 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::Model
- 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 a9ace10093..bffd8b5d0f 100644
--- a/activerecord/lib/active_record/relation/query_methods.rb
+++ b/activerecord/lib/active_record/relation/query_methods.rb
@@ -4,6 +4,54 @@ module ActiveRecord
module QueryMethods
extend ActiveSupport::Concern
+ # WhereChain objects act as placeholder for queries in which #where does not have any parameter.
+ # In this case, #where must be chained with #not to return a new relation.
+ class WhereChain
+ def initialize(scope)
+ @scope = scope
+ end
+
+ # Returns a new relation expressing WHERE + NOT condition according to
+ # the conditions in the arguments.
+ #
+ # +not+ accepts conditions as a string, array, or hash. See #where for
+ # more details on each format.
+ #
+ # User.where.not("name = 'Jon'")
+ # # SELECT * FROM users WHERE NOT (name = 'Jon')
+ #
+ # User.where.not(["name = ?", "Jon"])
+ # # SELECT * FROM users WHERE NOT (name = 'Jon')
+ #
+ # User.where.not(name: "Jon")
+ # # SELECT * FROM users WHERE name != 'Jon'
+ #
+ # User.where.not(name: nil)
+ # # SELECT * FROM users WHERE name IS NOT NULL
+ #
+ # User.where.not(name: %w(Ko1 Nobu))
+ # # SELECT * FROM users WHERE name NOT IN ('Ko1', 'Nobu')
+ #
+ # User.where.not(name: "Jon", role: "admin")
+ # # SELECT * FROM users WHERE name != 'Jon' AND role != 'admin'
+ def not(opts, *rest)
+ where_value = @scope.send(:build_where, opts, rest).map do |rel|
+ case rel
+ when Arel::Nodes::In
+ Arel::Nodes::NotIn.new(rel.left, rel.right)
+ when Arel::Nodes::Equality
+ Arel::Nodes::NotEqual.new(rel.left, rel.right)
+ when String
+ Arel::Nodes::Not.new(Arel::Nodes::SqlLiteral.new(rel))
+ else
+ Arel::Nodes::Not.new(rel)
+ end
+ end
+ @scope.where_values += where_value
+ @scope
+ end
+ end
+
Relation::MULTI_VALUE_METHODS.each do |name|
class_eval <<-CODE, __FILE__, __LINE__ + 1
def #{name}_values # def select_values
@@ -52,6 +100,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
@@ -63,14 +119,15 @@ module ActiveRecord
#
# User.includes(:posts).where('posts.name = ?', 'example').references(:posts)
def includes(*args)
- args.empty? ? self : spawn.includes!(*args)
+ check_if_method_has_arguments!(:includes, args)
+ spawn.includes!(*args)
end
- # Like #includes, but modifies the relation in place.
- def includes!(*args)
- args.reject! {|a| a.blank? }
+ def includes!(*args) # :nodoc:
+ args.reject!(&:blank?)
+ args.flatten!
- self.includes_values = (includes_values + args).flatten.uniq
+ self.includes_values |= args
self
end
@@ -81,11 +138,11 @@ module ActiveRecord
# FROM "users" LEFT OUTER JOIN "posts" ON "posts"."user_id" =
# "users"."id"
def eager_load(*args)
- args.blank? ? self : spawn.eager_load!(*args)
+ check_if_method_has_arguments!(:eager_load, args)
+ spawn.eager_load!(*args)
end
- # Like #eager_load, but modifies relation in place.
- def eager_load!(*args)
+ def eager_load!(*args) # :nodoc:
self.eager_load_values += args
self
end
@@ -95,11 +152,11 @@ module ActiveRecord
# User.preload(:posts)
# => SELECT "posts".* FROM "posts" WHERE "posts"."user_id" IN (1, 2, 3)
def preload(*args)
- args.blank? ? self : spawn.preload!(*args)
+ check_if_method_has_arguments!(:preload, args)
+ spawn.preload!(*args)
end
- # Like #preload, but modifies relation in place.
- def preload!(*args)
+ def preload!(*args) # :nodoc:
self.preload_values += args
self
end
@@ -113,14 +170,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)
- args.blank? ? self : spawn.references!(*args)
+ check_if_method_has_arguments!(:references, args)
+ spawn.references!(*args)
end
- # Like #references, but modifies relation in place.
- def references!(*args)
+ 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
@@ -148,6 +206,16 @@ module ActiveRecord
# Model.select(:field, :other_field, :and_one_more)
# # => [#<Model field: "value", other_field: "value", and_one_more: "value">]
#
+ # You can also use one or more strings, which will be used unchanged as SELECT fields.
+ #
+ # Model.select('field AS field_one', 'other_field AS field_two')
+ # # => [#<Model field: "value", other_field: "value">]
+ #
+ # If an alias was specified, it will be accessible from the resulting objects:
+ #
+ # Model.select('field AS field_one').first.field_one
+ # # => "value"
+ #
# Accessing attributes of an object that do not have fields retrieved by a select
# will throw <tt>ActiveModel::MissingAttributeError</tt>:
#
@@ -162,9 +230,10 @@ module ActiveRecord
end
end
- # Like #select, but modifies relation in place.
- def select!(*fields)
- self.select_values += fields.flatten
+ def select!(*fields) # :nodoc:
+ fields.flatten!
+
+ self.select_values += fields
self
end
@@ -180,12 +249,15 @@ module ActiveRecord
#
# User.group(:name)
# => [#<User id: 3, name: "Foo", ...>, #<User id: 2, name: "Oscar", ...>]
+ #
+ # User.group('name AS grouped_name, age')
+ # => [#<User id: 3, name: "Foo", age: 21, ...>, #<User id: 2, name: "Oscar", age: 21, ...>, #<User id: 5, name: "Foo", age: 23, ...>]
def group(*args)
- args.blank? ? self : spawn.group!(*args)
+ check_if_method_has_arguments!(:group, args)
+ spawn.group!(*args)
end
- # Like #group, but modifies relation in place.
- def group!(*args)
+ def group!(*args) # :nodoc:
args.flatten!
self.group_values += args
@@ -212,20 +284,14 @@ module ActiveRecord
# User.order(:name, email: :desc)
# => SELECT "users".* FROM "users" ORDER BY "users"."name" ASC, "users"."email" DESC
def order(*args)
- args.blank? ? self : spawn.order!(*args)
+ check_if_method_has_arguments!(:order, args)
+ spawn.order!(*args)
end
- # Like #order, but modifies relation in place.
- def order!(*args)
- args.flatten!
-
- validate_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?
+ def order!(*args) # :nodoc:
+ preprocess_order_args(args)
- self.order_values = args + self.order_values
+ self.order_values += args
self
end
@@ -237,32 +303,103 @@ 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)
- args.blank? ? self : spawn.reorder!(*args)
+ check_if_method_has_arguments!(:reorder, args)
+ spawn.reorder!(*args)
end
- # Like #reorder, but modifies relation in place.
- def reorder!(*args)
- args.flatten!
-
- validate_order_args args
+ def reorder!(*args) # :nodoc:
+ preprocess_order_args(args)
self.reordering_value = true
self.order_values = args
self
end
+ VALID_UNSCOPING_VALUES = Set.new([:where, :select, :group, :order, :lock,
+ :limit, :offset, :joins, :includes, :from,
+ :readonly, :having])
+
+ # Removes an unwanted relation that is already defined on a chain of relations.
+ # This is useful when passing around chains of relations and would like to
+ # modify the relations without reconstructing the entire chain.
+ #
+ # User.order('email DESC').unscope(:order) == User.all
+ #
+ # The method arguments are symbols which correspond to the names of the methods
+ # which should be unscoped. The valid arguments are given in VALID_UNSCOPING_VALUES.
+ # The method can also be called with multiple arguments. For example:
+ #
+ # User.order('email DESC').select('id').where(name: "John")
+ # .unscope(:order, :select, :where) == User.all
+ #
+ # One can additionally pass a hash as an argument to unscope specific :where values.
+ # This is done by passing a hash with a single key-value pair. The key should be
+ # :where and the value should be the where value to unscope. For example:
+ #
+ # 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.
+ #
+ # 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:
+ #
+ # Post.comments.except(:order)
+ #
+ # will still have an order if it comes from the default_scope on Comment.
+ def unscope(*args)
+ check_if_method_has_arguments!(:unscope, args)
+ spawn.unscope!(*args)
+ end
+
+ def unscope!(*args) # :nodoc:
+ args.flatten!
+
+ args.each do |scope|
+ case scope
+ when Symbol
+ symbol_unscoping(scope)
+ when Hash
+ scope.each do |key, target_value|
+ if key != :where
+ raise ArgumentError, "Hash arguments in .unscope(*args) must have :where as the key."
+ end
+
+ Array(target_value).each do |val|
+ where_unscoping(val)
+ end
+ end
+ else
+ raise ArgumentError, "Unrecognized scoping: #{args.inspect}. Use .unscope(where: :attribute_name) or .unscope(:order), for example."
+ end
+ end
+
+ self
+ end
+
# Performs a joins on +args+:
#
# User.joins(:posts)
# => SELECT "users".* FROM "users" INNER JOIN "posts" ON "posts"."user_id" = "users"."id"
+ #
+ # You can use strings in order to customize your joins:
+ #
+ # User.joins("LEFT JOIN bookmarks ON bookmarks.bookmarkable_type = 'Post' AND bookmarks.user_id = users.id")
+ # => SELECT "users".* FROM "users" LEFT JOIN bookmarks ON bookmarks.bookmarkable_type = 'Post' AND bookmarks.user_id = users.id
def joins(*args)
- args.compact.blank? ? self : spawn.joins!(*args.flatten)
+ check_if_method_has_arguments!(:joins, args)
+
+ args.compact!
+ args.flatten!
+
+ spawn.joins!(*args)
end
- # Like #joins, but modifies relation in place.
- def joins!(*args)
+ def joins!(*args) # :nodoc:
self.joins_values += args
self
end
@@ -271,7 +408,7 @@ module ActiveRecord
spawn.bind!(value)
end
- def bind!(value)
+ def bind!(value) # :nodoc:
self.bind_values += [value]
self
end
@@ -357,17 +494,17 @@ module ActiveRecord
# author = Author.find(1)
#
# # The following queries will be equivalent:
- # Post.where(:author => author)
- # Post.where(:author_id => author)
+ # Post.where(author: author)
+ # Post.where(author_id: author)
#
# This also works with polymorphic belongs_to relationships:
#
- # treasure = Treasure.create(:name => 'gold coins')
- # treasure.price_estimates << PriceEstimate.create(:price => 125)
+ # treasure = Treasure.create(name: 'gold coins')
+ # treasure.price_estimates << PriceEstimate.create(price: 125)
#
# # The following queries will be equivalent:
- # PriceEstimate.where(:estimate_of => treasure)
- # PriceEstimate.where(:estimate_of_type => 'Treasure', :estimate_of_id => treasure)
+ # PriceEstimate.where(estimate_of: treasure)
+ # PriceEstimate.where(estimate_of_type: 'Treasure', estimate_of_id: treasure)
#
# === Joins
#
@@ -379,22 +516,41 @@ module ActiveRecord
# For hash conditions, you can either use the table name in the key, or use a sub-hash.
#
# User.joins(:posts).where({ "posts.published" => true })
- # User.joins(:posts).where({ :posts => { :published => true } })
+ # User.joins(:posts).where({ posts: { published: true } })
+ #
+ # === no argument
+ #
+ # If no argument is passed, #where returns a new instance of WhereChain, that
+ # can be chained with #not to return a new relation that negates the where clause.
#
- # === empty condition
+ # User.where.not(name: "Jon")
+ # # SELECT * FROM users WHERE name != 'Jon'
#
- # If the condition returns true for blank?, then where is a no-op and returns the current relation.
- def where(opts, *rest)
- opts.blank? ? self : spawn.where!(opts, *rest)
+ # See WhereChain for more details on #not.
+ #
+ # === blank condition
+ #
+ # If the condition is any blank-ish object, then #where is a no-op and returns
+ # the current relation.
+ def where(opts = :chain, *rest)
+ if opts == :chain
+ WhereChain.new(spawn)
+ elsif opts.blank?
+ self
+ else
+ spawn.where!(opts, *rest)
+ end
end
- # #where! is identical to #where, except that instead of returning a new relation, it adds
- # the condition to the existing relation.
- def where!(opts, *rest)
- references!(PredicateBuilder.references(opts)) if Hash === opts
+ def where!(opts = :chain, *rest) # :nodoc:
+ if opts == :chain
+ WhereChain.new(self)
+ else
+ references!(PredicateBuilder.references(opts)) if Hash === opts
- self.where_values += build_where(opts, rest)
- self
+ self.where_values += build_where(opts, rest)
+ self
+ end
end
# Allows to specify a HAVING clause. Note that you can't use HAVING
@@ -405,8 +561,7 @@ module ActiveRecord
opts.blank? ? self : spawn.having!(opts, *rest)
end
- # Like #having, but modifies relation in place.
- def having!(opts, *rest)
+ def having!(opts, *rest) # :nodoc:
references!(PredicateBuilder.references(opts)) if Hash === opts
self.having_values += build_where(opts, rest)
@@ -422,8 +577,7 @@ module ActiveRecord
spawn.limit!(value)
end
- # Like #limit, but modifies relation in place.
- def limit!(value)
+ def limit!(value) # :nodoc:
self.limit_value = value
self
end
@@ -439,8 +593,7 @@ module ActiveRecord
spawn.offset!(value)
end
- # Like #offset, but modifies relation in place.
- def offset!(value)
+ def offset!(value) # :nodoc:
self.offset_value = value
self
end
@@ -451,8 +604,7 @@ module ActiveRecord
spawn.lock!(locks)
end
- # Like #lock, but modifies relation in place.
- def lock!(locks = true)
+ def lock!(locks = true) # :nodoc:
case locks
when String, TrueClass, NilClass
self.lock_value = locks || true
@@ -468,7 +620,7 @@ module ActiveRecord
#
# 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 quering the database.
+ # 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.
@@ -478,13 +630,13 @@ module ActiveRecord
#
# For example:
#
- # @posts = current_user.visible_posts.where(:name => params[:name])
+ # @posts = current_user.visible_posts.where(name: params[:name])
# # => the visible_posts method is expected to return a chainable Relation
#
# def visible_posts
# case role
# when 'Country Manager'
- # Post.where(:country => country)
+ # Post.where(country: country)
# when 'Reviewer'
# Post.published
# when 'Bad User'
@@ -496,6 +648,10 @@ module ActiveRecord
extending(NullRelation)
end
+ def none! # :nodoc:
+ extending!(NullRelation)
+ end
+
# Sets readonly attributes for the returned relation. If value is
# true (default), attempting to update a record will result in an error.
#
@@ -506,8 +662,7 @@ module ActiveRecord
spawn.readonly!(value)
end
- # Like #readonly, but modifies relation in place.
- def readonly!(value = true)
+ def readonly!(value = true) # :nodoc:
self.readonly_value = value
self
end
@@ -529,12 +684,7 @@ module ActiveRecord
spawn.create_with!(value)
end
- # Like #create_with but modifies the relation in place. Raises
- # +ImmutableRelation+ if the relation has already been loaded.
- #
- # users = User.all.create_with!(name: 'Oscar')
- # users.new.name # => 'Oscar'
- def create_with!(value)
+ def create_with!(value) # :nodoc:
self.create_with_value = value ? create_with_value.merge(value) : {}
self
end
@@ -556,8 +706,7 @@ module ActiveRecord
spawn.from!(value, subquery_name)
end
- # Like #from, but modifies relation in place.
- def from!(value, subquery_name = nil)
+ def from!(value, subquery_name = nil) # :nodoc:
self.from_value = [value, subquery_name]
self
end
@@ -567,20 +716,22 @@ module ActiveRecord
# User.select(:name)
# # => Might return two records with the same name
#
- # User.select(:name).uniq
- # # => Returns 1 record per unique name
+ # User.select(:name).distinct
+ # # => Returns 1 record per distinct name
#
- # User.select(:name).uniq.uniq(false)
+ # User.select(:name).distinct.distinct(false)
# # => You can also remove the uniqueness
- def uniq(value = true)
- spawn.uniq!(value)
+ def distinct(value = true)
+ spawn.distinct!(value)
end
+ alias uniq distinct
- # Like #uniq, but modifies relation in place.
- def uniq!(value = true)
- self.uniq_value = value
+ # Like #distinct, but modifies relation in place.
+ def distinct!(value = true) # :nodoc:
+ self.distinct_value = value
self
end
+ alias uniq! distinct!
# Used to extend a scope with additional methods, either through
# a module or through a block provided.
@@ -626,11 +777,11 @@ module ActiveRecord
end
end
- # Like #extending, but modifies relation in place.
- def extending!(*modules, &block)
- modules << Module.new(&block) if block_given?
+ def extending!(*modules, &block) # :nodoc:
+ 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
@@ -643,37 +794,36 @@ module ActiveRecord
spawn.reverse_order!
end
- # Like #reverse_order, but modifies relation in place.
- def reverse_order!
+ def reverse_order! # :nodoc:
self.reverse_order_value = !reverse_order_value
self
end
# 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 - ['']).uniq)
- 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)
build_select(arel, select_values.uniq)
- arel.distinct(uniq_value)
+ arel.distinct(distinct_value)
arel.from(build_from) if from_value
arel.lock(lock_value) if lock_value
@@ -682,14 +832,45 @@ module ActiveRecord
private
+ def symbol_unscoping(scope)
+ if !VALID_UNSCOPING_VALUES.include?(scope)
+ raise ArgumentError, "Called unscope() with invalid unscoping argument ':#{scope}'. Valid arguments are :#{VALID_UNSCOPING_VALUES.to_a.join(", :")}."
+ end
+
+ single_val_method = Relation::SINGLE_VALUE_METHODS.include?(scope)
+ unscope_code = :"#{scope}_value#{'s' unless single_val_method}="
+
+ case scope
+ when :order
+ self.send(:reverse_order_value=, false)
+ result = []
+ else
+ result = [] unless single_val_method
+ end
+
+ self.send(unscope_code, result)
+ end
+
+ def where_unscoping(target_value)
+ target_value_sym = target_value.to_sym
+
+ where_values.reject! do |rel|
+ case rel
+ when Arel::Nodes::In, Arel::Nodes::NotIn, Arel::Nodes::Equality, Arel::Nodes::NotEqual
+ subrelation = (rel.left.kind_of?(Arel::Attributes::Attribute) ? rel.left : rel.right)
+ subrelation.name.to_sym == target_value_sym
+ else
+ raise "unscope(where: #{target_value.inspect}) failed: unscoping #{rel.class} is unimplemented."
+ end
+ end
+ 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)
@@ -701,22 +882,34 @@ 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
+ opts = PredicateBuilder.resolve_column_aliases(klass, opts)
attributes = @klass.send(:expand_hash_conditions_for_aggregates, opts)
+
+ attributes.values.grep(ActiveRecord::Relation) do |rel|
+ self.bind_values += rel.bind_values
+ end
+
PredicateBuilder.build_from_hash(klass, attributes, table)
else
[opts]
@@ -728,6 +921,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
@@ -738,24 +932,22 @@ module ActiveRecord
buckets = joins.group_by do |join|
case join
when String
- 'string_join'
+ :string_join
when Hash, Symbol, Array
- 'association_join'
- when ActiveRecord::Associations::JoinDependency::JoinAssociation
- 'stashed_join'
+ :association_join
+ when ActiveRecord::Associations::JoinDependency
+ :stashed_join
when Arel::Nodes::Join
- 'join_node'
+ :join_node
else
raise 'unknown class: %s' % join.class.name
end
end
- 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
+ association_joins = buckets[:association_join] || []
+ stashed_association_joins = buckets[:stashed_join] || []
+ join_nodes = (buckets[:join_node] || []).uniq
+ string_joins = (buckets[:string_join] || []).map(&:strip).uniq
join_list = join_nodes + custom_join_ast(manager, string_joins)
@@ -765,23 +957,17 @@ 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)
- 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
arel.project(*selects)
else
arel.project(@klass.arel_table[Arel.star])
@@ -791,12 +977,12 @@ module ActiveRecord
def reverse_sql_order(order_query)
order_query = ["#{quoted_table_name}.#{quoted_primary_key} ASC"] if order_query.empty?
- order_query.map do |o|
+ order_query.flat_map do |o|
case o
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
@@ -809,18 +995,19 @@ module ActiveRecord
else
o
end
- end.flatten
+ end
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?).map do |order|
+ orders = orders.flat_map do |order|
case order
when Symbol
table[order].asc
@@ -829,18 +1016,53 @@ module ActiveRecord
else
order
end
- end.flatten
+ 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|
+ arg.is_a?(Symbol) ? Arel::Nodes::Ascending.new(klass.arel_table[arg]) : arg
+ end
+ 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.
+ #
+ # Example:
+ #
+ # Post.references() # => raises an error
+ # Post.references([]) # => does not raise an error
+ #
+ # This particular method should be called with a method_name and the args
+ # passed into that method as an input. For example:
+ #
+ # def references(*args)
+ # check_if_method_has_arguments!("references", args)
+ # ...
+ # end
+ def check_if_method_has_arguments!(method_name, args)
+ if args.blank?
+ raise ArgumentError, "The method .#{method_name}() must contain arguments."
+ end
+ end
end
end
diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb
index 5394c1b28b..2552cbd234 100644
--- a/activerecord/lib/active_record/relation/spawn_methods.rb
+++ b/activerecord/lib/active_record/relation/spawn_methods.rb
@@ -12,14 +12,11 @@ module ActiveRecord
# Merges in the conditions from <tt>other</tt>, if <tt>other</tt> is an <tt>ActiveRecord::Relation</tt>.
# Returns an array representing the intersection of the resulting records with <tt>other</tt>, if <tt>other</tt> is an array.
- #
- # ==== Examples
- #
- # Post.where(:published => true).joins(:comments).merge( Comment.where(:spam => false) )
+ # Post.where(published: true).joins(:comments).merge( Comment.where(spam: false) )
# # Performs a single join query with both where conditions.
#
# recent_posts = Post.order('created_at DESC').first(5)
- # Post.where(:published => true).merge(recent_posts)
+ # Post.where(published: true).merge(recent_posts)
# # Returns the intersection of all published posts with the 5 most recently created posts.
# # (This is just an example. You'd probably want to do this with a single query!)
#
@@ -29,7 +26,6 @@ module ActiveRecord
# # => Post.where(published: true).joins(:comments)
#
# This is mainly intended for sharing common conditions between multiple associations.
- #
def merge(other)
if other.is_a?(Array)
to_a & other
@@ -40,8 +36,7 @@ module ActiveRecord
end
end
- # Like #merge, but applies changes in place.
- def merge!(other)
+ def merge!(other) # :nodoc:
if !other.is_a?(Relation) && other.respond_to?(:to_proc)
instance_exec(&other)
else
@@ -52,31 +47,26 @@ module ActiveRecord
# Removes from the query the condition(s) specified in +skips+.
#
- # Example:
- #
# Post.order('id asc').except(:order) # discards the order condition
# Post.where('id > 10').order('id asc').except(:where) # discards the where condition but keeps the order
- #
def except(*skips)
- result = Relation.new(klass, table, values.except(*skips))
- result.default_scoped = default_scoped
- result.extend(*extending_values) if extending_values.any?
- result
+ relation_with values.except(*skips)
end
# Removes any condition from the query other than the one(s) specified in +onlies+.
#
- # Example:
- #
# Post.order('id asc').only(:where) # discards the order condition
# Post.order('id asc').only(:where, :order) # uses the specified order
- #
def only(*onlies)
- result = Relation.new(klass, table, values.slice(*onlies))
- result.default_scoped = default_scoped
- result.extend(*extending_values) if extending_values.any?
- result
+ relation_with values.slice(*onlies)
end
+ private
+
+ def relation_with(values) # :nodoc:
+ result = Relation.create(klass, table, values)
+ result.extend(*extending_values) if extending_values.any?
+ result
+ end
end
end
diff --git a/activerecord/lib/active_record/result.rb b/activerecord/lib/active_record/result.rb
index 425b9b41d8..1dc3bf3f12 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
@@ -52,14 +89,29 @@ module ActiveRecord
end
private
+
def hash_rows
@hash_rows ||=
begin
# We freeze the strings to prevent them getting duped when
- # used as keys in ActiveRecord::Model's @attributes hash
+ # 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
new file mode 100644
index 0000000000..63e6738622
--- /dev/null
+++ b/activerecord/lib/active_record/runtime_registry.rb
@@ -0,0 +1,17 @@
+require 'active_support/per_thread_registry'
+
+module ActiveRecord
+ # This is a thread locals registry for Active Record. For example:
+ #
+ # ActiveRecord::RuntimeRegistry.connection_handler
+ #
+ # returns the connection handler local to the current thread.
+ #
+ # See the documentation of <tt>ActiveSupport::PerThreadRegistry</tt>
+ # for further details.
+ class RuntimeRegistry # :nodoc:
+ extend ActiveSupport::PerThreadRegistry
+
+ attr_accessor :connection_handler, :sql_runtime, :connection_id
+ end
+end
diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb
index f3e47a958e..cab8fd745a 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>.
@@ -17,7 +17,7 @@ module ActiveRecord
# Accepts an array, hash, or string of SQL conditions and sanitizes
# them into a valid SQL fragment for a WHERE clause.
# ["name='%s' and group_id='%s'", "foo'bar", 4] returns "name='foo''bar' and group_id='4'"
- # { :name => "foo'bar", :group_id => 4 } returns "name='foo''bar' and group_id='4'"
+ # { name: "foo'bar", group_id: 4 } returns "name='foo''bar' and group_id='4'"
# "name='foo''bar' and group_id='4'" returns "name='foo''bar' and group_id='4'"
def sanitize_sql_for_conditions(condition, table_name = self.table_name)
return nil if condition.blank?
@@ -32,11 +32,11 @@ module ActiveRecord
# Accepts an array, hash, or string of SQL conditions and sanitizes
# them into a valid SQL fragment for a SET clause.
- # { :name => nil, :group_id => 4 } returns "name = NULL , group_id='4'"
- def sanitize_sql_for_assignment(assignments)
+ # { name: nil, group_id: 4 } returns "name = NULL , group_id='4'"
+ def sanitize_sql_for_assignment(assignments, default_table_name = self.table_name)
case assignments
when Array; sanitize_sql_array(assignments)
- when Hash; sanitize_sql_hash_for_assignment(assignments)
+ when Hash; sanitize_sql_hash_for_assignment(assignments, default_table_name)
else assignments
end
end
@@ -46,12 +46,12 @@ module ActiveRecord
# aggregate attribute values.
# Given:
# class Person < ActiveRecord::Base
- # composed_of :address, :class_name => "Address",
- # :mapping => [%w(address_street street), %w(address_city city)]
+ # composed_of :address, class_name: "Address",
+ # mapping: [%w(address_street street), %w(address_city city)]
# end
# Then:
- # { :address => Address.new("813 abc st.", "chicago") }
- # # => { :address_street => "813 abc st.", :address_city => "chicago" }
+ # { address: Address.new("813 abc st.", "chicago") }
+ # # => { address_street: "813 abc st.", address_city: "chicago" }
def expand_hash_conditions_for_aggregates(attrs)
expanded_attrs = {}
attrs.each do |attr, value|
@@ -72,35 +72,36 @@ module ActiveRecord
end
# Sanitizes a hash of attribute/value pairs into SQL conditions for a WHERE clause.
- # { :name => "foo'bar", :group_id => 4 }
+ # { name: "foo'bar", group_id: 4 }
# # => "name='foo''bar' and group_id= 4"
- # { :status => nil, :group_id => [1,2,3] }
+ # { status: nil, group_id: [1,2,3] }
# # => "status IS NULL and group_id IN (1,2,3)"
- # { :age => 13..18 }
+ # { age: 13..18 }
# # => "age BETWEEN 13 AND 18"
# { 'other_records.id' => 7 }
# # => "`other_records`.`id` = 7"
- # { :other_records => { :id => 7 } }
+ # { other_records: { id: 7 } }
# # => "`other_records`.`id` = 7"
# And for value objects on a composed_of relationship:
- # { :address => Address.new("123 abc st.", "chicago") }
+ # { address: Address.new("123 abc st.", "chicago") }
# # => "address_street='123 abc st.' and address_city='chicago'"
def sanitize_sql_hash_for_conditions(attrs, default_table_name = self.table_name)
+ attrs = PredicateBuilder.resolve_column_aliases self, attrs
attrs = expand_hash_conditions_for_aggregates(attrs)
table = Arel::Table.new(table_name, arel_engine).alias(default_table_name)
- PredicateBuilder.build_from_hash(self.class, attrs, table).map { |b|
+ PredicateBuilder.build_from_hash(self, attrs, table).map { |b|
connection.visitor.accept b
}.join(' AND ')
end
alias_method :sanitize_sql_hash, :sanitize_sql_hash_for_conditions
# Sanitizes a hash of attribute/value pairs into SQL conditions for a SET clause.
- # { :status => nil, :group_id => 1 }
+ # { status: nil, group_id: 1 }
# # => "status = NULL , group_id = 1"
- def sanitize_sql_hash_for_assignment(attrs)
+ def sanitize_sql_hash_for_assignment(attrs, table)
attrs.map do |attr, value|
- "#{connection.quote_column_name(attr)} = #{quote_bound_value(value)}"
+ "#{connection.quote_table_name_for_assignment(table, attr)} = #{quote_bound_value(value)}"
end.join(', ')
end
@@ -126,7 +127,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,7 +145,7 @@ 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
diff --git a/activerecord/lib/active_record/schema.rb b/activerecord/lib/active_record/schema.rb
index eaa4aa7086..4bfd0167a4 100644
--- a/activerecord/lib/active_record/schema.rb
+++ b/activerecord/lib/active_record/schema.rb
@@ -29,16 +29,21 @@ module ActiveRecord
# ActiveRecord::Schema is only supported by database adapters that also
# support migrations, the two features being very similar.
class Schema < Migration
+
+ # Returns the migrations paths.
+ #
+ # ActiveRecord::Schema.new.migrations_paths
+ # # => ["db/migrate"] # Rails migration path by default.
def migrations_paths
ActiveRecord::Migrator.migrations_paths
end
- def define(info, &block)
+ def define(info, &block) # :nodoc:
instance_eval(&block)
unless info[:version].blank?
initialize_schema_migrations_table
- assume_migrated_upto_version(info[:version], migrations_paths)
+ connection.assume_migrated_upto_version(info[:version], migrations_paths)
end
end
diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb
index 36bde44e7c..e055d571ab 100644
--- a/activerecord/lib/active_record/schema_dumper.rb
+++ b/activerecord/lib/active_record/schema_dumper.rb
@@ -17,13 +17,24 @@ 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)
header(stream)
+ extensions(stream)
tables(stream)
trailer(stream)
stream
@@ -31,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)
@@ -66,6 +78,18 @@ HEADER
stream.puts "end"
end
+ def extensions(stream)
+ return unless @connection.supports_extensions?
+ extensions = @connection.extensions
+ if extensions.any?
+ stream.puts " # These are extensions that must be enabled in order to support this database"
+ extensions.each do |extension|
+ stream.puts " enable_extension #{extension.inspect}"
+ end
+ stream.puts
+ end
+ end
+
def tables(stream)
@connection.tables.sort.each do |tbl|
next if ['schema_migrations', ignore_tables].flatten.any? do |ignored|
@@ -93,9 +117,13 @@ HEADER
end
tbl.print " create_table #{remove_prefix_and_suffix(table).inspect}"
- if columns.detect { |c| c.name == pk }
+ pkcol = columns.detect { |c| c.name == pk }
+ if pkcol
if pk != 'id'
tbl.print %Q(, primary_key: "#{pk}")
+ elsif pkcol.sql_type == 'uuid'
+ tbl.print ", id: :uuid"
+ tbl.print %Q(, default: "#{pkcol.default_function}") if pkcol.default_function
end
else
tbl.print ", id: false"
@@ -105,7 +133,7 @@ HEADER
# then dump all non-primary key columns
column_specs = columns.map do |column|
- raise StandardError, "Unknown type '#{column.sql_type}' for column '#{column.name}'" if @types[column.type].nil?
+ raise StandardError, "Unknown type '#{column.sql_type}' for column '#{column.name}'" unless @connection.valid_type?(column.type)
next if column.name == pk
@connection.column_spec(column, @types)
end.compact
@@ -172,6 +200,10 @@ HEADER
statement_parts << ('where: ' + index.where.inspect) if index.where
+ statement_parts << ('using: ' + index.using.inspect) if index.using
+
+ statement_parts << ('type: ' + index.type.inspect) if index.type
+
' ' + statement_parts.join(', ')
end
@@ -181,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 9830abe7d8..a9d164e366 100644
--- a/activerecord/lib/active_record/schema_migration.rb
+++ b/activerecord/lib/active_record/schema_migration.rb
@@ -4,28 +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
- unless connection.table_exists?(table_name)
- connection.create_table(table_name, :id => false) do |t|
- t.column :version, :string, :null => false
+ 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
+ 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.rb b/activerecord/lib/active_record/scoping.rb
index 0c3fd1bd29..0cf3d59985 100644
--- a/activerecord/lib/active_record/scoping.rb
+++ b/activerecord/lib/active_record/scoping.rb
@@ -1,3 +1,4 @@
+require 'active_support/per_thread_registry'
module ActiveRecord
module Scoping
@@ -10,11 +11,11 @@ module ActiveRecord
module ClassMethods
def current_scope #:nodoc:
- Thread.current["#{self}_current_scope"]
+ ScopeRegistry.value_for(:current_scope, base_class.to_s)
end
def current_scope=(scope) #:nodoc:
- Thread.current["#{self}_current_scope"] = scope
+ ScopeRegistry.set_value_for(:current_scope, base_class.to_s, scope)
end
end
@@ -26,5 +27,56 @@ module ActiveRecord
end
end
+ # This class stores the +:current_scope+ and +:ignore_default_scope+ values
+ # for different classes. The registry is stored as a thread local, which is
+ # accessed through +ScopeRegistry.current+.
+ #
+ # This class allows you to store and get the scope values on different
+ # classes and different types of scopes. For example, if you are attempting
+ # to get the current_scope for the +Board+ model, then you would use the
+ # following code:
+ #
+ # registry = ActiveRecord::Scoping::ScopeRegistry
+ # registry.set_value_for(:current_scope, "Board", some_new_scope)
+ #
+ # Now when you run:
+ #
+ # registry.value_for(:current_scope, "Board")
+ #
+ # You will obtain whatever was defined in +some_new_scope+. The +value_for+
+ # and +set_value_for+ methods are delegated to the current +ScopeRegistry+
+ # object, so the above example code can also be called as:
+ #
+ # ActiveRecord::Scoping::ScopeRegistry.set_value_for(:current_scope,
+ # "Board", some_new_scope)
+ class ScopeRegistry # :nodoc:
+ extend ActiveSupport::PerThreadRegistry
+
+ VALID_SCOPE_TYPES = [:current_scope, :ignore_default_scope]
+
+ def initialize
+ @registry = Hash.new { |hash, key| hash[key] = {} }
+ end
+
+ # Obtains the value for a given +scope_name+ and +variable_name+.
+ def value_for(scope_type, variable_name)
+ raise_invalid_scope_type!(scope_type)
+ @registry[scope_type][variable_name]
+ end
+
+ # Sets the +value+ for a given +scope_type+ and +variable_name+.
+ def set_value_for(scope_type, variable_name, value)
+ raise_invalid_scope_type!(scope_type)
+ @registry[scope_type][variable_name] = value
+ end
+
+ private
+
+ def raise_invalid_scope_type!(scope_type)
+ if !VALID_SCOPE_TYPES.include?(scope_type)
+ raise ArgumentError, "Invalid scope type '#{scope_type}' sent to the registry. Scope types must be included in VALID_SCOPE_TYPES"
+ end
+ end
+ end
end
end
diff --git a/activerecord/lib/active_record/scoping/default.rb b/activerecord/lib/active_record/scoping/default.rb
index 6835d0e01b..01fec31544 100644
--- a/activerecord/lib/active_record/scoping/default.rb
+++ b/activerecord/lib/active_record/scoping/default.rb
@@ -1,4 +1,3 @@
-
module ActiveRecord
module Scoping
module Default
@@ -6,7 +5,8 @@ module ActiveRecord
included do
# Stores the default scope for the class.
- class_attribute :default_scopes, instance_writer: false
+ class_attribute :default_scopes, instance_writer: false, instance_predicate: false
+
self.default_scopes = []
end
@@ -28,14 +28,6 @@ module ActiveRecord
# Post.unscoped {
# Post.limit(10) # Fires "SELECT * FROM posts LIMIT 10"
# }
- #
- # It is recommended that you use the block form of unscoped because
- # chaining unscoped with +scope+ does not work. Assuming that
- # +published+ is a +scope+, the following two statements
- # are equal: the +default_scope+ is applied on both.
- #
- # Post.unscoped.published
- # Post.published
def unscoped
block_given? ? relation.scoping { yield } : relation
end
@@ -91,15 +83,14 @@ 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 = default_scopes + [scope]
+ self.default_scopes += [scope]
end
def build_default_scope # :nodoc:
@@ -109,22 +100,18 @@ 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
end
def ignore_default_scope? # :nodoc:
- Thread.current["#{self}_ignore_default_scope"]
+ ScopeRegistry.value_for(:ignore_default_scope, self)
end
def ignore_default_scope=(ignore) # :nodoc:
- Thread.current["#{self}_ignore_default_scope"] = ignore
+ ScopeRegistry.set_value_for(:ignore_default_scope, self, ignore)
end
# The ignore_default_scope flag is used to prevent an infinite recursion
@@ -140,7 +127,6 @@ module ActiveRecord
self.ignore_default_scope = false
end
end
-
end
end
end
diff --git a/activerecord/lib/active_record/scoping/named.rb b/activerecord/lib/active_record/scoping/named.rb
index fb5f5b5be0..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?
@@ -116,7 +112,7 @@ module ActiveRecord
# Scopes can also be used while creating/building a record.
#
# class Article < ActiveRecord::Base
- # scope :published, -> { where(published: true) }
+ # scope :published, -> { where(published: true) }
# end
#
# Article.published.new.published # => true
@@ -126,7 +122,7 @@ module ActiveRecord
# on scopes. Assuming the following setup:
#
# class Article < ActiveRecord::Base
- # scope :published, -> { where(published: true) }
+ # scope :published, -> { where(published: true) }
# scope :featured, -> { where(featured: true) }
#
# def self.latest_article
@@ -134,37 +130,22 @@ module ActiveRecord
# end
#
# def self.titles
- # map(&:title)
+ # pluck(:title)
# end
- #
# end
#
# We are able to call the methods like this:
#
# Article.published.featured.latest_article
# Article.featured.titles
-
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|
- options = body.respond_to?(:call) ? unscoped { body.call(*args) } : body
- relation = all.merge(options)
+ scope = all.scoping { body.call(*args) }
+ scope = scope.extending(extension) if extension
- extension ? relation.extending(extension) : relation
+ scope || all
end
end
end
diff --git a/activerecord/lib/active_record/serialization.rb b/activerecord/lib/active_record/serialization.rb
index e8dd312a47..bd9079b596 100644
--- a/activerecord/lib/active_record/serialization.rb
+++ b/activerecord/lib/active_record/serialization.rb
@@ -1,19 +1,11 @@
module ActiveRecord #:nodoc:
- ActiveSupport.on_load(:active_record_config) do
- mattr_accessor :include_root_in_json, instance_accessor: false
- self.include_root_in_json = true
- end
-
# = Active Record Serialization
module Serialization
extend ActiveSupport::Concern
include ActiveModel::Serializers::JSON
included do
- singleton_class.class_eval do
- remove_method :include_root_in_json
- delegate :include_root_in_json, to: 'ActiveRecord::Model'
- end
+ self.include_root_in_json = false
end
def serializable_hash(options = nil)
diff --git a/activerecord/lib/active_record/serializers/xml_serializer.rb b/activerecord/lib/active_record/serializers/xml_serializer.rb
index 834d01a1e8..1a766093d0 100644
--- a/activerecord/lib/active_record/serializers/xml_serializer.rb
+++ b/activerecord/lib/active_record/serializers/xml_serializer.rb
@@ -36,7 +36,7 @@ module ActiveRecord #:nodoc:
#
# For instance:
#
- # topic.to_xml(:skip_instruct => true, :except => [ :id, :bonus_time, :written_on, :replies_count ])
+ # topic.to_xml(skip_instruct: true, except: [ :id, :bonus_time, :written_on, :replies_count ])
#
# <topic>
# <title>The First Topic</title>
@@ -50,7 +50,7 @@ module ActiveRecord #:nodoc:
#
# To include first level associations use <tt>:include</tt>:
#
- # firm.to_xml :include => [ :account, :clients ]
+ # firm.to_xml include: [ :account, :clients ]
#
# <?xml version="1.0" encoding="UTF-8"?>
# <firm>
@@ -81,7 +81,7 @@ module ActiveRecord #:nodoc:
# associated with models.
#
# proc = Proc.new { |options, record| options[:builder].tag!('name-reverse', record.name.reverse) }
- # firm.to_xml :procs => [ proc ]
+ # firm.to_xml procs: [ proc ]
#
# <firm>
# # ... normal attributes as shown above ...
@@ -90,7 +90,7 @@ module ActiveRecord #:nodoc:
#
# To include deeper levels of associations pass a hash like this:
#
- # firm.to_xml :include => {:account => {}, :clients => {:include => :address}}
+ # firm.to_xml include: {account: {}, clients: {include: :address}}
# <?xml version="1.0" encoding="UTF-8"?>
# <firm>
# <id type="integer">1</id>
@@ -120,7 +120,7 @@ module ActiveRecord #:nodoc:
#
# To include any methods on the model being called use <tt>:methods</tt>:
#
- # firm.to_xml :methods => [ :calculated_earnings, :real_earnings ]
+ # firm.to_xml methods: [ :calculated_earnings, :real_earnings ]
#
# <firm>
# # ... normal attributes as shown above ...
@@ -132,7 +132,7 @@ module ActiveRecord #:nodoc:
# modified version of the options hash that was given to +to_xml+:
#
# proc = Proc.new { |options| options[:builder].tag!('abc', 'def') }
- # firm.to_xml :procs => [ proc ]
+ # firm.to_xml procs: [ proc ]
#
# <firm>
# # ... normal attributes as shown above ...
@@ -164,7 +164,7 @@ module ActiveRecord #:nodoc:
# def to_xml(options = {})
# require 'builder'
# options[:indent] ||= 2
- # xml = options[:builder] ||= ::Builder::XmlMarkup.new(:indent => options[:indent])
+ # xml = options[:builder] ||= ::Builder::XmlMarkup.new(indent: options[:indent])
# xml.instruct! unless options[:skip_instruct]
# xml.level_one do
# xml.tag!(:second_level, 'content')
diff --git a/activerecord/lib/active_record/statement_cache.rb b/activerecord/lib/active_record/statement_cache.rb
new file mode 100644
index 0000000000..dd4ee0c4a0
--- /dev/null
+++ b/activerecord/lib/active_record/statement_cache.rb
@@ -0,0 +1,26 @@
+module ActiveRecord
+
+ # Statement cache is used to cache a single statement in order to avoid creating the AST again.
+ # Initializing the cache is done by passing the statement in the initialization block:
+ #
+ # cache = ActiveRecord::StatementCache.new do
+ # Book.where(name: "my book").limit(100)
+ # end
+ #
+ # The cached statement is executed by using the +execute+ method:
+ #
+ # cache.execute
+ #
+ # The relation returned by the block is cached, and for each +execute+ call the cached relation gets duped.
+ # Database is queried when +to_a+ is called on the relation.
+ class StatementCache
+ def initialize
+ @relation = yield
+ raise ArgumentError.new("Statement cannot be nil") if @relation.nil?
+ end
+
+ def execute
+ @relation.dup.to_a
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/store.rb b/activerecord/lib/active_record/store.rb
index df7f58c81f..b841b977fc 100644
--- a/activerecord/lib/active_record/store.rb
+++ b/activerecord/lib/active_record/store.rb
@@ -42,21 +42,19 @@ module ActiveRecord
#
# All stored values are automatically available through accessors on the Active Record
# object, but sometimes you want to specialize this behavior. This can be done by overwriting
- # the default accessors (using the same name as the attribute) and calling
- # <tt>read_store_attribute(store_attribute_name, attr_name)</tt> and
- # <tt>write_store_attribute(store_attribute_name, attr_name, value)</tt> to actually
- # change things.
+ # the default accessors (using the same name as the attribute) and calling <tt>super</tt>
+ # to actually change things.
#
# class Song < ActiveRecord::Base
# # Uses a stored integer to hold the volume adjustment of the song
# store :settings, accessors: [:volume_adjustment]
#
# def volume_adjustment=(decibels)
- # write_store_attribute(:settings, :volume_adjustment, decibels.to_i)
+ # super(decibels.to_i)
# end
#
# def volume_adjustment
- # read_store_attribute(:settings, :volume_adjustment).to_i
+ # super.to_i
# end
# end
module Store
@@ -75,43 +73,89 @@ module ActiveRecord
def store_accessor(store_attribute, *keys)
keys = keys.flatten
- keys.each do |key|
- define_method("#{key}=") do |value|
- write_store_attribute(store_attribute, key, value)
- end
- define_method(key) do
- read_store_attribute(store_attribute, key)
+ _store_accessors_module.module_eval do
+ keys.each do |key|
+ define_method("#{key}=") do |value|
+ write_store_attribute(store_attribute, key, value)
+ end
+
+ define_method(key) do
+ read_store_attribute(store_attribute, key)
+ end
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
+
+ def _store_accessors_module
+ @_store_accessors_module ||= begin
+ mod = Module.new
+ include mod
+ mod
+ end
+ end
end
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?(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:
@@ -129,17 +173,17 @@ 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)
case obj
- when HashWithIndifferentAccess
+ when ActiveSupport::HashWithIndifferentAccess
obj
when Hash
obj.with_indifferent_access
else
- HashWithIndifferentAccess.new
+ ActiveSupport::HashWithIndifferentAccess.new
end
end
end
diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb
index fda51b3d76..be7d496d15 100644
--- a/activerecord/lib/active_record/tasks/database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/database_tasks.rb
@@ -1,9 +1,44 @@
module ActiveRecord
module Tasks # :nodoc:
- module DatabaseTasks # :nodoc:
+ class DatabaseAlreadyExists < StandardError; end # :nodoc:
+ class DatabaseNotSupported < StandardError; end # :nodoc:
+
+ # <tt>ActiveRecord::Tasks::DatabaseTasks</tt> is a utility class, which encapsulates
+ # logic behind common tasks used to manage database and migrations.
+ #
+ # The tasks defined here are used in rake tasks provided by Active Record.
+ #
+ # In order to use DatabaseTasks, a few config values need to be set. All the needed
+ # config values are set by Rails already, so it's necessary to do it only if you
+ # want to change the defaults or when you want to use Active Record outside of Rails
+ # (in such case after configuring the database tasks, you can also use the rake tasks
+ # defined in Active Record).
+ #
+ #
+ # The possible config values are:
+ #
+ # * +env+: current environment (like Rails.env).
+ # * +database_configuration+: configuration of your databases (as in +config/database.yml+).
+ # * +db_dir+: your +db+ directory.
+ # * +fixtures_path+: a path to fixtures directory.
+ # * +migrations_paths+: a list of paths to directories with migrations.
+ # * +seed_loader+: an object which will load seeds, it needs to respond to the +load_seed+ method.
+ # * +root+: a path to the root of the application.
+ #
+ # Example usage of +DatabaseTasks+ outside Rails could look as such:
+ #
+ # include ActiveRecord::Tasks
+ # DatabaseTasks.database_configuration = YAML.load(File.read('my_database_config.yml'))
+ # DatabaseTasks.db_dir = 'db'
+ # # other settings...
+ #
+ # DatabaseTasks.create_current('production')
+ module DatabaseTasks
extend self
attr_writer :current_config
+ attr_accessor :database_configuration, :migrations_paths, :seed_loader, :db_dir,
+ :fixtures_path, :env, :root
LOCAL_HOSTS = ['127.0.0.1', 'localhost']
@@ -12,12 +47,12 @@ module ActiveRecord
@tasks[pattern] = task
end
- register_task(/mysql/, ActiveRecord::Tasks::MySQLDatabaseTasks)
- register_task(/postgresql/, ActiveRecord::Tasks::PostgreSQLDatabaseTasks)
- register_task(/sqlite/, ActiveRecord::Tasks::SQLiteDatabaseTasks)
+ register_task(/mysql/, ActiveRecord::Tasks::MySQLDatabaseTasks)
+ register_task(/postgresql/, ActiveRecord::Tasks::PostgreSQLDatabaseTasks)
+ register_task(/sqlite/, ActiveRecord::Tasks::SQLiteDatabaseTasks)
def current_config(options = {})
- options.reverse_merge! :env => Rails.env
+ options.reverse_merge! :env => env
if options.has_key?(:config)
@current_config = options[:config]
else
@@ -32,6 +67,8 @@ module ActiveRecord
def create(*arguments)
configuration = arguments.first
class_for_adapter(configuration['adapter']).new(*arguments).create
+ rescue DatabaseAlreadyExists
+ $stderr.puts "#{configuration['database']} already exists"
rescue Exception => error
$stderr.puts error, *(error.backtrace)
$stderr.puts "Couldn't create database for #{configuration.inspect}"
@@ -41,7 +78,7 @@ module ActiveRecord
each_local_configuration { |configuration| create configuration }
end
- def create_current(environment = Rails.env)
+ def create_current(environment = env)
each_current_configuration(environment) { |configuration|
create configuration
}
@@ -64,7 +101,7 @@ module ActiveRecord
each_local_configuration { |configuration| drop configuration }
end
- def drop_current(environment = Rails.env)
+ def drop_current(environment = env)
each_current_configuration(environment) { |configuration|
drop configuration
}
@@ -74,7 +111,7 @@ module ActiveRecord
drop database_url_config
end
- def charset_current(environment = Rails.env)
+ def charset_current(environment = env)
charset ActiveRecord::Base.configurations[environment]
end
@@ -83,7 +120,7 @@ module ActiveRecord
class_for_adapter(configuration['adapter']).new(*arguments).charset
end
- def collation_current(environment = Rails.env)
+ def collation_current(environment = env)
collation ActiveRecord::Base.configurations[environment]
end
@@ -108,6 +145,24 @@ module ActiveRecord
class_for_adapter(configuration['adapter']).new(*arguments).structure_load(filename)
end
+ def check_schema_file(filename)
+ unless File.exist?(filename)
+ message = %{#{filename} doesn't exist yet. Run `rake db:migrate` to create it, then try again.}
+ 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
+ end
+ end
+
+ def load_seed
+ if seed_loader
+ seed_loader.load_seed
+ else
+ raise "You tried to load seed data, but no seed loader is specified. Please specify seed " +
+ "loader with ActiveRecord::Tasks::DatabaseTasks.seed_loader = your_seed_loader\n" +
+ "Seed loader should respond to load_seed method"
+ end
+ end
+
private
def database_url_config
@@ -117,12 +172,15 @@ module ActiveRecord
def class_for_adapter(adapter)
key = @tasks.keys.detect { |pattern| adapter[pattern] }
+ unless key
+ raise DatabaseNotSupported, "Rake tasks not supported by '#{adapter}' adapter"
+ end
@tasks[key]
end
def each_current_configuration(environment)
environments = [environment]
- environments << 'test' if environment.development?
+ environments << 'test' if environment == 'development'
configurations = ActiveRecord::Base.configurations.values_at(*environments)
configurations.compact.each do |configuration|
diff --git a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb
index 3d27c97254..c755831e6d 100644
--- a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb
@@ -1,7 +1,6 @@
module ActiveRecord
module Tasks # :nodoc:
class MySQLDatabaseTasks # :nodoc:
-
DEFAULT_CHARSET = ENV['CHARSET'] || 'utf8'
DEFAULT_COLLATION = ENV['COLLATION'] || 'utf8_unicode_ci'
ACCESS_DENIED_ERROR = 1045
@@ -16,18 +15,25 @@ module ActiveRecord
establish_connection configuration_without_database
connection.create_database configuration['database'], creation_options
establish_connection configuration
+ rescue ActiveRecord::StatementInvalid => error
+ if /database exists/ === error.message
+ raise DatabaseAlreadyExists
+ else
+ raise
+ end
rescue error_class => error
- raise error unless error.errno == ACCESS_DENIED_ERROR
-
- $stdout.print error.error
- establish_connection root_configuration_without_database
- connection.create_database configuration['database'], creation_options
- connection.execute grant_statement.gsub(/\s+/, ' ').strip
- establish_connection configuration
- rescue error_class => error
- $stderr.puts error.error
- $stderr.puts "Couldn't create database for #{configuration.inspect}, #{creation_options.inspect}"
- $stderr.puts "(If you set the charset manually, make sure you have a matching collation)" if configuration['encoding']
+ if error.respond_to?(:errno) && error.errno == ACCESS_DENIED_ERROR
+ $stdout.print error.error
+ establish_connection root_configuration_without_database
+ connection.create_database configuration['database'], creation_options
+ if configuration['username'] != 'root'
+ connection.execute grant_statement.gsub(/\s+/, ' ').strip
+ end
+ establish_connection configuration
+ else
+ $stderr.puts "Couldn't create database for #{configuration.inspect}, #{creation_options.inspect}"
+ $stderr.puts "(If you set the charset manually, make sure you have a matching collation)" if configuration['encoding']
+ end
end
def drop
@@ -53,7 +59,10 @@ module ActiveRecord
args.concat(["--result-file", "#{filename}"])
args.concat(["--no-data"])
args.concat(["#{configuration['database']}"])
- Kernel.system(*args)
+ unless Kernel.system(*args)
+ $stderr.puts "Could not dump the database structure. "\
+ "Make sure `mysqldump` is in your PATH and check the command output for warnings."
+ end
end
def structure_load(filename)
@@ -87,14 +96,15 @@ module ActiveRecord
end
def error_class
- case configuration['adapter']
- when /jdbc/
+ if configuration['adapter'] =~ /jdbc/
require 'active_record/railties/jdbcmysql_error'
ArJdbcMySQL::Error
- when /mysql2/
+ elsif defined?(Mysql2)
Mysql2::Error
- else
+ elsif defined?(Mysql)
Mysql::Error
+ else
+ StandardError
end
end
@@ -124,11 +134,11 @@ 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
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 ea5cb888fb..3d02ee07d0 100644
--- a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb
@@ -3,7 +3,6 @@ require 'shellwords'
module ActiveRecord
module Tasks # :nodoc:
class PostgreSQLDatabaseTasks # :nodoc:
-
DEFAULT_ENCODING = ENV['CHARSET'] || 'utf8'
delegate :connection, :establish_connection, :clear_active_connections!,
@@ -18,6 +17,12 @@ module ActiveRecord
connection.create_database configuration['database'],
configuration.merge('encoding' => encoding)
establish_connection configuration
+ rescue ActiveRecord::StatementInvalid => error
+ if /database .* already exists/ === error.message
+ raise DatabaseAlreadyExists
+ else
+ raise
+ end
end
def drop
@@ -54,7 +59,7 @@ module ActiveRecord
def structure_load(filename)
set_psql_env
- Kernel.system("psql -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 da01058a82..5688931db2 100644
--- a/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb
@@ -1,18 +1,14 @@
module ActiveRecord
module Tasks # :nodoc:
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
def create
- if File.exist?(configuration['database'])
- $stderr.puts "#{configuration['database']} already exists"
- return
- end
+ raise DatabaseAlreadyExists if File.exist?(configuration['database'])
establish_connection configuration
connection
diff --git a/activerecord/lib/active_record/test_case.rb b/activerecord/lib/active_record/test_case.rb
deleted file mode 100644
index c035ad43a2..0000000000
--- a/activerecord/lib/active_record/test_case.rb
+++ /dev/null
@@ -1,95 +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
- yield
- ensure
- 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
- 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 (?!(table_info))/, /^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]
-
- [oracle_ignored, mysql_ignored, postgresql_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 bf95ccb298..9253150c4f 100644
--- a/activerecord/lib/active_record/timestamp.rb
+++ b/activerecord/lib/active_record/timestamp.rb
@@ -1,10 +1,5 @@
module ActiveRecord
- ActiveSupport.on_load(:active_record_config) do
- mattr_accessor :record_timestamps, instance_accessor: false
- self.record_timestamps = true
- end
-
# = Active Record Timestamp
#
# Active Record automatically timestamps create and update operations if the
@@ -15,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
#
@@ -37,16 +32,18 @@ module ActiveRecord
extend ActiveSupport::Concern
included do
- config_attribute :record_timestamps, instance_writer: true
+ class_attribute :record_timestamps
+ self.record_timestamps = true
end
def initialize_dup(other) # :nodoc:
clear_timestamp_attributes
+ super
end
private
- def create
+ def create_record
if self.record_timestamps
current_time = current_time_from_proper_timezone
@@ -60,7 +57,7 @@ module ActiveRecord
super
end
- def update(*args)
+ def update_record(*args)
if should_record_timestamps?
current_time = current_time_from_proper_timezone
@@ -74,7 +71,7 @@ module ActiveRecord
end
def should_record_timestamps?
- self.record_timestamps && (!partial_updates? || changed? || (attributes.keys & self.class.serialized_attributes.keys).present?)
+ self.record_timestamps && (!partial_writes? || changed? || (attributes.keys & self.class.serialized_attributes.keys).present?)
end
def timestamp_attributes_for_create_in_model
@@ -101,6 +98,12 @@ 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?
+ timestamps.map { |ts| ts.to_time }.max
+ end
+ end
+
def current_time_from_proper_timezone
self.class.default_timezone == :utc ? Time.now.utc : Time.now
end
diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb
index 934393b4e7..45313b5e75 100644
--- a/activerecord/lib/active_record/transactions.rb
+++ b/activerecord/lib/active_record/transactions.rb
@@ -4,12 +4,15 @@ module ActiveRecord
# See ActiveRecord::Transactions::ClassMethods for documentation.
module Transactions
extend ActiveSupport::Concern
+ ACTIONS = [:create, :destroy, :update]
class TransactionError < ActiveRecordError # :nodoc:
end
included do
- define_callbacks :commit, :rollback, :terminator => "result == false", :scope => [:kind, :name]
+ define_callbacks :commit, :rollback,
+ terminator: ->(_, result) { result == false },
+ scope: [:kind, :name]
end
# = Active Record Transactions
@@ -108,10 +111,10 @@ module ActiveRecord
#
# # Suppose that we have a Number model with a unique column called 'i'.
# Number.transaction do
- # Number.create(:i => 0)
+ # Number.create(i: 0)
# begin
# # This will raise a unique constraint error...
- # Number.create(:i => 0)
+ # Number.create(i: 0)
# rescue ActiveRecord::StatementInvalid
# # ...which we ignore.
# end
@@ -119,7 +122,7 @@ module ActiveRecord
# # On PostgreSQL, the transaction is now unusable. The following
# # statement will cause a PostgreSQL error, even though the unique
# # constraint is no longer violated:
- # Number.create(:i => 1)
+ # Number.create(i: 1)
# # => "PGError: ERROR: current transaction is aborted, commands
# # ignored until end of transaction block"
# end
@@ -134,9 +137,9 @@ module ActiveRecord
# transaction. For example, the following behavior may be surprising:
#
# User.transaction do
- # User.create(:username => 'Kotori')
+ # User.create(username: 'Kotori')
# User.transaction do
- # User.create(:username => 'Nemu')
+ # User.create(username: 'Nemu')
# raise ActiveRecord::Rollback
# end
# end
@@ -147,19 +150,19 @@ module ActiveRecord
# real transaction is committed.
#
# In order to get a ROLLBACK for the nested transaction you may ask for a real
- # sub-transaction by passing <tt>:requires_new => true</tt>. If anything goes wrong,
+ # sub-transaction by passing <tt>requires_new: true</tt>. If anything goes wrong,
# the database rolls back to the beginning of the sub-transaction without rolling
# back the parent transaction. If we add it to the previous example:
#
# User.transaction do
- # User.create(:username => 'Kotori')
- # User.transaction(:requires_new => true) do
- # User.create(:username => 'Nemu')
+ # User.create(username: 'Kotori')
+ # User.transaction(requires_new: true) do
+ # User.create(username: 'Nemu')
# raise ActiveRecord::Rollback
# end
# end
#
- # only "Kotori" is created. (This works on MySQL and PostgreSQL, but not on SQLite3.)
+ # only "Kotori" is created. This works on MySQL and PostgreSQL. SQLite3 version >= '3.6.8' also supports it.
#
# Most databases don't support true nested transactions. At the time of
# writing, the only database that we're aware of that supports true nested
@@ -194,7 +197,7 @@ module ActiveRecord
# automatically released. The following example demonstrates the problem:
#
# Model.connection.transaction do # BEGIN
- # Model.connection.transaction(:requires_new => true) do # CREATE SAVEPOINT active_record_1
+ # Model.connection.transaction(requires_new: true) do # CREATE SAVEPOINT active_record_1
# Model.connection.create_table(...) # active_record_1 now automatically released
# end # RELEASE savepoint active_record_1
# # ^^^^ BOOM! database error!
@@ -213,22 +216,17 @@ module ActiveRecord
# You can specify that the callback should only be fired by a certain action with
# the +:on+ option:
#
- # after_commit :do_foo, :on => :create
- # after_commit :do_bar, :on => :update
- # after_commit :do_baz, :on => :destroy
+ # after_commit :do_foo, on: :create
+ # after_commit :do_bar, on: :update
+ # after_commit :do_baz, on: :destroy
#
- # Also, to have the callback fired on create and update, but not on destroy:
- #
- # after_commit :do_zoo, :if => :persisted?
+ # after_commit :do_foo_bar, on: [:create, :update]
+ # after_commit :do_bar_baz, on: [:update, :destroy]
#
# Note that transactional fixtures do not play well with this feature. Please
# use the +test_after_commit+ gem to have these hooks fired in tests.
def after_commit(*args, &block)
- options = args.last
- if options.is_a?(Hash) && options[:on]
- options[:if] = Array(options[:if])
- options[:if] << "transaction_include_action?(:#{options[:on]})"
- end
+ set_options_for_callbacks!(args)
set_callback(:commit, :after, *args, &block)
end
@@ -236,12 +234,27 @@ module ActiveRecord
#
# Please check the documentation of +after_commit+ for options.
def after_rollback(*args, &block)
+ set_options_for_callbacks!(args)
+ set_callback(:rollback, :after, *args, &block)
+ end
+
+ private
+
+ def set_options_for_callbacks!(args)
options = args.last
if options.is_a?(Hash) && options[:on]
+ assert_valid_transaction_action(options[:on])
options[:if] = Array(options[:if])
- options[:if] << "transaction_include_action?(:#{options[:on]})"
+ fire_on = Array(options[:on])
+ options[:if] << "transaction_include_any_action?(#{fire_on})"
+ end
+ end
+
+ def assert_valid_transaction_action(actions)
+ actions = Array(actions)
+ if (actions - ACTIONS).any?
+ raise ArgumentError, ":on conditions for after_commit and after_rollback callbacks have to be one of #{ACTIONS.join(",")}"
end
- set_callback(:rollback, :after, *args, &block)
end
end
@@ -275,22 +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.
def committed! #:nodoc:
- run_callbacks :commit
+ 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)
@@ -325,9 +342,14 @@ module ActiveRecord
# Save the new record state and id of a record so it can be restored later if a transaction fails.
def remember_transaction_record_state #:nodoc:
@_start_transaction_state[:id] = id if has_attribute?(self.class.primary_key)
- @_start_transaction_state[:new_record] = @new_record
- @_start_transaction_state[:destroyed] = @destroyed
+ unless @_start_transaction_state.include?(:new_record)
+ @_start_transaction_state[:new_record] = @new_record
+ end
+ unless @_start_transaction_state.include?(:destroyed)
+ @_start_transaction_state[:destroyed] = @destroyed
+ end
@_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) + 1
+ @_start_transaction_state[:frozen?] = @attributes.frozen?
end
# Clear the new record state and id of a record.
@@ -339,11 +361,11 @@ 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 = @attributes.frozen?
- @attributes = @attributes.dup if was_frozen
+ was_frozen = restore_state[:frozen?]
+ @attributes = @attributes.dup if @attributes.frozen?
@new_record = restore_state[:new_record]
@destroyed = restore_state[:destroyed]
if restore_state.has_key?(:id)
@@ -353,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
@@ -364,14 +385,16 @@ module ActiveRecord
end
# Determine if a transaction included an action for :create, :update, or :destroy. Used in filtering callbacks.
- def transaction_include_action?(action) #:nodoc:
- case action
- when :create
- transaction_record_state(:new_record)
- when :destroy
- destroyed?
- when :update
- !(transaction_record_state(:new_record) || destroyed?)
+ def transaction_include_any_action?(actions) #:nodoc:
+ actions.any? do |action|
+ case action
+ when :create
+ transaction_record_state(:new_record)
+ when :destroy
+ destroyed?
+ when :update
+ !(transaction_record_state(:new_record) || destroyed?)
+ end
end
end
end
diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb
index 3706885881..26dca415ff 100644
--- a/activerecord/lib/active_record/validations.rb
+++ b/activerecord/lib/active_record/validations.rb
@@ -74,8 +74,7 @@ module ActiveRecord
protected
def perform_validations(options={}) # :nodoc:
- perform_validation = options[:validate] != false
- perform_validation ? valid?(options[:context]) : true
+ options[:validate] == false || valid?(options[:context])
end
end
end
diff --git a/activerecord/lib/active_record/validations/associated.rb b/activerecord/lib/active_record/validations/associated.rb
index 7f1972ccf9..b4785d3ba4 100644
--- a/activerecord/lib/active_record/validations/associated.rb
+++ b/activerecord/lib/active_record/validations/associated.rb
@@ -2,15 +2,15 @@ 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
end
module ClassMethods
- # Validates whether the associated object or objects are all valid
- # themselves. Works with any kind of association.
+ # Validates whether the associated object or objects are all valid.
+ # Works with any kind of association.
#
# class Book < ActiveRecord::Base
# has_many :pages
diff --git a/activerecord/lib/active_record/validations/presence.rb b/activerecord/lib/active_record/validations/presence.rb
index 81a3521d24..6b14c39686 100644
--- a/activerecord/lib/active_record/validations/presence.rb
+++ b/activerecord/lib/active_record/validations/presence.rb
@@ -5,8 +5,10 @@ module ActiveRecord
super
attributes.each do |attribute|
next unless record.class.reflect_on_association(attribute)
- value = record.send(attribute)
- if Array(value).all? { |r| r.marked_for_destruction? }
+ associated_records = Array(record.send(attribute))
+
+ # Superclass validates presence. Ensure present records aren't about to be destroyed.
+ if associated_records.present? && associated_records.all? { |r| r.marked_for_destruction? }
record.errors.add(attribute, :blank, options)
end
end
diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb
index 5fa6a0b892..b55af692d6 100644
--- a/activerecord/lib/active_record/validations/uniqueness.rb
+++ b/activerecord/lib/active_record/validations/uniqueness.rb
@@ -1,54 +1,35 @@
-require 'active_support/core_ext/array/prepend_and_append'
-
module ActiveRecord
module Validations
class UniquenessValidator < ActiveModel::EachValidator # :nodoc:
def initialize(options)
- super(options.reverse_merge(:case_sensitive => true))
- end
-
- # Unfortunately, we have to tie Uniqueness validators to a class.
- def setup(klass)
- @klass = klass
+ if options[:conditions] && !options[:conditions].respond_to?(:call)
+ raise ArgumentError, "#{options[:conditions]} was passed as :conditions but is not callable. " \
+ "Pass a callable instead: `conditions: -> { where(approved: true) }`"
+ end
+ super({ case_sensitive: true }.merge!(options))
+ @klass = options[:class]
end
def validate_each(record, attribute, value)
finder_class = find_finder_class_for(record)
table = finder_class.arel_table
-
- coder = record.class.serialized_attributes[attribute.to_s]
-
- if value && coder
- value = coder.dump value
- end
+ value = deserialize_attribute(record, attribute, value)
relation = build_relation(finder_class, table, attribute, value)
- relation = relation.and(table[finder_class.primary_key.to_sym].not_eq(record.send(:id))) if record.persisted?
-
- Array(options[:scope]).each do |scope_item|
- reflection = record.class.reflect_on_association(scope_item)
- if reflection
- scope_value = record.send(reflection.foreign_key)
- scope_item = reflection.foreign_key
- else
- scope_value = record.read_attribute(scope_item)
- end
- relation = relation.and(table[scope_item].eq(scope_value))
- end
-
+ relation = relation.and(table[finder_class.primary_key.to_sym].not_eq(record.id)) if record.persisted?
+ relation = scope_relation(record, table, relation)
relation = finder_class.unscoped.where(relation)
-
- if options[:conditions]
- relation = relation.merge(options[:conditions])
- end
+ relation = relation.merge(options[:conditions]) if options[:conditions]
if relation.exists?
- record.errors.add(attribute, :taken, options.except(:case_sensitive, :scope, :conditions).merge(:value => value))
+ error_options = options.except(:case_sensitive, :scope, :conditions)
+ error_options[:value] = value
+
+ record.errors.add(attribute, :taken, error_options)
end
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
@@ -58,7 +39,7 @@ module ActiveRecord
class_hierarchy = [record.class]
while class_hierarchy.first != @klass
- class_hierarchy.prepend(class_hierarchy.first.superclass)
+ class_hierarchy.unshift(class_hierarchy.first.superclass)
end
class_hierarchy.detect { |klass| !klass.abstract_class? }
@@ -71,18 +52,37 @@ module ActiveRecord
end
column = klass.columns_hash[attribute.to_s]
- value = column.limit ? value.to_s[0, column.limit] : value.to_s if !value.nil? && column.text?
+ value = klass.connection.type_cast(value, column)
+ value = value.to_s[0, column.limit] if value && column.limit && column.text?
if !options[:case_sensitive] && value && column.text?
# will use SQL LOWER function before comparison, unless it detects a case insensitive collation
- relation = klass.connection.case_insensitive_comparison(table, attribute, column, value)
+ klass.connection.case_insensitive_comparison(table, attribute, column, value)
else
- value = klass.connection.case_sensitive_modifier(value) unless value.nil?
- relation = table[attribute].eq(value)
+ value = klass.connection.case_sensitive_modifier(value) unless value.nil?
+ table[attribute].eq(value)
+ end
+ end
+
+ def scope_relation(record, table, relation)
+ Array(options[:scope]).each do |scope_item|
+ if reflection = record.class.reflect_on_association(scope_item)
+ scope_value = record.send(reflection.foreign_key)
+ scope_item = reflection.foreign_key
+ else
+ scope_value = record.read_attribute(scope_item)
+ end
+ relation = relation.and(table[scope_item].eq(scope_value))
end
relation
end
+
+ def deserialize_attribute(record, attribute, value)
+ coder = record.class.serialized_attributes[attribute.to_s]
+ value = coder.dump value if value && coder
+ value
+ end
end
module ClassMethods
@@ -115,7 +115,7 @@ module ActiveRecord
# of the title attribute:
#
# class Article < ActiveRecord::Base
- # validates_uniqueness_of :title, conditions: where('status != ?', 'archived')
+ # validates_uniqueness_of :title, conditions: -> { where.not(status: 'archived') }
# end
#
# When the record is created, a check is performed to make sure that no
@@ -131,7 +131,7 @@ module ActiveRecord
# the uniqueness constraint.
# * <tt>:conditions</tt> - Specify the conditions to be included as a
# <tt>WHERE</tt> SQL fragment to limit the uniqueness constraint lookup
- # (e.g. <tt>conditions: where('status = ?', 'active')</tt>).
+ # (e.g. <tt>conditions: -> { where(status: 'active') }</tt>).
# * <tt>:case_sensitive</tt> - Looks for an exact match. Ignored by
# non-text columns (+true+ by default).
# * <tt>:allow_nil</tt> - If set to +true+, skips this validation if the
@@ -197,8 +197,8 @@ module ActiveRecord
# 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.
+ # 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 0c35adc11d..de5fd05468 100644
--- a/activerecord/lib/active_record/version.rb
+++ b/activerecord/lib/active_record/version.rb
@@ -1,10 +1,11 @@
module ActiveRecord
- module VERSION #:nodoc:
- MAJOR = 4
- MINOR = 0
- TINY = 0
- PRE = "beta"
+ # Returns the version of the currently loaded ActiveRecord as a Gem::Version
+ def self.version
+ Gem::Version.new "4.1.0.beta"
+ end
- STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.')
+ module VERSION #:nodoc:
+ MAJOR, MINOR, TINY, PRE = ActiveRecord.version.segments
+ STRING = ActiveRecord.version.to_s
end
end
diff --git a/activerecord/lib/rails/generators/active_record.rb b/activerecord/lib/rails/generators/active_record.rb
index 297cd094c2..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
- class Base < Rails::Generators::NamedBase #:nodoc:
- include Rails::Generators::Migration
+ module Generators # :nodoc:
+ class Base < Rails::Generators::NamedBase # :nodoc:
+ 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) #:nodoc:
- 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/lib/rails/generators/active_record/migration/migration_generator.rb b/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb
index a3c274d9b9..3968acba64 100644
--- a/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb
+++ b/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb
@@ -1,20 +1,25 @@
require 'rails/generators/active_record'
module ActiveRecord
- module Generators
- class MigrationGenerator < Base
+ module Generators # :nodoc:
+ class MigrationGenerator < Base # :nodoc:
argument :attributes, :type => :array, :default => [], :banner => "field[:type][:index] field[:type][:index]"
def create_migration_file
set_local_assigns!
validate_file_name!
- migration_template "migration.rb", "db/migrate/#{file_name}.rb"
+ migration_template @migration_template, "db/migrate/#{file_name}.rb"
end
protected
attr_reader :migration_action, :join_tables
+ # sets the default migration template that is being used for the generation of the migration
+ # depending on the arguments which would be sent out in the command line, the migration template
+ # and the table name instance variables are setup.
+
def set_local_assigns!
+ @migration_template = "migration.rb"
case file_name
when /^(add|remove)_.*_(?:to|from)_(.*)/
@migration_action = $1
@@ -26,6 +31,9 @@ module ActiveRecord
set_index_names
end
+ when /^create_(.+)/
+ @table_name = $1.pluralize
+ @migration_template = "create_table_migration.rb"
end
end
@@ -42,9 +50,12 @@ module ActiveRecord
attribute.name.singularize.foreign_key
end.to_sym
end
-
- private
+ private
+ def attributes_with_index
+ attributes.select { |a| !a.reference? && a.has_index? }
+ end
+
def validate_file_name!
unless file_name =~ /^[_a-z0-9]+$/
raise IllegalMigrationNameError.new(file_name)
diff --git a/activerecord/lib/rails/generators/active_record/model/templates/migration.rb b/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb
index 3a3cf86d73..fd94a2d038 100644
--- a/activerecord/lib/rails/generators/active_record/model/templates/migration.rb
+++ b/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb
@@ -2,8 +2,12 @@ class <%= migration_class_name %> < ActiveRecord::Migration
def change
create_table :<%= table_name %> do |t|
<% attributes.each do |attribute| -%>
+<% if attribute.password_digest? -%>
+ t.string :password_digest<%= attribute.inject_options %>
+<% else -%>
t.<%= attribute.type %> :<%= attribute.name %><%= attribute.inject_options %>
<% end -%>
+<% end -%>
<% if options[:timestamps] %>
t.timestamps
<% end -%>
diff --git a/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb b/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb
index d5c07aecd3..ae9c74fd05 100644
--- a/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb
+++ b/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb
@@ -21,28 +21,16 @@ class <%= migration_class_name %> < ActiveRecord::Migration
end
end
<%- else -%>
- def up
+ def change
<% attributes.each do |attribute| -%>
<%- if migration_action -%>
<%- if attribute.reference? -%>
- remove_reference :<%= table_name %>, :<%= attribute.name %><%= ', polymorphic: true' if attribute.polymorphic? %>
+ remove_reference :<%= table_name %>, :<%= attribute.name %><%= attribute.inject_options %>
<%- else -%>
- remove_column :<%= table_name %>, :<%= attribute.name %>
- <%- end -%>
-<%- end -%>
-<%- end -%>
- end
-
- def down
-<% attributes.reverse.each do |attribute| -%>
-<%- if migration_action -%>
- <%- if attribute.reference? -%>
- add_reference :<%= table_name %>, :<%= attribute.name %><%= attribute.inject_options %>
- <%- else -%>
- add_column :<%= table_name %>, :<%= attribute.name %>, :<%= attribute.type %><%= attribute.inject_options %>
<%- if attribute.has_index? -%>
- add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %>
+ remove_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %>
<%- end -%>
+ remove_column :<%= table_name %>, :<%= attribute.name %>, :<%= attribute.type %><%= attribute.inject_options %>
<%- end -%>
<%- end -%>
<%- end -%>
diff --git a/activerecord/lib/rails/generators/active_record/model/model_generator.rb b/activerecord/lib/rails/generators/active_record/model/model_generator.rb
index 8e6ef20285..7e8d68ce69 100644
--- a/activerecord/lib/rails/generators/active_record/model/model_generator.rb
+++ b/activerecord/lib/rails/generators/active_record/model/model_generator.rb
@@ -1,8 +1,8 @@
require 'rails/generators/active_record'
module ActiveRecord
- module Generators
- class ModelGenerator < Base
+ module Generators # :nodoc:
+ class ModelGenerator < Base # :nodoc:
argument :attributes, :type => :array, :default => [], :banner => "field[:type][:index] field[:type][:index]"
check_class_collision
@@ -12,10 +12,13 @@ module ActiveRecord
class_option :parent, :type => :string, :desc => "The parent class for the generated model"
class_option :indexes, :type => :boolean, :default => true, :desc => "Add indexes for references and belongs_to columns"
+
+ # creates the migration file for the model.
+
def create_migration_file
return unless options[:migration] && options[:parent].nil?
attributes.each { |a| a.attr_options.delete(:index) if a.reference? && !a.has_index? } if options[:indexes] == false
- migration_template "migration.rb", "db/migrate/create_#{table_name}.rb"
+ migration_template "../../migration/templates/create_table_migration.rb", "db/migrate/create_#{table_name}.rb"
end
def create_model_file
@@ -39,6 +42,7 @@ module ActiveRecord
protected
+ # Used by the migration template to determine the parent name of the model
def parent_class_name
options[:parent] || "ActiveRecord::Base"
end
diff --git a/activerecord/lib/rails/generators/active_record/model/templates/model.rb b/activerecord/lib/rails/generators/active_record/model/templates/model.rb
index 056f55470c..808598699b 100644
--- a/activerecord/lib/rails/generators/active_record/model/templates/model.rb
+++ b/activerecord/lib/rails/generators/active_record/model/templates/model.rb
@@ -1,7 +1,10 @@
<% module_namespacing do -%>
class <%= class_name %> < <%= parent_class_name.classify %>
-<% attributes.select {|attr| attr.reference? }.each do |attribute| -%>
+<% attributes.select(&:reference?).each do |attribute| -%>
belongs_to :<%= attribute.name %><%= ', polymorphic: true' if attribute.polymorphic? %>
<% end -%>
+<% if attributes.any?(&:password_digest?) -%>
+ has_secure_password
+<% end -%>
end
<% end -%>
diff --git a/activerecord/lib/rails/generators/active_record/observer/observer_generator.rb b/activerecord/lib/rails/generators/active_record/observer/observer_generator.rb
deleted file mode 100644
index c1c0e3f25b..0000000000
--- a/activerecord/lib/rails/generators/active_record/observer/observer_generator.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-require 'rails/generators/active_record'
-
-module ActiveRecord
- module Generators
- class ObserverGenerator < Base
- check_class_collision :suffix => "Observer"
-
- def create_observer_file
- template 'observer.rb', File.join('app/models', class_path, "#{file_name}_observer.rb")
- end
-
- hook_for :test_framework
- end
- end
-end
diff --git a/activerecord/lib/rails/generators/active_record/observer/templates/observer.rb b/activerecord/lib/rails/generators/active_record/observer/templates/observer.rb
deleted file mode 100644
index eaa256a9bd..0000000000
--- a/activerecord/lib/rails/generators/active_record/observer/templates/observer.rb
+++ /dev/null
@@ -1,4 +0,0 @@
-<% module_namespacing do -%>
-class <%= class_name %>Observer < ActiveRecord::Observer
-end
-<% end -%>
diff --git a/activerecord/test/cases/adapter_test.rb b/activerecord/test/cases/adapter_test.rb
index 93b01a3934..dd355e8d0c 100644
--- a/activerecord/test/cases/adapter_test.rb
+++ b/activerecord/test/cases/adapter_test.rb
@@ -1,4 +1,5 @@
require "cases/helper"
+require "models/book"
module ActiveRecord
class AdapterTest < ActiveRecord::TestCase
@@ -6,6 +7,19 @@ module ActiveRecord
@connection = ActiveRecord::Base.connection
end
+ ##
+ # PostgreSQL does not support null bytes in strings
+ unless current_adapter?(:PostgreSQLAdapter)
+ def test_update_prepared_statement
+ b = Book.create(name: "my \x00 book")
+ b.reload
+ assert_equal "my \x00 book", b.name
+ b.update_attributes(name: "my other \x00 book")
+ b.reload
+ assert_equal "my other \x00 book", b.name
+ end
+ end
+
def test_tables
tables = @connection.tables
assert tables.include?("accounts")
@@ -69,16 +83,16 @@ module ActiveRecord
def test_not_specifying_database_name_for_cross_database_selects
begin
assert_nothing_raised do
- ActiveRecord::Model.establish_connection(ActiveRecord::Base.configurations['arunit'].except(:database))
+ ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations['arunit'].except(:database))
config = ARTest.connection_config
- ActiveRecord::Model.connection.execute(
+ ActiveRecord::Base.connection.execute(
"SELECT #{config['arunit']['database']}.pirates.*, #{config['arunit2']['database']}.courses.* " \
"FROM #{config['arunit']['database']}.pirates, #{config['arunit2']['database']}.courses"
)
end
ensure
- ActiveRecord::Model.establish_connection 'arunit'
+ ActiveRecord::Base.establish_connection 'arunit'
end
end
end
@@ -100,7 +114,7 @@ module ActiveRecord
end
end
- # test resetting sequences in odd tables in postgreSQL
+ # test resetting sequences in odd tables in PostgreSQL
if ActiveRecord::Base.connection.respond_to?(:reset_pk_sequence!)
require 'models/movie'
require 'models/subscriber'
@@ -153,25 +167,32 @@ module ActiveRecord
else
@connection.execute "INSERT INTO fk_test_has_fk (fk_id) VALUES (0)"
end
- # should deleted created record as otherwise disable_referential_integrity will try to enable contraints after executed block
+ # should delete created record as otherwise disable_referential_integrity will try to enable constraints after executed block
# and will fail (at least on Oracle)
@connection.execute "DELETE FROM fk_test_has_fk"
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
end
class AdapterTestWithoutTransaction < ActiveRecord::TestCase
self.use_transactional_fixtures = false
+ class Klass < ActiveRecord::Base
+ end
+
def setup
- @klass = Class.new(ActiveRecord::Base)
- @klass.establish_connection 'arunit'
- @connection = @klass.connection
+ Klass.establish_connection 'arunit'
+ @connection = Klass.connection
end
def teardown
- @klass.remove_connection
+ Klass.remove_connection
end
test "transaction state is reset after a reconnect" do
diff --git a/activerecord/test/cases/adapters/mysql/active_schema_test.rb b/activerecord/test/cases/adapters/mysql/active_schema_test.rb
index 94fc3564df..0878925a6c 100644
--- a/activerecord/test/cases/adapters/mysql/active_schema_test.rb
+++ b/activerecord/test/cases/adapters/mysql/active_schema_test.rb
@@ -2,47 +2,68 @@ require "cases/helper"
class ActiveSchemaTest < ActiveRecord::TestCase
def setup
- ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter.class_eval do
+ @connection = ActiveRecord::Base.remove_connection
+ ActiveRecord::Base.establish_connection(@connection)
+
+ ActiveRecord::Base.connection.singleton_class.class_eval do
alias_method :execute_without_stub, :execute
- remove_method :execute
def execute(sql, name = nil) return sql end
end
end
def teardown
- ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter.class_eval do
- remove_method :execute
- alias_method :execute, :execute_without_stub
- end
+ ActiveRecord::Base.remove_connection
+ ActiveRecord::Base.establish_connection(@connection)
end
def test_add_index
# add_index calls index_name_exists? which can't work since execute is stubbed
- ActiveRecord::ConnectionAdapters::MysqlAdapter.send(:define_method, :index_name_exists?) do |*|
- false
- end
- expected = "CREATE INDEX `index_people_on_last_name` ON `people` (`last_name`)"
+ def (ActiveRecord::Base.connection).index_name_exists?(*); false; end
+
+ expected = "CREATE INDEX `index_people_on_last_name` ON `people` (`last_name`) "
assert_equal expected, add_index(:people, :last_name, :length => nil)
- expected = "CREATE INDEX `index_people_on_last_name` ON `people` (`last_name`(10))"
+ expected = "CREATE INDEX `index_people_on_last_name` ON `people` (`last_name`(10)) "
assert_equal expected, add_index(:people, :last_name, :length => 10)
- expected = "CREATE INDEX `index_people_on_last_name_and_first_name` ON `people` (`last_name`(15), `first_name`(15))"
+ expected = "CREATE INDEX `index_people_on_last_name_and_first_name` ON `people` (`last_name`(15), `first_name`(15)) "
assert_equal expected, add_index(:people, [:last_name, :first_name], :length => 15)
- expected = "CREATE INDEX `index_people_on_last_name_and_first_name` ON `people` (`last_name`(15), `first_name`)"
+ expected = "CREATE INDEX `index_people_on_last_name_and_first_name` ON `people` (`last_name`(15), `first_name`) "
assert_equal expected, add_index(:people, [:last_name, :first_name], :length => {:last_name => 15})
- expected = "CREATE INDEX `index_people_on_last_name_and_first_name` ON `people` (`last_name`(15), `first_name`(10))"
+ expected = "CREATE INDEX `index_people_on_last_name_and_first_name` ON `people` (`last_name`(15), `first_name`(10)) "
assert_equal expected, add_index(:people, [:last_name, :first_name], :length => {:last_name => 15, :first_name => 10})
- ActiveRecord::ConnectionAdapters::MysqlAdapter.send(:remove_method, :index_name_exists?)
+
+ %w(SPATIAL FULLTEXT UNIQUE).each do |type|
+ expected = "CREATE #{type} INDEX `index_people_on_last_name` ON `people` (`last_name`) "
+ assert_equal expected, add_index(:people, :last_name, :type => type)
+ end
+
+ %w(btree hash).each do |using|
+ expected = "CREATE INDEX `index_people_on_last_name` USING #{using} ON `people` (`last_name`) "
+ assert_equal expected, add_index(:people, :last_name, :using => using)
+ end
+
+ expected = "CREATE INDEX `index_people_on_last_name` USING btree ON `people` (`last_name`(10)) "
+ assert_equal expected, add_index(:people, :last_name, :length => 10, :using => :btree)
+
+ expected = "CREATE INDEX `index_people_on_last_name` USING btree ON `people` (`last_name`(10)) ALGORITHM = COPY"
+ assert_equal expected, add_index(:people, :last_name, :length => 10, using: :btree, algorithm: :copy)
+
+ assert_raise ArgumentError do
+ add_index(:people, :last_name, algorithm: :coyp)
+ end
+
+ expected = "CREATE INDEX `index_people_on_last_name_and_first_name` USING btree ON `people` (`last_name`(15), `first_name`(15)) "
+ assert_equal expected, add_index(:people, [:last_name, :first_name], :length => 15, :using => :btree)
end
def test_drop_table
assert_equal "DROP TABLE `people`", drop_table(:people)
end
- if current_adapter?(:MysqlAdapter) or current_adapter?(:Mysql2Adapter)
+ if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
def test_create_mysql_database_with_encoding
assert_equal "CREATE DATABASE `matt` DEFAULT CHARACTER SET `utf8`", create_database(:matt)
assert_equal "CREATE DATABASE `aimonetti` DEFAULT CHARACTER SET `latin1`", create_database(:aimonetti, {:charset => 'latin1'})
@@ -70,8 +91,7 @@ class ActiveSchemaTest < ActiveRecord::TestCase
def test_add_timestamps
with_real_execute do
begin
- ActiveRecord::Base.connection.create_table :delete_me do |t|
- end
+ ActiveRecord::Base.connection.create_table :delete_me
ActiveRecord::Base.connection.add_timestamps :delete_me
assert column_present?('delete_me', 'updated_at', 'datetime')
assert column_present?('delete_me', 'created_at', 'datetime')
@@ -98,22 +118,20 @@ class ActiveSchemaTest < ActiveRecord::TestCase
private
def with_real_execute
- #we need to actually modify some data, so we make execute point to the original method
- ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter.class_eval do
+ ActiveRecord::Base.connection.singleton_class.class_eval do
alias_method :execute_with_stub, :execute
remove_method :execute
alias_method :execute, :execute_without_stub
end
+
yield
ensure
- #before finishing, we restore the alias to the mock-up method
- ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter.class_eval do
+ ActiveRecord::Base.connection.singleton_class.class_eval do
remove_method :execute
alias_method :execute, :execute_with_stub
end
end
-
def method_missing(method_symbol, *arguments)
ActiveRecord::Base.connection.send(method_symbol, *arguments)
end
diff --git a/activerecord/test/cases/adapters/mysql/connection_test.rb b/activerecord/test/cases/adapters/mysql/connection_test.rb
index 4bccd2cc59..1844a2e0dc 100644
--- a/activerecord/test/cases/adapters/mysql/connection_test.rb
+++ b/activerecord/test/cases/adapters/mysql/connection_test.rb
@@ -1,32 +1,37 @@
require "cases/helper"
class MysqlConnectionTest < ActiveRecord::TestCase
+ class Klass < ActiveRecord::Base
+ end
+
def setup
super
- @connection = ActiveRecord::Model.connection
+ @connection = ActiveRecord::Base.connection
end
def test_mysql_reconnect_attribute_after_connection_with_reconnect_true
run_without_connection do |orig_connection|
- ActiveRecord::Model.establish_connection(orig_connection.merge({:reconnect => true}))
- assert ActiveRecord::Model.connection.raw_connection.reconnect
+ ActiveRecord::Base.establish_connection(orig_connection.merge({:reconnect => true}))
+ assert ActiveRecord::Base.connection.raw_connection.reconnect
end
end
def test_connect_with_url
- run_without_connection do |orig|
+ 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 = Class.new(ActiveRecord::Base)
- klass.establish_connection(url)
- assert_equal ar_config['database'], klass.connection.current_database
+ Klass.establish_connection(url)
+ assert_equal ar_config['database'], Klass.connection.current_database
end
end
def test_mysql_reconnect_attribute_after_connection_with_reconnect_false
run_without_connection do |orig_connection|
- ActiveRecord::Model.establish_connection(orig_connection.merge({:reconnect => false}))
- assert !ActiveRecord::Model.connection.raw_connection.reconnect
+ ActiveRecord::Base.establish_connection(orig_connection.merge({:reconnect => false}))
+ assert !ActiveRecord::Base.connection.raw_connection.reconnect
end
end
@@ -117,7 +122,7 @@ class MysqlConnectionTest < ActiveRecord::TestCase
# Test that MySQL allows multiple results for stored procedures
if defined?(Mysql) && Mysql.const_defined?(:CLIENT_MULTI_RESULTS)
def test_multi_results
- rows = ActiveRecord::Model.connection.select_rows('CALL ten();')
+ rows = ActiveRecord::Base.connection.select_rows('CALL ten();')
assert_equal 10, rows[0][0].to_i, "ten() did not return 10 as expected: #{rows.inspect}"
assert @connection.active?, "Bad connection use by 'MysqlAdapter.select_rows'"
end
@@ -130,21 +135,38 @@ class MysqlConnectionTest < ActiveRecord::TestCase
def test_mysql_strict_mode_disabled_dont_override_global_sql_mode
run_without_connection do |orig_connection|
- ActiveRecord::Model.establish_connection(orig_connection.merge({:strict => false}))
- global_sql_mode = ActiveRecord::Model.connection.exec_query "SELECT @@GLOBAL.sql_mode"
- session_sql_mode = ActiveRecord::Model.connection.exec_query "SELECT @@SESSION.sql_mode"
+ ActiveRecord::Base.establish_connection(orig_connection.merge({:strict => false}))
+ global_sql_mode = ActiveRecord::Base.connection.exec_query "SELECT @@GLOBAL.sql_mode"
+ session_sql_mode = ActiveRecord::Base.connection.exec_query "SELECT @@SESSION.sql_mode"
assert_equal global_sql_mode.rows, session_sql_mode.rows
end
end
+ def test_mysql_set_session_variable
+ run_without_connection do |orig_connection|
+ ActiveRecord::Base.establish_connection(orig_connection.deep_merge({:variables => {:default_week_format => 3}}))
+ session_mode = ActiveRecord::Base.connection.exec_query "SELECT @@SESSION.DEFAULT_WEEK_FORMAT"
+ assert_equal 3, session_mode.rows.first.first.to_i
+ end
+ end
+
+ def test_mysql_set_session_variable_to_default
+ run_without_connection do |orig_connection|
+ ActiveRecord::Base.establish_connection(orig_connection.deep_merge({:variables => {:default_week_format => :default}}))
+ global_mode = ActiveRecord::Base.connection.exec_query "SELECT @@GLOBAL.DEFAULT_WEEK_FORMAT"
+ session_mode = ActiveRecord::Base.connection.exec_query "SELECT @@SESSION.DEFAULT_WEEK_FORMAT"
+ assert_equal global_mode.rows, session_mode.rows
+ end
+ end
+
private
def run_without_connection
- original_connection = ActiveRecord::Model.remove_connection
+ original_connection = ActiveRecord::Base.remove_connection
begin
yield original_connection
ensure
- ActiveRecord::Model.establish_connection(original_connection)
+ ActiveRecord::Base.establish_connection(original_connection)
end
end
end
diff --git a/activerecord/test/cases/adapters/mysql/enum_test.rb b/activerecord/test/cases/adapters/mysql/enum_test.rb
index 40af317ad1..f4e7a3ef0a 100644
--- a/activerecord/test/cases/adapters/mysql/enum_test.rb
+++ b/activerecord/test/cases/adapters/mysql/enum_test.rb
@@ -5,6 +5,6 @@ class MysqlEnumTest < ActiveRecord::TestCase
end
def test_enum_limit
- assert_equal 5, EnumTest.columns.first.limit
+ assert_equal 6, EnumTest.columns.first.limit
end
end
diff --git a/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb b/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb
index ddfe42b375..9ad0744aee 100644
--- a/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb
+++ b/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb
@@ -16,6 +16,15 @@ module ActiveRecord
eosql
end
+ def test_valid_column
+ column = @conn.columns('ex').find { |col| col.name == 'id' }
+ assert @conn.valid_type?(column.type)
+ end
+
+ def test_invalid_column
+ assert_not @conn.valid_type?(:foobar)
+ end
+
def test_client_encoding
assert_equal Encoding::UTF_8, @conn.client_encoding
end
@@ -49,13 +58,11 @@ module ActiveRecord
end
def test_tables_quoting
- begin
- @conn.tables(nil, "foo-bar", nil)
- flunk
- rescue => e
- # assertion for *quoted* database properly
- assert_match(/database 'foo-bar'/, e.inspect)
- end
+ @conn.tables(nil, "foo-bar", nil)
+ flunk
+ rescue => e
+ # assertion for *quoted* database properly
+ assert_match(/database 'foo-bar'/, e.inspect)
end
def test_pk_and_sequence_for
@@ -88,14 +95,39 @@ module ActiveRecord
assert_equal @conn.default_sequence_name('ex_with_custom_index_type_pk', 'id'), seq
end
+ def test_tinyint_integer_typecasting
+ @conn.exec_query('drop table if exists ex_with_non_boolean_tinyint_column')
+ @conn.exec_query(<<-eosql)
+ CREATE TABLE `ex_with_non_boolean_tinyint_column` (
+ `status` TINYINT(4))
+ eosql
+ insert(@conn, { 'status' => 2 }, 'ex_with_non_boolean_tinyint_column')
+
+ result = @conn.exec_query('SELECT status FROM ex_with_non_boolean_tinyint_column')
+
+ 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)
+ def insert(ctx, data, table='ex')
binds = data.map { |name, value|
- [ctx.columns('ex').find { |x| x.name == name }, value]
+ [ctx.columns(table).find { |x| x.name == name }, value]
}
columns = binds.map(&:first).map(&:name)
- sql = "INSERT INTO ex (#{columns.join(", ")})
+ sql = "INSERT INTO #{table} (#{columns.join(", ")})
VALUES (#{(['?'] * columns.length).join(', ')})"
ctx.exec_insert(sql, 'SQL', binds)
diff --git a/activerecord/test/cases/adapters/mysql/reserved_word_test.rb b/activerecord/test/cases/adapters/mysql/reserved_word_test.rb
index 5164acf77f..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
@@ -63,11 +63,6 @@ class MysqlReservedWordTest < ActiveRecord::TestCase
assert_nothing_raised { @connection.rename_column(:group, :order, :values) }
end
- # dump structure of table with reserved word name
- def test_structure_dump
- assert_nothing_raised { @connection.structure_dump }
- end
-
# introspect table with reserved word name
def test_introspect
assert_nothing_raised { @connection.columns(:group) }
diff --git a/activerecord/test/cases/adapters/mysql/schema_test.rb b/activerecord/test/cases/adapters/mysql/schema_test.rb
index d94bb629a7..807a7a155e 100644
--- a/activerecord/test/cases/adapters/mysql/schema_test.rb
+++ b/activerecord/test/cases/adapters/mysql/schema_test.rb
@@ -35,6 +35,28 @@ module ActiveRecord
def test_table_exists_wrong_schema
assert(!@connection.table_exists?("#{@db_name}.zomg"), "table should not exist")
end
+
+ def test_dump_indexes
+ index_a_name = 'index_key_tests_on_snack'
+ index_b_name = 'index_key_tests_on_pizza'
+ index_c_name = 'index_key_tests_on_awesome'
+
+ table = 'key_tests'
+
+ indexes = @connection.indexes(table).sort_by {|i| i.name}
+ assert_equal 3,indexes.size
+
+ index_a = indexes.select{|i| i.name == index_a_name}[0]
+ index_b = indexes.select{|i| i.name == index_b_name}[0]
+ index_c = indexes.select{|i| i.name == index_c_name}[0]
+ assert_equal :btree, index_a.using
+ assert_nil index_a.type
+ assert_equal :btree, index_b.using
+ assert_nil index_b.type
+
+ assert_nil index_c.using
+ assert_equal :fulltext, index_c.type
+ end
end
end
end
diff --git a/activerecord/test/cases/adapters/mysql/sql_types_test.rb b/activerecord/test/cases/adapters/mysql/sql_types_test.rb
new file mode 100644
index 0000000000..1ddb1b91c9
--- /dev/null
+++ b/activerecord/test/cases/adapters/mysql/sql_types_test.rb
@@ -0,0 +1,14 @@
+require "cases/helper"
+
+class SqlTypesTest < ActiveRecord::TestCase
+ def test_binary_types
+ assert_equal 'varbinary(64)', type_to_sql(:binary, 64)
+ assert_equal 'varbinary(4095)', type_to_sql(:binary, 4095)
+ assert_equal 'blob(4096)', type_to_sql(:binary, 4096)
+ assert_equal 'blob', type_to_sql(:binary)
+ end
+
+ def type_to_sql(*args)
+ ActiveRecord::Base.connection.type_to_sql(*args)
+ end
+end
diff --git a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb
index a83399d0cd..4ccf568406 100644
--- a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb
@@ -2,40 +2,61 @@ require "cases/helper"
class ActiveSchemaTest < ActiveRecord::TestCase
def setup
- ActiveRecord::ConnectionAdapters::Mysql2Adapter.class_eval do
+ @connection = ActiveRecord::Base.remove_connection
+ ActiveRecord::Base.establish_connection(@connection)
+
+ ActiveRecord::Base.connection.singleton_class.class_eval do
alias_method :execute_without_stub, :execute
- remove_method :execute
def execute(sql, name = nil) return sql end
end
end
def teardown
- ActiveRecord::ConnectionAdapters::Mysql2Adapter.class_eval do
- remove_method :execute
- alias_method :execute, :execute_without_stub
- end
+ ActiveRecord::Base.remove_connection
+ ActiveRecord::Base.establish_connection(@connection)
end
def test_add_index
# add_index calls index_name_exists? which can't work since execute is stubbed
- ActiveRecord::ConnectionAdapters::Mysql2Adapter.send(:define_method, :index_name_exists?) do |*|
- false
- end
- expected = "CREATE INDEX `index_people_on_last_name` ON `people` (`last_name`)"
+ def (ActiveRecord::Base.connection).index_name_exists?(*); false; end
+
+ expected = "CREATE INDEX `index_people_on_last_name` ON `people` (`last_name`) "
assert_equal expected, add_index(:people, :last_name, :length => nil)
- expected = "CREATE INDEX `index_people_on_last_name` ON `people` (`last_name`(10))"
+ expected = "CREATE INDEX `index_people_on_last_name` ON `people` (`last_name`(10)) "
assert_equal expected, add_index(:people, :last_name, :length => 10)
- expected = "CREATE INDEX `index_people_on_last_name_and_first_name` ON `people` (`last_name`(15), `first_name`(15))"
+ expected = "CREATE INDEX `index_people_on_last_name_and_first_name` ON `people` (`last_name`(15), `first_name`(15)) "
assert_equal expected, add_index(:people, [:last_name, :first_name], :length => 15)
- expected = "CREATE INDEX `index_people_on_last_name_and_first_name` ON `people` (`last_name`(15), `first_name`)"
+ expected = "CREATE INDEX `index_people_on_last_name_and_first_name` ON `people` (`last_name`(15), `first_name`) "
assert_equal expected, add_index(:people, [:last_name, :first_name], :length => {:last_name => 15})
- expected = "CREATE INDEX `index_people_on_last_name_and_first_name` ON `people` (`last_name`(15), `first_name`(10))"
+ expected = "CREATE INDEX `index_people_on_last_name_and_first_name` ON `people` (`last_name`(15), `first_name`(10)) "
assert_equal expected, add_index(:people, [:last_name, :first_name], :length => {:last_name => 15, :first_name => 10})
- ActiveRecord::ConnectionAdapters::Mysql2Adapter.send(:remove_method, :index_name_exists?)
+
+ %w(SPATIAL FULLTEXT UNIQUE).each do |type|
+ expected = "CREATE #{type} INDEX `index_people_on_last_name` ON `people` (`last_name`) "
+ assert_equal expected, add_index(:people, :last_name, :type => type)
+ end
+
+ %w(btree hash).each do |using|
+ expected = "CREATE INDEX `index_people_on_last_name` USING #{using} ON `people` (`last_name`) "
+ assert_equal expected, add_index(:people, :last_name, :using => using)
+ end
+
+ expected = "CREATE INDEX `index_people_on_last_name` USING btree ON `people` (`last_name`(10)) "
+ assert_equal expected, add_index(:people, :last_name, :length => 10, :using => :btree)
+
+ expected = "CREATE INDEX `index_people_on_last_name` USING btree ON `people` (`last_name`(10)) ALGORITHM = COPY"
+ assert_equal expected, add_index(:people, :last_name, :length => 10, using: :btree, algorithm: :copy)
+
+ assert_raise ArgumentError do
+ add_index(:people, :last_name, algorithm: :coyp)
+ end
+
+ expected = "CREATE INDEX `index_people_on_last_name_and_first_name` USING btree ON `people` (`last_name`(15), `first_name`(15)) "
+ assert_equal expected, add_index(:people, [:last_name, :first_name], :length => 15, :using => :btree)
end
def test_drop_table
@@ -70,8 +91,7 @@ class ActiveSchemaTest < ActiveRecord::TestCase
def test_add_timestamps
with_real_execute do
begin
- ActiveRecord::Base.connection.create_table :delete_me do |t|
- end
+ ActiveRecord::Base.connection.create_table :delete_me
ActiveRecord::Base.connection.add_timestamps :delete_me
assert column_present?('delete_me', 'updated_at', 'datetime')
assert column_present?('delete_me', 'created_at', 'datetime')
@@ -98,22 +118,20 @@ class ActiveSchemaTest < ActiveRecord::TestCase
private
def with_real_execute
- #we need to actually modify some data, so we make execute point to the original method
- ActiveRecord::ConnectionAdapters::Mysql2Adapter.class_eval do
+ ActiveRecord::Base.connection.singleton_class.class_eval do
alias_method :execute_with_stub, :execute
remove_method :execute
alias_method :execute, :execute_without_stub
end
+
yield
ensure
- #before finishing, we restore the alias to the mock-up method
- ActiveRecord::ConnectionAdapters::Mysql2Adapter.class_eval do
+ ActiveRecord::Base.connection.singleton_class.class_eval do
remove_method :execute
alias_method :execute, :execute_with_stub
end
end
-
def method_missing(method_symbol, *arguments)
ActiveRecord::Base.connection.send(method_symbol, *arguments)
end
diff --git a/activerecord/test/cases/adapters/mysql2/connection_test.rb b/activerecord/test/cases/adapters/mysql2/connection_test.rb
index c63e4fe5b6..679c515e8c 100644
--- a/activerecord/test/cases/adapters/mysql2/connection_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/connection_test.rb
@@ -3,14 +3,14 @@ require "cases/helper"
class MysqlConnectionTest < ActiveRecord::TestCase
def setup
super
- @connection = ActiveRecord::Model.connection
- @connection.extend(LogIntercepter)
- @connection.intercepted = true
+ @subscriber = SQLSubscriber.new
+ ActiveSupport::Notifications.subscribe('sql.active_record', @subscriber)
+ @connection = ActiveRecord::Base.connection
end
def teardown
- @connection.intercepted = false
- @connection.logged = []
+ ActiveSupport::Notifications.unsubscribe(@subscriber)
+ super
end
def test_no_automatic_reconnection_after_timeout
@@ -46,29 +46,40 @@ class MysqlConnectionTest < ActiveRecord::TestCase
def test_mysql_strict_mode_disabled_dont_override_global_sql_mode
run_without_connection do |orig_connection|
- ActiveRecord::Model.establish_connection(orig_connection.merge({:strict => false}))
- global_sql_mode = ActiveRecord::Model.connection.exec_query "SELECT @@GLOBAL.sql_mode"
- session_sql_mode = ActiveRecord::Model.connection.exec_query "SELECT @@SESSION.sql_mode"
+ ActiveRecord::Base.establish_connection(orig_connection.merge({:strict => false}))
+ global_sql_mode = ActiveRecord::Base.connection.exec_query "SELECT @@GLOBAL.sql_mode"
+ session_sql_mode = ActiveRecord::Base.connection.exec_query "SELECT @@SESSION.sql_mode"
assert_equal global_sql_mode.rows, session_sql_mode.rows
end
end
- def test_logs_name_structure_dump
- @connection.structure_dump
- assert_equal "SCHEMA", @connection.logged[0][1]
- assert_equal "SCHEMA", @connection.logged[2][1]
+ def test_mysql_set_session_variable
+ run_without_connection do |orig_connection|
+ ActiveRecord::Base.establish_connection(orig_connection.deep_merge({:variables => {:default_week_format => 3}}))
+ session_mode = ActiveRecord::Base.connection.exec_query "SELECT @@SESSION.DEFAULT_WEEK_FORMAT"
+ assert_equal 3, session_mode.rows.first.first.to_i
+ end
+ end
+
+ def test_mysql_set_session_variable_to_default
+ run_without_connection do |orig_connection|
+ ActiveRecord::Base.establish_connection(orig_connection.deep_merge({:variables => {:default_week_format => :default}}))
+ global_mode = ActiveRecord::Base.connection.exec_query "SELECT @@GLOBAL.DEFAULT_WEEK_FORMAT"
+ session_mode = ActiveRecord::Base.connection.exec_query "SELECT @@SESSION.DEFAULT_WEEK_FORMAT"
+ assert_equal global_mode.rows, session_mode.rows
+ end
end
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
@@ -76,11 +87,11 @@ class MysqlConnectionTest < ActiveRecord::TestCase
private
def run_without_connection
- original_connection = ActiveRecord::Model.remove_connection
+ original_connection = ActiveRecord::Base.remove_connection
begin
yield original_connection
ensure
- ActiveRecord::Model.establish_connection(original_connection)
+ ActiveRecord::Base.establish_connection(original_connection)
end
end
end
diff --git a/activerecord/test/cases/adapters/mysql2/enum_test.rb b/activerecord/test/cases/adapters/mysql2/enum_test.rb
index f3a05e48ad..6dd9a5ec87 100644
--- a/activerecord/test/cases/adapters/mysql2/enum_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/enum_test.rb
@@ -5,6 +5,6 @@ class Mysql2EnumTest < ActiveRecord::TestCase
end
def test_enum_limit
- assert_equal 5, EnumTest.columns.first.limit
+ assert_equal 6, EnumTest.columns.first.limit
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 1017b0758d..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
@@ -63,11 +63,6 @@ class MysqlReservedWordTest < ActiveRecord::TestCase
assert_nothing_raised { @connection.rename_column(:group, :order, :values) }
end
- # dump structure of table with reserved word name
- def test_structure_dump
- assert_nothing_raised { @connection.structure_dump }
- end
-
# introspect table with reserved word name
def test_introspect
assert_nothing_raised { @connection.columns(:group) }
@@ -89,7 +84,6 @@ class MysqlReservedWordTest < ActiveRecord::TestCase
assert_nothing_raised { x.save }
assert_nothing_raised { Group.find_by_order('y') }
assert_nothing_raised { Group.find(1) }
- x = Group.find(1)
end
# has_one association with reserved-word table name
diff --git a/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb b/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb
new file mode 100644
index 0000000000..9ecd901eac
--- /dev/null
+++ b/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb
@@ -0,0 +1,26 @@
+require "cases/helper"
+
+module ActiveRecord
+ module ConnectionAdapters
+ class Mysql2Adapter
+ class SchemaMigrationsTest < ActiveRecord::TestCase
+ def test_initializes_schema_migrations_for_encoding_utf8mb4
+ conn = ActiveRecord::Base.connection
+
+ smtn = ActiveRecord::Migrator.schema_migrations_table_name
+ conn.drop_table(smtn) if conn.table_exists?(smtn)
+
+ config = conn.instance_variable_get(:@config)
+ original_encoding = config[:encoding]
+
+ config[:encoding] = 'utf8mb4'
+ conn.initialize_schema_migrations_table
+
+ assert conn.column_exists?(smtn, :version, :string, limit: Mysql2Adapter::MAX_INDEX_LENGTH_FOR_UTF8MB4)
+ ensure
+ config[:encoding] = original_encoding
+ end
+ 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 2c0ed73c92..5db60ff8a0 100644
--- a/activerecord/test/cases/adapters/mysql2/schema_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/schema_test.rb
@@ -37,15 +37,34 @@ module ActiveRecord
end
def test_tables_quoting
- begin
- @connection.tables(nil, "foo-bar", nil)
- flunk
- rescue => e
- # assertion for *quoted* database properly
- assert_match(/database 'foo-bar'/, e.inspect)
- end
+ @connection.tables(nil, "foo-bar", nil)
+ flunk
+ rescue => e
+ # assertion for *quoted* database properly
+ assert_match(/database 'foo-bar'/, e.inspect)
end
+ def test_dump_indexes
+ index_a_name = 'index_key_tests_on_snack'
+ index_b_name = 'index_key_tests_on_pizza'
+ index_c_name = 'index_key_tests_on_awesome'
+
+ table = 'key_tests'
+
+ indexes = @connection.indexes(table).sort_by {|i| i.name}
+ assert_equal 3,indexes.size
+
+ index_a = indexes.select{|i| i.name == index_a_name}[0]
+ index_b = indexes.select{|i| i.name == index_b_name}[0]
+ index_c = indexes.select{|i| i.name == index_c_name}[0]
+ assert_equal :btree, index_a.using
+ assert_nil index_a.type
+ assert_equal :btree, index_b.using
+ assert_nil index_b.type
+
+ assert_nil index_c.using
+ assert_equal :fulltext, index_c.type
+ end
end
end
end
diff --git a/activerecord/test/cases/adapters/mysql2/sql_types_test.rb b/activerecord/test/cases/adapters/mysql2/sql_types_test.rb
new file mode 100644
index 0000000000..1ddb1b91c9
--- /dev/null
+++ b/activerecord/test/cases/adapters/mysql2/sql_types_test.rb
@@ -0,0 +1,14 @@
+require "cases/helper"
+
+class SqlTypesTest < ActiveRecord::TestCase
+ def test_binary_types
+ assert_equal 'varbinary(64)', type_to_sql(:binary, 64)
+ assert_equal 'varbinary(4095)', type_to_sql(:binary, 4095)
+ assert_equal 'blob(4096)', type_to_sql(:binary, 4096)
+ assert_equal 'blob', type_to_sql(:binary)
+ end
+
+ def type_to_sql(*args)
+ ActiveRecord::Base.connection.type_to_sql(*args)
+ 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 1b4f4a5fc9..22dd48e113 100644
--- a/activerecord/test/cases/adapters/postgresql/active_schema_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/active_schema_test.rb
@@ -16,6 +16,7 @@ class PostgresqlActiveSchemaTest < ActiveRecord::TestCase
def test_create_database_with_encoding
assert_equal %(CREATE DATABASE "matt" ENCODING = 'utf8'), create_database(:matt)
assert_equal %(CREATE DATABASE "aimonetti" ENCODING = 'latin1'), create_database(:aimonetti, :encoding => :latin1)
+ assert_equal %(CREATE DATABASE "aimonetti" ENCODING = 'latin1'), create_database(:aimonetti, 'encoding' => :latin1)
end
def test_create_database_with_collation_and_ctype
@@ -24,14 +25,30 @@ 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')
+ 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'")
- ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.send(:remove_method, :index_name_exists?)
+ expected = %(CREATE INDEX CONCURRENTLY "index_people_on_last_name" ON "people" ("last_name"))
+ assert_equal expected, add_index(:people, :last_name, algorithm: :concurrently)
+
+ %w(gin gist hash btree).each do |type|
+ expected = %(CREATE INDEX "index_people_on_last_name" ON "people" USING #{type} ("last_name"))
+ assert_equal expected, add_index(:people, :last_name, using: type)
+
+ expected = %(CREATE INDEX CONCURRENTLY "index_people_on_last_name" ON "people" USING #{type} ("last_name"))
+ assert_equal expected, add_index(:people, :last_name, using: type, algorithm: :concurrently)
+ end
+
+ assert_raise ArgumentError do
+ add_index(:people, :last_name, algorithm: :copy)
+ end
+ expected = %(CREATE UNIQUE INDEX "index_people_on_last_name" ON "people" USING gist ("last_name"))
+ assert_equal expected, add_index(:people, :last_name, :unique => true, :using => :gist)
+
+ 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)
end
private
diff --git a/activerecord/test/cases/adapters/postgresql/array_test.rb b/activerecord/test/cases/adapters/postgresql/array_test.rb
index 8774bf626f..9536cceb1d 100644
--- a/activerecord/test/cases/adapters/postgresql/array_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/array_test.rb
@@ -12,7 +12,8 @@ class PostgresqlArrayTest < ActiveRecord::TestCase
@connection = ActiveRecord::Base.connection
@connection.transaction do
@connection.create_table('pg_arrays') do |t|
- t.string 'tags', :array => true
+ t.string 'tags', array: true
+ t.integer 'ratings', array: true
end
end
@column = PgArray.columns.find { |c| c.name == 'tags' }
@@ -27,6 +28,27 @@ class PostgresqlArrayTest < ActiveRecord::TestCase
assert @column.array
end
+ 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
assert @column
@@ -57,42 +79,57 @@ 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
+ tag_values = ["val1", "val2", "val3_with_'_multiple_quote_'_chars"]
+ @connection.insert_fixture({"tags" => tag_values}, "pg_arrays" )
+ 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
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
new file mode 100644
index 0000000000..b8dd35c4c5
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/bytea_test.rb
@@ -0,0 +1,104 @@
+# encoding: utf-8
+
+require "cases/helper"
+require 'active_record/base'
+require 'active_record/connection_adapters/postgresql_adapter'
+
+class PostgresqlByteaTest < ActiveRecord::TestCase
+ class ByteaDataType < ActiveRecord::Base
+ self.table_name = 'bytea_data_type'
+ end
+
+ def setup
+ @connection = ActiveRecord::Base.connection
+ begin
+ @connection.transaction do
+ @connection.create_table('bytea_data_type') do |t|
+ t.binary 'payload'
+ t.binary 'serialized'
+ end
+ end
+ end
+ @column = ByteaDataType.columns.find { |c| c.name == 'payload' }
+ assert(@column.is_a?(ActiveRecord::ConnectionAdapters::PostgreSQLColumn))
+ end
+
+ def teardown
+ @connection.execute 'drop table if exists bytea_data_type'
+ end
+
+ def test_column
+ assert_equal :binary, @column.type
+ end
+
+ def test_type_cast_binary_converts_the_encoding
+ assert @column
+
+ data = "\u001F\x8B"
+ assert_equal('UTF-8', data.encoding.name)
+ assert_equal('ASCII-8BIT', @column.type_cast(data).encoding.name)
+ end
+
+ def test_type_cast_binary_value
+ data = "\u001F\x8B".force_encoding("BINARY")
+ assert_equal(data, @column.type_cast(data))
+ end
+
+ def test_type_case_nil
+ assert_equal(nil, @column.type_cast(nil))
+ end
+
+ def test_read_value
+ data = "\u001F"
+ @connection.execute "insert into bytea_data_type (payload) VALUES ('#{data}')"
+ record = ByteaDataType.first
+ assert_equal(data, record.payload)
+ record.delete
+ end
+
+ def test_read_nil_value
+ @connection.execute "insert into bytea_data_type (payload) VALUES (null)"
+ record = ByteaDataType.first
+ assert_equal(nil, record.payload)
+ record.delete
+ end
+
+ def test_write_value
+ data = "\u001F"
+ record = ByteaDataType.create(payload: data)
+ assert_not record.new_record?
+ assert_equal(data, record.payload)
+ end
+
+ def test_write_binary
+ data = File.read(File.join(File.dirname(__FILE__), '..', '..', '..', 'assets', 'example.log'))
+ assert(data.size > 1)
+ record = ByteaDataType.create(payload: data)
+ 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)
+ 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 1ff307c735..81aa977c59 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,74 +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_reconnection_after_simulated_disconnection_with_verify
- assert @connection.active?
- original_connection_pid = @connection.query('select pg_backend_pid()')
-
- # Fail with bad connection on next query attempt.
- raw_connection = @connection.raw_connection
- raw_connection_class = class << raw_connection ; self ; end
- raw_connection_class.class_eval <<-CODE, __FILE__, __LINE__ + 1
- def query_fake(*args)
- if !( @called ||= false )
- self.stubs(:status).returns(PGconn::CONNECTION_BAD)
- @called = true
- raise PGError
- else
- self.unstub(:status)
- query_unfake(*args)
- end
- end
-
- alias query_unfake query
- alias query query_fake
- CODE
-
- begin
- @connection.verify!
- new_connection_pid = @connection.query('select pg_backend_pid()')
- ensure
- raw_connection_class.class_eval <<-CODE
- alias query query_unfake
- undef query_fake
- CODE
- end
-
- assert_not_equal original_connection_pid, new_connection_pid, "Should have a new underlying connection pid"
+ 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
@@ -154,5 +128,46 @@ module ActiveRecord
end
end
+ def test_set_session_variable_true
+ run_without_connection do |orig_connection|
+ ActiveRecord::Base.establish_connection(orig_connection.deep_merge({:variables => {:debug_print_plan => true}}))
+ set_true = ActiveRecord::Base.connection.exec_query "SHOW DEBUG_PRINT_PLAN"
+ assert_equal set_true.rows, [["on"]]
+ end
+ end
+
+ def test_set_session_variable_false
+ run_without_connection do |orig_connection|
+ ActiveRecord::Base.establish_connection(orig_connection.deep_merge({:variables => {:debug_print_plan => false}}))
+ set_false = ActiveRecord::Base.connection.exec_query "SHOW DEBUG_PRINT_PLAN"
+ assert_equal set_false.rows, [["off"]]
+ end
+ end
+
+ def test_set_session_variable_nil
+ run_without_connection do |orig_connection|
+ # This should be a no-op that does not raise an error
+ ActiveRecord::Base.establish_connection(orig_connection.deep_merge({:variables => {:debug_print_plan => nil}}))
+ end
+ end
+
+ def test_set_session_variable_default
+ run_without_connection do |orig_connection|
+ # This should execute a query that does not raise an error
+ ActiveRecord::Base.establish_connection(orig_connection.deep_merge({:variables => {:debug_print_plan => :default}}))
+ end
+ end
+
+ private
+
+ def run_without_connection
+ original_connection = ActiveRecord::Base.remove_connection
+ begin
+ yield original_connection
+ ensure
+ ActiveRecord::Base.establish_connection(original_connection)
+ end
+ end
+
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/datatype_test.rb b/activerecord/test/cases/adapters/postgresql/datatype_test.rb
index c7ce43d71e..c5ff8cb609 100644
--- a/activerecord/test/cases/adapters/postgresql/datatype_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/datatype_test.rb
@@ -3,6 +3,9 @@ require "cases/helper"
class PostgresqlArray < ActiveRecord::Base
end
+class PostgresqlRange < ActiveRecord::Base
+end
+
class PostgresqlTsvector < ActiveRecord::Base
end
@@ -30,6 +33,9 @@ end
class PostgresqlUUID < ActiveRecord::Base
end
+class PostgresqlLtree < ActiveRecord::Base
+end
+
class PostgresqlDataTypeTest < ActiveRecord::TestCase
self.use_transactional_fixtures = false
@@ -37,43 +43,157 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase
@connection = ActiveRecord::Base.connection
@connection.execute("set lc_monetary = 'C'")
- @connection.execute("INSERT INTO postgresql_arrays (commission_by_quarter, nicknames) VALUES ( '{35000,21000,18000,17000}', '{foo,bar,baz}' )")
+ @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("INSERT INTO postgresql_tsvectors (text_vector) VALUES (' ''text'' ''vector'' ')")
+ @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)
- @connection.execute("INSERT INTO postgresql_moneys (wealth) VALUES ('567.89'::money)")
- @connection.execute("INSERT INTO postgresql_moneys (wealth) VALUES ('-567.89'::money)")
+ @connection.execute("INSERT INTO postgresql_moneys (id, wealth) VALUES (1, '567.89'::money)")
+ @connection.execute("INSERT INTO postgresql_moneys (id, wealth) VALUES (2, '-567.89'::money)")
@first_money = PostgresqlMoney.find(1)
@second_money = PostgresqlMoney.find(2)
- @connection.execute("INSERT INTO postgresql_numbers (single, double) VALUES (123.456, 123456.789)")
+ @connection.execute("INSERT INTO postgresql_numbers (id, single, double) VALUES (1, 123.456, 123456.789)")
@first_number = PostgresqlNumber.find(1)
- @connection.execute("INSERT INTO postgresql_times (time_interval, scaled_time_interval) VALUES ('1 year 2 days ago', '3 weeks ago')")
+ @connection.execute("INSERT INTO postgresql_times (id, time_interval, scaled_time_interval) VALUES (1, '1 year 2 days ago', '3 weeks ago')")
@first_time = PostgresqlTime.find(1)
- @connection.execute("INSERT INTO postgresql_network_addresses (cidr_address, inet_address, mac_address) VALUES('192.168.0/24', '172.16.1.254/32', '01:23:45:67:89:0a')")
+ @connection.execute("INSERT INTO postgresql_network_addresses (id, cidr_address, inet_address, mac_address) VALUES(1, '192.168.0/24', '172.16.1.254/32', '01:23:45:67:89:0a')")
@first_network_address = PostgresqlNetworkAddress.find(1)
- @connection.execute("INSERT INTO postgresql_bit_strings (bit_string, bit_string_varying) VALUES (B'00010101', X'15')")
+ @connection.execute("INSERT INTO postgresql_bit_strings (id, bit_string, bit_string_varying) VALUES (1, B'00010101', X'15')")
@first_bit_string = PostgresqlBitString.find(1)
- @connection.execute("INSERT INTO postgresql_oids (obj_id) VALUES (1234)")
+ @connection.execute("INSERT INTO postgresql_oids (id, obj_id) VALUES (1, 1234)")
@first_oid = PostgresqlOid.find(1)
- @connection.execute("INSERT INTO postgresql_timestamp_with_zones (time) VALUES ('2010-01-01 10:00:00-1')")
+ @connection.execute("INSERT INTO postgresql_timestamp_with_zones (id, time) VALUES (1, '2010-01-01 10:00:00-1')")
- @connection.execute("INSERT INTO postgresql_uuids (guid, compact_guid) VALUES('d96c3da0-96c1-012f-1316-64ce8f32c6d8', 'f06c715096c1012f131764ce8f32c6d8')")
+ @connection.execute("INSERT INTO postgresql_uuids (id, guid, compact_guid) VALUES(1, 'd96c3da0-96c1-012f-1316-64ce8f32c6d8', 'f06c715096c1012f131764ce8f32c6d8')")
@first_uuid = PostgresqlUUID.find(1)
end
+ def teardown
+ [PostgresqlArray, PostgresqlTsvector, PostgresqlMoney, PostgresqlNumber, PostgresqlTime, PostgresqlNetworkAddress,
+ PostgresqlBitString, PostgresqlOid, PostgresqlTimestampWithZone, PostgresqlUUID].each(&:delete_all)
+ end
+
def test_data_type_of_array_types
assert_equal :integer, @first_array.column_for_attribute(:commission_by_quarter).type
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
@@ -120,20 +240,216 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase
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_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_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_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_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_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_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_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_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')
+ @first_range.tstz_range = new_tstzrange
+ assert @first_range.save
+ assert @first_range.reload
+ assert_equal new_tstzrange, @first_range.tstz_range
+ @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_nil @first_range.tstz_range
+ 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)
+ @first_range.ts_range = new_tsrange
+ assert @first_range.save
+ assert @first_range.reload
+ assert_equal new_tsrange, @first_range.ts_range
+ @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_nil @first_range.ts_range
+ 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')
+ @first_range.num_range = new_numrange
+ assert @first_range.save
+ assert @first_range.reload
+ assert_equal new_numrange, @first_range.num_range
+ @first_range.num_range = BigDecimal.new('0.5')...BigDecimal.new('0.5')
+ assert @first_range.save
+ assert @first_range.reload
+ assert_nil @first_range.num_range
+ 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)
+ @first_range.date_range = new_daterange
+ assert @first_range.save
+ assert @first_range.reload
+ assert_equal new_daterange, @first_range.date_range
+ @first_range.date_range = Date.new(2012, 2, 3)...Date.new(2012, 2, 3)
+ assert @first_range.save
+ assert @first_range.reload
+ assert_nil @first_range.date_range
+ 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
+ @first_range.int4_range = new_int4range
+ assert @first_range.save
+ assert @first_range.reload
+ assert_equal new_int4range, @first_range.int4_range
+ @first_range.int4_range = 3...3
+ assert @first_range.save
+ assert @first_range.reload
+ assert_nil @first_range.int4_range
+ 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
+ @first_range.int8_range = new_int8range
+ assert @first_range.save
+ assert @first_range.reload
+ assert_equal new_int8range, @first_range.int8_range
+ @first_range.int8_range = 39999...39999
+ assert @first_range.save
+ assert @first_range.reload
+ assert_nil @first_range.int8_range
+ 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
@@ -171,31 +487,31 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase
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
@@ -204,28 +520,28 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase
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
@@ -235,54 +551,66 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase
def test_update_bit_string
new_bit_string = '11111111'
- new_bit_string_varying = 'FF'
- assert @first_bit_string.bit_string = new_bit_string
- assert @first_bit_string.bit_string_varying = new_bit_string_varying
+ new_bit_string_varying = '0xFF'
+ @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
+ with_timezone_config default: :utc, aware_attributes: true do
+ @connection.reconnect!
- ActiveRecord::Base.time_zone_aware_attributes = true
- ActiveRecord::Base.default_timezone = :utc
-
- @connection.reconnect!
-
- @first_timestamp_with_zone = PostgresqlTimestampWithZone.find(1)
- assert_equal Time.utc(2010,1,1, 11,0,0), @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
+ 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/explain_test.rb b/activerecord/test/cases/adapters/postgresql/explain_test.rb
index 619d581d5f..0b61f61572 100644
--- a/activerecord/test/cases/adapters/postgresql/explain_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/explain_test.rb
@@ -22,13 +22,6 @@ module ActiveRecord
assert_match %(EXPLAIN for: SELECT "audit_logs".* FROM "audit_logs" WHERE "audit_logs"."developer_id" IN (1)), explain
assert_match %(Seq Scan on audit_logs), explain
end
-
- def test_dont_explain_for_set_search_path
- queries = Thread.current[:available_queries_for_explain] = []
- ActiveRecord::Base.connection.schema_search_path = "public"
- assert queries.empty?
- end
-
end
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/hstore_test.rb b/activerecord/test/cases/adapters/postgresql/hstore_test.rb
index 23bafde17b..de724486c2 100644
--- a/activerecord/test/cases/adapters/postgresql/hstore_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/hstore_test.rb
@@ -7,19 +7,30 @@ 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
- begin
- @connection.transaction do
- @connection.create_table('hstores') do |t|
- t.hstore 'tags', :default => ''
- end
- end
- rescue ActiveRecord::StatementInvalid
+
+ 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
+ end
+
+ @connection.reconnect!
+
+ @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' }
end
@@ -27,10 +38,48 @@ 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
+
+ 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_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
+ end
+
+ 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
+
def test_type_cast_hstore
assert @column
@@ -44,6 +93,24 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase
assert_equal({'c'=>'}','"a"'=>'b "a b'}, @column.type_cast(%q(c=>"}", "\"a\""=>"b \"a b")))
end
+ def test_with_store_accessors
+ x = Hstore.new(language: "fr", timezone: "GMT")
+ assert_equal "fr", x.language
+ assert_equal "GMT", x.timezone
+
+ x.save!
+ x = Hstore.first
+ assert_equal "fr", x.language
+ assert_equal "GMT", x.timezone
+
+ x.language = "de"
+ x.save!
+
+ x = Hstore.first
+ assert_equal "de", x.language
+ assert_equal "GMT", x.timezone
+ end
+
def test_gen1
assert_equal(%q(" "=>""), @column.class.hstore_to_string({' '=>''}))
end
@@ -140,6 +207,10 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase
assert_cycle('ca' => 'cà', 'ac' => 'àc')
end
+ def test_multiline
+ assert_cycle("a\nb" => "c\nd")
+ end
+
private
def assert_cycle hash
# test creation
diff --git a/activerecord/test/cases/adapters/postgresql/json_test.rb b/activerecord/test/cases/adapters/postgresql/json_test.rb
index d64037eec0..c33c7ef968 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
@@ -31,6 +34,28 @@ class PostgresqlJSONTest < ActiveRecord::TestCase
assert_equal :json, @column.type
end
+ def test_change_table_supports_json
+ @connection.transaction do
+ @connection.change_table('json_data_type') do |t|
+ t.json 'users', default: '{}'
+ end
+ JsonDataType.reset_column_information
+ column = JsonDataType.columns.find { |c| c.name == 'users' }
+ assert_equal :json, column.type
+
+ raise ActiveRecord::Rollback # reset the schema change
+ end
+ ensure
+ 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
@@ -68,4 +93,32 @@ class PostgresqlJSONTest < ActiveRecord::TestCase
x = JsonDataType.first
assert_equal(nil, x.payload)
end
+
+ def test_select_array_json_value
+ @connection.execute %q|insert into json_data_type (payload) VALUES ('["v0",{"k1":"v1"}]')|
+ x = JsonDataType.first
+ assert_equal(['v0', {'k1' => 'v1'}], x.payload)
+ end
+
+ def test_rewrite_array_json_value
+ @connection.execute %q|insert into json_data_type (payload) VALUES ('["v0",{"k1":"v1"}]')|
+ x = JsonDataType.first
+ x.payload = ['v1', {'k2' => 'v2'}, 'v3']
+ assert x.save!
+ end
+
+ def test_with_store_accessors
+ x = JsonDataType.new(resolution: "320×480")
+ assert_equal "320×480", x.resolution
+
+ x.save!
+ x = JsonDataType.first
+ assert_equal "320×480", x.resolution
+
+ x.resolution = "640×1136"
+ x.save!
+
+ x = JsonDataType.first
+ assert_equal "640×1136", x.resolution
+ end
end
diff --git a/activerecord/test/cases/adapters/postgresql/ltree_test.rb b/activerecord/test/cases/adapters/postgresql/ltree_test.rb
new file mode 100644
index 0000000000..5d12ca75ca
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/ltree_test.rb
@@ -0,0 +1,41 @@
+# encoding: utf-8
+require "cases/helper"
+require 'active_record/base'
+require 'active_record/connection_adapters/postgresql_adapter'
+
+class PostgresqlLtreeTest < ActiveRecord::TestCase
+ class Ltree < ActiveRecord::Base
+ self.table_name = 'ltrees'
+ end
+
+ def setup
+ @connection = ActiveRecord::Base.connection
+ @connection.transaction do
+ @connection.create_table('ltrees') do |t|
+ t.ltree 'path'
+ end
+ end
+ rescue ActiveRecord::StatementInvalid
+ skip "do not test on PG without ltree"
+ end
+
+ def teardown
+ @connection.execute 'drop table if exists ltrees'
+ end
+
+ def test_column
+ column = Ltree.columns_hash['path']
+ assert_equal :ltree, column.type
+ end
+
+ def test_write
+ ltree = Ltree.new(path: '1.2.3.4')
+ assert ltree.save!
+ end
+
+ def test_select
+ @connection.execute "insert into ltrees (path) VALUES ('1.2.3')"
+ ltree = Ltree.first
+ assert_equal '1.2.3', ltree.path
+ 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 f1362dd15f..8b017760b1 100644
--- a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb
@@ -10,6 +10,15 @@ module ActiveRecord
@connection.exec_query('create table ex(id serial primary key, number integer, data character varying(255))')
end
+ def test_valid_column
+ column = @connection.columns('ex').find { |col| col.name == 'id' }
+ assert @connection.valid_type?(column.type)
+ end
+
+ def test_invalid_column
+ assert_not @connection.valid_type?(:foobar)
+ end
+
def test_primary_key
assert_equal 'id', @connection.primary_key('ex')
end
@@ -216,9 +225,44 @@ module ActiveRecord
assert_equal "(number > 100)", index.where
end
- def test_distinct_with_nulls
- 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"])
+ def test_columns_for_distinct_zero_orders
+ assert_equal "posts.id",
+ @connection.columns_for_distinct("posts.id", [])
+ 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_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_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_columns_for_distinct_with_arel_order
+ order = Object.new
+ def order.to_sql
+ "posts.created_at desc"
+ end
+ assert_equal "posts.id, posts.created_at AS alias_0",
+ @connection.columns_for_distinct("posts.id", [order])
+ end
+
+ def test_columns_for_distinct_with_nulls
+ assert_equal "posts.title, posts.updater_id AS alias_0", @connection.columns_for_distinct("posts.title", ["posts.updater_id desc nulls first"])
+ assert_equal "posts.title, posts.updater_id AS alias_0", @connection.columns_for_distinct("posts.title", ["posts.updater_id desc nulls last"])
+ end
+
+ def test_raise_error_when_cannot_translate_exception
+ assert_raise TypeError do
+ @connection.send(:log, nil) { @connection.execute(nil) }
+ end
end
private
diff --git a/activerecord/test/cases/adapters/postgresql/quoting_test.rb b/activerecord/test/cases/adapters/postgresql/quoting_test.rb
index 685f0ea74f..1122f8b9a1 100644
--- a/activerecord/test/cases/adapters/postgresql/quoting_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/quoting_test.rb
@@ -44,6 +44,19 @@ module ActiveRecord
c = Column.new(nil, 1, 'float')
assert_equal "'Infinity'", @conn.quote(infinity, c)
end
+
+ def test_quote_cast_numeric
+ fixnum = 666
+ 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/schema_test.rb b/activerecord/test/cases/adapters/postgresql/schema_test.rb
index cd31900d4e..e8dd188ec8 100644
--- a/activerecord/test/cases/adapters/postgresql/schema_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/schema_test.rb
@@ -11,16 +11,19 @@ class SchemaTest < ActiveRecord::TestCase
INDEX_B_NAME = 'b_index_things_on_different_columns_in_each_schema'
INDEX_C_NAME = 'c_index_full_text_search'
INDEX_D_NAME = 'd_index_things_on_description_desc'
+ INDEX_E_NAME = 'e_index_things_on_name_vector'
INDEX_A_COLUMN = 'name'
INDEX_B_COLUMN_S1 = 'email'
INDEX_B_COLUMN_S2 = 'moment'
INDEX_C_COLUMN = %q{(to_tsvector('english', coalesce(things.name, '')))}
INDEX_D_COLUMN = 'description'
+ INDEX_E_COLUMN = 'name_vector'
COLUMNS = [
'id integer',
'name character varying(50)',
'email character varying(50)',
'description character varying(100)',
+ 'name_vector tsvector',
'moment timestamp without time zone default now()'
]
PK_TABLE_NAME = 'table_with_pk'
@@ -61,6 +64,8 @@ class SchemaTest < ActiveRecord::TestCase
@connection.execute "CREATE INDEX #{INDEX_C_NAME} ON #{SCHEMA2_NAME}.#{TABLE_NAME} USING gin (#{INDEX_C_COLUMN});"
@connection.execute "CREATE INDEX #{INDEX_D_NAME} ON #{SCHEMA_NAME}.#{TABLE_NAME} USING btree (#{INDEX_D_COLUMN} DESC);"
@connection.execute "CREATE INDEX #{INDEX_D_NAME} ON #{SCHEMA2_NAME}.#{TABLE_NAME} USING btree (#{INDEX_D_COLUMN} DESC);"
+ @connection.execute "CREATE INDEX #{INDEX_E_NAME} ON #{SCHEMA_NAME}.#{TABLE_NAME} USING gin (#{INDEX_E_COLUMN});"
+ @connection.execute "CREATE INDEX #{INDEX_E_NAME} ON #{SCHEMA2_NAME}.#{TABLE_NAME} USING gin (#{INDEX_E_COLUMN});"
@connection.execute "CREATE TABLE #{SCHEMA_NAME}.#{PK_TABLE_NAME} (id serial primary key)"
@connection.execute "CREATE SEQUENCE #{SCHEMA_NAME}.#{UNMATCHED_SEQUENCE_NAME}"
@connection.execute "CREATE TABLE #{SCHEMA_NAME}.#{UNMATCHED_PK_TABLE_NAME} (id integer NOT NULL DEFAULT nextval('#{SCHEMA_NAME}.#{UNMATCHED_SEQUENCE_NAME}'::regclass), CONSTRAINT unmatched_pkey PRIMARY KEY (id))"
@@ -236,15 +241,15 @@ class SchemaTest < ActiveRecord::TestCase
end
def test_dump_indexes_for_schema_one
- do_dump_index_tests_for_schema(SCHEMA_NAME, INDEX_A_COLUMN, INDEX_B_COLUMN_S1, INDEX_D_COLUMN)
+ do_dump_index_tests_for_schema(SCHEMA_NAME, INDEX_A_COLUMN, INDEX_B_COLUMN_S1, INDEX_D_COLUMN, INDEX_E_COLUMN)
end
def test_dump_indexes_for_schema_two
- do_dump_index_tests_for_schema(SCHEMA2_NAME, INDEX_A_COLUMN, INDEX_B_COLUMN_S2, INDEX_D_COLUMN)
+ do_dump_index_tests_for_schema(SCHEMA2_NAME, INDEX_A_COLUMN, INDEX_B_COLUMN_S2, INDEX_D_COLUMN, INDEX_E_COLUMN)
end
def test_dump_indexes_for_schema_multiple_schemas_in_search_path
- do_dump_index_tests_for_schema("public, #{SCHEMA_NAME}", INDEX_A_COLUMN, INDEX_B_COLUMN_S1, INDEX_D_COLUMN)
+ do_dump_index_tests_for_schema("public, #{SCHEMA_NAME}", INDEX_A_COLUMN, INDEX_B_COLUMN_S1, INDEX_D_COLUMN, INDEX_E_COLUMN)
end
def test_with_uppercase_index_name
@@ -344,15 +349,20 @@ class SchemaTest < ActiveRecord::TestCase
@connection.schema_search_path = "'$user', public"
end
- def do_dump_index_tests_for_schema(this_schema_name, first_index_column_name, second_index_column_name, third_index_column_name)
+ def do_dump_index_tests_for_schema(this_schema_name, first_index_column_name, second_index_column_name, third_index_column_name, fourth_index_column_name)
with_schema_search_path(this_schema_name) do
indexes = @connection.indexes(TABLE_NAME).sort_by {|i| i.name}
- assert_equal 3,indexes.size
+ assert_equal 4,indexes.size
do_dump_index_assertions_for_one_index(indexes[0], INDEX_A_NAME, first_index_column_name)
do_dump_index_assertions_for_one_index(indexes[1], INDEX_B_NAME, second_index_column_name)
do_dump_index_assertions_for_one_index(indexes[2], INDEX_D_NAME, third_index_column_name)
+ do_dump_index_assertions_for_one_index(indexes[3], INDEX_E_NAME, fourth_index_column_name)
+ indexes.select{|i| i.name != INDEX_E_NAME}.each do |index|
+ assert_equal :btree, index.using
+ end
+ assert_equal :gin, indexes.select{|i| i.name == INDEX_E_NAME}[0].using
assert_equal :desc, indexes.select{|i| i.name == INDEX_D_NAME}[0].orders[INDEX_D_COLUMN]
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/sql_types_test.rb b/activerecord/test/cases/adapters/postgresql/sql_types_test.rb
new file mode 100644
index 0000000000..d7d40f6385
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/sql_types_test.rb
@@ -0,0 +1,18 @@
+require "cases/helper"
+
+class SqlTypesTest < ActiveRecord::TestCase
+ def test_binary_types
+ assert_equal 'bytea', type_to_sql(:binary, 100_000)
+ assert_raise ActiveRecord::ActiveRecordError do
+ type_to_sql :binary, 4294967295
+ end
+ assert_equal 'text', type_to_sql(:text, 100_000)
+ assert_raise ActiveRecord::ActiveRecordError do
+ type_to_sql :text, 4294967295
+ end
+ end
+
+ def type_to_sql(*args)
+ ActiveRecord::Base.connection.type_to_sql(*args)
+ end
+end
diff --git a/activerecord/test/cases/adapters/postgresql/statement_pool_test.rb b/activerecord/test/cases/adapters/postgresql/statement_pool_test.rb
index f1c4b85126..c5fd40accc 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
+ def test_cache_is_per_pid
+ return skip('must support fork') unless Process.respond_to?(:fork)
- 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
- 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 26507ad654..dbc69a529c 100644
--- a/activerecord/test/cases/adapters/postgresql/timestamp_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/timestamp_test.rb
@@ -1,7 +1,16 @@
require 'cases/helper'
require 'models/developer'
+require 'models/topic'
class TimestampTest < ActiveRecord::TestCase
+ fixtures :topics
+
+ def test_group_by_date
+ keys = Topic.group("date_trunc('month', created_at)").count.keys
+ assert_operator keys.length, :>, 0
+ keys.each { |k| assert_kind_of Time, k }
+ end
+
def test_load_infinity_and_beyond
unless current_adapter?(:PostgreSQLAdapter)
return skip("only tested on postgresql")
@@ -75,6 +84,15 @@ class TimestampTest < ActiveRecord::TestCase
assert_equal '4', pg_datetime_precision('foos', 'updated_at')
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
+ end
+
private
def pg_datetime_precision(table_name, column_name)
diff --git a/activerecord/test/cases/adapters/postgresql/uuid_test.rb b/activerecord/test/cases/adapters/postgresql/uuid_test.rb
new file mode 100644
index 0000000000..a753a23c09
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/uuid_test.rb
@@ -0,0 +1,136 @@
+# encoding: utf-8
+
+require "cases/helper"
+require 'active_record/base'
+require 'active_record/connection_adapters/postgresql_adapter'
+
+class PostgresqlUUIDTest < ActiveRecord::TestCase
+ class UUID < ActiveRecord::Base
+ self.table_name = 'pg_uuids'
+ end
+
+ 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
+ end
+
+ @connection.reconnect!
+
+ @connection.transaction do
+ @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
+ end
+ end
+
+ def teardown
+ @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
+
+ 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_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, default: "uuid_generate_v1\(\)"/, schema.string)
+ assert_match(/t\.uuid "other_uuid", default: "uuid_generate_v4\(\)"/, schema.string)
+ end
+end
+
+class PostgresqlUUIDTestNilDefault < ActiveRecord::TestCase
+ class UUID < ActiveRecord::Base
+ self.table_name = 'pg_uuids'
+ end
+
+ def setup
+ @connection = ActiveRecord::Base.connection
+ @connection.reconnect!
+
+ @connection.transaction do
+ @connection.create_table('pg_uuids', id: false) do |t|
+ t.primary_key :id, :uuid, default: nil
+ t.string 'name'
+ end
+ end
+ end
+
+ def teardown
+ @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
+ FROM pg_attribute a
+ LEFT JOIN pg_attrdef d ON a.attrelid = d.adrelid AND a.attnum = d.adnum
+ WHERE a.attname='id' AND a.attrelid = 'pg_uuids'::regclass").first
+ assert_nil col_desc["default"]
+ end
+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!
+
+ @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
+
+ def test_collection_association_with_uuid
+ post = UuidPost.create!
+ comment = post.uuid_comments.create!
+ assert post.uuid_comments.find(comment.id)
+ 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/copy_table_test.rb b/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb
index 74288a98d1..e78cb88562 100644
--- a/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb
+++ b/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb
@@ -1,7 +1,7 @@
require "cases/helper"
class CopyTableTest < ActiveRecord::TestCase
- fixtures :customers, :companies, :comments
+ fixtures :customers, :companies, :comments, :binaries
def setup
@connection = ActiveRecord::Base.connection
@@ -32,6 +32,11 @@ class CopyTableTest < ActiveRecord::TestCase
end
end
+ def test_copy_table_allows_to_pass_options_to_create_table
+ @connection.create_table('blocker_table')
+ test_copy_table('customers', 'blocker_table', force: true)
+ end
+
def test_copy_table_with_index
test_copy_table('comments', 'comments_with_index') do
@connection.add_index('comments_with_index', ['post_id', 'type'])
@@ -43,11 +48,13 @@ class CopyTableTest < ActiveRecord::TestCase
end
def test_copy_table_without_primary_key
- test_copy_table('developers_projects', 'programmers_projects')
+ test_copy_table('developers_projects', 'programmers_projects') do
+ assert_nil @connection.primary_key('programmers_projects')
+ end
end
def test_copy_table_with_id_col_that_is_not_primary_key
- test_copy_table('goofy_string_id', 'goofy_string_id2') do |from, to, options|
+ test_copy_table('goofy_string_id', 'goofy_string_id2') do
original_id = @connection.columns('goofy_string_id').detect{|col| col.name == 'id' }
copied_id = @connection.columns('goofy_string_id2').detect{|col| col.name == 'id' }
assert_equal original_id.type, copied_id.type
@@ -58,13 +65,17 @@ class CopyTableTest < ActiveRecord::TestCase
end
def test_copy_table_with_unconventional_primary_key
- test_copy_table('owners', 'owners_unconventional') do |from, to, options|
+ test_copy_table('owners', 'owners_unconventional') do
original_pk = @connection.primary_key('owners')
copied_pk = @connection.primary_key('owners_unconventional')
assert_equal original_pk, copied_pk
end
end
+ def test_copy_table_with_binary_column
+ test_copy_table 'binaries', 'binaries2'
+ end
+
protected
def copy_table(from, to, options = {})
@connection.copy_table(from, to, {:temporary => true}.merge(options))
@@ -79,7 +90,7 @@ protected
end
def table_indexes_without_name(table)
- @connection.indexes('comments_with_index').delete(:name)
+ @connection.indexes(table).delete(:name)
end
def row_count(table)
diff --git a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb
index 2ba9143cd5..a7b2764fc1 100644
--- a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb
+++ b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb
@@ -17,7 +17,7 @@ module ActiveRecord
@conn.extend(Module.new { def logger; end })
column = Struct.new(:type, :name).new(:string, "foo")
binary = SecureRandom.hex
- expected = binary.dup.encode!('utf-8')
+ expected = binary.dup.encode!(Encoding::UTF_8)
assert_equal expected, @conn.type_cast(binary, column)
end
diff --git a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb
index 4e26c5dda1..ce7c869eec 100644
--- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb
+++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb
@@ -21,13 +21,26 @@ module ActiveRecord
)
eosql
- @conn.extend(LogIntercepter)
- @conn.intercepted = true
+ @subscriber = SQLSubscriber.new
+ ActiveSupport::Notifications.subscribe('sql.active_record', @subscriber)
+ end
+
+ def test_valid_column
+ column = @conn.columns('items').find { |col| col.name == 'id' }
+ assert @conn.valid_type?(column.type)
+ 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
+ # 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
@@ -154,6 +167,12 @@ module ActiveRecord
DualEncoding.connection.drop_table('dual_encodings')
end
+ def test_type_cast_should_not_mutate_encoding
+ name = 'hello'.force_encoding(Encoding::ASCII_8BIT)
+ Owner.create(name: name)
+ assert_equal Encoding::ASCII_8BIT, name.encoding
+ end
+
def test_execute
@conn.execute "INSERT INTO items (number) VALUES (10)"
records = @conn.execute "SELECT * FROM items"
@@ -237,7 +256,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
@@ -249,7 +268,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
@@ -287,10 +306,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
@@ -335,11 +354,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..5a4fe63580
--- /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.exists? dir.join('db')
+ assert File.exist? dir.join('db/foo.sqlite3')
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/aggregations_test.rb b/activerecord/test/cases/aggregations_test.rb
index 48b06a767f..5536702f58 100644
--- a/activerecord/test/cases/aggregations_test.rb
+++ b/activerecord/test/cases/aggregations_test.rb
@@ -1,6 +1,5 @@
require "cases/helper"
require 'models/customer'
-require 'active_support/core_ext/exception'
class AggregationsTest < ActiveRecord::TestCase
fixtures :customers
@@ -26,7 +25,7 @@ class AggregationsTest < ActiveRecord::TestCase
def test_immutable_value_objects
customers(:david).balance = Money.new(100)
- assert_raise(ActiveSupport::FrozenObjectError) { customers(:david).balance.instance_eval { @amount = 20 } }
+ assert_raise(RuntimeError) { customers(:david).balance.instance_eval { @amount = 20 } }
end
def test_inferred_mapping
@@ -142,7 +141,6 @@ class AggregationsTest < ActiveRecord::TestCase
end
class OverridingAggregationsTest < ActiveRecord::TestCase
- class Name; end
class DifferentName; end
class Person < ActiveRecord::Base
diff --git a/activerecord/test/cases/ar_schema_test.rb b/activerecord/test/cases/ar_schema_test.rb
index b2eac0349b..500df52cd8 100644
--- a/activerecord/test/cases/ar_schema_test.rb
+++ b/activerecord/test/cases/ar_schema_test.rb
@@ -7,10 +7,14 @@ if ActiveRecord::Base.connection.supports_migrations?
def setup
@connection = ActiveRecord::Base.connection
+ ActiveRecord::SchemaMigration.drop_table
end
def teardown
@connection.drop_table :fruits rescue nil
+ @connection.drop_table :nep_fruits rescue nil
+ @connection.drop_table :nep_schema_migrations rescue nil
+ ActiveRecord::SchemaMigration.delete_all rescue nil
end
def test_schema_define
@@ -28,6 +32,24 @@ if ActiveRecord::Base.connection.supports_migrations?
assert_equal 7, ActiveRecord::Migrator::current_version
end
+ def test_schema_define_w_table_name_prefix
+ table_name = ActiveRecord::SchemaMigration.table_name
+ ActiveRecord::Base.table_name_prefix = "nep_"
+ ActiveRecord::SchemaMigration.table_name = "nep_#{table_name}"
+ ActiveRecord::Schema.define(:version => 7) do
+ create_table :fruits do |t|
+ t.column :color, :string
+ t.column :fruit_size, :string # NOTE: "size" is reserved in Oracle
+ t.column :texture, :string
+ t.column :flavor, :string
+ end
+ end
+ assert_equal 7, ActiveRecord::Migrator::current_version
+ ensure
+ ActiveRecord::Base.table_name_prefix = ""
+ ActiveRecord::SchemaMigration.table_name = table_name
+ end
+
def test_schema_raises_an_error_for_invalid_column_type
assert_raise NoMethodError do
ActiveRecord::Schema.define(:version => 8) do
@@ -45,5 +67,4 @@ if ActiveRecord::Base.connection.supports_migrations?
assert_nothing_raised { @connection.select_all "SELECT * FROM fruits" }
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 5f7825783b..a79f145e31 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,
@@ -21,9 +23,9 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
:posts, :tags, :taggings, :comments, :sponsors, :members
def test_belongs_to
- Client.find(3).firm.name
- assert_equal companies(:first_firm).name, Client.find(3).firm.name
- assert_not_nil Client.find(3).firm, "Microsoft should have a firm"
+ firm = Client.find(3).firm
+ assert_not_nil firm
+ assert_equal companies(:first_firm).name, firm.name
end
def test_belongs_to_with_primary_key
@@ -33,7 +35,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
def test_belongs_to_with_primary_key_joins_on_correct_column
sql = Client.joins(:firm_with_primary_key).to_sql
- if current_adapter?(:MysqlAdapter) or current_adapter?(:Mysql2Adapter)
+ if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
assert_no_match(/`firm_with_primary_keys_companies`\.`id`/, sql)
assert_match(/`firm_with_primary_keys_companies`\.`name`/, sql)
elsif current_adapter?(:OracleAdapter)
@@ -63,6 +65,13 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
assert_equal apple.id, citibank.firm_id
end
+ def test_id_assignment
+ apple = Firm.create("name" => "Apple")
+ citibank = Account.create("credit_limit" => 10)
+ citibank.firm_id = apple
+ assert_nil citibank.firm_id
+ end
+
def test_natural_assignment_with_primary_key
apple = Firm.create("name" => "Apple")
citibank = Client.create("name" => "Primary key client")
@@ -109,6 +118,34 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
assert_equal apple.id, citibank.firm_id
end
+ def test_building_the_belonging_object_with_implicit_sti_base_class
+ account = Account.new
+ company = account.build_firm
+ assert_kind_of Company, company, "Expected #{company.class} to be a Company"
+ end
+
+ def test_building_the_belonging_object_with_explicit_sti_base_class
+ account = Account.new
+ company = account.build_firm(:type => "Company")
+ assert_kind_of Company, company, "Expected #{company.class} to be a Company"
+ end
+
+ def test_building_the_belonging_object_with_sti_subclass
+ account = Account.new
+ company = account.build_firm(:type => "Firm")
+ assert_kind_of Firm, company, "Expected #{company.class} to be a Firm"
+ end
+
+ def test_building_the_belonging_object_with_an_invalid_type
+ account = Account.new
+ assert_raise(ActiveRecord::SubclassNotFound) { account.build_firm(:type => "InvalidType") }
+ end
+
+ def test_building_the_belonging_object_with_an_unrelated_type
+ account = Account.new
+ assert_raise(ActiveRecord::SubclassNotFound) { account.build_firm(:type => "Account") }
+ end
+
def test_building_the_belonging_object_with_primary_key
client = Client.create(:name => "Primary key client")
apple = client.build_firm_with_primary_key("name" => "Apple")
@@ -197,15 +234,15 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
end
def test_belongs_to_counter_with_assigning_nil
- p = Post.find(1)
- c = Comment.find(1)
+ post = Post.find(1)
+ comment = Comment.find(1)
- assert_equal p.id, c.post_id
- assert_equal 2, Post.find(p.id).comments.size
+ assert_equal post.id, comment.post_id
+ assert_equal 2, Post.find(post.id).comments.size
- c.post = nil
+ comment.post = nil
- assert_equal 1, Post.find(p.id).comments.size
+ assert_equal 1, Post.find(post.id).comments.size
end
def test_belongs_to_with_primary_key_counter
@@ -228,56 +265,56 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
end
def test_belongs_to_counter_with_reassigning
- t1 = Topic.create("title" => "t1")
- t2 = Topic.create("title" => "t2")
- r1 = Reply.new("title" => "r1", "content" => "r1")
- r1.topic = t1
+ topic1 = Topic.create("title" => "t1")
+ topic2 = Topic.create("title" => "t2")
+ reply1 = Reply.new("title" => "r1", "content" => "r1")
+ reply1.topic = topic1
- assert r1.save
- assert_equal 1, Topic.find(t1.id).replies.size
- assert_equal 0, Topic.find(t2.id).replies.size
+ assert reply1.save
+ assert_equal 1, Topic.find(topic1.id).replies.size
+ assert_equal 0, Topic.find(topic2.id).replies.size
- r1.topic = Topic.find(t2.id)
+ reply1.topic = Topic.find(topic2.id)
assert_no_queries do
- r1.topic = t2
+ reply1.topic = topic2
end
- assert r1.save
- assert_equal 0, Topic.find(t1.id).replies.size
- assert_equal 1, Topic.find(t2.id).replies.size
+ assert reply1.save
+ assert_equal 0, Topic.find(topic1.id).replies.size
+ assert_equal 1, Topic.find(topic2.id).replies.size
- r1.topic = nil
+ reply1.topic = nil
- assert_equal 0, Topic.find(t1.id).replies.size
- assert_equal 0, Topic.find(t2.id).replies.size
+ assert_equal 0, Topic.find(topic1.id).replies.size
+ assert_equal 0, Topic.find(topic2.id).replies.size
- r1.topic = t1
+ reply1.topic = topic1
- assert_equal 1, Topic.find(t1.id).replies.size
- assert_equal 0, Topic.find(t2.id).replies.size
+ assert_equal 1, Topic.find(topic1.id).replies.size
+ assert_equal 0, Topic.find(topic2.id).replies.size
- r1.destroy
+ reply1.destroy
- assert_equal 0, Topic.find(t1.id).replies.size
- assert_equal 0, Topic.find(t2.id).replies.size
+ assert_equal 0, Topic.find(topic1.id).replies.size
+ assert_equal 0, Topic.find(topic2.id).replies.size
end
def test_belongs_to_reassign_with_namespaced_models_and_counters
- t1 = Web::Topic.create("title" => "t1")
- t2 = Web::Topic.create("title" => "t2")
- r1 = Web::Reply.new("title" => "r1", "content" => "r1")
- r1.topic = t1
+ topic1 = Web::Topic.create("title" => "t1")
+ topic2 = Web::Topic.create("title" => "t2")
+ reply1 = Web::Reply.new("title" => "r1", "content" => "r1")
+ reply1.topic = topic1
- assert r1.save
- assert_equal 1, Web::Topic.find(t1.id).replies.size
- assert_equal 0, Web::Topic.find(t2.id).replies.size
+ assert reply1.save
+ assert_equal 1, Web::Topic.find(topic1.id).replies.size
+ assert_equal 0, Web::Topic.find(topic2.id).replies.size
- r1.topic = Web::Topic.find(t2.id)
+ reply1.topic = Web::Topic.find(topic2.id)
- assert r1.save
- assert_equal 0, Web::Topic.find(t1.id).replies.size
- assert_equal 1, Web::Topic.find(t2.id).replies.size
+ assert reply1.save
+ assert_equal 0, Web::Topic.find(topic1.id).replies.size
+ assert_equal 1, Web::Topic.find(topic2.id).replies.size
end
def test_belongs_to_counter_after_save
@@ -289,12 +326,51 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
assert_equal 1, Topic.find(topic.id)[:replies_count]
end
- def test_belongs_to_counter_after_update_attributes
- topic = Topic.create!(:title => "37s")
- topic.replies.create!(:title => "re: 37s", :content => "rails")
+ 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_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")
assert_equal 1, Topic.find(topic.id)[:replies_count]
- topic.update_attributes(:title => "37signals")
+ topic.update(title: "37signals")
assert_equal 1, Topic.find(topic.id)[:replies_count]
end
@@ -303,7 +379,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
@@ -332,9 +408,9 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
end
def test_new_record_with_foreign_key_but_no_object
- c = Client.new("firm_id" => 1)
+ client = Client.new("firm_id" => 1)
# sometimes tests on Oracle fail if ORDER BY is not provided therefore add always :order with :first
- assert_equal Firm.all.merge!(:order => "id").first, c.firm_with_basic_id
+ assert_equal Firm.all.merge!(:order => "id").first, client.firm_with_basic_id
end
def test_setting_foreign_key_after_nil_target_loaded
@@ -356,8 +432,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
@@ -379,6 +454,26 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
assert_equal 15, topic.replies.size
end
+ def test_counter_cache_double_destroy
+ topic = Topic.create :title => "Zoom-zoom-zoom"
+
+ 5.times do
+ topic.replies.create(:title => "re: zoom", :content => "speedy quick!")
+ end
+
+ assert_equal 5, topic.reload[:replies_count]
+ assert_equal 5, topic.replies.size
+
+ reply = topic.replies.first
+
+ reply.destroy
+ assert_equal 4, topic.reload[:replies_count]
+
+ reply.destroy
+ assert_equal 4, topic.reload[:replies_count]
+ assert_equal 4, topic.replies.size
+ end
+
def test_custom_counter_cache
reply = Reply.create(:title => "re: zoom", :content => "speedy quick!")
assert_equal 0, reply[:replies_count]
@@ -510,6 +605,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
@@ -539,6 +636,11 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
assert_equal new_firm.name, "Apple"
end
+ def test_attributes_are_set_without_error_when_initialized_from_belongs_to_association_with_array_in_where_clause
+ new_account = Account.where(:credit_limit => [ 50, 60 ]).new
+ assert_nil new_account.credit_limit
+ end
+
def test_reassigning_the_parent_id_updates_the_object
client = companies(:second_client)
diff --git a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb
index 80bca7f63e..811d91f849 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.count(:distinct => true)
- 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
diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb
index 124bf65d3a..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
@@ -73,6 +74,11 @@ class EagerAssociationTest < ActiveRecord::TestCase
end
end
+ def test_has_many_through_with_order
+ authors = Author.includes(:favorite_authors).to_a
+ assert_no_queries { authors.map(&:favorite_authors) }
+ end
+
def test_with_two_tables_in_from_without_getting_double_quoted
posts = Post.select("posts.*").from("authors, posts").eager_load(:comments).where("posts.author_id = authors.id").order("posts.id").to_a
assert_equal 2, posts.first.comments.size
@@ -93,31 +99,31 @@ class EagerAssociationTest < ActiveRecord::TestCase
end
def test_preloading_has_many_in_multiple_queries_with_more_ids_than_database_can_handle
- Post.connection.expects(:in_clause_length).at_least_once.returns(5)
+ Comment.connection.expects(:in_clause_length).at_least_once.returns(5)
posts = Post.all.merge!(:includes=>:comments).to_a
assert_equal 11, posts.size
end
def test_preloading_has_many_in_one_queries_when_database_has_no_limit_on_ids_it_can_handle
- Post.connection.expects(:in_clause_length).at_least_once.returns(nil)
+ Comment.connection.expects(:in_clause_length).at_least_once.returns(nil)
posts = Post.all.merge!(:includes=>:comments).to_a
assert_equal 11, posts.size
end
def test_preloading_habtm_in_multiple_queries_with_more_ids_than_database_can_handle
- Post.connection.expects(:in_clause_length).at_least_once.returns(5)
+ Comment.connection.expects(:in_clause_length).at_least_once.returns(5)
posts = Post.all.merge!(:includes=>:categories).to_a
assert_equal 11, posts.size
end
def test_preloading_habtm_in_one_queries_when_database_has_no_limit_on_ids_it_can_handle
- Post.connection.expects(:in_clause_length).at_least_once.returns(nil)
+ Comment.connection.expects(:in_clause_length).at_least_once.returns(nil)
posts = Post.all.merge!(:includes=>:categories).to_a
assert_equal 11, posts.size
end
def test_load_associated_records_in_one_query_when_adapter_has_no_limit
- Post.connection.expects(:in_clause_length).at_least_once.returns(nil)
+ Comment.connection.expects(:in_clause_length).at_least_once.returns(nil)
post = posts(:welcome)
assert_queries(2) do
@@ -126,7 +132,7 @@ class EagerAssociationTest < ActiveRecord::TestCase
end
def test_load_associated_records_in_several_queries_when_many_ids_passed
- Post.connection.expects(:in_clause_length).at_least_once.returns(1)
+ Comment.connection.expects(:in_clause_length).at_least_once.returns(1)
post1, post2 = posts(:welcome), posts(:thinking)
assert_queries(3) do
@@ -135,7 +141,7 @@ class EagerAssociationTest < ActiveRecord::TestCase
end
def test_load_associated_records_in_one_query_when_a_few_ids_passed
- Post.connection.expects(:in_clause_length).at_least_once.returns(3)
+ Comment.connection.expects(:in_clause_length).at_least_once.returns(3)
post = posts(:welcome)
assert_queries(2) do
@@ -188,7 +194,7 @@ class EagerAssociationTest < ActiveRecord::TestCase
end
end
- def test_finding_with_includes_on_has_one_assocation_with_same_include_includes_only_once
+ def test_finding_with_includes_on_has_one_association_with_same_include_includes_only_once
author = authors(:david)
post = author.post_about_thinking_with_last_comment
last_comment = post.last_comment
@@ -212,8 +218,9 @@ class EagerAssociationTest < ActiveRecord::TestCase
def test_finding_with_includes_on_null_belongs_to_association_with_same_include_includes_only_once
post = posts(:welcome)
- post.update_attributes!(:author => nil)
- post = assert_queries(1) { Post.all.merge!(:includes => {:author_with_address => :author_address}).find(post.id) } # find the post, then find the author which is null so no query for the author or address
+ post.update!(author: nil)
+ post = assert_queries(1) { Post.all.merge!(includes: {author_with_address: :author_address}).find(post.id) }
+ # find the post, then find the author which is null so no query for the author or address
assert_no_queries do
assert_equal nil, post.author_with_address
end
@@ -221,7 +228,7 @@ class EagerAssociationTest < ActiveRecord::TestCase
def test_finding_with_includes_on_null_belongs_to_polymorphic_association
sponsor = sponsors(:moustache_club_sponsor_for_groucho)
- sponsor.update_attributes!(:sponsorable => nil)
+ sponsor.update!(sponsorable: nil)
sponsor = assert_queries(1) { Sponsor.all.merge!(:includes => :sponsorable).find(sponsor.id) }
assert_no_queries do
assert_equal nil, sponsor.sponsorable
@@ -244,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
@@ -296,7 +304,7 @@ class EagerAssociationTest < ActiveRecord::TestCase
def test_eager_association_loading_with_belongs_to_and_foreign_keys
pets = Pet.all.merge!(:includes => :owner).to_a
- assert_equal 3, pets.length
+ assert_equal 4, pets.length
end
def test_eager_association_loading_with_belongs_to
@@ -339,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
@@ -360,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
@@ -375,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
@@ -461,7 +463,7 @@ class EagerAssociationTest < ActiveRecord::TestCase
posts_with_comments = people(:michael).posts.merge(:includes => :comments, :order => 'posts.id').to_a
posts_with_author = people(:michael).posts.merge(:includes => :author, :order => 'posts.id').to_a
posts_with_comments_and_author = people(:michael).posts.merge(:includes => [ :comments, :author ], :order => 'posts.id').to_a
- assert_equal 2, posts_with_comments.inject(0) { |sum, post| sum += post.comments.size }
+ assert_equal 2, posts_with_comments.inject(0) { |sum, post| sum + post.comments.size }
assert_equal authors(:david), assert_no_queries { posts_with_author.first.author }
assert_equal authors(:david), assert_no_queries { posts_with_comments_and_author.first.author }
end
@@ -517,7 +519,7 @@ class EagerAssociationTest < ActiveRecord::TestCase
def test_eager_with_has_many_and_limit
posts = Post.all.merge!(:order => 'posts.id asc', :includes => [ :author, :comments ], :limit => 2).to_a
assert_equal 2, posts.size
- assert_equal 3, posts.inject(0) { |sum, post| sum += post.comments.size }
+ assert_equal 3, posts.inject(0) { |sum, post| sum + post.comments.size }
end
def test_eager_with_has_many_and_limit_and_conditions
@@ -541,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
@@ -616,8 +614,8 @@ class EagerAssociationTest < ActiveRecord::TestCase
general = categories.find { |c| c == categories(:general) }
technology = categories.find { |c| c == categories(:technology) }
- post1 = general.posts.to_a.find { |p| p == posts(:welcome) }
- post2 = technology.posts.to_a.find { |p| p == posts(:welcome) }
+ post1 = general.posts.to_a.find { |p| p == welcome }
+ post2 = technology.posts.to_a.find { |p| p == welcome }
assert_equal post1.object_id, post2.object_id
end
@@ -750,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
@@ -944,6 +944,12 @@ class EagerAssociationTest < ActiveRecord::TestCase
assert_equal 3, Developer.all.merge!(:includes => 'projects', :where => { 'developers_projects.access_level' => 1 }, :limit => 5).to_a.size
end
+ def test_dont_create_temporary_active_record_instances
+ Developer.instance_count = 0
+ developers = Developer.all.merge!(:includes => 'projects', :where => { 'developers_projects.access_level' => 1 }, :limit => 5).to_a
+ assert_equal developers.count, Developer.instance_count
+ end
+
def test_order_on_join_table_with_include_and_limit
assert_equal 5, Developer.all.merge!(:includes => 'projects', :order => 'developers_projects.joined_on DESC', :limit => 5).to_a.size
end
@@ -984,10 +990,10 @@ class EagerAssociationTest < ActiveRecord::TestCase
post = Post.create!(:title => 'Beaches', :body => "I like beaches!")
Reader.create! :person => people(:david), :post => post
LazyReader.create! :person => people(:susan), :post => post
-
+
assert_equal 1, post.lazy_readers.to_a.size
assert_equal 2, post.lazy_readers_skimmers_or_not.to_a.size
-
+
post_with_readers = Post.includes(:lazy_readers_skimmers_or_not).find(post.id)
assert_equal 2, post_with_readers.lazy_readers_skimmers_or_not.to_a.size
end
@@ -1133,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 }
@@ -1155,4 +1165,33 @@ class EagerAssociationTest < ActiveRecord::TestCase
Post.where('1 = 0').scoping { Comment.preload(:post).find(1).post }
)
end
+
+ test "preloading does not cache has many association subset when preloaded with a through association" do
+ author = Author.includes(:comments_with_order_and_conditions, :posts).first
+ assert_no_queries { assert_equal 2, author.comments_with_order_and_conditions.size }
+ assert_no_queries { assert_equal 5, author.posts.size, "should not cache a subset of the association" }
+ end
+
+ test "preloading a through association twice does not reset it" do
+ members = Member.includes(current_membership: :club).includes(:club).to_a
+ assert_no_queries {
+ assert_equal 3, members.map(&:current_membership).map(&:club).size
+ }
+ end
+
+ 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..f8f2832ab1 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,7 @@ class AssociationsExtensionsTest < ActiveRecord::TestCase
private
- def extension_name(model)
- builder = ActiveRecord::Associations::Builder::HasMany.new(model, :association_name, nil, {}) { }
- builder.send(:wrap_block_extension)
- builder.extension_module.name
+ def extend!(model)
+ ActiveRecord::Associations::Builder::HasMany.define_extensions(model, :association_name) { }
end
end
diff --git a/activerecord/test/cases/associations/habtm_join_table_test.rb b/activerecord/test/cases/associations/habtm_join_table_test.rb
deleted file mode 100644
index fe2b82f2c1..0000000000
--- a/activerecord/test/cases/associations/habtm_join_table_test.rb
+++ /dev/null
@@ -1,35 +0,0 @@
-require 'cases/helper'
-
-class MyReader < ActiveRecord::Base
- has_and_belongs_to_many :my_books
-end
-
-class MyBook < ActiveRecord::Base
- has_and_belongs_to_many :my_readers
-end
-
-class HabtmJoinTableTest < ActiveRecord::TestCase
- def setup
- ActiveRecord::Base.connection.create_table :my_books, :force => true do |t|
- t.string :name
- end
- assert ActiveRecord::Base.connection.table_exists?(:my_books)
-
- ActiveRecord::Base.connection.create_table :my_readers, :force => true do |t|
- t.string :name
- end
- assert ActiveRecord::Base.connection.table_exists?(:my_readers)
-
- ActiveRecord::Base.connection.create_table :my_books_my_readers, :force => true do |t|
- t.integer :my_book_id
- t.integer :my_reader_id
- end
- assert ActiveRecord::Base.connection.table_exists?(:my_books_my_readers)
- end
-
- def teardown
- ActiveRecord::Base.connection.drop_table :my_books
- ActiveRecord::Base.connection.drop_table :my_readers
- ActiveRecord::Base.connection.drop_table :my_books_my_readers
- 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 42f5b69d4e..be928ec8ee 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
@@ -316,7 +303,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
dev.projects << projects(:active_record)
assert_equal 3, dev.projects.size
- assert_equal 1, dev.projects.uniq.size
+ assert_equal 1, dev.projects.distinct.size
end
def test_uniq_before_the_fact
@@ -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
@@ -669,16 +605,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 +632,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 +646,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 +718,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,27 +729,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
-
- def test_counting_should_not_fire_sql_if_parent_is_unsaved
- assert_no_queries do
- assert_equal 0, Developer.new.projects.count
- end
- 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
@@ -851,15 +768,14 @@ 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' }
+ test "has and belongs to many associations on new records use null relations" do
+ projects = Developer.new.projects
+ assert_no_queries do
+ assert_equal [], projects
+ assert_equal [], projects.where(title: 'omg')
+ assert_equal [], projects.pluck(:title)
+ assert_equal 0, projects.count
+ end
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
end
diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb
index 50c23c863f..dfc8a68e8c 100644
--- a/activerecord/test/cases/associations/has_many_associations_test.rb
+++ b/activerecord/test/cases/associations/has_many_associations_test.rb
@@ -19,79 +19,9 @@ require 'models/line_item'
require 'models/car'
require 'models/bulb'
require 'models/engine'
-
-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
+require 'models/categorization'
+require 'models/minivan'
+require 'models/speedometer'
class HasManyAssociationsTestForReorderWithJoinDependency < ActiveRecord::TestCase
fixtures :authors, :posts, :comments
@@ -108,12 +38,31 @@ end
class HasManyAssociationsTest < ActiveRecord::TestCase
fixtures :accounts, :categories, :companies, :developers, :projects,
:developers_projects, :topics, :authors, :comments,
- :people, :posts, :readers, :taggings, :cars, :essays
+ :people, :posts, :readers, :taggings, :cars, :essays,
+ :categorizations
def setup
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
@@ -131,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')
@@ -144,6 +100,51 @@ 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 strategey"
+ end
+
+ def test_building_the_associated_object_with_implicit_sti_base_class
+ firm = DependentFirm.new
+ company = firm.companies.build
+ assert_kind_of Company, company, "Expected #{company.class} to be a Company"
+ end
+
+ def test_building_the_associated_object_with_explicit_sti_base_class
+ firm = DependentFirm.new
+ company = firm.companies.build(:type => "Company")
+ assert_kind_of Company, company, "Expected #{company.class} to be a Company"
+ end
+
+ def test_building_the_associated_object_with_sti_subclass
+ firm = DependentFirm.new
+ company = firm.companies.build(:type => "Client")
+ assert_kind_of Client, company, "Expected #{company.class} to be a Client"
+ end
+
+ def test_building_the_associated_object_with_an_invalid_type
+ firm = DependentFirm.new
+ assert_raise(ActiveRecord::SubclassNotFound) { firm.companies.build(:type => "Invalid") }
+ end
+
+ def test_building_the_associated_object_with_an_unrelated_type
+ firm = DependentFirm.new
+ 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')
@@ -262,12 +263,6 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal firm.limited_clients.length, firm.limited_clients.count
end
- def test_counting_should_not_fire_sql_if_parent_is_unsaved
- assert_no_queries do
- assert_equal 0, Person.new.readers.count
- end
- end
-
def test_finding
assert_equal 2, Firm.all.merge!(:order => "id").first.clients.length
end
@@ -276,21 +271,15 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal 2, Firm.order(:id).find{|f| f.id > 0}.clients.length
end
- def test_find_with_blank_conditions
- [[], {}, nil, ""].each do |blank|
- assert_equal 2, Firm.all.merge!(:order => "id").first.clients.where(blank).to_a.size
- end
- end
-
def test_find_many_with_merged_options
assert_equal 1, companies(:first_firm).limited_clients.size
assert_equal 1, companies(:first_firm).limited_clients.to_a.size
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
@@ -327,37 +316,6 @@ 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
@@ -385,20 +343,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
@@ -582,6 +536,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") }
@@ -609,6 +570,13 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal 3, company.clients_of_firm.size
end
+ def test_collection_not_empty_after_building
+ company = companies(:first_firm)
+ assert_predicate company.contracts, :empty?
+ company.contracts.build
+ assert_not_predicate company.contracts, :empty?
+ end
+
def test_collection_size_twice_for_regressions
post = posts(:thinking)
assert_equal 0, post.readers.size
@@ -728,6 +696,15 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal topic.replies.to_a.size, topic.replies_count
end
+ def test_pushing_association_updates_counter_cache
+ topic = Topic.order("id ASC").first
+ reply = Reply.create!
+
+ assert_difference "topic.reload.replies_count", 1 do
+ topic.replies << reply
+ end
+ end
+
def test_deleting_updates_counter_cache_without_dependent_option
post = posts(:welcome)
@@ -754,6 +731,45 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
end
+ def test_custom_named_counter_cache
+ topic = topics(:first)
+
+ assert_difference "topic.reload.replies_count", -1 do
+ topic.approved_replies.clear
+ end
+ end
+
+ def test_calling_update_attributes_on_id_changes_the_counter_cache
+ topic = Topic.order("id ASC").first
+ original_count = topic.replies.to_a.size
+ assert_equal original_count, topic.replies_count
+
+ first_reply = topic.replies.first
+ first_reply.update_attributes(:parent_id => nil)
+ assert_equal original_count - 1, topic.reload.replies_count
+
+ first_reply.update_attributes(:parent_id => topic.id)
+ assert_equal original_count, topic.reload.replies_count
+ end
+
+ def test_calling_update_attributes_changing_ids_doesnt_change_counter_cache
+ topic1 = Topic.find(1)
+ topic2 = Topic.find(3)
+ original_count1 = topic1.replies.to_a.size
+ original_count2 = topic2.replies.to_a.size
+
+ reply1 = topic1.replies.first
+ reply2 = topic2.replies.first
+
+ reply1.update_attributes(:parent_id => topic2.id)
+ assert_equal original_count1 - 1, topic1.reload.replies_count
+ assert_equal original_count2 + 1, topic2.reload.replies_count
+
+ reply2.update_attributes(:parent_id => topic1.id)
+ assert_equal original_count1, topic1.reload.replies_count
+ assert_equal original_count2, topic2.reload.replies_count
+ end
+
def test_deleting_a_collection
force_signal37_to_load_all_clients_of_firm
companies(:first_firm).clients_of_firm.create("name" => "Another Client")
@@ -765,13 +781,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
@@ -845,18 +861,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
@@ -1104,21 +1135,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')
@@ -1145,7 +1161,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_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
@@ -1172,7 +1195,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
@@ -1233,24 +1256,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
@@ -1284,7 +1327,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
@@ -1295,28 +1338,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
@@ -1390,6 +1422,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?
@@ -1399,15 +1439,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
@@ -1523,7 +1565,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
@@ -1570,6 +1612,14 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal [tagging], post.taggings
end
+ def test_build_with_polymorphic_has_many_does_not_allow_to_override_type_and_id
+ welcome = posts(:welcome)
+ tagging = welcome.taggings.build(:taggable_id => 99, :taggable_type => 'ShouldNotChange')
+
+ assert_equal welcome.id, tagging.taggable_id
+ assert_equal 'Post', tagging.taggable_type
+ end
+
def test_dont_call_save_callbacks_twice_on_has_many
firm = companies(:first_firm)
contract = firm.contracts.create!
@@ -1585,6 +1635,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
@@ -1639,13 +1705,56 @@ 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' }
+ test "has many associations on new records use null relations" do
+ post = Post.new
+
+ assert_no_queries do
+ assert_equal [], post.comments
+ assert_equal [], post.comments.where(body: 'omg')
+ assert_equal [], post.comments.pluck(:body)
+ assert_equal 0, post.comments.sum(:id)
+ assert_equal 0, post.comments.count
+ end
+ end
+
+ test "collection proxy respects default scope" do
+ author = authors(:mary)
+ assert !author.first_posts.exists?
end
- test ":counter_sql is deprecated" do
- klass = Class.new(ActiveRecord::Base)
- assert_deprecated { klass.has_many :foo, :counter_sql => 'lol' }
+ test "association with extend option" do
+ post = posts(:welcome)
+ assert_equal "lifo", post.comments_with_extend.author
+ assert_equal "hello", post.comments_with_extend.greeting
+ end
+
+ test "association with extend option with multiple extensions" do
+ post = posts(:welcome)
+ assert_equal "lifo", post.comments_with_extend_2.author
+ assert_equal "hello", post.comments_with_extend_2.greeting
+ end
+
+ test "delete record with complex joins" do
+ david = authors(:david)
+
+ post = david.posts.first
+ post.type = 'PostWithSpecialCategorization'
+ post.save
+
+ categorization = post.categorizations.first
+ categorization.special = true
+ categorization.save
+
+ assert_not_equal [], david.posts_with_special_categorizations
+ david.posts_with_special_categorizations = []
+ assert_equal [], david.posts_with_special_categorizations
+ end
+
+ test "does not duplicate associations when used with natural primary keys" do
+ speedometer = Speedometer.create!(id: '4')
+ speedometer.minivans.create!(minivan_id: 'a-van-red' ,name: 'a van', color: 'red')
+
+ 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
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 b2a5d9d6f7..c450b1beb5 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)
@@ -330,6 +503,17 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
end
end
+ def test_update_counter_caches_on_replace_association
+ post = posts(:welcome)
+ tag = post.tags.create!(:name => 'doomed')
+ tag.tagged_posts << posts(:thinking)
+
+ tag.tagged_posts = []
+ post.reload
+
+ assert_equal(post.taggings.count, post.taggings_count)
+ end
+
def test_replace_association
assert_queries(4){posts(:welcome);people(:david);people(:michael); posts(:welcome).people(true)}
@@ -571,8 +755,13 @@ 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 1, owners(:blackbeard).toys.count
+ assert_equal 2, owners(:blackbeard).toys.count
end
def test_find_on_has_many_association_collection_with_include_and_conditions
@@ -596,7 +785,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
@@ -695,7 +884,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
def test_can_update_through_association
assert_nothing_raised do
- people(:michael).posts.first.update_attributes!(:title => "Can write")
+ people(:michael).posts.first.update!(title: "Can write")
end
end
@@ -766,12 +955,6 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
assert_equal 1, authors(:mary).categories.general.count
end
- def test_counting_should_not_fire_sql_if_parent_is_unsaved
- assert_no_queries do
- assert_equal 0, Person.new.posts.count
- end
- end
-
def test_has_many_through_belongs_to_should_update_when_the_through_foreign_key_changes
post = posts(:eager_other)
@@ -876,4 +1059,34 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
post = tags(:general).tagged_posts.create! :title => "foo", :body => "bar"
assert_equal [tags(:general)], post.reload.tags
end
+
+ def test_has_many_through_obeys_order_on_through_association
+ owner = owners(:blackbeard)
+ assert owner.toys.to_sql.include?("pets.name desc")
+ assert_equal ["parrot", "bulbul"], owner.toys.map { |r| r.pet.name }
+ end
+
+ test "has many through associations on new records use null relations" do
+ person = Person.new
+
+ assert_no_queries do
+ assert_equal [], person.posts
+ assert_equal [], person.posts.where(body: 'omg')
+ assert_equal [], person.posts.pluck(:body)
+ assert_equal 0, person.posts.sum(:tags_count)
+ assert_equal 0, person.posts.count
+ end
+ end
+
+ test "has many through with default scope on the target" do
+ person = people(:michael)
+ assert_equal [posts(:thinking)], person.first_posts
+
+ 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 2d3cb654df..cdd386187b 100644
--- a/activerecord/test/cases/associations/has_one_associations_test.rb
+++ b/activerecord/test/cases/associations/has_one_associations_test.rb
@@ -6,6 +6,8 @@ require 'models/ship'
require 'models/pirate'
require 'models/car'
require 'models/bulb'
+require 'models/author'
+require 'models/post'
class HasOneAssociationsTest < ActiveRecord::TestCase
self.use_transactional_fixtures = false unless supports_savepoints?
@@ -156,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)
@@ -206,6 +192,40 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
assert_equal account, firm.account
end
+ def test_build_association_dont_create_transaction
+ assert_no_queries {
+ Firm.new.build_account
+ }
+ end
+
+ def test_building_the_associated_object_with_implicit_sti_base_class
+ firm = DependentFirm.new
+ company = firm.build_company
+ assert_kind_of Company, company, "Expected #{company.class} to be a Company"
+ end
+
+ def test_building_the_associated_object_with_explicit_sti_base_class
+ firm = DependentFirm.new
+ company = firm.build_company(:type => "Company")
+ assert_kind_of Company, company, "Expected #{company.class} to be a Company"
+ end
+
+ def test_building_the_associated_object_with_sti_subclass
+ firm = DependentFirm.new
+ company = firm.build_company(:type => "Client")
+ assert_kind_of Client, company, "Expected #{company.class} to be a Client"
+ end
+
+ def test_building_the_associated_object_with_an_invalid_type
+ firm = DependentFirm.new
+ assert_raise(ActiveRecord::SubclassNotFound) { firm.build_company(:type => "Invalid") }
+ end
+
+ def test_building_the_associated_object_with_an_unrelated_type
+ firm = DependentFirm.new
+ assert_raise(ActiveRecord::SubclassNotFound) { firm.build_company(:type => "Account") }
+ end
+
def test_build_and_create_should_not_happen_within_scope
pirate = pirates(:blackbeard)
scoped_count = pirate.association(:foo_bulb).scope.where_values.count
@@ -485,5 +505,34 @@ 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
+ 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(2) do
+ # One query for updating name and second query for updating pirate_id
+ pirate.ship = 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
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..f2723f2e18 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
diff --git a/activerecord/test/cases/associations/inner_join_association_test.rb b/activerecord/test/cases/associations/inner_join_association_test.rb
index 4f246f575e..9fe5ff50d9 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_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
@@ -82,7 +87,7 @@ class InnerJoinAssociationTest < ActiveRecord::TestCase
def test_calculate_honors_implicit_inner_joins_and_distinct_and_conditions
real_count = Author.all.to_a.select {|a| a.posts.any? {|p| p.title =~ /^Welcome/} }.length
- authors_with_welcoming_post_titles = Author.all.merge!(:joins => :posts, :where => "posts.title like 'Welcome%'").calculate(:count, 'authors.id', :distinct => true)
+ authors_with_welcoming_post_titles = Author.all.merge!(joins: :posts, where: "posts.title like 'Welcome%'").distinct.calculate(:count, 'authors.id')
assert_equal real_count, authors_with_welcoming_post_titles, "inner join and conditions should have only returned authors posting titles starting with 'Welcome'"
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 aad48e7ce9..893030345f 100644
--- a/activerecord/test/cases/associations/inverse_associations_test.rb
+++ b/activerecord/test/cases/associations/inverse_associations_test.rb
@@ -5,6 +5,102 @@ require 'models/interest'
require 'models/zine'
require 'models/club'
require 'models/sponsor'
+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)
+
+ assert_respond_to car_reflection, :has_inverse?
+ assert car_reflection.has_inverse?, "The Car reflection should have an inverse"
+ assert_equal bulb_reflection, car_reflection.inverse_of, "The Car reflection's inverse should be the Bulb reflection"
+
+ assert_respond_to bulb_reflection, :has_inverse?
+ assert bulb_reflection.has_inverse?, "The Bulb reflection should have an inverse"
+ assert_equal car_reflection, bulb_reflection.inverse_of, "The Bulb reflection's inverse should be the Car reflection"
+ end
+
+ def test_has_many_and_belongs_to_should_find_inverse_automatically
+ comment_reflection = Comment.reflect_on_association(:ratings)
+ rating_reflection = Rating.reflect_on_association(:comment)
+
+ assert_respond_to comment_reflection, :has_inverse?
+ assert comment_reflection.has_inverse?, "The Comment reflection should have an inverse"
+ assert_equal rating_reflection, comment_reflection.inverse_of, "The Comment reflection's inverse should be the Rating reflection"
+ end
+
+ def test_has_one_and_belongs_to_automatic_inverse_shares_objects
+ car = Car.first
+ bulb = Bulb.create!(car: car)
+
+ assert_equal car.bulb, bulb, "The Car's bulb should be the original bulb"
+
+ car.bulb.color = "Blue"
+ assert_equal car.bulb.color, bulb.color, "Changing the bulb's color on the car association should change the bulb's color"
+
+ bulb.color = "Red"
+ assert_equal bulb.color, car.bulb.color, "Changing the bulb's color should change the bulb's color on the car association"
+ end
+
+ def test_has_many_and_belongs_to_automatic_inverse_shares_objects_on_rating
+ comment = Comment.first
+ rating = Rating.create!(comment: comment)
+
+ assert_equal rating.comment, comment, "The Rating's comment should be the original Comment"
+
+ rating.comment.body = "Brogramming is the act of programming, like a bro."
+ assert_equal rating.comment.body, comment.body, "Changing the Comment's body on the association should change the original Comment's body"
+
+ comment.body = "Broseiden is the king of the sea of bros."
+ assert_equal comment.body, rating.comment.body, "Changing the original Comment's body should change the Comment's body on the association"
+ end
+
+ def test_has_many_and_belongs_to_automatic_inverse_shares_objects_on_comment
+ rating = Rating.create!
+ comment = Comment.first
+ rating.comment = comment
+
+ assert_equal rating.comment, comment, "The Rating's comment should be the original Comment"
+
+ rating.comment.body = "Brogramming is the act of programming, like a bro."
+ assert_equal rating.comment.body, comment.body, "Changing the Comment's body on the association should change the original Comment's body"
+
+ comment.body = "Broseiden is the king of the sea of bros."
+ assert_equal comment.body, rating.comment.body, "Changing the original Comment's body should change the Comment's body on the association"
+ end
+
+ def test_polymorphic_and_has_many_through_relationships_should_not_have_inverses
+ sponsor_reflection = Sponsor.reflect_on_association(:sponsorable)
+
+ assert_respond_to sponsor_reflection, :has_inverse?
+ assert !sponsor_reflection.has_inverse?, "A polymorphic association should not find an inverse automatically"
+
+ club_reflection = Club.reflect_on_association(:members)
+
+ assert_respond_to club_reflection, :has_inverse?
+ assert !club_reflection.has_inverse?, "A has_many_through association should not find an inverse automatically"
+ end
+end
class InverseAssociationTests < ActiveRecord::TestCase
def test_should_allow_for_inverse_of_options_in_associations
@@ -235,6 +331,22 @@ class InverseHasManyTests < ActiveRecord::TestCase
assert_equal m.name, i.man.name, "Name of man should be the same after changes to newly-created-child-owned instance"
end
+ def test_parent_instance_should_be_shared_within_create_block_of_new_child
+ man = Man.first
+ interest = man.interests.build do |i|
+ assert i.man.equal?(man), "Man of child should be the same instance as a parent"
+ end
+ assert interest.man.equal?(man), "Man of the child should still be the same instance as a parent"
+ end
+
+ def test_parent_instance_should_be_shared_within_build_block_of_new_child
+ man = Man.first
+ interest = man.interests.build do |i|
+ assert i.man.equal?(man), "Man of child should be the same instance as a parent"
+ end
+ assert interest.man.equal?(man), "Man of the child should still be the same instance as a parent"
+ end
+
def test_parent_instance_should_be_shared_with_poked_in_child
m = men(:gordon)
i = Interest.create(:topic => 'Industrial Revolution Re-enactment')
@@ -261,13 +373,92 @@ class InverseHasManyTests < ActiveRecord::TestCase
def test_parent_instance_should_be_shared_with_first_and_last_child
man = Man.first
+
assert man.interests.first.man.equal? man
assert man.interests.last.man.equal? man
end
+ def test_parent_instance_should_be_shared_with_first_n_and_last_n_children
+ man = Man.first
+
+ interests = man.interests.first(2)
+ assert interests[0].man.equal? man
+ assert interests[1].man.equal? man
+
+ interests = man.interests.last(2)
+ assert interests[0].man.equal? man
+ assert interests[1].man.equal? man
+ end
+
+ def test_parent_instance_should_find_child_instance_using_child_instance_id
+ man = Man.create!
+ interest = Interest.create!
+ man.interests = [interest]
+
+ assert interest.equal?(man.interests.first), "The inverse association should use the interest already created and held in memory"
+ assert interest.equal?(man.interests.find(interest.id)), "The inverse association should use the interest already created and held in memory"
+ assert man.equal?(man.interests.first.man), "Two inversion should lead back to the same object that was originally held"
+ assert man.equal?(man.interests.find(interest.id).man), "Two inversions should lead back to the same object that was originally held"
+ end
+
+ def test_parent_instance_should_find_child_instance_using_child_instance_id_when_created
+ man = Man.create!
+ interest = Interest.create!(man: man)
+
+ assert man.equal?(man.interests.first.man), "Two inverses should lead back to the same object that was originally held"
+ assert man.equal?(man.interests.find(interest.id).man), "Two inversions should lead back to the same object that was originally held"
+
+ assert_equal man.name, man.interests.find(interest.id).man.name, "The name of the man should match before the name is changed"
+ man.name = "Ben Bitdiddle"
+ assert_equal man.name, man.interests.find(interest.id).man.name, "The name of the man should match after the parent name is changed"
+ man.interests.find(interest.id).man.name = "Alyssa P. Hacker"
+ 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 = 245324523
+ assert_raise(ActiveRecord::RecordNotFound) { man.interests.find(invalid_id) }
+
+ invalid_ids = [8432342, 2390102913, 2453245234523452]
+ assert_raise(ActiveRecord::RecordNotFound) { man.interests.find(invalid_ids) }
+ end
+
+ def test_raise_record_not_found_error_when_no_ids_are_passed
+ man = Man.create!
+
+ assert_raise(ActiveRecord::RecordNotFound) { man.interests.find() }
+ end
+
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
@@ -412,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/join_model_test.rb b/activerecord/test/cases/associations/join_model_test.rb
index 9b00c21b52..aabeea025f 100644
--- a/activerecord/test/cases/associations/join_model_test.rb
+++ b/activerecord/test/cases/associations/join_model_test.rb
@@ -397,14 +397,14 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
end
def test_has_many_through_polymorphic_has_many
- assert_equal taggings(:welcome_general, :thinking_general), authors(:david).taggings.uniq.sort_by { |t| t.id }
+ assert_equal taggings(:welcome_general, :thinking_general), authors(:david).taggings.distinct.sort_by { |t| t.id }
end
def test_include_has_many_through_polymorphic_has_many
author = Author.includes(:taggings).find authors(:david).id
expected_taggings = taggings(:welcome_general, :thinking_general)
assert_no_queries do
- assert_equal expected_taggings, author.taggings.uniq.sort_by { |t| t.id }
+ assert_equal expected_taggings, author.taggings.distinct.sort_by { |t| t.id }
end
end
@@ -443,8 +443,8 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
def test_has_many_through_uses_conditions_specified_on_the_has_many_association
author = Author.first
- assert_present author.comments
- assert_blank author.nonexistant_comments
+ assert author.comments.present?
+ assert author.nonexistant_comments.blank?
end
def test_has_many_through_uses_correct_attributes
@@ -464,7 +464,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
assert saved_post.reload.tags(true).include?(new_tag)
- new_post = Post.new(:title => "Association replacmenet works!", :body => "You best believe it.")
+ new_post = Post.new(:title => "Association replacement works!", :body => "You best believe it.")
saved_tag = tags(:general)
new_post.tags << saved_tag
diff --git a/activerecord/test/cases/associations/nested_through_associations_test.rb b/activerecord/test/cases/associations/nested_through_associations_test.rb
index 03d99d19f6..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
@@ -221,6 +223,9 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase
end
def test_has_many_through_has_many_with_has_and_belongs_to_many_source_reflection_preload_via_joins
+ # preload table schemas
+ Author.joins(:post_categories).first
+
assert_includes_and_joins_equal(
Author.where('categories.id' => categories(:cooking).id),
[authors(:bob)], :post_categories
@@ -237,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
@@ -246,6 +252,9 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase
end
def test_has_many_through_has_and_belongs_to_many_with_has_many_source_reflection_preload_via_joins
+ # preload table schemas
+ Category.joins(:post_comments).first
+
assert_includes_and_joins_equal(
Category.where('comments.id' => comments(:more_greetings).id).order('categories.id'),
[categories(:general), categories(:technology)], :post_comments
@@ -262,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
@@ -271,6 +280,9 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase
end
def test_has_many_through_has_many_with_has_many_through_habtm_source_reflection_preload_via_joins
+ # preload table schemas
+ Author.joins(:category_post_comments).first
+
assert_includes_and_joins_equal(
Author.where('comments.id' => comments(:does_it_hurt).id).order('authors.id'),
[authors(:david), authors(:mary)], :category_post_comments
@@ -360,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
@@ -401,7 +413,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase
# Mary and Bob both have posts in misc, but they are the only ones.
authors = Author.joins(:similar_posts).where('posts.id' => posts(:misc_by_bob).id)
- assert_equal [authors(:mary), authors(:bob)], authors.uniq.sort_by(&:id)
+ assert_equal [authors(:mary), authors(:bob)], authors.distinct.sort_by(&:id)
# Check the polymorphism of taggings is being observed correctly (in both joins)
authors = Author.joins(:similar_posts).where('taggings.taggable_type' => 'FakeModel')
diff --git a/activerecord/test/cases/associations_test.rb b/activerecord/test/cases/associations_test.rb
index c0f1945cec..48e6fc5cd4 100644
--- a/activerecord/test/cases/associations_test.rb
+++ b/activerecord/test/cases/associations_test.rb
@@ -18,6 +18,8 @@ require 'models/ship'
require 'models/liquid'
require 'models/molecule'
require 'models/electron'
+require 'models/man'
+require 'models/interest'
class AssociationsTest < ActiveRecord::TestCase
fixtures :accounts, :companies, :developers, :projects, :developers_projects,
@@ -95,7 +97,7 @@ class AssociationsTest < ActiveRecord::TestCase
def test_force_reload
firm = Firm.new("name" => "A New Firm, Inc")
firm.save
- firm.clients.each {|c|} # forcing to load all clients
+ firm.clients.each {} # forcing to load all clients
assert firm.clients.empty?, "New firm shouldn't have client objects"
assert_equal 0, firm.clients.size, "New firm should have 0 clients"
@@ -172,6 +174,18 @@ class AssociationProxyTest < ActiveRecord::TestCase
assert_equal 1, josh.posts.size
end
+ def test_append_behaves_like_push
+ josh = Author.new(:name => "Josh")
+ josh.posts.append Post.new(:title => "New on Edge", :body => "More cool stuff!")
+ assert josh.posts.loaded?
+ assert_equal 1, josh.posts.size
+ end
+
+ def test_prepend_is_not_defined
+ josh = Author.new(:name => "Josh")
+ assert_raises(NoMethodError) { josh.posts.prepend Post.new }
+ end
+
def test_save_on_parent_does_not_load_target
david = developers(:david)
@@ -203,7 +217,7 @@ class AssociationProxyTest < ActiveRecord::TestCase
assert_equal post.body, "More cool stuff!"
end
- def test_reload_returns_assocition
+ def test_reload_returns_association
david = developers(:david)
assert_nothing_raised do
assert_equal david.projects, david.projects.reload.reload
@@ -225,10 +239,25 @@ class AssociationProxyTest < ActiveRecord::TestCase
assert david.projects.scope.is_a?(ActiveRecord::Relation)
assert_equal david.projects, david.projects.scope
end
+
+ test "proxy object is cached" do
+ david = developers(:david)
+ assert david.projects.equal?(david.projects)
+ end
+
+ test "inverses get set of subsets of the association" do
+ man = Man.create
+ man.interests.create
+
+ man = Man.find(man.id)
+
+ assert_queries(1) do
+ assert_equal man, man.interests.where("1=1").first.man
+ end
+ end
end
class OverridingAssociationsTest < ActiveRecord::TestCase
- class Person < ActiveRecord::Base; end
class DifferentPerson < ActiveRecord::Base; end
class PeopleList < ActiveRecord::Base
@@ -249,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
@@ -257,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
@@ -289,6 +318,14 @@ class OverridingAssociationsTest < ActiveRecord::TestCase
DifferentPeopleList.reflect_on_association(:has_one)
)
end
+
+ def test_requires_symbol_argument
+ assert_raises ArgumentError do
+ Class.new(Post) do
+ belongs_to "author"
+ end
+ end
+ end
end
class GeneratedMethodsTest < ActiveRecord::TestCase
diff --git a/activerecord/test/cases/attribute_methods/read_test.rb b/activerecord/test/cases/attribute_methods/read_test.rb
index da5d9d8c2a..c0659fddef 100644
--- a/activerecord/test/cases/attribute_methods/read_test.rb
+++ b/activerecord/test/cases/attribute_methods/read_test.rb
@@ -11,19 +11,10 @@ module ActiveRecord
def setup
@klass = Class.new do
def self.superclass; Base; end
- def self.active_record_super; Base; end
def self.base_class; self; end
- extend ActiveRecord::Configuration
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
@@ -58,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 c2b58fd7d1..9c66ed354e 100644
--- a/activerecord/test/cases/attribute_methods_test.rb
+++ b/activerecord/test/cases/attribute_methods_test.rb
@@ -12,6 +12,8 @@ require 'models/contact'
require 'models/keyboard'
class AttributeMethodsTest < ActiveRecord::TestCase
+ include InTimeZone
+
fixtures :topics, :developers, :companies, :computers
def setup
@@ -25,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!"
@@ -67,7 +77,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase
end
def test_boolean_attributes
- assert ! Topic.find(1).approved?
+ assert !Topic.find(1).approved?
assert Topic.find(2).approved?
end
@@ -82,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
@@ -128,6 +138,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase
assert_equal '10', keyboard.id_before_type_cast
assert_equal nil, keyboard.read_attribute_before_type_cast('id')
assert_equal '10', keyboard.read_attribute_before_type_cast('key_number')
+ assert_equal '10', keyboard.read_attribute_before_type_cast(:key_number)
end
# Syck calls respond_to? before actually calling initialize
@@ -139,13 +150,10 @@ class AttributeMethodsTest < ActiveRecord::TestCase
assert_respond_to topic, :title
end
- # IRB inspects the return value of "MyModel.allocate"
- # by inspecting it.
+ # IRB inspects the return value of "MyModel.allocate".
def test_allocated_object_can_be_inspected
topic = Topic.allocate
- topic.instance_eval { @attributes = nil }
- assert_nothing_raised { topic.inspect }
- assert topic.inspect, "#<Topic not initialized>"
+ assert_equal "#<Topic not initialized>", topic.inspect
end
def test_array_content
@@ -157,8 +165,8 @@ class AttributeMethodsTest < ActiveRecord::TestCase
end
def test_read_attributes_before_type_cast
- category = Category.new({:name=>"Test categoty", :type => nil})
- category_attrs = {"name"=>"Test categoty", "id" => nil, "type" => nil, "categorizations_count" => nil}
+ category = Category.new({:name=>"Test category", :type => nil})
+ category_attrs = {"name"=>"Test category", "id" => nil, "type" => nil, "categorizations_count" => nil}
assert_equal category_attrs , category.attributes_before_type_cast
end
@@ -287,6 +295,12 @@ class AttributeMethodsTest < ActiveRecord::TestCase
assert_equal "Don't change the topic", topic[:title]
end
+ def test_read_attribute_raises_missing_attribute_error_when_not_exists
+ computer = Computer.select('id').first
+ assert_raises(ActiveModel::MissingAttributeError) { computer[:developer] }
+ assert_raises(ActiveModel::MissingAttributeError) { computer[:extendedWarranty] }
+ end
+
def test_read_attribute_when_false
topic = topics(:first)
topic.approved = false
@@ -305,26 +319,17 @@ class AttributeMethodsTest < ActiveRecord::TestCase
def test_read_write_boolean_attribute
topic = Topic.new
- # puts ""
- # puts "New Topic"
- # puts topic.inspect
topic.approved = "false"
- # puts "Expecting false"
- # puts topic.inspect
assert !topic.approved?, "approved should be false"
+
topic.approved = "false"
- # puts "Expecting false"
- # puts topic.inspect
assert !topic.approved?, "approved should be false"
+
topic.approved = "true"
- # puts "Expecting true"
- # puts topic.inspect
assert topic.approved?, "approved should be true"
+
topic.approved = "true"
- # puts "Expecting true"
- # puts topic.inspect
assert topic.approved?, "approved should be true"
- # puts ""
end
def test_overridden_write_attribute
@@ -714,6 +719,15 @@ class AttributeMethodsTest < ActiveRecord::TestCase
assert_raise(ActiveRecord::UnknownAttributeError) { @target.new.attributes = { :title => "Ants in pants" } }
end
+ def test_bulk_update_raise_unknown_attribute_errro
+ error = assert_raises(ActiveRecord::UnknownAttributeError) {
+ @target.new(:hello => "world")
+ }
+ assert @target, error.record
+ assert "hello", error.attribute
+ assert "unknown attribute: hello", error.message
+ end
+
def test_read_attribute_overwrites_private_method_not_considered_implemented
# simulate a model with a db column that shares its name an inherited
# private method (e.g. Object#system)
@@ -745,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
@@ -787,27 +786,13 @@ class AttributeMethodsTest < ActiveRecord::TestCase
private
def cached_columns
- Topic.columns.find_all { |column|
- !Topic.serialized_attributes.include? column.name
- }.map(&:name)
+ Topic.columns.map(&:name) - Topic.serialized_attributes.keys
end
def time_related_columns_on_topic
Topic.columns.select { |c| [:time, :date, :datetime, :timestamp].include?(c.type) }
end
- def in_time_zone(zone)
- old_zone = Time.zone
- old_tz = ActiveRecord::Base.time_zone_aware_attributes
-
- Time.zone = zone ? ActiveSupport::TimeZone[zone] : nil
- ActiveRecord::Base.time_zone_aware_attributes = !zone.nil?
- yield
- ensure
- Time.zone = old_zone
- ActiveRecord::Base.time_zone_aware_attributes = old_tz
- end
-
def privatize(method_signature)
@target.class_eval(<<-private_method, __FILE__, __LINE__ + 1)
private
diff --git a/activerecord/test/cases/autosave_association_test.rb b/activerecord/test/cases/autosave_association_test.rb
index 4afd33f273..517d2674a7 100644
--- a/activerecord/test/cases/autosave_association_test.rb
+++ b/activerecord/test/cases/autosave_association_test.rb
@@ -144,7 +144,7 @@ class TestDefaultAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCas
firm = Firm.first
firm.account = Account.first
- assert_queries(Firm.partial_updates? ? 0 : 1) { firm.save! }
+ assert_queries(Firm.partial_writes? ? 0 : 1) { firm.save! }
firm = Firm.first.dup
firm.account = Account.first
@@ -161,16 +161,16 @@ class TestDefaultAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCas
end
def test_callbacks_firing_order_on_update
- eye = Eye.create(:iris_attributes => {:color => 'honey'})
- eye.update_attributes(:iris_attributes => {:color => 'green'})
+ eye = Eye.create(iris_attributes: {color: 'honey'})
+ eye.update(iris_attributes: {color: 'green'})
assert_equal [true, false], eye.after_update_callbacks_stack
end
def test_callbacks_firing_order_on_save
- eye = Eye.create(:iris_attributes => {:color => 'honey'})
+ eye = Eye.create(iris_attributes: {color: 'honey'})
assert_equal [false, false], eye.after_save_callbacks_stack
- eye.update_attributes(:iris_attributes => {:color => 'blue'})
+ eye.update(iris_attributes: {color: 'blue'})
assert_equal [false, false, false, false], eye.after_save_callbacks_stack
end
end
@@ -439,7 +439,7 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa
end
def test_assign_ids_for_through_a_belongs_to
- post = Post.new(:title => "Assigning IDs works!", :body => "You heared it here first, folks!")
+ post = Post.new(:title => "Assigning IDs works!", :body => "You heard it here first, folks!")
post.person_ids = [people(:david).id, people(:michael).id]
post.save
post.reload
@@ -566,7 +566,7 @@ class TestDefaultAutosaveAssociationOnNewRecord < ActiveRecord::TestCase
end
class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
- self.use_transactional_fixtures = false unless supports_savepoints?
+ self.use_transactional_fixtures = false
def setup
super
@@ -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}") }
@@ -764,6 +771,20 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
assert_equal 2, @pirate.birds.reload.length
end
+ def test_should_save_new_record_that_has_same_value_as_existing_record_marked_for_destruction_on_field_that_has_unique_index
+ Bird.connection.add_index :birds, :name, unique: true
+
+ 3.times { |i| @pirate.birds.create(name: "unique_birds_#{i}") }
+
+ @pirate.birds[0].mark_for_destruction
+ @pirate.birds.build(name: @pirate.birds[0].name)
+ @pirate.save!
+
+ assert_equal 3, @pirate.birds.reload.length
+ ensure
+ Bird.connection.remove_index :birds, column: :name
+ end
+
# Add and remove callbacks tests for association collections.
%w{ method proc }.each do |callback_type|
define_method("test_should_run_add_callback_#{callback_type}s_for_has_many") do
@@ -846,8 +867,10 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
@pirate.parrots.each { |parrot| parrot.mark_for_destruction }
assert @pirate.save
- assert_queries(0) do
- assert @pirate.save
+ Pirate.transaction do
+ assert_queries(0) do
+ assert @pirate.save
+ end
end
end
@@ -1335,7 +1358,7 @@ class TestAutosaveAssociationValidationsOnAHasOneAssociation < ActiveRecord::Tes
assert !@pirate.valid?
end
- test "should not automatically asd validate associations without :validate => true" do
+ test "should not automatically add validate associations without :validate => true" do
assert @pirate.valid?
@pirate.non_validated_ship.name = ''
assert @pirate.valid?
@@ -1417,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 0f859bf452..82b20e8cee 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'
@@ -21,11 +24,10 @@ require 'models/parrot'
require 'models/person'
require 'models/edge'
require 'models/joke'
-require 'models/bulb'
require 'models/bird'
-require 'models/teapot'
+require 'models/car'
+require 'models/bulb'
require 'rexml/document'
-require 'active_support/core_ext/exception'
class FirstAbstractClass < ActiveRecord::Base
self.abstract_class = true
@@ -76,12 +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)
@@ -124,7 +120,7 @@ class BasicsTest < ActiveRecord::TestCase
assert_nil Edge.primary_key
end
- unless current_adapter?(:PostgreSQLAdapter,:OracleAdapter,:SQLServerAdapter)
+ unless current_adapter?(:PostgreSQLAdapter, :OracleAdapter, :SQLServerAdapter)
def test_limit_with_comma
assert Topic.limit("1,2").to_a
end
@@ -141,19 +137,19 @@ class BasicsTest < ActiveRecord::TestCase
end
end
- def test_limit_should_sanitize_sql_injection_for_limit_without_comas
+ def test_limit_should_sanitize_sql_injection_for_limit_without_commas
assert_raises(ArgumentError) do
Topic.limit("1 select * from schema").to_a
end
end
- def test_limit_should_sanitize_sql_injection_for_limit_with_comas
+ def test_limit_should_sanitize_sql_injection_for_limit_with_commas
assert_raises(ArgumentError) do
Topic.limit("1, 7 procedure help()").to_a
end
end
- unless current_adapter?(:MysqlAdapter) || current_adapter?(:Mysql2Adapter)
+ unless current_adapter?(:MysqlAdapter, :Mysql2Adapter)
def test_limit_should_allow_sql_literal
assert_equal 1, Topic.limit(Arel.sql('2-1')).to_a.length
end
@@ -222,7 +218,7 @@ class BasicsTest < ActiveRecord::TestCase
)
# For adapters which support microsecond resolution.
- if current_adapter?(:PostgreSQLAdapter) || current_adapter?(:SQLite3Adapter)
+ if current_adapter?(:PostgreSQLAdapter, :SQLite3Adapter)
assert_equal 11, Topic.find(1).written_on.sec
assert_equal 223300, Topic.find(1).written_on.usec
assert_equal 9900, Topic.find(2).written_on.usec
@@ -232,7 +228,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
@@ -245,7 +241,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)
@@ -260,18 +256,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)
@@ -300,13 +298,11 @@ class BasicsTest < ActiveRecord::TestCase
end
def test_initialize_with_invalid_attribute
- begin
- Topic.new({ "title" => "test",
- "last_read(1i)" => "2005", "last_read(2i)" => "2", "last_read(3i)" => "31"})
- rescue ActiveRecord::MultiparameterAssignmentErrors => ex
- assert_equal(1, ex.errors.size)
- assert_equal("last_read", ex.errors[0].attribute)
- end
+ Topic.new({ "title" => "test",
+ "last_read(1i)" => "2005", "last_read(2i)" => "2", "last_read(3i)" => "31"})
+ rescue ActiveRecord::MultiparameterAssignmentErrors => ex
+ assert_equal(1, ex.errors.size)
+ assert_equal("last_read", ex.errors[0].attribute)
end
def test_create_after_initialize_without_block
@@ -321,27 +317,12 @@ class BasicsTest < ActiveRecord::TestCase
assert_equal(true, cb.frickinawesome)
end
- def test_first_or_create
- parrot = Bird.first_or_create(:color => 'green', :name => 'parrot')
- assert parrot.persisted?
- the_same_parrot = Bird.first_or_create(:color => 'yellow', :name => 'macaw')
- assert_equal parrot, the_same_parrot
- end
-
- def test_first_or_create_bang
- assert_raises(ActiveRecord::RecordInvalid) { Bird.first_or_create! }
- parrot = Bird.first_or_create!(:color => 'green', :name => 'parrot')
- assert parrot.persisted?
- the_same_parrot = Bird.first_or_create!(:color => 'yellow', :name => 'macaw')
- assert_equal parrot, the_same_parrot
- end
-
- def test_first_or_initialize
- parrot = Bird.first_or_initialize(:color => 'green', :name => 'parrot')
- assert_kind_of Bird, parrot
- assert !parrot.persisted?
- assert parrot.new_record?
- assert parrot.valid?
+ def test_create_after_initialize_with_array_param
+ cbs = CustomBulb.create([{ name: 'Dude' }, { name: 'Bob' }])
+ assert_equal 'Dude', cbs[0].name
+ assert_equal 'Bob', cbs[1].name
+ assert cbs[0].frickinawesome
+ assert !cbs[1].frickinawesome
end
def test_load
@@ -471,7 +452,7 @@ class BasicsTest < ActiveRecord::TestCase
Post.reset_table_name
end
- if current_adapter?(:MysqlAdapter) or current_adapter?(:Mysql2Adapter)
+ if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
def test_update_all_with_order_and_limit
assert_equal 1, Topic.limit(1).order('id DESC').update_all(:content => 'bulk updated!')
end
@@ -508,25 +489,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
@@ -573,11 +554,27 @@ class BasicsTest < ActiveRecord::TestCase
assert_equal [ Topic.find(1) ], [ Topic.find(2).topic ] & [ Topic.find(1) ]
end
- def test_comparison
- topic_1 = Topic.create!
- topic_2 = Topic.create!
+ 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 [topic_2, topic_1].sort, [topic_1, topic_2]
+ 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
@@ -586,6 +583,13 @@ class BasicsTest < ActiveRecord::TestCase
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
@@ -593,16 +597,30 @@ class BasicsTest < ActiveRecord::TestCase
post.reload
assert_equal "cannot change this", post.title
- post.update_attributes(:title => "try to change", :body => "changed")
+ post.update(title: "try to change", body: "changed")
post.reload
assert_equal "cannot change this", post.title
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
+
+ def test_respect_internal_encoding
+ if current_adapter?(:PostgreSQLAdapter)
+ skip 'pg does not respect internal encoding and always returns utf8'
+ end
+ 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
def test_non_valid_identifier_column_name
@@ -617,16 +635,24 @@ class BasicsTest < ActiveRecord::TestCase
assert_equal 'value2', weird.read_attribute('a$b')
end
+ def test_group_weirds_by_from
+ Weird.create('a$b' => 'value', :from => 'aaron')
+ count = Weird.group(Weird.arel_table[:from]).count
+ assert_equal 1, count['aaron']
+ end
+
def test_attributes_on_dummy_time
# 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
@@ -814,19 +840,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
@@ -851,7 +876,7 @@ class BasicsTest < ActiveRecord::TestCase
# Reload and check that we have all the geometric attributes.
h = Geometric.find(g.id)
- assert_equal '(5,6.1)', h.a_point
+ assert_equal [5.0, 6.1], h.a_point
assert_equal '[(2,3),(5.5,7)]', h.a_line_segment
assert_equal '(5.5,7),(2,3)', h.a_box # reordered to store upper right corner then bottom left corner
assert_equal '[(2,3),(5.5,7),(8.5,11)]', h.a_path
@@ -880,7 +905,7 @@ class BasicsTest < ActiveRecord::TestCase
# Reload and check that we have all the geometric attributes.
h = Geometric.find(g.id)
- assert_equal '(5,6.1)', h.a_point
+ assert_equal [5.0, 6.1], h.a_point
assert_equal '[(2,3),(5.5,7)]', h.a_line_segment
assert_equal '(5.5,7),(2,3)', h.a_box # reordered to store upper right corner then bottom left corner
assert_equal '((2,3),(5.5,7),(8.5,11))', h.a_path
@@ -891,6 +916,29 @@ class BasicsTest < ActiveRecord::TestCase
objs = Geometric.find_by_sql ["select isclosed(a_path) from geometrics where id = ?", g.id]
assert_equal true, objs[0].isclosed
+
+ # test native ruby formats when defining the geometric types
+ g = Geometric.new(
+ :a_point => [5.0, 6.1],
+ #:a_line => '((2.0, 3), (5.5, 7.0))' # line type is currently unsupported in postgresql
+ :a_line_segment => '((2.0, 3), (5.5, 7.0))',
+ :a_box => '(2.0, 3), (5.5, 7.0)',
+ :a_path => '((2.0, 3), (5.5, 7.0), (8.5, 11.0))', # ( ) is a closed path
+ :a_polygon => '2.0, 3, 5.5, 7.0, 8.5, 11.0',
+ :a_circle => '((5.3, 10.4), 2)'
+ )
+
+ assert g.save
+
+ # Reload and check that we have all the geometric attributes.
+ h = Geometric.find(g.id)
+
+ assert_equal [5.0, 6.1], h.a_point
+ assert_equal '[(2,3),(5.5,7)]', h.a_line_segment
+ assert_equal '(5.5,7),(2,3)', h.a_box # reordered to store upper right corner then bottom left corner
+ assert_equal '((2,3),(5.5,7),(8.5,11))', h.a_path
+ assert_equal '((2,3),(5.5,7),(8.5,11))', h.a_polygon
+ assert_equal '<(5.3,10.4),2>', h.a_circle
end
end
@@ -995,7 +1043,7 @@ class BasicsTest < ActiveRecord::TestCase
def test_reload_with_exclusive_scope
dev = DeveloperCalledDavid.first
- dev.update_attributes!( :name => "NotDavid" )
+ dev.update!(name: "NotDavid" )
assert_equal dev, dev.reload
end
@@ -1035,7 +1083,7 @@ class BasicsTest < ActiveRecord::TestCase
Joke.reset_sequence_name
end
- def test_dont_clear_inheritnce_column_when_setting_explicitly
+ def test_dont_clear_inheritance_column_when_setting_explicitly
Joke.inheritance_column = "my_type"
before_inherit = Joke.inheritance_column
@@ -1102,7 +1150,7 @@ class BasicsTest < ActiveRecord::TestCase
res6 = Post.count_by_sql "SELECT COUNT(DISTINCT p.id) FROM posts p, comments co WHERE p.#{QUOTED_TYPE} = 'Post' AND p.id=co.post_id"
res7 = nil
assert_nothing_raised do
- res7 = Post.where("p.#{QUOTED_TYPE} = 'Post' AND p.id=co.post_id").joins("p, comments co").select("p.id").count(distinct: true)
+ res7 = Post.where("p.#{QUOTED_TYPE} = 'Post' AND p.id=co.post_id").joins("p, comments co").select("p.id").distinct.count
end
assert_equal res6, res7
end
@@ -1153,8 +1201,8 @@ class BasicsTest < ActiveRecord::TestCase
end
def test_find_keeps_multiple_group_values
- combined = Developer.all.merge!(:group => 'developers.name, developers.salary, developers.id, developers.created_at, developers.updated_at').to_a
- assert_equal combined, Developer.all.merge!(:group => ['developers.name', 'developers.salary', 'developers.id', 'developers.created_at', 'developers.updated_at']).to_a
+ combined = Developer.all.merge!(:group => 'developers.name, developers.salary, developers.id, developers.created_at, developers.updated_at, developers.created_on, developers.updated_on').to_a
+ assert_equal combined, Developer.all.merge!(:group => ['developers.name', 'developers.salary', 'developers.id', 'developers.created_at', 'developers.updated_at', 'developers.created_on', 'developers.updated_on']).to_a
end
def test_find_symbol_ordered_last
@@ -1196,10 +1244,6 @@ class BasicsTest < ActiveRecord::TestCase
# Concrete subclasses an abstract class which has a type column.
assert !SubStiPost.descends_from_active_record?
-
- assert Teapot.descends_from_active_record?
- assert !OtherTeapot.descends_from_active_record?
- assert CoolTeapot.descends_from_active_record?
end
def test_find_on_abstract_base_class_doesnt_use_type_condition
@@ -1221,93 +1265,6 @@ class BasicsTest < ActiveRecord::TestCase
assert_no_queries { assert true }
end
- def test_to_param_should_return_string
- assert_kind_of String, Client.first.to_param
- end
-
- def test_to_param_returns_id_even_if_not_persisted
- client = Client.new
- client.id = 1
- assert_equal "1", client.to_param
- 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, 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
@@ -1359,9 +1316,9 @@ class BasicsTest < ActiveRecord::TestCase
def test_clear_cache!
# preheat cache
- c1 = Post.connection.schema_cache.columns['posts']
+ c1 = Post.connection.schema_cache.columns('posts')
ActiveRecord::Base.clear_cache!
- c2 = Post.connection.schema_cache.columns['posts']
+ c2 = Post.connection.schema_cache.columns('posts')
assert_not_equal c1, c2
end
@@ -1370,9 +1327,9 @@ class BasicsTest < ActiveRecord::TestCase
UnloadablePost.send(:current_scope=, UnloadablePost.all)
UnloadablePost.unloadable
- assert_not_nil Thread.current[:UnloadablePost_current_scope]
+ assert_not_nil ActiveRecord::Scoping::ScopeRegistry.value_for(:current_scope, "UnloadablePost")
ActiveSupport::Dependencies.remove_unloadable_constants!
- assert_nil Thread.current[:UnloadablePost_current_scope]
+ assert_nil ActiveRecord::Scoping::ScopeRegistry.value_for(:current_scope, "UnloadablePost")
ensure
Object.class_eval{ remove_const :UnloadablePost } if defined?(UnloadablePost)
end
@@ -1402,6 +1359,36 @@ class BasicsTest < ActiveRecord::TestCase
assert_equal 1, post.comments.length
end
+ def test_marshal_between_processes
+ skip "can't marshal between processes when using an in-memory db" if in_memory_db?
+ skip "fork isn't supported" unless Process.respond_to?(:fork)
+
+ # 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
+
def test_marshalling_new_record_round_trip_with_associations
post = Post.new
post.comments.build
@@ -1424,27 +1411,11 @@ class BasicsTest < ActiveRecord::TestCase
assert_equal [], AbstractCompany.attribute_names
end
- def test_cache_key_for_existing_record_is_not_timezone_dependent
- ActiveRecord::Base.time_zone_aware_attributes = true
-
- Time.zone = "UTC"
- utc_key = Developer.first.cache_key
-
- Time.zone = "EST"
- est_key = Developer.first.cache_key
-
- assert_equal utc_key, est_key
- end
-
- def test_cache_key_format_for_existing_record_with_updated_at
- dev = Developer.first
- assert_equal "developers/#{dev.id}-#{dev.updated_at.utc.to_s(:nsec)}", dev.cache_key
- end
-
- def test_cache_key_format_for_existing_record_with_nil_updated_at
- dev = Developer.first
- dev.update_columns(updated_at: nil)
- assert_match(/\/#{dev.id}$/, dev.cache_key)
+ def test_touch_should_raise_error_on_a_new_object
+ company = Company.new(:rating => 1, :name => "37signals", :firm_name => "37signals")
+ assert_raises(ActiveRecord::ActiveRecordError) do
+ company.touch :updated_at
+ end
end
def test_uniq_delegates_to_scoped
@@ -1453,13 +1424,10 @@ class BasicsTest < ActiveRecord::TestCase
assert_equal scope, Bird.uniq
end
- def test_active_record_super
- assert_equal ActiveRecord::Model, ActiveRecord::Base.active_record_super
- assert_equal ActiveRecord::Base, Topic.active_record_super
- assert_equal Topic, ImportantTopic.active_record_super
- assert_equal ActiveRecord::Model, Teapot.active_record_super
- assert_equal Teapot, OtherTeapot.active_record_super
- assert_equal ActiveRecord::Model, CoolTeapot.active_record_super
+ def test_distinct_delegates_to_scoped
+ scope = stub
+ Bird.stubs(:all).returns(mock(:distinct => scope))
+ assert_equal scope, Bird.distinct
end
def test_table_name_with_2_abstract_subclasses
@@ -1468,7 +1436,7 @@ class BasicsTest < ActiveRecord::TestCase
def test_column_types_typecast
topic = Topic.first
- refute_equal 't.lo', topic.author_name
+ assert_not_equal 't.lo', topic.author_name
attrs = topic.attributes.dup
attrs.delete 'id'
@@ -1526,4 +1494,60 @@ class BasicsTest < ActiveRecord::TestCase
klass = Class.new(ActiveRecord::Base)
assert_equal ['foo'], klass.all.merge!(select: 'foo').select_values
end
+
+ test "connection_handler can be overridden" do
+ klass = Class.new(ActiveRecord::Base)
+ orig_handler = klass.connection_handler
+ new_handler = ActiveRecord::ConnectionAdapters::ConnectionHandler.new
+ thread_connection_handler = nil
+
+ t = Thread.new do
+ klass.connection_handler = new_handler
+ thread_connection_handler = klass.connection_handler
+ end
+ t.join
+
+ assert_equal klass.connection_handler, orig_handler
+ assert_equal thread_connection_handler, new_handler
+ end
+
+ test "new threads get default the default connection handler" do
+ klass = Class.new(ActiveRecord::Base)
+ orig_handler = klass.connection_handler
+ handler = nil
+
+ t = Thread.new do
+ handler = klass.connection_handler
+ end
+ t.join
+
+ assert_equal handler, orig_handler
+ assert_equal klass.connection_handler, orig_handler
+ assert_equal klass.default_connection_handler, orig_handler
+ end
+
+ test "changing a connection handler in a main thread does not poison the other threads" do
+ klass = Class.new(ActiveRecord::Base)
+ orig_handler = klass.connection_handler
+ new_handler = ActiveRecord::ConnectionAdapters::ConnectionHandler.new
+ after_handler = nil
+ latch1 = ActiveSupport::Concurrency::Latch.new
+ latch2 = ActiveSupport::Concurrency::Latch.new
+
+ t = Thread.new do
+ klass.connection_handler = new_handler
+ latch1.release
+ latch2.await
+ after_handler = klass.connection_handler
+ end
+
+ latch1.await
+
+ klass.connection_handler = orig_handler
+ latch2.release
+ t.join
+
+ assert_equal after_handler, new_handler
+ assert_equal orig_handler, klass.connection_handler
+ end
end
diff --git a/activerecord/test/cases/batches_test.rb b/activerecord/test/cases/batches_test.rb
index 12d5245fbd..38c2560d69 100644
--- a/activerecord/test/cases/batches_test.rb
+++ b/activerecord/test/cases/batches_test.rb
@@ -11,21 +11,39 @@ class EachTest < ActiveRecord::TestCase
Post.count('id') # preheat arel's table cache
end
- def test_each_should_excecute_one_query_per_batch
- assert_queries(Post.count + 1) do
+ def test_each_should_execute_one_query_per_batch
+ assert_queries(@total + 1) do
Post.find_each(:batch_size => 1) do |post|
assert_kind_of Post, post
end
end
end
- def test_each_should_not_return_query_chain_and_execcute_only_one_query
+ def test_each_should_not_return_query_chain_and_execute_only_one_query
assert_queries(1) do
result = Post.find_each(:batch_size => 100000){ }
assert_nil result
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,8 +68,18 @@ 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(Post.count + 1) do
+ assert_queries(@total + 1) do
Post.find_in_batches(:batch_size => 1) do |batch|
assert_kind_of Array, batch
assert_kind_of Post, batch.first
@@ -60,7 +88,7 @@ class EachTest < ActiveRecord::TestCase
end
def test_find_in_batches_should_start_from_the_start_option
- assert_queries(Post.count) do
+ assert_queries(@total) do
Post.find_in_batches(:batch_size => 1, :start => 2) do |batch|
assert_kind_of Array, batch
assert_kind_of Post, batch.first
@@ -68,15 +96,13 @@ class EachTest < ActiveRecord::TestCase
end
end
- def test_find_in_batches_shouldnt_excute_query_unless_needed
- post_count = Post.count
-
+ def test_find_in_batches_shouldnt_execute_query_unless_needed
assert_queries(2) do
- Post.find_in_batches(:batch_size => post_count) {|batch| assert_kind_of Array, batch }
+ Post.find_in_batches(:batch_size => @total) {|batch| assert_kind_of Array, batch }
end
assert_queries(1) do
- Post.find_in_batches(:batch_size => post_count + 1) {|batch| assert_kind_of Array, batch }
+ Post.find_in_batches(:batch_size => @total + 1) {|batch| assert_kind_of Array, batch }
end
end
@@ -136,4 +162,12 @@ class EachTest < ActiveRecord::TestCase
assert_equal nick_order_subscribers[1..-1].map(&:id), subscribers.map(&:id)
end
+
+ def test_find_in_batches_should_use_any_column_as_primary_key_when_start_is_not_specified
+ assert_queries(Subscriber.count + 1) do
+ Subscriber.find_each(:batch_size => 1) do |subscriber|
+ assert_kind_of Subscriber, subscriber
+ end
+ end
+ end
end
diff --git a/activerecord/test/cases/binary_test.rb b/activerecord/test/cases/binary_test.rb
index 25d2896ab0..9a486cf8b8 100644
--- a/activerecord/test/cases/binary_test.rb
+++ b/activerecord/test/cases/binary_test.rb
@@ -23,7 +23,7 @@ unless current_adapter?(:SybaseAdapter, :DB2Adapter, :FirebirdAdapter)
# Mysql adapter doesn't properly encode things, so we have to do it
if current_adapter?(:MysqlAdapter)
- name.force_encoding('UTF-8')
+ name.force_encoding(Encoding::UTF_8)
end
assert_equal 'いただきます!', name
end
diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb
index 6cb6c469d2..2c41656b3d 100644
--- a/activerecord/test/cases/calculations_test.rb
+++ b/activerecord/test/cases/calculations_test.rb
@@ -6,6 +6,10 @@ 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'
Company.has_many :accounts
@@ -25,14 +29,19 @@ class CalculationsTest < ActiveRecord::TestCase
assert_equal 53.0, value
end
+ def test_should_resolve_aliased_attributes
+ assert_equal 318, Account.sum(:available_credit)
+ end
+
def test_should_return_decimal_average_of_integer_field
value = Account.average(:id)
assert_equal 3.5, value
end
def test_should_return_integer_average_if_db_returns_such
- Account.connection.stubs :select_value => 3
- value = Account.average(:id)
+ ShipPart.delete_all
+ ShipPart.create!(:id => 3, :name => 'foo')
+ value = ShipPart.average(:id)
assert_equal 3, value
end
@@ -92,25 +101,24 @@ class CalculationsTest < ActiveRecord::TestCase
end
def test_should_order_by_grouped_field
- c = Account.all.merge!(:group => :firm_id, :order => "firm_id").sum(:credit_limit)
+ c = Account.group(:firm_id).order("firm_id").sum(:credit_limit)
assert_equal [1, 2, 6, 9], c.keys.compact
end
def test_should_order_by_calculation
- c = Account.all.merge!(:group => :firm_id, :order => "sum_credit_limit desc, firm_id").sum(:credit_limit)
+ c = Account.group(:firm_id).order("sum_credit_limit desc, firm_id").sum(:credit_limit)
assert_equal [105, 60, 53, 50, 50], c.keys.collect { |k| c[k] }
assert_equal [6, 2, 9, 1], c.keys.compact
end
def test_should_limit_calculation
- c = Account.all.merge!(:where => "firm_id IS NOT NULL",
- :group => :firm_id, :order => "firm_id", :limit => 2).sum(:credit_limit)
+ c = Account.where("firm_id IS NOT NULL").group(:firm_id).order("firm_id").limit(2).sum(:credit_limit)
assert_equal [1, 2], c.keys.compact
end
def test_should_limit_calculation_with_offset
- c = Account.all.merge!(:where => "firm_id IS NOT NULL", :group => :firm_id,
- :order => "firm_id", :limit => 2, :offset => 1).sum(:credit_limit)
+ c = Account.where("firm_id IS NOT NULL").group(:firm_id).order("firm_id").
+ limit(2).offset(1).sum(:credit_limit)
assert_equal [2, 6], c.keys.compact
end
@@ -159,9 +167,17 @@ 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.all.merge!(:group => :firm_id,
- :having => 'sum(credit_limit) > 50').sum(:credit_limit)
+ c = Account.group(:firm_id).having('sum(credit_limit) > 50').sum(:credit_limit)
assert_nil c[1]
assert_equal 105, c[6]
assert_equal 60, c[2]
@@ -196,17 +212,15 @@ class CalculationsTest < ActiveRecord::TestCase
end
def test_should_group_by_summed_field_with_conditions
- c = Account.all.merge!(:where => 'firm_id > 1',
- :group => :firm_id).sum(:credit_limit)
+ c = Account.where('firm_id > 1').group(:firm_id).sum(:credit_limit)
assert_nil c[1]
assert_equal 105, c[6]
assert_equal 60, c[2]
end
def test_should_group_by_summed_field_with_conditions_and_having
- c = Account.all.merge!(:where => 'firm_id > 1',
- :group => :firm_id,
- :having => 'sum(credit_limit) > 60').sum(:credit_limit)
+ c = Account.where('firm_id > 1').group(:firm_id).
+ having('sum(credit_limit) > 60').sum(:credit_limit)
assert_nil c[1]
assert_equal 105, c[6]
assert_nil c[2]
@@ -239,21 +253,12 @@ class CalculationsTest < ActiveRecord::TestCase
end
def test_should_group_by_association_with_non_numeric_foreign_key
- ActiveRecord::Base.connection.expects(:select_all).returns([{"count_all" => 1, "firm_id" => "ABC"}])
-
- firm = mock()
- firm.expects(:id).returns("ABC")
- firm.expects(:class).returns(Firm)
- Company.expects(:find).with(["ABC"]).returns([firm])
+ Speedometer.create! id: 'ABC'
+ Minivan.create! id: 'OMG', speedometer_id: 'ABC'
- column = mock()
- column.expects(:name).at_least_once.returns(:firm_id)
- column.expects(:type_cast).with("ABC").returns("ABC")
- Account.expects(:columns).at_least_once.returns([column])
-
- c = Account.group(:firm).count(:all)
+ c = Minivan.group(:speedometer).count(:all)
first_key = c.keys.first
- assert_equal Firm, first_key.class
+ assert_equal Speedometer, first_key.class
assert_equal 1, c[first_key]
end
@@ -310,8 +315,8 @@ class CalculationsTest < ActiveRecord::TestCase
end
def test_should_count_selected_field_with_include
- assert_equal 6, Account.includes(:firm).count(:distinct => true)
- assert_equal 4, Account.includes(:firm).select(:credit_limit).count(:distinct => true)
+ assert_equal 6, Account.includes(:firm).distinct.count
+ assert_equal 4, Account.includes(:firm).distinct.select(:credit_limit).count
end
def test_should_not_perform_joined_include_by_default
@@ -327,7 +332,7 @@ class CalculationsTest < ActiveRecord::TestCase
def test_should_count_scoped_select
Account.update_all("credit_limit = NULL")
- assert_equal 0, Account.all.merge!(:select => "credit_limit").count
+ assert_equal 0, Account.select("credit_limit").count
end
def test_should_count_scoped_select_with_options
@@ -335,28 +340,37 @@ class CalculationsTest < ActiveRecord::TestCase
Account.last.update_columns('credit_limit' => 49)
Account.first.update_columns('credit_limit' => 51)
- assert_equal 1, Account.all.merge!(:select => "credit_limit").where('credit_limit >= 50').count
+ assert_equal 1, Account.select("credit_limit").where('credit_limit >= 50').count
end
def test_should_count_manual_select_with_include
- assert_equal 6, Account.all.merge!(:select => "DISTINCT accounts.id", :includes => :firm).count
+ assert_equal 6, Account.select("DISTINCT accounts.id").includes(:firm).count
end
def test_count_with_column_parameter
assert_equal 5, Account.count(:firm_id)
end
+ def test_count_with_distinct
+ assert_equal 4, Account.select(:credit_limit).distinct.count
+ assert_equal 4, Account.select(:credit_limit).uniq.count
+ end
+
+ def test_count_with_aliased_attribute
+ assert_equal 6, Account.count(:available_credit)
+ end
+
def test_count_with_column_and_options_parameter
assert_equal 2, Account.where("credit_limit = 50 AND firm_id IS NOT NULL").count(:firm_id)
end
def test_should_count_field_in_joined_table
assert_equal 5, Account.joins(:firm).count('companies.id')
- assert_equal 4, Account.joins(:firm).count('companies.id', :distinct => true)
+ assert_equal 4, Account.joins(:firm).distinct.count('companies.id')
end
def test_should_count_field_in_joined_table_with_group_by
- c = Account.all.merge!(:group => 'accounts.firm_id', :joins => :firm).count('companies.id')
+ c = Account.group('accounts.firm_id').joins(:firm).count('companies.id')
[1,6,2,9].each { |firm_id| assert c.keys.include?(firm_id) }
end
@@ -378,6 +392,10 @@ class CalculationsTest < ActiveRecord::TestCase
end
end
+ def test_sum_expression_returns_zero_when_no_records_to_sum
+ assert_equal 0, Account.where('1 = 2').sum("2 * credit_limit")
+ end
+
def test_count_with_from_option
assert_equal Company.count(:all), Company.from('companies').count(:all)
assert_equal Account.where("credit_limit = 50").count(:all),
@@ -386,32 +404,12 @@ class CalculationsTest < ActiveRecord::TestCase
Company.where(:type => "Firm").from('companies').count(:type)
end
- def test_count_with_block_acts_as_array
- accounts = Account.where('id > 0')
- assert_equal Account.count, accounts.count { true }
- assert_equal 0, accounts.count { false }
- assert_equal Account.where('credit_limit > 50').size, accounts.count { |account| account.credit_limit > 50 }
- assert_equal Account.count, Account.count { true }
- assert_equal 0, Account.count { false }
- end
-
- def test_sum_with_block_acts_as_array
- accounts = Account.where('id > 0')
- assert_equal Account.sum(:credit_limit), accounts.sum { |account| account.credit_limit }
- assert_equal Account.sum(:credit_limit) + Account.count, accounts.sum{ |account| account.credit_limit + 1 }
- assert_equal 0, accounts.sum { |account| 0 }
- end
-
def test_sum_with_from_option
assert_equal Account.sum(:credit_limit), Account.from('accounts').sum(:credit_limit)
assert_equal Account.where("credit_limit > 50").sum(:credit_limit),
Account.where("credit_limit > 50").from('accounts').sum(:credit_limit)
end
- def test_sum_array_compatibility
- assert_equal Account.sum(:credit_limit), Account.sum(&:credit_limit)
- 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),
@@ -433,34 +431,19 @@ class CalculationsTest < ActiveRecord::TestCase
def test_maximum_with_not_auto_table_name_prefix_if_column_included
Company.create!(:name => "test", :contracts => [Contract.new(:developer_id => 7)])
- # TODO: Investigate why PG isn't being typecast
- if current_adapter?(:PostgreSQLAdapter) || current_adapter?(:MysqlAdapter)
- assert_equal "7", Company.includes(:contracts).maximum(:developer_id)
- else
- assert_equal 7, Company.includes(:contracts).maximum(:developer_id)
- end
+ assert_equal 7, Company.includes(:contracts).maximum(:developer_id)
end
def test_minimum_with_not_auto_table_name_prefix_if_column_included
Company.create!(:name => "test", :contracts => [Contract.new(:developer_id => 7)])
- # TODO: Investigate why PG isn't being typecast
- if current_adapter?(:PostgreSQLAdapter) || current_adapter?(:MysqlAdapter)
- assert_equal "7", Company.includes(:contracts).minimum(:developer_id)
- else
- assert_equal 7, Company.includes(:contracts).minimum(:developer_id)
- end
+ assert_equal 7, Company.includes(:contracts).minimum(:developer_id)
end
def test_sum_with_not_auto_table_name_prefix_if_column_included
Company.create!(:name => "test", :contracts => [Contract.new(:developer_id => 7)])
- # TODO: Investigate why PG isn't being typecast
- if current_adapter?(:MysqlAdapter) || current_adapter?(:PostgreSQLAdapter)
- assert_equal "7", Company.includes(:contracts).sum(:developer_id)
- else
- assert_equal 7, Company.includes(:contracts).sum(:developer_id)
- end
+ assert_equal 7, Company.includes(:contracts).sum(:developer_id)
end
@@ -481,7 +464,7 @@ class CalculationsTest < ActiveRecord::TestCase
approved_topics_count = Topic.group(:approved).count(:author_name)[true]
assert_equal approved_topics_count, 3
# Count the number of distinct authors for approved Topics
- distinct_authors_for_approved_count = Topic.group(:approved).count(:author_name, :distinct => true)[true]
+ distinct_authors_for_approved_count = Topic.group(:approved).distinct.count(:author_name)[true]
assert_equal distinct_authors_for_approved_count, 2
end
@@ -489,6 +472,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)
@@ -507,6 +495,10 @@ class CalculationsTest < ActiveRecord::TestCase
assert_equal [contract.id], company.contracts.pluck(:id)
end
+ def test_pluck_on_aliased_attribute
+ assert_equal 'The First Topic', Topic.order(:id).pluck(:heading).first
+ end
+
def test_pluck_with_serialization
t = Topic.create!(:content => { :foo => :bar })
assert_equal [{:foo => :bar}], Topic.where(:id => t.id).pluck(:content)
@@ -546,6 +538,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)
@@ -583,4 +580,10 @@ class CalculationsTest < ActiveRecord::TestCase
assert_equal ["Over There"], Possession.pluck(:where)
end
+
+ def test_pluck_replaces_select_clause
+ taks_relation = Topic.select(:approved, :id).order(:id)
+ assert_equal [1,2,3,4], taks_relation.pluck(:id)
+ assert_equal [false, true, true, true], taks_relation.pluck(:approved)
+ end
end
diff --git a/activerecord/test/cases/callbacks_test.rb b/activerecord/test/cases/callbacks_test.rb
index 7457bafd4e..c8f56e3c73 100644
--- a/activerecord/test/cases/callbacks_test.rb
+++ b/activerecord/test/cases/callbacks_test.rb
@@ -43,7 +43,7 @@ class CallbackDeveloper < ActiveRecord::Base
end
class CallbackDeveloperWithFalseValidation < CallbackDeveloper
- before_validation proc { |model| model.history << [:before_validation, :returning_false]; return false }
+ before_validation proc { |model| model.history << [:before_validation, :returning_false]; false }
before_validation proc { |model| model.history << [:before_validation, :should_never_get_here] }
end
@@ -520,7 +520,7 @@ class CallbacksTest < ActiveRecord::TestCase
], david.history
end
- def test_inheritence_of_callbacks
+ def test_inheritance_of_callbacks
parent = ParentDeveloper.new
assert !parent.after_save_called
parent.save
diff --git a/activerecord/test/cases/clone_test.rb b/activerecord/test/cases/clone_test.rb
index d91646efca..5e43082c33 100644
--- a/activerecord/test/cases/clone_test.rb
+++ b/activerecord/test/cases/clone_test.rb
@@ -29,5 +29,12 @@ module ActiveRecord
topic.author_name = 'Aaron'
assert_equal 'Aaron', cloned.author_name
end
+
+ def test_freezing_a_cloned_model_does_not_freeze_clone
+ cloned = Topic.new
+ clone = cloned.clone
+ cloned.freeze
+ assert_not clone.frozen?
+ end
end
end
diff --git a/activerecord/test/cases/coders/yaml_column_test.rb b/activerecord/test/cases/coders/yaml_column_test.rb
index b874adc081..b72c54f97b 100644
--- a/activerecord/test/cases/coders/yaml_column_test.rb
+++ b/activerecord/test/cases/coders/yaml_column_test.rb
@@ -43,10 +43,20 @@ module ActiveRecord
assert_equal [], coder.load([])
end
- def test_load_swallows_yaml_exceptions
+ def test_load_doesnt_swallow_yaml_exceptions
coder = YAMLColumn.new
bad_yaml = '--- {'
- assert_equal bad_yaml, coder.load(bad_yaml)
+ assert_raises(Psych::SyntaxError) do
+ coder.load(bad_yaml)
+ end
+ end
+
+ def test_load_doesnt_handle_undefined_class_or_module
+ coder = YAMLColumn.new
+ missing_class_yaml = '--- !ruby/object:DoesNotExistAndShouldntEver {}\n'
+ assert_raises(ArgumentError) do
+ coder.load(missing_class_yaml)
+ end
end
end
end
diff --git a/activerecord/test/cases/column_definition_test.rb b/activerecord/test/cases/column_definition_test.rb
index bd2fbaa7db..dbb2f223cd 100644
--- a/activerecord/test/cases/column_definition_test.rb
+++ b/activerecord/test/cases/column_definition_test.rb
@@ -8,6 +8,7 @@ module ActiveRecord
def @adapter.native_database_types
{:string => "varchar"}
end
+ @viz = @adapter.schema_creation
end
def test_can_set_coder
@@ -35,25 +36,25 @@ module ActiveRecord
def test_should_not_include_default_clause_when_default_is_null
column = Column.new("title", nil, "varchar(20)")
column_def = ColumnDefinition.new(
- @adapter, column.name, "string",
+ column.name, "string",
column.limit, column.precision, column.scale, column.default, column.null)
- assert_equal "title varchar(20)", column_def.to_sql
+ assert_equal "title varchar(20)", @viz.accept(column_def)
end
def test_should_include_default_clause_when_default_is_present
column = Column.new("title", "Hello", "varchar(20)")
column_def = ColumnDefinition.new(
- @adapter, column.name, "string",
+ column.name, "string",
column.limit, column.precision, column.scale, column.default, column.null)
- assert_equal %Q{title varchar(20) DEFAULT 'Hello'}, column_def.to_sql
+ assert_equal %Q{title varchar(20) DEFAULT 'Hello'}, @viz.accept(column_def)
end
def test_should_specify_not_null_if_null_option_is_false
column = Column.new("title", "Hello", "varchar(20)", false)
column_def = ColumnDefinition.new(
- @adapter, column.name, "string",
+ column.name, "string",
column.limit, column.precision, column.scale, column.default, column.null)
- assert_equal %Q{title varchar(20) DEFAULT 'Hello' NOT NULL}, column_def.to_sql
+ assert_equal %Q{title varchar(20) DEFAULT 'Hello' NOT NULL}, @viz.accept(column_def)
end
if current_adapter?(:MysqlAdapter)
diff --git a/activerecord/test/cases/column_test.rb b/activerecord/test/cases/column_test.rb
index a7b63d15c9..2a6d8cc2ab 100644
--- a/activerecord/test/cases/column_test.rb
+++ b/activerecord/test/cases/column_test.rb
@@ -1,10 +1,14 @@
require "cases/helper"
+require 'models/company'
module ActiveRecord
module ConnectionAdapters
class ColumnTest < ActiveRecord::TestCase
def test_type_cast_boolean
column = Column.new("field", nil, "boolean")
+ assert column.type_cast('').nil?
+ assert column.type_cast(nil).nil?
+
assert column.type_cast(true)
assert column.type_cast(1)
assert column.type_cast('1')
@@ -14,15 +18,21 @@ module ActiveRecord
assert column.type_cast('TRUE')
assert column.type_cast('on')
assert column.type_cast('ON')
- assert !column.type_cast(false)
- assert !column.type_cast(0)
- assert !column.type_cast('0')
- assert !column.type_cast('f')
- assert !column.type_cast('F')
- assert !column.type_cast('false')
- assert !column.type_cast('FALSE')
- assert !column.type_cast('off')
- assert !column.type_cast('OFF')
+
+ # explicitly check for false vs nil
+ assert_equal false, column.type_cast(false)
+ assert_equal false, column.type_cast(0)
+ assert_equal false, column.type_cast('0')
+ assert_equal false, column.type_cast('f')
+ assert_equal false, column.type_cast('F')
+ assert_equal false, column.type_cast('false')
+ assert_equal false, column.type_cast('FALSE')
+ assert_equal false, column.type_cast('off')
+ assert_equal false, column.type_cast('OFF')
+ assert_equal false, column.type_cast(' ')
+ assert_equal false, column.type_cast("\u3000\r\n")
+ assert_equal false, column.type_cast("\u0000")
+ assert_equal false, column.type_cast('SOMETHING RANDOM')
end
def test_type_cast_integer
@@ -33,26 +43,40 @@ module ActiveRecord
assert_equal 0, column.type_cast('bad1')
assert_equal 0, column.type_cast('bad')
assert_equal 1, column.type_cast(1.7)
+ assert_equal 0, column.type_cast(false)
+ assert_equal 1, column.type_cast(true)
assert_nil column.type_cast(nil)
end
def test_type_cast_non_integer_to_integer
column = Column.new("field", nil, "integer")
- assert_raises(NoMethodError) do
- column.type_cast([])
- end
- assert_raises(NoMethodError) do
- column.type_cast(true)
- end
- assert_raises(NoMethodError) do
- column.type_cast(false)
- end
+ assert_nil column.type_cast([1,2])
+ assert_nil column.type_cast({1 => 2})
+ assert_nil column.type_cast((1..2))
+ end
+
+ def test_type_cast_activerecord_to_integer
+ column = Column.new("field", nil, "integer")
+ firm = Firm.create(:name => 'Apple')
+ assert_nil column.type_cast(firm)
+ end
+
+ def test_type_cast_object_without_to_i_to_integer
+ column = Column.new("field", nil, "integer")
+ assert_nil column.type_cast(Object.new)
+ end
+
+ def test_type_cast_nan_and_infinity_to_integer
+ column = Column.new("field", nil, "integer")
+ assert_nil column.type_cast(Float::NAN)
+ assert_nil column.type_cast(1.0/0.0)
end
def test_type_cast_time
column = Column.new("field", nil, "time")
+ assert_equal nil, column.type_cast(nil)
assert_equal nil, column.type_cast('')
- assert_equal nil, column.type_cast(' ')
+ assert_equal nil, column.type_cast('ABC')
time_string = Time.now.utc.strftime("%T")
assert_equal time_string, column.type_cast(time_string).strftime("%T")
@@ -60,8 +84,10 @@ module ActiveRecord
def test_type_cast_datetime_and_timestamp
[Column.new("field", nil, "datetime"), Column.new("field", nil, "timestamp")].each do |column|
+ assert_equal nil, column.type_cast(nil)
assert_equal nil, column.type_cast('')
assert_equal nil, column.type_cast(' ')
+ assert_equal nil, column.type_cast('ABC')
datetime_string = Time.now.utc.strftime("%FT%T")
assert_equal datetime_string, column.type_cast(datetime_string).strftime("%FT%T")
@@ -70,8 +96,10 @@ module ActiveRecord
def test_type_cast_date
column = Column.new("field", nil, "date")
+ assert_equal nil, column.type_cast(nil)
assert_equal nil, column.type_cast('')
- assert_equal nil, column.type_cast(' ')
+ assert_equal nil, column.type_cast(' ')
+ assert_equal nil, column.type_cast('ABC')
date_string = Time.now.utc.strftime("%F")
assert_equal date_string, column.type_cast(date_string).strftime("%F")
@@ -82,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/abstract_adapter_test.rb b/activerecord/test/cases/connection_adapters/abstract_adapter_test.rb
index 3e3d6e2769..eb2fe5639b 100644
--- a/activerecord/test/cases/connection_adapters/abstract_adapter_test.rb
+++ b/activerecord/test/cases/connection_adapters/abstract_adapter_test.rb
@@ -2,6 +2,15 @@ require "cases/helper"
module ActiveRecord
module ConnectionAdapters
+ class ConnectionPool
+ def insert_connection_for_test!(c)
+ synchronize do
+ @connections << c
+ @available.add c
+ end
+ end
+ end
+
class AbstractAdapterTest < ActiveRecord::TestCase
attr_reader :adapter
@@ -10,19 +19,18 @@ module ActiveRecord
end
def test_in_use?
- # FIXME: change to refute in Rails 4.0 / mt
- assert !adapter.in_use?, 'adapter is not in use'
+ assert_not adapter.in_use?, 'adapter is not in use'
assert adapter.lease, 'lease adapter'
assert adapter.in_use?, 'adapter is in use'
end
def test_lease_twice
assert adapter.lease, 'should lease adapter'
- assert !adapter.lease, 'should not lease adapter'
+ assert_not adapter.lease, 'should not lease adapter'
end
def test_last_use
- assert !adapter.last_use
+ assert_not adapter.last_use
adapter.lease
assert adapter.last_use
end
@@ -31,7 +39,7 @@ module ActiveRecord
assert adapter.lease, 'lease adapter'
assert adapter.in_use?, 'adapter is in use'
adapter.expire
- assert !adapter.in_use?, 'adapter is in use'
+ assert_not adapter.in_use?, 'adapter is in use'
end
def test_close
@@ -45,7 +53,7 @@ module ActiveRecord
# Close should put the adapter back in the pool
adapter.close
- assert !adapter.in_use?
+ assert_not adapter.in_use?
assert_equal adapter, pool.connection
end
diff --git a/activerecord/test/cases/connection_adapters/connection_handler_test.rb b/activerecord/test/cases/connection_adapters/connection_handler_test.rb
index 4467ddfc39..3e33b30144 100644
--- a/activerecord/test/cases/connection_adapters/connection_handler_test.rb
+++ b/activerecord/test/cases/connection_adapters/connection_handler_test.rb
@@ -4,11 +4,11 @@ module ActiveRecord
module ConnectionAdapters
class ConnectionHandlerTest < ActiveRecord::TestCase
def setup
- @klass = Class.new { include ActiveRecord::Tag }
- @subklass = Class.new(@klass) { include ActiveRecord::Tag }
+ @klass = Class.new(Base) { def self.name; 'klass'; end }
+ @subklass = Class.new(@klass) { def self.name; 'subklass'; end }
@handler = ConnectionHandler.new
- @handler.establish_connection @klass, Base.connection_pool.spec
+ @pool = @handler.establish_connection(@klass, Base.connection_pool.spec)
end
def test_retrieve_connection
@@ -36,13 +36,17 @@ module ActiveRecord
end
def test_retrieve_connection_pool_uses_superclass_pool_after_subclass_establish_and_remove
- @handler.establish_connection 'north america', Base.connection_pool.spec
- assert_same @handler.retrieve_connection_pool(@klass),
- @handler.retrieve_connection_pool(@subklass)
+ sub_pool = @handler.establish_connection(@subklass, Base.connection_pool.spec)
+ assert_same sub_pool, @handler.retrieve_connection_pool(@subklass)
@handler.remove_connection @subklass
- assert_same @handler.retrieve_connection_pool(@klass),
- @handler.retrieve_connection_pool(@subklass)
+ assert_same @pool, @handler.retrieve_connection_pool(@subklass)
+ end
+
+ def test_connection_pools
+ assert_deprecated do
+ assert_equal({ Base.connection_pool.spec => @pool }, @handler.connection_pools)
+ end
end
end
end
diff --git a/activerecord/test/cases/connection_adapters/schema_cache_test.rb b/activerecord/test/cases/connection_adapters/schema_cache_test.rb
index 541e983758..ecad7c942f 100644
--- a/activerecord/test/cases/connection_adapters/schema_cache_test.rb
+++ b/activerecord/test/cases/connection_adapters/schema_cache_test.rb
@@ -9,49 +9,46 @@ module ActiveRecord
end
def test_primary_key
- assert_equal 'id', @cache.primary_keys['posts']
+ assert_equal 'id', @cache.primary_keys('posts')
end
def test_primary_key_for_non_existent_table
- assert_nil @cache.primary_keys['omgponies']
+ assert_nil @cache.primary_keys('omgponies')
end
def test_caches_columns
- columns = @cache.columns['posts']
- assert_equal columns, @cache.columns['posts']
+ columns = @cache.columns('posts')
+ assert_equal columns, @cache.columns('posts')
end
def test_caches_columns_hash
- columns_hash = @cache.columns_hash['posts']
- assert_equal columns_hash, @cache.columns_hash['posts']
+ columns_hash = @cache.columns_hash('posts')
+ assert_equal columns_hash, @cache.columns_hash('posts')
end
def test_clearing
- @cache.columns['posts']
- @cache.columns_hash['posts']
- @cache.tables['posts']
- @cache.primary_keys['posts']
+ @cache.columns('posts')
+ @cache.columns_hash('posts')
+ @cache.tables('posts')
+ @cache.primary_keys('posts')
@cache.clear!
- assert_equal 0, @cache.columns.size
- assert_equal 0, @cache.columns_hash.size
- assert_equal 0, @cache.tables.size
- assert_equal 0, @cache.primary_keys.size
+ assert_equal 0, @cache.size
end
def test_dump_and_load
- @cache.columns['posts']
- @cache.columns_hash['posts']
- @cache.tables['posts']
- @cache.primary_keys['posts']
+ @cache.columns('posts')
+ @cache.columns_hash('posts')
+ @cache.tables('posts')
+ @cache.primary_keys('posts')
@cache = Marshal.load(Marshal.dump(@cache))
- assert_equal 12, @cache.columns['posts'].size
- assert_equal 12, @cache.columns_hash['posts'].size
- assert @cache.tables['posts']
- assert_equal 'id', @cache.primary_keys['posts']
+ assert_equal 12, @cache.columns('posts').size
+ assert_equal 12, @cache.columns_hash('posts').size
+ assert @cache.tables('posts')
+ assert_equal 'id', @cache.primary_keys('posts')
end
end
diff --git a/activerecord/test/cases/connection_management_test.rb b/activerecord/test/cases/connection_management_test.rb
index fe1b40d884..df17732fff 100644
--- a/activerecord/test/cases/connection_management_test.rb
+++ b/activerecord/test/cases/connection_management_test.rb
@@ -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 0718d0886f..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
@@ -185,7 +186,7 @@ module ActiveRecord
assert_not_nil connection
threads = []
4.times do |i|
- threads << Thread.new(i) do |pool_count|
+ threads << Thread.new(i) do
connection = pool.connection
assert_not_nil connection
connection.close
@@ -327,6 +328,17 @@ module ActiveRecord
def test_pool_sets_connection_visitor
assert @pool.connection.visitor.is_a?(Arel::Visitors::ToSql)
end
+
+ # make sure exceptions are thrown when establish_connection
+ # is called with an anonymous class
+ def test_anonymous_class_exception
+ anonymous = Class.new(ActiveRecord::Base)
+ handler = ActiveRecord::Base.connection_handler
+
+ assert_raises(RuntimeError) {
+ handler.establish_connection anonymous, nil
+ }
+ end
end
end
end
diff --git a/activerecord/test/cases/connection_specification/resolver_test.rb b/activerecord/test/cases/connection_specification/resolver_test.rb
index 434d2b7ba5..c8dfc3244b 100644
--- a/activerecord/test/cases/connection_specification/resolver_test.rb
+++ b/activerecord/test/cases/connection_specification/resolver_test.rb
@@ -8,33 +8,54 @@ module ActiveRecord
Resolver.new(spec, {}).spec.config
end
+ def test_url_invalid_adapter
+ assert_raises(LoadError) do
+ resolve 'ridiculous://foo?encoding=utf8'
+ end
+ 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_host_no_db
- skip "only if mysql is available" unless defined?(MysqlAdapter)
- spec = resolve 'mysql://foo?encoding=utf8'
+ spec = resolve 'abstract://foo?encoding=utf8'
assert_equal({
- :adapter => "mysql",
- :host => "foo",
- :encoding => "utf8" }, spec)
+ adapter: "abstract",
+ host: "foo",
+ encoding: "utf8" }, spec)
end
def test_url_host_db
- skip "only if mysql is available" unless defined?(MysqlAdapter)
- spec = resolve 'mysql://foo/bar?encoding=utf8'
+ spec = resolve 'abstract://foo/bar?encoding=utf8'
assert_equal({
- :adapter => "mysql",
- :database => "bar",
- :host => "foo",
- :encoding => "utf8" }, spec)
+ adapter: "abstract",
+ database: "bar",
+ host: "foo",
+ encoding: "utf8" }, spec)
end
def test_url_port
- skip "only if mysql is available" unless defined?(MysqlAdapter)
- spec = resolve 'mysql://foo:123?encoding=utf8'
+ spec = resolve 'abstract://foo:123?encoding=utf8'
assert_equal({
- :adapter => "mysql",
- :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]
+ end
+
+ def test_descriptive_error_message_when_adapter_is_missing
+ error = assert_raise(LoadError) do
+ resolve(adapter: 'non-existing')
+ end
+
+ assert_match "Could not load 'active_record/connection_adapters/non-existing_adapter'", error.message
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 fc46a249c8..ee3d8a81c2 100644
--- a/activerecord/test/cases/counter_cache_test.rb
+++ b/activerecord/test/cases/counter_cache_test.rb
@@ -51,6 +51,13 @@ class CounterCacheTest < ActiveRecord::TestCase
end
end
+ 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
+
test "reset counters with string argument" do
Topic.increment_counter('replies_count', @topic.id)
@@ -115,10 +122,25 @@ 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
+
+ assert_difference 'joanna.reload.dogs_count', -1 do
+ david.destroy
+ end
+ end
+
test "reset the right counter if two have the same foreign key" do
michael = people(:michael)
assert_nothing_raised(ActiveRecord::StatementInvalid) do
- Person.reset_counters(michael.id, :followers)
+ Person.reset_counters(michael.id, :friends_too)
end
end
@@ -131,4 +153,11 @@ class CounterCacheTest < ActiveRecord::TestCase
Subscriber.reset_counters(subscriber.id, 'books')
end
end
+
+ test "the passed symbol needs to be an association name" do
+ e = assert_raises(ArgumentError) do
+ Topic.reset_counters(@topic.id, :replies_count)
+ end
+ assert_equal "'Topic' has no association called 'replies_count'", e.message
+ end
end
diff --git a/activerecord/test/cases/date_time_test.rb b/activerecord/test/cases/date_time_test.rb
index 3deb0dac99..c0491bbee5 100644
--- a/activerecord/test/cases/date_time_test.rb
+++ b/activerecord/test/cases/date_time_test.rb
@@ -5,18 +5,18 @@ 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(*time_values).utc_offset, 86400)
+ local_offset = Rational(Time.local(*time_values).utc_offset, 86400)
now = DateTime.civil(*(time_values + [local_offset]))
task = Task.new
task.starting = now
task.save!
- # check against Time.local_time, since some platforms will return a Time instead of a DateTime
- assert_equal Time.local_time(*time_values), Task.find(task.id).starting
+ # check against Time.local, since some platforms will return a Time instead of a DateTime
+ assert_equal Time.local(*time_values), Task.find(task.id).starting
end
end
end
diff --git a/activerecord/test/cases/defaults_test.rb b/activerecord/test/cases/defaults_test.rb
index 72f1c99ca0..7e3d91e08c 100644
--- a/activerecord/test/cases/defaults_test.rb
+++ b/activerecord/test/cases/defaults_test.rb
@@ -39,7 +39,32 @@ class DefaultTest < ActiveRecord::TestCase
end
end
-if current_adapter?(:MysqlAdapter) or current_adapter?(:Mysql2Adapter)
+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
# open a new transaction. When in transactional fixtures mode, this will
@@ -51,11 +76,60 @@ if current_adapter?(:MysqlAdapter) or current_adapter?(:Mysql2Adapter)
# We don't want that to happen, so we disable transactional fixtures here.
self.use_transactional_fixtures = false
- # MySQL 5 and higher is quirky with not null text/blob columns.
- # With MySQL Text/blob columns cannot have defaults. If the column is not
- # null MySQL will report that the column has a null default
- # but it behaves as though the column had a default of ''
- def test_mysql_text_not_null_defaults
+ def using_strict(strict)
+ connection = ActiveRecord::Base.remove_connection
+ ActiveRecord::Base.establish_connection connection.merge(strict: strict)
+ yield
+ ensure
+ ActiveRecord::Base.remove_connection
+ ActiveRecord::Base.establish_connection connection
+ end
+
+ # MySQL cannot have defaults on text/blob columns. It reports the
+ # default value as null.
+ #
+ # Despite this, in non-strict mode, MySQL will use an empty string
+ # as the default value of the field, if no other value is
+ # specified.
+ #
+ # Therefore, in non-strict mode, we want column.default to report
+ # an empty string as its default, to be consistent with that.
+ #
+ # In strict mode, column.default should be nil.
+ def test_mysql_text_not_null_defaults_non_strict
+ using_strict(false) do
+ with_text_blob_not_null_table do |klass|
+ assert_equal '', klass.columns_hash['non_null_blob'].default
+ assert_equal '', klass.columns_hash['non_null_text'].default
+
+ assert_nil klass.columns_hash['null_blob'].default
+ assert_nil klass.columns_hash['null_text'].default
+
+ instance = klass.create!
+
+ assert_equal '', instance.non_null_text
+ assert_equal '', instance.non_null_blob
+
+ assert_nil instance.null_text
+ assert_nil instance.null_blob
+ end
+ end
+ end
+
+ def test_mysql_text_not_null_defaults_strict
+ using_strict(true) do
+ with_text_blob_not_null_table do |klass|
+ assert_nil klass.columns_hash['non_null_blob'].default
+ assert_nil klass.columns_hash['non_null_text'].default
+ assert_nil klass.columns_hash['null_blob'].default
+ assert_nil klass.columns_hash['null_text'].default
+
+ assert_raises(ActiveRecord::StatementInvalid) { klass.create }
+ end
+ end
+ end
+
+ def with_text_blob_not_null_table
klass = Class.new(ActiveRecord::Base)
klass.table_name = 'test_mysql_text_not_null_defaults'
klass.connection.create_table klass.table_name do |t|
@@ -64,19 +138,8 @@ if current_adapter?(:MysqlAdapter) or current_adapter?(:Mysql2Adapter)
t.column :null_text, :text, :null => true
t.column :null_blob, :blob, :null => true
end
- assert_equal '', klass.columns_hash['non_null_blob'].default
- assert_equal '', klass.columns_hash['non_null_text'].default
- assert_nil klass.columns_hash['null_blob'].default
- assert_nil klass.columns_hash['null_text'].default
-
- assert_nothing_raised do
- instance = klass.create!
- assert_equal '', instance.non_null_text
- assert_equal '', instance.non_null_blob
- assert_nil instance.null_text
- assert_nil instance.null_blob
- end
+ yield klass
ensure
klass.connection.drop_table(klass.table_name) rescue nil
end
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 dde36e7f72..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 behaviour 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_blank @test_klass.methods.grep(/scoped_by_type/)
- @test_klass.scoped_by_type(nil)
- assert_present @test_klass.methods.grep(/scoped_by_type/)
- 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 7334514f9a..9d7f57bf85 100644
--- a/activerecord/test/cases/dirty_test.rb
+++ b/activerecord/test/cases/dirty_test.rb
@@ -12,7 +12,7 @@ class Pirate # Just reopening it, not defining it
after_update :check_changes
private
- # after_save/update in sweepers, observers, and the model itself
+ # after_save/update and the model itself
# can end up checking dirty status and acting on the results
def check_changes
if self.changed?
@@ -27,6 +27,8 @@ class NumericData < ActiveRecord::Base
end
class DirtyTest < ActiveRecord::TestCase
+ include InTimeZone
+
# Dummy to force column loads so query counts are clean.
def setup
Person.create :first_name => 'foo'
@@ -77,6 +79,8 @@ class DirtyTest < ActiveRecord::TestCase
assert pirate.created_on_changed?
assert_kind_of ActiveSupport::TimeWithZone, pirate.created_on_was
assert_equal old_created_on, pirate.created_on_was
+ pirate.created_on = old_created_on
+ assert !pirate.created_on_changed?
end
end
@@ -121,31 +125,30 @@ class DirtyTest < ActiveRecord::TestCase
end
def test_time_attributes_changes_without_time_zone
+ with_timezone_config aware_attributes: false do
+ target = Class.new(ActiveRecord::Base)
+ target.table_name = 'pirates'
- target = Class.new(ActiveRecord::Base)
- target.table_name = 'pirates'
-
- target.time_zone_aware_attributes = false
-
- # 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
@@ -202,6 +205,22 @@ class DirtyTest < ActiveRecord::TestCase
end
end
+ def test_nullable_datetime_not_marked_as_changed_if_new_value_is_blank
+ in_time_zone 'Edinburgh' do
+ target = Class.new(ActiveRecord::Base)
+ target.table_name = 'topics'
+
+ topic = target.create
+ assert_nil topic.written_on
+
+ ["", nil].each do |value|
+ topic.written_on = value
+ assert_nil topic.written_on
+ assert !topic.written_on_changed?
+ end
+ end
+ end
+
def test_integer_zero_to_string_zero_not_marked_as_changed
pirate = Pirate.new
pirate.parrot_id = 0
@@ -226,6 +245,21 @@ class DirtyTest < ActiveRecord::TestCase
assert !pirate.changed?
end
+ def test_float_zero_to_string_zero_not_marked_as_changed
+ data = NumericData.new :temperature => 0.0
+ data.save!
+
+ assert_not data.changed?
+
+ data.temperature = '0'
+ assert_empty data.changes
+
+ data.temperature = '0.0'
+ assert_empty data.changes
+
+ data.temperature = '0.00'
+ assert_empty data.changes
+ end
def test_zero_to_blank_marked_as_changed
pirate = Pirate.new
@@ -311,12 +345,12 @@ class DirtyTest < ActiveRecord::TestCase
pirate = Pirate.new(:catchphrase => 'foo')
old_updated_on = 1.hour.ago.beginning_of_day
- with_partial_updates Pirate, false do
+ with_partial_writes Pirate, false do
assert_queries(2) { 2.times { pirate.save! } }
Pirate.where(id: pirate.id).update_all(:updated_on => old_updated_on)
end
- with_partial_updates Pirate, true do
+ with_partial_writes Pirate, true do
assert_queries(0) { 2.times { pirate.save! } }
assert_equal old_updated_on, pirate.reload.updated_on
@@ -329,12 +363,12 @@ class DirtyTest < ActiveRecord::TestCase
person = Person.new(:first_name => 'foo')
old_lock_version = 1
- with_partial_updates Person, false do
+ with_partial_writes Person, false do
assert_queries(2) { 2.times { person.save! } }
Person.where(id: person.id).update_all(:first_name => 'baz')
end
- with_partial_updates Person, true do
+ with_partial_writes Person, true do
assert_queries(0) { 2.times { person.save! } }
assert_equal old_lock_version, person.reload.lock_version
@@ -408,8 +442,8 @@ class DirtyTest < ActiveRecord::TestCase
assert !pirate.catchphrase_changed?
end
- def test_save_should_store_serialized_attributes_even_with_partial_updates
- with_partial_updates(Topic) do
+ def test_save_should_store_serialized_attributes_even_with_partial_writes
+ with_partial_writes(Topic) do
topic = Topic.create!(:content => {:a => "a"})
topic.content[:b] = "b"
#assert topic.changed? # Known bug, will fail
@@ -421,7 +455,7 @@ class DirtyTest < ActiveRecord::TestCase
end
def test_save_always_should_update_timestamps_when_serialized_attributes_are_present
- with_partial_updates(Topic) do
+ with_partial_writes(Topic) do
topic = Topic.create!(:content => {:a => "a"})
topic.save!
@@ -434,8 +468,8 @@ class DirtyTest < ActiveRecord::TestCase
end
end
- def test_save_should_not_save_serialized_attribute_with_partial_updates_if_not_present
- with_partial_updates(Topic) do
+ def test_save_should_not_save_serialized_attribute_with_partial_writes_if_not_present
+ with_partial_writes(Topic) do
Topic.create!(:author_name => 'Bill', :content => {:a => "a"})
topic = Topic.select('id, author_name').first
topic.update_columns author_name: 'John'
@@ -502,7 +536,7 @@ class DirtyTest < ActiveRecord::TestCase
assert !pirate.previous_changes.key?('created_on')
pirate = Pirate.find_by_catchphrase("Thar She Blows!")
- pirate.update_attributes(:catchphrase => "Ahoy!")
+ pirate.update(catchphrase: "Ahoy!")
assert_equal 2, pirate.previous_changes.size
assert_equal ["Thar She Blows!", "Ahoy!"], pirate.previous_changes['catchphrase']
@@ -536,23 +570,22 @@ class DirtyTest < ActiveRecord::TestCase
end
end
- def test_setting_time_attributes_with_time_zone_field_to_same_time_should_not_be_marked_as_a_change
+ def test_datetime_attribute_can_be_updated_with_fractional_seconds
in_time_zone 'Paris' do
target = Class.new(ActiveRecord::Base)
- target.table_name = 'pirates'
+ target.table_name = 'topics'
- created_on = Time.now
+ written_on = Time.utc(2012, 12, 1, 12, 0, 0).in_time_zone('Paris')
- pirate = target.create(:created_on => created_on)
- pirate.reload # Here mysql truncate the usec value to 0
+ topic = target.create(:written_on => written_on)
+ topic.written_on += 0.3
- pirate.created_on = created_on
- assert !pirate.created_on_changed?
+ assert topic.written_on_changed?, 'Fractional second update not detected'
end
end
test "partial insert" do
- with_partial_updates Person do
+ with_partial_writes Person do
jon = nil
assert_sql(/first_name/i) do
jon = Person.create! first_name: 'Jon'
@@ -568,7 +601,7 @@ class DirtyTest < ActiveRecord::TestCase
end
test "partial insert with empty values" do
- with_partial_updates Aircraft do
+ with_partial_writes Aircraft do
a = Aircraft.create!
a.reload
assert_not_nil a.id
@@ -576,12 +609,12 @@ class DirtyTest < ActiveRecord::TestCase
end
private
- def with_partial_updates(klass, on = true)
- old = klass.partial_updates?
- klass.partial_updates = on
+ def with_partial_writes(klass, on = true)
+ old = klass.partial_writes?
+ klass.partial_writes = on
yield
ensure
- klass.partial_updates = old
+ klass.partial_writes = old
end
def check_pirate_after_save_failure(pirate)
@@ -590,16 +623,4 @@ class DirtyTest < ActiveRecord::TestCase
assert_equal %w(parrot_id), pirate.changed
assert_nil pirate.parrot_id_was
end
-
- def in_time_zone(zone)
- old_zone = Time.zone
- old_tz = ActiveRecord::Base.time_zone_aware_attributes
-
- Time.zone = zone ? ActiveSupport::TimeZone[zone] : nil
- ActiveRecord::Base.time_zone_aware_attributes = !zone.nil?
- yield
- ensure
- Time.zone = old_zone
- ActiveRecord::Base.time_zone_aware_attributes = old_tz
- end
end
diff --git a/activerecord/test/cases/disconnected_test.rb b/activerecord/test/cases/disconnected_test.rb
new file mode 100644
index 0000000000..1fecfd077e
--- /dev/null
+++ b/activerecord/test/cases/disconnected_test.rb
@@ -0,0 +1,27 @@
+require "cases/helper"
+
+class TestRecord < ActiveRecord::Base
+end
+
+class TestDisconnectedAdapter < ActiveRecord::TestCase
+ self.use_transactional_fixtures = false
+
+ def setup
+ skip "in-memory database mustn't disconnect" if in_memory_db?
+ @connection = ActiveRecord::Base.connection
+ end
+
+ def teardown
+ return if in_memory_db?
+ spec = ActiveRecord::Base.connection_config
+ ActiveRecord::Base.establish_connection(spec)
+ end
+
+ 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
diff --git a/activerecord/test/cases/dup_test.rb b/activerecord/test/cases/dup_test.rb
index 71b2b16608..1e6ccecfab 100644
--- a/activerecord/test/cases/dup_test.rb
+++ b/activerecord/test/cases/dup_test.rb
@@ -107,5 +107,30 @@ module ActiveRecord
assert Topic.after_initialize_called
end
+ def test_dup_validity_is_independent
+ repair_validations(Topic) do
+ Topic.validates_presence_of :title
+ topic = Topic.new("title" => "Literature")
+ topic.valid?
+
+ duped = topic.dup
+ duped.title = nil
+ assert duped.invalid?
+
+ topic.title = nil
+ duped.title = 'Mathematics'
+ assert topic.invalid?
+ assert duped.valid?
+ end
+ end
+
+ def test_dup_with_default_scope
+ prev_default_scopes = Topic.default_scopes
+ Topic.default_scopes = [proc { Topic.where(:approved => true) }]
+ topic = Topic.new(:approved => false)
+ assert !topic.dup.approved?, "should not be overridden by default scopes"
+ ensure
+ Topic.default_scopes = prev_default_scopes
+ end
end
end
diff --git a/activerecord/test/cases/explain_subscriber_test.rb b/activerecord/test/cases/explain_subscriber_test.rb
index b425967678..b00e2744b9 100644
--- a/activerecord/test/cases/explain_subscriber_test.rb
+++ b/activerecord/test/cases/explain_subscriber_test.rb
@@ -1,55 +1,59 @@
require 'cases/helper'
+require 'active_record/explain_subscriber'
+require 'active_record/explain_registry'
if ActiveRecord::Base.connection.supports_explain?
class ExplainSubscriberTest < ActiveRecord::TestCase
SUBSCRIBER = ActiveRecord::ExplainSubscriber.new
- def test_collects_nothing_if_available_queries_for_explain_is_nil
- with_queries(nil) do
- SUBSCRIBER.finish(nil, nil, {})
- assert_nil Thread.current[:available_queries_for_explain]
- end
+ def setup
+ ActiveRecord::ExplainRegistry.reset
+ ActiveRecord::ExplainRegistry.collect = true
end
def test_collects_nothing_if_the_payload_has_an_exception
- with_queries([]) do |queries|
- SUBSCRIBER.finish(nil, nil, :exception => Exception.new)
- assert queries.empty?
- end
+ SUBSCRIBER.finish(nil, nil, exception: Exception.new)
+ assert queries.empty?
end
def test_collects_nothing_for_ignored_payloads
- with_queries([]) do |queries|
- ActiveRecord::ExplainSubscriber::IGNORED_PAYLOADS.each do |ip|
- SUBSCRIBER.finish(nil, nil, :name => ip)
- end
- assert queries.empty?
+ ActiveRecord::ExplainSubscriber::IGNORED_PAYLOADS.each do |ip|
+ SUBSCRIBER.finish(nil, nil, name: ip)
end
+ assert queries.empty?
+ end
+
+ def test_collects_nothing_if_collect_is_false
+ ActiveRecord::ExplainRegistry.collect = false
+ SUBSCRIBER.finish(nil, nil, name: 'SQL', sql: 'select 1 from users', binds: [1, 2])
+ assert queries.empty?
end
def test_collects_pairs_of_queries_and_binds
sql = 'select 1 from users'
binds = [1, 2]
- with_queries([]) do |queries|
- SUBSCRIBER.finish(nil, nil, :name => 'SQL', :sql => sql, :binds => binds)
- assert_equal 1, queries.size
- assert_equal sql, queries[0][0]
- assert_equal binds, queries[0][1]
- end
+ SUBSCRIBER.finish(nil, nil, name: 'SQL', sql: sql, binds: binds)
+ assert_equal 1, queries.size
+ assert_equal sql, queries[0][0]
+ assert_equal binds, queries[0][1]
end
- def test_collects_nothing_if_unexplained_sqls
- with_queries([]) do |queries|
- SUBSCRIBER.finish(nil, nil, :name => 'SQL', :sql => 'SHOW max_identifier_length')
- assert queries.empty?
- end
+ def test_collects_nothing_if_the_statement_is_not_whitelisted
+ SUBSCRIBER.finish(nil, nil, name: 'SQL', sql: 'SHOW max_identifier_length')
+ assert queries.empty?
+ end
+
+ def test_collects_nothing_if_the_statement_is_only_partially_matched
+ SUBSCRIBER.finish(nil, nil, name: 'SQL', sql: 'select_db yo_mama')
+ assert queries.empty?
+ end
+
+ def teardown
+ ActiveRecord::ExplainRegistry.reset
end
- def with_queries(queries)
- Thread.current[:available_queries_for_explain] = queries
- yield queries
- ensure
- Thread.current[:available_queries_for_explain] = nil
+ def queries
+ ActiveRecord::ExplainRegistry.queries
end
end
end
diff --git a/activerecord/test/cases/explain_test.rb b/activerecord/test/cases/explain_test.rb
index 6dce8ccdd1..6dac5db111 100644
--- a/activerecord/test/cases/explain_test.rb
+++ b/activerecord/test/cases/explain_test.rb
@@ -14,50 +14,13 @@ if ActiveRecord::Base.connection.supports_explain?
base.connection
end
- def test_logging_query_plan_with_logger
- base.logger.expects(:warn).with do |message|
- message.starts_with?('EXPLAIN for:')
- end
-
- with_threshold(0) do
- Car.where(:name => 'honda').to_a
- end
- end
-
- def test_logging_query_plan_without_logger
- original = base.logger
- base.logger = nil
-
- class << base.logger
- def warn; raise "Should not be called" end
- end
-
- with_threshold(0) do
- car = Car.where(:name => 'honda').first
- assert_equal 'honda', car.name
- end
- ensure
- base.logger = original
- end
-
- def test_collect_queries_for_explain
- base.auto_explain_threshold_in_seconds = nil
- queries = Thread.current[:available_queries_for_explain] = []
-
- with_threshold(0) do
- Car.where(:name => 'honda').to_a
- end
-
- sql, binds = queries[0]
- assert_match "SELECT", sql
- assert_match "honda", sql
- assert_equal [], binds
- ensure
- Thread.current[:available_queries_for_explain] = nil
+ def test_relation_explain
+ message = Car.where(:name => 'honda').explain
+ assert_match(/^EXPLAIN for:/, message)
end
def test_collecting_queries_for_explain
- result, queries = ActiveRecord::Base.collecting_queries_for_explain do
+ queries = ActiveRecord::Base.collecting_queries_for_explain do
Car.where(:name => 'honda').to_a
end
@@ -65,17 +28,6 @@ if ActiveRecord::Base.connection.supports_explain?
assert_match "SELECT", sql
assert_match "honda", sql
assert_equal [], binds
- assert_equal [cars(:honda)], result
- end
-
- def test_logging_query_plan_when_counting_by_sql
- base.logger.expects(:warn).with do |message|
- message.starts_with?('EXPLAIN for:')
- end
-
- with_threshold(0) do
- Car.count_by_sql "SELECT COUNT(*) FROM cars WHERE name = 'honda'"
- end
end
def test_exec_explain_with_no_binds
@@ -108,20 +60,13 @@ if ActiveRecord::Base.connection.supports_explain?
assert_equal expected, base.exec_explain(queries)
end
- def test_silence_auto_explain
- base.expects(:collecting_sqls_for_explain).never
+ def test_unsupported_connection_adapter
+ connection.stubs(:supports_explain?).returns(false)
+
base.logger.expects(:warn).never
- base.silence_auto_explain do
- with_threshold(0) { Car.all }
- end
- end
- def with_threshold(threshold)
- current_threshold = base.auto_explain_threshold_in_seconds
- base.auto_explain_threshold_in_seconds = threshold
- yield
- ensure
- base.auto_explain_threshold_in_seconds = current_threshold
+ Car.where(:name => 'honda').to_a
end
+
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 d44ac21b05..4188b32731 100644
--- a/activerecord/test/cases/finder_test.rb
+++ b/activerecord/test/cases/finder_test.rb
@@ -11,14 +11,34 @@ require 'models/project'
require 'models/developer'
require 'models/customer'
require 'models/toy'
+require 'models/matey'
class FinderTest < ActiveRecord::TestCase
fixtures :companies, :topics, :entrants, :developers, :developers_projects, :posts, :comments, :accounts, :authors, :customers, :categories, :categorizations
+ def test_find_by_id_with_hash
+ assert_raises(ActiveRecord::StatementInvalid) do
+ Post.find_by_id(:limit => 1)
+ end
+ end
+
+ def test_find_by_title_and_id_with_hash
+ assert_raises(ActiveRecord::StatementInvalid) do
+ Post.find_by_title_and_id('foo', :limit => 1)
+ end
+ end
+
def test_find
assert_equal(topics(:first).title, Topic.find(1).title)
end
+ def test_symbols_table_ref
+ Post.first # warm up
+ x = Symbol.all_symbols.count
+ Post.where("title" => {"xxxqqqq" => "bar"})
+ assert_equal x, Symbol.all_symbols.count
+ end
+
# find should handle strings that come from URLs
# (example: Category.find(params[:id]))
def test_find_with_string
@@ -26,16 +46,19 @@ class FinderTest < ActiveRecord::TestCase
end
def test_exists
- assert Topic.exists?(1)
- assert Topic.exists?("1")
- assert Topic.exists?(:author_name => "David")
- 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
@@ -52,49 +75,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).uniq.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_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_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
@@ -443,7 +479,7 @@ 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
end
@@ -452,7 +488,7 @@ class FinderTest < ActiveRecord::TestCase
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
end
@@ -461,7 +497,7 @@ class FinderTest < ActiveRecord::TestCase
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
end
@@ -470,7 +506,7 @@ class FinderTest < ActiveRecord::TestCase
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
end
@@ -581,7 +617,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
@@ -610,6 +646,11 @@ class FinderTest < ActiveRecord::TestCase
assert_nil Topic.find_by_heading("The First Topic!")
end
+ def test_find_by_one_attribute_bang_with_blank_defined
+ blank_topic = BlankTopic.create(title: "The Blank One")
+ assert_equal blank_topic, BlankTopic.find_by_title!("The Blank One")
+ end
+
def test_find_by_one_attribute_with_conditions
assert_equal accounts(:rails_core_account), Account.where('firm_id = ?', 6).find_by_credit_limit(50)
end
@@ -816,6 +857,14 @@ class FinderTest < ActiveRecord::TestCase
rescue ActiveRecord::RecordNotFound => e
assert_equal 'Couldn\'t find Toy with name=Hello World!', e.message
end
+ ensure
+ 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
@@ -837,11 +886,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/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb
index b0b29f5f42..bffff07089 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,11 +537,6 @@ class CustomConnectionFixturesTest < ActiveRecord::TestCase
fixtures :courses
self.use_transactional_fixtures = false
- def test_connection
- assert_kind_of Course, courses(:ruby)
- assert_equal Course.connection, courses(:ruby).connection
- end
-
def test_leaky_destroy
assert_nothing_raised { courses(:ruby) }
courses(:ruby).destroy
@@ -521,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
@@ -563,7 +618,7 @@ class FixturesBrokenRollbackTest < ActiveRecord::TestCase
end
private
- def load_fixtures
+ def load_fixtures(config)
raise 'argh'
end
end
@@ -573,7 +628,16 @@ class LoadAllFixturesTest < ActiveRecord::TestCase
fixtures :all
def test_all_there
- assert_equal %w(developers people tasks), fixture_table_names.sort
+ assert_equal %w(admin/accounts admin/users developers people tasks), fixture_table_names.sort
+ end
+end
+
+class LoadAllFixturesWithPathnameTest < ActiveRecord::TestCase
+ self.fixture_path = Pathname.new(FIXTURES_ROOT).join('all')
+ fixtures :all
+
+ def test_all_there
+ assert_equal %w(admin/accounts admin/users developers people tasks), fixture_table_names.sort
end
end
diff --git a/activerecord/test/cases/forbidden_attributes_protection_test.rb b/activerecord/test/cases/forbidden_attributes_protection_test.rb
index 9a2172f41e..981a75faf6 100644
--- a/activerecord/test/cases/forbidden_attributes_protection_test.rb
+++ b/activerecord/test/cases/forbidden_attributes_protection_test.rb
@@ -1,6 +1,7 @@
require 'cases/helper'
require 'active_support/core_ext/hash/indifferent_access'
require 'models/person'
+require 'models/company'
class ProtectedParams < ActiveSupport::HashWithIndifferentAccess
attr_accessor :permitted
@@ -40,10 +41,29 @@ class ForbiddenAttributesProtectionTest < ActiveRecord::TestCase
assert_equal 'm', person.gender
end
+ def test_forbidden_attributes_cannot_be_used_for_sti_inheritance_column
+ params = ProtectedParams.new(type: 'Client')
+ assert_raises(ActiveModel::ForbiddenAttributesError) do
+ Company.new(params)
+ end
+ end
+
+ def test_permitted_attributes_can_be_used_for_sti_inheritance_column
+ params = ProtectedParams.new(type: 'Client')
+ params.permit!
+ person = Company.new(params)
+ assert_equal person.class, Client
+ end
+
def test_regular_hash_should_still_be_used_for_mass_assignment
person = Person.new(first_name: 'Guille', gender: 'm')
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 cff6689c15..34e8f1be0f 100644
--- a/activerecord/test/cases/helper.rb
+++ b/activerecord/test/cases/helper.rb
@@ -2,8 +2,7 @@ require File.expand_path('../../../../load_paths', __FILE__)
require 'config'
-gem 'minitest'
-require 'minitest/autorun'
+require 'active_support/testing/autorun'
require 'stringio'
require 'active_record'
@@ -16,14 +15,14 @@ require 'support/connection'
# TODO: Move all these random hacks into the ARTest namespace and into the support/ dir
+Thread.abort_on_exception = true
+
# Show backtraces for deprecated behavior for quicker cleanup.
ActiveSupport::Deprecation.debug = true
# Connect to the database
ARTest.connect
-require 'support/mysql'
-
# Quote "type" if it's a reserved word for the current connection.
QUOTED_TYPE = ActiveRecord::Base.connection.quote_column_name('type')
@@ -50,11 +49,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']
@@ -94,7 +140,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
@@ -120,18 +166,36 @@ class << Time
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
+class SQLSubscriber
+ attr_reader :logged
+ attr_reader :payloads
+
+ def initialize
+ @logged = []
+ @payloads = []
+ end
+
+ def start(name, id, payload)
+ @payloads << payload
+ @logged << [payload[:sql], payload[:name], payload[:binds]]
end
+
+ def finish(name, id, payload); end
end
+
+module InTimeZone
+ private
+
+ def in_time_zone(zone)
+ old_zone = Time.zone
+ old_tz = ActiveRecord::Base.time_zone_aware_attributes
+
+ Time.zone = zone ? ActiveSupport::TimeZone[zone] : nil
+ ActiveRecord::Base.time_zone_aware_attributes = !zone.nil?
+ yield
+ ensure
+ Time.zone = old_zone
+ ActiveRecord::Base.time_zone_aware_attributes = old_tz
+ end
+end
diff --git a/activerecord/test/cases/inclusion_test.rb b/activerecord/test/cases/inclusion_test.rb
deleted file mode 100644
index 8f095e4953..0000000000
--- a/activerecord/test/cases/inclusion_test.rb
+++ /dev/null
@@ -1,133 +0,0 @@
-require 'cases/helper'
-require 'models/teapot'
-
-class BasicInclusionModelTest < ActiveRecord::TestCase
- def test_basic_model
- Teapot.create!(:name => "Ronnie Kemper")
- assert_equal "Ronnie Kemper", Teapot.first.name
- end
-
- def test_initialization
- t = Teapot.new(:name => "Bob")
- assert_equal "Bob", t.name
- end
-
- def test_inherited_model
- teapot = CoolTeapot.create!(:name => "Bob")
- teapot.reload
-
- assert_equal "Bob", teapot.name
- assert_equal "mmm", teapot.aaahhh
- end
-
- def test_generated_feature_methods
- assert Teapot < Teapot::GeneratedFeatureMethods
- end
-
- def test_exists
- t = Teapot.create!(:name => "Ronnie Kemper")
- assert Teapot.exists?(t)
- end
-
- def test_predicate_builder
- t = Teapot.create!(:name => "Bob")
- assert_equal "Bob", Teapot.where(:id => [t]).first.name
- assert_equal "Bob", Teapot.where(:id => t).first.name
- end
-
- def test_nested_model
- assert_equal "ceiling_teapots", Ceiling::Teapot.table_name
- end
-end
-
-class InclusionUnitTest < ActiveRecord::TestCase
- def setup
- @klass = Class.new { include ActiveRecord::Model }
- end
-
- def test_non_abstract_class
- assert !@klass.abstract_class?
- end
-
- def test_abstract_class
- @klass.abstract_class = true
- assert @klass.abstract_class?
- end
-
- def test_establish_connection
- assert @klass.respond_to?(:establish_connection)
- assert ActiveRecord::Model.respond_to?(:establish_connection)
- end
-
- def test_adapter_connection
- name = "#{ActiveRecord::Base.connection_config[:adapter]}_connection"
- assert @klass.respond_to?(name)
- assert ActiveRecord::Model.respond_to?(name)
- end
-
- def test_connection_handler
- assert_equal ActiveRecord::Base.connection_handler, @klass.connection_handler
- end
-
- def test_mirrored_configuration
- ActiveRecord::Base.time_zone_aware_attributes = true
- assert @klass.time_zone_aware_attributes
- ActiveRecord::Base.time_zone_aware_attributes = false
- assert !@klass.time_zone_aware_attributes
- ensure
- ActiveRecord::Base.time_zone_aware_attributes = false
- end
-
- # Doesn't really test anything, but this is here to ensure warnings don't occur
- def test_included_twice
- @klass.send :include, ActiveRecord::Model
- end
-
- def test_deprecation_proxy
- proxy = ActiveRecord::Model::DeprecationProxy.new
-
- assert_equal ActiveRecord::Model.name, proxy.name
- assert_equal ActiveRecord::Base.superclass, assert_deprecated { proxy.superclass }
-
- sup, sup2 = nil, nil
- ActiveSupport.on_load(:__test_active_record_model_deprecation) do
- sup = superclass
- sup2 = send(:superclass)
- end
- assert_deprecated do
- ActiveSupport.run_load_hooks(:__test_active_record_model_deprecation, proxy)
- end
- assert_equal ActiveRecord::Base.superclass, sup
- assert_equal ActiveRecord::Base.superclass, sup2
- end
-
- test "including in deprecation proxy" do
- model, base = ActiveRecord::Model.dup, ActiveRecord::Base.dup
- proxy = ActiveRecord::Model::DeprecationProxy.new(model, base)
-
- mod = Module.new
- proxy.include mod
- assert model < mod
- end
-
- test "extending in deprecation proxy" do
- model, base = ActiveRecord::Model.dup, ActiveRecord::Base.dup
- proxy = ActiveRecord::Model::DeprecationProxy.new(model, base)
-
- mod = Module.new
- assert_deprecated { proxy.extend mod }
- assert base.singleton_class < mod
- end
-end
-
-class InclusionFixturesTest < ActiveRecord::TestCase
- fixtures :teapots
-
- def test_fixtured_record
- assert_equal "Bob", teapots(:bob).name
- end
-
- def test_timestamped_fixture
- assert_not_nil teapots(:bob).created_at
- end
-end
diff --git a/activerecord/test/cases/inheritance_test.rb b/activerecord/test/cases/inheritance_test.rb
index 8fded9159f..73cf99a5d7 100644
--- a/activerecord/test/cases/inheritance_test.rb
+++ b/activerecord/test/cases/inheritance_test.rb
@@ -4,7 +4,6 @@ require 'models/person'
require 'models/post'
require 'models/project'
require 'models/subscriber'
-require 'models/teapot'
require 'models/vegetables'
class InheritanceTest < ActiveRecord::TestCase
@@ -69,6 +68,7 @@ class InheritanceTest < ActiveRecord::TestCase
end
def test_company_descends_from_active_record
+ assert !ActiveRecord::Base.descends_from_active_record?
assert AbstractCompany.descends_from_active_record?, 'AbstractCompany should descend from ActiveRecord::Base'
assert Company.descends_from_active_record?, 'Company should descend from ActiveRecord::Base'
assert !Class.new(Company).descends_from_active_record?, 'Company subclass should not descend from ActiveRecord::Base'
@@ -81,10 +81,6 @@ class InheritanceTest < ActiveRecord::TestCase
assert_equal SubStiPost, SubStiPost.base_class
end
- def test_active_record_model_included_base_class
- assert_equal Teapot, Teapot.base_class
- end
-
def test_abstract_inheritance_base_class
assert_equal LoosePerson, LoosePerson.base_class
assert_equal LooseDescendant, LooseDescendant.base_class
@@ -93,11 +89,7 @@ class InheritanceTest < ActiveRecord::TestCase
end
def test_base_class_activerecord_error
- klass = Class.new {
- extend ActiveRecord::Configuration
- include ActiveRecord::Inheritance
- }
-
+ klass = Class.new { include ActiveRecord::Inheritance }
assert_raise(ActiveRecord::ActiveRecordError) { klass.base_class }
end
@@ -165,6 +157,58 @@ class InheritanceTest < ActiveRecord::TestCase
assert_kind_of Cabbage, savoy
end
+ def test_inheritance_new_with_default_class
+ company = Company.new
+ assert_equal Company, company.class
+ end
+
+ def test_inheritance_new_with_base_class
+ company = Company.new(:type => 'Company')
+ assert_equal Company, company.class
+ end
+
+ def test_inheritance_new_with_subclass
+ firm = Company.new(:type => 'Firm')
+ assert_equal Firm, firm.class
+ end
+
+ def test_new_with_abstract_class
+ e = assert_raises(NotImplementedError) do
+ AbstractCompany.new
+ end
+ assert_equal("AbstractCompany is an abstract class and can not 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)
+ end
+
+ def test_new_with_invalid_type
+ assert_raise(ActiveRecord::SubclassNotFound) { Company.new(:type => 'InvalidType') }
+ end
+
+ def test_new_with_unrelated_type
+ assert_raise(ActiveRecord::SubclassNotFound) { Company.new(:type => 'Account') }
+ end
+
+ def test_new_with_complex_inheritance
+ assert_nothing_raised { Client.new(type: 'VerySpecialClient') }
+ end
+
+ def test_new_with_autoload_paths
+ path = File.expand_path('../../models/autoloadable', __FILE__)
+ ActiveSupport::Dependencies.autoload_paths << path
+
+ firm = Company.new(:type => 'ExtraFirm')
+ assert_equal ExtraFirm, firm.class
+ ensure
+ ActiveSupport::Dependencies.autoload_paths.reject! { |p| p == path }
+ ActiveSupport::Dependencies.clear
+ end
+
def test_inheritance_condition
assert_equal 10, Company.count
assert_equal 2, Firm.count
@@ -269,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
new file mode 100644
index 0000000000..406aacb056
--- /dev/null
+++ b/activerecord/test/cases/integration_test.rb
@@ -0,0 +1,84 @@
+require 'cases/helper'
+require 'models/company'
+require 'models/developer'
+require 'models/car'
+require 'models/bulb'
+
+class IntegrationTest < ActiveRecord::TestCase
+ fixtures :companies, :developers
+
+ def test_to_param_should_return_string
+ assert_kind_of String, Client.first.to_param
+ end
+
+ def test_to_param_returns_nil_if_not_persisted
+ client = Client.new
+ assert_equal nil, client.to_param
+ end
+
+ def test_to_param_returns_id_if_not_persisted_but_id_is_set
+ client = Client.new
+ client.id = 1
+ assert_equal '1', client.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
+ dev = Developer.first
+ assert_equal "developers/#{dev.id}-#{dev.updated_at.utc.to_s(:nsec)}", dev.cache_key
+ end
+
+ def test_cache_key_format_for_existing_record_with_updated_at_and_custom_cache_timestamp_format
+ dev = CachedDeveloper.first
+ assert_equal "cached_developers/#{dev.id}-#{dev.updated_at.utc.to_s(:number)}", dev.cache_key
+ end
+
+ def test_cache_key_changes_when_child_touched
+ car = Car.create
+ Bulb.create(car: car)
+
+ key = car.cache_key
+ car.bulb.touch
+ car.reload
+ assert_not_equal key, car.cache_key
+ end
+
+ def test_cache_key_format_for_existing_record_with_nil_updated_timestamps
+ dev = Developer.first
+ dev.update_columns(updated_at: nil, updated_on: nil)
+ assert_match(/\/#{dev.id}$/, dev.cache_key)
+ end
+
+ def test_cache_key_for_updated_on
+ dev = Developer.first
+ dev.updated_at = nil
+ assert_equal "developers/#{dev.id}-#{dev.updated_on.utc.to_s(:nsec)}", dev.cache_key
+ end
+
+ def test_cache_key_for_newer_updated_at
+ dev = Developer.first
+ dev.updated_at += 3600
+ assert_equal "developers/#{dev.id}-#{dev.updated_at.utc.to_s(:nsec)}", dev.cache_key
+ end
+
+ def test_cache_key_for_newer_updated_on
+ dev = Developer.first
+ dev.updated_on += 3600
+ assert_equal "developers/#{dev.id}-#{dev.updated_on.utc.to_s(:nsec)}", dev.cache_key
+ end
+
+ def test_cache_key_format_is_precise_enough
+ dev = Developer.first
+ key = dev.cache_key
+ dev.touch
+ assert_not_equal key, dev.cache_key
+ 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..f2d8f18ec7
--- /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}(no database connection)", Bird.inspect
+ end
+end
diff --git a/activerecord/test/cases/invertible_migration_test.rb b/activerecord/test/cases/invertible_migration_test.rb
index 8f1cdd47ea..428145d00b 100644
--- a/activerecord/test/cases/invertible_migration_test.rb
+++ b/activerecord/test/cases/invertible_migration_test.rb
@@ -17,6 +17,37 @@ module ActiveRecord
end
end
+ class InvertibleRevertMigration < SilentMigration
+ def change
+ revert do
+ create_table("horses") do |t|
+ t.column :content, :text
+ t.column :remind_at, :datetime
+ end
+ end
+ end
+ end
+
+ class InvertibleByPartsMigration < SilentMigration
+ attr_writer :test
+ def change
+ create_table("new_horses") do |t|
+ t.column :breed, :string
+ end
+ reversible do |dir|
+ @test.yield :both
+ dir.up { @test.yield :up }
+ dir.down { @test.yield :down }
+ end
+ revert do
+ create_table("horses") do |t|
+ t.column :content, :text
+ t.column :remind_at, :datetime
+ end
+ end
+ end
+ end
+
class NonInvertibleMigration < SilentMigration
def change
create_table("horses") do |t|
@@ -27,6 +58,24 @@ module ActiveRecord
end
end
+ class RemoveIndexMigration1 < SilentMigration
+ def self.up
+ create_table("horses") do |t|
+ t.column :name, :string
+ t.column :color, :string
+ t.index [:name, :color]
+ end
+ end
+ end
+
+ class RemoveIndexMigration2 < SilentMigration
+ def change
+ change_table("horses") do |t|
+ t.remove_index [:name, :color]
+ end
+ end
+ end
+
class LegacyMigration < ActiveRecord::Migration
def self.up
create_table("horses") do |t|
@@ -40,9 +89,28 @@ module ActiveRecord
end
end
+ class RevertWholeMigration < SilentMigration
+ def initialize(name = self.class.name, version = nil, migration)
+ @migration = migration
+ super(name, version)
+ end
+
+ def change
+ revert @migration
+ end
+ end
+
+ class NestedRevertWholeMigration < RevertWholeMigration
+ def change
+ revert { super }
+ end
+ end
+
def teardown
- if ActiveRecord::Base.connection.table_exists?("horses")
- ActiveRecord::Base.connection.drop_table("horses")
+ %w[horses new_horses].each do |table|
+ if ActiveRecord::Base.connection.table_exists?(table)
+ ActiveRecord::Base.connection.drop_table(table)
+ end
end
end
@@ -54,6 +122,16 @@ module ActiveRecord
end
end
+ def test_exception_on_removing_index_without_column_option
+ RemoveIndexMigration1.new.migrate(:up)
+ migration = RemoveIndexMigration2.new
+ migration.migrate(:up)
+
+ assert_raises(IrreversibleMigration) do
+ migration.migrate(:down)
+ end
+ end
+
def test_migrate_up
migration = InvertibleMigration.new
migration.migrate(:up)
@@ -67,6 +145,83 @@ module ActiveRecord
assert !migration.connection.table_exists?("horses")
end
+ def test_migrate_revert
+ migration = InvertibleMigration.new
+ revert = InvertibleRevertMigration.new
+ migration.migrate :up
+ revert.migrate :up
+ assert !migration.connection.table_exists?("horses")
+ revert.migrate :down
+ assert migration.connection.table_exists?("horses")
+ migration.migrate :down
+ assert !migration.connection.table_exists?("horses")
+ end
+
+ def test_migrate_revert_by_part
+ InvertibleMigration.new.migrate :up
+ received = []
+ migration = InvertibleByPartsMigration.new
+ migration.test = ->(dir){
+ assert migration.connection.table_exists?("horses")
+ assert migration.connection.table_exists?("new_horses")
+ received << dir
+ }
+ migration.migrate :up
+ assert_equal [:both, :up], received
+ assert !migration.connection.table_exists?("horses")
+ assert migration.connection.table_exists?("new_horses")
+ migration.migrate :down
+ assert_equal [:both, :up, :both, :down], received
+ assert migration.connection.table_exists?("horses")
+ assert !migration.connection.table_exists?("new_horses")
+ end
+
+ def test_migrate_revert_whole_migration
+ migration = InvertibleMigration.new
+ [LegacyMigration, InvertibleMigration].each do |klass|
+ revert = RevertWholeMigration.new(klass)
+ migration.migrate :up
+ revert.migrate :up
+ assert !migration.connection.table_exists?("horses")
+ revert.migrate :down
+ assert migration.connection.table_exists?("horses")
+ migration.migrate :down
+ assert !migration.connection.table_exists?("horses")
+ end
+ end
+
+ def test_migrate_nested_revert_whole_migration
+ revert = NestedRevertWholeMigration.new(InvertibleRevertMigration)
+ revert.migrate :down
+ assert revert.connection.table_exists?("horses")
+ revert.migrate :up
+ assert !revert.connection.table_exists?("horses")
+ end
+
+ def test_revert_order
+ block = Proc.new{|t| t.string :name }
+ recorder = ActiveRecord::Migration::CommandRecorder.new(ActiveRecord::Base.connection)
+ recorder.instance_eval do
+ create_table("apples", &block)
+ revert do
+ create_table("bananas", &block)
+ revert do
+ create_table("clementines")
+ create_table("dates")
+ end
+ create_table("elderberries")
+ end
+ revert do
+ create_table("figs")
+ create_table("grapes")
+ end
+ end
+ assert_equal [[:create_table, ["apples"], block], [:drop_table, ["elderberries"], nil],
+ [:create_table, ["clementines"], nil], [:create_table, ["dates"], nil],
+ [:drop_table, ["bananas"], block], [:drop_table, ["grapes"], nil],
+ [:drop_table, ["figs"], nil]], recorder.commands
+ end
+
def test_legacy_up
LegacyMigration.migrate :up
assert ActiveRecord::Base.connection.table_exists?("horses"), "horses should exist"
diff --git a/activerecord/test/cases/json_serialization_test.rb b/activerecord/test/cases/json_serialization_test.rb
index a86b165c78..a222675918 100644
--- a/activerecord/test/cases/json_serialization_test.rb
+++ b/activerecord/test/cases/json_serialization_test.rb
@@ -6,7 +6,21 @@ require 'models/tagging'
require 'models/tag'
require 'models/comment'
+module JsonSerializationHelpers
+ private
+
+ def set_include_root_in_json(value)
+ original_root_in_json = ActiveRecord::Base.include_root_in_json
+ ActiveRecord::Base.include_root_in_json = value
+ yield
+ ensure
+ ActiveRecord::Base.include_root_in_json = original_root_in_json
+ end
+end
+
class JsonSerializationTest < ActiveRecord::TestCase
+ include JsonSerializationHelpers
+
class NamespacedContact < Contact
column :name, :string
end
@@ -23,20 +37,24 @@ class JsonSerializationTest < ActiveRecord::TestCase
end
def test_should_demodulize_root_in_json
- @contact = NamespacedContact.new :name => 'whatever'
- json = @contact.to_json
- assert_match %r{^\{"namespaced_contact":\{}, json
+ set_include_root_in_json(true) do
+ @contact = NamespacedContact.new name: 'whatever'
+ json = @contact.to_json
+ assert_match %r{^\{"namespaced_contact":\{}, json
+ end
end
def test_should_include_root_in_json
- json = @contact.to_json
-
- assert_match %r{^\{"contact":\{}, json
- assert_match %r{"name":"Konata Izumi"}, json
- assert_match %r{"age":16}, json
- assert json.include?(%("created_at":#{ActiveSupport::JSON.encode(Time.utc(2006, 8, 1))}))
- assert_match %r{"awesome":true}, json
- assert_match %r{"preferences":\{"shows":"anime"\}}, json
+ set_include_root_in_json(true) do
+ json = @contact.to_json
+
+ assert_match %r{^\{"contact":\{}, json
+ assert_match %r{"name":"Konata Izumi"}, json
+ assert_match %r{"age":16}, json
+ assert json.include?(%("created_at":#{ActiveSupport::JSON.encode(Time.utc(2006, 8, 1))}))
+ assert_match %r{"awesome":true}, json
+ assert_match %r{"preferences":\{"shows":"anime"\}}, json
+ end
end
def test_should_encode_all_encodable_attributes
@@ -141,6 +159,8 @@ end
class DatabaseConnectedJsonEncodingTest < ActiveRecord::TestCase
fixtures :authors, :posts, :comments, :tags, :taggings
+ include JsonSerializationHelpers
+
def setup
@david = authors(:david)
@mary = authors(:mary)
@@ -227,23 +247,21 @@ class DatabaseConnectedJsonEncodingTest < ActiveRecord::TestCase
end
def test_should_allow_only_option_for_list_of_authors
- ActiveRecord::Base.include_root_in_json = false
- authors = [@david, @mary]
- assert_equal %([{"name":"David"},{"name":"Mary"}]), ActiveSupport::JSON.encode(authors, :only => :name)
- ensure
- ActiveRecord::Base.include_root_in_json = true
+ set_include_root_in_json(false) do
+ authors = [@david, @mary]
+ assert_equal %([{"name":"David"},{"name":"Mary"}]), ActiveSupport::JSON.encode(authors, only: :name)
+ end
end
def test_should_allow_except_option_for_list_of_authors
- ActiveRecord::Base.include_root_in_json = false
- authors = [@david, @mary]
- encoded = ActiveSupport::JSON.encode(authors, :except => [
- :name, :author_address_id, :author_address_extra_id,
- :organization_id, :owned_essay_id
- ])
- assert_equal %([{"id":1},{"id":2}]), encoded
- ensure
- ActiveRecord::Base.include_root_in_json = true
+ set_include_root_in_json(false) do
+ authors = [@david, @mary]
+ encoded = ActiveSupport::JSON.encode(authors, except: [
+ :name, :author_address_id, :author_address_extra_id,
+ :organization_id, :owned_essay_id
+ ])
+ assert_equal %([{"id":1},{"id":2}]), encoded
+ end
end
def test_should_allow_includes_for_list_of_authors
@@ -262,17 +280,21 @@ class DatabaseConnectedJsonEncodingTest < ActiveRecord::TestCase
end
def test_should_allow_options_for_hash_of_authors
- authors_hash = {
- 1 => @david,
- 2 => @mary
- }
- assert_equal %({"1":{"author":{"name":"David"}}}), ActiveSupport::JSON.encode(authors_hash, :only => [1, :name])
+ set_include_root_in_json(true) do
+ authors_hash = {
+ 1 => @david,
+ 2 => @mary
+ }
+ assert_equal %({"1":{"author":{"name":"David"}}}), ActiveSupport::JSON.encode(authors_hash, only: [1, :name])
+ end
end
def test_should_be_able_to_encode_relation
- authors_relation = Author.where(:id => [@david.id, @mary.id])
+ set_include_root_in_json(true) do
+ authors_relation = Author.where(id: [@david.id, @mary.id])
- json = ActiveSupport::JSON.encode authors_relation, :only => :name
- assert_equal '[{"author":{"name":"David"}},{"author":{"name":"Mary"}}]', json
+ json = ActiveSupport::JSON.encode authors_relation, only: :name
+ assert_equal '[{"author":{"name":"David"}},{"author":{"name":"Mary"}}]', json
+ end
end
end
diff --git a/activerecord/test/cases/lifecycle_test.rb b/activerecord/test/cases/lifecycle_test.rb
deleted file mode 100644
index 0b78f2e46b..0000000000
--- a/activerecord/test/cases/lifecycle_test.rb
+++ /dev/null
@@ -1,256 +0,0 @@
-require 'cases/helper'
-require 'models/topic'
-require 'models/developer'
-require 'models/reply'
-require 'models/minimalistic'
-require 'models/comment'
-
-class SpecialDeveloper < Developer; end
-
-class DeveloperObserver < ActiveRecord::Observer
- def calls
- @calls ||= []
- end
-
- def before_save(developer)
- calls << developer
- end
-end
-
-class SalaryChecker < ActiveRecord::Observer
- observe :special_developer
- attr_accessor :last_saved
-
- def before_save(developer)
- return developer.salary > 80000
- end
-
- module Implementation
- def after_save(developer)
- self.last_saved = developer
- end
- end
- include Implementation
-
-end
-
-class TopicaAuditor < ActiveRecord::Observer
- observe :topic
-
- attr_reader :topic
-
- def after_find(topic)
- @topic = topic
- end
-end
-
-class TopicObserver < ActiveRecord::Observer
- attr_reader :topic
-
- def after_find(topic)
- @topic = topic
- end
-
- # Create an after_save callback, so a notify_observer hook is created
- # on :topic.
- def after_save(nothing)
- end
-end
-
-class MinimalisticObserver < ActiveRecord::Observer
- attr_reader :minimalistic
-
- def after_find(minimalistic)
- @minimalistic = minimalistic
- end
-end
-
-class MultiObserver < ActiveRecord::Observer
- attr_reader :record
-
- def self.observed_class() [ Topic, Developer ] end
-
- cattr_reader :last_inherited
- @@last_inherited = nil
-
- def observed_class_inherited_with_testing(subclass)
- observed_class_inherited_without_testing(subclass)
- @@last_inherited = subclass
- end
-
- alias_method_chain :observed_class_inherited, :testing
-
- def after_find(record)
- @record = record
- end
-end
-
-class ValidatedComment < Comment
- attr_accessor :callers
-
- before_validation :record_callers
-
- after_validation do
- record_callers
- end
-
- def record_callers
- callers << self.class if callers
- end
-end
-
-class ValidatedCommentObserver < ActiveRecord::Observer
- attr_accessor :callers
-
- def after_validation(model)
- callers << self.class if callers
- end
-end
-
-
-class AroundTopic < Topic
-end
-
-class AroundTopicObserver < ActiveRecord::Observer
- observe :around_topic
- def topic_ids
- @topic_ids ||= []
- end
-
- def around_save(topic)
- topic_ids << topic.id
- yield(topic)
- topic_ids << topic.id
- end
-end
-
-class LifecycleTest < ActiveRecord::TestCase
- fixtures :topics, :developers, :minimalistics
-
- def test_before_destroy
- topic = Topic.find(1)
- assert_difference 'Topic.count', -(1 + topic.replies.size) do
- topic.destroy
- end
- end
-
- def test_auto_observer
- topic_observer = TopicaAuditor.instance
- assert_nil TopicaAuditor.observed_class
- assert_equal [Topic], TopicaAuditor.observed_classes.to_a
-
- topic = Topic.find(1)
- assert_equal topic.title, topic_observer.topic.title
- end
-
- def test_inferred_auto_observer
- topic_observer = TopicObserver.instance
- assert_equal Topic, TopicObserver.observed_class
-
- topic = Topic.find(1)
- assert_equal topic.title, topic_observer.topic.title
- end
-
- def test_observing_two_classes
- multi_observer = MultiObserver.instance
-
- topic = Topic.find(1)
- assert_equal topic.title, multi_observer.record.title
-
- developer = Developer.find(1)
- assert_equal developer.name, multi_observer.record.name
- end
-
- def test_observing_subclasses
- multi_observer = MultiObserver.instance
-
- developer = SpecialDeveloper.find(1)
- assert_equal developer.name, multi_observer.record.name
-
- klass = Class.new(Developer)
- assert_equal klass, multi_observer.last_inherited
-
- developer = klass.find(1)
- assert_equal developer.name, multi_observer.record.name
- end
-
- def test_after_find_can_be_observed_when_its_not_defined_on_the_model
- observer = MinimalisticObserver.instance
- assert_equal Minimalistic, MinimalisticObserver.observed_class
-
- minimalistic = Minimalistic.find(1)
- assert_equal minimalistic, observer.minimalistic
- end
-
- def test_after_find_can_be_observed_when_its_defined_on_the_model
- observer = TopicObserver.instance
- assert_equal Topic, TopicObserver.observed_class
-
- topic = Topic.find(1)
- assert_equal topic, observer.topic
- end
-
- def test_invalid_observer
- assert_raise(ArgumentError) { Topic.observers = Object.new; Topic.instantiate_observers }
- end
-
- test "model callbacks fire before observers are notified" do
- callers = []
-
- comment = ValidatedComment.new
- comment.callers = ValidatedCommentObserver.instance.callers = callers
-
- comment.valid?
- assert_equal [ValidatedComment, ValidatedComment, ValidatedCommentObserver], callers,
- "model callbacks did not fire before observers were notified"
- end
-
- test "able to save developer" do
- SalaryChecker.instance # activate
- developer = SpecialDeveloper.new :name => 'Roger', :salary => 100000
- assert developer.save, "developer with normal salary failed to save"
- end
-
- test "unable to save developer with low salary" do
- SalaryChecker.instance # activate
- developer = SpecialDeveloper.new :name => 'Rookie', :salary => 50000
- assert !developer.save, "allowed to save a developer with too low salary"
- end
-
- test "able to call methods defined with included module" do # https://rails.lighthouseapp.com/projects/8994/tickets/6065-activerecordobserver-is-not-aware-of-method-added-by-including-modules
- SalaryChecker.instance # activate
- developer = SpecialDeveloper.create! :name => 'Roger', :salary => 100000
- assert_equal developer, SalaryChecker.instance.last_saved
- end
-
- test "around filter from observer should accept block" do
- observer = AroundTopicObserver.instance
- topic = AroundTopic.new
- topic.save
- assert_nil observer.topic_ids.first
- assert_not_nil observer.topic_ids.last
- end
-
- test "able to disable observers" do
- observer = DeveloperObserver.instance # activate
- observer.calls.clear
-
- ActiveRecord::Base.observers.disable DeveloperObserver do
- Developer.create! :name => 'Ancestor', :salary => 100000
- SpecialDeveloper.create! :name => 'Descendent', :salary => 100000
- end
-
- assert_equal [], observer.calls
- end
-
- def test_observer_is_called_once
- observer = DeveloperObserver.instance # activate
- observer.calls.clear
-
- developer = Developer.create! :name => 'Ancestor', :salary => 100000
- special_developer = SpecialDeveloper.create! :name => 'Descendent', :salary => 100000
-
- assert_equal [developer, special_developer], observer.calls
- end
-
-end
diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb
index 2392516395..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")
@@ -193,11 +207,19 @@ class OptimisticLockingTest < ActiveRecord::TestCase
def test_lock_without_default_sets_version_to_zero
t1 = LockWithoutDefault.new
assert_equal 0, t1.lock_version
+
+ t1.save
+ t1 = LockWithoutDefault.find(t1.id)
+ assert_equal 0, t1.lock_version
end
def test_lock_with_custom_column_without_default_sets_version_to_zero
t1 = LockWithCustomColumnWithoutDefault.new
assert_equal 0, t1.custom_lock_version
+
+ t1.save
+ t1 = LockWithCustomColumnWithoutDefault.find(t1.id)
+ assert_equal 0, t1.custom_lock_version
end
def test_readonly_attributes
@@ -207,7 +229,7 @@ class OptimisticLockingTest < ActiveRecord::TestCase
s.reload
assert_equal "unchangeable name", s.name
- s.update_attributes(:name => "changed name")
+ s.update(name: "changed name")
s.reload
assert_equal "unchangeable name", s.name
end
@@ -234,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
@@ -250,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
@@ -341,9 +367,6 @@ end
# is so cumbersome. Will deadlock Ruby threads if the underlying db.execute
# blocks, so separate script called by Kernel#system is needed.
# (See exec vs. async_exec in the PostgreSQL adapter.)
-
-# TODO: The Sybase, and OpenBase adapters currently have no support for pessimistic locking
-
unless current_adapter?(:SybaseAdapter, :OpenBaseAdapter) || in_memory_db?
class PessimisticLockingTest < ActiveRecord::TestCase
self.use_transactional_fixtures = false
diff --git a/activerecord/test/cases/log_subscriber_test.rb b/activerecord/test/cases/log_subscriber_test.rb
index 70d00aecf9..3bdc5a1302 100644
--- a/activerecord/test/cases/log_subscriber_test.rb
+++ b/activerecord/test/cases/log_subscriber_test.rb
@@ -1,4 +1,5 @@
require "cases/helper"
+require "models/binary"
require "models/developer"
require "models/post"
require "active_support/log_subscriber/test_helper"
@@ -7,6 +8,19 @@ class LogSubscriberTest < ActiveRecord::TestCase
include ActiveSupport::LogSubscriber::TestHelper
include ActiveSupport::Logger::Severity
+ class TestDebugLogSubscriber < ActiveRecord::LogSubscriber
+ attr_reader :debugs
+
+ def initialize
+ @debugs = []
+ super
+ end
+
+ def debug message
+ @debugs << message
+ end
+ end
+
fixtures :posts
def setup
@@ -29,30 +43,34 @@ class LogSubscriberTest < ActiveRecord::TestCase
def test_schema_statements_are_ignored
event = Struct.new(:duration, :payload)
- logger = Class.new(ActiveRecord::LogSubscriber) {
- attr_accessor :debugs
-
- def initialize
- @debugs = []
- super
- end
-
- def debug message
- @debugs << message
- end
- }.new
+ logger = TestDebugLogSubscriber.new
assert_equal 0, logger.debugs.length
- logger.sql(event.new(0, { :sql => 'hi mom!' }))
+ logger.sql(event.new(0, sql: 'hi mom!'))
assert_equal 1, logger.debugs.length
- logger.sql(event.new(0, { :sql => 'hi mom!', :name => 'foo' }))
+ logger.sql(event.new(0, sql: 'hi mom!', name: 'foo'))
assert_equal 2, logger.debugs.length
- logger.sql(event.new(0, { :sql => 'hi mom!', :name => 'SCHEMA' }))
+ logger.sql(event.new(0, sql: 'hi mom!', name: 'SCHEMA'))
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)
+
+ logger = TestDebugLogSubscriber.new
+ logger.sql(event.new(0, sql: 'hi mom!', binds: [[nil, 1]]))
+ assert_equal 1, logger.debugs.length
+ end
+
def test_basic_query_logging
Developer.all.load
wait
@@ -100,4 +118,12 @@ class LogSubscriberTest < ActiveRecord::TestCase
def test_initializes_runtime
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)
+ end
end
diff --git a/activerecord/test/cases/migration/change_schema_test.rb b/activerecord/test/cases/migration/change_schema_test.rb
index 17c1634444..e37dca856d 100644
--- a/activerecord/test/cases/migration/change_schema_test.rb
+++ b/activerecord/test/cases/migration/change_schema_test.rb
@@ -35,7 +35,7 @@ module ActiveRecord
t.column :foo, :string
end
- assert_equal %w(foo id), connection.columns(:testings).map(&:name).sort
+ assert_equal %w(id foo), connection.columns(:testings).map(&:name)
end
def test_create_table_with_not_null_column
@@ -50,7 +50,7 @@ module ActiveRecord
def test_create_table_with_defaults
# MySQL doesn't allow defaults on TEXT or BLOB columns.
- mysql = current_adapter?(:MysqlAdapter) || current_adapter?(:Mysql2Adapter)
+ mysql = current_adapter?(:MysqlAdapter, :Mysql2Adapter)
connection.create_table :testings do |t|
t.column :one, :string, :default => "hello"
@@ -74,6 +74,35 @@ module ActiveRecord
assert_equal "hello", five.default unless mysql
end
+ def test_add_column_with_array
+ if current_adapter?(:PostgreSQLAdapter)
+ 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
+ else
+ skip "array option only supported in PostgreSQLAdapter"
+ end
+ end
+
+ def test_create_table_with_array_column
+ if current_adapter?(:PostgreSQLAdapter)
+ 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
+ else
+ skip "array option only supported in PostgreSQLAdapter"
+ end
+ end
+
def test_create_table_with_limits
connection.create_table :testings do |t|
t.column :foo, :string, :limit => 255
@@ -99,7 +128,7 @@ module ActiveRecord
assert_equal 'smallint', one.sql_type
assert_equal 'integer', four.sql_type
assert_equal 'bigint', eight.sql_type
- elsif current_adapter?(:MysqlAdapter) or current_adapter?(:Mysql2Adapter)
+ elsif current_adapter?(:MysqlAdapter, :Mysql2Adapter)
assert_match 'int(11)', default.sql_type
assert_match 'tinyint', one.sql_type
assert_match 'int', four.sql_type
@@ -119,7 +148,7 @@ module ActiveRecord
t.column :foo, :string
end
- assert_equal %w(foo testing_id), connection.columns(:testings).map(&:name).sort
+ assert_equal %w(testing_id foo), connection.columns(:testings).map(&:name)
end
def test_create_table_with_primary_key_prefix_as_table_name
@@ -129,7 +158,27 @@ module ActiveRecord
t.column :foo, :string
end
- assert_equal %w(foo testingid), connection.columns(:testings).map(&:name).sort
+ assert_equal %w(testingid foo), connection.columns(:testings).map(&:name)
+ end
+
+ def test_create_table_raises_when_redefining_primary_key_column
+ error = assert_raise(ArgumentError) do
+ connection.create_table :testings do |t|
+ t.column :id, :string
+ end
+ end
+
+ assert_equal "you can't redefine the primary key column 'id'. To define a custom primary key, pass { id: false } to create_table.", error.message
+ end
+
+ def test_create_table_raises_when_redefining_custom_primary_key_column
+ error = assert_raise(ArgumentError) do
+ connection.create_table :testings, primary_key: :testing_id do |t|
+ t.column :testing_id, :string
+ end
+ end
+
+ assert_equal "you can't redefine the primary key column 'testing_id'. To define a custom primary key, pass { id: false } to create_table.", error.message
end
def test_create_table_with_timestamps_should_create_datetime_columns
@@ -215,7 +264,7 @@ module ActiveRecord
end
end
- def test_keeping_default_and_notnull_constaint_on_change
+ def test_keeping_default_and_notnull_constraints_on_change
connection.create_table :testings do |t|
t.column :title, :string
end
@@ -273,7 +322,7 @@ module ActiveRecord
end
assert connection.column_exists?(:testings, :foo)
- refute connection.column_exists?(:testings, :bar)
+ assert_not connection.column_exists?(:testings, :bar)
end
def test_column_exists_with_type
@@ -283,10 +332,10 @@ module ActiveRecord
end
assert connection.column_exists?(:testings, :foo, :string)
- refute connection.column_exists?(:testings, :foo, :integer)
+ assert_not connection.column_exists?(:testings, :foo, :integer)
assert connection.column_exists?(:testings, :bar, :decimal)
- refute connection.column_exists?(:testings, :bar, :integer)
+ assert_not connection.column_exists?(:testings, :bar, :integer)
end
def test_column_exists_with_definition
@@ -298,13 +347,13 @@ module ActiveRecord
end
assert connection.column_exists?(:testings, :foo, :string, limit: 100)
- refute connection.column_exists?(:testings, :foo, :string, limit: nil)
+ assert_not connection.column_exists?(:testings, :foo, :string, limit: nil)
assert connection.column_exists?(:testings, :bar, :decimal, precision: 8, scale: 2)
- refute connection.column_exists?(:testings, :bar, :decimal, precision: nil, scale: nil)
+ assert_not connection.column_exists?(:testings, :bar, :decimal, precision: nil, scale: nil)
assert connection.column_exists?(:testings, :taggable_id, :integer, null: false)
- refute connection.column_exists?(:testings, :taggable_id, :integer, null: true)
+ assert_not connection.column_exists?(:testings, :taggable_id, :integer, null: true)
assert connection.column_exists?(:testings, :taggable_type, :string, default: 'Photo')
- refute connection.column_exists?(:testings, :taggable_type, :string, default: nil)
+ assert_not connection.column_exists?(:testings, :taggable_type, :string, default: nil)
end
def test_column_exists_on_table_with_no_options_parameter_supplied
diff --git a/activerecord/test/cases/migration/change_table_test.rb b/activerecord/test/cases/migration/change_table_test.rb
index 4614be9650..8065541bfe 100644
--- a/activerecord/test/cases/migration/change_table_test.rb
+++ b/activerecord/test/cases/migration/change_table_test.rb
@@ -1,23 +1,11 @@
require "cases/migration/helper"
+require "minitest/mock"
module ActiveRecord
class Migration
class TableTest < ActiveRecord::TestCase
- class MockConnection < MiniTest::Mock
- def native_database_types
- {
- :string => 'varchar(255)',
- :integer => 'integer',
- }
- end
-
- def type_to_sql(type, limit, precision, scale)
- native_database_types[type]
- end
- end
-
def setup
- @connection = MockConnection.new
+ @connection = MiniTest::Mock.new
end
def teardown
@@ -98,26 +86,18 @@ module ActiveRecord
end
end
- def string_column
- @connection.native_database_types[:string]
- end
-
- def integer_column
- @connection.native_database_types[:integer]
- end
-
def test_integer_creates_integer_column
with_change_table do |t|
- @connection.expect :add_column, nil, [:delete_me, :foo, integer_column, {}]
- @connection.expect :add_column, nil, [:delete_me, :bar, integer_column, {}]
+ @connection.expect :add_column, nil, [:delete_me, :foo, :integer, {}]
+ @connection.expect :add_column, nil, [:delete_me, :bar, :integer, {}]
t.integer :foo, :bar
end
end
def test_string_creates_string_column
with_change_table do |t|
- @connection.expect :add_column, nil, [:delete_me, :foo, string_column, {}]
- @connection.expect :add_column, nil, [:delete_me, :bar, string_column, {}]
+ @connection.expect :add_column, nil, [:delete_me, :foo, :string, {}]
+ @connection.expect :add_column, nil, [:delete_me, :bar, :string, {}]
t.string :foo, :bar
end
end
@@ -164,6 +144,13 @@ module ActiveRecord
end
end
+ def test_rename_index_renames_index
+ with_change_table do |t|
+ @connection.expect :rename_index, nil, [:delete_me, :bar, :baz]
+ t.rename_index :bar, :baz
+ end
+ end
+
def test_change_changes_column
with_change_table do |t|
@connection.expect :change_column, nil, [:delete_me, :bar, :string, {}]
@@ -187,14 +174,14 @@ module ActiveRecord
def test_remove_drops_single_column
with_change_table do |t|
- @connection.expect :remove_column, nil, [:delete_me, :bar]
+ @connection.expect :remove_columns, nil, [:delete_me, :bar]
t.remove :bar
end
end
def test_remove_drops_multiple_columns
with_change_table do |t|
- @connection.expect :remove_column, nil, [:delete_me, :bar, :baz]
+ @connection.expect :remove_columns, nil, [:delete_me, :bar, :baz]
t.remove :bar, :baz
end
end
diff --git a/activerecord/test/cases/migration/column_attributes_test.rb b/activerecord/test/cases/migration/column_attributes_test.rb
index b88db384a0..aa606ac8bb 100644
--- a/activerecord/test/cases/migration/column_attributes_test.rb
+++ b/activerecord/test/cases/migration/column_attributes_test.rb
@@ -16,32 +16,23 @@ module ActiveRecord
end
def test_add_remove_single_field_using_string_arguments
- refute 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
- refute TestModel.column_methods_hash.key?(:last_name)
+ assert_no_column TestModel, :last_name
end
def test_add_remove_single_field_using_symbol_arguments
- refute 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
- refute TestModel.column_methods_hash.key?(:last_name)
+ assert_no_column TestModel, :last_name
end
def test_unabstracted_database_dependent_types
@@ -168,26 +159,6 @@ 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
diff --git a/activerecord/test/cases/migration/rename_column_test.rb b/activerecord/test/cases/migration/columns_test.rb
index d1a85ee5e4..2d7a7ec73a 100644
--- a/activerecord/test/cases/migration/rename_column_test.rb
+++ b/activerecord/test/cases/migration/columns_test.rb
@@ -2,7 +2,7 @@ require "cases/migration/helper"
module ActiveRecord
class Migration
- class RenameColumnTest < ActiveRecord::TestCase
+ class ColumnsTest < ActiveRecord::TestCase
include ActiveRecord::Migration::TestHelper
self.use_transactional_fixtures = false
@@ -55,13 +55,20 @@ module ActiveRecord
default_before = connection.columns("test_models").find { |c| c.name == "salary" }.default
assert_equal 70000, default_before
- rename_column "test_models", "salary", "anual_salary"
+ rename_column "test_models", "salary", "annual_salary"
- assert TestModel.column_names.include?("anual_salary")
- default_after = connection.columns("test_models").find { |c| c.name == "anual_salary" }.default
+ assert TestModel.column_names.include?("annual_salary")
+ default_after = connection.columns("test_models").find { |c| c.name == "annual_salary" }.default
assert_equal 70000, default_after
end
+ if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
+ def test_mysql_rename_column_preserves_auto_increment
+ rename_column "test_models", "id", "id_test"
+ assert_equal "auto_increment", connection.columns("test_models").find { |c| c.name == "id_test" }.extra
+ end
+ end
+
def test_rename_nonexistent_column
exception = if current_adapter?(:PostgreSQLAdapter, :OracleAdapter)
ActiveRecord::StatementInvalid
@@ -84,16 +91,48 @@ module ActiveRecord
add_column "test_models", :hat_name, :string
add_index :test_models, :hat_name
- # FIXME: we should test that the index goes away
+ assert_equal 1, connection.indexes('test_models').size
+ rename_column "test_models", "hat_name", "name"
+
+ assert_equal ['index_test_models_on_name'], connection.indexes('test_models').map(&:name)
+ end
+
+ def test_rename_column_with_multi_column_index
+ add_column "test_models", :hat_size, :integer
+ add_column "test_models", :hat_style, :string, limit: 100
+ add_index "test_models", ["hat_style", "hat_size"], unique: true
+
+ rename_column "test_models", "hat_size", 'size'
+ if current_adapter? :OracleAdapter
+ assert_equal ['i_test_models_hat_style_size'], connection.indexes('test_models').map(&:name)
+ else
+ assert_equal ['index_test_models_on_hat_style_and_size'], connection.indexes('test_models').map(&:name)
+ end
+
+ rename_column "test_models", "hat_style", 'style'
+ if current_adapter? :OracleAdapter
+ assert_equal ['i_test_models_style_size'], connection.indexes('test_models').map(&:name)
+ else
+ assert_equal ['index_test_models_on_style_and_size'], connection.indexes('test_models').map(&:name)
+ end
+ end
+
+ def test_rename_column_does_not_rename_custom_named_index
+ add_column "test_models", :hat_name, :string
+ add_index :test_models, :hat_name, :name => 'idx_hat_name'
+
+ assert_equal 1, connection.indexes('test_models').size
rename_column "test_models", "hat_name", "name"
+ assert_equal ['idx_hat_name'], connection.indexes('test_models').map(&:name)
end
def test_remove_column_with_index
add_column "test_models", :hat_name, :string
add_index :test_models, :hat_name
- # FIXME: we should test that the index goes away
+ assert_equal 1, connection.indexes('test_models').size
remove_column("test_models", "hat_name")
+ assert_equal 0, connection.indexes('test_models').size
end
def test_remove_column_with_multi_column_index
@@ -101,14 +140,25 @@ module ActiveRecord
add_column "test_models", :hat_style, :string, :limit => 100
add_index "test_models", ["hat_style", "hat_size"], :unique => true
- # FIXME: we should test that the index goes away
+ assert_equal 1, connection.indexes('test_models').size
remove_column("test_models", "hat_size")
+
+ # Every database and/or database adapter has their own behavior
+ # if it drops the multi-column index when any of the indexed columns dropped by remove_column.
+ if current_adapter?(:PostgreSQLAdapter, :OracleAdapter)
+ assert_equal [], connection.indexes('test_models').map(&:name)
+ else
+ assert_equal ['index_test_models_on_hat_style_and_hat_size'], connection.indexes('test_models').map(&:name)
+ end
end
- # FIXME: we need to test that these calls do something
def test_change_type_of_not_null_column
change_column "test_models", "updated_at", :datetime, :null => false
change_column "test_models", "updated_at", :datetime, :null => false
+
+ TestModel.reset_column_information
+ assert_equal false, TestModel.columns_hash['updated_at'].null
+ ensure
change_column "test_models", "updated_at", :datetime, :null => true
end
@@ -119,7 +169,7 @@ module ActiveRecord
change_column "test_models", "funny", :boolean, :null => false, :default => true
TestModel.reset_column_information
- refute TestModel.columns_hash["funny"].null, "Column 'funny' must *not* allow nulls at this point"
+ assert_not TestModel.columns_hash["funny"].null, "Column 'funny' must *not* allow nulls at this point"
change_column "test_models", "funny", :boolean, :null => true
TestModel.reset_column_information
@@ -138,7 +188,7 @@ module ActiveRecord
new_columns = connection.columns(TestModel.table_name)
- refute new_columns.find { |c| c.name == 'age' and c.type == :integer }
+ assert_not new_columns.find { |c| c.name == 'age' and c.type == :integer }
assert new_columns.find { |c| c.name == 'age' and c.type == :string }
old_columns = connection.columns(TestModel.table_name)
@@ -149,7 +199,7 @@ module ActiveRecord
change_column :test_models, :approved, :boolean, :default => false
new_columns = connection.columns(TestModel.table_name)
- refute new_columns.find { |c| c.name == 'approved' and c.type == :boolean and c.default == true }
+ assert_not new_columns.find { |c| c.name == 'approved' and c.type == :boolean and c.default == true }
assert new_columns.find { |c| c.name == 'approved' and c.type == :boolean and c.default == false }
change_column :test_models, :approved, :boolean, :default => true
end
@@ -160,7 +210,7 @@ module ActiveRecord
change_column "test_models", "contributor", :boolean, :default => nil
TestModel.reset_column_information
- refute TestModel.new.contributor?
+ assert_not TestModel.new.contributor?
assert_nil TestModel.new.contributor
end
@@ -170,7 +220,28 @@ module ActiveRecord
change_column "test_models", "administrator", :boolean, :default => false
TestModel.reset_column_information
- refute TestModel.new.administrator?
+ assert_not TestModel.new.administrator?
+ end
+
+ def test_change_column_with_custom_index_name
+ add_column "test_models", "category", :string
+ add_index :test_models, :category, name: 'test_models_categories_idx'
+
+ assert_equal ['test_models_categories_idx'], connection.indexes('test_models').map(&:name)
+ change_column "test_models", "category", :string, null: false, default: 'article'
+
+ assert_equal ['test_models_categories_idx'], connection.indexes('test_models').map(&:name)
+ end
+
+ def test_change_column_with_long_index_name
+ table_name_prefix = 'test_models_'
+ long_index_name = table_name_prefix + ('x' * (connection.allowed_index_name_length - table_name_prefix.length))
+ add_column "test_models", "category", :string
+ add_index :test_models, :category, name: long_index_name
+
+ change_column "test_models", "category", :string, null: false, default: 'article'
+
+ assert_equal [long_index_name], connection.indexes('test_models').map(&:name)
end
def test_change_column_default
@@ -189,6 +260,20 @@ module ActiveRecord
def test_remove_column_no_second_parameter_raises_exception
assert_raise(ArgumentError) { connection.remove_column("funny") }
end
+
+ def test_removing_and_renaming_column_preserves_custom_primary_key
+ connection.create_table "my_table", primary_key: "my_table_id", force: true do |t|
+ t.integer "col_one"
+ t.string "col_two", limit: 128, null: false
+ end
+
+ remove_column("my_table", "col_two")
+ rename_column("my_table", "col_one", "col_three")
+
+ assert_equal 'my_table_id', connection.primary_key('my_table')
+ ensure
+ connection.drop_table(:my_table) rescue nil
+ 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 f2213ee6aa..1b205d372f 100644
--- a/activerecord/test/cases/migration/command_recorder_test.rb
+++ b/activerecord/test/cases/migration/command_recorder_test.rb
@@ -26,7 +26,7 @@ module ActiveRecord
}.new)
assert recorder.respond_to?(:create_table), 'respond_to? create_table'
recorder.send(:create_table, :horses)
- assert_equal [[:create_table, [:horses]]], recorder.commands
+ assert_equal [[:create_table, [:horses], nil]], recorder.commands
end
def test_unknown_commands_delegate
@@ -34,10 +34,15 @@ module ActiveRecord
assert_equal 'bar', recorder.foo
end
- def test_unknown_commands_raise_exception_if_they_cannot_delegate
- @recorder.record :execute, ['some sql']
+ def test_inverse_of_raise_exception_on_unknown_commands
assert_raises(ActiveRecord::IrreversibleMigration) do
- @recorder.inverse
+ @recorder.inverse_of :execute, ['some sql']
+ end
+ end
+
+ def test_irreversible_commands_raise_exception
+ assert_raises(ActiveRecord::IrreversibleMigration) do
+ @recorder.revert{ @recorder.execute 'some sql' }
end
end
@@ -46,121 +51,206 @@ module ActiveRecord
assert_equal 1, @recorder.commands.length
end
- def test_inverse
- @recorder.record :create_table, [:system_settings]
- assert_equal 1, @recorder.inverse.length
+ def test_inverted_commands_are_reversed
+ @recorder.revert do
+ @recorder.record :create_table, [:hello]
+ @recorder.record :create_table, [:world]
+ end
+ tables = @recorder.commands.map{|_cmd, args, _block| args}
+ assert_equal [[:world], [:hello]], tables
+ end
- @recorder.record :rename_table, [:old, :new]
- assert_equal 2, @recorder.inverse.length
+ def test_revert_order
+ block = Proc.new{|t| t.string :name }
+ @recorder.instance_eval do
+ create_table("apples", &block)
+ revert do
+ create_table("bananas", &block)
+ revert do
+ create_table("clementines", &block)
+ create_table("dates")
+ end
+ create_table("elderberries")
+ end
+ revert do
+ create_table("figs", &block)
+ create_table("grapes")
+ end
+ end
+ assert_equal [[:create_table, ["apples"], block], [:drop_table, ["elderberries"], nil],
+ [:create_table, ["clementines"], block], [:create_table, ["dates"], nil],
+ [:drop_table, ["bananas"], block], [:drop_table, ["grapes"], nil],
+ [:drop_table, ["figs"], block]], @recorder.commands
end
- def test_inverted_commands_are_reveresed
- @recorder.record :create_table, [:hello]
- @recorder.record :create_table, [:world]
- tables = @recorder.inverse.map(&:last)
- assert_equal [[:world], [:hello]], tables
+ def test_invert_change_table
+ @recorder.revert do
+ @recorder.change_table :fruits do |t|
+ t.string :name
+ t.rename :kind, :cultivar
+ end
+ end
+ assert_equal [
+ [:rename_column, [:fruits, :cultivar, :kind]],
+ [:remove_column, [:fruits, :name, :string, {}], nil],
+ ], @recorder.commands
+
+ assert_raises(ActiveRecord::IrreversibleMigration) do
+ @recorder.revert do
+ @recorder.change_table :fruits do |t|
+ t.remove :kind
+ end
+ end
+ end
end
def test_invert_create_table
- @recorder.record :create_table, [:system_settings]
- drop_table = @recorder.inverse.first
- assert_equal [:drop_table, [:system_settings]], drop_table
+ @recorder.revert do
+ @recorder.record :create_table, [:system_settings]
+ end
+ drop_table = @recorder.commands.first
+ assert_equal [:drop_table, [:system_settings], nil], drop_table
end
- def test_invert_create_table_with_options
- @recorder.record :create_table, [:people_reminders, {:id => false}]
- drop_table = @recorder.inverse.first
- assert_equal [:drop_table, [:people_reminders]], drop_table
+ def test_invert_create_table_with_options_and_block
+ block = Proc.new{}
+ drop_table = @recorder.inverse_of :create_table, [:people_reminders, id: false], &block
+ assert_equal [:drop_table, [:people_reminders, id: false], block], drop_table
+ end
+
+ def test_invert_drop_table
+ block = Proc.new{}
+ create_table = @recorder.inverse_of :drop_table, [:people_reminders, id: false], &block
+ assert_equal [:create_table, [:people_reminders, id: false], block], create_table
+ end
+
+ def test_invert_drop_table_without_a_block_nor_option
+ assert_raises(ActiveRecord::IrreversibleMigration) do
+ @recorder.inverse_of :drop_table, [:people_reminders]
+ end
end
def test_invert_create_join_table
- @recorder.record :create_join_table, [:musics, :artists]
- drop_table = @recorder.inverse.first
- assert_equal [:drop_table, [:artists_musics]], drop_table
+ drop_join_table = @recorder.inverse_of :create_join_table, [:musics, :artists]
+ assert_equal [:drop_join_table, [:musics, :artists], nil], drop_join_table
end
def test_invert_create_join_table_with_table_name
- @recorder.record :create_join_table, [:musics, :artists, {:table_name => :catalog}]
- drop_table = @recorder.inverse.first
- assert_equal [:drop_table, [:catalog]], drop_table
+ drop_join_table = @recorder.inverse_of :create_join_table, [:musics, :artists, table_name: :catalog]
+ assert_equal [:drop_join_table, [:musics, :artists, table_name: :catalog], nil], drop_join_table
+ end
+
+ def test_invert_drop_join_table
+ block = Proc.new{}
+ create_join_table = @recorder.inverse_of :drop_join_table, [:musics, :artists, table_name: :catalog], &block
+ assert_equal [:create_join_table, [:musics, :artists, table_name: :catalog], block], create_join_table
end
def test_invert_rename_table
- @recorder.record :rename_table, [:old, :new]
- rename = @recorder.inverse.first
+ rename = @recorder.inverse_of :rename_table, [:old, :new]
assert_equal [:rename_table, [:new, :old]], rename
end
def test_invert_add_column
- @recorder.record :add_column, [:table, :column, :type, {}]
- remove = @recorder.inverse.first
- assert_equal [:remove_column, [:table, :column]], remove
+ remove = @recorder.inverse_of :add_column, [:table, :column, :type, {}]
+ assert_equal [:remove_column, [:table, :column, :type, {}], nil], remove
+ end
+
+ def test_invert_remove_column
+ add = @recorder.inverse_of :remove_column, [:table, :column, :type, {}]
+ assert_equal [:add_column, [:table, :column, :type, {}], nil], add
+ end
+
+ def test_invert_remove_column_without_type
+ assert_raises(ActiveRecord::IrreversibleMigration) do
+ @recorder.inverse_of :remove_column, [:table, :column]
+ end
end
def test_invert_rename_column
- @recorder.record :rename_column, [:table, :old, :new]
- rename = @recorder.inverse.first
+ rename = @recorder.inverse_of :rename_column, [:table, :old, :new]
assert_equal [:rename_column, [:table, :new, :old]], rename
end
def test_invert_add_index
- @recorder.record :add_index, [:table, [:one, :two], {:options => true}]
- remove = @recorder.inverse.first
- assert_equal [:remove_index, [:table, {:column => [:one, :two]}]], remove
+ remove = @recorder.inverse_of :add_index, [:table, [:one, :two], options: true]
+ assert_equal [:remove_index, [:table, {column: [:one, :two], options: true}]], remove
end
def test_invert_add_index_with_name
- @recorder.record :add_index, [:table, [:one, :two], {:name => "new_index"}]
- remove = @recorder.inverse.first
- assert_equal [:remove_index, [:table, {:name => "new_index"}]], remove
+ remove = @recorder.inverse_of :add_index, [:table, [:one, :two], name: "new_index"]
+ assert_equal [:remove_index, [:table, {column: [:one, :two], name: "new_index"}]], remove
end
def test_invert_add_index_with_no_options
- @recorder.record :add_index, [:table, [:one, :two]]
- remove = @recorder.inverse.first
- assert_equal [:remove_index, [:table, {:column => [:one, :two]}]], remove
+ remove = @recorder.inverse_of :add_index, [:table, [:one, :two]]
+ assert_equal [:remove_index, [:table, {column: [:one, :two]}]], remove
+ end
+
+ def test_invert_remove_index
+ add = @recorder.inverse_of :remove_index, [:table, {column: [:one, :two], options: true}]
+ assert_equal [:add_index, [:table, [:one, :two], options: true]], add
+ end
+
+ def test_invert_remove_index_with_name
+ add = @recorder.inverse_of :remove_index, [:table, {column: [:one, :two], name: "new_index"}]
+ assert_equal [:add_index, [:table, [:one, :two], name: "new_index"]], add
+ end
+
+ def test_invert_remove_index_with_no_special_options
+ add = @recorder.inverse_of :remove_index, [:table, {column: [:one, :two]}]
+ assert_equal [:add_index, [:table, [:one, :two], {}]], add
+ end
+
+ def test_invert_remove_index_with_no_column
+ assert_raises(ActiveRecord::IrreversibleMigration) do
+ @recorder.inverse_of :remove_index, [:table, name: "new_index"]
+ end
end
def test_invert_rename_index
- @recorder.record :rename_index, [:table, :old, :new]
- rename = @recorder.inverse.first
+ rename = @recorder.inverse_of :rename_index, [:table, :old, :new]
assert_equal [:rename_index, [:table, :new, :old]], rename
end
def test_invert_add_timestamps
- @recorder.record :add_timestamps, [:table]
- remove = @recorder.inverse.first
- assert_equal [:remove_timestamps, [:table]], remove
+ remove = @recorder.inverse_of :add_timestamps, [:table]
+ assert_equal [:remove_timestamps, [:table], nil], remove
end
def test_invert_remove_timestamps
- @recorder.record :remove_timestamps, [:table]
- add = @recorder.inverse.first
- assert_equal [:add_timestamps, [:table]], add
+ add = @recorder.inverse_of :remove_timestamps, [:table]
+ assert_equal [:add_timestamps, [:table], nil], add
end
def test_invert_add_reference
- @recorder.record :add_reference, [:table, :taggable, { polymorphic: true }]
- remove = @recorder.inverse.first
- assert_equal [:remove_reference, [:table, :taggable, { polymorphic: true }]], remove
+ remove = @recorder.inverse_of :add_reference, [:table, :taggable, { polymorphic: true }]
+ assert_equal [:remove_reference, [:table, :taggable, { polymorphic: true }], nil], remove
end
def test_invert_add_belongs_to_alias
- @recorder.record :add_belongs_to, [:table, :user]
- remove = @recorder.inverse.first
- assert_equal [:remove_reference, [:table, :user]], remove
+ remove = @recorder.inverse_of :add_belongs_to, [:table, :user]
+ assert_equal [:remove_reference, [:table, :user], nil], remove
end
def test_invert_remove_reference
- @recorder.record :remove_reference, [:table, :taggable, { polymorphic: true }]
- add = @recorder.inverse.first
- assert_equal [:add_reference, [:table, :taggable, { polymorphic: true }]], add
+ add = @recorder.inverse_of :remove_reference, [:table, :taggable, { polymorphic: true }]
+ assert_equal [:add_reference, [:table, :taggable, { polymorphic: true }], nil], add
end
def test_invert_remove_belongs_to_alias
- @recorder.record :remove_belongs_to, [:table, :user]
- add = @recorder.inverse.first
- assert_equal [:add_reference, [:table, :user]], add
+ 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
diff --git a/activerecord/test/cases/migration/create_join_table_test.rb b/activerecord/test/cases/migration/create_join_table_test.rb
index cd1b0e8b47..efaec0f823 100644
--- a/activerecord/test/cases/migration/create_join_table_test.rb
+++ b/activerecord/test/cases/migration/create_join_table_test.rb
@@ -35,6 +35,12 @@ module ActiveRecord
assert_equal %w(artist_id music_id), connection.columns(:artists_musics).map(&:name).sort
end
+ def test_create_join_table_with_symbol_and_string
+ connection.create_join_table :artists, 'musics'
+
+ assert_equal %w(artist_id music_id), connection.columns(:artists_musics).map(&:name).sort
+ end
+
def test_create_join_table_with_the_proper_order
connection.create_join_table :videos, :musics
@@ -72,6 +78,48 @@ module ActiveRecord
assert_equal [%w(artist_id music_id)], connection.indexes(:artists_musics).map(&:columns)
end
+
+ def test_drop_join_table
+ connection.create_join_table :artists, :musics
+ connection.drop_join_table :artists, :musics
+
+ assert !connection.tables.include?('artists_musics')
+ end
+
+ def test_drop_join_table_with_strings
+ connection.create_join_table :artists, :musics
+ connection.drop_join_table 'artists', 'musics'
+
+ assert !connection.tables.include?('artists_musics')
+ end
+
+ def test_drop_join_table_with_the_proper_order
+ connection.create_join_table :videos, :musics
+ connection.drop_join_table :videos, :musics
+
+ assert !connection.tables.include?('musics_videos')
+ end
+
+ def test_drop_join_table_with_the_table_name
+ connection.create_join_table :artists, :musics, table_name: :catalog
+ connection.drop_join_table :artists, :musics, table_name: :catalog
+
+ assert !connection.tables.include?('catalog')
+ end
+
+ def test_drop_join_table_with_the_table_name_as_string
+ connection.create_join_table :artists, :musics, table_name: 'catalog'
+ connection.drop_join_table :artists, :musics, table_name: 'catalog'
+
+ assert !connection.tables.include?('catalog')
+ end
+
+ def test_drop_join_table_with_column_options
+ connection.create_join_table :artists, :musics, column_options: {null: true}
+ connection.drop_join_table :artists, :musics, column_options: {null: true}
+
+ assert !connection.tables.include?('artists_musics')
+ end
end
end
end
diff --git a/activerecord/test/cases/migration/helper.rb b/activerecord/test/cases/migration/helper.rb
index 768ebc5861..e28feedcf9 100644
--- a/activerecord/test/cases/migration/helper.rb
+++ b/activerecord/test/cases/migration/helper.rb
@@ -2,12 +2,10 @@ require "cases/helper"
module ActiveRecord
class Migration
- class << self
- attr_accessor :message_count
- end
+ class << self; attr_accessor :message_count; end
+ self.message_count = 0
def puts(text="")
- ActiveRecord::Migration.message_count ||= 0
ActiveRecord::Migration.message_count += 1
end
diff --git a/activerecord/test/cases/migration/index_test.rb b/activerecord/test/cases/migration/index_test.rb
index dd9492924c..04521a5f5a 100644
--- a/activerecord/test/cases/migration/index_test.rb
+++ b/activerecord/test/cases/migration/index_test.rb
@@ -35,7 +35,7 @@ module ActiveRecord
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
- refute connection.index_name_exists?(table_name, 'old_idx', false)
+ assert_not connection.index_name_exists?(table_name, 'old_idx', false)
assert connection.index_name_exists?(table_name, 'new_idx', true)
end
@@ -55,19 +55,31 @@ module ActiveRecord
assert_raise(ArgumentError) { connection.remove_index(table_name, "no_such_index") }
end
- def test_add_index_name_length_limit
- good_index_name = 'x' * connection.index_name_length
+ def test_add_index_works_with_long_index_names
+ connection.add_index(table_name, "foo", name: good_index_name)
+
+ assert connection.index_name_exists?(table_name, good_index_name, false)
+ connection.remove_index(table_name, name: good_index_name)
+ end
+
+ def test_add_index_does_not_accept_too_long_index_names
too_long_index_name = good_index_name + 'x'
- assert_raises(ArgumentError) {
- connection.add_index(table_name, "foo", :name => too_long_index_name)
+ e = assert_raises(ArgumentError) {
+ connection.add_index(table_name, "foo", name: too_long_index_name)
}
+ assert_match(/too long; the limit is #{connection.allowed_index_name_length} characters/, e.message)
- refute connection.index_name_exists?(table_name, too_long_index_name, false)
+ assert_not connection.index_name_exists?(table_name, too_long_index_name, false)
connection.add_index(table_name, "foo", :name => good_index_name)
+ end
+
+ def test_internal_index_with_name_matching_database_limit
+ good_index_name = 'x' * connection.index_name_length
+ connection.add_index(table_name, "foo", name: good_index_name, internal: true)
assert connection.index_name_exists?(table_name, good_index_name, false)
- connection.remove_index(table_name, :name => good_index_name)
+ connection.remove_index(table_name, name: good_index_name)
end
def test_index_symbol_names
@@ -75,7 +87,7 @@ module ActiveRecord
assert connection.index_exists?(table_name, :foo, :name => :symbol_index_name)
connection.remove_index table_name, :name => :symbol_index_name
- refute connection.index_exists?(table_name, :foo, :name => :symbol_index_name)
+ assert_not connection.index_exists?(table_name, :foo, :name => :symbol_index_name)
end
def test_index_exists
@@ -91,6 +103,12 @@ module ActiveRecord
assert connection.index_exists?(:testings, [:foo, :bar])
end
+ def test_valid_index_options
+ assert_raise ArgumentError do
+ connection.add_index :testings, :foo, unqiue: true
+ end
+ end
+
def test_unique_index_exists
connection.add_index :testings, :foo, :unique => true
@@ -180,6 +198,12 @@ module ActiveRecord
connection.remove_index("testings", "last_name")
assert !connection.index_exists?("testings", "last_name")
end
+
+ private
+ def good_index_name
+ 'x' * connection.allowed_index_name_length
+ end
+
end
end
end
diff --git a/activerecord/test/cases/migration/logger_test.rb b/activerecord/test/cases/migration/logger_test.rb
index ee0c20747e..97efb94b66 100644
--- a/activerecord/test/cases/migration/logger_test.rb
+++ b/activerecord/test/cases/migration/logger_test.rb
@@ -7,6 +7,7 @@ module ActiveRecord
self.use_transactional_fixtures = false
Migration = Struct.new(:name, :version) do
+ def disable_ddl_transaction; false end
def migrate direction
# do nothing
end
@@ -34,4 +35,3 @@ module ActiveRecord
end
end
end
-
diff --git a/activerecord/test/cases/migration/references_index_test.rb b/activerecord/test/cases/migration/references_index_test.rb
index 264a99f9ce..3ff89524fe 100644
--- a/activerecord/test/cases/migration/references_index_test.rb
+++ b/activerecord/test/cases/migration/references_index_test.rb
@@ -29,7 +29,7 @@ module ActiveRecord
t.references :foo
end
- refute connection.index_exists?(table_name, :foo_id, :name => :index_testings_on_foo_id)
+ assert_not connection.index_exists?(table_name, :foo_id, :name => :index_testings_on_foo_id)
end
def test_does_not_create_index_explicit
@@ -37,7 +37,7 @@ module ActiveRecord
t.references :foo, :index => false
end
- refute connection.index_exists?(table_name, :foo_id, :name => :index_testings_on_foo_id)
+ assert_not connection.index_exists?(table_name, :foo_id, :name => :index_testings_on_foo_id)
end
def test_creates_index_with_options
@@ -75,7 +75,7 @@ module ActiveRecord
t.references :foo
end
- refute connection.index_exists?(table_name, :foo_id, :name => :index_testings_on_foo_id)
+ assert_not connection.index_exists?(table_name, :foo_id, :name => :index_testings_on_foo_id)
end
def test_does_not_create_index_for_existing_table_explicit
@@ -84,7 +84,7 @@ module ActiveRecord
t.references :foo, :index => false
end
- refute connection.index_exists?(table_name, :foo_id, :name => :index_testings_on_foo_id)
+ assert_not connection.index_exists?(table_name, :foo_id, :name => :index_testings_on_foo_id)
end
def test_creates_polymorphic_index_for_existing_table
diff --git a/activerecord/test/cases/migration/references_statements_test.rb b/activerecord/test/cases/migration/references_statements_test.rb
index 144302bd4a..e9545f2cce 100644
--- a/activerecord/test/cases/migration/references_statements_test.rb
+++ b/activerecord/test/cases/migration/references_statements_test.rb
@@ -22,7 +22,7 @@ module ActiveRecord
def test_does_not_create_reference_type_column
add_reference table_name, :taggable
- refute column_exists?(table_name, :taggable_type, :string)
+ assert_not column_exists?(table_name, :taggable_type, :string)
end
def test_creates_reference_type_column
@@ -37,7 +37,7 @@ module ActiveRecord
def test_does_not_create_reference_id_index
add_reference table_name, :user
- refute index_exists?(table_name, :user_id)
+ assert_not index_exists?(table_name, :user_id)
end
def test_creates_polymorphic_index
@@ -57,34 +57,34 @@ module ActiveRecord
def test_deletes_reference_id_column
remove_reference table_name, :supplier
- refute column_exists?(table_name, :supplier_id, :integer)
+ assert_not column_exists?(table_name, :supplier_id, :integer)
end
def test_deletes_reference_id_index
remove_reference table_name, :supplier
- refute index_exists?(table_name, :supplier_id)
+ assert_not index_exists?(table_name, :supplier_id)
end
-
+
def test_does_not_delete_reference_type_column
with_polymorphic_column do
remove_reference table_name, :supplier
- refute column_exists?(table_name, :supplier_id, :integer)
+ assert_not column_exists?(table_name, :supplier_id, :integer)
assert column_exists?(table_name, :supplier_type, :string)
end
end
-
+
def test_deletes_reference_type_column
with_polymorphic_column do
remove_reference table_name, :supplier, polymorphic: true
- refute column_exists?(table_name, :supplier_type, :string)
+ assert_not column_exists?(table_name, :supplier_type, :string)
end
end
def test_deletes_polymorphic_index
with_polymorphic_column do
remove_reference table_name, :supplier, polymorphic: true
- refute index_exists?(table_name, [:supplier_id, :supplier_type])
+ assert_not index_exists?(table_name, [:supplier_id, :supplier_type])
end
end
@@ -95,7 +95,7 @@ module ActiveRecord
def test_remove_belongs_to_alias
remove_belongs_to table_name, :supplier
- refute column_exists?(table_name, :supplier_id, :integer)
+ assert_not column_exists?(table_name, :supplier_id, :integer)
end
private
diff --git a/activerecord/test/cases/migration/rename_table_test.rb b/activerecord/test/cases/migration/rename_table_test.rb
index 21901bec3c..22dbd7c38b 100644
--- a/activerecord/test/cases/migration/rename_table_test.rb
+++ b/activerecord/test/cases/migration/rename_table_test.rb
@@ -63,7 +63,17 @@ module ActiveRecord
connection.enable_identity_insert("octopi", false) if current_adapter?(:SybaseAdapter)
assert_equal 'http://www.foreverflying.com/octopus-black7.jpg', connection.select_value("SELECT url FROM octopi WHERE id=1")
- assert connection.indexes(:octopi).first.columns.include?("url")
+ index = connection.indexes(:octopi).first
+ assert index.columns.include?("url")
+ assert_equal 'index_octopi_on_url', index.name
+ end
+
+ def test_rename_table_does_not_rename_custom_named_index
+ add_index :test_models, :url, name: 'special_url_idx'
+
+ rename_table :test_models, :octopi
+
+ assert_equal ['special_url_idx'], connection.indexes(:octopi).map(&:name)
end
def test_rename_table_for_postgresql_should_also_rename_default_sequence
diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb
index 3c0d2b18d9..acfde2a27a 100644
--- a/activerecord/test/cases/migration_test.rb
+++ b/activerecord/test/cases/migration_test.rb
@@ -24,15 +24,19 @@ class MigrationTest < ActiveRecord::TestCase
def setup
super
- %w(reminders people_reminders prefix_reminders_suffix).each do |table|
+ %w(reminders people_reminders prefix_reminders_suffix p_things_s).each do |table|
Reminder.connection.drop_table(table) rescue nil
end
Reminder.reset_column_information
ActiveRecord::Migration.verbose = true
ActiveRecord::Migration.message_count = 0
+ ActiveRecord::Base.connection.schema_cache.clear!
end
def teardown
+ ActiveRecord::Base.table_name_prefix = ""
+ ActiveRecord::Base.table_name_suffix = ""
+
ActiveRecord::Base.connection.initialize_schema_migrations_table
ActiveRecord::Base.connection.execute "DELETE FROM #{ActiveRecord::Migrator.schema_migrations_table_name}"
@@ -44,6 +48,7 @@ class MigrationTest < ActiveRecord::TestCase
%w(reminders people_reminders prefix_reminders_suffix).each do |table|
Reminder.connection.drop_table(table) rescue nil
end
+ Reminder.reset_table_name
Reminder.reset_column_information
%w(last_name key bio age height wealth birthday favorite_day
@@ -172,20 +177,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,11 +235,15 @@ class MigrationTest < ActiveRecord::TestCase
skip "not supported on #{ActiveRecord::Base.connection.class}"
end
- refute Person.column_methods_hash.include?(:last_name)
+ assert_no_column Person, :last_name
- migration = Struct.new(:name, :version) {
- def migrate(x); raise 'Something broke'; end
- }.new('zomg', 100)
+ 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)
@@ -244,30 +251,99 @@ class MigrationTest < ActiveRecord::TestCase
assert_equal "An error has occurred, this and all later migrations canceled:\n\nSomething broke", e.message
+ assert_no_column Person, :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}"
+ end
+
+ 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
+
+ migrator = ActiveRecord::Migrator.new(:up, [migration], 100)
+
+ e = assert_raise(StandardError) { migrator.run }
+
+ assert_equal "An error has occurred, this migration was canceled:\n\nSomething broke", e.message
+
+ assert_no_column Person, :last_name,
+ "On error, the Migrator should revert schema changes but it did not."
+ end
+
+ def test_migration_without_transaction
+ unless ActiveRecord::Base.connection.supports_ddl_transactions?
+ skip "not supported on #{ActiveRecord::Base.connection.class}"
+ end
+
+ assert_no_column Person, :last_name
+
+ 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
+
+ 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
+
+ assert_column Person, :last_name,
+ "without ddl transactions, the Migrator should not rollback on error but it did."
+ ensure
Person.reset_column_information
- refute Person.column_methods_hash.include?(:last_name)
+ if Person.column_names.include?('last_name')
+ Person.connection.remove_column('people', 'last_name')
+ 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.table_name_prefix = ""
- ActiveRecord::Base.table_name_suffix = ""
+ 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_"
@@ -275,7 +351,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
@@ -284,11 +362,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)
- ActiveRecord::Base.table_name_prefix = ""
- ActiveRecord::Base.table_name_suffix = ""
+ 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
@@ -307,8 +413,6 @@ class MigrationTest < ActiveRecord::TestCase
assert_equal "hello world", Thing.first.content
ensure
- ActiveRecord::Base.table_name_prefix = ''
- ActiveRecord::Base.table_name_suffix = ''
Thing.reset_table_name
Thing.reset_sequence_name
end
@@ -326,9 +430,6 @@ class MigrationTest < ActiveRecord::TestCase
WeNeedReminders.down
assert_raise(ActiveRecord::StatementInvalid) { Reminder.first }
ensure
- ActiveRecord::Base.table_name_prefix = ''
- ActiveRecord::Base.table_name_suffix = ''
- Reminder.reset_table_name
Reminder.reset_sequence_name
end
@@ -344,11 +445,7 @@ class MigrationTest < ActiveRecord::TestCase
columns = Person.connection.columns(:binary_testings)
data_column = columns.detect { |c| c.name == "data" }
- if current_adapter?(:MysqlAdapter) or current_adapter?(:Mysql2Adapter)
- assert_equal '', data_column.default
- else
- assert_nil data_column.default
- end
+ assert_nil data_column.default
Person.connection.drop_table :binary_testings rescue nil
end
@@ -436,11 +533,29 @@ class ReservedWordsMigrationTest < ActiveRecord::TestCase
end
end
+class ExplicitlyNamedIndexMigrationTest < ActiveRecord::TestCase
+ def test_drop_index_by_name
+ connection = Person.connection
+ connection.create_table :values, force: true do |t|
+ t.integer :value
+ end
+
+ assert_nothing_raised ArgumentError do
+ connection.add_index :values, :value, name: 'a_different_name'
+ connection.remove_index :values, column: :value, name: 'a_different_name'
+ end
+
+ connection.drop_table :values rescue nil
+ end
+end
+
if ActiveRecord::Base.connection.supports_bulk_alter?
class BulkAlterTableMigrationsTest < ActiveRecord::TestCase
def setup
@connection = Person.connection
@connection.create_table(:delete_me, :force => true) {|t| }
+ Person.reset_column_information
+ Person.reset_sequence_name
end
def teardown
@@ -595,8 +710,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)"
@@ -619,10 +734,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)
@@ -637,8 +752,8 @@ class CopyMigrationsTest < ActiveRecord::TestCase
Time.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)
@@ -662,10 +777,10 @@ class CopyMigrationsTest < ActiveRecord::TestCase
Time.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
@@ -682,8 +797,8 @@ class CopyMigrationsTest < ActiveRecord::TestCase
Time.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"})
@@ -694,6 +809,26 @@ class CopyMigrationsTest < ActiveRecord::TestCase
clear
end
+ def test_copying_migrations_preserving_magic_comments
+ ActiveRecord::Base.timestamped_migrations = false
+ @migrations_path = MIGRATIONS_ROOT + "/valid"
+ @existing_migrations = Dir[@migrations_path + "/*.rb"]
+
+ copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/magic"})
+ 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)"
+ assert_equal expected, IO.readlines(@migrations_path + "/4_currencies_have_symbols.bukkits.rb")[0..1].join.chomp
+
+ files_count = Dir[@migrations_path + "/*.rb"].length
+ copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/magic"})
+ assert_equal files_count, Dir[@migrations_path + "/*.rb"].length
+ assert copied.empty?
+ ensure
+ clear
+ end
+
def test_skipping_migrations
@migrations_path = MIGRATIONS_ROOT + "/valid_with_timestamps"
@existing_migrations = Dir[@migrations_path + "/*.rb"]
@@ -737,8 +872,8 @@ class CopyMigrationsTest < ActiveRecord::TestCase
Time.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
@@ -752,11 +887,20 @@ class CopyMigrationsTest < ActiveRecord::TestCase
Time.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 1e16addcf3..3f9854200d 100644
--- a/activerecord/test/cases/migrator_test.rb
+++ b/activerecord/test/cases/migrator_test.rb
@@ -29,6 +29,7 @@ module ActiveRecord
def teardown
super
ActiveRecord::SchemaMigration.delete_all rescue nil
+ ActiveRecord::Migration.verbose = true
end
def test_migrator_with_duplicate_names
@@ -84,10 +85,10 @@ module ActiveRecord
end
end
- def test_deprecated_constructor
- assert_deprecated do
- ActiveRecord::Migrator.new(:up, MIGRATIONS_ROOT + "/valid")
- end
+ def test_finds_migrations_in_numbered_directory
+ migrations = ActiveRecord::Migrator.migrations [MIGRATIONS_ROOT + '/10_urban']
+ assert_equal 9, migrations[0].version
+ assert_equal 'AddExpressions', migrations[0].name
end
def test_relative_migrations
@@ -115,11 +116,11 @@ module ActiveRecord
ActiveRecord::Migrator.new(:up, pass_one).migrate
assert pass_one.first.went_up
- refute pass_one.first.went_down
+ assert_not pass_one.first.went_down
pass_two = [Sensor.new('One', 1), Sensor.new('Three', 3)]
ActiveRecord::Migrator.new(:up, pass_two).migrate
- refute pass_two[0].went_up
+ assert_not pass_two[0].went_up
assert pass_two[1].went_up
assert pass_two.all? { |x| !x.went_down }
@@ -129,7 +130,7 @@ module ActiveRecord
ActiveRecord::Migrator.new(:down, pass_three).migrate
assert pass_three[0].went_down
- refute pass_three[1].went_down
+ assert_not pass_three[1].went_down
assert pass_three[2].went_down
end
@@ -301,7 +302,7 @@ module ActiveRecord
_, migrator = migrator_class(3)
ActiveRecord::Base.connection.execute("DROP TABLE schema_migrations")
- refute ActiveRecord::Base.connection.table_exists?('schema_migrations')
+ assert_not ActiveRecord::Base.connection.table_exists?('schema_migrations')
migrator.migrate("valid", 1)
assert ActiveRecord::Base.connection.table_exists?('schema_migrations')
end
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..b82409bfbe 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,30 +243,31 @@ 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
diff --git a/activerecord/test/cases/multiple_db_test.rb b/activerecord/test/cases/multiple_db_test.rb
index 42461e8ecb..2e386a172a 100644
--- a/activerecord/test/cases/multiple_db_test.rb
+++ b/activerecord/test/cases/multiple_db_test.rb
@@ -96,12 +96,12 @@ class MultipleDbTest < ActiveRecord::TestCase
unless in_memory_db?
def test_associations_should_work_when_model_has_no_connection
begin
- ActiveRecord::Model.remove_connection
+ ActiveRecord::Base.remove_connection
assert_nothing_raised ActiveRecord::ConnectionNotEstablished do
College.first.courses.first
end
ensure
- ActiveRecord::Model.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 fe9eddbdec..2f89699df7 100644
--- a/activerecord/test/cases/nested_attributes_test.rb
+++ b/activerecord/test/cases/nested_attributes_test.rb
@@ -79,10 +79,10 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase
def test_should_disable_allow_destroy_by_default
Pirate.accepts_nested_attributes_for :ship
- pirate = Pirate.create!(:catchphrase => "Don' botharrr talkin' like one, savvy?")
- ship = pirate.create_ship(:name => 'Nights Dirty Lightning')
+ pirate = Pirate.create!(catchphrase: "Don' botharrr talkin' like one, savvy?")
+ ship = pirate.create_ship(name: 'Nights Dirty Lightning')
- pirate.update_attributes(:ship_attributes => { '_destroy' => true, :id => ship.id })
+ pirate.update(ship_attributes: { '_destroy' => true, :id => ship.id })
assert_nothing_raised(ActiveRecord::RecordNotFound) { pirate.ship.reload }
end
@@ -125,33 +125,47 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase
def test_reject_if_with_a_proc_which_returns_true_always_for_has_one
Pirate.accepts_nested_attributes_for :ship, :reject_if => proc {|attributes| true }
- pirate = Pirate.new(:catchphrase => "Stop wastin' me time")
- ship = pirate.create_ship(:name => 's1')
- pirate.update_attributes({:ship_attributes => { :name => 's2', :id => ship.id } })
+ pirate = Pirate.new(catchphrase: "Stop wastin' me time")
+ ship = pirate.create_ship(name: 's1')
+ pirate.update({ship_attributes: { name: 's2', id: ship.id } })
assert_equal 's1', ship.reload.name
end
+ def test_reuse_already_built_new_record
+ pirate = Pirate.new
+ ship_built_first = pirate.build_ship
+ pirate.ship_attributes = { name: 'Ship 1' }
+ assert_equal ship_built_first.object_id, pirate.ship.object_id
+ end
+
+ def test_do_not_allow_assigning_foreign_key_when_reusing_existing_new_record
+ pirate = Pirate.create!(catchphrase: "Don' botharrr talkin' like one, savvy?")
+ pirate.build_ship
+ pirate.ship_attributes = { name: 'Ship 1', pirate_id: pirate.id + 1 }
+ assert_equal pirate.id, pirate.ship.pirate_id
+ end
+
def test_reject_if_with_a_proc_which_returns_true_always_for_has_many
Man.accepts_nested_attributes_for :interests, :reject_if => proc {|attributes| true }
- man = Man.create(:name => "John")
- interest = man.interests.create(:topic => 'photography')
- man.update_attributes({:interests_attributes => { :topic => 'gardening', :id => interest.id } })
+ man = Man.create(name: "John")
+ interest = man.interests.create(topic: 'photography')
+ man.update({interests_attributes: { topic: 'gardening', id: interest.id } })
assert_equal 'photography', interest.reload.topic
end
def test_destroy_works_independent_of_reject_if
Man.accepts_nested_attributes_for :interests, :reject_if => proc {|attributes| true }, :allow_destroy => true
- man = Man.create(:name => "Jon")
- interest = man.interests.create(:topic => 'the ladies')
- man.update_attributes({:interests_attributes => { :_destroy => "1", :id => interest.id } })
+ man = Man.create(name: "Jon")
+ interest = man.interests.create(topic: 'the ladies')
+ man.update({interests_attributes: { _destroy: "1", id: interest.id } })
assert man.reload.interests.empty?
end
def test_has_many_association_updating_a_single_record
Man.accepts_nested_attributes_for(:interests)
- man = Man.create(:name => 'John')
- interest = man.interests.create(:topic => 'photography')
- man.update_attributes({:interests_attributes => {:topic => 'gardening', :id => interest.id}})
+ man = Man.create(name: 'John')
+ interest = man.interests.create(topic: 'photography')
+ man.update({interests_attributes: {topic: 'gardening', id: interest.id}})
assert_equal 'gardening', interest.reload.topic
end
@@ -167,7 +181,7 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase
def test_first_and_array_index_zero_methods_return_the_same_value_when_nested_attributes_are_set_to_update_existing_record
Man.accepts_nested_attributes_for(:interests)
man = Man.create(:name => "John")
- interest = man.interests.create :topic => 'gardning'
+ interest = man.interests.create :topic => 'gardening'
man = Man.find man.id
man.interests_attributes = [{:id => interest.id, :topic => 'gardening'}]
assert_equal man.interests.first.topic, man.interests[0].topic
@@ -185,6 +199,17 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase
assert_equal "James", mean_pirate.parrot.name
assert_equal "blue", mean_pirate.parrot.color
end
+
+ def test_accepts_nested_attributes_for_can_be_overridden_in_subclasses
+ Pirate.accepts_nested_attributes_for(:parrot)
+
+ mean_pirate_class = Class.new(Pirate) do
+ accepts_nested_attributes_for :parrot
+ end
+ mean_pirate = mean_pirate_class.new
+ mean_pirate.parrot_attributes = { :name => "James" }
+ assert_equal "James", mean_pirate.parrot.name
+ end
end
class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase
@@ -273,8 +298,8 @@ class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase
@pirate.ship.destroy
[1, '1', true, 'true'].each do |truth|
- ship = @pirate.reload.create_ship(:name => 'Mister Pablo')
- @pirate.update_attributes(:ship_attributes => { :id => ship.id, :_destroy => truth })
+ ship = @pirate.reload.create_ship(name: 'Mister Pablo')
+ @pirate.update(ship_attributes: { id: ship.id, _destroy: truth })
assert_nil @pirate.reload.ship
assert_raise(ActiveRecord::RecordNotFound) { Ship.find(ship.id) }
@@ -283,7 +308,7 @@ class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase
def test_should_not_destroy_an_existing_record_if_destroy_is_not_truthy
[nil, '0', 0, 'false', false].each do |not_truth|
- @pirate.update_attributes(:ship_attributes => { :id => @pirate.ship.id, :_destroy => not_truth })
+ @pirate.update(ship_attributes: { id: @pirate.ship.id, _destroy: not_truth })
assert_equal @ship, @pirate.reload.ship
end
@@ -292,7 +317,7 @@ class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase
def test_should_not_destroy_an_existing_record_if_allow_destroy_is_false
Pirate.accepts_nested_attributes_for :ship, :allow_destroy => false, :reject_if => proc { |attributes| attributes.empty? }
- @pirate.update_attributes(:ship_attributes => { :id => @pirate.ship.id, :_destroy => '1' })
+ @pirate.update(ship_attributes: { id: @pirate.ship.id, _destroy: '1' })
assert_equal @ship, @pirate.reload.ship
@@ -300,14 +325,14 @@ class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase
end
def test_should_also_work_with_a_HashWithIndifferentAccess
- @pirate.ship_attributes = HashWithIndifferentAccess.new(:id => @ship.id, :name => 'Davy Jones Gold Dagger')
+ @pirate.ship_attributes = ActiveSupport::HashWithIndifferentAccess.new(:id => @ship.id, :name => 'Davy Jones Gold Dagger')
assert @pirate.ship.persisted?
assert_equal 'Davy Jones Gold Dagger', @pirate.ship.name
end
- def test_should_work_with_update_attributes_as_well
- @pirate.update_attributes({ :catchphrase => 'Arr', :ship_attributes => { :id => @ship.id, :name => 'Mister Pablo' } })
+ def test_should_work_with_update_as_well
+ @pirate.update({ catchphrase: 'Arr', ship_attributes: { id: @ship.id, name: 'Mister Pablo' } })
@pirate.reload
assert_equal 'Arr', @pirate.catchphrase
@@ -331,22 +356,22 @@ class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase
end
def test_should_accept_update_only_option
- @pirate.update_attributes(:update_only_ship_attributes => { :id => @pirate.ship.id, :name => 'Mayflower' })
+ @pirate.update(update_only_ship_attributes: { id: @pirate.ship.id, name: 'Mayflower' })
end
def test_should_create_new_model_when_nothing_is_there_and_update_only_is_true
@ship.delete
- @pirate.reload.update_attributes(:update_only_ship_attributes => { :name => 'Mayflower' })
+ @pirate.reload.update(update_only_ship_attributes: { name: 'Mayflower' })
assert_not_nil @pirate.ship
end
def test_should_update_existing_when_update_only_is_true_and_no_id_is_given
@ship.delete
- @ship = @pirate.create_update_only_ship(:name => 'Nights Dirty Lightning')
+ @ship = @pirate.create_update_only_ship(name: 'Nights Dirty Lightning')
- @pirate.update_attributes(:update_only_ship_attributes => { :name => 'Mayflower' })
+ @pirate.update(update_only_ship_attributes: { name: 'Mayflower' })
assert_equal 'Mayflower', @ship.reload.name
assert_equal @ship, @pirate.reload.ship
@@ -354,9 +379,9 @@ class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase
def test_should_update_existing_when_update_only_is_true_and_id_is_given
@ship.delete
- @ship = @pirate.create_update_only_ship(:name => 'Nights Dirty Lightning')
+ @ship = @pirate.create_update_only_ship(name: 'Nights Dirty Lightning')
- @pirate.update_attributes(:update_only_ship_attributes => { :name => 'Mayflower', :id => @ship.id })
+ @pirate.update(update_only_ship_attributes: { name: 'Mayflower', id: @ship.id })
assert_equal 'Mayflower', @ship.reload.name
assert_equal @ship, @pirate.reload.ship
@@ -365,9 +390,9 @@ class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase
def test_should_destroy_existing_when_update_only_is_true_and_id_is_given_and_is_marked_for_destruction
Pirate.accepts_nested_attributes_for :update_only_ship, :update_only => true, :allow_destroy => true
@ship.delete
- @ship = @pirate.create_update_only_ship(:name => 'Nights Dirty Lightning')
+ @ship = @pirate.create_update_only_ship(name: 'Nights Dirty Lightning')
- @pirate.update_attributes(:update_only_ship_attributes => { :name => 'Mayflower', :id => @ship.id, :_destroy => true })
+ @pirate.update(update_only_ship_attributes: { name: 'Mayflower', id: @ship.id, _destroy: true })
assert_nil @pirate.reload.ship
assert_raise(ActiveRecord::RecordNotFound) { Ship.find(@ship.id) }
@@ -457,31 +482,29 @@ class TestNestedAttributesOnABelongsToAssociation < ActiveRecord::TestCase
def test_should_destroy_an_existing_record_if_there_is_a_matching_id_and_destroy_is_truthy
@ship.pirate.destroy
[1, '1', true, 'true'].each do |truth|
- pirate = @ship.reload.create_pirate(:catchphrase => 'Arr')
- @ship.update_attributes(:pirate_attributes => { :id => pirate.id, :_destroy => truth })
+ pirate = @ship.reload.create_pirate(catchphrase: 'Arr')
+ @ship.update(pirate_attributes: { id: pirate.id, _destroy: truth })
assert_raise(ActiveRecord::RecordNotFound) { pirate.reload }
end
end
def test_should_unset_association_when_an_existing_record_is_destroyed
- @ship.reload
original_pirate_id = @ship.pirate.id
- @ship.attributes = {:pirate_attributes => {:id => @ship.pirate.id, :_destroy => true}}
- @ship.save!
+ @ship.update! pirate_attributes: { id: @ship.pirate.id, _destroy: true }
- assert_empty Pirate.where(["id = ?", original_pirate_id])
+ assert_empty Pirate.where(id: original_pirate_id)
assert_nil @ship.pirate_id
assert_nil @ship.pirate
@ship.reload
- assert_empty Pirate.where(["id = ?", original_pirate_id])
+ assert_empty Pirate.where(id: original_pirate_id)
assert_nil @ship.pirate_id
assert_nil @ship.pirate
end
def test_should_not_destroy_an_existing_record_if_destroy_is_not_truthy
[nil, '0', 0, 'false', false].each do |not_truth|
- @ship.update_attributes(:pirate_attributes => { :id => @ship.pirate.id, :_destroy => not_truth })
+ @ship.update(pirate_attributes: { id: @ship.pirate.id, _destroy: not_truth })
assert_nothing_raised(ActiveRecord::RecordNotFound) { @ship.pirate.reload }
end
end
@@ -489,14 +512,14 @@ class TestNestedAttributesOnABelongsToAssociation < ActiveRecord::TestCase
def test_should_not_destroy_an_existing_record_if_allow_destroy_is_false
Ship.accepts_nested_attributes_for :pirate, :allow_destroy => false, :reject_if => proc { |attributes| attributes.empty? }
- @ship.update_attributes(:pirate_attributes => { :id => @ship.pirate.id, :_destroy => '1' })
+ @ship.update(pirate_attributes: { id: @ship.pirate.id, _destroy: '1' })
assert_nothing_raised(ActiveRecord::RecordNotFound) { @ship.pirate.reload }
-
+ ensure
Ship.accepts_nested_attributes_for :pirate, :allow_destroy => true, :reject_if => proc { |attributes| attributes.empty? }
end
- def test_should_work_with_update_attributes_as_well
- @ship.update_attributes({ :name => 'Mister Pablo', :pirate_attributes => { :catchphrase => 'Arr' } })
+ def test_should_work_with_update_as_well
+ @ship.update({ name: 'Mister Pablo', pirate_attributes: { catchphrase: 'Arr' } })
@ship.reload
assert_equal 'Mister Pablo', @ship.name
@@ -525,18 +548,18 @@ class TestNestedAttributesOnABelongsToAssociation < ActiveRecord::TestCase
def test_should_update_existing_when_update_only_is_true_and_no_id_is_given
@pirate.delete
- @pirate = @ship.create_update_only_pirate(:catchphrase => 'Aye')
+ @pirate = @ship.create_update_only_pirate(catchphrase: 'Aye')
- @ship.update_attributes(:update_only_pirate_attributes => { :catchphrase => 'Arr' })
+ @ship.update(update_only_pirate_attributes: { catchphrase: 'Arr' })
assert_equal 'Arr', @pirate.reload.catchphrase
assert_equal @pirate, @ship.reload.update_only_pirate
end
def test_should_update_existing_when_update_only_is_true_and_id_is_given
@pirate.delete
- @pirate = @ship.create_update_only_pirate(:catchphrase => 'Aye')
+ @pirate = @ship.create_update_only_pirate(catchphrase: 'Aye')
- @ship.update_attributes(:update_only_pirate_attributes => { :catchphrase => 'Arr', :id => @pirate.id })
+ @ship.update(update_only_pirate_attributes: { catchphrase: 'Arr', id: @pirate.id })
assert_equal 'Arr', @pirate.reload.catchphrase
assert_equal @pirate, @ship.reload.update_only_pirate
@@ -545,9 +568,9 @@ class TestNestedAttributesOnABelongsToAssociation < ActiveRecord::TestCase
def test_should_destroy_existing_when_update_only_is_true_and_id_is_given_and_is_marked_for_destruction
Ship.accepts_nested_attributes_for :update_only_pirate, :update_only => true, :allow_destroy => true
@pirate.delete
- @pirate = @ship.create_update_only_pirate(:catchphrase => 'Aye')
+ @pirate = @ship.create_update_only_pirate(catchphrase: 'Aye')
- @ship.update_attributes(:update_only_pirate_attributes => { :catchphrase => 'Arr', :id => @pirate.id, :_destroy => true })
+ @ship.update(update_only_pirate_attributes: { catchphrase: 'Arr', id: @pirate.id, _destroy: true })
assert_raise(ActiveRecord::RecordNotFound) { @pirate.reload }
@@ -573,7 +596,7 @@ module NestedAttributesOnACollectionAssociationTests
def test_should_take_a_hash_with_string_keys_and_assign_the_attributes_to_the_associated_models
@alternate_params[association_getter].stringify_keys!
- @pirate.update_attributes @alternate_params
+ @pirate.update @alternate_params
assert_equal ['Grace OMalley', 'Privateers Greed'], [@child_1.reload.name, @child_2.reload.name]
end
@@ -584,7 +607,7 @@ module NestedAttributesOnACollectionAssociationTests
end
def test_should_also_work_with_a_HashWithIndifferentAccess
- @pirate.send(association_setter, HashWithIndifferentAccess.new('foo' => HashWithIndifferentAccess.new(:id => @child_1.id, :name => 'Grace OMalley')))
+ @pirate.send(association_setter, ActiveSupport::HashWithIndifferentAccess.new('foo' => ActiveSupport::HashWithIndifferentAccess.new(:id => @child_1.id, :name => 'Grace OMalley')))
@pirate.save
assert_equal 'Grace OMalley', @child_1.reload.name
end
@@ -619,10 +642,10 @@ module NestedAttributesOnACollectionAssociationTests
def test_should_refresh_saved_records_when_not_overwriting_unsaved_updates
@pirate.reload
- record = @pirate.class.reflect_on_association(@association_name).klass.new(:name => 'Grace OMalley')
+ record = @pirate.class.reflect_on_association(@association_name).klass.new(name: 'Grace OMalley')
@pirate.send(@association_name) << record
record.save!
- @pirate.send(@association_name).last.update_attributes!(:name => 'Polly')
+ @pirate.send(@association_name).last.update!(name: 'Polly')
assert_equal 'Polly', @pirate.send(@association_name).send(:load_target).last.name
end
@@ -709,17 +732,17 @@ module NestedAttributesOnACollectionAssociationTests
end
end
- def test_should_work_with_update_attributes_as_well
- @pirate.update_attributes(:catchphrase => 'Arr',
+ def test_should_work_with_update_as_well
+ @pirate.update(catchphrase: 'Arr',
association_getter => { 'foo' => { :id => @child_1.id, :name => 'Grace OMalley' }})
assert_equal 'Grace OMalley', @child_1.reload.name
end
def test_should_update_existing_records_and_add_new_ones_that_have_no_id
- @alternate_params[association_getter]['baz'] = { :name => 'Buccaneers Servant' }
+ @alternate_params[association_getter]['baz'] = { name: 'Buccaneers Servant' }
assert_difference('@pirate.send(@association_name).count', +1) do
- @pirate.update_attributes @alternate_params
+ @pirate.update @alternate_params
end
assert_equal ['Grace OMalley', 'Privateers Greed', 'Buccaneers Servant'].to_set, @pirate.reload.send(@association_name).map(&:name).to_set
end
@@ -741,7 +764,7 @@ module NestedAttributesOnACollectionAssociationTests
[nil, '', '0', 0, 'false', false].each do |false_variable|
@alternate_params[association_getter]['foo']['_destroy'] = false_variable
assert_no_difference('@pirate.send(@association_name).count') do
- @pirate.update_attributes(@alternate_params)
+ @pirate.update(@alternate_params)
end
end
end
@@ -774,30 +797,12 @@ 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)
- Interest.reflect_on_association(:man).options.delete(:inverse_of)
-
- 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! }
end
- def test_numeric_colum_changes_from_zero_to_no_empty_string
+ def test_numeric_column_changes_from_zero_to_no_empty_string
Man.accepts_nested_attributes_for(:interests)
repair_validations(Interest) do
@@ -805,7 +810,7 @@ module NestedAttributesOnACollectionAssociationTests
man = Man.create(name: 'John')
interest = man.interests.create(topic: 'bar', zine_id: 0)
assert interest.save
- assert !man.update_attributes({interests_attributes: { id: interest.id, zine_id: 'foo' }})
+ assert !man.update({interests_attributes: { id: interest.id, zine_id: 'foo' }})
end
end
@@ -936,18 +941,18 @@ class TestNestedAttributesWithNonStandardPrimaryKeys < ActiveRecord::TestCase
end
def test_should_update_existing_records_with_non_standard_primary_key
- @owner.update_attributes(@params)
+ @owner.update(@params)
assert_equal ['Foo', 'Bar'], @owner.pets.map(&:name)
end
- def test_attr_accessor_of_child_should_be_value_provided_during_update_attributes
+ def test_attr_accessor_of_child_should_be_value_provided_during_update
@owner = owners(:ashley)
@pet1 = pets(:chew)
attributes = {:pets_attributes => { "1"=> { :id => @pet1.id,
:name => "Foo2",
:current_user => "John",
:_destroy=>true }}}
- @owner.update_attributes(attributes)
+ @owner.update(attributes)
assert_equal 'John', Pet.after_destroy_output
end
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 b5f32a57b2..6cd3e2154e 100644
--- a/activerecord/test/cases/persistence_test.rb
+++ b/activerecord/test/cases/persistence_test.rb
@@ -12,14 +12,13 @@ require 'models/minimalistic'
require 'models/warehouse_thing'
require 'models/parrot'
require 'models/minivan'
+require 'models/owner'
require 'models/person'
require 'models/pet'
require 'models/toy'
require 'rexml/document'
-require 'active_support/core_ext/exception'
-
-class PersistencesTest < ActiveRecord::TestCase
+class PersistenceTest < ActiveRecord::TestCase
fixtures :topics, :companies, :developers, :projects, :computers, :accounts, :minimalistics, 'warehouse-things', :authors, :categorizations, :categories, :posts, :minivans, :pets, :toys
# Oracle UPDATE does not support ORDER BY
@@ -140,6 +139,19 @@ class PersistencesTest < 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_delete_many
original_count = Topic.count
Topic.delete(deleting = [1, 2])
@@ -243,20 +255,20 @@ class PersistencesTest < ActiveRecord::TestCase
assert_equal "David", topic2.author_name
end
- def test_update
+ def test_update_object
topic = Topic.new
topic.title = "Another New Topic"
topic.written_on = "2003-12-12 23:23:00"
topic.save
- topicReloaded = Topic.find(topic.id)
- assert_equal("Another New Topic", topicReloaded.title)
+ topic_reloaded = Topic.find(topic.id)
+ assert_equal("Another New Topic", topic_reloaded.title)
- topicReloaded.title = "Updated topic"
- topicReloaded.save
+ topic_reloaded.title = "Updated topic"
+ topic_reloaded.save
- topicReloadedAgain = Topic.find(topic.id)
+ topic_reloaded_again = Topic.find(topic.id)
- assert_equal("Updated topic", topicReloadedAgain.title)
+ assert_equal("Updated topic", topic_reloaded_again.title)
end
def test_update_columns_not_equal_attributes
@@ -264,12 +276,12 @@ class PersistencesTest < ActiveRecord::TestCase
topic.title = "Still another topic"
topic.save
- topicReloaded = Topic.allocate
- topicReloaded.init_with(
+ topic_reloaded = Topic.allocate
+ topic_reloaded.init_with(
'attributes' => topic.attributes.merge('does_not_exist' => 'test')
)
- topicReloaded.title = 'A New Topic'
- assert_nothing_raised { topicReloaded.save }
+ topic_reloaded.title = 'A New Topic'
+ assert_nothing_raised { topic_reloaded.save }
end
def test_update_for_record_with_only_primary_key
@@ -280,12 +292,39 @@ class PersistencesTest < ActiveRecord::TestCase
def test_update_sti_type
assert_instance_of Reply, topics(:second)
- topic = topics(:second).becomes(Topic)
+ topic = topics(:second).becomes!(Topic)
assert_instance_of Topic, topic
topic.save!
assert_instance_of Topic, Topic.find(topic.id)
end
+ def test_preserve_original_sti_type
+ reply = topics(:second)
+ assert_equal "Reply", reply.type
+
+ topic = reply.becomes(Topic)
+ assert_equal "Reply", reply.type
+
+ assert_instance_of Topic, topic
+ assert_equal "Reply", topic.type
+ end
+
+ def test_update_after_create
+ klass = Class.new(Topic) do
+ def self.name; 'Topic'; end
+ after_create do
+ update_attribute("author_name", "David")
+ end
+ end
+ topic = klass.new
+ topic.title = "Another New Topic"
+ topic.save
+
+ topic_reloaded = Topic.find(topic.id)
+ assert_equal("Another New Topic", topic_reloaded.title)
+ assert_equal("David", topic_reloaded.author_name)
+ end
+
def test_delete
topic = Topic.find(1)
assert_equal topic, topic.delete, 'topic.delete did not return self'
@@ -354,7 +393,7 @@ class PersistencesTest < ActiveRecord::TestCase
client.delete
assert client.frozen?
assert_kind_of Firm, client.firm
- assert_raise(ActiveSupport::FrozenObjectError) { client.name = "something else" }
+ assert_raise(RuntimeError) { client.name = "something else" }
end
def test_destroy_new_record
@@ -368,7 +407,7 @@ class PersistencesTest < ActiveRecord::TestCase
client.destroy
assert client.frozen?
assert_kind_of Firm, client.firm
- assert_raise(ActiveSupport::FrozenObjectError) { client.name = "something else" }
+ assert_raise(RuntimeError) { client.name = "something else" }
end
def test_update_attribute
@@ -380,10 +419,6 @@ class PersistencesTest < ActiveRecord::TestCase
assert !Topic.find(1).approved?
end
- def test_update_attribute_does_not_choke_on_nil
- assert Topic.find(1).update_attributes(nil)
- end
-
def test_update_attribute_for_readonly_attribute
minivan = Minivan.find('m1')
assert_raises(ActiveRecord::ActiveRecordError) { minivan.update_attribute(:color, 'black') }
@@ -488,7 +523,7 @@ class PersistencesTest < ActiveRecord::TestCase
def test_update_column_with_one_changed_and_one_updated
t = Topic.order('id').limit(1).first
- title, author_name = t.title, t.author_name
+ author_name = t.author_name
t.author_name = 'John'
t.update_column(:title, 'super_title')
assert_equal 'John', t.author_name
@@ -501,6 +536,14 @@ class PersistencesTest < ActiveRecord::TestCase
assert_equal 'super_title', t.title
end
+ def test_update_column_with_default_scope
+ developer = DeveloperCalledDavid.first
+ developer.name = 'John'
+ developer.save!
+
+ assert developer.update_column(:name, 'Will'), 'did not update record due to default scope'
+ end
+
def test_update_columns
topic = Topic.find(1)
topic.update_columns({ "approved" => true, title: "Sebastian Topic" })
@@ -529,7 +572,7 @@ class PersistencesTest < ActiveRecord::TestCase
def test_update_columns_should_not_leave_the_object_dirty
topic = Topic.find(1)
- topic.update_attributes({ "content" => "Have a nice day", :author_name => "Jose" })
+ topic.update({ "content" => "Have a nice day", :author_name => "Jose" })
topic.reload
topic.update_columns({ content: "You too", "author_name" => "Sebastian" })
@@ -592,6 +635,43 @@ class PersistencesTest < ActiveRecord::TestCase
assert_equal 'super_title', t.title
end
+ def test_update_columns_changing_id
+ topic = Topic.find(1)
+ topic.update_columns(id: 123)
+ assert_equal 123, topic.id
+ topic.reload
+ assert_equal 123, topic.id
+ end
+
+ def test_update_columns_returns_boolean
+ topic = Topic.find(1)
+ assert_equal true, topic.update_columns(title: "New title")
+ end
+
+ def test_update_columns_with_default_scope
+ developer = DeveloperCalledDavid.first
+ developer.name = 'John'
+ developer.save!
+
+ assert developer.update_columns(name: 'Will'), 'did not update record due to default scope'
+ end
+
+ def test_update
+ topic = Topic.find(1)
+ assert !topic.approved?
+ assert_equal "The First Topic", topic.title
+
+ topic.update("approved" => true, "title" => "The First Topic Updated")
+ topic.reload
+ assert topic.approved?
+ assert_equal "The First Topic Updated", topic.title
+
+ topic.update(approved: false, title: "The First Topic")
+ topic.reload
+ assert !topic.approved?
+ assert_equal "The First Topic", topic.title
+ end
+
def test_update_attributes
topic = Topic.find(1)
assert !topic.approved?
@@ -602,10 +682,51 @@ class PersistencesTest < ActiveRecord::TestCase
assert topic.approved?
assert_equal "The First Topic Updated", topic.title
- topic.update_attributes(:approved => false, :title => "The First Topic")
+ topic.update_attributes(approved: false, title: "The First Topic")
topic.reload
assert !topic.approved?
assert_equal "The First Topic", topic.title
+
+ assert_raise(ActiveRecord::RecordNotUnique, ActiveRecord::StatementInvalid) do
+ topic.update_attributes(id: 3, title: "Hm is it possible?")
+ end
+ assert_not_equal "Hm is it possible?", Topic.find(3).title
+
+ topic.update_attributes(id: 1234)
+ assert_nothing_raised { topic.reload }
+ 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)
+ assert_equal "The Second Topic of the day", reply.title
+ assert_equal "Have a nice day", reply.content
+
+ reply.update!("title" => "The Second Topic of the day updated", "content" => "Have a nice evening")
+ reply.reload
+ assert_equal "The Second Topic of the day updated", reply.title
+ assert_equal "Have a nice evening", reply.content
+
+ reply.update!(title: "The Second Topic of the day", content: "Have a nice day")
+ reply.reload
+ assert_equal "The Second Topic of the day", reply.title
+ assert_equal "Have a nice day", reply.content
+
+ assert_raise(ActiveRecord::RecordInvalid) { reply.update!(title: nil, content: "Have a nice evening") }
+ ensure
+ Reply.reset_callbacks(:validate)
end
def test_update_attributes!
@@ -619,12 +740,12 @@ class PersistencesTest < ActiveRecord::TestCase
assert_equal "The Second Topic of the day updated", reply.title
assert_equal "Have a nice evening", reply.content
- reply.update_attributes!(:title => "The Second Topic of the day", :content => "Have a nice day")
+ reply.update_attributes!(title: "The Second Topic of the day", content: "Have a nice day")
reply.reload
assert_equal "The Second Topic of the day", reply.title
assert_equal "Have a nice day", reply.content
- assert_raise(ActiveRecord::RecordInvalid) { reply.update_attributes!(:title => nil, :content => "Have a nice evening") }
+ assert_raise(ActiveRecord::RecordInvalid) { reply.update_attributes!(title: nil, content: "Have a nice evening") }
ensure
Reply.reset_callbacks(:validate)
end
diff --git a/activerecord/test/cases/pooled_connections_test.rb b/activerecord/test/cases/pooled_connections_test.rb
index 0a6354f5cc..626c6aeaf8 100644
--- a/activerecord/test/cases/pooled_connections_test.rb
+++ b/activerecord/test/cases/pooled_connections_test.rb
@@ -7,42 +7,26 @@ class PooledConnectionsTest < ActiveRecord::TestCase
def setup
@per_test_teardown = []
- @connection = ActiveRecord::Model.remove_connection
+ @connection = ActiveRecord::Base.remove_connection
end
def teardown
- ActiveRecord::Model.clear_all_connections!
- ActiveRecord::Model.establish_connection(@connection)
+ ActiveRecord::Base.clear_all_connections!
+ ActiveRecord::Base.establish_connection(@connection)
@per_test_teardown.each {|td| td.call }
end
- def checkout_connections
- ActiveRecord::Model.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::Model.establish_connection(@connection.merge({:pool => pool_size, :checkout_timeout => 0.5}))
+ ActiveRecord::Base.establish_connection(@connection.merge({:pool => pool_size, :checkout_timeout => 0.5}))
@connection_count = 0
@timed_out = 0
threads.times do
Thread.new do
begin
- conn = ActiveRecord::Model.connection_pool.checkout
+ conn = ActiveRecord::Base.connection_pool.checkout
sleep 0.1
- ActiveRecord::Model.connection_pool.checkin conn
+ ActiveRecord::Base.connection_pool.checkin conn
@connection_count += 1
rescue ActiveRecord::ConnectionTimeoutError
@timed_out += 1
@@ -55,13 +39,13 @@ class PooledConnectionsTest < ActiveRecord::TestCase
checkout_checkin_connections 1, 2
assert_equal 2, @connection_count
assert_equal 0, @timed_out
- assert_equal 1, ActiveRecord::Model.connection_pool.connections.size
+ assert_equal 1, ActiveRecord::Base.connection_pool.connections.size
end
private
def add_record(name)
- ActiveRecord::Model.connection_pool.with_connection { Project.create! :name => name }
+ ActiveRecord::Base.connection_pool.with_connection { Project.create! :name => name }
end
end unless current_adapter?(:FrontBase) || in_memory_db?
diff --git a/activerecord/test/cases/primary_keys_test.rb b/activerecord/test/cases/primary_keys_test.rb
index e6e50a4cd4..aa125c70c5 100644
--- a/activerecord/test/cases/primary_keys_test.rb
+++ b/activerecord/test/cases/primary_keys_test.rb
@@ -188,31 +188,31 @@ class PrimaryKeyWithNoConnectionTest < ActiveRecord::TestCase
def test_set_primary_key_with_no_connection
return skip("disconnect wipes in-memory db") if in_memory_db?
- connection = ActiveRecord::Model.remove_connection
+ connection = ActiveRecord::Base.remove_connection
model = Class.new(ActiveRecord::Base)
model.primary_key = 'foo'
assert_equal 'foo', model.primary_key
- ActiveRecord::Model.establish_connection(connection)
+ ActiveRecord::Base.establish_connection(connection)
assert_equal 'foo', model.primary_key
end
end
-if current_adapter?(:MysqlAdapter) or current_adapter?(:Mysql2Adapter)
+if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
class PrimaryKeyWithAnsiQuotesTest < ActiveRecord::TestCase
self.use_transactional_fixtures = false
-
- def test_primaery_key_method_with_ansi_quotes
+
+ def test_primary_key_method_with_ansi_quotes
con = ActiveRecord::Base.connection
con.execute("SET SESSION sql_mode='ANSI_QUOTES'")
assert_equal "id", con.primary_key("topics")
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 51a285a2b4..136fda664c 100644
--- a/activerecord/test/cases/query_cache_test.rb
+++ b/activerecord/test/cases/query_cache_test.rb
@@ -167,7 +167,7 @@ class QueryCacheTest < ActiveRecord::TestCase
# Oracle adapter returns count() as Fixnum or Float
if current_adapter?(:OracleAdapter)
assert_kind_of Numeric, Task.connection.select_value("SELECT count(*) AS count_all FROM tasks")
- elsif current_adapter?(:SQLite3Adapter) || current_adapter?(:Mysql2Adapter)
+ elsif current_adapter?(:SQLite3Adapter, :Mysql2Adapter)
# Future versions of the sqlite3 adapter will return numeric
assert_instance_of Fixnum,
Task.connection.select_value("SELECT count(*) AS count_all FROM tasks")
@@ -184,6 +184,17 @@ class QueryCacheTest < ActiveRecord::TestCase
assert_queries(2) { task.lock!; task.lock! }
end
end
+
+ def test_cache_is_available_when_connection_is_connected
+ conf = ActiveRecord::Base.configurations
+
+ ActiveRecord::Base.configurations = {}
+ Task.cache do
+ assert_queries(1) { Task.find(1); Task.find(1) }
+ end
+ ensure
+ ActiveRecord::Base.configurations = conf
+ end
end
class QueryCacheExpiryTest < ActiveRecord::TestCase
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 a9d46f4fba..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
@@ -35,18 +40,18 @@ class ReflectionTest < ActiveRecord::TestCase
def test_read_attribute_names
assert_equal(
- %w( id title author_name author_email_address bonus_time written_on last_read content important group approved replies_count parent_id parent_title type created_at updated_at ).sort,
+ %w( id title author_name author_email_address bonus_time written_on last_read content important group approved replies_count unique_replies_count parent_id parent_title type created_at updated_at ).sort,
@first.attribute_names.sort
)
end
def test_columns
- assert_equal 17, Topic.columns.length
+ assert_equal 18, Topic.columns.length
end
def test_columns_are_returned_in_the_order_they_were_declared
column_names = Topic.columns.map { |column| column.name }
- assert_equal %w(id title author_name author_email_address written_on bonus_time last_read content important approved replies_count parent_id parent_title type group created_at updated_at), column_names
+ assert_equal %w(id title author_name author_email_address written_on bonus_time last_read content important approved replies_count unique_replies_count parent_id parent_title type group created_at updated_at), column_names
end
def test_content_columns
@@ -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..c171c5e14e
--- /dev/null
+++ b/activerecord/test/cases/relation/delegation_test.rb
@@ -0,0 +1,98 @@
+require 'cases/helper'
+require 'models/post'
+require 'models/comment'
+
+module ActiveRecord
+ class DelegationTest < ActiveRecord::TestCase
+ fixtures :posts
+
+ def assert_responds(target, method)
+ assert target.respond_to?(method)
+ assert_nothing_raised do
+ method_arity = target.to_a.method(method).arity
+
+ if method_arity.zero?
+ target.send(method)
+ elsif method_arity < 0
+ if method == :shuffle!
+ target.send(method)
+ else
+ target.send(method, 1)
+ end
+ else
+ raise NotImplementedError
+ end
+ end
+ end
+ end
+
+ class DelegationAssociationTest < DelegationTest
+ def target
+ Post.first.comments
+ end
+
+ [:map, :collect].each do |method|
+ test "##{method} is delgated" do
+ assert_responds(target, method)
+ assert_equal(target.pluck(:body), target.send(method) {|post| post.body })
+ end
+
+ test "##{method}! is not delgated" do
+ assert_deprecated do
+ assert_responds(target, "#{method}!")
+ end
+ end
+ end
+
+ [:compact!, :flatten!, :reject!, :reverse!, :rotate!,
+ :shuffle!, :slice!, :sort!, :sort_by!].each do |method|
+ test "##{method} delegation is deprecated" do
+ assert_deprecated do
+ assert_responds(target, method)
+ end
+ end
+ end
+
+ [:select!, :uniq!].each do |method|
+ test "##{method} is implemented" do
+ assert_responds(target, method)
+ end
+ end
+ end
+
+ class DelegationRelationTest < DelegationTest
+ def target
+ Comment.where.not(body: nil)
+ end
+
+ [:map, :collect].each do |method|
+ test "##{method} is delgated" do
+ assert_responds(target, method)
+ assert_equal(target.pluck(:body), target.send(method) {|post| post.body })
+ end
+
+ test "##{method}! is not delgated" do
+ assert_deprecated do
+ assert_responds(target, "#{method}!")
+ end
+ end
+ end
+
+ [:compact!, :flatten!, :reject!, :reverse!, :rotate!,
+ :shuffle!, :slice!, :sort!, :sort_by!].each do |method|
+ test "##{method} delegation is deprecated" do
+ assert_deprecated do
+ assert_responds(target, method)
+ end
+ end
+ end
+
+ [:select!, :uniq!].each do |method|
+ test "##{method} is triggers an immutable error" do
+ assert_raises ActiveRecord::ImmutableRelation do
+ assert_responds(target, method)
+ end
+ end
+ end
+ 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..020fb24afa
--- /dev/null
+++ b/activerecord/test/cases/relation/mutation_test.rb
@@ -0,0 +1,148 @@
+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 arel_table
+ Post.arel_table
+ end
+
+ 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'), :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)
+ 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
new file mode 100644
index 0000000000..92d1e013e8
--- /dev/null
+++ b/activerecord/test/cases/relation/where_chain_test.rb
@@ -0,0 +1,80 @@
+require 'cases/helper'
+require 'models/post'
+require 'models/comment'
+
+module ActiveRecord
+ class WhereChainTest < ActiveRecord::TestCase
+ fixtures :posts
+
+ def setup
+ super
+ @name = 'title'
+ end
+
+ def test_not_eq
+ expected = Arel::Nodes::NotEqual.new(Post.arel_table[@name], 'hello')
+ relation = Post.where.not(title: 'hello')
+ assert_equal([expected], relation.where_values)
+ end
+
+ def test_not_null
+ expected = Arel::Nodes::NotEqual.new(Post.arel_table[@name], nil)
+ relation = Post.where.not(title: nil)
+ assert_equal([expected], relation.where_values)
+ 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])
+ assert_equal([expected], relation.where_values)
+ end
+
+ def test_association_not_eq
+ expected = Arel::Nodes::NotEqual.new(Comment.arel_table[@name], 'hello')
+ relation = Post.joins(:comments).where.not(comments: {title: 'hello'})
+ assert_equal(expected.to_sql, relation.where_values.first.to_sql)
+ end
+
+ def test_not_eq_with_preceding_where
+ relation = Post.where(title: 'hello').where.not(title: 'world')
+
+ expected = Arel::Nodes::Equality.new(Post.arel_table[@name], 'hello')
+ assert_equal(expected, relation.where_values.first)
+
+ expected = Arel::Nodes::NotEqual.new(Post.arel_table[@name], 'world')
+ assert_equal(expected, relation.where_values.last)
+ end
+
+ def test_not_eq_with_succeeding_where
+ relation = Post.where.not(title: 'hello').where(title: 'world')
+
+ expected = Arel::Nodes::NotEqual.new(Post.arel_table[@name], 'hello')
+ assert_equal(expected, relation.where_values.first)
+
+ expected = Arel::Nodes::Equality.new(Post.arel_table[@name], 'world')
+ assert_equal(expected, relation.where_values.last)
+ end
+
+ def test_not_eq_with_string_parameter
+ expected = Arel::Nodes::Not.new("title = 'hello'")
+ relation = Post.where.not("title = 'hello'")
+ assert_equal([expected], relation.where_values)
+ end
+
+ def test_not_eq_with_array_parameter
+ expected = Arel::Nodes::Not.new("title = 'hello'")
+ relation = Post.where.not(['title = ?', 'hello'])
+ assert_equal([expected], relation.where_values)
+ end
+
+ def test_chaining_multiple
+ relation = Post.where.not(author_id: [1, 2]).where.not(title: 'ruby on rails')
+
+ expected = Arel::Nodes::NotIn.new(Post.arel_table['author_id'], [1, 2])
+ assert_equal(expected, relation.where_values[0])
+
+ expected = Arel::Nodes::NotEqual.new(Post.arel_table[@name], 'ruby on rails')
+ assert_equal(expected, relation.where_values[1])
+ end
+ end
+end
diff --git a/activerecord/test/cases/relation/where_test.rb b/activerecord/test/cases/relation/where_test.rb
index 9c0b139dbf..3e460fa3d6 100644
--- a/activerecord/test/cases/relation/where_test.rb
+++ b/activerecord/test/cases/relation/where_test.rb
@@ -5,10 +5,24 @@ require 'models/treasure'
require 'models/post'
require 'models/comment'
require 'models/edge'
+require 'models/topic'
module ActiveRecord
class WhereTest < ActiveRecord::TestCase
- fixtures :posts, :edges
+ fixtures :posts, :edges, :authors
+
+ def test_where_copies_bind_params
+ author = authors(:david)
+ posts = author.posts.where('posts.id != 1')
+ joined = Post.where(id: posts)
+
+ assert_operator joined.length, :>, 0
+
+ joined.each { |post|
+ assert_equal author, post.author
+ assert_not_equal 1, post.id
+ }
+ end
def test_belongs_to_shallow_where
author = Author.new
@@ -67,6 +81,38 @@ 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')
+
+ assert_equal expected.to_sql, actual.to_sql
+ end
+
def test_where_error
assert_raises(ActiveRecord::StatementInvalid) do
Post.where(:id => { 'posts.author_id' => 10 }).first
@@ -82,8 +128,18 @@ module ActiveRecord
assert_equal 0, Post.where(:posts => {}).count
end
+ def test_where_with_table_name_and_empty_array
+ assert_equal 0, Post.where(:id => []).count
+ end
+
def test_where_with_empty_hash_and_no_foreign_key
assert_equal 0, Edge.where(:sink => {}).count
end
+
+ def test_where_with_blank_conditions
+ [[], {}, nil, ""].each do |blank|
+ assert_equal 4, Edge.where(blank).order("sink_id").to_a.size
+ end
+ end
end
end
diff --git a/activerecord/test/cases/relation_test.rb b/activerecord/test/cases/relation_test.rb
index 6399111be6..70d113fb39 100644
--- a/activerecord/test/cases/relation_test.rb
+++ b/activerecord/test/cases/relation_test.rb
@@ -1,31 +1,37 @@
require "cases/helper"
require 'models/post'
require 'models/comment'
+require 'models/author'
+require 'models/rating'
module ActiveRecord
class RelationTest < ActiveRecord::TestCase
- fixtures :posts, :comments
+ fixtures :posts, :comments, :authors
- class FakeKlass < Struct.new(:table_name)
+ class FakeKlass < Struct.new(:table_name, :name)
+ extend ActiveRecord::Delegation::DelegateCache
+
+ inherited self
+
+ def self.connection
+ Post.connection
+ end
end
def test_construction
- relation = nil
- assert_nothing_raised do
- relation = Relation.new :a, :b
- end
- assert_equal :a, relation.klass
+ relation = Relation.new FakeKlass, :b
+ assert_equal FakeKlass, relation.klass
assert_equal :b, relation.table
assert !relation.loaded, 'relation is not loaded'
end
def test_responds_to_model_and_returns_klass
- relation = Relation.new :a, :b
- assert_equal :a, relation.model
+ relation = Relation.new FakeKlass, :b
+ assert_equal FakeKlass, relation.model
end
def test_initialize_single_values
- relation = Relation.new :a, :b
+ relation = Relation.new FakeKlass, :b
(Relation::SINGLE_VALUE_METHODS - [:create_with]).each do |method|
assert_nil relation.send("#{method}_value"), method.to_s
end
@@ -33,19 +39,19 @@ module ActiveRecord
end
def test_multi_value_initialize
- relation = Relation.new :a, :b
+ relation = Relation.new FakeKlass, :b
Relation::MULTI_VALUE_METHODS.each do |method|
assert_equal [], relation.send("#{method}_values"), method.to_s
end
end
def test_extensions
- relation = Relation.new :a, :b
+ relation = Relation.new FakeKlass, :b
assert_equal [], relation.extensions
end
def test_empty_where_values_hash
- relation = Relation.new :a, :b
+ relation = Relation.new FakeKlass, :b
assert_equal({}, relation.where_values_hash)
relation.where! :hello
@@ -74,12 +80,12 @@ 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
- relation = Relation.new :a, :b
+ relation = Relation.new FakeKlass, :b
assert_equal({}, relation.scope_for_create)
end
@@ -109,32 +115,38 @@ 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 :a, :b
+ relation = Relation.new FakeKlass, :b
assert !relation.eager_loading?
end
def test_eager_load_values
- relation = Relation.new :a, :b
+ relation = Relation.new FakeKlass, :b
relation.eager_load! :b
assert relation.eager_loading?
end
def test_references_values
- relation = Relation.new :a, :b
+ relation = Relation.new FakeKlass, :b
assert_equal [], relation.references_values
relation = relation.references(:foo).references(:omg, :lol)
assert_equal ['foo', 'omg', 'lol'], relation.references_values
end
def test_references_values_dont_duplicate
- relation = Relation.new :a, :b
+ relation = Relation.new FakeKlass, :b
relation = relation.references(:foo).references(:foo)
assert_equal ['foo'], relation.references_values
end
test 'merging a hash into a relation' do
- relation = Relation.new :a, :b
+ relation = Relation.new FakeKlass, :b
relation = relation.merge where: :lol, readonly: true
assert_equal [:lol], relation.where_values
@@ -142,7 +154,7 @@ module ActiveRecord
end
test 'merging an empty hash into a relation' do
- assert_equal [], Relation.new(:a, :b).merge({}).where_values
+ assert_equal [], Relation.new(FakeKlass, :b).merge({}).where_values
end
test 'merging a hash with unknown keys raises' do
@@ -150,7 +162,7 @@ module ActiveRecord
end
test '#values returns a dup of the values' do
- relation = Relation.new(:a, :b).where! :foo
+ relation = Relation.new(FakeKlass, :b).where! :foo
values = relation.values
values[:where] = nil
@@ -158,103 +170,57 @@ module ActiveRecord
end
test 'relations can be created with a values hash' do
- relation = Relation.new(:a, :b, where: [:foo])
+ relation = Relation.new(FakeKlass, :b, where: [:foo])
assert_equal [:foo], relation.where_values
end
test 'merging a single where value' do
- relation = Relation.new(:a, :b)
+ relation = Relation.new(FakeKlass, :b)
relation.merge!(where: :foo)
assert_equal [:foo], relation.where_values
end
test 'merging a hash interpolates conditions' do
- klass = stub
- 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
- end
-
- class RelationMutationTest < ActiveSupport::TestCase
- def relation
- @relation ||= Relation.new :a, :b
- end
- (Relation::MULTI_VALUE_METHODS - [:references, :extending]).each do |method|
- test "##{method}!" do
- assert relation.public_send("#{method}!", :foo).equal?(relation)
- assert_equal [:foo], relation.public_send("#{method}_values")
- end
+ 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
- test '#references!' do
- assert relation.references!(:foo).equal?(relation)
- assert relation.references_values.include?('foo')
+ 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
- test 'extending!' do
- mod, mod2 = Module.new, Module.new
+ def test_respond_to_for_non_selected_element
+ post = Post.select(:title).first
+ assert_equal false, post.respond_to?(:body), "post should not respond_to?(:body) since invoking it raises exception"
- assert 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
+ 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
- (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
+ 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
- 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 '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
end
end
diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb
index fdbc0a3fdb..c9c7ac04b3 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,13 @@ 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_conditions
assert_equal ["David"], Author.where(:name => 'David').map(&:name)
assert_equal ['Mary'], Author.where(["name = ?", 'Mary']).map(&:name)
@@ -157,19 +169,23 @@ class RelationTest < ActiveRecord::TestCase
assert_equal 4, topics.to_a.size
assert_equal topics(:first).title, topics.first.title
end
-
+
def test_finding_with_assoc_order
topics = Topic.order(:id => :desc)
assert_equal 4, topics.to_a.size
assert_equal topics(:fourth).title, topics.first.title
end
-
+
def test_finding_with_reverted_assoc_order
topics = Topic.order(:id => :asc).reverse_order
assert_equal 4, topics.to_a.size
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 +196,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 +274,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 +294,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 +305,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
@@ -321,6 +342,22 @@ class RelationTest < ActiveRecord::TestCase
assert_equal 1, person_with_reader_and_post.size
end
+ def test_no_arguments_to_query_methods_raise_errors
+ assert_raises(ArgumentError) { Topic.references() }
+ assert_raises(ArgumentError) { Topic.includes() }
+ assert_raises(ArgumentError) { Topic.preload() }
+ assert_raises(ArgumentError) { Topic.group() }
+ assert_raises(ArgumentError) { Topic.reorder() }
+ end
+
+ def test_blank_like_arguments_to_query_methods_dont_raise_errors
+ assert_nothing_raised { Topic.references([]) }
+ assert_nothing_raised { Topic.includes([]) }
+ assert_nothing_raised { Topic.preload([]) }
+ assert_nothing_raised { Topic.group([]) }
+ assert_nothing_raised { Topic.reorder([]) }
+ end
+
def test_scoped_responds_to_delegated_methods
relation = Topic.all
@@ -351,7 +388,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
@@ -404,6 +441,13 @@ class RelationTest < ActiveRecord::TestCase
end
end
+ def test_preload_applies_to_all_chained_preloaded_scopes
+ assert_queries(3) do
+ post = Post.with_comments.with_tags.first
+ assert post
+ end
+ end
+
def test_find_with_included_associations
assert_queries(2) do
posts = Post.includes(:comments).order('posts.id')
@@ -442,6 +486,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 }
@@ -457,6 +509,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 }
@@ -469,6 +529,7 @@ class RelationTest < ActiveRecord::TestCase
expected_taggings = taggings(:welcome_general, :thinking_general)
assert_no_queries do
+ assert_equal expected_taggings, author.taggings.distinct.sort_by { |t| t.id }
assert_equal expected_taggings, author.taggings.uniq.sort_by { |t| t.id }
end
@@ -509,7 +570,7 @@ class RelationTest < ActiveRecord::TestCase
def test_find_in_empty_array
authors = Author.all.where(:id => [])
- assert_blank authors.to_a
+ assert authors.to_a.blank?
end
def test_where_with_ar_object
@@ -573,6 +634,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
@@ -691,6 +782,13 @@ class RelationTest < ActiveRecord::TestCase
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))
@@ -723,7 +821,7 @@ class RelationTest < ActiveRecord::TestCase
def test_relation_merging_with_locks
devs = Developer.lock.where("salary >= 80000").order("id DESC").merge(Developer.limit(2))
- assert_present devs.locked
+ assert devs.locked.present?
end
def test_relation_merging_with_preload
@@ -759,11 +857,11 @@ class RelationTest < ActiveRecord::TestCase
def test_count_with_distinct
posts = Post.all
- assert_equal 3, posts.count(:comments_count, :distinct => true)
- assert_equal 11, posts.count(:comments_count, :distinct => false)
+ assert_equal 3, posts.distinct(true).count(:comments_count)
+ assert_equal 11, posts.distinct(false).count(:comments_count)
- assert_equal 3, posts.select(:comments_count).count(:distinct => true)
- assert_equal 11, posts.select(:comments_count).count(:distinct => false)
+ assert_equal 3, posts.distinct(true).select(:comments_count).count
+ assert_equal 11, posts.distinct(false).select(:comments_count).count
end
def test_count_explicit_columns
@@ -1058,6 +1156,39 @@ class RelationTest < ActiveRecord::TestCase
assert_equal 'parrot', parrot.name
end
+ def test_find_or_create_by
+ assert_nil Bird.find_by(name: 'bob')
+
+ bird = Bird.find_or_create_by(name: 'bob')
+ assert bird.persisted?
+
+ assert_equal bird, Bird.find_or_create_by(name: 'bob')
+ end
+
+ def test_find_or_create_by_with_create_with
+ assert_nil Bird.find_by(name: 'bob')
+
+ bird = Bird.create_with(color: 'green').find_or_create_by(name: 'bob')
+ assert bird.persisted?
+ assert_equal 'green', bird.color
+
+ assert_equal bird, Bird.create_with(color: 'blue').find_or_create_by(name: 'bob')
+ end
+
+ def test_find_or_create_by!
+ assert_raises(ActiveRecord::RecordInvalid) { Bird.find_or_create_by!(color: 'green') }
+ end
+
+ def test_find_or_initialize_by
+ assert_nil Bird.find_by(name: 'bob')
+
+ bird = Bird.find_or_initialize_by(name: 'bob')
+ assert bird.new_record?
+ bird.save!
+
+ assert_equal bird, Bird.find_or_initialize_by(name: 'bob')
+ end
+
def test_explicit_create_scope
hens = Bird.where(:name => 'hen')
assert_equal 'hen', hens.new.name
@@ -1110,20 +1241,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
@@ -1143,20 +1274,9 @@ 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
@@ -1206,7 +1326,7 @@ class RelationTest < ActiveRecord::TestCase
assert_equal posts(:welcome), comments(:greetings).post
end
- def test_uniq
+ def test_distinct
tag1 = Tag.create(:name => 'Foo')
tag2 = Tag.create(:name => 'Foo')
@@ -1214,14 +1334,25 @@ class RelationTest < ActiveRecord::TestCase
assert_equal ['Foo', 'Foo'], query.map(&:name)
assert_sql(/DISTINCT/) do
+ assert_equal ['Foo'], query.distinct.map(&:name)
assert_equal ['Foo'], query.uniq.map(&:name)
end
assert_sql(/DISTINCT/) do
+ assert_equal ['Foo'], query.distinct(true).map(&:name)
assert_equal ['Foo'], query.uniq(true).map(&:name)
end
+ assert_equal ['Foo', 'Foo'], query.distinct(true).distinct(false).map(&:name)
assert_equal ['Foo', 'Foo'], query.uniq(true).uniq(false).map(&:name)
end
+ def test_doesnt_add_having_values_if_options_are_blank
+ scope = Post.having('')
+ assert_equal [], scope.having_values
+
+ scope = Post.having([])
+ assert_equal [], scope.having_values
+ end
+
def test_references_triggers_eager_loading
scope = Post.includes(:comments)
assert !scope.eager_loading?
@@ -1267,6 +1398,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
@@ -1358,6 +1507,15 @@ class RelationTest < ActiveRecord::TestCase
end
end
+ test "loaded relations cannot be mutated by extending!" do
+ relation = Post.all
+ relation.to_a
+
+ assert_raises(ActiveRecord::ImmutableRelation) do
+ relation.extending! Module.new
+ end
+ end
+
test "relations show the records in #inspect" do
relation = Post.limit(2)
assert_equal "#<ActiveRecord::Relation [#{Post.limit(2).map(&:inspect).join(', ')}]>", relation.inspect
@@ -1396,4 +1554,99 @@ class RelationTest < ActiveRecord::TestCase
end
assert_no_queries { relation.to_a }
end
+
+ test 'group with select and includes' do
+ authors_count = Post.select('author_id, COUNT(author_id) AS num_posts').
+ group('author_id').order('author_id').includes(:author).to_a
+
+ assert_no_queries do
+ result = authors_count.map do |post|
+ [post.num_posts, post.author.try(:name)]
+ end
+
+ expected = [[1, nil], [5, "David"], [3, "Mary"], [2, "Bob"]]
+ assert_equal expected, result
+ end
+ end
+
+ test "delegations do not leak to other classes" do
+ Topic.all.by_lifo
+ assert Topic.all.class.method_defined?(:by_lifo)
+ assert !Post.all.respond_to?(:by_lifo)
+ 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")
+
+ 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
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 817897ceac..766b2ff2ef 100644
--- a/activerecord/test/cases/sanitize_test.rb
+++ b/activerecord/test/cases/sanitize_test.rb
@@ -1,10 +1,21 @@
require "cases/helper"
require 'models/binary'
+require 'models/author'
+require 'models/post'
class SanitizeTest < ActiveRecord::TestCase
def setup
end
+ def test_sanitize_sql_hash_handles_associations
+ quoted_bambi = ActiveRecord::Base.connection.quote("Bambi")
+ quoted_column_name = ActiveRecord::Base.connection.quote_column_name("name")
+ quoted_table_name = ActiveRecord::Base.connection.quote_table_name("adorable_animals")
+ expected_value = "#{quoted_table_name}.#{quoted_column_name} = #{quoted_bambi}"
+
+ assert_equal expected_value, Binary.send(:sanitize_sql_hash, {adorable_animals: {name: 'Bambi'}})
+ end
+
def test_sanitize_sql_array_handles_string_interpolation
quoted_bambi = ActiveRecord::Base.connection.quote_string("Bambi")
assert_equal "name=#{quoted_bambi}", Binary.send(:sanitize_sql_array, ["name=%s", "Bambi"])
@@ -22,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 80f46c6b08..1ee8e60924 100644
--- a/activerecord/test/cases/schema_dumper_test.rb
+++ b/activerecord/test/cases/schema_dumper_test.rb
@@ -1,14 +1,9 @@
require "cases/helper"
-
class SchemaDumperTest < ActiveRecord::TestCase
- def initialize(*)
- super
- ActiveRecord::SchemaMigration.create_table
- end
-
def setup
super
+ ActiveRecord::SchemaMigration.create_table
@stream = StringIO.new
end
@@ -116,7 +111,7 @@ class SchemaDumperTest < ActiveRecord::TestCase
assert_match %r{c_int_4.*}, output
assert_no_match %r{c_int_4.*limit:}, output
- elsif current_adapter?(:MysqlAdapter) or current_adapter?(:Mysql2Adapter)
+ elsif current_adapter?(:MysqlAdapter, :Mysql2Adapter)
assert_match %r{c_int_1.*limit: 1}, output
assert_match %r{c_int_2.*limit: 2}, output
assert_match %r{c_int_3.*limit: 3}, output
@@ -182,13 +177,19 @@ class SchemaDumperTest < ActiveRecord::TestCase
def test_schema_dumps_index_columns_in_right_order
index_definition = standard_dump.split(/\n/).grep(/add_index.*companies/).first.strip
- assert_equal 'add_index "companies", ["firm_id", "type", "rating"], name: "company_index"', index_definition
+ if current_adapter?(:MysqlAdapter) || current_adapter?(:Mysql2Adapter) || current_adapter?(:PostgreSQLAdapter)
+ assert_equal 'add_index "companies", ["firm_id", "type", "rating"], name: "company_index", using: :btree', index_definition
+ else
+ assert_equal 'add_index "companies", ["firm_id", "type", "rating"], name: "company_index"', index_definition
+ end
end
def test_schema_dumps_partial_indices
index_definition = standard_dump.split(/\n/).grep(/add_index.*company_partial_index/).first.strip
if current_adapter?(:PostgreSQLAdapter)
- assert_equal 'add_index "companies", ["firm_id", "type"], name: "company_partial_index", where: "(rating > 10)"', index_definition
+ assert_equal 'add_index "companies", ["firm_id", "type"], name: "company_partial_index", where: "(rating > 10)", using: :btree', index_definition
+ elsif current_adapter?(:MysqlAdapter) || current_adapter?(:Mysql2Adapter)
+ assert_equal 'add_index "companies", ["firm_id", "type"], name: "company_partial_index", using: :btree', index_definition
else
assert_equal 'add_index "companies", ["firm_id", "type"], name: "company_partial_index"', index_definition
end
@@ -201,12 +202,23 @@ class SchemaDumperTest < ActiveRecord::TestCase
assert_match %r(primary_key: "movieid"), match[1], "non-standard primary key not preserved"
end
- if current_adapter?(:MysqlAdapter) or current_adapter?(:Mysql2Adapter)
+ 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
assert_match %r{t.text\s+"body",\s+null: false$}, output
end
+ def test_schema_dump_includes_length_for_mysql_binary_fields
+ output = standard_dump
+ assert_match %r{t.binary\s+"var_binary",\s+limit: 255$}, output
+ assert_match %r{t.binary\s+"var_binary_large",\s+limit: 4095$}, output
+ end
+
def test_schema_dump_includes_length_for_mysql_blob_and_text_fields
output = standard_dump
assert_match %r{t.binary\s+"tiny_blob",\s+limit: 255$}, output
@@ -218,6 +230,12 @@ class SchemaDumperTest < ActiveRecord::TestCase
assert_match %r{t.text\s+"medium_text",\s+limit: 16777215$}, output
assert_match %r{t.text\s+"long_text",\s+limit: 2147483647$}, output
end
+
+ def test_schema_dumps_index_type
+ output = standard_dump
+ assert_match %r{add_index "key_tests", \["awesome"\], name: "index_key_tests_on_awesome", type: :fulltext}, output
+ assert_match %r{add_index "key_tests", \["pizza"\], name: "index_key_tests_on_pizza", using: :btree}, output
+ end
end
def test_schema_dump_includes_decimal_options
@@ -229,6 +247,26 @@ class SchemaDumperTest < ActiveRecord::TestCase
end
if current_adapter?(:PostgreSQLAdapter)
+ def test_schema_dump_includes_bigint_default
+ output = standard_dump
+ assert_match %r{t.integer\s+"bigint_default",\s+limit: 8,\s+default: 0}, output
+ end
+
+ def test_schema_dump_includes_extensions
+ connection = ActiveRecord::Base.connection
+ skip unless connection.supports_extensions?
+
+ 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
+ end
+
def test_schema_dump_includes_xml_shorthand_definition
output = standard_dump
if %r{create_table "postgresql_xml_data_type"} =~ output
@@ -245,28 +283,28 @@ class SchemaDumperTest < ActiveRecord::TestCase
def test_schema_dump_includes_inet_shorthand_definition
output = standard_dump
- if %r{create_table "postgresql_network_address"} =~ output
- assert_match %r{t.inet "inet_address"}, output
+ if %r{create_table "postgresql_network_addresses"} =~ output
+ assert_match %r{t.inet\s+"inet_address",\s+default: "192.168.1.1"}, output
end
end
def test_schema_dump_includes_cidr_shorthand_definition
output = standard_dump
- if %r{create_table "postgresql_network_address"} =~ output
- assert_match %r{t.cidr "cidr_address"}, output
+ if %r{create_table "postgresql_network_addresses"} =~ output
+ assert_match %r{t.cidr\s+"cidr_address",\s+default: "192.168.1.0/24"}, output
end
end
def test_schema_dump_includes_macaddr_shorthand_definition
output = standard_dump
- if %r{create_table "postgresql_network_address"} =~ output
- assert_match %r{t.macaddr "macaddr_address"}, output
+ if %r{create_table "postgresql_network_addresses"} =~ output
+ assert_match %r{t.macaddr\s+"mac_address",\s+default: "ff:ff:ff:ff:ff:ff"}, output
end
end
def test_schema_dump_includes_uuid_shorthand_definition
output = standard_dump
- if %r{create_table "poistgresql_uuids"} =~ output
+ if %r{create_table "postgresql_uuids"} =~ output
assert_match %r{t.uuid "guid"}, output
end
end
@@ -278,6 +316,13 @@ class SchemaDumperTest < ActiveRecord::TestCase
end
end
+ def test_schema_dump_includes_ltrees_shorthand_definition
+ output = standard_dump
+ if %r{create_table "postgresql_ltrees"} =~ output
+ assert_match %r[t.ltree "path"], output
+ end
+ end
+
def test_schema_dump_includes_arrays_shorthand_definition
output = standard_dump
if %r{create_table "postgresql_arrays"} =~ output
diff --git a/activerecord/test/cases/scoping/default_scoping_test.rb b/activerecord/test/cases/scoping/default_scoping_test.rb
new file mode 100644
index 0000000000..76f395ba83
--- /dev/null
+++ b/activerecord/test/cases/scoping/default_scoping_test.rb
@@ -0,0 +1,384 @@
+require 'cases/helper'
+require 'models/post'
+require 'models/developer'
+
+class DefaultScopingTest < ActiveRecord::TestCase
+ fixtures :developers, :posts
+
+ def test_default_scope
+ expected = Developer.all.merge!(:order => 'salary DESC').to_a.collect { |dev| dev.salary }
+ received = DeveloperOrderedBySalary.all.collect { |dev| dev.salary }
+ assert_equal expected, received
+ end
+
+ def test_default_scope_as_class_method
+ assert_equal [developers(:david).becomes(ClassMethodDeveloperCalledDavid)], ClassMethodDeveloperCalledDavid.all
+ end
+
+ def test_default_scope_as_class_method_referencing_scope
+ assert_equal [developers(:david).becomes(ClassMethodReferencingScopeDeveloperCalledDavid)], ClassMethodReferencingScopeDeveloperCalledDavid.all
+ end
+
+ def test_default_scope_as_block_referencing_scope
+ assert_equal [developers(:david).becomes(LazyBlockReferencingScopeDeveloperCalledDavid)], LazyBlockReferencingScopeDeveloperCalledDavid.all
+ end
+
+ def test_default_scope_with_lambda
+ assert_equal [developers(:david).becomes(LazyLambdaDeveloperCalledDavid)], LazyLambdaDeveloperCalledDavid.all
+ end
+
+ def test_default_scope_with_block
+ assert_equal [developers(:david).becomes(LazyBlockDeveloperCalledDavid)], LazyBlockDeveloperCalledDavid.all
+ end
+
+ def test_default_scope_with_callable
+ assert_equal [developers(:david).becomes(CallableDeveloperCalledDavid)], CallableDeveloperCalledDavid.all
+ end
+
+ def test_default_scope_is_unscoped_on_find
+ assert_equal 1, DeveloperCalledDavid.count
+ assert_equal 11, DeveloperCalledDavid.unscoped.count
+ end
+
+ def test_default_scope_is_unscoped_on_create
+ assert_nil DeveloperCalledJamis.unscoped.create!.name
+ end
+
+ def test_default_scope_with_conditions_string
+ assert_equal Developer.where(name: 'David').map(&:id).sort, DeveloperCalledDavid.all.map(&:id).sort
+ assert_equal nil, DeveloperCalledDavid.create!.name
+ end
+
+ def test_default_scope_with_conditions_hash
+ assert_equal Developer.where(name: 'Jamis').map(&:id).sort, DeveloperCalledJamis.all.map(&:id).sort
+ assert_equal 'Jamis', DeveloperCalledJamis.create!.name
+ end
+
+ def test_default_scoping_with_threads
+ skip "in-memory database mustn't disconnect" if in_memory_db?
+
+ 2.times do
+ Thread.new {
+ assert DeveloperOrderedBySalary.all.to_sql.include?('salary DESC')
+ DeveloperOrderedBySalary.connection.close
+ }.join
+ end
+ end
+
+ def test_default_scope_with_inheritance
+ wheres = InheritedPoorDeveloperCalledJamis.all.where_values_hash
+ assert_equal "Jamis", wheres['name']
+ assert_equal 50000, wheres['salary']
+ end
+
+ def test_default_scope_with_module_includes
+ wheres = ModuleIncludedPoorDeveloperCalledJamis.all.where_values_hash
+ assert_equal "Jamis", wheres['name']
+ assert_equal 50000, wheres['salary']
+ end
+
+ def test_default_scope_with_multiple_calls
+ wheres = MultiplePoorDeveloperCalledJamis.all.where_values_hash
+ assert_equal "Jamis", wheres['name']
+ assert_equal 50000, wheres['salary']
+ end
+
+ def test_scope_overwrites_default
+ expected = Developer.all.merge!(order: 'salary DESC, name DESC').to_a.collect { |dev| dev.name }
+ received = DeveloperOrderedBySalary.by_name.to_a.collect { |dev| dev.name }
+ assert_equal expected, received
+ end
+
+ def test_reorder_overrides_default_scope_order
+ expected = Developer.order('name DESC').collect { |dev| dev.name }
+ received = DeveloperOrderedBySalary.reorder('name DESC').collect { |dev| dev.name }
+ assert_equal expected, received
+ end
+
+ def test_order_after_reorder_combines_orders
+ 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] }
+ assert_equal expected, received
+ end
+
+ def test_unscope_after_reordering_and_combining
+ expected = Developer.order('id DESC, name DESC').collect { |dev| [dev.name, dev.id] }
+ received = DeveloperOrderedBySalary.reorder('name DESC').unscope(:order).order('id DESC, name DESC').collect { |dev| [dev.name, dev.id] }
+ assert_equal expected, received
+
+ expected_2 = Developer.all.collect { |dev| [dev.name, dev.id] }
+ received_2 = Developer.order('id DESC, name DESC').unscope(:order).collect { |dev| [dev.name, dev.id] }
+ assert_equal expected_2, received_2
+
+ expected_3 = Developer.all.collect { |dev| [dev.name, dev.id] }
+ received_3 = Developer.reorder('name DESC').unscope(:order).collect { |dev| [dev.name, dev.id] }
+ assert_equal expected_3, received_3
+ end
+
+ def test_unscope_with_where_attributes
+ expected = Developer.order('salary DESC').collect(&:name)
+ received = DeveloperOrderedBySalary.where(name: 'David').unscope(where: :name).collect(&:name)
+ assert_equal expected, received
+
+ 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(&: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
+ expected = Developer.order('salary DESC').collect { |dev| dev.name }
+ received = DeveloperOrderedBySalary.where(name: 'Jamis').where(id: 1).unscope(where: [:name, :id]).collect { |dev| dev.name }
+ assert_equal expected, received
+ end
+
+ def test_unscope_with_grouping_attributes
+ expected = Developer.order('salary DESC').collect { |dev| dev.name }
+ received = DeveloperOrderedBySalary.group(:name).unscope(:group).collect { |dev| dev.name }
+ assert_equal expected, received
+
+ expected_2 = Developer.order('salary DESC').collect { |dev| dev.name }
+ received_2 = DeveloperOrderedBySalary.group("name").unscope(:group).collect { |dev| dev.name }
+ assert_equal expected_2, received_2
+ end
+
+ def test_unscope_with_limit_in_query
+ expected = Developer.order('salary DESC').collect { |dev| dev.name }
+ received = DeveloperOrderedBySalary.limit(1).unscope(:limit).collect { |dev| dev.name }
+ assert_equal expected, received
+ end
+
+ def test_order_to_unscope_reordering
+ scope = DeveloperOrderedBySalary.order('salary DESC, name ASC').reverse_order.unscope(:order)
+ assert !(scope.to_sql =~ /order/i)
+ end
+
+ def test_unscope_reverse_order
+ expected = Developer.all.collect { |dev| dev.name }
+ received = Developer.order('salary DESC').reverse_order.unscope(:order).collect { |dev| dev.name }
+ assert_equal expected, received
+ end
+
+ def test_unscope_select
+ expected = Developer.order('salary ASC').collect { |dev| dev.name }
+ received = Developer.order('salary DESC').reverse_order.select(:name => "Jamis").unscope(:select).collect { |dev| dev.name }
+ assert_equal expected, received
+
+ expected_2 = Developer.all.collect { |dev| dev.id }
+ received_2 = Developer.select(:name).unscope(:select).collect { |dev| dev.id }
+ assert_equal expected_2, received_2
+ end
+
+ def test_unscope_offset
+ expected = Developer.all.collect { |dev| dev.name }
+ received = Developer.offset(5).unscope(:offset).collect { |dev| dev.name }
+ assert_equal expected, received
+ end
+
+ def test_unscope_joins_and_select_on_developers_projects
+ expected = Developer.all.collect { |dev| dev.name }
+ received = Developer.joins('JOIN developers_projects ON id = developer_id').select(:id).unscope(:joins, :select).collect { |dev| dev.name }
+ assert_equal expected, received
+ end
+
+ def test_unscope_includes
+ expected = Developer.all.collect { |dev| dev.name }
+ received = Developer.includes(:projects).select(:id).unscope(:includes, :select).collect { |dev| dev.name }
+ assert_equal expected, received
+ end
+
+ def test_unscope_having
+ expected = DeveloperOrderedBySalary.all.collect { |dev| dev.name }
+ received = DeveloperOrderedBySalary.having("name IN ('Jamis', 'David')").unscope(:having).collect { |dev| dev.name }
+ assert_equal expected, received
+ end
+
+ def test_unscope_and_scope
+ developer_klass = Class.new(Developer) do
+ scope :by_name, -> name { unscope(where: :name).where(name: name) }
+ end
+
+ expected = developer_klass.where(name: 'Jamis').collect { |dev| [dev.name, dev.id] }
+ received = developer_klass.where(name: 'David').by_name('Jamis').collect { |dev| [dev.name, dev.id] }
+ assert_equal expected, received
+ end
+
+ def test_unscope_errors_with_invalid_value
+ assert_raises(ArgumentError) do
+ Developer.includes(:projects).where(name: "Jamis").unscope(:stupidly_incorrect_value)
+ end
+
+ assert_raises(ArgumentError) do
+ Developer.all.unscope(:includes, :select, :some_broken_value)
+ end
+
+ assert_raises(ArgumentError) do
+ Developer.order('name DESC').reverse_order.unscope(:reverse_order)
+ end
+
+ assert_raises(ArgumentError) do
+ Developer.order('name DESC').where(name: "Jamis").unscope()
+ end
+ end
+
+ def test_unscope_errors_with_non_where_hash_keys
+ assert_raises(ArgumentError) do
+ Developer.where(name: "Jamis").limit(4).unscope(limit: 4)
+ end
+
+ assert_raises(ArgumentError) do
+ Developer.where(name: "Jamis").unscope("where" => :name)
+ end
+ end
+
+ def test_unscope_errors_with_non_symbol_or_hash_arguments
+ assert_raises(ArgumentError) do
+ Developer.where(name: "Jamis").limit(3).unscope("limit")
+ end
+
+ assert_raises(ArgumentError) do
+ Developer.select("id").unscope("select")
+ end
+
+ assert_raises(ArgumentError) do
+ Developer.select("id").unscope(5)
+ end
+ end
+
+ def test_order_in_default_scope_should_not_prevail
+ expected = Developer.all.merge!(order: 'salary desc').to_a.collect { |dev| dev.salary }
+ received = DeveloperOrderedBySalary.all.merge!(order: 'salary').to_a.collect { |dev| dev.salary }
+ assert_equal expected, received
+ end
+
+ def test_create_attribute_overwrites_default_scoping
+ assert_equal 'David', PoorDeveloperCalledJamis.create!(:name => 'David').name
+ assert_equal 200000, PoorDeveloperCalledJamis.create!(:name => 'David', :salary => 200000).salary
+ end
+
+ def test_create_attribute_overwrites_default_values
+ assert_equal nil, PoorDeveloperCalledJamis.create!(:salary => nil).salary
+ assert_equal 50000, PoorDeveloperCalledJamis.create!(:name => 'David').salary
+ end
+
+ def test_default_scope_attribute
+ jamis = PoorDeveloperCalledJamis.new(:name => 'David')
+ assert_equal 50000, jamis.salary
+ end
+
+ def test_where_attribute
+ aaron = PoorDeveloperCalledJamis.where(:salary => 20).new(:name => 'Aaron')
+ assert_equal 20, aaron.salary
+ assert_equal 'Aaron', aaron.name
+ end
+
+ def test_where_attribute_merge
+ aaron = PoorDeveloperCalledJamis.where(:name => 'foo').new(:name => 'Aaron')
+ assert_equal 'Aaron', aaron.name
+ end
+
+ def test_scope_composed_by_limit_and_then_offset_is_equal_to_scope_composed_by_offset_and_then_limit
+ posts_limit_offset = Post.limit(3).offset(2)
+ posts_offset_limit = Post.offset(2).limit(3)
+ assert_equal posts_limit_offset, posts_offset_limit
+ end
+
+ def test_create_with_merge
+ aaron = PoorDeveloperCalledJamis.create_with(:name => 'foo', :salary => 20).merge(
+ PoorDeveloperCalledJamis.create_with(:name => 'Aaron')).new
+ assert_equal 20, aaron.salary
+ assert_equal 'Aaron', aaron.name
+
+ aaron = PoorDeveloperCalledJamis.create_with(:name => 'foo', :salary => 20).
+ create_with(:name => 'Aaron').new
+ assert_equal 20, aaron.salary
+ assert_equal 'Aaron', aaron.name
+ end
+
+ def test_create_with_reset
+ jamis = PoorDeveloperCalledJamis.create_with(:name => 'Aaron').create_with(nil).new
+ assert_equal 'Jamis', jamis.name
+ end
+
+ # FIXME: I don't know if this is *desired* behavior, but it is *today's*
+ # behavior.
+ def test_create_with_empty_hash_will_not_reset
+ jamis = PoorDeveloperCalledJamis.create_with(:name => 'Aaron').create_with({}).new
+ assert_equal 'Aaron', jamis.name
+ end
+
+ def test_unscoped_with_named_scope_should_not_have_default_scope
+ assert_equal [DeveloperCalledJamis.find(developers(:poor_jamis).id)], DeveloperCalledJamis.poor
+
+ assert DeveloperCalledJamis.unscoped.poor.include?(developers(:david).becomes(DeveloperCalledJamis))
+
+ assert_equal 11, DeveloperCalledJamis.unscoped.length
+ assert_equal 1, DeveloperCalledJamis.poor.length
+ assert_equal 10, DeveloperCalledJamis.unscoped.poor.length
+ assert_equal 10, DeveloperCalledJamis.unscoped { DeveloperCalledJamis.poor }.length
+ end
+
+ def test_default_scope_select_ignored_by_aggregations
+ assert_equal DeveloperWithSelect.all.to_a.count, DeveloperWithSelect.count
+ end
+
+ def test_default_scope_select_ignored_by_grouped_aggregations
+ assert_equal Hash[Developer.all.group_by(&:salary).map { |s, d| [s, d.count] }],
+ DeveloperWithSelect.group(:salary).count
+ end
+
+ def test_default_scope_order_ignored_by_aggregations
+ assert_equal DeveloperOrderedBySalary.all.count, DeveloperOrderedBySalary.count
+ end
+
+ def test_default_scope_find_last
+ assert DeveloperOrderedBySalary.count > 1, "need more than one row for test"
+
+ lowest_salary_dev = DeveloperOrderedBySalary.find(developers(:poor_jamis).id)
+ assert_equal lowest_salary_dev, DeveloperOrderedBySalary.last
+ end
+
+ def test_default_scope_include_with_count
+ d = DeveloperWithIncludes.create!
+ d.audit_logs.create! :message => 'foo'
+
+ 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
+ ThreadsafeDeveloper.connection.close
+ end
+ threads << Thread.new do
+ assert_equal 1, ThreadsafeDeveloper.all.to_a.count
+ ThreadsafeDeveloper.connection.close
+ end
+ threads.each(&:join)
+ end
+end
diff --git a/activerecord/test/cases/named_scope_test.rb b/activerecord/test/cases/scoping/named_scoping_test.rb
index bd121126e7..72c9787b84 100644
--- a/activerecord/test/cases/named_scope_test.rb
+++ b/activerecord/test/cases/scoping/named_scoping_test.rb
@@ -6,7 +6,7 @@ require 'models/reply'
require 'models/author'
require 'models/developer'
-class NamedScopeTest < ActiveRecord::TestCase
+class NamedScopingTest < ActiveRecord::TestCase
fixtures :posts, :authors, :topics, :comments, :author_addresses
def test_implements_enumerable
@@ -60,11 +60,6 @@ class NamedScopeTest < 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?
@@ -271,6 +266,19 @@ class NamedScopeTest < ActiveRecord::TestCase
assert_equal 'lifo', topic.author_name
end
+ # Method delegation for scope names which look like /\A[a-zA-Z_]\w*[!?]?\z/
+ # has been done by evaluating a string with a plain def statement. For scope
+ # names which contain spaces this approach doesn't work.
+ def test_spaces_in_scope_names
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = "topics"
+ scope :"title containing space", -> { where("title LIKE '% %'") }
+ scope :approved, -> { where(:approved => true) }
+ end
+ assert_equal klass.send(:"title containing space"), klass.where("title LIKE '% %'")
+ assert_equal klass.approved.send(:"title containing space"), klass.approved.where("title LIKE '% %'")
+ end
+
def test_find_all_should_behave_like_select
assert_equal Topic.base.to_a.select(&:approved), Topic.base.to_a.find_all(&:approved)
end
@@ -309,7 +317,7 @@ class NamedScopeTest < ActiveRecord::TestCase
assert_equal post.comments.size, Post.joins(join).joins(join).where("posts.id = #{post.id}").size
end
- def test_chaining_should_use_latest_conditions_when_creating
+ def test_chaining_applies_last_conditions_when_creating
post = Topic.rejected.new
assert !post.approved?
@@ -323,13 +331,13 @@ class NamedScopeTest < ActiveRecord::TestCase
assert post.approved?
end
- def test_chaining_should_use_latest_conditions_when_searching
+ def test_chaining_combines_conditions_when_searching
# Normal hash conditions
- assert_equal Topic.where(:approved => true).to_a, Topic.rejected.approved.to_a
- assert_equal Topic.where(:approved => false).to_a, Topic.approved.rejected.to_a
+ assert_equal Topic.where(approved: false).where(approved: true).to_a, Topic.rejected.approved.to_a
+ assert_equal Topic.where(approved: true).where(approved: false).to_a, Topic.approved.rejected.to_a
# Nested hash conditions with same keys
- assert_equal [posts(:sti_comments)], Post.with_special_comments.with_very_special_comments.to_a
+ assert_equal [], Post.with_special_comments.with_very_special_comments.to_a
# Nested hash conditions with different keys
assert_equal [posts(:sti_comments)], Post.with_special_comments.with_post(4).to_a.uniq
@@ -427,23 +435,17 @@ class NamedScopeTest < ActiveRecord::TestCase
end
end
- def test_eager_scopes_are_deprecated
+ def test_eager_default_scope_relations_are_remove
klass = Class.new(ActiveRecord::Base)
klass.table_name = 'posts'
- assert_deprecated do
- klass.scope :welcome_2, klass.where(:id => posts(:welcome).id)
+ assert_raises(ArgumentError) do
+ klass.send(:default_scope, 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
- klass = Class.new(ActiveRecord::Base)
- klass.table_name = 'posts'
-
- assert_deprecated do
- klass.send(:default_scope, klass.where(:id => posts(:welcome).id))
- end
- assert_equal [posts(:welcome).title], klass.all.map(&:title)
+ def test_subclass_merges_scopes_properly
+ assert_equal 1, SpecialComment.where(body: 'go crazy').created.count
end
+
end
diff --git a/activerecord/test/cases/relation_scoping_test.rb b/activerecord/test/cases/scoping/relation_scoping_test.rb
index d318dab1e1..0018fc06f2 100644
--- a/activerecord/test/cases/relation_scoping_test.rb
+++ b/activerecord/test/cases/scoping/relation_scoping_test.rb
@@ -161,6 +161,28 @@ class RelationScopingTest < ActiveRecord::TestCase
assert !Developer.all.where_values.include?("name = 'Jamis'")
end
+
+ def test_default_scope_filters_on_joins
+ assert_equal 1, DeveloperFilteredOnJoins.all.count
+ assert_equal DeveloperFilteredOnJoins.all.first, developers(:david).becomes(DeveloperFilteredOnJoins)
+ end
+
+ def test_update_all_default_scope_filters_on_joins
+ DeveloperFilteredOnJoins.update_all(:salary => 65000)
+ assert_equal 65000, Developer.find(developers(:david).id).salary
+
+ # has not changed jamis
+ assert_not_equal 65000, Developer.find(developers(:jamis).id).salary
+ end
+
+ def test_delete_all_default_scope_filters_on_joins
+ assert_not_equal [], DeveloperFilteredOnJoins.all
+
+ DeveloperFilteredOnJoins.delete_all()
+
+ assert_equal [], DeveloperFilteredOnJoins.all
+ assert_not_equal [], Developer.all
+ end
end
class NestedRelationScopingTest < ActiveRecord::TestCase
@@ -227,7 +249,7 @@ class NestedRelationScopingTest < ActiveRecord::TestCase
def test_nested_exclusive_scope_for_create
comment = Comment.create_with(:body => "Hey guys, nested scopes are broken. Please fix!").scoping do
Comment.unscoped.create_with(:post_id => 1).scoping do
- assert_blank Comment.new.body
+ assert Comment.new.body.blank?
Comment.create :body => "Hey guys"
end
end
@@ -271,7 +293,7 @@ class HasManyScopingTest< ActiveRecord::TestCase
assert_equal [magician], people(:michael).bad_references
end
- def test_should_default_scope_on_associations_is_overriden_by_association_conditions
+ def test_should_default_scope_on_associations_is_overridden_by_association_conditions
reference = references(:michael_unicyclist).becomes(BadReference)
assert_equal [reference], people(:michael).fixed_bad_references
end
@@ -307,212 +329,3 @@ class HasAndBelongsToManyScopingTest< ActiveRecord::TestCase
end
end
end
-
-class DefaultScopingTest < ActiveRecord::TestCase
- fixtures :developers, :posts
-
- def test_default_scope
- expected = Developer.all.merge!(:order => 'salary DESC').to_a.collect { |dev| dev.salary }
- received = DeveloperOrderedBySalary.all.collect { |dev| dev.salary }
- assert_equal expected, received
- end
-
- def test_default_scope_as_class_method
- assert_equal [developers(:david).becomes(ClassMethodDeveloperCalledDavid)], ClassMethodDeveloperCalledDavid.all
- end
-
- def test_default_scope_as_class_method_referencing_scope
- assert_equal [developers(:david).becomes(ClassMethodReferencingScopeDeveloperCalledDavid)], ClassMethodReferencingScopeDeveloperCalledDavid.all
- end
-
- def test_default_scope_as_block_referencing_scope
- assert_equal [developers(:david).becomes(LazyBlockReferencingScopeDeveloperCalledDavid)], LazyBlockReferencingScopeDeveloperCalledDavid.all
- end
-
- def test_default_scope_with_lambda
- assert_equal [developers(:david).becomes(LazyLambdaDeveloperCalledDavid)], LazyLambdaDeveloperCalledDavid.all
- end
-
- def test_default_scope_with_block
- assert_equal [developers(:david).becomes(LazyBlockDeveloperCalledDavid)], LazyBlockDeveloperCalledDavid.all
- end
-
- def test_default_scope_with_callable
- assert_equal [developers(:david).becomes(CallableDeveloperCalledDavid)], CallableDeveloperCalledDavid.all
- end
-
- def test_default_scope_is_unscoped_on_find
- assert_equal 1, DeveloperCalledDavid.count
- assert_equal 11, DeveloperCalledDavid.unscoped.count
- end
-
- def test_default_scope_is_unscoped_on_create
- assert_nil DeveloperCalledJamis.unscoped.create!.name
- end
-
- def test_default_scope_with_conditions_string
- assert_equal Developer.where(name: 'David').map(&:id).sort, DeveloperCalledDavid.all.map(&:id).sort
- assert_equal nil, DeveloperCalledDavid.create!.name
- end
-
- def test_default_scope_with_conditions_hash
- assert_equal Developer.where(name: 'Jamis').map(&:id).sort, DeveloperCalledJamis.all.map(&:id).sort
- 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
- end
- end
-
- def test_default_scope_with_inheritance
- wheres = InheritedPoorDeveloperCalledJamis.all.where_values_hash
- assert_equal "Jamis", wheres[:name]
- assert_equal 50000, wheres[:salary]
- end
-
- def test_default_scope_with_module_includes
- wheres = ModuleIncludedPoorDeveloperCalledJamis.all.where_values_hash
- assert_equal "Jamis", wheres[:name]
- assert_equal 50000, wheres[:salary]
- end
-
- def test_default_scope_with_multiple_calls
- wheres = MultiplePoorDeveloperCalledJamis.all.where_values_hash
- assert_equal "Jamis", wheres[:name]
- assert_equal 50000, wheres[:salary]
- end
-
- def test_scope_overwrites_default
- expected = Developer.all.merge!(:order => ' name DESC, salary DESC').to_a.collect { |dev| dev.name }
- received = DeveloperOrderedBySalary.by_name.to_a.collect { |dev| dev.name }
- assert_equal expected, received
- end
-
- def test_reorder_overrides_default_scope_order
- expected = Developer.order('name DESC').collect { |dev| dev.name }
- received = DeveloperOrderedBySalary.reorder('name DESC').collect { |dev| dev.name }
- assert_equal expected, received
- end
-
- def test_order_after_reorder_combines_orders
- expected = Developer.order('id DESC, name 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_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 }
- assert_equal expected, received
- end
-
- def test_create_attribute_overwrites_default_scoping
- assert_equal 'David', PoorDeveloperCalledJamis.create!(:name => 'David').name
- assert_equal 200000, PoorDeveloperCalledJamis.create!(:name => 'David', :salary => 200000).salary
- end
-
- def test_create_attribute_overwrites_default_values
- assert_equal nil, PoorDeveloperCalledJamis.create!(:salary => nil).salary
- assert_equal 50000, PoorDeveloperCalledJamis.create!(:name => 'David').salary
- end
-
- def test_default_scope_attribute
- jamis = PoorDeveloperCalledJamis.new(:name => 'David')
- assert_equal 50000, jamis.salary
- end
-
- def test_where_attribute
- aaron = PoorDeveloperCalledJamis.where(:salary => 20).new(:name => 'Aaron')
- assert_equal 20, aaron.salary
- assert_equal 'Aaron', aaron.name
- end
-
- def test_where_attribute_merge
- aaron = PoorDeveloperCalledJamis.where(:name => 'foo').new(:name => 'Aaron')
- assert_equal 'Aaron', aaron.name
- end
-
- def test_scope_composed_by_limit_and_then_offset_is_equal_to_scope_composed_by_offset_and_then_limit
- posts_limit_offset = Post.limit(3).offset(2)
- posts_offset_limit = Post.offset(2).limit(3)
- assert_equal posts_limit_offset, posts_offset_limit
- end
-
- def test_create_with_merge
- aaron = PoorDeveloperCalledJamis.create_with(:name => 'foo', :salary => 20).merge(
- PoorDeveloperCalledJamis.create_with(:name => 'Aaron')).new
- assert_equal 20, aaron.salary
- assert_equal 'Aaron', aaron.name
-
- aaron = PoorDeveloperCalledJamis.create_with(:name => 'foo', :salary => 20).
- create_with(:name => 'Aaron').new
- assert_equal 20, aaron.salary
- assert_equal 'Aaron', aaron.name
- end
-
- def test_create_with_reset
- jamis = PoorDeveloperCalledJamis.create_with(:name => 'Aaron').create_with(nil).new
- assert_equal 'Jamis', jamis.name
- end
-
- # FIXME: I don't know if this is *desired* behavior, but it is *today's*
- # behavior.
- def test_create_with_empty_hash_will_not_reset
- jamis = PoorDeveloperCalledJamis.create_with(:name => 'Aaron').create_with({}).new
- assert_equal 'Aaron', jamis.name
- end
-
- def test_unscoped_with_named_scope_should_not_have_default_scope
- assert_equal [DeveloperCalledJamis.find(developers(:poor_jamis).id)], DeveloperCalledJamis.poor
-
- assert DeveloperCalledJamis.unscoped.poor.include?(developers(:david).becomes(DeveloperCalledJamis))
- assert_equal 10, DeveloperCalledJamis.unscoped.poor.length
- end
-
- def test_default_scope_select_ignored_by_aggregations
- assert_equal DeveloperWithSelect.all.to_a.count, DeveloperWithSelect.count
- end
-
- def test_default_scope_select_ignored_by_grouped_aggregations
- assert_equal Hash[Developer.all.group_by(&:salary).map { |s, d| [s, d.count] }],
- DeveloperWithSelect.group(:salary).count
- end
-
- def test_default_scope_order_ignored_by_aggregations
- assert_equal DeveloperOrderedBySalary.all.count, DeveloperOrderedBySalary.count
- end
-
- def test_default_scope_find_last
- assert DeveloperOrderedBySalary.count > 1, "need more than one row for test"
-
- lowest_salary_dev = DeveloperOrderedBySalary.find(developers(:poor_jamis).id)
- assert_equal lowest_salary_dev, DeveloperOrderedBySalary.last
- end
-
- def test_default_scope_include_with_count
- d = DeveloperWithIncludes.create!
- d.audit_logs.create! :message => 'foo'
-
- 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
- end
- threads.each(&:join)
- end
-end
diff --git a/activerecord/test/cases/serialization_test.rb b/activerecord/test/cases/serialization_test.rb
index 25b860878a..c46060a646 100644
--- a/activerecord/test/cases/serialization_test.rb
+++ b/activerecord/test/cases/serialization_test.rb
@@ -18,6 +18,10 @@ class SerializationTest < ActiveRecord::TestCase
}
end
+ def test_include_root_in_json_is_false_by_default
+ assert_equal false, ActiveRecord::Base.include_root_in_json, "include_root_in_json should be false by default but was not"
+ end
+
def test_serialize_should_be_reversible
FORMATS.each do |format|
@serialized = Contact.new.send("to_#{format}")
@@ -45,4 +49,20 @@ class SerializationTest < ActiveRecord::TestCase
assert_equal @contact_attributes[:awesome], contact.awesome, "For #{format}"
end
end
+
+ def test_include_root_in_json_allows_inheritance
+ original_root_in_json = ActiveRecord::Base.include_root_in_json
+ ActiveRecord::Base.include_root_in_json = true
+
+ klazz = Class.new(ActiveRecord::Base)
+ klazz.table_name = 'topics'
+ assert klazz.include_root_in_json
+
+ klazz.include_root_in_json = false
+ assert ActiveRecord::Base.include_root_in_json
+ assert !klazz.include_root_in_json
+ assert !klazz.new.include_root_in_json
+ ensure
+ ActiveRecord::Base.include_root_in_json = original_root_in_json
+ end
end
diff --git a/activerecord/test/cases/serialized_attribute_test.rb b/activerecord/test/cases/serialized_attribute_test.rb
index f24ee54cd2..bc67da8d27 100644
--- a/activerecord/test/cases/serialized_attribute_test.rb
+++ b/activerecord/test/cases/serialized_attribute_test.rb
@@ -1,5 +1,8 @@
-require "cases/helper"
+require 'cases/helper'
require 'models/topic'
+require 'models/reply'
+require 'models/person'
+require 'models/traffic_light'
require 'bcrypt'
class SerializedAttributeTest < ActiveRecord::TestCase
@@ -16,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)
@@ -56,11 +53,21 @@ class SerializedAttributeTest < ActiveRecord::TestCase
def test_serialized_attribute_before_type_cast_returns_unserialized_value
Topic.serialize :content, Hash
- t = Topic.new(:content => { :foo => :bar })
- assert_equal({ :foo => :bar }, t.content_before_type_cast)
+ t = Topic.new(content: { foo: :bar })
+ assert_equal({ foo: :bar }, t.content_before_type_cast)
t.save!
t.reload
- assert_equal({ :foo => :bar }, t.content_before_type_cast)
+ assert_equal({ foo: :bar }, t.content_before_type_cast)
+ end
+
+ def test_serialized_attributes_before_type_cast_returns_unserialized_value
+ Topic.serialize :content, Hash
+
+ t = Topic.new(content: { foo: :bar })
+ assert_equal({ foo: :bar }, t.attributes_before_type_cast["content"])
+ t.save!
+ t.reload
+ assert_equal({ foo: :bar }, t.attributes_before_type_cast["content"])
end
def test_serialized_attribute_calling_dup_method
@@ -202,4 +209,40 @@ class SerializedAttributeTest < ActiveRecord::TestCase
assert_kind_of BCrypt::Password, topic.content
assert_equal(true, topic.content == password, 'password should equal')
end
+
+ def test_serialize_attribute_via_select_method_when_time_zone_available
+ with_timezone_config aware_attributes: true do
+ Topic.serialize(:content, MyObject)
+
+ 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 }
+ end
+ end
+
+ def test_serialize_attribute_can_be_serialized_in_an_integer_column
+ insures = ['life']
+ person = SerializedPerson.new(first_name: 'David', insures: insures)
+ assert person.save
+ person = person.reload
+ assert_equal(insures, person.insures)
+ end
+
+ def test_regression_serialized_default_on_text_column_with_null_false
+ light = TrafficLight.new
+ assert_equal [], light.state
+ assert_equal [], light.long_state
+ end
+
+ def test_serialized_column_should_not_be_wrapped_twice
+ Topic.serialize(:content, MyObject)
+
+ myobj = MyObject.new('value1', 'value2')
+ Topic.create(content: myobj)
+ Topic.create(content: myobj)
+ 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/statement_cache_test.rb b/activerecord/test/cases/statement_cache_test.rb
new file mode 100644
index 0000000000..76da49707f
--- /dev/null
+++ b/activerecord/test/cases/statement_cache_test.rb
@@ -0,0 +1,64 @@
+require 'cases/helper'
+require 'models/book'
+require 'models/liquid'
+require 'models/molecule'
+require 'models/electron'
+
+module ActiveRecord
+ class StatementCacheTest < ActiveRecord::TestCase
+ def setup
+ @connection = ActiveRecord::Base.connection
+ end
+
+ def test_statement_cache_with_simple_statement
+ cache = ActiveRecord::StatementCache.new do
+ Book.where(name: "my book").where("author_id > 3")
+ end
+
+ Book.create(name: "my book", author_id: 4)
+
+ books = cache.execute
+ assert_equal "my book", books[0].name
+ end
+
+ def test_statement_cache_with_nil_statement_raises_error
+ assert_raise(ArgumentError) do
+ ActiveRecord::StatementCache.new do
+ nil
+ end
+ end
+ end
+
+ def test_statement_cache_with_complex_statement
+ cache = ActiveRecord::StatementCache.new do
+ Liquid.joins(:molecules => :electrons).where('molecules.name' => 'dioxane', 'electrons.name' => 'lepton')
+ end
+
+ salty = Liquid.create(name: 'salty')
+ molecule = salty.molecules.create(name: 'dioxane')
+ molecule.electrons.create(name: 'lepton')
+
+ liquids = cache.execute
+ assert_equal "salty", liquids[0].name
+ end
+
+ def test_statement_cache_values_differ
+ cache = ActiveRecord::StatementCache.new do
+ Book.where(name: "my book")
+ end
+
+ 3.times do
+ Book.create(name: "my book")
+ end
+
+ first_books = cache.execute
+
+ 3.times do
+ Book.create(name: "my book")
+ end
+
+ additional_books = cache.execute
+ assert first_books != additional_books
+ end
+ end
+end
diff --git a/activerecord/test/cases/store_test.rb b/activerecord/test/cases/store_test.rb
index 562ca8d9ff..0c9f7ccd55 100644
--- a/activerecord/test/cases/store_test.rb
+++ b/activerecord/test/cases/store_test.rb
@@ -35,6 +35,12 @@ class StoreTest < ActiveRecord::TestCase
assert_equal '(123) 456-7890', @john.phone_number
end
+ test "overriding a read accessor using super" do
+ @john.settings[:color] = nil
+
+ assert_equal 'red', @john.color
+ end
+
test "updating the store will mark it as changed" do
@john.color = 'red'
assert @john.settings_changed?
@@ -66,10 +72,16 @@ class StoreTest < ActiveRecord::TestCase
assert_equal '1234567890', @john.settings[:phone_number]
end
+ test "overriding a write accessor using super" do
+ @john.color = 'yellow'
+
+ assert_equal 'blue', @john.color
+ end
+
test "preserve store attributes data in HashWithIndifferentAccess format without any conversion" do
- @john.json_data = HashWithIndifferentAccess.new(:height => 'tall', 'weight' => 'heavy')
+ @john.json_data = ActiveSupport::HashWithIndifferentAccess.new(:height => 'tall', 'weight' => 'heavy')
@john.height = 'low'
- assert_equal true, @john.json_data.instance_of?(HashWithIndifferentAccess)
+ assert_equal true, @john.json_data.instance_of?(ActiveSupport::HashWithIndifferentAccess)
assert_equal 'low', @john.json_data[:height]
assert_equal 'low', @john.json_data['height']
assert_equal 'heavy', @john.json_data[:weight]
@@ -95,7 +107,7 @@ class StoreTest < ActiveRecord::TestCase
test "convert store attributes from any format other than Hash or HashWithIndifferent access losing the data" do
@john.json_data = "somedata"
@john.height = 'low'
- assert_equal true, @john.json_data.instance_of?(HashWithIndifferentAccess)
+ assert_equal true, @john.json_data.instance_of?(ActiveSupport::HashWithIndifferentAccess)
assert_equal 'low', @john.json_data[:height]
assert_equal 'low', @john.json_data['height']
assert_equal false, @john.json_data.delete_if { |k, v| k == 'height' }.any?
@@ -139,9 +151,15 @@ 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 }
- end
+ 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
end
diff --git a/activerecord/test/cases/tasks/database_tasks_test.rb b/activerecord/test/cases/tasks/database_tasks_test.rb
index 4f3489b7a5..e9000fef25 100644
--- a/activerecord/test/cases/tasks/database_tasks_test.rb
+++ b/activerecord/test/cases/tasks/database_tasks_test.rb
@@ -11,10 +11,10 @@ module ActiveRecord
end
ADAPTERS_TASKS = {
- :mysql => :mysql_tasks,
- :mysql2 => :mysql_tasks,
- :postgresql => :postgresql_tasks,
- :sqlite3 => :sqlite_tasks
+ mysql: :mysql_tasks,
+ mysql2: :mysql_tasks,
+ postgresql: :postgresql_tasks,
+ sqlite3: :sqlite_tasks
}
class DatabaseTasksRegisterTask < ActiveRecord::TestCase
@@ -31,8 +31,14 @@ module ActiveRecord
ActiveRecord::Tasks::DatabaseTasks.register_task(/foo/, klazz)
ActiveRecord::Tasks::DatabaseTasks.structure_dump({'adapter' => :foo}, "awesome-file.sql")
end
+
+ def test_unregistered_task
+ assert_raise(ActiveRecord::Tasks::DatabaseNotSupported) do
+ ActiveRecord::Tasks::DatabaseTasks.structure_dump({'adapter' => :bar}, "awesome-file.sql")
+ end
+ end
end
-
+
class DatabaseTasksCreateTest < ActiveRecord::TestCase
include DatabaseTasksSetupper
@@ -258,7 +264,7 @@ module ActiveRecord
class DatabaseTasksCharsetTest < ActiveRecord::TestCase
include DatabaseTasksSetupper
-
+
ADAPTERS_TASKS.each do |k, v|
define_method("test_#{k}_charset") do
eval("@#{v}").expects(:charset)
@@ -269,7 +275,7 @@ module ActiveRecord
class DatabaseTasksCollationTest < ActiveRecord::TestCase
include DatabaseTasksSetupper
-
+
ADAPTERS_TASKS.each do |k, v|
define_method("test_#{k}_collation") do
eval("@#{v}").expects(:collation)
@@ -299,4 +305,11 @@ module ActiveRecord
end
end
end
+
+ class DatabaseTasksCheckSchemaFileTest < ActiveRecord::TestCase
+ def test_check_schema_file
+ Kernel.expects(:abort).with(regexp_matches(/awesome-file.sql/))
+ ActiveRecord::Tasks::DatabaseTasks.check_schema_file("awesome-file.sql")
+ end
+ end
end
diff --git a/activerecord/test/cases/tasks/mysql_rake_test.rb b/activerecord/test/cases/tasks/mysql_rake_test.rb
index 69a049fcfa..bdcf31043a 100644
--- a/activerecord/test/cases/tasks/mysql_rake_test.rb
+++ b/activerecord/test/cases/tasks/mysql_rake_test.rb
@@ -53,6 +53,16 @@ module ActiveRecord
ActiveRecord::Tasks::DatabaseTasks.create @configuration
end
+
+ def test_create_when_database_exists_outputs_info_to_stderr
+ $stderr.expects(:puts).with("my-app-db already exists").once
+
+ ActiveRecord::Base.connection.stubs(:create_database).raises(
+ ActiveRecord::StatementInvalid.new("Can't create database 'dev'; database exists:")
+ )
+
+ ActiveRecord::Tasks::DatabaseTasks.create @configuration
+ end
end
class MysqlDBCreateAsRootTest < ActiveRecord::TestCase
@@ -61,7 +71,7 @@ module ActiveRecord
return skip("only tested on mysql")
end
- @connection = stub(:create_database => true, :execute => true)
+ @connection = stub("Connection", create_database: true)
@error = Mysql::Error.new "Invalid permissions"
@configuration = {
'adapter' => 'mysql',
@@ -80,6 +90,7 @@ module ActiveRecord
end
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")
@@ -87,6 +98,7 @@ module ActiveRecord
end
def test_connection_established_as_root
+ assert_permissions_granted_for "pat"
ActiveRecord::Base.expects(:establish_connection).with(
'adapter' => 'mysql',
'database' => nil,
@@ -98,6 +110,7 @@ module ActiveRecord
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')
@@ -105,12 +118,18 @@ module ActiveRecord
end
def test_grant_privileges_for_normal_user
- @connection.expects(:execute).with("GRANT ALL PRIVILEGES ON my-app-db.* TO 'pat'@'localhost' IDENTIFIED BY 'wossname' WITH GRANT OPTION;")
+ assert_permissions_granted_for "pat"
+ ActiveRecord::Tasks::DatabaseTasks.create @configuration
+ end
+ def test_do_not_grant_privileges_for_root_user
+ @configuration['username'] = 'root'
+ @configuration['password'] = ''
ActiveRecord::Tasks::DatabaseTasks.create @configuration
end
def test_connection_established_as_normal_user
+ assert_permissions_granted_for "pat"
ActiveRecord::Base.expects(:establish_connection).returns do
ActiveRecord::Base.expects(:establish_connection).with(
'adapter' => 'mysql',
@@ -132,6 +151,13 @@ module ActiveRecord
ActiveRecord::Tasks::DatabaseTasks.create @configuration
end
+
+ private
+ def assert_permissions_granted_for(db_user)
+ db_name = @configuration['database']
+ db_password = @configuration['password']
+ @connection.expects(:execute).with("GRANT ALL PRIVILEGES ON #{db_name}.* TO '#{db_user}'@'localhost' IDENTIFIED BY '#{db_password}' WITH GRANT OPTION;")
+ end
end
class MySQLDBDropTest < ActiveRecord::TestCase
@@ -239,10 +265,30 @@ module ActiveRecord
def test_structure_dump
filename = "awesome-file.sql"
- Kernel.expects(:system).with("mysqldump", "--result-file", filename, "--no-data", "test-db")
+ Kernel.expects(:system).with("mysqldump", "--result-file", filename, "--no-data", "test-db").returns(true)
ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename)
end
+
+ def test_warn_when_external_structure_dump_fails
+ filename = "awesome-file.sql"
+ Kernel.expects(:system).with("mysqldump", "--result-file", filename, "--no-data", "test-db").returns(false)
+
+ warnings = capture(:stderr) do
+ ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename)
+ end
+
+ 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/postgresql_rake_test.rb b/activerecord/test/cases/tasks/postgresql_rake_test.rb
index 62acd53003..6ea225178f 100644
--- a/activerecord/test/cases/tasks/postgresql_rake_test.rb
+++ b/activerecord/test/cases/tasks/postgresql_rake_test.rb
@@ -61,6 +61,16 @@ module ActiveRecord
ActiveRecord::Tasks::DatabaseTasks.create @configuration
end
+
+ def test_create_when_database_exists_outputs_info_to_stderr
+ $stderr.expects(:puts).with("my-app-db already exists").once
+
+ ActiveRecord::Base.connection.stubs(:create_database).raises(
+ ActiveRecord::StatementInvalid.new('database "my-app-db" already exists')
+ )
+
+ ActiveRecord::Tasks::DatabaseTasks.create @configuration
+ end
end
class PostgreSQLDBDropTest < ActiveRecord::TestCase
@@ -196,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
@@ -215,9 +225,16 @@ module ActiveRecord
Kernel.stubs(:system)
end
- def test_structure_dump
+ def test_structure_load
filename = "awesome-file.sql"
- Kernel.expects(:system).with("psql -f #{filename} my-app-db")
+ Kernel.expects(:system).with("psql -q -f #{filename} my-app-db")
+
+ 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
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/test_case.rb b/activerecord/test/cases/test_case.rb
index f3f7054794..8c6d189b0c 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]
+ 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/test/cases/timestamp_test.rb b/activerecord/test/cases/timestamp_test.rb
index bb034848e1..ff1b01556d 100644
--- a/activerecord/test/cases/timestamp_test.rb
+++ b/activerecord/test/cases/timestamp_test.rb
@@ -113,6 +113,18 @@ class TimestampTest < ActiveRecord::TestCase
assert_not_equal previously_owner_updated_at, pet.owner.updated_at
end
+ def test_saving_a_new_record_belonging_to_invalid_parent_with_touch_should_not_raise_exception
+ klass = Class.new(Owner) do
+ def self.name; 'Owner'; end
+ validate { errors.add(:base, :invalid) }
+ end
+
+ pet = Pet.new(owner: klass.new)
+ pet.save!
+
+ assert pet.owner.new_record?
+ end
+
def test_saving_a_record_with_a_belongs_to_that_specifies_touching_a_specific_attribute_the_parent_should_update_that_attribute
klass = Class.new(ActiveRecord::Base) do
def self.name; 'Pet'; end
@@ -164,6 +176,106 @@ class TimestampTest < ActiveRecord::TestCase
assert_not_equal time, owner.updated_at
end
+ def test_touching_a_record_touches_polymorphic_record
+ klass = Class.new(ActiveRecord::Base) do
+ def self.name; 'Toy'; end
+ end
+
+ wheel_klass = Class.new(ActiveRecord::Base) do
+ def self.name; 'Wheel'; end
+ belongs_to :wheelable, :polymorphic => true, :touch => true
+ end
+
+ toy = klass.first
+ time = 3.days.ago
+ toy.update_columns(updated_at: time)
+
+ wheel = wheel_klass.new
+ wheel.wheelable = toy
+ wheel.save
+ wheel.touch
+
+ assert_not_equal time, toy.updated_at
+ end
+
+ def test_changing_parent_of_a_record_touches_both_new_and_old_parent_record
+ klass = Class.new(ActiveRecord::Base) do
+ def self.name; 'Toy'; end
+ belongs_to :pet, touch: true
+ end
+
+ toy1 = klass.find(1)
+ old_pet = toy1.pet
+
+ toy2 = klass.find(2)
+ new_pet = toy2.pet
+ time = 3.days.ago.at_beginning_of_hour
+
+ old_pet.update_columns(updated_at: time)
+ new_pet.update_columns(updated_at: time)
+
+ toy1.pet = new_pet
+ toy1.save!
+
+ old_pet.reload
+ new_pet.reload
+
+ assert_not_equal time, new_pet.updated_at
+ 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
+ end
+
+ wheel_klass = 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)
+
+ wheel = wheel_klass.new
+ wheel.wheelable = toy1
+ wheel.save!
+
+ time = 3.days.ago.at_beginning_of_hour
+
+ toy1.update_columns(updated_at: time)
+ toy2.update_columns(updated_at: time)
+
+ wheel.wheelable = toy2
+ wheel.save!
+
+ toy1.reload
+ toy2.reload
+
+ assert_not_equal time, toy1.updated_at
+ assert_not_equal time, toy2.updated_at
+ end
+
+ def test_clearing_association_touches_the_old_record
+ klass = Class.new(ActiveRecord::Base) do
+ def self.name; 'Toy'; end
+ belongs_to :pet, touch: true
+ end
+
+ toy = klass.find(1)
+ pet = toy.pet
+ time = 3.days.ago.at_beginning_of_hour
+
+ pet.update_columns(updated_at: time)
+
+ toy.pet = nil
+ toy.save!
+
+ pet.reload
+
+ assert_not_equal time, pet.updated_at
+ end
+
def test_timestamp_attributes_for_create
toy = Toy.first
assert_equal toy.send(:timestamp_attributes_for_create), [:created_at, :created_on]
diff --git a/activerecord/test/cases/transaction_callbacks_test.rb b/activerecord/test/cases/transaction_callbacks_test.rb
index 961ba8d9ba..5644a35385 100644
--- a/activerecord/test/cases/transaction_callbacks_test.rb
+++ b/activerecord/test/cases/transaction_callbacks_test.rb
@@ -5,9 +5,29 @@ class TransactionCallbacksTest < ActiveRecord::TestCase
self.use_transactional_fixtures = false
fixtures :topics
+ class ReplyWithCallbacks < ActiveRecord::Base
+ self.table_name = :topics
+
+ belongs_to :topic, foreign_key: "parent_id"
+
+ validates_presence_of :content
+
+ after_commit :do_after_commit, on: :create
+
+ def history
+ @history ||= []
+ end
+
+ def do_after_commit
+ history << :commit_on_create
+ end
+ end
+
class TopicWithCallbacks < ActiveRecord::Base
self.table_name = :topics
+ has_many :replies, class_name: "ReplyWithCallbacks", foreign_key: "parent_id"
+
after_commit{|record| record.send(:do_after_commit, nil)}
after_commit(:on => :create){|record| record.send(:do_after_commit, :create)}
after_commit(:on => :update){|record| record.send(:do_after_commit, :update)}
@@ -93,6 +113,13 @@ class TransactionCallbacksTest < ActiveRecord::TestCase
assert_equal [:commit_on_create], @new_record.history
end
+ def test_only_call_after_commit_on_create_after_transaction_commits_for_new_record_if_create_succeeds_creating_through_association
+ topic = TopicWithCallbacks.create!(:title => "New topic", :written_on => Date.today)
+ reply = topic.replies.create
+
+ assert_equal [], reply.history
+ end
+
def test_call_after_rollback_after_transaction_rollsback
@first.after_commit_block{|r| r.history << :after_commit}
@first.after_rollback_block{|r| r.history << :after_rollback}
@@ -155,9 +182,9 @@ class TransactionCallbacksTest < ActiveRecord::TestCase
end
def test_call_after_rollback_when_commit_fails
- @first.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.connection.class.class_eval do
+ @first.class.connection.singleton_class.class_eval do
def commit_db_transaction; raise "boom!"; end
end
@@ -167,8 +194,8 @@ class TransactionCallbacksTest < ActiveRecord::TestCase
assert !@first.save rescue nil
assert_equal [:after_rollback], @first.history
ensure
- @first.connection.class.send(:remove_method, :commit_db_transaction)
- @first.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
@@ -244,117 +271,47 @@ class TransactionCallbacksTest < ActiveRecord::TestCase
assert_equal :rollback, @first.last_after_transaction_error
assert_equal [:after_rollback], @second.history
end
-end
-
-
-class TransactionObserverCallbacksTest < ActiveRecord::TestCase
- self.use_transactional_fixtures = false
- fixtures :topics
- class TopicWithObserverAttached < ActiveRecord::Base
- self.table_name = :topics
- def history
- @history ||= []
- end
+ def test_after_rollback_callbacks_should_validate_on_condition
+ assert_raise(ArgumentError) { Topic.send(:after_rollback, :on => :save) }
end
- class TopicWithObserverAttachedObserver < ActiveRecord::Observer
- def after_commit(record)
- record.history.push "after_commit"
- end
-
- def after_rollback(record)
- record.history.push "after_rollback"
- end
+ def test_after_commit_callbacks_should_validate_on_condition
+ assert_raise(ArgumentError) { Topic.send(:after_commit, :on => :save) }
end
+end
- def test_after_commit_called
- assert TopicWithObserverAttachedObserver.instance, 'should have observer'
-
- topic = TopicWithObserverAttached.new
- topic.save!
-
- assert_equal %w{ after_commit }, topic.history
- end
+class CallbacksOnMultipleActionsTest < ActiveRecord::TestCase
+ self.use_transactional_fixtures = false
- def test_after_rollback_called
- assert TopicWithObserverAttachedObserver.instance, 'should have observer'
+ class TopicWithCallbacksOnMultipleActions < ActiveRecord::Base
+ self.table_name = :topics
- topic = TopicWithObserverAttached.new
+ after_commit(on: [:create, :destroy]) { |record| record.history << :create_and_destroy }
+ after_commit(on: [:create, :update]) { |record| record.history << :create_and_update }
+ after_commit(on: [:update, :destroy]) { |record| record.history << :update_and_destroy }
- Topic.transaction do
- topic.save!
- raise ActiveRecord::Rollback
+ def clear_history
+ @history = []
end
- assert topic.id.nil?
- assert !topic.persisted?
- assert_equal %w{ after_rollback }, topic.history
- end
-
- class TopicWithManualRollbackObserverAttached < ActiveRecord::Base
- self.table_name = :topics
def history
@history ||= []
end
end
- class TopicWithManualRollbackObserverAttachedObserver < ActiveRecord::Observer
- def after_save(record)
- record.history.push "after_save"
- raise ActiveRecord::Rollback
- end
- end
-
- def test_after_save_called_with_manual_rollback
- assert TopicWithManualRollbackObserverAttachedObserver.instance, 'should have observer'
-
- topic = TopicWithManualRollbackObserverAttached.new
-
- assert !topic.save
- assert_equal nil, topic.id
- assert !topic.persisted?
- assert_equal %w{ after_save }, topic.history
- end
- def test_after_save_called_with_manual_rollback_bang
- assert TopicWithManualRollbackObserverAttachedObserver.instance, 'should have observer'
-
- topic = TopicWithManualRollbackObserverAttached.new
-
- topic.save!
- assert_equal nil, topic.id
- assert !topic.persisted?
- assert_equal %w{ after_save }, topic.history
- end
-end
-
-class SaveFromAfterCommitBlockTest < ActiveRecord::TestCase
- self.use_transactional_fixtures = false
-
- class TopicWithSaveInCallback < ActiveRecord::Base
- self.table_name = :topics
- after_commit :cache_topic, :on => :create
- after_commit :call_update, :on => :update
- attr_accessor :cached, :record_updated
-
- def call_update
- self.record_updated = true
- end
-
- def cache_topic
- unless cached
- self.cached = true
- self.save
- else
- self.cached = false
- end
- end
- end
+ def test_after_commit_on_multiple_actions
+ topic = TopicWithCallbacksOnMultipleActions.new
+ topic.save
+ assert_equal [:create_and_update, :create_and_destroy], topic.history
- def test_after_commit_in_save
- topic = TopicWithSaveInCallback.new()
+ topic.clear_history
+ topic.approved = true
topic.save
- assert_equal true, topic.cached
- assert_equal true, topic.record_updated
+ assert_equal [:update_and_destroy, :create_and_update], topic.history
+
+ topic.clear_history
+ topic.destroy
+ assert_equal [:update_and_destroy, :create_and_destroy], topic.history
end
end
diff --git a/activerecord/test/cases/transaction_isolation_test.rb b/activerecord/test/cases/transaction_isolation_test.rb
index a396da6645..4f1cb99b68 100644
--- a/activerecord/test/cases/transaction_isolation_test.rb
+++ b/activerecord/test/cases/transaction_isolation_test.rb
@@ -77,7 +77,7 @@ class TransactionIsolationTest < ActiveRecord::TestCase
Tag.transaction(isolation: :repeatable_read) do
tag.reload
- Tag2.find(tag.id).update_attributes(name: 'emily')
+ Tag2.find(tag.id).update(name: 'emily')
tag.reload
assert_equal 'jon', tag.name
diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb
index bb4f2c8064..980981903a 100644
--- a/activerecord/test/cases/transactions_test.rb
+++ b/activerecord/test/cases/transactions_test.rb
@@ -14,6 +14,21 @@ class TransactionTest < ActiveRecord::TestCase
@first, @second = Topic.find(1, 2).sort_by { |t| t.id }
end
+ def test_raise_after_destroy
+ assert_not @first.frozen?
+
+ assert_raises(RuntimeError) {
+ Topic.transaction do
+ @first.destroy
+ assert @first.frozen?
+ raise
+ end
+ }
+
+ assert @first.reload
+ assert_not @first.frozen?
+ end
+
def test_successful
Topic.transaction do
@first.approved = true
@@ -102,21 +117,35 @@ class TransactionTest < ActiveRecord::TestCase
assert !Topic.find(1).approved?
end
- def test_update_attributes_should_rollback_on_failure
+ 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
assert posts_count > 0
- status = author.update_attributes(:name => nil, :post_ids => [])
+ status = author.update(name: nil, post_ids: [])
assert !status
assert_equal posts_count, author.posts(true).size
end
- def test_update_attributes_should_rollback_on_failure!
+ def test_update_should_rollback_on_failure!
author = Author.find(1)
posts_count = author.posts.size
assert posts_count > 0
assert_raise(ActiveRecord::RecordInvalid) do
- author.update_attributes!(:name => nil, :post_ids => [])
+ author.update!(name: nil, post_ids: [])
end
assert_equal posts_count, author.posts(true).size
end
@@ -346,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')
@@ -362,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'
@@ -395,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)
@@ -436,6 +487,34 @@ class TransactionTest < ActiveRecord::TestCase
end
end
+ def test_transactions_state_from_rollback
+ connection = Topic.connection
+ transaction = ActiveRecord::ConnectionAdapters::ClosedTransaction.new(connection).begin
+
+ assert transaction.open?
+ assert !transaction.state.rolledback?
+ assert !transaction.state.committed?
+
+ transaction.perform_rollback
+
+ assert transaction.state.rolledback?
+ assert !transaction.state.committed?
+ end
+
+ def test_transactions_state_from_commit
+ connection = Topic.connection
+ transaction = ActiveRecord::ConnectionAdapters::ClosedTransaction.new(connection).begin
+
+ assert transaction.open?
+ assert !transaction.state.rolledback?
+ assert !transaction.state.committed?
+
+ transaction.perform_commit
+
+ assert !transaction.state.rolledback?
+ assert transaction.state.committed?
+ end
+
private
%w(validation save destroy).each do |filter|
@@ -493,22 +572,22 @@ if current_adapter?(:PostgreSQLAdapter)
# 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
- Thread.new do
- Topic.transaction do
- topic = Topic.find(1)
- topic.approved = !topic.approved?
- topic.save!
- topic.approved = !topic.approved?
- topic.save!
- end
- Topic.connection.close
+ skip "in memory db can't share a db between threads" if in_memory_db?
+
+ threads = 3.times.map do
+ Thread.new do
+ Topic.transaction do
+ topic = Topic.find(1)
+ topic.approved = !topic.approved?
+ assert topic.save!
+ topic.approved = !topic.approved?
+ assert topic.save!
end
+ Topic.connection.close
end
-
- threads.each { |t| t.join }
end
+
+ threads.each { |t| t.join }
end
# Test for dirty reads among simultaneous transactions.
@@ -560,14 +639,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/unconnected_test.rb b/activerecord/test/cases/unconnected_test.rb
index 5a69054445..e82ca3f93d 100644
--- a/activerecord/test/cases/unconnected_test.rb
+++ b/activerecord/test/cases/unconnected_test.rb
@@ -7,13 +7,13 @@ class TestUnconnectedAdapter < ActiveRecord::TestCase
self.use_transactional_fixtures = false
def setup
- @underlying = ActiveRecord::Model.connection
- @specification = ActiveRecord::Model.remove_connection
+ @underlying = ActiveRecord::Base.connection
+ @specification = ActiveRecord::Base.remove_connection
end
def teardown
@underlying = nil
- ActiveRecord::Model.establish_connection(@specification)
+ ActiveRecord::Base.establish_connection(@specification)
load_schema if in_memory_db?
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 2f5ee32538..32d2bf746f 100644
--- a/activerecord/test/cases/validations/i18n_generate_message_validation_test.rb
+++ b/activerecord/test/cases/validations/i18n_generate_message_validation_test.rb
@@ -54,4 +54,23 @@ class I18nGenerateMessageValidationTest < ActiveRecord::TestCase
end
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')
+ 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')
+ 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')
+ 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')
+ end
end
diff --git a/activerecord/test/cases/validations/presence_validation_test.rb b/activerecord/test/cases/validations/presence_validation_test.rb
index cd9175f454..1de8934406 100644
--- a/activerecord/test/cases/validations/presence_validation_test.rb
+++ b/activerecord/test/cases/validations/presence_validation_test.rb
@@ -18,6 +18,13 @@ class PresenceValidationTest < ActiveRecord::TestCase
assert b.valid?
end
+ def test_validates_presence_of_has_one
+ Boy.validates_presence_of(:face)
+ b = Boy.new
+ assert b.invalid?, "should not be valid if has_one association missing"
+ assert_equal 1, b.errors[:face].size, "validates_presence_of should only add one error"
+ end
+
def test_validates_presence_of_has_one_marked_for_destruction
Boy.validates_presence_of(:face)
b = Boy.new
diff --git a/activerecord/test/cases/validations/uniqueness_validation_test.rb b/activerecord/test/cases/validations/uniqueness_validation_test.rb
index 46212e49b6..2b33f01783 100644
--- a/activerecord/test/cases/validations/uniqueness_validation_test.rb
+++ b/activerecord/test/cases/validations/uniqueness_validation_test.rb
@@ -30,6 +30,11 @@ class ReplyWithTitleObject < Reply
def title; ReplyTitle.new; end
end
+class Employee < ActiveRecord::Base
+ self.table_name = 'postgresql_arrays'
+ validates_uniqueness_of :nicknames
+end
+
class UniquenessValidationTest < ActiveRecord::TestCase
fixtures :topics, 'warehouse-things', :developers
@@ -49,7 +54,7 @@ class UniquenessValidationTest < ActiveRecord::TestCase
assert !t2.save, "Shouldn't save t2 as unique"
assert_equal ["has already been taken"], t2.errors[:title]
- t2.title = "Now Im really also unique"
+ t2.title = "Now I am really also unique"
assert t2.save, "Should now save t2 as unique"
end
@@ -263,7 +268,7 @@ class UniquenessValidationTest < ActiveRecord::TestCase
end
def test_validate_case_sensitive_uniqueness_with_attribute_passed_as_integer
- Topic.validates_uniqueness_of(:title, :case_sensitve => true)
+ Topic.validates_uniqueness_of(:title, :case_sensitive => true)
Topic.create!('title' => 101)
t2 = Topic.new('title' => 101)
@@ -341,16 +346,34 @@ class UniquenessValidationTest < ActiveRecord::TestCase
assert w6.errors[:city].any?, "Should have errors for city"
assert_equal ["has already been taken"], w6.errors[:city], "Should have uniqueness message for city"
end
-
+
def test_validate_uniqueness_with_conditions
- Topic.validates_uniqueness_of(:title, :conditions => Topic.where('approved = ?', true))
+ Topic.validates_uniqueness_of :title, conditions: -> { where(approved: true) }
Topic.create("title" => "I'm a topic", "approved" => true)
Topic.create("title" => "I'm an unapproved topic", "approved" => false)
-
+
t3 = Topic.new("title" => "I'm a topic", "approved" => true)
assert !t3.valid?, "t3 shouldn't be valid"
-
+
t4 = Topic.new("title" => "I'm an unapproved topic", "approved" => false)
assert t4.valid?, "t4 should be valid"
end
+
+ def test_validate_uniqueness_with_non_callable_conditions_is_not_supported
+ assert_raises(ArgumentError) {
+ Topic.validates_uniqueness_of :title, conditions: Topic.where(approved: true)
+ }
+ 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
+
+ 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
diff --git a/activerecord/test/cases/validations_repair_helper.rb b/activerecord/test/cases/validations_repair_helper.rb
index 11912ca1cc..c02b3241cd 100644
--- a/activerecord/test/cases/validations_repair_helper.rb
+++ b/activerecord/test/cases/validations_repair_helper.rb
@@ -6,7 +6,7 @@ module ActiveRecord
def repair_validations(*model_classes)
teardown do
model_classes.each do |k|
- k.reset_callbacks(:validate)
+ k.clear_validators!
end
end
end
@@ -16,7 +16,7 @@ module ActiveRecord
yield
ensure
model_classes.each do |k|
- k.reset_callbacks(:validate)
+ k.clear_validators!
end
end
end
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..83a710b1b7 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
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/dog_lovers.yml b/activerecord/test/fixtures/dog_lovers.yml
index d3e5e4a1aa..3f4c6c9e4c 100644
--- a/activerecord/test/fixtures/dog_lovers.yml
+++ b/activerecord/test/fixtures/dog_lovers.yml
@@ -2,3 +2,6 @@ david:
id: 1
bred_dogs_count: 0
trained_dogs_count: 1
+joanna:
+ id: 2
+ dogs_count: 1
diff --git a/activerecord/test/fixtures/dogs.yml b/activerecord/test/fixtures/dogs.yml
index 16d19be2c5..b5eb2c7b74 100644
--- a/activerecord/test/fixtures/dogs.yml
+++ b/activerecord/test/fixtures/dogs.yml
@@ -1,3 +1,4 @@
sophie:
id: 1
trainer_id: 1
+ dog_lover_id: 2
diff --git a/activerecord/test/fixtures/friendships.yml b/activerecord/test/fixtures/friendships.yml
index 1ee09175bf..ae0abe0162 100644
--- a/activerecord/test/fixtures/friendships.yml
+++ b/activerecord/test/fixtures/friendships.yml
@@ -1,4 +1,4 @@
Connection 1:
id: 1
- person_id: 1
- friend_id: 2 \ No newline at end of file
+ friend_id: 1
+ follower_id: 2
diff --git a/activerecord/test/fixtures/people.yml b/activerecord/test/fixtures/people.yml
index e640a38f1f..0ec05e8d56 100644
--- a/activerecord/test/fixtures/people.yml
+++ b/activerecord/test/fixtures/people.yml
@@ -5,6 +5,7 @@ michael:
number1_fan_id: 3
gender: M
followers_count: 1
+ friends_too_count: 1
david:
id: 2
first_name: David
@@ -12,6 +13,7 @@ david:
number1_fan_id: 1
gender: M
followers_count: 1
+ friends_too_count: 1
susan:
id: 3
first_name: Susan
@@ -19,3 +21,4 @@ susan:
number1_fan_id: 1
gender: F
followers_count: 1
+ friends_too_count: 1
diff --git a/activerecord/test/fixtures/pets.yml b/activerecord/test/fixtures/pets.yml
index a1601a53f0..2ec4f53e6d 100644
--- a/activerecord/test/fixtures/pets.yml
+++ b/activerecord/test/fixtures/pets.yml
@@ -12,3 +12,8 @@ mochi:
pet_id: 3
name: mochi
owner_id: 2
+
+bulbul:
+ pet_id: 4
+ name: bulbul
+ owner_id: 1
diff --git a/activerecord/test/fixtures/readers.yml b/activerecord/test/fixtures/readers.yml
index 8a6076655b..14b883f041 100644
--- a/activerecord/test/fixtures/readers.yml
+++ b/activerecord/test/fixtures/readers.yml
@@ -2,8 +2,10 @@ michael_welcome:
id: 1
post_id: 1
person_id: 1
+ first_post_id: 2
michael_authorless:
id: 2
post_id: 3
- person_id: 1 \ No newline at end of file
+ person_id: 1
+ first_post_id: 3
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/fixtures/toys.yml b/activerecord/test/fixtures/toys.yml
index 037e335e0a..ae9044ec62 100644
--- a/activerecord/test/fixtures/toys.yml
+++ b/activerecord/test/fixtures/toys.yml
@@ -2,3 +2,13 @@ bone:
toy_id: 1
name: Bone
pet_id: 1
+
+doll:
+ toy_id: 2
+ name: Doll
+ pet_id: 2
+
+bulbuli:
+ toy_id: 3
+ name: Bulbuli
+ pet_id: 4
diff --git a/activerecord/test/fixtures/traffic_lights.yml b/activerecord/test/fixtures/traffic_lights.yml
index 6dabd53474..81b4e47959 100644
--- a/activerecord/test/fixtures/traffic_lights.yml
+++ b/activerecord/test/fixtures/traffic_lights.yml
@@ -4,3 +4,7 @@ uk:
- Green
- Red
- Orange
+ long_state:
+ - "Green, go ahead"
+ - "Red, wait"
+ - "Orange, caution light is about to switch" \ No newline at end of file
diff --git a/activerecord/test/migrations/10_urban/9_add_expressions.rb b/activerecord/test/migrations/10_urban/9_add_expressions.rb
new file mode 100644
index 0000000000..79a342e574
--- /dev/null
+++ b/activerecord/test/migrations/10_urban/9_add_expressions.rb
@@ -0,0 +1,11 @@
+class AddExpressions < ActiveRecord::Migration
+ def self.up
+ create_table("expressions") do |t|
+ t.column :expression, :string
+ end
+ end
+
+ def self.down
+ drop_table "expressions"
+ end
+end
diff --git a/activerecord/test/migrations/magic/1_currencies_have_symbols.rb b/activerecord/test/migrations/magic/1_currencies_have_symbols.rb
new file mode 100644
index 0000000000..c066c068c2
--- /dev/null
+++ b/activerecord/test/migrations/magic/1_currencies_have_symbols.rb
@@ -0,0 +1,12 @@
+# coding: ISO-8859-15
+
+class CurrenciesHaveSymbols < ActiveRecord::Migration
+ def self.up
+ # We use for default currency symbol
+ add_column "currencies", "symbol", :string, :default => ""
+ end
+
+ def self.down
+ remove_column "currencies", "symbol"
+ end
+end
diff --git a/activerecord/test/models/admin/user.rb b/activerecord/test/models/admin/user.rb
index 467f3ccd39..4c3b71e8f9 100644
--- a/activerecord/test/models/admin/user.rb
+++ b/activerecord/test/models/admin/user.rb
@@ -1,10 +1,24 @@
class Admin::User < ActiveRecord::Base
+ class Coder
+ def initialize(default = {})
+ @default = default
+ end
+
+ def dump(o)
+ ActiveSupport::JSON.encode(o || @default)
+ end
+
+ def load(s)
+ s.present? ? ActiveSupport::JSON.decode(s) : @default.clone
+ end
+ end
+
belongs_to :account
store :settings, :accessors => [ :color, :homepage ]
store_accessor :settings, :favorite_food
store :preferences, :accessors => [ :remember_login ]
- store :json_data, :accessors => [ :height, :weight ], :coder => JSON
- store :json_data_empty, :accessors => [ :is_a_good_guy ], :coder => JSON
+ store :json_data, :accessors => [ :height, :weight ], :coder => Coder.new
+ store :json_data_empty, :accessors => [ :is_a_good_guy ], :coder => Coder.new
def phone_number
read_store_attribute(:settings, :phone_number).gsub(/(\d{3})(\d{3})(\d{4})/,'(\1) \2-\3')
@@ -13,4 +27,13 @@ class Admin::User < ActiveRecord::Base
def phone_number=(value)
write_store_attribute(:settings, :phone_number, value && value.gsub(/[^\d]/,''))
end
+
+ def color
+ super || 'red'
+ end
+
+ def color=(value)
+ value = 'blue' unless %w(black red green blue).include?(value)
+ super
+ end
end
diff --git a/activerecord/test/models/author.rb b/activerecord/test/models/author.rb
index 77f4a2ec87..794d1af43d 100644
--- a/activerecord/test/models/author.rb
+++ b/activerecord/test/models/author.rb
@@ -1,5 +1,6 @@
class Author < ActiveRecord::Base
has_many :posts
+ has_one :post
has_many :very_special_comments, :through => :posts
has_many :posts_with_comments, -> { includes(:comments) }, :class_name => "Post"
has_many :popular_grouped_posts, -> { includes(:comments).group("type").having("SUM(comments_count) > 1").select("type") }, :class_name => "Post"
@@ -7,21 +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_extension, :class_name => "Post" do #, :extend => ProxyTestExtension
- def testing_proxy_owner
- proxy_owner
- end
- def testing_proxy_reflection
- proxy_reflection
- end
- def testing_proxy_target
- proxy_target
- end
- end
+ has_many :posts_with_special_categorizations, :class_name => 'PostWithSpecialCategorization'
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
@@ -35,11 +29,17 @@ 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_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, -> { uniq.order('comments.id') }, :through => :posts, :source => :comments
- has_many :ordered_uniq_comments_desc, -> { uniq.order('comments.id DESC') }, :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
has_many :readonly_comments, -> { readonly }, :through => :posts, :source => :comments
has_many :special_posts
@@ -86,20 +86,20 @@ class Author < ActiveRecord::Base
has_many :categories_like_general, -> { where(:name => 'General') }, :through => :categorizations, :source => :category, :class_name => 'Category'
has_many :categorized_posts, :through => :categorizations, :source => :post
- has_many :unique_categorized_posts, -> { uniq }, :through => :categorizations, :source => :post
+ has_many :unique_categorized_posts, -> { distinct }, :through => :categorizations, :source => :post
has_many :nothings, :through => :kateggorisatons, :class_name => 'Category'
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
has_many :tagging_tags, :through => :taggings, :source => :tag
- has_many :similar_posts, -> { uniq }, :through => :tags, :source => :tagged_posts
+ has_many :similar_posts, -> { distinct }, :through => :tags, :source => :tagged_posts
has_many :distinct_tags, -> { select("DISTINCT tags.*").order("tags.name") }, :through => :posts, :source => :tags
has_many :tags_with_primary_key, :through => :posts
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/autoloadable/extra_firm.rb b/activerecord/test/models/autoloadable/extra_firm.rb
new file mode 100644
index 0000000000..5578ba0d9b
--- /dev/null
+++ b/activerecord/test/models/autoloadable/extra_firm.rb
@@ -0,0 +1,2 @@
+class ExtraFirm < Company
+end
diff --git a/activerecord/test/models/book.rb b/activerecord/test/models/book.rb
index ce81a37966..5458a28cc9 100644
--- a/activerecord/test/models/book.rb
+++ b/activerecord/test/models/book.rb
@@ -2,7 +2,7 @@ class Book < ActiveRecord::Base
has_many :authors
has_many :citations, :foreign_key => 'book1_id'
- has_many :references, -> { uniq }, :through => :citations, :source => :reference_of
+ has_many :references, -> { distinct }, :through => :citations, :source => :reference_of
has_many :subscriptions
has_many :subscribers, :through => :subscriptions
diff --git a/activerecord/test/models/bulb.rb b/activerecord/test/models/bulb.rb
index e4c0278c0d..4361188e21 100644
--- a/activerecord/test/models/bulb.rb
+++ b/activerecord/test/models/bulb.rb
@@ -1,6 +1,6 @@
class Bulb < ActiveRecord::Base
default_scope { where(:name => 'defaulty') }
- belongs_to :car
+ belongs_to :car, :touch => true
attr_reader :scope_after_initialize, :attributes_after_initialize
@@ -37,3 +37,9 @@ class CustomBulb < Bulb
self.frickinawesome = true if name == 'Dude'
end
end
+
+class FunkyBulb < Bulb
+ before_destroy do
+ raise "before_destroy was called"
+ 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..6d257dbe7e 100644
--- a/activerecord/test/models/car.rb
+++ b/activerecord/test/models/car.rb
@@ -1,11 +1,9 @@
class Car < ActiveRecord::Base
-
has_many :bulbs
+ has_many :funky_bulbs, class_name: 'FunkyBulb', 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/category.rb b/activerecord/test/models/category.rb
index f8c8ebb70c..7da39a8e33 100644
--- a/activerecord/test/models/category.rb
+++ b/activerecord/test/models/category.rb
@@ -31,9 +31,4 @@ class Category < ActiveRecord::Base
end
class SpecialCategory < Category
-
- def self.what_are_you
- 'a special category...'
- end
-
end
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 24a65b0f2f..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
+ 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/comment.rb b/activerecord/test/models/comment.rb
index 4b2015fe01..ede5fbd0c6 100644
--- a/activerecord/test/models/comment.rb
+++ b/activerecord/test/models/comment.rb
@@ -29,16 +29,10 @@ class Comment < ActiveRecord::Base
end
class SpecialComment < Comment
- def self.what_are_you
- 'a special comment...'
- end
end
class SubSpecialComment < SpecialComment
end
class VerySpecialComment < Comment
- def self.what_are_you
- 'a very special comment...'
- end
end
diff --git a/activerecord/test/models/company.rb b/activerecord/test/models/company.rb
index 17b17724e8..0b0b304121 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,11 @@ 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
+ 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 +53,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',
@@ -111,13 +96,7 @@ end
class DependentFirm < Company
has_one :account, :foreign_key => "firm_id", :dependent => :nullify
has_many :companies, :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
+ has_one :company, :foreign_key => 'client_of', :dependent => :nullify
end
class RestrictedWithExceptionFirm < Company
@@ -140,9 +119,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
@@ -192,7 +175,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
@@ -205,6 +187,8 @@ class Account < ActiveRecord::Base
belongs_to :firm, :class_name => 'Company'
belongs_to :unautosaved_firm, :foreign_key => "firm_id", :class_name => "Firm", :autosave => false
+ alias_attribute :available_credit, :credit_limit
+
def self.destroyed_account_ids
@destroyed_account_ids ||= Hash.new { |h,k| h[k] = [] }
end
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 622dd75aeb..a26de55758 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
@@ -44,6 +38,8 @@ class Developer < ActiveRecord::Base
has_and_belongs_to_many :special_projects, :join_table => 'developers_projects', :association_foreign_key => 'project_id'
has_many :audit_logs
+ has_many :contracts
+ has_many :firms, :through => :contracts, :source => :firm
scope :jamises, -> { where(:name => 'Jamis') }
@@ -57,6 +53,16 @@ class Developer < ActiveRecord::Base
def log=(message)
audit_logs.build :message => message
end
+
+ after_find :track_instance_count
+ cattr_accessor :instance_count
+
+ def track_instance_count
+ self.class.instance_count ||= 0
+ self.class.instance_count += 1
+ end
+ private :track_instance_count
+
end
class AuditLog < ActiveRecord::Base
@@ -91,6 +97,15 @@ class DeveloperWithIncludes < ActiveRecord::Base
default_scope { includes(:audit_logs) }
end
+class DeveloperFilteredOnJoins < ActiveRecord::Base
+ self.table_name = 'developers'
+ has_and_belongs_to_many :projects, -> { order('projects.id') }, :foreign_key => 'developer_id', :join_table => 'developers_projects'
+
+ def self.default_scope
+ joins(:projects).where(:projects => { :name => 'Active Controller' })
+ end
+end
+
class DeveloperOrderedBySalary < ActiveRecord::Base
self.table_name = 'developers'
default_scope { order('salary DESC') }
@@ -224,3 +239,8 @@ class ThreadsafeDeveloper < ActiveRecord::Base
limit(1)
end
end
+
+class CachedDeveloper < ActiveRecord::Base
+ self.table_name = "developers"
+ self.cache_timestamp_format = :number
+end
diff --git a/activerecord/test/models/dog.rb b/activerecord/test/models/dog.rb
index 72b7d33a86..b02b8447b8 100644
--- a/activerecord/test/models/dog.rb
+++ b/activerecord/test/models/dog.rb
@@ -1,4 +1,5 @@
class Dog < ActiveRecord::Base
- belongs_to :breeder, :class_name => "DogLover", :counter_cache => :bred_dogs_count
- belongs_to :trainer, :class_name => "DogLover", :counter_cache => :trained_dogs_count
+ belongs_to :breeder, class_name: "DogLover", counter_cache: :bred_dogs_count
+ belongs_to :trainer, class_name: "DogLover", counter_cache: :trained_dogs_count
+ belongs_to :doglover, foreign_key: :dog_lover_id, class_name: "DogLover", counter_cache: true
end
diff --git a/activerecord/test/models/dog_lover.rb b/activerecord/test/models/dog_lover.rb
index a33dc575c5..2c5be94aea 100644
--- a/activerecord/test/models/dog_lover.rb
+++ b/activerecord/test/models/dog_lover.rb
@@ -1,4 +1,5 @@
class DogLover < ActiveRecord::Base
- has_many :trained_dogs, :class_name => "Dog", :foreign_key => :trainer_id
- has_many :bred_dogs, :class_name => "Dog", :foreign_key => :breeder_id
+ has_many :trained_dogs, class_name: "Dog", foreign_key: :trainer_id, dependent: :destroy
+ has_many :bred_dogs, class_name: "Dog", foreign_key: :breeder_id
+ has_many :dogs
end
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/friendship.rb b/activerecord/test/models/friendship.rb
index 6b4f7acc38..4b411ca8e0 100644
--- a/activerecord/test/models/friendship.rb
+++ b/activerecord/test/models/friendship.rb
@@ -1,4 +1,6 @@
class Friendship < ActiveRecord::Base
belongs_to :friend, class_name: 'Person'
- belongs_to :follower, foreign_key: 'friend_id', class_name: 'Person', counter_cache: :followers_count
+ # friend_too exists to test a bug, and probably shouldn't be used elsewhere
+ belongs_to :friend_too, foreign_key: 'friend_id', class_name: 'Person', counter_cache: :friends_too_count
+ belongs_to :follower, class_name: 'Person'
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/liquid.rb b/activerecord/test/models/liquid.rb
index 6cfd443e75..69d4d7df1a 100644
--- a/activerecord/test/models/liquid.rb
+++ b/activerecord/test/models/liquid.rb
@@ -1,5 +1,4 @@
class Liquid < ActiveRecord::Base
self.table_name = :liquid
- has_many :molecules, -> { uniq }
+ has_many :molecules, -> { distinct }
end
-
diff --git a/activerecord/test/models/man.rb b/activerecord/test/models/man.rb
index 4bff92dc98..f4d127730c 100644
--- a/activerecord/test/models/man.rb
+++ b/activerecord/test/models/man.rb
@@ -6,4 +6,5 @@ class Man < ActiveRecord::Base
# 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 1134b09d8b..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
+ 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 fe619f8732..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
+ 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/owner.rb b/activerecord/test/models/owner.rb
index fea55f4535..1c7ed4aa3e 100644
--- a/activerecord/test/models/owner.rb
+++ b/activerecord/test/models/owner.rb
@@ -1,5 +1,5 @@
class Owner < ActiveRecord::Base
self.primary_key = :owner_id
- has_many :pets
+ has_many :pets, -> { order 'pets.name desc' }
has_many :toys, :through => :pets
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 6ad0cf6987..1a282dbce4 100644
--- a/activerecord/test/models/person.rb
+++ b/activerecord/test/models/person.rb
@@ -8,13 +8,17 @@ class Person < ActiveRecord::Base
has_many :posts_with_no_comments, -> { includes(:comments).where('comments.id is null').references(:comments) },
:through => :readers, :source => :post
- has_many :followers, foreign_key: 'friend_id', class_name: 'Friendship'
+ has_many :friendships, foreign_key: 'friend_id'
+ # friends_too exists to test a bug, and probably shouldn't be used elsewhere
+ has_many :friends_too, foreign_key: 'friend_id', class_name: 'Friendship'
+ has_many :followers, through: :friendships
has_many :references
has_many :bad_references
has_many :fixed_bad_references, -> { where :favourite => true }, :class_name => 'BadReference'
has_one :favourite_reference, -> { where 'favourite=?', true }, :class_name => 'Reference'
has_many :posts_with_comments_sorted_by_comment_id, -> { includes(:comments).order('comments.id') }, :through => :readers, :source => :post
+ has_many :first_posts, -> { where(id: [1, 2]) }, through: :readers
has_many :jobs, :through => :references
has_many :jobs_with_dependent_destroy, :source => :job, :through => :references, :dependent => :destroy
@@ -28,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') }
@@ -100,3 +105,24 @@ class NestedPerson < ActiveRecord::Base
assign_attributes({ :best_friend_attributes => { :first_name => new_name } })
end
end
+
+class Insure
+ INSURES = %W{life annuality}
+
+ def self.load mask
+ INSURES.select do |insure|
+ (1 << INSURES.index(insure)) & mask.to_i > 0
+ end
+ end
+
+ def self.dump insures
+ numbers = insures.map { |insure| INSURES.index(insure) }
+ numbers.inject(0) { |sum, n| sum + (1 << n) }
+ end
+end
+
+class SerializedPerson < ActiveRecord::Base
+ self.table_name = 'people'
+
+ serialize :insures, Insure
+end
diff --git a/activerecord/test/models/pet.rb b/activerecord/test/models/pet.rb
index 3cd5bceed5..f7970d7aab 100644
--- a/activerecord/test/models/pet.rb
+++ b/activerecord/test/models/pet.rb
@@ -1,5 +1,4 @@
class Pet < ActiveRecord::Base
-
attr_accessor :current_user
self.primary_key = :pet_id
@@ -13,5 +12,4 @@ class Pet < ActiveRecord::Base
after_destroy do |record|
Pet.after_destroy_output = record.current_user
end
-
end
diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb
index c995f59a15..faf539a562 100644
--- a/activerecord/test/models/post.rb
+++ b/activerecord/test/models/post.rb
@@ -1,20 +1,28 @@
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'
end
end
+ module NamedExtension2
+ def greeting
+ "hello"
+ end
+ end
+
scope :containing_the_letter_a, -> { where("body LIKE '%a%'") }
scope :ranked_by_comments, -> { order("comments_count DESC") }
scope :limit_by, lambda {|l| limit(l) }
- belongs_to :author do
- def greeting
- "hello"
- end
- end
+ belongs_to :author
belongs_to :author_with_posts, -> { includes(:posts) }, :class_name => "Author", :foreign_key => :author_id
belongs_to :author_with_address, -> { includes(:author_address) }, :class_name => "Author", :foreign_key => :author_id
@@ -29,6 +37,9 @@ class Post < ActiveRecord::Base
scope :with_very_special_comments, -> { joins(:comments).where(:comments => {:type => 'VerySpecialComment'}) }
scope :with_post, ->(post_id) { joins(:comments).where(:comments => { :post_id => post_id }) }
+ scope :with_comments, -> { preload(:comments) }
+ scope :with_tags, -> { preload(:taggings) }
+
has_many :comments do
def find_most_recent
order("id DESC").first
@@ -43,9 +54,20 @@ class Post < ActiveRecord::Base
end
end
+ has_many :comments_with_extend, extend: NamedExtension, class_name: "Comment", foreign_key: "post_id" do
+ def greeting
+ "hello"
+ end
+ end
+
+ has_many :comments_with_extend_2, extend: [NamedExtension, NamedExtension2], class_name: "Comment", foreign_key: "post_id"
+
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' },
@@ -59,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'
@@ -109,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) },
@@ -151,18 +174,6 @@ class SubStiPost < StiPost
self.table_name = Post.table_name
end
-ActiveSupport::Deprecation.silence do
- class DeprecatedPostWithComment < ActiveRecord::Base
- self.table_name = 'posts'
- default_scope where("posts.comments_count > 0").order("posts.comments_count ASC")
- end
-end
-
-class PostForAuthor < ActiveRecord::Base
- self.table_name = 'posts'
- cattr_accessor :selected_author
-end
-
class FirstPost < ActiveRecord::Base
self.table_name = 'posts'
default_scope { where(:id => 1) }
@@ -177,6 +188,11 @@ class PostWithDefaultInclude < ActiveRecord::Base
has_many :comments, :foreign_key => :post_id
end
+class PostWithSpecialCategorization < Post
+ has_many :categorizations, :foreign_key => :post_id
+ default_scope { where(:type => 'PostWithSpecialCategorization').joins(:categorizations).where(:categorizations => { :special => true }) }
+end
+
class PostWithDefaultScope < ActiveRecord::Base
self.table_name = 'posts'
default_scope { order(:title) }
diff --git a/activerecord/test/models/project.rb b/activerecord/test/models/project.rb
index af3ec4be83..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, -> { uniq.order 'developers.name desc, developers.id desc' }
+ 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, -> { uniq.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'").uniq }, :class_name => "Developer"
- has_and_belongs_to_many :developers_named_david_with_hash_conditions, -> { where(:name => 'David').uniq }, :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}"},
@@ -40,7 +26,4 @@ class Project < ActiveRecord::Base
end
class SpecialProject < Project
- def hello_world
- "hello there!"
- end
end
diff --git a/activerecord/test/models/reader.rb b/activerecord/test/models/reader.rb
index f8fb9c573e..3a6b7fad34 100644
--- a/activerecord/test/models/reader.rb
+++ b/activerecord/test/models/reader.rb
@@ -2,6 +2,7 @@ class Reader < ActiveRecord::Base
belongs_to :post
belongs_to :person, :inverse_of => :readers
belongs_to :single_person, :class_name => 'Person', :foreign_key => :person_id, :inverse_of => :reader
+ belongs_to :first_post, -> { where(id: [2, 3]) }
end
class SecureReader < ActiveRecord::Base
diff --git a/activerecord/test/models/reference.rb b/activerecord/test/models/reference.rb
index 561b431766..c2f9068f57 100644
--- a/activerecord/test/models/reference.rb
+++ b/activerecord/test/models/reference.rb
@@ -4,15 +4,14 @@ class Reference < ActiveRecord::Base
has_many :agents_posts_authors, :through => :person
- class << self
- attr_accessor :make_comments
- end
+ class << self; attr_accessor :make_comments; end
+ self.make_comments = false
before_destroy :make_comments
def make_comments
if self.class.make_comments
- person.update_attributes :comments => "Reference destroyed"
+ person.update comments: "Reference destroyed"
end
end
end
diff --git a/activerecord/test/models/reply.rb b/activerecord/test/models/reply.rb
index 079e325aad..3e82e55d89 100644
--- a/activerecord/test/models/reply.rb
+++ b/activerecord/test/models/reply.rb
@@ -1,14 +1,13 @@
require 'models/topic'
class Reply < Topic
- scope :base, -> { scoped }
-
belongs_to :topic, :foreign_key => "parent_id", :counter_cache => true
belongs_to :topic_with_primary_key, :class_name => "Topic", :primary_key => "title", :foreign_key => "parent_title", :counter_cache => "replies_count"
has_many :replies, :class_name => "SillyReply", :dependent => :destroy, :foreign_key => "parent_id"
end
class UniqueReply < Reply
+ belongs_to :topic, :foreign_key => 'parent_id', :counter_cache => true
validates_uniqueness_of :content, :scope => 'parent_id'
end
diff --git a/activerecord/test/models/speedometer.rb b/activerecord/test/models/speedometer.rb
index 0a7d38d8ec..497c3aba9a 100644
--- a/activerecord/test/models/speedometer.rb
+++ b/activerecord/test/models/speedometer.rb
@@ -1,4 +1,6 @@
class Speedometer < ActiveRecord::Base
self.primary_key = :speedometer_id
belongs_to :dashboard
+
+ has_many :minivans
end
diff --git a/activerecord/test/models/teapot.rb b/activerecord/test/models/teapot.rb
deleted file mode 100644
index b035b18c1b..0000000000
--- a/activerecord/test/models/teapot.rb
+++ /dev/null
@@ -1,35 +0,0 @@
-class Teapot
- # I'm a little teapot,
- # Short and stout,
- # Here is my handle
- # Here is my spout
- # When I get all steamed up,
- # Hear me shout,
- # Tip me over and pour me out!
- #
- # HELL YEAH TEAPOT SONG
-
- include ActiveRecord::Model
-end
-
-class OtherTeapot < Teapot
-end
-
-class OMFGIMATEAPOT
- def aaahhh
- "mmm"
- end
-end
-
-class CoolTeapot < OMFGIMATEAPOT
- include ActiveRecord::Model
- self.table_name = "teapots"
-end
-
-class Ceiling
- include ActiveRecord::Model
-
- class Teapot
- include ActiveRecord::Model
- end
-end
diff --git a/activerecord/test/models/topic.rb b/activerecord/test/models/topic.rb
index 4b27c16681..40c8e97fc2 100644
--- a/activerecord/test/models/topic.rb
+++ b/activerecord/test/models/topic.rb
@@ -33,7 +33,7 @@ class Topic < ActiveRecord::Base
end
has_many :replies, :dependent => :destroy, :foreign_key => "parent_id"
- has_many :replies_with_primary_key, :class_name => "Reply", :dependent => :destroy, :primary_key => "title", :foreign_key => "parent_title"
+ has_many :approved_replies, -> { approved }, class_name: 'Reply', foreign_key: "parent_id", counter_cache: 'replies_count'
has_many :unique_replies, :dependent => :destroy, :foreign_key => "parent_id"
has_many :silly_unique_replies, :dependent => :destroy, :foreign_key => "parent_id"
@@ -106,6 +106,13 @@ class ImportantTopic < Topic
serialize :important, Hash
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?
+ true
+ end
+end
+
module Web
class Topic < ActiveRecord::Base
has_many :replies, :dependent => :destroy, :foreign_key => "parent_id", :class_name => 'Web::Reply'
diff --git a/activerecord/test/models/traffic_light.rb b/activerecord/test/models/traffic_light.rb
index 228f3f7bd4..a6b7edb882 100644
--- a/activerecord/test/models/traffic_light.rb
+++ b/activerecord/test/models/traffic_light.rb
@@ -1,3 +1,4 @@
class TrafficLight < ActiveRecord::Base
serialize :state, Array
+ serialize :long_state, Array
end
diff --git a/activerecord/test/schema/mysql2_specific_schema.rb b/activerecord/test/schema/mysql2_specific_schema.rb
index 24a43d7ece..a9a6514c9d 100644
--- a/activerecord/test/schema/mysql2_specific_schema.rb
+++ b/activerecord/test/schema/mysql2_specific_schema.rb
@@ -1,15 +1,29 @@
ActiveRecord::Schema.define do
- create_table :binary_fields, :force => true do |t|
- t.binary :tiny_blob, :limit => 255
- t.binary :normal_blob, :limit => 65535
- t.binary :medium_blob, :limit => 16777215
- t.binary :long_blob, :limit => 2147483647
- t.text :tiny_text, :limit => 255
- t.text :normal_text, :limit => 65535
- t.text :medium_text, :limit => 16777215
- t.text :long_text, :limit => 2147483647
+ create_table :binary_fields, force: true do |t|
+ t.binary :var_binary, limit: 255
+ t.binary :var_binary_large, limit: 4095
+ t.column :tiny_blob, 'tinyblob', limit: 255
+ t.binary :normal_blob, limit: 65535
+ t.binary :medium_blob, limit: 16777215
+ t.binary :long_blob, limit: 2147483647
+ t.text :tiny_text, limit: 255
+ t.text :normal_text, limit: 65535
+ t.text :medium_text, limit: 16777215
+ t.text :long_text, limit: 2147483647
end
+ add_index :binary_fields, :var_binary
+
+ create_table :key_tests, force: true, :options => 'ENGINE=MyISAM' do |t|
+ t.string :awesome
+ t.string :pizza
+ t.string :snacks
+ end
+
+ add_index :key_tests, :awesome, :type => :fulltext, :name => 'index_key_tests_on_awesome'
+ add_index :key_tests, :pizza, :using => :btree, :name => 'index_key_tests_on_pizza'
+ add_index :key_tests, :snacks, :name => 'index_key_tests_on_snack'
+
ActiveRecord::Base.connection.execute <<-SQL
DROP PROCEDURE IF EXISTS ten;
SQL
@@ -38,7 +52,7 @@ SQL
ActiveRecord::Base.connection.execute <<-SQL
CREATE TABLE enum_tests (
- enum_column ENUM('true','false')
+ enum_column ENUM('text','blob','tiny','medium','long')
)
SQL
end
diff --git a/activerecord/test/schema/mysql_specific_schema.rb b/activerecord/test/schema/mysql_specific_schema.rb
index 802c08b819..f2cffca52c 100644
--- a/activerecord/test/schema/mysql_specific_schema.rb
+++ b/activerecord/test/schema/mysql_specific_schema.rb
@@ -1,15 +1,29 @@
ActiveRecord::Schema.define do
- create_table :binary_fields, :force => true do |t|
- t.binary :tiny_blob, :limit => 255
- t.binary :normal_blob, :limit => 65535
- t.binary :medium_blob, :limit => 16777215
- t.binary :long_blob, :limit => 2147483647
- t.text :tiny_text, :limit => 255
- t.text :normal_text, :limit => 65535
- t.text :medium_text, :limit => 16777215
- t.text :long_text, :limit => 2147483647
+ create_table :binary_fields, force: true do |t|
+ t.binary :var_binary, limit: 255
+ t.binary :var_binary_large, limit: 4095
+ t.column :tiny_blob, 'tinyblob', limit: 255
+ t.binary :normal_blob, limit: 65535
+ t.binary :medium_blob, limit: 16777215
+ t.binary :long_blob, limit: 2147483647
+ t.text :tiny_text, limit: 255
+ t.text :normal_text, limit: 65535
+ t.text :medium_text, limit: 16777215
+ t.text :long_text, limit: 2147483647
end
+ add_index :binary_fields, :var_binary
+
+ create_table :key_tests, force: true, :options => 'ENGINE=MyISAM' do |t|
+ t.string :awesome
+ t.string :pizza
+ t.string :snacks
+ end
+
+ add_index :key_tests, :awesome, :type => :fulltext, :name => 'index_key_tests_on_awesome'
+ add_index :key_tests, :pizza, :using => :btree, :name => 'index_key_tests_on_pizza'
+ add_index :key_tests, :snacks, :name => 'index_key_tests_on_snack'
+
ActiveRecord::Base.connection.execute <<-SQL
DROP PROCEDURE IF EXISTS ten;
SQL
@@ -49,7 +63,7 @@ SQL
ActiveRecord::Base.connection.execute <<-SQL
CREATE TABLE enum_tests (
- enum_column ENUM('true','false')
+ enum_column ENUM('text','blob','tiny','medium','long')
)
SQL
diff --git a/activerecord/test/schema/postgresql_specific_schema.rb b/activerecord/test/schema/postgresql_specific_schema.rb
index d0e7338f15..6b7012a172 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_tsvectors postgresql_hstores postgresql_arrays postgresql_moneys postgresql_numbers postgresql_times postgresql_network_addresses postgresql_bit_strings postgresql_uuids
+ %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
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
@@ -32,6 +32,7 @@ ActiveRecord::Schema.define do
char3 text default 'a text field',
positive_integer integer default 1,
negative_integer integer default -1,
+ bigint_default bigint default 0::bigint,
decimal_number decimal(3,2) default 2.78,
multiline_default text DEFAULT '--- []
@@ -73,6 +74,18 @@ _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,
@@ -89,6 +102,15 @@ _SQL
_SQL
end
+ if 't' == select_value("select 'ltree'=ANY(select typname from pg_type)")
+ execute <<_SQL
+ CREATE TABLE postgresql_ltrees (
+ id SERIAL PRIMARY KEY,
+ path ltree
+ );
+_SQL
+ end
+
if 't' == select_value("select 'json'=ANY(select typname from pg_type)")
execute <<_SQL
CREATE TABLE postgresql_json_data_type (
@@ -124,9 +146,9 @@ _SQL
execute <<_SQL
CREATE TABLE postgresql_network_addresses (
id SERIAL PRIMARY KEY,
- cidr_address CIDR,
- inet_address INET,
- mac_address MACADDR
+ cidr_address CIDR default '192.168.1.0/24',
+ inet_address INET default '192.168.1.1',
+ mac_address MACADDR default 'ff:ff:ff:ff:ff:ff'
);
_SQL
@@ -152,7 +174,7 @@ _SQL
);
_SQL
-begin
+ begin
execute <<_SQL
CREATE TABLE postgresql_partitioned_table_parent (
id SERIAL PRIMARY KEY,
@@ -174,14 +196,14 @@ begin
BEFORE INSERT ON postgresql_partitioned_table_parent
FOR EACH ROW EXECUTE PROCEDURE partitioned_insert_trigger();
_SQL
-rescue ActiveRecord::StatementInvalid => e
- if e.message =~ /language "plpgsql" does not exist/
- execute "CREATE LANGUAGE 'plpgsql';"
- retry
- else
- raise e
+ rescue ActiveRecord::StatementInvalid => e
+ if e.message =~ /language "plpgsql" does not exist/
+ execute "CREATE LANGUAGE 'plpgsql';"
+ retry
+ else
+ raise e
+ end
end
-end
begin
execute <<_SQL
@@ -190,7 +212,13 @@ end
data xml
);
_SQL
-rescue #This version of PostgreSQL either has no XML support or is was not compiled with XML support: skipping table
+ rescue #This version of PostgreSQL either has no XML support or is was not compiled with XML support: skipping table
+ end
+
+ # This table is to verify if the :limit option is being ignored for text and binary columns
+ create_table :limitless_fields, force: true do |t|
+ t.binary :binary, limit: 100_000
+ t.text :text, limit: 100_000
end
end
diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb
index 2e4ec96933..88a686d436 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)
@@ -115,6 +117,7 @@ ActiveRecord::Schema.define do
t.integer :engines_count
t.integer :wheels_count
t.column :lock_version, :integer, :null => false, :default => 0
+ t.timestamps
end
create_table :categories, :force => true do |t|
@@ -182,6 +185,7 @@ ActiveRecord::Schema.define do
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|
t.string :name
@@ -218,6 +222,8 @@ ActiveRecord::Schema.define do
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|
@@ -227,14 +233,16 @@ ActiveRecord::Schema.define do
t.integer :access_level, :default => 1
end
- create_table :dog_lovers, :force => true do |t|
- t.integer :trained_dogs_count, :default => 0
- t.integer :bred_dogs_count, :default => 0
+ create_table :dog_lovers, force: true do |t|
+ t.integer :trained_dogs_count, default: 0
+ t.integer :bred_dogs_count, default: 0
+ t.integer :dogs_count, default: 0
end
create_table :dogs, :force => true do |t|
t.integer :trainer_id
t.integer :breeder_id
+ t.integer :dog_lover_id
end
create_table :edges, :force => true, :id => false do |t|
@@ -277,7 +285,7 @@ ActiveRecord::Schema.define do
create_table :friendships, :force => true do |t|
t.integer :friend_id
- t.integer :person_id
+ t.integer :follower_id
end
create_table :goofy_string_id, :force => true, :id => false do |t|
@@ -491,8 +499,10 @@ ActiveRecord::Schema.define do
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.references :best_friend
t.references :best_friend_of
+ t.integer :insures, null: false, default: 0
t.timestamps
end
@@ -565,6 +575,7 @@ ActiveRecord::Schema.define do
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|
@@ -648,12 +659,6 @@ ActiveRecord::Schema.define do
t.datetime :ending
end
- create_table :teapots, :force => true do |t|
- t.string :name
- t.string :type
- t.timestamps
- end
-
create_table :topics, :force => true do |t|
t.string :title
t.string :author_name
@@ -672,6 +677,7 @@ ActiveRecord::Schema.define do
end
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
@@ -688,6 +694,7 @@ ActiveRecord::Schema.define do
create_table :traffic_lights, :force => true do |t|
t.string :location
t.string :state
+ t.text :long_state, :null => false
t.datetime :created_at
t.datetime :updated_at
end
@@ -776,7 +783,25 @@ ActiveRecord::Schema.define do
end
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
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 92736e0ca9..196b3a9493 100644
--- a/activerecord/test/support/connection.rb
+++ b/activerecord/test/support/connection.rb
@@ -13,9 +13,9 @@ module ARTest
def self.connect
puts "Using #{connection_name}"
- ActiveRecord::Model.logger = ActiveSupport::Logger.new("debug.log")
- ActiveRecord::Model.configurations = connection_config
- ActiveRecord::Model.establish_connection 'arunit'
+ 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'
end
end
diff --git a/activerecord/test/support/mysql.rb b/activerecord/test/support/mysql.rb
deleted file mode 100644
index 7a66415e64..0000000000
--- a/activerecord/test/support/mysql.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-if defined?(Mysql)
- class Mysql
- class Error
- # This monkey patch fixes annoy warning with mysql-2.8.1.gem when executing testcases.
- def errno_with_fix_warnings
- silence_warnings { errno_without_fix_warnings }
- end
- alias_method_chain :errno, :fix_warnings
- end
- end
-end