aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord
diff options
context:
space:
mode:
Diffstat (limited to 'activerecord')
-rw-r--r--activerecord/CHANGELOG.md648
-rw-r--r--activerecord/README.rdoc23
-rw-r--r--activerecord/Rakefile191
-rw-r--r--activerecord/activerecord.gemspec2
-rw-r--r--activerecord/lib/active_record.rb3
-rw-r--r--activerecord/lib/active_record/aggregations.rb20
-rw-r--r--activerecord/lib/active_record/associations.rb49
-rw-r--r--activerecord/lib/active_record/associations/alias_tracker.rb12
-rw-r--r--activerecord/lib/active_record/associations/association.rb4
-rw-r--r--activerecord/lib/active_record/associations/association_scope.rb24
-rw-r--r--activerecord/lib/active_record/associations/belongs_to_association.rb2
-rw-r--r--activerecord/lib/active_record/associations/builder/association.rb19
-rw-r--r--activerecord/lib/active_record/associations/builder/belongs_to.rb7
-rw-r--r--activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb11
-rw-r--r--activerecord/lib/active_record/associations/builder/has_one.rb4
-rw-r--r--activerecord/lib/active_record/associations/builder/singular_association.rb9
-rw-r--r--activerecord/lib/active_record/associations/collection_association.rb16
-rw-r--r--activerecord/lib/active_record/associations/has_many_association.rb71
-rw-r--r--activerecord/lib/active_record/associations/has_many_through_association.rb70
-rw-r--r--activerecord/lib/active_record/associations/join_dependency.rb9
-rw-r--r--activerecord/lib/active_record/associations/join_dependency/join_association.rb2
-rw-r--r--activerecord/lib/active_record/associations/preloader.rb15
-rw-r--r--activerecord/lib/active_record/associations/preloader/association.rb33
-rw-r--r--activerecord/lib/active_record/associations/through_association.rb21
-rw-r--r--activerecord/lib/active_record/attribute.rb120
-rw-r--r--activerecord/lib/active_record/attribute_assignment.rb28
-rw-r--r--activerecord/lib/active_record/attribute_decorators.rb66
-rw-r--r--activerecord/lib/active_record/attribute_methods.rb131
-rw-r--r--activerecord/lib/active_record/attribute_methods/before_type_cast.rb4
-rw-r--r--activerecord/lib/active_record/attribute_methods/dirty.rb124
-rw-r--r--activerecord/lib/active_record/attribute_methods/primary_key.rb13
-rw-r--r--activerecord/lib/active_record/attribute_methods/read.rb71
-rw-r--r--activerecord/lib/active_record/attribute_methods/serialization.rb157
-rw-r--r--activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb60
-rw-r--r--activerecord/lib/active_record/attribute_methods/write.rb35
-rw-r--r--activerecord/lib/active_record/attribute_set.rb77
-rw-r--r--activerecord/lib/active_record/attribute_set/builder.rb32
-rw-r--r--activerecord/lib/active_record/attributes.rb122
-rw-r--r--activerecord/lib/active_record/autosave_association.rb62
-rw-r--r--activerecord/lib/active_record/base.rb30
-rw-r--r--activerecord/lib/active_record/coders/json.rb13
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb6
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb46
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/quoting.rb114
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb35
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb102
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb42
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb131
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/transaction.rb92
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_adapter.rb100
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb233
-rw-r--r--activerecord/lib/active_record/connection_adapters/column.rb263
-rw-r--r--activerecord/lib/active_record/connection_adapters/connection_specification.rb6
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb15
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql_adapter.rb198
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb42
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/cast.rb168
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/column.rb146
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb7
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid.rb463
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb96
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb52
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/bit_varying.rb13
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb14
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb46
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb11
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb27
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/decimal.rb13
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb17
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/float.rb21
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb59
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/inet.rb13
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/infinity.rb13
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/integer.rb11
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb35
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb23
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb43
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb43
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb76
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb15
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/time.rb11
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb85
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb26
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/vector.rb26
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/xml.rb32
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb161
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb8
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb154
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb109
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/utils.rb66
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb418
-rw-r--r--activerecord/lib/active_record/connection_adapters/schema_cache.rb42
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb89
-rw-r--r--activerecord/lib/active_record/core.rb74
-rw-r--r--activerecord/lib/active_record/counter_cache.rb20
-rw-r--r--activerecord/lib/active_record/enum.rb17
-rw-r--r--activerecord/lib/active_record/errors.rb43
-rw-r--r--activerecord/lib/active_record/explain.rb2
-rw-r--r--activerecord/lib/active_record/fixtures.rb41
-rw-r--r--activerecord/lib/active_record/inheritance.rb41
-rw-r--r--activerecord/lib/active_record/locking/optimistic.rb50
-rw-r--r--activerecord/lib/active_record/log_subscriber.rb2
-rw-r--r--activerecord/lib/active_record/migration.rb39
-rw-r--r--activerecord/lib/active_record/migration/command_recorder.rb19
-rw-r--r--activerecord/lib/active_record/migration/join_table.rb2
-rw-r--r--activerecord/lib/active_record/model_schema.rb87
-rw-r--r--activerecord/lib/active_record/nested_attributes.rb6
-rw-r--r--activerecord/lib/active_record/null_relation.rb20
-rw-r--r--activerecord/lib/active_record/persistence.rb22
-rw-r--r--activerecord/lib/active_record/query_cache.rb5
-rw-r--r--activerecord/lib/active_record/querying.rb9
-rw-r--r--activerecord/lib/active_record/railties/databases.rake75
-rw-r--r--activerecord/lib/active_record/reflection.rb309
-rw-r--r--activerecord/lib/active_record/relation.rb30
-rw-r--r--activerecord/lib/active_record/relation/calculations.rb59
-rw-r--r--activerecord/lib/active_record/relation/delegation.rb2
-rw-r--r--activerecord/lib/active_record/relation/finder_methods.rb13
-rw-r--r--activerecord/lib/active_record/relation/merger.rb2
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder.rb4
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder/array_handler.rb27
-rw-r--r--activerecord/lib/active_record/relation/query_methods.rb13
-rw-r--r--activerecord/lib/active_record/result.rb25
-rw-r--r--activerecord/lib/active_record/sanitization.rb2
-rw-r--r--activerecord/lib/active_record/schema_dumper.rb63
-rw-r--r--activerecord/lib/active_record/schema_migration.rb11
-rw-r--r--activerecord/lib/active_record/serializers/xml_serializer.rb10
-rw-r--r--activerecord/lib/active_record/store.rb29
-rw-r--r--activerecord/lib/active_record/tasks/database_tasks.rb23
-rw-r--r--activerecord/lib/active_record/tasks/mysql_database_tasks.rb4
-rw-r--r--activerecord/lib/active_record/timestamp.rb8
-rw-r--r--activerecord/lib/active_record/transactions.rb28
-rw-r--r--activerecord/lib/active_record/type.rb20
-rw-r--r--activerecord/lib/active_record/type/binary.rb40
-rw-r--r--activerecord/lib/active_record/type/boolean.rb19
-rw-r--r--activerecord/lib/active_record/type/date.rb46
-rw-r--r--activerecord/lib/active_record/type/date_time.rb43
-rw-r--r--activerecord/lib/active_record/type/decimal.rb27
-rw-r--r--activerecord/lib/active_record/type/decimal_without_scale.rb11
-rw-r--r--activerecord/lib/active_record/type/float.rb19
-rw-r--r--activerecord/lib/active_record/type/hash_lookup_type_map.rb19
-rw-r--r--activerecord/lib/active_record/type/integer.rb23
-rw-r--r--activerecord/lib/active_record/type/mutable.rb16
-rw-r--r--activerecord/lib/active_record/type/numeric.rb36
-rw-r--r--activerecord/lib/active_record/type/serialized.rb51
-rw-r--r--activerecord/lib/active_record/type/string.rb36
-rw-r--r--activerecord/lib/active_record/type/text.rb11
-rw-r--r--activerecord/lib/active_record/type/time.rb26
-rw-r--r--activerecord/lib/active_record/type/time_value.rb38
-rw-r--r--activerecord/lib/active_record/type/type_map.rb48
-rw-r--r--activerecord/lib/active_record/type/value.rb94
-rw-r--r--activerecord/lib/active_record/validations.rb18
-rw-r--r--activerecord/lib/active_record/validations/presence.rb2
-rw-r--r--activerecord/lib/active_record/validations/uniqueness.rb19
-rw-r--r--activerecord/test/active_record/connection_adapters/fake_adapter.rb1
-rw-r--r--activerecord/test/cases/adapter_test.rb24
-rw-r--r--activerecord/test/cases/adapters/mysql/connection_test.rb15
-rw-r--r--activerecord/test/cases/adapters/mysql/consistency_test.rb48
-rw-r--r--activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb2
-rw-r--r--activerecord/test/cases/adapters/mysql/quoting_test.rb4
-rw-r--r--activerecord/test/cases/adapters/mysql/schema_test.rb38
-rw-r--r--activerecord/test/cases/adapters/mysql2/boolean_test.rb4
-rw-r--r--activerecord/test/cases/adapters/mysql2/connection_test.rb15
-rw-r--r--activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb4
-rw-r--r--activerecord/test/cases/adapters/postgresql/array_test.rb155
-rw-r--r--activerecord/test/cases/adapters/postgresql/bit_string_test.rb76
-rw-r--r--activerecord/test/cases/adapters/postgresql/bytea_test.rb11
-rw-r--r--activerecord/test/cases/adapters/postgresql/citext_test.rb6
-rw-r--r--activerecord/test/cases/adapters/postgresql/composite_test.rb98
-rw-r--r--activerecord/test/cases/adapters/postgresql/connection_test.rb2
-rw-r--r--activerecord/test/cases/adapters/postgresql/datatype_test.rb239
-rw-r--r--activerecord/test/cases/adapters/postgresql/domain_test.rb3
-rw-r--r--activerecord/test/cases/adapters/postgresql/enum_test.rb11
-rw-r--r--activerecord/test/cases/adapters/postgresql/extension_migration_test.rb63
-rw-r--r--activerecord/test/cases/adapters/postgresql/full_text_test.rb26
-rw-r--r--activerecord/test/cases/adapters/postgresql/geometric_test.rb72
-rw-r--r--activerecord/test/cases/adapters/postgresql/hstore_test.rb105
-rw-r--r--activerecord/test/cases/adapters/postgresql/infinity_test.rb44
-rw-r--r--activerecord/test/cases/adapters/postgresql/json_test.rb92
-rw-r--r--activerecord/test/cases/adapters/postgresql/ltree_test.rb3
-rw-r--r--activerecord/test/cases/adapters/postgresql/money_test.rb96
-rw-r--r--activerecord/test/cases/adapters/postgresql/network_test.rb71
-rw-r--r--activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb38
-rw-r--r--activerecord/test/cases/adapters/postgresql/quoting_test.rb19
-rw-r--r--activerecord/test/cases/adapters/postgresql/range_test.rb21
-rw-r--r--activerecord/test/cases/adapters/postgresql/schema_test.rb39
-rw-r--r--activerecord/test/cases/adapters/postgresql/timestamp_test.rb75
-rw-r--r--activerecord/test/cases/adapters/postgresql/type_lookup_test.rb15
-rw-r--r--activerecord/test/cases/adapters/postgresql/utils_test.rb47
-rw-r--r--activerecord/test/cases/adapters/postgresql/uuid_test.rb88
-rw-r--r--activerecord/test/cases/adapters/postgresql/xml_test.rb20
-rw-r--r--activerecord/test/cases/adapters/sqlite3/copy_table_test.rb1
-rw-r--r--activerecord/test/cases/adapters/sqlite3/quoting_test.rb25
-rw-r--r--activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb26
-rw-r--r--activerecord/test/cases/ar_schema_test.rb24
-rw-r--r--activerecord/test/cases/associations/belongs_to_associations_test.rb35
-rw-r--r--activerecord/test/cases/associations/callbacks_test.rb2
-rw-r--r--activerecord/test/cases/associations/cascaded_eager_loading_test.rb4
-rw-r--r--activerecord/test/cases/associations/deprecated_counter_cache_on_has_many_through_test.rb26
-rw-r--r--activerecord/test/cases/associations/eager_test.rb100
-rw-r--r--activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb61
-rw-r--r--activerecord/test/cases/associations/has_many_associations_test.rb74
-rw-r--r--activerecord/test/cases/associations/has_many_through_associations_test.rb50
-rw-r--r--activerecord/test/cases/associations/has_one_through_associations_test.rb14
-rw-r--r--activerecord/test/cases/associations/inner_join_association_test.rb10
-rw-r--r--activerecord/test/cases/associations/inverse_associations_test.rb13
-rw-r--r--activerecord/test/cases/associations/join_model_test.rb25
-rw-r--r--activerecord/test/cases/associations/nested_through_associations_test.rb3
-rw-r--r--activerecord/test/cases/associations/required_test.rb82
-rw-r--r--activerecord/test/cases/attribute_decorators_test.rb124
-rw-r--r--activerecord/test/cases/attribute_methods/read_test.rb1
-rw-r--r--activerecord/test/cases/attribute_methods/serialization_test.rb29
-rw-r--r--activerecord/test/cases/attribute_methods_test.rb110
-rw-r--r--activerecord/test/cases/attribute_set_test.rb165
-rw-r--r--activerecord/test/cases/attribute_test.rb142
-rw-r--r--activerecord/test/cases/attributes_test.rb111
-rw-r--r--activerecord/test/cases/autosave_association_test.rb13
-rw-r--r--activerecord/test/cases/base_test.rb91
-rw-r--r--activerecord/test/cases/binary_test.rb6
-rw-r--r--activerecord/test/cases/bind_parameter_test.rb8
-rw-r--r--activerecord/test/cases/calculations_test.rb16
-rw-r--r--activerecord/test/cases/column_definition_test.rb58
-rw-r--r--activerecord/test/cases/column_test.rb123
-rw-r--r--activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb12
-rw-r--r--activerecord/test/cases/connection_adapters/mysql_type_lookup_test.rb61
-rw-r--r--activerecord/test/cases/connection_adapters/schema_cache_test.rb4
-rw-r--r--activerecord/test/cases/connection_adapters/type_lookup_test.rb101
-rw-r--r--activerecord/test/cases/core_test.rb68
-rw-r--r--activerecord/test/cases/counter_cache_test.rb26
-rw-r--r--activerecord/test/cases/defaults_test.rb9
-rw-r--r--activerecord/test/cases/dirty_test.rb61
-rw-r--r--activerecord/test/cases/dup_test.rb21
-rw-r--r--activerecord/test/cases/finder_test.rb15
-rw-r--r--activerecord/test/cases/fixtures_test.rb6
-rw-r--r--activerecord/test/cases/helper.rb3
-rw-r--r--activerecord/test/cases/inheritance_test.rb8
-rw-r--r--activerecord/test/cases/locking_test.rb11
-rw-r--r--activerecord/test/cases/log_subscriber_test.rb7
-rw-r--r--activerecord/test/cases/migration/change_schema_test.rb45
-rw-r--r--activerecord/test/cases/migration/change_table_test.rb14
-rw-r--r--activerecord/test/cases/migration/column_attributes_test.rb154
-rw-r--r--activerecord/test/cases/migration/columns_test.rb27
-rw-r--r--activerecord/test/cases/migration/command_recorder_test.rb42
-rw-r--r--activerecord/test/cases/migration/create_join_table_test.rb24
-rw-r--r--activerecord/test/cases/migration/foreign_key_test.rb242
-rw-r--r--activerecord/test/cases/migration/index_test.rb94
-rw-r--r--activerecord/test/cases/migration/logger_test.rb2
-rw-r--r--activerecord/test/cases/migration/pending_migrations_test.rb53
-rw-r--r--activerecord/test/cases/migration/references_statements_test.rb5
-rw-r--r--activerecord/test/cases/migration/rename_table_test.rb20
-rw-r--r--activerecord/test/cases/migration_test.rb63
-rw-r--r--activerecord/test/cases/modules_test.rb28
-rw-r--r--activerecord/test/cases/multiparameter_attributes_test.rb4
-rw-r--r--activerecord/test/cases/persistence_test.rb91
-rw-r--r--activerecord/test/cases/pooled_connections_test.rb2
-rw-r--r--activerecord/test/cases/primary_keys_test.rb73
-rw-r--r--activerecord/test/cases/quoting_test.rb46
-rw-r--r--activerecord/test/cases/reflection_test.rb109
-rw-r--r--activerecord/test/cases/relation/delegation_test.rb2
-rw-r--r--activerecord/test/cases/relation/merging_test.rb10
-rw-r--r--activerecord/test/cases/relation/where_chain_test.rb2
-rw-r--r--activerecord/test/cases/relation/where_test.rb33
-rw-r--r--activerecord/test/cases/relation_test.rb28
-rw-r--r--activerecord/test/cases/relations_test.rb61
-rw-r--r--activerecord/test/cases/result_test.rb44
-rw-r--r--activerecord/test/cases/schema_dumper_test.rb61
-rw-r--r--activerecord/test/cases/serialization_test.rb27
-rw-r--r--activerecord/test/cases/serialized_attribute_test.rb147
-rw-r--r--activerecord/test/cases/store_test.rb31
-rw-r--r--activerecord/test/cases/tasks/database_tasks_test.rb36
-rw-r--r--activerecord/test/cases/tasks/mysql_rake_test.rb6
-rw-r--r--activerecord/test/cases/tasks/postgresql_rake_test.rb2
-rw-r--r--activerecord/test/cases/tasks/sqlite_rake_test.rb2
-rw-r--r--activerecord/test/cases/test_case.rb25
-rw-r--r--activerecord/test/cases/timestamp_test.rb31
-rw-r--r--activerecord/test/cases/transactions_test.rb33
-rw-r--r--activerecord/test/cases/type/decimal_test.rb33
-rw-r--r--activerecord/test/cases/type/string_test.rb36
-rw-r--r--activerecord/test/cases/type/type_map_test.rb130
-rw-r--r--activerecord/test/cases/types_test.rb163
-rw-r--r--activerecord/test/cases/validations/presence_validation_test.rb5
-rw-r--r--activerecord/test/cases/validations_test.rb32
-rw-r--r--activerecord/test/cases/xml_serialization_test.rb17
-rw-r--r--activerecord/test/cases/yaml_serialization_test.rb52
-rw-r--r--activerecord/test/config.example.yml30
-rw-r--r--activerecord/test/fixtures/books.yml2
-rw-r--r--activerecord/test/fixtures/fk_test_has_pk.yml2
-rw-r--r--activerecord/test/fixtures/posts.yml2
-rw-r--r--activerecord/test/fixtures/topics.yml10
-rw-r--r--activerecord/test/models/author.rb2
-rw-r--r--activerecord/test/models/club.rb7
-rw-r--r--activerecord/test/models/comment.rb15
-rw-r--r--activerecord/test/models/company_in_module.rb18
-rw-r--r--activerecord/test/models/contact.rb1
-rw-r--r--activerecord/test/models/developer.rb8
-rw-r--r--activerecord/test/models/face.rb2
-rw-r--r--activerecord/test/models/man.rb1
-rw-r--r--activerecord/test/models/owner.rb12
-rw-r--r--activerecord/test/models/pirate.rb6
-rw-r--r--activerecord/test/models/post.rb15
-rw-r--r--activerecord/test/models/publisher.rb2
-rw-r--r--activerecord/test/models/publisher/article.rb4
-rw-r--r--activerecord/test/models/publisher/magazine.rb3
-rw-r--r--activerecord/test/models/ship.rb6
-rw-r--r--activerecord/test/models/tagging.rb2
-rw-r--r--activerecord/test/schema/postgresql_specific_schema.rb17
-rw-r--r--activerecord/test/schema/schema.rb48
-rw-r--r--activerecord/test/schema/sqlite_specific_schema.rb6
-rw-r--r--activerecord/test/support/ddl_helper.rb4
-rw-r--r--activerecord/test/support/schema_dumping_helper.rb11
309 files changed, 9979 insertions, 4504 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md
index 7568773aad..b679d64472 100644
--- a/activerecord/CHANGELOG.md
+++ b/activerecord/CHANGELOG.md
@@ -1,4 +1,621 @@
-* Give ActiveRecord::PredicateBuilder private methods the privacy they deserve
+* No verbose backtrace by db:drop when database does not exist.
+
+ Fixes #16295.
+
+ *Kenn Ejima*
+
+* Add support for Postgresql JSONB.
+
+ Example:
+
+ create_table :posts do |t|
+ t.jsonb :meta_data
+ end
+
+ *Philippe Creux*, *Chris Teague*
+
+* `db:purge` with MySQL respects `Rails.env`.
+
+ *Yves Senn*
+
+* `change_column_default :table, :column, nil` with PostgreSQL will issue a
+ `DROP DEFAULT` instead of a `DEFAULT NULL` query.
+
+ Fixes #16261.
+
+ *Matthew Draper*, *Yves Senn*
+
+* Allow to specify a type for the foreign key column in `references`
+ and `add_reference`.
+
+ Example:
+
+ change_table :vehicle do |t|
+ t.references :station, type: :uuid
+ end
+
+ *Andrey Novikov*, *Łukasz Sarnacki*
+
+* `create_join_table` removes a common prefix when generating the join table.
+ This matches the existing behavior of HABTM associations.
+
+ Fixes #13683.
+
+ *Stefan Kanev*
+
+* Dont swallow errors on compute_type when having a bad alias_method on
+ a class.
+
+ *arthurnn*
+
+* PostgreSQL invalid `uuid` are convert to nil.
+
+ *Abdelkader Boudih*
+
+* Restore 4.0 behavior for using serialize attributes with `JSON` as coder.
+
+ With 4.1.x, `serialize` started returning a string when `JSON` was passed as
+ the second attribute. It will now return a hash as per previous versions.
+
+ Example:
+
+ class Post < ActiveRecord::Base
+ serialize :comment, JSON
+ end
+
+ class Comment
+ include ActiveModel::Model
+ attr_accessor :category, :text
+ end
+
+ post = Post.create!
+ post.comment = Comment.new(category: "Animals", text: "This is a comment about squirrels.")
+ post.save!
+
+ # 4.0
+ post.comment # => {"category"=>"Animals", "text"=>"This is a comment about squirrels."}
+
+ # 4.1 before
+ post.comment # => "#<Comment:0x007f80ab48ff98>"
+
+ # 4.1 after
+ post.comment # => {"category"=>"Animals", "text"=>"This is a comment about squirrels."}
+
+ When using `JSON` as the coder in `serialize`, Active Record will use the
+ new `ActiveRecord::Coders::JSON` coder which delegates its `dump/load` to
+ `ActiveSupport::JSON.encode/decode`. This ensures special objects are dumped
+ correctly using the `#as_json` hook.
+
+ To keep the previous behaviour, supply a custom coder instead
+ ([example](https://gist.github.com/jenncoop/8c4142bbe59da77daa63)).
+
+ Fixes #15594.
+
+ *Jenn Cooper*
+
+* Do not use `RENAME INDEX` syntax for MariaDB 10.0.
+
+ Fixes #15931.
+
+ *Jeff Browning*
+
+* Calling `#empty?` on a `has_many` association would use the value from the
+ counter cache if one exists.
+
+ *David Verhasselt*
+
+* Fix the schema dump generated for tables without constraints and with
+ primary key with default value of custom PostgreSQL function result.
+
+ Fixes #16111.
+
+ *Andrey Novikov*
+
+* Fix the SQL generated when a `delete_all` is run on an association to not
+ produce an `IN` statements.
+
+ Before:
+
+ UPDATE "categorizations" SET "category_id" = NULL WHERE
+ "categorizations"."category_id" = 1 AND "categorizations"."id" IN (1, 2)
+
+ After:
+
+ UPDATE "categorizations" SET "category_id" = NULL WHERE
+ "categorizations"."category_id" = 1
+
+ *Eileen M. Uchitelle, Aaron Patterson*
+
+* Avoid type casting boolean and ActiveSupport::Duration values to numeric
+ values for string columns. Otherwise, in some database, the string column
+ values will be coerced to a numeric allowing false or 0.seconds match any
+ string starting with a non-digit.
+
+ Example:
+
+ App.where(apikey: false) # => SELECT * FROM users WHERE apikey = '0'
+
+ *Dylan Thacker-Smith*
+
+* Add a `:required` option to singular associations, providing a nicer
+ API for presence validations on associations.
+
+ *Sean Griffin*
+
+* Fixed error in `reset_counters` when associations have `select` scope.
+ (Call to `count` generates invalid SQL.)
+
+ *Cade Truitt*
+
+* After a successful `reload`, `new_record?` is always false.
+
+ Fixes #12101.
+
+ *Matthew Draper*
+
+* PostgreSQL renaming table doesn't attempt to rename non existent sequences.
+
+ *Abdelkader Boudih*
+
+* Move 'dependent: :destroy' handling for 'belongs_to'
+ from 'before_destroy' to 'after_destroy' callback chain
+
+ Fixes #12380.
+
+ *Ivan Antropov*
+
+* Detect in-place modifications on String attributes.
+
+ Before this change user have to mark the attribute as changed to it be persisted
+ in the database. Now it is not required anymore.
+
+ Before:
+
+ user = User.first
+ user.name << ' Griffin'
+ user.name_will_change!
+ user.save
+ user.reload.name # => "Sean Griffin"
+
+ After:
+
+ user = User.first
+ user.name << ' Griffin'
+ user.save
+ user.reload.name # => "Sean Griffin"
+
+ *Sean Griffin*
+
+* Add `ActiveRecord::Base#validate!` that raises `RecordInvalid` if the record
+ is invalid.
+
+ *Bogdan Gusiev*, *Marc Schütz*
+
+* Support for adding and removing foreign keys. Foreign keys are now
+ a part of `schema.rb`. This is supported by Mysql2Adapter, MysqlAdapter
+ and PostgreSQLAdapter.
+
+ Many thanks to *Matthew Higgins* for laying the foundation with his work on
+ [foreigner](https://github.com/matthuhiggins/foreigner).
+
+ Example:
+
+ # within your migrations:
+ add_foreign_key :articles, :authors
+ remove_foreign_key :articles, :authors
+
+ *Yves Senn*
+
+* Fix subtle bugs regarding attribute assignment on models with no primary
+ key. `'id'` will no longer be part of the attributes hash.
+
+ *Sean Griffin*
+
+* Deprecate automatic counter caches on `has_many :through`. The behavior was
+ broken and inconsistent.
+
+ *Sean Griffin*
+
+* `preload` preserves readonly flag for associations.
+
+ See #15853.
+
+ *Yves Senn*
+
+* Assume numeric types have changed if they were assigned to a value that
+ would fail numericality validation, regardless of the old value. Previously
+ this would only occur if the old value was 0.
+
+ Example:
+
+ model = Model.create!(number: 5)
+ model.number = '5wibble'
+ model.number_changed? # => true
+
+ Fixes #14731.
+
+ *Sean Griffin*
+
+* `reload` no longer merges with the existing attributes.
+ The attribute hash is fully replaced. The record is put into the same state
+ as it would be with `Model.find(model.id)`.
+
+ *Sean Griffin*
+
+* The object returned from `select_all` must respond to `column_types`.
+ If this is not the case a `NoMethodError` is raised.
+
+ *Sean Griffin*
+
+* `has_many :through` associations will no longer save the through record
+ twice when added in an `after_create` callback defined before the
+ associations.
+
+ Fixes #3798.
+
+ *Sean Griffin*
+
+* Detect in-place modifications of PG array types
+
+ *Sean Griffin*
+
+* Add `bin/rake db:purge` task to empty the current database.
+
+ *Yves Senn*
+
+* Deprecate `serialized_attributes` without replacement.
+
+ *Sean Griffin*
+
+* Correctly extract IPv6 addresses from `DATABASE_URI`: the square brackets
+ are part of the URI structure, not the actual host.
+
+ Fixes #15705.
+
+ *Andy Bakun*, *Aaron Stone*
+
+* Ensure both parent IDs are set on join records when both sides of a
+ through association are new.
+
+ *Sean Griffin*
+
+* `ActiveRecord::Dirty` now detects in-place changes to mutable values.
+ Serialized attributes on ActiveRecord models will no longer save when
+ unchanged.
+
+ Fixes #8328.
+
+ *Sean Griffin*
+
+* Pluck now works when selecting columns from different tables with the same
+ name.
+
+ Fixes #15649.
+
+ *Sean Griffin*
+
+* Remove `cache_attributes` and friends. All attributes are cached.
+
+ *Sean Griffin*
+
+* Remove deprecated method `ActiveRecord::Base.quoted_locking_column`.
+
+ *Akshay Vishnoi*
+
+* `ActiveRecord::FinderMethods.find` with block can handle proc parameter as
+ `Enumerable#find` does.
+
+ Fixes #15382.
+
+ *James Yang*
+
+* Make timezone aware attributes work with PostgreSQL array columns.
+
+ Fixes #13402.
+
+ *Kuldeep Aggarwal*, *Sean Griffin*
+
+* `ActiveRecord::SchemaMigration` has no primary key regardless of the
+ `primary_key_prefix_type` configuration.
+
+ Fixes #15051.
+
+ *JoseLuis Torres*, *Yves Senn*
+
+* `rake db:migrate:status` works with legacy migration numbers like `00018_xyz.rb`.
+
+ Fixes #15538.
+
+ *Yves Senn*
+
+* Baseclass becomes! subclass.
+
+ Before this change, a record which changed its STI type, could not be
+ updated.
+
+ Fixes #14785.
+
+ *Matthew Draper*, *Earl St Sauver*, *Edo Balvers*
+
+* Remove deprecated `ActiveRecord::Migrator.proper_table_name`. Use the
+ `proper_table_name` instance method on `ActiveRecord::Migration` instead.
+
+ *Akshay Vishnoi*
+
+* Fix regression on eager loading association based on SQL query rather than
+ existing column.
+
+ Fixes #15480.
+
+ *Lauro Caetano*, *Carlos Antonio da Silva*
+
+* Deprecate returning `nil` from `column_for_attribute` when no column exists.
+ It will return a null object in Rails 5.0
+
+ *Sean Griffin*
+
+* Implemented ActiveRecord::Base#pretty_print to work with PP.
+
+ *Ethan*
+
+* Preserve type when dumping PostgreSQL point, bit, bit varying and money
+ columns.
+
+ *Yves Senn*
+
+* New records remain new after YAML serialization.
+
+ *Sean Griffin*
+
+* PostgreSQL support default values for enum types. Fixes #7814.
+
+ *Yves Senn*
+
+* PostgreSQL `default_sequence_name` respects schema. Fixes #7516.
+
+ *Yves Senn*
+
+* Fixed `columns_for_distinct` of postgresql adapter to work correctly
+ with orders without sort direction modifiers.
+
+ *Nikolay Kondratyev*
+
+* PostgreSQL `reset_pk_sequence!` respects schemas. Fixes #14719.
+
+ *Yves Senn*
+
+* Keep PostgreSQL `hstore` and `json` attributes as `Hash` in `@attributes`.
+ Fixes duplication in combination with `store_accessor`.
+
+ Fixes #15369.
+
+ *Yves Senn*
+
+* `rake railties:install:migrations` respects the order of railties.
+
+ *Arun Agrawal*
+
+* Fix redefine a has_and_belongs_to_many inside inherited class
+ Fixing regression case, where redefining the same has_an_belongs_to_many
+ definition into a subclass would raise.
+
+ Fixes #14983.
+
+ *arthurnn*
+
+* Fix has_and_belongs_to_many public reflection.
+ When defining a has_and_belongs_to_many, internally we convert that to two has_many.
+ But as `reflections` is a public API, people expect to see the right macro.
+
+ Fixes #14682.
+
+ *arthurnn*
+
+* Fixed serialization for records with an attribute named `format`.
+
+ Fixes #15188.
+
+ *Godfrey Chan*
+
+* When a `group` is set, `sum`, `size`, `average`, `minimum` and `maximum`
+ on a NullRelation should return a Hash.
+
+ *Kuldeep Aggarwal*
+
+* Fixed serialized fields returning serialized data after being updated with
+ `update_column`.
+
+ *Simon Hørup Eskildsen*
+
+* Fixed polymorphic eager loading when using a String as foreign key.
+
+ Fixes #14734.
+
+ *Lauro Caetano*
+
+* Change belongs_to touch to be consistent with timestamp updates
+
+ If a model is set up with a belongs_to: touch relationship the parent
+ record will only be touched if the record was modified. This makes it
+ consistent with timestamp updating on the record itself.
+
+ *Brock Trappitt*
+
+* Fixed the inferred table name of a has_and_belongs_to_many auxiliar
+ table inside a schema.
+
+ Fixes #14824.
+
+ *Eric Chahin*
+
+* Remove unused `:timestamp` type. Transparently alias it to `:datetime`
+ in all cases. Fixes inconsistencies when column types are sent outside of
+ `ActiveRecord`, such as for XML Serialization.
+
+ *Sean Griffin*
+
+* Fix bug that added `table_name_prefix` and `table_name_suffix` to
+ extension names in PostgreSQL when migrating.
+
+ *Joao Carlos*
+
+* The `:index` option in migrations, which previously was only available for
+ `references`, now works with any column types.
+
+ *Marc Schütz*
+
+* Add support for counter name to be passed as parameter on `CounterCache::ClassMethods#reset_counters`.
+
+ *jnormore*
+
+* Restrict deletion of record when using `delete_all` with `uniq`, `group`, `having`
+ or `offset`.
+
+ In these cases the generated query ignored them and that caused unintended
+ records to be deleted.
+
+ Fixes #11985.
+
+ *Leandro Facchinetti*
+
+* Floats with limit >= 25 that get turned into doubles in MySQL no longer have
+ their limit dropped from the schema.
+
+ Fixes #14135.
+
+ *Aaron Nelson*
+
+* Fix how to calculate associated class name when using namespaced has_and_belongs_to_many
+ association.
+
+ Fixes #14709.
+
+ *Kassio Borges*
+
+* `ActiveRecord::Relation::Merger#filter_binds` now compares equivalent symbols and
+ strings in column names as equal.
+
+ This fixes a rare case in which more bind values are passed than there are
+ placeholders for them in the generated SQL statement, which can make PostgreSQL
+ throw a `StatementInvalid` exception.
+
+ *Nat Budin*
+
+* Fix `stored_attributes` to correctly merge the details of stored
+ attributes defined in parent classes.
+
+ Fixes #14672.
+
+ *Brad Bennett*, *Jessica Yao*, *Lakshmi Parthasarathy*
+
+* `change_column_default` allows `[]` as argument to `change_column_default`.
+
+ Fixes #11586.
+
+ *Yves Senn*
+
+* Handle `name` and `"char"` column types in the PostgreSQL adapter.
+
+ `name` and `"char"` are special character types used internally by
+ PostgreSQL and are used by internal system catalogs. These field types
+ can sometimes show up in structure-sniffing queries that feature internal system
+ structures or with certain PostgreSQL extensions.
+
+ *J Smith*, *Yves Senn*
+
+* Fix `PostgreSQLAdapter::OID::Float#type_cast` to convert Infinity and
+ NaN PostgreSQL values into a native Ruby `Float::INFINITY` and `Float::NAN`
+
+ Before:
+
+ Point.create(value: 1.0/0)
+ Point.last.value # => 0.0
+
+ After:
+
+ Point.create(value: 1.0/0)
+ Point.last.value # => Infinity
+
+ *Innokenty Mikhailov*
+
+* Allow the PostgreSQL adapter to handle bigserial primary key types again.
+
+ Fixes #10410.
+
+ *Patrick Robertson*
+
+* Deprecate joining, eager loading and preloading of instance dependent
+ associations without replacement. These operations happen before instances
+ are created. The current behavior is unexpected and can result in broken
+ behavior.
+
+ Fixes #15024.
+
+ *Yves Senn*
+
+* Fixed has_and_belongs_to_many's CollectionAssociation size calculation.
+
+ has_and_belongs_to_many should fall back to using the normal CollectionAssociation's
+ size calculation if the collection is not cached or loaded.
+
+ Fixes #14913, #14914.
+
+ *Fred Wu*
+
+* Return a non zero status when running `rake db:migrate:status` and migration table does
+ not exist.
+
+ *Paul B.*
+
+* Add support for module-level `table_name_suffix` in models.
+
+ This makes `table_name_suffix` work the same way as `table_name_prefix` when
+ using namespaced models.
+
+ *Jenner LaFave*
+
+* Revert the behaviour of `ActiveRecord::Relation#join` changed through 4.0 => 4.1 to 4.0.
+
+ In 4.1.0 `Relation#join` is delegated to `Arel#SelectManager`.
+ In 4.0 series it is delegated to `Array#join`.
+
+ *Bogdan Gusiev*
+
+* Log nil binary column values correctly.
+
+ When an object with a binary column is updated with a nil value
+ in that column, the SQL logger would throw an exception when trying
+ to log that nil value. This only occurs when updating a record
+ that already has a non-nil value in that column since an initial nil
+ value isn't included in the SQL anyway (at least, when dirty checking
+ is enabled.) The column's new value will now be logged as `<NULL binary data>`
+ to parallel the existing `<N bytes of binary data>` for non-nil values.
+
+ *James Coleman*
+
+* Rails will now pass a custom validation context through to autosave associations
+ in order to validate child associations with the same context.
+
+ Fixes #13854.
+
+ *Eric Chahin*, *Aaron Nelson*, *Kevin Casey*
+
+* Stringify all variables keys of MySQL connection configuration.
+
+ When `sql_mode` variable for MySQL adapters set in configuration as `String`
+ was ignored and overwritten by strict mode option.
+
+ Fixes #14895.
+
+ *Paul Nikitochkin*
+
+* Ensure SQLite3 statements are closed on errors.
+
+ Fixes #13631.
+
+ *Timur Alperovich*
+
+* Give ActiveRecord::PredicateBuilder private methods the privacy they deserve.
*Hector Satre*
@@ -66,7 +683,7 @@
*Eric Chahin*
-* `sanitize_sql_like` helper method to escape a string for safe use in a SQL
+* `sanitize_sql_like` helper method to escape a string for safe use in an SQL
LIKE statement.
Example:
@@ -102,7 +719,7 @@
*Lauro Caetano*
* Calling `delete_all` on an unloaded `CollectionProxy` no longer
- generates a SQL statement containing each id of the collection:
+ generates an SQL statement containing each id of the collection:
Before:
@@ -143,7 +760,7 @@
* Auto-generate stable fixture UUIDs on PostgreSQL.
- Fixes: #11524
+ Fixes #11524.
*Roderick van Domburg*
@@ -227,10 +844,6 @@
*Luke Steensen*
-* Make possible to change `record_timestamps` inside Callbacks.
-
- *Tieg Zaharia*
-
* Fixed error where .persisted? throws SystemStackError for an unsaved model with a
custom primary key that didn't save due to validation error.
@@ -261,12 +874,6 @@
*Cody Cutrer*, *Steve Rice*, *Rafael Mendonça Franca*
-* Save `has_one` association even if the record doesn't changed.
-
- Fixes #14407.
-
- *Rafael Mendonça França*
-
* Use singular table name in generated migrations when
`ActiveRecord::Base.pluralize_table_names` is `false`.
@@ -326,12 +933,11 @@
*arthurnn*
-* Passing an Active Record object to `find` is now deprecated. Call `.id`
- on the object first.
-
* Passing an Active Record object to `find` or `exists?` is now deprecated.
Call `.id` on the object first.
+ *Aaron Patterson*
+
* Only use BINARY for MySQL case sensitive uniqueness check when column has a case insensitive collation.
*Ryuta Kamizono*
@@ -345,6 +951,12 @@
*Troy Kruthoff*, *Lachlan Sylvester*
+* Only save has_one associations if record has changes.
+ Previously after save related callbacks, such as `#after_commit`, were triggered when the has_one
+ object did not get saved to the db.
+
+ *Alan Kennedy*
+
* Allow strings to specify the `#order` value.
Example:
@@ -374,7 +986,7 @@
*Vilius Luneckas* *Ahmed AbouElhamayed*
* `before_add` callbacks are fired before the record is saved on
- `has_and_belongs_to_many` assocations *and* on `has_many :through`
+ `has_and_belongs_to_many` associations *and* on `has_many :through`
associations. Before this change, `before_add` callbacks would be fired
before the record was saved on `has_and_belongs_to_many` associations, but
*not* on `has_many :through` associations.
diff --git a/activerecord/README.rdoc b/activerecord/README.rdoc
index e04abe9b37..f4777919d3 100644
--- a/activerecord/README.rdoc
+++ b/activerecord/README.rdoc
@@ -1,4 +1,4 @@
-= Active Record -- Object-relational mapping put on rails
+= Active Record -- Object-relational mapping in Rails
Active Record connects classes to relational database tables to establish an
almost zero-configuration persistence layer for applications. The library
@@ -20,8 +20,10 @@ A short rundown of some of the major features:
class Product < ActiveRecord::Base
end
- The Product class is automatically mapped to the table named "products",
- which might look like this:
+ {Learn more}[link:classes/ActiveRecord/Base.html]
+
+The Product class is automatically mapped to the table named "products",
+which might look like this:
CREATE TABLE products (
id int(11) NOT NULL auto_increment,
@@ -29,10 +31,8 @@ A short rundown of some of the major features:
PRIMARY KEY (id)
);
- This would also define the following accessors: `Product#name` and
- `Product#name=(new_name)`
-
- {Learn more}[link:classes/ActiveRecord/Base.html]
+This would also define the following accessors: `Product#name` and
+`Product#name=(new_name)`.
* Associations between objects defined by simple class methods.
@@ -130,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.rubyforge.org] and Logger[http://www.ruby-doc.org/stdlib/libdoc/logger/rdoc].
+* Logging support for Log4r[https://github.com/colbygk/log4r] 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')
@@ -208,6 +208,11 @@ API documentation is at:
* http://api.rubyonrails.org
-Bug reports and feature requests can be filed with the rest for the Ruby on Rails project here:
+Bug reports can be filed for the Ruby on Rails project here:
* https://github.com/rails/rails/issues
+
+Feature requests should be discussed on the rails-core mailing list here:
+
+* https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-core
+
diff --git a/activerecord/Rakefile b/activerecord/Rakefile
index 6f8948f987..7769966a22 100644
--- a/activerecord/Rakefile
+++ b/activerecord/Rakefile
@@ -38,33 +38,36 @@ namespace :test do
end
end
+desc 'Build MySQL and PostgreSQL test databases'
namespace :db do
- desc 'Build MySQL and PostgreSQL test databases'
- task create: ['mysql:build_databases', 'postgresql:build_databases']
- desc 'Drop MySQL and PostgreSQL test databases'
- task drop: ['mysql:drop_databases', 'postgresql:drop_databases']
+ task :create => ['db:mysql:build', 'db:postgresql:build']
+ task :drop => ['db:mysql:drop', 'db:postgresql:drop']
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]+/]
- t.libs << 'test'
- t.test_files = (Dir.glob( "test/cases/**/*_test.rb" ).reject {
- |x| x =~ /\/adapters\//
- } + Dir.glob("test/cases/adapters/#{adapter_short}/**/*_test.rb")).sort
-
- t.warning = true
- t.verbose = true
- }
-
- task "isolated_test_#{adapter}" do
- adapter_short = adapter == 'db2' ? adapter : adapter[/^[a-z0-9]+/]
- puts [adapter, adapter_short].inspect
- (Dir["test/cases/**/*_test.rb"].reject {
- |x| x =~ /\/adapters\//
- } + Dir["test/cases/adapters/#{adapter_short}/**/*_test.rb"]).all? do |file|
- sh(Gem.ruby, '-w' ,"-Itest", file)
- end or raise "Failures"
+%w( mysql mysql2 postgresql sqlite3 sqlite3_mem db2 oracle jdbcmysql jdbcpostgresql jdbcsqlite3 jdbcderby jdbch2 jdbchsqldb ).each do |adapter|
+ namespace :test do
+ Rake::TestTask.new(adapter => "#{adapter}:env") { |t|
+ adapter_short = adapter == 'db2' ? adapter : adapter[/^[a-z0-9]+/]
+ t.libs << 'test'
+ t.test_files = (Dir.glob( "test/cases/**/*_test.rb" ).reject {
+ |x| x =~ /\/adapters\//
+ } + Dir.glob("test/cases/adapters/#{adapter_short}/**/*_test.rb")).sort
+
+ t.warning = true
+ t.verbose = true
+ }
+
+ namespace :isolated do
+ task adapter => "#{adapter}:env" do
+ adapter_short = adapter == 'db2' ? adapter : adapter[/^[a-z0-9]+/]
+ puts [adapter, adapter_short].inspect
+ (Dir["test/cases/**/*_test.rb"].reject {
+ |x| x =~ /\/adapters\//
+ } + Dir["test/cases/adapters/#{adapter_short}/**/*_test.rb"]).all? do |file|
+ sh(Gem.ruby, '-w' ,"-Itest", file)
+ end or raise "Failures"
+ end
+ end
end
namespace adapter do
@@ -76,8 +79,8 @@ end
end
# Make sure the adapter test evaluates the env setting task
- task "test_#{adapter}" => "#{adapter}:env"
- task "isolated_test_#{adapter}" => "#{adapter}:env"
+ task "test_#{adapter}" => ["#{adapter}:env", "test:#{adapter}"]
+ task "isolated_test_#{adapter}" => ["#{adapter}:env", "test:isolated:#{adapter}"]
end
rule '.sqlite3' do |t|
@@ -89,109 +92,58 @@ task :test_sqlite3 => [
'test/fixtures/fixture_database_2.sqlite3'
]
-namespace :mysql do
- desc 'Build the MySQL test databases'
- task :build_databases do
- config = ARTest.config['connections']['mysql']
- %x( mysql --user=#{config['arunit']['username']} -e "create DATABASE #{config['arunit']['database']} DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_unicode_ci ")
- %x( mysql --user=#{config['arunit2']['username']} -e "create DATABASE #{config['arunit2']['database']} DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_unicode_ci ")
- end
-
- desc 'Drop the MySQL test databases'
- task :drop_databases do
- config = ARTest.config['connections']['mysql']
- %x( mysqladmin --user=#{config['arunit']['username']} -f drop #{config['arunit']['database']} )
- %x( mysqladmin --user=#{config['arunit2']['username']} -f drop #{config['arunit2']['database']} )
- end
-
- desc 'Rebuild the MySQL test databases'
- task :rebuild_databases => [:drop_databases, :build_databases]
-end
-
-task :build_mysql_databases => 'mysql:build_databases'
-task :drop_mysql_databases => 'mysql:drop_databases'
-task :rebuild_mysql_databases => 'mysql:rebuild_databases'
-
-
-namespace :postgresql do
- desc 'Build the PostgreSQL test databases'
- task :build_databases do
- config = ARTest.config['connections']['postgresql']
- %x( createdb -E UTF8 -T template0 #{config['arunit']['database']} )
- %x( createdb -E UTF8 -T template0 #{config['arunit2']['database']} )
+namespace :db do
+ namespace :mysql do
+ desc 'Build the MySQL test databases'
+ task :build do
+ config = ARTest.config['connections']['mysql']
+ %x( mysql --user=#{config['arunit']['username']} -e "create DATABASE #{config['arunit']['database']} DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_unicode_ci ")
+ %x( mysql --user=#{config['arunit2']['username']} -e "create DATABASE #{config['arunit2']['database']} DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_unicode_ci ")
+ 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"
+ desc 'Drop the MySQL test databases'
+ task :drop do
+ config = ARTest.config['connections']['mysql']
+ %x( mysqladmin --user=#{config['arunit']['username']} -f drop #{config['arunit']['database']} )
+ %x( mysqladmin --user=#{config['arunit2']['username']} -f drop #{config['arunit2']['database']} )
end
- end
- desc 'Drop the PostgreSQL test databases'
- task :drop_databases do
- config = ARTest.config['connections']['postgresql']
- %x( dropdb #{config['arunit']['database']} )
- %x( dropdb #{config['arunit2']['database']} )
+ desc 'Rebuild the MySQL test databases'
+ task :rebuild => [:drop, :build]
end
- desc 'Rebuild the PostgreSQL test databases'
- task :rebuild_databases => [:drop_databases, :build_databases]
-end
-
-task :build_postgresql_databases => 'postgresql:build_databases'
-task :drop_postgresql_databases => 'postgresql:drop_databases'
-task :rebuild_postgresql_databases => 'postgresql:rebuild_databases'
-
-
-namespace :frontbase do
- desc 'Build the FrontBase test databases'
- task :build_databases => :rebuild_frontbase_databases
-
- desc 'Rebuild the FrontBase test databases'
- task :rebuild_databases do
- build_frontbase_database = Proc.new do |db_name, sql_definition_file|
- %(
- STOP DATABASE #{db_name};
- DELETE DATABASE #{db_name};
- CREATE DATABASE #{db_name};
+ namespace :postgresql do
+ desc 'Build the PostgreSQL test databases'
+ task :build do
+ config = ARTest.config['connections']['postgresql']
+ %x( createdb -E UTF8 -T template0 #{config['arunit']['database']} )
+ %x( createdb -E UTF8 -T template0 #{config['arunit2']['database']} )
- CONNECT TO #{db_name} AS SESSION_NAME USER _SYSTEM;
- SET COMMIT FALSE;
-
- CREATE USER RAILS;
- CREATE SCHEMA RAILS AUTHORIZATION RAILS;
- COMMIT;
-
- SET SESSION AUTHORIZATION RAILS;
- SCRIPT '#{sql_definition_file}';
-
- COMMIT;
-
- DISCONNECT ALL;
- )
+ # prepare hstore
+ if %x( createdb --version ).strip.gsub(/(.*)(\d\.\d\.\d)$/, "\\2") < "9.1.0"
+ puts "Please prepare hstore data type. See http://www.postgresql.org/docs/9.0/static/hstore.html"
+ end
end
- config = ARTest.config['connections']['frontbase']
- create_activerecord_unittest = build_frontbase_database[config['arunit']['database'], File.join(SCHEMA_ROOT, 'frontbase.sql')]
- create_activerecord_unittest2 = build_frontbase_database[config['arunit2']['database'], File.join(SCHEMA_ROOT, 'frontbase2.sql')]
- execute_frontbase_sql = Proc.new do |sql|
- system(<<-SHELL)
- /Library/FrontBase/bin/sql92 <<-SQL
- #{sql}
- SQL
- SHELL
+
+ desc 'Drop the PostgreSQL test databases'
+ task :drop do
+ config = ARTest.config['connections']['postgresql']
+ %x( dropdb #{config['arunit']['database']} )
+ %x( dropdb #{config['arunit2']['database']} )
end
- execute_frontbase_sql[create_activerecord_unittest]
- execute_frontbase_sql[create_activerecord_unittest2]
+
+ desc 'Rebuild the PostgreSQL test databases'
+ task :rebuild => [:drop, :build]
end
end
-task :build_frontbase_databases => 'frontbase:build_databases'
-task :rebuild_frontbase_databases => 'frontbase:rebuild_databases'
+task :build_mysql_databases => 'db:mysql:build'
+task :drop_mysql_databases => 'db:mysql:drop'
+task :rebuild_mysql_databases => 'db:mysql:rebuild'
-spec = eval(File.read('activerecord.gemspec'))
-
-Gem::PackageTask.new(spec) do |p|
- p.gem_spec = spec
-end
+task :build_postgresql_databases => 'db:postgresql:build'
+task :drop_postgresql_databases => 'db:postgresql:drop'
+task :rebuild_postgresql_databases => 'db:postgresql:rebuild'
task :lines do
lines, codelines, total_lines, total_codelines = 0, 0, 0, 0
@@ -217,6 +169,11 @@ task :lines do
puts "Total: Lines #{total_lines}, LOC #{total_codelines}"
end
+spec = eval(File.read('activerecord.gemspec'))
+
+Gem::PackageTask.new(spec) do |p|
+ p.gem_spec = spec
+end
# Publishing ------------------------------------------------------
diff --git a/activerecord/activerecord.gemspec b/activerecord/activerecord.gemspec
index d397c9e016..8075008574 100644
--- a/activerecord/activerecord.gemspec
+++ b/activerecord/activerecord.gemspec
@@ -24,5 +24,5 @@ Gem::Specification.new do |s|
s.add_dependency 'activesupport', version
s.add_dependency 'activemodel', version
- s.add_dependency 'arel', '~> 5.0.0'
+ s.add_dependency 'arel', '~> 6.0.0'
end
diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb
index f856c482d6..9028970a3d 100644
--- a/activerecord/lib/active_record.rb
+++ b/activerecord/lib/active_record.rb
@@ -27,10 +27,12 @@ require 'active_model'
require 'arel'
require 'active_record/version'
+require 'active_record/attribute_set'
module ActiveRecord
extend ActiveSupport::Autoload
+ autoload :Attribute
autoload :Base
autoload :Callbacks
autoload :Core
@@ -95,6 +97,7 @@ module ActiveRecord
module Coders
autoload :YAMLColumn, 'active_record/coders/yaml_column'
+ autoload :JSON, 'active_record/coders/json'
end
module AttributeMethods
diff --git a/activerecord/lib/active_record/aggregations.rb b/activerecord/lib/active_record/aggregations.rb
index 0d5313956b..e576ec4d40 100644
--- a/activerecord/lib/active_record/aggregations.rb
+++ b/activerecord/lib/active_record/aggregations.rb
@@ -129,10 +129,10 @@ module ActiveRecord
# is an instance of the value class. Specifying a custom converter allows the new value to be automatically
# converted to an instance of value class if necessary.
#
- # For example, the NetworkResource model has +network_address+ and +cidr_range+ attributes that
- # should be aggregated using the NetAddr::CIDR value class (http://netaddr.rubyforge.org). The constructor
- # for the value class is called +create+ and it expects a CIDR address string as a parameter. New
- # values can be assigned to the value object using either another NetAddr::CIDR object, a string
+ # For example, the NetworkResource model has +network_address+ and +cidr_range+ attributes that should be
+ # aggregated using the NetAddr::CIDR value class (http://www.ruby-doc.org/gems/docs/n/netaddr-1.5.0/NetAddr/CIDR.html).
+ # The constructor for the value class is called +create+ and it expects a CIDR address string as a parameter.
+ # New values can be assigned to the value object using either another NetAddr::CIDR object, a string
# or an array. The <tt>:constructor</tt> and <tt>:converter</tt> options can be used to meet
# these requirements:
#
@@ -230,8 +230,8 @@ module ActiveRecord
private
def reader_method(name, class_name, mapping, allow_nil, constructor)
define_method(name) do
- if @aggregation_cache[name].nil? && (!allow_nil || mapping.any? {|pair| !read_attribute(pair.first).nil? })
- attrs = mapping.collect {|pair| read_attribute(pair.first)}
+ if @aggregation_cache[name].nil? && (!allow_nil || mapping.any? {|key, _| !read_attribute(key).nil? })
+ attrs = mapping.collect {|key, _| read_attribute(key)}
object = constructor.respond_to?(:call) ?
constructor.call(*attrs) :
class_name.constantize.send(constructor, *attrs)
@@ -244,15 +244,19 @@ module ActiveRecord
def writer_method(name, class_name, mapping, allow_nil, converter)
define_method("#{name}=") do |part|
klass = class_name.constantize
+ if part.is_a?(Hash)
+ part = klass.new(*part.values)
+ end
+
unless part.is_a?(klass) || converter.nil? || part.nil?
part = converter.respond_to?(:call) ? converter.call(part) : klass.send(converter, part)
end
if part.nil? && allow_nil
- mapping.each { |pair| self[pair.first] = nil }
+ mapping.each { |key, _| self[key] = nil }
@aggregation_cache[name] = nil
else
- mapping.each { |pair| self[pair.first] = part.send(pair.last) }
+ mapping.each { |key, value| self[key] = part.send(value) }
@aggregation_cache[name] = part.freeze
end
end
diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb
index 265bad7bc2..ec78d10124 100644
--- a/activerecord/lib/active_record/associations.rb
+++ b/activerecord/lib/active_record/associations.rb
@@ -50,7 +50,7 @@ module ActiveRecord
def initialize(reflection)
through_reflection = reflection.through_reflection
source_reflection_names = reflection.source_reflection_names
- source_associations = reflection.through_reflection.klass.reflect_on_all_associations.collect { |a| a.name.inspect }
+ source_associations = reflection.through_reflection.klass._reflections.keys
super("Could not find the source association(s) #{source_reflection_names.collect{ |a| a.inspect }.to_sentence(:two_words_connector => ' or ', :last_word_connector => ', or ', :locale => :en)} in model #{through_reflection.klass}. Try 'has_many #{reflection.name.inspect}, :through => #{through_reflection.name.inspect}, :source => <name>'. Is it one of #{source_associations.to_sentence(:two_words_connector => ' or ', :last_word_connector => ', or ', :locale => :en)}?")
end
end
@@ -151,7 +151,7 @@ module ActiveRecord
association = association_instance_get(name)
if association.nil?
- raise AssociationNotFoundError.new(self, name) unless reflection = self.class.reflect_on_association(name)
+ raise AssociationNotFoundError.new(self, name) unless reflection = self.class._reflect_on_association(name)
association = reflection.association_class.new(self, reflection)
association_instance_set(name, association)
end
@@ -202,12 +202,13 @@ module ActiveRecord
# For instance, +attributes+ and +connection+ would be bad choices for association names.
#
# == Auto-generated methods
+ # See also Instance Public methods below for more details.
#
# === Singular associations (one-to-one)
# | | belongs_to |
# generated methods | belongs_to | :polymorphic | has_one
# ----------------------------------+------------+--------------+---------
- # other | X | X | X
+ # other(force_reload=false) | X | X | X
# other=(other) | X | X | X
# build_other(attributes={}) | X | | X
# create_other(attributes={}) | X | | X
@@ -217,7 +218,7 @@ module ActiveRecord
# | | | has_many
# generated methods | habtm | has_many | :through
# ----------------------------------+-------+----------+----------
- # others | X | X | X
+ # others(force_reload=false) | X | X | X
# others=(other,other,...) | X | X | X
# other_ids | X | X | X
# other_ids=(id,id,...) | X | X | X
@@ -419,6 +420,10 @@ module ActiveRecord
# has_many :birthday_events, ->(user) { where starts_on: user.birthday }, class_name: 'Event'
# end
#
+ # Note: Joining, eager loading and preloading of these associations is not fully possible.
+ # These operations happen before instance creation and the scope will be called with a +nil+ argument.
+ # This can lead to unexpected behavior and is deprecated.
+ #
# == Association callbacks
#
# Similar to the normal callbacks that hook into the life cycle of an Active Record object,
@@ -712,9 +717,9 @@ module ActiveRecord
# == Eager loading of associations
#
# Eager loading is a way to find objects of a certain class and a number of named associations.
- # This is one of the easiest ways of to prevent the dreaded 1+N problem in which fetching 100
+ # This is one of the easiest ways of to prevent the dreaded N+1 problem in which fetching 100
# posts that each need to display their author triggers 101 database queries. Through the
- # use of eager loading, the 101 queries can be reduced to 2.
+ # use of eager loading, the number of queries will be reduced from 101 to 2.
#
# class Post < ActiveRecord::Base
# belongs_to :author
@@ -774,16 +779,15 @@ 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.
+ #
# If you want to load all posts (including posts with no approved comments) then write
# your own LEFT OUTER JOIN query using ON
#
- # Post.joins('LEFT OUTER JOIN comments ON comments.post_id = posts.id AND comments.approved = true')
+ # Post.joins("LEFT OUTER JOIN comments ON comments.post_id = posts.id AND comments.approved = '1'")
#
- # 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.
- #
- # 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:
+ # In this case it is usually more natural to include an association which has conditions defined on it:
#
# class Post < ActiveRecord::Base
# has_many :approved_comments, -> { where approved: true }, class_name: 'Comment'
@@ -1305,6 +1309,10 @@ module ActiveRecord
# that is the inverse of this <tt>has_one</tt> association. Does not work in combination
# with <tt>:through</tt> or <tt>:as</tt> options.
# See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail.
+ # [:required]
+ # When set to +true+, the association will also have its presence validated.
+ # This will validate the association itself, not the id. You can use
+ # +:inverse_of+ to avoid an extra query during validation.
#
# Option examples:
# has_one :credit_card, dependent: :destroy # destroys the associated credit card
@@ -1316,6 +1324,7 @@ module ActiveRecord
# has_one :boss, readonly: :true
# has_one :club, through: :membership
# has_one :primary_address, -> { where primary: true }, through: :addressables, source: :addressable
+ # has_one :credit_card, required: true
def has_one(name, scope = nil, options = {})
reflection = Builder::HasOne.build(self, name, scope, options)
Reflection.add_reflection self, name, reflection
@@ -1409,7 +1418,7 @@ module ActiveRecord
#
# Note that <tt>accepts_nested_attributes_for</tt> sets <tt>:autosave</tt> to <tt>true</tt>.
# [:touch]
- # If true, the associated object will be touched (the updated_at/on attributes set to now)
+ # If true, the associated object will be touched (the updated_at/on attributes set to current time)
# when this record is either saved or destroyed. If you specify a symbol, that attribute
# will be updated with the current time in addition to the updated_at/on attribute.
# [:inverse_of]
@@ -1417,6 +1426,10 @@ module ActiveRecord
# object that is the inverse of this <tt>belongs_to</tt> association. Does not work in
# combination with the <tt>:polymorphic</tt> options.
# See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail.
+ # [:required]
+ # When set to +true+, the association will also have its presence validated.
+ # This will validate the association itself, not the id. You can use
+ # +:inverse_of+ to avoid an extra query during validation.
#
# Option examples:
# belongs_to :firm, foreign_key: "client_of"
@@ -1429,6 +1442,7 @@ module ActiveRecord
# belongs_to :post, counter_cache: true
# belongs_to :company, touch: true
# belongs_to :company, touch: :employees_last_updated_at
+ # belongs_to :company, required: true
def belongs_to(name, scope = nil, options = {})
reflection = Builder::BelongsTo.build(self, name, scope, options)
Reflection.add_reflection self, name, reflection
@@ -1573,14 +1587,22 @@ module ActiveRecord
scope = nil
end
+ habtm_reflection = ActiveRecord::Reflection::HasAndBelongsToManyReflection.new(name, scope, options, self)
+
builder = Builder::HasAndBelongsToMany.new name, self, options
join_model = builder.through_model
+ # FIXME: we should move this to the internal constants. Also people
+ # should never directly access this constant so I'm not happy about
+ # setting it.
+ const_set join_model.name, join_model
+
middle_reflection = builder.middle_reflection join_model
Builder::HasMany.define_callbacks self, middle_reflection
Reflection.add_reflection self, middle_reflection.name, middle_reflection
+ middle_reflection.parent_reflection = [name.to_s, habtm_reflection]
include Module.new {
class_eval <<-RUBY, __FILE__, __LINE__ + 1
@@ -1601,6 +1623,7 @@ module ActiveRecord
end
has_many name, scope, hm_options, &extension
+ self._reflections[name.to_s].parent_reflection = [name.to_s, habtm_reflection]
end
end
end
diff --git a/activerecord/lib/active_record/associations/alias_tracker.rb b/activerecord/lib/active_record/associations/alias_tracker.rb
index 85109aee6c..a6a1947148 100644
--- a/activerecord/lib/active_record/associations/alias_tracker.rb
+++ b/activerecord/lib/active_record/associations/alias_tracker.rb
@@ -32,8 +32,18 @@ module ActiveRecord
join.left.downcase.scan(
/join(?:\s+\w+)?\s+(\S+\s+)?#{quoted_name}\son/
).size
- else
+ elsif join.respond_to? :left
join.left.table_name == name ? 1 : 0
+ else
+ # this branch is reached by two tests:
+ #
+ # activerecord/test/cases/associations/cascaded_eager_loading_test.rb:37
+ # with :posts
+ #
+ # activerecord/test/cases/associations/eager_test.rb:1133
+ # with :comments
+ #
+ 0
end
end
diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb
index 9ad2d2fb12..f1c36cd047 100644
--- a/activerecord/lib/active_record/associations/association.rb
+++ b/activerecord/lib/active_record/associations/association.rb
@@ -160,7 +160,7 @@ module ActiveRecord
def marshal_load(data)
reflection_name, ivars = data
ivars.each { |name, val| instance_variable_set(name, val) }
- @reflection = @owner.class.reflect_on_association(reflection_name)
+ @reflection = @owner.class._reflect_on_association(reflection_name)
end
def initialize_attributes(record) #:nodoc:
@@ -179,7 +179,7 @@ module ActiveRecord
def creation_attributes
attributes = {}
- if (reflection.macro == :has_one || reflection.macro == :has_many) && !options[:through]
+ if (reflection.has_one? || reflection.collection?) && !options[:through]
attributes[reflection.foreign_key] = owner[reflection.active_record_primary_key]
if reflection.options[:as]
diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb
index f1a3b23d5a..519d4d8651 100644
--- a/activerecord/lib/active_record/associations/association_scope.rb
+++ b/activerecord/lib/active_record/associations/association_scope.rb
@@ -47,15 +47,8 @@ module ActiveRecord
def self.get_bind_values(owner, chain)
bvs = []
chain.each_with_index do |reflection, i|
- if reflection.source_macro == :belongs_to
- foreign_key = reflection.foreign_key
- else
- foreign_key = reflection.active_record_primary_key
- end
-
if reflection == chain.last
- bvs << owner[foreign_key]
-
+ bvs << reflection.join_id_for(owner)
if reflection.type
bvs << owner.class.base_class.name
end
@@ -112,18 +105,9 @@ module ActiveRecord
chain.each_with_index do |reflection, i|
table, foreign_table = tables.shift, tables.first
- if reflection.source_macro == :belongs_to
- if reflection.options[:polymorphic]
- key = reflection.association_primary_key(assoc_klass)
- else
- key = reflection.association_primary_key
- end
-
- foreign_key = reflection.foreign_key
- else
- key = reflection.foreign_key
- foreign_key = reflection.active_record_primary_key
- end
+ join_keys = reflection.join_keys(assoc_klass)
+ key = join_keys.key
+ foreign_key = join_keys.foreign_key
if reflection == chain.last
bind_val = bind scope, table.table_name, key.to_s, owner[foreign_key], tracker
diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb
index 1edd4fa3aa..81fdd681de 100644
--- a/activerecord/lib/active_record/associations/belongs_to_association.rb
+++ b/activerecord/lib/active_record/associations/belongs_to_association.rb
@@ -92,7 +92,7 @@ module ActiveRecord
# has_one associations.
def invertible_for?(record)
inverse = inverse_reflection_for(record)
- inverse && inverse.macro == :has_one
+ inverse && inverse.has_one?
end
def target_id
diff --git a/activerecord/lib/active_record/associations/builder/association.rb b/activerecord/lib/active_record/associations/builder/association.rb
index f085fd1cfd..947d61ee7b 100644
--- a/activerecord/lib/active_record/associations/builder/association.rb
+++ b/activerecord/lib/active_record/associations/builder/association.rb
@@ -36,6 +36,7 @@ module ActiveRecord::Associations::Builder
reflection = builder.build(model)
define_accessors model, reflection
define_callbacks model, reflection
+ define_validations model, reflection
builder.define_extensions model
reflection
end
@@ -85,7 +86,11 @@ module ActiveRecord::Associations::Builder
end
def self.define_callbacks(model, reflection)
- add_before_destroy_callbacks(model, reflection) if reflection.options[:dependent]
+ if dependent = reflection.options[:dependent]
+ check_dependent_options(dependent)
+ add_destroy_callbacks(model, reflection)
+ end
+
Association.extensions.each do |extension|
extension.build model, reflection
end
@@ -120,17 +125,23 @@ module ActiveRecord::Associations::Builder
CODE
end
+ def self.define_validations(model, reflection)
+ # noop
+ end
+
def self.valid_dependent_options
raise NotImplementedError
end
private
- 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]}"
+ def self.check_dependent_options(dependent)
+ unless valid_dependent_options.include? dependent
+ raise ArgumentError, "The :dependent option must be one of #{valid_dependent_options}, but is :#{dependent}"
end
+ end
+ def self.add_destroy_callbacks(model, reflection)
name = reflection.name
model.before_destroy lambda { |o| o.association(name).handle_dependency }
end
diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb
index 47cc1f4b34..954ea3878a 100644
--- a/activerecord/lib/active_record/associations/builder/belongs_to.rb
+++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb
@@ -103,9 +103,14 @@ module ActiveRecord::Associations::Builder
BelongsTo.touch_record(record, foreign_key, n, touch)
}
- model.after_save callback
+ model.after_save callback, if: :changed?
model.after_touch callback
model.after_destroy callback
end
+
+ def self.add_destroy_callbacks(model, reflection)
+ name = reflection.name
+ model.after_destroy lambda { |o| o.association(name).handle_dependency }
+ 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 e472277374..34a555dfd4 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
@@ -11,11 +11,14 @@ module ActiveRecord::Associations::Builder
end
def join_table
- @join_table ||= [@lhs_class.table_name, klass.table_name].sort.join("\0").gsub(/^(.*_)(.+)\0\1(.+)/, '\1\2_\3').gsub("\0", "_")
+ @join_table ||= [@lhs_class.table_name, klass.table_name].sort.join("\0").gsub(/^(.*[._])(.+)\0\1(.+)/, '\1\2_\3').gsub("\0", "_")
end
private
- def klass; @rhs_class_name.constantize; end
+
+ def klass
+ @lhs_class.send(:compute_type, @rhs_class_name)
+ end
end
def self.build(lhs_class, name, options)
@@ -60,13 +63,13 @@ module ActiveRecord::Associations::Builder
def self.add_left_association(name, options)
belongs_to name, options
- self.left_reflection = reflect_on_association(name)
+ self.left_reflection = _reflect_on_association(name)
end
def self.add_right_association(name, options)
rhs_name = name.to_s.singularize.to_sym
belongs_to rhs_name, options
- self.right_reflection = reflect_on_association(rhs_name)
+ self.right_reflection = _reflect_on_association(rhs_name)
end
}
diff --git a/activerecord/lib/active_record/associations/builder/has_one.rb b/activerecord/lib/active_record/associations/builder/has_one.rb
index f359efd496..c194c8ae9a 100644
--- a/activerecord/lib/active_record/associations/builder/has_one.rb
+++ b/activerecord/lib/active_record/associations/builder/has_one.rb
@@ -5,7 +5,7 @@ module ActiveRecord::Associations::Builder
end
def valid_options
- valid = super + [:order, :as]
+ valid = super + [:as]
valid += [:through, :source, :source_type] if options[:through]
valid
end
@@ -16,7 +16,7 @@ module ActiveRecord::Associations::Builder
private
- def self.add_before_destroy_callbacks(model, reflection)
+ def self.add_destroy_callbacks(model, reflection)
super unless reflection.options[:through]
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 e655c389a6..6e6dd7204c 100644
--- a/activerecord/lib/active_record/associations/builder/singular_association.rb
+++ b/activerecord/lib/active_record/associations/builder/singular_association.rb
@@ -3,7 +3,7 @@
module ActiveRecord::Associations::Builder
class SingularAssociation < Association #:nodoc:
def valid_options
- super + [:remote, :dependent, :primary_key, :inverse_of]
+ super + [:dependent, :primary_key, :inverse_of, :required]
end
def self.define_accessors(model, reflection)
@@ -27,5 +27,12 @@ module ActiveRecord::Associations::Builder
end
CODE
end
+
+ def self.define_validations(model, reflection)
+ super
+ if reflection.options[:required]
+ model.validates_presence_of reflection.name
+ end
+ end
end
end
diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb
index 1c84973920..065a2cff01 100644
--- a/activerecord/lib/active_record/associations/collection_association.rb
+++ b/activerecord/lib/active_record/associations/collection_association.rb
@@ -55,9 +55,9 @@ module ActiveRecord
# Implements the ids writer method, e.g. foo.item_ids= for Foo.has_many :items
def ids_writer(ids)
- pk_column = reflection.primary_key_column
+ pk_type = reflection.primary_key_type
ids = Array(ids).reject { |id| id.blank? }
- ids.map! { |i| pk_column.type_cast(i) }
+ ids.map! { |i| pk_type.type_cast_from_user(i) }
replace(klass.find(ids).index_by { |r| r.id }.values_at(*ids))
end
@@ -194,7 +194,7 @@ module ActiveRecord
options[:dependent]
end
- delete_all_with_dependency(dependent).tap do
+ delete_or_nullify_all_records(dependent).tap do
reset
loaded!
end
@@ -244,6 +244,7 @@ module ActiveRecord
# are actually removed from the database, that depends precisely on
# +delete_records+. They are in any case removed from the collection.
def delete(*records)
+ return if records.empty?
_options = records.extract_options!
dependent = _options[:dependent] || options[:dependent]
@@ -251,20 +252,13 @@ module ActiveRecord
delete_or_destroy(records, dependent)
end
- def delete_all_with_dependency(dependent)
- if dependent == :destroy
- delete_or_destroy(load_target, dependent)
- else
- delete_records(:all, dependent)
- end
- end
-
# Deletes the +records+ and removes them from this association calling
# +before_remove+ , +after_remove+ , +before_destroy+ and +after_destroy+ callbacks.
#
# Note that this method removes records from the database ignoring the
# +:dependent+ option.
def destroy(*records)
+ return if records.empty?
records = find(records) if records.any? { |record| record.kind_of?(Fixnum) || record.kind_of?(String) }
delete_or_destroy(records, :destroy)
end
diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb
index aac85a36c8..2a97d0ed31 100644
--- a/activerecord/lib/active_record/associations/has_many_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_association.rb
@@ -41,6 +41,14 @@ module ActiveRecord
end
end
+ def empty?
+ if has_cached_counter?
+ size.zero?
+ else
+ super
+ end
+ end
+
private
# Returns the number of records in this collection.
@@ -80,9 +88,20 @@ module ActiveRecord
end
def update_counter(difference, reflection = reflection())
+ update_counter_in_database(difference, reflection)
+ update_counter_in_memory(difference, reflection)
+ end
+
+ def update_counter_in_database(difference, reflection = reflection())
if has_cached_counter?(reflection)
counter = cached_counter_attribute_name(reflection)
owner.class.update_counters(owner.id, counter => difference)
+ end
+ end
+
+ def update_counter_in_memory(difference, reflection = reflection())
+ if has_cached_counter?(reflection)
+ counter = cached_counter_attribute_name(reflection)
owner[counter] += difference
owner.changed_attributes.delete(counter) # eww
end
@@ -100,28 +119,37 @@ module ActiveRecord
# Hence this method.
def inverse_updates_counter_cache?(reflection = reflection())
counter_name = cached_counter_attribute_name(reflection)
- reflection.klass.reflect_on_all_associations(:belongs_to).any? { |inverse_reflection|
+ inverse_updates_counter_named?(counter_name, reflection)
+ end
+
+ def inverse_updates_counter_named?(counter_name, reflection = reflection())
+ reflection.klass._reflections.values.any? { |inverse_reflection|
+ :belongs_to == inverse_reflection.macro &&
inverse_reflection.counter_cache_column == counter_name
}
end
+ def delete_count(method, scope)
+ if method == :delete_all
+ scope.delete_all
+ else
+ scope.update_all(reflection.foreign_key => nil)
+ end
+ end
+
+ def delete_or_nullify_all_records(method)
+ count = delete_count(method, self.scope)
+ update_counter(-count)
+ end
+
# Deletes the records according to the <tt>:dependent</tt> option.
def delete_records(records, method)
if method == :destroy
records.each(&:destroy!)
update_counter(-records.length) unless inverse_updates_counter_cache?
else
- if records == :all || !reflection.klass.primary_key
- scope = self.scope
- else
- scope = self.scope.where(reflection.klass.primary_key => records)
- end
-
- if method == :delete_all
- update_counter(-scope.delete_all)
- else
- update_counter(-scope.update_all(reflection.foreign_key => nil))
- end
+ scope = self.scope.where(reflection.klass.primary_key => records)
+ update_counter(-delete_count(method, scope))
end
end
@@ -132,6 +160,25 @@ module ActiveRecord
false
end
end
+
+ def concat_records(records, *)
+ update_counter_if_success(super, records.length)
+ end
+
+ def _create_record(attributes, *)
+ if attributes.is_a?(Array)
+ super
+ else
+ update_counter_if_success(super, 1)
+ end
+ end
+
+ def update_counter_if_success(saved_successfully, difference)
+ if saved_successfully
+ update_counter_in_memory(difference)
+ end
+ saved_successfully
+ 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 f3af8605cd..007e3bc555 100644
--- a/activerecord/lib/active_record/associations/has_many_through_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_through_association.rb
@@ -12,17 +12,18 @@ module ActiveRecord
@through_association = nil
end
- # Returns the size of the collection by executing a SELECT COUNT(*) query if the collection hasn't been
- # loaded and calling collection.size if it has. If it's more likely than not that the collection does
- # have a size larger than zero, and you need to fetch that collection afterwards, it'll take one fewer
- # SELECT query if you use #length.
+ # Returns the size of the collection by executing a SELECT COUNT(*) query
+ # if the collection hasn't been loaded, and by calling collection.size if
+ # it has. If the collection will likely have a size greater than zero,
+ # and if fetching the collection will be needed afterwards, one less
+ # SELECT query will be generated by using #length instead.
def size
if has_cached_counter?
owner.read_attribute cached_counter_attribute_name(reflection)
elsif loaded?
target.size
else
- count
+ super
end
end
@@ -62,7 +63,15 @@ module ActiveRecord
end
save_through_record(record)
- update_counter(1)
+ if has_cached_counter? && !through_reflection_updates_counter_cache?
+ ActiveSupport::Deprecation.warn(<<-MESSAGE.strip_heredoc)
+ Automatic updating of counter caches on through associations has been
+ deprecated, and will be removed in Rails 5.0. Instead, please set the
+ appropriate counter_cache options on the has_many and belongs_to for
+ your associations to #{through_reflection.name}.
+ MESSAGE
+ update_counter_in_database(1)
+ end
record
end
@@ -72,25 +81,29 @@ module ActiveRecord
@through_association ||= owner.association(through_reflection.name)
end
- # We temporarily cache through record that has been build, because if we build a
- # through record in build_record and then subsequently call insert_record, then we
- # want to use the exact same object.
+ # The through record (built with build_record) is temporarily cached
+ # so that it may be reused if insert_record is subsequently called.
#
- # However, after insert_record has been called, we clear the cache entry because
- # we want it to be possible to have multiple instances of the same record in an
- # association
+ # However, after insert_record has been called, the cache is cleared in
+ # order to allow multiple instances of the same record in an association.
def build_through_record(record)
@through_records[record.object_id] ||= begin
ensure_mutable
- through_record = through_association.build through_scope_attributes
+ through_record = through_association.build(*options_for_through_record)
through_record.send("#{source_reflection.name}=", record)
through_record
end
end
+ def options_for_through_record
+ [through_scope_attributes]
+ end
+
def through_scope_attributes
- scope.where_values_hash(through_association.reflection.name.to_s)
+ scope.where_values_hash(through_association.reflection.name.to_s).
+ except!(through_association.reflection.foreign_key,
+ through_association.reflection.klass.inheritance_column)
end
def save_through_record(record)
@@ -106,9 +119,9 @@ module ActiveRecord
inverse = source_reflection.inverse_of
if inverse
- if inverse.macro == :has_many
+ if inverse.collection?
record.send(inverse.name) << build_through_record(record)
- elsif inverse.macro == :has_one
+ elsif inverse.has_one?
record.send("#{inverse.name}=", build_through_record(record))
end
end
@@ -117,7 +130,7 @@ module ActiveRecord
end
def target_reflection_has_associated_record?
- !(through_reflection.macro == :belongs_to && owner[through_reflection.foreign_key].blank?)
+ !(through_reflection.belongs_to? && owner[through_reflection.foreign_key].blank?)
end
def update_through_counter?(method)
@@ -131,13 +144,13 @@ module ActiveRecord
end
end
+ def delete_or_nullify_all_records(method)
+ delete_records(load_target, method)
+ end
+
def delete_records(records, method)
ensure_not_nested
- # This is unoptimised; it will load all the target records
- # even when we just want to delete everything.
- records = load_target if records == :all
-
scope = through_association.scope
scope.where! construct_join_attributes(*records)
@@ -171,7 +184,7 @@ module ActiveRecord
klass.decrement_counter counter, records.map(&:id)
end
- if through_reflection.macro == :has_many && update_through_counter?(method)
+ if through_reflection.collection? && update_through_counter?(method)
update_counter(-count, through_reflection)
end
@@ -181,14 +194,18 @@ module ActiveRecord
def through_records_for(record)
attributes = construct_join_attributes(record)
candidates = Array.wrap(through_association.target)
- candidates.find_all { |c| c.attributes.slice(*attributes.keys) == attributes }
+ candidates.find_all do |c|
+ attributes.all? do |key, value|
+ c.public_send(key) == value
+ end
+ end
end
def delete_through_records(records)
records.each do |record|
through_records = through_records_for(record)
- if through_reflection.macro == :has_many
+ if through_reflection.collection?
through_records.each { |r| through_association.target.delete(r) }
else
if through_records.include?(through_association.target)
@@ -209,6 +226,11 @@ module ActiveRecord
def invertible_for?(record)
false
end
+
+ def through_reflection_updates_counter_cache?
+ counter_name = cached_counter_attribute_name
+ inverse_updates_counter_named?(counter_name, through_reflection)
+ end
end
end
end
diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb
index b7dc037a65..ec5c189cd3 100644
--- a/activerecord/lib/active_record/associations/join_dependency.rb
+++ b/activerecord/lib/active_record/associations/join_dependency.rb
@@ -131,7 +131,6 @@ module ActiveRecord
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|
@@ -144,8 +143,7 @@ module ActiveRecord
column_aliases = aliases.column_aliases join_root
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)
+ parent = parents[row_hash[primary_key]] ||= join_root.instantiate(row_hash, column_aliases)
construct(parent, join_root, row_hash, result_set, seen, model_cache, aliases)
}
@@ -207,7 +205,7 @@ module ActiveRecord
end
def find_reflection(klass, name)
- klass.reflect_on_association(name) or
+ klass._reflect_on_association(name) or
raise ConfigurationError, "Association named '#{ name }' was not found on #{ klass.name }; perhaps you misspelled it?"
end
@@ -215,8 +213,9 @@ module ActiveRecord
associations.map do |name, right|
reflection = find_reflection base_klass, name
reflection.check_validity!
+ reflection.check_eager_loadable!
- if reflection.options[:polymorphic]
+ if reflection.polymorphic?
raise EagerLoadPolymorphicError.new(reflection)
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 a0e83c0a02..719eff9acc 100644
--- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb
+++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb
@@ -95,7 +95,7 @@ module ActiveRecord
# end
#
# If I execute `Physician.joins(:appointments).to_a` then
- # reflection # => #<ActiveRecord::Reflection::AssociationReflection @macro=:has_many ...>
+ # reflection # => #<ActiveRecord::Reflection::HasManyReflection ...>
# table # => #<Arel::Table @name="appointments" ...>
# key # => physician_id
# foreign_table # => #<Arel::Table @name="physicians" ...>
diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb
index 311684d886..7519fec10a 100644
--- a/activerecord/lib/active_record/associations/preloader.rb
+++ b/activerecord/lib/active_record/associations/preloader.rb
@@ -112,13 +112,14 @@ module ActiveRecord
end
def preloaders_for_hash(association, records, scope)
- parent, child = association.to_a.first # hash should only be of length 1
+ association.flat_map { |parent, child|
+ loaders = preloaders_for_one parent, records, scope
- 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
+ recs = loaders.flat_map(&:preloaded_records).uniq
+ loaders.concat Array.wrap(child).flat_map { |assoc|
+ preloaders_on assoc, recs, scope
+ }
+ loaders
}
end
@@ -142,6 +143,7 @@ module ActiveRecord
def grouped_records(association, records)
h = {}
records.each do |record|
+ next unless record
assoc = record.association(association)
klasses = h[assoc.reflection] ||= {}
(klasses[assoc.klass] ||= []) << record
@@ -175,6 +177,7 @@ module ActiveRecord
if owners.first.association(reflection.name).loaded?
return AlreadyLoaded
end
+ reflection.check_preloadable!
case reflection.macro
when :has_many
diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb
index bf461070e0..c0639742be 100644
--- a/activerecord/lib/active_record/associations/preloader/association.rb
+++ b/activerecord/lib/active_record/associations/preloader/association.rb
@@ -57,9 +57,15 @@ module ActiveRecord
end
def owners_by_key
- @owners_by_key ||= owners.group_by do |owner|
- owner[owner_key_name]
- end
+ @owners_by_key ||= if key_conversion_required?
+ owners.group_by do |owner|
+ owner[owner_key_name].to_s
+ end
+ else
+ owners.group_by do |owner|
+ owner[owner_key_name]
+ end
+ end
end
def options
@@ -93,13 +99,28 @@ module ActiveRecord
records_by_owner
end
+ def key_conversion_required?
+ association_key_type != owner_key_type
+ end
+
+ def association_key_type
+ @klass.type_for_attribute(association_key_name.to_s).type
+ end
+
+ def owner_key_type
+ @model.type_for_attribute(owner_key_name.to_s).type
+ end
+
def load_slices(slices)
@preloaded_records = slices.flat_map { |slice|
records_for(slice)
}
@preloaded_records.map { |record|
- [record, record[association_key_name]]
+ key = record[association_key_name]
+ key = key.to_s if key_conversion_required?
+
+ [record, key]
}
end
@@ -130,6 +151,10 @@ module ActiveRecord
end
end
+ if preload_values[:readonly] || values[:readonly]
+ scope.readonly!
+ end
+
if options[:as]
scope.where!(klass.table_name => { reflection.type => model.base_class.sti_name })
end
diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb
index f8a85b8a6f..f00fef8b9e 100644
--- a/activerecord/lib/active_record/associations/through_association.rb
+++ b/activerecord/lib/active_record/associations/through_association.rb
@@ -41,12 +41,16 @@ module ActiveRecord
def construct_join_attributes(*records)
ensure_mutable
- join_attributes = {
- source_reflection.foreign_key =>
- records.map { |record|
- record.send(source_reflection.association_primary_key(reflection.klass))
- }
- }
+ if source_reflection.association_primary_key(reflection.klass) == reflection.klass.primary_key
+ join_attributes = { source_reflection.name => records }
+ else
+ join_attributes = {
+ source_reflection.foreign_key =>
+ records.map { |record|
+ record.send(source_reflection.association_primary_key(reflection.klass))
+ }
+ }
+ end
if options[:source_type]
join_attributes[source_reflection.foreign_type] =
@@ -63,14 +67,13 @@ module ActiveRecord
# Note: this does not capture all cases, for example it would be crazy to try to
# properly support stale-checking for nested associations.
def stale_state
- if through_reflection.macro == :belongs_to
+ if through_reflection.belongs_to?
owner[through_reflection.foreign_key] && owner[through_reflection.foreign_key].to_s
end
end
def foreign_key_present?
- through_reflection.macro == :belongs_to &&
- !owner[through_reflection.foreign_key].nil?
+ through_reflection.belongs_to? && !owner[through_reflection.foreign_key].nil?
end
def ensure_mutable
diff --git a/activerecord/lib/active_record/attribute.rb b/activerecord/lib/active_record/attribute.rb
new file mode 100644
index 0000000000..6d38224830
--- /dev/null
+++ b/activerecord/lib/active_record/attribute.rb
@@ -0,0 +1,120 @@
+module ActiveRecord
+ class Attribute # :nodoc:
+ class << self
+ def from_database(name, value, type)
+ FromDatabase.new(name, value, type)
+ end
+
+ def from_user(name, value, type)
+ FromUser.new(name, value, type)
+ end
+
+ def null(name)
+ Null.new(name)
+ end
+
+ def uninitialized(name, type)
+ Uninitialized.new(name, type)
+ end
+ end
+
+ attr_reader :name, :value_before_type_cast, :type
+
+ # This method should not be called directly.
+ # Use #from_database or #from_user
+ def initialize(name, value_before_type_cast, type)
+ @name = name
+ @value_before_type_cast = value_before_type_cast
+ @type = type
+ end
+
+ def value
+ # `defined?` is cheaper than `||=` when we get back falsy values
+ @value = type_cast(value_before_type_cast) unless defined?(@value)
+ @value
+ end
+
+ def value_for_database
+ type.type_cast_for_database(value)
+ end
+
+ def changed_from?(old_value)
+ type.changed?(old_value, value, value_before_type_cast)
+ end
+
+ def changed_in_place_from?(old_value)
+ type.changed_in_place?(old_value, value)
+ end
+
+ def with_value_from_user(value)
+ self.class.from_user(name, value, type)
+ end
+
+ def with_value_from_database(value)
+ self.class.from_database(name, value, type)
+ end
+
+ def type_cast
+ raise NotImplementedError
+ end
+
+ def initialized?
+ true
+ end
+
+ protected
+
+ def initialize_dup(other)
+ if defined?(@value) && @value.duplicable?
+ @value = @value.dup
+ end
+ end
+
+ class FromDatabase < Attribute # :nodoc:
+ def type_cast(value)
+ type.type_cast_from_database(value)
+ end
+ end
+
+ class FromUser < Attribute # :nodoc:
+ def type_cast(value)
+ type.type_cast_from_user(value)
+ end
+ end
+
+ class Null < Attribute # :nodoc:
+ def initialize(name)
+ super(name, nil, Type::Value.new)
+ end
+
+ def value
+ nil
+ end
+
+ def with_value_from_database(value)
+ raise ActiveModel::MissingAttributeError, "can't write unknown attribute `#{name}`"
+ end
+ alias_method :with_value_from_user, :with_value_from_database
+ end
+
+ class Uninitialized < Attribute # :nodoc:
+ def initialize(name, type)
+ super(name, nil, type)
+ end
+
+ def value
+ if block_given?
+ yield name
+ end
+ end
+
+ def value_for_database
+ end
+
+ def initialized?
+ false
+ end
+ end
+ private_constant :FromDatabase, :FromUser, :Null, :Uninitialized
+ end
+end
diff --git a/activerecord/lib/active_record/attribute_assignment.rb b/activerecord/lib/active_record/attribute_assignment.rb
index 30fa2c8ba5..2887db3bf7 100644
--- a/activerecord/lib/active_record/attribute_assignment.rb
+++ b/activerecord/lib/active_record/attribute_assignment.rb
@@ -11,6 +11,15 @@ module ActiveRecord
# If the passed hash responds to <tt>permitted?</tt> method and the return value
# of this method is +false+ an <tt>ActiveModel::ForbiddenAttributesError</tt>
# exception is raised.
+ #
+ # cat = Cat.new(name: "Gorby", status: "yawning")
+ # cat.attributes # => { "name" => "Gorby", "status" => "yawning", "created_at" => nil, "updated_at" => nil}
+ # cat.assign_attributes(status: "sleeping")
+ # cat.attributes # => { "name" => "Gorby", "status" => "sleeping", "created_at" => nil, "updated_at" => nil }
+ #
+ # New attributes will be persisted in the database when the object is saved.
+ #
+ # Aliased to <tt>attributes=</tt>.
def assign_attributes(new_attributes)
if !new_attributes.respond_to?(:stringify_keys)
raise ArgumentError, "When assigning attributes, you must pass a hash as an argument."
@@ -106,7 +115,7 @@ module ActiveRecord
end
class MultiparameterAttribute #:nodoc:
- attr_reader :object, :name, :values, :column
+ attr_reader :object, :name, :values, :cast_type
def initialize(object, name, values)
@object = object
@@ -117,22 +126,22 @@ module ActiveRecord
def read_value
return if values.values.compact.empty?
- @column = object.class.reflect_on_aggregation(name.to_sym) || object.column_for_attribute(name)
- klass = column.klass
+ @cast_type = object.type_for_attribute(name)
+ klass = cast_type.klass
if klass == Time
read_time
elsif klass == Date
read_date
else
- read_other(klass)
+ read_other
end
end
private
def instantiate_time_object(set_values)
- if object.class.send(:create_time_zone_conversion_attribute?, name, column)
+ if object.class.send(:create_time_zone_conversion_attribute?, name, cast_type)
Time.zone.local(*set_values)
else
Time.send(object.class.default_timezone, *set_values)
@@ -140,9 +149,9 @@ module ActiveRecord
end
def read_time
- # If column is a :time (and not :date or :timestamp) there is no need to validate if
+ # If column is a :time (and not :date or :datetime) there is no need to validate if
# there are year/month/day fields
- if column.type == :time
+ if cast_type.type == :time
# if the column is a time set the values to their defaults as January 1, 1970, but only if they're nil
{ 1 => 1970, 2 => 1, 3 => 1 }.each do |key,value|
values[key] ||= value
@@ -172,13 +181,12 @@ module ActiveRecord
end
end
- def read_other(klass)
+ def read_other
max_position = extract_max_param
positions = (1..max_position)
validate_required_parameters!(positions)
- set_values = values.values_at(*positions)
- klass.new(*set_values)
+ values.slice(*positions)
end
# Checks whether some blank date parameter exists. Note that this is different
diff --git a/activerecord/lib/active_record/attribute_decorators.rb b/activerecord/lib/active_record/attribute_decorators.rb
new file mode 100644
index 0000000000..5b96623b6e
--- /dev/null
+++ b/activerecord/lib/active_record/attribute_decorators.rb
@@ -0,0 +1,66 @@
+module ActiveRecord
+ module AttributeDecorators # :nodoc:
+ extend ActiveSupport::Concern
+
+ included do
+ class_attribute :attribute_type_decorations, instance_accessor: false # :internal:
+ self.attribute_type_decorations = TypeDecorator.new
+ end
+
+ module ClassMethods # :nodoc:
+ def decorate_attribute_type(column_name, decorator_name, &block)
+ matcher = ->(name, _) { name == column_name.to_s }
+ key = "_#{column_name}_#{decorator_name}"
+ decorate_matching_attribute_types(matcher, key, &block)
+ end
+
+ def decorate_matching_attribute_types(matcher, decorator_name, &block)
+ clear_caches_calculated_from_columns
+ decorator_name = decorator_name.to_s
+
+ # Create new hashes so we don't modify parent classes
+ self.attribute_type_decorations = attribute_type_decorations.merge(decorator_name => [matcher, block])
+ end
+
+ private
+
+ def add_user_provided_columns(*)
+ super.map do |column|
+ decorated_type = attribute_type_decorations.apply(column.name, column.cast_type)
+ column.with_type(decorated_type)
+ end
+ end
+ end
+
+ class TypeDecorator # :nodoc:
+ delegate :clear, to: :@decorations
+
+ def initialize(decorations = {})
+ @decorations = decorations
+ end
+
+ def merge(*args)
+ TypeDecorator.new(@decorations.merge(*args))
+ end
+
+ def apply(name, type)
+ decorations = decorators_for(name, type)
+ decorations.inject(type) do |new_type, block|
+ block.call(new_type)
+ end
+ end
+
+ private
+
+ def decorators_for(name, type)
+ matching(name, type).map(&:last)
+ end
+
+ def matching(name, type)
+ @decorations.values.select do |(matcher, _)|
+ matcher.call(name, type)
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb
index 4b1733619a..a2bb78dfcc 100644
--- a/activerecord/lib/active_record/attribute_methods.rb
+++ b/activerecord/lib/active_record/attribute_methods.rb
@@ -18,6 +18,8 @@ module ActiveRecord
include TimeZoneConversion
include Dirty
include Serialization
+
+ delegate :column_for_attribute, to: :class
end
AttrNames = Module.new {
@@ -48,7 +50,11 @@ module ActiveRecord
end
private
- def method_body; raise NotImplementedError; end
+
+ # Override this method in the subclasses for method body.
+ def method_body(method_name, const_name)
+ raise NotImplementedError, "Subclasses must implement a method_body(method_name, const_name) method."
+ end
end
module ClassMethods
@@ -66,7 +72,8 @@ module ActiveRecord
# Generates all the attribute related methods for columns in the database
# accessors, mutators and query methods.
def define_attribute_methods # :nodoc:
- # Use a mutex; we don't want two thread simultaneously trying to define
+ return false if @attribute_methods_generated
+ # Use a mutex; we don't want two threads simultaneously trying to define
# attribute methods.
generated_attribute_methods.synchronize do
return false if @attribute_methods_generated
@@ -149,16 +156,6 @@ module ActiveRecord
end
end
- def find_generated_attribute_method(method_name) # :nodoc:
- klass = self
- until klass == Base
- gen_methods = klass.generated_attribute_methods
- return gen_methods.instance_method(method_name) if method_defined_within?(method_name, gen_methods, Object)
- klass = klass.superclass
- end
- nil
- end
-
# Returns +true+ if +attribute+ is an attribute method and table exists,
# +false+ otherwise.
#
@@ -187,23 +184,29 @@ module ActiveRecord
[]
end
end
- end
- # If we haven't generated any methods yet, generate them, then
- # see if we've created the method we're looking for.
- def method_missing(method, *args, &block) # :nodoc:
- self.class.define_attribute_methods
- if respond_to_without_attributes?(method)
- # make sure to invoke the correct attribute method, as we might have gotten here via a `super`
- # call in a overwritten attribute method
- if attribute_method = self.class.find_generated_attribute_method(method)
- # this is probably horribly slow, but should only happen at most once for a given AR class
- attribute_method.bind(self).call(*args, &block)
- else
- send(method, *args, &block)
+ # Returns the column object for the named attribute.
+ # Returns nil if the named attribute does not exist.
+ #
+ # class Person < ActiveRecord::Base
+ # end
+ #
+ # person = Person.new
+ # person.column_for_attribute(:name) # the result depends on the ConnectionAdapter
+ # # => #<ActiveRecord::ConnectionAdapters::Column:0x007ff4ab083980 @name="name", @sql_type="varchar(255)", @null=true, ...>
+ #
+ # person.column_for_attribute(:nothing)
+ # # => nil
+ def column_for_attribute(name)
+ column = columns_hash[name.to_s]
+ if column.nil?
+ ActiveSupport::Deprecation.warn(<<-MESSAGE.strip_heredoc)
+ `column_for_attribute` will return a null object for non-existent columns
+ in Rails 5.0. Use `has_attribute?` if you need to check for an
+ attribute's existence.
+ MESSAGE
end
- else
- super
+ column
end
end
@@ -224,18 +227,14 @@ module ActiveRecord
# person.respond_to('age?') # => true
# person.respond_to(:nothing) # => false
def respond_to?(name, include_private = false)
+ return false unless super
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)
+ if defined?(@attributes) && self.class.column_names.include?(name)
return has_attribute?(name)
end
@@ -252,7 +251,7 @@ module ActiveRecord
# person.has_attribute?('age') # => true
# person.has_attribute?(:nothing) # => false
def has_attribute?(attr_name)
- @attributes.has_key?(attr_name.to_s)
+ @attributes.key?(attr_name.to_s)
end
# Returns an array of names for the attributes available on this object.
@@ -276,14 +275,7 @@ module ActiveRecord
# 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)
- }
- end
-
- # Placeholder so it can be overriden when needed by serialization
- def attributes_for_coder # :nodoc:
- attributes
+ @attributes.to_hash
end
# Returns an <tt>#inspect</tt>-like string for the value of the
@@ -326,39 +318,24 @@ module ActiveRecord
# 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
+ # task = Task.new(title: '', is_done: false)
+ # task.attribute_present?(:title) # => false
+ # task.attribute_present?(:is_done) # => true
+ # task.title = 'Buy milk'
+ # task.is_done = true
+ # task.attribute_present?(:title) # => true
+ # task.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 +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 date column is cast to a date object, like Date.new(2004, 12, 12)). It raises
# <tt>ActiveModel::MissingAttributeError</tt> if the identified attribute is missing.
#
+ # Note: +:id+ is always present.
+ #
# Alias for the <tt>read_attribute</tt> method.
#
# class Person < ActiveRecord::Base
@@ -392,13 +369,6 @@ module ActiveRecord
protected
- 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) # :nodoc:
value = send(reader_method, attribute_name)
value.duplicable? ? value.clone : value
@@ -416,7 +386,7 @@ module ActiveRecord
def attribute_method?(attr_name) # :nodoc:
# We check defined? because Syck calls respond_to? before actually calling initialize.
- defined?(@attributes) && @attributes.include?(attr_name)
+ defined?(@attributes) && @attributes.key?(attr_name)
end
private
@@ -435,16 +405,16 @@ 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) && !readonly_attribute?(name)
+ attribute_names.reject do |name|
+ readonly_attribute?(name)
end
end
# Filters out the primary keys, from the attribute names, when the primary
# key is to be generated (e.g. the id attribute has no value).
def attributes_for_create(attribute_names)
- attribute_names.select do |name|
- column_for_attribute(name) && !(pk_attribute?(name) && id.nil?)
+ attribute_names.reject do |name|
+ pk_attribute?(name) && id.nil?
end
end
@@ -453,13 +423,10 @@ module ActiveRecord
end
def pk_attribute?(name)
- column_for_attribute(name).primary
+ name == self.class.primary_key
end
def typecasted_attribute_value(name)
- # 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
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 f596a8b02e..fd61febd57 100644
--- a/activerecord/lib/active_record/attribute_methods/before_type_cast.rb
+++ b/activerecord/lib/active_record/attribute_methods/before_type_cast.rb
@@ -43,7 +43,7 @@ module ActiveRecord
# 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.to_s]
+ @attributes[attr_name.to_s].value_before_type_cast
end
# Returns a hash of attributes before typecasting and deserialization.
@@ -57,7 +57,7 @@ module ActiveRecord
# 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
+ @attributes.values_before_type_cast
end
private
diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb
index 99070f127b..b58295a106 100644
--- a/activerecord/lib/active_record/attribute_methods/dirty.rb
+++ b/activerecord/lib/active_record/attribute_methods/dirty.rb
@@ -34,28 +34,47 @@ module ActiveRecord
# <tt>reload</tt> the record and clears changed attributes.
def reload(*)
super.tap do
- reset_changes
+ clear_changes_information
end
end
- def initialize_dup(other) # :nodoc:
- super
- init_changed_attributes
- end
+ def initialize_dup(other) # :nodoc:
+ super
+ calculate_changes_from_defaults
+ end
+
+ def changed?
+ super || changed_in_place.any?
+ end
+
+ def changed
+ super | changed_in_place
+ end
- private
- def initialize_internals_callback
+ def attribute_changed?(attr_name, options = {})
+ result = super
+ # We can't change "from" something in place. Only setters can define
+ # "from" and "to"
+ result ||= changed_in_place?(attr_name) unless options.key?(:from)
+ result
+ end
+
+ def changes_applied
+ super
+ store_original_raw_attributes
+ end
+
+ def clear_changes_information
super
- init_changed_attributes
+ original_raw_attributes.clear
end
- def init_changed_attributes
+ private
+
+ def calculate_changes_from_defaults
@changed_attributes = nil
- # 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])
+ self.class.column_defaults.each do |attr, orig_value|
+ changed_attributes[attr] = orig_value if _field_changed?(attr, orig_value)
end
end
@@ -63,19 +82,35 @@ module ActiveRecord
def write_attribute(attr, value)
attr = attr.to_s
- save_changed_attribute(attr, value)
+ old_value = old_attribute_value(attr)
- super(attr, value)
+ result = super
+ store_original_raw_attribute(attr)
+ save_changed_attribute(attr, old_value)
+ result
end
- def save_changed_attribute(attr, value)
- # The attribute already has an unsaved change.
+ def raw_write_attribute(attr, value)
+ attr = attr.to_s
+
+ result = super
+ original_raw_attributes[attr] = value
+ result
+ end
+
+ def save_changed_attribute(attr, old_value)
+ if attribute_changed?(attr)
+ changed_attributes.delete(attr) unless _field_changed?(attr, old_value)
+ else
+ changed_attributes[attr] = old_value if _field_changed?(attr, old_value)
+ end
+ end
+
+ def old_attribute_value(attr)
if attribute_changed?(attr)
- old = changed_attributes[attr]
- changed_attributes.delete(attr) unless _field_changed?(attr, old, value)
+ changed_attributes[attr]
else
- old = clone_attribute_value(:read_attribute, attr)
- changed_attributes[attr] = old if _field_changed?(attr, old, value)
+ clone_attribute_value(:read_attribute, attr)
end
end
@@ -93,34 +128,39 @@ module ActiveRecord
changed
end
- def _field_changed?(attr, old, value)
- if column = column_for_attribute(attr)
- if column.number? && (changes_from_nil_to_empty_string?(column, old, value) ||
- changes_from_zero_to_string?(old, value))
- value = nil
- else
- value = column.type_cast(value)
- end
+ def _field_changed?(attr, old_value)
+ @attributes[attr].changed_from?(old_value)
+ end
+
+ def changed_in_place
+ self.class.attribute_names.select do |attr_name|
+ changed_in_place?(attr_name)
end
+ end
- old != value
+ def changed_in_place?(attr_name)
+ old_value = original_raw_attribute(attr_name)
+ @attributes[attr_name].changed_in_place_from?(old_value)
end
- def changes_from_nil_to_empty_string?(column, old, value)
- # For nullable numeric columns, NULL gets stored in database for blank (i.e. '') values.
- # Hence we don't record it as a change if the value changes from nil to ''.
- # If an old value of 0 is set to '' we want this to get changed to nil as otherwise it'll
- # be typecast back to 0 (''.to_i => 0)
- column.null && (old.nil? || old == 0) && value.blank?
+ def original_raw_attribute(attr_name)
+ original_raw_attributes.fetch(attr_name) do
+ read_attribute_before_type_cast(attr_name)
+ end
end
- 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? && non_zero?(value)
+ def original_raw_attributes
+ @original_raw_attributes ||= {}
end
- def non_zero?(value)
- value !~ /\A0+(\.0+)?\z/
+ def store_original_raw_attribute(attr_name)
+ original_raw_attributes[attr_name] = @attributes[attr_name].value_for_database
+ end
+
+ def store_original_raw_attributes
+ attribute_names.each do |attr|
+ store_original_raw_attribute(attr)
+ end
end
end
end
diff --git a/activerecord/lib/active_record/attribute_methods/primary_key.rb b/activerecord/lib/active_record/attribute_methods/primary_key.rb
index 931209b07b..cadad60ddd 100644
--- a/activerecord/lib/active_record/attribute_methods/primary_key.rb
+++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb
@@ -15,8 +15,10 @@ module ActiveRecord
# Returns the primary key value.
def id
- sync_with_transaction_state
- read_attribute(self.class.primary_key)
+ if pk = self.class.primary_key
+ sync_with_transaction_state
+ read_attribute(pk)
+ end
end
# Sets the primary key value.
@@ -81,12 +83,9 @@ module ActiveRecord
end
def get_primary_key(base_name) #:nodoc:
- return 'id' if base_name.blank?
-
- case primary_key_prefix_type
- when :table_name
+ if base_name && primary_key_prefix_type == :table_name
base_name.foreign_key(false)
- when :table_name_with_underscore
+ elsif base_name && primary_key_prefix_type == :table_name_with_underscore
base_name.foreign_key
else
if ActiveRecord::Base != self && table_exists?
diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb
index d01e9aea59..10869dfc1e 100644
--- a/activerecord/lib/active_record/attribute_methods/read.rb
+++ b/activerecord/lib/active_record/attribute_methods/read.rb
@@ -22,7 +22,7 @@ module ActiveRecord
# 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.
+ # key the @attributes in read_attribute.
def method_body(method_name, const_name)
<<-EOMETHOD
def #{method_name}
@@ -35,35 +35,22 @@ module ActiveRecord
extend ActiveSupport::Concern
- ATTRIBUTE_TYPES_CACHED_BY_DEFAULT = [:datetime, :timestamp, :time, :date]
-
- included do
- class_attribute :attribute_types_cached_by_default, instance_writer: false
- self.attribute_types_cached_by_default = ATTRIBUTE_TYPES_CACHED_BY_DEFAULT
- end
-
module ClassMethods
- # +cache_attributes+ allows you to declare which converted attribute
- # values should be cached. Usually caching only pays off for attributes
- # with expensive conversion methods, like time related columns (e.g.
- # +created_at+, +updated_at+).
- def cache_attributes(*attribute_names)
- cached_attributes.merge attribute_names.map { |attr| attr.to_s }
+ [:cache_attributes, :cached_attributes, :cache_attribute?].each do |method_name|
+ define_method method_name do |*|
+ cached_attributes_deprecation_warning(method_name)
+ true
+ end
end
- # Returns the attributes which are cached. By default time related columns
- # with datatype <tt>:datetime, :timestamp, :time, :date</tt> are cached.
- def cached_attributes
- @cached_attributes ||= columns.select { |c| cacheable_column?(c) }.map { |col| col.name }.to_set
- end
+ protected
- # Returns +true+ if the provided attribute is being cached.
- def cache_attribute?(attr_name)
- cached_attributes.include?(attr_name)
+ def cached_attributes_deprecation_warning(method_name)
+ ActiveSupport::Deprecation.warn(<<-MESSAGE.strip_heredoc)
+ Calling `#{method_name}` is no longer necessary. All attributes are cached.
+ MESSAGE
end
- protected
-
if Module.methods_transplantable?
def define_method_attribute(name)
method = ReaderMethodCache[name]
@@ -89,45 +76,15 @@ module ActiveRecord
end
end
end
-
- private
-
- def cacheable_column?(column)
- if attribute_types_cached_by_default == ATTRIBUTE_TYPES_CACHED_BY_DEFAULT
- ! serialized_attributes.include? column.name
- else
- attribute_types_cached_by_default.include?(column.type)
- end
- end
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 date column is cast
# to a date object, like Date.new(2004, 12, 12)).
- def read_attribute(attr_name)
- # If it's cached, just return it
- # We use #[] first as a perf optimization for non-nil values. See https://gist.github.com/jonleighton/3552829.
+ def read_attribute(attr_name, &block)
name = attr_name.to_s
- @attributes_cache[name] || @attributes_cache.fetch(name) {
- 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
- }
-
- if self.class.cache_attribute?(name)
- @attributes_cache[name] = column.type_cast(value)
- else
- column.type_cast value
- end
- }
+ name = self.class.primary_key if name == 'id'
+ @attributes.fetch_value(name, &block)
end
private
diff --git a/activerecord/lib/active_record/attribute_methods/serialization.rb b/activerecord/lib/active_record/attribute_methods/serialization.rb
index c3466153d6..264ce2bdfa 100644
--- a/activerecord/lib/active_record/attribute_methods/serialization.rb
+++ b/activerecord/lib/active_record/attribute_methods/serialization.rb
@@ -3,20 +3,7 @@ module ActiveRecord
module Serialization
extend ActiveSupport::Concern
- included do
- # Returns a hash of all the attributes that have been specified for
- # 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
@@ -50,140 +37,32 @@ module ActiveRecord
# serialize :preferences, Hash
# end
def serialize(attr_name, class_name_or_coder = Object)
- include Behavior
-
- coder = if [:load, :dump].all? { |x| class_name_or_coder.respond_to?(x) }
+ # When ::JSON is used, force it to go through the Active Support JSON encoder
+ # to ensure special objects (e.g. Active Record models) are dumped correctly
+ # using the #as_json hook.
+ coder = if class_name_or_coder == ::JSON
+ Coders::JSON
+ elsif [:load, :dump].all? { |x| class_name_or_coder.respond_to?(x) }
class_name_or_coder
else
Coders::YAMLColumn.new(class_name_or_coder)
end
- # merge new serialized attribute and create new hash to ensure that each class in inheritance hierarchy
- # has its own hash of own serialized attributes
- self.serialized_attributes = serialized_attributes.merge(attr_name.to_s => coder)
- end
- end
-
- class Type # :nodoc:
- def initialize(column)
- @column = column
- end
-
- def type_cast(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(v = value)
- state == :serialized ? unserialize(v) : value
- end
-
- def serialized_value
- state == :unserialized ? serialize : value
- end
-
- def unserialize(v)
- self.state = :unserialized
- self.value = coder.load(v)
- end
-
- def serialize
- self.state = :serialized
- self.value = coder.dump(value)
- end
- end
-
- # 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:
- extend ActiveSupport::Concern
-
- module ClassMethods # :nodoc:
- def initialize_attributes(attributes, options = {})
- serialized = (options.delete(:serialized) { true }) ? :serialized : :unserialized
- super(attributes, options)
-
- serialized_attributes.each do |key, coder|
- if attributes.key?(key)
- attributes[key] = Attribute.new(coder, attributes[key], serialized)
- end
- end
-
- attributes
+ decorate_attribute_type(attr_name, :serialize) do |type|
+ Type::Serialized.new(type, coder)
end
end
- def should_record_timestamps?
- super || (self.record_timestamps && (attributes.keys & self.class.serialized_attributes.keys).present?)
- end
-
- def keys_for_partial_write
- super | (attributes.keys & self.class.serialized_attributes.keys)
- end
-
- def type_cast_attribute_for_write(column, value)
- if column && coder = self.class.serialized_attributes[column.name]
- Attribute.new(coder, value, :unserialized)
- else
- super
- 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
- else
- 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
-
- def attributes_for_coder
- attribute_names.each_with_object({}) do |name, attrs|
- attrs[name] = if self.class.serialized_attributes.include?(name)
- @attributes[name].serialized_value
- else
- read_attribute(name)
- end
- end
+ def serialized_attributes
+ ActiveSupport::Deprecation.warn(<<-WARNING.strip_heredoc)
+ `serialized_attributes` is deprecated without replacement, and will
+ be removed in Rails 5.0.
+ WARNING
+ @serialized_attributes ||= Hash[
+ columns.select { |t| t.cast_type.is_a?(Type::Serialized) }.map { |c|
+ [c.name, c.cast_type.coder]
+ }
+ ]
end
end
end
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 f168282ea3..f439bd1ffe 100644
--- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
+++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
@@ -1,18 +1,27 @@
module ActiveRecord
module AttributeMethods
module TimeZoneConversion
- class Type # :nodoc:
- def initialize(column)
- @column = column
+ class TimeZoneConverter < SimpleDelegator # :nodoc:
+ def type_cast_from_database(value)
+ convert_time_to_time_zone(super)
end
- def type_cast(value)
- value = @column.type_cast(value)
- value.acts_like?(:time) ? value.in_time_zone : value
+ def type_cast_from_user(value)
+ if value.is_a?(Array)
+ value.map { |v| type_cast_from_user(v) }
+ elsif value.respond_to?(:in_time_zone)
+ value.in_time_zone
+ end
end
- def type
- @column.type
+ def convert_time_to_time_zone(value)
+ if value.is_a?(Array)
+ value.map { |v| convert_time_to_time_zone(v) }
+ elsif value.acts_like?(:time)
+ value.in_time_zone
+ else
+ value
+ end
end
end
@@ -27,31 +36,26 @@ module ActiveRecord
end
module ClassMethods
- protected
- # Defined for all +datetime+ and +timestamp+ attributes when +time_zone_aware_attributes+ are enabled.
- # This enhanced write method will automatically convert the time passed to it to the zone stored in Time.zone.
- 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}=(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)
- else
- super
+ private
+
+ def inherited(subclass)
+ # We need to apply this decorator here, rather than on module inclusion. The closure
+ # created by the matcher would otherwise evaluate for `ActiveRecord::Base`, not the
+ # sub class being decorated. As such, changes to `time_zone_aware_attributes`, or
+ # `skip_time_zone_conversion_for_attributes` would not be picked up.
+ subclass.class_eval do
+ matcher = ->(name, type) { create_time_zone_conversion_attribute?(name, type) }
+ decorate_matching_attribute_types(matcher, :_time_zone_conversion) do |type|
+ TimeZoneConverter.new(type)
+ end
end
+ super
end
- private
- def create_time_zone_conversion_attribute?(name, column)
+ def create_time_zone_conversion_attribute?(name, cast_type)
time_zone_aware_attributes &&
!self.skip_time_zone_conversion_for_attributes.include?(name.to_sym) &&
- (:datetime == column.type || :timestamp == column.type)
+ (:datetime == cast_type.type)
end
end
end
diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb
index c853fc0917..b3c8209a74 100644
--- a/activerecord/lib/active_record/attribute_methods/write.rb
+++ b/activerecord/lib/active_record/attribute_methods/write.rb
@@ -26,8 +26,6 @@ module ActiveRecord
protected
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 {
@@ -55,24 +53,12 @@ module ActiveRecord
# specified +value+. Empty strings for fixnum and float columns are
# turned into +nil+.
def write_attribute(attr_name, value)
- attr_name = attr_name.to_s
- attr_name = self.class.primary_key if attr_name == 'id' && self.class.primary_key
- @attributes_cache.delete(attr_name)
- column = column_for_attribute(attr_name)
-
- # If we're dealing with a binary column, write the data to the cache
- # so we don't attempt to typecast multiple times.
- if column && column.binary?
- @attributes_cache[attr_name] = value
- end
+ write_attribute_with_type_cast(attr_name, value, true)
+ end
- if column || @attributes.has_key?(attr_name)
- @attributes[attr_name] = type_cast_attribute_for_write(column, value)
- else
- raise ActiveModel::MissingAttributeError, "can't write unknown attribute `#{attr_name}'"
- end
+ def raw_write_attribute(attr_name, value)
+ write_attribute_with_type_cast(attr_name, value, false)
end
- alias_method :raw_write_attribute, :write_attribute
private
# Handle *= for method_missing.
@@ -80,10 +66,17 @@ module ActiveRecord
write_attribute(attribute_name, value)
end
- def type_cast_attribute_for_write(column, value)
- return value unless column
+ def write_attribute_with_type_cast(attr_name, value, should_type_cast)
+ attr_name = attr_name.to_s
+ attr_name = self.class.primary_key if attr_name == 'id' && self.class.primary_key
+
+ if should_type_cast
+ @attributes.write_from_user(attr_name, value)
+ else
+ @attributes.write_from_database(attr_name, value)
+ end
- column.type_cast_for_write value
+ value
end
end
end
diff --git a/activerecord/lib/active_record/attribute_set.rb b/activerecord/lib/active_record/attribute_set.rb
new file mode 100644
index 0000000000..98ac63c7e1
--- /dev/null
+++ b/activerecord/lib/active_record/attribute_set.rb
@@ -0,0 +1,77 @@
+require 'active_record/attribute_set/builder'
+
+module ActiveRecord
+ class AttributeSet # :nodoc:
+ delegate :keys, to: :initialized_attributes
+
+ def initialize(attributes)
+ @attributes = attributes
+ end
+
+ def [](name)
+ attributes[name] || Attribute.null(name)
+ end
+
+ def values_before_type_cast
+ attributes.transform_values(&:value_before_type_cast)
+ end
+
+ def to_hash
+ initialized_attributes.transform_values(&:value)
+ end
+ alias_method :to_h, :to_hash
+
+ def key?(name)
+ attributes.key?(name) && self[name].initialized?
+ end
+
+ def fetch_value(name, &block)
+ self[name].value(&block)
+ end
+
+ def write_from_database(name, value)
+ attributes[name] = self[name].with_value_from_database(value)
+ end
+
+ def write_from_user(name, value)
+ attributes[name] = self[name].with_value_from_user(value)
+ end
+
+ def freeze
+ @attributes.freeze
+ super
+ end
+
+ def initialize_dup(_)
+ @attributes = attributes.transform_values(&:dup)
+ super
+ end
+
+ def initialize_clone(_)
+ @attributes = attributes.clone
+ super
+ end
+
+ def reset(key)
+ if key?(key)
+ write_from_database(key, nil)
+ end
+ end
+
+ def ensure_initialized(key)
+ unless self[key].initialized?
+ write_from_database(key, nil)
+ end
+ end
+
+ protected
+
+ attr_reader :attributes
+
+ private
+
+ def initialized_attributes
+ attributes.select { |_, attr| attr.initialized? }
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/attribute_set/builder.rb b/activerecord/lib/active_record/attribute_set/builder.rb
new file mode 100644
index 0000000000..1e146a07da
--- /dev/null
+++ b/activerecord/lib/active_record/attribute_set/builder.rb
@@ -0,0 +1,32 @@
+module ActiveRecord
+ class AttributeSet # :nodoc:
+ class Builder # :nodoc:
+ attr_reader :types
+
+ def initialize(types)
+ @types = types
+ end
+
+ def build_from_database(values = {}, additional_types = {})
+ attributes = build_attributes_from_values(values, additional_types)
+ add_uninitialized_attributes(attributes)
+ AttributeSet.new(attributes)
+ end
+
+ private
+
+ def build_attributes_from_values(values, additional_types)
+ values.each_with_object({}) do |(name, value), hash|
+ type = additional_types.fetch(name, types[name])
+ hash[name] = Attribute.from_database(name, value, type)
+ end
+ end
+
+ def add_uninitialized_attributes(attributes)
+ types.except(*attributes.keys).each do |name, type|
+ attributes[name] = Attribute.uninitialized(name, type)
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/attributes.rb b/activerecord/lib/active_record/attributes.rb
new file mode 100644
index 0000000000..890a1314d9
--- /dev/null
+++ b/activerecord/lib/active_record/attributes.rb
@@ -0,0 +1,122 @@
+module ActiveRecord
+ module Attributes # :nodoc:
+ extend ActiveSupport::Concern
+
+ Type = ActiveRecord::Type
+
+ included do
+ class_attribute :user_provided_columns, instance_accessor: false # :internal:
+ self.user_provided_columns = {}
+ end
+
+ module ClassMethods # :nodoc:
+ # Defines or overrides a attribute on this model. This allows customization of
+ # Active Record's type casting behavior, as well as adding support for user defined
+ # types.
+ #
+ # +name+ The name of the methods to define attribute methods for, and the column which
+ # this will persist to.
+ #
+ # +cast_type+ A type object that contains information about how to type cast the value.
+ # See the examples section for more information.
+ #
+ # ==== Options
+ # The options hash accepts the following options:
+ #
+ # +default+ is the default value that the column should use on a new record.
+ #
+ # ==== Examples
+ #
+ # The type detected by Active Record can be overridden.
+ #
+ # # db/schema.rb
+ # create_table :store_listings, force: true do |t|
+ # t.decimal :price_in_cents
+ # end
+ #
+ # # app/models/store_listing.rb
+ # class StoreListing < ActiveRecord::Base
+ # end
+ #
+ # store_listing = StoreListing.new(price_in_cents: '10.1')
+ #
+ # # before
+ # store_listing.price_in_cents # => BigDecimal.new(10.1)
+ #
+ # class StoreListing < ActiveRecord::Base
+ # attribute :price_in_cents, Type::Integer.new
+ # end
+ #
+ # # after
+ # store_listing.price_in_cents # => 10
+ #
+ # Users may also define their own custom types, as long as they respond to the methods
+ # defined on the value type. The `type_cast` method on your type object will be called
+ # with values both from the database, and from your controllers. See
+ # `ActiveRecord::Attributes::Type::Value` for the expected API. It is recommended that your
+ # type objects inherit from an existing type, or the base value type.
+ #
+ # class MoneyType < ActiveRecord::Type::Integer
+ # def type_cast(value)
+ # if value.include?('$')
+ # price_in_dollars = value.gsub(/\$/, '').to_f
+ # price_in_dollars * 100
+ # else
+ # value.to_i
+ # end
+ # end
+ # end
+ #
+ # class StoreListing < ActiveRecord::Base
+ # attribute :price_in_cents, MoneyType.new
+ # end
+ #
+ # store_listing = StoreListing.new(price_in_cents: '$10.00')
+ # store_listing.price_in_cents # => 1000
+ def attribute(name, cast_type, options = {})
+ name = name.to_s
+ clear_caches_calculated_from_columns
+ # Assign a new hash to ensure that subclasses do not share a hash
+ self.user_provided_columns = user_provided_columns.merge(name => connection.new_column(name, options[:default], cast_type))
+ end
+
+ # Returns an array of column objects for the table associated with this class.
+ def columns
+ @columns ||= add_user_provided_columns(connection.schema_cache.columns(table_name))
+ end
+
+ # Returns a hash of column objects for the table associated with this class.
+ def columns_hash
+ @columns_hash ||= Hash[columns.map { |c| [c.name, c] }]
+ end
+
+ def reset_column_information # :nodoc:
+ super
+ clear_caches_calculated_from_columns
+ end
+
+ private
+
+ def add_user_provided_columns(schema_columns)
+ existing_columns = schema_columns.map do |column|
+ user_provided_columns[column.name] || column
+ end
+
+ existing_column_names = existing_columns.map(&:name)
+ new_columns = user_provided_columns.except(*existing_column_names).values
+
+ existing_columns + new_columns
+ end
+
+ def clear_caches_calculated_from_columns
+ @attributes_builder = nil
+ @column_names = nil
+ @column_types = nil
+ @columns = nil
+ @columns_hash = nil
+ @content_columns = nil
+ @default_attributes = nil
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb
index f149d8f127..dd92e29199 100644
--- a/activerecord/lib/active_record/autosave_association.rb
+++ b/activerecord/lib/active_record/autosave_association.rb
@@ -147,6 +147,7 @@ module ActiveRecord
private
def define_non_cyclic_method(name, &block)
+ return if method_defined?(name)
define_method(name) do |*args|
result = true; @_already_called ||= {}
# Loop prevention for validation of associations
@@ -179,30 +180,26 @@ module ActiveRecord
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
+ if collection
+ before_save :before_save_collection_association
+
+ define_non_cyclic_method(save_method) { save_collection_association(reflection) }
+ after_save save_method
+ elsif reflection.has_one?
+ define_method(save_method) { save_has_one_association(reflection) } unless method_defined?(save_method)
+ # Configures two callbacks instead of a single after_save so that
+ # 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
if reflection.validate? && !method_defined?(validation_method)
@@ -273,9 +270,11 @@ module ActiveRecord
# 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? }
+ self.class._reflections.values.any? do |reflection|
+ if reflection.options[:autosave]
+ association = association_instance_get(reflection.name)
+ association && Array.wrap(association.target).any? { |a| a.changed_for_autosave? }
+ end
end
end
@@ -304,7 +303,8 @@ module ActiveRecord
def association_valid?(reflection, record)
return true if record.destroyed? || record.marked_for_destruction?
- unless valid = record.valid?
+ validation_context = self.validation_context unless [:create, :update].include?(self.validation_context)
+ unless valid = record.valid?(validation_context)
if reflection.options[:autosave]
record.errors.each do |attribute, message|
attribute = "#{reflection.name}.#{attribute}"
@@ -362,6 +362,7 @@ module ActiveRecord
raise ActiveRecord::Rollback unless saved
end
+ @new_record_before_save = false
end
# reconstruct the scope now that we know the owner's id
@@ -380,15 +381,16 @@ module ActiveRecord
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
+ elsif autosave != false
key = reflection.options[:primary_key] ? send(reflection.options[:primary_key]) : id
- if autosave != false && (autosave || new_record? || record_changed?(reflection, record, key))
+ if (autosave && record.changed_for_autosave?) || new_record? || record_changed?(reflection, record, key)
unless reflection.through_reflection
record[reflection.foreign_key] = key
end
diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb
index db4d5f0129..f978fbd0a4 100644
--- a/activerecord/lib/active_record/base.rb
+++ b/activerecord/lib/active_record/base.rb
@@ -9,16 +9,19 @@ require 'active_support/core_ext/class/delegating_attributes'
require 'active_support/core_ext/array/extract_options'
require 'active_support/core_ext/hash/deep_merge'
require 'active_support/core_ext/hash/slice'
+require 'active_support/core_ext/hash/transform_values'
require 'active_support/core_ext/string/behavior'
require 'active_support/core_ext/kernel/singleton_class'
require 'active_support/core_ext/module/introspection'
require 'active_support/core_ext/object/duplicable'
require 'active_support/core_ext/class/subclasses'
require 'arel'
+require 'active_record/attribute_decorators'
require 'active_record/errors'
require 'active_record/log_subscriber'
require 'active_record/explain_subscriber'
require 'active_record/relation/delegation'
+require 'active_record/attributes'
module ActiveRecord #:nodoc:
# = Active Record
@@ -138,6 +141,7 @@ module ActiveRecord #:nodoc:
#
# In addition to the basic accessors, query methods are also automatically available on the Active Record object.
# Query methods allow you to test whether an attribute value is present.
+ # For numeric values, present is defined as non-zero.
#
# 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:
@@ -217,25 +221,9 @@ module ActiveRecord #:nodoc:
#
# == Single table inheritance
#
- # Active Record allows inheritance by storing the name of the class in a column that by
- # default is named "type" (can be changed by overwriting <tt>Base.inheritance_column</tt>).
- # This means that an inheritance looking like this:
- #
- # class Company < ActiveRecord::Base; end
- # class Firm < Company; end
- # class Client < Company; end
- # class PriorityClient < Client; end
- #
- # 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.
- #
- # 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
- # for differentiating between them or reloading the right type with find.
- #
- # Note, all the attributes for all the cases are kept in the same table. Read more:
- # http://www.martinfowler.com/eaaCatalog/singleTableInheritance.html
+ # Active Record allows inheritance by storing the name of the class in a
+ # column that is named "type" by default. See ActiveRecord::Inheritance for
+ # more details.
#
# == Connection to multiple databases in different models
#
@@ -306,11 +294,13 @@ module ActiveRecord #:nodoc:
include Integration
include Validations
include CounterCache
+ include Attributes
+ include AttributeDecorators
include Locking::Optimistic
include Locking::Pessimistic
include AttributeMethods
- include Timestamp
include Callbacks
+ include Timestamp
include Associations
include ActiveModel::SecurePassword
include AutosaveAssociation
diff --git a/activerecord/lib/active_record/coders/json.rb b/activerecord/lib/active_record/coders/json.rb
new file mode 100644
index 0000000000..75d3bfe625
--- /dev/null
+++ b/activerecord/lib/active_record/coders/json.rb
@@ -0,0 +1,13 @@
+module ActiveRecord
+ module Coders # :nodoc:
+ class JSON # :nodoc:
+ def self.dump(obj)
+ ActiveSupport::JSON.encode(obj)
+ end
+
+ def self.load(json)
+ ActiveSupport::JSON.decode(json) unless json.nil?
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
index db80c0faee..cb75070e3a 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
@@ -364,7 +364,7 @@ module ActiveRecord
conn.expire
end
- release conn, owner
+ release owner
@available.add conn
end
@@ -377,7 +377,7 @@ module ActiveRecord
@connections.delete conn
@available.delete conn
- release conn, conn.owner
+ release conn.owner
@available.add checkout_new_connection if @available.any_waiting?
end
@@ -425,7 +425,7 @@ module ActiveRecord
end
end
- def release(conn, owner)
+ def release(owner)
thread_id = owner.object_id
@reserved_connections.delete thread_id
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 bc47412405..98e96099cb 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
@@ -193,7 +193,7 @@ module ActiveRecord
# * You are creating a nested (savepoint) transaction
#
# The mysql, mysql2 and postgresql adapters support setting the transaction
- # isolation level. However, support is disabled for mysql versions below 5,
+ # isolation level. However, support is disabled for MySQL versions below 5,
# because they are affected by a bug[http://bugs.mysql.com/bug.php?id=39170]
# which means the isolation level gets persisted outside the transaction.
def transaction(options = {})
@@ -203,58 +203,30 @@ module ActiveRecord
if options[:isolation]
raise ActiveRecord::TransactionIsolationError, "cannot set isolation when joining a transaction"
end
-
yield
else
- within_new_transaction(options) { yield }
+ transaction_manager.within_new_transaction(options) { yield }
end
rescue ActiveRecord::Rollback
# rollbacks are silently swallowed
end
- def within_new_transaction(options = {}) #:nodoc:
- transaction = begin_transaction(options)
- yield
- rescue Exception => error
- rollback_transaction if transaction
- raise
- ensure
- begin
- commit_transaction unless error
- rescue Exception
- rollback_transaction
- raise
- end
- end
+ attr_reader :transaction_manager #:nodoc:
- def current_transaction #:nodoc:
- @transaction
- end
+ delegate :within_new_transaction, :open_transactions, :current_transaction, :begin_transaction, :commit_transaction, :rollback_transaction, to: :transaction_manager
def transaction_open?
- @transaction.open?
- end
-
- def begin_transaction(options = {}) #:nodoc:
- @transaction = @transaction.begin(options)
- end
-
- def commit_transaction #:nodoc:
- @transaction = @transaction.commit
- end
-
- def rollback_transaction #:nodoc:
- @transaction = @transaction.rollback
+ current_transaction.open?
end
def reset_transaction #:nodoc:
- @transaction = ClosedTransaction.new(self)
+ @transaction_manager = TransactionManager.new(self)
end
# Register a record with the current transaction so that its after_commit and after_rollback callbacks
# can be called.
def add_transaction_record(record)
- @transaction.add_record(record)
+ current_transaction.add_record(record)
end
# Begins the transaction (and turns off auto-committing).
@@ -334,8 +306,8 @@ module ActiveRecord
end
# The default strategy for an UPDATE with joins is to use a subquery. This doesn't work
- # on mysql (even when aliasing the tables), but mysql allows using JOIN directly in
- # an UPDATE statement, so in the mysql adapters we redefine this to do that.
+ # on MySQL (even when aliasing the tables), but MySQL allows using JOIN directly in
+ # an UPDATE statement, so in the MySQL adapters we redefine this to do that.
def join_to_update(update, select) #:nodoc:
key = update.key
subselect = subquery_for(key, select)
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
index 75501852ed..eb88845913 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
@@ -9,34 +9,11 @@ module ActiveRecord
# records are quoted as their primary key
return value.quoted_id if value.respond_to?(:quoted_id)
- case value
- when String, ActiveSupport::Multibyte::Chars
- value = value.to_s
- return "'#{quote_string(value)}'" unless column
-
- case column.type
- when :integer then value.to_i.to_s
- when :float then value.to_f.to_s
- else
- "'#{quote_string(value)}'"
- end
-
- when true, false
- if column && column.type == :integer
- value ? '1' : '0'
- else
- value ? quoted_true : quoted_false
- end
- # BigDecimals need to be put in a non-normalized form and quoted.
- when nil then "NULL"
- when BigDecimal then value.to_s('F')
- when Numeric, ActiveSupport::Duration then value.to_s
- when Date, Time then "'#{quoted_date(value)}'"
- when Symbol then "'#{quote_string(value.to_s)}'"
- when Class then "'#{value.to_s}'"
- else
- "'#{quote_string(YAML.dump(value))}'"
+ if column
+ value = column.cast_type.type_cast_for_database(value)
end
+
+ _quote(value)
end
# Cast a +value+ to a type that the database understands. For example,
@@ -47,34 +24,14 @@ module ActiveRecord
return value.id
end
- case value
- when String, ActiveSupport::Multibyte::Chars
- value = value.to_s
- return value unless column
-
- case column.type
- when :integer then value.to_i
- when :float then value.to_f
- else
- value
- end
-
- when true, false
- if column && column.type == :integer
- value ? 1 : 0
- else
- value ? 't' : 'f'
- end
- # BigDecimals need to be put in a non-normalized form and quoted.
- when nil then nil
- when BigDecimal then value.to_s('F')
- when Numeric then value
- when Date, Time then quoted_date(value)
- when Symbol then value.to_s
- else
- to_type = column ? " to #{column.type}" : ""
- raise TypeError, "can't cast #{value.class}#{to_type}"
+ if column
+ value = column.cast_type.type_cast_for_database(value)
end
+
+ _type_cast(value)
+ rescue TypeError
+ to_type = column ? " to #{column.type}" : ""
+ raise TypeError, "can't cast #{value.class}#{to_type}"
end
# Quotes a string, escaping any ' (single quote) and \ (backslash)
@@ -99,7 +56,7 @@ module ActiveRecord
# 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
+ # We override this in the sqlite3 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}")
@@ -109,10 +66,18 @@ module ActiveRecord
"'t'"
end
+ def unquoted_true
+ 't'
+ end
+
def quoted_false
"'f'"
end
+ def unquoted_false
+ 'f'
+ end
+
def quoted_date(value)
if value.acts_like?(:time)
zone_conversion_method = ActiveRecord::Base.default_timezone == :utc ? :getutc : :getlocal
@@ -124,6 +89,45 @@ module ActiveRecord
value.to_s(:db)
end
+
+ private
+
+ def types_which_need_no_typecasting
+ [nil, Numeric, String]
+ end
+
+ def _quote(value)
+ case value
+ when String, ActiveSupport::Multibyte::Chars, Type::Binary::Data
+ "'#{quote_string(value.to_s)}'"
+ when true then quoted_true
+ when false then quoted_false
+ when nil then "NULL"
+ # BigDecimals need to be put in a non-normalized form and quoted.
+ when BigDecimal then value.to_s('F')
+ when Numeric, ActiveSupport::Duration then value.to_s
+ when Date, Time then "'#{quoted_date(value)}'"
+ when Symbol then "'#{quote_string(value.to_s)}'"
+ when Class then "'#{value.to_s}'"
+ else
+ "'#{quote_string(YAML.dump(value))}'"
+ end
+ end
+
+ def _type_cast(value)
+ case value
+ when Symbol, ActiveSupport::Multibyte::Chars, Type::Binary::Data
+ value.to_s
+ when true then unquoted_true
+ when false then unquoted_false
+ # BigDecimals need to be put in a non-normalized form and quoted.
+ when BigDecimal then value.to_s('F')
+ when Date, Time then quoted_date(value)
+ when *types_which_need_no_typecasting
+ value
+ else raise TypeError
+ end
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb
index 47fe501752..adad6cd542 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb
@@ -23,6 +23,8 @@ module ActiveRecord
def visit_AlterTable(o)
sql = "ALTER TABLE #{quote_table_name(o.name)} "
sql << o.adds.map { |col| visit_AddColumn col }.join(' ')
+ sql << o.foreign_key_adds.map { |fk| visit_AddForeignKey fk }.join(' ')
+ sql << o.foreign_key_drops.map { |fk| visit_DropForeignKey fk }.join(' ')
end
def visit_ColumnDefinition(o)
@@ -41,6 +43,21 @@ module ActiveRecord
create_sql
end
+ def visit_AddForeignKey(o)
+ sql = <<-SQL.strip_heredoc
+ ADD CONSTRAINT #{quote_column_name(o.name)}
+ FOREIGN KEY (#{quote_column_name(o.column)})
+ REFERENCES #{quote_table_name(o.to_table)} (#{quote_column_name(o.primary_key)})
+ SQL
+ sql << " #{action_sql('DELETE', o.on_delete)}" if o.on_delete
+ sql << " #{action_sql('UPDATE', o.on_update)}" if o.on_update
+ sql
+ end
+
+ def visit_DropForeignKey(name)
+ "DROP CONSTRAINT #{quote_column_name(name)}"
+ end
+
def column_options(o)
column_options = {}
column_options[:null] = o.null unless o.null.nil?
@@ -77,6 +94,7 @@ module ActiveRecord
def quote_value(value, column)
column.sql_type ||= type_to_sql(column.type, column.limit, column.precision, column.scale)
+ column.cast_type ||= type_for_column(column)
@conn.quote(value, column)
end
@@ -84,6 +102,23 @@ module ActiveRecord
def options_include_default?(options)
options.include?(:default) && !(options[:null] == false && options[:default].nil?)
end
+
+ def action_sql(action, dependency)
+ case dependency
+ when :nullify then "ON #{action} SET NULL"
+ when :cascade then "ON #{action} CASCADE"
+ when :restrict then "ON #{action} RESTRICT"
+ else
+ raise ArgumentError, <<-MSG.strip_heredoc
+ '#{dependency}' is not supported for :on_update or :on_delete.
+ Supported values are: :nullify, :cascade, :restrict
+ MSG
+ end
+ end
+
+ def type_for_column(column)
+ @conn.lookup_cast_type(column.sql_type)
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
index 71c3a4378b..e44ccb7d81 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
@@ -15,7 +15,7 @@ module ActiveRecord
# are typically created by methods in TableDefinition, and added to the
# +columns+ attribute of said TableDefinition object, in order to be used
# for generating a number of table creation or table changing SQL statements.
- class ColumnDefinition < Struct.new(:name, :type, :limit, :precision, :scale, :default, :null, :first, :after, :primary_key, :sql_type) #:nodoc:
+ class ColumnDefinition < Struct.new(:name, :type, :limit, :precision, :scale, :default, :null, :first, :after, :primary_key, :sql_type, :cast_type) #:nodoc:
def primary_key?
primary_key || type.to_sym == :primary_key
@@ -25,6 +25,37 @@ module ActiveRecord
class ChangeColumnDefinition < Struct.new(:column, :type, :options) #:nodoc:
end
+ class ForeignKeyDefinition < Struct.new(:from_table, :to_table, :options) #:nodoc:
+ def name
+ options[:name]
+ end
+
+ def column
+ options[:column]
+ end
+
+ def primary_key
+ options[:primary_key] || default_primary_key
+ end
+
+ def on_delete
+ options[:on_delete]
+ end
+
+ def on_update
+ options[:on_update]
+ end
+
+ def custom_primary_key?
+ options[:primary_key] != default_primary_key
+ end
+
+ private
+ def default_primary_key
+ "id"
+ end
+ end
+
# Represents the schema of an SQL table in an abstract way. This class
# provides methods for manipulating the schema representation.
#
@@ -99,9 +130,11 @@ module ActiveRecord
# Specifies the precision for a <tt>:decimal</tt> column.
# * <tt>:scale</tt> -
# Specifies the scale for a <tt>:decimal</tt> column.
+ # * <tt>:index</tt> -
+ # Create an index for the column. Can be either <tt>true</tt> or an options hash.
#
- # For clarity's sake: the precision is the number of significant digits,
- # while the scale is the number of digits that can be stored following
+ # Note: The precision is the total number of significant digits
+ # and the scale is the number of digits that can be stored following
# the decimal point. For example, the number 123.45 has a precision of 5
# and a scale of 2. A decimal with a precision of 5 and a scale of 2 can
# range from -999.99 to 999.99.
@@ -123,17 +156,8 @@ module ActiveRecord
# Default is (38,0).
# * DB2: <tt>:precision</tt> [1..63], <tt>:scale</tt> [0..62].
# Default unknown.
- # * Firebird: <tt>:precision</tt> [1..18], <tt>:scale</tt> [0..18].
- # Default (9,0). Internal types NUMERIC and DECIMAL have different
- # storage rules, decimal being better.
- # * FrontBase?: <tt>:precision</tt> [1..38], <tt>:scale</tt> [0..38].
- # Default (38,0). WARNING Max <tt>:precision</tt>/<tt>:scale</tt> for
- # NUMERIC is 19, and DECIMAL is 38.
# * SqlServer?: <tt>:precision</tt> [1..38], <tt>:scale</tt> [0..38].
# Default (38,0).
- # * Sybase: <tt>:precision</tt> [1..38], <tt>:scale</tt> [0..38].
- # Default (38,0).
- # * OpenBase?: Documentation unclear. Claims storage in <tt>double</tt>.
#
# This method returns <tt>self</tt>.
#
@@ -172,18 +196,21 @@ module ActiveRecord
# What can be written like this with the regular calls to column:
#
# 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
+ # t.column :shop_id, :integer
+ # t.column :creator_id, :integer
+ # t.column :item_number, :string
+ # t.column :name, :string, default: "Untitled"
+ # t.column :value, :string, default: "Untitled"
+ # t.column :created_at, :datetime
+ # t.column :updated_at, :datetime
# end
+ # add_index :products, :item_number
#
# can also be written as follows using the short-hand:
#
# create_table :products do |t|
# t.integer :shop_id, :creator_id
+ # t.string :item_number, index: true
# t.string :name, :value, default: "Untitled"
# t.timestamps
# end
@@ -219,6 +246,8 @@ module ActiveRecord
raise ArgumentError, "you can't redefine the primary key column '#{name}'. To define a custom primary key, pass { id: false } to create_table."
end
+ index_options = options.delete(:index)
+ index(name, index_options.is_a?(Hash) ? index_options : {}) if index_options
@columns_hash[name] = new_column_definition(name, type, options)
self
end
@@ -251,12 +280,22 @@ module ActiveRecord
column(:updated_at, :datetime, options)
end
+ # Adds a reference. Optionally adds a +type+ column, if <tt>:polymorphic</tt> option is provided.
+ # <tt>references</tt> and <tt>belongs_to</tt> are acceptable. The reference column will be an +integer+
+ # by default, the <tt>:type</tt> option can be used to specify a different type.
+ #
+ # t.references(:user)
+ # t.references(:user, type: "string")
+ # t.belongs_to(:supplier, polymorphic: true)
+ #
+ # See SchemaStatements#add_reference
def references(*args)
options = args.extract_options!
polymorphic = options.delete(:polymorphic)
index_options = options.delete(:index)
+ type = options.delete(:type) || :integer
args.each do |col|
- column("#{col}_id", :integer, options)
+ column("#{col}_id", type, options)
column("#{col}_type", :string, polymorphic.is_a?(Hash) ? polymorphic : options) if polymorphic
index(polymorphic ? %w(id type).map { |t| "#{col}_#{t}" } : "#{col}_id", index_options.is_a?(Hash) ? index_options : {}) if index_options
end
@@ -264,6 +303,7 @@ module ActiveRecord
alias :belongs_to :references
def new_column_definition(name, type, options) # :nodoc:
+ type = aliased_types[type] || type
column = create_column_definition name, type
limit = options.fetch(:limit) do
native[type][:limit] if native[type].is_a?(Hash)
@@ -294,18 +334,36 @@ module ActiveRecord
def native
@native
end
+
+ def aliased_types
+ HashWithIndifferentAccess.new(
+ timestamp: :datetime,
+ )
+ end
end
class AlterTable # :nodoc:
attr_reader :adds
+ attr_reader :foreign_key_adds
+ attr_reader :foreign_key_drops
def initialize(td)
@td = td
@adds = []
+ @foreign_key_adds = []
+ @foreign_key_drops = []
end
def name; @td.name; end
+ def add_foreign_key(to_table, options)
+ @foreign_key_adds << ForeignKeyDefinition.new(name, to_table, options)
+ end
+
+ def drop_foreign_key(name)
+ @foreign_key_drops << name
+ end
+
def add_column(name, type, options)
name = name.to_s
type = type.to_sym
@@ -452,11 +510,14 @@ module ActiveRecord
end
# Adds a reference. Optionally adds a +type+ column, if <tt>:polymorphic</tt> option is provided.
- # <tt>references</tt> and <tt>belongs_to</tt> are acceptable.
+ # <tt>references</tt> and <tt>belongs_to</tt> are acceptable. The reference column will be an +integer+
+ # by default, the <tt>:type</tt> option can be used to specify a different type.
#
# t.references(:user)
+ # t.references(:user, type: "string")
# t.belongs_to(:supplier, polymorphic: true)
#
+ # See SchemaStatements#add_reference
def references(*args)
options = args.extract_options!
args.each do |ref_name|
@@ -471,6 +532,7 @@ module ActiveRecord
# t.remove_references(:user)
# t.remove_belongs_to(:supplier, polymorphic: true)
#
+ # See SchemaStatements#remove_reference
def remove_references(*args)
options = args.extract_options!
args.each do |ref_name|
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 cdf0cbe218..9bd0401e40 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb
@@ -1,5 +1,3 @@
-require 'ipaddr'
-
module ActiveRecord
module ConnectionAdapters # :nodoc:
# The goal of this module is to move Adapter specific column
@@ -20,19 +18,13 @@ module ActiveRecord
def prepare_column_options(column, types)
spec = {}
spec[:name] = column.name.inspect
-
- # AR has an optimization which handles zero-scale decimals as integers. This
- # code ensures that the dumper still dumps the column as a decimal.
- spec[:type] = if column.type == :integer && /^(numeric|decimal)/ =~ column.sql_type
- 'decimal'
- else
- column.type.to_s
- end
- spec[:limit] = column.limit.inspect if column.limit != types[column.type][:limit] && spec[:type] != 'decimal'
+ spec[:type] = column.type.to_s
+ spec[:limit] = column.limit.inspect if column.limit != types[column.type][:limit]
spec[:precision] = column.precision.inspect if column.precision
spec[:scale] = column.scale.inspect if column.scale
spec[:null] = 'false' unless column.null
- spec[:default] = default_string(column.default) if column.has_default?
+ spec[:default] = schema_default(column) if column.has_default?
+ spec.delete(:default) if spec[:default].nil?
spec
end
@@ -43,28 +35,12 @@ module ActiveRecord
private
- def default_string(value)
- case value
- when BigDecimal
- 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
+ def schema_default(column)
+ default = column.type_cast_from_database(column.default)
+ unless default.nil?
+ column.type_cast_for_schema(default)
end
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
index aa99822389..10753defc2 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
@@ -71,7 +71,8 @@ module ActiveRecord
# 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 &&
+ column_name = column_name.to_s
+ columns(table_name).any?{ |c| c.name == column_name &&
(!type || c.type == type) &&
(!options.key?(:limit) || c.limit == options[:limit]) &&
(!options.key?(:precision) || c.precision == options[:precision]) &&
@@ -601,12 +602,18 @@ module ActiveRecord
end
# Adds a reference. Optionally adds a +type+ column, if <tt>:polymorphic</tt> option is provided.
+ # The reference column is an +integer+ by default, the <tt>:type</tt> option can be used to specify
+ # a different type.
# <tt>add_reference</tt> and <tt>add_belongs_to</tt> are acceptable.
#
- # ====== Create a user_id column
+ # ====== Create a user_id integer column
#
# add_reference(:products, :user)
#
+ # ====== Create a user_id string column
+ #
+ # add_reference(:products, :user, type: :string)
+ #
# ====== Create a supplier_id and supplier_type columns
#
# add_belongs_to(:products, :supplier, polymorphic: true)
@@ -618,7 +625,8 @@ module ActiveRecord
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)
+ type = options.delete(:type) || :integer
+ add_column(table_name, "#{ref_name}_id", type, options)
add_column(table_name, "#{ref_name}_type", :string, polymorphic.is_a?(Hash) ? polymorphic : options) if polymorphic
add_index(table_name, polymorphic ? %w[id type].map{ |t| "#{ref_name}_#{t}" } : "#{ref_name}_id", index_options.is_a?(Hash) ? index_options : {}) if index_options
end
@@ -641,6 +649,115 @@ module ActiveRecord
end
alias :remove_belongs_to :remove_reference
+ # Returns an array of foreign keys for the given table.
+ # The foreign keys are represented as +ForeignKeyDefinition+ objects.
+ def foreign_keys(table_name)
+ raise NotImplementedError, "foreign_keys is not implemented"
+ end
+
+ # Adds a new foreign key. +from_table+ is the table with the key column,
+ # +to_table+ contains the referenced primary key.
+ #
+ # The foreign key will be named after the following pattern: <tt>fk_rails_<identifier></tt>.
+ # +identifier+ is a 10 character long random string. A custom name can be specified with
+ # the <tt>:name</tt> option.
+ #
+ # ====== Creating a simple foreign key
+ #
+ # add_foreign_key :articles, :authors
+ #
+ # generates:
+ #
+ # ALTER TABLE "articles" ADD CONSTRAINT articles_author_id_fk FOREIGN KEY ("author_id") REFERENCES "authors" ("id")
+ #
+ # ====== Creating a foreign key on a specific column
+ #
+ # add_foreign_key :articles, :users, column: :author_id, primary_key: "lng_id"
+ #
+ # generates:
+ #
+ # ALTER TABLE "articles" ADD CONSTRAINT fk_rails_58ca3d3a82 FOREIGN KEY ("author_id") REFERENCES "users" ("lng_id")
+ #
+ # ====== Creating a cascading foreign key
+ #
+ # add_foreign_key :articles, :authors, on_delete: :cascade
+ #
+ # generates:
+ #
+ # ALTER TABLE "articles" ADD CONSTRAINT articles_author_id_fk FOREIGN KEY ("author_id") REFERENCES "authors" ("id") ON DELETE CASCADE
+ #
+ # The +options+ hash can include the following keys:
+ # [<tt>:column</tt>]
+ # The foreign key column name on +from_table+. Defaults to <tt>to_table.singularize + "_id"</tt>
+ # [<tt>:primary_key</tt>]
+ # The primary key column name on +to_table+. Defaults to +id+.
+ # [<tt>:name</tt>]
+ # The constraint name. Defaults to <tt>fk_rails_<identifier></tt>.
+ # [<tt>:on_delete</tt>]
+ # Action that happens <tt>ON DELETE</tt>. Valid values are +:nullify+, +:cascade:+ and +:restrict+
+ # [<tt>:on_update</tt>]
+ # Action that happens <tt>ON UPDATE</tt>. Valid values are +:nullify+, +:cascade:+ and +:restrict+
+ def add_foreign_key(from_table, to_table, options = {})
+ return unless supports_foreign_keys?
+
+ options[:column] ||= foreign_key_column_for(to_table)
+
+ options = {
+ column: options[:column],
+ primary_key: options[:primary_key],
+ name: foreign_key_name(from_table, options),
+ on_delete: options[:on_delete],
+ on_update: options[:on_update]
+ }
+ at = create_alter_table from_table
+ at.add_foreign_key to_table, options
+
+ execute schema_creation.accept(at)
+ end
+
+ # Removes the given foreign key from the table.
+ #
+ # Removes the foreign key on +accounts.branch_id+.
+ #
+ # remove_foreign_key :accounts, :branches
+ #
+ # Removes the foreign key on +accounts.owner_id+.
+ #
+ # remove_foreign_key :accounts, column: :owner_id
+ #
+ # Removes the foreign key named +special_fk_name+ on the +accounts+ table.
+ #
+ # remove_foreign_key :accounts, name: :special_fk_name
+ #
+ def remove_foreign_key(from_table, options_or_to_table = {})
+ return unless supports_foreign_keys?
+
+ if options_or_to_table.is_a?(Hash)
+ options = options_or_to_table
+ else
+ options = { column: foreign_key_column_for(options_or_to_table) }
+ end
+
+ fk_name_to_delete = options.fetch(:name) do
+ fk_to_delete = foreign_keys(from_table).detect {|fk| fk.column == options[:column] }
+
+ if fk_to_delete
+ fk_to_delete.name
+ else
+ raise ArgumentError, "Table '#{from_table}' has no foreign key on column '#{options[:column]}'"
+ end
+ end
+
+ at = create_alter_table from_table
+ at.drop_foreign_key fk_name_to_delete
+
+ execute schema_creation.accept(at)
+ end
+
+ def foreign_key_column_for(table_name) # :nodoc:
+ "#{table_name.to_s.singularize}_id"
+ end
+
def dump_schema_information #:nodoc:
sm_table = ActiveRecord::Migrator.schema_migrations_table_name
@@ -787,7 +904,7 @@ module ActiveRecord
return option_strings
end
- # Overridden by the mysql adapter for supporting index lengths
+ # Overridden by the MySQL adapter for supporting index lengths
def quoted_columns_for_index(column_names, options = {})
option_strings = Hash[column_names.map {|name| [name, '']}]
@@ -851,6 +968,12 @@ module ActiveRecord
def create_alter_table(name)
AlterTable.new create_table_definition(name, false, {})
end
+
+ def foreign_key_name(table_name, options) # :nodoc:
+ options.fetch(:name) do
+ "fk_rails_#{SecureRandom.hex(5)}"
+ end
+ end
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 bc4884b538..3a266512a9 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb
@@ -1,5 +1,59 @@
module ActiveRecord
module ConnectionAdapters
+ class TransactionManager #:nodoc:
+ def initialize(connection)
+ @stack = []
+ @connection = connection
+ end
+
+ def begin_transaction(options = {})
+ transaction_class = @stack.empty? ? RealTransaction : SavepointTransaction
+ transaction = transaction_class.new(@connection, current_transaction, options)
+
+ @stack.push(transaction)
+ transaction
+ end
+
+ def commit_transaction
+ @stack.pop.commit
+ end
+
+ def rollback_transaction
+ @stack.pop.rollback
+ end
+
+ def within_new_transaction(options = {})
+ transaction = begin_transaction options
+ yield
+ rescue Exception => error
+ transaction.rollback if transaction
+ raise
+ ensure
+ begin
+ transaction.commit unless error
+ rescue Exception
+ transaction.rollback
+ raise
+ ensure
+ @stack.pop if transaction
+ end
+ end
+
+ def open_transactions
+ @stack.size
+ end
+
+ def current_transaction
+ @stack.last || closed_transaction
+ end
+
+ private
+
+ def closed_transaction
+ @closed_transaction ||= ClosedTransaction.new(@connection)
+ end
+ end
+
class Transaction #:nodoc:
attr_reader :connection
@@ -11,6 +65,10 @@ module ActiveRecord
def state
@state
end
+
+ def savepoint_name
+ nil
+ end
end
class TransactionState
@@ -78,45 +136,28 @@ module ActiveRecord
@parent = parent
@records = []
- @finishing = false
@joinable = options.fetch(:joinable, true)
end
- # 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?
- @finishing
- end
def joinable?
- @joinable && !finishing?
+ @joinable
end
def number
- if finishing?
- parent.number
- else
- parent.number + 1
- end
+ parent.number + 1
end
def begin(options = {})
- if finishing?
- parent.begin
- else
- SavepointTransaction.new(connection, self, options)
- end
+ SavepointTransaction.new(connection, self, options)
end
def rollback
- @finishing = true
perform_rollback
parent
end
def commit
- @finishing = true
perform_commit
parent
end
@@ -183,24 +224,29 @@ module ActiveRecord
end
class SavepointTransaction < OpenTransaction #:nodoc:
+ attr_reader :savepoint_name
+
def initialize(connection, parent, options = {})
if options[:isolation]
raise ActiveRecord::TransactionIsolationError, "cannot set transaction isolation in a nested transaction"
end
super
- connection.create_savepoint
+
+ # Savepoint name only counts the Savepoint transactions, so we need to subtract 1
+ @savepoint_name = "active_record_#{number - 1}"
+ connection.create_savepoint(@savepoint_name)
end
def perform_rollback
- connection.rollback_to_savepoint
+ connection.rollback_to_savepoint(@savepoint_name)
rollback_records
end
def perform_commit
@state.set_state(:committed)
@state.parent = parent.state
- connection.release_savepoint
+ connection.release_savepoint(@savepoint_name)
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 78343cf4f5..99c728814a 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
@@ -1,6 +1,7 @@
require 'date'
require 'bigdecimal'
require 'bigdecimal/util'
+require 'active_record/type'
require 'active_support/core_ext/benchmark'
require 'active_record/connection_adapters/schema_cache'
require 'active_record/connection_adapters/abstract/schema_dumper'
@@ -13,7 +14,10 @@ module ActiveRecord
module ConnectionAdapters # :nodoc:
extend ActiveSupport::Autoload
- autoload :Column
+ autoload_at 'active_record/connection_adapters/column' do
+ autoload :Column
+ autoload :NullColumn
+ end
autoload :ConnectionSpecification
autoload_at 'active_record/connection_adapters/abstract/schema_definitions' do
@@ -41,6 +45,7 @@ module ActiveRecord
end
autoload_at 'active_record/connection_adapters/abstract/transaction' do
+ autoload :TransactionManager
autoload :ClosedTransaction
autoload :RealTransaction
autoload :SavepointTransaction
@@ -120,7 +125,7 @@ module ActiveRecord
end
def collector
- if @prepared_statements
+ if prepared_statements
SQLString.new
else
BindCollector.new
@@ -229,6 +234,11 @@ module ActiveRecord
false
end
+ # Does this adapter support creating foreign key constraints?
+ def supports_foreign_keys?
+ false
+ end
+
# This is meant to be implemented by the adapters that support extensions
def disable_extension(name)
end
@@ -324,10 +334,6 @@ module ActiveRecord
@connection
end
- def open_transactions
- @transaction.number
- end
-
def create_savepoint(name = nil)
end
@@ -352,7 +358,7 @@ module ActiveRecord
end
def current_savepoint_name
- "active_record_#{open_transactions}"
+ current_transaction.savepoint_name
end
# Check the connection back in to the connection pool
@@ -360,8 +366,80 @@ module ActiveRecord
pool.checkin self
end
+ def type_map # :nodoc:
+ @type_map ||= Type::TypeMap.new.tap do |mapping|
+ initialize_type_map(mapping)
+ end
+ end
+
+ def new_column(name, default, cast_type, sql_type = nil, null = true)
+ Column.new(name, default, cast_type, sql_type, null)
+ end
+
+ def lookup_cast_type(sql_type) # :nodoc:
+ type_map.lookup(sql_type)
+ end
+
protected
+ def initialize_type_map(m) # :nodoc:
+ register_class_with_limit m, %r(boolean)i, Type::Boolean
+ register_class_with_limit m, %r(char)i, Type::String
+ register_class_with_limit m, %r(binary)i, Type::Binary
+ register_class_with_limit m, %r(text)i, Type::Text
+ register_class_with_limit m, %r(date)i, Type::Date
+ register_class_with_limit m, %r(time)i, Type::Time
+ register_class_with_limit m, %r(datetime)i, Type::DateTime
+ register_class_with_limit m, %r(float)i, Type::Float
+ register_class_with_limit m, %r(int)i, Type::Integer
+
+ m.alias_type %r(blob)i, 'binary'
+ m.alias_type %r(clob)i, 'text'
+ m.alias_type %r(timestamp)i, 'datetime'
+ m.alias_type %r(numeric)i, 'decimal'
+ m.alias_type %r(number)i, 'decimal'
+ m.alias_type %r(double)i, 'float'
+
+ m.register_type(%r(decimal)i) do |sql_type|
+ scale = extract_scale(sql_type)
+ precision = extract_precision(sql_type)
+
+ if scale == 0
+ # FIXME: Remove this class as well
+ Type::DecimalWithoutScale.new(precision: precision)
+ else
+ Type::Decimal.new(precision: precision, scale: scale)
+ end
+ end
+ end
+
+ def reload_type_map # :nodoc:
+ type_map.clear
+ initialize_type_map(type_map)
+ end
+
+ def register_class_with_limit(mapping, key, klass) # :nodoc:
+ mapping.register_type(key) do |*args|
+ limit = extract_limit(args.last)
+ klass.new(limit: limit)
+ end
+ end
+
+ def extract_scale(sql_type) # :nodoc:
+ case sql_type
+ when /\((\d+)\)/ then 0
+ when /\((\d+)(,(\d+))\)/ then $3.to_i
+ end
+ end
+
+ def extract_precision(sql_type) # :nodoc:
+ $1.to_i if sql_type =~ /\((\d+)(,\d+)?\)/
+ end
+
+ def extract_limit(sql_type) # :nodoc:
+ $1.to_i if sql_type =~ /\((.*)\)/
+ end
+
def translate_exception_class(e, sql)
message = "#{e.class.name}: #{e.message}: #{sql}"
@logger.error message if @logger
@@ -388,7 +466,13 @@ module ActiveRecord
end
def without_prepared_statement?(binds)
- !@prepared_statements || binds.empty?
+ !prepared_statements || binds.empty?
+ end
+
+ def column_for(table_name, column_name) # :nodoc:
+ column_name = column_name.to_s
+ columns(table_name).detect { |c| c.name == column_name } ||
+ raise(ActiveRecordError, "No such column: #{table_name}.#{column_name}")
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 75c58ac7d9..e5417a9556 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
@@ -12,6 +12,10 @@ module ActiveRecord
private
+ def visit_DropForeignKey(name)
+ "DROP FOREIGN KEY #{name}"
+ end
+
def visit_TableDefinition(o)
name = o.name
create_sql = "CREATE#{' TEMPORARY' if o.temporary} TABLE #{quote_table_name(name)} "
@@ -56,29 +60,25 @@ module ActiveRecord
class Column < ConnectionAdapters::Column # :nodoc:
attr_reader :collation, :strict, :extra
- def initialize(name, default, sql_type = nil, null = true, collation = nil, strict = false, extra = "")
+ def initialize(name, default, cast_type, sql_type = nil, null = true, collation = nil, strict = false, extra = "")
@strict = strict
@collation = collation
@extra = extra
- super(name, default, sql_type, null)
+ super(name, default, cast_type, sql_type, null)
+ assert_valid_default(default)
+ extract_default
end
- def extract_default(default)
+ def extract_default
if blob_or_text_column?
- if default.blank?
- null || strict ? nil : ''
- else
- raise ArgumentError, "#{type} columns cannot have a default value: #{default.inspect}"
- end
- elsif missing_default_forged_as_empty_string?(default)
- nil
- else
- super
+ @default = null || strict ? nil : ''
+ elsif missing_default_forged_as_empty_string?(@default)
+ @default = nil
end
end
def has_default?
- return false if blob_or_text_column? #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
@@ -86,54 +86,12 @@ module ActiveRecord
sql_type =~ /blob/i || type == :text
end
- # Must return the relevant concrete adapter
- def adapter
- raise NotImplementedError
- end
-
def case_sensitive?
collation && !collation.match(/_ci$/)
end
private
- def simplified_type(field_type)
- return :boolean if adapter.emulate_booleans && field_type.downcase.index("tinyint(1)")
-
- case field_type
- when /enum/i, /set/i then :string
- when /year/i then :integer
- when /bit/i then :binary
- else
- super
- end
- end
-
- 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
- 255
- when /medium/i
- 16777215
- when /long/i
- 2147483647 # mysql only allows 2^31-1, not 2^32-1, somewhat inconsistently with the tiny/medium/normal cases
- else
- super # we could return 65535 here, but we leave it undecorated by default
- end
- when /^bigint/i; 8
- when /^int/i; 4
- when /^mediumint/i; 3
- when /^smallint/i; 2
- when /^tinyint/i; 1
- else
- super
- end
- end
-
# MySQL misreports NOT NULL column default when none is given.
# We can't detect this for columns which may have a legitimate ''
# default (string) but we can for others (integer, datetime, boolean,
@@ -144,6 +102,12 @@ module ActiveRecord
def missing_default_forged_as_empty_string?(default)
type != :string && !null && default == ''
end
+
+ def assert_valid_default(default)
+ if blob_or_text_column? && default.present?
+ raise ArgumentError, "#{type} columns cannot have a default value: #{default.inspect}"
+ end
+ end
end
##
@@ -173,7 +137,6 @@ module ActiveRecord
:float => { :name => "float" },
:decimal => { :name => "decimal" },
:datetime => { :name => "datetime" },
- :timestamp => { :name => "datetime" },
:time => { :name => "time" },
:date => { :name => "date" },
:binary => { :name => "blob" },
@@ -221,17 +184,6 @@ module ActiveRecord
true
end
- def type_cast(value, column)
- case value
- when TrueClass
- 1
- when FalseClass
- 0
- else
- super
- end
- end
-
# MySQL 4 technically support transaction isolation, but it is affected by a bug
# where the transaction level gets persisted for the whole session:
#
@@ -244,6 +196,10 @@ module ActiveRecord
true
end
+ def supports_foreign_keys?
+ true
+ end
+
def native_database_types
NATIVE_DATABASE_TYPES
end
@@ -260,12 +216,11 @@ module ActiveRecord
raise NotImplementedError
end
- # Overridden by the adapters to instantiate their specific Column type.
- def new_column(field, default, type, null, collation, extra = "") # :nodoc:
- Column.new(field, default, type, null, collation, extra)
+ def new_column(field, default, cast_type, sql_type = nil, null = true, collation = "", extra = "") # :nodoc:
+ Column.new(field, default, cast_type, sql_type, null, collation, strict_mode?, extra)
end
- # Must return the Mysql error number from the exception, if the exception has an
+ # Must return the MySQL error number from the exception, if the exception has an
# error number.
def error_number(exception) # :nodoc:
raise NotImplementedError
@@ -273,12 +228,9 @@ module ActiveRecord
# QUOTING ==================================================
- def quote(value, column = nil)
- 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")
+ def _quote(value) # :nodoc:
+ if value.is_a?(Type::Binary::Data)
+ "x'#{value.hex}'"
else
super
end
@@ -296,10 +248,18 @@ module ActiveRecord
QUOTED_TRUE
end
+ def unquoted_true
+ 1
+ end
+
def quoted_false
QUOTED_FALSE
end
+ def unquoted_false
+ 0
+ end
+
# REFERENTIAL INTEGRITY ====================================
def disable_referential_integrity #:nodoc:
@@ -315,6 +275,11 @@ module ActiveRecord
# DATABASE STATEMENTS ======================================
+ def clear_cache!
+ super
+ reload_type_map
+ end
+
# Executes the SQL statement in the context of this connection.
def execute(sql, name = nil)
log(sql, name) { @connection.query(sql) }
@@ -424,7 +389,7 @@ module ActiveRecord
end
def table_exists?(name)
- return false unless name
+ return false unless name.present?
return true if tables(nil, nil, name).any?
name = name.to_s
@@ -468,7 +433,9 @@ module ActiveRecord
execute_and_free(sql, 'SCHEMA') do |result|
each_hash(result).map do |field|
field_name = set_field_encoding(field[:Field])
- new_column(field_name, field[:Default], field[:Type], field[:Null] == "YES", field[:Collation], field[:Extra])
+ sql_type = field[:Type]
+ cast_type = lookup_cast_type(sql_type)
+ new_column(field_name, field[:Default], cast_type, sql_type, field[:Null] == "YES", field[:Collation], field[:Extra])
end
end
end
@@ -506,7 +473,7 @@ module ActiveRecord
end
def rename_index(table_name, old_name, new_name)
- if (version[0] == 5 && version[1] >= 7) || version[0] >= 6
+ if supports_rename_index?
execute "ALTER TABLE #{quote_table_name(table_name)} RENAME INDEX #{quote_table_name(old_name)} TO #{quote_table_name(new_name)}"
else
super
@@ -542,6 +509,34 @@ module ActiveRecord
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
+ def foreign_keys(table_name)
+ fk_info = select_all <<-SQL.strip_heredoc
+ SELECT fk.referenced_table_name as 'to_table'
+ ,fk.referenced_column_name as 'primary_key'
+ ,fk.column_name as 'column'
+ ,fk.constraint_name as 'name'
+ FROM information_schema.key_column_usage fk
+ WHERE fk.referenced_column_name is not null
+ AND fk.table_schema = '#{@config[:database]}'
+ AND fk.table_name = '#{table_name}'
+ SQL
+
+ create_table_info = select_one("SHOW CREATE TABLE #{quote_table_name(table_name)}")["Create Table"]
+
+ fk_info.map do |row|
+ options = {
+ column: row['column'],
+ name: row['name'],
+ primary_key: row['primary_key']
+ }
+
+ options[:on_update] = extract_foreign_key_action(create_table_info, row['name'], "UPDATE")
+ options[:on_delete] = extract_foreign_key_action(create_table_info, row['name'], "DELETE")
+
+ ForeignKeyDefinition.new(table_name, row['to_table'], options)
+ end
+ 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
@@ -642,6 +637,34 @@ module ActiveRecord
protected
+ def initialize_type_map(m) # :nodoc:
+ super
+ m.register_type(%r(enum)i) do |sql_type|
+ limit = sql_type[/^enum\((.+)\)/i, 1]
+ .split(',').map{|enum| enum.strip.length - 2}.max
+ Type::String.new(limit: limit)
+ end
+
+ m.register_type %r(tinytext)i, Type::Text.new(limit: 255)
+ m.register_type %r(tinyblob)i, Type::Binary.new(limit: 255)
+ m.register_type %r(mediumtext)i, Type::Text.new(limit: 16777215)
+ m.register_type %r(mediumblob)i, Type::Binary.new(limit: 16777215)
+ m.register_type %r(longtext)i, Type::Text.new(limit: 2147483647)
+ m.register_type %r(longblob)i, Type::Binary.new(limit: 2147483647)
+ m.register_type %r(^bigint)i, Type::Integer.new(limit: 8)
+ m.register_type %r(^int)i, Type::Integer.new(limit: 4)
+ m.register_type %r(^mediumint)i, Type::Integer.new(limit: 3)
+ m.register_type %r(^smallint)i, Type::Integer.new(limit: 2)
+ m.register_type %r(^tinyint)i, Type::Integer.new(limit: 1)
+ m.register_type %r(^float)i, Type::Float.new(limit: 24)
+ m.register_type %r(^double)i, Type::Float.new(limit: 53)
+
+ m.alias_type %r(tinyint\(1\))i, 'boolean' if emulate_booleans
+ m.alias_type %r(set)i, 'varchar'
+ m.alias_type %r(year)i, 'integer'
+ m.alias_type %r(bit)i, 'binary'
+ end
+
# MySQL is too stupid to create a temporary table for use subquery, so we have
# to give it some prompting in the form of a subsubquery. Ugh!
def subquery_for(key, select)
@@ -711,15 +734,13 @@ module ActiveRecord
end
def rename_column_sql(table_name, column_name, new_column_name)
- 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
+ column = column_for(table_name, column_name)
+ options = {
+ name: new_column_name,
+ default: column.default,
+ null: column.null,
+ auto_increment: column.extra == "auto_increment"
+ }
current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'", 'SCHEMA')["Type"]
schema_creation.accept ChangeColumnDefinition.new column, current_type, options
@@ -753,34 +774,39 @@ module ActiveRecord
private
+ def version
+ @version ||= full_version.scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map { |v| v.to_i }
+ end
+
+ def mariadb?
+ full_version =~ /mariadb/i
+ end
+
def supports_views?
version[0] >= 5
end
- def column_for(table_name, column_name)
- unless column = columns(table_name).find { |c| c.name == column_name.to_s }
- raise "No such column: #{table_name}.#{column_name}"
- end
- column
+ def supports_rename_index?
+ mariadb? ? false : (version[0] == 5 && version[1] >= 7) || version[0] >= 6
end
def configure_connection
- variables = @config[:variables] || {}
+ variables = @config.fetch(:variables, {}).stringify_keys
# By default, MySQL 'where id is null' selects the last inserted id.
# Turn this off. http://dev.rubyonrails.org/ticket/6778
- variables[:sql_auto_is_null] = 0
+ 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)
+ 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'
+ unless variables.has_key?('sql_mode')
+ variables['sql_mode'] = strict_mode? ? 'STRICT_ALL_TABLES' : ''
end
# NAMES does not have an equals sign, see
@@ -801,6 +827,15 @@ module ActiveRecord
# ...and send them all in one query
@connection.query "SET #{encoding} #{variable_assignments}"
end
+
+ def extract_foreign_key_action(structure, name, action) # :nodoc:
+ if structure =~ /CONSTRAINT #{quote_column_name(name)} FOREIGN KEY .* REFERENCES .* ON #{action} (CASCADE|SET NULL|RESTRICT)/
+ case $1
+ when 'CASCADE'; :cascade
+ when 'SET NULL'; :nullify
+ end
+ end
+ 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 187eefb9e4..1f1e2c46f4 100644
--- a/activerecord/lib/active_record/connection_adapters/column.rb
+++ b/activerecord/lib/active_record/connection_adapters/column.rb
@@ -13,107 +13,36 @@ 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, :default_function
- attr_accessor :primary, :coder
+ attr_reader :name, :cast_type, :null, :sql_type, :default, :default_function
- alias :encoded? :coder
+ delegate :type, :precision, :scale, :limit, :klass, :accessor,
+ :text?, :number?, :binary?, :changed?,
+ :type_cast_from_user, :type_cast_from_database, :type_cast_for_database,
+ :type_cast_for_schema,
+ to: :cast_type
# Instantiates a new column in the table.
#
# +name+ is the column's name, such as <tt>supplier_id</tt> in <tt>supplier_id int(11)</tt>.
# +default+ is the type-casted default value, such as +new+ in <tt>sales_stage varchar(20) default 'new'</tt>.
+ # +cast_type+ is the object used for type casting and type information.
# +sql_type+ is used to extract the column's length, if necessary. For example +60+ in
# <tt>company_name varchar(60)</tt>.
# It will be mapped to one of the standard Rails SQL types in the <tt>type</tt> attribute.
# +null+ determines if this column allows +NULL+ values.
- def initialize(name, default, sql_type = nil, null = true)
+ def initialize(name, default, cast_type, sql_type = nil, null = true)
@name = name
+ @cast_type = cast_type
@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 = default
@default_function = nil
- @primary = nil
- @coder = nil
- end
-
- # Returns +true+ if the column is either of type string or text.
- def text?
- type == :string || type == :text
- end
-
- # Returns +true+ if the column is either of type integer, float or decimal.
- def number?
- type == :integer || type == :float || type == :decimal
end
def has_default?
!default.nil?
end
- # Returns the Ruby class that corresponds to the abstract data type.
- def klass
- case type
- when :integer then Fixnum
- when :float then Float
- when :decimal then BigDecimal
- when :datetime, :timestamp, :time then Time
- when :date then Date
- when :text, :string, :binary then String
- when :boolean then Object
- end
- end
-
- def binary?
- type == :binary
- end
-
- # Casts a Ruby value to something appropriate for writing to the database.
- def type_cast_for_write(value)
- return value unless number?
-
- case value
- when FalseClass
- 0
- when TrueClass
- 1
- when String
- value.presence
- else
- value
- end
- end
-
- # Casts value to an appropriate instance.
- def type_cast(value)
- return nil if value.nil?
- return coder.load(value) if encoded?
-
- klass = self.class
-
- case type
- when :string, :text
- case value
- when TrueClass; "1"
- when FalseClass; "0"
- else
- value.to_s
- end
- 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)
- when :time then klass.string_to_dummy_time(value)
- when :date then klass.value_to_date(value)
- when :binary then klass.binary_to_string(value)
- when :boolean then klass.value_to_boolean(value)
- else value
- end
- end
-
# Returns the human name of the column name.
#
# ===== Examples
@@ -122,177 +51,11 @@ module ActiveRecord
Base.human_attribute_name(@name)
end
- def extract_default(default)
- type_cast(default)
- end
-
- class << self
- # Used to convert from BLOBs to Strings
- def binary_to_string(value)
- value
+ def with_type(type)
+ dup.tap do |clone|
+ clone.instance_variable_set('@cast_type', type)
end
-
- def value_to_date(value)
- if value.is_a?(String)
- return nil if value.empty?
- fast_string_to_date(value) || fallback_string_to_date(value)
- elsif value.respond_to?(:to_date)
- value.to_date
- else
- value
- end
- end
-
- def string_to_time(string)
- return string unless string.is_a?(String)
- 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.empty?
-
- dummy_time_string = "2000-01-01 #{string}"
-
- fast_string_to_time(dummy_time_string) || begin
- time_hash = Date._parse(dummy_time_string)
- return nil if time_hash[:hour].nil?
- new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction))
- end
- end
-
- # convert something to a boolean
- def value_to_boolean(value)
- 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
- # subclasses of BigDecimal will be handled
- # in the else clause
- if value.class == BigDecimal
- value
- elsif value.respond_to?(:to_d)
- value.to_d
- else
- value.to_s.to_d
- end
- end
-
- protected
- # '0.123456' -> 123456
- # '1.123456' -> 123456
- def microseconds(time)
- time[:sec_fraction] ? (time[:sec_fraction] * 1_000_000).to_i : 0
- end
-
- def new_date(year, mon, mday)
- if year && year != 0
- Date.new(year, mon, mday) rescue nil
- end
- end
-
- 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)
-
- 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)
- if string =~ Format::ISO_DATE
- new_date $1.to_i, $2.to_i, $3.to_i
- end
- end
-
- # Doesn't handle time zones.
- def fast_string_to_time(string)
- if string =~ Format::ISO_DATETIME
- microsec = ($7.to_r * 1_000_000).to_i
- new_time $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, microsec
- end
- end
-
- def fallback_string_to_date(string)
- new_date(*::Date._parse(string, false).values_at(:year, :mon, :mday))
- end
-
- def fallback_string_to_time(string)
- time_hash = Date._parse(string)
- time_hash[:sec_fraction] = microseconds(time_hash)
-
- new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction, :offset))
- end
end
-
- private
- def extract_limit(sql_type)
- $1.to_i if sql_type =~ /\((.*)\)/
- end
-
- def extract_precision(sql_type)
- $2.to_i if sql_type =~ /^(numeric|decimal|number)\((\d+)(,\d+)?\)/i
- end
-
- def extract_scale(sql_type)
- case sql_type
- when /^(numeric|decimal|number)\((\d+)\)/i then 0
- when /^(numeric|decimal|number)\((\d+)(,(\d+))\)/i then $4.to_i
- end
- end
-
- def simplified_type(field_type)
- case field_type
- when /int/i
- :integer
- when /float|double/i
- :float
- when /decimal|numeric|number/i
- extract_scale(field_type) == 0 ? :integer : :decimal
- when /datetime/i
- :datetime
- when /timestamp/i
- :timestamp
- when /time/i
- :time
- when /date/i
- :date
- when /clob/i, /text/i
- :text
- when /blob/i, /binary/i
- :binary
- when /char/i
- :string
- when /boolean/i
- :boolean
- end
- end
end
end
# :startdoc:
diff --git a/activerecord/lib/active_record/connection_adapters/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/connection_specification.rb
index b79d1a4458..5693031053 100644
--- a/activerecord/lib/active_record/connection_adapters/connection_specification.rb
+++ b/activerecord/lib/active_record/connection_adapters/connection_specification.rb
@@ -85,7 +85,7 @@ module ActiveRecord
"password" => uri.password,
"port" => uri.port,
"database" => database_from_path,
- "host" => uri.host })
+ "host" => uri.hostname })
end
end
@@ -160,7 +160,7 @@ module ActiveRecord
# config = { "production" => { "host" => "localhost", "database" => "foo", "adapter" => "sqlite3" } }
# spec = Resolver.new(config).spec(:production)
# spec.adapter_method
- # # => "sqlite3"
+ # # => "sqlite3_connection"
# spec.config
# # => { "host" => "localhost", "database" => "foo", "adapter" => "sqlite3" }
#
@@ -250,7 +250,7 @@ module ActiveRecord
# Connection details inside of the "url" key win any merge conflicts
def resolve_hash_connection(spec)
if spec["url"] && spec["url"] !~ /^jdbc:/
- connection_hash = resolve_string_connection(spec.delete("url"))
+ connection_hash = resolve_url_connection(spec.delete("url"))
spec.merge!(connection_hash)
end
spec
diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
index 233af252d6..39d52e6349 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
@@ -29,13 +29,6 @@ module ActiveRecord
module ConnectionAdapters
class Mysql2Adapter < AbstractMysqlAdapter
-
- class Column < AbstractMysqlAdapter::Column # :nodoc:
- def adapter
- Mysql2Adapter
- end
- end
-
ADAPTER_NAME = 'Mysql2'
def initialize(connection, logger, connection_options, config)
@@ -69,10 +62,6 @@ module ActiveRecord
end
end
- 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)
exception.error_number if exception.respond_to?(:error_number)
end
@@ -280,8 +269,8 @@ module ActiveRecord
super
end
- def version
- @version ||= @connection.info[:version].scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map { |v| v.to_i }
+ def full_version
+ @full_version ||= @connection.info[:version]
end
def set_field_encoding field_name
diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
index e6aa2ba921..a03bc28744 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
@@ -66,35 +66,6 @@ module ActiveRecord
# * <tt>:sslcipher</tt> - Necessary to use MySQL with an SSL connection.
#
class MysqlAdapter < AbstractMysqlAdapter
-
- class Column < AbstractMysqlAdapter::Column #:nodoc:
- def self.string_to_time(value)
- return super unless Mysql::Time === value
- new_time(
- value.year,
- value.month,
- value.day,
- value.hour,
- value.minute,
- value.second,
- value.second_part)
- end
-
- def self.string_to_dummy_time(v)
- return super unless Mysql::Time === v
- new_time(2000, 01, 01, v.hour, v.minute, v.second, v.second_part)
- end
-
- def self.string_to_date(v)
- return super unless Mysql::Time === v
- new_date(v.year, v.month, v.day)
- end
-
- def adapter
- MysqlAdapter
- end
- end
-
ADAPTER_NAME = 'MySQL'
class StatementPool < ConnectionAdapters::StatementPool
@@ -156,10 +127,6 @@ module ActiveRecord
end
end
- 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:
exception.errno if exception.respond_to?(:errno)
end
@@ -222,6 +189,7 @@ module ActiveRecord
# Clears the prepared statements cache.
def clear_cache!
+ super
@statements.clear
end
@@ -294,126 +262,70 @@ module ActiveRecord
@connection.insert_id
end
- module Fields
- class Type
- def type; end
-
- def type_cast_for_write(value)
- value
- end
- end
-
- class Identity < Type
- def type_cast(value); value; end
- end
-
- class Integer < Type
- def type_cast(value)
- return if value.nil?
-
- value.to_i rescue value ? 1 : 0
- end
- end
-
- class Date < Type
- def type; :date; end
-
- def type_cast(value)
- return if value.nil?
-
- # FIXME: probably we can improve this since we know it is mysql
- # specific
- ConnectionAdapters::Column.value_to_date value
- end
- end
-
- class DateTime < Type
- def type; :datetime; end
-
- def type_cast(value)
- return if value.nil?
-
- # FIXME: probably we can improve this since we know it is mysql
- # specific
- ConnectionAdapters::Column.string_to_time value
- end
- end
-
- class Time < Type
- def type; :time; end
-
- def type_cast(value)
- return if value.nil?
-
- # FIXME: probably we can improve this since we know it is mysql
- # specific
- ConnectionAdapters::Column.string_to_dummy_time value
- end
- end
-
- class Float < Type
- def type; :float; end
-
- def type_cast(value)
- return if value.nil?
-
- value.to_f
+ module Fields # :nodoc:
+ class DateTime < Type::DateTime # :nodoc:
+ def cast_value(value)
+ if Mysql::Time === value
+ new_time(
+ value.year,
+ value.month,
+ value.day,
+ value.hour,
+ value.minute,
+ value.second,
+ value.second_part)
+ else
+ super
+ end
end
end
- class Decimal < Type
- def type_cast(value)
- return if value.nil?
-
- ConnectionAdapters::Column.value_to_decimal value
+ class Time < Type::Time # :nodoc:
+ def cast_value(value)
+ if Mysql::Time === value
+ new_time(
+ 2000,
+ 01,
+ 01,
+ value.hour,
+ value.minute,
+ value.second,
+ value.second_part)
+ else
+ super
+ end
end
end
- class Boolean < Type
- def type_cast(value)
- return if value.nil?
+ class << self
+ TYPES = Type::HashLookupTypeMap.new # :nodoc:
- ConnectionAdapters::Column.value_to_boolean value
- end
- end
-
- TYPES = {}
-
- # Register an MySQL +type_id+ with a typecasting object in
- # +type+.
- def self.register_type(type_id, type)
- TYPES[type_id] = type
- end
+ delegate :register_type, :alias_type, to: :TYPES
- def self.alias_type(new, old)
- 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 }
+ def find_type(field)
+ if field.type == Mysql::Field::TYPE_TINY && field.length > 1
+ TYPES.lookup(Mysql::Field::TYPE_LONG)
+ else
+ TYPES.lookup(field.type)
+ end
end
end
- register_type Mysql::Field::TYPE_TINY, Fields::Boolean.new
- register_type Mysql::Field::TYPE_LONG, Fields::Integer.new
+ register_type Mysql::Field::TYPE_TINY, Type::Boolean.new
+ register_type Mysql::Field::TYPE_LONG, Type::Integer.new
alias_type Mysql::Field::TYPE_LONGLONG, Mysql::Field::TYPE_LONG
alias_type Mysql::Field::TYPE_NEWDECIMAL, Mysql::Field::TYPE_LONG
- register_type Mysql::Field::TYPE_VAR_STRING, Fields::Identity.new
- register_type Mysql::Field::TYPE_BLOB, Fields::Identity.new
- register_type Mysql::Field::TYPE_DATE, Fields::Date.new
+ register_type Mysql::Field::TYPE_DATE, Type::Date.new
register_type Mysql::Field::TYPE_DATETIME, Fields::DateTime.new
register_type Mysql::Field::TYPE_TIME, Fields::Time.new
- register_type Mysql::Field::TYPE_FLOAT, Fields::Float.new
+ register_type Mysql::Field::TYPE_FLOAT, Type::Float.new
+ end
- Mysql::Field.constants.grep(/TYPE/).map { |class_name|
- Mysql::Field.const_get class_name
- }.reject { |const| TYPES.key? const }.each do |const|
- register_type const, Fields::Identity.new
- end
+ def initialize_type_map(m) # :nodoc:
+ super
+ m.register_type %r(datetime)i, Fields::DateTime.new
+ m.register_type %r(time)i, Fields::Time.new
end
def exec_without_stmt(sql, name = 'SQL') # :nodoc:
@@ -431,7 +343,7 @@ module ActiveRecord
fields << field_name
if field.decimals > 0
- types[field_name] = Fields::Decimal.new
+ types[field_name] = Type::Decimal.new
else
types[field_name] = Fields.find_type field
end
@@ -447,7 +359,7 @@ module ActiveRecord
end
end
- def execute_and_free(sql, name = nil)
+ def execute_and_free(sql, name = nil) # :nodoc:
result = execute(sql, name)
ret = yield result
result.free
@@ -460,7 +372,7 @@ module ActiveRecord
end
alias :create :insert_sql
- def exec_delete(sql, name, binds)
+ def exec_delete(sql, name, binds) # :nodoc:
affected_rows = 0
exec_query(sql, name, binds) do |n|
@@ -497,7 +409,7 @@ module ActiveRecord
stmt.execute(*type_casted_binds.map { |_, val| val })
rescue Mysql::Error => e
# Older versions of MySQL leave the prepared statement in a bad
- # place when an error occurs. To support older mysql versions, we
+ # place when an error occurs. To support older MySQL versions, we
# need to close the statement and delete the statement from the
# cache.
stmt.close
@@ -558,9 +470,9 @@ module ActiveRecord
rows
end
- # Returns the version of the connected MySQL server.
- def version
- @version ||= @connection.server_info.scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map { |v| v.to_i }
+ # Returns the full version of the connected MySQL server.
+ def full_version
+ @full_version ||= @connection.server_info
end
def set_field_encoding field_name
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 0b218f2bfd..1b74c039ce 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb
@@ -1,7 +1,7 @@
module ActiveRecord
module ConnectionAdapters
- class PostgreSQLColumn < Column
- module ArrayParser
+ module PostgreSQL
+ module ArrayParser # :nodoc:
DOUBLE_QUOTE = '"'
BACKSLASH = "\\"
@@ -9,35 +9,23 @@ module ActiveRecord
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
- # a large amount of Ruby objects that will need to be garbage
- # collected. pg_array_parser has a C and Java extension
- begin
- require 'pg_array_parser'
- include PgArrayParser
- rescue LoadError
- def parse_pg_array(string)
- parse_data(string)
+ def parse_pg_array(string) # :nodoc:
+ local_index = 0
+ array = []
+ while(local_index < string.length)
+ case string[local_index]
+ when BRACKET_OPEN
+ local_index,array = parse_array_contents(array, string, local_index + 1)
+ when BRACKET_CLOSE
+ return array
end
+ local_index += 1
end
- def parse_data(string)
- local_index = 0
- array = []
- while(local_index < string.length)
- case string[local_index]
- when BRACKET_OPEN
- local_index,array = parse_array_contents(array, string, local_index + 1)
- when BRACKET_CLOSE
- return array
- end
- local_index += 1
- end
+ array
+ end
- array
- end
+ private
def parse_array_contents(array, string, index)
is_escaping = false
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb b/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb
deleted file mode 100644
index 551a9289c3..0000000000
--- a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb
+++ /dev/null
@@ -1,168 +0,0 @@
-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'; 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, array_member = false)
- if Hash === object
- string = object.map { |k, v| "#{escape_hstore(k)}=>#{escape_hstore(v)}" }.join(',')
- string = escape_hstore(string) if array_member
- string
- else
- object
- end
- end
-
- def string_to_hstore(string)
- if string.nil?
- nil
- elsif String === string
- Hash[string.scan(HstorePair).map { |k, v|
- 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
- string
- end
- end
-
- def json_to_string(object)
- if Hash === object || Array === object
- ActiveSupport::JSON.encode(object)
- else
- object
- end
- end
-
- def array_to_string(value, column, adapter)
- casted_values = value.map do |val|
- if String === val
- if val == "NULL"
- "\"#{val}\""
- else
- quote_and_escape(adapter.type_cast(val, column, true))
- end
- else
- adapter.type_cast(val, column, true)
- end
- end
- "{#{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)
- else
- string
- end
- end
-
- def string_to_cidr(string)
- if string.nil?
- nil
- elsif String === string
- begin
- IPAddr.new(string)
- rescue ArgumentError
- nil
- end
- else
- string
- end
- end
-
- def cidr_to_string(object)
- if IPAddr === object
- "#{object.to_s}/#{object.instance_variable_get(:@mask_addr).to_s(2).count('1')}"
- else
- object
- end
- end
-
- def string_to_array(string, oid)
- parse_pg_array(string).map {|val| type_cast_array(oid, val)}
- end
-
- private
-
- HstorePair = begin
- quoted_string = /"[^"\\]*(?:\\.[^"\\]*)*"/
- unquoted_string = /(?:\\.|[^\s,])[^\s=,\\]*(?:\\.[^\s=,\\]*|=[^,>])*/
- /(#{quoted_string}|#{unquoted_string})\s*=>\s*(#{quoted_string}|#{unquoted_string})/
- end
-
- def escape_hstore(value)
- if value.nil?
- 'NULL'
- else
- if value == ""
- '""'
- else
- '"%s"' % value.to_s.gsub(/(["\\])/, '\\\\\1')
- end
- end
- end
-
- ARRAY_ESCAPE = "\\" * 2 * 2 # escape the backslash twice for PG arrays
-
- def quote_and_escape(value)
- case value
- when "NULL", Numeric
- value
- else
- value = value.gsub(/\\/, ARRAY_ESCAPE)
- value.gsub!(/"/,"\\\"")
- "\"#{value}\""
- 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
-end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb
index 2cbcd5fd50..37e5c3859c 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb
@@ -4,155 +4,17 @@ module ActiveRecord
class PostgreSQLColumn < Column #:nodoc:
attr_accessor :array
- def initialize(name, default, oid_type, sql_type = nil, null = true)
- @oid_type = oid_type
- default_value = self.class.extract_value_from_default(default)
-
+ def initialize(name, default, cast_type, sql_type = nil, null = true, default_function = nil)
if sql_type =~ /\[\]$/
@array = true
- super(name, default_value, sql_type[0..sql_type.length - 3], null)
+ super(name, default, cast_type, sql_type[0..sql_type.length - 3], null)
else
@array = false
- super(name, default_value, sql_type, null)
- end
-
- @default_function = default if has_default_function?(default_value, default)
- end
-
- def number?
- !array && super
- end
-
- def text?
- !array && super
- end
-
- # :stopdoc:
- class << self
- include ConnectionAdapters::PostgreSQLColumn::Cast
- include ConnectionAdapters::PostgreSQLColumn::ArrayParser
- attr_accessor :money_precision
- end
- # :startdoc:
-
- # Extracts the value from a PostgreSQL column default definition.
- def self.extract_value_from_default(default)
- # This is a performance optimization for Ruby 1.9.2 in development.
- # If the value is nil, we return nil straight away without checking
- # the regular expressions. If we check each regular expression,
- # Regexp#=== will call NilClass#to_str, which will trigger
- # method_missing (defined by whiny nil in ActiveSupport) which
- # makes this method very very slow.
- return default unless default
-
- case default
- when /\A'(.*)'::(num|date|tstz|ts|int4|int8)range\z/m
- $1
- # Numeric types
- when /\A\(?(-?\d+(\.\d*)?\)?(::bigint)?)\z/
- $1
- # Character types
- when /\A\(?'(.*)'::.*\b(?:character varying|bpchar|text)\z/m
- $1.gsub(/''/, "'")
- # Binary data types
- when /\A'(.*)'::bytea\z/m
- $1
- # Date/time types
- when /\A'(.+)'::(?:time(?:stamp)? with(?:out)? time zone|date)\z/
- $1
- when /\A'(.*)'::interval\z/
- $1
- # Boolean type
- when 'true'
- true
- when 'false'
- false
- # Geometric types
- when /\A'(.*)'::(?:point|line|lseg|box|"?path"?|polygon|circle)\z/
- $1
- # Network address types
- when /\A'(.*)'::(?:cidr|inet|macaddr)\z/
- $1
- # Bit string types
- when /\AB'(.*)'::"?bit(?: varying)?"?\z/
- $1
- # XML type
- when /\A'(.*)'::xml\z/m
- $1
- # Arrays
- when /\A'(.*)'::"?\D+"?\[\]\z/
- $1
- # Hstore
- when /\A'(.*)'::hstore\z/
- $1
- # JSON
- when /\A'(.*)'::json\z/
- $1
- # Object identifier types
- when /\A-?\d+\z/
- $1
- else
- # Anything else is blank, some user type, or some function
- # and we can't know the value of that, so return nil.
- nil
+ super(name, default, cast_type, sql_type, null)
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
+ @default_function = default_function
end
-
- def type_cast(value)
- return if value.nil?
- return super if encoded?
-
- @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
- when /^smallint/i; 2
- when /^timestamp/i; nil
- else super
- end
- end
-
- # Extracts the scale from PostgreSQL-specific data types.
- def extract_scale(sql_type)
- # Money type has a fixed scale of 2.
- sql_type =~ /^money/ ? 2 : super
- end
-
- # Extracts the precision from PostgreSQL-specific data types.
- def extract_precision(sql_type)
- if sql_type == 'money'
- self.class.money_precision
- elsif sql_type =~ /timestamp/i
- $1.to_i if sql_type =~ /\((\d+)\)/
- else
- super
- end
- end
-
- # Maps PostgreSQL-specific data types to logical Rails types.
- def simplified_type(field_type)
- @oid_type.simplified_type(field_type) || super
- 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 168b08ba75..89a7257d77 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb
@@ -1,6 +1,6 @@
module ActiveRecord
module ConnectionAdapters
- class PostgreSQLAdapter < AbstractAdapter
+ module PostgreSQL
module DatabaseStatements
def explain(arel, binds = [])
sql = "EXPLAIN #{to_sql(arel, binds)}"
@@ -94,6 +94,11 @@ module ActiveRecord
super.insert
end
+ # The internal PostgreSQL identifier of the money data type.
+ MONEY_COLUMN_TYPE_OID = 790 #:nodoc:
+ # The internal PostgreSQL identifier of the BYTEA data type.
+ BYTEA_COLUMN_TYPE_OID = 17 #:nodoc:
+
# create a 2D array representing the result set
def result_as_array(res) #:nodoc:
# check if we have any binary column and if they need escaping
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb
index 540b3694b5..d28a2b4fa0 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb
@@ -1,440 +1,35 @@
-require 'active_record/connection_adapters/abstract_adapter'
+require 'active_record/connection_adapters/postgresql/oid/infinity'
+
+require 'active_record/connection_adapters/postgresql/oid/array'
+require 'active_record/connection_adapters/postgresql/oid/bit'
+require 'active_record/connection_adapters/postgresql/oid/bit_varying'
+require 'active_record/connection_adapters/postgresql/oid/bytea'
+require 'active_record/connection_adapters/postgresql/oid/cidr'
+require 'active_record/connection_adapters/postgresql/oid/date'
+require 'active_record/connection_adapters/postgresql/oid/date_time'
+require 'active_record/connection_adapters/postgresql/oid/decimal'
+require 'active_record/connection_adapters/postgresql/oid/enum'
+require 'active_record/connection_adapters/postgresql/oid/float'
+require 'active_record/connection_adapters/postgresql/oid/hstore'
+require 'active_record/connection_adapters/postgresql/oid/inet'
+require 'active_record/connection_adapters/postgresql/oid/integer'
+require 'active_record/connection_adapters/postgresql/oid/json'
+require 'active_record/connection_adapters/postgresql/oid/jsonb'
+require 'active_record/connection_adapters/postgresql/oid/money'
+require 'active_record/connection_adapters/postgresql/oid/point'
+require 'active_record/connection_adapters/postgresql/oid/range'
+require 'active_record/connection_adapters/postgresql/oid/specialized_string'
+require 'active_record/connection_adapters/postgresql/oid/time'
+require 'active_record/connection_adapters/postgresql/oid/uuid'
+require 'active_record/connection_adapters/postgresql/oid/vector'
+require 'active_record/connection_adapters/postgresql/oid/xml'
+
+require 'active_record/connection_adapters/postgresql/oid/type_map_initializer'
module ActiveRecord
module ConnectionAdapters
- class PostgreSQLAdapter < AbstractAdapter
- module OID
- class Type
- def type; end
- def simplified_type(sql_type); type end
-
- def infinity(options = {})
- ::Float::INFINITY * (options[:negative] ? -1 : 1)
- end
- end
-
- class Identity < Type
- def type_cast(value)
- value
- end
- end
-
- class String < Type
- def type; :string end
-
- def type_cast(value)
- return if value.nil?
-
- value.to_s
- end
- end
-
- class SpecializedString < OID::String
- def type; @type end
-
- def initialize(type)
- @type = type
- end
- end
-
- class Text < OID::String
- def type; :text end
- end
-
- class Bit < Type
- def type; :string end
-
- def type_cast(value)
- if ::String === value
- ConnectionAdapters::PostgreSQLColumn.string_to_bit value
- else
- value
- end
- end
- end
-
- class Bytea < Type
- def type; :binary end
-
- def type_cast(value)
- return if value.nil?
- PGconn.unescape_bytea value
- end
- end
-
- class Money < Type
- def type; :decimal end
-
- 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.]/, '')
- when /^-?\D+[\d.]+,\d{2}$/ # (2)
- value.gsub!(/[^-\d,]/, '').sub!(/,/, '.')
- end
-
- ConnectionAdapters::Column.value_to_decimal value
- end
- end
-
- class Vector < Type
- attr_reader :delim, :subtype
-
- # +delim+ corresponds to the `typdelim` column in the pg_types
- # table. +subtype+ is derived from the `typelem` column in the
- # pg_types table.
- def initialize(delim, subtype)
- @delim = delim
- @subtype = subtype
- end
-
- # FIXME: this should probably split on +delim+ and use +subtype+
- # to cast the values. Unfortunately, the current Rails behavior
- # is to just return the string.
- def type_cast(value)
- value
- end
- end
-
- class Point < Type
- def type; :string end
-
- def type_cast(value)
- if ::String === value
- ConnectionAdapters::PostgreSQLColumn.string_to_point value
- else
- value
- end
- end
- end
-
- class Array < Type
- def type; @subtype.type end
-
- attr_reader :subtype
- def initialize(subtype)
- @subtype = subtype
- end
-
- def type_cast(value)
- if ::String === value
- ConnectionAdapters::PostgreSQLColumn.string_to_array value, @subtype
- else
- value
- end
- end
- end
-
- class Range < Type
- attr_reader :subtype
- def simplified_type(sql_type); sql_type.to_sym end
-
- def initialize(subtype)
- @subtype = subtype
- end
-
- def extract_bounds(value)
- from, to = value[1..-2].split(',')
- {
- from: (value[1] == ',' || from == '-infinity') ? @subtype.infinity(negative: true) : from,
- to: (value[-2] == ',' || to == 'infinity') ? @subtype.infinity : to,
- exclude_start: (value[0] == '('),
- exclude_end: (value[-1] == ')')
- }
- end
-
- def infinity?(value)
- value.respond_to?(:infinite?) && value.infinite?
- end
-
- def type_cast_single(value)
- infinity?(value) ? value : @subtype.type_cast(value)
- end
-
- def type_cast(value)
- return if value.nil? || value == 'empty'
- return value if value.is_a?(::Range)
-
- extracted = extract_bounds(value)
- from = type_cast_single extracted[:from]
- to = type_cast_single extracted[:to]
-
- if !infinity?(from) && extracted[:exclude_start]
- if from.respond_to?(:succ)
- from = from.succ
- ActiveSupport::Deprecation.warn <<-MESSAGE
-Excluding the beginning of a Range is only partialy supported through `#succ`.
-This is not reliable and will be removed in the future.
- MESSAGE
- else
- raise ArgumentError, "The Ruby Range object does not support excluding the beginning of a Range. (unsupported value: '#{value}')"
- end
- end
- ::Range.new(from, to, extracted[:exclude_end])
- end
- end
-
- class Integer < Type
- def type; :integer end
-
- def type_cast(value)
- return if value.nil?
-
- ConnectionAdapters::Column.value_to_integer value
- end
- end
-
- class Boolean < Type
- def type; :boolean end
-
- def type_cast(value)
- return if value.nil?
-
- ConnectionAdapters::Column.value_to_boolean value
- end
- end
-
- class Timestamp < Type
- def type; :timestamp; end
- def simplified_type(sql_type)
- :datetime
- end
-
- def type_cast(value)
- return if value.nil?
-
- # FIXME: probably we can improve this since we know it is PG
- # specific
- ConnectionAdapters::PostgreSQLColumn.string_to_time value
- end
- end
-
- class Date < Type
- def type; :date; end
-
- def type_cast(value)
- return if value.nil?
-
- # FIXME: probably we can improve this since we know it is PG
- # specific
- ConnectionAdapters::Column.value_to_date value
- end
- end
-
- class Time < Type
- def type; :time end
-
- def type_cast(value)
- return if value.nil?
-
- # FIXME: probably we can improve this since we know it is PG
- # specific
- ConnectionAdapters::Column.string_to_dummy_time value
- end
- end
-
- class Float < Type
- def type; :float end
-
- def type_cast(value)
- return if value.nil?
-
- value.to_f
- end
- end
-
- class Decimal < Type
- def type; :decimal end
-
- def type_cast(value)
- return if value.nil?
-
- ConnectionAdapters::Column.value_to_decimal value
- end
-
- def infinity(options = {})
- BigDecimal.new("Infinity") * (options[:negative] ? -1 : 1)
- end
- end
-
- class Enum < Type
- def type; :enum end
-
- def type_cast(value)
- value.to_s
- end
- end
-
- class Hstore < Type
- def type; :hstore end
-
- 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
- def type; :cidr end
- def type_cast(value)
- return if value.nil?
-
- ConnectionAdapters::PostgreSQLColumn.string_to_cidr value
- end
- end
- class Inet < Cidr
- def type; :inet end
- end
-
- class Json < Type
- def type; :json end
-
- 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 Uuid < Type
- def type; :uuid end
- def type_cast(value)
- value.presence
- end
- end
-
- class TypeMap
- def initialize
- @mapping = {}
- end
-
- def []=(oid, type)
- @mapping[oid] = type
- end
-
- def [](oid)
- @mapping[oid]
- end
-
- def clear
- @mapping.clear
- end
-
- def key?(oid)
- @mapping.key? oid
- end
-
- def fetch(ftype, fmod)
- # The type for the numeric depends on the width of the field,
- # so we'll do something special here.
- #
- # When dealing with decimal columns:
- #
- # places after decimal = fmod - 4 & 0xffff
- # places before decimal = (fmod - 4) >> 16 & 0xffff
- if ftype == 1700 && (fmod - 4 & 0xffff).zero?
- ftype = 23
- end
-
- @mapping.fetch(ftype) { |oid| yield oid, fmod }
- end
- end
-
- # When the PG adapter connects, the pg_type table is queried. The
- # key of this hash maps to the `typname` column from the table.
- # type_map is then dynamically built with oids as the key and type
- # objects as values.
- NAMES = Hash.new { |h,k| # :nodoc:
- h[k] = OID::Identity.new
- }
-
- # Register an OID type named +name+ with a typecasting object in
- # +type+. +name+ should correspond to the `typname` column in
- # the `pg_type` table.
- def self.register_type(name, type)
- NAMES[name] = type
- end
-
- # Alias the +old+ type to the +new+ type.
- def self.alias_type(new, old)
- NAMES[new] = NAMES[old]
- end
-
- # Is +name+ a registered type?
- def self.registered_type?(name)
- NAMES.key? name
- end
-
- register_type 'int2', OID::Integer.new
- alias_type 'int4', 'int2'
- alias_type 'int8', 'int2'
- alias_type 'oid', 'int2'
- register_type 'numeric', OID::Decimal.new
- register_type 'float4', OID::Float.new
- alias_type 'float8', 'float4'
- register_type 'text', OID::Text.new
- register_type 'varchar', OID::String.new
- alias_type 'char', 'varchar'
- alias_type 'bpchar', 'varchar'
- register_type 'bool', OID::Boolean.new
- register_type 'bit', OID::Bit.new
- alias_type 'varbit', 'bit'
- register_type 'timestamp', OID::Timestamp.new
- alias_type 'timestamptz', 'timestamp'
- register_type 'date', OID::Date.new
- register_type 'time', OID::Time.new
-
- register_type 'money', OID::Money.new
- register_type 'bytea', OID::Bytea.new
- register_type 'point', OID::Point.new
- register_type 'hstore', OID::Hstore.new
- register_type 'json', OID::Json.new
- register_type 'cidr', OID::Cidr.new
- register_type 'inet', OID::Inet.new
- register_type 'uuid', OID::Uuid.new
- register_type 'xml', SpecializedString.new(:xml)
- register_type 'tsvector', SpecializedString.new(:tsvector)
- register_type 'macaddr', SpecializedString.new(:macaddr)
- register_type 'citext', SpecializedString.new(:citext)
- register_type 'ltree', SpecializedString.new(:ltree)
-
- # FIXME: why are we keeping these types as strings?
- alias_type 'interval', 'varchar'
- alias_type 'path', 'varchar'
- alias_type 'line', 'varchar'
- alias_type 'polygon', 'varchar'
- alias_type 'circle', 'varchar'
- alias_type 'lseg', 'varchar'
- alias_type 'box', 'varchar'
+ module PostgreSQL
+ module OID # :nodoc:
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb
new file mode 100644
index 0000000000..cd5efe2bb8
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb
@@ -0,0 +1,96 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Array < Type::Value # :nodoc:
+ include Type::Mutable
+
+ # Loads pg_array_parser if available. String parsing can be
+ # performed quicker by a native extension, which will not create
+ # a large amount of Ruby objects that will need to be garbage
+ # collected. pg_array_parser has a C and Java extension
+ begin
+ require 'pg_array_parser'
+ include PgArrayParser
+ rescue LoadError
+ require 'active_record/connection_adapters/postgresql/array_parser'
+ include PostgreSQL::ArrayParser
+ end
+
+ attr_reader :subtype, :delimiter
+ delegate :type, to: :subtype
+
+ def initialize(subtype, delimiter = ',')
+ @subtype = subtype
+ @delimiter = delimiter
+ end
+
+ def type_cast_from_database(value)
+ if value.is_a?(::String)
+ type_cast_array(parse_pg_array(value), :type_cast_from_database)
+ else
+ super
+ end
+ end
+
+ def type_cast_from_user(value)
+ type_cast_array(value, :type_cast_from_user)
+ end
+
+ def type_cast_for_database(value)
+ if value.is_a?(::Array)
+ cast_value_for_database(value)
+ else
+ super
+ end
+ end
+
+ private
+
+ def type_cast_array(value, method)
+ if value.is_a?(::Array)
+ value.map { |item| type_cast_array(item, method) }
+ else
+ @subtype.public_send(method, value)
+ end
+ end
+
+ def cast_value_for_database(value)
+ if value.is_a?(::Array)
+ casted_values = value.map { |item| cast_value_for_database(item) }
+ "{#{casted_values.join(delimiter)}}"
+ else
+ quote_and_escape(subtype.type_cast_for_database(value))
+ end
+ end
+
+ ARRAY_ESCAPE = "\\" * 2 * 2 # escape the backslash twice for PG arrays
+
+ def quote_and_escape(value)
+ case value
+ when ::String
+ if string_requires_quoting?(value)
+ value = value.gsub(/\\/, ARRAY_ESCAPE)
+ value.gsub!(/"/,"\\\"")
+ %("#{value}")
+ else
+ value
+ end
+ when nil then "NULL"
+ else value
+ end
+ end
+
+ # See http://www.postgresql.org/docs/9.2/static/arrays.html#ARRAYS-IO
+ # for a list of all cases in which strings will be quoted.
+ def string_requires_quoting?(string)
+ string.empty? ||
+ string == "NULL" ||
+ string =~ /[\{\}"\\\s]/ ||
+ string.include?(delimiter)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb
new file mode 100644
index 0000000000..1dbb40ca1d
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb
@@ -0,0 +1,52 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Bit < Type::Value # :nodoc:
+ def type
+ :bit
+ end
+
+ def type_cast(value)
+ if ::String === value
+ case value
+ when /^0x/i
+ value[2..-1].hex.to_s(2) # Hexadecimal notation
+ else
+ value # Bit-string notation
+ end
+ else
+ value
+ end
+ end
+
+ def type_cast_for_database(value)
+ Data.new(super) if value
+ end
+
+ class Data
+ def initialize(value)
+ @value = value
+ end
+
+ def to_s
+ value
+ end
+
+ def binary?
+ /\A[01]*\Z/ === value
+ end
+
+ def hex?
+ /\A[0-9A-F]*\Z/i === value
+ end
+
+ protected
+
+ attr_reader :value
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit_varying.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit_varying.rb
new file mode 100644
index 0000000000..4c21097d48
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit_varying.rb
@@ -0,0 +1,13 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class BitVarying < OID::Bit # :nodoc:
+ def type
+ :bit_varying
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb
new file mode 100644
index 0000000000..997613d7be
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb
@@ -0,0 +1,14 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Bytea < Type::Binary # :nodoc:
+ def type_cast_from_database(value)
+ return if value.nil?
+ PGconn.unescape_bytea(super)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb
new file mode 100644
index 0000000000..a53b4ee8e2
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb
@@ -0,0 +1,46 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Cidr < Type::Value # :nodoc:
+ def type
+ :cidr
+ end
+
+ def type_cast_for_schema(value)
+ 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
+ end
+
+ def type_cast_for_database(value)
+ if IPAddr === value
+ "#{value.to_s}/#{value.instance_variable_get(:@mask_addr).to_s(2).count('1')}"
+ else
+ value
+ end
+ end
+
+ def cast_value(value)
+ if value.nil?
+ nil
+ elsif String === value
+ begin
+ IPAddr.new(value)
+ rescue ArgumentError
+ nil
+ end
+ else
+ value
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb
new file mode 100644
index 0000000000..1d8d264530
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb
@@ -0,0 +1,11 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Date < Type::Date # :nodoc:
+ include Infinity
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb
new file mode 100644
index 0000000000..b9e7894e5c
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb
@@ -0,0 +1,27 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class DateTime < Type::DateTime # :nodoc:
+ include Infinity
+
+ def cast_value(value)
+ if value.is_a?(::String)
+ case value
+ when 'infinity' then ::Float::INFINITY
+ when '-infinity' then -::Float::INFINITY
+ when / BC$/
+ astronomical_year = format("%04d", -value[/^\d+/].to_i + 1)
+ super(value.sub(/ BC$/, "").sub(/^\d+/, astronomical_year))
+ else
+ super
+ end
+ else
+ value
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/decimal.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/decimal.rb
new file mode 100644
index 0000000000..43d22c8daf
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/decimal.rb
@@ -0,0 +1,13 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Decimal < Type::Decimal # :nodoc:
+ def infinity(options = {})
+ BigDecimal.new("Infinity") * (options[:negative] ? -1 : 1)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb
new file mode 100644
index 0000000000..77d5038efd
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb
@@ -0,0 +1,17 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Enum < Type::Value # :nodoc:
+ def type
+ :enum
+ end
+
+ def type_cast(value)
+ value.to_s
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/float.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/float.rb
new file mode 100644
index 0000000000..78ef94b912
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/float.rb
@@ -0,0 +1,21 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Float < Type::Float # :nodoc:
+ include Infinity
+
+ def cast_value(value)
+ case value
+ when ::Float then value
+ when 'Infinity' then ::Float::INFINITY
+ when '-Infinity' then -::Float::INFINITY
+ when 'NaN' then ::Float::NAN
+ else value.to_f
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb
new file mode 100644
index 0000000000..be4525c94f
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb
@@ -0,0 +1,59 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Hstore < Type::Value # :nodoc:
+ include Type::Mutable
+
+ def type
+ :hstore
+ end
+
+ def type_cast_from_database(value)
+ if value.is_a?(::String)
+ ::Hash[value.scan(HstorePair).map { |k, v|
+ v = v.upcase == 'NULL' ? nil : v.gsub(/\A"(.*)"\Z/m,'\1').gsub(/\\(.)/, '\1')
+ k = k.gsub(/\A"(.*)"\Z/m,'\1').gsub(/\\(.)/, '\1')
+ [k, v]
+ }]
+ else
+ value
+ end
+ end
+
+ def type_cast_for_database(value)
+ if value.is_a?(::Hash)
+ value.map { |k, v| "#{escape_hstore(k)}=>#{escape_hstore(v)}" }.join(', ')
+ else
+ value
+ end
+ end
+
+ def accessor
+ ActiveRecord::Store::StringKeyedHashAccessor
+ end
+
+ private
+
+ HstorePair = begin
+ quoted_string = /"[^"\\]*(?:\\.[^"\\]*)*"/
+ unquoted_string = /(?:\\.|[^\s,])[^\s=,\\]*(?:\\.[^\s=,\\]*|=[^,>])*/
+ /(#{quoted_string}|#{unquoted_string})\s*=>\s*(#{quoted_string}|#{unquoted_string})/
+ end
+
+ def escape_hstore(value)
+ if value.nil?
+ 'NULL'
+ else
+ if value == ""
+ '""'
+ else
+ '"%s"' % value.to_s.gsub(/(["\\])/, '\\\\\1')
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/inet.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/inet.rb
new file mode 100644
index 0000000000..96486fa65b
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/inet.rb
@@ -0,0 +1,13 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Inet < Cidr # :nodoc:
+ def type
+ :inet
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/infinity.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/infinity.rb
new file mode 100644
index 0000000000..e47780399a
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/infinity.rb
@@ -0,0 +1,13 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ module Infinity # :nodoc:
+ def infinity(options = {})
+ options[:negative] ? -::Float::INFINITY : ::Float::INFINITY
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/integer.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/integer.rb
new file mode 100644
index 0000000000..59abdc0009
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/integer.rb
@@ -0,0 +1,11 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Integer < Type::Integer # :nodoc:
+ include Infinity
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb
new file mode 100644
index 0000000000..e12ddd9901
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb
@@ -0,0 +1,35 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Json < Type::Value # :nodoc:
+ include Type::Mutable
+
+ def type
+ :json
+ end
+
+ def type_cast_from_database(value)
+ if value.is_a?(::String)
+ ::ActiveSupport::JSON.decode(value)
+ else
+ super
+ end
+ end
+
+ def type_cast_for_database(value)
+ if value.is_a?(::Array) || value.is_a?(::Hash)
+ ::ActiveSupport::JSON.encode(value)
+ else
+ super
+ end
+ end
+
+ def accessor
+ ActiveRecord::Store::StringKeyedHashAccessor
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb
new file mode 100644
index 0000000000..34ed32ad35
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb
@@ -0,0 +1,23 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Jsonb < Json # :nodoc:
+ def type
+ :jsonb
+ end
+
+ def changed_in_place?(raw_old_value, new_value)
+ # Postgres does not preserve insignificant whitespaces when
+ # roundtripping jsonb columns. This causes some false positives for
+ # the comparison here. Therefore, we need to parse and re-dump the
+ # raw value here to ensure the insignificant whitespaces are
+ # consitent with our encoder's output.
+ raw_old_value = type_cast_for_database(type_cast_from_database(raw_old_value))
+ super(raw_old_value, new_value)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb
new file mode 100644
index 0000000000..df890c2ed6
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb
@@ -0,0 +1,43 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Money < Type::Decimal # :nodoc:
+ include Infinity
+
+ class_attribute :precision
+
+ def type
+ :money
+ end
+
+ def scale
+ 2
+ end
+
+ def cast_value(value)
+ 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.]/, '')
+ when /^-?\D+[\d.]+,\d{2}$/ # (2)
+ value.gsub!(/[^-\d,]/, '').sub!(/,/, '.')
+ end
+
+ super(value)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb
new file mode 100644
index 0000000000..bac8b01d6b
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb
@@ -0,0 +1,43 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Point < Type::Value # :nodoc:
+ include Type::Mutable
+
+ def type
+ :point
+ end
+
+ def type_cast(value)
+ case value
+ when ::String
+ if value[0] == '(' && value[-1] == ')'
+ value = value[1...-1]
+ end
+ type_cast(value.split(','))
+ when ::Array
+ value.map { |v| Float(v) }
+ else
+ value
+ end
+ end
+
+ def type_cast_for_database(value)
+ if value.is_a?(::Array)
+ "(#{number_for_point(value[0])},#{number_for_point(value[1])})"
+ else
+ super
+ end
+ end
+
+ private
+
+ def number_for_point(number)
+ number.to_s.gsub(/\.0$/, '')
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb
new file mode 100644
index 0000000000..ae967d5167
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb
@@ -0,0 +1,76 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Range < Type::Value # :nodoc:
+ attr_reader :subtype, :type
+
+ def initialize(subtype, type)
+ @subtype = subtype
+ @type = type
+ end
+
+ def type_cast_for_schema(value)
+ value.inspect.gsub('Infinity', '::Float::INFINITY')
+ end
+
+ def cast_value(value)
+ return if value == 'empty'
+ return value if value.is_a?(::Range)
+
+ extracted = extract_bounds(value)
+ from = type_cast_single extracted[:from]
+ to = type_cast_single extracted[:to]
+
+ if !infinity?(from) && extracted[:exclude_start]
+ if from.respond_to?(:succ)
+ from = from.succ
+ ActiveSupport::Deprecation.warn <<-MESSAGE
+Excluding the beginning of a Range is only partialy supported through `#succ`.
+This is not reliable and will be removed in the future.
+ MESSAGE
+ else
+ raise ArgumentError, "The Ruby Range object does not support excluding the beginning of a Range. (unsupported value: '#{value}')"
+ end
+ end
+ ::Range.new(from, to, extracted[:exclude_end])
+ end
+
+ def type_cast_for_database(value)
+ if value.is_a?(::Range)
+ from = type_cast_single_for_database(value.begin)
+ to = type_cast_single_for_database(value.end)
+ "[#{from},#{to}#{value.exclude_end? ? ')' : ']'}"
+ else
+ super
+ end
+ end
+
+ private
+
+ def type_cast_single(value)
+ infinity?(value) ? value : @subtype.type_cast_from_database(value)
+ end
+
+ def type_cast_single_for_database(value)
+ infinity?(value) ? '' : @subtype.type_cast_for_database(value)
+ end
+
+ def extract_bounds(value)
+ from, to = value[1..-2].split(',')
+ {
+ from: (value[1] == ',' || from == '-infinity') ? @subtype.infinity(negative: true) : from,
+ to: (value[-2] == ',' || to == 'infinity') ? @subtype.infinity : to,
+ exclude_start: (value[0] == '('),
+ exclude_end: (value[-1] == ')')
+ }
+ end
+
+ def infinity?(value)
+ value.respond_to?(:infinite?) && value.infinite?
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb
new file mode 100644
index 0000000000..2d2fede4e8
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb
@@ -0,0 +1,15 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class SpecializedString < Type::String # :nodoc:
+ attr_reader :type
+
+ def initialize(type)
+ @type = type
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/time.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/time.rb
new file mode 100644
index 0000000000..8f0246eddb
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/time.rb
@@ -0,0 +1,11 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Time < Type::Time # :nodoc:
+ include Infinity
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb
new file mode 100644
index 0000000000..e396ff4a1e
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb
@@ -0,0 +1,85 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ # This class uses the data from PostgreSQL pg_type table to build
+ # the OID -> Type mapping.
+ # - OID is and integer representing the type.
+ # - Type is an OID::Type object.
+ # This class has side effects on the +store+ passed during initialization.
+ class TypeMapInitializer # :nodoc:
+ def initialize(store)
+ @store = store
+ end
+
+ def run(records)
+ nodes = records.reject { |row| @store.key? row['oid'].to_i }
+ mapped, nodes = nodes.partition { |row| @store.key? row['typname'] }
+ ranges, nodes = nodes.partition { |row| row['typtype'] == 'r' }
+ enums, nodes = nodes.partition { |row| row['typtype'] == 'e' }
+ domains, nodes = nodes.partition { |row| row['typtype'] == 'd' }
+ arrays, nodes = nodes.partition { |row| row['typinput'] == 'array_in' }
+ composites, nodes = nodes.partition { |row| row['typelem'] != '0' }
+
+ mapped.each { |row| register_mapped_type(row) }
+ enums.each { |row| register_enum_type(row) }
+ domains.each { |row| register_domain_type(row) }
+ arrays.each { |row| register_array_type(row) }
+ ranges.each { |row| register_range_type(row) }
+ composites.each { |row| register_composite_type(row) }
+ end
+
+ private
+ def register_mapped_type(row)
+ alias_type row['oid'], row['typname']
+ end
+
+ def register_enum_type(row)
+ register row['oid'], OID::Enum.new
+ end
+
+ def register_array_type(row)
+ if subtype = @store.lookup(row['typelem'].to_i)
+ register row['oid'], OID::Array.new(subtype, row['typdelim'])
+ end
+ end
+
+ def register_range_type(row)
+ if subtype = @store.lookup(row['rngsubtype'].to_i)
+ register row['oid'], OID::Range.new(subtype, row['typname'].to_sym)
+ end
+ end
+
+ def register_domain_type(row)
+ if base_type = @store.lookup(row["typbasetype"].to_i)
+ register row['oid'], base_type
+ else
+ warn "unknown base type (OID: #{row["typbasetype"]}) for domain #{row["typname"]}."
+ end
+ end
+
+ def register_composite_type(row)
+ if subtype = @store.lookup(row['typelem'].to_i)
+ register row['oid'], OID::Vector.new(row['typdelim'], subtype)
+ end
+ end
+
+ def register(oid, oid_type)
+ oid = assert_valid_registration(oid, oid_type)
+ @store.register_type(oid, oid_type)
+ end
+
+ def alias_type(oid, target)
+ oid = assert_valid_registration(oid, target)
+ @store.alias_type(oid, target)
+ end
+
+ def assert_valid_registration(oid, oid_type)
+ raise ArgumentError, "can't register nil type for OID #{oid}" if oid_type.nil?
+ oid.to_i
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb
new file mode 100644
index 0000000000..dd97393eac
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb
@@ -0,0 +1,26 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Uuid < Type::Value # :nodoc:
+ RFC_4122 = %r{\A\{?[a-fA-F0-9]{4}-?
+ [a-fA-F0-9]{4}-?
+ [a-fA-F0-9]{4}-?
+ [1-5][a-fA-F0-9]{3}-?
+ [8-Bab][a-fA-F0-9]{3}-?
+ [a-fA-F0-9]{4}-?
+ [a-fA-F0-9]{4}-?
+ [a-fA-F0-9]{4}-?\}?\z}x
+
+ def type
+ :uuid
+ end
+
+ def type_cast(value)
+ value.to_s[RFC_4122, 0]
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/vector.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/vector.rb
new file mode 100644
index 0000000000..de4187b028
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/vector.rb
@@ -0,0 +1,26 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Vector < Type::Value # :nodoc:
+ attr_reader :delim, :subtype
+
+ # +delim+ corresponds to the `typdelim` column in the pg_types
+ # table. +subtype+ is derived from the `typelem` column in the
+ # pg_types table.
+ def initialize(delim, subtype)
+ @delim = delim
+ @subtype = subtype
+ end
+
+ # FIXME: this should probably split on +delim+ and use +subtype+
+ # to cast the values. Unfortunately, the current Rails behavior
+ # is to just return the string.
+ def type_cast(value)
+ value
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/xml.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/xml.rb
new file mode 100644
index 0000000000..7323f12763
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/xml.rb
@@ -0,0 +1,32 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Xml < Type::String # :nodoc:
+ def type
+ :xml
+ end
+
+ def text?
+ false
+ end
+
+ def type_cast_for_database(value)
+ return unless value
+ Data.new(super)
+ end
+
+ class Data # :nodoc:
+ def initialize(value)
+ @value = value
+ end
+
+ def to_s
+ @value
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
index 403e37fde9..cf5c8d288e 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
@@ -1,6 +1,6 @@
module ActiveRecord
module ConnectionAdapters
- class PostgreSQLAdapter < AbstractAdapter
+ module PostgreSQL
module Quoting
# Escapes binary strings for bytea input to the database.
def escape_bytea(value)
@@ -18,124 +18,18 @@ 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 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 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 sql_type
- when 'inet', 'cidr' then super(PostgreSQLColumn.cidr_to_string(value), column)
- else super
- end
when Float
- if value.infinite? && column.type == :datetime
- "'#{value.to_s.downcase}'"
- elsif value.infinite? || value.nan?
+ if value.infinite? || value.nan?
"'#{value.to_s}'"
else
super
end
- when Numeric
- 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 sql_type
- when 'bytea' then "'#{escape_bytea(value)}'"
- when 'xml' then "xml '#{quote_string(value)}'"
- when /^bit/
- case value
- when /^[01]*$/ then "B'#{value}'" # Bit-string notation
- when /^[0-9A-F]*$/i then "X'#{value}'" # Hexadecimal notation
- end
- else
- super
- end
else
super
end
end
- def type_cast(value, column, array_member = false)
- return super(value, column) unless column
-
- case value
- when Range
- if /range$/ =~ column.sql_type
- PostgreSQLColumn.range_to_string(value)
- else
- super(value, column)
- end
- when NilClass
- if column.array && array_member
- 'NULL'
- elsif column.array
- value
- else
- super(value, column)
- end
- when Array
- case column.sql_type
- when 'point' then PostgreSQLColumn.point_to_string(value)
- when 'json' then PostgreSQLColumn.json_to_string(value)
- else
- if column.array
- PostgreSQLColumn.array_to_string(value, column, self)
- else
- super(value, column)
- end
- end
- when String
- if 'bytea' == column.sql_type
- # Return a bind param hash with format as binary.
- # See http://deveiate.org/code/pg/PGconn.html#method-i-exec_prepared-doc
- # for more information
- { value: value, format: 1 }
- else
- super(value, column)
- end
- when Hash
- case column.sql_type
- when 'hstore' then PostgreSQLColumn.hstore_to_string(value, array_member)
- when 'json' then PostgreSQLColumn.json_to_string(value)
- else super(value, column)
- end
- when IPAddr
- if %w(inet cidr).include? column.sql_type
- PostgreSQLColumn.cidr_to_string(value)
- else
- super(value, column)
- end
- else
- super(value, column)
- end
- end
-
# Quotes strings for use in SQL input.
def quote_string(s) #:nodoc:
@connection.escape(s)
@@ -150,14 +44,7 @@ module ActiveRecord
# - "schema.name".table_name
# - "schema.name"."table.name"
def quote_table_name(name)
- schema, name_part = extract_pg_identifier_from_name(name.to_s)
-
- unless name_part
- quote_column_name(schema)
- else
- table_name, name_part = extract_pg_identifier_from_name(name_part)
- "#{quote_column_name(schema)}.#{quote_column_name(table_name)}"
- end
+ Utils.extract_schema_qualified_name(name.to_s).quoted
end
def quote_table_name_for_assignment(table, attr)
@@ -177,8 +64,9 @@ module ActiveRecord
result = "#{result}.#{sprintf("%06d", value.usec)}"
end
- if value.year < 0
- result = result.sub(/^-/, "") + " BC"
+ if value.year <= 0
+ bce_year = format("%04d", -value.year + 1)
+ result = result.sub(/^-?\d+/, bce_year) + " BC"
end
result
end
@@ -187,8 +75,41 @@ module ActiveRecord
def quote_default_value(value, column) #:nodoc:
if column.type == :uuid && value =~ /\(\)/
value
- else
- quote(value)
+ else
+ quote(value, column)
+ end
+ end
+
+ private
+
+ def _quote(value)
+ case value
+ when Type::Binary::Data
+ "'#{escape_bytea(value.to_s)}'"
+ when OID::Xml::Data
+ "xml '#{quote_string(value.to_s)}'"
+ when OID::Bit::Data
+ if value.binary?
+ "B'#{value}'"
+ elsif value.hex?
+ "X'#{value}'"
+ end
+ else
+ super
+ end
+ end
+
+ def _type_cast(value)
+ case value
+ when Type::Binary::Data
+ # Return a bind param hash with format as binary.
+ # See http://deveiate.org/code/pg/PGconn.html#method-i-exec_prepared-doc
+ # for more information
+ { value: value.to_s, format: 1 }
+ when OID::Xml::Data, OID::Bit::Data
+ value.to_s
+ else
+ super
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 bc775394a6..52b307c432 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb
@@ -1,12 +1,12 @@
module ActiveRecord
module ConnectionAdapters
- class PostgreSQLAdapter < AbstractAdapter
- module ReferentialIntegrity
- def supports_disable_referential_integrity? #:nodoc:
+ module PostgreSQL
+ module ReferentialIntegrity # :nodoc:
+ def supports_disable_referential_integrity? # :nodoc:
true
end
- def disable_referential_integrity #:nodoc:
+ def disable_referential_integrity # :nodoc:
if supports_disable_referential_integrity?
begin
execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} DISABLE TRIGGER ALL" }.join(";"))
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb
new file mode 100644
index 0000000000..83554bbf74
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb
@@ -0,0 +1,154 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module ColumnMethods
+ def xml(*args)
+ options = args.extract_options!
+ column(args[0], :xml, options)
+ end
+
+ def tsvector(*args)
+ options = args.extract_options!
+ 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
+
+ def cidr(name, options = {})
+ column(name, :cidr, options)
+ end
+
+ def macaddr(name, options = {})
+ column(name, :macaddr, options)
+ end
+
+ def uuid(name, options = {})
+ column(name, :uuid, options)
+ end
+
+ def json(name, options = {})
+ column(name, :json, options)
+ end
+
+ def jsonb(name, options = {})
+ column(name, :jsonb, options)
+ end
+
+ def citext(name, options = {})
+ column(name, :citext, options)
+ end
+
+ def point(name, options = {})
+ column(name, :point, options)
+ end
+
+ def bit(name, options)
+ column(name, :bit, options)
+ end
+
+ def bit_varying(name, options)
+ column(name, :bit_varying, options)
+ end
+
+ def money(name, options)
+ column(name, :money, options)
+ end
+ end
+
+ class ColumnDefinition < ActiveRecord::ConnectionAdapters::ColumnDefinition
+ attr_accessor :array
+ 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
+ column = self[name]
+ column.array = options[:array]
+
+ self
+ end
+
+ private
+
+ def create_column_definition(name, type)
+ PostgreSQL::ColumnDefinition.new name, type
+ end
+ end
+
+ class Table < ActiveRecord::ConnectionAdapters::Table
+ include ColumnMethods
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
index 1dc7a6f0fd..7042817672 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
@@ -1,18 +1,18 @@
module ActiveRecord
module ConnectionAdapters
- class PostgreSQLAdapter < AbstractAdapter
+ module PostgreSQL
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_type = type_to_sql(o.type, o.limit, o.precision, o.scale)
sql = "ADD COLUMN #{quote_column_name(o.name)} #{sql_type}"
add_column_options!(sql, column_options(o))
end
def visit_ColumnDefinition(o)
sql = super
- if o.primary_key? && o.type == :uuid
+ if o.primary_key? && o.type != :primary_key
sql << " PRIMARY KEY "
add_column_options!(sql, column_options(o))
end
@@ -31,10 +31,14 @@ module ActiveRecord
super
end
end
- end
- def schema_creation
- SchemaCreation.new self
+ def type_for_column(column)
+ if column.array
+ @conn.lookup_cast_type("#{column.sql_type}[]")
+ else
+ super
+ end
+ end
end
module SchemaStatements
@@ -101,16 +105,16 @@ module ActiveRecord
# If the schema is not specified as part of +name+ then it will only find tables within
# the current schema search path (regardless of permissions to access tables in other schemas)
def table_exists?(name)
- schema, table = Utils.extract_schema_and_table(name.to_s)
- return false unless table
+ name = Utils.extract_schema_qualified_name(name.to_s)
+ return false unless name.identifier
exec_query(<<-SQL, 'SCHEMA').rows.first[0].to_i > 0
SELECT COUNT(*)
FROM pg_class c
LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE c.relkind IN ('r','v','m') -- (r)elation/table, (v)iew, (m)aterialized view
- AND c.relname = '#{table.gsub(/(^"|"$)/,'')}'
- AND n.nspname = #{schema ? "'#{schema}'" : 'ANY (current_schemas(false))'}
+ AND c.relname = '#{name.identifier}'
+ AND n.nspname = #{name.schema ? "'#{name.schema}'" : 'ANY (current_schemas(false))'}
SQL
end
@@ -182,13 +186,15 @@ module ActiveRecord
def columns(table_name)
# Limit, precision, and scale are all handled by the superclass.
column_definitions(table_name).map do |column_name, type, default, notnull, oid, fmod|
- oid = get_oid_type(oid.to_i, fmod.to_i, column_name)
- PostgreSQLColumn.new(column_name, default, oid, type, notnull == 'f')
+ oid = get_oid_type(oid.to_i, fmod.to_i, column_name, type)
+ default_value = extract_value_from_default(oid, default)
+ default_function = extract_default_function(default_value, default)
+ new_column(column_name, default_value, oid, type, notnull == 'f', default_function)
end
end
- def column_for(table_name, column_name) #:nodoc:
- columns(table_name).detect { |c| c.name == column_name.to_s }
+ def new_column(name, default, cast_type, sql_type = nil, null = true, default_function = nil) # :nodoc:
+ PostgreSQLColumn.new(name, default, cast_type, sql_type, null, default_function)
end
# Returns the current database name.
@@ -275,9 +281,9 @@ module ActiveRecord
def default_sequence_name(table_name, pk = nil) #:nodoc:
result = serial_sequence(table_name, pk || 'id')
return nil unless result
- result.split('.').last
+ Utils.extract_schema_qualified_name(result)
rescue ActiveRecord::StatementInvalid
- "#{table_name}_#{pk || 'id'}_seq"
+ PostgreSQL::Name.new(nil, "#{table_name}_#{pk || 'id'}_seq")
end
def serial_sequence(table, column)
@@ -314,17 +320,19 @@ module ActiveRecord
# First try looking for a sequence with a dependency on the
# given table's primary key.
result = query(<<-end_sql, 'SCHEMA')[0]
- SELECT attr.attname, seq.relname
+ SELECT attr.attname, nsp.nspname, seq.relname
FROM pg_class seq,
pg_attribute attr,
pg_depend dep,
- pg_constraint cons
+ pg_constraint cons,
+ pg_namespace nsp
WHERE seq.oid = dep.objid
AND seq.relkind = 'S'
AND attr.attrelid = dep.refobjid
AND attr.attnum = dep.refobjsubid
AND attr.attrelid = cons.conrelid
AND attr.attnum = cons.conkey[1]
+ AND seq.relnamespace = nsp.oid
AND cons.contype = 'p'
AND dep.classid = 'pg_class'::regclass
AND dep.refobjid = '#{quote_table_name(table)}'::regclass
@@ -332,7 +340,7 @@ module ActiveRecord
if result.nil? or result.empty?
result = query(<<-end_sql, 'SCHEMA')[0]
- SELECT attr.attname,
+ SELECT attr.attname, nsp.nspname,
CASE
WHEN pg_get_expr(def.adbin, def.adrelid) !~* 'nextval' THEN NULL
WHEN split_part(pg_get_expr(def.adbin, def.adrelid), '''', 2) ~ '.' THEN
@@ -344,13 +352,19 @@ module ActiveRecord
JOIN pg_attribute attr ON (t.oid = attrelid)
JOIN pg_attrdef def ON (adrelid = attrelid AND adnum = attnum)
JOIN pg_constraint cons ON (conrelid = adrelid AND adnum = conkey[1])
+ JOIN pg_namespace nsp ON (t.relnamespace = nsp.oid)
WHERE t.oid = '#{quote_table_name(table)}'::regclass
AND cons.contype = 'p'
AND pg_get_expr(def.adbin, def.adrelid) ~* 'nextval|uuid_generate'
end_sql
end
- [result.first, result.last]
+ pk = result.shift
+ if result.last
+ [pk, PostgreSQL::Name.new(*result)]
+ else
+ [pk, nil]
+ end
rescue
nil
end
@@ -369,8 +383,8 @@ module ActiveRecord
end
# Renames a table.
- # Also renames a table's primary key sequence if the sequence name matches the
- # Active Record default.
+ # Also renames a table's primary key sequence if the sequence name exists and
+ # matches the Active Record default.
#
# Example:
# rename_table('octopuses', 'octopi')
@@ -378,7 +392,7 @@ module ActiveRecord
clear_cache!
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 == "#{table_name}_#{pk}_seq"
+ if seq && seq.identifier == "#{table_name}_#{pk}_seq"
new_seq = "#{new_name}_#{pk}_seq"
execute "ALTER TABLE #{quote_table_name(seq)} RENAME TO #{quote_table_name(new_seq)}"
end
@@ -409,7 +423,16 @@ module ActiveRecord
def change_column_default(table_name, column_name, default)
clear_cache!
column = column_for(table_name, column_name)
- execute "ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} SET DEFAULT #{quote_default_value(default, column)}" if column
+ return unless column
+
+ alter_column_query = "ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} %s"
+ if default.nil?
+ # <tt>DEFAULT NULL</tt> results in the same behavior as <tt>DROP DEFAULT</tt>. However, PostgreSQL will
+ # cast the default to the columns type, which leaves us with a default like "default NULL::character varying".
+ execute alter_column_query % "DROP DEFAULT"
+ else
+ execute alter_column_query % "SET DEFAULT #{quote_default_value(default, column)}"
+ end
end
def change_column_null(table_name, column_name, null, default = nil)
@@ -441,6 +464,42 @@ module ActiveRecord
execute "ALTER INDEX #{quote_column_name(old_name)} RENAME TO #{quote_table_name(new_name)}"
end
+ def foreign_keys(table_name)
+ fk_info = select_all <<-SQL.strip_heredoc
+ SELECT t2.relname AS to_table, a1.attname AS column, a2.attname AS primary_key, c.conname AS name, c.confupdtype AS on_update, c.confdeltype AS on_delete
+ FROM pg_constraint c
+ JOIN pg_class t1 ON c.conrelid = t1.oid
+ JOIN pg_class t2 ON c.confrelid = t2.oid
+ JOIN pg_attribute a1 ON a1.attnum = c.conkey[1] AND a1.attrelid = t1.oid
+ JOIN pg_attribute a2 ON a2.attnum = c.confkey[1] AND a2.attrelid = t2.oid
+ JOIN pg_namespace t3 ON c.connamespace = t3.oid
+ WHERE c.contype = 'f'
+ AND t1.relname = #{quote(table_name)}
+ AND t3.nspname = ANY (current_schemas(false))
+ ORDER BY c.conname
+ SQL
+
+ fk_info.map do |row|
+ options = {
+ column: row['column'],
+ name: row['name'],
+ primary_key: row['primary_key']
+ }
+
+ options[:on_delete] = extract_foreign_key_action(row['on_delete'])
+ options[:on_update] = extract_foreign_key_action(row['on_update'])
+ ForeignKeyDefinition.new(table_name, row['to_table'], options)
+ end
+ end
+
+ def extract_foreign_key_action(specifier) # :nodoc:
+ case specifier
+ when 'c'; :cascade
+ when 'n'; :nullify
+ when 'r'; :restrict
+ end
+ end
+
def index_name_length
63
end
@@ -490,7 +549,7 @@ module ActiveRecord
# Convert Arel node to string
s = s.to_sql unless s.is_a?(String)
# Remove any ASC/DESC modifiers
- s.gsub(/\s+(ASC|DESC)\s*(NULLS\s+(FIRST|LAST)\s*)?/i, '')
+ s.gsub(/\s+(?:ASC|DESC)?\s*(?:NULLS\s+(?:FIRST|LAST)\s*)?/i, '')
}.reject(&:blank?).map.with_index { |column, i| "#{column} AS alias_#{i}" }
[super, *order_columns].join(', ')
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb b/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb
new file mode 100644
index 0000000000..0290bcb48c
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb
@@ -0,0 +1,66 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ # Value Object to hold a schema qualified name.
+ # This is usually the name of a PostgreSQL relation but it can also represent
+ # schema qualified type names. +schema+ and +identifier+ are unquoted to prevent
+ # double quoting.
+ class Name # :nodoc:
+ SEPARATOR = "."
+ attr_reader :schema, :identifier
+
+ def initialize(schema, identifier)
+ @schema, @identifier = unquote(schema), unquote(identifier)
+ end
+
+ def to_s
+ parts.join SEPARATOR
+ end
+
+ def quoted
+ parts.map { |p| PGconn.quote_ident(p) }.join SEPARATOR
+ end
+
+ def ==(o)
+ o.class == self.class && o.parts == parts
+ end
+ alias_method :eql?, :==
+
+ def hash
+ parts.hash
+ end
+
+ protected
+ def unquote(part)
+ return unless part
+ part.gsub(/(^"|"$)/,'')
+ end
+
+ def parts
+ @parts ||= [@schema, @identifier].compact
+ end
+ end
+
+ module Utils # :nodoc:
+ extend self
+
+ # Returns an instance of <tt>ActiveRecord::ConnectionAdapters::PostgreSQL::Name</tt>
+ # extracted from +string+.
+ # +schema+ is nil if not specified in +string+.
+ # +schema+ and +identifier+ exclude surrounding quotes (regardless of whether provided in +string+)
+ # +string+ supports the range of schema/table references understood by PostgreSQL, for example:
+ #
+ # * <tt>table_name</tt>
+ # * <tt>"table.name"</tt>
+ # * <tt>schema_name.table_name</tt>
+ # * <tt>schema_name."table.name"</tt>
+ # * <tt>"schema_name".table_name</tt>
+ # * <tt>"schema.name"."table name"</tt>
+ def extract_schema_qualified_name(string)
+ table, schema = string.scan(/[^".\s]+|"[^"]*"/)[0..1].reverse
+ PostgreSQL::Name.new(schema, table)
+ end
+ end
+ 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 764cb576d9..eede374678 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
@@ -1,13 +1,15 @@
require 'active_record/connection_adapters/abstract_adapter'
require 'active_record/connection_adapters/statement_pool'
+
+require 'active_record/connection_adapters/postgresql/utils'
+require 'active_record/connection_adapters/postgresql/column'
require 'active_record/connection_adapters/postgresql/oid'
-require 'active_record/connection_adapters/postgresql/cast'
-require 'active_record/connection_adapters/postgresql/array_parser'
require 'active_record/connection_adapters/postgresql/quoting'
+require 'active_record/connection_adapters/postgresql/referential_integrity'
+require 'active_record/connection_adapters/postgresql/schema_definitions'
require 'active_record/connection_adapters/postgresql/schema_statements'
require 'active_record/connection_adapters/postgresql/database_statements'
-require 'active_record/connection_adapters/postgresql/referential_integrity'
-require 'active_record/connection_adapters/postgresql/column'
+
require 'arel/visitors/bind_visitor'
# Make sure we're using pg high enough for PGResult#values
@@ -72,139 +74,6 @@ module ActiveRecord
# In addition, default connection parameters of libpq can be set per environment variables.
# See http://www.postgresql.org/docs/9.1/static/libpq-envars.html .
class PostgreSQLAdapter < AbstractAdapter
- class ColumnDefinition < ActiveRecord::ConnectionAdapters::ColumnDefinition
- attr_accessor :array
- end
-
- module ColumnMethods
- def xml(*args)
- options = args.extract_options!
- column(args[0], 'xml', options)
- end
-
- def tsvector(*args)
- options = args.extract_options!
- 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
-
- def cidr(name, options = {})
- column(name, 'cidr', options)
- end
-
- def macaddr(name, options = {})
- column(name, 'macaddr', options)
- end
-
- def uuid(name, options = {})
- column(name, 'uuid', options)
- end
-
- def json(name, options = {})
- column(name, 'json', options)
- end
-
- def citext(name, options = {})
- column(name, 'citext', 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 citext(name, options = {})
- column(name, 'citext', options)
- end
-
- def column(name, type = nil, options = {})
- super
- column = self[name]
- column.array = options[:array]
-
- self
- end
-
- private
-
- def create_column_definition(name, type)
- ColumnDefinition.new name, type
- end
- end
-
- class Table < ActiveRecord::ConnectionAdapters::Table
- include ColumnMethods
- end
-
ADAPTER_NAME = 'PostgreSQL'
NATIVE_DATABASE_TYPES = {
@@ -215,7 +84,6 @@ module ActiveRecord
float: { name: "float" },
decimal: { name: "decimal" },
datetime: { name: "timestamp" },
- timestamp: { name: "timestamp" },
time: { name: "time" },
date: { name: "date" },
daterange: { name: "daterange" },
@@ -235,13 +103,19 @@ module ActiveRecord
uuid: { name: "uuid" },
json: { name: "json" },
ltree: { name: "ltree" },
- citext: { name: "citext" }
+ citext: { name: "citext" },
+ point: { name: "point" },
+ bit: { name: "bit" },
+ bit_varying: { name: "bit varying" },
+ money: { name: "money" },
}
- include Quoting
- include ReferentialIntegrity
- include SchemaStatements
- include DatabaseStatements
+ OID = PostgreSQL::OID #:nodoc:
+
+ include PostgreSQL::Quoting
+ include PostgreSQL::ReferentialIntegrity
+ include PostgreSQL::SchemaStatements
+ include PostgreSQL::DatabaseStatements
include Savepoints
# Returns 'PostgreSQL' as adapter name for identification purposes.
@@ -249,9 +123,13 @@ module ActiveRecord
ADAPTER_NAME
end
+ def schema_creation # :nodoc:
+ PostgreSQL::SchemaCreation.new self
+ end
+
# Adds `:array` option to the default set provided by the
# AbstractAdapter
- def prepare_column_options(column, types)
+ def prepare_column_options(column, types) # :nodoc:
spec = super
spec[:array] = 'true' if column.respond_to?(:array) && column.array
spec[:default] = "\"#{column.default_function}\"" if column.default_function
@@ -281,6 +159,10 @@ module ActiveRecord
true
end
+ def supports_foreign_keys?
+ true
+ end
+
def index_algorithms
{ concurrently: 'CONCURRENTLY' }
end
@@ -363,7 +245,7 @@ module ActiveRecord
raise "Your version of PostgreSQL (#{postgresql_version}) is too old, please upgrade!"
end
- @type_map = OID::TypeMap.new
+ @type_map = Type::HashLookupTypeMap.new
initialize_type_map(type_map)
@local_tz = execute('SHOW TIME ZONE', 'SCHEMA').first["TimeZone"]
@use_insert_returning = @config.key?(:insert_returning) ? self.class.type_cast_config_to_boolean(@config[:insert_returning]) : true
@@ -428,10 +310,6 @@ module ActiveRecord
self.client_min_messages = old
end
- def supports_insert_with_returning?
- true
- end
-
def supports_ddl_transactions?
true
end
@@ -470,14 +348,13 @@ module ActiveRecord
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
+ res.cast_values.first
end
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 }
+ exec_query("SELECT extname from pg_extension", "SCHEMA").cast_values
else
super
end
@@ -494,25 +371,6 @@ module ActiveRecord
exec_query "SET SESSION AUTHORIZATION #{user}"
end
- module Utils
- extend self
-
- # Returns an array of <tt>[schema_name, table_name]</tt> extracted from +name+.
- # +schema_name+ is nil if not specified in +name+.
- # +schema_name+ and +table_name+ exclude surrounding quotes (regardless of whether provided in +name+)
- # +name+ supports the range of schema/table references understood by PostgreSQL, for example:
- #
- # * <tt>table_name</tt>
- # * <tt>"table.name"</tt>
- # * <tt>schema_name.table_name</tt>
- # * <tt>schema_name."table.name"</tt>
- # * <tt>"schema.name"."table name"</tt>
- def extract_schema_and_table(name)
- table, schema = name.scan(/[^".\s]+|"[^"]*"/)[0..1].collect{|m| m.gsub(/(^"|"$)/,'') }.reverse
- [schema, table]
- end
- end
-
def use_insert_returning?
@use_insert_returning
end
@@ -522,7 +380,12 @@ module ActiveRecord
end
def update_table_definition(table_name, base) #:nodoc:
- Table.new(table_name, base)
+ PostgreSQL::Table.new(table_name, base)
+ end
+
+ def lookup_cast_type(sql_type) # :nodoc:
+ oid = execute("SELECT #{quote(sql_type)}::regtype::oid", "SCHEMA").first['oid'].to_i
+ super(oid)
end
protected
@@ -551,46 +414,129 @@ module ActiveRecord
private
- def type_map
- @type_map
- end
-
- def get_oid_type(oid, fmod, column_name)
+ def get_oid_type(oid, fmod, column_name, sql_type = '') # :nodoc:
if !type_map.key?(oid)
- initialize_type_map(type_map, [oid])
+ load_additional_types(type_map, [oid])
end
- type_map.fetch(oid, fmod) {
+ type_map.fetch(oid, fmod, sql_type) {
warn "unknown OID #{oid}: failed to recognize type of '#{column_name}'. It will be treated as String."
- type_map[oid] = OID::Identity.new
+ Type::Value.new.tap do |cast_type|
+ type_map.register_type(oid, cast_type)
+ end
}
end
- def reload_type_map
- type_map.clear
- initialize_type_map(type_map)
- end
-
- def add_oid(row, records_by_oid, type_map)
- return type_map if type_map.key? row['type_elem'].to_i
+ def initialize_type_map(m) # :nodoc:
+ register_class_with_limit m, 'int2', OID::Integer
+ m.alias_type 'int4', 'int2'
+ m.alias_type 'int8', 'int2'
+ m.alias_type 'oid', 'int2'
+ m.register_type 'float4', OID::Float.new
+ m.alias_type 'float8', 'float4'
+ m.register_type 'text', Type::Text.new
+ register_class_with_limit m, 'varchar', Type::String
+ m.alias_type 'char', 'varchar'
+ m.alias_type 'name', 'varchar'
+ m.alias_type 'bpchar', 'varchar'
+ m.register_type 'bool', Type::Boolean.new
+ register_class_with_limit m, 'bit', OID::Bit
+ register_class_with_limit m, 'varbit', OID::BitVarying
+ m.alias_type 'timestamptz', 'timestamp'
+ m.register_type 'date', OID::Date.new
+ m.register_type 'time', OID::Time.new
+
+ m.register_type 'money', OID::Money.new
+ m.register_type 'bytea', OID::Bytea.new
+ m.register_type 'point', OID::Point.new
+ m.register_type 'hstore', OID::Hstore.new
+ m.register_type 'json', OID::Json.new
+ m.register_type 'jsonb', OID::Jsonb.new
+ m.register_type 'cidr', OID::Cidr.new
+ m.register_type 'inet', OID::Inet.new
+ m.register_type 'uuid', OID::Uuid.new
+ m.register_type 'xml', OID::Xml.new
+ m.register_type 'tsvector', OID::SpecializedString.new(:tsvector)
+ m.register_type 'macaddr', OID::SpecializedString.new(:macaddr)
+ m.register_type 'citext', OID::SpecializedString.new(:citext)
+ m.register_type 'ltree', OID::SpecializedString.new(:ltree)
+
+ # FIXME: why are we keeping these types as strings?
+ m.alias_type 'interval', 'varchar'
+ m.alias_type 'path', 'varchar'
+ m.alias_type 'line', 'varchar'
+ m.alias_type 'polygon', 'varchar'
+ m.alias_type 'circle', 'varchar'
+ m.alias_type 'lseg', 'varchar'
+ m.alias_type 'box', 'varchar'
+
+ m.register_type 'timestamp' do |_, _, sql_type|
+ precision = extract_precision(sql_type)
+ OID::DateTime.new(precision: precision)
+ end
- if OID.registered_type? row['typname']
- # this composite type is explicitly registered
- vector = OID::NAMES[row['typname']]
- else
- # use the default for composite types
- unless type_map.key? row['typelem'].to_i
- add_oid records_by_oid[row['typelem']], records_by_oid, type_map
+ m.register_type 'numeric' do |_, fmod, sql_type|
+ precision = extract_precision(sql_type)
+ scale = extract_scale(sql_type)
+
+ # The type for the numeric depends on the width of the field,
+ # so we'll do something special here.
+ #
+ # When dealing with decimal columns:
+ #
+ # places after decimal = fmod - 4 & 0xffff
+ # places before decimal = (fmod - 4) >> 16 & 0xffff
+ if fmod && (fmod - 4 & 0xffff).zero?
+ # FIXME: Remove this class, and the second argument to
+ # lookups on PG
+ Type::DecimalWithoutScale.new(precision: precision)
+ else
+ OID::Decimal.new(precision: precision, scale: scale)
end
+ end
- vector = OID::Vector.new row['typdelim'], type_map[row['typelem'].to_i]
+ load_additional_types(m)
+ end
+
+ def extract_limit(sql_type) # :nodoc:
+ case sql_type
+ when /^bigint/i; 8
+ when /^smallint/i; 2
+ else super
+ end
+ end
+
+ # Extracts the value from a PostgreSQL column default definition.
+ def extract_value_from_default(oid, default) # :nodoc:
+ case default
+ # Quoted types
+ when /\A[\(B]?'(.*)'::/m
+ $1.gsub(/''/, "'")
+ # Boolean types
+ when 'true', 'false'
+ default
+ # Numeric types
+ when /\A\(?(-?\d+(\.\d*)?\)?(::bigint)?)\z/
+ $1
+ # Object identifier types
+ when /\A-?\d+\z/
+ $1
+ else
+ # Anything else is blank, some user type, or some function
+ # and we can't know the value of that, so return nil.
+ nil
end
+ end
- type_map[row['oid'].to_i] = vector
- type_map
+ def extract_default_function(default_value, default) # :nodoc:
+ default if has_default_function?(default_value, default)
end
- def initialize_type_map(type_map, oids = nil)
+ def has_default_function?(default_value, default) # :nodoc:
+ !default_value && (%r{\w+\(.*\)} === default)
+ end
+
+ def load_additional_types(type_map, oids = nil) # :nodoc:
if supports_ranges?
query = <<-SQL
SELECT t.oid, t.typname, t.typelem, t.typdelim, t.typinput, r.rngsubtype, t.typtype, t.typbasetype
@@ -608,52 +554,9 @@ module ActiveRecord
query += "WHERE t.oid::integer IN (%s)" % oids.join(", ")
end
- result = execute(query, 'SCHEMA')
- ranges, nodes = result.partition { |row| row['typtype'] == 'r' }
- enums, nodes = nodes.partition { |row| row['typtype'] == 'e' }
- domains, nodes = nodes.partition { |row| row['typtype'] == 'd' }
- arrays, nodes = nodes.partition { |row| row['typinput'] == 'array_in' }
- leaves, nodes = nodes.partition { |row| row['typelem'] == '0' }
-
- # populate the enum types
- enums.each do |row|
- type_map[row['oid'].to_i] = OID::Enum.new
- end
-
- # populate the base types
- leaves.find_all { |row| OID.registered_type? row['typname'] }.each do |row|
- type_map[row['oid'].to_i] = OID::NAMES[row['typname']]
- end
-
- records_by_oid = result.group_by { |row| row['oid'] }
-
- # populate composite types
- nodes.each do |row|
- add_oid row, records_by_oid, type_map
- end
-
- # populate array types
- arrays.find_all { |row| type_map.key? row['typelem'].to_i }.each do |row|
- array = OID::Array.new type_map[row['typelem'].to_i]
- type_map[row['oid'].to_i] = array
- end
-
- # populate range types
- ranges.find_all { |row| type_map.key? row['rngsubtype'].to_i }.each do |row|
- subtype = type_map[row['rngsubtype'].to_i]
- range = OID::Range.new subtype
- type_map[row['oid'].to_i] = range
- end
-
- # populate domain types
- domains.each do |row|
- base_type_oid = row["typbasetype"].to_i
- if base_type = type_map[base_type_oid]
- type_map[row['oid'].to_i] = base_type
- else
- warn "unknown base type (OID: #{base_type_oid}) for domain #{row["typname"]}."
- end
- end
+ initializer = OID::TypeMapInitializer.new(type_map)
+ records = execute(query, 'SCHEMA')
+ initializer.run(records)
end
FEATURE_NOT_SUPPORTED = "0A000" #:nodoc:
@@ -667,7 +570,7 @@ module ActiveRecord
end
def exec_no_cache(sql, name, binds)
- log(sql, name, binds) { @connection.async_exec(sql) }
+ log(sql, name, binds) { @connection.async_exec(sql, []) }
end
def exec_cache(sql, name, binds)
@@ -725,11 +628,6 @@ module ActiveRecord
@statements[sql_key]
end
- # The internal PostgreSQL identifier of the money data type.
- MONEY_COLUMN_TYPE_OID = 790 #:nodoc:
- # The internal PostgreSQL identifier of the BYTEA data type.
- BYTEA_COLUMN_TYPE_OID = 17 #:nodoc:
-
# Connects to a PostgreSQL server and sets up the adapter depending on the
# connected server's characteristics.
def connect
@@ -738,7 +636,7 @@ module ActiveRecord
# Money type has a fixed precision of 10 in PostgreSQL 8.2 and below, and as of
# PostgreSQL 8.3 it has a fixed precision of 19. PostgreSQLColumn.extract_precision
# should know about this but can't detect it there, so deal with it here.
- PostgreSQLColumn.money_precision = (postgresql_version >= 80300) ? 19 : 10
+ OID::Money.precision = (postgresql_version >= 80300) ? 19 : 10
configure_connection
rescue ::PG::Error => error
@@ -820,7 +718,7 @@ module ActiveRecord
# Query implementation notes:
# - format_type includes the column size constraint, e.g. varchar(50)
# - ::regclass is a function that gives the id for a table name
- def column_definitions(table_name) #:nodoc:
+ def column_definitions(table_name) # :nodoc:
exec_query(<<-end_sql, 'SCHEMA').rows
SELECT a.attname, format_type(a.atttypid, a.atttypmod),
pg_get_expr(d.adbin, d.adrelid), a.attnotnull, a.atttypid, a.atttypmod
@@ -832,23 +730,13 @@ module ActiveRecord
end_sql
end
- def extract_pg_identifier_from_name(name)
- match_data = name.start_with?('"') ? name.match(/\"([^\"]+)\"/) : name.match(/([^\.]+)/)
-
- if match_data
- rest = name[match_data[0].length, name.length]
- rest = rest[1, rest.length] if rest.start_with? "."
- [match_data[1], (rest.length > 0 ? rest : nil)]
- end
- end
-
- def extract_table_ref_from_insert_sql(sql)
+ def extract_table_ref_from_insert_sql(sql) # :nodoc:
sql[/into\s+([^\(]*).*values\s*\(/im]
$1.strip if $1
end
- def create_table_definition(name, temporary, options, as = nil)
- TableDefinition.new native_database_types, name, temporary, options, as
+ def create_table_definition(name, temporary, options, as = nil) # :nodoc:
+ PostgreSQL::TableDefinition.new native_database_types, name, temporary, options, as
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/schema_cache.rb b/activerecord/lib/active_record/connection_adapters/schema_cache.rb
index e5c9f6f54a..4d8afcf16a 100644
--- a/activerecord/lib/active_record/connection_adapters/schema_cache.rb
+++ b/activerecord/lib/active_record/connection_adapters/schema_cache.rb
@@ -12,11 +12,10 @@ module ActiveRecord
@columns_hash = {}
@primary_keys = {}
@tables = {}
- prepare_default_proc
end
def primary_keys(table_name)
- @primary_keys[table_name]
+ @primary_keys[table_name] ||= table_exists?(table_name) ? connection.primary_key(table_name) : nil
end
# A cached lookup for table existence.
@@ -29,9 +28,9 @@ module ActiveRecord
# Add internal cache for table with +table_name+.
def add(table_name)
if table_exists?(table_name)
- @primary_keys[table_name]
- @columns[table_name]
- @columns_hash[table_name]
+ primary_keys(table_name)
+ columns(table_name)
+ columns_hash(table_name)
end
end
@@ -40,14 +39,16 @@ module ActiveRecord
end
# Get the columns for a table
- def columns(table)
- @columns[table]
+ def columns(table_name)
+ @columns[table_name] ||= connection.columns(table_name)
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]
+ def columns_hash(table_name)
+ @columns_hash[table_name] ||= Hash[columns(table_name).map { |col|
+ [col.name, col]
+ }]
end
# Clears out internal caches
@@ -76,32 +77,11 @@ 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 { |val|
- Hash[val]
- }
+ [@version, @columns, @columns_hash, @primary_keys, @tables]
end
def marshal_load(array)
@version, @columns, @columns_hash, @primary_keys, @tables = array
- prepare_default_proc
- end
-
- private
-
- def prepare_default_proc
- @columns.default_proc = Proc.new do |h, table_name|
- h[table_name] = connection.columns(table_name)
- end
-
- @columns_hash.default_proc = Proc.new do |h, table_name|
- h[table_name] = Hash[columns(table_name).map { |col|
- [col.name, col]
- }]
- end
-
- @primary_keys.default_proc = Proc.new do |h, table_name|
- h[table_name] = table_exists?(table_name) ? connection.primary_key(table_name) : nil
- end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
index dd4261cec7..faf1cdc686 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
@@ -14,9 +14,9 @@ module ActiveRecord
raise ArgumentError, "No database file specified. Missing argument: database"
end
- # 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.
+ # 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 ':memory:' != config[:database]
config[:database] = File.expand_path(config[:database], Rails.root) if defined?(Rails.root)
dirname = File.dirname(config[:database])
@@ -41,13 +41,21 @@ module ActiveRecord
end
module ConnectionAdapters #:nodoc:
- class SQLite3Column < Column #:nodoc:
- class << self
- def binary_to_string(value)
- if value.encoding != Encoding::ASCII_8BIT
- value = value.force_encoding(Encoding::ASCII_8BIT)
- end
- value
+ class SQLite3Binary < Type::Binary # :nodoc:
+ def cast_value(value)
+ if value.encoding != Encoding::ASCII_8BIT
+ value = value.force_encoding(Encoding::ASCII_8BIT)
+ end
+ value
+ end
+ end
+
+ class SQLite3String < Type::String # :nodoc:
+ def type_cast_for_database(value)
+ if value.is_a?(::String) && value.encoding == Encoding::ASCII_8BIT
+ value.encode(Encoding::UTF_8)
+ else
+ super
end
end
end
@@ -69,7 +77,6 @@ module ActiveRecord
float: { name: "float" },
decimal: { name: "decimal" },
datetime: { name: "datetime" },
- timestamp: { name: "datetime" },
time: { name: "time" },
date: { name: "date" },
binary: { name: "blob" },
@@ -222,10 +229,19 @@ module ActiveRecord
# QUOTING ==================================================
- def quote(value, column = nil)
- if value.kind_of?(String) && column && column.type == :binary
- s = value.unpack("H*")[0]
- "x'#{s}'"
+ def _quote(value) # :nodoc:
+ case value
+ when Type::Binary::Data
+ "x'#{value.hex}'"
+ else
+ super
+ end
+ end
+
+ def _type_cast(value) # :nodoc:
+ case value
+ when BigDecimal
+ value.to_f
else
super
end
@@ -253,19 +269,6 @@ module ActiveRecord
end
end
- def type_cast(value, column) # :nodoc:
- return value.to_f if BigDecimal === value
- return super unless String === value
- return super unless column && value
-
- 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 = value.encode Encoding::UTF_8
- end
- value
- end
-
# DATABASE STATEMENTS ======================================
def explain(arel, binds = [])
@@ -296,9 +299,12 @@ module ActiveRecord
# 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
- stmt.close
+ begin
+ cols = stmt.columns
+ records = stmt.to_a
+ ensure
+ stmt.close
+ end
stmt = records
else
cache = @statements[sql] ||= {
@@ -379,7 +385,7 @@ module ActiveRecord
table_name && tables(nil, table_name).any?
end
- # Returns an array of +SQLite3Column+ objects for the table specified by +table_name+.
+ # Returns an array of +Column+ objects for the table specified by +table_name+.
def columns(table_name) #:nodoc:
table_structure(table_name).map do |field|
case field["dflt_value"]
@@ -391,7 +397,9 @@ module ActiveRecord
field["dflt_value"] = $1.gsub('""', '"')
end
- SQLite3Column.new(field['name'], field['dflt_value'], field['type'], field['notnull'].to_i == 0)
+ sql_type = field['type']
+ cast_type = lookup_cast_type(sql_type)
+ new_column(field['name'], field['dflt_value'], cast_type, sql_type, field['notnull'].to_i == 0)
end
end
@@ -492,14 +500,19 @@ module ActiveRecord
end
def rename_column(table_name, column_name, new_column_name) #:nodoc:
- unless columns(table_name).detect{|c| c.name == column_name.to_s }
- 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)
+ column = column_for(table_name, column_name)
+ alter_table(table_name, rename: {column.name => new_column_name.to_s})
+ rename_column_indexes(table_name, column.name, new_column_name)
end
protected
+
+ def initialize_type_map(m)
+ super
+ m.register_type(/binary/i, SQLite3Binary.new)
+ register_class_with_limit m, %r(char)i, SQLite3String
+ end
+
def select(sql, name = nil, binds = []) #:nodoc:
exec_query(sql, name, binds)
end
diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb
index 4571cc0786..d22806fbdf 100644
--- a/activerecord/lib/active_record/core.rb
+++ b/activerecord/lib/active_record/core.rb
@@ -16,7 +16,6 @@ module ActiveRecord
mattr_accessor :logger, instance_writer: false
##
- # :singleton-method:
# Contains the database configuration - as is typically stored in config/database.yml -
# as a Hash.
#
@@ -108,6 +107,11 @@ module ActiveRecord
end
module ClassMethods
+ def allocate
+ define_attribute_methods
+ super
+ end
+
def initialize_find_by_cache
self.find_by_statement_cache = {}.extend(Mutex_m)
end
@@ -249,16 +253,12 @@ module ActiveRecord
# # Instantiates a single new object
# 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)
- @column_types_override = nil
- @column_types = self.class.column_types
+ @attributes = self.class.default_attributes.dup
init_internals
initialize_internals_callback
+ self.class.define_attribute_methods
# +options+ argument is only needed to make protected_attributes gem easier to hook.
# Remove it when we drop support to this gem.
init_attributes(attributes, options) if attributes
@@ -278,13 +278,13 @@ module ActiveRecord
# post.init_with('attributes' => { 'title' => 'hello world' })
# post.title # => 'hello world'
def init_with(coder)
- @attributes = self.class.initialize_attributes(coder['attributes'])
- @column_types_override = coder['column_types']
- @column_types = self.class.column_types
+ @attributes = coder['attributes']
init_internals
- @new_record = false
+ @new_record = coder['new_record']
+
+ self.class.define_attribute_methods
run_callbacks :find
run_callbacks :initialize
@@ -320,17 +320,13 @@ module ActiveRecord
##
def initialize_dup(other) # :nodoc:
- cloned_attributes = other.clone_attributes(:read_attribute_before_type_cast)
- self.class.initialize_attributes(cloned_attributes, :serialized => false)
-
- @attributes = cloned_attributes
- @attributes[self.class.primary_key] = nil
+ @attributes = @attributes.dup
+ @attributes.reset(self.class.primary_key)
run_callbacks(:initialize) unless _initialize_callbacks.empty?
@aggregation_cache = {}
@association_cache = {}
- @attributes_cache = {}
@new_record = true
@destroyed = false
@@ -351,7 +347,10 @@ module ActiveRecord
# Post.new.encode_with(coder)
# coder # => {"attributes" => {"id" => nil, ... }}
def encode_with(coder)
- coder['attributes'] = attributes_for_coder
+ # FIXME: Remove this when we better serialize attributes
+ coder['raw_attributes'] = attributes_before_type_cast
+ coder['attributes'] = @attributes
+ coder['new_record'] = new_record?
end
# Returns true if +comparison_object+ is the same exact object, or +comparison_object+
@@ -374,7 +373,11 @@ module ActiveRecord
# Delegates to id in order to allow two records of the same type and id to work with something like:
# [ Person.find(1), Person.find(2), Person.find(3) ] & [ Person.find(1), Person.find(4) ] # => [ Person.find(1) ]
def hash
- id.hash
+ if id
+ id.hash
+ else
+ super
+ end
end
# Clone and freeze the attributes hash such that associations are still
@@ -430,6 +433,29 @@ module ActiveRecord
"#<#{self.class} #{inspection}>"
end
+ # Takes a PP and prettily prints this record to it, allowing you to get a nice result from `pp record`
+ # when pp is required.
+ def pretty_print(pp)
+ pp.object_address_group(self) do
+ if defined?(@attributes) && @attributes
+ column_names = self.class.column_names.select { |name| has_attribute?(name) || new_record? }
+ pp.seplist(column_names, proc { pp.text ',' }) do |column_name|
+ column_value = read_attribute(column_name)
+ pp.breakable ' '
+ pp.group(1) do
+ pp.text column_name
+ pp.text ':'
+ pp.breakable
+ pp.pp column_value
+ end
+ end
+ else
+ pp.breakable ' '
+ pp.text 'not initialized'
+ end
+ end
+ end
+
# 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
@@ -493,12 +519,10 @@ module ActiveRecord
end
def init_internals
- pk = self.class.primary_key
- @attributes[pk] = nil unless @attributes.key?(pk)
+ @attributes.ensure_initialized(self.class.primary_key)
@aggregation_cache = {}
@association_cache = {}
- @attributes_cache = {}
@readonly = false
@destroyed = false
@marked_for_destruction = false
@@ -518,5 +542,11 @@ module ActiveRecord
def init_attributes(attributes, options)
assign_attributes(attributes)
end
+
+ def thaw
+ if frozen?
+ @attributes = @attributes.dup
+ end
+ end
end
end
diff --git a/activerecord/lib/active_record/counter_cache.rb b/activerecord/lib/active_record/counter_cache.rb
index b7b790322a..a33c7c64a7 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 association counters to reset
+ # * +counters+ - One or more association counters to reset. Association name or counter name can be given.
#
# ==== Examples
#
@@ -19,9 +19,14 @@ module ActiveRecord
# Post.reset_counters(1, :comments)
def reset_counters(id, *counters)
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
+ counters.each do |counter_association|
+ has_many_association = _reflect_on_association(counter_association.to_sym)
+ unless has_many_association
+ has_many = reflect_on_all_associations(:has_many)
+ has_many_association = has_many.find { |association| association.counter_cache_column && association.counter_cache_column.to_sym == counter_association.to_sym }
+ counter_association = has_many_association.plural_name if has_many_association
+ end
+ raise ArgumentError, "'#{self.name}' has no association called '#{counter_association}'" unless has_many_association
if has_many_association.is_a? ActiveRecord::Reflection::ThroughReflection
has_many_association = has_many_association.through_reflection
@@ -29,12 +34,11 @@ module ActiveRecord
foreign_key = has_many_association.foreign_key.to_s
child_class = has_many_association.klass
- belongs_to = child_class.reflect_on_all_associations(:belongs_to)
- reflection = belongs_to.find { |e| e.foreign_key.to_s == foreign_key && e.options[:counter_cache].present? }
+ reflection = child_class._reflections.values.find { |e| :belongs_to == e.macro && e.foreign_key.to_s == foreign_key && e.options[:counter_cache].present? }
counter_name = reflection.counter_cache_column
stmt = unscoped.where(arel_table[primary_key].eq(object.id)).arel.compile_update({
- arel_table[counter_name] => object.send(association).count
+ arel_table[counter_name] => object.send(counter_association).count(:all)
}, primary_key)
connection.update stmt
end
@@ -162,7 +166,7 @@ module ActiveRecord
end
def each_counter_cached_associations
- reflections.each do |name, reflection|
+ _reflections.each do |name, reflection|
yield association(name) if reflection.belongs_to? && reflection.counter_cache_column
end
end
diff --git a/activerecord/lib/active_record/enum.rb b/activerecord/lib/active_record/enum.rb
index 18f1ca26de..f0ee433d0b 100644
--- a/activerecord/lib/active_record/enum.rb
+++ b/activerecord/lib/active_record/enum.rb
@@ -27,8 +27,10 @@ module ActiveRecord
# conversation.status # => nil
#
# Scopes based on the allowed values of the enum field will be provided
- # as well. With the above example, it will create an +active+ and +archived+
- # scope.
+ # as well. With the above example:
+ #
+ # Conversation.active
+ # Conversation.archived
#
# You can set the default value from the database declaration, like:
#
@@ -67,12 +69,12 @@ module ActiveRecord
#
# Where conditions on an enum attribute must use the ordinal value of an enum.
module Enum
- def self.extended(base)
+ def self.extended(base) # :nodoc:
base.class_attribute(:defined_enums)
base.defined_enums = {}
end
- def inherited(base)
+ def inherited(base) # :nodoc:
base.defined_enums = defined_enums.deep_dup
super
end
@@ -138,17 +140,14 @@ module ActiveRecord
@_enum_methods_module ||= begin
mod = Module.new do
private
- def save_changed_attribute(attr_name, value)
+ def save_changed_attribute(attr_name, old)
if (mapping = self.class.defined_enums[attr_name.to_s])
+ value = read_attribute(attr_name)
if attribute_changed?(attr_name)
- old = changed_attributes[attr_name]
-
if mapping[old] == value
changed_attributes.delete(attr_name)
end
else
- old = clone_attribute_value(:read_attribute, attr_name)
-
if old != value
changed_attributes[attr_name] = mapping.key old
end
diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb
index 71efbb8f93..52c70977ef 100644
--- a/activerecord/lib/active_record/errors.rb
+++ b/activerecord/lib/active_record/errors.rb
@@ -30,17 +30,18 @@ module ActiveRecord
class SerializationTypeMismatch < ActiveRecordError
end
- # Raised when adapter not specified on connection (or configuration file <tt>config/database.yml</tt>
- # misses adapter field).
+ # Raised when adapter not specified on connection (or configuration file
+ # +config/database.yml+ misses adapter field).
class AdapterNotSpecified < ActiveRecordError
end
- # Raised when Active Record cannot find database adapter specified in <tt>config/database.yml</tt> or programmatically.
+ # Raised when Active Record cannot find database adapter specified in
+ # +config/database.yml+ or programmatically.
class AdapterNotFound < ActiveRecordError
end
- # Raised when connection to the database could not been established (for example when <tt>connection=</tt>
- # is given a nil object).
+ # Raised when connection to the database could not been established (for
+ # example when +connection=+ is given a nil object).
class ConnectionNotEstablished < ActiveRecordError
end
@@ -82,19 +83,17 @@ module ActiveRecord
class InvalidForeignKey < WrappedDatabaseException
end
- # Raised when number of bind variables in statement given to <tt>:condition</tt> key (for example,
- # when using +find+ method)
- # does not match number of expected variables.
+ # Raised when number of bind variables in statement given to +:condition+ key
+ # (for example, when using +find+ method) does not match number of expected
+ # values supplied.
#
- # For example, in
+ # For example, when there are two placeholders with only one value supplied:
#
# Location.where("lat = ? AND lng = ?", 53.7362)
- #
- # two placeholders are given but only one variable to fill them.
class PreparedStatementInvalid < ActiveRecordError
end
- # Raised when a given database does not exist
+ # Raised when a given database does not exist.
class NoDatabaseError < StatementInvalid
end
@@ -102,7 +101,8 @@ module ActiveRecord
# instantiation, for example, when two users edit the same wiki page and one starts editing and saves
# the page before the other.
#
- # Read more about optimistic locking in ActiveRecord::Locking module RDoc.
+ # Read more about optimistic locking in ActiveRecord::Locking module
+ # documentation.
class StaleObjectError < ActiveRecordError
attr_reader :record, :attempted_action
@@ -114,8 +114,9 @@ module ActiveRecord
end
- # Raised when association is being configured improperly or
- # user tries to use offset and limit together with has_many or has_and_belongs_to_many associations.
+ # Raised when association is being configured improperly or user tries to use
+ # offset and limit together with +has_many+ or +has_and_belongs_to_many+
+ # associations.
class ConfigurationError < ActiveRecordError
end
@@ -153,7 +154,8 @@ module ActiveRecord
class Rollback < ActiveRecordError
end
- # Raised when attribute has a name reserved by Active Record (when attribute has name of one of Active Record instance methods).
+ # Raised when attribute has a name reserved by Active Record (when attribute
+ # has name of one of Active Record instance methods).
class DangerousAttributeError < ActiveRecordError
end
@@ -171,7 +173,7 @@ module ActiveRecord
end
# Raised when an error occurred while doing a mass assignment to an attribute through the
- # <tt>attributes=</tt> method. The exception has an +attribute+ property that is the name of the
+ # +attributes=+ method. The exception has an +attribute+ property that is the name of the
# offending attribute.
class AttributeAssignmentError < ActiveRecordError
attr_reader :exception, :attribute
@@ -217,6 +219,13 @@ module ActiveRecord
class ImmutableRelation < ActiveRecordError
end
+ # TransactionIsolationError will be raised under the following conditions:
+ #
+ # * The adapter does not support setting the isolation level
+ # * You are joining an existing open transaction
+ # * You are creating a nested (savepoint) transaction
+ #
+ # The mysql, mysql2 and postgresql adapters support setting the transaction isolation level.
class TransactionIsolationError < ActiveRecordError
end
end
diff --git a/activerecord/lib/active_record/explain.rb b/activerecord/lib/active_record/explain.rb
index e65dab07ba..727a9befc1 100644
--- a/activerecord/lib/active_record/explain.rb
+++ b/activerecord/lib/active_record/explain.rb
@@ -27,7 +27,7 @@ module ActiveRecord
end.join("\n")
end.join("\n")
- # Overriding inspect to be more human readable, specially in the console.
+ # Overriding inspect to be more human readable, especially in the console.
def str.inspect
self
end
diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb
index 47d32fae05..4306b36ae1 100644
--- a/activerecord/lib/active_record/fixtures.rb
+++ b/activerecord/lib/active_record/fixtures.rb
@@ -2,7 +2,7 @@ require 'erb'
require 'yaml'
require 'zlib'
require 'active_support/dependencies'
-require 'active_support/core_ext/securerandom'
+require 'active_support/core_ext/digest/uuid'
require 'active_record/fixture_set/file'
require 'active_record/errors'
@@ -15,9 +15,10 @@ module ActiveRecord
# They are stored in YAML files, one file per model, which are placed in the directory
# appointed by <tt>ActiveSupport::TestCase.fixture_path=(path)</tt> (this is automatically
# configured for Rails, so you can just put your files in <tt><your-rails-app>/test/fixtures/</tt>).
- # The fixture file ends with the <tt>.yml</tt> file extension (Rails example:
- # <tt><your-rails-app>/test/fixtures/web_sites.yml</tt>). The format of a fixture file looks
- # like this:
+ # The fixture file ends with the +.yml+ file extension, for example:
+ # <tt><your-rails-app>/test/fixtures/web_sites.yml</tt>).
+ #
+ # The format of a fixture file looks like this:
#
# rubyonrails:
# id: 1
@@ -33,7 +34,7 @@ module ActiveRecord
# is followed by an indented list of key/value pairs in the "key: value" format. Records are
# separated by a blank line for your viewing pleasure.
#
- # Note that fixtures are unordered. If you want ordered fixtures, use the omap YAML type.
+ # Note: Fixtures are unordered. If you want ordered fixtures, use the omap YAML type.
# See http://yaml.org/type/omap.html
# for the specification. You will need ordered fixtures when you have foreign key constraints
# on keys in the same table. This is commonly needed for tree structures. Example:
@@ -61,8 +62,8 @@ module ActiveRecord
# end
# end
#
- # By default, <tt>test_helper.rb</tt> will load all of your fixtures into your test database,
- # so this test will succeed.
+ # By default, +test_helper.rb+ will load all of your fixtures into your test
+ # database, so this test will succeed.
#
# The testing environment will automatically load the all fixtures into the database before each
# test. To ensure consistent data, the environment deletes the fixtures before running the load.
@@ -374,8 +375,9 @@ module ActiveRecord
#
# == Support for YAML defaults
#
- # You probably already know how to use YAML to set and reuse defaults in
- # your <tt>database.yml</tt> file. You can use the same technique in your fixtures:
+ # You can set and reuse defaults in your fixtures YAML file.
+ # This is the same technique used in the +database.yml+ file to specify
+ # defaults:
#
# DEFAULTS: &DEFAULTS
# created_on: <%= 3.weeks.ago.to_s(:db) %>
@@ -391,7 +393,8 @@ module ActiveRecord
# Any fixture labeled "DEFAULTS" is safely ignored.
class FixtureSet
#--
- # An instance of FixtureSet is normally stored in a single YAML file and possibly in a folder with the same name.
+ # An instance of FixtureSet is normally stored in a single YAML file and
+ # possibly in a folder with the same name.
#++
MAX_ID = 2 ** 30 - 1
@@ -461,13 +464,7 @@ module ActiveRecord
@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 to `set_fixture_class` will be removed in Rails 4.2. Use the class itself instead.")
- end
- !insert_class(@class_names, k, klass)
- }
+ @class_names.delete_if { |klass_name, klass| !insert_class(@class_names, klass_name, klass) }
end
def [](fs_name)
@@ -554,7 +551,7 @@ module ActiveRecord
# Integer identifiers are values less than 2^30. UUIDs are RFC 4122 version 5 SHA-1 hashes.
def self.identify(label, column_type = :integer)
if column_type == :uuid
- SecureRandom.uuid_v5(SecureRandom::UUID_OID_NAMESPACE, label.to_s)
+ Digest::UUID.uuid_v5(Digest::UUID::OID_NAMESPACE, label.to_s)
else
Zlib.crc32(label.to_s) % MAX_ID
end
@@ -573,10 +570,6 @@ module ActiveRecord
@config = config
@model_class = nil
- if class_name.is_a?(String)
- ActiveSupport::Deprecation.warn("The ability to pass in strings as a class name to `FixtureSet.new` will be removed in Rails 4.2. Use 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
@@ -649,14 +642,14 @@ module ActiveRecord
model_class
end
- reflection_class.reflect_on_all_associations.each do |association|
+ reflection_class._reflections.values.each do |association|
case association.macro
when :belongs_to
# Do not replace association name with association foreign key if they are named the same
fk_name = (association.options[:foreign_key] || "#{association.name}_id").to_s
if association.name.to_s != fk_name && value = row.delete(association.name.to_s)
- if association.options[:polymorphic] && value.sub!(/\s*\(([^\)]*)\)\s*$/, "")
+ if association.polymorphic? && value.sub!(/\s*\(([^\)]*)\)\s*$/, "")
# support polymorphic belongs_to as "label (Type)"
row[association.foreign_type] = $1
end
diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb
index 08fc91c9df..251d682a02 100644
--- a/activerecord/lib/active_record/inheritance.rb
+++ b/activerecord/lib/active_record/inheritance.rb
@@ -1,6 +1,37 @@
require 'active_support/core_ext/hash/indifferent_access'
module ActiveRecord
+ # == Single table inheritance
+ #
+ # Active Record allows inheritance by storing the name of the class in a column that by
+ # default is named "type" (can be changed by overwriting <tt>Base.inheritance_column</tt>).
+ # This means that an inheritance looking like this:
+ #
+ # class Company < ActiveRecord::Base; end
+ # class Firm < Company; end
+ # class Client < Company; end
+ # class PriorityClient < Client; end
+ #
+ # 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.
+ #
+ # Be aware that because the type column is an attribute on the record every new
+ # subclass will instantly be marked as dirty and the type column will be included
+ # in the list of changed attributes on the record. This is different from non
+ # STI classes:
+ #
+ # Company.new.changed? # => false
+ # Firm.new.changed? # => true
+ # Firm.new.changes # => {"type"=>["","Firm"]}
+ #
+ # 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
+ # for differentiating between them or reloading the right type with find.
+ #
+ # Note, all the attributes for all the cases are kept in the same table. Read more:
+ # http://www.martinfowler.com/eaaCatalog/singleTableInheritance.html
+ #
module Inheritance
extend ActiveSupport::Concern
@@ -120,14 +151,8 @@ module ActiveRecord
candidates << type_name
candidates.each do |candidate|
- begin
- constant = ActiveSupport::Dependencies.constantize(candidate)
- return constant if candidate == constant.to_s
- # We don't want to swallow NoMethodError < NameError errors
- rescue NoMethodError
- raise
- rescue NameError
- end
+ constant = ActiveSupport::Dependencies.safe_constantize(candidate)
+ return constant if candidate == constant.to_s
end
raise NameError.new("uninitialized constant #{candidates.first}", candidates.first)
diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb
index 4d63b04d9f..52eeb8ae1f 100644
--- a/activerecord/lib/active_record/locking/optimistic.rb
+++ b/activerecord/lib/active_record/locking/optimistic.rb
@@ -66,7 +66,7 @@ module ActiveRecord
send(lock_col + '=', previous_lock_value + 1)
end
- def _update_record(attribute_names = @attributes.keys) #:nodoc:
+ def _update_record(attribute_names = self.attribute_names) #:nodoc:
return super unless locking_enabled?
return 0 if attribute_names.empty?
@@ -141,7 +141,7 @@ module ActiveRecord
# Set the column to use for optimistic locking. Defaults to +lock_version+.
def locking_column=(value)
- @column_defaults = nil
+ clear_caches_calculated_from_columns
@locking_column = value.to_s
end
@@ -151,12 +151,6 @@ module ActiveRecord
@locking_column
end
- # 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
-
# Reset the column used for optimistic locking back to the +lock_version+ default.
def reset_locking_column
self.locking_column = DEFAULT_LOCKING_COLUMN
@@ -169,18 +163,42 @@ module ActiveRecord
super
end
- def column_defaults
- @column_defaults ||= begin
- defaults = super
-
- if defaults.key?(locking_column) && lock_optimistically
- defaults[locking_column] ||= 0
+ private
+
+ # We need to apply this decorator here, rather than on module inclusion. The closure
+ # created by the matcher would otherwise evaluate for `ActiveRecord::Base`, not the
+ # sub class being decorated. As such, changes to `lock_optimistically`, or
+ # `locking_column` would not be picked up.
+ def inherited(subclass)
+ subclass.class_eval do
+ is_lock_column = ->(name, _) { lock_optimistically && name == locking_column }
+ decorate_matching_attribute_types(is_lock_column, :_optimistic_locking) do |type|
+ LockingType.new(type)
end
-
- defaults
end
+ super
end
end
end
+
+ class LockingType < SimpleDelegator # :nodoc:
+ def type_cast_from_database(value)
+ # `nil` *should* be changed to 0
+ super.to_i
+ end
+
+ def changed?(old_value, *)
+ # Ensure we save if the default was `nil`
+ super || old_value == 0
+ end
+
+ def init_with(coder)
+ __setobj__(coder['subtype'])
+ end
+
+ def encode_with(coder)
+ coder['subtype'] = __getobj__
+ end
+ end
end
end
diff --git a/activerecord/lib/active_record/log_subscriber.rb b/activerecord/lib/active_record/log_subscriber.rb
index 654ef21b07..eb64d197f0 100644
--- a/activerecord/lib/active_record/log_subscriber.rb
+++ b/activerecord/lib/active_record/log_subscriber.rb
@@ -25,7 +25,7 @@ module ActiveRecord
if column.binary?
# This specifically deals with the PG adapter that casts bytea columns into a Hash.
value = value[:value] if value.is_a?(Hash)
- value = "<#{value.bytesize} bytes of binary data>"
+ value = value ? "<#{value.bytesize} bytes of binary data>" : "<NULL binary data>"
end
[column.name, value]
diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb
index b6b02322d7..e94b6ae9eb 100644
--- a/activerecord/lib/active_record/migration.rb
+++ b/activerecord/lib/active_record/migration.rb
@@ -195,7 +195,7 @@ module ActiveRecord
# == Database support
#
# Migrations are currently supported in MySQL, PostgreSQL, SQLite,
- # SQL Server, Sybase, and Oracle (all supported databases except DB2).
+ # SQL Server, and Oracle (all supported databases except DB2).
#
# == More examples
#
@@ -372,13 +372,21 @@ module ActiveRecord
end
def call(env)
- mtime = ActiveRecord::Migrator.last_migration.mtime.to_i
- if @last_check < mtime
- ActiveRecord::Migration.check_pending!
- @last_check = mtime
+ if connection.supports_migrations?
+ mtime = ActiveRecord::Migrator.last_migration.mtime.to_i
+ if @last_check < mtime
+ ActiveRecord::Migration.check_pending!(connection)
+ @last_check = mtime
+ end
end
@app.call(env)
end
+
+ private
+
+ def connection
+ ActiveRecord::Base.connection
+ end
end
class << self
@@ -640,9 +648,11 @@ module ActiveRecord
say_with_time "#{method}(#{arg_list})" do
unless @connection.respond_to? :revert
- unless arguments.empty? || method == :execute
+ unless arguments.empty? || [:execute, :enable_extension, :disable_extension].include?(method)
arguments[0] = proper_table_name(arguments.first, table_name_options)
- arguments[1] = proper_table_name(arguments.second, table_name_options) if method == :rename_table
+ if [:rename_table, :add_foreign_key].include?(method)
+ arguments[1] = proper_table_name(arguments.second, table_name_options)
+ end
end
end
return super unless connection.respond_to?(method)
@@ -711,7 +721,7 @@ module ActiveRecord
if ActiveRecord::Base.timestamped_migrations
[Time.now.utc.strftime("%Y%m%d%H%M%S"), "%.14d" % number].max
else
- "%.3d" % number
+ SchemaMigration.normalize_migration_number(number)
end
end
@@ -851,19 +861,6 @@ module ActiveRecord
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
@migrations_paths ||= ['db/migrate']
# just to not break things if someone uses: migration_path = some_string
diff --git a/activerecord/lib/active_record/migration/command_recorder.rb b/activerecord/lib/active_record/migration/command_recorder.rb
index 5337106001..36256415df 100644
--- a/activerecord/lib/active_record/migration/command_recorder.rb
+++ b/activerecord/lib/active_record/migration/command_recorder.rb
@@ -74,7 +74,9 @@ module ActiveRecord
:rename_index, :rename_column, :add_index, :remove_index, :add_timestamps, :remove_timestamps,
:change_column_default, :add_reference, :remove_reference, :transaction,
:drop_join_table, :drop_table, :execute_block, :enable_extension,
- :change_column, :execute, :remove_columns, :change_column_null # irreversible methods need to be here too
+ :change_column, :execute, :remove_columns, :change_column_null,
+ :add_foreign_key, :remove_foreign_key
+ # irreversible methods need to be here too
].each do |method|
class_eval <<-EOV, __FILE__, __LINE__ + 1
def #{method}(*args, &block) # def create_table(*args, &block)
@@ -167,6 +169,21 @@ module ActiveRecord
[:change_column_null, args]
end
+ def invert_add_foreign_key(args)
+ from_table, to_table, add_options = args
+ add_options ||= {}
+
+ if add_options[:name]
+ options = { name: add_options[:name] }
+ elsif add_options[:column]
+ options = { column: add_options[:column] }
+ else
+ options = to_table
+ end
+
+ [:remove_foreign_key, [from_table, options]]
+ end
+
# Forwards any missing method call to the \target.
def method_missing(method, *args, &block)
if @delegate.respond_to?(method)
diff --git a/activerecord/lib/active_record/migration/join_table.rb b/activerecord/lib/active_record/migration/join_table.rb
index ebf64cbcdc..05569fadbd 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.to_s, table_2.to_s].sort.join("_").to_sym
+ ModelSchema.derive_join_table_name(table_1, table_2).to_sym
end
end
end
diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb
index dc5ff02882..850220babd 100644
--- a/activerecord/lib/active_record/model_schema.rb
+++ b/activerecord/lib/active_record/model_schema.rb
@@ -29,6 +29,10 @@ module ActiveRecord
# :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.
+ #
+ # If you are organising your models within modules, you can add a suffix to the models within
+ # a namespace by defining a singleton method in the parent module called table_name_suffix which
+ # returns your chosen suffix.
class_attribute :table_name_suffix, instance_writer: false
self.table_name_suffix = ""
@@ -47,6 +51,19 @@ module ActiveRecord
self.pluralize_table_names = true
self.inheritance_column = 'type'
+
+ delegate :type_for_attribute, to: :class
+ end
+
+ # Derives the join table name for +first_table+ and +second_table+. The
+ # table names appear in alphabetical order. A common prefix is removed
+ # (useful for namespaced models like Music::Artist and Music::Record):
+ #
+ # artists, records => artists_records
+ # records, artists => artists_records
+ # music_artists, music_records => music_artists_records
+ def self.derive_join_table_name(first_table, second_table) # :nodoc:
+ [first_table.to_s, second_table.to_s].sort.join("\0").gsub(/^(.*_)(.+)\0\1(.+)/, '\1\2_\3').gsub("\0", "_")
end
module ClassMethods
@@ -153,6 +170,10 @@ module ActiveRecord
(parents.detect{ |p| p.respond_to?(:table_name_prefix) } || self).table_name_prefix
end
+ def full_table_name_suffix #:nodoc:
+ (parents.detect {|p| p.respond_to?(:table_name_suffix) } || self).table_name_suffix
+ end
+
# Defines the name of the table column which will store the class name on single-table
# inheritance situations.
#
@@ -190,7 +211,7 @@ module ActiveRecord
# given block. This is required for Oracle and is useful for any
# database which relies on sequences for primary key generation.
#
- # If a sequence name is not explicitly set when using Oracle or Firebird,
+ # If a sequence name is not explicitly set when using Oracle,
# it will default to the commonly used pattern of: #{table_name}_seq
#
# If a sequence name is not explicitly set when using PostgreSQL, it
@@ -209,50 +230,29 @@ module ActiveRecord
connection.schema_cache.table_exists?(table_name)
end
- # 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|
- col = col.dup
- col.primary = (col.name == primary_key)
- col
- end
- end
-
- # Returns a hash of column objects for the table associated with this class.
- def columns_hash
- @columns_hash ||= Hash[columns.map { |c| [c.name, c] }]
+ def attributes_builder # :nodoc:
+ @attributes_builder ||= AttributeSet::Builder.new(column_types)
end
def column_types # :nodoc:
- @column_types ||= decorate_columns(columns_hash.dup)
- end
-
- def decorate_columns(columns_hash) # :nodoc:
- return if columns_hash.empty?
-
- @serialized_column_names ||= self.columns_hash.keys.find_all do |name|
- serialized_attributes.key?(name)
- end
-
- @serialized_column_names.each do |name|
- columns_hash[name] = AttributeMethods::Serialization::Type.new(columns_hash[name])
- end
-
- @time_zone_column_names ||= self.columns_hash.find_all do |name, col|
- create_time_zone_conversion_attribute?(name, col)
- end.map!(&:first)
-
- @time_zone_column_names.each do |name|
- columns_hash[name] = AttributeMethods::TimeZoneConversion::Type.new(columns_hash[name])
+ @column_types ||= columns_hash.transform_values(&:cast_type).tap do |h|
+ h.default = Type::Value.new
end
+ end
- columns_hash
+ def type_for_attribute(attr_name) # :nodoc:
+ column_types[attr_name]
end
# Returns a hash where the keys are column names and the values are
# default values when instantiating the AR object for this table.
def column_defaults
- @column_defaults ||= Hash[columns.map { |c| [c.name, c.default] }]
+ default_attributes.to_hash
+ end
+
+ def default_attributes # :nodoc:
+ @default_attributes ||= attributes_builder.build_from_database(
+ columns_hash.transform_values(&:default))
end
# Returns an array of column names as strings.
@@ -263,7 +263,7 @@ module ActiveRecord
# Returns an array of column objects where the primary id, all columns ending in "_id" or "_count",
# and columns used for single table inheritance have been removed.
def content_columns
- @content_columns ||= columns.reject { |c| c.primary || c.name =~ /(_id|_count)$/ || c.name == inheritance_column }
+ @content_columns ||= columns.reject { |c| c.name == primary_key || c.name =~ /(_id|_count)$/ || c.name == inheritance_column }
end
# Resets all the cached information about columns, which will cause them
@@ -298,27 +298,17 @@ module ActiveRecord
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
+ @default_attributes = 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
- # attributes when they are initialized. (e.g. attribute
- # serialization)
- def initialize_attributes(attributes, options = {}) #:nodoc:
- attributes
- end
-
private
# Guesses the table name, but does not decorate it with prefix and suffix information.
@@ -337,7 +327,8 @@ module ActiveRecord
contained = contained.singularize if parent.pluralize_table_names
contained += '_'
end
- "#{full_table_name_prefix}#{contained}#{undecorated_table_name(name)}#{table_name_suffix}"
+
+ "#{full_table_name_prefix}#{contained}#{undecorated_table_name(name)}#{full_table_name_suffix}"
else
# STI subclasses always use their superclass' table.
base.table_name
diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb
index e6195e48a5..8a2a06f2ca 100644
--- a/activerecord/lib/active_record/nested_attributes.rb
+++ b/activerecord/lib/active_record/nested_attributes.rb
@@ -305,7 +305,7 @@ module ActiveRecord
options[:reject_if] = REJECT_ALL_BLANK_PROC if options[:reject_if] == :all_blank
attr_names.each do |association_name|
- if reflection = reflect_on_association(association_name)
+ if reflection = _reflect_on_association(association_name)
reflection.autosave = true
add_autosave_association_callbacks(reflection)
@@ -516,7 +516,7 @@ module ActiveRecord
# Determines if a hash contains a truthy _destroy key.
def has_destroy_flag?(hash)
- ConnectionAdapters::Column.value_to_boolean(hash['_destroy'])
+ Type::Boolean.new.type_cast_from_user(hash['_destroy'])
end
# Determines if a new record should be rejected by checking
@@ -542,7 +542,7 @@ module ActiveRecord
end
def raise_nested_attributes_record_not_found!(association_name, record_id)
- raise RecordNotFound, "Couldn't find #{self.class.reflect_on_association(association_name).klass.name} with ID=#{record_id} for #{self.class.name} with ID=#{id}"
+ 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
end
diff --git a/activerecord/lib/active_record/null_relation.rb b/activerecord/lib/active_record/null_relation.rb
index 05d0c41678..807c301596 100644
--- a/activerecord/lib/active_record/null_relation.rb
+++ b/activerecord/lib/active_record/null_relation.rb
@@ -23,7 +23,7 @@ module ActiveRecord
end
def size
- 0
+ calculate :size, nil
end
def empty?
@@ -47,14 +47,28 @@ module ActiveRecord
end
def sum(*)
- 0
+ calculate :sum, nil
+ end
+
+ def average(*)
+ calculate :average, nil
+ end
+
+ def minimum(*)
+ calculate :minimum, nil
+ end
+
+ def maximum(*)
+ calculate :maximum, nil
end
def calculate(operation, _column_name, _options = {})
# TODO: Remove _options argument as soon we remove support to
# activerecord-deprecated_finders.
- if operation == :count
+ if [:count, :sum, :size].include? operation
group_values.any? ? Hash.new : 0
+ elsif [:average, :minimum, :maximum].include?(operation) && group_values.any?
+ Hash.new
else
nil
end
diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb
index 13d7432773..96e44c2f59 100644
--- a/activerecord/lib/active_record/persistence.rb
+++ b/activerecord/lib/active_record/persistence.rb
@@ -37,7 +37,7 @@ module ActiveRecord
end
# Given an attributes hash, +instantiate+ returns a new instance of
- # the appropriate class.
+ # the appropriate class. Accepts only keys as strings.
#
# For example, +Post.all+ may return Comments, Messages, and Emails
# by storing the record's subclass in a +type+ attribute. By calling
@@ -46,10 +46,10 @@ module ActiveRecord
#
# 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)
+ def instantiate(attributes, column_types = {})
+ klass = discriminate_class_for_record(attributes)
+ attributes = klass.attributes_builder.build_from_database(attributes, column_types)
+ klass.allocate.init_with('attributes' => attributes, 'new_record' => false)
end
private
@@ -180,7 +180,6 @@ module ActiveRecord
def becomes(klass)
became = klass.new
became.instance_variable_set("@attributes", @attributes)
- became.instance_variable_set("@attributes_cache", @attributes_cache)
became.instance_variable_set("@changed_attributes", @changed_attributes) if defined?(@changed_attributes)
became.instance_variable_set("@new_record", new_record?)
became.instance_variable_set("@destroyed", destroyed?)
@@ -396,11 +395,8 @@ module ActiveRecord
self.class.unscoped { self.class.find(id) }
end
- @attributes.update(fresh_object.instance_variable_get('@attributes'))
-
- @column_types = self.class.column_types
- @column_types_override = fresh_object.instance_variable_get('@column_types_override')
- @attributes_cache = {}
+ @attributes = fresh_object.instance_variable_get('@attributes')
+ @new_record = false
self
end
@@ -490,7 +486,7 @@ module ActiveRecord
# Updates the associated record with values matching those of the instance attributes.
# Returns the number of affected rows.
- def _update_record(attribute_names = @attributes.keys)
+ def _update_record(attribute_names = self.attribute_names)
attributes_values = arel_attributes_with_values_for_update(attribute_names)
if attributes_values.empty?
0
@@ -501,7 +497,7 @@ module ActiveRecord
# Creates a record with values matching those of the instance attributes
# and returns its id.
- def _create_record(attribute_names = @attributes.keys)
+ def _create_record(attribute_names = self.attribute_names)
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 df8654e5c1..16ad942912 100644
--- a/activerecord/lib/active_record/query_cache.rb
+++ b/activerecord/lib/active_record/query_cache.rb
@@ -29,9 +29,10 @@ module ActiveRecord
end
def call(env)
- enabled = ActiveRecord::Base.connection.query_cache_enabled
+ connection = ActiveRecord::Base.connection
+ enabled = connection.query_cache_enabled
connection_id = ActiveRecord::Base.connection_id
- ActiveRecord::Base.connection.enable_query_cache!
+ connection.enable_query_cache!
response = @app.call(env)
response[2] = Rack::BodyProxy.new(response[2]) do
diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb
index ef138c6f80..a9ddd9141f 100644
--- a/activerecord/lib/active_record/querying.rb
+++ b/activerecord/lib/active_record/querying.rb
@@ -37,14 +37,7 @@ module ActiveRecord
# Post.find_by_sql ["SELECT body FROM comments WHERE author = :user_id OR approved_by = :user_id", { :user_id => user_id }]
def find_by_sql(sql, binds = [])
result_set = connection.select_all(sanitize_sql(sql), "#{name} Load", binds)
- column_types = {}
-
- 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
-
+ column_types = result_set.column_types.except(*columns_hash.keys)
result_set.map { |record| instantiate(record, column_types) }
end
diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake
index 6b0459ea37..fa94df7a52 100644
--- a/activerecord/lib/active_record/railties/databases.rake
+++ b/activerecord/lib/active_record/railties/databases.rake
@@ -28,6 +28,17 @@ db_namespace = namespace :db do
ActiveRecord::Tasks::DatabaseTasks.drop_current
end
+ namespace :purge do
+ task :all => :load_config do
+ ActiveRecord::Tasks::DatabaseTasks.purge_all
+ end
+ end
+
+ # desc "Empty the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:drop:all to drop all databases in the config). Without RAILS_ENV it defaults to purging the development and test databases."
+ task :purge => [:load_config] do
+ ActiveRecord::Tasks::DatabaseTasks.purge_current
+ end
+
desc "Migrate the database (options: VERSION=x, VERBOSE=false, SCOPE=blog)."
task :migrate => [:environment, :load_config] do
ActiveRecord::Migration.verbose = ENV["VERBOSE"] ? ENV["VERBOSE"] == "true" : true
@@ -82,22 +93,21 @@ db_namespace = namespace :db do
desc 'Display status of migrations'
task :status => [:environment, :load_config] do
- unless ActiveRecord::Base.connection.table_exists?(ActiveRecord::Migrator.schema_migrations_table_name)
- puts 'Schema migrations table does not exist yet.'
- next # means "return" for rake task
+ unless ActiveRecord::SchemaMigration.table_exists?
+ abort 'Schema migrations table does not exist yet.'
end
- db_list = ActiveRecord::Base.connection.select_values("SELECT version FROM #{ActiveRecord::Migrator.schema_migrations_table_name}")
- db_list.map! { |version| "%.3d" % version }
- file_list = []
- ActiveRecord::Migrator.migrations_paths.each do |path|
- Dir.foreach(path) do |file|
- # match "20091231235959_some_name.rb" and "001_some_name.rb" pattern
- if match_data = /^(\d{3,})_(.+)\.rb$/.match(file)
- status = db_list.delete(match_data[1]) ? 'up' : 'down'
- file_list << [status, match_data[1], match_data[2].humanize]
+ db_list = ActiveRecord::SchemaMigration.normalized_versions
+
+ file_list =
+ ActiveRecord::Migrator.migrations_paths.flat_map do |path|
+ # match "20091231235959_some_name.rb" and "001_some_name.rb" pattern
+ Dir.foreach(path).grep(/^(\d{3,})_(.+)\.rb$/) do
+ version = ActiveRecord::SchemaMigration.normalize_migration_number($1)
+ status = db_list.delete(version) ? 'up' : 'down'
+ [status, version, $2.humanize]
+ end
end
- end
- end
+
db_list.map! do |version|
['up', version, '********** NO FILE **********']
end
@@ -105,8 +115,8 @@ db_namespace = namespace :db do
puts "\ndatabase: #{ActiveRecord::Base.connection_config[:database]}\n\n"
puts "#{'Status'.center(8)} #{'Migration ID'.ljust(14)} Migration Name"
puts "-" * 50
- (db_list + file_list).sort_by {|migration| migration[1]}.each do |migration|
- puts "#{migration[0].center(8)} #{migration[1].ljust(14)} #{migration[2]}"
+ (db_list + file_list).sort_by { |_, version, _| version }.each do |status, version, name|
+ puts "#{status.center(8)} #{version.ljust(14)} #{name}"
end
puts
end
@@ -178,17 +188,21 @@ db_namespace = namespace :db do
task :load => [:environment, :load_config] do
require 'active_record/fixtures'
- base_dir = if ENV['FIXTURES_PATH']
- File.join [Rails.root, ENV['FIXTURES_PATH'] || %w{test fixtures}].flatten
- else
- ActiveRecord::Tasks::DatabaseTasks.fixtures_path
- end
+ base_dir = ActiveRecord::Tasks::DatabaseTasks.fixtures_path
- fixtures_dir = File.join [base_dir, ENV['FIXTURES_DIR']].compact
+ fixtures_dir = if ENV['FIXTURES_DIR']
+ File.join base_dir, ENV['FIXTURES_DIR']
+ else
+ base_dir
+ end
- (ENV['FIXTURES'] ? ENV['FIXTURES'].split(',') : Dir["#{fixtures_dir}/**/*.yml"].map {|f| f[(fixtures_dir.size + 1)..-5] }).each do |fixture_file|
- ActiveRecord::FixtureSet.create_fixtures(fixtures_dir, fixture_file)
- end
+ fixture_files = if ENV['FIXTURES']
+ ENV['FIXTURES'].split(',')
+ else
+ Pathname.glob("#{fixtures_dir}/**/*.yml").map {|f| f.basename.sub_ext('').to_s }
+ end
+
+ ActiveRecord::FixtureSet.create_fixtures(fixtures_dir, fixture_files)
end
# desc "Search for a fixture given a LABEL or ID. Specify an alternative path (eg. spec/fixtures) using FIXTURES_PATH=spec/fixtures."
@@ -200,12 +214,7 @@ db_namespace = namespace :db do
puts %Q(The fixture ID for "#{label}" is #{ActiveRecord::FixtureSet.identify(label)}.) if label
- base_dir = if ENV['FIXTURES_PATH']
- File.join [Rails.root, ENV['FIXTURES_PATH'] || %w{test fixtures}].flatten
- else
- ActiveRecord::Tasks::DatabaseTasks.fixtures_path
- end
-
+ base_dir = ActiveRecord::Tasks::DatabaseTasks.fixtures_path
Dir["#{base_dir}/**/*.yml"].each do |file|
if data = YAML::load(ERB.new(IO.read(file)).result)
@@ -278,7 +287,7 @@ db_namespace = namespace :db do
db_namespace['structure:dump'].reenable
end
- # desc "Recreate the databases from the structure.sql file"
+ desc "Recreate the databases from the structure.sql file"
task :load => [:environment, :load_config] do
ActiveRecord::Tasks::DatabaseTasks.load_schema(:sql, ENV['DB_STRUCTURE'])
end
@@ -367,7 +376,7 @@ namespace :railties do
task :migrations => :'db:load_config' do
to_load = ENV['FROM'].blank? ? :all : ENV['FROM'].split(",").map {|n| n.strip }
railties = {}
- Rails.application.railties.each do |railtie|
+ Rails.application.migration_railties.each do |railtie|
next unless to_load == :all || to_load.include?(railtie.railtie_name)
if railtie.respond_to?(:paths) && (path = railtie.paths['db/migrate'].first)
diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb
index 95485ddada..1672128aa3 100644
--- a/activerecord/lib/active_record/reflection.rb
+++ b/activerecord/lib/active_record/reflection.rb
@@ -6,25 +6,32 @@ module ActiveRecord
extend ActiveSupport::Concern
included do
- class_attribute :reflections
+ class_attribute :_reflections
class_attribute :aggregate_reflections
- self.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)
+ klass = case macro
+ when :composed_of
+ AggregateReflection
+ when :has_many
+ HasManyReflection
+ when :has_one
+ HasOneReflection
+ when :belongs_to
+ BelongsToReflection
+ else
+ raise "Unsupported Macro: #{macro}"
+ end
+
+ reflection = klass.new(name, scope, options, ar)
+ options[:through] ? ThroughReflection.new(reflection) : reflection
end
def self.add_reflection(ar, name, reflection)
- ar.reflections = ar.reflections.merge(name.to_s => reflection)
+ ar._reflections = ar._reflections.merge(name.to_s => reflection)
end
def self.add_aggregate_reflection(ar, name, reflection)
@@ -53,6 +60,24 @@ module ActiveRecord
aggregate_reflections[aggregation.to_s]
end
+ # Returns a Hash of name of the reflection as the key and a AssociationReflection as the value.
+ #
+ # Account.reflections # => {balance: AggregateReflection}
+ #
+ # @api public
+ def reflections
+ ref = {}
+ _reflections.each do |name, reflection|
+ parent_name, parent_reflection = reflection.parent_reflection
+ if parent_name
+ ref[parent_name] = parent_reflection
+ else
+ ref[name] = reflection
+ end
+ end
+ ref
+ end
+
# Returns an array of AssociationReflection objects for all the
# associations in the class. If you only want to reflect on a certain
# association type, pass in the symbol (<tt>:has_many</tt>, <tt>:has_one</tt>,
@@ -63,6 +88,7 @@ module ActiveRecord
# Account.reflect_on_all_associations # returns an array of all associations
# Account.reflect_on_all_associations(:has_many) # returns an array of all has_many associations
#
+ # @api public
def reflect_on_all_associations(macro = nil)
association_reflections = reflections.values
macro ? association_reflections.select { |reflection| reflection.macro == macro } : association_reflections
@@ -73,36 +99,87 @@ module ActiveRecord
# Account.reflect_on_association(:owner) # returns the owner AssociationReflection
# Invoice.reflect_on_association(:line_items).macro # returns :has_many
#
+ # @api public
def reflect_on_association(association)
reflections[association.to_s]
end
+ # @api private
+ def _reflect_on_association(association) #:nodoc:
+ _reflections[association.to_s]
+ end
+
# Returns an array of AssociationReflection objects for all associations which have <tt>:autosave</tt> enabled.
+ #
+ # @api public
def reflect_on_all_autosave_associations
reflections.values.select { |reflection| reflection.options[:autosave] }
end
end
+ # Holds all the methods that are shared between MacroReflection, AssociationReflection
+ # and ThroughReflection
+ class AbstractReflection # :nodoc:
+ def table_name
+ klass.table_name
+ end
+
+ # 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 quoted_table_name
+ klass.quoted_table_name
+ end
+
+ def primary_key_type
+ klass.type_for_attribute(klass.primary_key)
+ end
+
+ # Returns the class name for the macro.
+ #
+ # <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
+ end
+
+ JoinKeys = Struct.new(:key, :foreign_key) # :nodoc:
+
+ def join_keys(assoc_klass)
+ if source_macro == :belongs_to
+ if polymorphic?
+ reflection_key = association_primary_key(assoc_klass)
+ else
+ reflection_key = association_primary_key
+ end
+ reflection_foreign_key = foreign_key
+ else
+ reflection_foreign_key = active_record_primary_key
+ reflection_key = foreign_key
+ end
+ JoinKeys.new(reflection_key, reflection_foreign_key)
+ end
+ end
# Base class for AggregateReflection and AssociationReflection. Objects of
# AggregateReflection and AssociationReflection are returned by the Reflection::ClassMethods.
#
# MacroReflection
- # AggregateReflection
# AssociationReflection
- # ThroughReflection
- class MacroReflection
+ # AggregateReflection
+ # HasManyReflection
+ # HasOneReflection
+ # BelongsToReflection
+ # ThroughReflection
+ class MacroReflection < AbstractReflection
# Returns the name of the macro.
#
# <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>has_many :clients</tt> returns <tt>:has_many</tt>
- attr_reader :macro
-
attr_reader :scope
# Returns the hash of options used for the macro.
@@ -115,8 +192,7 @@ module ActiveRecord
attr_reader :plural_name # :nodoc:
- def initialize(macro, name, scope, options, active_record)
- @macro = macro
+ def initialize(name, scope, options, active_record)
@name = name
@scope = scope
@options = options
@@ -129,6 +205,10 @@ module ActiveRecord
def autosave=(autosave)
@automatic_inverse_of = false
@options[:autosave] = autosave
+ _, parent_reflection = self.parent_reflection
+ if parent_reflection
+ parent_reflection.autosave = autosave
+ end
end
# Returns the class for the macro.
@@ -136,15 +216,11 @@ module ActiveRecord
# <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
+ @klass ||= compute_class(class_name)
end
- # Returns the class name for the macro.
- #
- # <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
+ def compute_class(name)
+ name.constantize
end
# Returns +true+ if +self+ and +other_aggregation+ have the same +name+ attribute, +active_record+ attribute,
@@ -189,14 +265,18 @@ module ActiveRecord
# a new association object. Use +build_association+ or +create_association+
# instead. This allows plugins to hook into association object creation.
def klass
- @klass ||= active_record.send(:compute_type, class_name)
+ @klass ||= compute_class(class_name)
+ end
+
+ def compute_class(name)
+ active_record.send(:compute_type, name)
end
attr_reader :type, :foreign_type
+ attr_accessor :parent_reflection # [:name, Reflection]
- def initialize(macro, name, scope, options, active_record)
+ def initialize(name, scope, options, active_record)
super
- @collection = :has_many == macro
@automatic_inverse_of = nil
@type = options[:as] && "#{options[:as]}_type"
@foreign_type = options[:foreign_type] || "#{name}_type"
@@ -207,7 +287,7 @@ module ActiveRecord
def association_scope_cache(conn, owner)
key = conn.prepared_statements
- if options[:polymorphic]
+ if polymorphic?
key = [key, owner.read_attribute(@foreign_type)]
end
@association_scope_cache[key] ||= @scope_lock.synchronize {
@@ -215,24 +295,10 @@ module ActiveRecord
}
end
- # 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
- klass.table_name
- end
-
- def quoted_table_name
- klass.quoted_table_name
- end
-
def join_table
@join_table ||= options[:join_table] || derive_join_table
end
@@ -241,10 +307,6 @@ module ActiveRecord
@foreign_key ||= options[:foreign_key] || derive_foreign_key
end
- def primary_key_column
- klass.columns_hash[klass.primary_key]
- end
-
def association_foreign_key
@association_foreign_key ||= options[:association_foreign_key] || class_name.foreign_key
end
@@ -271,13 +333,32 @@ module ActiveRecord
end
def check_validity_of_inverse!
- unless options[:polymorphic]
+ unless polymorphic?
if has_inverse? && inverse_of.nil?
raise InverseOfAssociationNotFoundError.new(self)
end
end
end
+ def check_preloadable!
+ return unless scope
+
+ if scope.arity > 0
+ ActiveSupport::Deprecation.warn <<-WARNING
+The association scope '#{name}' is instance dependent (the scope block takes an argument).
+Preloading happens before the individual instances are created. This means that there is no instance
+being passed to the association scope. This will most likely result in broken or incorrect behavior.
+Joining, Preloading and eager loading of these associations is deprecated and will be removed in the future.
+ WARNING
+ end
+ end
+ alias :check_eager_loadable! :check_preloadable!
+
+ def join_id_for(owner) #:nodoc:
+ key = (source_macro == :belongs_to) ? foreign_key : active_record_primary_key
+ owner[key]
+ end
+
def through_reflection
nil
end
@@ -302,7 +383,7 @@ module ActiveRecord
scope ? [[scope]] : [[]]
end
- alias :source_macro :macro
+ def source_macro; macro; end
def has_inverse?
inverse_name
@@ -311,12 +392,12 @@ module ActiveRecord
def inverse_of
return unless inverse_name
- @inverse_of ||= klass.reflect_on_association inverse_name
+ @inverse_of ||= klass._reflect_on_association inverse_name
end
def polymorphic_inverse_of(associated_class)
if has_inverse?
- if inverse_relationship = associated_class.reflect_on_association(options[:inverse_of])
+ if inverse_relationship = associated_class._reflect_on_association(options[:inverse_of])
inverse_relationship
else
raise InverseOfAssociationNotFoundError.new(self, associated_class)
@@ -324,11 +405,16 @@ module ActiveRecord
end
end
+ # Returns the macro type.
+ #
+ # <tt>has_many :clients</tt> returns <tt>:has_many</tt>
+ def macro; raise NotImplementedError; end
+
# Returns whether or not this association reflection is for a collection
# association. Returns +true+ if the +macro+ is either +has_many+ or
# +has_and_belongs_to_many+, +false+ otherwise.
def collection?
- @collection
+ false
end
# Returns whether or not the association should be validated as part of
@@ -341,7 +427,7 @@ module ActiveRecord
# * 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)
+ !options[:validate].nil? ? options[:validate] : (options[:autosave] == true || collection?)
end
# Returns +true+ if +self+ is a +belongs_to+ reflection.
@@ -349,10 +435,15 @@ module ActiveRecord
macro == :belongs_to
end
+ # Returns +true+ if +self+ is a +has_one+ reflection.
+ def has_one?
+ macro == :has_one
+ end
+
def association_class
case macro
when :belongs_to
- if options[:polymorphic]
+ if polymorphic?
Associations::BelongsToPolymorphicAssociation
else
Associations::BelongsToAssociation
@@ -373,7 +464,7 @@ module ActiveRecord
end
def polymorphic?
- options.key? :polymorphic
+ options[:polymorphic]
end
VALID_AUTOMATIC_INVERSE_MACROS = [:has_many, :has_one, :belongs_to]
@@ -390,7 +481,7 @@ module ActiveRecord
def calculate_constructable(macro, options)
case macro
when :belongs_to
- !options[:polymorphic]
+ !polymorphic?
when :has_one
!options[:through]
else
@@ -414,10 +505,10 @@ module ActiveRecord
# 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
+ inverse_name = ActiveSupport::Inflector.underscore(options[:as] || active_record.name).to_sym
begin
- reflection = klass.reflect_on_association(inverse_name)
+ 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.
@@ -479,7 +570,7 @@ module ActiveRecord
end
def derive_join_table
- [active_record.table_name, klass.table_name].sort.join("\0").gsub(/^(.*_)(.+)\0\1(.+)/, '\1\2_\3').gsub("\0", "_")
+ ModelSchema.derive_join_table_name active_record.table_name, klass.table_name
end
def primary_key(klass)
@@ -487,15 +578,61 @@ module ActiveRecord
end
end
+ class HasManyReflection < AssociationReflection #:nodoc:
+ def initialize(name, scope, options, active_record)
+ super(name, scope, options, active_record)
+ end
+
+ def macro; :has_many; end
+
+ def collection?
+ true
+ end
+ end
+
+ class HasOneReflection < AssociationReflection #:nodoc:
+ def initialize(name, scope, options, active_record)
+ super(name, scope, options, active_record)
+ end
+
+ def macro; :has_one; end
+ end
+
+ class BelongsToReflection < AssociationReflection #:nodoc:
+ def initialize(name, scope, options, active_record)
+ super(name, scope, options, active_record)
+ end
+
+ def macro; :belongs_to; end
+ end
+
+ class HasAndBelongsToManyReflection < AssociationReflection #:nodoc:
+ def initialize(name, scope, options, active_record)
+ super
+ end
+
+ def macro; :has_and_belongs_to_many; end
+
+ def collection?
+ true
+ end
+ end
+
# Holds all the meta-data about a :through association as it was specified
# in the Active Record class.
- class ThroughReflection < AssociationReflection #:nodoc:
+ class ThroughReflection < AbstractReflection #:nodoc:
+ attr_reader :delegate_reflection
delegate :foreign_key, :foreign_type, :association_foreign_key,
:active_record_primary_key, :type, :to => :source_reflection
- def initialize(macro, name, scope, options, active_record)
- super
- @source_reflection_name = options[:source]
+ def initialize(delegate_reflection)
+ @delegate_reflection = delegate_reflection
+ @klass = delegate_reflection.options[:class]
+ @source_reflection_name = delegate_reflection.options[:source]
+ end
+
+ def klass
+ @klass ||= delegate_reflection.compute_class(class_name)
end
# Returns the source of the through reflection. It checks both a singularized
@@ -513,10 +650,10 @@ module ActiveRecord
#
# 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">
+ # # => <ActiveRecord::Reflection::BelongsToReflection: @name=:tag, @active_record=Tagging, @plural_name="tags">
#
def source_reflection
- through_reflection.klass.reflect_on_association(source_reflection_name)
+ through_reflection.klass._reflect_on_association(source_reflection_name)
end
# Returns the AssociationReflection object specified in the <tt>:through</tt> option
@@ -529,10 +666,10 @@ module ActiveRecord
#
# tags_reflection = Post.reflect_on_association(:tags)
# tags_reflection.through_reflection
- # # => <ActiveRecord::Reflection::AssociationReflection: @macro=:has_many, @name=:taggings, @active_record=Post, @plural_name="taggings">
+ # # => <ActiveRecord::Reflection::HasManyReflection: @name=:taggings, @active_record=Post, @plural_name="taggings">
#
def through_reflection
- active_record.reflect_on_association(options[:through])
+ active_record._reflect_on_association(options[:through])
end
# Returns an array of reflections which are involved in this association. Each item in the
@@ -549,8 +686,8 @@ module ActiveRecord
#
# 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>]
+ # # => [<ActiveRecord::Reflection::ThroughReflection: @delegate_reflection=#<ActiveRecord::Reflection::HasManyReflection: @name=:tags...>,
+ # <ActiveRecord::Reflection::HasManyReflection: @name=:taggings, @options={}, @active_record=Post>]
#
def chain
@chain ||= begin
@@ -639,7 +776,7 @@ module ActiveRecord
names = [name.to_s.singularize, name].collect { |n| n.to_sym }.uniq
names = names.find_all { |n|
- through_reflection.klass.reflect_on_association(n)
+ through_reflection.klass._reflect_on_association(n)
}
if names.length > 1
@@ -672,7 +809,7 @@ directive on your declaration like:
raise HasManyThroughAssociationNotFoundError.new(active_record.name, self)
end
- if through_reflection.options[:polymorphic]
+ if through_reflection.polymorphic?
raise HasManyThroughAssociationPolymorphicThroughError.new(active_record.name, self)
end
@@ -680,15 +817,15 @@ directive on your declaration like:
raise HasManyThroughSourceAssociationNotFoundError.new(self)
end
- if options[:source_type] && source_reflection.options[:polymorphic].nil?
+ if options[:source_type] && !source_reflection.polymorphic?
raise HasManyThroughAssociationPointlessSourceTypeError.new(active_record.name, self, source_reflection)
end
- if source_reflection.options[:polymorphic] && options[:source_type].nil?
+ if source_reflection.polymorphic? && options[:source_type].nil?
raise HasManyThroughAssociationPolymorphicSourceError.new(active_record.name, self, source_reflection)
end
- if macro == :has_one && through_reflection.collection?
+ if has_one? && through_reflection.collection?
raise HasOneThroughCantAssociateThroughCollection.new(active_record.name, self, through_reflection)
end
@@ -697,15 +834,25 @@ directive on your declaration like:
protected
- def actual_source_reflection # FIXME: this is a horrible name
- source_reflection.actual_source_reflection
- end
+ def actual_source_reflection # FIXME: this is a horrible name
+ source_reflection.send(:actual_source_reflection)
+ end
+
+ def primary_key(klass)
+ klass.primary_key || raise(UnknownPrimaryKey.new(klass))
+ end
private
def derive_class_name
# get the class_name of the belongs_to association of the through reflection
options[:source_type] || source_reflection.class_name
end
+
+ delegate_methods = AssociationReflection.public_instance_methods -
+ public_instance_methods
+
+ delegate(*delegate_methods, to: :delegate_reflection)
+
end
end
end
diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb
index 24b33ab0a8..ad54d84665 100644
--- a/activerecord/lib/active_record/relation.rb
+++ b/activerecord/lib/active_record/relation.rb
@@ -12,6 +12,7 @@ module ActiveRecord
SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :from, :reordering,
:reverse_order, :distinct, :create_with, :uniq]
+ INVALID_METHODS_FOR_DELETE_ALL = [:limit, :distinct, :offset, :group, :having]
VALUE_METHODS = MULTI_VALUE_METHODS + SINGLE_VALUE_METHODS
@@ -73,7 +74,14 @@ module ActiveRecord
def _update_record(values, id, id_was) # :nodoc:
substitutes, binds = substitute_values values
- um = @klass.unscoped.where(@klass.arel_table[@klass.primary_key].eq(id_was || id)).arel.compile_update(substitutes, @klass.primary_key)
+
+ scope = @klass.unscoped
+
+ if @klass.finder_needs_type_condition?
+ scope.unscope!(where: @klass.inheritance_column)
+ end
+
+ um = scope.where(@klass.arel_table[@klass.primary_key].eq(id_was || id)).arel.compile_update(substitutes, @klass.primary_key)
@klass.connection.update(
um,
@@ -234,6 +242,11 @@ module ActiveRecord
@records
end
+ # Serializes the relation objects Array.
+ def encode_with(coder)
+ coder.represent_seq(nil, to_a)
+ end
+
def as_json(options = nil) #:nodoc:
to_a.as_json(options)
end
@@ -430,12 +443,21 @@ module ActiveRecord
# If you need to destroy dependent associations or call your <tt>before_*</tt> or
# +after_destroy+ callbacks, use the +destroy_all+ method instead.
#
- # If a limit scope is supplied, +delete_all+ raises an ActiveRecord error:
+ # If an invalid method is supplied, +delete_all+ raises an ActiveRecord error:
#
# Post.limit(100).delete_all
- # # => ActiveRecord::ActiveRecordError: delete_all doesn't support limit scope
+ # # => ActiveRecord::ActiveRecordError: delete_all doesn't support limit
def delete_all(conditions = nil)
- raise ActiveRecordError.new("delete_all doesn't support limit scope") if self.limit_value
+ invalid_methods = INVALID_METHODS_FOR_DELETE_ALL.select { |method|
+ if MULTI_VALUE_METHODS.include?(method)
+ send("#{method}_values").any?
+ else
+ send("#{method}_value")
+ end
+ }
+ if invalid_methods.any?
+ raise ActiveRecordError.new("delete_all doesn't support #{invalid_methods.join(', ')}")
+ end
if conditions
where(conditions).delete_all
diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb
index 0b56430b34..90e99957f6 100644
--- a/activerecord/lib/active_record/relation/calculations.rb
+++ b/activerecord/lib/active_record/relation/calculations.rb
@@ -19,6 +19,22 @@ module ActiveRecord
#
# Person.group(:city).count
# # => { 'Rome' => 5, 'Paris' => 3 }
+ #
+ # If +count+ is used with +group+ for multiple columns, it returns a Hash whose
+ # keys are an array containing the individual values of each column and the value
+ # of each key would be the +count+.
+ #
+ # Article.group(:status, :category).count
+ # # => {["draft", "business"]=>10, ["draft", "technology"]=>4,
+ # ["published", "business"]=>0, ["published", "technology"]=>2}
+ #
+ # If +count+ is used with +select+, it will count the selected columns:
+ #
+ # Person.select(:age).count
+ # # => counts the number of different age values
+ #
+ # Note: not all valid +select+ expressions are valid +count+ expressions. The specifics differ
+ # between databases. In invalid cases, an error from the database is thrown.
def count(column_name = nil, options = {})
# TODO: Remove options argument as soon we remove support to
# activerecord-deprecated_finders.
@@ -162,18 +178,7 @@ module ActiveRecord
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) { result.identity_type }
- }
- end
-
- result = result.map do |attributes|
- values = klass.initialize_attributes(attributes).values
-
- columns.zip(values).map { |column, value| column.type_cast value }
- end
- columns.one? ? result.map!(&:first) : result
+ result.cast_values(klass.column_types)
end
end
@@ -242,7 +247,7 @@ module ActiveRecord
return 0 if relation.limit_value == 0
query_builder = build_count_subquery(relation, column_name, distinct)
- bind_values = relation.bind_values
+ bind_values = query_builder.bind_values + relation.bind_values
else
column = aggregate_column(column_name)
@@ -259,7 +264,7 @@ module ActiveRecord
row = result.first
value = row && row.values.first
column = result.column_types.fetch(column_alias) do
- column_for(column_name)
+ type_for(column_name)
end
type_cast_calculated_value(value, column, operation)
@@ -269,8 +274,8 @@ module ActiveRecord
group_attrs = group_values
if group_attrs.first.respond_to?(:to_sym)
- association = @klass.reflect_on_association(group_attrs.first.to_sym)
- associated = group_attrs.size == 1 && association && association.macro == :belongs_to # only count belongs_to associations
+ association = @klass._reflect_on_association(group_attrs.first.to_sym)
+ associated = group_attrs.size == 1 && association && association.belongs_to? # only count belongs_to associations
group_fields = Array(associated ? association.foreign_key : group_attrs)
else
group_fields = group_attrs
@@ -322,14 +327,14 @@ module ActiveRecord
Hash[calculated_data.map do |row|
key = group_columns.map { |aliaz, col_name|
column = calculated_data.column_types.fetch(aliaz) do
- column_for(col_name)
+ type_for(col_name)
end
type_cast_calculated_value(row[aliaz], column)
}
key = key.first if key.size == 1
key = key_records[key] if associated
- column_type = calculated_data.column_types.fetch(aggregate_alias) { column_for(column_name) }
+ column_type = calculated_data.column_types.fetch(aggregate_alias) { type_for(column_name) }
[key, type_cast_calculated_value(row[aggregate_alias], column_type, operation)]
end]
end
@@ -356,24 +361,20 @@ module ActiveRecord
@klass.connection.table_alias_for(table_name)
end
- def column_for(field)
+ def type_for(field)
field_name = field.respond_to?(:name) ? field.name.to_s : field.to_s.split('.').last
- @klass.columns_hash[field_name]
+ @klass.type_for_attribute(field_name)
end
- def type_cast_calculated_value(value, column, operation = nil)
+ def type_cast_calculated_value(value, type, operation = nil)
case operation
when 'count' then value.to_i
- when 'sum' then type_cast_using_column(value || 0, column)
+ when 'sum' then type.type_cast_from_database(value || 0)
when 'average' then value.respond_to?(:to_d) ? value.to_d : value
- else type_cast_using_column(value, column)
+ else type.type_cast_from_database(value)
end
end
- def type_cast_using_column(value, column)
- 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?
@@ -389,9 +390,11 @@ module ActiveRecord
aliased_column = aggregate_column(column_name == :all ? 1 : column_name).as(column_alias)
relation.select_values = [aliased_column]
- subquery = relation.arel.as(subquery_alias)
+ arel = relation.arel
+ subquery = arel.as(subquery_alias)
sm = Arel::SelectManager.new relation.engine
+ sm.bind_values = arel.bind_values
select_value = operation_over_aggregate_column(column_alias, 'count', distinct)
sm.project(select_value).from(subquery)
end
diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb
index 9c666dcd3b..50f4d5c7ab 100644
--- a/activerecord/lib/active_record/relation/delegation.rb
+++ b/activerecord/lib/active_record/relation/delegation.rb
@@ -43,7 +43,7 @@ module ActiveRecord
:keep_if, :pop, :shift, :delete_at, :compact, :select!
].to_set # :nodoc:
- delegate :to_xml, :to_yaml, :length, :collect, :map, :each, :all?, :include?, :to_ary, to: :to_a
+ delegate :to_xml, :to_yaml, :length, :collect, :map, :each, :all?, :include?, :to_ary, :join, to: :to_a
delegate :table_name, :quoted_table_name, :primary_key, :quoted_primary_key,
:connection, :columns_hash, :to => :klass
diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb
index db32ae12a8..0c9c761f97 100644
--- a/activerecord/lib/active_record/relation/finder_methods.rb
+++ b/activerecord/lib/active_record/relation/finder_methods.rb
@@ -65,7 +65,7 @@ module ActiveRecord
# # 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) }
+ to_a.find(*args) { |*block_args| yield(*block_args) }
else
find_with_ids(*args)
end
@@ -336,7 +336,16 @@ module ActiveRecord
end
def find_with_associations
- join_dependency = construct_join_dependency
+ # NOTE: the JoinDependency constructed here needs to know about
+ # any joins already present in `self`, so pass them in
+ #
+ # failing to do so means that in cases like activerecord/test/cases/associations/inner_join_association_test.rb:136
+ # incorrect SQL is generated. In that case, the join dependency for
+ # SpecialCategorizations is constructed without knowledge of the
+ # preexisting join in joins_values to categorizations (by way of
+ # the `has_many :through` for categories).
+ #
+ join_dependency = construct_join_dependency(joins_values)
aliases = join_dependency.aliases
relation = select aliases.columns
diff --git a/activerecord/lib/active_record/relation/merger.rb b/activerecord/lib/active_record/relation/merger.rb
index fcb28a18f6..ac41d0aa80 100644
--- a/activerecord/lib/active_record/relation/merger.rb
+++ b/activerecord/lib/active_record/relation/merger.rb
@@ -156,7 +156,7 @@ module ActiveRecord
def filter_binds(lhs_binds, removed_wheres)
return lhs_binds if removed_wheres.empty?
- set = Set.new removed_wheres.map { |x| x.left.name }
+ set = Set.new removed_wheres.map { |x| x.left.name.to_s }
lhs_binds.dup.delete_if { |col,_| set.include? col.name }
end
diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb
index d40f276968..eff5c8f09c 100644
--- a/activerecord/lib/active_record/relation/predicate_builder.rb
+++ b/activerecord/lib/active_record/relation/predicate_builder.rb
@@ -26,7 +26,7 @@ module ActiveRecord
queries << '1=0'
else
table = Arel::Table.new(column, default_table.engine)
- association = klass.reflect_on_association(column.to_sym)
+ association = klass._reflect_on_association(column.to_sym)
value.each do |k, v|
queries.concat expand(association && association.klass, table, k, v)
@@ -55,7 +55,7 @@ module ActiveRecord
#
# For polymorphic relationships, find the foreign key and type:
# PriceEstimate.where(estimate_of: treasure)
- if klass && reflection = klass.reflect_on_association(column.to_sym)
+ if klass && reflection = klass._reflect_on_association(column.to_sym)
if reflection.polymorphic? && base_class = polymorphic_base_class_from_value(value)
queries << build(table[reflection.foreign_type], base_class)
end
diff --git a/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb
index 2f6c34ac08..78dba8be06 100644
--- a/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb
+++ b/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb
@@ -2,28 +2,33 @@ module ActiveRecord
class PredicateBuilder
class ArrayHandler # :nodoc:
def call(attribute, value)
+ return attribute.in([]) if value.empty?
+
values = value.map { |x| x.is_a?(Base) ? x.id : x }
ranges, values = values.partition { |v| v.is_a?(Range) }
+ nils, values = values.partition(&:nil?)
- values_predicate = if values.include?(nil)
- values = values.compact
-
+ values_predicate =
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))
+ when 0 then NullPredicate
+ when 1 then attribute.eq(values.first)
+ else attribute.in(values)
end
- else
- attribute.in(values)
+
+ unless nils.empty?
+ values_predicate = values_predicate.or(attribute.eq(nil))
end
array_predicates = ranges.map { |range| attribute.in(range) }
array_predicates << values_predicate
array_predicates.inject { |composite, predicate| composite.or(predicate) }
end
+
+ module NullPredicate
+ def self.or(other)
+ other
+ end
+ 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 416f2305d2..1262b2c291 100644
--- a/activerecord/lib/active_record/relation/query_methods.rb
+++ b/activerecord/lib/active_record/relation/query_methods.rb
@@ -573,15 +573,11 @@ WARNING
end
end
- def where!(opts = :chain, *rest) # :nodoc:
- if opts == :chain
- WhereChain.new(self)
- else
- references!(PredicateBuilder.references(opts)) if Hash === opts
+ def where!(opts, *rest) # :nodoc:
+ references!(PredicateBuilder.references(opts)) if Hash === opts
- self.where_values += build_where(opts, rest)
- self
- end
+ self.where_values += build_where(opts, rest)
+ self
end
# Allows you to change a previously set where condition for a given attribute, instead of appending to that condition.
@@ -950,7 +946,6 @@ WARNING
[@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)
bv_len = bind_values.length
tmp_opts, bind_values = create_binds(opts, bv_len)
diff --git a/activerecord/lib/active_record/result.rb b/activerecord/lib/active_record/result.rb
index 228b2aa60f..8405fdaeb9 100644
--- a/activerecord/lib/active_record/result.rb
+++ b/activerecord/lib/active_record/result.rb
@@ -31,7 +31,7 @@ module ActiveRecord
class Result
include Enumerable
- IDENTITY_TYPE = Class.new { def type_cast(v); v; end }.new # :nodoc:
+ IDENTITY_TYPE = Type::Value.new # :nodoc:
attr_reader :columns, :rows, :column_types
@@ -42,14 +42,6 @@ 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
if block_given?
hash_rows.each { |row| yield row }
@@ -82,6 +74,15 @@ module ActiveRecord
hash_rows.last
end
+ def cast_values(type_overrides = {}) # :nodoc:
+ types = columns.map { |name| column_type(name, type_overrides) }
+ result = rows.map do |values|
+ types.zip(values).map { |type, value| type.type_cast_from_database(value) }
+ end
+
+ columns.one? ? result.map!(&:first) : result
+ end
+
def initialize_copy(other)
@columns = columns.dup
@rows = rows.dup
@@ -91,6 +92,12 @@ module ActiveRecord
private
+ def column_type(name, type_overrides = {})
+ type_overrides.fetch(name) do
+ column_types.fetch(name, IDENTITY_TYPE)
+ end
+ end
+
def hash_rows
@hash_rows ||=
begin
diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb
index 1aa93ffbb3..ff70cbed0f 100644
--- a/activerecord/lib/active_record/sanitization.rb
+++ b/activerecord/lib/active_record/sanitization.rb
@@ -107,7 +107,7 @@ module ActiveRecord
end.join(', ')
end
- # Sanitizes a +string+ so that it is safe to use within a sql
+ # Sanitizes a +string+ so that it is safe to use within an SQL
# LIKE statement. This method uses +escape_character+ to escape all occurrences of "\", "_" and "%"
def sanitize_sql_like(string, escape_character = "\\")
pattern = Regexp.union(escape_character, "%", "_")
diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb
index e055d571ab..fae6427ea1 100644
--- a/activerecord/lib/active_record/schema_dumper.rb
+++ b/activerecord/lib/active_record/schema_dumper.rb
@@ -91,16 +91,17 @@ HEADER
end
def tables(stream)
- @connection.tables.sort.each do |tbl|
- next if ['schema_migrations', ignore_tables].flatten.any? do |ignored|
- case ignored
- when String; remove_prefix_and_suffix(tbl) == ignored
- when Regexp; remove_prefix_and_suffix(tbl) =~ ignored
- else
- raise StandardError, 'ActiveRecord::SchemaDumper.ignore_tables accepts an array of String and / or Regexp values.'
- end
+ sorted_tables = @connection.tables.sort
+
+ sorted_tables.each do |table_name|
+ table(table_name, stream) unless ignored?(table_name)
+ end
+
+ # dump foreign keys at the end to make sure all dependent tables exist.
+ if @connection.supports_foreign_keys?
+ sorted_tables.each do |tbl|
+ foreign_keys(tbl, stream)
end
- table(tbl, stream)
end
end
@@ -112,7 +113,8 @@ HEADER
# first dump primary key column
if @connection.respond_to?(:pk_and_sequence_for)
pk, _ = @connection.pk_and_sequence_for(table)
- elsif @connection.respond_to?(:primary_key)
+ end
+ if !pk && @connection.respond_to?(:primary_key)
pk = @connection.primary_key(table)
end
@@ -212,8 +214,49 @@ HEADER
end
end
+ def foreign_keys(table, stream)
+ if (foreign_keys = @connection.foreign_keys(table)).any?
+ add_foreign_key_statements = foreign_keys.map do |foreign_key|
+ parts = [
+ 'add_foreign_key ' + remove_prefix_and_suffix(foreign_key.from_table).inspect,
+ remove_prefix_and_suffix(foreign_key.to_table).inspect,
+ ]
+
+ if foreign_key.column != @connection.foreign_key_column_for(foreign_key.to_table)
+ parts << ('column: ' + foreign_key.column.inspect)
+ end
+
+ if foreign_key.custom_primary_key?
+ parts << ('primary_key: ' + foreign_key.primary_key.inspect)
+ end
+
+ if foreign_key.name !~ /^fk_rails_[0-9a-f]{10}$/
+ parts << ('name: ' + foreign_key.name.inspect)
+ end
+
+ parts << ('on_update: ' + foreign_key.on_update.inspect) if foreign_key.on_update
+ parts << ('on_delete: ' + foreign_key.on_delete.inspect) if foreign_key.on_delete
+
+ ' ' + parts.join(', ')
+ end
+
+ stream.puts add_foreign_key_statements.sort.join("\n")
+ end
+ end
+
def remove_prefix_and_suffix(table)
table.gsub(/^(#{@options[:table_name_prefix]})(.+)(#{@options[:table_name_suffix]})$/, "\\2")
end
+
+ def ignored?(table_name)
+ ['schema_migrations', ignore_tables].flatten.any? do |ignored|
+ case ignored
+ when String; remove_prefix_and_suffix(table_name) == ignored
+ when Regexp; remove_prefix_and_suffix(table_name) =~ ignored
+ else
+ raise StandardError, 'ActiveRecord::SchemaDumper.ignore_tables accepts an array of String and / or Regexp values.'
+ end
+ end
+ end
end
end
diff --git a/activerecord/lib/active_record/schema_migration.rb b/activerecord/lib/active_record/schema_migration.rb
index a9d164e366..b5038104ac 100644
--- a/activerecord/lib/active_record/schema_migration.rb
+++ b/activerecord/lib/active_record/schema_migration.rb
@@ -5,6 +5,9 @@ require 'active_record/base'
module ActiveRecord
class SchemaMigration < ActiveRecord::Base
class << self
+ def primary_key
+ nil
+ end
def table_name
"#{table_name_prefix}#{ActiveRecord::Base.schema_migrations_table_name}#{table_name_suffix}"
@@ -36,6 +39,14 @@ module ActiveRecord
connection.drop_table(table_name)
end
end
+
+ def normalize_migration_number(number)
+ "%.3d" % number.to_i
+ end
+
+ def normalized_versions
+ pluck(:version).map { |v| normalize_migration_number v }
+ end
end
def version
diff --git a/activerecord/lib/active_record/serializers/xml_serializer.rb b/activerecord/lib/active_record/serializers/xml_serializer.rb
index 1a766093d0..c2484d02ed 100644
--- a/activerecord/lib/active_record/serializers/xml_serializer.rb
+++ b/activerecord/lib/active_record/serializers/xml_serializer.rb
@@ -180,13 +180,9 @@ module ActiveRecord #:nodoc:
class Attribute < ActiveModel::Serializers::Xml::Serializer::Attribute #:nodoc:
def compute_type
klass = @serializable.class
- type = if klass.serialized_attributes.key?(name)
- super
- elsif klass.columns_hash.key?(name)
- klass.columns_hash[name].type
- else
- NilClass
- end
+ column = klass.columns_hash[name] || Type::Value.new
+
+ type = ActiveSupport::XmlMini::TYPE_NAMES[value.class.name] || column.type
{ :text => :string,
:time => :datetime }[type] || type
diff --git a/activerecord/lib/active_record/store.rb b/activerecord/lib/active_record/store.rb
index 79a6ccbda0..3c291f28e3 100644
--- a/activerecord/lib/active_record/store.rb
+++ b/activerecord/lib/active_record/store.rb
@@ -66,8 +66,9 @@ module ActiveRecord
extend ActiveSupport::Concern
included do
- class_attribute :stored_attributes, instance_accessor: false
- self.stored_attributes = {}
+ class << self
+ attr_accessor :local_stored_attributes
+ end
end
module ClassMethods
@@ -93,18 +94,26 @@ module ActiveRecord
# 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
+ self.local_stored_attributes ||= {}
+ self.local_stored_attributes[store_attribute] ||= []
+ self.local_stored_attributes[store_attribute] |= keys
end
- def _store_accessors_module
+ def _store_accessors_module # :nodoc:
@_store_accessors_module ||= begin
mod = Module.new
include mod
mod
end
end
+
+ def stored_attributes
+ parent = superclass.respond_to?(:stored_attributes) ? superclass.stored_attributes : {}
+ if self.local_stored_attributes
+ parent.merge!(self.local_stored_attributes) { |k, a, b| a | b }
+ end
+ parent
+ end
end
protected
@@ -120,10 +129,10 @@ module ActiveRecord
private
def store_accessor_for(store_attribute)
- @column_types[store_attribute.to_s].accessor
+ type_for_attribute(store_attribute.to_s).accessor
end
- class HashAccessor
+ class HashAccessor # :nodoc:
def self.read(object, attribute, key)
prepare(object, attribute)
object.public_send(attribute)[key]
@@ -142,7 +151,7 @@ module ActiveRecord
end
end
- class StringKeyedHashAccessor < HashAccessor
+ class StringKeyedHashAccessor < HashAccessor # :nodoc:
def self.read(object, attribute, key)
super object, attribute, key.to_s
end
@@ -152,7 +161,7 @@ module ActiveRecord
end
end
- class IndifferentHashAccessor < ActiveRecord::Store::HashAccessor
+ class IndifferentHashAccessor < ActiveRecord::Store::HashAccessor # :nodoc:
def self.prepare(object, store_attribute)
attribute = object.send(store_attribute)
unless attribute.is_a?(ActiveSupport::HashWithIndifferentAccess)
diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb
index 168b338b97..b7315ed4b3 100644
--- a/activerecord/lib/active_record/tasks/database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/database_tasks.rb
@@ -6,7 +6,7 @@ module ActiveRecord
# <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.
+ # The tasks defined here are used with 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
@@ -14,7 +14,6 @@ module ActiveRecord
# (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).
@@ -59,7 +58,11 @@ module ActiveRecord
end
def fixtures_path
- @fixtures_path ||= File.join(root, 'test', 'fixtures')
+ @fixtures_path ||= if ENV['FIXTURES_PATH']
+ File.join(root, ENV['FIXTURES_PATH'])
+ else
+ File.join(root, 'test', 'fixtures')
+ end
end
def root
@@ -107,6 +110,8 @@ module ActiveRecord
def drop(*arguments)
configuration = arguments.first
class_for_adapter(configuration['adapter']).new(*arguments).drop
+ rescue ActiveRecord::NoDatabaseError
+ $stderr.puts "Database '#{configuration['database']}' does not exist"
rescue Exception => error
$stderr.puts error, *(error.backtrace)
$stderr.puts "Couldn't drop #{configuration['database']}"
@@ -144,6 +149,18 @@ module ActiveRecord
class_for_adapter(configuration['adapter']).new(configuration).purge
end
+ def purge_all
+ each_local_configuration { |configuration|
+ purge configuration
+ }
+ end
+
+ def purge_current(environment = env)
+ each_current_configuration(environment) { |configuration|
+ purge configuration
+ }
+ end
+
def structure_dump(*arguments)
configuration = arguments.first
filename = arguments.delete_at 1
diff --git a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb
index c755831e6d..d890196f47 100644
--- a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb
@@ -42,7 +42,7 @@ module ActiveRecord
end
def purge
- establish_connection :test
+ establish_connection configuration
connection.recreate_database configuration['database'], creation_options
end
@@ -124,7 +124,7 @@ IDENTIFIED BY '#{configuration['password']}' WITH GRANT OPTION;
end
def root_password
- $stdout.print "Please provide the root password for your mysql installation\n>"
+ $stdout.print "Please provide the root password for your MySQL installation\n>"
$stdin.gets.strip
end
diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb
index 6c30ccab72..e2e37e7c00 100644
--- a/activerecord/lib/active_record/timestamp.rb
+++ b/activerecord/lib/active_record/timestamp.rb
@@ -99,9 +99,11 @@ module ActiveRecord
end
def max_updated_column_timestamp(timestamp_names = timestamp_attributes_for_update)
- if (timestamps = timestamp_names.map { |attr| self[attr] }.compact).present?
- timestamps.map { |ts| ts.to_time }.max
- end
+ timestamp_names
+ .map { |attr| self[attr] }
+ .compact
+ .map(&:to_time)
+ .max
end
def current_time_from_proper_timezone
diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb
index 17f76b63b3..7e4dc4c895 100644
--- a/activerecord/lib/active_record/transactions.rb
+++ b/activerecord/lib/active_record/transactions.rb
@@ -1,5 +1,3 @@
-require 'thread'
-
module ActiveRecord
# See ActiveRecord::Transactions::ClassMethods for documentation.
module Transactions
@@ -295,7 +293,7 @@ module ActiveRecord
def committed! #:nodoc:
run_callbacks :commit if destroyed? || persisted?
ensure
- @_start_transaction_state.clear
+ force_clear_transaction_record_state
end
# Call the +after_rollback+ callbacks. The +force_restore_state+ argument indicates if the record
@@ -328,7 +326,7 @@ module ActiveRecord
begin
status = yield
rescue ActiveRecord::Rollback
- @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1
+ clear_transaction_record_state
status = nil
end
@@ -341,7 +339,7 @@ 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[:id] = id
unless @_start_transaction_state.include?(:new_record)
@_start_transaction_state[:new_record] = @new_record
end
@@ -349,13 +347,18 @@ module ActiveRecord
@_start_transaction_state[:destroyed] = @destroyed
end
@_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) + 1
- @_start_transaction_state[:frozen?] = @attributes.frozen?
+ @_start_transaction_state[:frozen?] = frozen?
end
# Clear the new record state and id of a record.
def clear_transaction_record_state #:nodoc:
@_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1
- @_start_transaction_state.clear if @_start_transaction_state[:level] < 1
+ force_clear_transaction_record_state if @_start_transaction_state[:level] < 1
+ end
+
+ # Force to clear the transaction record state.
+ def force_clear_transaction_record_state #:nodoc:
+ @_start_transaction_state.clear
end
# Restore the new record state and id of a record that was previously saved by a call to save_record_state.
@@ -364,17 +367,10 @@ module ActiveRecord
transaction_level = (@_start_transaction_state[:level] || 0) - 1
if transaction_level < 1 || force
restore_state = @_start_transaction_state
- was_frozen = restore_state[:frozen?]
- @attributes = @attributes.dup if @attributes.frozen?
+ thaw unless restore_state[:frozen?]
@new_record = restore_state[:new_record]
@destroyed = restore_state[:destroyed]
- if restore_state.has_key?(:id)
- write_attribute(self.class.primary_key, restore_state[:id])
- else
- @attributes.delete(self.class.primary_key)
- @attributes_cache.delete(self.class.primary_key)
- end
- @attributes.freeze if was_frozen
+ write_attribute(self.class.primary_key, restore_state[:id])
end
end
end
diff --git a/activerecord/lib/active_record/type.rb b/activerecord/lib/active_record/type.rb
new file mode 100644
index 0000000000..f1384e0bb2
--- /dev/null
+++ b/activerecord/lib/active_record/type.rb
@@ -0,0 +1,20 @@
+require 'active_record/type/mutable'
+require 'active_record/type/numeric'
+require 'active_record/type/time_value'
+require 'active_record/type/value'
+
+require 'active_record/type/binary'
+require 'active_record/type/boolean'
+require 'active_record/type/date'
+require 'active_record/type/date_time'
+require 'active_record/type/decimal'
+require 'active_record/type/decimal_without_scale'
+require 'active_record/type/float'
+require 'active_record/type/integer'
+require 'active_record/type/serialized'
+require 'active_record/type/string'
+require 'active_record/type/text'
+require 'active_record/type/time'
+
+require 'active_record/type/type_map'
+require 'active_record/type/hash_lookup_type_map'
diff --git a/activerecord/lib/active_record/type/binary.rb b/activerecord/lib/active_record/type/binary.rb
new file mode 100644
index 0000000000..d29ff4e494
--- /dev/null
+++ b/activerecord/lib/active_record/type/binary.rb
@@ -0,0 +1,40 @@
+module ActiveRecord
+ module Type
+ class Binary < Value # :nodoc:
+ def type
+ :binary
+ end
+
+ def binary?
+ true
+ end
+
+ def type_cast(value)
+ if value.is_a?(Data)
+ value.to_s
+ else
+ super
+ end
+ end
+
+ def type_cast_for_database(value)
+ return if value.nil?
+ Data.new(super)
+ end
+
+ class Data # :nodoc:
+ def initialize(value)
+ @value = value.to_s
+ end
+
+ def to_s
+ @value
+ end
+
+ def hex
+ @value.unpack('H*')[0]
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/boolean.rb b/activerecord/lib/active_record/type/boolean.rb
new file mode 100644
index 0000000000..06dd17ed28
--- /dev/null
+++ b/activerecord/lib/active_record/type/boolean.rb
@@ -0,0 +1,19 @@
+module ActiveRecord
+ module Type
+ class Boolean < Value # :nodoc:
+ def type
+ :boolean
+ end
+
+ private
+
+ def cast_value(value)
+ if value == ''
+ nil
+ else
+ ConnectionAdapters::Column::TRUE_VALUES.include?(value)
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/date.rb b/activerecord/lib/active_record/type/date.rb
new file mode 100644
index 0000000000..d90a6069b7
--- /dev/null
+++ b/activerecord/lib/active_record/type/date.rb
@@ -0,0 +1,46 @@
+module ActiveRecord
+ module Type
+ class Date < Value # :nodoc:
+ def type
+ :date
+ end
+
+ def klass
+ ::Date
+ end
+
+ def type_cast_for_schema(value)
+ "'#{value.to_s(:db)}'"
+ end
+
+ private
+
+ def cast_value(value)
+ if value.is_a?(::String)
+ return if value.empty?
+ fast_string_to_date(value) || fallback_string_to_date(value)
+ elsif value.respond_to?(:to_date)
+ value.to_date
+ else
+ value
+ end
+ end
+
+ def fast_string_to_date(string)
+ if string =~ ConnectionAdapters::Column::Format::ISO_DATE
+ new_date $1.to_i, $2.to_i, $3.to_i
+ end
+ end
+
+ def fallback_string_to_date(string)
+ new_date(*::Date._parse(string, false).values_at(:year, :mon, :mday))
+ end
+
+ def new_date(year, mon, mday)
+ if year && year != 0
+ ::Date.new(year, mon, mday) rescue nil
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/date_time.rb b/activerecord/lib/active_record/type/date_time.rb
new file mode 100644
index 0000000000..5f19608a33
--- /dev/null
+++ b/activerecord/lib/active_record/type/date_time.rb
@@ -0,0 +1,43 @@
+module ActiveRecord
+ module Type
+ class DateTime < Value # :nodoc:
+ include TimeValue
+
+ def type
+ :datetime
+ end
+
+ def type_cast_for_database(value)
+ zone_conversion_method = ActiveRecord::Base.default_timezone == :utc ? :getutc : :getlocal
+
+ if value.acts_like?(:time)
+ value.send(zone_conversion_method)
+ else
+ super
+ end
+ end
+
+ private
+
+ def cast_value(string)
+ return string unless string.is_a?(::String)
+ return if string.empty?
+
+ fast_string_to_time(string) || fallback_string_to_time(string)
+ end
+
+ # '0.123456' -> 123456
+ # '1.123456' -> 123456
+ def microseconds(time)
+ time[:sec_fraction] ? (time[:sec_fraction] * 1_000_000).to_i : 0
+ end
+
+ def fallback_string_to_time(string)
+ time_hash = ::Date._parse(string)
+ time_hash[:sec_fraction] = microseconds(time_hash)
+
+ new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction, :offset))
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/decimal.rb b/activerecord/lib/active_record/type/decimal.rb
new file mode 100644
index 0000000000..ba5d244729
--- /dev/null
+++ b/activerecord/lib/active_record/type/decimal.rb
@@ -0,0 +1,27 @@
+module ActiveRecord
+ module Type
+ class Decimal < Value # :nodoc:
+ include Numeric
+
+ def type
+ :decimal
+ end
+
+ def type_cast_for_schema(value)
+ value.to_s
+ end
+
+ private
+
+ def cast_value(value)
+ if value.is_a?(::Numeric) || value.is_a?(::String)
+ BigDecimal(value, precision.to_i)
+ elsif value.respond_to?(:to_d)
+ value.to_d
+ else
+ cast_value(value.to_s)
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/decimal_without_scale.rb b/activerecord/lib/active_record/type/decimal_without_scale.rb
new file mode 100644
index 0000000000..cabdcecdd7
--- /dev/null
+++ b/activerecord/lib/active_record/type/decimal_without_scale.rb
@@ -0,0 +1,11 @@
+require 'active_record/type/integer'
+
+module ActiveRecord
+ module Type
+ class DecimalWithoutScale < Integer # :nodoc:
+ def type
+ :decimal
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/float.rb b/activerecord/lib/active_record/type/float.rb
new file mode 100644
index 0000000000..42eb44b9a9
--- /dev/null
+++ b/activerecord/lib/active_record/type/float.rb
@@ -0,0 +1,19 @@
+module ActiveRecord
+ module Type
+ class Float < Value # :nodoc:
+ include Numeric
+
+ def type
+ :float
+ end
+
+ alias type_cast_for_database type_cast
+
+ private
+
+ def cast_value(value)
+ value.to_f
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/hash_lookup_type_map.rb b/activerecord/lib/active_record/type/hash_lookup_type_map.rb
new file mode 100644
index 0000000000..bf92680268
--- /dev/null
+++ b/activerecord/lib/active_record/type/hash_lookup_type_map.rb
@@ -0,0 +1,19 @@
+module ActiveRecord
+ module Type
+ class HashLookupTypeMap < TypeMap # :nodoc:
+ delegate :key?, to: :@mapping
+
+ def lookup(type, *args)
+ @mapping.fetch(type, proc { default_value }).call(type, *args)
+ end
+
+ def fetch(type, *args, &block)
+ @mapping.fetch(type, block).call(type, *args)
+ end
+
+ def alias_type(type, alias_type)
+ register_type(type) { |_, *args| lookup(alias_type, *args) }
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/integer.rb b/activerecord/lib/active_record/type/integer.rb
new file mode 100644
index 0000000000..08477d1303
--- /dev/null
+++ b/activerecord/lib/active_record/type/integer.rb
@@ -0,0 +1,23 @@
+module ActiveRecord
+ module Type
+ class Integer < Value # :nodoc:
+ include Numeric
+
+ def type
+ :integer
+ end
+
+ alias type_cast_for_database type_cast
+
+ private
+
+ def cast_value(value)
+ case value
+ when true then 1
+ when false then 0
+ else value.to_i rescue nil
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/mutable.rb b/activerecord/lib/active_record/type/mutable.rb
new file mode 100644
index 0000000000..066617ea59
--- /dev/null
+++ b/activerecord/lib/active_record/type/mutable.rb
@@ -0,0 +1,16 @@
+module ActiveRecord
+ module Type
+ module Mutable # :nodoc:
+ def type_cast_from_user(value)
+ type_cast_from_database(type_cast_for_database(value))
+ end
+
+ # +raw_old_value+ will be the `_before_type_cast` version of the
+ # value (likely a string). +new_value+ will be the current, type
+ # cast value.
+ def changed_in_place?(raw_old_value, new_value)
+ raw_old_value != type_cast_for_database(new_value)
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/numeric.rb b/activerecord/lib/active_record/type/numeric.rb
new file mode 100644
index 0000000000..fa43266504
--- /dev/null
+++ b/activerecord/lib/active_record/type/numeric.rb
@@ -0,0 +1,36 @@
+module ActiveRecord
+ module Type
+ module Numeric # :nodoc:
+ def number?
+ true
+ end
+
+ def type_cast(value)
+ value = case value
+ when true then 1
+ when false then 0
+ when ::String then value.presence
+ else value
+ end
+ super(value)
+ end
+
+ def changed?(old_value, _new_value, new_value_before_type_cast) # :nodoc:
+ super || number_to_non_number?(old_value, new_value_before_type_cast)
+ end
+
+ private
+
+ def number_to_non_number?(old_value, new_value_before_type_cast)
+ old_value != nil && non_numeric_string?(new_value_before_type_cast)
+ end
+
+ def non_numeric_string?(value)
+ # 'wibble'.to_i will give zero, we want to make sure
+ # that we aren't marking int zero to string zero as
+ # changed.
+ value.to_s !~ /\A\d+\.?\d*\z/
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/serialized.rb b/activerecord/lib/active_record/type/serialized.rb
new file mode 100644
index 0000000000..42bbed7103
--- /dev/null
+++ b/activerecord/lib/active_record/type/serialized.rb
@@ -0,0 +1,51 @@
+module ActiveRecord
+ module Type
+ class Serialized < SimpleDelegator # :nodoc:
+ include Mutable
+
+ attr_reader :subtype, :coder
+
+ def initialize(subtype, coder)
+ @subtype = subtype
+ @coder = coder
+ super(subtype)
+ end
+
+ def type_cast_from_database(value)
+ if is_default_value?(value)
+ value
+ else
+ coder.load(super)
+ end
+ end
+
+ def type_cast_for_database(value)
+ return if value.nil?
+ unless is_default_value?(value)
+ super coder.dump(value)
+ end
+ end
+
+ def accessor
+ ActiveRecord::Store::IndifferentHashAccessor
+ end
+
+ def init_with(coder)
+ @subtype = coder['subtype']
+ @coder = coder['coder']
+ __setobj__(@subtype)
+ end
+
+ def encode_with(coder)
+ coder['subtype'] = @subtype
+ coder['coder'] = @coder
+ end
+
+ private
+
+ def is_default_value?(value)
+ value == coder.load(nil)
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/string.rb b/activerecord/lib/active_record/type/string.rb
new file mode 100644
index 0000000000..150defb106
--- /dev/null
+++ b/activerecord/lib/active_record/type/string.rb
@@ -0,0 +1,36 @@
+module ActiveRecord
+ module Type
+ class String < Value # :nodoc:
+ def type
+ :string
+ end
+
+ def changed_in_place?(raw_old_value, new_value)
+ if new_value.is_a?(::String)
+ raw_old_value != new_value
+ end
+ end
+
+ def type_cast_for_database(value)
+ case value
+ when ::Numeric, ActiveSupport::Duration then value.to_s
+ when ::String then ::String.new(value)
+ when true then "1"
+ when false then "0"
+ else super
+ end
+ end
+
+ private
+
+ def cast_value(value)
+ case value
+ when true then "1"
+ when false then "0"
+ # String.new is slightly faster than dup
+ else ::String.new(value.to_s)
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/text.rb b/activerecord/lib/active_record/type/text.rb
new file mode 100644
index 0000000000..26f980f060
--- /dev/null
+++ b/activerecord/lib/active_record/type/text.rb
@@ -0,0 +1,11 @@
+require 'active_record/type/string'
+
+module ActiveRecord
+ module Type
+ class Text < String # :nodoc:
+ def type
+ :text
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/time.rb b/activerecord/lib/active_record/type/time.rb
new file mode 100644
index 0000000000..41f7d97f0c
--- /dev/null
+++ b/activerecord/lib/active_record/type/time.rb
@@ -0,0 +1,26 @@
+module ActiveRecord
+ module Type
+ class Time < Value # :nodoc:
+ include TimeValue
+
+ def type
+ :time
+ end
+
+ private
+
+ def cast_value(value)
+ return value unless value.is_a?(::String)
+ return if value.empty?
+
+ dummy_time_value = "2000-01-01 #{value}"
+
+ fast_string_to_time(dummy_time_value) || begin
+ time_hash = ::Date._parse(dummy_time_value)
+ return if time_hash[:hour].nil?
+ new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction))
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/time_value.rb b/activerecord/lib/active_record/type/time_value.rb
new file mode 100644
index 0000000000..d611d72dd4
--- /dev/null
+++ b/activerecord/lib/active_record/type/time_value.rb
@@ -0,0 +1,38 @@
+module ActiveRecord
+ module Type
+ module TimeValue # :nodoc:
+ def klass
+ ::Time
+ end
+
+ def type_cast_for_schema(value)
+ "'#{value.to_s(:db)}'"
+ end
+
+ private
+
+ def new_time(year, mon, mday, hour, min, sec, microsec, offset = nil)
+ # Treat 0000-00-00 00:00:00 as nil.
+ return if year.nil? || (year == 0 && mon == 0 && mday == 0)
+
+ if offset
+ time = ::Time.utc(year, mon, mday, hour, min, sec, microsec) rescue nil
+ return unless time
+
+ time -= offset
+ Base.default_timezone == :utc ? time : time.getlocal
+ else
+ ::Time.public_send(Base.default_timezone, year, mon, mday, hour, min, sec, microsec) rescue nil
+ end
+ end
+
+ # Doesn't handle time zones.
+ def fast_string_to_time(string)
+ if string =~ ConnectionAdapters::Column::Format::ISO_DATETIME
+ microsec = ($7.to_r * 1_000_000).to_i
+ new_time $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, microsec
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/type_map.rb b/activerecord/lib/active_record/type/type_map.rb
new file mode 100644
index 0000000000..88c5f9c497
--- /dev/null
+++ b/activerecord/lib/active_record/type/type_map.rb
@@ -0,0 +1,48 @@
+module ActiveRecord
+ module Type
+ class TypeMap # :nodoc:
+ def initialize
+ @mapping = {}
+ end
+
+ def lookup(lookup_key, *args)
+ matching_pair = @mapping.reverse_each.detect do |key, _|
+ key === lookup_key
+ end
+
+ if matching_pair
+ matching_pair.last.call(lookup_key, *args)
+ else
+ default_value
+ end
+ end
+
+ def register_type(key, value = nil, &block)
+ raise ::ArgumentError unless value || block
+
+ if block
+ @mapping[key] = block
+ else
+ @mapping[key] = proc { value }
+ end
+ end
+
+ def alias_type(key, target_key)
+ register_type(key) do |sql_type, *args|
+ metadata = sql_type[/\(.*\)/, 0]
+ lookup("#{target_key}#{metadata}", *args)
+ end
+ end
+
+ def clear
+ @mapping.clear
+ end
+
+ private
+
+ def default_value
+ @default_value ||= Value.new
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/value.rb b/activerecord/lib/active_record/type/value.rb
new file mode 100644
index 0000000000..e0a783fb45
--- /dev/null
+++ b/activerecord/lib/active_record/type/value.rb
@@ -0,0 +1,94 @@
+module ActiveRecord
+ module Type
+ class Value # :nodoc:
+ attr_reader :precision, :scale, :limit
+
+ # Valid options are +precision+, +scale+, and +limit+. They are only
+ # used when dumping schema.
+ def initialize(options = {})
+ options.assert_valid_keys(:precision, :scale, :limit)
+ @precision = options[:precision]
+ @scale = options[:scale]
+ @limit = options[:limit]
+ end
+
+ # The simplified type that this object represents. Returns a symbol such
+ # as +:string+ or +:integer+
+ def type; end
+
+ # Type casts a string from the database into the appropriate ruby type.
+ # Classes which do not need separate type casting behavior for database
+ # and user provided values should override +cast_value+ instead.
+ def type_cast_from_database(value)
+ type_cast(value)
+ end
+
+ # Type casts a value from user input (e.g. from a setter). This value may
+ # be a string from the form builder, or an already type cast value
+ # provided manually to a setter.
+ #
+ # Classes which do not need separate type casting behavior for database
+ # and user provided values should override +type_cast+ or +cast_value+
+ # instead.
+ def type_cast_from_user(value)
+ type_cast(value)
+ end
+
+ # Cast a value from the ruby type to a type that the database knows how
+ # to understand. The returned value from this method should be a
+ # +String+, +Numeric+, +Date+, +Time+, +Symbol+, +true+, +false+, or
+ # +nil+
+ def type_cast_for_database(value)
+ value
+ end
+
+ # Type cast a value for schema dumping. This method is private, as we are
+ # hoping to remove it entirely.
+ def type_cast_for_schema(value) # :nodoc:
+ value.inspect
+ end
+
+ # These predicates are not documented, as I need to look further into
+ # their use, and see if they can be removed entirely.
+ def number? # :nodoc:
+ false
+ end
+
+ def binary? # :nodoc:
+ false
+ end
+
+ def klass # :nodoc:
+ end
+
+ # Determines whether a value has changed for dirty checking. +old_value+
+ # and +new_value+ will always be type-cast. Types should not need to
+ # override this method.
+ def changed?(old_value, new_value, _new_value_before_type_cast)
+ old_value != new_value
+ end
+
+ # Determines whether the mutable value has been modified since it was
+ # read. Returns +false+ by default. This method should not need to be
+ # overriden directly. Types which return a mutable value should include
+ # +Type::Mutable+, which will define this method.
+ def changed_in_place?(*)
+ false
+ end
+
+ private
+
+ def type_cast(value)
+ cast_value(value) unless value.nil?
+ end
+
+ # Convenience method for types which do not need separate type casting
+ # behavior for user and database inputs. Called by
+ # `type_cast_from_database` and `type_cast_from_user` for all values
+ # except `nil`.
+ def cast_value(value) # :doc:
+ value
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb
index 9999624fcf..b4b33804de 100644
--- a/activerecord/lib/active_record/validations.rb
+++ b/activerecord/lib/active_record/validations.rb
@@ -54,7 +54,7 @@ module ActiveRecord
# Attempts to save the record just like Base#save but will raise a +RecordInvalid+
# exception instead of returning +false+ if the record is not valid.
def save!(options={})
- perform_validations(options) ? super : raise(RecordInvalid.new(self))
+ perform_validations(options) ? super : raise_record_invalid
end
# Runs all the validations within the specified context. Returns +true+ if
@@ -75,8 +75,24 @@ module ActiveRecord
alias_method :validate, :valid?
+ # Runs all the validations within the specified context. Returns +true+ if
+ # no errors are found, raises +RecordInvalid+ otherwise.
+ #
+ # If the argument is +false+ (default is +nil+), the context is set to <tt>:create</tt> if
+ # <tt>new_record?</tt> is +true+, and to <tt>:update</tt> if it is not.
+ #
+ # Validations with no <tt>:on</tt> option will run no matter the context. Validations with
+ # some <tt>:on</tt> option will only run in the specified context.
+ def validate!(context = nil)
+ valid?(context) || raise_record_invalid
+ end
+
protected
+ def raise_record_invalid
+ raise(RecordInvalid.new(self))
+ end
+
def perform_validations(options={}) # :nodoc:
options[:validate] == false || valid?(options[:context])
end
diff --git a/activerecord/lib/active_record/validations/presence.rb b/activerecord/lib/active_record/validations/presence.rb
index 9a19483da3..e586744818 100644
--- a/activerecord/lib/active_record/validations/presence.rb
+++ b/activerecord/lib/active_record/validations/presence.rb
@@ -4,7 +4,7 @@ module ActiveRecord
def validate(record)
super
attributes.each do |attribute|
- next unless record.class.reflect_on_association(attribute)
+ next unless record.class._reflect_on_association(attribute)
associated_records = Array.wrap(record.send(attribute))
# Superclass validates presence. Ensure present records aren't about to be destroyed.
diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb
index ee080451a9..2a34969a8c 100644
--- a/activerecord/lib/active_record/validations/uniqueness.rb
+++ b/activerecord/lib/active_record/validations/uniqueness.rb
@@ -14,7 +14,6 @@ module ActiveRecord
finder_class = find_finder_class_for(record)
table = finder_class.arel_table
value = map_enum_attribute(finder_class, attribute, value)
- 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.id)) if record.persisted?
@@ -47,9 +46,9 @@ module ActiveRecord
end
def build_relation(klass, table, attribute, value) #:nodoc:
- if reflection = klass.reflect_on_association(attribute)
+ if reflection = klass._reflect_on_association(attribute)
attribute = reflection.foreign_key
- value = value.attributes[reflection.primary_key_column.name] unless value.nil?
+ value = value.attributes[reflection.klass.primary_key] unless value.nil?
end
attribute_name = attribute.to_s
@@ -62,9 +61,11 @@ module ActiveRecord
column = klass.columns_hash[attribute_name]
value = klass.connection.type_cast(value, column)
- value = value.to_s[0, column.limit] if value && column.limit && column.text?
+ if value.is_a?(String) && column.limit
+ value = value.to_s[0, column.limit]
+ end
- if !options[:case_sensitive] && value && column.text?
+ if !options[:case_sensitive] && value.is_a?(String)
# will use SQL LOWER function before comparison, unless it detects a case insensitive collation
klass.connection.case_insensitive_comparison(table, attribute, column, value)
else
@@ -74,7 +75,7 @@ module ActiveRecord
def scope_relation(record, table, relation)
Array(options[:scope]).each do |scope_item|
- if reflection = record.class.reflect_on_association(scope_item)
+ if reflection = record.class._reflect_on_association(scope_item)
scope_value = record.send(reflection.foreign_key)
scope_item = reflection.foreign_key
else
@@ -86,12 +87,6 @@ module ActiveRecord
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
-
def map_enum_attribute(klass, attribute, value)
mapping = klass.defined_enums[attribute.to_s]
value = mapping[value] if value && mapping
diff --git a/activerecord/test/active_record/connection_adapters/fake_adapter.rb b/activerecord/test/active_record/connection_adapters/fake_adapter.rb
index 59324c4857..64cde143a1 100644
--- a/activerecord/test/active_record/connection_adapters/fake_adapter.rb
+++ b/activerecord/test/active_record/connection_adapters/fake_adapter.rb
@@ -29,6 +29,7 @@ module ActiveRecord
@columns[table_name] << ActiveRecord::ConnectionAdapters::Column.new(
name.to_s,
options[:default],
+ lookup_cast_type(sql_type.to_s),
sql_type.to_s,
options[:null])
end
diff --git a/activerecord/test/cases/adapter_test.rb b/activerecord/test/cases/adapter_test.rb
index 90953ce6cd..6f84bae432 100644
--- a/activerecord/test/cases/adapter_test.rb
+++ b/activerecord/test/cases/adapter_test.rb
@@ -46,9 +46,7 @@ module ActiveRecord
@connection.add_index :accounts, :firm_id, :name => idx_name
indexes = @connection.indexes("accounts")
assert_equal "accounts", indexes.first.table
- # OpenBase does not have the concept of a named index
- # Indexes are merely properties of columns.
- assert_equal idx_name, indexes.first.name unless current_adapter?(:OpenBaseAdapter)
+ assert_equal idx_name, indexes.first.name
assert !indexes.first.unique
assert_equal ["firm_id"], indexes.first.columns
else
@@ -127,14 +125,12 @@ module ActiveRecord
assert_equal 1, Movie.create(:name => 'fight club').id
end
- if ActiveRecord::Base.connection.adapter_name != "FrontBase"
- def test_reset_table_with_non_integer_pk
- Subscriber.delete_all
- Subscriber.connection.reset_pk_sequence! 'subscribers'
- sub = Subscriber.new(:name => 'robert drake')
- sub.id = 'bob drake'
- assert_nothing_raised { sub.save! }
- end
+ def test_reset_table_with_non_integer_pk
+ Subscriber.delete_all
+ Subscriber.connection.reset_pk_sequence! 'subscribers'
+ sub = Subscriber.new(:name => 'robert drake')
+ sub.id = 'bob drake'
+ assert_nothing_raised { sub.save! }
end
end
@@ -144,7 +140,7 @@ module ActiveRecord
@connection.execute "INSERT INTO subscribers(nick) VALUES('me')"
end
end
-
+
unless current_adapter?(:SQLite3Adapter)
def test_foreign_key_violations_are_translated_to_specific_exception
assert_raises(ActiveRecord::InvalidForeignKey) do
@@ -157,7 +153,7 @@ module ActiveRecord
end
end
end
-
+
def test_foreign_key_violations_are_translated_to_specific_exception_with_validate_false
klass_has_fk = Class.new(ActiveRecord::Base) do
self.table_name = 'fk_test_has_fk'
@@ -196,7 +192,7 @@ module ActiveRecord
def test_select_methods_passing_a_association_relation
author = Author.create!(name: 'john')
Post.create!(author: author, title: 'foo', body: 'bar')
- query = author.posts.select(:title)
+ query = author.posts.where(title: 'foo').select(:title)
assert_equal({"title" => "foo"}, @connection.select_one(query.arel, nil, query.bind_values))
assert_equal({"title" => "foo"}, @connection.select_one(query))
assert @connection.select_all(query).is_a?(ActiveRecord::Result)
diff --git a/activerecord/test/cases/adapters/mysql/connection_test.rb b/activerecord/test/cases/adapters/mysql/connection_test.rb
index 412efa22ff..b0759dffde 100644
--- a/activerecord/test/cases/adapters/mysql/connection_test.rb
+++ b/activerecord/test/cases/adapters/mysql/connection_test.rb
@@ -134,12 +134,11 @@ class MysqlConnectionTest < ActiveRecord::TestCase
assert_equal [["STRICT_ALL_TABLES"]], result.rows
end
- def test_mysql_strict_mode_disabled_dont_override_global_sql_mode
+ def test_mysql_strict_mode_disabled
run_without_connection do |orig_connection|
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
+ result = ActiveRecord::Base.connection.exec_query "SELECT @@SESSION.sql_mode"
+ assert_equal [['']], result.rows
end
end
@@ -151,6 +150,14 @@ class MysqlConnectionTest < ActiveRecord::TestCase
end
end
+ def test_mysql_sql_mode_variable_overides_strict_mode
+ run_without_connection do |orig_connection|
+ ActiveRecord::Base.establish_connection(orig_connection.deep_merge(variables: { 'sql_mode' => 'ansi' }))
+ result = ActiveRecord::Base.connection.exec_query 'SELECT @@SESSION.sql_mode'
+ assert_not_equal [['STRICT_ALL_TABLES']], result.rows
+ 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}}))
diff --git a/activerecord/test/cases/adapters/mysql/consistency_test.rb b/activerecord/test/cases/adapters/mysql/consistency_test.rb
new file mode 100644
index 0000000000..083d533bb2
--- /dev/null
+++ b/activerecord/test/cases/adapters/mysql/consistency_test.rb
@@ -0,0 +1,48 @@
+require "cases/helper"
+
+class MysqlConsistencyTest < ActiveRecord::TestCase
+ self.use_transactional_fixtures = false
+
+ class Consistency < ActiveRecord::Base
+ self.table_name = "mysql_consistency"
+ end
+
+ setup do
+ @old_emulate_booleans = ActiveRecord::ConnectionAdapters::MysqlAdapter.emulate_booleans
+ ActiveRecord::ConnectionAdapters::MysqlAdapter.emulate_booleans = false
+
+ @connection = ActiveRecord::Base.connection
+ @connection.create_table("mysql_consistency") do |t|
+ t.boolean "a_bool"
+ t.string "a_string"
+ end
+ Consistency.reset_column_information
+ Consistency.create!
+ end
+
+ teardown do
+ ActiveRecord::ConnectionAdapters::MysqlAdapter.emulate_booleans = @old_emulate_booleans
+ @connection.drop_table "mysql_consistency"
+ end
+
+ test "boolean columns with random value type cast to 0 when emulate_booleans is false" do
+ with_new = Consistency.new
+ with_last = Consistency.last
+ with_new.a_bool = 'wibble'
+ with_last.a_bool = 'wibble'
+
+ assert_equal 0, with_new.a_bool
+ assert_equal 0, with_last.a_bool
+ end
+
+ test "string columns call #to_s" do
+ with_new = Consistency.new
+ with_last = Consistency.last
+ thing = Object.new
+ with_new.a_string = thing
+ with_last.a_string = thing
+
+ assert_equal thing.to_s, with_new.a_string
+ assert_equal thing.to_s, with_last.a_string
+ 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 1699380eb3..28106d3772 100644
--- a/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb
+++ b/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb
@@ -105,7 +105,7 @@ module ActiveRecord
result = @conn.exec_query('SELECT status FROM ex')
- assert_equal 2, result.column_types['status'].type_cast(result.last['status'])
+ assert_equal 2, result.column_types['status'].type_cast_from_database(result.last['status'])
end
end
diff --git a/activerecord/test/cases/adapters/mysql/quoting_test.rb b/activerecord/test/cases/adapters/mysql/quoting_test.rb
index 3d1330efb8..d8a954efa8 100644
--- a/activerecord/test/cases/adapters/mysql/quoting_test.rb
+++ b/activerecord/test/cases/adapters/mysql/quoting_test.rb
@@ -9,13 +9,13 @@ module ActiveRecord
end
def test_type_cast_true
- c = Column.new(nil, 1, 'boolean')
+ c = Column.new(nil, 1, Type::Boolean.new)
assert_equal 1, @conn.type_cast(true, nil)
assert_equal 1, @conn.type_cast(true, c)
end
def test_type_cast_false
- c = Column.new(nil, 1, 'boolean')
+ c = Column.new(nil, 1, Type::Boolean.new)
assert_equal 0, @conn.type_cast(false, nil)
assert_equal 0, @conn.type_cast(false, c)
end
diff --git a/activerecord/test/cases/adapters/mysql/schema_test.rb b/activerecord/test/cases/adapters/mysql/schema_test.rb
index 807a7a155e..87c5277e64 100644
--- a/activerecord/test/cases/adapters/mysql/schema_test.rb
+++ b/activerecord/test/cases/adapters/mysql/schema_test.rb
@@ -17,6 +17,44 @@ module ActiveRecord
self.table_name = "#{db}.#{table}"
def self.name; 'Post'; end
end
+
+ @connection.create_table "mysql_doubles"
+ end
+
+ teardown do
+ @connection.execute "drop table if exists mysql_doubles"
+ end
+
+ class MysqlDouble < ActiveRecord::Base
+ self.table_name = "mysql_doubles"
+ end
+
+ def test_float_limits
+ @connection.add_column :mysql_doubles, :float_no_limit, :float
+ @connection.add_column :mysql_doubles, :float_short, :float, limit: 5
+ @connection.add_column :mysql_doubles, :float_long, :float, limit: 53
+
+ @connection.add_column :mysql_doubles, :float_23, :float, limit: 23
+ @connection.add_column :mysql_doubles, :float_24, :float, limit: 24
+ @connection.add_column :mysql_doubles, :float_25, :float, limit: 25
+ MysqlDouble.reset_column_information
+
+ column_no_limit = MysqlDouble.columns.find { |c| c.name == 'float_no_limit' }
+ column_short = MysqlDouble.columns.find { |c| c.name == 'float_short' }
+ column_long = MysqlDouble.columns.find { |c| c.name == 'float_long' }
+
+ column_23 = MysqlDouble.columns.find { |c| c.name == 'float_23' }
+ column_24 = MysqlDouble.columns.find { |c| c.name == 'float_24' }
+ column_25 = MysqlDouble.columns.find { |c| c.name == 'float_25' }
+
+ # Mysql floats are precision 0..24, Mysql doubles are precision 25..53
+ assert_equal 24, column_no_limit.limit
+ assert_equal 24, column_short.limit
+ assert_equal 53, column_long.limit
+
+ assert_equal 24, column_23.limit
+ assert_equal 24, column_24.limit
+ assert_equal 53, column_25.limit
end
def test_schema
diff --git a/activerecord/test/cases/adapters/mysql2/boolean_test.rb b/activerecord/test/cases/adapters/mysql2/boolean_test.rb
index 267aa232d9..f3c711a64b 100644
--- a/activerecord/test/cases/adapters/mysql2/boolean_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/boolean_test.rb
@@ -47,7 +47,7 @@ class Mysql2BooleanTest < ActiveRecord::TestCase
assert_equal "1", attributes["published"]
assert_equal 1, @connection.type_cast(true, boolean_column)
- assert_equal 1, @connection.type_cast(true, string_column)
+ assert_equal "1", @connection.type_cast(true, string_column)
end
test "test type casting without emulated booleans" do
@@ -60,7 +60,7 @@ class Mysql2BooleanTest < ActiveRecord::TestCase
assert_equal "1", attributes["published"]
assert_equal 1, @connection.type_cast(true, boolean_column)
- assert_equal 1, @connection.type_cast(true, string_column)
+ assert_equal "1", @connection.type_cast(true, string_column)
end
test "with booleans stored as 1 and 0" do
diff --git a/activerecord/test/cases/adapters/mysql2/connection_test.rb b/activerecord/test/cases/adapters/mysql2/connection_test.rb
index 182d9409c7..3b35e69e0d 100644
--- a/activerecord/test/cases/adapters/mysql2/connection_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/connection_test.rb
@@ -60,12 +60,11 @@ class MysqlConnectionTest < ActiveRecord::TestCase
assert_equal [["STRICT_ALL_TABLES"]], result.rows
end
- def test_mysql_strict_mode_disabled_dont_override_global_sql_mode
+ def test_mysql_strict_mode_disabled
run_without_connection do |orig_connection|
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
+ result = ActiveRecord::Base.connection.exec_query "SELECT @@SESSION.sql_mode"
+ assert_equal [['']], result.rows
end
end
@@ -77,6 +76,14 @@ class MysqlConnectionTest < ActiveRecord::TestCase
end
end
+ def test_mysql_sql_mode_variable_overides_strict_mode
+ run_without_connection do |orig_connection|
+ ActiveRecord::Base.establish_connection(orig_connection.deep_merge(variables: { 'sql_mode' => 'ansi' }))
+ result = ActiveRecord::Base.connection.exec_query 'SELECT @@SESSION.sql_mode'
+ assert_not_equal [['STRICT_ALL_TABLES']], result.rows
+ 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}}))
diff --git a/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb b/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb
index ec73ec35aa..9c49599d34 100644
--- a/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb
@@ -6,12 +6,12 @@ module ActiveRecord
class SchemaMigrationsTest < ActiveRecord::TestCase
def test_renaming_index_on_foreign_key
connection.add_index "engines", "car_id"
- connection.execute "ALTER TABLE engines ADD CONSTRAINT fk_engines_cars FOREIGN KEY (car_id) REFERENCES cars(id)"
+ connection.add_foreign_key :engines, :cars, name: "fk_engines_cars"
connection.rename_index("engines", "index_engines_on_car_id", "idx_renamed")
assert_equal ["idx_renamed"], connection.indexes("engines").map(&:name)
ensure
- connection.execute "ALTER TABLE engines DROP FOREIGN KEY fk_engines_cars"
+ connection.remove_foreign_key :engines, name: "fk_engines_cars"
end
def test_initializes_schema_migrations_for_encoding_utf8mb4
diff --git a/activerecord/test/cases/adapters/postgresql/array_test.rb b/activerecord/test/cases/adapters/postgresql/array_test.rb
index 18dd4a6de8..8df1b7d18c 100644
--- a/activerecord/test/cases/adapters/postgresql/array_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/array_test.rb
@@ -1,22 +1,33 @@
# encoding: utf-8
require "cases/helper"
-require 'active_record/base'
-require 'active_record/connection_adapters/postgresql_adapter'
class PostgresqlArrayTest < ActiveRecord::TestCase
+ include InTimeZone
+ OID = ActiveRecord::ConnectionAdapters::PostgreSQL::OID
+
class PgArray < ActiveRecord::Base
self.table_name = 'pg_arrays'
end
def setup
@connection = ActiveRecord::Base.connection
+
+ unless @connection.extension_enabled?('hstore')
+ @connection.enable_extension 'hstore'
+ @connection.commit_db_transaction
+ end
+
+ @connection.reconnect!
+
@connection.transaction do
@connection.create_table('pg_arrays') do |t|
t.string 'tags', array: true
t.integer 'ratings', array: true
+ t.datetime :datetimes, array: true
+ t.hstore :hstores, array: true
end
end
- @column = PgArray.columns.find { |c| c.name == 'tags' }
+ @column = PgArray.columns_hash['tags']
end
teardown do
@@ -27,7 +38,6 @@ class PostgresqlArrayTest < ActiveRecord::TestCase
assert_equal :string, @column.type
assert_equal "character varying", @column.sql_type
assert @column.array
- assert_not @column.text?
assert_not @column.number?
assert_not @column.binary?
@@ -40,9 +50,8 @@ class PostgresqlArrayTest < ActiveRecord::TestCase
def test_default
@connection.add_column 'pg_arrays', 'score', :integer, array: true, default: [4, 4, 2]
PgArray.reset_column_information
- column = PgArray.columns_hash["score"]
- assert_equal([4, 4, 2], column.default)
+ assert_equal([4, 4, 2], PgArray.column_defaults['score'])
assert_equal([4, 4, 2], PgArray.new.score)
ensure
PgArray.reset_column_information
@@ -51,9 +60,8 @@ class PostgresqlArrayTest < ActiveRecord::TestCase
def test_default_strings
@connection.add_column 'pg_arrays', 'names', :string, array: true, default: ["foo", "bar"]
PgArray.reset_column_information
- column = PgArray.columns_hash["names"]
- assert_equal(["foo", "bar"], column.default)
+ assert_equal(["foo", "bar"], PgArray.column_defaults['names'])
assert_equal(["foo", "bar"], PgArray.new.names)
ensure
PgArray.reset_column_information
@@ -61,13 +69,13 @@ class PostgresqlArrayTest < ActiveRecord::TestCase
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: "{}"
+ @connection.change_column :pg_arrays, :snippets, :text, array: true, default: []
PgArray.reset_column_information
- column = PgArray.columns.find { |c| c.name == 'snippets' }
+ column = PgArray.columns_hash['snippets']
assert_equal :text, column.type
- assert_equal [], column.default
+ assert_equal [], PgArray.column_defaults['snippets']
assert column.array
end
@@ -80,38 +88,56 @@ class PostgresqlArrayTest < ActiveRecord::TestCase
end
end
- def test_type_cast_array
- data = '{1,2,3}'
- oid_type = @column.instance_variable_get('@oid_type').subtype
- # we are getting the instance variable in this test, but in the
- # normal use of string_to_array, it's called from the OID::Array
- # class and will have the OID instance that will provide the type
- # casting
- array = @column.class.string_to_array data, oid_type
- assert_equal(['1', '2', '3'], array)
- assert_equal(['1', '2', '3'], @column.type_cast(data))
+ def test_change_column_default_with_array
+ @connection.change_column_default :pg_arrays, :tags, []
- assert_equal([], @column.type_cast('{}'))
- assert_equal([nil], @column.type_cast('{NULL}'))
+ PgArray.reset_column_information
+ assert_equal [], PgArray.column_defaults['tags']
+ end
+
+ def test_type_cast_array
+ assert_equal(['1', '2', '3'], @column.type_cast_from_database('{1,2,3}'))
+ assert_equal([], @column.type_cast_from_database('{}'))
+ assert_equal([nil], @column.type_cast_from_database('{NULL}'))
end
def test_type_cast_integers
x = PgArray.new(ratings: ['1', '2'])
- assert x.save!
- assert_equal(['1', '2'], x.ratings)
+
+ assert_equal([1, 2], x.ratings)
+
+ x.save!
+ x.reload
+
+ assert_equal([1, 2], x.ratings)
end
- def test_rewrite
+ def test_select_with_strings
@connection.execute "insert into pg_arrays (tags) VALUES ('{1,2,3}')"
x = PgArray.first
- x.tags = ['1','2','3','4']
- assert x.save!
+ assert_equal(['1','2','3'], x.tags)
end
- def test_select
+ def test_rewrite_with_strings
@connection.execute "insert into pg_arrays (tags) VALUES ('{1,2,3}')"
x = PgArray.first
- assert_equal(['1','2','3'], x.tags)
+ x.tags = ['1','2','3','4']
+ x.save!
+ assert_equal ['1','2','3','4'], x.reload.tags
+ end
+
+ def test_select_with_integers
+ @connection.execute "insert into pg_arrays (ratings) VALUES ('{1,2,3}')"
+ x = PgArray.first
+ assert_equal([1, 2, 3], x.ratings)
+ end
+
+ def test_rewrite_with_integers
+ @connection.execute "insert into pg_arrays (ratings) VALUES ('{1,2,3}')"
+ x = PgArray.first
+ x.ratings = [2, '3', 4]
+ x.save!
+ assert_equal [2, 3, 4], x.reload.ratings
end
def test_multi_dimensional_with_strings
@@ -165,14 +191,71 @@ class PostgresqlArrayTest < ActiveRecord::TestCase
assert_equal("[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]", record.attribute_for_inspect(:ratings))
end
- def test_update_all
- pg_array = PgArray.create! tags: ["one", "two", "three"]
+ def test_escaping
+ unknown = 'foo\\",bar,baz,\\'
+ tags = ["hello_#{unknown}"]
+ ar = PgArray.create!(tags: tags)
+ ar.reload
+ assert_equal tags, ar.tags
+ end
- PgArray.update_all tags: ["four", "five"]
- assert_equal ["four", "five"], pg_array.reload.tags
+ def test_string_quoting_rules_match_pg_behavior
+ tags = ["", "one{", "two}", %(three"), "four\\", "five ", "six\t", "seven\n", "eight,", "nine", "ten\r", "NULL"]
+ x = PgArray.create!(tags: tags)
+ x.reload
- PgArray.update_all tags: []
- assert_equal [], pg_array.reload.tags
+ assert_equal x.tags_before_type_cast, PgArray.columns_hash['tags'].type_cast_for_database(tags)
+ end
+
+ def test_quoting_non_standard_delimiters
+ strings = ["hello,", "world;"]
+ comma_delim = OID::Array.new(ActiveRecord::Type::String.new, ',')
+ semicolon_delim = OID::Array.new(ActiveRecord::Type::String.new, ';')
+
+ assert_equal %({"hello,",world;}), comma_delim.type_cast_for_database(strings)
+ assert_equal %({hello,;"world;"}), semicolon_delim.type_cast_for_database(strings)
+ end
+
+ def test_mutate_array
+ x = PgArray.create!(tags: %w(one two))
+
+ x.tags << "three"
+ x.save!
+ x.reload
+
+ assert_equal %w(one two three), x.tags
+ assert_not x.changed?
+ end
+
+ def test_mutate_value_in_array
+ x = PgArray.create!(hstores: [{ a: 'a' }, { b: 'b' }])
+
+ x.hstores.first['a'] = 'c'
+ x.save!
+ x.reload
+
+ assert_equal [{ 'a' => 'c' }, { 'b' => 'b' }], x.hstores
+ assert_not x.changed?
+ end
+
+ def test_datetime_with_timezone_awareness
+ tz = "Pacific Time (US & Canada)"
+
+ in_time_zone tz do
+ PgArray.reset_column_information
+ time_string = Time.current.to_s
+ time = Time.zone.parse(time_string)
+
+ record = PgArray.new(datetimes: [time_string])
+ assert_equal [time], record.datetimes
+ assert_equal ActiveSupport::TimeZone[tz], record.datetimes.first.time_zone
+
+ record.save!
+ record.reload
+
+ assert_equal [time], record.datetimes
+ assert_equal ActiveSupport::TimeZone[tz], record.datetimes.first.time_zone
+ end
end
private
diff --git a/activerecord/test/cases/adapters/postgresql/bit_string_test.rb b/activerecord/test/cases/adapters/postgresql/bit_string_test.rb
new file mode 100644
index 0000000000..72222c01fd
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/bit_string_test.rb
@@ -0,0 +1,76 @@
+# -*- coding: utf-8 -*-
+require "cases/helper"
+require 'support/connection_helper'
+require 'support/schema_dumping_helper'
+
+class PostgresqlBitStringTest < ActiveRecord::TestCase
+ include ConnectionHelper
+ include SchemaDumpingHelper
+
+ class PostgresqlBitString < ActiveRecord::Base; end
+
+ def setup
+ @connection = ActiveRecord::Base.connection
+ @connection.create_table('postgresql_bit_strings', :force => true) do |t|
+ t.bit :a_bit, default: "00000011", limit: 8
+ t.bit_varying :a_bit_varying, default: "0011", limit: 4
+ end
+ end
+
+ def teardown
+ return unless @connection
+ @connection.execute 'DROP TABLE IF EXISTS postgresql_bit_strings'
+ end
+
+ def test_bit_string_column
+ column = PostgresqlBitString.columns_hash["a_bit"]
+ assert_equal :bit, column.type
+ assert_equal "bit(8)", column.sql_type
+ assert_not column.number?
+ assert_not column.binary?
+ assert_not column.array
+ end
+
+ def test_bit_string_varying_column
+ column = PostgresqlBitString.columns_hash["a_bit_varying"]
+ assert_equal :bit_varying, column.type
+ assert_equal "bit varying(4)", column.sql_type
+ assert_not column.number?
+ assert_not column.binary?
+ assert_not column.array
+ end
+
+ def test_default
+ assert_equal "00000011", PostgresqlBitString.column_defaults['a_bit']
+ assert_equal "00000011", PostgresqlBitString.new.a_bit
+
+ assert_equal "0011", PostgresqlBitString.column_defaults['a_bit_varying']
+ assert_equal "0011", PostgresqlBitString.new.a_bit_varying
+ end
+
+ def test_schema_dumping
+ output = dump_table_schema("postgresql_bit_strings")
+ assert_match %r{t\.bit\s+"a_bit",\s+limit: 8,\s+default: "00000011"$}, output
+ assert_match %r{t\.bit_varying\s+"a_bit_varying",\s+limit: 4,\s+default: "0011"$}, output
+ end
+
+ def test_assigning_invalid_hex_string_raises_exception
+ assert_raises(ActiveRecord::StatementInvalid) { PostgresqlBitString.create! a_bit: "FF" }
+ assert_raises(ActiveRecord::StatementInvalid) { PostgresqlBitString.create! a_bit_varying: "FF" }
+ end
+
+ def test_roundtrip
+ PostgresqlBitString.create! a_bit: "00001010", a_bit_varying: "0101"
+ record = PostgresqlBitString.first
+ assert_equal "00001010", record.a_bit
+ assert_equal "0101", record.a_bit_varying
+
+ record.a_bit = "11111111"
+ record.a_bit_varying = "0xF"
+ record.save!
+
+ assert record.reload
+ assert_equal "11111111", record.a_bit
+ assert_equal "1111", record.a_bit_varying
+ end
+end
diff --git a/activerecord/test/cases/adapters/postgresql/bytea_test.rb b/activerecord/test/cases/adapters/postgresql/bytea_test.rb
index e3478856c8..7872f91943 100644
--- a/activerecord/test/cases/adapters/postgresql/bytea_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/bytea_test.rb
@@ -1,8 +1,5 @@
# 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
@@ -19,7 +16,7 @@ class PostgresqlByteaTest < ActiveRecord::TestCase
end
end
end
- @column = ByteaDataType.columns.find { |c| c.name == 'payload' }
+ @column = ByteaDataType.columns_hash['payload']
assert(@column.is_a?(ActiveRecord::ConnectionAdapters::PostgreSQLColumn))
end
@@ -36,16 +33,16 @@ class PostgresqlByteaTest < ActiveRecord::TestCase
data = "\u001F\x8B"
assert_equal('UTF-8', data.encoding.name)
- assert_equal('ASCII-8BIT', @column.type_cast(data).encoding.name)
+ assert_equal('ASCII-8BIT', @column.type_cast_from_database(data).encoding.name)
end
def test_type_cast_binary_value
data = "\u001F\x8B".force_encoding("BINARY")
- assert_equal(data, @column.type_cast(data))
+ assert_equal(data, @column.type_cast_from_database(data))
end
def test_type_case_nil
- assert_equal(nil, @column.type_cast(nil))
+ assert_equal(nil, @column.type_cast_from_database(nil))
end
def test_read_value
diff --git a/activerecord/test/cases/adapters/postgresql/citext_test.rb b/activerecord/test/cases/adapters/postgresql/citext_test.rb
index 948bf49a54..2acb64f81c 100644
--- a/activerecord/test/cases/adapters/postgresql/citext_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/citext_test.rb
@@ -1,8 +1,5 @@
# encoding: utf-8
-
require 'cases/helper'
-require 'active_record/base'
-require 'active_record/connection_adapters/postgresql_adapter'
if ActiveRecord::Base.connection.supports_extensions?
class PostgresqlCitextTest < ActiveRecord::TestCase
@@ -38,7 +35,6 @@ if ActiveRecord::Base.connection.supports_extensions?
column = Citext.columns_hash['cival']
assert_equal :citext, column.type
assert_equal 'citext', column.sql_type
- assert_not column.text?
assert_not column.number?
assert_not column.binary?
assert_not column.array
@@ -50,7 +46,7 @@ if ActiveRecord::Base.connection.supports_extensions?
t.citext 'username'
end
Citext.reset_column_information
- column = Citext.columns.find { |c| c.name == 'username' }
+ column = Citext.columns_hash['username']
assert_equal :citext, column.type
raise ActiveRecord::Rollback # reset the schema change
diff --git a/activerecord/test/cases/adapters/postgresql/composite_test.rb b/activerecord/test/cases/adapters/postgresql/composite_test.rb
index 224b1b770b..cfab5ca902 100644
--- a/activerecord/test/cases/adapters/postgresql/composite_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/composite_test.rb
@@ -1,19 +1,17 @@
# -*- coding: utf-8 -*-
require "cases/helper"
-require 'active_record/base'
-require 'active_record/connection_adapters/postgresql_adapter'
+require 'support/connection_helper'
+
+module PostgresqlCompositeBehavior
+ include ConnectionHelper
-class PostgresqlCompositeTest < ActiveRecord::TestCase
class PostgresqlComposite < ActiveRecord::Base
self.table_name = "postgresql_composites"
end
- teardown do
- @connection.execute 'DROP TABLE IF EXISTS postgresql_composites'
- @connection.execute 'DROP TYPE IF EXISTS full_address'
- end
-
def setup
+ super
+
@connection = ActiveRecord::Base.connection
@connection.transaction do
@connection.execute <<-SQL
@@ -29,18 +27,37 @@ class PostgresqlCompositeTest < ActiveRecord::TestCase
end
end
+ def teardown
+ super
+
+ @connection.execute 'DROP TABLE IF EXISTS postgresql_composites'
+ @connection.execute 'DROP TYPE IF EXISTS full_address'
+ reset_connection
+ PostgresqlComposite.reset_column_information
+ end
+end
+
+# Composites are mapped to `OID::Identity` by default. The user is informed by a warning like:
+# "unknown OID 5653508: failed to recognize type of 'address'. It will be treated as String."
+# To take full advantage of composite types, we suggest you register your own +OID::Type+.
+# See PostgresqlCompositeWithCustomOIDTest
+class PostgresqlCompositeTest < ActiveRecord::TestCase
+ include PostgresqlCompositeBehavior
+
def test_column
+ ensure_warning_is_issued
+
column = PostgresqlComposite.columns_hash["address"]
- # TODO: Composite columns should have a type
assert_nil column.type
assert_equal "full_address", column.sql_type
assert_not column.number?
- assert_not column.text?
assert_not column.binary?
assert_not column.array
end
def test_composite_mapping
+ ensure_warning_is_issued
+
@connection.execute "INSERT INTO postgresql_composites VALUES (1, ROW('Paris', 'Champs-Élysées'));"
composite = PostgresqlComposite.first
assert_equal "(Paris,Champs-Élysées)", composite.address
@@ -50,4 +67,65 @@ class PostgresqlCompositeTest < ActiveRecord::TestCase
assert_equal '(Paris,"Rue Basse")', composite.reload.address
end
+
+ private
+ def ensure_warning_is_issued
+ warning = capture(:stderr) do
+ PostgresqlComposite.columns_hash
+ end
+ assert_match(/unknown OID \d+: failed to recognize type of 'address'\. It will be treated as String\./, warning)
+ end
+end
+
+class PostgresqlCompositeWithCustomOIDTest < ActiveRecord::TestCase
+ include PostgresqlCompositeBehavior
+
+ class FullAddressType < ActiveRecord::Type::Value
+ def type; :full_address end
+
+ def type_cast_from_database(value)
+ if value =~ /\("?([^",]*)"?,"?([^",]*)"?\)/
+ FullAddress.new($1, $2)
+ end
+ end
+
+ def type_cast_from_user(value)
+ value
+ end
+
+ def type_cast_for_database(value)
+ return if value.nil?
+ "(#{value.city},#{value.street})"
+ end
+ end
+
+ FullAddress = Struct.new(:city, :street)
+
+ def setup
+ super
+
+ @connection.type_map.register_type "full_address", FullAddressType.new
+ end
+
+ def test_column
+ column = PostgresqlComposite.columns_hash["address"]
+ assert_equal :full_address, column.type
+ assert_equal "full_address", column.sql_type
+ assert_not column.number?
+ assert_not column.binary?
+ assert_not column.array
+ end
+
+ def test_composite_mapping
+ @connection.execute "INSERT INTO postgresql_composites VALUES (1, ROW('Paris', 'Champs-Élysées'));"
+ composite = PostgresqlComposite.first
+ assert_equal "Paris", composite.address.city
+ assert_equal "Champs-Élysées", composite.address.street
+
+ composite.address = FullAddress.new("Paris", "Rue Basse")
+ composite.save!
+
+ assert_equal 'Paris', composite.reload.address.city
+ assert_equal 'Rue Basse', composite.reload.address.street
+ end
end
diff --git a/activerecord/test/cases/adapters/postgresql/connection_test.rb b/activerecord/test/cases/adapters/postgresql/connection_test.rb
index 5f84c893c0..d26cda46fa 100644
--- a/activerecord/test/cases/adapters/postgresql/connection_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/connection_test.rb
@@ -121,7 +121,7 @@ module ActiveRecord
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
+ plan = res.column_types['QUERY PLAN'].type_cast_from_database res.rows.first.first
assert_operator plan.length, :>, 0
end
diff --git a/activerecord/test/cases/adapters/postgresql/datatype_test.rb b/activerecord/test/cases/adapters/postgresql/datatype_test.rb
index e7dda1a1af..a0a34e4b87 100644
--- a/activerecord/test/cases/adapters/postgresql/datatype_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/datatype_test.rb
@@ -1,13 +1,6 @@
require "cases/helper"
+require 'support/ddl_helper'
-class PostgresqlArray < ActiveRecord::Base
-end
-
-class PostgresqlTsvector < ActiveRecord::Base
-end
-
-class PostgresqlMoney < ActiveRecord::Base
-end
class PostgresqlNumber < ActiveRecord::Base
end
@@ -15,18 +8,9 @@ end
class PostgresqlTime < ActiveRecord::Base
end
-class PostgresqlNetworkAddress < ActiveRecord::Base
-end
-
-class PostgresqlBitString < ActiveRecord::Base
-end
-
class PostgresqlOid < ActiveRecord::Base
end
-class PostgresqlTimestampWithZone < ActiveRecord::Base
-end
-
class PostgresqlLtree < ActiveRecord::Base
end
@@ -35,62 +19,23 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase
def setup
@connection = ActiveRecord::Base.connection
- @connection.execute("set lc_monetary = 'C'")
-
- @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 (id, text_vector) VALUES (1, ' ''text'' ''vector'' ')")
-
- @first_tsvector = PostgresqlTsvector.find(1)
-
- @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 (id, single, double) VALUES (1, 123.456, 123456.789)")
+ @connection.execute("INSERT INTO postgresql_numbers (id, single, double) VALUES (2, '-Infinity', 'Infinity')")
+ @connection.execute("INSERT INTO postgresql_numbers (id, single, double) VALUES (3, 123.456, 'NaN')")
@first_number = PostgresqlNumber.find(1)
+ @second_number = PostgresqlNumber.find(2)
+ @third_number = PostgresqlNumber.find(3)
@connection.execute("INSERT INTO postgresql_times (id, time_interval, scaled_time_interval) VALUES (1, '1 year 2 days ago', '3 weeks ago')")
@first_time = PostgresqlTime.find(1)
- @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 (id, bit_string, bit_string_varying) VALUES (1, B'00010101', X'15')")
- @first_bit_string = PostgresqlBitString.find(1)
-
@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 (id, time) VALUES (1, '2010-01-01 10:00:00-1')")
end
teardown do
- [PostgresqlArray, PostgresqlTsvector, PostgresqlMoney, PostgresqlNumber, PostgresqlTime, PostgresqlNetworkAddress,
- PostgresqlBitString, PostgresqlOid, PostgresqlTimestampWithZone].each(&:delete_all)
- end
-
- def test_array_escaping
- unknown = %(foo\\",bar,baz,\\)
- nicknames = ["hello_#{unknown}"]
- ar = PostgresqlArray.create!(nicknames: nicknames, id: 100)
- ar.reload
- assert_equal nicknames, ar.nicknames
- 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_tsvector_types
- assert_equal :tsvector, @first_tsvector.column_for_attribute(:text_vector).type
- end
-
- def test_data_type_of_money_types
- assert_equal :decimal, @first_money.column_for_attribute(:wealth).type
+ [PostgresqlNumber, PostgresqlTime, PostgresqlOid].each(&:delete_all)
end
def test_data_type_of_number_types
@@ -103,57 +48,16 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase
assert_equal :string, @first_time.column_for_attribute(:scaled_time_interval).type
end
- def test_data_type_of_network_address_types
- assert_equal :cidr, @first_network_address.column_for_attribute(:cidr_address).type
- assert_equal :inet, @first_network_address.column_for_attribute(:inet_address).type
- assert_equal :macaddr, @first_network_address.column_for_attribute(:mac_address).type
- end
-
- def test_data_type_of_bit_string_types
- assert_equal :string, @first_bit_string.column_for_attribute(:bit_string).type
- assert_equal :string, @first_bit_string.column_for_attribute(:bit_string_varying).type
- end
-
def test_data_type_of_oid_types
assert_equal :integer, @first_oid.column_for_attribute(:obj_id).type
end
- def test_array_values
- assert_equal [35000,21000,18000,17000], @first_array.commission_by_quarter
- assert_equal ['foo','bar','baz'], @first_array.nicknames
- end
-
- def test_tsvector_values
- assert_equal "'text' 'vector'", @first_tsvector.text_vector
- 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_update_tsvector
- new_text_vector = "'new' 'text' 'vector'"
- @first_tsvector.text_vector = new_text_vector
- assert @first_tsvector.save
- assert @first_tsvector.reload
- @first_tsvector.text_vector = new_text_vector
- assert @first_tsvector.save
- assert @first_tsvector.reload
- assert_equal new_text_vector, @first_tsvector.text_vector
- end
-
def test_number_values
assert_equal 123.456, @first_number.single
assert_equal 123456.789, @first_number.double
+ assert_equal(-::Float::INFINITY, @second_number.single)
+ assert_equal ::Float::INFINITY, @second_number.double
+ assert_same ::Float::NAN, @third_number.double
end
def test_time_values
@@ -161,56 +65,10 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase
assert_equal '-21 days', @first_time.scaled_time_interval
end
- def test_network_address_values_ipaddr
- cidr_address = IPAddr.new '192.168.0.0/24'
- inet_address = IPAddr.new '172.16.1.254'
-
- assert_equal cidr_address, @first_network_address.cidr_address
- assert_equal inet_address, @first_network_address.inet_address
- assert_equal '01:23:45:67:89:0a', @first_network_address.mac_address
- end
-
- def test_bit_string_values
- assert_equal '00010101', @first_bit_string.bit_string
- assert_equal '00010101', @first_bit_string.bit_string_varying
- end
-
def test_oid_values
assert_equal 1234, @first_oid.obj_id
end
- def test_update_integer_array
- new_value = [32800,95000,29350,17000]
- @first_array.commission_by_quarter = new_value
- assert @first_array.save
- assert @first_array.reload
- 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 new_value, @first_array.commission_by_quarter
- end
-
- def test_update_text_array
- new_value = ['robby','robert','rob','robbie']
- @first_array.nicknames = new_value
- assert @first_array.save
- assert @first_array.reload
- assert_equal new_value, @first_array.nicknames
- @first_array.nicknames = new_value
- assert @first_array.save
- assert @first_array.reload
- assert_equal new_value, @first_array.nicknames
- end
-
- def test_update_money
- new_value = BigDecimal.new('123.45')
- @first_money.wealth = new_value
- assert @first_money.save
- assert @first_money.reload
- assert_equal new_value, @first_money.wealth
- end
-
def test_update_number
new_single = 789.012
new_double = 789012.345
@@ -229,51 +87,6 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase
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'
- @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
- assert_equal @first_network_address.inet_address, new_inet_address
- assert_equal @first_network_address.mac_address, new_mac_address
- end
-
- def test_update_bit_string
- new_bit_string = '11111111'
- 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 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
@first_oid.obj_id = new_value
@@ -281,30 +94,26 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase
assert @first_oid.reload
assert_equal new_value, @first_oid.obj_id
end
+end
- def test_timestamp_with_zone_values_with_rails_time_zone_support
- with_timezone_config default: :utc, aware_attributes: true do
- @connection.reconnect!
+class PostgresqlInternalDataTypeTest < ActiveRecord::TestCase
+ include DdlHelper
- @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
- @connection.reconnect!
+ setup do
+ @connection = ActiveRecord::Base.connection
end
- def test_timestamp_with_zone_values_without_rails_time_zone_support
- 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')
+ def test_name_column_type
+ with_example_table @connection, 'ex', 'data name' do
+ column = @connection.columns('ex').find { |col| col.name == 'data' }
+ assert_equal :string, column.type
+ end
+ end
- @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
+ def test_char_column_type
+ with_example_table @connection, 'ex', 'data "char"' do
+ column = @connection.columns('ex').find { |col| col.name == 'data' }
+ assert_equal :string, column.type
end
- ensure
- @connection.reconnect!
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/domain_test.rb b/activerecord/test/cases/adapters/postgresql/domain_test.rb
index 5286a847a4..1500adb42d 100644
--- a/activerecord/test/cases/adapters/postgresql/domain_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/domain_test.rb
@@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
require "cases/helper"
require 'support/connection_helper'
-require 'active_record/base'
-require 'active_record/connection_adapters/postgresql_adapter'
class PostgresqlDomainTest < ActiveRecord::TestCase
include ConnectionHelper
@@ -32,7 +30,6 @@ class PostgresqlDomainTest < ActiveRecord::TestCase
assert_equal :decimal, column.type
assert_equal "custom_money", column.sql_type
assert column.number?
- assert_not column.text?
assert_not column.binary?
assert_not column.array
end
diff --git a/activerecord/test/cases/adapters/postgresql/enum_test.rb b/activerecord/test/cases/adapters/postgresql/enum_test.rb
index 4146b117f6..d99c4a292e 100644
--- a/activerecord/test/cases/adapters/postgresql/enum_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/enum_test.rb
@@ -34,11 +34,20 @@ class PostgresqlEnumTest < ActiveRecord::TestCase
assert_equal :enum, column.type
assert_equal "mood", column.sql_type
assert_not column.number?
- assert_not column.text?
assert_not column.binary?
assert_not column.array
end
+ def test_enum_defaults
+ @connection.add_column 'postgresql_enums', 'good_mood', :mood, default: 'happy'
+ PostgresqlEnum.reset_column_information
+
+ assert_equal "happy", PostgresqlEnum.column_defaults['good_mood']
+ assert_equal "happy", PostgresqlEnum.new.good_mood
+ ensure
+ PostgresqlEnum.reset_column_information
+ end
+
def test_enum_mapping
@connection.execute "INSERT INTO postgresql_enums VALUES (1, 'sad');"
enum = PostgresqlEnum.first
diff --git a/activerecord/test/cases/adapters/postgresql/extension_migration_test.rb b/activerecord/test/cases/adapters/postgresql/extension_migration_test.rb
new file mode 100644
index 0000000000..7b99fcdda0
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/extension_migration_test.rb
@@ -0,0 +1,63 @@
+require "cases/helper"
+
+class PostgresqlExtensionMigrationTest < ActiveRecord::TestCase
+ self.use_transactional_fixtures = false
+
+ class EnableHstore < ActiveRecord::Migration
+ def change
+ enable_extension "hstore"
+ end
+ end
+
+ class DisableHstore < ActiveRecord::Migration
+ def change
+ disable_extension "hstore"
+ end
+ end
+
+ def setup
+ super
+
+ @connection = ActiveRecord::Base.connection
+
+ unless @connection.supports_extensions?
+ return skip("no extension support")
+ end
+
+ @old_schema_migration_tabel_name = ActiveRecord::SchemaMigration.table_name
+ @old_tabel_name_prefix = ActiveRecord::Base.table_name_prefix
+ @old_tabel_name_suffix = ActiveRecord::Base.table_name_suffix
+
+ ActiveRecord::Base.table_name_prefix = "p_"
+ ActiveRecord::Base.table_name_suffix = "_s"
+ ActiveRecord::SchemaMigration.delete_all rescue nil
+ ActiveRecord::SchemaMigration.table_name = "p_schema_migrations_s"
+ ActiveRecord::Migration.verbose = false
+ end
+
+ def teardown
+ ActiveRecord::Base.table_name_prefix = @old_tabel_name_prefix
+ ActiveRecord::Base.table_name_suffix = @old_tabel_name_suffix
+ ActiveRecord::SchemaMigration.delete_all rescue nil
+ ActiveRecord::Migration.verbose = true
+ ActiveRecord::SchemaMigration.table_name = @old_schema_migration_tabel_name
+
+ super
+ end
+
+ def test_enable_extension_migration_ignores_prefix_and_suffix
+ @connection.disable_extension("hstore")
+
+ migrations = [EnableHstore.new(nil, 1)]
+ ActiveRecord::Migrator.new(:up, migrations).migrate
+ assert @connection.extension_enabled?("hstore"), "extension hstore should be enabled"
+ end
+
+ def test_disable_extension_migration_ignores_prefix_and_suffix
+ @connection.enable_extension("hstore")
+
+ migrations = [DisableHstore.new(nil, 1)]
+ ActiveRecord::Migrator.new(:up, migrations).migrate
+ assert_not @connection.extension_enabled?("hstore"), "extension hstore should not be enabled"
+ end
+end
diff --git a/activerecord/test/cases/adapters/postgresql/full_text_test.rb b/activerecord/test/cases/adapters/postgresql/full_text_test.rb
new file mode 100644
index 0000000000..9dadb177ca
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/full_text_test.rb
@@ -0,0 +1,26 @@
+# encoding: utf-8
+require "cases/helper"
+
+class PostgresqlFullTextTest < ActiveRecord::TestCase
+ class PostgresqlTsvector < ActiveRecord::Base; end
+
+ def test_tsvector_column
+ column = PostgresqlTsvector.columns_hash["text_vector"]
+ assert_equal :tsvector, column.type
+ assert_equal "tsvector", column.sql_type
+ assert_not column.number?
+ assert_not column.binary?
+ assert_not column.array
+ end
+
+ def test_update_tsvector
+ PostgresqlTsvector.create text_vector: "'text' 'vector'"
+ tsvector = PostgresqlTsvector.first
+ assert_equal "'text' 'vector'", tsvector.text_vector
+
+ tsvector.text_vector = "'new' 'text' 'vector'"
+ tsvector.save!
+ assert tsvector.reload
+ assert_equal "'new' 'text' 'vector'", tsvector.text_vector
+ end
+end
diff --git a/activerecord/test/cases/adapters/postgresql/geometric_test.rb b/activerecord/test/cases/adapters/postgresql/geometric_test.rb
new file mode 100644
index 0000000000..6c0adbbeaa
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/geometric_test.rb
@@ -0,0 +1,72 @@
+# -*- coding: utf-8 -*-
+require "cases/helper"
+require 'support/connection_helper'
+require 'support/schema_dumping_helper'
+
+class PostgresqlPointTest < ActiveRecord::TestCase
+ include ConnectionHelper
+ include SchemaDumpingHelper
+
+ class PostgresqlPoint < ActiveRecord::Base; end
+
+ def setup
+ @connection = ActiveRecord::Base.connection
+ @connection.transaction do
+ @connection.create_table('postgresql_points') do |t|
+ t.point :x
+ t.point :y, default: [12.2, 13.3]
+ t.point :z, default: "(14.4,15.5)"
+ end
+ end
+ end
+
+ teardown do
+ @connection.execute 'DROP TABLE IF EXISTS postgresql_points'
+ end
+
+ def test_column
+ column = PostgresqlPoint.columns_hash["x"]
+ assert_equal :point, column.type
+ assert_equal "point", column.sql_type
+ assert_not column.number?
+ assert_not column.binary?
+ assert_not column.array
+ end
+
+ def test_default
+ assert_equal [12.2, 13.3], PostgresqlPoint.column_defaults['y']
+ assert_equal [12.2, 13.3], PostgresqlPoint.new.y
+
+ assert_equal [14.4, 15.5], PostgresqlPoint.column_defaults['z']
+ assert_equal [14.4, 15.5], PostgresqlPoint.new.z
+ end
+
+ def test_schema_dumping
+ output = dump_table_schema("postgresql_points")
+ assert_match %r{t\.point\s+"x"$}, output
+ assert_match %r{t\.point\s+"y",\s+default: \[12\.2, 13\.3\]$}, output
+ assert_match %r{t\.point\s+"z",\s+default: \[14\.4, 15\.5\]$}, output
+ end
+
+ def test_roundtrip
+ PostgresqlPoint.create! x: [10, 25.2]
+ record = PostgresqlPoint.first
+ assert_equal [10, 25.2], record.x
+
+ record.x = [1.1, 2.2]
+ record.save!
+ assert record.reload
+ assert_equal [1.1, 2.2], record.x
+ end
+
+ def test_mutation
+ p = PostgresqlPoint.create! x: [10, 20]
+
+ p.x[1] = 25
+ p.save!
+ p.reload
+
+ assert_equal [10.0, 25.0], p.x
+ assert_not p.changed?
+ end
+end
diff --git a/activerecord/test/cases/adapters/postgresql/hstore_test.rb b/activerecord/test/cases/adapters/postgresql/hstore_test.rb
index c24c4b0d56..1296eb72c0 100644
--- a/activerecord/test/cases/adapters/postgresql/hstore_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/hstore_test.rb
@@ -28,7 +28,7 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase
t.hstore 'settings'
end
end
- @column = Hstore.columns.find { |c| c.name == 'tags' }
+ @column = Hstore.columns_hash['tags']
end
teardown do
@@ -56,7 +56,6 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase
assert_equal :hstore, @column.type
assert_equal "hstore", @column.sql_type
assert_not @column.number?
- assert_not @column.text?
assert_not @column.binary?
assert_not @column.array
end
@@ -64,9 +63,8 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase
def test_default
@connection.add_column 'hstores', 'permissions', :hstore, default: '"users"=>"read", "articles"=>"write"'
Hstore.reset_column_information
- column = Hstore.columns_hash["permissions"]
- assert_equal({"users"=>"read", "articles"=>"write"}, column.default)
+ assert_equal({"users"=>"read", "articles"=>"write"}, Hstore.column_defaults['permissions'])
assert_equal({"users"=>"read", "articles"=>"write"}, Hstore.new.permissions)
ensure
Hstore.reset_column_information
@@ -78,7 +76,7 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase
t.hstore 'users', default: ''
end
Hstore.reset_column_information
- column = Hstore.columns.find { |c| c.name == 'users' }
+ column = Hstore.columns_hash['users']
assert_equal :hstore, column.type
raise ActiveRecord::Rollback # reset the schema change
@@ -106,22 +104,17 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase
def test_cast_value_on_write
x = Hstore.new tags: {"bool" => true, "number" => 5}
+ assert_equal({"bool" => true, "number" => 5}, x.tags_before_type_cast)
assert_equal({"bool" => "true", "number" => "5"}, x.tags)
x.save
assert_equal({"bool" => "true", "number" => "5"}, x.reload.tags)
end
def test_type_cast_hstore
- assert @column
-
- data = "\"1\"=>\"2\""
- hash = @column.class.string_to_hstore data
- assert_equal({'1' => '2'}, hash)
- assert_equal({'1' => '2'}, @column.type_cast(data))
-
- assert_equal({}, @column.type_cast(""))
- assert_equal({'key'=>nil}, @column.type_cast('key => NULL'))
- assert_equal({'c'=>'}','"a"'=>'b "a b'}, @column.type_cast(%q(c=>"}", "\"a\""=>"b \"a b")))
+ assert_equal({'1' => '2'}, @column.type_cast_from_database("\"1\"=>\"2\""))
+ assert_equal({}, @column.type_cast_from_database(""))
+ assert_equal({'key'=>nil}, @column.type_cast_from_database('key => NULL'))
+ assert_equal({'c'=>'}','"a"'=>'b "a b'}, @column.type_cast_from_database(%q(c=>"}", "\"a\""=>"b \"a b")))
end
def test_with_store_accessors
@@ -142,48 +135,78 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase
assert_equal "GMT", x.timezone
end
+ def test_duplication_with_store_accessors
+ x = Hstore.new(language: "fr", timezone: "GMT")
+ assert_equal "fr", x.language
+ assert_equal "GMT", x.timezone
+
+ y = x.dup
+ assert_equal "fr", y.language
+ assert_equal "GMT", y.timezone
+ end
+
+ def test_yaml_round_trip_with_store_accessors
+ x = Hstore.new(language: "fr", timezone: "GMT")
+ assert_equal "fr", x.language
+ assert_equal "GMT", x.timezone
+
+ y = YAML.load(YAML.dump(x))
+ assert_equal "fr", y.language
+ assert_equal "GMT", y.timezone
+ end
+
+ def test_changes_in_place
+ hstore = Hstore.create!(settings: { 'one' => 'two' })
+ hstore.settings['three'] = 'four'
+ hstore.save!
+ hstore.reload
+
+ assert_equal 'four', hstore.settings['three']
+ assert_not hstore.changed?
+ end
+
def test_gen1
- assert_equal(%q(" "=>""), @column.class.hstore_to_string({' '=>''}))
+ assert_equal(%q(" "=>""), @column.cast_type.type_cast_for_database({' '=>''}))
end
def test_gen2
- assert_equal(%q(","=>""), @column.class.hstore_to_string({','=>''}))
+ assert_equal(%q(","=>""), @column.cast_type.type_cast_for_database({','=>''}))
end
def test_gen3
- assert_equal(%q("="=>""), @column.class.hstore_to_string({'='=>''}))
+ assert_equal(%q("="=>""), @column.cast_type.type_cast_for_database({'='=>''}))
end
def test_gen4
- assert_equal(%q(">"=>""), @column.class.hstore_to_string({'>'=>''}))
+ assert_equal(%q(">"=>""), @column.cast_type.type_cast_for_database({'>'=>''}))
end
def test_parse1
- assert_equal({'a'=>nil,'b'=>nil,'c'=>'NuLl','null'=>'c'}, @column.type_cast('a=>null,b=>NuLl,c=>"NuLl",null=>c'))
+ assert_equal({'a'=>nil,'b'=>nil,'c'=>'NuLl','null'=>'c'}, @column.type_cast_from_database('a=>null,b=>NuLl,c=>"NuLl",null=>c'))
end
def test_parse2
- assert_equal({" " => " "}, @column.type_cast("\\ =>\\ "))
+ assert_equal({" " => " "}, @column.type_cast_from_database("\\ =>\\ "))
end
def test_parse3
- assert_equal({"=" => ">"}, @column.type_cast("==>>"))
+ assert_equal({"=" => ">"}, @column.type_cast_from_database("==>>"))
end
def test_parse4
- assert_equal({"=a"=>"q=w"}, @column.type_cast('\=a=>q=w'))
+ assert_equal({"=a"=>"q=w"}, @column.type_cast_from_database('\=a=>q=w'))
end
def test_parse5
- assert_equal({"=a"=>"q=w"}, @column.type_cast('"=a"=>q\=w'))
+ assert_equal({"=a"=>"q=w"}, @column.type_cast_from_database('"=a"=>q\=w'))
end
def test_parse6
- assert_equal({"\"a"=>"q>w"}, @column.type_cast('"\"a"=>q>w'))
+ assert_equal({"\"a"=>"q>w"}, @column.type_cast_from_database('"\"a"=>q>w'))
end
def test_parse7
- assert_equal({"\"a"=>"q\"w"}, @column.type_cast('\"a=>q"w'))
+ assert_equal({"\"a"=>"q\"w"}, @column.type_cast_from_database('\"a=>q"w'))
end
def test_rewrite
@@ -265,14 +288,32 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase
assert_cycle("a\nb" => "c\nd")
end
- def test_update_all
- hstore = Hstore.create! tags: { "one" => "two" }
+ class TagCollection
+ def initialize(hash); @hash = hash end
+ def to_hash; @hash end
+ def self.load(hash); new(hash) end
+ def self.dump(object); object.to_hash end
+ end
- Hstore.update_all tags: { "three" => "four" }
- assert_equal({ "three" => "four" }, hstore.reload.tags)
+ class HstoreWithSerialize < Hstore
+ serialize :tags, TagCollection
+ end
+
+ def test_hstore_with_serialized_attributes
+ HstoreWithSerialize.create! tags: TagCollection.new({"one" => "two"})
+ record = HstoreWithSerialize.first
+ assert_instance_of TagCollection, record.tags
+ assert_equal({"one" => "two"}, record.tags.to_hash)
+ record.tags = TagCollection.new("three" => "four")
+ record.save!
+ assert_equal({"three" => "four"}, HstoreWithSerialize.first.tags.to_hash)
+ end
- Hstore.update_all tags: { }
- assert_equal({ }, hstore.reload.tags)
+ def test_clone_hstore_with_serialized_attributes
+ HstoreWithSerialize.create! tags: TagCollection.new({"one" => "two"})
+ record = HstoreWithSerialize.first
+ dupe = record.dup
+ assert_equal({"one" => "two"}, dupe.tags.to_hash)
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/infinity_test.rb b/activerecord/test/cases/adapters/postgresql/infinity_test.rb
new file mode 100644
index 0000000000..22e8873333
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/infinity_test.rb
@@ -0,0 +1,44 @@
+require "cases/helper"
+
+class PostgresqlInfinityTest < ActiveRecord::TestCase
+ class PostgresqlInfinity < ActiveRecord::Base
+ end
+
+ setup do
+ @connection = ActiveRecord::Base.connection
+ @connection.create_table(:postgresql_infinities) do |t|
+ t.float :float
+ t.datetime :datetime
+ end
+ end
+
+ teardown do
+ @connection.execute("DROP TABLE IF EXISTS postgresql_infinities")
+ end
+
+ test "type casting infinity on a float column" do
+ record = PostgresqlInfinity.create!(float: Float::INFINITY)
+ record.reload
+ assert_equal Float::INFINITY, record.float
+ end
+
+ test "update_all with infinity on a float column" do
+ record = PostgresqlInfinity.create!
+ PostgresqlInfinity.update_all(float: Float::INFINITY)
+ record.reload
+ assert_equal Float::INFINITY, record.float
+ end
+
+ test "type casting infinity on a datetime column" do
+ record = PostgresqlInfinity.create!(datetime: Float::INFINITY)
+ record.reload
+ assert_equal Float::INFINITY, record.datetime
+ end
+
+ test "update_all with infinity on a datetime column" do
+ record = PostgresqlInfinity.create!
+ PostgresqlInfinity.update_all(datetime: Float::INFINITY)
+ record.reload
+ assert_equal Float::INFINITY, record.datetime
+ end
+end
diff --git a/activerecord/test/cases/adapters/postgresql/json_test.rb b/activerecord/test/cases/adapters/postgresql/json_test.rb
index ee793ffff2..86ba849445 100644
--- a/activerecord/test/cases/adapters/postgresql/json_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/json_test.rb
@@ -4,7 +4,7 @@ require "cases/helper"
require 'active_record/base'
require 'active_record/connection_adapters/postgresql_adapter'
-class PostgresqlJSONTest < ActiveRecord::TestCase
+module PostgresqlJSONSharedTestCases
class JsonDataType < ActiveRecord::Base
self.table_name = 'json_data_type'
@@ -16,36 +16,34 @@ class PostgresqlJSONTest < ActiveRecord::TestCase
begin
@connection.transaction do
@connection.create_table('json_data_type') do |t|
- t.json 'payload', :default => {}
- t.json 'settings'
+ t.public_send column_type, 'payload', default: {} # t.json 'payload', default: {}
+ t.public_send column_type, 'settings' # t.json 'settings'
end
end
rescue ActiveRecord::StatementInvalid
skip "do not test on PG without json"
end
- @column = JsonDataType.columns.find { |c| c.name == 'payload' }
+ @column = JsonDataType.columns_hash['payload']
end
- teardown do
+ def teardown
@connection.execute 'drop table if exists json_data_type'
end
def test_column
column = JsonDataType.columns_hash["payload"]
- assert_equal :json, column.type
- assert_equal "json", column.sql_type
+ assert_equal column_type, column.type
+ assert_equal column_type.to_s, column.sql_type
assert_not column.number?
- assert_not column.text?
assert_not column.binary?
assert_not column.array
end
def test_default
- @connection.add_column 'json_data_type', 'permissions', :json, default: '{"users": "read", "posts": ["read", "write"]}'
+ @connection.add_column 'json_data_type', 'permissions', column_type, default: '{"users": "read", "posts": ["read", "write"]}'
JsonDataType.reset_column_information
- column = JsonDataType.columns_hash["permissions"]
- assert_equal({"users"=>"read", "posts"=>["read", "write"]}, column.default)
+ assert_equal({"users"=>"read", "posts"=>["read", "write"]}, JsonDataType.column_defaults['permissions'])
assert_equal({"users"=>"read", "posts"=>["read", "write"]}, JsonDataType.new.permissions)
ensure
JsonDataType.reset_column_information
@@ -54,11 +52,11 @@ class PostgresqlJSONTest < ActiveRecord::TestCase
def test_change_table_supports_json
@connection.transaction do
@connection.change_table('json_data_type') do |t|
- t.json 'users', default: '{}'
+ t.public_send column_type, 'users', default: '{}' # t.json 'users', default: '{}'
end
JsonDataType.reset_column_information
- column = JsonDataType.columns.find { |c| c.name == 'users' }
- assert_equal :json, column.type
+ column = JsonDataType.columns_hash['users']
+ assert_equal column_type, column.type
raise ActiveRecord::Rollback # reset the schema change
end
@@ -68,6 +66,7 @@ class PostgresqlJSONTest < ActiveRecord::TestCase
def test_cast_value_on_write
x = JsonDataType.new payload: {"string" => "foo", :symbol => :bar}
+ assert_equal({"string" => "foo", :symbol => :bar}, x.payload_before_type_cast)
assert_equal({"string" => "foo", "symbol" => "bar"}, x.payload)
x.save
assert_equal({"string" => "foo", "symbol" => "bar"}, x.reload.payload)
@@ -77,13 +76,13 @@ class PostgresqlJSONTest < ActiveRecord::TestCase
column = JsonDataType.columns_hash["payload"]
data = "{\"a_key\":\"a_value\"}"
- hash = column.class.string_to_json data
+ hash = column.type_cast_from_database(data)
assert_equal({'a_key' => 'a_value'}, hash)
- assert_equal({'a_key' => 'a_value'}, column.type_cast(data))
+ assert_equal({'a_key' => 'a_value'}, column.type_cast_from_database(data))
- assert_equal({}, column.type_cast("{}"))
- assert_equal({'key'=>nil}, column.type_cast('{"key": null}'))
- assert_equal({'c'=>'}','"a"'=>'b "a b'}, column.type_cast(%q({"c":"}", "\"a\"":"b \"a b"})))
+ assert_equal({}, column.type_cast_from_database("{}"))
+ assert_equal({'key'=>nil}, column.type_cast_from_database('{"key": null}'))
+ assert_equal({'c'=>'}','"a"'=>'b "a b'}, column.type_cast_from_database(%q({"c":"}", "\"a\"":"b \"a b"})))
end
def test_rewrite
@@ -139,13 +138,56 @@ class PostgresqlJSONTest < ActiveRecord::TestCase
assert_equal "640×1136", x.resolution
end
- def test_update_all
- json = JsonDataType.create! payload: { "one" => "two" }
+ def test_duplication_with_store_accessors
+ x = JsonDataType.new(resolution: "320×480")
+ assert_equal "320×480", x.resolution
+
+ y = x.dup
+ assert_equal "320×480", y.resolution
+ end
+
+ def test_yaml_round_trip_with_store_accessors
+ x = JsonDataType.new(resolution: "320×480")
+ assert_equal "320×480", x.resolution
+
+ y = YAML.load(YAML.dump(x))
+ assert_equal "320×480", y.resolution
+ end
+
+ def test_changes_in_place
+ json = JsonDataType.new
+ assert_not json.changed?
+
+ json.payload = { 'one' => 'two' }
+ assert json.changed?
+ assert json.payload_changed?
+
+ json.save!
+ assert_not json.changed?
+
+ json.payload['three'] = 'four'
+ assert json.payload_changed?
+
+ json.save!
+ json.reload
+
+ assert_equal({ 'one' => 'two', 'three' => 'four' }, json.payload)
+ assert_not json.changed?
+ end
+end
+
+class PostgresqlJSONTest < ActiveRecord::TestCase
+ include PostgresqlJSONSharedTestCases
+
+ def column_type
+ :json
+ end
+end
- JsonDataType.update_all payload: { "three" => "four" }
- assert_equal({ "three" => "four" }, json.reload.payload)
+class PostgresqlJSONBTest < ActiveRecord::TestCase
+ include PostgresqlJSONSharedTestCases
- JsonDataType.update_all payload: { }
- assert_equal({ }, json.reload.payload)
+ def column_type
+ :jsonb
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/ltree_test.rb b/activerecord/test/cases/adapters/postgresql/ltree_test.rb
index 718f37a380..889e369bd6 100644
--- a/activerecord/test/cases/adapters/postgresql/ltree_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/ltree_test.rb
@@ -1,7 +1,5 @@
# 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
@@ -33,7 +31,6 @@ class PostgresqlLtreeTest < ActiveRecord::TestCase
assert_equal :ltree, column.type
assert_equal "ltree", column.sql_type
assert_not column.number?
- assert_not column.text?
assert_not column.binary?
assert_not column.array
end
diff --git a/activerecord/test/cases/adapters/postgresql/money_test.rb b/activerecord/test/cases/adapters/postgresql/money_test.rb
new file mode 100644
index 0000000000..87183174f2
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/money_test.rb
@@ -0,0 +1,96 @@
+# encoding: utf-8
+require "cases/helper"
+require 'support/schema_dumping_helper'
+
+class PostgresqlMoneyTest < ActiveRecord::TestCase
+ include SchemaDumpingHelper
+
+ class PostgresqlMoney < ActiveRecord::Base; end
+
+ setup do
+ @connection = ActiveRecord::Base.connection
+ @connection.execute("set lc_monetary = 'C'")
+ @connection.create_table('postgresql_moneys') do |t|
+ t.column "wealth", "money"
+ t.column "depth", "money", default: "150.55"
+ end
+ end
+
+ teardown do
+ @connection.execute 'DROP TABLE IF EXISTS postgresql_moneys'
+ end
+
+ def test_column
+ column = PostgresqlMoney.columns_hash["wealth"]
+ assert_equal :money, column.type
+ assert_equal "money", column.sql_type
+ assert_equal 2, column.scale
+ assert column.number?
+ assert_not column.binary?
+ assert_not column.array
+ end
+
+ def test_default
+ assert_equal BigDecimal.new("150.55"), PostgresqlMoney.column_defaults['depth']
+ assert_equal BigDecimal.new("150.55"), PostgresqlMoney.new.depth
+ end
+
+ def test_money_values
+ @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)
+ assert_equal 567.89, first_money.wealth
+ assert_equal(-567.89, second_money.wealth)
+ end
+
+ def test_money_type_cast
+ column = PostgresqlMoney.columns_hash['wealth']
+ assert_equal(12345678.12, column.type_cast_from_user("$12,345,678.12"))
+ assert_equal(12345678.12, column.type_cast_from_user("$12.345.678,12"))
+ assert_equal(-1.15, column.type_cast_from_user("-$1.15"))
+ assert_equal(-2.25, column.type_cast_from_user("($2.25)"))
+ end
+
+ def test_schema_dumping
+ output = dump_table_schema("postgresql_moneys")
+ assert_match %r{t\.money\s+"wealth",\s+scale: 2$}, output
+ assert_match %r{t\.money\s+"depth",\s+scale: 2,\s+default: 150.55$}, output
+ end
+
+ def test_create_and_update_money
+ money = PostgresqlMoney.create(wealth: "987.65")
+ assert_equal 987.65, money.wealth
+
+ new_value = BigDecimal.new('123.45')
+ money.wealth = new_value
+ money.save!
+ money.reload
+ assert_equal new_value, money.wealth
+ end
+
+ def test_update_all_with_money_string
+ money = PostgresqlMoney.create!
+ PostgresqlMoney.update_all(wealth: "987.65")
+ money.reload
+
+ assert_equal 987.65, money.wealth
+ end
+
+ def test_update_all_with_money_big_decimal
+ money = PostgresqlMoney.create!
+ PostgresqlMoney.update_all(wealth: '123.45'.to_d)
+ money.reload
+
+ assert_equal 123.45, money.wealth
+ end
+
+ def test_update_all_with_money_numeric
+ money = PostgresqlMoney.create!
+ PostgresqlMoney.update_all(wealth: 123.45)
+ money.reload
+
+ assert_equal 123.45, money.wealth
+ end
+end
diff --git a/activerecord/test/cases/adapters/postgresql/network_test.rb b/activerecord/test/cases/adapters/postgresql/network_test.rb
new file mode 100644
index 0000000000..4f4c1103fa
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/network_test.rb
@@ -0,0 +1,71 @@
+# encoding: utf-8
+require "cases/helper"
+
+class PostgresqlNetworkTest < ActiveRecord::TestCase
+ class PostgresqlNetworkAddress < ActiveRecord::Base
+ end
+
+ def test_cidr_column
+ column = PostgresqlNetworkAddress.columns_hash["cidr_address"]
+ assert_equal :cidr, column.type
+ assert_equal "cidr", column.sql_type
+ assert_not column.number?
+ assert_not column.binary?
+ assert_not column.array
+ end
+
+ def test_inet_column
+ column = PostgresqlNetworkAddress.columns_hash["inet_address"]
+ assert_equal :inet, column.type
+ assert_equal "inet", column.sql_type
+ assert_not column.number?
+ assert_not column.binary?
+ assert_not column.array
+ end
+
+ def test_macaddr_column
+ column = PostgresqlNetworkAddress.columns_hash["mac_address"]
+ assert_equal :macaddr, column.type
+ assert_equal "macaddr", column.sql_type
+ assert_not column.number?
+ assert_not column.binary?
+ assert_not column.array
+ end
+
+ def test_network_types
+ PostgresqlNetworkAddress.create(cidr_address: '192.168.0.0/24',
+ inet_address: '172.16.1.254/32',
+ mac_address: '01:23:45:67:89:0a')
+
+ address = PostgresqlNetworkAddress.first
+ assert_equal IPAddr.new('192.168.0.0/24'), address.cidr_address
+ assert_equal IPAddr.new('172.16.1.254'), address.inet_address
+ assert_equal '01:23:45:67:89:0a', address.mac_address
+
+ address.cidr_address = '10.1.2.3/32'
+ address.inet_address = '10.0.0.0/8'
+ address.mac_address = 'bc:de:f0:12:34:56'
+
+ address.save!
+ assert address.reload
+ assert_equal IPAddr.new('10.1.2.3/32'), address.cidr_address
+ assert_equal IPAddr.new('10.0.0.0/8'), address.inet_address
+ assert_equal 'bc:de:f0:12:34:56', address.mac_address
+ end
+
+ def test_invalid_network_address
+ invalid_address = PostgresqlNetworkAddress.new(cidr_address: 'invalid addr',
+ inet_address: 'invalid addr')
+ assert_nil invalid_address.cidr_address
+ assert_nil invalid_address.inet_address
+ assert_equal 'invalid addr', invalid_address.cidr_address_before_type_cast
+ assert_equal 'invalid addr', invalid_address.inet_address_before_type_cast
+ assert invalid_address.save
+
+ invalid_address.reload
+ assert_nil invalid_address.cidr_address
+ assert_nil invalid_address.inet_address
+ assert_nil invalid_address.cidr_address_before_type_cast
+ assert_nil invalid_address.inet_address_before_type_cast
+ 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 b7791078db..cfff1f980b 100644
--- a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb
@@ -134,18 +134,18 @@ module ActiveRecord
end
def test_default_sequence_name
- assert_equal 'accounts_id_seq',
+ assert_equal PostgreSQL::Name.new('public', 'accounts_id_seq'),
@connection.default_sequence_name('accounts', 'id')
- assert_equal 'accounts_id_seq',
+ assert_equal PostgreSQL::Name.new('public', 'accounts_id_seq'),
@connection.default_sequence_name('accounts')
end
def test_default_sequence_name_bad_table
- assert_equal 'zomg_id_seq',
+ assert_equal PostgreSQL::Name.new(nil, 'zomg_id_seq'),
@connection.default_sequence_name('zomg', 'id')
- assert_equal 'zomg_id_seq',
+ assert_equal PostgreSQL::Name.new(nil, 'zomg_id_seq'),
@connection.default_sequence_name('zomg')
end
@@ -216,7 +216,7 @@ module ActiveRecord
)
seq = @connection.pk_and_sequence_for('ex').last
- assert_equal 'ex_id_seq', seq
+ assert_equal PostgreSQL::Name.new("public", "ex_id_seq"), seq
@connection.exec_query(
"DELETE FROM pg_depend WHERE objid = 'ex2_id_seq'::regclass AND refobjid = 'ex'::regclass AND deptype = 'a'"
@@ -353,6 +353,17 @@ module ActiveRecord
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_columns_for_distinct_without_order_specifiers
+ assert_equal "posts.title, posts.updater_id AS alias_0",
+ @connection.columns_for_distinct("posts.title", ["posts.updater_id"])
+
+ assert_equal "posts.title, posts.updater_id AS alias_0",
+ @connection.columns_for_distinct("posts.title", ["posts.updater_id nulls last"])
+
+ assert_equal "posts.title, posts.updater_id AS alias_0",
+ @connection.columns_for_distinct("posts.title", ["posts.updater_id nulls first"])
+ end
+
def test_raise_error_when_cannot_translate_exception
assert_raise TypeError do
@connection.send(:log, nil) { @connection.execute(nil) }
@@ -396,6 +407,23 @@ module ActiveRecord
reset_connection
end
+ def test_unparsed_defaults_are_at_least_set_when_saving
+ with_example_table "id SERIAL PRIMARY KEY, number INTEGER NOT NULL DEFAULT (4 + 4) * 2 / 4" do
+ number_klass = Class.new(ActiveRecord::Base) do
+ self.table_name = 'ex'
+ end
+ column = number_klass.columns_hash["number"]
+ assert_nil column.default
+ assert_nil column.default_function
+
+ first_number = number_klass.new
+ assert_nil first_number.number
+
+ first_number.save!
+ assert_equal 4, first_number.reload.number
+ end
+ end
+
private
def insert(ctx, data)
binds = data.map { |name, value|
diff --git a/activerecord/test/cases/adapters/postgresql/quoting_test.rb b/activerecord/test/cases/adapters/postgresql/quoting_test.rb
index 51846e22d9..11d5173d37 100644
--- a/activerecord/test/cases/adapters/postgresql/quoting_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/quoting_test.rb
@@ -10,13 +10,13 @@ module ActiveRecord
end
def test_type_cast_true
- c = PostgreSQLColumn.new(nil, 1, OID::Boolean.new, 'boolean')
+ c = PostgreSQLColumn.new(nil, 1, Type::Boolean.new, 'boolean')
assert_equal 't', @conn.type_cast(true, nil)
assert_equal 't', @conn.type_cast(true, c)
end
def test_type_cast_false
- c = PostgreSQLColumn.new(nil, 1, OID::Boolean.new, 'boolean')
+ c = PostgreSQLColumn.new(nil, 1, Type::Boolean.new, 'boolean')
assert_equal 'f', @conn.type_cast(false, nil)
assert_equal 'f', @conn.type_cast(false, c)
end
@@ -47,9 +47,9 @@ module ActiveRecord
def test_quote_cast_numeric
fixnum = 666
- c = PostgreSQLColumn.new(nil, nil, OID::String.new, 'varchar')
+ c = PostgreSQLColumn.new(nil, nil, Type::String.new, 'varchar')
assert_equal "'666'", @conn.quote(fixnum, c)
- c = PostgreSQLColumn.new(nil, nil, OID::Text.new, 'text')
+ c = PostgreSQLColumn.new(nil, nil, Type::Text.new, 'text')
assert_equal "'666'", @conn.quote(fixnum, c)
end
@@ -57,6 +57,17 @@ module ActiveRecord
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
+
+ def test_quote_range
+ range = "1,2]'; SELECT * FROM users; --".."a"
+ c = PostgreSQLColumn.new(nil, nil, OID::Range.new(Type::Integer.new, :int8range))
+ assert_equal "'[1,0]'", @conn.quote(range, c)
+ end
+
+ def test_quote_bit_string
+ c = PostgreSQLColumn.new(nil, 1, OID::Bit.new)
+ assert_equal nil, @conn.quote("'); SELECT * FROM users; /*\n01\n*/--", c)
+ end
end
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/range_test.rb b/activerecord/test/cases/adapters/postgresql/range_test.rb
index 060b17d071..d812cd01c4 100644
--- a/activerecord/test/cases/adapters/postgresql/range_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/range_test.rb
@@ -1,7 +1,5 @@
require "cases/helper"
require 'support/connection_helper'
-require 'active_record/base'
-require 'active_record/connection_adapters/postgresql_adapter'
if ActiveRecord::Base.connection.supports_ranges?
class PostgresqlRange < ActiveRecord::Base
@@ -158,7 +156,7 @@ _SQL
assert_equal 0.5..0.7, @first_range.float_range
assert_equal 0.5...0.7, @second_range.float_range
assert_equal 0.5...Float::INFINITY, @third_range.float_range
- assert_equal (-Float::INFINITY...Float::INFINITY), @fourth_range.float_range
+ assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.float_range)
assert_nil @empty_range.float_range
end
@@ -264,6 +262,23 @@ _SQL
assert_raises(ArgumentError) { PostgresqlRange.create!(float_range: "(0.5, 0.7]") }
end
+ def test_update_all_with_ranges
+ PostgresqlRange.create!
+
+ PostgresqlRange.update_all(int8_range: 1..100)
+
+ assert_equal 1...101, PostgresqlRange.first.int8_range
+ end
+
+ def test_ranges_correctly_escape_input
+ range = "-1,2]'; DROP TABLE postgresql_ranges; --".."a"
+ PostgresqlRange.update_all(int8_range: range)
+
+ assert_nothing_raised do
+ PostgresqlRange.first
+ end
+ end
+
private
def assert_equal_round_trip(range, attribute, value)
round_trip(range, attribute, value)
diff --git a/activerecord/test/cases/adapters/postgresql/schema_test.rb b/activerecord/test/cases/adapters/postgresql/schema_test.rb
index 11ec7599a3..9e5fd17dc4 100644
--- a/activerecord/test/cases/adapters/postgresql/schema_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/schema_test.rb
@@ -50,6 +50,16 @@ class SchemaTest < ActiveRecord::TestCase
self.table_name = 'things'
end
+ class Song < ActiveRecord::Base
+ self.table_name = "music.songs"
+ has_and_belongs_to_many :albums
+ end
+
+ class Album < ActiveRecord::Base
+ self.table_name = "music.albums"
+ has_and_belongs_to_many :songs
+ end
+
def setup
@connection = ActiveRecord::Base.connection
@connection.execute "CREATE SCHEMA #{SCHEMA_NAME} CREATE TABLE #{TABLE_NAME} (#{COLUMNS.join(',')})"
@@ -109,6 +119,22 @@ class SchemaTest < ActiveRecord::TestCase
assert !@connection.schema_names.include?("test_schema3")
end
+ def test_habtm_table_name_with_schema
+ ActiveRecord::Base.connection.execute <<-SQL
+ DROP SCHEMA IF EXISTS music CASCADE;
+ CREATE SCHEMA music;
+ CREATE TABLE music.albums (id serial primary key);
+ CREATE TABLE music.songs (id serial primary key);
+ CREATE TABLE music.albums_songs (album_id integer, song_id integer);
+ SQL
+
+ song = Song.create
+ Album.create
+ assert_equal song, Song.includes(:albums).references(:albums).first
+ ensure
+ ActiveRecord::Base.connection.execute "DROP SCHEMA music CASCADE;"
+ end
+
def test_raise_drop_schema_with_nonexisting_schema
assert_raises(ActiveRecord::StatementInvalid) do
@connection.drop_schema "test_schema3"
@@ -305,14 +331,15 @@ class SchemaTest < ActiveRecord::TestCase
end
def test_pk_and_sequence_for_with_schema_specified
+ pg_name = ActiveRecord::ConnectionAdapters::PostgreSQL::Name
[
%("#{SCHEMA_NAME}"."#{PK_TABLE_NAME}"),
%("#{SCHEMA_NAME}"."#{UNMATCHED_PK_TABLE_NAME}")
].each do |given|
pk, seq = @connection.pk_and_sequence_for(given)
assert_equal 'id', pk, "primary key should be found when table referenced as #{given}"
- assert_equal "#{PK_TABLE_NAME}_id_seq", seq, "sequence name should be found when table referenced as #{given}" if given == %("#{SCHEMA_NAME}"."#{PK_TABLE_NAME}")
- assert_equal "#{UNMATCHED_SEQUENCE_NAME}", seq, "sequence name should be found when table referenced as #{given}" if given == %("#{SCHEMA_NAME}"."#{UNMATCHED_PK_TABLE_NAME}")
+ assert_equal pg_name.new(SCHEMA_NAME, "#{PK_TABLE_NAME}_id_seq"), seq, "sequence name should be found when table referenced as #{given}" if given == %("#{SCHEMA_NAME}"."#{PK_TABLE_NAME}")
+ assert_equal pg_name.new(SCHEMA_NAME, UNMATCHED_SEQUENCE_NAME), seq, "sequence name should be found when table referenced as #{given}" if given == %("#{SCHEMA_NAME}"."#{UNMATCHED_PK_TABLE_NAME}")
end
end
@@ -352,6 +379,14 @@ class SchemaTest < ActiveRecord::TestCase
end
end
+ def test_reset_pk_sequence
+ sequence_name = "#{SCHEMA_NAME}.#{UNMATCHED_SEQUENCE_NAME}"
+ @connection.execute "SELECT setval('#{sequence_name}', 123)"
+ assert_equal "124", @connection.select_value("SELECT nextval('#{sequence_name}')")
+ @connection.reset_pk_sequence!("#{SCHEMA_NAME}.#{UNMATCHED_PK_TABLE_NAME}")
+ assert_equal "1", @connection.select_value("SELECT nextval('#{sequence_name}')")
+ end
+
private
def columns(table_name)
@connection.send(:column_definitions, table_name).map do |name, type, default|
diff --git a/activerecord/test/cases/adapters/postgresql/timestamp_test.rb b/activerecord/test/cases/adapters/postgresql/timestamp_test.rb
index 4d29a20e66..3614b29190 100644
--- a/activerecord/test/cases/adapters/postgresql/timestamp_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/timestamp_test.rb
@@ -2,6 +2,47 @@ require 'cases/helper'
require 'models/developer'
require 'models/topic'
+class PostgresqlTimestampTest < ActiveRecord::TestCase
+ class PostgresqlTimestampWithZone < ActiveRecord::Base; end
+
+ self.use_transactional_fixtures = false
+
+ setup do
+ @connection = ActiveRecord::Base.connection
+ @connection.execute("INSERT INTO postgresql_timestamp_with_zones (id, time) VALUES (1, '2010-01-01 10:00:00-1')")
+ end
+
+ teardown do
+ PostgresqlTimestampWithZone.delete_all
+ end
+
+ def test_timestamp_with_zone_values_with_rails_time_zone_support
+ with_timezone_config default: :utc, aware_attributes: true do
+ @connection.reconnect!
+
+ timestamp = PostgresqlTimestampWithZone.find(1)
+ assert_equal Time.utc(2010,1,1, 11,0,0), timestamp.time
+ assert_instance_of Time, timestamp.time
+ end
+ ensure
+ @connection.reconnect!
+ end
+
+ def test_timestamp_with_zone_values_without_rails_time_zone_support
+ 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')
+
+ timestamp = PostgresqlTimestampWithZone.find(1)
+ assert_equal Time.utc(2010,1,1, 11,0,0), timestamp.time
+ assert_instance_of Time, timestamp.time
+ end
+ ensure
+ @connection.reconnect!
+ end
+end
+
class TimestampTest < ActiveRecord::TestCase
fixtures :topics
@@ -82,20 +123,32 @@ class TimestampTest < ActiveRecord::TestCase
assert_equal date, Developer.find_by_name("aaron").updated_at
end
+ def test_bc_timestamp_leap_year
+ date = Time.utc(-4, 2, 29)
+ Developer.create!(:name => "taihou", :updated_at => date)
+ assert_equal date, Developer.find_by_name("taihou").updated_at
+ end
+
+ def test_bc_timestamp_year_zero
+ date = Time.utc(0, 4, 7)
+ Developer.create!(:name => "yahagi", :updated_at => date)
+ assert_equal date, Developer.find_by_name("yahagi").updated_at
+ end
+
private
- def pg_datetime_precision(table_name, column_name)
- results = ActiveRecord::Base.connection.execute("SELECT column_name, datetime_precision FROM information_schema.columns WHERE table_name ='#{table_name}'")
- result = results.find do |result_hash|
- result_hash["column_name"] == column_name
- end
- result && result["datetime_precision"]
+ def pg_datetime_precision(table_name, column_name)
+ results = ActiveRecord::Base.connection.execute("SELECT column_name, datetime_precision FROM information_schema.columns WHERE table_name ='#{table_name}'")
+ result = results.find do |result_hash|
+ result_hash["column_name"] == column_name
end
+ result && result["datetime_precision"]
+ end
- def activerecord_column_option(tablename, column_name, option)
- result = ActiveRecord::Base.connection.columns(tablename).find do |column|
- column.name == column_name
- end
- result && result.send(option)
+ def activerecord_column_option(tablename, column_name, option)
+ result = ActiveRecord::Base.connection.columns(tablename).find do |column|
+ column.name == column_name
end
+ result && result.send(option)
+ end
end
diff --git a/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb b/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb
new file mode 100644
index 0000000000..23817198b1
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb
@@ -0,0 +1,15 @@
+require 'cases/helper'
+
+class PostgresqlTypeLookupTest < ActiveRecord::TestCase
+ setup do
+ @connection = ActiveRecord::Base.connection
+ end
+
+ test "array delimiters are looked up correctly" do
+ box_array = @connection.type_map.lookup(1020)
+ int_array = @connection.type_map.lookup(1007)
+
+ assert_equal ';', box_array.delimiter
+ assert_equal ',', int_array.delimiter
+ end
+end
diff --git a/activerecord/test/cases/adapters/postgresql/utils_test.rb b/activerecord/test/cases/adapters/postgresql/utils_test.rb
index 9e7b08ef34..3fdb6888d9 100644
--- a/activerecord/test/cases/adapters/postgresql/utils_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/utils_test.rb
@@ -1,9 +1,10 @@
require 'cases/helper'
class PostgreSQLUtilsTest < ActiveSupport::TestCase
- include ActiveRecord::ConnectionAdapters::PostgreSQLAdapter::Utils
+ Name = ActiveRecord::ConnectionAdapters::PostgreSQL::Name
+ include ActiveRecord::ConnectionAdapters::PostgreSQL::Utils
- def test_extract_schema_and_table
+ def test_extract_schema_qualified_name
{
%(table_name) => [nil,'table_name'],
%("table.name") => [nil,'table.name'],
@@ -14,7 +15,47 @@ class PostgreSQLUtilsTest < ActiveSupport::TestCase
%("even spaces".table) => ['even spaces','table'],
%(schema."table.name") => ['schema', 'table.name']
}.each do |given, expect|
- assert_equal expect, extract_schema_and_table(given)
+ assert_equal Name.new(*expect), extract_schema_qualified_name(given)
end
end
end
+
+class PostgreSQLNameTest < ActiveSupport::TestCase
+ Name = ActiveRecord::ConnectionAdapters::PostgreSQL::Name
+
+ test "represents itself as schema.name" do
+ obj = Name.new("public", "articles")
+ assert_equal "public.articles", obj.to_s
+ end
+
+ test "without schema, represents itself as name only" do
+ obj = Name.new(nil, "articles")
+ assert_equal "articles", obj.to_s
+ end
+
+ test "quoted returns a string representation usable in a query" do
+ assert_equal %("articles"), Name.new(nil, "articles").quoted
+ assert_equal %("public"."articles"), Name.new("public", "articles").quoted
+ end
+
+ test "prevents double quoting" do
+ name = Name.new('"quoted_schema"', '"quoted_table"')
+ assert_equal "quoted_schema.quoted_table", name.to_s
+ assert_equal %("quoted_schema"."quoted_table"), name.quoted
+ end
+
+ test "equality based on state" do
+ assert_equal Name.new("access", "users"), Name.new("access", "users")
+ assert_equal Name.new(nil, "users"), Name.new(nil, "users")
+ assert_not_equal Name.new(nil, "users"), Name.new("access", "users")
+ assert_not_equal Name.new("access", "users"), Name.new("public", "users")
+ assert_not_equal Name.new("public", "users"), Name.new("public", "articles")
+ end
+
+ test "can be used as hash key" do
+ hash = {Name.new("schema", "article_seq") => "success"}
+ assert_equal "success", hash[Name.new("schema", "article_seq")]
+ assert_equal nil, hash[Name.new("schema", "articles")]
+ assert_equal nil, hash[Name.new("public", "article_seq")]
+ end
+end
diff --git a/activerecord/test/cases/adapters/postgresql/uuid_test.rb b/activerecord/test/cases/adapters/postgresql/uuid_test.rb
index bdf8e15e3e..66006d718f 100644
--- a/activerecord/test/cases/adapters/postgresql/uuid_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/uuid_test.rb
@@ -9,15 +9,6 @@ module PostgresqlUUIDHelper
@connection ||= ActiveRecord::Base.connection
end
- def enable_uuid_ossp
- unless connection.extension_enabled?('uuid-ossp')
- connection.enable_extension 'uuid-ossp'
- connection.commit_db_transaction
- end
-
- connection.reconnect!
- end
-
def drop_table(name)
connection.execute "drop table if exists #{name}"
end
@@ -43,14 +34,16 @@ class PostgresqlUUIDTest < ActiveRecord::TestCase
def test_change_column_default
@connection.add_column :uuid_data_type, :thingy, :uuid, null: false, default: "uuid_generate_v1()"
UUIDType.reset_column_information
- column = UUIDType.columns.find { |c| c.name == 'thingy' }
+ column = UUIDType.columns_hash['thingy']
assert_equal "uuid_generate_v1()", column.default_function
-
+
@connection.change_column :uuid_data_type, :thingy, :uuid, null: false, default: "uuid_generate_v4()"
-
+
UUIDType.reset_column_information
- column = UUIDType.columns.find { |c| c.name == 'thingy' }
+ column = UUIDType.columns_hash['thingy']
assert_equal "uuid_generate_v4()", column.default_function
+ ensure
+ UUIDType.reset_column_information
end
def test_data_type_of_uuid_types
@@ -58,7 +51,6 @@ class PostgresqlUUIDTest < ActiveRecord::TestCase
assert_equal :uuid, column.type
assert_equal "uuid", column.sql_type
assert_not column.number?
- assert_not column.text?
assert_not column.binary?
assert_not column.array
end
@@ -68,6 +60,43 @@ class PostgresqlUUIDTest < ActiveRecord::TestCase
assert_equal(nil, UUIDType.last.guid)
end
+ def test_treat_invalid_uuid_as_nil
+ uuid = UUIDType.create! guid: 'foobar'
+ assert_equal(nil, uuid.guid)
+ end
+
+ def test_invalid_uuid_dont_modify_before_type_cast
+ uuid = UUIDType.new guid: 'foobar'
+ assert_equal 'foobar', uuid.guid_before_type_cast
+ end
+
+ def test_rfc_4122_regex
+ # Valid uuids
+ ['A0EEBC99-9C0B-4EF8-BB6D-6BB9BD380A11',
+ '{a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11}',
+ 'a0eebc999c0b4ef8bb6d6bb9bd380a11',
+ 'a0ee-bc99-9c0b-4ef8-bb6d-6bb9-bd38-0a11',
+ '{a0eebc99-9c0b4ef8-bb6d6bb9-bd380a11}'].each do |valid_uuid|
+ uuid = UUIDType.new guid: valid_uuid
+ assert_not_nil uuid.guid
+ end
+
+ # Invalid uuids
+ [['A0EEBC99-9C0B-4EF8-BB6D-6BB9BD380A11'],
+ Hash.new,
+ 0,
+ 0.0,
+ true,
+ 'Z0000C99-9C0B-4EF8-BB6D-6BB9BD380A11',
+ '{a0eebc99-9c0b-4ef8-fb6d-6bb9bd380a11}',
+ 'a0eebc999r0b4ef8ab6d6bb9bd380a11',
+ 'a0ee-bc99------4ef8-bb6d-6bb9-bd38-0a11',
+ '{a0eebc99-bb6d6bb9-bd380a11}'].each do |invalid_uuid|
+ uuid = UUIDType.new guid: invalid_uuid
+ assert_nil uuid.guid
+ end
+ end
+
def test_uuid_formats
["A0EEBC99-9C0B-4EF8-BB6D-6BB9BD380A11",
"{a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11}",
@@ -89,16 +118,32 @@ class PostgresqlUUIDGenerationTest < ActiveRecord::TestCase
end
setup do
- enable_uuid_ossp
+ enable_uuid_ossp!(connection)
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
+
+ # Create custom PostgreSQL function to generate UUIDs
+ # to test dumping tables which columns have defaults with custom functions
+ connection.execute <<-SQL
+ CREATE OR REPLACE FUNCTION my_uuid_generator() RETURNS uuid
+ AS $$ SELECT * FROM uuid_generate_v4() $$
+ LANGUAGE SQL VOLATILE;
+ SQL
+
+ # Create such a table with custom function as default value generator
+ connection.create_table('pg_uuids_2', id: :uuid, default: 'my_uuid_generator()') do |t|
+ t.string 'name'
+ t.uuid 'other_uuid_2', default: 'my_uuid_generator()'
+ end
end
teardown do
drop_table "pg_uuids"
+ drop_table 'pg_uuids_2'
+ connection.execute 'DROP FUNCTION IF EXISTS my_uuid_generator();'
end
if ActiveRecord::Base.connection.supports_extensions?
@@ -130,6 +175,13 @@ class PostgresqlUUIDGenerationTest < ActiveRecord::TestCase
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
+
+ def test_schema_dumper_for_uuid_primary_key_with_custom_default
+ schema = StringIO.new
+ ActiveRecord::SchemaDumper.dump(connection, schema)
+ assert_match(/\bcreate_table "pg_uuids_2", id: :uuid, default: "my_uuid_generator\(\)"/, schema.string)
+ assert_match(/t\.uuid "other_uuid_2", default: "my_uuid_generator\(\)"/, schema.string)
+ end
end
end
@@ -137,7 +189,7 @@ class PostgresqlUUIDTestNilDefault < ActiveRecord::TestCase
include PostgresqlUUIDHelper
setup do
- enable_uuid_ossp
+ enable_uuid_ossp!(connection)
connection.create_table('pg_uuids', id: false) do |t|
t.primary_key :id, :uuid, default: nil
@@ -174,14 +226,14 @@ class PostgresqlUUIDTestInverseOf < ActiveRecord::TestCase
end
setup do
- enable_uuid_ossp
+ enable_uuid_ossp!(connection)
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.references :uuid_post, type: :uuid
t.string 'content'
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/xml_test.rb b/activerecord/test/cases/adapters/postgresql/xml_test.rb
index ae299697b1..4165dd5ac9 100644
--- a/activerecord/test/cases/adapters/postgresql/xml_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/xml_test.rb
@@ -1,8 +1,5 @@
# 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
@@ -14,13 +11,13 @@ class PostgresqlXMLTest < ActiveRecord::TestCase
begin
@connection.transaction do
@connection.create_table('xml_data_type') do |t|
- t.xml 'payload', default: {}
+ t.xml 'payload'
end
end
rescue ActiveRecord::StatementInvalid
skip "do not test on PG without xml"
end
- @column = XmlDataType.columns.find { |c| c.name == 'payload' }
+ @column = XmlDataType.columns_hash['payload']
end
teardown do
@@ -35,4 +32,17 @@ class PostgresqlXMLTest < ActiveRecord::TestCase
@connection.execute %q|insert into xml_data_type (payload) VALUES(null)|
assert_nil XmlDataType.first.payload
end
+
+ def test_round_trip
+ data = XmlDataType.new(payload: "<foo>bar</foo>")
+ assert_equal "<foo>bar</foo>", data.payload
+ data.save!
+ assert_equal "<foo>bar</foo>", data.reload.payload
+ end
+
+ def test_update_all
+ data = XmlDataType.create!
+ XmlDataType.update_all(payload: "<bar>baz</bar>")
+ assert_equal "<bar>baz</bar>", data.reload.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 b478db749d..13b754d226 100644
--- a/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb
+++ b/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb
@@ -60,7 +60,6 @@ class CopyTableTest < ActiveRecord::TestCase
assert_equal original_id.type, copied_id.type
assert_equal original_id.sql_type, copied_id.sql_type
assert_equal original_id.limit, copied_id.limit
- assert_equal original_id.primary, copied_id.primary
end
end
diff --git a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb
index ba89487838..ac8332e2fa 100644
--- a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb
+++ b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb
@@ -15,7 +15,7 @@ module ActiveRecord
def test_type_cast_binary_encoding_without_logger
@conn.extend(Module.new { def logger; end })
- column = Struct.new(:type, :name).new(:string, "foo")
+ column = Column.new(nil, nil, Type::String.new)
binary = SecureRandom.hex
expected = binary.dup.encode!(Encoding::UTF_8)
assert_equal expected, @conn.type_cast(binary, column)
@@ -47,13 +47,13 @@ module ActiveRecord
end
def test_type_cast_true
- c = Column.new(nil, 1, 'int')
+ c = Column.new(nil, 1, Type::Integer.new)
assert_equal 't', @conn.type_cast(true, nil)
assert_equal 1, @conn.type_cast(true, c)
end
def test_type_cast_false
- c = Column.new(nil, 1, 'int')
+ c = Column.new(nil, 1, Type::Integer.new)
assert_equal 'f', @conn.type_cast(false, nil)
assert_equal 0, @conn.type_cast(false, c)
end
@@ -61,16 +61,16 @@ module ActiveRecord
def test_type_cast_string
assert_equal '10', @conn.type_cast('10', nil)
- c = Column.new(nil, 1, 'int')
+ c = Column.new(nil, 1, Type::Integer.new)
assert_equal 10, @conn.type_cast('10', c)
- c = Column.new(nil, 1, 'float')
+ c = Column.new(nil, 1, Type::Float.new)
assert_equal 10.1, @conn.type_cast('10.1', c)
- c = Column.new(nil, 1, 'binary')
+ c = Column.new(nil, 1, Type::Binary.new)
assert_equal '10.1', @conn.type_cast('10.1', c)
- c = Column.new(nil, 1, 'date')
+ c = Column.new(nil, 1, Type::Date.new)
assert_equal '10.1', @conn.type_cast('10.1', c)
end
@@ -84,7 +84,7 @@ module ActiveRecord
assert_raise(TypeError) { @conn.type_cast(obj, nil) }
end
- def test_quoted_id
+ def test_type_cast_object_which_responds_to_quoted_id
quoted_id_obj = Class.new {
def quoted_id
"'zomg'"
@@ -100,9 +100,16 @@ module ActiveRecord
def quoted_id
"'zomg'"
end
- }
+ }.new
assert_raise(TypeError) { @conn.type_cast(quoted_id_obj, nil) }
end
+
+ def test_quoting_binary_strings
+ value = "hello".encode('ascii-8bit')
+ column = Column.new(nil, 1, SQLite3String.new)
+
+ assert_equal "'hello'", @conn.quote(value, column)
+ end
end
end
end
diff --git a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb
index 2630a0f3a4..b2bf9480dd 100644
--- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb
+++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb
@@ -57,10 +57,11 @@ module ActiveRecord
end
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.
+ # sqlite3 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
@@ -339,7 +340,7 @@ module ActiveRecord
column = @conn.columns('ex').find { |x|
x.name == 'number'
}
- assert_equal 10, column.default
+ assert_equal '10', column.default
end
end
@@ -416,6 +417,21 @@ module ActiveRecord
assert @conn.respond_to?(:disable_extension)
end
+ def test_statement_closed
+ db = SQLite3::Database.new(ActiveRecord::Base.
+ configurations['arunit']['database'])
+ statement = SQLite3::Statement.new(db,
+ 'CREATE TABLE statement_test (number integer not null)')
+ statement.stubs(:step).raises(SQLite3::BusyException, 'busy')
+ statement.stubs(:columns).once.returns([])
+ statement.expects(:close).once
+ SQLite3::Statement.stubs(:new).returns(statement)
+
+ assert_raises ActiveRecord::StatementInvalid do
+ @conn.exec_query 'select * from statement_test'
+ end
+ end
+
private
def assert_logged logs
diff --git a/activerecord/test/cases/ar_schema_test.rb b/activerecord/test/cases/ar_schema_test.rb
index 811695938e..8700b20dee 100644
--- a/activerecord/test/cases/ar_schema_test.rb
+++ b/activerecord/test/cases/ar_schema_test.rb
@@ -17,6 +17,20 @@ if ActiveRecord::Base.connection.supports_migrations?
ActiveRecord::SchemaMigration.delete_all rescue nil
end
+ def test_has_no_primary_key
+ old_primary_key_prefix_type = ActiveRecord::Base.primary_key_prefix_type
+ ActiveRecord::Base.primary_key_prefix_type = :table_name_with_underscore
+ assert_nil ActiveRecord::SchemaMigration.primary_key
+
+ ActiveRecord::SchemaMigration.create_table
+ assert_difference "ActiveRecord::SchemaMigration.count", 1 do
+ ActiveRecord::SchemaMigration.create version: 12
+ end
+ ensure
+ ActiveRecord::SchemaMigration.drop_table
+ ActiveRecord::Base.primary_key_prefix_type = old_primary_key_prefix_type
+ end
+
def test_schema_define
ActiveRecord::Schema.define(:version => 7) do
create_table :fruits do |t|
@@ -34,6 +48,7 @@ if ActiveRecord::Base.connection.supports_migrations?
def test_schema_define_w_table_name_prefix
table_name = ActiveRecord::SchemaMigration.table_name
+ old_table_name_prefix = ActiveRecord::Base.table_name_prefix
ActiveRecord::Base.table_name_prefix = "nep_"
ActiveRecord::SchemaMigration.table_name = "nep_#{table_name}"
ActiveRecord::Schema.define(:version => 7) do
@@ -46,7 +61,7 @@ if ActiveRecord::Base.connection.supports_migrations?
end
assert_equal 7, ActiveRecord::Migrator::current_version
ensure
- ActiveRecord::Base.table_name_prefix = ""
+ ActiveRecord::Base.table_name_prefix = old_table_name_prefix
ActiveRecord::SchemaMigration.table_name = table_name
end
@@ -66,5 +81,12 @@ if ActiveRecord::Base.connection.supports_migrations?
end
assert_nothing_raised { @connection.select_all "SELECT * FROM fruits" }
end
+
+ def test_normalize_version
+ assert_equal "118", ActiveRecord::SchemaMigration.normalize_migration_number("0000118")
+ assert_equal "002", ActiveRecord::SchemaMigration.normalize_migration_number("2")
+ assert_equal "017", ActiveRecord::SchemaMigration.normalize_migration_number("0017")
+ assert_equal "20131219224947", ActiveRecord::SchemaMigration.normalize_migration_number("20131219224947")
+ 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 3b484a0d64..25555bd75c 100644
--- a/activerecord/test/cases/associations/belongs_to_associations_test.rb
+++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb
@@ -369,6 +369,13 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
assert_queries(2) { line_item.update amount: 10 }
end
+ def test_belongs_to_with_touch_option_on_empty_update
+ line_item = LineItem.create!
+ Invoice.create!(line_items: [line_item])
+
+ assert_queries(0) { line_item.save }
+ end
+
def test_belongs_to_with_touch_option_on_destroy
line_item = LineItem.create!
Invoice.create!(line_items: [line_item])
@@ -563,6 +570,19 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
assert companies(:first_client).readonly_firm.readonly?
end
+ def test_test_polymorphic_assignment_foreign_key_type_string
+ comment = Comment.first
+ comment.author = Author.first
+ comment.resource = Member.first
+ comment.save
+
+ assert_equal Comment.all.to_a,
+ Comment.includes(:author).to_a
+
+ assert_equal Comment.all.to_a,
+ Comment.includes(:resource).to_a
+ end
+
def test_polymorphic_assignment_foreign_type_field_updating
# should update when assigning a saved record
sponsor = Sponsor.new
@@ -767,8 +787,8 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
post = posts(:welcome)
comment = comments(:greetings)
- assert_difference lambda { post.reload.taggings_count }, -1 do
- assert_difference 'comment.reload.taggings_count', +1 do
+ assert_difference lambda { post.reload.tags_count }, -1 do
+ assert_difference 'comment.reload.tags_count', +1 do
tagging.taggable = comment
end
end
@@ -915,3 +935,14 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
assert_equal 1, Column.count
end
end
+
+class BelongsToWithForeignKeyTest < ActiveRecord::TestCase
+ fixtures :authors, :author_addresses
+
+ def test_destroy_linked_models
+ address = AuthorAddress.create!
+ author = Author.create! name: "Author", author_address_id: address.id
+
+ author.destroy!
+ end
+end
diff --git a/activerecord/test/cases/associations/callbacks_test.rb b/activerecord/test/cases/associations/callbacks_test.rb
index 968f36e92c..5b7e462f64 100644
--- a/activerecord/test/cases/associations/callbacks_test.rb
+++ b/activerecord/test/cases/associations/callbacks_test.rb
@@ -159,7 +159,7 @@ class AssociationCallbacksTest < ActiveRecord::TestCase
activerecord.reload
assert activerecord.developers_with_callbacks.size == 2
end
- log_array = activerecord.developers_with_callbacks.flat_map {|d| ["before_removing#{d.id}","after_removing#{d.id}"]}.sort
+ activerecord.developers_with_callbacks.flat_map {|d| ["before_removing#{d.id}","after_removing#{d.id}"]}.sort
assert activerecord.developers_with_callbacks.clear
assert_predicate activerecord.developers_log, :empty?
end
diff --git a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb
index 71c0609df5..51d8e0523e 100644
--- a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb
+++ b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb
@@ -35,9 +35,9 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase
def test_eager_association_loading_with_hmt_does_not_table_name_collide_when_joining_associations
assert_nothing_raised do
- Author.joins(:posts).eager_load(:comments).where(:posts => {:taggings_count => 1}).to_a
+ Author.joins(:posts).eager_load(:comments).where(:posts => {:tags_count => 1}).to_a
end
- authors = Author.joins(:posts).eager_load(:comments).where(:posts => {:taggings_count => 1}).to_a
+ authors = Author.joins(:posts).eager_load(:comments).where(:posts => {:tags_count => 1}).to_a
assert_equal 1, assert_no_queries { authors.size }
assert_equal 10, assert_no_queries { authors[0].comments.size }
end
diff --git a/activerecord/test/cases/associations/deprecated_counter_cache_on_has_many_through_test.rb b/activerecord/test/cases/associations/deprecated_counter_cache_on_has_many_through_test.rb
new file mode 100644
index 0000000000..48f7ddbe83
--- /dev/null
+++ b/activerecord/test/cases/associations/deprecated_counter_cache_on_has_many_through_test.rb
@@ -0,0 +1,26 @@
+require "cases/helper"
+
+class DeprecatedCounterCacheOnHasManyThroughTest < ActiveRecord::TestCase
+ class Post < ActiveRecord::Base
+ has_many :taggings, as: :taggable
+ has_many :tags, through: :taggings
+ end
+
+ class Tagging < ActiveRecord::Base
+ belongs_to :taggable, polymorphic: true
+ belongs_to :tag
+ end
+
+ class Tag < ActiveRecord::Base
+ end
+
+ test "counter caches are updated in the database if the belongs_to association doesn't specify a counter cache" do
+ post = Post.create!(title: 'Hello', body: 'World!')
+ assert_deprecated { post.tags << Tag.create!(name: 'whatever') }
+
+ assert_equal 1, post.tags.size
+ assert_equal 1, post.tags_count
+ assert_equal 1, post.reload.tags.size
+ assert_equal 1, post.reload.tags_count
+ end
+end
diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb
index 7eaa5adc86..21912fdf0f 100644
--- a/activerecord/test/cases/associations/eager_test.rb
+++ b/activerecord/test/cases/associations/eager_test.rb
@@ -534,21 +534,13 @@ class EagerAssociationTest < ActiveRecord::TestCase
end
def test_eager_with_has_many_and_limit_and_conditions
- if current_adapter?(:OpenBaseAdapter)
- posts = Post.all.merge!(:includes => [ :author, :comments ], :limit => 2, :where => "FETCHBLOB(posts.body) = 'hello'", :order => "posts.id").to_a
- else
- posts = Post.all.merge!(:includes => [ :author, :comments ], :limit => 2, :where => "posts.body = 'hello'", :order => "posts.id").to_a
- end
+ posts = Post.all.merge!(:includes => [ :author, :comments ], :limit => 2, :where => "posts.body = 'hello'", :order => "posts.id").to_a
assert_equal 2, posts.size
assert_equal [4,5], posts.collect { |p| p.id }
end
def test_eager_with_has_many_and_limit_and_conditions_array
- if current_adapter?(:OpenBaseAdapter)
- posts = Post.all.merge!(:includes => [ :author, :comments ], :limit => 2, :where => [ "FETCHBLOB(posts.body) = ?", 'hello' ], :order => "posts.id").to_a
- else
- posts = Post.all.merge!(:includes => [ :author, :comments ], :limit => 2, :where => [ "posts.body = ?", 'hello' ], :order => "posts.id").to_a
- end
+ posts = Post.all.merge!(:includes => [ :author, :comments ], :limit => 2, :where => [ "posts.body = ?", 'hello' ], :order => "posts.id").to_a
assert_equal 2, posts.size
assert_equal [4,5], posts.collect { |p| p.id }
end
@@ -826,11 +818,15 @@ class EagerAssociationTest < ActiveRecord::TestCase
end
def test_preload_with_interpolation
- post = Post.includes(:comments_with_interpolated_conditions).find(posts(:welcome).id)
- assert_equal [comments(:greetings)], post.comments_with_interpolated_conditions
+ assert_deprecated do
+ post = Post.includes(:comments_with_interpolated_conditions).find(posts(:welcome).id)
+ assert_equal [comments(:greetings)], post.comments_with_interpolated_conditions
+ end
- post = Post.joins(:comments_with_interpolated_conditions).find(posts(:welcome).id)
- assert_equal [comments(:greetings)], post.comments_with_interpolated_conditions
+ assert_deprecated do
+ post = Post.joins(:comments_with_interpolated_conditions).find(posts(:welcome).id)
+ assert_equal [comments(:greetings)], post.comments_with_interpolated_conditions
+ end
end
def test_polymorphic_type_condition
@@ -936,13 +932,7 @@ class EagerAssociationTest < ActiveRecord::TestCase
end
def test_count_with_include
- if current_adapter?(:SybaseAdapter)
- assert_equal 3, authors(:david).posts_with_comments.where("len(comments.body) > 15").references(:comments).count
- elsif current_adapter?(:OpenBaseAdapter)
- assert_equal 3, authors(:david).posts_with_comments.where("length(FETCHBLOB(comments.body)) > 15").references(:comments).count
- else
- assert_equal 3, authors(:david).posts_with_comments.where("length(comments.body) > 15").references(:comments).count
- end
+ assert_equal 3, authors(:david).posts_with_comments.where("length(comments.body) > 15").references(:comments).count
end
def test_load_with_sti_sharing_association
@@ -1177,6 +1167,13 @@ class EagerAssociationTest < ActiveRecord::TestCase
)
end
+ test "deep preload" do
+ post = Post.preload(author: :posts, comments: :post).first
+
+ assert_predicate post.author.association(:posts), :loaded?
+ assert_predicate post.comments.first.association(:post), :loaded?
+ 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 }
@@ -1206,6 +1203,15 @@ class EagerAssociationTest < ActiveRecord::TestCase
end
end
+ test "preloading the same association twice works" do
+ Member.create!
+ members = Member.preload(:current_membership).includes(current_membership: :club).all.to_a
+ assert_no_queries {
+ members_with_membership = members.select(&:current_membership)
+ assert_equal 3, members_with_membership.map(&:current_membership).map(&:club).size
+ }
+ end
+
test "preloading with a polymorphic association and using the existential predicate" do
assert_equal authors(:david), authors(:david).essays.includes(:writer).first.writer
@@ -1232,4 +1238,56 @@ class EagerAssociationTest < ActiveRecord::TestCase
assert_equal 2, author.posts.size
}
end
+
+ test "including association based on sql condition and no database column" do
+ assert_equal pets(:parrot), Owner.including_last_pet.first.last_pet
+ end
+
+ test "include instance dependent associations is deprecated" do
+ message = "association scope 'posts_with_signature' is"
+ assert_deprecated message do
+ begin
+ Author.includes(:posts_with_signature).to_a
+ rescue NoMethodError
+ # it's expected that preloading of this association fails
+ end
+ end
+
+ assert_deprecated message do
+ Author.preload(:posts_with_signature).to_a rescue NoMethodError
+ end
+
+ assert_deprecated message do
+ Author.eager_load(:posts_with_signature).to_a
+ end
+ end
+
+ test "preloading readonly association" do
+ # has-one
+ firm = Firm.where(id: "1").preload(:readonly_account).first!
+ assert firm.readonly_account.readonly?
+
+ # has_and_belongs_to_many
+ project = Project.where(id: "2").preload(:readonly_developers).first!
+ assert project.readonly_developers.first.readonly?
+
+ # has-many :through
+ david = Author.where(id: "1").preload(:readonly_comments).first!
+ assert david.readonly_comments.first.readonly?
+ end
+
+ test "eager-loading readonly association" do
+ skip "eager_load does not yet preserve readonly associations"
+ # has-one
+ firm = Firm.where(id: "1").eager_load(:readonly_account).first!
+ assert firm.readonly_account.readonly?
+
+ # has_and_belongs_to_many
+ project = Project.where(id: "2").eager_load(:readonly_developers).first!
+ assert project.readonly_developers.first.readonly?
+
+ # has-many :through
+ david = Author.where(id: "1").eager_load(:readonly_comments).first!
+ assert david.readonly_comments.first.readonly?
+ 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 5d33634da2..cc58a4a1a2 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
@@ -22,6 +22,9 @@ require 'models/sponsor'
require 'models/country'
require 'models/treaty'
require 'models/vertex'
+require 'models/publisher'
+require 'models/publisher/article'
+require 'models/publisher/magazine'
require 'active_support/core_ext/string/conversions'
class ProjectWithAfterCreateHook < ActiveRecord::Base
@@ -67,6 +70,14 @@ class DeveloperWithSymbolsForKeys < ActiveRecord::Base
:foreign_key => "developer_id"
end
+class SubDeveloper < Developer
+ self.table_name = 'developers'
+ has_and_belongs_to_many :special_projects,
+ :join_table => 'developers_projects',
+ :foreign_key => "project_id",
+ :association_foreign_key => "developer_id"
+end
+
class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
fixtures :accounts, :companies, :categories, :posts, :categories_posts, :developers, :projects, :developers_projects,
:parrots, :pirates, :parrots_pirates, :treasures, :price_estimates, :tags, :taggings
@@ -83,6 +94,12 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
country.treaties << treaty
end
+ def test_marshal_dump
+ post = posts :welcome
+ preloaded = Post.includes(:categories).find post.id
+ assert_equal preloaded, Marshal.load(Marshal.dump(preloaded))
+ end
+
def test_should_property_quote_string_primary_keys
setup_data_for_habtm_case
@@ -217,6 +234,24 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
assert_equal developers(:poor_jamis, :jamis, :david), projects(:active_record).developers
end
+ def test_habtm_collection_size_from_build
+ devel = Developer.create("name" => "Fred Wu")
+ devel.projects << Project.create("name" => "Grimetime")
+ devel.projects.build
+
+ assert_equal 2, devel.projects.size
+ end
+
+ def test_habtm_collection_size_from_params
+ devel = Developer.new({
+ projects_attributes: {
+ '0' => {}
+ }
+ })
+
+ assert_equal 1, devel.projects.size
+ end
+
def test_build
devel = Developer.find(1)
proj = assert_no_queries { devel.projects.build("name" => "Projekt") }
@@ -787,7 +822,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
assert_equal [], Pirate.where(id: redbeard.id)
end
- test "has and belongs to many associations on new records use null relations" do
+ def test_has_and_belongs_to_many_associations_on_new_records_use_null_relations
projects = Developer.new.projects
assert_no_queries do
assert_equal [], projects
@@ -824,4 +859,28 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
def test_custom_join_table
assert_equal 'edges', Vertex.reflect_on_association(:sources).join_table
end
+
+ def test_has_and_belongs_to_many_in_a_namespaced_model_pointing_to_a_namespaced_model
+ magazine = Publisher::Magazine.create
+ article = Publisher::Article.create
+ magazine.articles << article
+ magazine.save
+
+ assert_includes magazine.articles, article
+ end
+
+ def test_has_and_belongs_to_many_in_a_namespaced_model_pointing_to_a_non_namespaced_model
+ article = Publisher::Article.create
+ tag = Tag.create
+ article.tags << tag
+ article.save
+
+ assert_includes article.tags, tag
+ end
+
+ def test_redefine_habtm
+ child = SubDeveloper.new("name" => "Aredridel")
+ child.special_projects << SpecialProject.new("name" => "Special Project")
+ assert child.save, 'child object should be saved'
+ 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 2f5c9d6e1b..fe961e871c 100644
--- a/activerecord/test/cases/associations/has_many_associations_test.rb
+++ b/activerecord/test/cases/associations/has_many_associations_test.rb
@@ -26,6 +26,8 @@ require 'models/reference'
require 'models/job'
require 'models/college'
require 'models/student'
+require 'models/pirate'
+require 'models/ship'
class HasManyAssociationsTestForReorderWithJoinDependency < ActiveRecord::TestCase
fixtures :authors, :posts, :comments
@@ -34,7 +36,7 @@ class HasManyAssociationsTestForReorderWithJoinDependency < ActiveRecord::TestCa
author = authors(:david)
# this can fail on adapters which require ORDER BY expressions to be included in the SELECT expression
# if the reorder clauses are not correctly handled
- assert author.posts_with_comments_sorted_by_comment_id.where('comments.id > 0').reorder('posts.comments_count DESC', 'posts.taggings_count DESC').last
+ assert author.posts_with_comments_sorted_by_comment_id.where('comments.id > 0').reorder('posts.comments_count DESC', 'posts.tags_count DESC').last
end
end
@@ -43,12 +45,18 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
fixtures :accounts, :categories, :companies, :developers, :projects,
:developers_projects, :topics, :authors, :comments,
:people, :posts, :readers, :taggings, :cars, :essays,
- :categorizations, :jobs
+ :categorizations, :jobs, :tags
def setup
Client.destroyed_client_ids.clear
end
+ def test_sti_subselect_count
+ tag = Tag.first
+ len = Post.tagged_with(tag.id).limit(10).size
+ assert_operator len, :>, 0
+ end
+
def test_anonymous_has_many
developer = Class.new(ActiveRecord::Base) {
self.table_name = 'developers'
@@ -764,6 +772,36 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal topic.replies.to_a.size, topic.replies_count
end
+ def test_counter_cache_updates_in_memory_after_concat
+ topic = Topic.create title: "Zoom-zoom-zoom"
+
+ topic.replies << Reply.create(title: "re: zoom", content: "speedy quick!")
+ assert_equal 1, topic.replies_count
+ assert_equal 1, topic.replies.size
+ assert_equal 1, topic.reload.replies.size
+ end
+
+ def test_counter_cache_updates_in_memory_after_create
+ topic = Topic.create title: "Zoom-zoom-zoom"
+
+ topic.replies.create!(title: "re: zoom", content: "speedy quick!")
+ assert_equal 1, topic.replies_count
+ assert_equal 1, topic.replies.size
+ assert_equal 1, topic.reload.replies.size
+ end
+
+ def test_counter_cache_updates_in_memory_after_create_with_array
+ topic = Topic.create title: "Zoom-zoom-zoom"
+
+ topic.replies.create!([
+ { title: "re: zoom", content: "speedy quick!" },
+ { title: "re: zoom 2", content: "OMG lol!" },
+ ])
+ assert_equal 2, topic.replies_count
+ assert_equal 2, topic.replies.size
+ assert_equal 2, topic.reload.replies.size
+ end
+
def test_pushing_association_updates_counter_cache
topic = Topic.order("id ASC").first
reply = Reply.create!
@@ -776,14 +814,14 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_deleting_updates_counter_cache_without_dependent_option
post = posts(:welcome)
- assert_difference "post.reload.taggings_count", -1 do
+ assert_difference "post.reload.tags_count", -1 do
post.taggings.delete(post.taggings.first)
end
end
def test_deleting_updates_counter_cache_with_dependent_delete_all
post = posts(:welcome)
- post.update_columns(taggings_with_delete_all_count: post.taggings_count)
+ post.update_columns(taggings_with_delete_all_count: post.tags_count)
assert_difference "post.reload.taggings_with_delete_all_count", -1 do
post.taggings_with_delete_all.delete(post.taggings_with_delete_all.first)
@@ -792,13 +830,20 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_deleting_updates_counter_cache_with_dependent_destroy
post = posts(:welcome)
- post.update_columns(taggings_with_destroy_count: post.taggings_count)
+ post.update_columns(taggings_with_destroy_count: post.tags_count)
assert_difference "post.reload.taggings_with_destroy_count", -1 do
post.taggings_with_destroy.delete(post.taggings_with_destroy.first)
end
end
+ def test_calling_empty_with_counter_cache
+ post = posts(:welcome)
+ assert_queries(0) do
+ assert_not post.comments.empty?
+ end
+ end
+
def test_custom_named_counter_cache
topic = topics(:first)
@@ -1877,4 +1922,23 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
end
end
+
+ test 'passes custom context validation to validate children' do
+ pirate = FamousPirate.new
+ pirate.famous_ships << ship = FamousShip.new
+
+ assert pirate.valid?
+ assert_not pirate.valid?(:conference)
+ assert_equal "can't be blank", ship.errors[:name].first
+ end
+
+ test 'association with instance dependent scope' do
+ bob = authors(:bob)
+ Post.create!(title: "signed post by bob", body: "stuff", author: authors(:bob))
+ Post.create!(title: "anonymous post", body: "more stuff", author: authors(:bob))
+ assert_equal ["misc post by bob", "other post by bob",
+ "signed post by bob"], bob.posts_with_signature.map(&:title).sort
+
+ assert_equal [], authors(:david).posts_with_signature.map(&:title)
+ 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 2e62189e7a..a85e020f0c 100644
--- a/activerecord/test/cases/associations/has_many_through_associations_test.rb
+++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb
@@ -330,6 +330,19 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
assert post.single_people.include?(person)
end
+ def test_both_parent_ids_set_when_saving_new
+ post = Post.new(title: 'Hello', body: 'world')
+ person = Person.new(first_name: 'Sean')
+
+ post.people = [person]
+ post.save
+
+ assert post.id
+ assert person.id
+ assert_equal post.id, post.readers.first.post_id
+ assert_equal person.id, post.readers.first.person_id
+ end
+
def test_delete_association
assert_queries(2){posts(:welcome);people(:michael); }
@@ -476,7 +489,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
post = posts(:welcome)
tag = post.tags.create!(:name => 'doomed')
- assert_difference ['post.reload.taggings_count', 'post.reload.tags_count'], -1 do
+ assert_difference ['post.reload.tags_count'], -1 do
posts(:welcome).tags.delete(tag)
end
end
@@ -486,7 +499,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
tag = post.tags.create!(:name => 'doomed')
post.update_columns(tags_with_destroy_count: post.tags.count)
- assert_difference ['post.reload.taggings_count', 'post.reload.tags_with_destroy_count'], -1 do
+ assert_difference ['post.reload.tags_with_destroy_count'], -1 do
posts(:welcome).tags_with_destroy.delete(tag)
end
end
@@ -496,7 +509,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
tag = post.tags.create!(:name => 'doomed')
post.update_columns(tags_with_nullify_count: post.tags.count)
- assert_no_difference 'post.reload.taggings_count' do
+ assert_no_difference 'post.reload.tags_count' do
assert_difference 'post.reload.tags_with_nullify_count', -1 do
posts(:welcome).tags_with_nullify.delete(tag)
end
@@ -511,14 +524,14 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
tag.tagged_posts = []
post.reload
- assert_equal(post.taggings.count, post.taggings_count)
+ assert_equal(post.taggings.count, post.tags_count)
end
def test_update_counter_caches_on_destroy
post = posts(:welcome)
tag = post.tags.create!(name: 'doomed')
- assert_difference 'post.reload.taggings_count', -1 do
+ assert_difference 'post.reload.tags_count', -1 do
tag.tagged_posts.destroy(post)
end
end
@@ -1126,4 +1139,31 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
assert_equal 2, post.lazy_readers_unscope_skimmers.to_a.size
assert_equal 2, post.lazy_people_unscope_skimmers.to_a.size
end
+
+ def test_has_many_through_add_with_sti_middle_relation
+ club = SuperClub.create!(name: 'Fight Club')
+ member = Member.create!(name: 'Tyler Durden')
+
+ club.members << member
+ assert_equal 1, SuperMembership.where(member_id: member.id, club_id: club.id).count
+ end
+
+ class ClubWithCallbacks < ActiveRecord::Base
+ self.table_name = 'clubs'
+ after_create :add_a_member
+
+ has_many :memberships, inverse_of: :club, foreign_key: :club_id
+ has_many :members, through: :memberships
+
+ def add_a_member
+ members << Member.last
+ end
+ end
+
+ def test_has_many_with_callback_before_association
+ Member.create!
+ club = ClubWithCallbacks.create!
+
+ assert_equal 1, club.reload.memberships.count
+ 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 a2725441b3..089cb0a3a2 100644
--- a/activerecord/test/cases/associations/has_one_through_associations_test.rb
+++ b/activerecord/test/cases/associations/has_one_through_associations_test.rb
@@ -45,6 +45,20 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase
assert_equal clubs(:moustache_club), new_member.club
end
+ def test_creating_association_sets_both_parent_ids_for_new
+ member = Member.new(name: 'Sean Griffin')
+ club = Club.new(name: 'Da Club')
+
+ member.club = club
+
+ member.save!
+
+ assert member.id
+ assert club.id
+ assert_equal member.id, member.current_membership.member_id
+ assert_equal club.id, member.current_membership.club_id
+ end
+
def test_replace_target_record
new_club = Club.create(:name => "Marx Bros")
@member.club = new_club
diff --git a/activerecord/test/cases/associations/inner_join_association_test.rb b/activerecord/test/cases/associations/inner_join_association_test.rb
index b23517b2f9..07cf65a760 100644
--- a/activerecord/test/cases/associations/inner_join_association_test.rb
+++ b/activerecord/test/cases/associations/inner_join_association_test.rb
@@ -126,4 +126,14 @@ class InnerJoinAssociationTest < ActiveRecord::TestCase
categories = author.categories.includes(:special_categorizations).references(:special_categorizations).to_a
assert_equal 2, categories.size
end
+
+ test "the correct records are loaded when including an aliased association" do
+ author = Author.create! name: "Jon"
+ author.categories.create! name: 'Not Special'
+ author.special_categories.create! name: 'Special'
+
+ categories = author.categories.eager_load(:special_categorizations).order(:name).to_a
+ assert_equal 0, categories.first.special_categorizations.size
+ assert_equal 1, categories.second.special_categorizations.size
+ end
end
diff --git a/activerecord/test/cases/associations/inverse_associations_test.rb b/activerecord/test/cases/associations/inverse_associations_test.rb
index 893030345f..60df4e14dd 100644
--- a/activerecord/test/cases/associations/inverse_associations_test.rb
+++ b/activerecord/test/cases/associations/inverse_associations_test.rb
@@ -100,6 +100,17 @@ class AutomaticInverseFindingTests < ActiveRecord::TestCase
assert_respond_to club_reflection, :has_inverse?
assert !club_reflection.has_inverse?, "A has_many_through association should not find an inverse automatically"
end
+
+ def test_polymorphic_relationships_should_still_not_have_inverses_when_non_polymorphic_relationship_has_the_same_name
+ man_reflection = Man.reflect_on_association(:polymorphic_face_without_inverse)
+ face_reflection = Face.reflect_on_association(:man)
+
+ assert_respond_to face_reflection, :has_inverse?
+ assert face_reflection.has_inverse?, "For this test, the non-polymorphic association must have an inverse"
+
+ assert_respond_to man_reflection, :has_inverse?
+ assert !man_reflection.has_inverse?, "The target of a polymorphic association should not find an inverse automatically"
+ end
end
class InverseAssociationTests < ActiveRecord::TestCase
@@ -333,7 +344,7 @@ class InverseHasManyTests < ActiveRecord::TestCase
def test_parent_instance_should_be_shared_within_create_block_of_new_child
man = Man.first
- interest = man.interests.build do |i|
+ interest = man.interests.create 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"
diff --git a/activerecord/test/cases/associations/join_model_test.rb b/activerecord/test/cases/associations/join_model_test.rb
index aabeea025f..cace7ba142 100644
--- a/activerecord/test/cases/associations/join_model_test.rb
+++ b/activerecord/test/cases/associations/join_model_test.rb
@@ -326,11 +326,11 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
end
def test_belongs_to_polymorphic_with_counter_cache
- assert_equal 1, posts(:welcome)[:taggings_count]
+ assert_equal 1, posts(:welcome)[:tags_count]
tagging = posts(:welcome).taggings.create(:tag => tags(:general))
- assert_equal 2, posts(:welcome, :reload)[:taggings_count]
+ assert_equal 2, posts(:welcome, :reload)[:tags_count]
tagging.destroy
- assert_equal 1, posts(:welcome, :reload)[:taggings_count]
+ assert_equal 1, posts(:welcome, :reload)[:tags_count]
end
def test_unavailable_through_reflection
@@ -489,7 +489,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
message = "Expected a Tag in tags collection, got #{wrong.class}.")
assert_nil( wrong = post_thinking.taggings.detect { |t| t.class != Tagging },
message = "Expected a Tagging in taggings collection, got #{wrong.class}.")
- assert_equal(count + 1, post_thinking.tags.size)
+ assert_equal(count + 1, post_thinking.reload.tags.size)
assert_equal(count + 1, post_thinking.tags(true).size)
assert_kind_of Tag, post_thinking.tags.create!(:name => 'foo')
@@ -497,7 +497,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
message = "Expected a Tag in tags collection, got #{wrong.class}.")
assert_nil( wrong = post_thinking.taggings.detect { |t| t.class != Tagging },
message = "Expected a Tagging in taggings collection, got #{wrong.class}.")
- assert_equal(count + 2, post_thinking.tags.size)
+ assert_equal(count + 2, post_thinking.reload.tags.size)
assert_equal(count + 2, post_thinking.tags(true).size)
assert_nothing_raised { post_thinking.tags.concat(Tag.create!(:name => 'abc'), Tag.create!(:name => 'def')) }
@@ -505,7 +505,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
message = "Expected a Tag in tags collection, got #{wrong.class}.")
assert_nil( wrong = post_thinking.taggings.detect { |t| t.class != Tagging },
message = "Expected a Tagging in taggings collection, got #{wrong.class}.")
- assert_equal(count + 4, post_thinking.tags.size)
+ assert_equal(count + 4, post_thinking.reload.tags.size)
assert_equal(count + 4, post_thinking.tags(true).size)
# Raises if the wrong reflection name is used to set the Edge belongs_to
@@ -554,34 +554,35 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
def test_delete_associate_when_deleting_from_has_many_through
count = posts(:thinking).tags.count
- tags_before = posts(:thinking).tags
+ tags_before = posts(:thinking).tags.sort
tag = Tag.create!(:name => 'doomed')
post_thinking = posts(:thinking)
post_thinking.tags << tag
assert_equal(count + 1, post_thinking.taggings(true).size)
- assert_equal(count + 1, post_thinking.tags(true).size)
+ assert_equal(count + 1, post_thinking.reload.tags(true).size)
+ assert_not_equal(tags_before, post_thinking.tags.sort)
assert_nothing_raised { post_thinking.tags.delete(tag) }
assert_equal(count, post_thinking.tags.size)
assert_equal(count, post_thinking.tags(true).size)
assert_equal(count, post_thinking.taggings(true).size)
- assert_equal(tags_before.sort, post_thinking.tags.sort)
+ assert_equal(tags_before, post_thinking.tags.sort)
end
def test_delete_associate_when_deleting_from_has_many_through_with_multiple_tags
count = posts(:thinking).tags.count
- tags_before = posts(:thinking).tags
+ tags_before = posts(:thinking).tags.sort
doomed = Tag.create!(:name => 'doomed')
doomed2 = Tag.create!(:name => 'doomed2')
quaked = Tag.create!(:name => 'quaked')
post_thinking = posts(:thinking)
post_thinking.tags << doomed << doomed2
- assert_equal(count + 2, post_thinking.tags(true).size)
+ assert_equal(count + 2, post_thinking.reload.tags(true).size)
assert_nothing_raised { post_thinking.tags.delete(doomed, doomed2, quaked) }
assert_equal(count, post_thinking.tags.size)
assert_equal(count, post_thinking.tags(true).size)
- assert_equal(tags_before.sort, post_thinking.tags.sort)
+ assert_equal(tags_before, post_thinking.tags.sort)
end
def test_deleting_junk_from_has_many_through_should_raise_type_mismatch
diff --git a/activerecord/test/cases/associations/nested_through_associations_test.rb b/activerecord/test/cases/associations/nested_through_associations_test.rb
index 8ef351cda8..31b68c940e 100644
--- a/activerecord/test/cases/associations/nested_through_associations_test.rb
+++ b/activerecord/test/cases/associations/nested_through_associations_test.rb
@@ -130,7 +130,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase
def test_has_many_through_has_one_through_with_has_one_source_reflection_preload
members = assert_queries(4) { Member.includes(:nested_sponsors).to_a }
mustache = sponsors(:moustache_club_sponsor_for_groucho)
- assert_no_queries do
+ assert_no_queries(ignore_none: false) do
assert_equal [mustache], members.first.nested_sponsors
end
end
@@ -153,6 +153,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase
end
def test_has_many_through_has_one_with_has_many_through_source_reflection_preload
+ ActiveRecord::Base.connection.table_alias_length # preheat cache
members = assert_queries(4) { Member.includes(:organization_member_details).to_a.sort_by(&:id) }
groucho_details, other_details = member_details(:groucho), member_details(:some_other_guy)
diff --git a/activerecord/test/cases/associations/required_test.rb b/activerecord/test/cases/associations/required_test.rb
new file mode 100644
index 0000000000..a6934a056e
--- /dev/null
+++ b/activerecord/test/cases/associations/required_test.rb
@@ -0,0 +1,82 @@
+require "cases/helper"
+
+class RequiredAssociationsTest < ActiveRecord::TestCase
+ self.use_transactional_fixtures = false
+
+ class Parent < ActiveRecord::Base
+ end
+
+ class Child < ActiveRecord::Base
+ end
+
+ setup do
+ @connection = ActiveRecord::Base.connection
+ @connection.create_table :parents, force: true
+ @connection.create_table :children, force: true do |t|
+ t.belongs_to :parent
+ end
+ end
+
+ teardown do
+ @connection.execute("DROP TABLE IF EXISTS parents")
+ @connection.execute("DROP TABLE IF EXISTS children")
+ end
+
+ test "belongs_to associations are not required by default" do
+ model = subclass_of(Child) do
+ belongs_to :parent, inverse_of: false,
+ class_name: "RequiredAssociationsTest::Parent"
+ end
+
+ assert model.new.save
+ assert model.new(parent: Parent.new).save
+ end
+
+ test "required belongs_to associations have presence validated" do
+ model = subclass_of(Child) do
+ belongs_to :parent, required: true, inverse_of: false,
+ class_name: "RequiredAssociationsTest::Parent"
+ end
+
+ record = model.new
+ assert_not record.save
+ assert_equal ["Parent can't be blank"], record.errors.full_messages
+
+ record.parent = Parent.new
+ assert record.save
+ end
+
+ test "has_one associations are not required by default" do
+ model = subclass_of(Parent) do
+ has_one :child, inverse_of: false,
+ class_name: "RequiredAssociationsTest::Child"
+ end
+
+ assert model.new.save
+ assert model.new(child: Child.new).save
+ end
+
+ test "required has_one associations have presence validated" do
+ model = subclass_of(Parent) do
+ has_one :child, required: true, inverse_of: false,
+ class_name: "RequiredAssociationsTest::Child"
+ end
+
+ record = model.new
+ assert_not record.save
+ assert_equal ["Child can't be blank"], record.errors.full_messages
+
+ record.child = Child.new
+ assert record.save
+ end
+
+ private
+
+ def subclass_of(klass, &block)
+ subclass = Class.new(klass, &block)
+ def subclass.name
+ superclass.name
+ end
+ subclass
+ end
+end
diff --git a/activerecord/test/cases/attribute_decorators_test.rb b/activerecord/test/cases/attribute_decorators_test.rb
new file mode 100644
index 0000000000..cbc2c4e5d7
--- /dev/null
+++ b/activerecord/test/cases/attribute_decorators_test.rb
@@ -0,0 +1,124 @@
+require 'cases/helper'
+
+module ActiveRecord
+ class AttributeDecoratorsTest < ActiveRecord::TestCase
+ class Model < ActiveRecord::Base
+ self.table_name = 'attribute_decorators_model'
+ end
+
+ class StringDecorator < SimpleDelegator
+ def initialize(delegate, decoration = "decorated!")
+ @decoration = decoration
+ super(delegate)
+ end
+
+ def type_cast_from_user(value)
+ "#{super} #{@decoration}"
+ end
+
+ alias type_cast_from_database type_cast_from_user
+ end
+
+ setup do
+ @connection = ActiveRecord::Base.connection
+ @connection.create_table :attribute_decorators_model, force: true do |t|
+ t.string :a_string
+ end
+ end
+
+ teardown do
+ return unless @connection
+ @connection.execute 'DROP TABLE IF EXISTS attribute_decorators_model'
+ Model.attribute_type_decorations.clear
+ Model.reset_column_information
+ end
+
+ test "attributes can be decorated" do
+ model = Model.new(a_string: 'Hello')
+ assert_equal 'Hello', model.a_string
+
+ Model.decorate_attribute_type(:a_string, :test) { |t| StringDecorator.new(t) }
+
+ model = Model.new(a_string: 'Hello')
+ assert_equal 'Hello decorated!', model.a_string
+ end
+
+ test "decoration does not eagerly load existing columns" do
+ assert_no_queries do
+ Model.decorate_attribute_type(:a_string, :test) { |t| StringDecorator.new(t) }
+ end
+ end
+
+ test "undecorated columns are not touched" do
+ Model.attribute :another_string, Type::String.new, default: 'something or other'
+ Model.decorate_attribute_type(:a_string, :test) { |t| StringDecorator.new(t) }
+
+ assert_equal 'something or other', Model.new.another_string
+ end
+
+ test "decorators can be chained" do
+ Model.decorate_attribute_type(:a_string, :test) { |t| StringDecorator.new(t) }
+ Model.decorate_attribute_type(:a_string, :other) { |t| StringDecorator.new(t) }
+
+ model = Model.new(a_string: 'Hello!')
+
+ assert_equal 'Hello! decorated! decorated!', model.a_string
+ end
+
+ test "decoration of the same type multiple times is idempotent" do
+ Model.decorate_attribute_type(:a_string, :test) { |t| StringDecorator.new(t) }
+ Model.decorate_attribute_type(:a_string, :test) { |t| StringDecorator.new(t) }
+
+ model = Model.new(a_string: 'Hello')
+ assert_equal 'Hello decorated!', model.a_string
+ end
+
+ test "decorations occur in order of declaration" do
+ Model.decorate_attribute_type(:a_string, :test) { |t| StringDecorator.new(t) }
+ Model.decorate_attribute_type(:a_string, :other) do |type|
+ StringDecorator.new(type, 'decorated again!')
+ end
+
+ model = Model.new(a_string: 'Hello!')
+
+ assert_equal 'Hello! decorated! decorated again!', model.a_string
+ end
+
+ test "decorating attributes does not modify parent classes" do
+ Model.attribute :another_string, Type::String.new, default: 'whatever'
+ Model.decorate_attribute_type(:a_string, :test) { |t| StringDecorator.new(t) }
+ child_class = Class.new(Model)
+ child_class.decorate_attribute_type(:another_string, :test) { |t| StringDecorator.new(t) }
+ child_class.decorate_attribute_type(:a_string, :other) { |t| StringDecorator.new(t) }
+
+ model = Model.new(a_string: 'Hello!')
+ child = child_class.new(a_string: 'Hello!')
+
+ assert_equal 'Hello! decorated!', model.a_string
+ assert_equal 'whatever', model.another_string
+ assert_equal 'Hello! decorated! decorated!', child.a_string
+ assert_equal 'whatever decorated!', child.another_string
+ end
+
+ class Multiplier < SimpleDelegator
+ def type_cast_from_user(value)
+ return if value.nil?
+ value * 2
+ end
+ alias type_cast_from_database type_cast_from_user
+ end
+
+ test "decorating with a proc" do
+ Model.attribute :an_int, Type::Integer.new
+ type_is_integer = proc { |_, type| type.type == :integer }
+ Model.decorate_matching_attribute_types type_is_integer, :multiplier do |type|
+ Multiplier.new(type)
+ end
+
+ model = Model.new(a_string: 'whatever', an_int: 1)
+
+ assert_equal 'whatever', model.a_string
+ assert_equal 2, model.an_int
+ end
+ end
+end
diff --git a/activerecord/test/cases/attribute_methods/read_test.rb b/activerecord/test/cases/attribute_methods/read_test.rb
index c0659fddef..4741ee8799 100644
--- a/activerecord/test/cases/attribute_methods/read_test.rb
+++ b/activerecord/test/cases/attribute_methods/read_test.rb
@@ -12,6 +12,7 @@ module ActiveRecord
@klass = Class.new do
def self.superclass; Base; end
def self.base_class; self; end
+ def self.decorate_matching_attribute_types(*); end
include ActiveRecord::AttributeMethods
diff --git a/activerecord/test/cases/attribute_methods/serialization_test.rb b/activerecord/test/cases/attribute_methods/serialization_test.rb
deleted file mode 100644
index 75de773961..0000000000
--- a/activerecord/test/cases/attribute_methods/serialization_test.rb
+++ /dev/null
@@ -1,29 +0,0 @@
-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 38e93288e4..ab67cf4085 100644
--- a/activerecord/test/cases/attribute_methods_test.rb
+++ b/activerecord/test/cases/attribute_methods_test.rb
@@ -143,7 +143,11 @@ class AttributeMethodsTest < ActiveRecord::TestCase
# Syck calls respond_to? before actually calling initialize
def test_respond_to_with_allocated_object
- topic = Topic.allocate
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = 'topics'
+ end
+
+ topic = klass.allocate
assert !topic.respond_to?("nothingness")
assert !topic.respond_to?(:nothingness)
assert_respond_to topic, "title"
@@ -253,6 +257,15 @@ class AttributeMethodsTest < ActiveRecord::TestCase
assert_equal @loaded_fixtures['computers']['workstation'].to_hash, Computer.first.attributes
end
+ def test_attributes_without_primary_key
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = 'developers_projects'
+ end
+
+ assert_equal klass.column_names, klass.new.attributes.keys
+ assert_not klass.new.attributes.key?('id')
+ end
+
def test_hashes_not_mangled
new_topic = { :title => "New Topic" }
new_topic_values = { :title => "AnotherTopic" }
@@ -299,6 +312,8 @@ class AttributeMethodsTest < ActiveRecord::TestCase
computer = Computer.select('id').first
assert_raises(ActiveModel::MissingAttributeError) { computer[:developer] }
assert_raises(ActiveModel::MissingAttributeError) { computer[:extendedWarranty] }
+ assert_raises(ActiveModel::MissingAttributeError) { computer[:no_column_exists] = 'Hello!' }
+ assert_nothing_raised { computer[:developer] = 'Hello!' }
end
def test_read_attribute_when_false
@@ -449,10 +464,10 @@ class AttributeMethodsTest < ActiveRecord::TestCase
end
def test_declared_suffixed_attribute_method_affects_respond_to_and_method_missing
- topic = @target.new(:title => 'Budget')
%w(_default _title_default _it! _candidate= able?).each do |suffix|
@target.class_eval "def attribute#{suffix}(*args) args end"
@target.attribute_method_suffix suffix
+ topic = @target.new(:title => 'Budget')
meth = "title#{suffix}"
assert topic.respond_to?(meth)
@@ -463,10 +478,10 @@ class AttributeMethodsTest < ActiveRecord::TestCase
end
def test_declared_affixed_attribute_method_affects_respond_to_and_method_missing
- topic = @target.new(:title => 'Budget')
[['mark_', '_for_update'], ['reset_', '!'], ['default_', '_value?']].each do |prefix, suffix|
@target.class_eval "def #{prefix}attribute#{suffix}(*args) args end"
@target.attribute_method_affix({ :prefix => prefix, :suffix => suffix })
+ topic = @target.new(:title => 'Budget')
meth = "#{prefix}title#{suffix}"
assert topic.respond_to?(meth)
@@ -515,43 +530,17 @@ class AttributeMethodsTest < ActiveRecord::TestCase
end
end
- def test_only_time_related_columns_are_meant_to_be_cached_by_default
- expected = %w(datetime timestamp time date).sort
- assert_equal expected, ActiveRecord::Base.attribute_types_cached_by_default.map(&:to_s).sort
- end
-
- def test_declaring_attributes_as_cached_adds_them_to_the_attributes_cached_by_default
- default_attributes = Topic.cached_attributes
- Topic.cache_attributes :replies_count
- expected = default_attributes + ["replies_count"]
- assert_equal expected.sort, Topic.cached_attributes.sort
- Topic.instance_variable_set "@cached_attributes", nil
- end
-
- def test_cacheable_columns_are_actually_cached
- assert_equal cached_columns.sort, Topic.cached_attributes.sort
- end
-
- def test_accessing_cached_attributes_caches_the_converted_values_and_nothing_else
- t = topics(:first)
- cache = t.instance_variable_get "@attributes_cache"
-
- assert_not_nil cache
- assert cache.empty?
+ def test_deprecated_cache_attributes
+ assert_deprecated do
+ Topic.cache_attributes :replies_count
+ end
- all_columns = Topic.columns.map(&:name)
- uncached_columns = all_columns - cached_columns
+ assert_deprecated do
+ Topic.cached_attributes
+ end
- all_columns.each do |attr_name|
- attribute_gets_cached = Topic.cache_attribute?(attr_name)
- val = t.send attr_name unless attr_name == "type"
- if attribute_gets_cached
- assert cached_columns.include?(attr_name)
- assert_equal val, cache[attr_name]
- else
- assert uncached_columns.include?(attr_name)
- assert !cache.include?(attr_name)
- end
+ assert_deprecated do
+ Topic.cache_attribute? :replies_count
end
end
@@ -843,6 +832,49 @@ class AttributeMethodsTest < ActiveRecord::TestCase
assert_equal !real_topic.title?, klass.find(real_topic.id).title?
end
+ def test_calling_super_when_parent_does_not_define_method_raises_error
+ klass = new_topic_like_ar_class do
+ def some_method_that_is_not_on_super
+ super
+ end
+ end
+
+ assert_raise(NoMethodError) do
+ klass.new.some_method_that_is_not_on_super
+ end
+ end
+
+ def test_attribute_method?
+ assert @target.attribute_method?(:title)
+ assert @target.attribute_method?(:title=)
+ assert_not @target.attribute_method?(:wibble)
+ end
+
+ def test_attribute_method_returns_false_if_table_does_not_exist
+ @target.table_name = 'wibble'
+ assert_not @target.attribute_method?(:title)
+ end
+
+ def test_attribute_names_on_new_record
+ model = @target.new
+
+ assert_equal @target.column_names, model.attribute_names
+ end
+
+ def test_attribute_names_on_queried_record
+ model = @target.last!
+
+ assert_equal @target.column_names, model.attribute_names
+ end
+
+ def test_attribute_names_with_custom_select
+ model = @target.select('id').last!
+
+ assert_equal ['id'], model.attribute_names
+ # Sanity check, make sure other columns exist
+ assert_not_equal ['id'], @target.column_names
+ end
+
private
def new_topic_like_ar_class(&block)
@@ -856,7 +888,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase
end
def cached_columns
- Topic.columns.map(&:name) - Topic.serialized_attributes.keys
+ Topic.columns.map(&:name)
end
def time_related_columns_on_topic
diff --git a/activerecord/test/cases/attribute_set_test.rb b/activerecord/test/cases/attribute_set_test.rb
new file mode 100644
index 0000000000..dc20c3c676
--- /dev/null
+++ b/activerecord/test/cases/attribute_set_test.rb
@@ -0,0 +1,165 @@
+require 'cases/helper'
+
+module ActiveRecord
+ class AttributeSetTest < ActiveRecord::TestCase
+ test "building a new set from raw attributes" do
+ builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new)
+ attributes = builder.build_from_database(foo: '1.1', bar: '2.2')
+
+ assert_equal 1, attributes[:foo].value
+ assert_equal 2.2, attributes[:bar].value
+ assert_equal :foo, attributes[:foo].name
+ assert_equal :bar, attributes[:bar].name
+ end
+
+ test "building with custom types" do
+ builder = AttributeSet::Builder.new(foo: Type::Float.new)
+ attributes = builder.build_from_database({ foo: '3.3', bar: '4.4' }, { bar: Type::Integer.new })
+
+ assert_equal 3.3, attributes[:foo].value
+ assert_equal 4, attributes[:bar].value
+ end
+
+ test "[] returns a null object" do
+ builder = AttributeSet::Builder.new(foo: Type::Float.new)
+ attributes = builder.build_from_database(foo: '3.3')
+
+ assert_equal '3.3', attributes[:foo].value_before_type_cast
+ assert_equal nil, attributes[:bar].value_before_type_cast
+ assert_equal :bar, attributes[:bar].name
+ end
+
+ test "duping creates a new hash and dups each attribute" do
+ builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::String.new)
+ attributes = builder.build_from_database(foo: 1, bar: 'foo')
+
+ # Ensure the type cast value is cached
+ attributes[:foo].value
+ attributes[:bar].value
+
+ duped = attributes.dup
+ duped.write_from_database(:foo, 2)
+ duped[:bar].value << 'bar'
+
+ assert_equal 1, attributes[:foo].value
+ assert_equal 2, duped[:foo].value
+ assert_equal 'foo', attributes[:bar].value
+ assert_equal 'foobar', duped[:bar].value
+ end
+
+ test "freezing cloned set does not freeze original" do
+ attributes = AttributeSet.new({})
+ clone = attributes.clone
+
+ clone.freeze
+
+ assert clone.frozen?
+ assert_not attributes.frozen?
+ end
+
+ test "to_hash returns a hash of the type cast values" do
+ builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new)
+ attributes = builder.build_from_database(foo: '1.1', bar: '2.2')
+
+ assert_equal({ foo: 1, bar: 2.2 }, attributes.to_hash)
+ assert_equal({ foo: 1, bar: 2.2 }, attributes.to_h)
+ end
+
+ test "values_before_type_cast" do
+ builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Integer.new)
+ attributes = builder.build_from_database(foo: '1.1', bar: '2.2')
+
+ assert_equal({ foo: '1.1', bar: '2.2' }, attributes.values_before_type_cast)
+ end
+
+ test "known columns are built with uninitialized attributes" do
+ attributes = attributes_with_uninitialized_key
+ assert attributes[:foo].initialized?
+ assert_not attributes[:bar].initialized?
+ end
+
+ test "uninitialized attributes are not included in the attributes hash" do
+ attributes = attributes_with_uninitialized_key
+ assert_equal({ foo: 1 }, attributes.to_hash)
+ end
+
+ test "uninitialized attributes are not included in keys" do
+ attributes = attributes_with_uninitialized_key
+ assert_equal [:foo], attributes.keys
+ end
+
+ test "uninitialized attributes return false for key?" do
+ attributes = attributes_with_uninitialized_key
+ assert attributes.key?(:foo)
+ assert_not attributes.key?(:bar)
+ end
+
+ test "unknown attributes return false for key?" do
+ attributes = attributes_with_uninitialized_key
+ assert_not attributes.key?(:wibble)
+ end
+
+ test "fetch_value returns the value for the given initialized attribute" do
+ builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new)
+ attributes = builder.build_from_database(foo: '1.1', bar: '2.2')
+
+ assert_equal 1, attributes.fetch_value(:foo)
+ assert_equal 2.2, attributes.fetch_value(:bar)
+ end
+
+ test "fetch_value returns nil for unknown attributes" do
+ attributes = attributes_with_uninitialized_key
+ assert_nil attributes.fetch_value(:wibble)
+ end
+
+ test "fetch_value uses the given block for uninitialized attributes" do
+ attributes = attributes_with_uninitialized_key
+ value = attributes.fetch_value(:bar) { |n| n.to_s + '!' }
+ assert_equal 'bar!', value
+ end
+
+ test "fetch_value returns nil for uninitialized attributes if no block is given" do
+ attributes = attributes_with_uninitialized_key
+ assert_nil attributes.fetch_value(:bar)
+ end
+
+ class MyType
+ def type_cast_from_user(value)
+ return if value.nil?
+ value + " from user"
+ end
+
+ def type_cast_from_database(value)
+ return if value.nil?
+ value + " from database"
+ end
+ end
+
+ test "write_from_database sets the attribute with database typecasting" do
+ builder = AttributeSet::Builder.new(foo: MyType.new)
+ attributes = builder.build_from_database
+
+ assert_nil attributes.fetch_value(:foo)
+
+ attributes.write_from_database(:foo, "value")
+
+ assert_equal "value from database", attributes.fetch_value(:foo)
+ end
+
+ test "write_from_user sets the attribute with user typecasting" do
+ builder = AttributeSet::Builder.new(foo: MyType.new)
+ attributes = builder.build_from_database
+
+ assert_nil attributes.fetch_value(:foo)
+
+ attributes.write_from_user(:foo, "value")
+
+ assert_equal "value from user", attributes.fetch_value(:foo)
+ end
+
+ def attributes_with_uninitialized_key
+ builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new)
+ builder.build_from_database(foo: '1.1')
+ end
+ end
+end
diff --git a/activerecord/test/cases/attribute_test.rb b/activerecord/test/cases/attribute_test.rb
new file mode 100644
index 0000000000..24452fdec2
--- /dev/null
+++ b/activerecord/test/cases/attribute_test.rb
@@ -0,0 +1,142 @@
+require 'cases/helper'
+require 'minitest/mock'
+
+module ActiveRecord
+ class AttributeTest < ActiveRecord::TestCase
+ setup do
+ @type = MiniTest::Mock.new
+ end
+
+ teardown do
+ assert @type.verify
+ end
+
+ test "from_database + read type casts from database" do
+ @type.expect(:type_cast_from_database, 'type cast from database', ['a value'])
+ attribute = Attribute.from_database(nil, 'a value', @type)
+
+ type_cast_value = attribute.value
+
+ assert_equal 'type cast from database', type_cast_value
+ end
+
+ test "from_user + read type casts from user" do
+ @type.expect(:type_cast_from_user, 'type cast from user', ['a value'])
+ attribute = Attribute.from_user(nil, 'a value', @type)
+
+ type_cast_value = attribute.value
+
+ assert_equal 'type cast from user', type_cast_value
+ end
+
+ test "reading memoizes the value" do
+ @type.expect(:type_cast_from_database, 'from the database', ['whatever'])
+ attribute = Attribute.from_database(nil, 'whatever', @type)
+
+ type_cast_value = attribute.value
+ second_read = attribute.value
+
+ assert_equal 'from the database', type_cast_value
+ assert_same type_cast_value, second_read
+ end
+
+ test "reading memoizes falsy values" do
+ @type.expect(:type_cast_from_database, false, ['whatever'])
+ attribute = Attribute.from_database(nil, 'whatever', @type)
+
+ attribute.value
+ attribute.value
+ end
+
+ test "read_before_typecast returns the given value" do
+ attribute = Attribute.from_database(nil, 'raw value', @type)
+
+ raw_value = attribute.value_before_type_cast
+
+ assert_equal 'raw value', raw_value
+ end
+
+ test "from_database + read_for_database type casts to and from database" do
+ @type.expect(:type_cast_from_database, 'read from database', ['whatever'])
+ @type.expect(:type_cast_for_database, 'ready for database', ['read from database'])
+ attribute = Attribute.from_database(nil, 'whatever', @type)
+
+ type_cast_for_database = attribute.value_for_database
+
+ assert_equal 'ready for database', type_cast_for_database
+ end
+
+ test "from_user + read_for_database type casts from the user to the database" do
+ @type.expect(:type_cast_from_user, 'read from user', ['whatever'])
+ @type.expect(:type_cast_for_database, 'ready for database', ['read from user'])
+ attribute = Attribute.from_user(nil, 'whatever', @type)
+
+ type_cast_for_database = attribute.value_for_database
+
+ assert_equal 'ready for database', type_cast_for_database
+ end
+
+ test "duping dups the value" do
+ @type.expect(:type_cast_from_database, 'type cast', ['a value'])
+ attribute = Attribute.from_database(nil, 'a value', @type)
+
+ value_from_orig = attribute.value
+ value_from_clone = attribute.dup.value
+ value_from_orig << ' foo'
+
+ assert_equal 'type cast foo', value_from_orig
+ assert_equal 'type cast', value_from_clone
+ end
+
+ test "duping does not dup the value if it is not dupable" do
+ @type.expect(:type_cast_from_database, false, ['a value'])
+ attribute = Attribute.from_database(nil, 'a value', @type)
+
+ assert_same attribute.value, attribute.dup.value
+ end
+
+ test "duping does not eagerly type cast if we have not yet type cast" do
+ attribute = Attribute.from_database(nil, 'a value', @type)
+ attribute.dup
+ end
+
+ class MyType
+ def type_cast_from_user(value)
+ value + " from user"
+ end
+
+ def type_cast_from_database(value)
+ value + " from database"
+ end
+ end
+
+ test "with_value_from_user returns a new attribute with the value from the user" do
+ old = Attribute.from_database(nil, "old", MyType.new)
+ new = old.with_value_from_user("new")
+
+ assert_equal "old from database", old.value
+ assert_equal "new from user", new.value
+ end
+
+ test "with_value_from_database returns a new attribute with the value from the database" do
+ old = Attribute.from_user(nil, "old", MyType.new)
+ new = old.with_value_from_database("new")
+
+ assert_equal "old from user", old.value
+ assert_equal "new from database", new.value
+ end
+
+ test "uninitialized attributes yield their name if a block is given to value" do
+ block = proc { |name| name.to_s + "!" }
+ foo = Attribute.uninitialized(:foo, nil)
+ bar = Attribute.uninitialized(:bar, nil)
+
+ assert_equal "foo!", foo.value(&block)
+ assert_equal "bar!", bar.value(&block)
+ end
+
+ test "uninitialized attributes have no value" do
+ assert_nil Attribute.uninitialized(:foo, nil).value
+ end
+ end
+end
diff --git a/activerecord/test/cases/attributes_test.rb b/activerecord/test/cases/attributes_test.rb
new file mode 100644
index 0000000000..79ef0502cb
--- /dev/null
+++ b/activerecord/test/cases/attributes_test.rb
@@ -0,0 +1,111 @@
+require 'cases/helper'
+
+class OverloadedType < ActiveRecord::Base
+ attribute :overloaded_float, Type::Integer.new
+ attribute :overloaded_string_with_limit, Type::String.new(limit: 50)
+ attribute :non_existent_decimal, Type::Decimal.new
+ attribute :string_with_default, Type::String.new, default: 'the overloaded default'
+end
+
+class ChildOfOverloadedType < OverloadedType
+end
+
+class GrandchildOfOverloadedType < ChildOfOverloadedType
+ attribute :overloaded_float, Type::Float.new
+end
+
+class UnoverloadedType < ActiveRecord::Base
+ self.table_name = 'overloaded_types'
+end
+
+module ActiveRecord
+ class CustomPropertiesTest < ActiveRecord::TestCase
+ def test_overloading_types
+ data = OverloadedType.new
+
+ data.overloaded_float = "1.1"
+ data.unoverloaded_float = "1.1"
+
+ assert_equal 1, data.overloaded_float
+ assert_equal 1.1, data.unoverloaded_float
+ end
+
+ def test_overloaded_properties_save
+ data = OverloadedType.new
+
+ data.overloaded_float = "2.2"
+ data.save!
+ data.reload
+
+ assert_equal 2, data.overloaded_float
+ assert_kind_of Fixnum, OverloadedType.last.overloaded_float
+ assert_equal 2.0, UnoverloadedType.last.overloaded_float
+ assert_kind_of Float, UnoverloadedType.last.overloaded_float
+ end
+
+ def test_properties_assigned_in_constructor
+ data = OverloadedType.new(overloaded_float: '3.3')
+
+ assert_equal 3, data.overloaded_float
+ end
+
+ def test_overloaded_properties_with_limit
+ assert_equal 50, OverloadedType.columns_hash['overloaded_string_with_limit'].limit
+ assert_equal 255, UnoverloadedType.columns_hash['overloaded_string_with_limit'].limit
+ end
+
+ def test_nonexistent_attribute
+ data = OverloadedType.new(non_existent_decimal: 1)
+
+ assert_equal BigDecimal.new(1), data.non_existent_decimal
+ assert_raise ActiveRecord::UnknownAttributeError do
+ UnoverloadedType.new(non_existent_decimal: 1)
+ end
+ end
+
+ def test_changing_defaults
+ data = OverloadedType.new
+ unoverloaded_data = UnoverloadedType.new
+
+ assert_equal 'the overloaded default', data.string_with_default
+ assert_equal 'the original default', unoverloaded_data.string_with_default
+ end
+
+ def test_children_inherit_custom_properties
+ data = ChildOfOverloadedType.new(overloaded_float: '4.4')
+
+ assert_equal 4, data.overloaded_float
+ end
+
+ def test_children_can_override_parents
+ data = GrandchildOfOverloadedType.new(overloaded_float: '4.4')
+
+ assert_equal 4.4, data.overloaded_float
+ end
+
+ def test_overloading_properties_does_not_change_column_order
+ column_names = OverloadedType.column_names
+ assert_equal %w(id overloaded_float unoverloaded_float overloaded_string_with_limit string_with_default non_existent_decimal), column_names
+ end
+
+ def test_caches_are_cleared
+ klass = Class.new(OverloadedType)
+
+ assert_equal 6, klass.columns.length
+ assert_not klass.columns_hash.key?('wibble')
+ assert_equal 6, klass.column_types.length
+ assert_equal 6, klass.column_defaults.length
+ assert_not klass.column_names.include?('wibble')
+ assert_equal 5, klass.content_columns.length
+
+ klass.attribute :wibble, Type::Value.new
+
+ assert_equal 7, klass.columns.length
+ assert klass.columns_hash.key?('wibble')
+ assert_equal 7, klass.column_types.length
+ assert_equal 7, klass.column_defaults.length
+ assert klass.column_names.include?('wibble')
+ assert_equal 6, klass.content_columns.length
+ end
+ end
+end
diff --git a/activerecord/test/cases/autosave_association_test.rb b/activerecord/test/cases/autosave_association_test.rb
index f7584c3a51..09892d50ba 100644
--- a/activerecord/test/cases/autosave_association_test.rb
+++ b/activerecord/test/cases/autosave_association_test.rb
@@ -683,10 +683,23 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
end
end
+ @ship.pirate.catchphrase = "Changed Catchphrase"
+
assert_raise(RuntimeError) { assert !@pirate.save }
assert_not_nil @pirate.reload.ship
end
+ def test_should_save_changed_has_one_changed_object_if_child_is_saved
+ @pirate.ship.name = "NewName"
+ assert @pirate.save
+ assert_equal "NewName", @pirate.ship.reload.name
+ end
+
+ def test_should_not_save_changed_has_one_unchanged_object_if_child_is_saved
+ @pirate.ship.expects(:save).never
+ assert @pirate.save
+ end
+
# belongs_to
def test_should_destroy_a_parent_association_as_part_of_the_save_transaction_if_it_was_marked_for_destroyal
assert !@ship.pirate.marked_for_destruction?
diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb
index 2e5b8cffa6..4c0b0c868a 100644
--- a/activerecord/test/cases/base_test.rb
+++ b/activerecord/test/cases/base_test.rb
@@ -102,8 +102,8 @@ class BasicsTest < ActiveRecord::TestCase
end
def test_columns_should_obey_set_primary_key
- pk = Subscriber.columns.find { |x| x.name == 'nick' }
- assert pk.primary, 'nick should be primary key'
+ pk = Subscriber.columns_hash[Subscriber.primary_key]
+ assert_equal 'nick', pk.name, 'nick should be primary key'
end
def test_primary_key_with_no_id
@@ -160,19 +160,11 @@ class BasicsTest < ActiveRecord::TestCase
end
def test_preserving_date_objects
- if current_adapter?(:SybaseAdapter)
- # Sybase ctlib does not (yet?) support the date type; use datetime instead.
- assert_kind_of(
- Time, Topic.find(1).last_read,
- "The last_read attribute should be of the Time class"
- )
- else
- # Oracle enhanced adapter allows to define Date attributes in model class (see topic.rb)
- assert_kind_of(
- Date, Topic.find(1).last_read,
- "The last_read attribute should be of the Date class"
- )
- end
+ # Oracle enhanced adapter allows to define Date attributes in model class (see topic.rb)
+ assert_kind_of(
+ Date, Topic.find(1).last_read,
+ "The last_read attribute should be of the Date class"
+ )
end
def test_previously_changed
@@ -480,8 +472,8 @@ class BasicsTest < ActiveRecord::TestCase
end
end
- # Oracle, and Sybase do not have a TIME datatype.
- unless current_adapter?(:OracleAdapter, :SybaseAdapter)
+ # Oracle does not have a TIME datatype.
+ unless current_adapter?(:OracleAdapter)
def test_utc_as_time_zone
with_timezone_config default: :utc do
attributes = { "bonus_time" => "5:42:00AM" }
@@ -515,12 +507,7 @@ class BasicsTest < ActiveRecord::TestCase
topic = Topic.find(topic.id)
assert_nil topic.last_read
- # Sybase adapter does not allow nulls in boolean columns
- if current_adapter?(:SybaseAdapter)
- assert topic.approved == false
- else
- assert_nil topic.approved
- end
+ assert_nil topic.approved
end
def test_equality
@@ -531,6 +518,10 @@ class BasicsTest < ActiveRecord::TestCase
assert_equal Topic.find('1-meowmeow'), Topic.find(1)
end
+ def test_find_by_slug_with_array
+ assert_equal Topic.find(['1-meowmeow', '2-hello']), Topic.find([1, 2])
+ end
+
def test_equality_of_new_records
assert_not_equal Topic.new, Topic.new
assert_equal false, Topic.new == Topic.new
@@ -685,8 +676,8 @@ class BasicsTest < ActiveRecord::TestCase
end
def test_attributes_on_dummy_time
- # Oracle, and Sybase do not have a TIME datatype.
- return true if current_adapter?(:OracleAdapter, :SybaseAdapter)
+ # Oracle does not have a TIME datatype.
+ return true if current_adapter?(:OracleAdapter)
with_timezone_config default: :local do
attributes = {
@@ -699,8 +690,8 @@ class BasicsTest < ActiveRecord::TestCase
end
def test_attributes_on_dummy_time_with_invalid_time
- # Oracle, and Sybase do not have a TIME datatype.
- return true if current_adapter?(:OracleAdapter, :SybaseAdapter)
+ # Oracle does not have a TIME datatype.
+ return true if current_adapter?(:OracleAdapter)
attributes = {
"bonus_time" => "not a time"
@@ -787,8 +778,14 @@ class BasicsTest < ActiveRecord::TestCase
assert_equal("c", duped_topic.title)
end
+ DeveloperSalary = Struct.new(:amount)
def test_dup_with_aggregate_of_same_name_as_attribute
- dev = DeveloperWithAggregate.find(1)
+ developer_with_aggregate = Class.new(ActiveRecord::Base) do
+ self.table_name = 'developers'
+ composed_of :salary, :class_name => 'BasicsTest::DeveloperSalary', :mapping => [%w(salary amount)]
+ end
+
+ dev = developer_with_aggregate.find(1)
assert_kind_of DeveloperSalary, dev.salary
dup = nil
@@ -987,6 +984,10 @@ class BasicsTest < ActiveRecord::TestCase
class NumericData < ActiveRecord::Base
self.table_name = 'numeric_data'
+
+ attribute :world_population, Type::Integer.new
+ attribute :my_house_population, Type::Integer.new
+ attribute :atoms_in_universe, Type::Integer.new
end
def test_big_decimal_conditions
@@ -1346,14 +1347,32 @@ class BasicsTest < ActiveRecord::TestCase
end
def test_compute_type_no_method_error
- ActiveSupport::Dependencies.stubs(:constantize).raises(NoMethodError)
+ ActiveSupport::Dependencies.stubs(:safe_constantize).raises(NoMethodError)
assert_raises NoMethodError do
ActiveRecord::Base.send :compute_type, 'InvalidModel'
end
end
+ def test_compute_type_on_undefined_method
+ error = nil
+ begin
+ Class.new(Author) do
+ alias_method :foo, :bar
+ end
+ rescue => e
+ error = e
+ end
+
+ ActiveSupport::Dependencies.stubs(:safe_constantize).raises(e)
+
+ exception = assert_raises NameError do
+ ActiveRecord::Base.send :compute_type, 'InvalidModel'
+ end
+ assert_equal error.message, exception.message
+ end
+
def test_compute_type_argument_error
- ActiveSupport::Dependencies.stubs(:constantize).raises(ArgumentError)
+ ActiveSupport::Dependencies.stubs(:safe_constantize).raises(ArgumentError)
assert_raises ArgumentError do
ActiveRecord::Base.send :compute_type, 'InvalidModel'
end
@@ -1487,15 +1506,14 @@ class BasicsTest < ActiveRecord::TestCase
attrs = topic.attributes.dup
attrs.delete 'id'
- typecast = Class.new {
+ typecast = Class.new(ActiveRecord::Type::Value) {
def type_cast value
"t.lo"
end
}
types = { 'author_name' => typecast.new }
- topic = Topic.allocate.init_with 'attributes' => attrs,
- 'column_types' => types
+ topic = Topic.instantiate(attrs, types)
assert_equal 't.lo', topic.author_name
end
@@ -1596,4 +1614,11 @@ class BasicsTest < ActiveRecord::TestCase
assert_equal after_handler, new_handler
assert_equal orig_handler, klass.connection_handler
end
+
+ # Note: This is a performance optimization for Array#uniq and Hash#[] with
+ # AR::Base objects. If the future has made this irrelevant, feel free to
+ # delete this.
+ test "records without an id have unique hashes" do
+ assert_not_equal Post.new.hash, Post.new.hash
+ end
end
diff --git a/activerecord/test/cases/binary_test.rb b/activerecord/test/cases/binary_test.rb
index 9a486cf8b8..ccf2be369d 100644
--- a/activerecord/test/cases/binary_test.rb
+++ b/activerecord/test/cases/binary_test.rb
@@ -2,9 +2,9 @@
require "cases/helper"
# Without using prepared statements, it makes no sense to test
-# BLOB data with DB2 or Firebird, because the length of a statement
+# BLOB data with DB2, because the length of a statement
# is limited to 32KB.
-unless current_adapter?(:SybaseAdapter, :DB2Adapter, :FirebirdAdapter)
+unless current_adapter?(:DB2Adapter)
require 'models/binary'
class BinaryTest < ActiveRecord::TestCase
@@ -21,7 +21,7 @@ unless current_adapter?(:SybaseAdapter, :DB2Adapter, :FirebirdAdapter)
name = binary.name
- # Mysql adapter doesn't properly encode things, so we have to do it
+ # MySQL adapter doesn't properly encode things, so we have to do it
if current_adapter?(:MysqlAdapter)
name.force_encoding(Encoding::UTF_8)
end
diff --git a/activerecord/test/cases/bind_parameter_test.rb b/activerecord/test/cases/bind_parameter_test.rb
index 40f73cd68c..0bc7ee6d64 100644
--- a/activerecord/test/cases/bind_parameter_test.rb
+++ b/activerecord/test/cases/bind_parameter_test.rb
@@ -21,7 +21,7 @@ module ActiveRecord
super
@connection = ActiveRecord::Base.connection
@subscriber = LogListener.new
- @pk = Topic.columns.find { |c| c.primary }
+ @pk = Topic.columns_hash[Topic.primary_key]
@subscription = ActiveSupport::Notifications.subscribe('sql.active_record', @subscriber)
end
@@ -60,12 +60,10 @@ module ActiveRecord
end
def test_logs_bind_vars
- pk = Topic.columns.find { |x| x.primary }
-
payload = {
:name => 'SQL',
:sql => 'select * from topics where id = ?',
- :binds => [[pk, 10]]
+ :binds => [[@pk, 10]]
}
event = ActiveSupport::Notifications::Event.new(
'foo',
@@ -87,7 +85,7 @@ module ActiveRecord
}.new
logger.sql event
- assert_match([[pk.name, 10]].inspect, logger.debugs.first)
+ assert_match([[@pk.name, 10]].inspect, logger.debugs.first)
end
end
end
diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb
index b8de78934e..319ea9260a 100644
--- a/activerecord/test/cases/calculations_test.rb
+++ b/activerecord/test/cases/calculations_test.rb
@@ -15,6 +15,10 @@ Company.has_many :accounts
class NumericData < ActiveRecord::Base
self.table_name = 'numeric_data'
+
+ attribute :world_population, Type::Integer.new
+ attribute :my_house_population, Type::Integer.new
+ attribute :atoms_in_universe, Type::Integer.new
end
class CalculationsTest < ActiveRecord::TestCase
@@ -49,11 +53,6 @@ class CalculationsTest < ActiveRecord::TestCase
assert_nil NumericData.average(:bank_balance)
end
- def test_type_cast_calculated_value_should_convert_db_averages_of_fixnum_class_to_decimal
- assert_equal 0, NumericData.all.send(:type_cast_calculated_value, 0, nil, 'avg')
- assert_equal 53.0, NumericData.all.send(:type_cast_calculated_value, 53, nil, 'avg')
- end
-
def test_should_get_maximum_of_field
assert_equal 60, Account.maximum(:credit_limit)
end
@@ -606,4 +605,11 @@ class CalculationsTest < ActiveRecord::TestCase
assert_equal [1,2,3,4,5], taks_relation.pluck(:id)
assert_equal [false, true, true, true, true], taks_relation.pluck(:approved)
end
+
+ def test_pluck_columns_with_same_name
+ expected = [["The First Topic", "The Second Topic of the day"], ["The Third Topic of the day", "The Fourth Topic of the day"]]
+ actual = Topic.joins(:replies)
+ .pluck('topics.title', 'replies_topics.title')
+ assert_equal expected, actual
+ end
end
diff --git a/activerecord/test/cases/column_definition_test.rb b/activerecord/test/cases/column_definition_test.rb
index c1dd1f1c69..bcfd66b4bf 100644
--- a/activerecord/test/cases/column_definition_test.rb
+++ b/activerecord/test/cases/column_definition_test.rb
@@ -11,30 +11,10 @@ module ActiveRecord
@viz = @adapter.schema_creation
end
- def test_can_set_coder
- column = Column.new("title", nil, "varchar(20)")
- column.coder = YAML
- assert_equal YAML, column.coder
- end
-
- def test_encoded?
- column = Column.new("title", nil, "varchar(20)")
- assert !column.encoded?
-
- column.coder = YAML
- assert column.encoded?
- end
-
- def test_type_case_coded_column
- column = Column.new("title", nil, "varchar(20)")
- column.coder = YAML
- assert_equal "hello", column.type_cast("--- hello")
- end
-
# Avoid column definitions in create table statements like:
# `title` varchar(255) DEFAULT NULL
def test_should_not_include_default_clause_when_default_is_null
- column = Column.new("title", nil, "varchar(20)")
+ column = Column.new("title", nil, Type::String.new(limit: 20))
column_def = ColumnDefinition.new(
column.name, "string",
column.limit, column.precision, column.scale, column.default, column.null)
@@ -42,7 +22,7 @@ module ActiveRecord
end
def test_should_include_default_clause_when_default_is_present
- column = Column.new("title", "Hello", "varchar(20)")
+ column = Column.new("title", "Hello", Type::String.new(limit: 20))
column_def = ColumnDefinition.new(
column.name, "string",
column.limit, column.precision, column.scale, column.default, column.null)
@@ -50,7 +30,7 @@ module ActiveRecord
end
def test_should_specify_not_null_if_null_option_is_false
- column = Column.new("title", "Hello", "varchar(20)", false)
+ column = Column.new("title", "Hello", Type::String.new(limit: 20), "varchar(20)", false)
column_def = ColumnDefinition.new(
column.name, "string",
column.limit, column.precision, column.scale, column.default, column.null)
@@ -59,68 +39,68 @@ module ActiveRecord
if current_adapter?(:MysqlAdapter)
def test_should_set_default_for_mysql_binary_data_types
- binary_column = MysqlAdapter::Column.new("title", "a", "binary(1)")
+ binary_column = MysqlAdapter::Column.new("title", "a", Type::Binary.new, "binary(1)")
assert_equal "a", binary_column.default
- varbinary_column = MysqlAdapter::Column.new("title", "a", "varbinary(1)")
+ varbinary_column = MysqlAdapter::Column.new("title", "a", Type::Binary.new, "varbinary(1)")
assert_equal "a", varbinary_column.default
end
def test_should_not_set_default_for_blob_and_text_data_types
assert_raise ArgumentError do
- MysqlAdapter::Column.new("title", "a", "blob")
+ MysqlAdapter::Column.new("title", "a", Type::Binary.new, "blob")
end
assert_raise ArgumentError do
- MysqlAdapter::Column.new("title", "Hello", "text")
+ MysqlAdapter::Column.new("title", "Hello", Type::Text.new)
end
- text_column = MysqlAdapter::Column.new("title", nil, "text")
+ text_column = MysqlAdapter::Column.new("title", nil, Type::Text.new)
assert_equal nil, text_column.default
- not_null_text_column = MysqlAdapter::Column.new("title", nil, "text", false)
+ not_null_text_column = MysqlAdapter::Column.new("title", nil, Type::Text.new, "text", false)
assert_equal "", not_null_text_column.default
end
def test_has_default_should_return_false_for_blob_and_text_data_types
- blob_column = MysqlAdapter::Column.new("title", nil, "blob")
+ blob_column = MysqlAdapter::Column.new("title", nil, Type::Binary.new, "blob")
assert !blob_column.has_default?
- text_column = MysqlAdapter::Column.new("title", nil, "text")
+ text_column = MysqlAdapter::Column.new("title", nil, Type::Text.new)
assert !text_column.has_default?
end
end
if current_adapter?(:Mysql2Adapter)
def test_should_set_default_for_mysql_binary_data_types
- binary_column = Mysql2Adapter::Column.new("title", "a", "binary(1)")
+ binary_column = Mysql2Adapter::Column.new("title", "a", Type::Binary.new, "binary(1)")
assert_equal "a", binary_column.default
- varbinary_column = Mysql2Adapter::Column.new("title", "a", "varbinary(1)")
+ varbinary_column = Mysql2Adapter::Column.new("title", "a", Type::Binary.new, "varbinary(1)")
assert_equal "a", varbinary_column.default
end
def test_should_not_set_default_for_blob_and_text_data_types
assert_raise ArgumentError do
- Mysql2Adapter::Column.new("title", "a", "blob")
+ Mysql2Adapter::Column.new("title", "a", Type::Binary.new, "blob")
end
assert_raise ArgumentError do
- Mysql2Adapter::Column.new("title", "Hello", "text")
+ Mysql2Adapter::Column.new("title", "Hello", Type::Text.new)
end
- text_column = Mysql2Adapter::Column.new("title", nil, "text")
+ text_column = Mysql2Adapter::Column.new("title", nil, Type::Text.new)
assert_equal nil, text_column.default
- not_null_text_column = Mysql2Adapter::Column.new("title", nil, "text", false)
+ not_null_text_column = Mysql2Adapter::Column.new("title", nil, Type::Text.new, "text", false)
assert_equal "", not_null_text_column.default
end
def test_has_default_should_return_false_for_blob_and_text_data_types
- blob_column = Mysql2Adapter::Column.new("title", nil, "blob")
+ blob_column = Mysql2Adapter::Column.new("title", nil, Type::Binary.new, "blob")
assert !blob_column.has_default?
- text_column = Mysql2Adapter::Column.new("title", nil, "text")
+ text_column = Mysql2Adapter::Column.new("title", nil, Type::Text.new)
assert !text_column.has_default?
end
end
diff --git a/activerecord/test/cases/column_test.rb b/activerecord/test/cases/column_test.rb
deleted file mode 100644
index 2a6d8cc2ab..0000000000
--- a/activerecord/test/cases/column_test.rb
+++ /dev/null
@@ -1,123 +0,0 @@
-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')
- assert column.type_cast('t')
- assert column.type_cast('T')
- assert column.type_cast('true')
- assert column.type_cast('TRUE')
- assert column.type_cast('on')
- assert column.type_cast('ON')
-
- # 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
- column = Column.new("field", nil, "integer")
- assert_equal 1, column.type_cast(1)
- assert_equal 1, column.type_cast('1')
- assert_equal 1, column.type_cast('1ignore')
- 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_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('ABC')
-
- time_string = Time.now.utc.strftime("%T")
- assert_equal time_string, column.type_cast(time_string).strftime("%T")
- end
-
- 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")
- end
- end
-
- 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('ABC')
-
- date_string = Time.now.utc.strftime("%F")
- assert_equal date_string, column.type_cast(date_string).strftime("%F")
- end
-
- def test_type_cast_duration_to_integer
- column = Column.new("field", nil, "integer")
- 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/merge_and_resolve_default_url_config_test.rb b/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb
index da852aaa02..e1b2804a18 100644
--- a/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb
+++ b/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb
@@ -144,6 +144,18 @@ module ActiveRecord
assert_equal nil, actual[:test]
end
+ def test_database_url_with_ipv6_host_and_port
+ ENV['DATABASE_URL'] = "postgres://[::1]:5454/foo"
+
+ config = {}
+ actual = resolve_config(config)
+ expected = { "adapter" => "postgresql",
+ "database" => "foo",
+ "host" => "::1",
+ "port" => 5454 }
+ assert_equal expected, actual["default_env"]
+ end
+
def test_url_sub_key_with_database_url
ENV['DATABASE_URL'] = "NOT-POSTGRES://localhost/NOT_FOO"
diff --git a/activerecord/test/cases/connection_adapters/mysql_type_lookup_test.rb b/activerecord/test/cases/connection_adapters/mysql_type_lookup_test.rb
new file mode 100644
index 0000000000..d4d67487db
--- /dev/null
+++ b/activerecord/test/cases/connection_adapters/mysql_type_lookup_test.rb
@@ -0,0 +1,61 @@
+require "cases/helper"
+
+if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
+module ActiveRecord
+ module ConnectionAdapters
+ class MysqlTypeLookupTest < ActiveRecord::TestCase
+ setup do
+ @connection = ActiveRecord::Base.connection
+ end
+
+ def test_boolean_types
+ emulate_booleans(true) do
+ assert_lookup_type :boolean, 'tinyint(1)'
+ assert_lookup_type :boolean, 'TINYINT(1)'
+ end
+ end
+
+ def test_string_types
+ assert_lookup_type :string, "enum('one', 'two', 'three')"
+ assert_lookup_type :string, "ENUM('one', 'two', 'three')"
+ assert_lookup_type :string, "set('one', 'two', 'three')"
+ assert_lookup_type :string, "SET('one', 'two', 'three')"
+ end
+
+ def test_binary_types
+ assert_lookup_type :binary, 'bit'
+ assert_lookup_type :binary, 'BIT'
+ end
+
+ def test_integer_types
+ emulate_booleans(false) do
+ assert_lookup_type :integer, 'tinyint(1)'
+ assert_lookup_type :integer, 'TINYINT(1)'
+ assert_lookup_type :integer, 'year'
+ assert_lookup_type :integer, 'YEAR'
+ end
+ end
+
+ private
+
+ def assert_lookup_type(type, lookup)
+ cast_type = @connection.type_map.lookup(lookup)
+ assert_equal type, cast_type.type
+ end
+
+ def emulate_booleans(value)
+ old_emulate_booleans = @connection.emulate_booleans
+ change_emulate_booleans(value)
+ yield
+ ensure
+ change_emulate_booleans(old_emulate_booleans)
+ end
+
+ def change_emulate_booleans(value)
+ @connection.emulate_booleans = value
+ @connection.clear_cache!
+ end
+ 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 ecad7c942f..c7531f5418 100644
--- a/activerecord/test/cases/connection_adapters/schema_cache_test.rb
+++ b/activerecord/test/cases/connection_adapters/schema_cache_test.rb
@@ -45,8 +45,8 @@ module ActiveRecord
@cache = Marshal.load(Marshal.dump(@cache))
- assert_equal 12, @cache.columns('posts').size
- assert_equal 12, @cache.columns_hash('posts').size
+ assert_equal 11, @cache.columns('posts').size
+ assert_equal 11, @cache.columns_hash('posts').size
assert @cache.tables('posts')
assert_equal 'id', @cache.primary_keys('posts')
end
diff --git a/activerecord/test/cases/connection_adapters/type_lookup_test.rb b/activerecord/test/cases/connection_adapters/type_lookup_test.rb
new file mode 100644
index 0000000000..d5c1dc1e5d
--- /dev/null
+++ b/activerecord/test/cases/connection_adapters/type_lookup_test.rb
@@ -0,0 +1,101 @@
+require "cases/helper"
+
+unless current_adapter?(:PostgreSQLAdapter) # PostgreSQL does not use type strigns for lookup
+module ActiveRecord
+ module ConnectionAdapters
+ class TypeLookupTest < ActiveRecord::TestCase
+ setup do
+ @connection = ActiveRecord::Base.connection
+ end
+
+ def test_boolean_types
+ assert_lookup_type :boolean, 'boolean'
+ assert_lookup_type :boolean, 'BOOLEAN'
+ end
+
+ def test_string_types
+ assert_lookup_type :string, 'char'
+ assert_lookup_type :string, 'varchar'
+ assert_lookup_type :string, 'VARCHAR'
+ assert_lookup_type :string, 'varchar(255)'
+ assert_lookup_type :string, 'character varying'
+ end
+
+ def test_binary_types
+ assert_lookup_type :binary, 'binary'
+ assert_lookup_type :binary, 'BINARY'
+ assert_lookup_type :binary, 'blob'
+ assert_lookup_type :binary, 'BLOB'
+ end
+
+ def test_text_types
+ assert_lookup_type :text, 'text'
+ assert_lookup_type :text, 'TEXT'
+ assert_lookup_type :text, 'clob'
+ assert_lookup_type :text, 'CLOB'
+ end
+
+ def test_date_types
+ assert_lookup_type :date, 'date'
+ assert_lookup_type :date, 'DATE'
+ end
+
+ def test_time_types
+ assert_lookup_type :time, 'time'
+ assert_lookup_type :time, 'TIME'
+ end
+
+ def test_datetime_types
+ assert_lookup_type :datetime, 'datetime'
+ assert_lookup_type :datetime, 'DATETIME'
+ assert_lookup_type :datetime, 'timestamp'
+ assert_lookup_type :datetime, 'TIMESTAMP'
+ end
+
+ def test_decimal_types
+ assert_lookup_type :decimal, 'decimal'
+ assert_lookup_type :decimal, 'decimal(2,8)'
+ assert_lookup_type :decimal, 'DECIMAL'
+ assert_lookup_type :decimal, 'numeric'
+ assert_lookup_type :decimal, 'numeric(2,8)'
+ assert_lookup_type :decimal, 'NUMERIC'
+ assert_lookup_type :decimal, 'number'
+ assert_lookup_type :decimal, 'number(2,8)'
+ assert_lookup_type :decimal, 'NUMBER'
+ end
+
+ def test_float_types
+ assert_lookup_type :float, 'float'
+ assert_lookup_type :float, 'FLOAT'
+ assert_lookup_type :float, 'double'
+ assert_lookup_type :float, 'DOUBLE'
+ end
+
+ def test_integer_types
+ assert_lookup_type :integer, 'integer'
+ assert_lookup_type :integer, 'INTEGER'
+ assert_lookup_type :integer, 'tinyint'
+ assert_lookup_type :integer, 'smallint'
+ assert_lookup_type :integer, 'bigint'
+ end
+
+ def test_decimal_without_scale
+ types = %w{decimal(2) decimal(2,0) numeric(2) numeric(2,0) number(2) number(2,0)}
+ types.each do |type|
+ cast_type = @connection.type_map.lookup(type)
+
+ assert_equal :decimal, cast_type.type
+ assert_equal 2, cast_type.type_cast_from_user(2.1)
+ end
+ end
+
+ private
+
+ def assert_lookup_type(type, lookup)
+ cast_type = @connection.type_map.lookup(lookup)
+ assert_equal type, cast_type.type
+ end
+ end
+ end
+end
+end
diff --git a/activerecord/test/cases/core_test.rb b/activerecord/test/cases/core_test.rb
index 2a52bf574c..715d92af99 100644
--- a/activerecord/test/cases/core_test.rb
+++ b/activerecord/test/cases/core_test.rb
@@ -1,6 +1,8 @@
require 'cases/helper'
require 'models/person'
require 'models/topic'
+require 'pp'
+require 'active_support/core_ext/string/strip'
class NonExistentTable < ActiveRecord::Base; end
@@ -30,4 +32,70 @@ class CoreTest < ActiveRecord::TestCase
def test_inspect_class_without_table
assert_equal "NonExistentTable(Table doesn't exist)", NonExistentTable.inspect
end
+
+ def test_pretty_print_new
+ topic = Topic.new
+ actual = ''
+ PP.pp(topic, StringIO.new(actual))
+ expected = <<-PRETTY.strip_heredoc
+ #<Topic:0xXXXXXX
+ id: nil,
+ title: nil,
+ author_name: nil,
+ author_email_address: "test@test.com",
+ written_on: nil,
+ bonus_time: nil,
+ last_read: nil,
+ content: nil,
+ important: nil,
+ approved: true,
+ replies_count: 0,
+ unique_replies_count: 0,
+ parent_id: nil,
+ parent_title: nil,
+ type: nil,
+ group: nil,
+ created_at: nil,
+ updated_at: nil>
+ PRETTY
+ assert actual.start_with?(expected.split('XXXXXX').first)
+ assert actual.end_with?(expected.split('XXXXXX').last)
+ end
+
+ def test_pretty_print_persisted
+ topic = topics(:first)
+ actual = ''
+ PP.pp(topic, StringIO.new(actual))
+ expected = <<-PRETTY.strip_heredoc
+ #<Topic:0x\\w+
+ id: 1,
+ title: "The First Topic",
+ author_name: "David",
+ author_email_address: "david@loudthinking.com",
+ written_on: 2003-07-16 14:28:11 UTC,
+ bonus_time: 2000-01-01 14:28:00 UTC,
+ last_read: Thu, 15 Apr 2004,
+ content: "Have a nice day",
+ important: nil,
+ approved: false,
+ replies_count: 1,
+ unique_replies_count: 0,
+ parent_id: nil,
+ parent_title: nil,
+ type: nil,
+ group: nil,
+ created_at: [^,]+,
+ updated_at: [^,>]+>
+ PRETTY
+ assert_match(/\A#{expected}\z/, actual)
+ end
+
+ def test_pretty_print_uninitialized
+ topic = Topic.allocate
+ actual = ''
+ PP.pp(topic, StringIO.new(actual))
+ expected = "#<Topic:XXXXXX not initialized>\n"
+ assert actual.start_with?(expected.split('XXXXXX').first)
+ assert actual.end_with?(expected.split('XXXXXX').last)
+ end
end
diff --git a/activerecord/test/cases/counter_cache_test.rb b/activerecord/test/cases/counter_cache_test.rb
index ee3d8a81c2..07a182070b 100644
--- a/activerecord/test/cases/counter_cache_test.rb
+++ b/activerecord/test/cases/counter_cache_test.rb
@@ -19,6 +19,7 @@ class CounterCacheTest < ActiveRecord::TestCase
class ::SpecialTopic < ::Topic
has_many :special_replies, :foreign_key => 'parent_id'
+ has_many :lightweight_special_replies, -> { select('topics.id, topics.title') }, :foreign_key => 'parent_id', :class_name => 'SpecialReply'
end
class ::SpecialReply < ::Reply
@@ -51,6 +52,16 @@ class CounterCacheTest < ActiveRecord::TestCase
end
end
+ test "reset counters by counter name" do
+ # throw the count off by 1
+ Topic.increment_counter(:replies_count, @topic.id)
+
+ # check that it gets reset
+ assert_difference '@topic.reload.replies_count', -1 do
+ Topic.reset_counters(@topic.id, :replies_count)
+ 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
@@ -154,10 +165,19 @@ class CounterCacheTest < ActiveRecord::TestCase
end
end
- test "the passed symbol needs to be an association name" do
+ test "the passed symbol needs to be an association name or counter name" do
e = assert_raises(ArgumentError) do
- Topic.reset_counters(@topic.id, :replies_count)
+ Topic.reset_counters(@topic.id, :undefined_count)
+ end
+ assert_equal "'Topic' has no association called 'undefined_count'", e.message
+ end
+
+ test "reset counter works with select declared on association" do
+ special = SpecialTopic.create!(:title => 'Special')
+ SpecialTopic.increment_counter(:replies_count, special.id)
+
+ assert_difference 'special.reload.replies_count', -1 do
+ SpecialTopic.reset_counters(special.id, :lightweight_special_replies)
end
- assert_equal "'Topic' has no association called 'replies_count'", e.message
end
end
diff --git a/activerecord/test/cases/defaults_test.rb b/activerecord/test/cases/defaults_test.rb
index 7d438803a1..c089e63128 100644
--- a/activerecord/test/cases/defaults_test.rb
+++ b/activerecord/test/cases/defaults_test.rb
@@ -18,7 +18,7 @@ class DefaultTest < ActiveRecord::TestCase
end
end
- if current_adapter?(:PostgreSQLAdapter, :FirebirdAdapter, :OpenBaseAdapter, :OracleAdapter)
+ if current_adapter?(:PostgreSQLAdapter, :OracleAdapter)
def test_default_integers
default = Default.new
assert_instance_of Fixnum, default.positive_integer
@@ -154,7 +154,7 @@ if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
t.column :omit, :integer, :null => false
end
- assert_equal 0, klass.columns_hash['zero'].default
+ assert_equal '0', klass.columns_hash['zero'].default
assert !klass.columns_hash['zero'].null
# 0 in MySQL 4, nil in 5.
assert [0, nil].include?(klass.columns_hash['omit'].default)
@@ -206,6 +206,11 @@ if current_adapter?(:PostgreSQLAdapter)
assert_equal "some text", Default.new.text_col, "Default of text column was not correctly parse after updating default using '::text' since postgreSQL will add parens to the default in db"
end
+ def test_default_containing_quote_and_colons
+ @connection.execute "ALTER TABLE defaults ALTER COLUMN string_col SET DEFAULT 'foo''::bar'"
+ assert_equal "foo'::bar", Default.new.string_col
+ end
+
teardown do
@connection.schema_search_path = @old_search_path
Default.reset_column_information
diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb
index df4183c065..69a7f25213 100644
--- a/activerecord/test/cases/dirty_test.rb
+++ b/activerecord/test/cases/dirty_test.rb
@@ -169,7 +169,19 @@ class DirtyTest < ActiveRecord::TestCase
pirate = Pirate.create!(:catchphrase => 'Yar!')
pirate.catchphrase = 'Ahoy!'
- pirate.reset_catchphrase!
+ assert_deprecated do
+ pirate.reset_catchphrase!
+ end
+ assert_equal "Yar!", pirate.catchphrase
+ assert_equal Hash.new, pirate.changes
+ assert !pirate.catchphrase_changed?
+ end
+
+ def test_restore_attribute!
+ pirate = Pirate.create!(:catchphrase => 'Yar!')
+ pirate.catchphrase = 'Ahoy!'
+
+ pirate.restore_catchphrase!
assert_equal "Yar!", pirate.catchphrase
assert_equal Hash.new, pirate.changes
assert !pirate.catchphrase_changed?
@@ -309,16 +321,14 @@ class DirtyTest < ActiveRecord::TestCase
def test_attribute_will_change!
pirate = Pirate.create!(:catchphrase => 'arr')
- pirate.catchphrase << ' matey'
assert !pirate.catchphrase_changed?
-
assert pirate.catchphrase_will_change!
assert pirate.catchphrase_changed?
- assert_equal ['arr matey', 'arr matey'], pirate.catchphrase_change
+ assert_equal ['arr', 'arr'], pirate.catchphrase_change
- pirate.catchphrase << '!'
+ pirate.catchphrase << ' matey!'
assert pirate.catchphrase_changed?
- assert_equal ['arr matey', 'arr matey!'], pirate.catchphrase_change
+ assert_equal ['arr', 'arr matey!'], pirate.catchphrase_change
end
def test_association_assignment_changes_foreign_key
@@ -400,7 +410,7 @@ class DirtyTest < ActiveRecord::TestCase
def test_dup_objects_should_not_copy_dirty_flag_from_creator
pirate = Pirate.create!(:catchphrase => "shiver me timbers")
pirate_dup = pirate.dup
- pirate_dup.reset_catchphrase!
+ pirate_dup.restore_catchphrase!
pirate.catchphrase = "I love Rum"
assert pirate.catchphrase_changed?
assert !pirate_dup.catchphrase_changed?
@@ -445,11 +455,20 @@ class DirtyTest < ActiveRecord::TestCase
def test_save_should_store_serialized_attributes_even_with_partial_writes
with_partial_writes(Topic) do
topic = Topic.create!(:content => {:a => "a"})
+
+ assert_not topic.changed?
+
topic.content[:b] = "b"
- #assert topic.changed? # Known bug, will fail
+
+ assert topic.changed?
+
topic.save!
+
+ assert_not topic.changed?
assert_equal "b", topic.content[:b]
+
topic.reload
+
assert_equal "b", topic.content[:b]
end
end
@@ -616,6 +635,32 @@ class DirtyTest < ActiveRecord::TestCase
end
end
+ test "defaults with type that implements `type_cast_for_database`" do
+ type = Class.new(ActiveRecord::Type::Value) do
+ def type_cast(value)
+ value.to_i
+ end
+
+ def type_cast_for_database(value)
+ value.to_s
+ end
+ end
+
+ model_class = Class.new(ActiveRecord::Base) do
+ self.table_name = 'numeric_data'
+ attribute :foo, type.new, default: 1
+ end
+
+ model = model_class.new
+ assert_not model.foo_changed?
+
+ model = model_class.new(foo: 1)
+ assert_not model.foo_changed?
+
+ model = model_class.new(foo: '1')
+ assert_not model.foo_changed?
+ end
+
private
def with_partial_writes(klass, on = true)
old = klass.partial_writes?
diff --git a/activerecord/test/cases/dup_test.rb b/activerecord/test/cases/dup_test.rb
index 1e6ccecfab..638cffe0e6 100644
--- a/activerecord/test/cases/dup_test.rb
+++ b/activerecord/test/cases/dup_test.rb
@@ -1,4 +1,5 @@
require "cases/helper"
+require 'models/reply'
require 'models/topic'
module ActiveRecord
@@ -32,6 +33,14 @@ module ActiveRecord
assert duped.new_record?, 'topic is new'
end
+ def test_dup_not_destroyed
+ topic = Topic.first
+ topic.destroy
+
+ duped = topic.dup
+ assert_not duped.destroyed?
+ end
+
def test_dup_has_no_id
topic = Topic.first
duped = topic.dup
@@ -132,5 +141,17 @@ module ActiveRecord
ensure
Topic.default_scopes = prev_default_scopes
end
+
+ def test_dup_without_primary_key
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = 'parrots_pirates'
+ end
+
+ record = klass.create!
+
+ assert_nothing_raised do
+ record.dup
+ end
+ end
end
end
diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb
index c0440744e9..40e51a0cdc 100644
--- a/activerecord/test/cases/finder_test.rb
+++ b/activerecord/test/cases/finder_test.rb
@@ -33,6 +33,17 @@ class FinderTest < ActiveRecord::TestCase
assert_equal(topics(:first).title, Topic.find(1).title)
end
+ def test_find_with_proc_parameter_and_block
+ exception = assert_raises(RuntimeError) do
+ Topic.all.find(-> { raise "should happen" }) { |e| e.title == "non-existing-title" }
+ end
+ assert_equal "should happen", exception.message
+
+ assert_nothing_raised(RuntimeError) do
+ Topic.all.find(-> { raise "should not happen" }) { |e| e.title == topics(:first).title }
+ end
+ end
+
def test_find_passing_active_record_object_is_deprecated
assert_deprecated do
Topic.find(Topic.last)
@@ -133,8 +144,8 @@ class FinderTest < ActiveRecord::TestCase
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?
+ assert_equal false, author.unique_categorized_posts.includes(:special_comments).order('comments.tags_count DESC').limit(0).exists?
+ assert_equal true, author.unique_categorized_posts.includes(:special_comments).order('comments.tags_count DESC').limit(1).exists?
end
def test_exists_with_empty_table_and_no_args_given
diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb
index 8bbc0af758..042fdaf0bb 100644
--- a/activerecord/test/cases/fixtures_test.rb
+++ b/activerecord/test/cases/fixtures_test.rb
@@ -84,12 +84,6 @@ 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"])
diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb
index eaf2cada9d..6a8aff4b69 100644
--- a/activerecord/test/cases/helper.rb
+++ b/activerecord/test/cases/helper.rb
@@ -9,6 +9,7 @@ require 'active_record'
require 'cases/test_case'
require 'active_support/dependencies'
require 'active_support/logger'
+require 'active_support/core_ext/string/strip'
require 'support/config'
require 'support/connection'
@@ -198,3 +199,5 @@ module InTimeZone
ActiveRecord::Base.time_zone_aware_attributes = old_tz
end
end
+
+require 'mocha/setup' # FIXME: stop using mocha
diff --git a/activerecord/test/cases/inheritance_test.rb b/activerecord/test/cases/inheritance_test.rb
index f5f85f2412..792950d24d 100644
--- a/activerecord/test/cases/inheritance_test.rb
+++ b/activerecord/test/cases/inheritance_test.rb
@@ -95,16 +95,8 @@ class InheritanceTest < ActiveRecord::TestCase
end
def test_a_bad_type_column
- #SQLServer need to turn Identity Insert On before manually inserting into the Identity column
- if current_adapter?(:SybaseAdapter)
- Company.connection.execute "SET IDENTITY_INSERT companies ON"
- end
Company.connection.insert "INSERT INTO companies (id, #{QUOTED_TYPE}, name) VALUES(100, 'bad_class!', 'Not happening')"
- #We then need to turn it back Off before continuing.
- if current_adapter?(:SybaseAdapter)
- Company.connection.execute "SET IDENTITY_INSERT companies OFF"
- end
assert_raise(ActiveRecord::SubclassNotFound) { Company.find(100) }
end
diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb
index c373dc1511..0c9dff2c25 100644
--- a/activerecord/test/cases/locking_test.rb
+++ b/activerecord/test/cases/locking_test.rb
@@ -273,8 +273,11 @@ class OptimisticLockingTest < ActiveRecord::TestCase
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 }
+ def test_yaml_dumping_with_lock_column
+ t1 = LockWithoutDefault.new
+ t2 = YAML.load(YAML.dump(t1))
+
+ assert_equal t1.attributes, t2.attributes
end
end
@@ -339,8 +342,6 @@ class OptimisticLockingWithSchemaChangeTest < ActiveRecord::TestCase
def add_counter_column_to(model, col='test_count')
model.connection.add_column model.table_name, col, :integer, :null => false, :default => 0
model.reset_column_information
- # OpenBase does not set a value to existing rows when adding a not null default column
- model.update_all(col => 0) if current_adapter?(:OpenBaseAdapter)
end
def remove_counter_column_from(model, col = :test_count)
@@ -367,7 +368,7 @@ 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.)
-unless current_adapter?(:SybaseAdapter, :OpenBaseAdapter) || in_memory_db?
+unless in_memory_db?
class PessimisticLockingTest < ActiveRecord::TestCase
self.use_transactional_fixtures = false
fixtures :people, :readers
diff --git a/activerecord/test/cases/log_subscriber_test.rb b/activerecord/test/cases/log_subscriber_test.rb
index 97c0350911..a578e81844 100644
--- a/activerecord/test/cases/log_subscriber_test.rb
+++ b/activerecord/test/cases/log_subscriber_test.rb
@@ -125,5 +125,12 @@ class LogSubscriberTest < ActiveRecord::TestCase
wait
assert_match(/<16 bytes of binary data>/, @logger.logged(:debug).join)
end
+
+ def test_nil_binary_data_is_logged
+ binary = Binary.create(data: "")
+ binary.update_attributes(data: nil)
+ wait
+ assert_match(/<NULL binary data>/, @logger.logged(:debug).join)
+ end
end
end
diff --git a/activerecord/test/cases/migration/change_schema_test.rb b/activerecord/test/cases/migration/change_schema_test.rb
index 5418d913b0..c66eaf1ee1 100644
--- a/activerecord/test/cases/migration/change_schema_test.rb
+++ b/activerecord/test/cases/migration/change_schema_test.rb
@@ -14,6 +14,7 @@ module ActiveRecord
teardown do
connection.drop_table :testings rescue nil
ActiveRecord::Base.primary_key_prefix_type = nil
+ ActiveRecord::Base.clear_cache!
end
def test_create_table_without_id
@@ -67,9 +68,9 @@ module ActiveRecord
five = columns.detect { |c| c.name == "five" } unless mysql
assert_equal "hello", one.default
- assert_equal true, two.default
- assert_equal false, three.default
- assert_equal 1, four.default
+ assert_equal true, two.type_cast_from_database(two.default)
+ assert_equal false, three.type_cast_from_database(three.default)
+ assert_equal '1', four.default
assert_equal "hello", five.default unless mysql
end
@@ -204,9 +205,9 @@ module ActiveRecord
connection.create_table table_name
end
- # Sybase, and SQLite3 will not allow you to add a NOT NULL
+ # SQLite3 will not allow you to add a NOT NULL
# column to a table without a default value.
- unless current_adapter?(:SybaseAdapter, :SQLite3Adapter)
+ unless current_adapter?(:SQLite3Adapter)
def test_add_column_not_null_without_default
connection.create_table :testings do |t|
t.column :foo, :string
@@ -225,18 +226,28 @@ module ActiveRecord
end
con = connection
- connection.enable_identity_insert("testings", true) if current_adapter?(:SybaseAdapter)
connection.execute "insert into testings (#{con.quote_column_name('id')}, #{con.quote_column_name('foo')}) values (1, 'hello')"
- connection.enable_identity_insert("testings", false) if current_adapter?(:SybaseAdapter)
assert_nothing_raised {connection.add_column :testings, :bar, :string, :null => false, :default => "default" }
assert_raises(ActiveRecord::StatementInvalid) do
- unless current_adapter?(:OpenBaseAdapter)
- connection.execute "insert into testings (#{con.quote_column_name('id')}, #{con.quote_column_name('foo')}, #{con.quote_column_name('bar')}) values (2, 'hello', NULL)"
- else
- connection.insert("INSERT INTO testings (#{con.quote_column_name('id')}, #{con.quote_column_name('foo')}, #{con.quote_column_name('bar')}) VALUES (2, 'hello', NULL)",
- "Testing Insert","id",2)
- end
+ connection.execute "insert into testings (#{con.quote_column_name('id')}, #{con.quote_column_name('foo')}, #{con.quote_column_name('bar')}) values (2, 'hello', NULL)"
+ end
+ end
+
+ def test_add_column_with_timestamp_type
+ connection.create_table :testings do |t|
+ t.column :foo, :timestamp
+ end
+
+ klass = Class.new(ActiveRecord::Base)
+ klass.table_name = 'testings'
+
+ assert_equal :datetime, klass.columns_hash['foo'].type
+
+ if current_adapter?(:PostgreSQLAdapter)
+ assert_equal 'timestamp without time zone', klass.columns_hash['foo'].sql_type
+ else
+ assert_equal klass.connection.type_to_sql('datetime'), klass.columns_hash['foo'].sql_type
end
end
@@ -264,7 +275,7 @@ module ActiveRecord
person_klass.connection.add_column "testings", "wealth", :integer, :null => false, :default => 99
person_klass.reset_column_information
- assert_equal 99, person_klass.columns_hash["wealth"].default
+ assert_equal 99, person_klass.column_defaults["wealth"]
assert_equal false, person_klass.columns_hash["wealth"].null
# Oracle needs primary key value from sequence
if current_adapter?(:OracleAdapter)
@@ -276,20 +287,20 @@ module ActiveRecord
# change column default to see that column doesn't lose its not null definition
person_klass.connection.change_column_default "testings", "wealth", 100
person_klass.reset_column_information
- assert_equal 100, person_klass.columns_hash["wealth"].default
+ assert_equal 100, person_klass.column_defaults["wealth"]
assert_equal false, person_klass.columns_hash["wealth"].null
# rename column to see that column doesn't lose its not null and/or default definition
person_klass.connection.rename_column "testings", "wealth", "money"
person_klass.reset_column_information
assert_nil person_klass.columns_hash["wealth"]
- assert_equal 100, person_klass.columns_hash["money"].default
+ assert_equal 100, person_klass.column_defaults["money"]
assert_equal false, person_klass.columns_hash["money"].null
# change column
person_klass.connection.change_column "testings", "money", :integer, :null => false, :default => 1000
person_klass.reset_column_information
- assert_equal 1000, person_klass.columns_hash["money"].default
+ assert_equal 1000, person_klass.column_defaults["money"]
assert_equal false, person_klass.columns_hash["money"].null
# change column, make it nullable and clear default
diff --git a/activerecord/test/cases/migration/change_table_test.rb b/activerecord/test/cases/migration/change_table_test.rb
index a6d506b04a..3e9d957ed3 100644
--- a/activerecord/test/cases/migration/change_table_test.rb
+++ b/activerecord/test/cases/migration/change_table_test.rb
@@ -72,6 +72,20 @@ module ActiveRecord
end
end
+ def test_references_column_type_with_polymorphic_and_type
+ with_change_table do |t|
+ @connection.expect :add_reference, nil, [:delete_me, :taggable, polymorphic: true, type: :string]
+ t.references :taggable, polymorphic: true, type: :string
+ end
+ end
+
+ def test_remove_references_column_type_with_polymorphic_and_type
+ with_change_table do |t|
+ @connection.expect :remove_reference, nil, [:delete_me, :taggable, polymorphic: true, type: :string]
+ t.remove_references :taggable, polymorphic: true, type: :string
+ end
+ end
+
def test_timestamps_creates_updated_at_and_created_at
with_change_table do |t|
@connection.expect :add_timestamps, nil, [:delete_me]
diff --git a/activerecord/test/cases/migration/column_attributes_test.rb b/activerecord/test/cases/migration/column_attributes_test.rb
index 6a02873cba..763aa88f72 100644
--- a/activerecord/test/cases/migration/column_attributes_test.rb
+++ b/activerecord/test/cases/migration/column_attributes_test.rb
@@ -51,46 +51,46 @@ module ActiveRecord
end
end
- # We specifically do a manual INSERT here, and then test only the SELECT
- # functionality. This allows us to more easily catch INSERT being broken,
- # but SELECT actually working fine.
- def test_native_decimal_insert_manual_vs_automatic
- correct_value = '0012345678901234567890.0123456789'.to_d
-
- connection.add_column "test_models", "wealth", :decimal, :precision => '30', :scale => '10'
-
- # Do a manual insertion
- if current_adapter?(:OracleAdapter)
- connection.execute "insert into test_models (id, wealth) values (people_seq.nextval, 12345678901234567890.0123456789)"
- elsif current_adapter?(:OpenBaseAdapter) || (current_adapter?(:MysqlAdapter) && Mysql.client_version < 50003) #before mysql 5.0.3 decimals stored as strings
- connection.execute "insert into test_models (wealth) values ('12345678901234567890.0123456789')"
- elsif current_adapter?(:PostgreSQLAdapter)
- connection.execute "insert into test_models (wealth) values (12345678901234567890.0123456789)"
- else
- connection.execute "insert into test_models (wealth) values (12345678901234567890.0123456789)"
- end
+ unless current_adapter?(:SQLite3Adapter)
+ # We specifically do a manual INSERT here, and then test only the SELECT
+ # functionality. This allows us to more easily catch INSERT being broken,
+ # but SELECT actually working fine.
+ def test_native_decimal_insert_manual_vs_automatic
+ correct_value = '0012345678901234567890.0123456789'.to_d
+
+ connection.add_column "test_models", "wealth", :decimal, :precision => '30', :scale => '10'
+
+ # Do a manual insertion
+ if current_adapter?(:OracleAdapter)
+ connection.execute "insert into test_models (id, wealth) values (people_seq.nextval, 12345678901234567890.0123456789)"
+ elsif current_adapter?(:MysqlAdapter) && Mysql.client_version < 50003 #before MySQL 5.0.3 decimals stored as strings
+ connection.execute "insert into test_models (wealth) values ('12345678901234567890.0123456789')"
+ elsif current_adapter?(:PostgreSQLAdapter)
+ connection.execute "insert into test_models (wealth) values (12345678901234567890.0123456789)"
+ else
+ connection.execute "insert into test_models (wealth) values (12345678901234567890.0123456789)"
+ end
- # SELECT
- row = TestModel.first
- assert_kind_of BigDecimal, row.wealth
+ # SELECT
+ row = TestModel.first
+ assert_kind_of BigDecimal, row.wealth
- # If this assert fails, that means the SELECT is broken!
- unless current_adapter?(:SQLite3Adapter)
- assert_equal correct_value, row.wealth
- end
+ # If this assert fails, that means the SELECT is broken!
+ unless current_adapter?(:SQLite3Adapter)
+ assert_equal correct_value, row.wealth
+ end
- # Reset to old state
- TestModel.delete_all
+ # Reset to old state
+ TestModel.delete_all
- # Now use the Rails insertion
- TestModel.create :wealth => BigDecimal.new("12345678901234567890.0123456789")
+ # Now use the Rails insertion
+ TestModel.create :wealth => BigDecimal.new("12345678901234567890.0123456789")
- # SELECT
- row = TestModel.first
- assert_kind_of BigDecimal, row.wealth
+ # SELECT
+ row = TestModel.first
+ assert_kind_of BigDecimal, row.wealth
- # If these asserts fail, that means the INSERT (create function, or cast to SQL) is broken!
- unless current_adapter?(:SQLite3Adapter)
+ # If these asserts fail, that means the INSERT (create function, or cast to SQL) is broken!
assert_equal correct_value, row.wealth
end
end
@@ -121,54 +121,54 @@ module ActiveRecord
end
end
- def test_native_types
- add_column "test_models", "first_name", :string
- add_column "test_models", "last_name", :string
- add_column "test_models", "bio", :text
- add_column "test_models", "age", :integer
- add_column "test_models", "height", :float
- add_column "test_models", "wealth", :decimal, :precision => '30', :scale => '10'
- add_column "test_models", "birthday", :datetime
- add_column "test_models", "favorite_day", :date
- add_column "test_models", "moment_of_truth", :datetime
- add_column "test_models", "male", :boolean
-
- TestModel.create :first_name => 'bob', :last_name => 'bobsen',
- :bio => "I was born ....", :age => 18, :height => 1.78,
- :wealth => BigDecimal.new("12345678901234567890.0123456789"),
- :birthday => 18.years.ago, :favorite_day => 10.days.ago,
- :moment_of_truth => "1782-10-10 21:40:18", :male => true
-
- bob = TestModel.first
- assert_equal 'bob', bob.first_name
- assert_equal 'bobsen', bob.last_name
- assert_equal "I was born ....", bob.bio
- assert_equal 18, bob.age
-
- # Test for 30 significant digits (beyond the 16 of float), 10 of them
- # after the decimal place.
-
- unless current_adapter?(:SQLite3Adapter)
+ unless current_adapter?(:SQLite3Adapter)
+ def test_native_types
+ add_column "test_models", "first_name", :string
+ add_column "test_models", "last_name", :string
+ add_column "test_models", "bio", :text
+ add_column "test_models", "age", :integer
+ add_column "test_models", "height", :float
+ add_column "test_models", "wealth", :decimal, :precision => '30', :scale => '10'
+ add_column "test_models", "birthday", :datetime
+ add_column "test_models", "favorite_day", :date
+ add_column "test_models", "moment_of_truth", :datetime
+ add_column "test_models", "male", :boolean
+
+ TestModel.create :first_name => 'bob', :last_name => 'bobsen',
+ :bio => "I was born ....", :age => 18, :height => 1.78,
+ :wealth => BigDecimal.new("12345678901234567890.0123456789"),
+ :birthday => 18.years.ago, :favorite_day => 10.days.ago,
+ :moment_of_truth => "1782-10-10 21:40:18", :male => true
+
+ bob = TestModel.first
+ assert_equal 'bob', bob.first_name
+ assert_equal 'bobsen', bob.last_name
+ assert_equal "I was born ....", bob.bio
+ assert_equal 18, bob.age
+
+ # Test for 30 significant digits (beyond the 16 of float), 10 of them
+ # after the decimal place.
+
assert_equal BigDecimal.new("0012345678901234567890.0123456789"), bob.wealth
- end
- assert_equal true, bob.male?
+ assert_equal true, bob.male?
- assert_equal String, bob.first_name.class
- assert_equal String, bob.last_name.class
- assert_equal String, bob.bio.class
- assert_equal Fixnum, bob.age.class
- assert_equal Time, bob.birthday.class
+ assert_equal String, bob.first_name.class
+ assert_equal String, bob.last_name.class
+ assert_equal String, bob.bio.class
+ assert_equal Fixnum, bob.age.class
+ assert_equal Time, bob.birthday.class
- if current_adapter?(:OracleAdapter, :SybaseAdapter)
- # Sybase, and Oracle don't differentiate between date/time
- assert_equal Time, bob.favorite_day.class
- else
- assert_equal Date, bob.favorite_day.class
- end
+ if current_adapter?(:OracleAdapter)
+ # Oracle doesn't differentiate between date/time
+ assert_equal Time, bob.favorite_day.class
+ else
+ assert_equal Date, bob.favorite_day.class
+ end
- assert_instance_of TrueClass, bob.male?
- assert_kind_of BigDecimal, bob.wealth
+ assert_instance_of TrueClass, bob.male?
+ assert_kind_of BigDecimal, bob.wealth
+ end
end
if current_adapter?(:MysqlAdapter, :Mysql2Adapter, :PostgreSQLAdapter)
diff --git a/activerecord/test/cases/migration/columns_test.rb b/activerecord/test/cases/migration/columns_test.rb
index 2d7a7ec73a..4e6d7963aa 100644
--- a/activerecord/test/cases/migration/columns_test.rb
+++ b/activerecord/test/cases/migration/columns_test.rb
@@ -53,13 +53,13 @@ module ActiveRecord
add_column 'test_models', 'salary', :integer, :default => 70000
default_before = connection.columns("test_models").find { |c| c.name == "salary" }.default
- assert_equal 70000, default_before
+ assert_equal '70000', default_before
rename_column "test_models", "salary", "annual_salary"
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
+ assert_equal '70000', default_after
end
if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
@@ -193,14 +193,21 @@ module ActiveRecord
old_columns = connection.columns(TestModel.table_name)
assert old_columns.find { |c|
- c.name == 'approved' && c.type == :boolean && c.default == true
+ default = c.type_cast_from_database(c.default)
+ c.name == 'approved' && c.type == :boolean && default == true
}
change_column :test_models, :approved, :boolean, :default => false
new_columns = connection.columns(TestModel.table_name)
- 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 }
+ assert_not new_columns.find { |c|
+ default = c.type_cast_from_database(c.default)
+ c.name == 'approved' and c.type == :boolean and default == true
+ }
+ assert new_columns.find { |c|
+ default = c.type_cast_from_database(c.default)
+ c.name == 'approved' and c.type == :boolean and default == false
+ }
change_column :test_models, :approved, :boolean, :default => true
end
@@ -274,6 +281,16 @@ module ActiveRecord
ensure
connection.drop_table(:my_table) rescue nil
end
+
+ def test_column_with_index
+ connection.create_table "my_table", force: true do |t|
+ t.string :item_number, index: true
+ end
+
+ assert connection.index_exists?("my_table", :item_number, name: :index_my_table_on_item_number)
+ 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 a925cf4c05..e955beae1a 100644
--- a/activerecord/test/cases/migration/command_recorder_test.rb
+++ b/activerecord/test/cases/migration/command_recorder_test.rb
@@ -157,6 +157,23 @@ module ActiveRecord
assert_equal [:remove_column, [:table, :column, :type, {}], nil], remove
end
+ def test_invert_change_column
+ assert_raises(ActiveRecord::IrreversibleMigration) do
+ @recorder.inverse_of :change_column, [:table, :column, :type, {}]
+ end
+ end
+
+ def test_invert_change_column_default
+ assert_raises(ActiveRecord::IrreversibleMigration) do
+ @recorder.inverse_of :change_column_default, [:table, :column, 'default_value']
+ end
+ end
+
+ def test_invert_change_column_null
+ add = @recorder.inverse_of :change_column_null, [:table, :column, true]
+ assert_equal [:change_column_null, [:table, :column, false]], add
+ end
+
def test_invert_remove_column
add = @recorder.inverse_of :remove_column, [:table, :column, :type, {}]
assert_equal [:add_column, [:table, :column, :type, {}], nil], add
@@ -253,6 +270,31 @@ module ActiveRecord
enable = @recorder.inverse_of :disable_extension, ['uuid-ossp']
assert_equal [:enable_extension, ['uuid-ossp'], nil], enable
end
+
+ def test_invert_add_foreign_key
+ enable = @recorder.inverse_of :add_foreign_key, [:dogs, :people]
+ assert_equal [:remove_foreign_key, [:dogs, :people]], enable
+ end
+
+ def test_invert_add_foreign_key_with_column
+ enable = @recorder.inverse_of :add_foreign_key, [:dogs, :people, column: "owner_id"]
+ assert_equal [:remove_foreign_key, [:dogs, column: "owner_id"]], enable
+ end
+
+ def test_invert_add_foreign_key_with_column_and_name
+ enable = @recorder.inverse_of :add_foreign_key, [:dogs, :people, column: "owner_id", name: "fk"]
+ assert_equal [:remove_foreign_key, [:dogs, name: "fk"]], enable
+ end
+
+ def test_remove_foreign_key_is_irreversible
+ assert_raises ActiveRecord::IrreversibleMigration do
+ @recorder.inverse_of :remove_foreign_key, [:dogs, column: "owner_id"]
+ end
+
+ assert_raises ActiveRecord::IrreversibleMigration do
+ @recorder.inverse_of :remove_foreign_key, [:dogs, name: "fk"]
+ end
+ end
end
end
end
diff --git a/activerecord/test/cases/migration/create_join_table_test.rb b/activerecord/test/cases/migration/create_join_table_test.rb
index 62b60f7f7b..bea9d6b2c9 100644
--- a/activerecord/test/cases/migration/create_join_table_test.rb
+++ b/activerecord/test/cases/migration/create_join_table_test.rb
@@ -119,6 +119,30 @@ module ActiveRecord
assert !connection.tables.include?('artists_musics')
end
+
+ def test_create_and_drop_join_table_with_common_prefix
+ with_table_cleanup do
+ connection.create_join_table 'audio_artists', 'audio_musics'
+ assert_includes connection.tables, 'audio_artists_musics'
+
+ connection.drop_join_table 'audio_artists', 'audio_musics'
+ assert !connection.tables.include?('audio_artists_musics'), "Should have dropped join table, but didn't"
+ end
+ end
+
+ private
+
+ def with_table_cleanup
+ tables_before = connection.tables
+
+ yield
+ ensure
+ tables_after = connection.tables - tables_before
+
+ tables_after.each do |table|
+ connection.execute "DROP TABLE #{table}"
+ end
+ end
end
end
end
diff --git a/activerecord/test/cases/migration/foreign_key_test.rb b/activerecord/test/cases/migration/foreign_key_test.rb
new file mode 100644
index 0000000000..c985092b4c
--- /dev/null
+++ b/activerecord/test/cases/migration/foreign_key_test.rb
@@ -0,0 +1,242 @@
+require 'cases/helper'
+require 'support/ddl_helper'
+require 'support/schema_dumping_helper'
+
+if ActiveRecord::Base.connection.supports_foreign_keys?
+module ActiveRecord
+ class Migration
+ class ForeignKeyTest < ActiveRecord::TestCase
+ include DdlHelper
+ include SchemaDumpingHelper
+
+ class Rocket < ActiveRecord::Base
+ end
+
+ class Astronaut < ActiveRecord::Base
+ end
+
+ setup do
+ @connection = ActiveRecord::Base.connection
+ @connection.create_table "rockets" do |t|
+ t.string :name
+ end
+
+ @connection.create_table "astronauts" do |t|
+ t.string :name
+ t.references :rocket
+ end
+ end
+
+ teardown do
+ if defined?(@connection)
+ @connection.execute "DROP TABLE IF EXISTS astronauts"
+ @connection.execute "DROP TABLE IF EXISTS rockets"
+ end
+ end
+
+ def test_foreign_keys
+ foreign_keys = @connection.foreign_keys("fk_test_has_fk")
+ assert_equal 1, foreign_keys.size
+
+ fk = foreign_keys.first
+ assert_equal "fk_test_has_fk", fk.from_table
+ assert_equal "fk_test_has_pk", fk.to_table
+ assert_equal "fk_id", fk.column
+ assert_equal "pk_id", fk.primary_key
+ assert_equal "fk_name", fk.name
+ end
+
+ def test_add_foreign_key_inferes_column
+ @connection.add_foreign_key :astronauts, :rockets
+
+ foreign_keys = @connection.foreign_keys("astronauts")
+ assert_equal 1, foreign_keys.size
+
+ fk = foreign_keys.first
+ assert_equal "astronauts", fk.from_table
+ assert_equal "rockets", fk.to_table
+ assert_equal "rocket_id", fk.column
+ assert_equal "id", fk.primary_key
+ assert_match(/^fk_rails_.{10}$/, fk.name)
+ end
+
+ def test_add_foreign_key_with_column
+ @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id"
+
+ foreign_keys = @connection.foreign_keys("astronauts")
+ assert_equal 1, foreign_keys.size
+
+ fk = foreign_keys.first
+ assert_equal "astronauts", fk.from_table
+ assert_equal "rockets", fk.to_table
+ assert_equal "rocket_id", fk.column
+ assert_equal "id", fk.primary_key
+ assert_match(/^fk_rails_.{10}$/, fk.name)
+ end
+
+ def test_add_foreign_key_with_non_standard_primary_key
+ with_example_table @connection, "space_shuttles", "pk integer PRIMARY KEY" do
+ @connection.add_foreign_key(:astronauts, :space_shuttles,
+ column: "rocket_id", primary_key: "pk", name: "custom_pk")
+
+ foreign_keys = @connection.foreign_keys("astronauts")
+ assert_equal 1, foreign_keys.size
+
+ fk = foreign_keys.first
+ assert_equal "astronauts", fk.from_table
+ assert_equal "space_shuttles", fk.to_table
+ assert_equal "pk", fk.primary_key
+
+ @connection.remove_foreign_key :astronauts, name: "custom_pk"
+ end
+ end
+
+ def test_add_on_delete_restrict_foreign_key
+ @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_delete: :restrict
+
+ foreign_keys = @connection.foreign_keys("astronauts")
+ assert_equal 1, foreign_keys.size
+
+ fk = foreign_keys.first
+ if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
+ # ON DELETE RESTRICT is the default on MySQL
+ assert_equal nil, fk.on_delete
+ else
+ assert_equal :restrict, fk.on_delete
+ end
+ end
+
+ def test_add_on_delete_cascade_foreign_key
+ @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_delete: :cascade
+
+ foreign_keys = @connection.foreign_keys("astronauts")
+ assert_equal 1, foreign_keys.size
+
+ fk = foreign_keys.first
+ assert_equal :cascade, fk.on_delete
+ end
+
+ def test_add_on_delete_nullify_foreign_key
+ @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_delete: :nullify
+
+ foreign_keys = @connection.foreign_keys("astronauts")
+ assert_equal 1, foreign_keys.size
+
+ fk = foreign_keys.first
+ assert_equal :nullify, fk.on_delete
+ end
+
+ def test_on_update_and_on_delete_raises_with_invalid_values
+ assert_raises ArgumentError do
+ @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_delete: :invalid
+ end
+
+ assert_raises ArgumentError do
+ @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_update: :invalid
+ end
+ end
+
+ def test_add_foreign_key_with_on_update
+ @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_update: :nullify
+
+ foreign_keys = @connection.foreign_keys("astronauts")
+ assert_equal 1, foreign_keys.size
+
+ fk = foreign_keys.first
+ assert_equal :nullify, fk.on_update
+ end
+
+ def test_remove_foreign_key_inferes_column
+ @connection.add_foreign_key :astronauts, :rockets
+
+ assert_equal 1, @connection.foreign_keys("astronauts").size
+ @connection.remove_foreign_key :astronauts, :rockets
+ assert_equal [], @connection.foreign_keys("astronauts")
+ end
+
+ def test_remove_foreign_key_by_column
+ @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id"
+
+ assert_equal 1, @connection.foreign_keys("astronauts").size
+ @connection.remove_foreign_key :astronauts, column: "rocket_id"
+ assert_equal [], @connection.foreign_keys("astronauts")
+ end
+
+ def test_remove_foreign_key_by_name
+ @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", name: "fancy_named_fk"
+
+ assert_equal 1, @connection.foreign_keys("astronauts").size
+ @connection.remove_foreign_key :astronauts, name: "fancy_named_fk"
+ assert_equal [], @connection.foreign_keys("astronauts")
+ end
+
+ def test_remove_foreign_non_existing_foreign_key_raises
+ assert_raises ArgumentError do
+ @connection.remove_foreign_key :astronauts, :rockets
+ end
+ end
+
+ def test_schema_dumping
+ @connection.add_foreign_key :astronauts, :rockets
+ output = dump_table_schema "astronauts"
+ assert_match %r{\s+add_foreign_key "astronauts", "rockets"$}, output
+ end
+
+ def test_schema_dumping_with_options
+ output = dump_table_schema "fk_test_has_fk"
+ assert_match %r{\s+add_foreign_key "fk_test_has_fk", "fk_test_has_pk", column: "fk_id", primary_key: "pk_id", name: "fk_name"$}, output
+ end
+
+ def test_schema_dumping_on_delete_and_on_update_options
+ @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_delete: :nullify, on_update: :cascade
+
+ output = dump_table_schema "astronauts"
+ assert_match %r{\s+add_foreign_key "astronauts",.+on_update: :cascade,.+on_delete: :nullify$}, output
+ end
+
+ class CreateCitiesAndHousesMigration < ActiveRecord::Migration
+ def change
+ create_table("cities") { |t| }
+
+ create_table("houses") do |t|
+ t.column :city_id, :integer
+ end
+ add_foreign_key :houses, :cities, column: "city_id"
+ end
+ end
+
+ def test_add_foreign_key_is_reversible
+ migration = CreateCitiesAndHousesMigration.new
+ silence_stream($stdout) { migration.migrate(:up) }
+ assert_equal 1, @connection.foreign_keys("houses").size
+ ensure
+ silence_stream($stdout) { migration.migrate(:down) }
+ end
+ end
+ end
+end
+else
+module ActiveRecord
+ class Migration
+ class NoForeignKeySupportTest < ActiveRecord::TestCase
+ setup do
+ @connection = ActiveRecord::Base.connection
+ end
+
+ def test_add_foreign_key_should_be_noop
+ @connection.add_foreign_key :clubs, :categories
+ end
+
+ def test_remove_foreign_key_should_be_noop
+ @connection.remove_foreign_key :clubs, :categories
+ end
+
+ def test_foreign_keys_should_raise_not_implemented
+ assert_raises NotImplementedError do
+ @connection.foreign_keys("clubs")
+ end
+ end
+ end
+ end
+end
+end
diff --git a/activerecord/test/cases/migration/index_test.rb b/activerecord/test/cases/migration/index_test.rb
index 35af11f672..93c3bfae7a 100644
--- a/activerecord/test/cases/migration/index_test.rb
+++ b/activerecord/test/cases/migration/index_test.rb
@@ -26,28 +26,25 @@ module ActiveRecord
ActiveRecord::Base.primary_key_prefix_type = nil
end
- unless current_adapter?(:OpenBaseAdapter)
- def test_rename_index
- # keep the names short to make Oracle and similar behave
- connection.add_index(table_name, [:foo], :name => 'old_idx')
- connection.rename_index(table_name, 'old_idx', 'new_idx')
-
- # if the adapter doesn't support the indexes call, pick defaults that let the test pass
- assert_not connection.index_name_exists?(table_name, 'old_idx', false)
- assert connection.index_name_exists?(table_name, 'new_idx', true)
- end
+ def test_rename_index
+ # keep the names short to make Oracle and similar behave
+ connection.add_index(table_name, [:foo], :name => 'old_idx')
+ connection.rename_index(table_name, 'old_idx', 'new_idx')
+
+ # if the adapter doesn't support the indexes call, pick defaults that let the test pass
+ assert_not connection.index_name_exists?(table_name, 'old_idx', false)
+ assert connection.index_name_exists?(table_name, 'new_idx', true)
+ end
- def test_double_add_index
+ def test_double_add_index
+ connection.add_index(table_name, [:foo], :name => 'some_idx')
+ assert_raises(ArgumentError) {
connection.add_index(table_name, [:foo], :name => 'some_idx')
- assert_raises(ArgumentError) {
- connection.add_index(table_name, [:foo], :name => 'some_idx')
- }
- end
+ }
+ end
- def test_remove_nonexistent_index
- # we do this by name, so OpenBase is a wash as noted above
- assert_raise(ArgumentError) { connection.remove_index(table_name, "no_such_index") }
- end
+ def test_remove_nonexistent_index
+ assert_raise(ArgumentError) { connection.remove_index(table_name, "no_such_index") }
end
def test_add_index_works_with_long_index_names
@@ -126,50 +123,37 @@ module ActiveRecord
connection.add_index("testings", "last_name")
connection.remove_index("testings", "last_name")
- # Orcl nds shrt indx nms. Sybs 2.
- # OpenBase does not have named indexes. You must specify a single column name
- unless current_adapter?(:SybaseAdapter, :OpenBaseAdapter)
+ connection.add_index("testings", ["last_name", "first_name"])
+ connection.remove_index("testings", :column => ["last_name", "first_name"])
+
+ # Oracle adapter cannot have specified index name larger than 30 characters
+ # Oracle adapter is shortening index name when just column list is given
+ unless current_adapter?(:OracleAdapter)
connection.add_index("testings", ["last_name", "first_name"])
- connection.remove_index("testings", :column => ["last_name", "first_name"])
-
- # Oracle adapter cannot have specified index name larger than 30 characters
- # Oracle adapter is shortening index name when just column list is given
- unless current_adapter?(:OracleAdapter)
- connection.add_index("testings", ["last_name", "first_name"])
- connection.remove_index("testings", :name => :index_testings_on_last_name_and_first_name)
- connection.add_index("testings", ["last_name", "first_name"])
- connection.remove_index("testings", "last_name_and_first_name")
- end
+ connection.remove_index("testings", :name => :index_testings_on_last_name_and_first_name)
connection.add_index("testings", ["last_name", "first_name"])
- connection.remove_index("testings", ["last_name", "first_name"])
+ connection.remove_index("testings", "last_name_and_first_name")
+ end
+ connection.add_index("testings", ["last_name", "first_name"])
+ connection.remove_index("testings", ["last_name", "first_name"])
- connection.add_index("testings", ["last_name"], :length => 10)
- connection.remove_index("testings", "last_name")
+ connection.add_index("testings", ["last_name"], :length => 10)
+ connection.remove_index("testings", "last_name")
- connection.add_index("testings", ["last_name"], :length => {:last_name => 10})
- connection.remove_index("testings", ["last_name"])
+ connection.add_index("testings", ["last_name"], :length => {:last_name => 10})
+ connection.remove_index("testings", ["last_name"])
- connection.add_index("testings", ["last_name", "first_name"], :length => 10)
- connection.remove_index("testings", ["last_name", "first_name"])
+ connection.add_index("testings", ["last_name", "first_name"], :length => 10)
+ connection.remove_index("testings", ["last_name", "first_name"])
- connection.add_index("testings", ["last_name", "first_name"], :length => {:last_name => 10, :first_name => 20})
- connection.remove_index("testings", ["last_name", "first_name"])
- end
+ connection.add_index("testings", ["last_name", "first_name"], :length => {:last_name => 10, :first_name => 20})
+ connection.remove_index("testings", ["last_name", "first_name"])
- # quoting
- # Note: changed index name from "key" to "key_idx" since "key" is a Firebird reserved word
- # OpenBase does not have named indexes. You must specify a single column name
- unless current_adapter?(:OpenBaseAdapter)
- connection.add_index("testings", ["key"], :name => "key_idx", :unique => true)
- connection.remove_index("testings", :name => "key_idx", :unique => true)
- end
+ connection.add_index("testings", ["key"], :name => "key_idx", :unique => true)
+ connection.remove_index("testings", :name => "key_idx", :unique => true)
- # Sybase adapter does not support indexes on :boolean columns
- # OpenBase does not have named indexes. You must specify a single column
- unless current_adapter?(:SybaseAdapter, :OpenBaseAdapter)
- connection.add_index("testings", %w(last_name first_name administrator), :name => "named_admin")
- connection.remove_index("testings", :name => "named_admin")
- end
+ connection.add_index("testings", %w(last_name first_name administrator), :name => "named_admin")
+ connection.remove_index("testings", :name => "named_admin")
# Selected adapters support index sort order
if current_adapter?(:SQLite3Adapter, :MysqlAdapter, :Mysql2Adapter, :PostgreSQLAdapter)
diff --git a/activerecord/test/cases/migration/logger_test.rb b/activerecord/test/cases/migration/logger_test.rb
index 84224e6e4c..319d3e1af3 100644
--- a/activerecord/test/cases/migration/logger_test.rb
+++ b/activerecord/test/cases/migration/logger_test.rb
@@ -3,7 +3,7 @@ require "cases/helper"
module ActiveRecord
class Migration
class LoggerTest < ActiveRecord::TestCase
- # mysql can't roll back ddl changes
+ # MySQL can't roll back ddl changes
self.use_transactional_fixtures = false
Migration = Struct.new(:name, :version) do
diff --git a/activerecord/test/cases/migration/pending_migrations_test.rb b/activerecord/test/cases/migration/pending_migrations_test.rb
new file mode 100644
index 0000000000..517ee695ce
--- /dev/null
+++ b/activerecord/test/cases/migration/pending_migrations_test.rb
@@ -0,0 +1,53 @@
+require 'cases/helper'
+require "minitest/mock"
+
+module ActiveRecord
+ class Migration
+ class PendingMigrationsTest < ActiveRecord::TestCase
+ def setup
+ super
+ @connection = MiniTest::Mock.new
+ @app = MiniTest::Mock.new
+ conn = @connection
+ @pending = Class.new(CheckPending) {
+ define_method(:connection) { conn }
+ }.new(@app)
+ @pending.instance_variable_set :@last_check, -1 # Force checking
+ end
+
+ def teardown
+ assert @connection.verify
+ assert @app.verify
+ super
+ end
+
+ def test_errors_if_pending
+ @connection.expect :supports_migrations?, true
+
+ ActiveRecord::Migrator.stub :needs_migration?, true do
+ assert_raise ActiveRecord::PendingMigrationError do
+ @pending.call(nil)
+ end
+ end
+ end
+
+ def test_checks_if_supported
+ @connection.expect :supports_migrations?, true
+ @app.expect :call, nil, [:foo]
+
+ ActiveRecord::Migrator.stub :needs_migration?, false do
+ @pending.call(:foo)
+ end
+ end
+
+ def test_doesnt_check_if_unsupported
+ @connection.expect :supports_migrations?, false
+ @app.expect :call, nil, [:foo]
+
+ ActiveRecord::Migrator.stub :needs_migration?, true do
+ @pending.call(:foo)
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/migration/references_statements_test.rb b/activerecord/test/cases/migration/references_statements_test.rb
index e9545f2cce..b8b4fa1135 100644
--- a/activerecord/test/cases/migration/references_statements_test.rb
+++ b/activerecord/test/cases/migration/references_statements_test.rb
@@ -55,6 +55,11 @@ module ActiveRecord
assert index_exists?(table_name, :tag_id, name: 'index_taggings_on_tag_id')
end
+ def test_creates_reference_id_with_specified_type
+ add_reference table_name, :user, type: :string
+ assert column_exists?(table_name, :user_id, :string)
+ end
+
def test_deletes_reference_id_column
remove_reference table_name, :supplier
assert_not column_exists?(table_name, :supplier_id, :integer)
diff --git a/activerecord/test/cases/migration/rename_table_test.rb b/activerecord/test/cases/migration/rename_table_test.rb
index 2a7fafc559..ba39fb1dec 100644
--- a/activerecord/test/cases/migration/rename_table_test.rb
+++ b/activerecord/test/cases/migration/rename_table_test.rb
@@ -42,13 +42,8 @@ module ActiveRecord
def test_rename_table
rename_table :test_models, :octopi
- # Using explicit id in insert for compatibility across all databases
- connection.enable_identity_insert("octopi", true) if current_adapter?(:SybaseAdapter)
-
connection.execute "INSERT INTO octopi (#{connection.quote_column_name('id')}, #{connection.quote_column_name('url')}) VALUES (1, 'http://www.foreverflying.com/octopus-black7.jpg')"
- connection.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")
end
@@ -57,10 +52,7 @@ module ActiveRecord
rename_table :test_models, :octopi
- # Using explicit id in insert for compatibility across all databases
- connection.enable_identity_insert("octopi", true) if current_adapter?(:SybaseAdapter)
connection.execute "INSERT INTO octopi (#{connection.quote_column_name('id')}, #{connection.quote_column_name('url')}) VALUES (1, 'http://www.foreverflying.com/octopus-black7.jpg')"
- connection.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")
index = connection.indexes(:octopi).first
@@ -82,7 +74,17 @@ module ActiveRecord
pk, seq = connection.pk_and_sequence_for('octopi')
- assert_equal "octopi_#{pk}_seq", seq
+ assert_equal ConnectionAdapters::PostgreSQL::Name.new("public", "octopi_#{pk}_seq"), seq
+ end
+
+ def test_renaming_table_doesnt_attempt_to_rename_non_existent_sequences
+ enable_uuid_ossp!(connection)
+ connection.create_table :cats, id: :uuid
+ assert_nothing_raised { rename_table :cats, :felines }
+ assert connection.table_exists? :felines
+ ensure
+ connection.drop_table :cats if connection.table_exists? :cats
+ connection.drop_table :felines if connection.table_exists? :felines
end
end
end
diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb
index 455ec78f68..ef3f073472 100644
--- a/activerecord/test/cases/migration_test.rb
+++ b/activerecord/test/cases/migration_test.rb
@@ -11,7 +11,13 @@ require MIGRATIONS_ROOT + "/rename/1_we_need_things"
require MIGRATIONS_ROOT + "/rename/2_rename_things"
require MIGRATIONS_ROOT + "/decimal/1_give_me_big_numbers"
-class BigNumber < ActiveRecord::Base; end
+class BigNumber < ActiveRecord::Base
+ unless current_adapter?(:PostgreSQLAdapter, :SQLite3Adapter)
+ attribute :value_of_e, Type::Integer.new
+ end
+ attribute :world_population, Type::Integer.new
+ attribute :my_house_population, Type::Integer.new
+end
class Reminder < ActiveRecord::Base; end
@@ -57,7 +63,7 @@ class MigrationTest < ActiveRecord::TestCase
end
Person.connection.remove_column("people", "first_name") rescue nil
Person.connection.remove_column("people", "middle_name") rescue nil
- Person.connection.add_column("people", "first_name", :string, :limit => 40)
+ Person.connection.add_column("people", "first_name", :string)
Person.reset_column_information
end
@@ -327,47 +333,6 @@ class MigrationTest < ActiveRecord::TestCase
Reminder.reset_table_name
end
- def test_proper_table_name_on_migrator
- reminder_class = new_isolated_reminder_class
- 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_class)
- end
- reminder_class.reset_table_name
- assert_deprecated do
- assert_equal reminder_class.table_name, ActiveRecord::Migrator.proper_table_name(reminder_class)
- end
-
- # 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_class.table_name_prefix = 'prefix_'
- reminder_class.table_name_suffix = '_suffix'
- reminder_class.reset_table_name
- assert_deprecated do
- assert_equal "prefix_reminders_suffix", ActiveRecord::Migrator.proper_table_name(reminder_class)
- end
- reminder_class.table_name_prefix = ''
- reminder_class.table_name_suffix = ''
- reminder_class.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_class.reset_table_name
- 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
reminder_class = new_isolated_reminder_class
migration = ActiveRecord::Migration.new
@@ -602,7 +567,7 @@ if ActiveRecord::Base.connection.supports_bulk_alter?
assert_equal 8, columns.size
[:name, :qualification, :experience].each {|s| assert_equal :string, column(s).type }
- assert_equal 0, column(:age).default
+ assert_equal '0', column(:age).default
end
def test_removing_columns
@@ -930,4 +895,14 @@ class CopyMigrationsTest < ActiveRecord::TestCase
ensure
ActiveRecord::Base.logger = old
end
+
+ private
+
+ def quietly
+ silence_stream(STDOUT) do
+ silence_stream(STDERR) do
+ yield
+ end
+ end
+ end
end
diff --git a/activerecord/test/cases/modules_test.rb b/activerecord/test/cases/modules_test.rb
index f7db195521..e87773df94 100644
--- a/activerecord/test/cases/modules_test.rb
+++ b/activerecord/test/cases/modules_test.rb
@@ -112,6 +112,34 @@ class ModulesTest < ActiveRecord::TestCase
classes.each(&:reset_table_name)
end
+ def test_module_table_name_suffix
+ assert_equal 'companies_suffixed', MyApplication::Business::Suffixed::Company.table_name, 'inferred table_name for ActiveRecord model in module with table_name_suffix'
+ assert_equal 'companies_suffixed', MyApplication::Business::Suffixed::Nested::Company.table_name, 'table_name for ActiveRecord model in nested module with a parent table_name_suffix'
+ assert_equal 'companies', MyApplication::Business::Suffixed::Firm.table_name, 'explicit table_name for ActiveRecord model in module with table_name_suffix should not be suffixed'
+ end
+
+ def test_module_table_name_suffix_with_global_suffix
+ classes = [ MyApplication::Business::Company,
+ MyApplication::Business::Firm,
+ MyApplication::Business::Client,
+ MyApplication::Business::Client::Contact,
+ MyApplication::Business::Developer,
+ MyApplication::Business::Project,
+ MyApplication::Business::Suffixed::Company,
+ MyApplication::Business::Suffixed::Nested::Company,
+ MyApplication::Billing::Account ]
+
+ ActiveRecord::Base.table_name_suffix = '_global'
+ classes.each(&:reset_table_name)
+ assert_equal 'companies_global', MyApplication::Business::Company.table_name, 'inferred table_name for ActiveRecord model in module without table_name_suffix'
+ assert_equal 'companies_suffixed', MyApplication::Business::Suffixed::Company.table_name, 'inferred table_name for ActiveRecord model in module with table_name_suffix'
+ assert_equal 'companies_suffixed', MyApplication::Business::Suffixed::Nested::Company.table_name, 'table_name for ActiveRecord model in nested module with a parent table_name_suffix'
+ assert_equal 'companies', MyApplication::Business::Suffixed::Firm.table_name, 'explicit table_name for ActiveRecord model in module with table_name_suffix should not be suffixed'
+ ensure
+ ActiveRecord::Base.table_name_suffix = ''
+ classes.each(&:reset_table_name)
+ end
+
def test_compute_type_can_infer_class_name_of_sibling_inside_module
old = ActiveRecord::Base.store_full_sti_class
ActiveRecord::Base.store_full_sti_class = true
diff --git a/activerecord/test/cases/multiparameter_attributes_test.rb b/activerecord/test/cases/multiparameter_attributes_test.rb
index c70a8f296f..14d4ef457d 100644
--- a/activerecord/test/cases/multiparameter_attributes_test.rb
+++ b/activerecord/test/cases/multiparameter_attributes_test.rb
@@ -240,8 +240,8 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase
Topic.skip_time_zone_conversion_for_attributes = []
end
- # Oracle, and Sybase do not have a TIME datatype.
- unless current_adapter?(:OracleAdapter, :SybaseAdapter)
+ # Oracle does not have a TIME datatype.
+ unless current_adapter?(:OracleAdapter)
def test_multiparameter_attributes_on_time_only_column_with_time_zone_aware_attributes_does_not_do_time_zone_conversion
with_timezone_config default: :utc, aware_attributes: true, zone: -28800 do
attributes = {
diff --git a/activerecord/test/cases/persistence_test.rb b/activerecord/test/cases/persistence_test.rb
index 9209672ac5..2170fe6118 100644
--- a/activerecord/test/cases/persistence_test.rb
+++ b/activerecord/test/cases/persistence_test.rb
@@ -1,4 +1,5 @@
require "cases/helper"
+require 'models/aircraft'
require 'models/post'
require 'models/comment'
require 'models/author'
@@ -19,7 +20,7 @@ require 'models/toy'
require 'rexml/document'
class PersistenceTest < ActiveRecord::TestCase
- fixtures :topics, :companies, :developers, :projects, :computers, :accounts, :minimalistics, 'warehouse-things', :authors, :categorizations, :categories, :posts, :minivans, :pets, :toys
+ fixtures :topics, :companies, :developers, :projects, :computers, :accounts, :minimalistics, 'warehouse-things', :authors, :author_addresses, :categorizations, :categories, :posts, :minivans, :pets, :toys
# Oracle UPDATE does not support ORDER BY
unless current_adapter?(:OracleAdapter)
@@ -234,19 +235,12 @@ class PersistenceTest < ActiveRecord::TestCase
end
def test_save_with_duping_of_destroyed_object
- developer = Developer.create(name: "Kuldeep")
+ developer = Developer.first
developer.destroy
new_developer = developer.dup
new_developer.save
assert new_developer.persisted?
- end
-
- def test_dup_of_destroyed_object_is_not_destroyed
- developer = Developer.create(name: "Kuldeep")
- developer.destroy
- new_developer = developer.dup
- new_developer.save
- assert_equal new_developer.destroyed?, false
+ assert_not new_developer.destroyed?
end
def test_create_many
@@ -256,11 +250,9 @@ class PersistenceTest < ActiveRecord::TestCase
end
def test_create_columns_not_equal_attributes
- topic = Topic.allocate.init_with(
- 'attributes' => {
- 'title' => 'Another New Topic',
- 'does_not_exist' => 'test'
- }
+ topic = Topic.instantiate(
+ 'title' => 'Another New Topic',
+ 'does_not_exist' => 'test'
)
assert_nothing_raised { topic.save }
end
@@ -306,10 +298,7 @@ class PersistenceTest < ActiveRecord::TestCase
topic.title = "Still another topic"
topic.save
- topic_reloaded = Topic.allocate
- topic_reloaded.init_with(
- 'attributes' => topic.attributes.merge('does_not_exist' => 'test')
- )
+ topic_reloaded = Topic.instantiate(topic.attributes.merge('does_not_exist' => 'test'))
topic_reloaded.title = 'A New Topic'
assert_nothing_raised { topic_reloaded.save }
end
@@ -339,6 +328,15 @@ class PersistenceTest < ActiveRecord::TestCase
assert_equal "Reply", topic.type
end
+ def test_update_sti_subclass_type
+ assert_instance_of Topic, topics(:first)
+
+ reply = topics(:first).becomes!(Reply)
+ assert_instance_of Reply, reply
+ reply.save!
+ assert_instance_of Reply, Reply.find(reply.id)
+ end
+
def test_update_after_create
klass = Class.new(Topic) do
def self.name; 'Topic'; end
@@ -511,14 +509,14 @@ class PersistenceTest < ActiveRecord::TestCase
def test_update_column_should_not_leave_the_object_dirty
topic = Topic.find(1)
- topic.update_column("content", "Have a nice day")
+ topic.update_column("content", "--- Have a nice day\n...\n")
topic.reload
- topic.update_column(:content, "You too")
+ topic.update_column(:content, "--- You too\n...\n")
assert_equal [], topic.changed
topic.reload
- topic.update_column("content", "Have a nice day")
+ topic.update_column("content", "--- Have a nice day\n...\n")
assert_equal [], topic.changed
end
@@ -602,14 +600,14 @@ class PersistenceTest < ActiveRecord::TestCase
def test_update_columns_should_not_leave_the_object_dirty
topic = Topic.find(1)
- topic.update({ "content" => "Have a nice day", :author_name => "Jose" })
+ topic.update({ "content" => "--- Have a nice day\n...\n", :author_name => "Jose" })
topic.reload
- topic.update_columns({ content: "You too", "author_name" => "Sebastian" })
+ topic.update_columns({ content: "--- You too\n...\n", "author_name" => "Sebastian" })
assert_equal [], topic.changed
topic.reload
- topic.update_columns({ content: "Have a nice day", author_name: "Jose" })
+ topic.update_columns({ content: "--- Have a nice day\n...\n", author_name: "Jose" })
assert_equal [], topic.changed
end
@@ -836,4 +834,47 @@ class PersistenceTest < ActiveRecord::TestCase
end
end
+ def test_persist_inherited_class_with_different_table_name
+ minimalistic_aircrafts = Class.new(Minimalistic) do
+ self.table_name = "aircraft"
+ end
+
+ assert_difference "Aircraft.count", 1 do
+ aircraft = minimalistic_aircrafts.create(name: "Wright Flyer")
+ aircraft.name = "Wright Glider"
+ aircraft.save
+ end
+
+ assert_equal "Wright Glider", Aircraft.last.name
+ end
+
+ def test_instantiate_creates_a_new_instance
+ post = Post.instantiate("title" => "appropriate documentation", "type" => "SpecialPost")
+ assert_equal "appropriate documentation", post.title
+ assert_instance_of SpecialPost, post
+
+ # body was not initialized
+ assert_raises ActiveModel::MissingAttributeError do
+ post.body
+ end
+ end
+
+ def test_reload_removes_custom_selects
+ post = Post.select('posts.*, 1 as wibble').last!
+
+ assert_equal 1, post[:wibble]
+ assert_nil post.reload[:wibble]
+ end
+
+ def test_find_via_reload
+ post = Post.new
+
+ assert post.new_record?
+
+ post.id = 1
+ post.reload
+
+ assert_equal "Welcome to the weblog", post.title
+ assert_not post.new_record?
+ end
end
diff --git a/activerecord/test/cases/pooled_connections_test.rb b/activerecord/test/cases/pooled_connections_test.rb
index dd0e934ec2..8eea10143f 100644
--- a/activerecord/test/cases/pooled_connections_test.rb
+++ b/activerecord/test/cases/pooled_connections_test.rb
@@ -48,4 +48,4 @@ class PooledConnectionsTest < ActiveRecord::TestCase
def add_record(name)
ActiveRecord::Base.connection_pool.with_connection { Project.create! :name => name }
end
-end unless current_adapter?(:FrontBase) || in_memory_db?
+end unless in_memory_db?
diff --git a/activerecord/test/cases/primary_keys_test.rb b/activerecord/test/cases/primary_keys_test.rb
index 51ddd406ed..b04df7ce43 100644
--- a/activerecord/test/cases/primary_keys_test.rb
+++ b/activerecord/test/cases/primary_keys_test.rb
@@ -92,6 +92,7 @@ class PrimaryKeysTest < ActiveRecord::TestCase
end
def test_primary_key_prefix
+ old_primary_key_prefix_type = ActiveRecord::Base.primary_key_prefix_type
ActiveRecord::Base.primary_key_prefix_type = :table_name
Topic.reset_primary_key
assert_equal "topicid", Topic.primary_key
@@ -103,6 +104,8 @@ class PrimaryKeysTest < ActiveRecord::TestCase
ActiveRecord::Base.primary_key_prefix_type = nil
Topic.reset_primary_key
assert_equal "id", Topic.primary_key
+ ensure
+ ActiveRecord::Base.primary_key_prefix_type = old_primary_key_prefix_type
end
def test_delete_should_quote_pkey
@@ -131,14 +134,22 @@ class PrimaryKeysTest < ActiveRecord::TestCase
end
def test_primary_key_returns_value_if_it_exists
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = 'developers'
+ end
+
if ActiveRecord::Base.connection.supports_primary_key?
- assert_equal 'id', ActiveRecord::Base.connection.primary_key('developers')
+ assert_equal 'id', klass.primary_key
end
end
def test_primary_key_returns_nil_if_it_does_not_exist
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = 'developers_projects'
+ end
+
if ActiveRecord::Base.connection.supports_primary_key?
- assert_nil ActiveRecord::Base.connection.primary_key('developers_projects')
+ assert_nil klass.primary_key
end
end
@@ -149,38 +160,6 @@ class PrimaryKeysTest < ActiveRecord::TestCase
assert_equal k.connection.quote_column_name("foo"), k.quoted_primary_key
end
- def test_two_models_with_same_table_but_different_primary_key
- k1 = Class.new(ActiveRecord::Base)
- k1.table_name = 'posts'
- k1.primary_key = 'id'
-
- k2 = Class.new(ActiveRecord::Base)
- k2.table_name = 'posts'
- k2.primary_key = 'title'
-
- assert k1.columns.find { |c| c.name == 'id' }.primary
- assert !k1.columns.find { |c| c.name == 'title' }.primary
- assert k1.columns_hash['id'].primary
- assert !k1.columns_hash['title'].primary
-
- assert !k2.columns.find { |c| c.name == 'id' }.primary
- assert k2.columns.find { |c| c.name == 'title' }.primary
- assert !k2.columns_hash['id'].primary
- assert k2.columns_hash['title'].primary
- end
-
- def test_models_with_same_table_have_different_columns
- k1 = Class.new(ActiveRecord::Base)
- k1.table_name = 'posts'
-
- k2 = Class.new(ActiveRecord::Base)
- k2.table_name = 'posts'
-
- k1.columns.zip(k2.columns).each do |col1, col2|
- assert !col1.equal?(col2)
- end
- end
-
def test_auto_detect_primary_key_from_schema
MixedCaseMonkey.reset_primary_key
assert_equal "monkeyID", MixedCaseMonkey.primary_key
@@ -219,3 +198,29 @@ if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
end
end
end
+
+if current_adapter?(:PostgreSQLAdapter)
+ class PrimaryKeyBigSerialTest < ActiveRecord::TestCase
+ self.use_transactional_fixtures = false
+
+ class Widget < ActiveRecord::Base
+ end
+
+ setup do
+ @connection = ActiveRecord::Base.connection
+ @connection.create_table(:widgets, id: :bigserial) { |t| }
+ end
+
+ teardown do
+ @connection.drop_table :widgets
+ end
+
+ def test_bigserial_primary_key
+ assert_equal "id", Widget.primary_key
+ assert_equal :integer, Widget.columns_hash[Widget.primary_key].type
+
+ widget = Widget.create!
+ assert_not_nil widget.id
+ end
+ end
+end
diff --git a/activerecord/test/cases/quoting_test.rb b/activerecord/test/cases/quoting_test.rb
index e2439b9a24..1d6ae2f67f 100644
--- a/activerecord/test/cases/quoting_test.rb
+++ b/activerecord/test/cases/quoting_test.rb
@@ -3,14 +3,6 @@ require "cases/helper"
module ActiveRecord
module ConnectionAdapters
class QuotingTest < ActiveRecord::TestCase
- class FakeColumn < ActiveRecord::ConnectionAdapters::Column
- attr_accessor :type
-
- def initialize type
- @type = type
- end
- end
-
def setup
@quoter = Class.new { include Quoting }.new
end
@@ -91,69 +83,56 @@ module ActiveRecord
def test_quote_with_quoted_id
assert_equal 1, @quoter.quote(Struct.new(:quoted_id).new(1), nil)
- assert_equal 1, @quoter.quote(Struct.new(:quoted_id).new(1), 'foo')
end
def test_quote_nil
assert_equal 'NULL', @quoter.quote(nil, nil)
- assert_equal 'NULL', @quoter.quote(nil, 'foo')
end
def test_quote_true
assert_equal @quoter.quoted_true, @quoter.quote(true, nil)
- assert_equal '1', @quoter.quote(true, Struct.new(:type).new(:integer))
end
def test_quote_false
assert_equal @quoter.quoted_false, @quoter.quote(false, nil)
- assert_equal '0', @quoter.quote(false, Struct.new(:type).new(:integer))
end
def test_quote_float
float = 1.2
assert_equal float.to_s, @quoter.quote(float, nil)
- assert_equal float.to_s, @quoter.quote(float, Object.new)
end
def test_quote_fixnum
fixnum = 1
assert_equal fixnum.to_s, @quoter.quote(fixnum, nil)
- assert_equal fixnum.to_s, @quoter.quote(fixnum, Object.new)
end
def test_quote_bignum
bignum = 1 << 100
assert_equal bignum.to_s, @quoter.quote(bignum, nil)
- assert_equal bignum.to_s, @quoter.quote(bignum, Object.new)
end
def test_quote_bigdecimal
bigdec = BigDecimal.new((1 << 100).to_s)
assert_equal bigdec.to_s('F'), @quoter.quote(bigdec, nil)
- assert_equal bigdec.to_s('F'), @quoter.quote(bigdec, Object.new)
end
def test_dates_and_times
@quoter.extend(Module.new { def quoted_date(value) 'lol' end })
assert_equal "'lol'", @quoter.quote(Date.today, nil)
- assert_equal "'lol'", @quoter.quote(Date.today, Object.new)
assert_equal "'lol'", @quoter.quote(Time.now, nil)
- assert_equal "'lol'", @quoter.quote(Time.now, Object.new)
assert_equal "'lol'", @quoter.quote(DateTime.now, nil)
- assert_equal "'lol'", @quoter.quote(DateTime.now, Object.new)
end
def test_crazy_object
crazy = Class.new.new
expected = "'#{YAML.dump(crazy)}'"
assert_equal expected, @quoter.quote(crazy, nil)
- assert_equal expected, @quoter.quote(crazy, Object.new)
end
def test_crazy_object_calls_quote_string
crazy = Class.new { def initialize; @lol = 'lo\l' end }.new
assert_match "lo\\\\l", @quoter.quote(crazy, nil)
- assert_match "lo\\\\l", @quoter.quote(crazy, Object.new)
end
def test_quote_string_no_column
@@ -165,36 +144,13 @@ module ActiveRecord
assert_equal "'lo\\\\l'", @quoter.quote(string, nil)
end
- def test_quote_string_int_column
- assert_equal "1", @quoter.quote('1', FakeColumn.new(:integer))
- assert_equal "1", @quoter.quote('1.2', FakeColumn.new(:integer))
- end
-
- def test_quote_string_float_column
- assert_equal "1.0", @quoter.quote('1', FakeColumn.new(:float))
- assert_equal "1.2", @quoter.quote('1.2', FakeColumn.new(:float))
- end
-
- def test_quote_as_mb_chars_binary_column
- string = ActiveSupport::Multibyte::Chars.new('lo\l')
- assert_equal "'lo\\\\l'", @quoter.quote(string, FakeColumn.new(:binary))
- end
-
- def test_quote_binary_without_string_to_binary
- assert_equal "'lo\\\\l'", @quoter.quote('lo\l', FakeColumn.new(:binary))
- end
-
def test_string_with_crazy_column
- assert_equal "'lo\\\\l'", @quoter.quote('lo\l', FakeColumn.new(:foo))
+ assert_equal "'lo\\\\l'", @quoter.quote('lo\l')
end
def test_quote_duration
assert_equal "1800", @quoter.quote(30.minutes)
end
-
- def test_quote_duration_int_column
- assert_equal "7200", @quoter.quote(2.hours, FakeColumn.new(:integer))
- end
end
end
end
diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb
index c085fcf161..84abaf0291 100644
--- a/activerecord/test/cases/reflection_test.rb
+++ b/activerecord/test/cases/reflection_test.rb
@@ -80,8 +80,14 @@ class ReflectionTest < ActiveRecord::TestCase
assert_equal :integer, @first.column_for_attribute("id").type
end
+ def test_non_existent_columns_return_nil
+ assert_deprecated do
+ assert_nil @first.column_for_attribute("attribute_that_doesnt_exist")
+ end
+ end
+
def test_reflection_klass_for_nested_class_name
- reflection = MacroReflection.new(:company, nil, nil, { :class_name => 'MyApplication::Business::Company' }, ActiveRecord::Base)
+ reflection = ActiveRecord::Reflection.create(:has_many, nil, nil, { :class_name => 'MyApplication::Business::Company' }, ActiveRecord::Base)
assert_nothing_raised do
assert_equal MyApplication::Business::Company, reflection.klass
end
@@ -91,21 +97,21 @@ class ReflectionTest < ActiveRecord::TestCase
ActiveSupport::Inflector.inflections do |inflect|
inflect.irregular 'plural_irregular', 'plurales_irregulares'
end
- reflection = AssociationReflection.new(:has_many, 'plurales_irregulares', nil, {}, ActiveRecord::Base)
+ reflection = ActiveRecord::Reflection.create(:has_many, 'plurales_irregulares', nil, {}, ActiveRecord::Base)
assert_equal 'PluralIrregular', reflection.class_name
end
def test_aggregation_reflection
reflection_for_address = AggregateReflection.new(
- :composed_of, :address, nil, { :mapping => [ %w(address_street street), %w(address_city city), %w(address_country country) ] }, Customer
+ :address, nil, { :mapping => [ %w(address_street street), %w(address_city city), %w(address_country country) ] }, Customer
)
reflection_for_balance = AggregateReflection.new(
- :composed_of, :balance, nil, { :class_name => "Money", :mapping => %w(balance amount) }, Customer
+ :balance, nil, { :class_name => "Money", :mapping => %w(balance amount) }, Customer
)
reflection_for_gps_location = AggregateReflection.new(
- :composed_of, :gps_location, nil, { }, Customer
+ :gps_location, nil, { }, Customer
)
assert Customer.reflect_on_all_aggregations.include?(reflection_for_gps_location)
@@ -129,7 +135,7 @@ class ReflectionTest < ActiveRecord::TestCase
end
def test_has_many_reflection
- reflection_for_clients = AssociationReflection.new(:has_many, :clients, nil, { :order => "id", :dependent => :destroy }, Firm)
+ reflection_for_clients = ActiveRecord::Reflection.create(:has_many, :clients, nil, { :order => "id", :dependent => :destroy }, Firm)
assert_equal reflection_for_clients, Firm.reflect_on_association(:clients)
@@ -141,7 +147,7 @@ class ReflectionTest < ActiveRecord::TestCase
end
def test_has_one_reflection
- reflection_for_account = AssociationReflection.new(:has_one, :account, nil, { :foreign_key => "firm_id", :dependent => :destroy }, Firm)
+ reflection_for_account = ActiveRecord::Reflection.create(:has_one, :account, nil, { :foreign_key => "firm_id", :dependent => :destroy }, Firm)
assert_equal reflection_for_account, Firm.reflect_on_association(:account)
assert_equal Account, Firm.reflect_on_association(:account).klass
@@ -200,7 +206,12 @@ class ReflectionTest < ActiveRecord::TestCase
end
def test_reflection_should_not_raise_error_when_compared_to_other_object
- assert_nothing_raised { Firm.reflections['clients'] == Object.new }
+ assert_not_equal Object.new, Firm._reflections['clients']
+ end
+
+ def test_has_and_belongs_to_many_reflection
+ assert_equal :has_and_belongs_to_many, Category.reflections['posts'].macro
+ assert_equal :posts, Category.reflect_on_all_associations(:has_and_belongs_to_many).first.name
end
def test_has_many_through_reflection
@@ -273,12 +284,12 @@ class ReflectionTest < ActiveRecord::TestCase
end
def test_association_primary_key_raises_when_missing_primary_key
- reflection = ActiveRecord::Reflection::AssociationReflection.new(:fuu, :edge, nil, {}, Author)
+ reflection = ActiveRecord::Reflection.create(:has_many, :edge, nil, {}, Author)
assert_raises(ActiveRecord::UnknownPrimaryKey) { reflection.association_primary_key }
through = Class.new(ActiveRecord::Reflection::ThroughReflection) {
define_method(:source_reflection) { reflection }
- }.new(:fuu, :edge, nil, {}, Author)
+ }.new(reflection)
assert_raises(ActiveRecord::UnknownPrimaryKey) { through.association_primary_key }
end
@@ -288,7 +299,7 @@ class ReflectionTest < ActiveRecord::TestCase
end
def test_active_record_primary_key_raises_when_missing_primary_key
- reflection = ActiveRecord::Reflection::AssociationReflection.new(:fuu, :author, nil, {}, Edge)
+ reflection = ActiveRecord::Reflection.create(:has_many, :author, nil, {}, Edge)
assert_raises(ActiveRecord::UnknownPrimaryKey) { reflection.active_record_primary_key }
end
@@ -306,32 +317,28 @@ class ReflectionTest < ActiveRecord::TestCase
end
def test_default_association_validation
- assert AssociationReflection.new(:has_many, :clients, nil, {}, Firm).validate?
+ assert ActiveRecord::Reflection.create(:has_many, :clients, nil, {}, Firm).validate?
- assert !AssociationReflection.new(:has_one, :client, nil, {}, Firm).validate?
- assert !AssociationReflection.new(:belongs_to, :client, nil, {}, Firm).validate?
- assert !AssociationReflection.new(:has_and_belongs_to_many, :clients, nil, {}, Firm).validate?
+ assert !ActiveRecord::Reflection.create(:has_one, :client, nil, {}, Firm).validate?
+ assert !ActiveRecord::Reflection.create(:belongs_to, :client, nil, {}, Firm).validate?
end
def test_always_validate_association_if_explicit
- assert AssociationReflection.new(:has_one, :client, nil, { :validate => true }, Firm).validate?
- assert AssociationReflection.new(:belongs_to, :client, nil, { :validate => true }, Firm).validate?
- assert AssociationReflection.new(:has_many, :clients, nil, { :validate => true }, Firm).validate?
- assert AssociationReflection.new(:has_and_belongs_to_many, :clients, nil, { :validate => true }, Firm).validate?
+ assert ActiveRecord::Reflection.create(:has_one, :client, nil, { :validate => true }, Firm).validate?
+ assert ActiveRecord::Reflection.create(:belongs_to, :client, nil, { :validate => true }, Firm).validate?
+ assert ActiveRecord::Reflection.create(:has_many, :clients, nil, { :validate => true }, Firm).validate?
end
def test_validate_association_if_autosave
- assert AssociationReflection.new(:has_one, :client, nil, { :autosave => true }, Firm).validate?
- assert AssociationReflection.new(:belongs_to, :client, nil, { :autosave => true }, Firm).validate?
- assert AssociationReflection.new(:has_many, :clients, nil, { :autosave => true }, Firm).validate?
- assert AssociationReflection.new(:has_and_belongs_to_many, :clients, nil, { :autosave => true }, Firm).validate?
+ assert ActiveRecord::Reflection.create(:has_one, :client, nil, { :autosave => true }, Firm).validate?
+ assert ActiveRecord::Reflection.create(:belongs_to, :client, nil, { :autosave => true }, Firm).validate?
+ assert ActiveRecord::Reflection.create(:has_many, :clients, nil, { :autosave => true }, Firm).validate?
end
def test_never_validate_association_if_explicit
- assert !AssociationReflection.new(:has_one, :client, nil, { :autosave => true, :validate => false }, Firm).validate?
- assert !AssociationReflection.new(:belongs_to, :client, nil, { :autosave => true, :validate => false }, Firm).validate?
- assert !AssociationReflection.new(:has_many, :clients, nil, { :autosave => true, :validate => false }, Firm).validate?
- assert !AssociationReflection.new(:has_and_belongs_to_many, :clients, nil, { :autosave => true, :validate => false }, Firm).validate?
+ assert !ActiveRecord::Reflection.create(:has_one, :client, nil, { :autosave => true, :validate => false }, Firm).validate?
+ assert !ActiveRecord::Reflection.create(:belongs_to, :client, nil, { :autosave => true, :validate => false }, Firm).validate?
+ assert !ActiveRecord::Reflection.create(:has_many, :clients, nil, { :autosave => true, :validate => false }, Firm).validate?
end
def test_foreign_key
@@ -353,11 +360,11 @@ class ReflectionTest < ActiveRecord::TestCase
category = Struct.new(:table_name, :pluralize_table_names).new('categories', true)
product = Struct.new(:table_name, :pluralize_table_names).new('products', true)
- reflection = AssociationReflection.new(:has_and_belongs_to_many, :categories, nil, {}, product)
+ reflection = ActiveRecord::Reflection.create(:has_many, :categories, nil, {}, product)
reflection.stubs(:klass).returns(category)
assert_equal 'categories_products', reflection.join_table
- reflection = AssociationReflection.new(:has_and_belongs_to_many, :products, nil, {}, category)
+ reflection = ActiveRecord::Reflection.create(:has_many, :products, nil, {}, category)
reflection.stubs(:klass).returns(product)
assert_equal 'categories_products', reflection.join_table
end
@@ -366,11 +373,11 @@ class ReflectionTest < ActiveRecord::TestCase
category = Struct.new(:table_name, :pluralize_table_names).new('catalog_categories', true)
product = Struct.new(:table_name, :pluralize_table_names).new('catalog_products', true)
- reflection = AssociationReflection.new(:has_and_belongs_to_many, :categories, nil, {}, product)
+ reflection = ActiveRecord::Reflection.create(:has_many, :categories, nil, {}, product)
reflection.stubs(:klass).returns(category)
assert_equal 'catalog_categories_products', reflection.join_table
- reflection = AssociationReflection.new(:has_and_belongs_to_many, :products, nil, {}, category)
+ reflection = ActiveRecord::Reflection.create(:has_many, :products, nil, {}, category)
reflection.stubs(:klass).returns(product)
assert_equal 'catalog_categories_products', reflection.join_table
end
@@ -379,11 +386,11 @@ class ReflectionTest < ActiveRecord::TestCase
category = Struct.new(:table_name, :pluralize_table_names).new('catalog_categories', true)
page = Struct.new(:table_name, :pluralize_table_names).new('content_pages', true)
- reflection = AssociationReflection.new(:has_and_belongs_to_many, :categories, nil, {}, page)
+ reflection = ActiveRecord::Reflection.create(:has_many, :categories, nil, {}, page)
reflection.stubs(:klass).returns(category)
assert_equal 'catalog_categories_content_pages', reflection.join_table
- reflection = AssociationReflection.new(:has_and_belongs_to_many, :pages, nil, {}, category)
+ reflection = ActiveRecord::Reflection.create(:has_many, :pages, nil, {}, category)
reflection.stubs(:klass).returns(page)
assert_equal 'catalog_categories_content_pages', reflection.join_table
end
@@ -392,15 +399,47 @@ class ReflectionTest < ActiveRecord::TestCase
category = Struct.new(:table_name, :pluralize_table_names).new('categories', true)
product = Struct.new(:table_name, :pluralize_table_names).new('products', true)
- reflection = AssociationReflection.new(:has_and_belongs_to_many, :categories, nil, { :join_table => 'product_categories' }, product)
+ reflection = ActiveRecord::Reflection.create(:has_many, :categories, nil, { :join_table => 'product_categories' }, product)
reflection.stubs(:klass).returns(category)
assert_equal 'product_categories', reflection.join_table
- reflection = AssociationReflection.new(:has_and_belongs_to_many, :products, nil, { :join_table => 'product_categories' }, category)
+ reflection = ActiveRecord::Reflection.create(:has_many, :products, nil, { :join_table => 'product_categories' }, category)
reflection.stubs(:klass).returns(product)
assert_equal 'product_categories', reflection.join_table
end
+ def test_includes_accepts_symbols
+ hotel = Hotel.create!
+ department = hotel.departments.create!
+ department.chefs.create!
+
+ assert_nothing_raised do
+ assert_equal department.chefs, Hotel.includes([departments: :chefs]).first.chefs
+ end
+ end
+
+ def test_includes_accepts_strings
+ hotel = Hotel.create!
+ department = hotel.departments.create!
+ department.chefs.create!
+
+ assert_nothing_raised do
+ assert_equal department.chefs, Hotel.includes(['departments' => 'chefs']).first.chefs
+ end
+ end
+
+ def test_reflect_on_association_accepts_symbols
+ assert_nothing_raised do
+ assert_equal Hotel.reflect_on_association(:departments).name, :departments
+ end
+ end
+
+ def test_reflect_on_association_accepts_strings
+ assert_nothing_raised do
+ assert_equal Hotel.reflect_on_association("departments").name, :departments
+ end
+ end
+
private
def assert_reflection(klass, association, options)
assert reflection = klass.reflect_on_association(association)
diff --git a/activerecord/test/cases/relation/delegation_test.rb b/activerecord/test/cases/relation/delegation_test.rb
index 9b2bfed039..29c9d0e2af 100644
--- a/activerecord/test/cases/relation/delegation_test.rb
+++ b/activerecord/test/cases/relation/delegation_test.rb
@@ -32,7 +32,7 @@ module ActiveRecord
:exclude?, :find_all, :flat_map, :group_by, :include?, :length,
:map, :none?, :one?, :partition, :reject, :reverse,
:sample, :second, :sort, :sort_by, :third,
- :to_ary, :to_set, :to_xml, :to_yaml
+ :to_ary, :to_set, :to_xml, :to_yaml, :join
]
ARRAY_DELEGATES.each do |method|
diff --git a/activerecord/test/cases/relation/merging_test.rb b/activerecord/test/cases/relation/merging_test.rb
index ff1c2a0d82..2b5c2fd5a4 100644
--- a/activerecord/test/cases/relation/merging_test.rb
+++ b/activerecord/test/cases/relation/merging_test.rb
@@ -17,8 +17,9 @@ class RelationMergingTest < ActiveRecord::TestCase
end
def test_relation_to_sql
- sql = Post.first.comments.to_sql
- assert_no_match(/\?/, sql)
+ post = Post.first
+ sql = post.comments.to_sql
+ assert_match(/.?post_id.? = #{post.id}\Z/i, sql)
end
def test_relation_merging_with_arel_equalities_keeps_last_equality
@@ -107,6 +108,11 @@ class RelationMergingTest < ActiveRecord::TestCase
merged = left.merge(right)
assert_equal post, merged.first
end
+
+ def test_merging_compares_symbols_and_strings_as_equal
+ post = PostThatLoadsCommentsInAnAfterSaveHook.create!(title: "First Post", body: "Blah blah blah.")
+ assert_equal "First comment!", post.comments.where(body: "First comment!").first_or_create.body
+ end
end
class MergingDifferentRelationsTest < ActiveRecord::TestCase
diff --git a/activerecord/test/cases/relation/where_chain_test.rb b/activerecord/test/cases/relation/where_chain_test.rb
index c6decaad89..b9e69bdb08 100644
--- a/activerecord/test/cases/relation/where_chain_test.rb
+++ b/activerecord/test/cases/relation/where_chain_test.rb
@@ -99,7 +99,7 @@ module ActiveRecord
assert_bound_ast value, Post.arel_table[@name], Arel::Nodes::NotEqual
assert_equal 'ruby on rails', bind.last
end
-
+
def test_rewhere_with_one_condition
relation = Post.where(title: 'hello').where(title: 'world').rewhere(title: 'alone')
diff --git a/activerecord/test/cases/relation/where_test.rb b/activerecord/test/cases/relation/where_test.rb
index 937f226b1d..a6a36a6fd9 100644
--- a/activerecord/test/cases/relation/where_test.rb
+++ b/activerecord/test/cases/relation/where_test.rb
@@ -6,10 +6,11 @@ require 'models/post'
require 'models/comment'
require 'models/edge'
require 'models/topic'
+require 'models/binary'
module ActiveRecord
class WhereTest < ActiveRecord::TestCase
- fixtures :posts, :edges, :authors
+ fixtures :posts, :edges, :authors, :binaries
def test_where_copies_bind_params
author = authors(:david)
@@ -179,5 +180,35 @@ module ActiveRecord
assert_equal 4, Edge.where(blank).order("sink_id").to_a.size
end
end
+
+ def test_where_with_integer_for_string_column
+ count = Post.where(:title => 0).count
+ assert_equal 0, count
+ end
+
+ def test_where_with_float_for_string_column
+ count = Post.where(:title => 0.0).count
+ assert_equal 0, count
+ end
+
+ def test_where_with_boolean_for_string_column
+ count = Post.where(:title => false).count
+ assert_equal 0, count
+ end
+
+ def test_where_with_decimal_for_string_column
+ count = Post.where(:title => BigDecimal.new(0)).count
+ assert_equal 0, count
+ end
+
+ def test_where_with_duration_for_string_column
+ count = Post.where(:title => 0.seconds).count
+ assert_equal 0, count
+ end
+
+ def test_where_with_integer_for_binary_column
+ count = Binary.where(:data => 0).count
+ assert_equal 0, count
+ end
end
end
diff --git a/activerecord/test/cases/relation_test.rb b/activerecord/test/cases/relation_test.rb
index fb0b906c07..3280945d09 100644
--- a/activerecord/test/cases/relation_test.rb
+++ b/activerecord/test/cases/relation_test.rb
@@ -235,5 +235,33 @@ module ActiveRecord
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
+
+ class EnsureRoundTripTypeCasting < ActiveRecord::Type::Value
+ def type
+ :string
+ end
+
+ def type_cast_from_database(value)
+ raise value unless value == "type cast for database"
+ "type cast from database"
+ end
+
+ def type_cast_for_database(value)
+ raise value unless value == "value from user"
+ "type cast for database"
+ end
+ end
+
+ class UpdateAllTestModel < ActiveRecord::Base
+ self.table_name = 'posts'
+
+ attribute :body, EnsureRoundTripTypeCasting.new
+ end
+
+ def test_update_all_goes_through_normal_type_casting
+ UpdateAllTestModel.update_all(body: "value from user", type: nil) # No STI
+
+ assert_equal "type cast from database", UpdateAllTestModel.first.body
+ end
end
end
diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb
index 68e62934c1..88df997a2f 100644
--- a/activerecord/test/cases/relations_test.rb
+++ b/activerecord/test/cases/relations_test.rb
@@ -366,6 +366,14 @@ class RelationTest < ActiveRecord::TestCase
assert_equal({ 'salary' => 100_000 }, Developer.none.where(salary: 100_000).where_values_hash)
end
+ def test_null_relation_sum
+ ac = Aircraft.new
+ assert_equal Hash.new, ac.engines.group(:id).sum(:id)
+ assert_equal 0, ac.engines.count
+ ac.save
+ assert_equal Hash.new, ac.engines.group(:id).sum(:id)
+ assert_equal 0, ac.engines.count
+ end
def test_null_relation_count
ac = Aircraft.new
@@ -376,6 +384,42 @@ class RelationTest < ActiveRecord::TestCase
assert_equal 0, ac.engines.count
end
+ def test_null_relation_size
+ ac = Aircraft.new
+ assert_equal Hash.new, ac.engines.group(:id).size
+ assert_equal 0, ac.engines.size
+ ac.save
+ assert_equal Hash.new, ac.engines.group(:id).size
+ assert_equal 0, ac.engines.size
+ end
+
+ def test_null_relation_average
+ ac = Aircraft.new
+ assert_equal Hash.new, ac.engines.group(:car_id).average(:id)
+ assert_equal nil, ac.engines.average(:id)
+ ac.save
+ assert_equal Hash.new, ac.engines.group(:car_id).average(:id)
+ assert_equal nil, ac.engines.average(:id)
+ end
+
+ def test_null_relation_minimum
+ ac = Aircraft.new
+ assert_equal Hash.new, ac.engines.group(:car_id).minimum(:id)
+ assert_equal nil, ac.engines.minimum(:id)
+ ac.save
+ assert_equal Hash.new, ac.engines.group(:car_id).minimum(:id)
+ assert_equal nil, ac.engines.minimum(:id)
+ end
+
+ def test_null_relation_maximum
+ ac = Aircraft.new
+ assert_equal Hash.new, ac.engines.group(:car_id).maximum(:id)
+ assert_equal nil, ac.engines.maximum(:id)
+ ac.save
+ assert_equal Hash.new, ac.engines.group(:car_id).maximum(:id)
+ assert_equal nil, ac.engines.maximum(:id)
+ end
+
def test_joins_with_nil_argument
assert_nothing_raised { DependentFirm.joins(nil).first }
end
@@ -699,6 +743,13 @@ class RelationTest < ActiveRecord::TestCase
assert_equal [], relation.to_a
end
+ def test_typecasting_where_with_array
+ ids = Author.pluck(:id)
+ slugs = ids.map { |id| "#{id}-as-a-slug" }
+
+ assert_equal Author.all.to_a, Author.where(id: slugs).to_a
+ end
+
def test_find_all_using_where_with_relation
david = authors(:david)
# switching the lines below would succeed in current rails
@@ -831,8 +882,12 @@ class RelationTest < ActiveRecord::TestCase
assert davids.loaded?
end
- def test_delete_all_limit_error
+ def test_delete_all_with_unpermitted_relation_raises_error
assert_raises(ActiveRecord::ActiveRecordError) { Author.limit(10).delete_all }
+ assert_raises(ActiveRecord::ActiveRecordError) { Author.uniq.delete_all }
+ assert_raises(ActiveRecord::ActiveRecordError) { Author.group(:name).delete_all }
+ assert_raises(ActiveRecord::ActiveRecordError) { Author.having('SUM(id) < 3').delete_all }
+ assert_raises(ActiveRecord::ActiveRecordError) { Author.offset(10).delete_all }
end
def test_select_with_aggregates
@@ -1677,4 +1732,8 @@ class RelationTest < ActiveRecord::TestCase
merged = left.merge(right)
assert_equal post, merged.first
end
+
+ def test_relation_join_method
+ assert_equal 'Thank you for the welcome,Thank you again for the welcome', Post.first.comments.join(",")
+ end
end
diff --git a/activerecord/test/cases/result_test.rb b/activerecord/test/cases/result_test.rb
index 2131b32a0c..d6decafad9 100644
--- a/activerecord/test/cases/result_test.rb
+++ b/activerecord/test/cases/result_test.rb
@@ -10,7 +10,7 @@ module ActiveRecord
])
end
- def test_to_hash_returns_row_hashes
+ test "to_hash returns row_hashes" do
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'},
@@ -18,13 +18,13 @@ module ActiveRecord
], result.to_hash
end
- def test_each_with_block_returns_row_hashes
+ test "each with block returns row hashes" do
result.each do |row|
assert_equal ['col_1', 'col_2'], row.keys
end
end
- def test_each_without_block_returns_an_enumerator
+ test "each without block returns an enumerator" do
result.each.with_index do |row, index|
assert_equal ['col_1', 'col_2'], row.keys
assert_kind_of Integer, index
@@ -32,9 +32,45 @@ module ActiveRecord
end
if Enumerator.method_defined? :size
- def test_each_without_block_returns_a_sized_enumerator
+ test "each without block returns a sized enumerator" do
assert_equal 3, result.each.size
end
end
+
+ test "cast_values returns rows after type casting" do
+ values = [["1.1", "2.2"], ["3.3", "4.4"]]
+ columns = ["col1", "col2"]
+ types = { "col1" => Type::Integer.new, "col2" => Type::Float.new }
+ result = Result.new(columns, values, types)
+
+ assert_equal [[1, 2.2], [3, 4.4]], result.cast_values
+ end
+
+ test "cast_values uses identity type for unknown types" do
+ values = [["1.1", "2.2"], ["3.3", "4.4"]]
+ columns = ["col1", "col2"]
+ types = { "col1" => Type::Integer.new }
+ result = Result.new(columns, values, types)
+
+ assert_equal [[1, "2.2"], [3, "4.4"]], result.cast_values
+ end
+
+ test "cast_values returns single dimensional array if single column" do
+ values = [["1.1"], ["3.3"]]
+ columns = ["col1"]
+ types = { "col1" => Type::Integer.new }
+ result = Result.new(columns, values, types)
+
+ assert_equal [1, 3], result.cast_values
+ end
+
+ test "cast_values can receive types to use instead" do
+ values = [["1.1", "2.2"], ["3.3", "4.4"]]
+ columns = ["col1", "col2"]
+ types = { "col1" => Type::Integer.new, "col2" => Type::Float.new }
+ result = Result.new(columns, values, types)
+
+ assert_equal [[1.1, 2.2], [3.3, 4.4]], result.cast_values("col1" => Type::Float.new)
+ end
end
end
diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb
index fd0ef2f89f..4e71d04bc0 100644
--- a/activerecord/test/cases/schema_dumper_test.rb
+++ b/activerecord/test/cases/schema_dumper_test.rb
@@ -1,10 +1,9 @@
require "cases/helper"
+require 'support/schema_dumping_helper'
class SchemaDumperTest < ActiveRecord::TestCase
- def setup
- super
+ setup do
ActiveRecord::SchemaMigration.create_table
- @stream = StringIO.new
end
def standard_dump
@@ -25,7 +24,8 @@ class SchemaDumperTest < ActiveRecord::TestCase
end
def test_magic_comment
- assert_match "# encoding: #{@stream.external_encoding.name}", standard_dump
+ output = standard_dump
+ assert_match "# encoding: #{@stream.external_encoding.name}", output
end
def test_schema_dump
@@ -63,7 +63,7 @@ class SchemaDumperTest < ActiveRecord::TestCase
next if column_set.empty?
lengths = column_set.map do |column|
- if match = column.match(/t\.(?:integer|decimal|float|datetime|timestamp|time|date|text|binary|string|boolean|uuid)\s+"/)
+ if match = column.match(/t\.(?:integer|decimal|float|datetime|timestamp|time|date|text|binary|string|boolean|uuid|point)\s+"/)
match[0].length
end
end
@@ -353,9 +353,9 @@ class SchemaDumperTest < ActiveRecord::TestCase
output = standard_dump
# Oracle supports precision up to 38 and it identifies decimals with scale 0 as integers
if current_adapter?(:OracleAdapter)
- assert_match %r{t.integer\s+"atoms_in_universe",\s+precision: 38,\s+scale: 0}, output
+ assert_match %r{t.integer\s+"atoms_in_universe",\s+precision: 38}, output
else
- assert_match %r{t.decimal\s+"atoms_in_universe",\s+precision: 55,\s+scale: 0}, output
+ assert_match %r{t.decimal\s+"atoms_in_universe",\s+precision: 55}, output
end
end
@@ -372,15 +372,28 @@ class SchemaDumperTest < ActiveRecord::TestCase
assert_match %r{create_table "subscribers", id: false}, output
end
+ if ActiveRecord::Base.connection.supports_foreign_keys?
+ def test_foreign_keys_are_dumped_at_the_bottom_to_circumvent_dependency_issues
+ output = standard_dump
+ assert_match(/^\s+add_foreign_key "fk_test_has_fk"[^\n]+\n\s+add_foreign_key "lessons_students"/, output)
+ end
+ end
+
class CreateDogMigration < ActiveRecord::Migration
def up
+ create_table("dog_owners") do |t|
+ end
+
create_table("dogs") do |t|
t.column :name, :string
+ t.column :owner_id, :integer
end
add_index "dogs", [:name]
+ add_foreign_key :dogs, :dog_owners, column: "owner_id" if supports_foreign_keys?
end
def down
drop_table("dogs")
+ drop_table("dog_owners")
end
end
@@ -394,13 +407,45 @@ class SchemaDumperTest < ActiveRecord::TestCase
output = standard_dump
assert_no_match %r{create_table "foo_.+_bar"}, output
- assert_no_match %r{create_index "foo_.+_bar"}, output
+ assert_no_match %r{add_index "foo_.+_bar"}, output
assert_no_match %r{create_table "schema_migrations"}, output
+
+ if ActiveRecord::Base.connection.supports_foreign_keys?
+ assert_no_match %r{add_foreign_key "foo_.+_bar"}, output
+ assert_no_match %r{add_foreign_key "[^"]+", "foo_.+_bar"}, output
+ end
ensure
migration.migrate(:down)
ActiveRecord::Base.table_name_suffix = ActiveRecord::Base.table_name_prefix = ''
$stdout = original
end
+end
+
+class SchemaDumperDefaultsTest < ActiveRecord::TestCase
+ include SchemaDumpingHelper
+
+ setup do
+ @connection = ActiveRecord::Base.connection
+ @connection.create_table :defaults, force: true do |t|
+ t.string :string_with_default, default: "Hello!"
+ t.date :date_with_default, default: '2014-06-05'
+ t.datetime :datetime_with_default, default: "2014-06-05 07:17:04"
+ t.time :time_with_default, default: "07:17:04"
+ end
+ end
+ teardown do
+ return unless @connection
+ @connection.execute 'DROP TABLE IF EXISTS defaults'
+ end
+
+ def test_schema_dump_defaults_with_universally_supported_types
+ output = dump_table_schema('defaults')
+
+ assert_match %r{t\.string\s+"string_with_default",\s+default: "Hello!"}, output
+ assert_match %r{t\.date\s+"date_with_default",\s+default: '2014-06-05'}, output
+ assert_match %r{t\.datetime\s+"datetime_with_default",\s+default: '2014-06-05 07:17:04'}, output
+ assert_match %r{t\.time\s+"time_with_default",\s+default: '2000-01-01 07:17:04'}, output
+ end
end
diff --git a/activerecord/test/cases/serialization_test.rb b/activerecord/test/cases/serialization_test.rb
index c46060a646..3f52e80e11 100644
--- a/activerecord/test/cases/serialization_test.rb
+++ b/activerecord/test/cases/serialization_test.rb
@@ -1,8 +1,11 @@
require "cases/helper"
require 'models/contact'
require 'models/topic'
+require 'models/book'
class SerializationTest < ActiveRecord::TestCase
+ fixtures :books
+
FORMATS = [ :xml, :json ]
def setup
@@ -65,4 +68,28 @@ class SerializationTest < ActiveRecord::TestCase
ensure
ActiveRecord::Base.include_root_in_json = original_root_in_json
end
+
+ def test_read_attribute_for_serialization_with_format_without_method_missing
+ klazz = Class.new(ActiveRecord::Base)
+ klazz.table_name = 'books'
+
+ book = klazz.new
+ assert_nil book.read_attribute_for_serialization(:format)
+ end
+
+ def test_read_attribute_for_serialization_with_format_after_init
+ klazz = Class.new(ActiveRecord::Base)
+ klazz.table_name = 'books'
+
+ book = klazz.new(format: 'paperback')
+ assert_equal 'paperback', book.read_attribute_for_serialization(:format)
+ end
+
+ def test_read_attribute_for_serialization_with_format_after_find
+ klazz = Class.new(ActiveRecord::Base)
+ klazz.table_name = 'books'
+
+ book = klazz.find(books(:awdr).id)
+ assert_equal 'paperback', book.read_attribute_for_serialization(:format)
+ end
end
diff --git a/activerecord/test/cases/serialized_attribute_test.rb b/activerecord/test/cases/serialized_attribute_test.rb
index 5609cf310c..f8d87a3661 100644
--- a/activerecord/test/cases/serialized_attribute_test.rb
+++ b/activerecord/test/cases/serialized_attribute_test.rb
@@ -3,10 +3,11 @@ require 'models/topic'
require 'models/reply'
require 'models/person'
require 'models/traffic_light'
+require 'models/post'
require 'bcrypt'
class SerializedAttributeTest < ActiveRecord::TestCase
- fixtures :topics
+ fixtures :topics, :posts
MyObject = Struct.new :attribute1, :attribute2
@@ -14,8 +15,17 @@ class SerializedAttributeTest < ActiveRecord::TestCase
Topic.serialize("content")
end
+ def test_serialize_does_not_eagerly_load_columns
+ assert_no_queries do
+ Topic.reset_column_information
+ Topic.serialize(:content)
+ end
+ end
+
def test_list_of_serialized_attributes
- assert_equal %w(content), Topic.serialized_attributes.keys
+ assert_deprecated do
+ assert_equal %w(content), Topic.serialized_attributes.keys
+ end
end
def test_serialized_attribute
@@ -29,12 +39,6 @@ class SerializedAttributeTest < ActiveRecord::TestCase
assert_equal(myobj, topic.content)
end
- def test_serialized_attribute_init_with
- topic = Topic.allocate
- topic.init_with('attributes' => { 'content' => '--- foo' })
- assert_equal 'foo', topic.content
- end
-
def test_serialized_attribute_in_base_class
Topic.serialize("content", Hash)
@@ -46,34 +50,56 @@ class SerializedAttributeTest < ActiveRecord::TestCase
assert_equal(hash, important_topic.content)
end
- # This test was added to fix GH #4004. Obviously the value returned
- # is not really the value 'before type cast' so we should maybe think
- # about changing that in the future.
- def test_serialized_attribute_before_type_cast_returns_unserialized_value
+ def test_serialized_attributes_from_database_on_subclass
Topic.serialize :content, Hash
- t = Topic.new(content: { foo: :bar })
- assert_equal({ foo: :bar }, t.content_before_type_cast)
+ t = Reply.new(content: { foo: :bar })
+ assert_equal({ foo: :bar }, t.content)
t.save!
- t.reload
- assert_equal({ foo: :bar }, t.content_before_type_cast)
+ t = Reply.last
+ assert_equal({ foo: :bar }, t.content)
end
- def test_serialized_attributes_before_type_cast_returns_unserialized_value
- Topic.serialize :content, Hash
+ def test_serialized_attribute_calling_dup_method
+ Topic.serialize :content, JSON
- t = Topic.new(content: { foo: :bar })
- assert_equal({ foo: :bar }, t.attributes_before_type_cast["content"])
+ orig = Topic.new(content: { foo: :bar })
+ clone = orig.dup
+ assert_equal(orig.content, clone.content)
+ end
+
+ def test_serialized_json_attribute_returns_unserialized_value
+ Topic.serialize :content, JSON
+ my_post = posts(:welcome)
+
+ t = Topic.new(content: my_post)
t.save!
t.reload
- assert_equal({ foo: :bar }, t.attributes_before_type_cast["content"])
+
+ assert_instance_of(Hash, t.content)
+ assert_equal(my_post.id, t.content["id"])
+ assert_equal(my_post.title, t.content["title"])
end
- def test_serialized_attribute_calling_dup_method
+ def test_json_read_legacy_null
Topic.serialize :content, JSON
- t = Topic.new(:content => { :foo => :bar }).dup
- assert_equal({ :foo => :bar }, t.content_before_type_cast)
+ # Force a row to have a JSON "null" instead of a database NULL (this is how
+ # null values are saved on 4.1 and before)
+ id = Topic.connection.insert "INSERT INTO topics (content) VALUES('null')"
+ t = Topic.find(id)
+
+ assert_nil t.content
+ end
+
+ def test_json_read_db_null
+ Topic.serialize :content, JSON
+
+ # Force a row to have a database NULL instead of a JSON "null"
+ id = Topic.connection.insert "INSERT INTO topics (content) VALUES(NULL)"
+ t = Topic.find(id)
+
+ assert_nil t.content
end
def test_serialized_attribute_declared_in_subclass
@@ -116,8 +142,10 @@ class SerializedAttributeTest < ActiveRecord::TestCase
def test_serialized_attribute_should_raise_exception_on_save_with_wrong_type
Topic.serialize(:content, Hash)
- topic = Topic.new(:content => "string")
- assert_raise(ActiveRecord::SerializationTypeMismatch) { topic.save }
+ assert_raise(ActiveRecord::SerializationTypeMismatch) do
+ topic = Topic.new(content: 'string')
+ topic.save
+ end
end
def test_should_raise_exception_on_serialized_attribute_with_type_mismatch
@@ -168,45 +196,22 @@ class SerializedAttributeTest < ActiveRecord::TestCase
end
def test_serialize_with_coder
- coder = Class.new {
- # Identity
- def load(thing)
- thing
- end
-
- # base 64
- def dump(thing)
- [thing].pack('m')
+ some_class = Struct.new(:foo) do
+ def self.dump(value)
+ value.foo
end
- }.new
- Topic.serialize(:content, coder)
- s = 'hello world'
- topic = Topic.new(:content => s)
- assert topic.save
- topic = topic.reload
- assert_equal [s].pack('m'), topic.content
- end
-
- def test_serialize_with_bcrypt_coder
- crypt_coder = Class.new {
- def load(thing)
- return unless thing
- BCrypt::Password.new thing
+ def self.load(value)
+ new(value)
end
+ end
- def dump(thing)
- BCrypt::Password.create(thing).to_s
- end
- }.new
-
- Topic.serialize(:content, crypt_coder)
- password = 'password'
- topic = Topic.new(:content => password)
- assert topic.save
- topic = topic.reload
- assert_kind_of BCrypt::Password, topic.content
- assert_equal(true, topic.content == password, 'password should equal')
+ Topic.serialize(:content, some_class)
+ topic = Topic.new(:content => some_class.new('my value'))
+ topic.save!
+ topic.reload
+ assert_kind_of some_class, topic.content
+ assert_equal topic.content, some_class.new('my value')
end
def test_serialize_attribute_via_select_method_when_time_zone_available
@@ -235,13 +240,19 @@ class SerializedAttributeTest < ActiveRecord::TestCase
assert_equal [], light.long_state
end
- def test_serialized_column_should_not_be_wrapped_twice
- Topic.serialize(:content, MyObject)
+ def test_serialized_column_should_unserialize_after_update_column
+ t = Topic.create(content: "first")
+ assert_equal("first", t.content)
- 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)
+ t.update_column(:content, Topic.type_for_attribute('content').type_cast_for_database("second"))
+ assert_equal("second", t.content)
+ end
+
+ def test_serialized_column_should_unserialize_after_update_attribute
+ t = Topic.create(content: "first")
+ assert_equal("first", t.content)
+
+ t.update_attribute(:content, "second")
+ assert_equal("second", t.content)
end
end
diff --git a/activerecord/test/cases/store_test.rb b/activerecord/test/cases/store_test.rb
index 978cee9cfb..e9cdf94c99 100644
--- a/activerecord/test/cases/store_test.rb
+++ b/activerecord/test/cases/store_test.rb
@@ -163,22 +163,24 @@ class StoreTest < ActiveRecord::TestCase
assert_equal [:width, :height], second_model.stored_attributes[:data]
end
- test "YAML coder initializes the store when a Nil value is given" do
- assert_equal({}, @john.params)
- end
+ test "stored_attributes are tracked per subclass" do
+ first_model = Class.new(ActiveRecord::Base) do
+ store_accessor :data, :color
+ end
+ second_model = Class.new(first_model) do
+ store_accessor :data, :width, :height
+ end
+ third_model = Class.new(first_model) do
+ store_accessor :data, :area, :volume
+ end
- test "attributes_for_coder should return stored fields already serialized" do
- attributes = {
- "id" => @john.id,
- "name"=> @john.name,
- "settings" => "--- !ruby/hash:ActiveSupport::HashWithIndifferentAccess\ncolor: black\n",
- "preferences" => "--- !ruby/hash:ActiveSupport::HashWithIndifferentAccess\nremember_login: true\n",
- "json_data" => "{\"height\":\"tall\"}", "json_data_empty"=>"{\"is_a_good_guy\":true}",
- "params" => "--- !ruby/hash:ActiveSupport::HashWithIndifferentAccess {}\n",
- "account_id"=> @john.account_id
- }
+ assert_equal [:color], first_model.stored_attributes[:data]
+ assert_equal [:color, :width, :height], second_model.stored_attributes[:data]
+ assert_equal [:color, :area, :volume], third_model.stored_attributes[:data]
+ end
- assert_equal attributes, @john.attributes_for_coder
+ test "YAML coder initializes the store when a Nil value is given" do
+ assert_equal({}, @john.params)
end
test "dump, load and dump again a model" do
@@ -187,7 +189,6 @@ class StoreTest < ActiveRecord::TestCase
assert_equal @john, loaded
second_dump = YAML.dump(loaded)
- assert_equal dumped, second_dump
assert_equal @john, YAML.load(second_dump)
end
end
diff --git a/activerecord/test/cases/tasks/database_tasks_test.rb b/activerecord/test/cases/tasks/database_tasks_test.rb
index bf9e14fa4f..0f48c8d5fc 100644
--- a/activerecord/test/cases/tasks/database_tasks_test.rb
+++ b/activerecord/test/cases/tasks/database_tasks_test.rb
@@ -205,7 +205,7 @@ module ActiveRecord
ActiveRecord::Tasks::DatabaseTasks.drop_all
end
- def test_creates_configurations_with_local_ip
+ def test_drops_configurations_with_local_ip
@configurations[:development].merge!('host' => '127.0.0.1')
ActiveRecord::Tasks::DatabaseTasks.expects(:drop)
@@ -213,7 +213,7 @@ module ActiveRecord
ActiveRecord::Tasks::DatabaseTasks.drop_all
end
- def test_creates_configurations_with_local_host
+ def test_drops_configurations_with_local_host
@configurations[:development].merge!('host' => 'localhost')
ActiveRecord::Tasks::DatabaseTasks.expects(:drop)
@@ -221,7 +221,7 @@ module ActiveRecord
ActiveRecord::Tasks::DatabaseTasks.drop_all
end
- def test_creates_configurations_with_blank_hosts
+ def test_drops_configurations_with_blank_hosts
@configurations[:development].merge!('host' => nil)
ActiveRecord::Tasks::DatabaseTasks.expects(:drop)
@@ -241,7 +241,7 @@ module ActiveRecord
ActiveRecord::Base.stubs(:configurations).returns(@configurations)
end
- def test_creates_current_environment_database
+ def test_drops_current_environment_database
ActiveRecord::Tasks::DatabaseTasks.expects(:drop).
with('database' => 'prod-db')
@@ -285,6 +285,34 @@ module ActiveRecord
end
end
+ class DatabaseTasksPurgeCurrentTest < ActiveRecord::TestCase
+ def test_purges_current_environment_database
+ configurations = {
+ 'development' => {'database' => 'dev-db'},
+ 'test' => {'database' => 'test-db'},
+ 'production' => {'database' => 'prod-db'}
+ }
+ ActiveRecord::Base.stubs(:configurations).returns(configurations)
+
+ ActiveRecord::Tasks::DatabaseTasks.expects(:purge).
+ with('database' => 'prod-db')
+
+ ActiveRecord::Tasks::DatabaseTasks.purge_current('production')
+ end
+ end
+
+ class DatabaseTasksPurgeAllTest < ActiveRecord::TestCase
+ def test_purge_all_local_configurations
+ configurations = {:development => {'database' => 'my-db'}}
+ ActiveRecord::Base.stubs(:configurations).returns(configurations)
+
+ ActiveRecord::Tasks::DatabaseTasks.expects(:purge).
+ with('database' => 'my-db')
+
+ ActiveRecord::Tasks::DatabaseTasks.purge_all
+ end
+ end
+
class DatabaseTasksCharsetTest < ActiveRecord::TestCase
include DatabaseTasksSetupper
diff --git a/activerecord/test/cases/tasks/mysql_rake_test.rb b/activerecord/test/cases/tasks/mysql_rake_test.rb
index 3e3a2828f3..f58535f044 100644
--- a/activerecord/test/cases/tasks/mysql_rake_test.rb
+++ b/activerecord/test/cases/tasks/mysql_rake_test.rb
@@ -1,5 +1,6 @@
require 'cases/helper'
+if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
module ActiveRecord
class MysqlDBCreateTest < ActiveRecord::TestCase
def setup
@@ -196,8 +197,8 @@ module ActiveRecord
ActiveRecord::Base.stubs(:establish_connection).returns(true)
end
- def test_establishes_connection_to_test_database
- ActiveRecord::Base.expects(:establish_connection).with(:test)
+ def test_establishes_connection_to_the_appropriate_database
+ ActiveRecord::Base.expects(:establish_connection).with(@configuration)
ActiveRecord::Tasks::DatabaseTasks.purge @configuration
end
@@ -307,3 +308,4 @@ module ActiveRecord
end
end
+end
diff --git a/activerecord/test/cases/tasks/postgresql_rake_test.rb b/activerecord/test/cases/tasks/postgresql_rake_test.rb
index 6ea225178f..0d574d071c 100644
--- a/activerecord/test/cases/tasks/postgresql_rake_test.rb
+++ b/activerecord/test/cases/tasks/postgresql_rake_test.rb
@@ -1,5 +1,6 @@
require 'cases/helper'
+if current_adapter?(:PostgreSQLAdapter)
module ActiveRecord
class PostgreSQLDBCreateTest < ActiveRecord::TestCase
def setup
@@ -241,3 +242,4 @@ module ActiveRecord
end
end
+end
diff --git a/activerecord/test/cases/tasks/sqlite_rake_test.rb b/activerecord/test/cases/tasks/sqlite_rake_test.rb
index da3471adf9..750d5e42dc 100644
--- a/activerecord/test/cases/tasks/sqlite_rake_test.rb
+++ b/activerecord/test/cases/tasks/sqlite_rake_test.rb
@@ -1,6 +1,7 @@
require 'cases/helper'
require 'pathname'
+if current_adapter?(:SQLite3Adapter)
module ActiveRecord
class SqliteDBCreateTest < ActiveRecord::TestCase
def setup
@@ -189,3 +190,4 @@ module ActiveRecord
end
end
end
+end
diff --git a/activerecord/test/cases/test_case.rb b/activerecord/test/cases/test_case.rb
index 803a054d7e..23a170388e 100644
--- a/activerecord/test/cases/test_case.rb
+++ b/activerecord/test/cases/test_case.rb
@@ -10,13 +10,24 @@ module ActiveRecord
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
+ assert_equal expected.to_s, actual.to_s, message
+ end
+
+ def capture(stream)
+ stream = stream.to_s
+ captured_stream = Tempfile.new(stream)
+ stream_io = eval("$#{stream}")
+ origin_stream = stream_io.dup
+ stream_io.reopen(captured_stream)
+
+ yield
+
+ stream_io.rewind
+ return captured_stream.read
+ ensure
+ captured_stream.close
+ captured_stream.unlink
+ stream_io.reopen(origin_stream)
end
def capture_sql
diff --git a/activerecord/test/cases/timestamp_test.rb b/activerecord/test/cases/timestamp_test.rb
index 77ab427be0..0472246f71 100644
--- a/activerecord/test/cases/timestamp_test.rb
+++ b/activerecord/test/cases/timestamp_test.rb
@@ -71,24 +71,6 @@ class TimestampTest < ActiveRecord::TestCase
assert_equal @previously_updated_at, @developer.updated_at
end
- def test_saving_when_callback_sets_record_timestamps_to_false_doesnt_update_its_timestamp
- klass = Class.new(Developer) do
- before_update :cancel_record_timestamps
- def cancel_record_timestamps
- self.record_timestamps = false
- return true
- end
- end
-
- developer = klass.first
- previously_updated_at = developer.updated_at
-
- developer.name = "New Name"
- developer.save!
-
- assert_equal previously_updated_at, developer.updated_at
- end
-
def test_touching_an_attribute_updates_timestamp
previously_created_at = @developer.created_at
@developer.touch(:created_at)
@@ -399,6 +381,19 @@ class TimestampTest < ActiveRecord::TestCase
assert_not_equal time, pet.updated_at
end
+ def test_timestamp_column_values_are_present_in_the_callbacks
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = 'people'
+
+ before_create do
+ self.born_at = self.created_at
+ end
+ end
+
+ person = klass.create first_name: 'David'
+ assert_not_equal person.born_at, nil
+ end
+
def test_timestamp_attributes_for_create
toy = Toy.first
assert_equal [:created_at, :created_on], toy.send(:timestamp_attributes_for_create)
diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb
index e6ed85394b..f28a7b00e2 100644
--- a/activerecord/test/cases/transactions_test.rb
+++ b/activerecord/test/cases/transactions_test.rb
@@ -123,6 +123,19 @@ class TransactionTest < ActiveRecord::TestCase
assert !Topic.find(1).approved?
end
+ def test_rolling_back_in_a_callback_rollbacks_before_save
+ def @first.before_save_for_transaction
+ raise ActiveRecord::Rollback
+ end
+ assert !@first.approved
+
+ Topic.transaction do
+ @first.approved = true
+ @first.save!
+ end
+ assert !Topic.find(@first.id).approved?, "Should not commit the approved flag"
+ end
+
def test_raising_exception_in_nested_transaction_restore_state_in_save
topic = Topic.new
@@ -411,6 +424,26 @@ class TransactionTest < ActiveRecord::TestCase
end
end
+ def test_savepoints_name
+ Topic.transaction do
+ assert_nil Topic.connection.current_savepoint_name
+ assert_nil Topic.connection.current_transaction.savepoint_name
+
+ Topic.transaction(requires_new: true) do
+ assert_equal "active_record_1", Topic.connection.current_savepoint_name
+ assert_equal "active_record_1", Topic.connection.current_transaction.savepoint_name
+
+ Topic.transaction(requires_new: true) do
+ assert_equal "active_record_2", Topic.connection.current_savepoint_name
+ assert_equal "active_record_2", Topic.connection.current_transaction.savepoint_name
+ end
+
+ assert_equal "active_record_1", Topic.connection.current_savepoint_name
+ assert_equal "active_record_1", Topic.connection.current_transaction.savepoint_name
+ end
+ end
+ end
+
def test_rollback_when_commit_raises
Topic.connection.expects(:begin_db_transaction)
Topic.connection.expects(:commit_db_transaction).raises('OH NOES')
diff --git a/activerecord/test/cases/type/decimal_test.rb b/activerecord/test/cases/type/decimal_test.rb
new file mode 100644
index 0000000000..951cd879dd
--- /dev/null
+++ b/activerecord/test/cases/type/decimal_test.rb
@@ -0,0 +1,33 @@
+require "cases/helper"
+
+module ActiveRecord
+ module Type
+ class DecimalTest < ActiveRecord::TestCase
+ def test_type_cast_decimal
+ type = Decimal.new
+ assert_equal BigDecimal.new("0"), type.type_cast_from_user(BigDecimal.new("0"))
+ assert_equal BigDecimal.new("123"), type.type_cast_from_user(123.0)
+ assert_equal BigDecimal.new("1"), type.type_cast_from_user(:"1")
+ end
+
+ def test_type_cast_decimal_from_rational_with_precision
+ type = Decimal.new(precision: 2)
+ assert_equal BigDecimal("0.33"), type.type_cast_from_user(Rational(1, 3))
+ end
+
+ def test_type_cast_decimal_from_rational_without_precision_defaults_to_18_36
+ type = Decimal.new
+ assert_equal BigDecimal("0.333333333333333333E0"), type.type_cast_from_user(Rational(1, 3))
+ end
+
+ def test_type_cast_decimal_from_object_responding_to_d
+ value = Object.new
+ def value.to_d
+ BigDecimal.new("1")
+ end
+ type = Decimal.new
+ assert_equal BigDecimal("1"), type.type_cast_from_user(value)
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/type/string_test.rb b/activerecord/test/cases/type/string_test.rb
new file mode 100644
index 0000000000..420177ed49
--- /dev/null
+++ b/activerecord/test/cases/type/string_test.rb
@@ -0,0 +1,36 @@
+require 'cases/helper'
+
+module ActiveRecord
+ class StringTypeTest < ActiveRecord::TestCase
+ test "type casting" do
+ type = Type::String.new
+ assert_equal "1", type.type_cast_from_user(true)
+ assert_equal "0", type.type_cast_from_user(false)
+ assert_equal "123", type.type_cast_from_user(123)
+ end
+
+ test "values are duped coming out" do
+ s = "foo"
+ type = Type::String.new
+ assert_not_same s, type.type_cast_from_user(s)
+ assert_not_same s, type.type_cast_from_database(s)
+ end
+
+ test "string mutations are detected" do
+ klass = Class.new(Base)
+ klass.table_name = 'authors'
+
+ author = klass.create!(name: 'Sean')
+ assert_not author.changed?
+
+ author.name << ' Griffin'
+ assert author.name_changed?
+
+ author.save!
+ author.reload
+
+ assert_equal 'Sean Griffin', author.name
+ assert_not author.changed?
+ end
+ end
+end
diff --git a/activerecord/test/cases/type/type_map_test.rb b/activerecord/test/cases/type/type_map_test.rb
new file mode 100644
index 0000000000..4e32f92dd0
--- /dev/null
+++ b/activerecord/test/cases/type/type_map_test.rb
@@ -0,0 +1,130 @@
+require "cases/helper"
+
+module ActiveRecord
+ module Type
+ class TypeMapTest < ActiveRecord::TestCase
+ def test_default_type
+ mapping = TypeMap.new
+
+ assert_kind_of Value, mapping.lookup(:undefined)
+ end
+
+ def test_registering_types
+ boolean = Boolean.new
+ mapping = TypeMap.new
+
+ mapping.register_type(/boolean/i, boolean)
+
+ assert_equal mapping.lookup('boolean'), boolean
+ end
+
+ def test_overriding_registered_types
+ time = Time.new
+ timestamp = DateTime.new
+ mapping = TypeMap.new
+
+ mapping.register_type(/time/i, time)
+ mapping.register_type(/time/i, timestamp)
+
+ assert_equal mapping.lookup('time'), timestamp
+ end
+
+ def test_fuzzy_lookup
+ string = String.new
+ mapping = TypeMap.new
+
+ mapping.register_type(/varchar/i, string)
+
+ assert_equal mapping.lookup('varchar(20)'), string
+ end
+
+ def test_aliasing_types
+ string = String.new
+ mapping = TypeMap.new
+
+ mapping.register_type(/string/i, string)
+ mapping.alias_type(/varchar/i, 'string')
+
+ assert_equal mapping.lookup('varchar'), string
+ end
+
+ def test_changing_type_changes_aliases
+ time = Time.new
+ timestamp = DateTime.new
+ mapping = TypeMap.new
+
+ mapping.register_type(/timestamp/i, time)
+ mapping.alias_type(/datetime/i, 'timestamp')
+ mapping.register_type(/timestamp/i, timestamp)
+
+ assert_equal mapping.lookup('datetime'), timestamp
+ end
+
+ def test_aliases_keep_metadata
+ mapping = TypeMap.new
+
+ mapping.register_type(/decimal/i) { |sql_type| sql_type }
+ mapping.alias_type(/number/i, 'decimal')
+
+ assert_equal mapping.lookup('number(20)'), 'decimal(20)'
+ assert_equal mapping.lookup('number'), 'decimal'
+ end
+
+ def test_register_proc
+ string = String.new
+ binary = Binary.new
+ mapping = TypeMap.new
+
+ mapping.register_type(/varchar/i) do |type|
+ if type.include?('(')
+ string
+ else
+ binary
+ end
+ end
+
+ assert_equal mapping.lookup('varchar(20)'), string
+ assert_equal mapping.lookup('varchar'), binary
+ end
+
+ def test_additional_lookup_args
+ mapping = TypeMap.new
+
+ mapping.register_type(/varchar/i) do |type, limit|
+ if limit > 255
+ 'text'
+ else
+ 'string'
+ end
+ end
+ mapping.alias_type(/string/i, 'varchar')
+
+ assert_equal mapping.lookup('varchar', 200), 'string'
+ assert_equal mapping.lookup('varchar', 400), 'text'
+ assert_equal mapping.lookup('string', 400), 'text'
+ end
+
+ def test_requires_value_or_block
+ mapping = TypeMap.new
+
+ assert_raises(ArgumentError) do
+ mapping.register_type(/only key/i)
+ end
+ end
+
+ def test_lookup_non_strings
+ mapping = HashLookupTypeMap.new
+
+ mapping.register_type(1, 'string')
+ mapping.register_type(2, 'int')
+ mapping.alias_type(3, 1)
+
+ assert_equal mapping.lookup(1), 'string'
+ assert_equal mapping.lookup(2), 'int'
+ assert_equal mapping.lookup(3), 'string'
+ assert_kind_of Type::Value, mapping.lookup(4)
+ end
+ end
+ end
+end
+
diff --git a/activerecord/test/cases/types_test.rb b/activerecord/test/cases/types_test.rb
new file mode 100644
index 0000000000..5c54812f30
--- /dev/null
+++ b/activerecord/test/cases/types_test.rb
@@ -0,0 +1,163 @@
+require "cases/helper"
+require 'models/company'
+
+module ActiveRecord
+ module ConnectionAdapters
+ class TypesTest < ActiveRecord::TestCase
+ def test_type_cast_boolean
+ type = Type::Boolean.new
+ assert type.type_cast_from_user('').nil?
+ assert type.type_cast_from_user(nil).nil?
+
+ assert type.type_cast_from_user(true)
+ assert type.type_cast_from_user(1)
+ assert type.type_cast_from_user('1')
+ assert type.type_cast_from_user('t')
+ assert type.type_cast_from_user('T')
+ assert type.type_cast_from_user('true')
+ assert type.type_cast_from_user('TRUE')
+ assert type.type_cast_from_user('on')
+ assert type.type_cast_from_user('ON')
+
+ # explicitly check for false vs nil
+ assert_equal false, type.type_cast_from_user(false)
+ assert_equal false, type.type_cast_from_user(0)
+ assert_equal false, type.type_cast_from_user('0')
+ assert_equal false, type.type_cast_from_user('f')
+ assert_equal false, type.type_cast_from_user('F')
+ assert_equal false, type.type_cast_from_user('false')
+ assert_equal false, type.type_cast_from_user('FALSE')
+ assert_equal false, type.type_cast_from_user('off')
+ assert_equal false, type.type_cast_from_user('OFF')
+ assert_equal false, type.type_cast_from_user(' ')
+ assert_equal false, type.type_cast_from_user("\u3000\r\n")
+ assert_equal false, type.type_cast_from_user("\u0000")
+ assert_equal false, type.type_cast_from_user('SOMETHING RANDOM')
+ end
+
+ def test_type_cast_integer
+ type = Type::Integer.new
+ assert_equal 1, type.type_cast_from_user(1)
+ assert_equal 1, type.type_cast_from_user('1')
+ assert_equal 1, type.type_cast_from_user('1ignore')
+ assert_equal 0, type.type_cast_from_user('bad1')
+ assert_equal 0, type.type_cast_from_user('bad')
+ assert_equal 1, type.type_cast_from_user(1.7)
+ assert_equal 0, type.type_cast_from_user(false)
+ assert_equal 1, type.type_cast_from_user(true)
+ assert_nil type.type_cast_from_user(nil)
+ end
+
+ def test_type_cast_non_integer_to_integer
+ type = Type::Integer.new
+ assert_nil type.type_cast_from_user([1,2])
+ assert_nil type.type_cast_from_user({1 => 2})
+ assert_nil type.type_cast_from_user((1..2))
+ end
+
+ def test_type_cast_activerecord_to_integer
+ type = Type::Integer.new
+ firm = Firm.create(:name => 'Apple')
+ assert_nil type.type_cast_from_user(firm)
+ end
+
+ def test_type_cast_object_without_to_i_to_integer
+ type = Type::Integer.new
+ assert_nil type.type_cast_from_user(Object.new)
+ end
+
+ def test_type_cast_nan_and_infinity_to_integer
+ type = Type::Integer.new
+ assert_nil type.type_cast_from_user(Float::NAN)
+ assert_nil type.type_cast_from_user(1.0/0.0)
+ end
+
+ def test_changing_integers
+ type = Type::Integer.new
+
+ assert type.changed?(5, 5, '5wibble')
+ assert_not type.changed?(5, 5, '5')
+ assert_not type.changed?(5, 5, '5.0')
+ assert_not type.changed?(nil, nil, nil)
+ end
+
+ def test_type_cast_float
+ type = Type::Float.new
+ assert_equal 1.0, type.type_cast_from_user("1")
+ end
+
+ def test_changing_float
+ type = Type::Float.new
+
+ assert type.changed?(5.0, 5.0, '5wibble')
+ assert_not type.changed?(5.0, 5.0, '5')
+ assert_not type.changed?(5.0, 5.0, '5.0')
+ assert_not type.changed?(nil, nil, nil)
+ end
+
+ def test_type_cast_binary
+ type = Type::Binary.new
+ assert_equal nil, type.type_cast_from_user(nil)
+ assert_equal "1", type.type_cast_from_user("1")
+ assert_equal 1, type.type_cast_from_user(1)
+ end
+
+ def test_type_cast_time
+ type = Type::Time.new
+ assert_equal nil, type.type_cast_from_user(nil)
+ assert_equal nil, type.type_cast_from_user('')
+ assert_equal nil, type.type_cast_from_user('ABC')
+
+ time_string = Time.now.utc.strftime("%T")
+ assert_equal time_string, type.type_cast_from_user(time_string).strftime("%T")
+ end
+
+ def test_type_cast_datetime_and_timestamp
+ type = Type::DateTime.new
+ assert_equal nil, type.type_cast_from_user(nil)
+ assert_equal nil, type.type_cast_from_user('')
+ assert_equal nil, type.type_cast_from_user(' ')
+ assert_equal nil, type.type_cast_from_user('ABC')
+
+ datetime_string = Time.now.utc.strftime("%FT%T")
+ assert_equal datetime_string, type.type_cast_from_user(datetime_string).strftime("%FT%T")
+ end
+
+ def test_type_cast_date
+ type = Type::Date.new
+ assert_equal nil, type.type_cast_from_user(nil)
+ assert_equal nil, type.type_cast_from_user('')
+ assert_equal nil, type.type_cast_from_user(' ')
+ assert_equal nil, type.type_cast_from_user('ABC')
+
+ date_string = Time.now.utc.strftime("%F")
+ assert_equal date_string, type.type_cast_from_user(date_string).strftime("%F")
+ end
+
+ def test_type_cast_duration_to_integer
+ type = Type::Integer.new
+ assert_equal 1800, type.type_cast_from_user(30.minutes)
+ assert_equal 7200, type.type_cast_from_user(2.hours)
+ end
+
+ def test_string_to_time_with_timezone
+ [:utc, :local].each do |zone|
+ with_timezone_config default: zone do
+ type = Type::DateTime.new
+ assert_equal Time.utc(2013, 9, 4, 0, 0, 0), type.type_cast_from_user("Wed, 04 Sep 2013 03:00:00 EAT")
+ end
+ end
+ end
+
+ if current_adapter?(:SQLite3Adapter)
+ def test_binary_encoding
+ type = SQLite3Binary.new
+ utf8_string = "a string".encode(Encoding::UTF_8)
+ type_cast = type.type_cast_from_user(utf8_string)
+
+ assert_equal Encoding::ASCII_8BIT, type_cast.encoding
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/validations/presence_validation_test.rb b/activerecord/test/cases/validations/presence_validation_test.rb
index 3790d3c8cf..4f38849131 100644
--- a/activerecord/test/cases/validations/presence_validation_test.rb
+++ b/activerecord/test/cases/validations/presence_validation_test.rb
@@ -52,14 +52,15 @@ class PresenceValidationTest < ActiveRecord::TestCase
end
def test_validates_presence_doesnt_convert_to_array
- Speedometer.validates_presence_of :dashboard
+ speedometer = Class.new(Speedometer)
+ speedometer.validates_presence_of :dashboard
dash = Dashboard.new
# dashboard has to_a method
def dash.to_a; ['(/)', '(\)']; end
- s = Speedometer.new
+ s = speedometer.new
s.dashboard = dash
assert_nothing_raised { s.valid? }
diff --git a/activerecord/test/cases/validations_test.rb b/activerecord/test/cases/validations_test.rb
index d80da06e27..55804f9576 100644
--- a/activerecord/test/cases/validations_test.rb
+++ b/activerecord/test/cases/validations_test.rb
@@ -14,28 +14,28 @@ class ValidationsTest < ActiveRecord::TestCase
# Other classes we mess with will be dealt with in the specific tests
repair_validations(Topic)
- def test_error_on_create
+ def test_valid_uses_create_context_when_new
r = WrongReply.new
r.title = "Wrong Create"
- assert !r.save
+ assert_not r.valid?
assert r.errors[:title].any?, "A reply with a bad title should mark that attribute as invalid"
assert_equal ["is Wrong Create"], r.errors[:title], "A reply with a bad content should contain an error"
end
- def test_error_on_update
+ def test_valid_uses_update_context_when_persisted
r = WrongReply.new
r.title = "Bad"
r.content = "Good"
- assert r.save, "First save should be successful"
+ assert r.save, "First validation should be successful"
r.title = "Wrong Update"
- assert !r.save, "Second save should fail"
+ assert_not r.valid?, "Second validation should fail"
assert r.errors[:title].any?, "A reply with a bad title should mark that attribute as invalid"
assert_equal ["is Wrong Update"], r.errors[:title], "A reply with a bad content should contain an error"
end
- def test_error_on_given_context
+ def test_valid_using_special_context
r = WrongReply.new(:title => "Valid title")
assert !r.valid?(:special_case)
assert_equal "Invalid", r.errors[:author_name].join
@@ -45,11 +45,11 @@ class ValidationsTest < ActiveRecord::TestCase
assert r.valid?(:special_case)
r.author_name = nil
- assert !r.save(:context => :special_case)
+ assert_not r.valid?(:special_case)
assert_equal "Invalid", r.errors[:author_name].join
r.author_name = "secret"
- assert r.save(:context => :special_case)
+ assert r.valid?(:special_case)
end
def test_validate
@@ -78,6 +78,20 @@ class ValidationsTest < ActiveRecord::TestCase
assert_equal r, invalid.record
end
+ def test_validate_with_bang
+ assert_raise(ActiveRecord::RecordInvalid) do
+ WrongReply.new.validate!
+ end
+ end
+
+ def test_validate_with_bang_and_context
+ assert_raise(ActiveRecord::RecordInvalid) do
+ WrongReply.new.validate!(:special_case)
+ end
+ r = WrongReply.new(:title => "Valid title", :author_name => "secret", :content => "Good")
+ assert r.validate!(:special_case)
+ end
+
def test_exception_on_create_bang_many
assert_raise(ActiveRecord::RecordInvalid) do
WrongReply.create!([ { "title" => "OK" }, { "title" => "Wrong Create" }])
@@ -100,7 +114,7 @@ class ValidationsTest < ActiveRecord::TestCase
end
end
- def test_create_without_validation
+ def test_save_without_validation
reply = WrongReply.new
assert !reply.save
assert reply.save(:validate => false)
diff --git a/activerecord/test/cases/xml_serialization_test.rb b/activerecord/test/cases/xml_serialization_test.rb
index 3cb617497d..c34e7d5a30 100644
--- a/activerecord/test/cases/xml_serialization_test.rb
+++ b/activerecord/test/cases/xml_serialization_test.rb
@@ -226,7 +226,6 @@ class DatabaseConnectedXmlSerializationTest < ActiveRecord::TestCase
xml = REXML::Document.new(topics(:first).to_xml(:indent => 0))
bonus_time_in_current_timezone = topics(:first).bonus_time.xmlschema
written_on_in_current_timezone = topics(:first).written_on.xmlschema
- last_read_in_current_timezone = topics(:first).last_read.xmlschema
assert_equal "topic", xml.root.name
assert_equal "The First Topic" , xml.elements["//title"].text
@@ -248,14 +247,9 @@ class DatabaseConnectedXmlSerializationTest < ActiveRecord::TestCase
assert_equal "integer", xml.elements["//parent-id"].attributes['type']
assert_equal "true", xml.elements["//parent-id"].attributes['nil']
- if current_adapter?(:SybaseAdapter)
- assert_equal last_read_in_current_timezone, xml.elements["//last-read"].text
- assert_equal "dateTime" , xml.elements["//last-read"].attributes['type']
- else
- # Oracle enhanced adapter allows to define Date attributes in model class (see topic.rb)
- assert_equal "2004-04-15", xml.elements["//last-read"].text
- assert_equal "date" , xml.elements["//last-read"].attributes['type']
- end
+ # Oracle enhanced adapter allows to define Date attributes in model class (see topic.rb)
+ assert_equal "2004-04-15", xml.elements["//last-read"].text
+ assert_equal "date" , xml.elements["//last-read"].attributes['type']
# Oracle and DB2 don't have true boolean or time-only fields
unless current_adapter?(:OracleAdapter, :DB2Adapter)
@@ -422,8 +416,9 @@ class DatabaseConnectedXmlSerializationTest < ActiveRecord::TestCase
def test_should_support_aliased_attributes
xml = Author.select("name as firstname").to_xml
- array = Hash.from_xml(xml)['authors']
- assert_equal array.size, array.select { |author| author.has_key? 'firstname' }.size
+ Author.all.each do |author|
+ assert xml.include?(%(<firstname>#{author.name}</firstname>)), xml
+ end
end
def test_array_to_xml_including_has_many_association
diff --git a/activerecord/test/cases/yaml_serialization_test.rb b/activerecord/test/cases/yaml_serialization_test.rb
index 15815d56e4..bce59b4fcd 100644
--- a/activerecord/test/cases/yaml_serialization_test.rb
+++ b/activerecord/test/cases/yaml_serialization_test.rb
@@ -1,8 +1,11 @@
require 'cases/helper'
require 'models/topic'
+require 'models/reply'
+require 'models/post'
+require 'models/author'
class YamlSerializationTest < ActiveRecord::TestCase
- fixtures :topics
+ fixtures :topics, :authors, :posts
def test_to_yaml_with_time_with_zone_should_not_raise_exception
with_timezone_config aware_attributes: true, zone: "Pacific Time (US & Canada)" do
@@ -23,13 +26,6 @@ class YamlSerializationTest < ActiveRecord::TestCase
assert_equal({:omg=>:lol}, YAML.load(YAML.dump(topic)).content)
end
- def test_encode_with_coder
- topic = Topic.first
- coder = {}
- topic.encode_with coder
- assert_equal({'attributes' => topic.attributes}, coder)
- end
-
def test_psych_roundtrip
topic = Topic.first
assert topic
@@ -47,4 +43,44 @@ class YamlSerializationTest < ActiveRecord::TestCase
def test_active_record_relation_serialization
[Topic.all].to_yaml
end
+
+ def test_raw_types_are_not_changed_on_round_trip
+ topic = Topic.new(parent_id: "123")
+ assert_equal "123", topic.parent_id_before_type_cast
+ assert_equal "123", YAML.load(YAML.dump(topic)).parent_id_before_type_cast
+ end
+
+ def test_cast_types_are_not_changed_on_round_trip
+ topic = Topic.new(parent_id: "123")
+ assert_equal 123, topic.parent_id
+ assert_equal 123, YAML.load(YAML.dump(topic)).parent_id
+ end
+
+ def test_new_records_remain_new_after_round_trip
+ topic = Topic.new
+
+ assert topic.new_record?, "Sanity check that new records are new"
+ assert YAML.load(YAML.dump(topic)).new_record?, "Record should be new after deserialization"
+
+ topic.save!
+
+ assert_not topic.new_record?, "Saved records are not new"
+ assert_not YAML.load(YAML.dump(topic)).new_record?, "Saved record should not be new after deserialization"
+
+ topic = Topic.select('title').last
+
+ assert_not topic.new_record?, "Loaded records without ID are not new"
+ assert_not YAML.load(YAML.dump(topic)).new_record?, "Record should not be new after deserialization"
+ end
+
+ def test_types_of_virtual_columns_are_not_changed_on_round_trip
+ author = Author.select('authors.*, count(posts.id) as posts_count')
+ .joins(:posts)
+ .group('authors.id')
+ .first
+ dumped = YAML.load(YAML.dump(author))
+
+ assert_equal 5, author.posts_count
+ assert_equal 5, dumped.posts_count
+ end
end
diff --git a/activerecord/test/config.example.yml b/activerecord/test/config.example.yml
index 479b8c050d..a54914c372 100644
--- a/activerecord/test/config.example.yml
+++ b/activerecord/test/config.example.yml
@@ -51,28 +51,6 @@ connections:
password: arunit
database: arunit2
- firebird:
- arunit:
- host: localhost
- username: rails
- password: rails
- charset: UTF8
- arunit2:
- host: localhost
- username: rails
- password: rails
- charset: UTF8
-
- frontbase:
- arunit:
- host: localhost
- username: rails
- session_name: unittest-<%= $$ %>
- arunit2:
- host: localhost
- username: rails
- session_name: unittest-<%= $$ %>
-
mysql:
arunit:
username: rails
@@ -130,11 +108,3 @@ connections:
arunit2:
adapter: sqlite3
database: ':memory:'
-
- sybase:
- arunit:
- host: database_ASE
- username: sa
- arunit2:
- host: database_ASE
- username: sa
diff --git a/activerecord/test/fixtures/books.yml b/activerecord/test/fixtures/books.yml
index fb48645456..abe56752c6 100644
--- a/activerecord/test/fixtures/books.yml
+++ b/activerecord/test/fixtures/books.yml
@@ -2,8 +2,10 @@ awdr:
author_id: 1
id: 1
name: "Agile Web Development with Rails"
+ format: "paperback"
rfr:
author_id: 1
id: 2
name: "Ruby for Rails"
+ format: "ebook"
diff --git a/activerecord/test/fixtures/fk_test_has_pk.yml b/activerecord/test/fixtures/fk_test_has_pk.yml
index c93952180b..73882bac41 100644
--- a/activerecord/test/fixtures/fk_test_has_pk.yml
+++ b/activerecord/test/fixtures/fk_test_has_pk.yml
@@ -1,2 +1,2 @@
first:
- id: 1 \ No newline at end of file
+ pk_id: 1 \ No newline at end of file
diff --git a/activerecord/test/fixtures/posts.yml b/activerecord/test/fixtures/posts.yml
index 7298096025..86d46f753a 100644
--- a/activerecord/test/fixtures/posts.yml
+++ b/activerecord/test/fixtures/posts.yml
@@ -4,7 +4,6 @@ welcome:
title: Welcome to the weblog
body: Such a lovely day
comments_count: 2
- taggings_count: 1
tags_count: 1
type: Post
@@ -14,7 +13,6 @@ thinking:
title: So I was thinking
body: Like I hopefully always am
comments_count: 1
- taggings_count: 1
tags_count: 1
type: SpecialPost
diff --git a/activerecord/test/fixtures/topics.yml b/activerecord/test/fixtures/topics.yml
index bf049abbf1..4c98b10380 100644
--- a/activerecord/test/fixtures/topics.yml
+++ b/activerecord/test/fixtures/topics.yml
@@ -6,7 +6,7 @@ first:
written_on: 2003-07-16t15:28:11.2233+01:00
last_read: 2004-04-15
bonus_time: 2005-01-30t15:28:00.00+01:00
- content: Have a nice day
+ content: "--- Have a nice day\n...\n"
approved: false
replies_count: 1
@@ -15,7 +15,7 @@ second:
title: The Second Topic of the day
author_name: Mary
written_on: 2004-07-15t15:28:00.0099+01:00
- content: Have a nice day
+ content: "--- Have a nice day\n...\n"
approved: true
replies_count: 0
parent_id: 1
@@ -26,7 +26,7 @@ third:
title: The Third Topic of the day
author_name: Carl
written_on: 2012-08-12t20:24:22.129346+00:00
- content: I'm a troll
+ content: "--- I'm a troll\n...\n"
approved: true
replies_count: 1
@@ -35,7 +35,7 @@ fourth:
title: The Fourth Topic of the day
author_name: Carl
written_on: 2006-07-15t15:28:00.0099+01:00
- content: Why not?
+ content: "--- Why not?\n...\n"
approved: true
type: Reply
parent_id: 3
@@ -45,5 +45,5 @@ fifth:
title: The Fifth Topic of the day
author_name: Jason
written_on: 2013-07-13t12:11:00.0099+01:00
- content: Omakase
+ content: "--- Omakase\n...\n"
approved: true
diff --git a/activerecord/test/models/author.rb b/activerecord/test/models/author.rb
index c197951c71..8949cf5826 100644
--- a/activerecord/test/models/author.rb
+++ b/activerecord/test/models/author.rb
@@ -140,6 +140,8 @@ class Author < ActiveRecord::Base
has_many :posts_with_default_include, :class_name => 'PostWithDefaultInclude'
has_many :comments_on_posts_with_default_include, :through => :posts_with_default_include, :source => :comments
+ has_many :posts_with_signature, ->(record) { where("posts.title LIKE ?", "%by #{record.name.downcase}%") }, class_name: "Post"
+
scope :relation_include_posts, -> { includes(:posts) }
scope :relation_include_tags, -> { includes(:tags) }
diff --git a/activerecord/test/models/club.rb b/activerecord/test/models/club.rb
index a762ad4bb5..6ceafe5858 100644
--- a/activerecord/test/models/club.rb
+++ b/activerecord/test/models/club.rb
@@ -14,3 +14,10 @@ class Club < ActiveRecord::Base
"I'm sorry sir, this is a *private* club, not a *pirate* club"
end
end
+
+class SuperClub < ActiveRecord::Base
+ self.table_name = "clubs"
+
+ has_many :memberships, class_name: 'SuperMembership', foreign_key: 'club_id'
+ has_many :members, through: :memberships
+end
diff --git a/activerecord/test/models/comment.rb b/activerecord/test/models/comment.rb
index ede5fbd0c6..15970758db 100644
--- a/activerecord/test/models/comment.rb
+++ b/activerecord/test/models/comment.rb
@@ -7,6 +7,9 @@ class Comment < ActiveRecord::Base
scope :created, -> { all }
belongs_to :post, :counter_cache => true
+ belongs_to :author, polymorphic: true
+ belongs_to :resource, polymorphic: true
+
has_many :ratings
belongs_to :first_post, :foreign_key => :post_id
@@ -26,6 +29,10 @@ class Comment < ActiveRecord::Base
all
end
scope :all_as_scope, -> { all }
+
+ def to_s
+ body
+ end
end
class SpecialComment < Comment
@@ -36,3 +43,11 @@ end
class VerySpecialComment < Comment
end
+
+class CommentThatAutomaticallyAltersPostBody < Comment
+ belongs_to :post, class_name: "PostThatLoadsCommentsInAnAfterSaveHook", foreign_key: :post_id
+
+ after_save do |comment|
+ comment.post.update_attributes(body: "Automatically altered")
+ end
+end
diff --git a/activerecord/test/models/company_in_module.rb b/activerecord/test/models/company_in_module.rb
index 38b0b6aafa..dae102d12b 100644
--- a/activerecord/test/models/company_in_module.rb
+++ b/activerecord/test/models/company_in_module.rb
@@ -46,6 +46,24 @@ module MyApplication
end
end
end
+
+ module Suffixed
+ def self.table_name_suffix
+ '_suffixed'
+ end
+
+ class Company < ActiveRecord::Base
+ end
+
+ class Firm < Company
+ self.table_name = 'companies'
+ end
+
+ module Nested
+ class Company < ActiveRecord::Base
+ end
+ end
+ end
end
module Billing
diff --git a/activerecord/test/models/contact.rb b/activerecord/test/models/contact.rb
index a1cb8d62b6..3ea17c3abf 100644
--- a/activerecord/test/models/contact.rb
+++ b/activerecord/test/models/contact.rb
@@ -8,6 +8,7 @@ module ContactFakeColumns
table_name => 'id'
}
+ column :id, :integer
column :name, :string
column :age, :integer
column :avatar, :binary
diff --git a/activerecord/test/models/developer.rb b/activerecord/test/models/developer.rb
index 762259ffa3..5bd2f00129 100644
--- a/activerecord/test/models/developer.rb
+++ b/activerecord/test/models/developer.rb
@@ -13,6 +13,8 @@ class Developer < ActiveRecord::Base
end
end
+ accepts_nested_attributes_for :projects
+
has_and_belongs_to_many :projects_extended_by_name,
-> { extending(DeveloperProjectsAssociationExtension) },
:class_name => "Project",
@@ -74,12 +76,6 @@ class AuditLog < ActiveRecord::Base
belongs_to :unvalidated_developer, :class_name => 'Developer'
end
-DeveloperSalary = Struct.new(:amount)
-class DeveloperWithAggregate < ActiveRecord::Base
- self.table_name = 'developers'
- composed_of :salary, :class_name => 'DeveloperSalary', :mapping => [%w(salary amount)]
-end
-
class DeveloperWithBeforeDestroyRaise < ActiveRecord::Base
self.table_name = 'developers'
has_and_belongs_to_many :projects, :join_table => 'developers_projects', :foreign_key => 'developer_id'
diff --git a/activerecord/test/models/face.rb b/activerecord/test/models/face.rb
index edb75d333f..91e46f83e5 100644
--- a/activerecord/test/models/face.rb
+++ b/activerecord/test/models/face.rb
@@ -1,6 +1,8 @@
class Face < ActiveRecord::Base
belongs_to :man, :inverse_of => :face
belongs_to :polymorphic_man, :polymorphic => true, :inverse_of => :polymorphic_face
+ # Oracle identifier lengh is limited to 30 bytes or less, `polymorphic` renamed `poly`
+ belongs_to :poly_man_without_inverse, :polymorphic => true
# These is a "broken" inverse_of for the purposes of testing
belongs_to :horrible_man, :class_name => 'Man', :inverse_of => :horrible_face
belongs_to :horrible_polymorphic_man, :polymorphic => true, :inverse_of => :horrible_polymorphic_face
diff --git a/activerecord/test/models/man.rb b/activerecord/test/models/man.rb
index f4d127730c..4fbb6b226b 100644
--- a/activerecord/test/models/man.rb
+++ b/activerecord/test/models/man.rb
@@ -1,6 +1,7 @@
class Man < ActiveRecord::Base
has_one :face, :inverse_of => :man
has_one :polymorphic_face, :class_name => 'Face', :as => :polymorphic_man, :inverse_of => :polymorphic_man
+ has_one :polymorphic_face_without_inverse, :class_name => 'Face', :as => :poly_man_without_inverse
has_many :interests, :inverse_of => :man
has_many :polymorphic_interests, :class_name => 'Interest', :as => :polymorphic_man, :inverse_of => :polymorphic_man
# These are "broken" inverse_of associations for the purposes of testing
diff --git a/activerecord/test/models/owner.rb b/activerecord/test/models/owner.rb
index cf24502d3a..2e3a9a3681 100644
--- a/activerecord/test/models/owner.rb
+++ b/activerecord/test/models/owner.rb
@@ -3,6 +3,18 @@ class Owner < ActiveRecord::Base
has_many :pets, -> { order 'pets.name desc' }
has_many :toys, :through => :pets
+ belongs_to :last_pet, class_name: 'Pet'
+ scope :including_last_pet, -> {
+ select(%q[
+ owners.*, (
+ select p.pet_id from pets p
+ where p.owner_id = owners.owner_id
+ order by p.name desc
+ limit 1
+ ) as last_pet_id
+ ]).includes(:last_pet)
+ }
+
after_commit :execute_blocks
def blocks
diff --git a/activerecord/test/models/pirate.rb b/activerecord/test/models/pirate.rb
index e472cf951d..90a3c3ecee 100644
--- a/activerecord/test/models/pirate.rb
+++ b/activerecord/test/models/pirate.rb
@@ -84,3 +84,9 @@ end
class DestructivePirate < Pirate
has_one :dependent_ship, :class_name => 'Ship', :foreign_key => :pirate_id, :dependent => :destroy
end
+
+class FamousPirate < ActiveRecord::Base
+ self.table_name = 'pirates'
+ has_many :famous_ships
+ validates_presence_of :catchphrase, on: :conference
+end \ No newline at end of file
diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb
index 6399a68d95..a29858213b 100644
--- a/activerecord/test/models/post.rb
+++ b/activerecord/test/models/post.rb
@@ -88,7 +88,7 @@ class Post < ActiveRecord::Base
has_and_belongs_to_many :categories
has_and_belongs_to_many :special_categories, :join_table => "categories_posts", :association_foreign_key => 'category_id'
- has_many :taggings, :as => :taggable
+ has_many :taggings, :as => :taggable, :counter_cache => :tags_count
has_many :tags, :through => :taggings do
def add_joins_and_select
select('tags.*, authors.id as author_id')
@@ -167,10 +167,6 @@ class Post < ActiveRecord::Base
return @log if message.nil?
@log << [message, side, new_record]
end
-
- def self.what_are_you
- 'a post...'
- end
end
class SpecialPost < Post; end
@@ -212,3 +208,12 @@ class SpecialPostWithDefaultScope < ActiveRecord::Base
self.table_name = 'posts'
default_scope { where(:id => [1, 5,6]) }
end
+
+class PostThatLoadsCommentsInAnAfterSaveHook < ActiveRecord::Base
+ self.table_name = 'posts'
+ has_many :comments, class_name: "CommentThatAutomaticallyAltersPostBody", foreign_key: :post_id
+
+ after_save do |post|
+ post.comments.load
+ end
+end
diff --git a/activerecord/test/models/publisher.rb b/activerecord/test/models/publisher.rb
new file mode 100644
index 0000000000..0d4a7f9235
--- /dev/null
+++ b/activerecord/test/models/publisher.rb
@@ -0,0 +1,2 @@
+module Publisher
+end
diff --git a/activerecord/test/models/publisher/article.rb b/activerecord/test/models/publisher/article.rb
new file mode 100644
index 0000000000..d73a8eb936
--- /dev/null
+++ b/activerecord/test/models/publisher/article.rb
@@ -0,0 +1,4 @@
+class Publisher::Article < ActiveRecord::Base
+ has_and_belongs_to_many :magazines
+ has_and_belongs_to_many :tags
+end
diff --git a/activerecord/test/models/publisher/magazine.rb b/activerecord/test/models/publisher/magazine.rb
new file mode 100644
index 0000000000..82e1a14008
--- /dev/null
+++ b/activerecord/test/models/publisher/magazine.rb
@@ -0,0 +1,3 @@
+class Publisher::Magazine < ActiveRecord::Base
+ has_and_belongs_to_many :articles
+end
diff --git a/activerecord/test/models/ship.rb b/activerecord/test/models/ship.rb
index 3da031946f..77a4728d0b 100644
--- a/activerecord/test/models/ship.rb
+++ b/activerecord/test/models/ship.rb
@@ -17,3 +17,9 @@ class Ship < ActiveRecord::Base
false
end
end
+
+class FamousShip < ActiveRecord::Base
+ self.table_name = 'ships'
+ belongs_to :famous_pirate
+ validates_presence_of :name, on: :conference
+end
diff --git a/activerecord/test/models/tagging.rb b/activerecord/test/models/tagging.rb
index f91f2ad2e9..a6c05da26a 100644
--- a/activerecord/test/models/tagging.rb
+++ b/activerecord/test/models/tagging.rb
@@ -8,6 +8,6 @@ class Tagging < ActiveRecord::Base
belongs_to :invalid_tag, :class_name => 'Tag', :foreign_key => 'tag_id'
belongs_to :blue_tag, -> { where :tags => { :name => 'Blue' } }, :class_name => 'Tag', :foreign_key => :tag_id
belongs_to :tag_with_primary_key, :class_name => 'Tag', :foreign_key => :tag_id, :primary_key => :custom_primary_key
- belongs_to :taggable, :polymorphic => true, :counter_cache => true
+ belongs_to :taggable, :polymorphic => true, :counter_cache => :tags_count
has_many :things, :through => :taggable
end
diff --git a/activerecord/test/schema/postgresql_specific_schema.rb b/activerecord/test/schema/postgresql_specific_schema.rb
index 4fcbf4dbd2..e9294a11b9 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 postgresql_ltrees
+ %w(postgresql_tsvectors postgresql_hstores postgresql_arrays postgresql_moneys postgresql_numbers postgresql_times postgresql_network_addresses postgresql_uuids postgresql_ltrees
postgresql_oids postgresql_xml_data_type defaults geometrics postgresql_timestamp_with_zones postgresql_partitioned_table postgresql_partitioned_table_parent postgresql_json_data_type postgresql_citext).each do |table_name|
execute "DROP TABLE IF EXISTS #{quote_table_name table_name}"
end
@@ -118,13 +118,6 @@ _SQL
end
execute <<_SQL
- CREATE TABLE postgresql_moneys (
- id SERIAL PRIMARY KEY,
- wealth MONEY
- );
-_SQL
-
- execute <<_SQL
CREATE TABLE postgresql_numbers (
id SERIAL PRIMARY KEY,
single REAL,
@@ -150,14 +143,6 @@ _SQL
_SQL
execute <<_SQL
- CREATE TABLE postgresql_bit_strings (
- id SERIAL PRIMARY KEY,
- bit_string BIT(8),
- bit_string_varying BIT VARYING(8)
- );
-_SQL
-
- execute <<_SQL
CREATE TABLE postgresql_oids (
id SERIAL PRIMARY KEY,
obj_id OID
diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb
index da3074e90f..a8b21904ac 100644
--- a/activerecord/test/schema/schema.rb
+++ b/activerecord/test/schema/schema.rb
@@ -9,14 +9,6 @@ ActiveRecord::Schema.define do
#put adapter specific setup here
case adapter_name
- # For Firebird, set the sequence values 10000 when create_table is called;
- # this prevents primary key collisions between "normally" created records
- # and fixture-based (YAML) records.
- when "Firebird"
- def create_table(*args, &block)
- ActiveRecord::Base.connection.create_table(*args, &block)
- ActiveRecord::Base.connection.execute "SET GENERATOR #{args.first}_seq TO 10000"
- end
when "PostgreSQL"
enable_uuid_ossp!(ActiveRecord::Base.connection)
create_table :uuid_parents, id: :uuid, force: true do |t|
@@ -62,6 +54,19 @@ ActiveRecord::Schema.define do
t.string :name
end
+ create_table :articles, force: true do |t|
+ end
+
+ create_table :articles_magazines, force: true do |t|
+ t.references :article
+ t.references :magazine
+ end
+
+ create_table :articles_tags, force: true do |t|
+ t.references :article
+ t.references :tag
+ end
+
create_table :audit_logs, force: true do |t|
t.column :message, :string, null: false
t.column :developer_id, :integer, null: false
@@ -79,6 +84,8 @@ ActiveRecord::Schema.define do
create_table :author_addresses, force: true do |t|
end
+ add_foreign_key :authors, :author_addresses
+
create_table :author_favorites, force: true do |t|
t.column :author_id, :integer
t.column :favorite_author_id, :integer
@@ -103,6 +110,7 @@ ActiveRecord::Schema.define do
create_table :books, force: true do |t|
t.integer :author_id
+ t.string :format
t.column :name, :string
t.column :status, :integer, default: 0
t.column :read_status, :integer, default: 0
@@ -184,9 +192,12 @@ ActiveRecord::Schema.define do
t.text :body, null: false
end
t.string :type
- t.integer :taggings_count, default: 0
+ t.integer :tags_count, default: 0
t.integer :children_count, default: 0
t.integer :parent_id
+ t.references :author, polymorphic: true
+ t.string :resource_id
+ t.string :resource_type
end
create_table :companies, force: true do |t|
@@ -385,6 +396,9 @@ ActiveRecord::Schema.define do
t.column :custom_lock_version, :integer
end
+ create_table :magazines, force: true do |t|
+ end
+
create_table :mateys, id: false, force: true do |t|
t.column :pirate_id, :integer
t.column :target_id, :integer
@@ -522,6 +536,7 @@ ActiveRecord::Schema.define do
t.references :best_friend
t.references :best_friend_of
t.integer :insures, null: false, default: 0
+ t.timestamp :born_at
t.timestamps
end
@@ -556,7 +571,6 @@ ActiveRecord::Schema.define do
end
t.string :type
t.integer :comments_count, default: 0
- t.integer :taggings_count, default: 0
t.integer :taggings_with_delete_all_count, default: 0
t.integer :taggings_with_destroy_count, default: 0
t.integer :tags_count, default: 0
@@ -767,6 +781,8 @@ ActiveRecord::Schema.define do
t.integer :man_id
t.integer :polymorphic_man_id
t.string :polymorphic_man_type
+ t.integer :poly_man_without_inverse_id
+ t.string :poly_man_without_inverse_type
t.integer :horrible_polymorphic_man_id
t.string :horrible_polymorphic_man_type
end
@@ -841,12 +857,18 @@ ActiveRecord::Schema.define do
t.integer :fk_id, null: false
end
- create_table :fk_test_has_pk, force: true do |t|
+ create_table :fk_test_has_pk, force: true, primary_key: "pk_id" do |t|
end
- execute "ALTER TABLE fk_test_has_fk ADD CONSTRAINT fk_name FOREIGN KEY (#{quote_column_name 'fk_id'}) REFERENCES #{quote_table_name 'fk_test_has_pk'} (#{quote_column_name 'id'})"
+ add_foreign_key :fk_test_has_fk, :fk_test_has_pk, column: "fk_id", name: "fk_name", primary_key: "pk_id"
+ add_foreign_key :lessons_students, :students
+ end
- execute "ALTER TABLE lessons_students ADD CONSTRAINT student_id_fk FOREIGN KEY (#{quote_column_name 'student_id'}) REFERENCES #{quote_table_name 'students'} (#{quote_column_name 'id'})"
+ create_table :overloaded_types, force: true do |t|
+ t.float :overloaded_float, default: 500
+ t.float :unoverloaded_float
+ t.string :overloaded_string_with_limit, limit: 255
+ t.string :string_with_default, default: 'the original default'
end
end
diff --git a/activerecord/test/schema/sqlite_specific_schema.rb b/activerecord/test/schema/sqlite_specific_schema.rb
index b7aff4f47d..b5552c2755 100644
--- a/activerecord/test/schema/sqlite_specific_schema.rb
+++ b/activerecord/test/schema/sqlite_specific_schema.rb
@@ -7,7 +7,7 @@ ActiveRecord::Schema.define do
execute "DROP TABLE fk_test_has_pk" rescue nil
execute <<_SQL
CREATE TABLE 'fk_test_has_pk' (
- 'id' INTEGER NOT NULL PRIMARY KEY
+ 'pk_id' INTEGER NOT NULL PRIMARY KEY
);
_SQL
@@ -16,7 +16,7 @@ _SQL
'id' INTEGER NOT NULL PRIMARY KEY,
'fk_id' INTEGER NOT NULL,
- FOREIGN KEY ('fk_id') REFERENCES 'fk_test_has_pk'('id')
+ FOREIGN KEY ('fk_id') REFERENCES 'fk_test_has_pk'('pk_id')
);
_SQL
-end \ No newline at end of file
+end
diff --git a/activerecord/test/support/ddl_helper.rb b/activerecord/test/support/ddl_helper.rb
index 0107babaaf..43cb235e01 100644
--- a/activerecord/test/support/ddl_helper.rb
+++ b/activerecord/test/support/ddl_helper.rb
@@ -1,8 +1,8 @@
module DdlHelper
def with_example_table(connection, table_name, definition = nil)
- connection.exec_query("CREATE TABLE #{table_name}(#{definition})")
+ connection.execute("CREATE TABLE #{table_name}(#{definition})")
yield
ensure
- connection.exec_query("DROP TABLE #{table_name}")
+ connection.execute("DROP TABLE #{table_name}")
end
end
diff --git a/activerecord/test/support/schema_dumping_helper.rb b/activerecord/test/support/schema_dumping_helper.rb
new file mode 100644
index 0000000000..2ae8d299e5
--- /dev/null
+++ b/activerecord/test/support/schema_dumping_helper.rb
@@ -0,0 +1,11 @@
+module SchemaDumpingHelper
+ def dump_table_schema(table, connection = ActiveRecord::Base.connection)
+ old_ignore_tables = ActiveRecord::SchemaDumper.ignore_tables
+ ActiveRecord::SchemaDumper.ignore_tables = connection.tables - [table]
+ stream = StringIO.new
+ ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream)
+ stream.string
+ ensure
+ ActiveRecord::SchemaDumper.ignore_tables = old_ignore_tables
+ end
+end