aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord
diff options
context:
space:
mode:
Diffstat (limited to 'activerecord')
-rw-r--r--activerecord/CHANGELOG.md229
-rw-r--r--activerecord/RUNNING_UNIT_TESTS.rdoc6
-rw-r--r--activerecord/lib/active_record/aggregations.rb3
-rw-r--r--activerecord/lib/active_record/associations.rb6
-rw-r--r--activerecord/lib/active_record/associations/association_scope.rb87
-rw-r--r--activerecord/lib/active_record/associations/belongs_to_association.rb3
-rw-r--r--activerecord/lib/active_record/associations/builder/singular_association.rb2
-rw-r--r--activerecord/lib/active_record/associations/collection_association.rb9
-rw-r--r--activerecord/lib/active_record/associations/has_many_association.rb26
-rw-r--r--activerecord/lib/active_record/associations/has_many_through_association.rb6
-rw-r--r--activerecord/lib/active_record/associations/join_dependency.rb7
-rw-r--r--activerecord/lib/active_record/associations/join_dependency/join_association.rb8
-rw-r--r--activerecord/lib/active_record/associations/preloader.rb2
-rw-r--r--activerecord/lib/active_record/associations/preloader/association.rb19
-rw-r--r--activerecord/lib/active_record/associations/preloader/through_association.rb5
-rw-r--r--activerecord/lib/active_record/associations/through_association.rb2
-rw-r--r--activerecord/lib/active_record/attribute.rb21
-rw-r--r--activerecord/lib/active_record/attribute_assignment.rb157
-rw-r--r--activerecord/lib/active_record/attribute_decorators.rb11
-rw-r--r--activerecord/lib/active_record/attribute_methods.rb37
-rw-r--r--activerecord/lib/active_record/attribute_methods/dirty.rb8
-rw-r--r--activerecord/lib/active_record/attribute_methods/query.rb2
-rw-r--r--activerecord/lib/active_record/attribute_methods/read.rb13
-rw-r--r--activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb43
-rw-r--r--activerecord/lib/active_record/attribute_set.rb24
-rw-r--r--activerecord/lib/active_record/attribute_set/builder.rb16
-rw-r--r--activerecord/lib/active_record/attributes.rb246
-rw-r--r--activerecord/lib/active_record/autosave_association.rb16
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb21
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/quoting.rb79
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb9
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb5
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb20
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/transaction.rb33
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_adapter.rb67
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb112
-rw-r--r--activerecord/lib/active_record/connection_adapters/column.rb44
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql_adapter.rb8
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/column.rb12
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid.rb6
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb10
-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.rb2
-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.rb2
-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.rb4
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb22
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb4
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/time.rb11
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb50
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb6
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb66
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb35
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb103
-rw-r--r--activerecord/lib/active_record/connection_adapters/sql_type_metadata.rb32
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb43
-rw-r--r--activerecord/lib/active_record/core.rb30
-rw-r--r--activerecord/lib/active_record/counter_cache.rb2
-rw-r--r--activerecord/lib/active_record/enum.rb98
-rw-r--r--activerecord/lib/active_record/errors.rb16
-rw-r--r--activerecord/lib/active_record/fixtures.rb15
-rw-r--r--activerecord/lib/active_record/inheritance.rb2
-rw-r--r--activerecord/lib/active_record/locale/en.yml1
-rw-r--r--activerecord/lib/active_record/locking/optimistic.rb4
-rw-r--r--activerecord/lib/active_record/log_subscriber.rb20
-rw-r--r--activerecord/lib/active_record/model_schema.rb93
-rw-r--r--activerecord/lib/active_record/nested_attributes.rb2
-rw-r--r--activerecord/lib/active_record/null_relation.rb12
-rw-r--r--activerecord/lib/active_record/persistence.rb7
-rw-r--r--activerecord/lib/active_record/querying.rb2
-rw-r--r--activerecord/lib/active_record/railties/databases.rake8
-rw-r--r--activerecord/lib/active_record/relation.rb71
-rw-r--r--activerecord/lib/active_record/relation/batches.rb40
-rw-r--r--activerecord/lib/active_record/relation/calculations.rb20
-rw-r--r--activerecord/lib/active_record/relation/finder_methods.rb8
-rw-r--r--activerecord/lib/active_record/relation/from_clause.rb32
-rw-r--r--activerecord/lib/active_record/relation/merger.rb51
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder.rb42
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb22
-rw-r--r--activerecord/lib/active_record/relation/query_attribute.rb19
-rw-r--r--activerecord/lib/active_record/relation/query_methods.rb283
-rw-r--r--activerecord/lib/active_record/relation/spawn_methods.rb5
-rw-r--r--activerecord/lib/active_record/relation/where_clause.rb173
-rw-r--r--activerecord/lib/active_record/relation/where_clause_factory.rb34
-rw-r--r--activerecord/lib/active_record/scoping.rb4
-rw-r--r--activerecord/lib/active_record/scoping/named.rb25
-rw-r--r--activerecord/lib/active_record/secure_token.rb5
-rw-r--r--activerecord/lib/active_record/serializers/xml_serializer.rb4
-rw-r--r--activerecord/lib/active_record/statement_cache.rb18
-rw-r--r--activerecord/lib/active_record/table_metadata.rb11
-rw-r--r--activerecord/lib/active_record/transactions.rb12
-rw-r--r--activerecord/lib/active_record/type.rb5
-rw-r--r--activerecord/lib/active_record/type/date.rb11
-rw-r--r--activerecord/lib/active_record/type/date_time.rb29
-rw-r--r--activerecord/lib/active_record/type/decimal.rb2
-rw-r--r--activerecord/lib/active_record/type/decorator.rb14
-rw-r--r--activerecord/lib/active_record/type/float.rb10
-rw-r--r--activerecord/lib/active_record/type/helpers.rb4
-rw-r--r--activerecord/lib/active_record/type/helpers/accepts_multiparameter_time.rb30
-rw-r--r--activerecord/lib/active_record/type/helpers/mutable.rb18
-rw-r--r--activerecord/lib/active_record/type/helpers/numeric.rb34
-rw-r--r--activerecord/lib/active_record/type/helpers/time_value.rb40
-rw-r--r--activerecord/lib/active_record/type/integer.rb24
-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.rb13
-rw-r--r--activerecord/lib/active_record/type/string.rb4
-rw-r--r--activerecord/lib/active_record/type/time.rb18
-rw-r--r--activerecord/lib/active_record/type/time_value.rb38
-rw-r--r--activerecord/lib/active_record/type/value.rb85
-rw-r--r--activerecord/lib/active_record/type_caster/connection.rb21
-rw-r--r--activerecord/lib/active_record/validations/length.rb12
-rw-r--r--activerecord/lib/active_record/validations/presence.rb1
-rw-r--r--activerecord/lib/active_record/validations/uniqueness.rb6
-rw-r--r--activerecord/test/active_record/connection_adapters/fake_adapter.rb9
-rw-r--r--activerecord/test/cases/adapter_test.rb4
-rw-r--r--activerecord/test/cases/adapters/mysql/connection_test.rb6
-rw-r--r--activerecord/test/cases/adapters/mysql/datetime_test.rb87
-rw-r--r--activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb7
-rw-r--r--activerecord/test/cases/adapters/mysql2/connection_test.rb7
-rw-r--r--activerecord/test/cases/adapters/mysql2/datetime_test.rb87
-rw-r--r--activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb18
-rw-r--r--activerecord/test/cases/adapters/postgresql/array_test.rb18
-rw-r--r--activerecord/test/cases/adapters/postgresql/bit_string_test.rb13
-rw-r--r--activerecord/test/cases/adapters/postgresql/bytea_test.rb8
-rw-r--r--activerecord/test/cases/adapters/postgresql/citext_test.rb8
-rw-r--r--activerecord/test/cases/adapters/postgresql/composite_test.rb11
-rw-r--r--activerecord/test/cases/adapters/postgresql/connection_test.rb6
-rw-r--r--activerecord/test/cases/adapters/postgresql/domain_test.rb6
-rw-r--r--activerecord/test/cases/adapters/postgresql/enum_test.rb6
-rw-r--r--activerecord/test/cases/adapters/postgresql/full_text_test.rb8
-rw-r--r--activerecord/test/cases/adapters/postgresql/geometric_test.rb6
-rw-r--r--activerecord/test/cases/adapters/postgresql/hstore_test.rb38
-rw-r--r--activerecord/test/cases/adapters/postgresql/json_test.rb41
-rw-r--r--activerecord/test/cases/adapters/postgresql/ltree_test.rb8
-rw-r--r--activerecord/test/cases/adapters/postgresql/money_test.rb22
-rw-r--r--activerecord/test/cases/adapters/postgresql/network_test.rb22
-rw-r--r--activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb17
-rw-r--r--activerecord/test/cases/adapters/postgresql/quoting_test.rb5
-rw-r--r--activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb7
-rw-r--r--activerecord/test/cases/adapters/postgresql/schema_test.rb10
-rw-r--r--activerecord/test/cases/adapters/postgresql/timestamp_test.rb64
-rw-r--r--activerecord/test/cases/adapters/postgresql/type_lookup_test.rb4
-rw-r--r--activerecord/test/cases/adapters/postgresql/uuid_test.rb8
-rw-r--r--activerecord/test/cases/adapters/postgresql/xml_test.rb3
-rw-r--r--activerecord/test/cases/adapters/sqlite3/quoting_test.rb2
-rw-r--r--activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb9
-rw-r--r--activerecord/test/cases/adapters/sqlite3/sqlite3_create_folder_test.rb1
-rw-r--r--activerecord/test/cases/associations/association_scope_test.rb7
-rw-r--r--activerecord/test/cases/associations/belongs_to_associations_test.rb4
-rw-r--r--activerecord/test/cases/associations/eager_test.rb8
-rw-r--r--activerecord/test/cases/associations/has_many_associations_test.rb85
-rw-r--r--activerecord/test/cases/associations/has_one_associations_test.rb10
-rw-r--r--activerecord/test/cases/associations/required_test.rb28
-rw-r--r--activerecord/test/cases/associations_test.rb4
-rw-r--r--activerecord/test/cases/attribute_decorators_test.rb8
-rw-r--r--activerecord/test/cases/attribute_methods/read_test.rb10
-rw-r--r--activerecord/test/cases/attribute_methods_test.rb67
-rw-r--r--activerecord/test/cases/attribute_set_test.rb21
-rw-r--r--activerecord/test/cases/attribute_test.rb19
-rw-r--r--activerecord/test/cases/attributes_test.rb87
-rw-r--r--activerecord/test/cases/autosave_association_test.rb5
-rw-r--r--activerecord/test/cases/base_test.rb5
-rw-r--r--activerecord/test/cases/batches_test.rb9
-rw-r--r--activerecord/test/cases/binary_test.rb1
-rw-r--r--activerecord/test/cases/bind_parameter_test.rb24
-rw-r--r--activerecord/test/cases/calculations_test.rb11
-rw-r--r--activerecord/test/cases/callbacks_test.rb21
-rw-r--r--activerecord/test/cases/column_definition_test.rb79
-rw-r--r--activerecord/test/cases/connection_adapters/type_lookup_test.rb5
-rw-r--r--activerecord/test/cases/core_test.rb11
-rw-r--r--activerecord/test/cases/counter_cache_test.rb18
-rw-r--r--activerecord/test/cases/date_time_precision_test.rb102
-rw-r--r--activerecord/test/cases/date_time_test.rb2
-rw-r--r--activerecord/test/cases/defaults_test.rb2
-rw-r--r--activerecord/test/cases/dirty_test.rb10
-rw-r--r--activerecord/test/cases/enum_test.rb60
-rw-r--r--activerecord/test/cases/explain_test.rb2
-rw-r--r--activerecord/test/cases/fixtures_test.rb21
-rw-r--r--activerecord/test/cases/helper.rb5
-rw-r--r--activerecord/test/cases/inheritance_test.rb4
-rw-r--r--activerecord/test/cases/integration_test.rb1
-rw-r--r--activerecord/test/cases/log_subscriber_test.rb8
-rw-r--r--activerecord/test/cases/migration/change_schema_test.rb17
-rw-r--r--activerecord/test/cases/migration/column_positioning_test.rb2
-rw-r--r--activerecord/test/cases/migration/columns_test.rb8
-rw-r--r--activerecord/test/cases/migration/command_recorder_test.rb5
-rw-r--r--activerecord/test/cases/migration/foreign_key_test.rb20
-rw-r--r--activerecord/test/cases/migration/references_foreign_key_test.rb14
-rw-r--r--activerecord/test/cases/migration/rename_table_test.rb4
-rw-r--r--activerecord/test/cases/migration/table_and_index_test.rb4
-rw-r--r--activerecord/test/cases/migration_test.rb40
-rw-r--r--activerecord/test/cases/multiparameter_attributes_test.rb12
-rw-r--r--activerecord/test/cases/multiple_db_test.rb14
-rw-r--r--activerecord/test/cases/nested_attributes_test.rb19
-rw-r--r--activerecord/test/cases/persistence_test.rb10
-rw-r--r--activerecord/test/cases/primary_keys_test.rb2
-rw-r--r--activerecord/test/cases/quoting_test.rb10
-rw-r--r--activerecord/test/cases/reflection_test.rb13
-rw-r--r--activerecord/test/cases/relation/merging_test.rb15
-rw-r--r--activerecord/test/cases/relation/mutation_test.rb10
-rw-r--r--activerecord/test/cases/relation/or_test.rb84
-rw-r--r--activerecord/test/cases/relation/predicate_builder_test.rb2
-rw-r--r--activerecord/test/cases/relation/where_chain_test.rb122
-rw-r--r--activerecord/test/cases/relation/where_clause_test.rb182
-rw-r--r--activerecord/test/cases/relation/where_test.rb46
-rw-r--r--activerecord/test/cases/relation_test.rb34
-rw-r--r--activerecord/test/cases/relations_test.rb80
-rw-r--r--activerecord/test/cases/schema_dumper_test.rb40
-rw-r--r--activerecord/test/cases/scoping/default_scoping_test.rb12
-rw-r--r--activerecord/test/cases/scoping/named_scoping_test.rb4
-rw-r--r--activerecord/test/cases/scoping/relation_scoping_test.rb8
-rw-r--r--activerecord/test/cases/secure_token_test.rb7
-rw-r--r--activerecord/test/cases/test_case.rb20
-rw-r--r--activerecord/test/cases/time_precision_test.rb65
-rw-r--r--activerecord/test/cases/transaction_callbacks_test.rb25
-rw-r--r--activerecord/test/cases/type/integer_test.rb39
-rw-r--r--activerecord/test/cases/type/unsigned_integer_test.rb4
-rw-r--r--activerecord/test/cases/types_test.rb21
-rw-r--r--activerecord/test/cases/validations/association_validation_test.rb3
-rw-r--r--activerecord/test/cases/validations/length_validation_test.rb20
-rw-r--r--activerecord/test/cases/validations/presence_validation_test.rb17
-rw-r--r--activerecord/test/cases/validations/uniqueness_validation_test.rb18
-rw-r--r--activerecord/test/cases/validations_test.rb3
-rw-r--r--activerecord/test/cases/xml_serialization_test.rb2
-rw-r--r--activerecord/test/fixtures/dead_parrots.yml5
-rw-r--r--activerecord/test/fixtures/live_parrots.yml4
-rw-r--r--activerecord/test/models/admin/randomly_named_c1.rb8
-rw-r--r--activerecord/test/models/car.rb2
-rw-r--r--activerecord/test/models/post.rb3
-rw-r--r--activerecord/test/models/randomly_named_c1.rb2
-rw-r--r--activerecord/test/models/ship.rb1
-rw-r--r--activerecord/test/models/ship_part.rb3
-rw-r--r--activerecord/test/models/treasure.rb1
-rw-r--r--activerecord/test/schema/schema.rb14
241 files changed, 3750 insertions, 2256 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md
index 8875b7ae25..e1cace7d88 100644
--- a/activerecord/CHANGELOG.md
+++ b/activerecord/CHANGELOG.md
@@ -1,3 +1,232 @@
+* Use SQL COUNT and LIMIT 1 queries for `none?` and `one?` methods if no block or limit is given,
+ instead of loading the entire collection to memory.
+ This applies to relations (e.g. `User.all`) as well as associations (e.g. `account.users`)
+
+ # Before:
+
+ users.none?
+ # SELECT "users".* FROM "users"
+
+ users.one?
+ # SELECT "users".* FROM "users"
+
+ # After:
+
+ users.none?
+ # SELECT 1 AS one FROM "users" LIMIT 1
+
+ users.one?
+ # SELECT COUNT(*) FROM "users"
+
+ *Eugene Gilburg*
+
+* Allow `:precision` option for time type columns.
+
+ *Ryuta Kamizono*
+
+* Have `enum` perform type casting consistently with the rest of Active
+ Record, such as `where`.
+
+ *Sean Griffin*
+
+* `scoping` no longer pollutes the current scope of sibling classes when using
+ STI. e.x.
+
+ StiOne.none.scoping do
+ StiTwo.all
+ end
+
+ Fixes #18806.
+
+ *Sean Griffin*
+
+* `remove_reference` with `foreign_key: true` removes the foreign key before
+ removing the column. This fixes a bug where it was not possible to remove
+ the column on MySQL.
+
+ Fixes #18664.
+
+ *Yves Senn*
+
+* `find_in_batches` now accepts an `:end_at` parameter that complements the `:start`
+ parameter to specify where to stop batch processing.
+
+ *Vipul A M*
+
+* Fix rounding problem for PostgreSQL timestamp column.
+
+ If timestamp column have the precision, it need to format according to
+ the precision of timestamp column.
+
+ *Ryuta Kamizono*
+
+* Respect the database default charset for `schema_migrations` table.
+
+ The charset of `version` column in `schema_migrations` table is depend
+ on the database default charset and collation rather than the encoding
+ of the connection.
+
+ *Ryuta Kamizono*
+
+* Raise `ArgumentError` when passing `nil` or `false` to `Relation#merge`.
+
+ These are not valid values to merge in a relation so it should warn the users
+ early.
+
+ *Rafael Mendonça França*
+
+* Use `SCHEMA` instead of `DB_STRUCTURE` for specifying structure file.
+
+ This makes the db:structure tasks consistent with test:load_structure.
+
+ *Dieter Komendera*
+
+* Respect custom primary keys for associations when calling `Relation#where`
+
+ Fixes #18813.
+
+ *Sean Griffin*
+
+* Fixed several edge cases which could result in a counter cache updating
+ twice or not updating at all for `has_many` and `has_many :through`.
+
+ Fixes #10865.
+
+ *Sean Griffin*
+
+* Foreign keys added by migrations were given random, generated names. This
+ meant a different `structure.sql` would be generated every time a developer
+ ran migrations on their machine.
+
+ The generated part of foreign key names is now a hash of the table name and
+ column name, which is consistent every time you run the migration.
+
+ *Chris Sinjakli*
+
+* Validation errors would be raised for parent records when an association
+ was saved when the parent had `validate: false`. It should not be the
+ responsibility of the model to validate an associated object unless the
+ object was created or modified by the parent.
+
+ This fixes the issue by skipping validations if the parent record is
+ persisted, not changed, and not marked for destruction.
+
+ Fixes #17621.
+
+ *Eileen M. Uchitelle, Aaron Patterson*
+
+* Fix n+1 query problem when eager loading nil associations (fixes #18312)
+
+ *Sammy Larbi*
+
+* Change the default error message from `can't be blank` to `must exist` for
+ the presence validator of the `:required` option on `belongs_to`/`has_one` associations.
+
+ *Henrik Nygren*
+
+* Fixed ActiveRecord::Relation#group method when argument is SQL reserved key word:
+
+ Example:
+
+ SplitTest.group(:key).count
+ Property.group(:value).count
+
+ *Bogdan Gusiev*
+
+* Added the `#or` method on ActiveRecord::Relation, allowing use of the OR
+ operator to combine WHERE or HAVING clauses.
+
+ Example:
+
+ Post.where('id = 1').or(Post.where('id = 2'))
+ # => SELECT * FROM posts WHERE (id = 1) OR (id = 2)
+
+ *Sean Griffin*, *Matthew Draper*, *Gael Muller*, *Olivier El Mekki*
+
+* Don't define autosave association callbacks twice from
+ `accepts_nested_attributes_for`.
+
+ Fixes #18704.
+
+ *Sean Griffin*
+
+* Integer types will no longer raise a `RangeError` when assigning an
+ attribute, but will instead raise when going to the database.
+
+ Fixes several vague issues which were never reported directly. See the
+ commit message from the commit which added this line for some examples.
+
+ *Sean Griffin*
+
+* Values which would error while being sent to the database (such as an
+ ASCII-8BIT string with invalid UTF-8 bytes on SQLite3), no longer error on
+ assignment. They will still error when sent to the database, but you are
+ given the ability to re-assign it to a valid value.
+
+ Fixes #18580.
+
+ *Sean Griffin*
+
+* Don't remove join dependencies in `Relation#exists?`
+
+ Fixes #18632.
+
+ *Sean Griffin*
+
+* Invalid values assigned to a JSON column are assumed to be `nil`.
+
+ Fixes #18629.
+
+ *Sean Griffin*
+
+* Add `ActiveRecord::Base#accessed_fields`, which can be used to quickly
+ discover which fields were read from a model when you are looking to only
+ select the data you need from the database.
+
+ *Sean Griffin*
+
+* Introduce the `:if_exists` option for `drop_table`.
+
+ Example:
+
+ drop_table(:posts, if_exists: true)
+
+ That would execute:
+
+ DROP TABLE IF EXISTS posts
+
+ If the table doesn't exist, `if_exists: false` (the default) raises an
+ exception whereas `if_exists: true` does nothing.
+
+ *Cody Cutrer*, *Stefan Kanev*, *Ryuta Kamizono*
+
+* Don't run SQL if attribute value is not changed for update_attribute method.
+
+ *Prathamesh Sonpatki*
+
+* `time` columns can now get affected by `time_zone_aware_attributes`. If you have
+ set `config.time_zone` to a value other than `'UTC'`, they will be treated
+ as in that time zone by default in Rails 5.1. If this is not the desired
+ behavior, you can set
+
+ ActiveRecord::Base.time_zone_aware_types = [:datetime]
+
+ A deprecation warning will be emitted if you have a `:time` column, and have
+ not explicitly opted out.
+
+ Fixes #3145.
+
+ *Sean Griffin*
+
+* Tests now run after_commit callbacks. You no longer have to declare
+ `uses_transaction ‘test name’` to test the results of an after_commit.
+
+ after_commit callbacks run after committing a transaction whose parent
+ is not `joinable?`: un-nested transactions, transactions within test cases,
+ and transactions in `console --sandbox`.
+
+ *arthurnn*, *Ravil Bayramgalin*, *Matthew Draper*
+
* `nil` as a value for a binary column in a query no longer logs as
"<NULL binary data>", and instead logs as just "nil".
diff --git a/activerecord/RUNNING_UNIT_TESTS.rdoc b/activerecord/RUNNING_UNIT_TESTS.rdoc
index 7e3460365b..bae40604b1 100644
--- a/activerecord/RUNNING_UNIT_TESTS.rdoc
+++ b/activerecord/RUNNING_UNIT_TESTS.rdoc
@@ -16,7 +16,7 @@ To run a set of tests:
You can also run tests that depend upon a specific database backend. For
example:
- $ bundle exec rake test_sqlite3
+ $ bundle exec rake test:sqlite3
Simply executing <tt>bundle exec rake test</tt> is equivalent to the following:
@@ -24,6 +24,10 @@ Simply executing <tt>bundle exec rake test</tt> is equivalent to the following:
$ bundle exec rake test:mysql2
$ bundle exec rake test:postgresql
$ bundle exec rake test:sqlite3
+
+Using the SQLite3 adapter with an in-memory database is the fastest way
+to run the tests:
+
$ bundle exec rake test:sqlite3_mem
There should be tests available for each database backend listed in the {Config
diff --git a/activerecord/lib/active_record/aggregations.rb b/activerecord/lib/active_record/aggregations.rb
index 1040e6e3bb..39077aea7e 100644
--- a/activerecord/lib/active_record/aggregations.rb
+++ b/activerecord/lib/active_record/aggregations.rb
@@ -245,7 +245,8 @@ module ActiveRecord
define_method("#{name}=") do |part|
klass = class_name.constantize
if part.is_a?(Hash)
- part = klass.new(*part.values)
+ raise ArgumentError unless part.size == part.keys.max
+ part = klass.new(*part.sort.map(&:last))
end
unless part.is_a?(klass) || converter.nil? || part.nil?
diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb
index 9aa5fb5e26..260ef71e64 100644
--- a/activerecord/lib/active_record/associations.rb
+++ b/activerecord/lib/active_record/associations.rb
@@ -1000,6 +1000,8 @@ module ActiveRecord
# callbacks declared either before or after the <tt>:dependent</tt> option
# can affect what it does.
#
+ # Note that <tt>:dependent</tt> option is ignored for +has_one+ <tt>:through</tt> associations.
+ #
# === Delete or destroy?
#
# +has_many+ and +has_and_belongs_to_many+ associations have the methods <tt>destroy</tt>,
@@ -1012,7 +1014,7 @@ module ActiveRecord
# record(s) being removed so that callbacks are run. However <tt>delete</tt> and <tt>delete_all</tt> will either
# do the deletion according to the strategy specified by the <tt>:dependent</tt> option, or
# if no <tt>:dependent</tt> option is given, then it will follow the default strategy.
- # The default strategy is <tt>:nullify</tt> (set the foreign keys to <tt>nil</tt>), except for
+ # The default strategy is to do nothing (leave the foreign keys with the parent ids set), except for
# +has_many+ <tt>:through</tt>, where the default strategy is <tt>delete_all</tt> (delete
# the join records, without running their callbacks).
#
@@ -1330,6 +1332,8 @@ module ActiveRecord
# * <tt>:nullify</tt> causes the foreign key to be set to +NULL+. Callbacks are not executed.
# * <tt>:restrict_with_exception</tt> causes an exception to be raised if there is an associated record
# * <tt>:restrict_with_error</tt> causes an error to be added to the owner if there is an associated object
+ #
+ # Note that <tt>:dependent</tt> option is ignored when using <tt>:through</tt> option.
# [:foreign_key]
# Specify the foreign key used for the association. By default this is guessed to be the name
# of this class in lower-case and "_id" suffixed. So a Person class that makes a +has_one+ association
diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb
index d06b7b3508..2416167834 100644
--- a/activerecord/lib/active_record/associations/association_scope.rb
+++ b/activerecord/lib/active_record/associations/association_scope.rb
@@ -2,42 +2,30 @@ module ActiveRecord
module Associations
class AssociationScope #:nodoc:
def self.scope(association, connection)
- INSTANCE.scope association, connection
- end
-
- class BindSubstitution
- def initialize(block)
- @block = block
- end
-
- def bind_value(scope, column, value, connection)
- substitute = connection.substitute_at(column)
- scope.bind_values += [[column, @block.call(value)]]
- substitute
- end
+ INSTANCE.scope(association, connection)
end
def self.create(&block)
- block = block ? block : lambda { |val| val }
- new BindSubstitution.new(block)
+ block ||= lambda { |val| val }
+ new(block)
end
- def initialize(bind_substitution)
- @bind_substitution = bind_substitution
+ def initialize(value_transformation)
+ @value_transformation = value_transformation
end
INSTANCE = create
def scope(association, connection)
- klass = association.klass
- reflection = association.reflection
- scope = klass.unscoped
- owner = association.owner
+ klass = association.klass
+ reflection = association.reflection
+ scope = klass.unscoped
+ owner = association.owner
alias_tracker = AliasTracker.create connection, association.klass.table_name, klass.type_caster
chain_head, chain_tail = get_chain(reflection, association, alias_tracker)
scope.extending! Array(reflection.options[:extend])
- add_constraints(scope, owner, klass, reflection, connection, chain_head, chain_tail)
+ add_constraints(scope, owner, klass, reflection, chain_head, chain_tail)
end
def join_type
@@ -61,43 +49,36 @@ module ActiveRecord
binds
end
+ protected
+
+ attr_reader :value_transformation
+
private
def join(table, constraint)
table.create_join(table, table.create_on(constraint), join_type)
end
- def column_for(table_name, column_name, connection)
- columns = connection.schema_cache.columns_hash(table_name)
- columns[column_name]
- end
-
- def bind_value(scope, column, value, connection)
- @bind_substitution.bind_value scope, column, value, connection
- end
-
- def bind(scope, table_name, column_name, value, connection)
- column = column_for table_name, column_name, connection
- bind_value scope, column, value, connection
- end
-
- def last_chain_scope(scope, table, reflection, owner, connection, association_klass)
+ def last_chain_scope(scope, table, reflection, owner, association_klass)
join_keys = reflection.join_keys(association_klass)
key = join_keys.key
foreign_key = join_keys.foreign_key
- bind_val = bind scope, table.table_name, key.to_s, owner[foreign_key], connection
- scope = scope.where(table[key].eq(bind_val))
+ value = transform_value(owner[foreign_key])
+ scope = scope.where(table.name => { key => value })
if reflection.type
- value = owner.class.base_class.name
- bind_val = bind scope, table.table_name, reflection.type, value, connection
- scope = scope.where(table[reflection.type].eq(bind_val))
- else
- scope
+ polymorphic_type = transform_value(owner.class.base_class.name)
+ scope = scope.where(table.name => { reflection.type => polymorphic_type })
end
+
+ scope
+ end
+
+ def transform_value(value)
+ value_transformation.call(value)
end
- def next_chain_scope(scope, table, reflection, connection, association_klass, foreign_table, next_reflection)
+ def next_chain_scope(scope, table, reflection, association_klass, foreign_table, next_reflection)
join_keys = reflection.join_keys(association_klass)
key = join_keys.key
foreign_key = join_keys.foreign_key
@@ -105,9 +86,8 @@ module ActiveRecord
constraint = table[key].eq(foreign_table[foreign_key])
if reflection.type
- value = next_reflection.klass.base_class.name
- bind_val = bind scope, table.table_name, reflection.type, value, connection
- scope = scope.where(table[reflection.type].eq(bind_val))
+ value = transform_value(next_reflection.klass.base_class.name)
+ scope = scope.where(table.name => { reflection.type => value })
end
scope = scope.joins(join(foreign_table, constraint))
@@ -138,10 +118,10 @@ module ActiveRecord
[runtime_reflection, previous_reflection]
end
- def add_constraints(scope, owner, association_klass, refl, connection, chain_head, chain_tail)
+ def add_constraints(scope, owner, association_klass, refl, chain_head, chain_tail)
owner_reflection = chain_tail
table = owner_reflection.alias_name
- scope = last_chain_scope(scope, table, owner_reflection, owner, connection, association_klass)
+ scope = last_chain_scope(scope, table, owner_reflection, owner, association_klass)
reflection = chain_head
loop do
@@ -151,7 +131,7 @@ module ActiveRecord
unless reflection == chain_tail
next_reflection = reflection.next
foreign_table = next_reflection.alias_name
- scope = next_chain_scope(scope, table, reflection, connection, association_klass, foreign_table, next_reflection)
+ scope = next_chain_scope(scope, table, reflection, association_klass, foreign_table, next_reflection)
end
# Exclude the scope of the association itself, because that
@@ -160,15 +140,14 @@ module ActiveRecord
item = eval_scope(reflection.klass, scope_chain_item, owner)
if scope_chain_item == refl.scope
- scope.merge! item.except(:where, :includes, :bind)
+ scope.merge! item.except(:where, :includes)
end
reflection.all_includes do
scope.includes! item.includes_values
end
- scope.where_values += item.where_values
- scope.bind_values += item.bind_values
+ scope.where_clause += item.where_clause
scope.order_values |= item.order_values
end
diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb
index c63b42e2a0..265a65c4c1 100644
--- a/activerecord/lib/active_record/associations/belongs_to_association.rb
+++ b/activerecord/lib/active_record/associations/belongs_to_association.rb
@@ -68,6 +68,9 @@ module ActiveRecord
def increment_counter(counter_cache_name)
if foreign_key_present?
klass.increment_counter(counter_cache_name, target_id)
+ if target && !stale_target?
+ target.increment(counter_cache_name)
+ end
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 1369212837..f6274c027e 100644
--- a/activerecord/lib/active_record/associations/builder/singular_association.rb
+++ b/activerecord/lib/active_record/associations/builder/singular_association.rb
@@ -31,7 +31,7 @@ module ActiveRecord::Associations::Builder
def self.define_validations(model, reflection)
super
if reflection.options[:required]
- model.validates_presence_of reflection.name
+ model.validates_presence_of reflection.name, message: :required
end
end
end
diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb
index f2c96e9a2a..33e4516bd1 100644
--- a/activerecord/lib/active_record/associations/collection_association.rb
+++ b/activerecord/lib/active_record/associations/collection_association.rb
@@ -151,6 +151,7 @@ module ActiveRecord
# be chained. Since << flattens its argument list and inserts each record,
# +push+ and +concat+ behave identically.
def concat(*records)
+ records = records.flatten
if owner.new_record?
load_target
concat_records(records)
@@ -318,7 +319,8 @@ module ActiveRecord
end
# Returns true if the collections is not empty.
- # Equivalent to +!collection.empty?+.
+ # If block given, loads all records and checks for one or more matches.
+ # Otherwise, equivalent to +!collection.empty?+.
def any?
if block_given?
load_target.any? { |*block_args| yield(*block_args) }
@@ -328,7 +330,8 @@ module ActiveRecord
end
# Returns true if the collection has more than 1 record.
- # Equivalent to +collection.size > 1+.
+ # If block given, loads all records and checks for two or more matches.
+ # Otherwise, equivalent to +collection.size > 1+.
def many?
if block_given?
load_target.many? { |*block_args| yield(*block_args) }
@@ -549,7 +552,7 @@ module ActiveRecord
def concat_records(records, should_raise = false)
result = true
- records.flatten.each do |record|
+ records.each do |record|
raise_on_type_mismatch!(record)
add_to_target(record) do |rec|
result &&= insert_record(rec, true, should_raise) unless owner.new_record?
diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb
index 2a782c06d0..ca27c9fdde 100644
--- a/activerecord/lib/active_record/associations/has_many_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_association.rb
@@ -85,7 +85,11 @@ module ActiveRecord
end
def cached_counter_attribute_name(reflection = reflection())
- options[:counter_cache] || "#{reflection.name}_count"
+ if reflection.options[:counter_cache]
+ reflection.options[:counter_cache].to_s
+ else
+ "#{reflection.name}_count"
+ end
end
def update_counter(difference, reflection = reflection())
@@ -101,7 +105,7 @@ module ActiveRecord
end
def update_counter_in_memory(difference, reflection = reflection())
- if has_cached_counter?(reflection)
+ if counter_must_be_updated_by_has_many?(reflection)
counter = cached_counter_attribute_name(reflection)
owner[counter] += difference
owner.send(:clear_attribute_changes, counter) # eww
@@ -118,18 +122,28 @@ module ActiveRecord
# it will be decremented twice.
#
# Hence this method.
- def inverse_updates_counter_cache?(reflection = reflection())
+ def inverse_which_updates_counter_cache(reflection = reflection())
counter_name = cached_counter_attribute_name(reflection)
- inverse_updates_counter_named?(counter_name, reflection)
+ inverse_which_updates_counter_named(counter_name, reflection)
end
+ alias inverse_updates_counter_cache? inverse_which_updates_counter_cache
- def inverse_updates_counter_named?(counter_name, reflection = reflection())
- reflection.klass._reflections.values.any? { |inverse_reflection|
+ def inverse_which_updates_counter_named(counter_name, reflection)
+ reflection.klass._reflections.values.find { |inverse_reflection|
inverse_reflection.belongs_to? &&
inverse_reflection.counter_cache_column == counter_name
}
end
+ def inverse_updates_counter_in_memory?(reflection)
+ inverse = inverse_which_updates_counter_cache(reflection)
+ inverse && inverse == reflection.inverse_of
+ end
+
+ def counter_must_be_updated_by_has_many?(reflection)
+ !inverse_updates_counter_in_memory?(reflection) && has_cached_counter?(reflection)
+ end
+
def delete_count(method, scope)
if method == :delete_all
scope.delete_all
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 f1e784d771..4897ec44e9 100644
--- a/activerecord/lib/active_record/associations/has_many_through_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_through_association.rb
@@ -143,7 +143,7 @@ module ActiveRecord
stmt.from scope.klass.arel_table
stmt.wheres = arel.constraints
- count = scope.klass.connection.delete(stmt, 'SQL', scope.bind_values)
+ count = scope.klass.connection.delete(stmt, 'SQL', scope.bound_attributes)
end
when :nullify
count = scope.update_all(source_reflection.foreign_key => nil)
@@ -160,9 +160,9 @@ module ActiveRecord
if through_reflection.collection? && update_through_counter?(method)
update_counter(-count, through_reflection)
+ else
+ update_counter(-count)
end
-
- update_counter(-count)
end
def through_records_for(record)
diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb
index 4b75370171..fcf06323e6 100644
--- a/activerecord/lib/active_record/associations/join_dependency.rb
+++ b/activerecord/lib/active_record/associations/join_dependency.rb
@@ -232,6 +232,7 @@ module ActiveRecord
end
def construct(ar_parent, parent, row, rs, seen, model_cache, aliases)
+ return if ar_parent.nil?
primary_id = ar_parent.id
parent.children.each do |node|
@@ -248,7 +249,11 @@ module ActiveRecord
key = aliases.column_alias(node, node.primary_key)
id = row[key]
- next if id.nil?
+ if id.nil?
+ nil_association = ar_parent.association(node.reflection.name)
+ nil_association.loaded!
+ next
+ end
model = seen[parent.base_klass][primary_id][node.base_klass][id]
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 c1ef86a95b..a6ad09a38a 100644
--- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb
+++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb
@@ -25,7 +25,7 @@ module ActiveRecord
def join_constraints(foreign_table, foreign_klass, node, join_type, tables, scope_chain, chain)
joins = []
- bind_values = []
+ binds = []
tables = tables.reverse
scope_chain_index = 0
@@ -66,7 +66,7 @@ module ActiveRecord
end
if rel && !rel.arel.constraints.empty?
- bind_values.concat rel.bind_values
+ binds += rel.bound_attributes
constraint = constraint.and rel.arel.constraints
end
@@ -75,7 +75,7 @@ module ActiveRecord
column = klass.columns_hash[reflection.type.to_s]
substitute = klass.connection.substitute_at(column)
- bind_values.push [column, value]
+ binds << Relation::QueryAttribute.new(column.name, value, klass.type_for_attribute(column.name))
constraint = constraint.and table[reflection.type].eq substitute
end
@@ -85,7 +85,7 @@ module ActiveRecord
foreign_table, foreign_klass = table, klass
end
- JoinInformation.new joins, bind_values
+ JoinInformation.new joins, binds
end
# Builds equality condition.
diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb
index 4358f3b581..97f4bd3811 100644
--- a/activerecord/lib/active_record/associations/preloader.rb
+++ b/activerecord/lib/active_record/associations/preloader.rb
@@ -89,7 +89,7 @@ module ActiveRecord
# { author: :avatar }
# [ :books, { author: :avatar } ]
- NULL_RELATION = Struct.new(:values, :bind_values).new({}, [])
+ NULL_RELATION = Struct.new(:values, :where_clause, :joins_values).new({}, Relation::WhereClause.empty, [])
def preload(records, associations, preload_scope = nil)
records = Array.wrap(records).compact.uniq
diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb
index afcaa5d55a..1dc8bff193 100644
--- a/activerecord/lib/active_record/associations/preloader/association.rb
+++ b/activerecord/lib/active_record/associations/preloader/association.rb
@@ -104,11 +104,11 @@ module ActiveRecord
end
def association_key_type
- @klass.column_for_attribute(association_key_name).type
+ @klass.type_for_attribute(association_key_name.to_s).type
end
def owner_key_type
- @model.column_for_attribute(owner_key_name).type
+ @model.type_for_attribute(owner_key_name.to_s).type
end
def load_slices(slices)
@@ -131,18 +131,19 @@ module ActiveRecord
def build_scope
scope = klass.unscoped
- values = reflection_scope.values
- reflection_binds = reflection_scope.bind_values
+ values = reflection_scope.values
preload_values = preload_scope.values
- preload_binds = preload_scope.bind_values
- scope.where_values = Array(values[:where]) + Array(preload_values[:where])
+ scope.where_clause = reflection_scope.where_clause + preload_scope.where_clause
scope.references_values = Array(values[:references]) + Array(preload_values[:references])
- scope.bind_values = (reflection_binds + preload_binds)
- scope._select! preload_values[:select] || values[:select] || table[Arel.star]
+ scope._select! preload_values[:select] || values[:select] || table[Arel.star]
scope.includes! preload_values[:includes] || values[:includes]
- scope.joins! preload_values[:joins] || values[:joins]
+ if preload_scope.joins_values.any?
+ scope.joins!(preload_scope.joins_values)
+ else
+ scope.joins!(reflection_scope.joins_values)
+ end
scope.order! preload_values[:order] || values[:order]
if preload_values[:readonly] || values[:readonly]
diff --git a/activerecord/lib/active_record/associations/preloader/through_association.rb b/activerecord/lib/active_record/associations/preloader/through_association.rb
index 12bf3ef138..56aa23b173 100644
--- a/activerecord/lib/active_record/associations/preloader/through_association.rb
+++ b/activerecord/lib/active_record/associations/preloader/through_association.rb
@@ -78,10 +78,9 @@ module ActiveRecord
if options[:source_type]
scope.where! reflection.foreign_type => options[:source_type]
else
- unless reflection_scope.where_values.empty?
+ unless reflection_scope.where_clause.empty?
scope.includes_values = Array(reflection_scope.values[:includes] || options[:source])
- scope.where_values = reflection_scope.values[:where]
- scope.bind_values = reflection_scope.bind_values
+ scope.where_clause = reflection_scope.where_clause
end
scope.references! reflection_scope.values[:references]
diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb
index 09828dbd9b..3ce9cffdbc 100644
--- a/activerecord/lib/active_record/associations/through_association.rb
+++ b/activerecord/lib/active_record/associations/through_association.rb
@@ -18,7 +18,7 @@ module ActiveRecord
reflection_scope = reflection.scope
if reflection_scope && reflection_scope.arity.zero?
- relation.merge!(reflection_scope)
+ relation = relation.merge(reflection_scope)
end
scope.merge!(
diff --git a/activerecord/lib/active_record/attribute.rb b/activerecord/lib/active_record/attribute.rb
index 51e4fae62e..91886f1324 100644
--- a/activerecord/lib/active_record/attribute.rb
+++ b/activerecord/lib/active_record/attribute.rb
@@ -51,7 +51,7 @@ module ActiveRecord
end
def changed_in_place_from?(old_value)
- type.changed_in_place?(old_value, value)
+ has_been_read? && type.changed_in_place?(old_value, value)
end
def with_value_from_user(value)
@@ -66,6 +66,10 @@ module ActiveRecord
self.class.with_cast_value(name, value, type)
end
+ def with_type(type)
+ self.class.new(name, value_before_type_cast, type)
+ end
+
def type_cast(*)
raise NotImplementedError
end
@@ -78,12 +82,21 @@ module ActiveRecord
false
end
+ def has_been_read?
+ defined?(@value)
+ end
+
def ==(other)
self.class == other.class &&
name == other.name &&
value_before_type_cast == other.value_before_type_cast &&
type == other.type
end
+ alias eql? ==
+
+ def hash
+ [self.class, name, value_before_type_cast, type].hash
+ end
protected
@@ -128,6 +141,10 @@ module ActiveRecord
nil
end
+ def with_type(type)
+ self.class.with_cast_value(name, nil, type)
+ end
+
def with_value_from_database(value)
raise ActiveModel::MissingAttributeError, "can't write unknown attribute `#{name}`"
end
@@ -152,6 +169,6 @@ module ActiveRecord
false
end
end
- private_constant :FromDatabase, :FromUser, :Null, :Uninitialized
+ private_constant :FromDatabase, :FromUser, :Null, :Uninitialized, :WithCastValue
end
end
diff --git a/activerecord/lib/active_record/attribute_assignment.rb b/activerecord/lib/active_record/attribute_assignment.rb
index bf64830417..483d4200bd 100644
--- a/activerecord/lib/active_record/attribute_assignment.rb
+++ b/activerecord/lib/active_record/attribute_assignment.rb
@@ -3,63 +3,32 @@ require 'active_model/forbidden_attributes_protection'
module ActiveRecord
module AttributeAssignment
extend ActiveSupport::Concern
- include ActiveModel::ForbiddenAttributesProtection
+ include ActiveModel::AttributeAssignment
- # Allows you to set all the attributes by passing in a hash of attributes with
- # keys matching the attribute names (which again matches the column names).
- #
- # If the passed hash responds to <tt>permitted?</tt> method and the return value
- # of this method is +false+ an <tt>ActiveModel::ForbiddenAttributesError</tt>
- # exception is raised.
- #
- # cat = Cat.new(name: "Gorby", status: "yawning")
- # cat.attributes # => { "name" => "Gorby", "status" => "yawning", "created_at" => nil, "updated_at" => nil}
- # cat.assign_attributes(status: "sleeping")
- # cat.attributes # => { "name" => "Gorby", "status" => "sleeping", "created_at" => nil, "updated_at" => nil }
- #
- # New attributes will be persisted in the database when the object is saved.
- #
- # Aliased to <tt>attributes=</tt>.
- def assign_attributes(new_attributes)
- if !new_attributes.respond_to?(:stringify_keys)
- raise ArgumentError, "When assigning attributes, you must pass a hash as an argument."
- end
- return if new_attributes.blank?
+ # Alias for `assign_attributes`. See +ActiveModel::AttributeAssignment+.
+ def attributes=(attributes)
+ assign_attributes(attributes)
+ end
- attributes = new_attributes.stringify_keys
- multi_parameter_attributes = []
- nested_parameter_attributes = []
+ private
- attributes = sanitize_for_mass_assignment(attributes)
+ def _assign_attributes(attributes) # :nodoc:
+ multi_parameter_attributes = {}
+ nested_parameter_attributes = {}
attributes.each do |k, v|
if k.include?("(")
- multi_parameter_attributes << [ k, v ]
+ multi_parameter_attributes[k] = attributes.delete(k)
elsif v.is_a?(Hash)
- nested_parameter_attributes << [ k, v ]
- else
- _assign_attribute(k, v)
+ nested_parameter_attributes[k] = attributes.delete(k)
end
end
+ super(attributes)
assign_nested_parameter_attributes(nested_parameter_attributes) unless nested_parameter_attributes.empty?
assign_multiparameter_attributes(multi_parameter_attributes) unless multi_parameter_attributes.empty?
end
- alias attributes= assign_attributes
-
- private
-
- def _assign_attribute(k, v)
- public_send("#{k}=", v)
- rescue NoMethodError
- if respond_to?("#{k}=")
- raise
- else
- raise UnknownAttributeError.new(self, k)
- end
- end
-
# Assign any deferred nested attributes after the base attributes have been set.
def assign_nested_parameter_attributes(pairs)
pairs.each { |k, v| _assign_attribute(k, v) }
@@ -81,7 +50,12 @@ module ActiveRecord
errors = []
callstack.each do |name, values_with_empty_parameters|
begin
- send("#{name}=", MultiparameterAttribute.new(self, name, values_with_empty_parameters).read_value)
+ if values_with_empty_parameters.each_value.all?(&:nil?)
+ values = nil
+ else
+ values = values_with_empty_parameters
+ end
+ send("#{name}=", values)
rescue => ex
errors << AttributeAssignmentError.new("error on assignment #{values_with_empty_parameters.values.inspect} to #{name} (#{ex.message})", ex, name)
end
@@ -113,100 +87,5 @@ module ActiveRecord
def find_parameter_position(multiparameter_name)
multiparameter_name.scan(/\(([0-9]*).*\)/).first.first.to_i
end
-
- class MultiparameterAttribute #:nodoc:
- attr_reader :object, :name, :values, :cast_type
-
- def initialize(object, name, values)
- @object = object
- @name = name
- @values = values
- end
-
- def read_value
- return if values.values.compact.empty?
-
- @cast_type = object.type_for_attribute(name)
- klass = cast_type.klass
-
- if klass == Time
- read_time
- elsif klass == Date
- read_date
- else
- read_other
- end
- end
-
- private
-
- def instantiate_time_object(set_values)
- if object.class.send(:create_time_zone_conversion_attribute?, name, cast_type)
- Time.zone.local(*set_values)
- else
- Time.send(object.class.default_timezone, *set_values)
- end
- end
-
- def read_time
- # If column is a :time (and not :date or :datetime) there is no need to validate if
- # there are year/month/day fields
- if cast_type.type == :time
- # if the column is a time set the values to their defaults as January 1, 1970, but only if they're nil
- { 1 => 1970, 2 => 1, 3 => 1 }.each do |key,value|
- values[key] ||= value
- end
- else
- # else column is a timestamp, so if Date bits were not provided, error
- validate_required_parameters!([1,2,3])
-
- # If Date bits were provided but blank, then return nil
- return if blank_date_parameter?
- end
-
- max_position = extract_max_param(6)
- set_values = values.values_at(*(1..max_position))
- # If Time bits are not there, then default to 0
- (3..5).each { |i| set_values[i] = set_values[i].presence || 0 }
- instantiate_time_object(set_values)
- end
-
- def read_date
- return if blank_date_parameter?
- set_values = values.values_at(1,2,3)
- begin
- Date.new(*set_values)
- rescue ArgumentError # if Date.new raises an exception on an invalid date
- instantiate_time_object(set_values).to_date # we instantiate Time object and convert it back to a date thus using Time's logic in handling invalid dates
- end
- end
-
- def read_other
- max_position = extract_max_param
- positions = (1..max_position)
- validate_required_parameters!(positions)
-
- values.slice(*positions)
- end
-
- # Checks whether some blank date parameter exists. Note that this is different
- # than the validate_required_parameters! method, since it just checks for blank
- # positions instead of missing ones, and does not raise in case one blank position
- # exists. The caller is responsible to handle the case of this returning true.
- def blank_date_parameter?
- (1..3).any? { |position| values[position].blank? }
- end
-
- # If some position is not provided, it errors out a missing parameter exception.
- def validate_required_parameters!(positions)
- if missing_parameter = positions.detect { |position| !values.key?(position) }
- raise ArgumentError.new("Missing Parameter - #{name}(#{missing_parameter})")
- end
- end
-
- def extract_max_param(upper_cap = 100)
- [values.keys.max, upper_cap].min
- end
- end
end
end
diff --git a/activerecord/lib/active_record/attribute_decorators.rb b/activerecord/lib/active_record/attribute_decorators.rb
index 5b96623b6e..7d0ae32411 100644
--- a/activerecord/lib/active_record/attribute_decorators.rb
+++ b/activerecord/lib/active_record/attribute_decorators.rb
@@ -15,7 +15,7 @@ module ActiveRecord
end
def decorate_matching_attribute_types(matcher, decorator_name, &block)
- clear_caches_calculated_from_columns
+ reload_schema_from_cache
decorator_name = decorator_name.to_s
# Create new hashes so we don't modify parent classes
@@ -24,10 +24,11 @@ module ActiveRecord
private
- def add_user_provided_columns(*)
- super.map do |column|
- decorated_type = attribute_type_decorations.apply(column.name, column.cast_type)
- column.with_type(decorated_type)
+ def load_schema!
+ super
+ attribute_types.each do |name, type|
+ decorated_type = attribute_type_decorations.apply(name, type)
+ define_attribute(name, decorated_type)
end
end
end
diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb
index 8f165fb1dc..9d58a19304 100644
--- a/activerecord/lib/active_record/attribute_methods.rb
+++ b/activerecord/lib/active_record/attribute_methods.rb
@@ -83,7 +83,7 @@ module ActiveRecord
generated_attribute_methods.synchronize do
return false if @attribute_methods_generated
superclass.define_attribute_methods unless self == base_class
- super(column_names)
+ super(attribute_names)
@attribute_methods_generated = true
end
true
@@ -185,7 +185,7 @@ module ActiveRecord
# # => ["id", "created_at", "updated_at", "name", "age"]
def attribute_names
@attribute_names ||= if !abstract_class? && table_exists?
- column_names
+ attribute_types.keys
else
[]
end
@@ -369,6 +369,39 @@ module ActiveRecord
write_attribute(attr_name, value)
end
+ # Returns the name of all database fields which have been read from this
+ # model. This can be useful in development mode to determine which fields
+ # need to be selected. For performance critical pages, selecting only the
+ # required fields can be an easy performance win (assuming you aren't using
+ # all of the fields on the model).
+ #
+ # For example:
+ #
+ # class PostsController < ActionController::Base
+ # after_action :print_accessed_fields, only: :index
+ #
+ # def index
+ # @posts = Post.all
+ # end
+ #
+ # private
+ #
+ # def print_accessed_fields
+ # p @posts.first.accessed_fields
+ # end
+ # end
+ #
+ # Which allows you to quickly change your code to:
+ #
+ # class PostsController < ActionController::Base
+ # def index
+ # @posts = Post.select(:id, :title, :author_id, :updated_at)
+ # end
+ # end
+ def accessed_fields
+ @attributes.accessed
+ end
+
protected
def clone_attribute_value(reader_method, attribute_name) # :nodoc:
diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb
index ce7f575150..7ba907f786 100644
--- a/activerecord/lib/active_record/attribute_methods/dirty.rb
+++ b/activerecord/lib/active_record/attribute_methods/dirty.rb
@@ -108,7 +108,7 @@ module ActiveRecord
end
def save_changed_attribute(attr, old_value)
- if attribute_changed?(attr)
+ if attribute_changed_by_setter?(attr)
clear_attribute_changes(attr) unless _field_changed?(attr, old_value)
else
set_attribute_was(attr, old_value) if _field_changed?(attr, old_value)
@@ -131,10 +131,8 @@ module ActiveRecord
partial_writes? ? super(keys_for_partial_write) : super
end
- # Serialized attributes should always be written in case they've been
- # changed in place.
def keys_for_partial_write
- changed & persistable_attribute_names
+ changed & self.class.column_names
end
def _field_changed?(attr, old_value)
@@ -165,7 +163,7 @@ module ActiveRecord
end
def store_original_raw_attribute(attr_name)
- original_raw_attributes[attr_name] = @attributes[attr_name].value_for_database
+ original_raw_attributes[attr_name] = @attributes[attr_name].value_for_database rescue nil
end
def store_original_raw_attributes
diff --git a/activerecord/lib/active_record/attribute_methods/query.rb b/activerecord/lib/active_record/attribute_methods/query.rb
index dc689f399a..553122a5fc 100644
--- a/activerecord/lib/active_record/attribute_methods/query.rb
+++ b/activerecord/lib/active_record/attribute_methods/query.rb
@@ -22,7 +22,7 @@ module ActiveRecord
return false if ActiveRecord::ConnectionAdapters::Column::FALSE_VALUES.include?(value)
!value.blank?
end
- elsif column.number?
+ elsif value.respond_to?(:zero?)
!value.zero?
else
!value.blank?
diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb
index 24e30b6608..0d989c2eca 100644
--- a/activerecord/lib/active_record/attribute_methods/read.rb
+++ b/activerecord/lib/active_record/attribute_methods/read.rb
@@ -69,9 +69,18 @@ module ActiveRecord
# This method exists to avoid the expensive primary_key check internally, without
# breaking compatibility with the read_attribute API
- def _read_attribute(attr_name) # :nodoc:
- @attributes.fetch_value(attr_name.to_s) { |n| yield n if block_given? }
+ if defined?(JRUBY_VERSION)
+ # This form is significantly faster on JRuby, and this is one of our biggest hotspots.
+ # https://github.com/jruby/jruby/pull/2562
+ def _read_attribute(attr_name, &block) # :nodoc
+ @attributes.fetch_value(attr_name.to_s, &block)
+ end
+ else
+ def _read_attribute(attr_name) # :nodoc:
+ @attributes.fetch_value(attr_name.to_s) { |n| yield n if block_given? }
+ end
end
+
alias :attribute :_read_attribute
private :attribute
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 777f7ab4d7..90c36e4b02 100644
--- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
+++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
@@ -2,8 +2,6 @@ module ActiveRecord
module AttributeMethods
module TimeZoneConversion
class TimeZoneConverter < DelegateClass(Type::Value) # :nodoc:
- include Type::Decorator
-
def type_cast_from_database(value)
convert_time_to_time_zone(super)
end
@@ -11,15 +9,19 @@ module ActiveRecord
def type_cast_from_user(value)
if value.is_a?(Array)
value.map { |v| type_cast_from_user(v) }
+ elsif value.is_a?(Hash)
+ set_time_zone_without_conversion(super)
elsif value.respond_to?(:in_time_zone)
begin
- value.in_time_zone || super
+ user_input_in_time_zone(value) || super
rescue ArgumentError
nil
end
end
end
+ private
+
def convert_time_to_time_zone(value)
if value.is_a?(Array)
value.map { |v| convert_time_to_time_zone(v) }
@@ -29,6 +31,10 @@ module ActiveRecord
value
end
end
+
+ def set_time_zone_without_conversion(value)
+ ::Time.zone.local_to_utc(value).in_time_zone
+ end
end
extend ActiveSupport::Concern
@@ -39,6 +45,9 @@ module ActiveRecord
class_attribute :skip_time_zone_conversion_for_attributes, instance_writer: false
self.skip_time_zone_conversion_for_attributes = []
+
+ class_attribute :time_zone_aware_types, instance_writer: false
+ self.time_zone_aware_types = [:datetime, :not_explicitly_configured]
end
module ClassMethods
@@ -59,9 +68,31 @@ module ActiveRecord
end
def create_time_zone_conversion_attribute?(name, cast_type)
- time_zone_aware_attributes &&
- !self.skip_time_zone_conversion_for_attributes.include?(name.to_sym) &&
- (:datetime == cast_type.type)
+ enabled_for_column = time_zone_aware_attributes &&
+ !self.skip_time_zone_conversion_for_attributes.include?(name.to_sym)
+ result = enabled_for_column &&
+ time_zone_aware_types.include?(cast_type.type)
+
+ if enabled_for_column &&
+ !result &&
+ cast_type.type == :time &&
+ time_zone_aware_types.include?(:not_explicitly_configured)
+ ActiveSupport::Deprecation.warn(<<-MESSAGE)
+ Time columns will become time zone aware in Rails 5.1. This
+ still causes `String`s to be parsed as if they were in `Time.zone`,
+ and `Time`s to be converted to `Time.zone`.
+
+ To keep the old behavior, you must add the following to your initializer:
+
+ config.active_record.time_zone_aware_types = [:datetime]
+
+ To silence this deprecation warning, add the following:
+
+ config.active_record.time_zone_aware_types << :time
+ MESSAGE
+ end
+
+ result
end
end
end
diff --git a/activerecord/lib/active_record/attribute_set.rb b/activerecord/lib/active_record/attribute_set.rb
index 66fcaf6945..013a7d0e01 100644
--- a/activerecord/lib/active_record/attribute_set.rb
+++ b/activerecord/lib/active_record/attribute_set.rb
@@ -10,6 +10,10 @@ module ActiveRecord
attributes[name] || Attribute.null(name)
end
+ def []=(name, value)
+ attributes[name] = value
+ end
+
def values_before_type_cast
attributes.transform_values(&:value_before_type_cast)
end
@@ -24,11 +28,19 @@ module ActiveRecord
end
def keys
- attributes.initialized_keys
+ attributes.each_key.select { |name| self[name].initialized? }
end
- def fetch_value(name)
- self[name].value { |n| yield n if block_given? }
+ if defined?(JRUBY_VERSION)
+ # This form is significantly faster on JRuby, and this is one of our biggest hotspots.
+ # https://github.com/jruby/jruby/pull/2562
+ def fetch_value(name, &block)
+ self[name].value(&block)
+ end
+ else
+ def fetch_value(name)
+ self[name].value { |n| yield n if block_given? }
+ end
end
def write_from_database(name, value)
@@ -49,7 +61,7 @@ module ActiveRecord
end
def initialize_dup(_)
- @attributes = attributes.dup
+ @attributes = attributes.deep_dup
super
end
@@ -64,6 +76,10 @@ module ActiveRecord
end
end
+ def accessed
+ attributes.select { |_, attr| attr.has_been_read? }.keys
+ end
+
protected
attr_reader :attributes
diff --git a/activerecord/lib/active_record/attribute_set/builder.rb b/activerecord/lib/active_record/attribute_set/builder.rb
index 3a76f5262d..e85777c335 100644
--- a/activerecord/lib/active_record/attribute_set/builder.rb
+++ b/activerecord/lib/active_record/attribute_set/builder.rb
@@ -20,7 +20,7 @@ module ActiveRecord
end
class LazyAttributeHash # :nodoc:
- delegate :select, :transform_values, to: :materialize
+ delegate :transform_values, :each_key, to: :materialize
def initialize(types, values, additional_types)
@types = types
@@ -45,15 +45,21 @@ module ActiveRecord
delegate_hash[key] = value
end
- def initialized_keys
- delegate_hash.keys | values.keys
- end
-
def initialize_dup(_)
@delegate_hash = delegate_hash.transform_values(&:dup)
super
end
+ def select
+ keys = types.keys | values.keys | delegate_hash.keys
+ keys.each_with_object({}) do |key, hash|
+ attribute = self[key]
+ if yield(key, attribute)
+ hash[key] = attribute
+ end
+ end
+ end
+
protected
attr_reader :types, :values, :additional_types, :delegate_hash
diff --git a/activerecord/lib/active_record/attributes.rb b/activerecord/lib/active_record/attributes.rb
index b263a89d79..2c475f3cda 100644
--- a/activerecord/lib/active_record/attributes.rb
+++ b/activerecord/lib/active_record/attributes.rb
@@ -1,33 +1,42 @@
module ActiveRecord
- module Attributes # :nodoc:
+ # See ActiveRecord::Attributes::ClassMethods for documentation
+ module Attributes
extend ActiveSupport::Concern
+ # :nodoc:
Type = ActiveRecord::Type
included do
- class_attribute :user_provided_columns, instance_accessor: false # :internal:
- class_attribute :user_provided_defaults, instance_accessor: false # :internal:
- self.user_provided_columns = {}
- self.user_provided_defaults = {}
-
- delegate :persistable_attribute_names, to: :class
+ class_attribute :attributes_to_define_after_schema_loads, instance_accessor: false # :internal:
+ self.attributes_to_define_after_schema_loads = {}
end
- module ClassMethods # :nodoc:
- # Defines or overrides a attribute on this model. This allows customization of
- # Active Record's type casting behavior, as well as adding support for user defined
- # types.
+ module ClassMethods
+ # Defines an attribute with a type on this model. It will override the
+ # type of existing attributes if needed. This allows control over how
+ # values are converted to and from SQL when assigned to a model. It also
+ # changes the behavior of values passed to
+ # ActiveRecord::QueryMethods#where. This will let you use
+ # your domain objects across much of Active Record, without having to
+ # rely on implementation details or monkey patching.
+ #
+ # +name+ The name of the methods to define attribute methods for, and the
+ # column which this will persist to.
+ #
+ # +cast_type+ A symbol such as +:string+ or +:integer+, or a type object
+ # to be used for this attribute. See the examples below for more
+ # information about providing custom type objects.
#
- # +name+ The name of the methods to define attribute methods for, and the column which
- # this will persist to.
+ # ==== Options
+ # The following options are accepted:
#
- # +cast_type+ A type object that contains information about how to type cast the value.
- # See the examples section for more information.
+ # +default+ The default value to use when no value is provided. If this option
+ # is not passed, the previous default value (if any) will be used.
+ # Otherwise, the default will be +nil+.
#
- # ==== Options
- # The options hash accepts the following options:
+ # +array+ (PG only) specifies that the type should be an array (see the examples below).
#
- # +default+ is the default value that the column should use on a new record.
+ # +range+ (PG only) specifies that the type should be a range (see the examples below).
#
# ==== Examples
#
@@ -48,25 +57,64 @@ module ActiveRecord
# store_listing.price_in_cents # => BigDecimal.new(10.1)
#
# class StoreListing < ActiveRecord::Base
- # attribute :price_in_cents, Type::Integer.new
+ # attribute :price_in_cents, :integer
# end
#
# # after
# store_listing.price_in_cents # => 10
#
- # Users may also define their own custom types, as long as they respond to the methods
- # defined on the value type. The +type_cast+ method on your type object will be called
- # with values both from the database, and from your controllers. See
- # +ActiveRecord::Attributes::Type::Value+ for the expected API. It is recommended that your
- # type objects inherit from an existing type, or the base value type.
+ # A default can also be provided.
+ #
+ # create_table :store_listings, force: true do |t|
+ # t.string :my_string, default: "original default"
+ # end
+ #
+ # StoreListing.new.my_string # => "original default"
+ #
+ # class StoreListing < ActiveRecord::Base
+ # attribute :my_string, :string, default: "new default"
+ # end
+ #
+ # StoreListing.new.my_string # => "new default"
+ #
+ # Attributes do not need to be backed by a database column.
+ #
+ # class MyModel < ActiveRecord::Base
+ # attribute :my_string, :string
+ # attribute :my_int_array, :integer, array: true
+ # attribute :my_float_range, :float, range: true
+ # end
+ #
+ # model = MyModel.new(
+ # my_string: "string",
+ # my_int_array: ["1", "2", "3"],
+ # my_float_range: "[1,3.5]",
+ # )
+ # model.attributes
+ # # =>
+ # {
+ # my_string: "string",
+ # my_int_array: [1, 2, 3],
+ # my_float_range: 1.0..3.5
+ # }
+ #
+ # ==== Creating Custom Types
+ #
+ # Users may also define their own custom types, as long as they respond
+ # to the methods defined on the value type. The method
+ # +type_cast_from_database+ or +type_cast_from_user+ will be called on
+ # your type object, with raw input from the database or from your
+ # controllers. See ActiveRecord::Type::Value for the expected API. It is
+ # recommended that your type objects inherit from an existing type, or
+ # from ActiveRecord::Type::Value
#
# class MoneyType < ActiveRecord::Type::Integer
- # def type_cast(value)
+ # def type_cast_from_user(value)
# if value.include?('$')
# price_in_dollars = value.gsub(/\$/, '').to_f
- # price_in_dollars * 100
+ # super(price_in_dollars * 100)
# else
- # value.to_i
+ # super
# end
# end
# end
@@ -77,70 +125,112 @@ module ActiveRecord
#
# store_listing = StoreListing.new(price_in_cents: '$10.00')
# store_listing.price_in_cents # => 1000
- def attribute(name, cast_type, options = {})
+ #
+ # For more details on creating custom types, see the documentation for
+ # ActiveRecord::Type::Value.
+ #
+ # ==== Querying
+ #
+ # When ActiveRecord::QueryMethods#where is called, it will
+ # use the type defined by the model class to convert the value to SQL,
+ # calling +type_cast_for_database+ on your type object. For example:
+ #
+ # class Money < Struct.new(:amount, :currency)
+ # end
+ #
+ # class MoneyType < Type::Value
+ # def initialize(currency_converter)
+ # @currency_converter = currency_converter
+ # end
+ #
+ # # value will be the result of +type_cast_from_database+ or
+ # # +type_cast_from_user+. Assumed to be in instance of +Money+ in
+ # # this case.
+ # def type_cast_for_database(value)
+ # value_in_bitcoins = @currency_converter.convert_to_bitcoins(value)
+ # value_in_bitcoins.amount
+ # end
+ # end
+ #
+ # class Product < ActiveRecord::Base
+ # currency_converter = ConversionRatesFromTheInternet.new
+ # attribute :price_in_bitcoins, MoneyType.new(currency_converter)
+ # end
+ #
+ # Product.where(price_in_bitcoins: Money.new(5, "USD"))
+ # # => SELECT * FROM products WHERE price_in_bitcoins = 0.02230
+ #
+ # Product.where(price_in_bitcoins: Money.new(5, "GBP"))
+ # # => SELECT * FROM products WHERE price_in_bitcoins = 0.03412
+ #
+ # ==== Dirty Tracking
+ #
+ # The type of an attribute is given the opportunity to change how dirty
+ # tracking is performed. The methods +changed?+ and +changed_in_place?+
+ # will be called from ActiveModel::Dirty. See the documentation for those
+ # methods in ActiveRecord::Type::Value for more details.
+ def attribute(name, cast_type, **options)
name = name.to_s
- clear_caches_calculated_from_columns
- # Assign a new hash to ensure that subclasses do not share a hash
- self.user_provided_columns = user_provided_columns.merge(name => cast_type)
-
- if options.key?(:default)
- self.user_provided_defaults = user_provided_defaults.merge(name => options[:default])
- end
- end
+ reload_schema_from_cache
- # Returns an array of column objects for the table associated with this class.
- def columns
- @columns ||= add_user_provided_columns(connection.schema_cache.columns(table_name))
+ self.attributes_to_define_after_schema_loads =
+ attributes_to_define_after_schema_loads.merge(
+ name => [cast_type, options]
+ )
end
- # Returns a hash of column objects for the table associated with this class.
- def columns_hash
- @columns_hash ||= Hash[columns.map { |c| [c.name, c] }]
+ # This is the low level API which sits beneath +attribute+. It only
+ # accepts type objects, and will do its work immediately instead of
+ # waiting for the schema to load. Automatic schema detection and
+ # ClassMethods#attribute both call this under the hood. While this method
+ # is provided so it can be used by plugin authors, application code
+ # should probably use ClassMethods#attribute.
+ #
+ # +name+ The name of the attribute being defined. Expected to be a +String+.
+ #
+ # +cast_type+ The type object to use for this attribute.
+ #
+ # +default+ The default value to use when no value is provided. If this option
+ # is not passed, the previous default value (if any) will be used.
+ # Otherwise, the default will be +nil+.
+ #
+ # +user_provided_default+ Whether the default value should be cast using
+ # +type_cast_from_user+ or +type_cast_from_database+.
+ def define_attribute(
+ name,
+ cast_type,
+ default: NO_DEFAULT_PROVIDED,
+ user_provided_default: true
+ )
+ attribute_types[name] = cast_type
+ define_default_attribute(name, default, cast_type, from_user: user_provided_default)
end
- def persistable_attribute_names # :nodoc:
- @persistable_attribute_names ||= connection.schema_cache.columns_hash(table_name).keys
- end
-
- def reset_column_information # :nodoc:
+ def load_schema! # :nodoc:
super
- clear_caches_calculated_from_columns
- end
-
- private
-
- def add_user_provided_columns(schema_columns)
- existing_columns = schema_columns.map do |column|
- new_type = user_provided_columns[column.name]
- if new_type
- column.with_type(new_type)
- else
- column
+ attributes_to_define_after_schema_loads.each do |name, (type, options)|
+ if type.is_a?(Symbol)
+ type = connection.type_for_attribute_options(type, **options.except(:default))
end
- end
- existing_column_names = existing_columns.map(&:name)
- new_columns = user_provided_columns.except(*existing_column_names).map do |(name, type)|
- connection.new_column(name, nil, type)
+ define_attribute(name, type, **options.slice(:default))
end
-
- existing_columns + new_columns
end
- def clear_caches_calculated_from_columns
- @arel_table = nil
- @attributes_builder = nil
- @column_names = nil
- @column_types = nil
- @columns = nil
- @columns_hash = nil
- @content_columns = nil
- @default_attributes = nil
- @persistable_attribute_names = nil
- end
+ private
- def raw_default_values
- super.merge(user_provided_defaults)
+ NO_DEFAULT_PROVIDED = Object.new # :nodoc:
+ private_constant :NO_DEFAULT_PROVIDED
+
+ def define_default_attribute(name, value, type, from_user:)
+ if value == NO_DEFAULT_PROVIDED
+ default_attribute = _default_attributes[name].with_type(type)
+ elsif from_user
+ default_attribute = Attribute.from_user(name, value, type)
+ else
+ default_attribute = Attribute.from_database(name, value, type)
+ end
+ _default_attributes[name] = default_attribute
end
end
end
diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb
index fcaaffb852..0792d19c3e 100644
--- a/activerecord/lib/active_record/autosave_association.rb
+++ b/activerecord/lib/active_record/autosave_association.rb
@@ -177,10 +177,8 @@ module ActiveRecord
# before actually defining them.
def add_autosave_association_callbacks(reflection)
save_method = :"autosave_associated_records_for_#{reflection.name}"
- validation_method = :"validate_associated_records_for_#{reflection.name}"
- collection = reflection.collection?
- if collection
+ if reflection.collection?
before_save :before_save_collection_association
define_non_cyclic_method(save_method) { save_collection_association(reflection) }
@@ -204,8 +202,18 @@ module ActiveRecord
before_save save_method
end
+ define_autosave_validation_callbacks(reflection)
+ end
+
+ def define_autosave_validation_callbacks(reflection)
+ validation_method = :"validate_associated_records_for_#{reflection.name}"
if reflection.validate? && !method_defined?(validation_method)
- method = (collection ? :validate_collection_association : :validate_single_association)
+ if reflection.collection?
+ method = :validate_collection_association
+ else
+ method = :validate_single_association
+ end
+
define_non_cyclic_method(validation_method) do
send(method, reflection)
# TODO: remove the following line as soon as the return value of
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 787d07c4c2..42ad285340 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
@@ -201,16 +201,14 @@ module ActiveRecord
# isolation level. However, support is disabled for MySQL versions below 5,
# because they are affected by a bug[http://bugs.mysql.com/bug.php?id=39170]
# which means the isolation level gets persisted outside the transaction.
- def transaction(options = {})
- options.assert_valid_keys :requires_new, :joinable, :isolation
-
- if !options[:requires_new] && current_transaction.joinable?
- if options[:isolation]
+ def transaction(requires_new: nil, isolation: nil, joinable: true)
+ if !requires_new && current_transaction.joinable?
+ if isolation
raise ActiveRecord::TransactionIsolationError, "cannot set isolation when joining a transaction"
end
yield
else
- transaction_manager.within_new_transaction(options) { yield }
+ transaction_manager.within_new_transaction(isolation: isolation, joinable: joinable) { yield }
end
rescue ActiveRecord::Rollback
# rollbacks are silently swallowed
@@ -234,6 +232,10 @@ module ActiveRecord
current_transaction.add_record(record)
end
+ def transaction_state
+ current_transaction.state
+ end
+
# Begins the transaction (and turns off auto-committing).
def begin_db_transaction() end
@@ -286,10 +288,11 @@ module ActiveRecord
columns = schema_cache.columns_hash(table_name)
binds = fixture.map do |name, value|
- [columns[name], value]
+ type = lookup_cast_type_from_column(columns[name])
+ Relation::QueryAttribute.new(name, value, type)
end
key_list = fixture.keys.map { |name| quote_column_name(name) }
- value_list = prepare_binds_for_database(binds).map do |_, value|
+ value_list = prepare_binds_for_database(binds).map do |value|
begin
quote(value)
rescue TypeError
@@ -381,7 +384,7 @@ module ActiveRecord
def binds_from_relation(relation, binds)
if relation.is_a?(Relation) && binds.empty?
- relation, binds = relation.arel, relation.bind_values
+ relation, binds = relation.arel, relation.bound_attributes
end
[relation, binds]
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
index c18caa2a2f..947e11c7bf 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
@@ -16,7 +16,7 @@ module ActiveRecord
https://github.com/rails/arel/commit/6160bfbda1d1781c3b08a33ec4955f170e95be11
for more information.
MSG
- value = column.cast_type.type_cast_for_database(value)
+ value = type_cast_from_column(column, value)
end
_quote(value)
@@ -31,7 +31,7 @@ module ActiveRecord
end
if column
- value = column.cast_type.type_cast_for_database(value)
+ value = type_cast_from_column(column, value)
end
_type_cast(value)
@@ -40,6 +40,40 @@ module ActiveRecord
raise TypeError, "can't cast #{value.class}#{to_type}"
end
+ # If you are having to call this function, you are likely doing something
+ # wrong. The column does not have sufficient type information if the user
+ # provided a custom type on the class level either explicitly (via
+ # `attribute`) or implicitly (via `serialize`,
+ # `time_zone_aware_attributes`). In almost all cases, the sql type should
+ # only be used to change quoting behavior, when the primitive to
+ # represent the type doesn't sufficiently reflect the differences
+ # (varchar vs binary) for example. The type used to get this primitive
+ # should have been provided before reaching the connection adapter.
+ def type_cast_from_column(column, value) # :nodoc:
+ if column
+ type = lookup_cast_type_from_column(column)
+ type.type_cast_for_database(value)
+ else
+ value
+ end
+ end
+
+ # See docs for +type_cast_from_column+
+ def lookup_cast_type_from_column(column) # :nodoc:
+ lookup_cast_type(column.sql_type)
+ end
+
+ def fetch_type_metadata(sql_type)
+ cast_type = lookup_cast_type(sql_type)
+ SqlTypeMetadata.new(
+ sql_type: sql_type,
+ type: cast_type.type,
+ limit: cast_type.limit,
+ precision: cast_type.precision,
+ scale: cast_type.scale,
+ )
+ end
+
# Quotes a string, escaping any ' (single quote) and \ (backslash)
# characters.
def quote_string(s)
@@ -68,6 +102,11 @@ module ActiveRecord
quote_table_name("#{table}.#{attr}")
end
+ def quote_default_expression(value, column) #:nodoc:
+ value = lookup_cast_type(column.sql_type).type_cast_for_database(value)
+ quote(value)
+ end
+
def quoted_true
"'t'"
end
@@ -93,21 +132,41 @@ module ActiveRecord
end
end
- value.to_s(:db)
+ result = value.to_s(:db)
+ if value.respond_to?(:usec) && value.usec > 0
+ "#{result}.#{sprintf("%06d", value.usec)}"
+ else
+ result
+ end
end
def prepare_binds_for_database(binds) # :nodoc:
- binds.map do |column, value|
- if column
- column_name = column.name
- value = column.cast_type.type_cast_for_database(value)
- end
- [column_name, value]
- end
+ binds.map(&:value_for_database)
+ end
+
+ def type_for_attribute_options(type_name, **options)
+ klass = type_classes_with_standard_constructor.fetch(type_name, Type::Value)
+ klass.new(**options)
end
private
+ def type_classes_with_standard_constructor
+ {
+ big_integer: Type::BigInteger,
+ binary: Type::Binary,
+ boolean: Type::Boolean,
+ date: Type::Date,
+ date_time: Type::DateTime,
+ decimal: Type::Decimal,
+ float: Type::Float,
+ integer: Type::Integer,
+ string: Type::String,
+ text: Type::Text,
+ time: Type::Time,
+ }
+ end
+
def types_which_need_no_typecasting
[nil, Numeric, String]
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 db20b60d60..bc8fa9b6cf 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb
@@ -28,7 +28,7 @@ module ActiveRecord
end
def visit_ColumnDefinition(o)
- o.sql_type = type_to_sql(o.type, o.limit, o.precision, o.scale)
+ o.sql_type ||= type_to_sql(o.type, o.limit, o.precision, o.scale)
column_sql = "#{quote_column_name(o.name)} #{o.sql_type}"
add_column_options!(column_sql, column_options(o)) unless o.type == :primary_key
column_sql
@@ -98,8 +98,7 @@ module ActiveRecord
end
def quote_default_expression(value, column)
- value = type_for_column(column).type_cast_for_database(value)
- @conn.quote(value)
+ @conn.quote_default_expression(value, column)
end
def options_include_default?(options)
@@ -118,10 +117,6 @@ module ActiveRecord
MSG
end
end
-
- def type_for_column(column)
- @conn.lookup_cast_type(column.sql_type)
- end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
index 7eaa89c9a7..8cd4b8e5b2 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, :auto_increment, :primary_key, :sql_type, :cast_type) #:nodoc:
+ class ColumnDefinition < Struct.new(:name, :type, :limit, :precision, :scale, :default, :null, :first, :after, :auto_increment, :primary_key, :sql_type) #:nodoc:
def primary_key?
primary_key || type.to_sym == :primary_key
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 42ea599a74..932aaf7aa7 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb
@@ -46,9 +46,10 @@ module ActiveRecord
private
def schema_default(column)
- default = column.type_cast_from_database(column.default)
+ type = lookup_cast_type_from_column(column)
+ default = type.type_cast_from_database(column.default)
unless default.nil?
- column.type_cast_for_schema(default)
+ type.type_cast_for_schema(default)
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 0f44c332ae..c9180f64db 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
@@ -1,4 +1,6 @@
require 'active_record/migration/join_table'
+require 'active_support/core_ext/string/access'
+require 'digest'
module ActiveRecord
module ConnectionAdapters # :nodoc:
@@ -380,7 +382,7 @@ module ActiveRecord
# it can be helpful to provide these in a migration's +change+ method so it can be reverted.
# In that case, +options+ and the block will be used by create_table.
def drop_table(table_name, options = {})
- execute "DROP TABLE #{quote_table_name(table_name)}"
+ execute "DROP TABLE#{' IF EXISTS' if options[:if_exists]} #{quote_table_name(table_name)}"
end
# Adds a new column to the named table.
@@ -660,7 +662,13 @@ module ActiveRecord
#
# remove_reference(:products, :supplier, polymorphic: true)
#
+ # ====== Remove the reference with a foreign key
+ #
+ # remove_reference(:products, :user, index: true, foreign_key: true)
+ #
def remove_reference(table_name, ref_name, options = {})
+ remove_foreign_key table_name, ref_name if options[:foreign_key]
+
remove_column(table_name, "#{ref_name}_id")
remove_column(table_name, "#{ref_name}_type") if options[:polymorphic]
end
@@ -832,6 +840,12 @@ module ActiveRecord
raise ArgumentError, "Error adding decimal column: precision cannot be empty if scale is specified"
end
+ elsif [:datetime, :time].include?(type) && precision ||= native[:precision]
+ if (0..6) === precision
+ column_type_sql << "(#{precision})"
+ else
+ raise(ActiveRecordError, "No #{native[:name]} type has precision of #{precision}. The allowed range of precision is from 0 to 6")
+ end
elsif (type != :primary_key) && (limit ||= native.is_a?(Hash) && native[:limit])
column_type_sql << "(#{limit})"
end
@@ -990,8 +1004,10 @@ module ActiveRecord
end
def foreign_key_name(table_name, options) # :nodoc:
+ identifier = "#{table_name}_#{options.fetch(:column)}_fk"
+ hashed_identifier = Digest::SHA256.hexdigest(identifier).first(10)
options.fetch(:name) do
- "fk_rails_#{SecureRandom.hex(5)}"
+ "fk_rails_#{hashed_identifier}"
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb
index 7535e9147a..11440e30d4 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb
@@ -55,11 +55,7 @@ module ActiveRecord
end
def add_record(record)
- if record.has_transactional_callbacks?
- records << record
- else
- record.set_transaction_state(@state)
- end
+ records << record
end
def rollback
@@ -111,7 +107,6 @@ module ActiveRecord
def rollback
connection.rollback_to_savepoint(savepoint_name)
super
- rollback_records
end
def commit
@@ -136,13 +131,11 @@ module ActiveRecord
def rollback
connection.rollback_db_transaction
super
- rollback_records
end
def commit
connection.commit_db_transaction
super
- commit_records
end
end
@@ -159,18 +152,28 @@ module ActiveRecord
else
SavepointTransaction.new(@connection, "active_record_#{@stack.size}", options)
end
+
@stack.push(transaction)
transaction
end
def commit_transaction
- transaction = @stack.pop
- transaction.commit
- transaction.records.each { |r| current_transaction.add_record(r) }
+ inner_transaction = @stack.pop
+ inner_transaction.commit
+
+ if current_transaction.joinable?
+ inner_transaction.records.each do |r|
+ r.add_to_transaction
+ end
+ else
+ inner_transaction.commit_records
+ end
end
- def rollback_transaction
- @stack.pop.rollback
+ def rollback_transaction(transaction = nil)
+ transaction ||= @stack.pop
+ transaction.rollback
+ transaction.rollback_records
end
def within_new_transaction(options = {})
@@ -182,12 +185,12 @@ module ActiveRecord
ensure
unless error
if Thread.current.status == 'aborting'
- rollback_transaction
+ rollback_transaction if transaction
else
begin
commit_transaction
rescue Exception
- transaction.rollback unless transaction.state.completed?
+ rollback_transaction(transaction) unless transaction.state.completed?
raise
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 c941c9f1eb..c307189980 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
@@ -4,6 +4,7 @@ require 'bigdecimal/util'
require 'active_record/type'
require 'active_support/core_ext/benchmark'
require 'active_record/connection_adapters/schema_cache'
+require 'active_record/connection_adapters/sql_type_metadata'
require 'active_record/connection_adapters/abstract/schema_dumper'
require 'active_record/connection_adapters/abstract/schema_creation'
require 'monitor'
@@ -114,7 +115,7 @@ module ActiveRecord
class BindCollector < Arel::Collectors::Bind
def compile(bvs, conn)
casted_binds = conn.prepare_binds_for_database(bvs)
- super(casted_binds.map { |_, value| conn.quote(value) })
+ super(casted_binds.map { |value| conn.quote(value) })
end
end
@@ -244,6 +245,11 @@ module ActiveRecord
false
end
+ # Does this adapter support datetime with precision?
+ def supports_datetime_with_precision?
+ false
+ end
+
# This is meant to be implemented by the adapters that support extensions
def disable_extension(name)
end
@@ -262,18 +268,6 @@ module ActiveRecord
{}
end
- # QUOTING ==================================================
-
- # Quote date/time values for use in SQL input. Includes microseconds
- # if the value is a Time responding to usec.
- def quoted_date(value) #:nodoc:
- if value.acts_like?(:time) && value.respond_to?(:usec)
- "#{super}.#{sprintf("%06d", value.usec)}"
- else
- super
- end
- end
-
# Returns a bind substitution value given a bind +column+
# NOTE: The column param is currently being used by the sqlserver-adapter
def substitute_at(column, _unused = 0)
@@ -366,9 +360,18 @@ module ActiveRecord
end
def case_insensitive_comparison(table, attribute, column, value)
- table[attribute].lower.eq(table.lower(value))
+ if can_perform_case_insensitive_comparison_for?(column)
+ table[attribute].lower.eq(table.lower(value))
+ else
+ case_sensitive_comparison(table, attribute, column, value)
+ end
end
+ def can_perform_case_insensitive_comparison_for?(column)
+ true
+ end
+ private :can_perform_case_insensitive_comparison_for?
+
def current_savepoint_name
current_transaction.savepoint_name
end
@@ -384,8 +387,8 @@ module ActiveRecord
end
end
- def new_column(name, default, cast_type, sql_type = nil, null = true)
- Column.new(name, default, cast_type, sql_type, null)
+ def new_column(name, default, sql_type_metadata = nil, null = true)
+ Column.new(name, default, sql_type_metadata, null)
end
def lookup_cast_type(sql_type) # :nodoc:
@@ -399,15 +402,15 @@ module ActiveRecord
protected
def initialize_type_map(m) # :nodoc:
- register_class_with_limit m, %r(boolean)i, Type::Boolean
- register_class_with_limit m, %r(char)i, Type::String
- register_class_with_limit m, %r(binary)i, Type::Binary
- register_class_with_limit m, %r(text)i, Type::Text
- register_class_with_limit m, %r(date)i, Type::Date
- register_class_with_limit m, %r(time)i, Type::Time
- register_class_with_limit m, %r(datetime)i, Type::DateTime
- register_class_with_limit m, %r(float)i, Type::Float
- register_class_with_limit m, %r(int)i, Type::Integer
+ register_class_with_limit m, %r(boolean)i, Type::Boolean
+ register_class_with_limit m, %r(char)i, Type::String
+ register_class_with_limit m, %r(binary)i, Type::Binary
+ register_class_with_limit m, %r(text)i, Type::Text
+ register_class_with_precision m, %r(date)i, Type::Date
+ register_class_with_precision m, %r(time)i, Type::Time
+ register_class_with_precision m, %r(datetime)i, Type::DateTime
+ register_class_with_limit m, %r(float)i, Type::Float
+ register_class_with_limit m, %r(int)i, Type::Integer
m.alias_type %r(blob)i, 'binary'
m.alias_type %r(clob)i, 'text'
@@ -441,6 +444,13 @@ module ActiveRecord
end
end
+ def register_class_with_precision(mapping, key, klass) # :nodoc:
+ mapping.register_type(key) do |*args|
+ precision = extract_precision(args.last)
+ klass.new(precision: precision)
+ end
+ end
+
def extract_scale(sql_type) # :nodoc:
case sql_type
when /\((\d+)\)/ then 0
@@ -453,7 +463,12 @@ module ActiveRecord
end
def extract_limit(sql_type) # :nodoc:
- $1.to_i if sql_type =~ /\((.*)\)/
+ case sql_type
+ when /^bigint/i
+ 8
+ when /\((.*)\)/
+ $1.to_i
+ end
end
def translate_exception_class(e, sql)
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 e9a3c26c32..0a9e599c3c 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
@@ -63,7 +63,7 @@ module ActiveRecord
def column_spec_for_primary_key(column)
spec = {}
- if column.extra == 'auto_increment'
+ if column.auto_increment?
return unless column.limit == 8
spec[:id] = ':bigint'
else
@@ -73,14 +73,17 @@ module ActiveRecord
spec
end
+ def prepare_column_options(column)
+ spec = super
+ spec.delete(:precision) if column.type == :datetime && column.precision == 0
+ spec
+ end
+
class Column < ConnectionAdapters::Column # :nodoc:
- attr_reader :collation, :strict, :extra
+ delegate :strict, :collation, :extra, to: :sql_type_metadata, allow_nil: true
- def initialize(name, default, cast_type, sql_type = nil, null = true, collation = nil, strict = false, extra = "")
- @strict = strict
- @collation = collation
- @extra = extra
- super(name, default, cast_type, sql_type, null)
+ def initialize(*)
+ super
assert_valid_default(default)
extract_default
end
@@ -88,7 +91,7 @@ module ActiveRecord
def extract_default
if blob_or_text_column?
@default = null || strict ? nil : ''
- elsif missing_default_forged_as_empty_string?(@default)
+ elsif missing_default_forged_as_empty_string?(default)
@default = nil
end
end
@@ -106,11 +109,8 @@ module ActiveRecord
collation && !collation.match(/_ci$/)
end
- def ==(other)
- super &&
- collation == other.collation &&
- strict == other.strict &&
- extra == other.extra
+ def auto_increment?
+ extra == 'auto_increment'
end
private
@@ -131,9 +131,33 @@ module ActiveRecord
raise ArgumentError, "#{type} columns cannot have a default value: #{default.inspect}"
end
end
+ end
+
+ class MysqlTypeMetadata < DelegateClass(SqlTypeMetadata) # :nodoc:
+ attr_reader :collation, :extra, :strict
+
+ def initialize(type_metadata, collation: "", extra: "", strict: false)
+ super(type_metadata)
+ @type_metadata = type_metadata
+ @collation = collation
+ @extra = extra
+ @strict = strict
+ end
+
+ def ==(other)
+ other.is_a?(MysqlTypeMetadata) &&
+ attributes_for_hash == other.attributes_for_hash
+ end
+ alias eql? ==
+
+ def hash
+ attributes_for_hash.hash
+ end
+
+ protected
def attributes_for_hash
- super + [collation, strict, extra]
+ [self.class, @type_metadata, collation, extra, strict]
end
end
@@ -227,6 +251,10 @@ module ActiveRecord
version[0] >= 5
end
+ def supports_datetime_with_precision?
+ (version[0] == 5 && version[1] >= 6) || version[0] >= 6
+ end
+
def native_database_types
NATIVE_DATABASE_TYPES
end
@@ -243,8 +271,8 @@ module ActiveRecord
raise NotImplementedError
end
- def new_column(field, default, cast_type, sql_type = nil, null = true, collation = "", extra = "") # :nodoc:
- Column.new(field, default, cast_type, sql_type, null, collation, strict_mode?, extra)
+ def new_column(field, default, sql_type_metadata = nil, null = true) # :nodoc:
+ Column.new(field, default, sql_type_metadata, null)
end
# Must return the MySQL error number from the exception, if the exception has an
@@ -467,8 +495,8 @@ module ActiveRecord
each_hash(result).map do |field|
field_name = set_field_encoding(field[:Field])
sql_type = field[:Type]
- cast_type = lookup_cast_type(sql_type)
- new_column(field_name, field[:Default], cast_type, sql_type, field[:Null] == "YES", field[:Collation], field[:Extra])
+ type_metadata = fetch_type_metadata(sql_type, field[:Collation], field[:Extra])
+ new_column(field_name, field[:Default], type_metadata, field[:Null] == "YES")
end
end
end
@@ -502,7 +530,7 @@ module ActiveRecord
end
def drop_table(table_name, options = {})
- execute "DROP#{' TEMPORARY' if options[:temporary]} TABLE #{quote_table_name(table_name)}#{' CASCADE' if options[:force] == :cascade}"
+ execute "DROP#{' TEMPORARY' if options[:temporary]} TABLE#{' IF EXISTS' if options[:if_exists]} #{quote_table_name(table_name)}#{' CASCADE' if options[:force] == :cascade}"
end
def rename_index(table_name, old_name, new_name)
@@ -599,13 +627,6 @@ module ActiveRecord
when 0x1000000..0xffffffff; 'longtext'
else raise(ActiveRecordError, "No text type has character length #{limit}")
end
- when 'datetime'
- return super unless precision
-
- case precision
- when 0..6; "datetime(#{precision})"
- else raise(ActiveRecordError, "No datetime type has precision of #{precision}. The allowed range of precision is from 0 to 6.")
- end
else
super
end
@@ -694,11 +715,6 @@ module ActiveRecord
m.alias_type %r(year)i, 'integer'
m.alias_type %r(bit)i, 'binary'
- m.register_type(%r(datetime)i) do |sql_type|
- precision = extract_precision(sql_type)
- MysqlDateTime.new(precision: precision)
- end
-
m.register_type(%r(enum)i) do |sql_type|
limit = sql_type[/^enum\((.+)\)/i, 1]
.split(',').map{|enum| enum.strip.length - 2}.max
@@ -716,6 +732,18 @@ module ActiveRecord
end
end
+ def extract_precision(sql_type)
+ if /datetime/ === sql_type
+ super || 0
+ else
+ super
+ end
+ end
+
+ def fetch_type_metadata(sql_type, collation = "", extra = "")
+ MysqlTypeMetadata.new(super(sql_type), collation: collation, extra: extra, strict: strict_mode?)
+ 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)
@@ -790,7 +818,7 @@ module ActiveRecord
options = {
default: column.default,
null: column.null,
- auto_increment: column.extra == "auto_increment"
+ auto_increment: column.auto_increment?
}
current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'", 'SCHEMA')["Type"]
@@ -894,22 +922,6 @@ module ActiveRecord
TableDefinition.new(native_database_types, name, temporary, options, as)
end
- class MysqlDateTime < Type::DateTime # :nodoc:
- def type_cast_for_database(value)
- if value.acts_like?(:time) && value.respond_to?(:usec)
- result = super.to_s(:db)
- case precision
- when 1..6
- "#{result}.#{sprintf("%0#{precision}d", value.usec / 10 ** (6 - precision))}"
- else
- result
- end
- else
- super
- end
- end
- end
-
class MysqlString < Type::String # :nodoc:
def type_cast_for_database(value)
case value
@@ -929,6 +941,10 @@ module ActiveRecord
end
end
end
+
+ def type_classes_with_standard_constructor
+ super.merge(string: MysqlString)
+ 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 e74de60a83..fa5ed07b8a 100644
--- a/activerecord/lib/active_record/connection_adapters/column.rb
+++ b/activerecord/lib/active_record/connection_adapters/column.rb
@@ -12,29 +12,21 @@ module ActiveRecord
ISO_DATETIME = /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)(\.\d+)?\z/
end
- attr_reader :name, :cast_type, :null, :sql_type, :default, :default_function
+ attr_reader :name, :null, :sql_type_metadata, :default, :default_function
- delegate :type, :precision, :scale, :limit, :klass, :accessor,
- :text?, :number?, :binary?, :changed?,
- :type_cast_from_user, :type_cast_from_database, :type_cast_for_database,
- :type_cast_for_schema,
- to: :cast_type
+ delegate :precision, :scale, :limit, :type, :sql_type, to: :sql_type_metadata, allow_nil: true
# Instantiates a new column in the table.
#
# +name+ is the column's name, such as <tt>supplier_id</tt> in <tt>supplier_id int(11)</tt>.
# +default+ is the type-casted default value, such as +new+ in <tt>sales_stage varchar(20) default 'new'</tt>.
- # +cast_type+ is the object used for type casting and type information.
- # +sql_type+ is used to extract the column's length, if necessary. For example +60+ in
- # <tt>company_name varchar(60)</tt>.
- # It will be mapped to one of the standard Rails SQL types in the <tt>type</tt> attribute.
+ # +sql_type_metadata+ is various information about the type of the column
# +null+ determines if this column allows +NULL+ values.
- def initialize(name, default, cast_type, sql_type = nil, null = true, default_function = nil)
- @name = name
- @cast_type = cast_type
- @sql_type = sql_type
- @null = null
- @default = default
+ def initialize(name, default, sql_type_metadata = nil, null = true, default_function = nil)
+ @name = name
+ @sql_type_metadata = sql_type_metadata
+ @null = null
+ @default = default
@default_function = default_function
end
@@ -50,19 +42,9 @@ module ActiveRecord
Base.human_attribute_name(@name)
end
- def with_type(type)
- dup.tap do |clone|
- clone.instance_variable_set('@cast_type', type)
- end
- end
-
def ==(other)
- other.name == name &&
- other.default == default &&
- other.cast_type == cast_type &&
- other.sql_type == sql_type &&
- other.null == null &&
- other.default_function == default_function
+ other.is_a?(Column) &&
+ attributes_for_hash == other.attributes_for_hash
end
alias :eql? :==
@@ -70,16 +52,16 @@ module ActiveRecord
attributes_for_hash.hash
end
- private
+ protected
def attributes_for_hash
- [self.class, name, default, cast_type, sql_type, null, default_function]
+ [self.class, name, default, sql_type_metadata, null, default_function]
end
end
class NullColumn < Column
def initialize(name)
- super name, nil, Type::Value.new
+ super(name, nil)
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
index 75f244b3f3..fac6f81540 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
@@ -39,7 +39,7 @@ module ActiveRecord
MAX_INDEX_LENGTH_FOR_UTF8MB4 = 191
def initialize_schema_migrations_table
- if @config[:encoding] == 'utf8mb4'
+ if charset == 'utf8mb4'
ActiveRecord::SchemaMigration.create_table(MAX_INDEX_LENGTH_FOR_UTF8MB4)
else
ActiveRecord::SchemaMigration.create_table
diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
index 23d8389abb..16f50fc594 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
@@ -395,11 +395,9 @@ module ActiveRecord
def exec_stmt(sql, name, binds)
cache = {}
- type_casted_binds = binds.map { |col, val|
- [col, type_cast(val, col)]
- }
+ type_casted_binds = binds.map { |attr| type_cast(attr.value_for_database) }
- log(sql, name, type_casted_binds) do
+ log(sql, name, binds) do
if binds.empty?
stmt = @connection.prepare(sql)
else
@@ -410,7 +408,7 @@ module ActiveRecord
end
begin
- stmt.execute(*type_casted_binds.map { |_, val| val })
+ stmt.execute(*type_casted_binds)
rescue Mysql::Error => e
# Older versions of MySQL leave the prepared statement in a bad
# place when an error occurs. To support older MySQL versions, we
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb
index acb1278499..0eb4fb468c 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb
@@ -2,19 +2,9 @@ module ActiveRecord
module ConnectionAdapters
# PostgreSQL-specific extensions to column definitions in a table.
class PostgreSQLColumn < Column #:nodoc:
- attr_reader :array
+ delegate :array, :oid, :fmod, to: :sql_type_metadata
alias :array? :array
- def initialize(name, default, cast_type, sql_type = nil, null = true, default_function = nil)
- if sql_type =~ /\[\]$/
- @array = true
- sql_type = sql_type[0..sql_type.length - 3]
- else
- @array = false
- end
- super
- end
-
def serial?
default_function && default_function =~ /\Anextval\(.*\)\z/
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb
index d28a2b4fa0..92349e2f9b 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb
@@ -1,25 +1,19 @@
-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'
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb
index c203e6c604..2608a2abab 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb
@@ -3,7 +3,7 @@ module ActiveRecord
module PostgreSQL
module OID # :nodoc:
class Array < Type::Value # :nodoc:
- include Type::Mutable
+ include Type::Helpers::Mutable
# Loads pg_array_parser if available. String parsing can be
# performed quicker by a native extension, which will not create
@@ -18,7 +18,7 @@ module ActiveRecord
end
attr_reader :subtype, :delimiter
- delegate :type, to: :subtype
+ delegate :type, :user_input_in_time_zone, to: :subtype
def initialize(subtype, delimiter = ',')
@subtype = subtype
@@ -48,6 +48,12 @@ module ActiveRecord
end
end
+ def ==(other)
+ other.is_a?(Array) &&
+ subtype == other.subtype &&
+ delimiter == other.delimiter
+ end
+
private
def type_cast_array(value, method)
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb
deleted file mode 100644
index 1d8d264530..0000000000
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-module ActiveRecord
- module ConnectionAdapters
- module PostgreSQL
- module OID # :nodoc:
- class Date < Type::Date # :nodoc:
- include Infinity
- end
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb
index b9e7894e5c..2c04c46131 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb
@@ -3,8 +3,6 @@ module ActiveRecord
module PostgreSQL
module OID # :nodoc:
class DateTime < Type::DateTime # :nodoc:
- include Infinity
-
def cast_value(value)
if value.is_a?(::String)
case value
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/float.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/float.rb
deleted file mode 100644
index 78ef94b912..0000000000
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/float.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-module ActiveRecord
- module ConnectionAdapters
- module PostgreSQL
- module OID # :nodoc:
- class Float < Type::Float # :nodoc:
- include Infinity
-
- def cast_value(value)
- case value
- when ::Float then value
- when 'Infinity' then ::Float::INFINITY
- when '-Infinity' then -::Float::INFINITY
- when 'NaN' then ::Float::NAN
- else value.to_f
- end
- end
- end
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb
index be4525c94f..b46e50c865 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb
@@ -3,7 +3,7 @@ module ActiveRecord
module PostgreSQL
module OID # :nodoc:
class Hstore < Type::Value # :nodoc:
- include Type::Mutable
+ include Type::Helpers::Mutable
def type
:hstore
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/infinity.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/infinity.rb
deleted file mode 100644
index e47780399a..0000000000
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/infinity.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-module ActiveRecord
- module ConnectionAdapters
- module PostgreSQL
- module OID # :nodoc:
- module Infinity # :nodoc:
- def infinity(options = {})
- options[:negative] ? -::Float::INFINITY : ::Float::INFINITY
- end
- end
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/integer.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/integer.rb
deleted file mode 100644
index 59abdc0009..0000000000
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/integer.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-module ActiveRecord
- module ConnectionAdapters
- module PostgreSQL
- module OID # :nodoc:
- class Integer < Type::Integer # :nodoc:
- include Infinity
- end
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb
index e12ddd9901..13dd037314 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb
@@ -3,7 +3,7 @@ module ActiveRecord
module PostgreSQL
module OID # :nodoc:
class Json < Type::Value # :nodoc:
- include Type::Mutable
+ include Type::Helpers::Mutable
def type
:json
@@ -11,7 +11,7 @@ module ActiveRecord
def type_cast_from_database(value)
if value.is_a?(::String)
- ::ActiveSupport::JSON.decode(value)
+ ::ActiveSupport::JSON.decode(value) rescue nil
else
super
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb
index df890c2ed6..2163674019 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb
@@ -3,8 +3,6 @@ module ActiveRecord
module PostgreSQL
module OID # :nodoc:
class Money < Type::Decimal # :nodoc:
- include Infinity
-
class_attribute :precision
def type
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb
index bac8b01d6b..4084725ed7 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb
@@ -3,7 +3,7 @@ module ActiveRecord
module PostgreSQL
module OID # :nodoc:
class Point < Type::Value # :nodoc:
- include Type::Mutable
+ include Type::Helpers::Mutable
def type
:point
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb
index 3adfb8b9d8..9d3633d109 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb
@@ -7,7 +7,7 @@ module ActiveRecord
class Range < Type::Value # :nodoc:
attr_reader :subtype, :type
- def initialize(subtype, type)
+ def initialize(subtype, type = :range)
@subtype = subtype
@type = type
end
@@ -40,6 +40,12 @@ module ActiveRecord
end
end
+ def ==(other)
+ other.is_a?(Range) &&
+ other.subtype == subtype &&
+ other.type == type
+ end
+
private
def type_cast_single(value)
@@ -53,13 +59,23 @@ module ActiveRecord
def extract_bounds(value)
from, to = value[1..-2].split(',')
{
- from: (value[1] == ',' || from == '-infinity') ? @subtype.infinity(negative: true) : from,
- to: (value[-2] == ',' || to == 'infinity') ? @subtype.infinity : to,
+ from: (value[1] == ',' || from == '-infinity') ? infinity(negative: true) : from,
+ to: (value[-2] == ',' || to == 'infinity') ? infinity : to,
exclude_start: (value[0] == '('),
exclude_end: (value[-1] == ')')
}
end
+ def infinity(negative: false)
+ if subtype.respond_to?(:infinity)
+ subtype.infinity(negative: negative)
+ elsif negative
+ -::Float::INFINITY
+ else
+ ::Float::INFINITY
+ end
+ end
+
def infinity?(value)
value.respond_to?(:infinite?) && value.infinite?
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb
index b2a42e9ebb..2d2fede4e8 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb
@@ -8,10 +8,6 @@ module ActiveRecord
def initialize(type)
@type = type
end
-
- def text?
- false
- 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
deleted file mode 100644
index 8f0246eddb..0000000000
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/time.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-module ActiveRecord
- module ConnectionAdapters
- module PostgreSQL
- module OID # :nodoc:
- class Time < Type::Time # :nodoc:
- include Infinity
- end
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
index 9de9e2c7dc..b8d0e26f85 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
@@ -52,12 +52,35 @@ module ActiveRecord
end
# Does not quote function default values for UUID columns
- def quote_default_value(value, column) #:nodoc:
+ def quote_default_expression(value, column) #:nodoc:
if column.type == :uuid && value =~ /\(\)/
value
- else
- value = column.cast_type.type_cast_for_database(value)
+ elsif column.respond_to?(:array?)
+ value = type_cast_from_column(column, value)
quote(value)
+ else
+ super
+ end
+ end
+
+ def lookup_cast_type_from_column(column) # :nodoc:
+ type_map.lookup(column.oid, column.fmod, column.sql_type)
+ end
+
+ def type_for_attribute_options(
+ type_name,
+ array: false,
+ range: false,
+ **options
+ )
+ if array
+ subtype = type_for_attribute_options(type_name, **options)
+ OID::Array.new(subtype)
+ elsif range
+ subtype = type_for_attribute_options(type_name, **options)
+ OID::Range.new(subtype)
+ else
+ super(type_name, **options)
end
end
@@ -99,6 +122,27 @@ module ActiveRecord
super
end
end
+
+ def type_classes_with_standard_constructor
+ super.merge(
+ bit: OID::Bit,
+ bit_varying: OID::BitVarying,
+ binary: OID::Bytea,
+ cidr: OID::Cidr,
+ date_time: OID::DateTime,
+ decimal: OID::Decimal,
+ enum: OID::Enum,
+ hstore: OID::Hstore,
+ inet: OID::Inet,
+ json: OID::Json,
+ jsonb: OID::Jsonb,
+ money: OID::Money,
+ point: OID::Point,
+ uuid: OID::Uuid,
+ vector: OID::Vector,
+ xml: OID::Xml,
+ )
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb
index a9522e152f..b9078d4c86 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb
@@ -76,15 +76,15 @@ module ActiveRecord
column(name, :point, options)
end
- def bit(name, options)
+ def bit(name, options = {})
column(name, :bit, options)
end
- def bit_varying(name, options)
+ def bit_varying(name, options = {})
column(name, :bit_varying, options)
end
- def money(name, options)
+ def money(name, options = {})
column(name, :money, options)
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 a90adcf4aa..503dda60f4 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
@@ -4,34 +4,11 @@ module ActiveRecord
class SchemaCreation < AbstractAdapter::SchemaCreation
private
- def column_options(o)
- column_options = super
- column_options[:array] = o.array
- column_options
- end
-
- def add_column_options!(sql, options)
- if options[:array]
- sql << '[]'
- end
+ def visit_ColumnDefinition(o)
+ o.sql_type = type_to_sql(o.type, o.limit, o.precision, o.scale)
+ o.sql_type << '[]' if o.array
super
end
-
- def quote_default_expression(value, column)
- if column.type == :uuid && value =~ /\(\)/
- value
- else
- super
- end
- end
-
- def type_for_column(column)
- if column.array
- @conn.lookup_cast_type("#{column.sql_type}[]")
- else
- super
- end
- end
end
module SchemaStatements
@@ -112,7 +89,7 @@ module ActiveRecord
end
def drop_table(table_name, options = {})
- execute "DROP TABLE #{quote_table_name(table_name)}#{' CASCADE' if options[:force] == :cascade}"
+ execute "DROP TABLE#{' IF EXISTS' if options[:if_exists]} #{quote_table_name(table_name)}#{' CASCADE' if options[:force] == :cascade}"
end
# Returns true if schema exists.
@@ -183,15 +160,17 @@ 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, type)
- default_value = extract_value_from_default(oid, default)
+ oid = oid.to_i
+ fmod = fmod.to_i
+ type_metadata = fetch_type_metadata(column_name, type, oid, fmod)
+ default_value = extract_value_from_default(default)
default_function = extract_default_function(default_value, default)
- new_column(column_name, default_value, oid, type, notnull == 'f', default_function)
+ new_column(column_name, default_value, type_metadata, notnull == 'f', default_function)
end
end
- def new_column(name, default, cast_type, sql_type = nil, null = true, default_function = nil) # :nodoc:
- PostgreSQLColumn.new(name, default, cast_type, sql_type, null, default_function)
+ def new_column(name, default, sql_type_metadata = nil, null = true, default_function = nil) # :nodoc:
+ PostgreSQLColumn.new(name, default, sql_type_metadata, null, default_function)
end
# Returns the current database name.
@@ -453,7 +432,7 @@ module ActiveRecord
# cast the default to the columns type, which leaves us with a default like "default NULL::character varying".
execute alter_column_query % "DROP DEFAULT"
else
- execute alter_column_query % "SET DEFAULT #{quote_default_value(default, column)}"
+ execute alter_column_query % "SET DEFAULT #{quote_default_expression(default, column)}"
end
end
@@ -461,7 +440,7 @@ module ActiveRecord
clear_cache!
unless null || default.nil?
column = column_for(table_name, column_name)
- execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote_default_value(default, column)} WHERE #{quote_column_name(column_name)} IS NULL") if column
+ execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote_default_expression(default, column)} WHERE #{quote_column_name(column_name)} IS NULL") if column
end
execute("ALTER TABLE #{quote_table_name(table_name)} ALTER #{quote_column_name(column_name)} #{null ? 'DROP' : 'SET'} NOT NULL")
end
@@ -555,13 +534,6 @@ module ActiveRecord
when 5..8; 'bigint'
else raise(ActiveRecordError, "No integer type has byte size #{limit}. Use a numeric with precision 0 instead.")
end
- when 'datetime'
- return super unless precision
-
- case precision
- when 0..6; "timestamp(#{precision})"
- else raise(ActiveRecordError, "No timestamp type has precision of #{precision}. The allowed range of precision is from 0 to 6")
- end
else
super
end
@@ -580,6 +552,18 @@ module ActiveRecord
[super, *order_columns].join(', ')
end
+
+ def fetch_type_metadata(column_name, sql_type, oid, fmod)
+ cast_type = get_oid_type(oid, fmod, column_name, sql_type)
+ simple_type = SqlTypeMetadata.new(
+ sql_type: sql_type,
+ type: cast_type.type,
+ limit: cast_type.limit,
+ precision: cast_type.precision,
+ scale: cast_type.scale,
+ )
+ PostgreSQLTypeMetadata.new(simple_type, oid: oid, fmod: fmod)
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb b/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb
new file mode 100644
index 0000000000..58715978f7
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb
@@ -0,0 +1,35 @@
+module ActiveRecord
+ module ConnectionAdapters
+ class PostgreSQLTypeMetadata < DelegateClass(SqlTypeMetadata)
+ attr_reader :oid, :fmod, :array
+
+ def initialize(type_metadata, oid: nil, fmod: nil)
+ super(type_metadata)
+ @type_metadata = type_metadata
+ @oid = oid
+ @fmod = fmod
+ @array = /\[\]$/ === type_metadata.sql_type
+ end
+
+ def sql_type
+ super.gsub(/\[\]$/, "")
+ end
+
+ def ==(other)
+ other.is_a?(PostgreSQLTypeMetadata) &&
+ attributes_for_hash == other.attributes_for_hash
+ end
+ alias eql? ==
+
+ def hash
+ attributes_for_hash.hash
+ end
+
+ protected
+
+ def attributes_for_hash
+ [self.class, @type_metadata, oid, fmod]
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
index f4f9747359..6d7e1075d7 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
@@ -1,14 +1,14 @@
-require 'active_record/connection_adapters/abstract_adapter'
-require 'active_record/connection_adapters/statement_pool'
-
-require 'active_record/connection_adapters/postgresql/utils'
-require 'active_record/connection_adapters/postgresql/column'
-require 'active_record/connection_adapters/postgresql/oid'
-require 'active_record/connection_adapters/postgresql/quoting'
-require 'active_record/connection_adapters/postgresql/referential_integrity'
-require 'active_record/connection_adapters/postgresql/schema_definitions'
-require 'active_record/connection_adapters/postgresql/schema_statements'
-require 'active_record/connection_adapters/postgresql/database_statements'
+require "active_record/connection_adapters/abstract_adapter"
+require "active_record/connection_adapters/postgresql/column"
+require "active_record/connection_adapters/postgresql/database_statements"
+require "active_record/connection_adapters/postgresql/oid"
+require "active_record/connection_adapters/postgresql/quoting"
+require "active_record/connection_adapters/postgresql/referential_integrity"
+require "active_record/connection_adapters/postgresql/schema_definitions"
+require "active_record/connection_adapters/postgresql/schema_statements"
+require "active_record/connection_adapters/postgresql/type_metadata"
+require "active_record/connection_adapters/postgresql/utils"
+require "active_record/connection_adapters/statement_pool"
require 'arel/visitors/bind_visitor'
@@ -180,6 +180,10 @@ module ActiveRecord
true
end
+ def supports_datetime_with_precision?
+ true
+ end
+
def index_algorithms
{ concurrently: 'CONCURRENTLY' }
end
@@ -255,6 +259,8 @@ module ActiveRecord
@table_alias_length = nil
connect
+ add_pg_decoders
+
@statements = StatementPool.new @connection,
self.class.type_cast_config_to_integer(config.fetch(:statement_limit) { 1000 })
@@ -459,11 +465,11 @@ module ActiveRecord
end
def initialize_type_map(m) # :nodoc:
- register_class_with_limit m, 'int2', OID::Integer
- register_class_with_limit m, 'int4', OID::Integer
- register_class_with_limit m, 'int8', OID::Integer
+ register_class_with_limit m, 'int2', Type::Integer
+ register_class_with_limit m, 'int4', Type::Integer
+ register_class_with_limit m, 'int8', Type::Integer
m.alias_type 'oid', 'int2'
- m.register_type 'float4', OID::Float.new
+ m.register_type 'float4', Type::Float.new
m.alias_type 'float8', 'float4'
m.register_type 'text', Type::Text.new
register_class_with_limit m, 'varchar', Type::String
@@ -474,8 +480,7 @@ module ActiveRecord
register_class_with_limit m, 'bit', OID::Bit
register_class_with_limit m, 'varbit', OID::BitVarying
m.alias_type 'timestamptz', 'timestamp'
- m.register_type 'date', OID::Date.new
- m.register_type 'time', OID::Time.new
+ m.register_type 'date', Type::Date.new
m.register_type 'money', OID::Money.new
m.register_type 'bytea', OID::Bytea.new
@@ -501,10 +506,8 @@ module ActiveRecord
m.alias_type 'lseg', 'varchar'
m.alias_type 'box', 'varchar'
- m.register_type 'timestamp' do |_, _, sql_type|
- precision = extract_precision(sql_type)
- OID::DateTime.new(precision: precision)
- end
+ register_class_with_precision m, 'time', Type::Time
+ register_class_with_precision m, 'timestamp', OID::DateTime
m.register_type 'numeric' do |_, fmod, sql_type|
precision = extract_precision(sql_type)
@@ -541,7 +544,7 @@ module ActiveRecord
end
# Extracts the value from a PostgreSQL column default definition.
- def extract_value_from_default(oid, default) # :nodoc:
+ def extract_value_from_default(default) # :nodoc:
case default
# Quoted types
when /\A[\(B]?'(.*)'::/m
@@ -609,12 +612,10 @@ module ActiveRecord
def exec_cache(sql, name, binds)
stmt_key = prepare_statement(sql)
- type_casted_binds = binds.map { |col, val|
- [col, type_cast(val, col)]
- }
+ type_casted_binds = binds.map { |attr| type_cast(attr.value_for_database) }
- log(sql, name, type_casted_binds, stmt_key) do
- @connection.exec_prepared(stmt_key, type_casted_binds.map { |_, val| val })
+ log(sql, name, binds, stmt_key) do
+ @connection.exec_prepared(stmt_key, type_casted_binds)
end
rescue ActiveRecord::StatementInvalid => e
pgerror = e.original_exception
@@ -764,6 +765,54 @@ module ActiveRecord
def create_table_definition(name, temporary = false, options = nil, as = nil) # :nodoc:
PostgreSQL::TableDefinition.new native_database_types, name, temporary, options, as
end
+
+ def can_perform_case_insensitive_comparison_for?(column)
+ @case_insensitive_cache ||= {}
+ @case_insensitive_cache[column.sql_type] ||= begin
+ sql = <<-end_sql
+ SELECT exists(
+ SELECT * FROM pg_proc
+ INNER JOIN pg_cast
+ ON casttarget::text::oidvector = proargtypes
+ WHERE proname = 'lower'
+ AND castsource = '#{column.sql_type}'::regtype::oid
+ )
+ end_sql
+ execute_and_clear(sql, "SCHEMA", []) do |result|
+ result.getvalue(0, 0) == 't'
+ end
+ end
+ end
+
+ def add_pg_decoders
+ coders_by_name = {
+ 'int2' => PG::TextDecoder::Integer,
+ 'int4' => PG::TextDecoder::Integer,
+ 'int8' => PG::TextDecoder::Integer,
+ 'oid' => PG::TextDecoder::Integer,
+ 'float4' => PG::TextDecoder::Float,
+ 'float8' => PG::TextDecoder::Float,
+ 'bool' => PG::TextDecoder::Boolean,
+ }
+ query = <<-SQL
+ SELECT t.oid, t.typname, t.typelem, t.typdelim, t.typinput, t.typtype, t.typbasetype
+ FROM pg_type as t
+ SQL
+ coders = execute_and_clear(query, "SCHEMA", []) do |result|
+ result
+ .map { |row| construct_coder(row, coders_by_name['typname']) }
+ .compact
+ end
+
+ map = PG::TypeMapByOid.new
+ coders.each { |coder| map.add_coder(coder) }
+ @connection.type_map_for_results = map
+ end
+
+ def construct_coder(row, coder_class)
+ return unless coder_class
+ coder_class.new(oid: row['oid'], name: row['typname'])
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/sql_type_metadata.rb b/activerecord/lib/active_record/connection_adapters/sql_type_metadata.rb
new file mode 100644
index 0000000000..ccb7e154ee
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/sql_type_metadata.rb
@@ -0,0 +1,32 @@
+module ActiveRecord
+ # :stopdoc:
+ module ConnectionAdapters
+ class SqlTypeMetadata
+ attr_reader :sql_type, :type, :limit, :precision, :scale
+
+ def initialize(sql_type: nil, type: nil, limit: nil, precision: nil, scale: nil)
+ @sql_type = sql_type
+ @type = type
+ @limit = limit
+ @precision = precision
+ @scale = scale
+ end
+
+ def ==(other)
+ other.is_a?(SqlTypeMetadata) &&
+ attributes_for_hash == other.attributes_for_hash
+ end
+ alias eql? ==
+
+ def hash
+ attributes_for_hash.hash
+ end
+
+ protected
+
+ def attributes_for_hash
+ [self.class, sql_type, type, limit, precision, scale]
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
index 03dfd29a0a..400b586c95 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
@@ -41,25 +41,6 @@ module ActiveRecord
end
module ConnectionAdapters #:nodoc:
- class SQLite3Binary < Type::Binary # :nodoc:
- def cast_value(value)
- if value.encoding != Encoding::ASCII_8BIT
- value = value.force_encoding(Encoding::ASCII_8BIT)
- end
- value
- end
- end
-
- class SQLite3String < Type::String # :nodoc:
- def type_cast_for_database(value)
- if value.is_a?(::String) && value.encoding == Encoding::ASCII_8BIT
- value.encode(Encoding::UTF_8)
- else
- super
- end
- end
- end
-
# The SQLite3 adapter works SQLite 3.6.16 or newer
# with the sqlite3-ruby drivers (available as gem from https://rubygems.org/gems/sqlite3).
#
@@ -239,6 +220,12 @@ module ActiveRecord
case value
when BigDecimal
value.to_f
+ when String
+ if value.encoding == Encoding::ASCII_8BIT
+ super(value.encode(Encoding::UTF_8))
+ else
+ super
+ end
else
super
end
@@ -280,11 +267,9 @@ module ActiveRecord
end
def exec_query(sql, name = nil, binds = [])
- type_casted_binds = binds.map { |col, val|
- [col, type_cast(val, col)]
- }
+ type_casted_binds = binds.map { |attr| type_cast(attr.value_for_database) }
- log(sql, name, type_casted_binds) do
+ log(sql, name, binds) do
# Don't cache statements if they are not prepared
if without_prepared_statement?(binds)
stmt = @connection.prepare(sql)
@@ -302,7 +287,7 @@ module ActiveRecord
stmt = cache[:stmt]
cols = cache[:cols] ||= stmt.columns
stmt.reset!
- stmt.bind_params type_casted_binds.map { |_, val| val }
+ stmt.bind_params type_casted_binds
end
ActiveRecord::Result.new(cols, stmt.to_a)
@@ -387,8 +372,8 @@ module ActiveRecord
end
sql_type = field['type']
- cast_type = lookup_cast_type(sql_type)
- new_column(field['name'], field['dflt_value'], cast_type, sql_type, field['notnull'].to_i == 0)
+ type_metadata = fetch_type_metadata(sql_type)
+ new_column(field['name'], field['dflt_value'], type_metadata, field['notnull'].to_i == 0)
end
end
@@ -495,12 +480,6 @@ module ActiveRecord
protected
- def initialize_type_map(m)
- super
- m.register_type(/binary/i, SQLite3Binary.new)
- register_class_with_limit m, %r(char)i, SQLite3String
- end
-
def table_structure(table_name)
structure = exec_query("PRAGMA table_info(#{quote_table_name(table_name)})", 'SCHEMA').to_hash
raise(ActiveRecord::StatementInvalid, "Could not find table '#{table_name}'") if structure.empty?
diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb
index a7aff9f724..44d587206d 100644
--- a/activerecord/lib/active_record/core.rb
+++ b/activerecord/lib/active_record/core.rb
@@ -107,20 +107,18 @@ module ActiveRecord
super
end
- def initialize_find_by_cache
+ def initialize_find_by_cache # :nodoc:
self.find_by_statement_cache = {}.extend(Mutex_m)
end
- def inherited(child_class)
+ def inherited(child_class) # :nodoc:
child_class.initialize_find_by_cache
super
end
- def find(*ids)
+ def find(*ids) # :nodoc:
# We don't have cache keys for this stuff yet
return super unless ids.length == 1
- # Allow symbols to super to maintain compatibility for deprecated finders until Rails 5
- return super if ids.first.kind_of?(Symbol)
return super if block_given? ||
primary_key.nil? ||
default_scopes.any? ||
@@ -152,7 +150,7 @@ module ActiveRecord
raise RecordNotFound, "Couldn't find #{name} with an out of range value for '#{primary_key}'"
end
- def find_by(*args)
+ def find_by(*args) # :nodoc:
return super if current_scope || !(Hash === args.first) || reflect_on_all_aggregations.any?
return super if default_scopes.any?
@@ -185,11 +183,11 @@ module ActiveRecord
end
end
- def find_by!(*args)
+ def find_by!(*args) # :nodoc:
find_by(*args) or raise RecordNotFound.new("Couldn't find #{name}")
end
- def initialize_generated_modules
+ def initialize_generated_modules # :nodoc:
generated_association_methods
end
@@ -210,7 +208,7 @@ module ActiveRecord
elsif !connected?
"#{super} (call '#{super}.connection' to establish a connection)"
elsif table_exists?
- attr_list = columns.map { |c| "#{c.name}: #{c.type}" } * ', '
+ attr_list = attribute_types.map { |name, type| "#{name}: #{type.type}" } * ', '
"#{super}(#{attr_list})"
else
"#{super}(Table doesn't exist)"
@@ -456,6 +454,7 @@ module ActiveRecord
# 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)
+ return super if custom_inspect_method_defined?
pp.object_address_group(self) do
if defined?(@attributes) && @attributes
column_names = self.class.column_names.select { |name| has_attribute?(name) || new_record? }
@@ -481,16 +480,16 @@ module ActiveRecord
Hash[methods.map! { |method| [method, public_send(method)] }].with_indifferent_access
end
+ private
+
def set_transaction_state(state) # :nodoc:
@transaction_state = state
end
def has_transactional_callbacks? # :nodoc:
- !_rollback_callbacks.empty? || !_commit_callbacks.empty? || !_create_callbacks.empty?
+ !_rollback_callbacks.empty? || !_commit_callbacks.empty?
end
- private
-
# Updates the attributes on this particular ActiveRecord object so that
# if it is associated with a transaction, then the state of the AR object
# will be updated to reflect the current state of the transaction
@@ -513,6 +512,8 @@ module ActiveRecord
end
def update_attributes_from_transaction_state(transaction_state, depth)
+ @reflects_state = [false] if depth == 0
+
if transaction_state && transaction_state.finalized? && !has_transactional_callbacks?
unless @reflects_state[depth]
restore_transaction_record_state if transaction_state.rolledback?
@@ -549,7 +550,6 @@ module ActiveRecord
@txn = nil
@_start_transaction_state = {}
@transaction_state = nil
- @reflects_state = [false]
end
def initialize_internals_callback
@@ -560,5 +560,9 @@ module ActiveRecord
@attributes = @attributes.dup
end
end
+
+ def custom_inspect_method_defined?
+ self.class.instance_method(:inspect).owner != ActiveRecord::Base.instance_method(:inspect).owner
+ end
end
end
diff --git a/activerecord/lib/active_record/counter_cache.rb b/activerecord/lib/active_record/counter_cache.rb
index 7d8e0a2063..82596b63df 100644
--- a/activerecord/lib/active_record/counter_cache.rb
+++ b/activerecord/lib/active_record/counter_cache.rb
@@ -156,7 +156,7 @@ module ActiveRecord
def each_counter_cached_associations
_reflections.each do |name, reflection|
- yield association(name) if reflection.belongs_to? && reflection.counter_cache_column
+ yield association(name.to_sym) if reflection.belongs_to? && reflection.counter_cache_column
end
end
diff --git a/activerecord/lib/active_record/enum.rb b/activerecord/lib/active_record/enum.rb
index f053372cfb..470e0b5d29 100644
--- a/activerecord/lib/active_record/enum.rb
+++ b/activerecord/lib/active_record/enum.rb
@@ -32,6 +32,12 @@ module ActiveRecord
# Conversation.active
# Conversation.archived
#
+ # Of course, you can also query them directly if the scopes doesn't fit your
+ # needs:
+ #
+ # Conversation.where(status: [:active, :archived])
+ # Conversation.where.not(status: :active)
+ #
# You can set the default value from the database declaration, like:
#
# create_table :conversations do |t|
@@ -59,15 +65,17 @@ module ActiveRecord
#
# In rare circumstances you might need to access the mapping directly.
# The mappings are exposed through a class method with the pluralized attribute
- # name:
+ # name, which return the mapping in a +HashWithIndifferentAccess+:
#
- # Conversation.statuses # => { "active" => 0, "archived" => 1 }
+ # Conversation.statuses[:active] # => 0
+ # Conversation.statuses["archived"] # => 1
#
- # Use that class method when you need to know the ordinal value of an enum:
+ # Use that class method when you need to know the ordinal value of an enum.
+ # For example, you can use that when manually building SQL strings:
#
# Conversation.where("status <> ?", Conversation.statuses[:archived])
#
- # Where conditions on an enum attribute must use the ordinal value of an enum.
+
module Enum
def self.extended(base) # :nodoc:
base.class_attribute(:defined_enums)
@@ -79,6 +87,38 @@ module ActiveRecord
super
end
+ class EnumType < Type::Value
+ def initialize(name, mapping)
+ @name = name
+ @mapping = mapping
+ end
+
+ def type_cast_from_user(value)
+ return if value.blank?
+
+ if mapping.has_key?(value)
+ value.to_s
+ elsif mapping.has_value?(value)
+ mapping.key(value)
+ else
+ raise ArgumentError, "'#{value}' is not a valid #{name}"
+ end
+ end
+
+ def type_cast_from_database(value)
+ return if value.nil?
+ mapping.key(value.to_i)
+ end
+
+ def type_cast_for_database(value)
+ mapping.fetch(value, value)
+ end
+
+ protected
+
+ attr_reader :name, :mapping
+ end
+
def enum(definitions)
klass = self
definitions.each do |name, values|
@@ -90,37 +130,19 @@ module ActiveRecord
detect_enum_conflict!(name, name.to_s.pluralize, true)
klass.singleton_class.send(:define_method, name.to_s.pluralize) { enum_values }
- _enum_methods_module.module_eval do
- # def status=(value) self[:status] = statuses[value] end
- klass.send(:detect_enum_conflict!, name, "#{name}=")
- define_method("#{name}=") { |value|
- if enum_values.has_key?(value) || value.blank?
- self[name] = enum_values[value]
- elsif enum_values.has_value?(value)
- # Assigning a value directly is not a end-user feature, hence it's not documented.
- # This is used internally to make building objects from the generated scopes work
- # as expected, i.e. +Conversation.archived.build.archived?+ should be true.
- self[name] = value
- else
- raise ArgumentError, "'#{value}' is not a valid #{name}"
- end
- }
-
- # def status() statuses.key self[:status] end
- klass.send(:detect_enum_conflict!, name, name)
- define_method(name) { enum_values.key self[name] }
+ detect_enum_conflict!(name, name)
+ detect_enum_conflict!(name, "#{name}=")
- # def status_before_type_cast() statuses.key self[:status] end
- klass.send(:detect_enum_conflict!, name, "#{name}_before_type_cast")
- define_method("#{name}_before_type_cast") { enum_values.key self[name] }
+ attribute name, EnumType.new(name, enum_values)
+ _enum_methods_module.module_eval do
pairs = values.respond_to?(:each_pair) ? values.each_pair : values.each_with_index
pairs.each do |value, i|
enum_values[value] = i
# def active?() status == 0 end
klass.send(:detect_enum_conflict!, name, "#{value}?")
- define_method("#{value}?") { self[name] == i }
+ define_method("#{value}?") { self[name] == value.to_s }
# def active!() update! status: :active end
klass.send(:detect_enum_conflict!, name, "#{value}!")
@@ -128,7 +150,7 @@ module ActiveRecord
# scope :active, -> { where status: 0 }
klass.send(:detect_enum_conflict!, name, value, true)
- klass.scope value, -> { klass.where name => i }
+ klass.scope value, -> { klass.where name => value }
end
end
defined_enums[name.to_s] = enum_values
@@ -138,25 +160,7 @@ module ActiveRecord
private
def _enum_methods_module
@_enum_methods_module ||= begin
- mod = Module.new do
- private
- def save_changed_attribute(attr_name, old)
- if (mapping = self.class.defined_enums[attr_name.to_s])
- value = _read_attribute(attr_name)
- if attribute_changed?(attr_name)
- if mapping[old] == value
- clear_attribute_changes([attr_name])
- end
- else
- if old != value
- set_attribute_was(attr_name, mapping.key(old))
- end
- end
- else
- super
- end
- end
- end
+ mod = Module.new
include mod
mod
end
diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb
index fc28ab585f..d710d96a9a 100644
--- a/activerecord/lib/active_record/errors.rb
+++ b/activerecord/lib/active_record/errors.rb
@@ -178,18 +178,10 @@ module ActiveRecord
class DangerousAttributeError < ActiveRecordError
end
- # Raised when unknown attributes are supplied via mass assignment.
- class UnknownAttributeError < NoMethodError
-
- attr_reader :record, :attribute
-
- def initialize(record, attribute)
- @record = record
- @attribute = attribute.to_s
- super("unknown attribute '#{attribute}' for #{@record.class}.")
- end
-
- end
+ UnknownAttributeError = ActiveSupport::Deprecation::DeprecatedConstantProxy.new( # :nodoc:
+ 'ActiveRecord::UnknownAttributeError',
+ 'ActiveModel::AttributeAssignment::UnknownAttributeError'
+ )
# Raised when an error occurred while doing a mass assignment to an attribute through the
# +attributes=+ method. The exception has an +attribute+ property that is the name of the
diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb
index 10e9be20b5..739be524da 100644
--- a/activerecord/lib/active_record/fixtures.rb
+++ b/activerecord/lib/active_record/fixtures.rb
@@ -1,6 +1,7 @@
require 'erb'
require 'yaml'
require 'zlib'
+require 'set'
require 'active_support/dependencies'
require 'active_support/core_ext/digest/uuid'
require 'active_record/fixture_set/file'
@@ -131,7 +132,7 @@ module ActiveRecord
# Digest::SHA2.hexdigest(File.read(Rails.root.join('test/fixtures', path)))
# end
# end
- # ActiveRecord::FixtureSet.context_class.send :include, FixtureFileHelpers
+ # ActiveRecord::FixtureSet.context_class.include FixtureFileHelpers
#
# - use the helper method in a fixture
# photo:
@@ -521,12 +522,16 @@ module ActiveRecord
update_all_loaded_fixtures fixtures_map
connection.transaction(:requires_new => true) do
+ deleted_tables = Set.new
fixture_sets.each do |fs|
conn = fs.model_class.respond_to?(:connection) ? fs.model_class.connection : connection
table_rows = fs.table_rows
table_rows.each_key do |table|
- conn.delete "DELETE FROM #{conn.quote_table_name(table)}", 'Fixture Delete'
+ unless deleted_tables.include? table
+ conn.delete "DELETE FROM #{conn.quote_table_name(table)}", 'Fixture Delete'
+ end
+ deleted_tables << table
end
table_rows.each do |fixture_set_name, rows|
@@ -661,7 +666,7 @@ module ActiveRecord
row[association.foreign_type] = $1
end
- fk_type = association.active_record.columns_hash[fk_name].type
+ fk_type = association.active_record.type_for_attribute(fk_name).type
row[fk_name] = ActiveRecord::FixtureSet.identify(value, fk_type)
end
when :has_many
@@ -691,7 +696,7 @@ module ActiveRecord
end
def primary_key_type
- @association.klass.column_types[@association.klass.primary_key].type
+ @association.klass.type_for_attribute(@association.klass.primary_key).type
end
end
@@ -711,7 +716,7 @@ module ActiveRecord
end
def primary_key_type
- @primary_key_type ||= model_class && model_class.column_types[model_class.primary_key].type
+ @primary_key_type ||= model_class && model_class.type_for_attribute(model_class.primary_key).type
end
def add_join_records(rows, row, association)
diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb
index fd1e22349b..24098f72dc 100644
--- a/activerecord/lib/active_record/inheritance.rb
+++ b/activerecord/lib/active_record/inheritance.rb
@@ -192,7 +192,7 @@ module ActiveRecord
# If this is a StrongParameters hash, and access to inheritance_column is not permitted,
# this will ignore the inheritance column and return nil
def subclass_from_attributes?(attrs)
- columns_hash.include?(inheritance_column) && attrs.is_a?(Hash)
+ attribute_names.include?(inheritance_column) && attrs.is_a?(Hash)
end
def subclass_from_attributes(attrs)
diff --git a/activerecord/lib/active_record/locale/en.yml b/activerecord/lib/active_record/locale/en.yml
index b1fbd38622..8a3c27e6da 100644
--- a/activerecord/lib/active_record/locale/en.yml
+++ b/activerecord/lib/active_record/locale/en.yml
@@ -7,6 +7,7 @@ en:
# Default error messages
errors:
messages:
+ required: "must exist"
taken: "has already been taken"
# Active Record models configuration
diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb
index 6f2b65c137..c5b10fcddf 100644
--- a/activerecord/lib/active_record/locking/optimistic.rb
+++ b/activerecord/lib/active_record/locking/optimistic.rb
@@ -144,7 +144,7 @@ module ActiveRecord
# Set the column to use for optimistic locking. Defaults to +lock_version+.
def locking_column=(value)
- clear_caches_calculated_from_columns
+ reload_schema_from_cache
@locking_column = value.to_s
end
@@ -184,7 +184,7 @@ module ActiveRecord
end
end
- class LockingType < SimpleDelegator # :nodoc:
+ class LockingType < DelegateClass(Type::Value) # :nodoc:
def type_cast_from_database(value)
# `nil` *should* be changed to 0
super.to_i
diff --git a/activerecord/lib/active_record/log_subscriber.rb b/activerecord/lib/active_record/log_subscriber.rb
index a5c7279db9..6b26d7be78 100644
--- a/activerecord/lib/active_record/log_subscriber.rb
+++ b/activerecord/lib/active_record/log_subscriber.rb
@@ -20,18 +20,14 @@ module ActiveRecord
@odd = false
end
- def render_bind(column, value)
- if column
- if column.binary? && value
- # This specifically deals with the PG adapter that casts bytea columns into a Hash.
- value = value[:value] if value.is_a?(Hash)
- value = "<#{value.bytesize} bytes of binary data>"
- end
-
- [column.name, value]
+ def render_bind(attribute)
+ value = if attribute.type.binary? && attribute.value
+ "<#{attribute.value.bytesize} bytes of binary data>"
else
- [nil, value]
+ attribute.value_for_database
end
+
+ [attribute.name, value]
end
def sql(event)
@@ -47,9 +43,7 @@ module ActiveRecord
binds = nil
unless (payload[:binds] || []).empty?
- binds = " " + payload[:binds].map { |col,v|
- render_bind(col, v)
- }.inspect
+ binds = " " + payload[:binds].map { |attr| render_bind(attr) }.inspect
end
if odd?
diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb
index 641512d323..75adcccce6 100644
--- a/activerecord/lib/active_record/model_schema.rb
+++ b/activerecord/lib/active_record/model_schema.rb
@@ -111,17 +111,6 @@ module ActiveRecord
# class Mouse < ActiveRecord::Base
# self.table_name = "mice"
# end
- #
- # Alternatively, you can override the table_name method to define your
- # own computation. (Possibly using <tt>super</tt> to manipulate the default
- # table name.) Example:
- #
- # class Post < ActiveRecord::Base
- # def self.table_name
- # "special_" + super
- # end
- # end
- # Post.table_name # => "special_posts"
def table_name
reset_table_name unless defined?(@table_name)
@table_name
@@ -132,9 +121,6 @@ module ActiveRecord
# class Project < ActiveRecord::Base
# self.table_name = "project"
# end
- #
- # You can also just define your own <tt>self.table_name</tt> method; see
- # the documentation for ActiveRecord::Base#table_name.
def table_name=(value)
value = value && value.to_s
@@ -231,28 +217,37 @@ module ActiveRecord
end
def attributes_builder # :nodoc:
- @attributes_builder ||= AttributeSet::Builder.new(column_types, primary_key)
+ @attributes_builder ||= AttributeSet::Builder.new(attribute_types, primary_key)
end
- def column_types # :nodoc:
- @column_types ||= columns_hash.transform_values(&:cast_type).tap do |h|
- h.default = Type::Value.new
- end
+ def columns_hash # :nodoc:
+ load_schema
+ @columns_hash
+ end
+
+ def columns
+ load_schema
+ @columns ||= columns_hash.values
+ end
+
+ def attribute_types # :nodoc:
+ load_schema
+ @attribute_types ||= Hash.new(Type::Value.new)
end
def type_for_attribute(attr_name) # :nodoc:
- column_types[attr_name]
+ attribute_types[attr_name]
end
# Returns a hash where the keys are column names and the values are
# default values when instantiating the AR object for this table.
def column_defaults
+ load_schema
_default_attributes.to_hash
end
def _default_attributes # :nodoc:
- @default_attributes ||= attributes_builder.build_from_database(
- raw_default_values)
+ @default_attributes ||= AttributeSet.new({})
end
# Returns an array of column names as strings.
@@ -295,19 +290,53 @@ module ActiveRecord
def reset_column_information
connection.clear_cache!
undefine_attribute_methods
- connection.schema_cache.clear_table_cache!(table_name) if table_exists?
+ connection.schema_cache.clear_table_cache!(table_name)
- @arel_engine = nil
- @arel_table = nil
- @column_names = nil
- @column_types = nil
- @content_columns = nil
- @default_attributes = nil
- @inheritance_column = nil unless defined?(@explicit_inheritance_column) && @explicit_inheritance_column
+ reload_schema_from_cache
end
private
+ def schema_loaded?
+ defined?(@columns_hash) && @columns_hash
+ end
+
+ def load_schema
+ unless schema_loaded?
+ load_schema!
+ end
+ end
+
+ def load_schema!
+ @columns_hash = connection.schema_cache.columns_hash(table_name)
+ @columns_hash.each do |name, column|
+ define_attribute(
+ name,
+ connection.lookup_cast_type_from_column(column),
+ default: column.default,
+ user_provided_default: false
+ )
+ end
+ end
+
+ def reload_schema_from_cache
+ @arel_engine = nil
+ @arel_table = nil
+ @column_names = nil
+ @attribute_types = nil
+ @content_columns = nil
+ @default_attributes = nil
+ @inheritance_column = nil unless defined?(@explicit_inheritance_column) && @explicit_inheritance_column
+ @attributes_builder = nil
+ @column_names = nil
+ @attribute_types = nil
+ @columns = nil
+ @columns_hash = nil
+ @content_columns = nil
+ @default_attributes = nil
+ @attribute_names = nil
+ end
+
# Guesses the table name, but does not decorate it with prefix and suffix information.
def undecorated_table_name(class_name = base_class.name)
table_name = class_name.to_s.demodulize.underscore
@@ -331,10 +360,6 @@ module ActiveRecord
base.table_name
end
end
-
- def raw_default_values
- columns_hash.transform_values(&:default)
- end
end
end
end
diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb
index 846e1162a9..117a128579 100644
--- a/activerecord/lib/active_record/nested_attributes.rb
+++ b/activerecord/lib/active_record/nested_attributes.rb
@@ -312,7 +312,7 @@ module ActiveRecord
attr_names.each do |association_name|
if reflection = _reflect_on_association(association_name)
reflection.autosave = true
- add_autosave_association_callbacks(reflection)
+ define_autosave_validation_callbacks(reflection)
nested_attributes_options = self.nested_attributes_options.dup
nested_attributes_options[association_name.to_sym] = options
diff --git a/activerecord/lib/active_record/null_relation.rb b/activerecord/lib/active_record/null_relation.rb
index b406da14dc..fa6eaa7e64 100644
--- a/activerecord/lib/active_record/null_relation.rb
+++ b/activerecord/lib/active_record/null_relation.rb
@@ -30,10 +30,18 @@ module ActiveRecord
true
end
+ def none?
+ true
+ end
+
def any?
false
end
+ def one?
+ false
+ end
+
def many?
false
end
@@ -75,5 +83,9 @@ module ActiveRecord
def exists?(_id = false)
false
end
+
+ def or(other)
+ other.spawn
+ end
end
end
diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb
index 6306a25745..af7aef6e43 100644
--- a/activerecord/lib/active_record/persistence.rb
+++ b/activerecord/lib/active_record/persistence.rb
@@ -178,6 +178,7 @@ module ActiveRecord
def destroy
raise ReadOnlyRecord, "#{self.class} is marked as readonly" if readonly?
destroy_associations
+ self.class.connection.add_transaction_record(self)
destroy_row if persisted?
@destroyed = true
freeze
@@ -245,8 +246,8 @@ module ActiveRecord
def update_attribute(name, value)
name = name.to_s
verify_readonly_attribute(name)
- send("#{name}=", value)
- save(validate: false)
+ public_send("#{name}=", value)
+ save(validate: false) if changed?
end
# Updates the attributes of the model from the passed-in hash and saves the
@@ -352,7 +353,7 @@ module ActiveRecord
# method toggles directly the underlying value without calling any setter.
# Returns +self+.
def toggle(attribute)
- self[attribute] = !send("#{attribute}?")
+ self[attribute] = !public_send("#{attribute}?")
self
end
diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb
index 91c9a0db99..4e597590e9 100644
--- a/activerecord/lib/active_record/querying.rb
+++ b/activerecord/lib/active_record/querying.rb
@@ -7,7 +7,7 @@ module ActiveRecord
delegate :find_by, :find_by!, to: :all
delegate :destroy, :destroy_all, :delete, :delete_all, :update, :update_all, to: :all
delegate :find_each, :find_in_batches, to: :all
- delegate :select, :group, :order, :except, :reorder, :limit, :offset, :joins,
+ delegate :select, :group, :order, :except, :reorder, :limit, :offset, :joins, :or,
:where, :rewhere, :preload, :eager_load, :includes, :from, :lock, :readonly,
:having, :create_with, :uniq, :distinct, :references, :none, :unscope, to: :all
delegate :count, :average, :minimum, :maximum, :sum, :calculate, to: :all
diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake
index 04c2be045d..2591e7492d 100644
--- a/activerecord/lib/active_record/railties/databases.rake
+++ b/activerecord/lib/active_record/railties/databases.rake
@@ -269,9 +269,9 @@ db_namespace = namespace :db do
end
namespace :structure do
- desc 'Dump the database structure to db/structure.sql. Specify another file with DB_STRUCTURE=db/my_structure.sql'
+ desc 'Dump the database structure to db/structure.sql. Specify another file with SCHEMA=db/my_structure.sql'
task :dump => [:environment, :load_config] do
- filename = ENV['DB_STRUCTURE'] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "structure.sql")
+ filename = ENV['SCHEMA'] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "structure.sql")
current_config = ActiveRecord::Tasks::DatabaseTasks.current_config
ActiveRecord::Tasks::DatabaseTasks.structure_dump(current_config, filename)
@@ -286,8 +286,8 @@ db_namespace = namespace :db do
end
desc "Recreate the databases from the structure.sql file"
- task :load => [:environment, :load_config] do
- ActiveRecord::Tasks::DatabaseTasks.load_schema_current(:sql, ENV['DB_STRUCTURE'])
+ task :load => [:load_config] do
+ ActiveRecord::Tasks::DatabaseTasks.load_schema_current(:sql, ENV['SCHEMA'])
end
task :load_if_sql => ['db:create', :environment] do
diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb
index dd78814c6a..6bbec7c0c0 100644
--- a/activerecord/lib/active_record/relation.rb
+++ b/activerecord/lib/active_record/relation.rb
@@ -1,18 +1,19 @@
# -*- coding: utf-8 -*-
-require 'arel/collectors/bind'
+require "arel/collectors/bind"
module ActiveRecord
# = Active Record Relation
class Relation
MULTI_VALUE_METHODS = [:includes, :eager_load, :preload, :select, :group,
- :order, :joins, :where, :having, :bind, :references,
+ :order, :joins, :references,
:extending, :unscope]
- SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :from, :reordering,
+ SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :reordering,
:reverse_order, :distinct, :create_with, :uniq]
+ CLAUSE_METHODS = [:where, :having, :from]
INVALID_METHODS_FOR_DELETE_ALL = [:limit, :distinct, :offset, :group, :having]
- VALUE_METHODS = MULTI_VALUE_METHODS + SINGLE_VALUE_METHODS
+ VALUE_METHODS = MULTI_VALUE_METHODS + SINGLE_VALUE_METHODS + CLAUSE_METHODS
include FinderMethods, Calculations, SpawnMethods, QueryMethods, Batches, Explain, Delegation
@@ -33,7 +34,6 @@ module ActiveRecord
# This method is a hot spot, so for now, use Hash[] to dup the hash.
# https://bugs.ruby-lang.org/issues/7166
@values = Hash[@values]
- @values[:bind] = @values[:bind].dup if @values.key? :bind
reset
end
@@ -81,7 +81,7 @@ module ActiveRecord
end
relation = scope.where(@klass.primary_key => (id_was || id))
- bvs = binds + relation.bind_values
+ bvs = binds + relation.bound_attributes
um = relation
.arel
.compile_update(substitutes, @klass.primary_key)
@@ -95,11 +95,11 @@ module ActiveRecord
def substitute_values(values) # :nodoc:
binds = values.map do |arel_attr, value|
- [@klass.columns_hash[arel_attr.name], value]
+ QueryAttribute.new(arel_attr.name, value, klass.type_for_attribute(arel_attr.name))
end
- substitutes = values.each_with_index.map do |(arel_attr, _), i|
- [arel_attr, @klass.connection.substitute_at(binds[i][0])]
+ substitutes = values.map do |(arel_attr, _)|
+ [arel_attr, connection.substitute_at(klass.columns_hash[arel_attr.name])]
end
[substitutes, binds]
@@ -271,6 +271,15 @@ module ActiveRecord
end
end
+ # Returns true if there are no records.
+ def none?
+ if block_given?
+ to_a.none? { |*block_args| yield(*block_args) }
+ else
+ empty?
+ end
+ end
+
# Returns true if there are any records.
def any?
if block_given?
@@ -280,6 +289,15 @@ module ActiveRecord
end
end
+ # Returns true if there is exactly one record.
+ def one?
+ if block_given?
+ to_a.one? { |*block_args| yield(*block_args) }
+ else
+ limit_value ? to_a.one? : size == 1
+ end
+ end
+
# Returns true if there is more than one record.
def many?
if block_given?
@@ -342,8 +360,7 @@ module ActiveRecord
stmt.wheres = arel.constraints
end
- bvs = arel.bind_values + bind_values
- @klass.connection.update stmt, 'SQL', bvs
+ @klass.connection.update stmt, 'SQL', bound_attributes
end
# Updates an object (or multiple objects) and saves it to the database, if validations pass.
@@ -467,8 +484,10 @@ module ActiveRecord
invalid_methods = INVALID_METHODS_FOR_DELETE_ALL.select { |method|
if MULTI_VALUE_METHODS.include?(method)
send("#{method}_values").any?
- else
+ elsif SINGLE_VALUE_METHODS.include?(method)
send("#{method}_value")
+ elsif CLAUSE_METHODS.include?(method)
+ send("#{method}_clause").any?
end
}
if invalid_methods.any?
@@ -487,7 +506,7 @@ module ActiveRecord
stmt.wheres = arel.constraints
end
- affected = @klass.connection.delete(stmt, 'SQL', bind_values)
+ affected = @klass.connection.delete(stmt, 'SQL', bound_attributes)
reset
affected
@@ -557,11 +576,10 @@ module ActiveRecord
find_with_associations { |rel| relation = rel }
end
- arel = relation.arel
- binds = arel.bind_values + relation.bind_values
+ binds = relation.bound_attributes
binds = connection.prepare_binds_for_database(binds)
- binds.map! { |_, value| connection.quote(value) }
- collect = visitor.accept(arel.ast, Arel::Collectors::Bind.new)
+ binds.map! { |value| connection.quote(value) }
+ collect = visitor.accept(relation.arel.ast, Arel::Collectors::Bind.new)
collect.substitute_binds(binds).join
end
end
@@ -571,22 +589,7 @@ module ActiveRecord
# User.where(name: 'Oscar').where_values_hash
# # => {name: "Oscar"}
def where_values_hash(relation_table_name = table_name)
- equalities = where_values.grep(Arel::Nodes::Equality).find_all { |node|
- node.left.relation.name == relation_table_name
- }
-
- binds = Hash[bind_values.find_all(&:first).map { |column, v| [column.name, v] }]
-
- Hash[equalities.map { |where|
- name = where.left.name
- [name, binds.fetch(name.to_s) {
- case where.right
- when Array then where.right.map(&:val)
- when Arel::Nodes::Casted, Arel::Nodes::Quoted
- where.right.val
- end
- }]
- }]
+ where_clause.to_h(relation_table_name)
end
def scope_for_create
@@ -649,7 +652,7 @@ module ActiveRecord
private
def exec_queries
- @records = eager_loading? ? find_with_associations : @klass.find_by_sql(arel, arel.bind_values + bind_values)
+ @records = eager_loading? ? find_with_associations : @klass.find_by_sql(arel, bound_attributes)
preload = preload_values
preload += includes_values unless eager_loading?
diff --git a/activerecord/lib/active_record/relation/batches.rb b/activerecord/lib/active_record/relation/batches.rb
index 4f0502ae75..a543341149 100644
--- a/activerecord/lib/active_record/relation/batches.rb
+++ b/activerecord/lib/active_record/relation/batches.rb
@@ -27,11 +27,12 @@ module ActiveRecord
#
# ==== Options
# * <tt>:batch_size</tt> - Specifies the size of the batch. Default to 1000.
- # * <tt>:start</tt> - Specifies the primary key value to start from.
+ # * <tt>:start</tt> - Specifies the primary key value to start from, inclusive of the value.
+ # * <tt>:end_at</tt> - Specifies the primary key value to end at, inclusive of the value.
# This is especially useful if you want multiple workers dealing with
# the same processing queue. You can make worker 1 handle all the records
# between id 0 and 10,000 and worker 2 handle from 10,000 and beyond
- # (by setting the +:start+ option on that worker).
+ # (by setting the +:start+ and +:end_at+ option on each worker).
#
# # Let's process for a batch of 2000 records, skipping the first 2000 rows
# Person.find_each(start: 2000, batch_size: 2000) do |person|
@@ -45,19 +46,20 @@ module ActiveRecord
#
# NOTE: You can't set the limit either, that's used to control
# the batch sizes.
- def find_each(options = {})
+ def find_each(start: nil, end_at: nil, batch_size: 1000)
if block_given?
- find_in_batches(options) do |records|
+ find_in_batches(start: start, end_at: end_at, batch_size: batch_size) do |records|
records.each { |record| yield record }
end
else
- enum_for :find_each, options do
- options[:start] ? where(table[primary_key].gteq(options[:start])).size : size
+ enum_for(:find_each, start: start, end_at: end_at, batch_size: batch_size) do
+ relation = self
+ apply_limits(relation, start, end_at).size
end
end
end
- # Yields each batch of records that was found by the find +options+ as
+ # Yields each batch of records that was found by the find options as
# an array.
#
# Person.where("age > 21").find_in_batches do |group|
@@ -77,11 +79,12 @@ module ActiveRecord
#
# ==== Options
# * <tt>:batch_size</tt> - Specifies the size of the batch. Default to 1000.
- # * <tt>:start</tt> - Specifies the primary key value to start from.
+ # * <tt>:start</tt> - Specifies the primary key value to start from, inclusive of the value.
+ # * <tt>:end_at</tt> - Specifies the primary key value to end at, inclusive of the value.
# This is especially useful if you want multiple workers dealing with
# the same processing queue. You can make worker 1 handle all the records
# between id 0 and 10,000 and worker 2 handle from 10,000 and beyond
- # (by setting the +:start+ option on that worker).
+ # (by setting the +:start+ and +:end_at+ option on each worker).
#
# # Let's process the next 2000 records
# Person.find_in_batches(start: 2000, batch_size: 2000) do |group|
@@ -95,16 +98,12 @@ module ActiveRecord
#
# NOTE: You can't set the limit either, that's used to control
# the batch sizes.
- def find_in_batches(options = {})
- options.assert_valid_keys(:start, :batch_size)
-
+ def find_in_batches(start: nil, end_at: nil, batch_size: 1000)
relation = self
- start = options[:start]
- batch_size = options[:batch_size] || 1000
unless block_given?
- return to_enum(:find_in_batches, options) do
- total = start ? where(table[primary_key].gteq(start)).size : size
+ return to_enum(:find_in_batches, start: start, end_at: end_at, batch_size: batch_size) do
+ total = apply_limits(relation, start, end_at).size
(total - 1).div(batch_size) + 1
end
end
@@ -114,7 +113,8 @@ module ActiveRecord
end
relation = relation.reorder(batch_order).limit(batch_size)
- records = start ? relation.where(table[primary_key].gteq(start)).to_a : relation.to_a
+ relation = apply_limits(relation, start, end_at)
+ records = relation.to_a
while records.any?
records_size = records.size
@@ -131,6 +131,12 @@ module ActiveRecord
private
+ def apply_limits(relation, start, end_at)
+ relation = relation.where(table[primary_key].gteq(start)) if start
+ relation = relation.where(table[primary_key].lteq(end_at)) if end_at
+ relation
+ end
+
def batch_order
"#{quoted_table_name}.#{quoted_primary_key} ASC"
end
diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb
index 1d4cb1a83b..63e0d2fc21 100644
--- a/activerecord/lib/active_record/relation/calculations.rb
+++ b/activerecord/lib/active_record/relation/calculations.rb
@@ -166,8 +166,8 @@ module ActiveRecord
relation.select_values = column_names.map { |cn|
columns_hash.key?(cn) ? arel_table[cn] : cn
}
- result = klass.connection.select_all(relation.arel, nil, relation.arel.bind_values + bind_values)
- result.cast_values(klass.column_types)
+ result = klass.connection.select_all(relation.arel, nil, bound_attributes)
+ result.cast_values(klass.attribute_types)
end
end
@@ -227,14 +227,11 @@ module ActiveRecord
column_alias = column_name
- bind_values = nil
-
if operation == "count" && (relation.limit_value || relation.offset_value)
# Shortcut when limit is zero.
return 0 if relation.limit_value == 0
query_builder = build_count_subquery(relation, column_name, distinct)
- bind_values = query_builder.bind_values + relation.bind_values
else
column = aggregate_column(column_name)
@@ -245,10 +242,9 @@ module ActiveRecord
relation.select_values = [select_value]
query_builder = relation.arel
- bind_values = query_builder.bind_values + relation.bind_values
end
- result = @klass.connection.select_all(query_builder, nil, bind_values)
+ result = @klass.connection.select_all(query_builder, nil, bound_attributes)
row = result.first
value = row && row.values.first
column = result.column_types.fetch(column_alias) do
@@ -290,7 +286,7 @@ module ActiveRecord
operation,
distinct).as(aggregate_alias)
]
- select_values += select_values unless having_values.empty?
+ select_values += select_values unless having_clause.empty?
select_values.concat group_fields.zip(group_aliases).map { |field,aliaz|
if field.respond_to?(:as)
@@ -304,11 +300,11 @@ module ActiveRecord
relation.group_values = group
relation.select_values = select_values
- calculated_data = @klass.connection.select_all(relation, nil, relation.arel.bind_values + bind_values)
+ calculated_data = @klass.connection.select_all(relation, nil, relation.bound_attributes)
if association
key_ids = calculated_data.collect { |row| row[group_aliases.first] }
- key_records = association.klass.base_class.find(key_ids)
+ key_records = association.klass.base_class.where(association.klass.base_class.primary_key => key_ids)
key_records = Hash[key_records.map { |r| [r.id, r] }]
end
@@ -378,11 +374,9 @@ module ActiveRecord
aliased_column = aggregate_column(column_name == :all ? 1 : column_name).as(column_alias)
relation.select_values = [aliased_column]
- arel = relation.arel
- subquery = arel.as(subquery_alias)
+ subquery = relation.arel.as(subquery_alias)
sm = Arel::SelectManager.new relation.engine
- sm.bind_values = arel.bind_values
select_value = operation_over_aggregate_column(column_alias, 'count', distinct)
sm.project(select_value).from(subquery)
end
diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb
index 088bc203b7..fb47d915ff 100644
--- a/activerecord/lib/active_record/relation/finder_methods.rb
+++ b/activerecord/lib/active_record/relation/finder_methods.rb
@@ -307,11 +307,11 @@ module ActiveRecord
relation = relation.where(conditions)
else
unless conditions == :none
- relation = where(primary_key => conditions)
+ relation = relation.where(primary_key => conditions)
end
end
- connection.select_value(relation, "#{name} Exists", relation.arel.bind_values + relation.bind_values) ? true : false
+ connection.select_value(relation, "#{name} Exists", relation.bound_attributes) ? true : false
end
# This method is called whenever no records are found with either a single
@@ -365,7 +365,7 @@ module ActiveRecord
[]
else
arel = relation.arel
- rows = connection.select_all(arel, 'SQL', arel.bind_values + relation.bind_values)
+ rows = connection.select_all(arel, 'SQL', relation.bound_attributes)
join_dependency.instantiate(rows, aliases)
end
end
@@ -410,7 +410,7 @@ module ActiveRecord
relation = relation.except(:select).select(values).distinct!
arel = relation.arel
- id_rows = @klass.connection.select_all(arel, 'SQL', arel.bind_values + relation.bind_values)
+ id_rows = @klass.connection.select_all(arel, 'SQL', relation.bound_attributes)
id_rows.map {|row| row[primary_key]}
end
diff --git a/activerecord/lib/active_record/relation/from_clause.rb b/activerecord/lib/active_record/relation/from_clause.rb
new file mode 100644
index 0000000000..a93952fa30
--- /dev/null
+++ b/activerecord/lib/active_record/relation/from_clause.rb
@@ -0,0 +1,32 @@
+module ActiveRecord
+ class Relation
+ class FromClause
+ attr_reader :value, :name
+
+ def initialize(value, name)
+ @value = value
+ @name = name
+ end
+
+ def binds
+ if value.is_a?(Relation)
+ value.bound_attributes
+ else
+ []
+ end
+ end
+
+ def merge(other)
+ self
+ end
+
+ def empty?
+ value.nil?
+ end
+
+ def self.empty
+ new(nil, nil)
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/relation/merger.rb b/activerecord/lib/active_record/relation/merger.rb
index afb0b208c3..65b607ff1c 100644
--- a/activerecord/lib/active_record/relation/merger.rb
+++ b/activerecord/lib/active_record/relation/merger.rb
@@ -49,9 +49,9 @@ module ActiveRecord
@other = other
end
- NORMAL_VALUES = Relation::SINGLE_VALUE_METHODS +
- Relation::MULTI_VALUE_METHODS -
- [:joins, :where, :order, :bind, :reverse_order, :lock, :create_with, :reordering, :from] # :nodoc:
+ NORMAL_VALUES = Relation::VALUE_METHODS -
+ Relation::CLAUSE_METHODS -
+ [:joins, :order, :reverse_order, :lock, :create_with, :reordering] # :nodoc:
def normal_values
NORMAL_VALUES
@@ -75,6 +75,7 @@ module ActiveRecord
merge_multi_values
merge_single_values
+ merge_clauses
merge_joins
relation
@@ -107,20 +108,6 @@ module ActiveRecord
end
def merge_multi_values
- lhs_wheres = relation.where_values
- rhs_wheres = other.where_values
-
- lhs_binds = relation.bind_values
- rhs_binds = other.bind_values
-
- removed, kept = partition_overwrites(lhs_wheres, rhs_wheres)
-
- where_values = kept + rhs_wheres
- bind_values = filter_binds(lhs_binds, removed) + rhs_binds
-
- relation.where_values = where_values
- relation.bind_values = bind_values
-
if other.reordering_value
# override any order specified in the original relation
relation.reorder! other.order_values
@@ -133,36 +120,18 @@ module ActiveRecord
end
def merge_single_values
- relation.from_value = other.from_value unless relation.from_value
- relation.lock_value = other.lock_value unless relation.lock_value
+ relation.lock_value ||= other.lock_value
unless other.create_with_value.blank?
relation.create_with_value = (relation.create_with_value || {}).merge(other.create_with_value)
end
end
- def filter_binds(lhs_binds, removed_wheres)
- return lhs_binds if removed_wheres.empty?
-
- set = Set.new removed_wheres.map { |x| x.left.name.to_s }
- lhs_binds.dup.delete_if { |col,_| set.include? col.name }
- end
-
- # Remove equalities from the existing relation with a LHS which is
- # present in the relation being merged in.
- # returns [things_to_remove, things_to_keep]
- def partition_overwrites(lhs_wheres, rhs_wheres)
- if lhs_wheres.empty? || rhs_wheres.empty?
- return [[], lhs_wheres]
- end
-
- nodes = rhs_wheres.find_all do |w|
- w.respond_to?(:operator) && w.operator == :==
- end
- seen = Set.new(nodes) { |node| node.left }
-
- lhs_wheres.partition do |w|
- w.respond_to?(:operator) && w.operator == :== && seen.include?(w.left)
+ def merge_clauses
+ CLAUSE_METHODS.each do |name|
+ clause = relation.send("#{name}_clause")
+ other_clause = other.send("#{name}_clause")
+ relation.send("#{name}_clause=", clause.merge(other_clause))
end
end
end
diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb
index 567efce8ae..43e9afe853 100644
--- a/activerecord/lib/active_record/relation/predicate_builder.rb
+++ b/activerecord/lib/active_record/relation/predicate_builder.rb
@@ -28,6 +28,11 @@ module ActiveRecord
expand_from_hash(attributes)
end
+ def create_binds(attributes)
+ attributes = convert_dot_notation_to_hash(attributes.stringify_keys)
+ create_binds_for_hash(attributes)
+ end
+
def expand(column, value)
# Find the foreign key when using queries such as:
# Post.where(author: author)
@@ -80,16 +85,43 @@ module ActiveRecord
attributes.flat_map do |key, value|
if value.is_a?(Hash)
- builder = self.class.new(table.associated_table(key))
- builder.expand_from_hash(value)
+ associated_predicate_builder(key).expand_from_hash(value)
else
expand(key, value)
end
end
end
+
+ def create_binds_for_hash(attributes)
+ result = attributes.dup
+ binds = []
+
+ attributes.each do |column_name, value|
+ case value
+ when Hash
+ attrs, bvs = associated_predicate_builder(column_name).create_binds_for_hash(value)
+ result[column_name] = attrs
+ binds += bvs
+ when Relation
+ binds += value.bound_attributes
+ else
+ if can_be_bound?(column_name, value)
+ result[column_name] = Arel::Nodes::BindParam.new
+ binds << Relation::QueryAttribute.new(column_name.to_s, value, table.type(column_name))
+ end
+ end
+ end
+
+ [result, binds]
+ end
+
private
+ def associated_predicate_builder(association_name)
+ self.class.new(table.associated_table(association_name))
+ end
+
def convert_dot_notation_to_hash(attributes)
dot_notation = attributes.keys.select { |s| s.include?(".") }
@@ -107,5 +139,11 @@ module ActiveRecord
def handler_for(object)
@handlers.detect { |klass, _| klass === object }.last
end
+
+ def can_be_bound?(column_name, value)
+ !value.nil? &&
+ handler_for(value).is_a?(BasicObjectHandler) &&
+ !table.associated_with?(column_name)
+ end
end
end
diff --git a/activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb
index aabcf20c1d..159889d3b8 100644
--- a/activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb
+++ b/activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb
@@ -31,7 +31,14 @@ module ActiveRecord
end
def ids
- value
+ case value
+ when Relation
+ value.select(primary_key)
+ when Array
+ value.map { |v| convert_to_id(v) }
+ else
+ convert_to_id(value)
+ end
end
def base_class
@@ -42,6 +49,10 @@ module ActiveRecord
private
+ def primary_key
+ associated_table.association_primary_key(base_class)
+ end
+
def polymorphic_base_class_from_value
case value
when Relation
@@ -53,6 +64,15 @@ module ActiveRecord
value.class.base_class
end
end
+
+ def convert_to_id(value)
+ case value
+ when Base
+ value._read_attribute(primary_key)
+ else
+ value
+ end
+ end
end
end
end
diff --git a/activerecord/lib/active_record/relation/query_attribute.rb b/activerecord/lib/active_record/relation/query_attribute.rb
new file mode 100644
index 0000000000..e69319b4de
--- /dev/null
+++ b/activerecord/lib/active_record/relation/query_attribute.rb
@@ -0,0 +1,19 @@
+require 'active_record/attribute'
+
+module ActiveRecord
+ class Relation
+ class QueryAttribute < Attribute
+ def type_cast(value)
+ value
+ end
+
+ def value_for_database
+ @value_for_database ||= super
+ end
+
+ def with_cast_value(value)
+ QueryAttribute.new(name, value, type)
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb
index d6e6cb4d05..7514401072 100644
--- a/activerecord/lib/active_record/relation/query_methods.rb
+++ b/activerecord/lib/active_record/relation/query_methods.rb
@@ -1,6 +1,9 @@
-require 'active_support/core_ext/array/wrap'
-require 'active_support/core_ext/string/filters'
+require "active_record/relation/from_clause"
+require "active_record/relation/query_attribute"
+require "active_record/relation/where_clause"
+require "active_record/relation/where_clause_factory"
require 'active_model/forbidden_attributes_protection'
+require 'active_support/core_ext/string/filters'
module ActiveRecord
module QueryMethods
@@ -39,23 +42,10 @@ module ActiveRecord
# User.where.not(name: "Jon", role: "admin")
# # SELECT * FROM users WHERE name != 'Jon' AND role != 'admin'
def not(opts, *rest)
- where_value = @scope.send(:build_where, opts, rest).map do |rel|
- case rel
- when NilClass
- raise ArgumentError, 'Invalid argument for .where.not(), got nil.'
- when Arel::Nodes::In
- Arel::Nodes::NotIn.new(rel.left, rel.right)
- when Arel::Nodes::Equality
- Arel::Nodes::NotEqual.new(rel.left, rel.right)
- when String
- Arel::Nodes::Not.new(Arel::Nodes::SqlLiteral.new(rel))
- else
- Arel::Nodes::Not.new(rel)
- end
- end
+ where_clause = @scope.send(:where_clause_factory).build(opts, rest)
@scope.references!(PredicateBuilder.references(opts)) if Hash === opts
- @scope.where_values += where_value
+ @scope.where_clause += where_clause.invert
@scope
end
end
@@ -90,6 +80,23 @@ module ActiveRecord
CODE
end
+ Relation::CLAUSE_METHODS.each do |name|
+ class_eval <<-CODE, __FILE__, __LINE__ + 1
+ def #{name}_clause # def where_clause
+ @values[:#{name}] || new_#{name}_clause # @values[:where] || new_where_clause
+ end # end
+ #
+ def #{name}_clause=(value) # def where_clause=(value)
+ assert_mutability! # assert_mutability!
+ @values[:#{name}] = value # @values[:where] = value
+ end # end
+ CODE
+ end
+
+ def bound_attributes
+ from_clause.binds + arel.bind_values + where_clause.binds + having_clause.binds
+ end
+
def create_with_value # :nodoc:
@values[:create_with] || {}
end
@@ -392,9 +399,8 @@ module ActiveRecord
raise ArgumentError, "Hash arguments in .unscope(*args) must have :where as the key."
end
- Array(target_value).each do |val|
- where_unscoping(val)
- end
+ target_values = Array(target_value).map(&:to_s)
+ self.where_clause = where_clause.except(*target_values)
end
else
raise ArgumentError, "Unrecognized scoping: #{args.inspect}. Use .unscope(where: :attribute_name) or .unscope(:order), for example."
@@ -425,15 +431,6 @@ module ActiveRecord
self
end
- def bind(value) # :nodoc:
- spawn.bind!(value)
- end
-
- def bind!(value) # :nodoc:
- self.bind_values += [value]
- self
- end
-
# Returns a new relation, which is the result of filtering the current relation
# according to the conditions in the arguments.
#
@@ -569,7 +566,7 @@ module ActiveRecord
references!(PredicateBuilder.references(opts))
end
- self.where_values += build_where(opts, rest)
+ self.where_clause += where_clause_factory.build(opts, rest)
self
end
@@ -585,6 +582,37 @@ module ActiveRecord
unscope(where: conditions.keys).where(conditions)
end
+ # Returns a new relation, which is the logical union of this relation and the one passed as an
+ # argument.
+ #
+ # The two relations must be structurally compatible: they must be scoping the same model, and
+ # they must differ only by +where+ (if no +group+ has been defined) or +having+ (if a +group+ is
+ # present). Neither relation may have a +limit+, +offset+, or +uniq+ set.
+ #
+ # Post.where("id = 1").or(Post.where("id = 2"))
+ # # SELECT `posts`.* FROM `posts` WHERE (('id = 1' OR 'id = 2'))
+ #
+ def or(other)
+ spawn.or!(other)
+ end
+
+ def or!(other) # :nodoc:
+ unless structurally_compatible_for_or?(other)
+ raise ArgumentError, 'Relation passed to #or must be structurally compatible'
+ end
+
+ self.where_clause = self.where_clause.or(other.where_clause)
+ self.having_clause = self.having_clause.or(other.having_clause)
+
+ self
+ end
+
+ private def structurally_compatible_for_or?(other) # :nodoc:
+ Relation::SINGLE_VALUE_METHODS.all? { |m| send("#{m}_value") == other.send("#{m}_value") } &&
+ (Relation::MULTI_VALUE_METHODS - [:extending]).all? { |m| send("#{m}_values") == other.send("#{m}_values") } &&
+ (Relation::CLAUSE_METHODS - [:having, :where]).all? { |m| send("#{m}_clause") != other.send("#{m}_clause") }
+ end
+
# Allows to specify a HAVING clause. Note that you can't use HAVING
# without also specifying a GROUP clause.
#
@@ -596,7 +624,7 @@ module ActiveRecord
def having!(opts, *rest) # :nodoc:
references!(PredicateBuilder.references(opts)) if Hash === opts
- self.having_values += build_where(opts, rest)
+ self.having_clause += having_clause_factory.build(opts, rest)
self
end
@@ -744,10 +772,7 @@ module ActiveRecord
end
def from!(value, subquery_name = nil) # :nodoc:
- self.from_value = [value, subquery_name]
- if value.is_a? Relation
- self.bind_values = value.arel.bind_values + value.bind_values + bind_values
- end
+ self.from_clause = Relation::FromClause.new(value, subquery_name)
self
end
@@ -858,21 +883,18 @@ module ActiveRecord
build_joins(arel, joins_values.flatten) unless joins_values.empty?
- collapse_wheres(arel, (where_values - [''])) #TODO: Add uniq with real value comparison / ignore uniqs that have binds
-
- arel.having(*having_values.uniq.reject(&:blank?)) unless having_values.empty?
-
+ arel.where(where_clause.ast) unless where_clause.empty?
+ arel.having(having_clause.ast) unless having_clause.empty?
arel.take(connection.sanitize_limit(limit_value)) if limit_value
arel.skip(offset_value.to_i) if offset_value
-
- arel.group(*group_values.uniq.reject(&:blank?)) unless group_values.empty?
+ arel.group(*arel_columns(group_values.uniq.reject(&:blank?))) unless group_values.empty?
build_order(arel)
- build_select(arel, select_values.uniq)
+ build_select(arel)
arel.distinct(distinct_value)
- arel.from(build_from) if from_value
+ arel.from(build_from) unless from_clause.empty?
arel.lock(lock_value) if lock_value
arel
@@ -883,114 +905,24 @@ module ActiveRecord
raise ArgumentError, "Called unscope() with invalid unscoping argument ':#{scope}'. Valid arguments are :#{VALID_UNSCOPING_VALUES.to_a.join(", :")}."
end
- single_val_method = Relation::SINGLE_VALUE_METHODS.include?(scope)
- unscope_code = "#{scope}_value#{'s' unless single_val_method}="
+ clause_method = Relation::CLAUSE_METHODS.include?(scope)
+ multi_val_method = Relation::MULTI_VALUE_METHODS.include?(scope)
+ if clause_method
+ unscope_code = "#{scope}_clause="
+ else
+ unscope_code = "#{scope}_value#{'s' if multi_val_method}="
+ end
case scope
when :order
result = []
- when :where
- self.bind_values = []
else
- result = [] unless single_val_method
+ result = [] if multi_val_method
end
self.send(unscope_code, result)
end
- def where_unscoping(target_value)
- target_value = target_value.to_s
-
- where_values.reject! do |rel|
- case rel
- when Arel::Nodes::Between, Arel::Nodes::In, Arel::Nodes::NotIn, Arel::Nodes::Equality, Arel::Nodes::NotEqual, Arel::Nodes::LessThanOrEqual, Arel::Nodes::GreaterThanOrEqual
- subrelation = (rel.left.kind_of?(Arel::Attributes::Attribute) ? rel.left : rel.right)
- subrelation.name.to_s == target_value
- end
- end
-
- bind_values.reject! { |col,_| col.name == target_value }
- end
-
- def custom_join_ast(table, joins)
- joins = joins.reject(&:blank?)
-
- return [] if joins.empty?
-
- joins.map! do |join|
- case join
- when Array
- join = Arel.sql(join.join(' ')) if array_of_strings?(join)
- when String
- join = Arel.sql(join)
- end
- table.create_string_join(join)
- end
- end
-
- def collapse_wheres(arel, wheres)
- predicates = wheres.map do |where|
- next where if ::Arel::Nodes::Equality === where
- where = Arel.sql(where) if String === where
- Arel::Nodes::Grouping.new(where)
- end
-
- arel.where(Arel::Nodes::And.new(predicates)) if predicates.present?
- end
-
- def build_where(opts, other = [])
- case opts
- when String, Array
- [@klass.send(:sanitize_sql, other.empty? ? opts : ([opts] + other))]
- when Hash
- opts = predicate_builder.resolve_column_aliases(opts)
-
- tmp_opts, bind_values = create_binds(opts)
- self.bind_values += bind_values
-
- attributes = @klass.send(:expand_hash_conditions_for_aggregates, tmp_opts)
- add_relations_to_bind_values(attributes)
-
- predicate_builder.build_from_hash(attributes)
- else
- [opts]
- end
- end
-
- def create_binds(opts)
- bindable, non_binds = opts.partition do |column, value|
- case value
- when String, Integer, ActiveRecord::StatementCache::Substitute
- @klass.columns_hash.include? column.to_s
- else
- false
- end
- end
-
- association_binds, non_binds = non_binds.partition do |column, value|
- value.is_a?(Hash) && association_for_table(column)
- end
-
- new_opts = {}
- binds = []
-
- bindable.each do |(column,value)|
- binds.push [@klass.columns_hash[column.to_s], value]
- new_opts[column] = connection.substitute_at(column)
- end
-
- association_binds.each do |(column, value)|
- association_relation = association_for_table(column).klass.send(:relation)
- association_new_opts, association_bind = association_relation.send(:create_binds, value)
- new_opts[column] = association_new_opts
- binds += association_bind
- end
-
- non_binds.each { |column,value| new_opts[column] = value }
-
- [new_opts, binds]
- end
-
def association_for_table(table_name)
table_name = table_name.to_s
@klass._reflect_on_association(table_name) ||
@@ -998,7 +930,8 @@ module ActiveRecord
end
def build_from
- opts, name = from_value
+ opts = from_clause.value
+ name = from_clause.name
case opts
when Relation
name ||= 'subquery'
@@ -1023,13 +956,14 @@ module ActiveRecord
raise 'unknown class: %s' % join.class.name
end
end
+ buckets.default = []
- association_joins = buckets[:association_join] || []
- stashed_association_joins = buckets[:stashed_join] || []
- join_nodes = (buckets[:join_node] || []).uniq
- string_joins = (buckets[:string_join] || []).map(&:strip).uniq
+ association_joins = buckets[:association_join]
+ stashed_association_joins = buckets[:stashed_join]
+ join_nodes = buckets[:join_node].uniq
+ string_joins = buckets[:string_join].map(&:strip).uniq
- join_list = join_nodes + custom_join_ast(manager, string_joins)
+ join_list = join_nodes + convert_join_strings_to_ast(manager, string_joins)
join_dependency = ActiveRecord::Associations::JoinDependency.new(
@klass,
@@ -1049,22 +983,31 @@ module ActiveRecord
manager
end
- def build_select(arel, selects)
- if !selects.empty?
- expanded_select = selects.map do |field|
- if (Symbol === field || String === field) && columns_hash.key?(field.to_s)
- arel_table[field]
- else
- field
- end
- end
+ def convert_join_strings_to_ast(table, joins)
+ joins
+ .flatten
+ .reject(&:blank?)
+ .map { |join| table.create_string_join(Arel.sql(join)) }
+ end
- arel.project(*expanded_select)
+ def build_select(arel)
+ if select_values.any?
+ arel.project(*arel_columns(select_values.uniq))
else
arel.project(@klass.arel_table[Arel.star])
end
end
+ def arel_columns(columns)
+ columns.map do |field|
+ if (Symbol === field || String === field) && columns_hash.key?(field.to_s)
+ arel_table[field]
+ else
+ field
+ end
+ end
+ end
+
def reverse_sql_order(order_query)
order_query = ["#{quoted_table_name}.#{quoted_primary_key} ASC"] if order_query.empty?
@@ -1083,10 +1026,6 @@ module ActiveRecord
end
end
- def array_of_strings?(o)
- o.is_a?(Array) && o.all? { |obj| obj.is_a?(String) }
- end
-
def build_order(arel)
orders = order_values.uniq
orders.reject!(&:blank?)
@@ -1154,16 +1093,18 @@ module ActiveRecord
end
end
- def add_relations_to_bind_values(attributes)
- if attributes.is_a?(Hash)
- attributes.each_value do |value|
- if value.is_a?(ActiveRecord::Relation)
- self.bind_values += value.arel.bind_values + value.bind_values
- else
- add_relations_to_bind_values(value)
- end
- end
- end
+ def new_where_clause
+ Relation::WhereClause.empty
+ end
+ alias new_having_clause new_where_clause
+
+ def where_clause_factory
+ @where_clause_factory ||= Relation::WhereClauseFactory.new(klass, predicate_builder)
+ end
+ alias having_clause_factory where_clause_factory
+
+ def new_from_clause
+ Relation::FromClause.empty
end
end
end
diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb
index 01bddea6c9..70da37fa84 100644
--- a/activerecord/lib/active_record/relation/spawn_methods.rb
+++ b/activerecord/lib/active_record/relation/spawn_methods.rb
@@ -32,7 +32,7 @@ module ActiveRecord
elsif other
spawn.merge!(other)
else
- self
+ raise ArgumentError, "invalid argument: #{other.inspect}."
end
end
@@ -58,9 +58,6 @@ module ActiveRecord
# Post.order('id asc').only(:where) # discards the order condition
# Post.order('id asc').only(:where, :order) # uses the specified order
def only(*onlies)
- if onlies.any? { |o| o == :where }
- onlies << :bind
- end
relation_with values.slice(*onlies)
end
diff --git a/activerecord/lib/active_record/relation/where_clause.rb b/activerecord/lib/active_record/relation/where_clause.rb
new file mode 100644
index 0000000000..f9b9e640ec
--- /dev/null
+++ b/activerecord/lib/active_record/relation/where_clause.rb
@@ -0,0 +1,173 @@
+module ActiveRecord
+ class Relation
+ class WhereClause # :nodoc:
+ attr_reader :binds
+
+ delegate :any?, :empty?, to: :predicates
+
+ def initialize(predicates, binds)
+ @predicates = predicates
+ @binds = binds
+ end
+
+ def +(other)
+ WhereClause.new(
+ predicates + other.predicates,
+ binds + other.binds,
+ )
+ end
+
+ def merge(other)
+ WhereClause.new(
+ predicates_unreferenced_by(other) + other.predicates,
+ non_conflicting_binds(other) + other.binds,
+ )
+ end
+
+ def except(*columns)
+ WhereClause.new(
+ predicates_except(columns),
+ binds_except(columns),
+ )
+ end
+
+ def or(other)
+ if empty?
+ self
+ elsif other.empty?
+ other
+ else
+ WhereClause.new(
+ [ast.or(other.ast)],
+ binds + other.binds
+ )
+ end
+ end
+
+ def to_h(table_name = nil)
+ equalities = predicates.grep(Arel::Nodes::Equality)
+ if table_name
+ equalities = equalities.select do |node|
+ node.left.relation.name == table_name
+ end
+ end
+
+ binds = self.binds.map { |attr| [attr.name, attr.value] }.to_h
+
+ equalities.map { |node|
+ name = node.left.name
+ [name, binds.fetch(name.to_s) {
+ case node.right
+ when Array then node.right.map(&:val)
+ when Arel::Nodes::Casted, Arel::Nodes::Quoted
+ node.right.val
+ end
+ }]
+ }.to_h
+ end
+
+ def ast
+ Arel::Nodes::And.new(predicates_with_wrapped_sql_literals)
+ end
+
+ def ==(other)
+ other.is_a?(WhereClause) &&
+ predicates == other.predicates &&
+ binds == other.binds
+ end
+
+ def invert
+ WhereClause.new(inverted_predicates, binds)
+ end
+
+ def self.empty
+ new([], [])
+ end
+
+ protected
+
+ attr_reader :predicates
+
+ def referenced_columns
+ @referenced_columns ||= begin
+ equality_nodes = predicates.select { |n| equality_node?(n) }
+ Set.new(equality_nodes, &:left)
+ end
+ end
+
+ private
+
+ def predicates_unreferenced_by(other)
+ predicates.reject do |n|
+ equality_node?(n) && other.referenced_columns.include?(n.left)
+ end
+ end
+
+ def equality_node?(node)
+ node.respond_to?(:operator) && node.operator == :==
+ end
+
+ def non_conflicting_binds(other)
+ conflicts = referenced_columns & other.referenced_columns
+ conflicts.map! { |node| node.name.to_s }
+ binds.reject { |attr| conflicts.include?(attr.name) }
+ end
+
+ def inverted_predicates
+ predicates.map { |node| invert_predicate(node) }
+ end
+
+ def invert_predicate(node)
+ case node
+ when NilClass
+ raise ArgumentError, 'Invalid argument for .where.not(), got nil.'
+ when Arel::Nodes::In
+ Arel::Nodes::NotIn.new(node.left, node.right)
+ when Arel::Nodes::Equality
+ Arel::Nodes::NotEqual.new(node.left, node.right)
+ when String
+ Arel::Nodes::Not.new(Arel::Nodes::SqlLiteral.new(node))
+ else
+ Arel::Nodes::Not.new(node)
+ end
+ end
+
+ def predicates_except(columns)
+ predicates.reject do |node|
+ case node
+ when Arel::Nodes::Between, Arel::Nodes::In, Arel::Nodes::NotIn, Arel::Nodes::Equality, Arel::Nodes::NotEqual, Arel::Nodes::LessThanOrEqual, Arel::Nodes::GreaterThanOrEqual
+ subrelation = (node.left.kind_of?(Arel::Attributes::Attribute) ? node.left : node.right)
+ columns.include?(subrelation.name.to_s)
+ end
+ end
+ end
+
+ def binds_except(columns)
+ binds.reject do |attr|
+ columns.include?(attr.name)
+ end
+ end
+
+ def predicates_with_wrapped_sql_literals
+ non_empty_predicates.map do |node|
+ if Arel::Nodes::Equality === node
+ node
+ else
+ wrap_sql_literal(node)
+ end
+ end
+ end
+
+ def non_empty_predicates
+ predicates - ['']
+ end
+
+ def wrap_sql_literal(node)
+ if ::String === node
+ node = Arel.sql(node)
+ end
+ Arel::Nodes::Grouping.new(node)
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/relation/where_clause_factory.rb b/activerecord/lib/active_record/relation/where_clause_factory.rb
new file mode 100644
index 0000000000..0430922be3
--- /dev/null
+++ b/activerecord/lib/active_record/relation/where_clause_factory.rb
@@ -0,0 +1,34 @@
+module ActiveRecord
+ class Relation
+ class WhereClauseFactory
+ def initialize(klass, predicate_builder)
+ @klass = klass
+ @predicate_builder = predicate_builder
+ end
+
+ def build(opts, other)
+ binds = []
+
+ case opts
+ when String, Array
+ parts = [klass.send(:sanitize_sql, other.empty? ? opts : ([opts] + other))]
+ when Hash
+ attributes = predicate_builder.resolve_column_aliases(opts)
+ attributes = klass.send(:expand_hash_conditions_for_aggregates, attributes)
+
+ attributes, binds = predicate_builder.create_binds(attributes)
+
+ parts = predicate_builder.build_from_hash(attributes)
+ else
+ parts = [opts]
+ end
+
+ WhereClause.new(parts, binds)
+ end
+
+ protected
+
+ attr_reader :klass, :predicate_builder
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/scoping.rb b/activerecord/lib/active_record/scoping.rb
index 3e43591672..fca4f1c9d3 100644
--- a/activerecord/lib/active_record/scoping.rb
+++ b/activerecord/lib/active_record/scoping.rb
@@ -11,11 +11,11 @@ module ActiveRecord
module ClassMethods
def current_scope #:nodoc:
- ScopeRegistry.value_for(:current_scope, base_class.to_s)
+ ScopeRegistry.value_for(:current_scope, self.to_s)
end
def current_scope=(scope) #:nodoc:
- ScopeRegistry.set_value_for(:current_scope, base_class.to_s, scope)
+ ScopeRegistry.set_value_for(:current_scope, self.to_s, scope)
end
end
diff --git a/activerecord/lib/active_record/scoping/named.rb b/activerecord/lib/active_record/scoping/named.rb
index 35420e6551..43c7b1c574 100644
--- a/activerecord/lib/active_record/scoping/named.rb
+++ b/activerecord/lib/active_record/scoping/named.rb
@@ -30,7 +30,13 @@ module ActiveRecord
end
def default_scoped # :nodoc:
- relation.merge(build_default_scope)
+ scope = build_default_scope
+
+ if scope
+ relation.spawn.merge!(scope)
+ else
+ relation
+ end
end
# Collects attributes from scopes that should be applied when creating
@@ -151,11 +157,20 @@ module ActiveRecord
extension = Module.new(&block) if block
- singleton_class.send(:define_method, name) do |*args|
- scope = all.scoping { body.call(*args) }
- scope = scope.extending(extension) if extension
+ if body.respond_to?(:to_proc)
+ singleton_class.send(:define_method, name) do |*args|
+ scope = all.scoping { instance_exec(*args, &body) }
+ scope = scope.extending(extension) if extension
+
+ scope || all
+ end
+ else
+ singleton_class.send(:define_method, name) do |*args|
+ scope = all.scoping { body.call(*args) }
+ scope = scope.extending(extension) if extension
- scope || all
+ scope || all
+ end
end
end
end
diff --git a/activerecord/lib/active_record/secure_token.rb b/activerecord/lib/active_record/secure_token.rb
index be3c3bc847..0990f815a7 100644
--- a/activerecord/lib/active_record/secure_token.rb
+++ b/activerecord/lib/active_record/secure_token.rb
@@ -5,7 +5,7 @@ module ActiveRecord
module ClassMethods
# Example using has_secure_token
#
- # # Schema: User(toke:string, auth_token:string)
+ # # Schema: User(token:string, auth_token:string)
# class User < ActiveRecord::Base
# has_secure_token
# has_secure_token :auth_token
@@ -27,7 +27,7 @@ module ActiveRecord
# Load securerandom only when has_secure_token is used.
require 'active_support/core_ext/securerandom'
define_method("regenerate_#{attribute}") { update! attribute => self.class.generate_unique_secure_token }
- before_create { self.send("#{attribute}=", self.class.generate_unique_secure_token) }
+ before_create { self.send("#{attribute}=", self.class.generate_unique_secure_token) unless self.send("#{attribute}?")}
end
def generate_unique_secure_token
@@ -36,4 +36,3 @@ module ActiveRecord
end
end
end
-
diff --git a/activerecord/lib/active_record/serializers/xml_serializer.rb b/activerecord/lib/active_record/serializers/xml_serializer.rb
index c2484d02ed..89b7e0be82 100644
--- a/activerecord/lib/active_record/serializers/xml_serializer.rb
+++ b/activerecord/lib/active_record/serializers/xml_serializer.rb
@@ -180,9 +180,9 @@ module ActiveRecord #:nodoc:
class Attribute < ActiveModel::Serializers::Xml::Serializer::Attribute #:nodoc:
def compute_type
klass = @serializable.class
- column = klass.columns_hash[name] || Type::Value.new
+ cast_type = klass.type_for_attribute(name)
- type = ActiveSupport::XmlMini::TYPE_NAMES[value.class.name] || column.type
+ type = ActiveSupport::XmlMini::TYPE_NAMES[value.class.name] || cast_type.type
{ :text => :string,
:time => :datetime }[type] || type
diff --git a/activerecord/lib/active_record/statement_cache.rb b/activerecord/lib/active_record/statement_cache.rb
index 3047a81ec4..95986c820c 100644
--- a/activerecord/lib/active_record/statement_cache.rb
+++ b/activerecord/lib/active_record/statement_cache.rb
@@ -48,7 +48,7 @@ module ActiveRecord
def sql_for(binds, connection)
val = @values.dup
binds = connection.prepare_binds_for_database(binds)
- @indexes.each { |i| val[i] = connection.quote(binds.shift.last) }
+ @indexes.each { |i| val[i] = connection.quote(binds.shift) }
val.join
end
end
@@ -67,21 +67,21 @@ module ActiveRecord
end
class BindMap # :nodoc:
- def initialize(bind_values)
+ def initialize(bound_attributes)
@indexes = []
- @bind_values = bind_values
+ @bound_attributes = bound_attributes
- bind_values.each_with_index do |(_, value), i|
- if Substitute === value
+ bound_attributes.each_with_index do |attr, i|
+ if Substitute === attr.value
@indexes << i
end
end
end
def bind(values)
- bvs = @bind_values.map(&:dup)
- @indexes.each_with_index { |offset,i| bvs[offset][1] = values[i] }
- bvs
+ bas = @bound_attributes.dup
+ @indexes.each_with_index { |offset,i| bas[offset] = bas[offset].with_cast_value(values[i]) }
+ bas
end
end
@@ -89,7 +89,7 @@ module ActiveRecord
def self.create(connection, block = Proc.new)
relation = block.call Params.new
- bind_map = BindMap.new relation.bind_values
+ bind_map = BindMap.new relation.bound_attributes
query_builder = connection.cacheable_query relation.arel
new query_builder, bind_map
end
diff --git a/activerecord/lib/active_record/table_metadata.rb b/activerecord/lib/active_record/table_metadata.rb
index 11e33e8dfe..3dd6321a97 100644
--- a/activerecord/lib/active_record/table_metadata.rb
+++ b/activerecord/lib/active_record/table_metadata.rb
@@ -1,6 +1,7 @@
module ActiveRecord
class TableMetadata # :nodoc:
delegate :foreign_type, :foreign_key, to: :association, prefix: true
+ delegate :association_primary_key, to: :association
def initialize(klass, arel_table, association = nil)
@klass = klass
@@ -22,6 +23,14 @@ module ActiveRecord
arel_table[column_name]
end
+ def type(column_name)
+ if klass
+ klass.type_for_attribute(column_name.to_s)
+ else
+ Type::Value.new
+ end
+ end
+
def associated_with?(association_name)
klass && klass._reflect_on_association(association_name)
end
@@ -34,7 +43,7 @@ module ActiveRecord
association_klass = association.klass
arel_table = association_klass.arel_table
else
- type_caster = TypeCaster::Connection.new(klass.connection, table_name)
+ type_caster = TypeCaster::Connection.new(klass, table_name)
association_klass = nil
arel_table = Arel::Table.new(table_name, type_caster: type_caster)
end
diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb
index 0fd2862b2c..dd405c7796 100644
--- a/activerecord/lib/active_record/transactions.rb
+++ b/activerecord/lib/active_record/transactions.rb
@@ -318,9 +318,12 @@ module ActiveRecord
# Add the record to the current transaction so that the +after_rollback+ and +after_commit+ callbacks
# can be called.
def add_to_transaction
- if self.class.connection.add_transaction_record(self)
- remember_transaction_record_state
+ if has_transactional_callbacks?
+ self.class.connection.add_transaction_record(self)
+ else
+ set_transaction_state(self.class.connection.transaction_state)
end
+ remember_transaction_record_state
end
# Executes +method+ within a transaction and captures its return value as a
@@ -378,7 +381,10 @@ module ActiveRecord
thaw unless restore_state[:frozen?]
@new_record = restore_state[:new_record]
@destroyed = restore_state[:destroyed]
- write_attribute(self.class.primary_key, restore_state[:id]) if self.class.primary_key
+ pk = self.class.primary_key
+ if pk && read_attribute(pk) != restore_state[:id]
+ write_attribute(pk, restore_state[:id])
+ end
end
end
end
diff --git a/activerecord/lib/active_record/type.rb b/activerecord/lib/active_record/type.rb
index 250e8d5b23..f18b076d58 100644
--- a/activerecord/lib/active_record/type.rb
+++ b/activerecord/lib/active_record/type.rb
@@ -1,7 +1,4 @@
-require 'active_record/type/decorator'
-require 'active_record/type/mutable'
-require 'active_record/type/numeric'
-require 'active_record/type/time_value'
+require 'active_record/type/helpers'
require 'active_record/type/value'
require 'active_record/type/big_integer'
diff --git a/activerecord/lib/active_record/type/date.rb b/activerecord/lib/active_record/type/date.rb
index d90a6069b7..3ceab59ebb 100644
--- a/activerecord/lib/active_record/type/date.rb
+++ b/activerecord/lib/active_record/type/date.rb
@@ -1,14 +1,12 @@
module ActiveRecord
module Type
class Date < Value # :nodoc:
+ include Helpers::AcceptsMultiparameterTime.new
+
def type
:date
end
- def klass
- ::Date
- end
-
def type_cast_for_schema(value)
"'#{value.to_s(:db)}'"
end
@@ -41,6 +39,11 @@ module ActiveRecord
::Date.new(year, mon, mday) rescue nil
end
end
+
+ def value_from_multiparameter_assignment(*)
+ time = super
+ time && time.to_date
+ end
end
end
end
diff --git a/activerecord/lib/active_record/type/date_time.rb b/activerecord/lib/active_record/type/date_time.rb
index 0a737815bc..a25f2521bb 100644
--- a/activerecord/lib/active_record/type/date_time.rb
+++ b/activerecord/lib/active_record/type/date_time.rb
@@ -1,24 +1,31 @@
module ActiveRecord
module Type
class DateTime < Value # :nodoc:
- include TimeValue
+ include Helpers::TimeValue
+ include Helpers::AcceptsMultiparameterTime.new(
+ defaults: { 4 => 0, 5 => 0 }
+ )
def type
:datetime
end
def type_cast_for_database(value)
- zone_conversion_method = ActiveRecord::Base.default_timezone == :utc ? :getutc : :getlocal
+ if precision && value.respond_to?(:usec)
+ number_of_insignificant_digits = 6 - precision
+ round_power = 10 ** number_of_insignificant_digits
+ value = value.change(usec: value.usec / round_power * round_power)
+ end
if value.acts_like?(:time)
+ zone_conversion_method = ActiveRecord::Base.default_timezone == :utc ? :getutc : :getlocal
+
if value.respond_to?(zone_conversion_method)
- value.send(zone_conversion_method)
- else
- value
+ value = value.send(zone_conversion_method)
end
- else
- super
end
+
+ value
end
private
@@ -42,6 +49,14 @@ module ActiveRecord
new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction, :offset))
end
+
+ def value_from_multiparameter_assignment(values_hash)
+ missing_parameter = (1..3).detect { |key| !values_hash.key?(key) }
+ if missing_parameter
+ raise ArgumentError, missing_parameter
+ end
+ super
+ end
end
end
end
diff --git a/activerecord/lib/active_record/type/decimal.rb b/activerecord/lib/active_record/type/decimal.rb
index 7b2bee2c42..867b5f75c7 100644
--- a/activerecord/lib/active_record/type/decimal.rb
+++ b/activerecord/lib/active_record/type/decimal.rb
@@ -1,7 +1,7 @@
module ActiveRecord
module Type
class Decimal < Value # :nodoc:
- include Numeric
+ include Helpers::Numeric
def type
:decimal
diff --git a/activerecord/lib/active_record/type/decorator.rb b/activerecord/lib/active_record/type/decorator.rb
deleted file mode 100644
index 9fce38ea44..0000000000
--- a/activerecord/lib/active_record/type/decorator.rb
+++ /dev/null
@@ -1,14 +0,0 @@
-module ActiveRecord
- module Type
- module Decorator # :nodoc:
- def init_with(coder)
- @subtype = coder['subtype']
- __setobj__(@subtype)
- end
-
- def encode_with(coder)
- coder['subtype'] = __getobj__
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/type/float.rb b/activerecord/lib/active_record/type/float.rb
index 42eb44b9a9..b3928242b7 100644
--- a/activerecord/lib/active_record/type/float.rb
+++ b/activerecord/lib/active_record/type/float.rb
@@ -1,7 +1,7 @@
module ActiveRecord
module Type
class Float < Value # :nodoc:
- include Numeric
+ include Helpers::Numeric
def type
:float
@@ -12,7 +12,13 @@ module ActiveRecord
private
def cast_value(value)
- value.to_f
+ 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
diff --git a/activerecord/lib/active_record/type/helpers.rb b/activerecord/lib/active_record/type/helpers.rb
new file mode 100644
index 0000000000..634d417d13
--- /dev/null
+++ b/activerecord/lib/active_record/type/helpers.rb
@@ -0,0 +1,4 @@
+require 'active_record/type/helpers/accepts_multiparameter_time'
+require 'active_record/type/helpers/numeric'
+require 'active_record/type/helpers/mutable'
+require 'active_record/type/helpers/time_value'
diff --git a/activerecord/lib/active_record/type/helpers/accepts_multiparameter_time.rb b/activerecord/lib/active_record/type/helpers/accepts_multiparameter_time.rb
new file mode 100644
index 0000000000..640943c5e9
--- /dev/null
+++ b/activerecord/lib/active_record/type/helpers/accepts_multiparameter_time.rb
@@ -0,0 +1,30 @@
+module ActiveRecord
+ module Type
+ module Helpers
+ class AcceptsMultiparameterTime < Module # :nodoc:
+ def initialize(defaults: {})
+ define_method(:type_cast_from_user) do |value|
+ if value.is_a?(Hash)
+ value_from_multiparameter_assignment(value)
+ else
+ super(value)
+ end
+ end
+
+ define_method(:value_from_multiparameter_assignment) do |values_hash|
+ defaults.each do |k, v|
+ values_hash[k] ||= v
+ end
+ return unless values_hash[1] && values_hash[2] && values_hash[3]
+ values = values_hash.sort.map(&:last)
+ ::Time.send(
+ ActiveRecord::Base.default_timezone,
+ *values
+ )
+ end
+ private :value_from_multiparameter_assignment
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/helpers/mutable.rb b/activerecord/lib/active_record/type/helpers/mutable.rb
new file mode 100644
index 0000000000..dc37f4a885
--- /dev/null
+++ b/activerecord/lib/active_record/type/helpers/mutable.rb
@@ -0,0 +1,18 @@
+module ActiveRecord
+ module Type
+ module Helpers
+ 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
+end
diff --git a/activerecord/lib/active_record/type/helpers/numeric.rb b/activerecord/lib/active_record/type/helpers/numeric.rb
new file mode 100644
index 0000000000..b0d4f03117
--- /dev/null
+++ b/activerecord/lib/active_record/type/helpers/numeric.rb
@@ -0,0 +1,34 @@
+module ActiveRecord
+ module Type
+ module Helpers
+ module Numeric # :nodoc:
+ 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
+end
diff --git a/activerecord/lib/active_record/type/helpers/time_value.rb b/activerecord/lib/active_record/type/helpers/time_value.rb
new file mode 100644
index 0000000000..6e14c3a9b5
--- /dev/null
+++ b/activerecord/lib/active_record/type/helpers/time_value.rb
@@ -0,0 +1,40 @@
+module ActiveRecord
+ module Type
+ module Helpers
+ module TimeValue # :nodoc:
+ def type_cast_for_schema(value)
+ "'#{value.to_s(:db)}'"
+ end
+
+ def user_input_in_time_zone(value)
+ value.in_time_zone
+ end
+
+ private
+
+ def new_time(year, mon, mday, hour, min, sec, microsec, offset = nil)
+ # Treat 0000-00-00 00:00:00 as nil.
+ return if year.nil? || (year == 0 && mon == 0 && mday == 0)
+
+ if offset
+ time = ::Time.utc(year, mon, mday, hour, min, sec, microsec) rescue nil
+ return unless time
+
+ time -= offset
+ Base.default_timezone == :utc ? time : time.getlocal
+ else
+ ::Time.public_send(Base.default_timezone, year, mon, mday, hour, min, sec, microsec) rescue nil
+ end
+ end
+
+ # Doesn't handle time zones.
+ def fast_string_to_time(string)
+ if string =~ ConnectionAdapters::Column::Format::ISO_DATETIME
+ microsec = ($7.to_r * 1_000_000).to_i
+ new_time $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, microsec
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/integer.rb b/activerecord/lib/active_record/type/integer.rb
index fc260a081a..2ab2402dfd 100644
--- a/activerecord/lib/active_record/type/integer.rb
+++ b/activerecord/lib/active_record/type/integer.rb
@@ -1,7 +1,11 @@
module ActiveRecord
module Type
class Integer < Value # :nodoc:
- include Numeric
+ include Helpers::Numeric
+
+ # Column storage size in bytes.
+ # 4 bytes means a MySQL int or Postgres integer as opposed to smallint etc.
+ DEFAULT_LIMIT = 4
def initialize(*)
super
@@ -12,13 +16,19 @@ module ActiveRecord
:integer
end
- alias type_cast_for_database type_cast
-
def type_cast_from_database(value)
return if value.nil?
value.to_i
end
+ def type_cast_for_database(value)
+ result = type_cast(value)
+ if result
+ ensure_in_range(result)
+ end
+ result
+ end
+
protected
attr_reader :range
@@ -30,20 +40,18 @@ module ActiveRecord
when true then 1
when false then 0
else
- result = value.to_i rescue nil
- ensure_in_range(result) if result
- result
+ value.to_i rescue nil
end
end
def ensure_in_range(value)
unless range.cover?(value)
- raise RangeError, "#{value} is out of range for #{self.class} with limit #{limit || 4}"
+ raise RangeError, "#{value} is out of range for #{self.class} with limit #{limit || DEFAULT_LIMIT}"
end
end
def max_value
- limit = self.limit || 4
+ limit = self.limit || DEFAULT_LIMIT
1 << (limit * 8 - 1) # 8 bits per byte with one bit for sign
end
diff --git a/activerecord/lib/active_record/type/mutable.rb b/activerecord/lib/active_record/type/mutable.rb
deleted file mode 100644
index 066617ea59..0000000000
--- a/activerecord/lib/active_record/type/mutable.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-module ActiveRecord
- module Type
- module Mutable # :nodoc:
- def type_cast_from_user(value)
- type_cast_from_database(type_cast_for_database(value))
- end
-
- # +raw_old_value+ will be the `_before_type_cast` version of the
- # value (likely a string). +new_value+ will be the current, type
- # cast value.
- def changed_in_place?(raw_old_value, new_value)
- raw_old_value != type_cast_for_database(new_value)
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/type/numeric.rb b/activerecord/lib/active_record/type/numeric.rb
deleted file mode 100644
index 674f996f38..0000000000
--- a/activerecord/lib/active_record/type/numeric.rb
+++ /dev/null
@@ -1,36 +0,0 @@
-module ActiveRecord
- module Type
- module Numeric # :nodoc:
- def number?
- true
- end
-
- def type_cast(value)
- value = case value
- when true then 1
- when false then 0
- when ::String then value.presence
- else value
- end
- super(value)
- end
-
- def changed?(old_value, _new_value, new_value_before_type_cast) # :nodoc:
- super || number_to_non_number?(old_value, new_value_before_type_cast)
- end
-
- private
-
- def number_to_non_number?(old_value, new_value_before_type_cast)
- old_value != nil && non_numeric_string?(new_value_before_type_cast)
- end
-
- def non_numeric_string?(value)
- # 'wibble'.to_i will give zero, we want to make sure
- # that we aren't marking int zero to string zero as
- # changed.
- value.to_s !~ /\A-?\d+\.?\d*\z/
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/type/serialized.rb b/activerecord/lib/active_record/type/serialized.rb
index 3cac03464e..6c6c520048 100644
--- a/activerecord/lib/active_record/type/serialized.rb
+++ b/activerecord/lib/active_record/type/serialized.rb
@@ -1,8 +1,7 @@
module ActiveRecord
module Type
class Serialized < DelegateClass(Type::Value) # :nodoc:
- include Mutable
- include Decorator
+ include Helpers::Mutable
attr_reader :subtype, :coder
@@ -36,16 +35,6 @@ module ActiveRecord
ActiveRecord::Store::IndifferentHashAccessor
end
- def init_with(coder)
- @coder = coder['coder']
- super
- end
-
- def encode_with(coder)
- coder['coder'] = @coder
- super
- end
-
private
def default_value?(value)
diff --git a/activerecord/lib/active_record/type/string.rb b/activerecord/lib/active_record/type/string.rb
index cf95e25be0..fbc0af2c5a 100644
--- a/activerecord/lib/active_record/type/string.rb
+++ b/activerecord/lib/active_record/type/string.rb
@@ -21,10 +21,6 @@ module ActiveRecord
end
end
- def text?
- true
- end
-
private
def cast_value(value)
diff --git a/activerecord/lib/active_record/type/time.rb b/activerecord/lib/active_record/type/time.rb
index 41f7d97f0c..19a10021bc 100644
--- a/activerecord/lib/active_record/type/time.rb
+++ b/activerecord/lib/active_record/type/time.rb
@@ -1,12 +1,28 @@
module ActiveRecord
module Type
class Time < Value # :nodoc:
- include TimeValue
+ include Helpers::TimeValue
+ include Helpers::AcceptsMultiparameterTime.new(
+ defaults: { 1 => 1970, 2 => 1, 3 => 1, 4 => 0, 5 => 0 }
+ )
def type
:time
end
+ def user_input_in_time_zone(value)
+ return unless value.present?
+
+ case value
+ when ::String
+ value = "2000-01-01 #{value}"
+ when ::Time
+ value = value.change(year: 2000, day: 1, month: 1)
+ end
+
+ super(value)
+ end
+
private
def cast_value(value)
diff --git a/activerecord/lib/active_record/type/time_value.rb b/activerecord/lib/active_record/type/time_value.rb
deleted file mode 100644
index d611d72dd4..0000000000
--- a/activerecord/lib/active_record/type/time_value.rb
+++ /dev/null
@@ -1,38 +0,0 @@
-module ActiveRecord
- module Type
- module TimeValue # :nodoc:
- def klass
- ::Time
- end
-
- def type_cast_for_schema(value)
- "'#{value.to_s(:db)}'"
- end
-
- private
-
- def new_time(year, mon, mday, hour, min, sec, microsec, offset = nil)
- # Treat 0000-00-00 00:00:00 as nil.
- return if year.nil? || (year == 0 && mon == 0 && mday == 0)
-
- if offset
- time = ::Time.utc(year, mon, mday, hour, min, sec, microsec) rescue nil
- return unless time
-
- time -= offset
- Base.default_timezone == :utc ? time : time.getlocal
- else
- ::Time.public_send(Base.default_timezone, year, mon, mday, hour, min, sec, microsec) rescue nil
- end
- end
-
- # Doesn't handle time zones.
- def fast_string_to_time(string)
- if string =~ ConnectionAdapters::Column::Format::ISO_DATETIME
- microsec = ($7.to_r * 1_000_000).to_i
- new_time $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, microsec
- end
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/type/value.rb b/activerecord/lib/active_record/type/value.rb
index 60ae47db3d..7338920f3b 100644
--- a/activerecord/lib/active_record/type/value.rb
+++ b/activerecord/lib/active_record/type/value.rb
@@ -1,35 +1,37 @@
module ActiveRecord
module Type
- class Value # :nodoc:
+ class Value
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]
+ def initialize(precision: nil, limit: nil, scale: nil)
+ @precision = precision
+ @scale = scale
+ @limit = limit
end
- # The simplified type that this object represents. Returns a symbol such
- # as +:string+ or +:integer+
- def type; end
+ def type # :nodoc:
+ 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.
+ # Convert a value from database input to the appropriate ruby type. The
+ # return value of this method will be returned from
+ # ActiveRecord::AttributeMethods::Read#read_attribute. See also
+ # Value#type_cast and Value#cast_value.
+ #
+ # +value+ The raw input, as provided from the database.
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.
+ # be a string from the form builder, or a ruby object passed to a setter.
+ # There is currently no way to differentiate between which source it came
+ # from.
#
- # Classes which do not need separate type casting behavior for database
- # and user provided values should override +type_cast+ or +cast_value+
- # instead.
+ # The return value of this method will be returned from
+ # ActiveRecord::AttributeMethods::Read#read_attribute. See also:
+ # Value#type_cast and Value#cast_value.
+ #
+ # +value+ The raw input, as provided to the attribute setter.
def type_cast_from_user(value)
type_cast(value)
end
@@ -37,7 +39,7 @@ module ActiveRecord
# 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+
+ # +nil+.
def type_cast_for_database(value)
value
end
@@ -50,21 +52,10 @@ module ActiveRecord
# These predicates are not documented, as I need to look further into
# their use, and see if they can be removed entirely.
- def text? # :nodoc:
- false
- end
-
- 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.
@@ -73,10 +64,23 @@ module ActiveRecord
end
# Determines whether the mutable value has been modified since it was
- # read. Returns +false+ by default. This method should not be overridden
- # directly. Types which return a mutable value should include
- # +Type::Mutable+, which will define this method.
- def changed_in_place?(*)
+ # read. Returns +false+ by default. If your type returns an object
+ # which could be mutated, you should override this method. You will need
+ # to either:
+ #
+ # - pass +new_value+ to Value#type_cast_for_database and compare it to
+ # +raw_old_value+
+ #
+ # or
+ #
+ # - pass +raw_old_value+ to Value#type_cast_from_database and compare it to
+ # +new_value+
+ #
+ # +raw_old_value+ The original value, before being passed to
+ # +type_cast_from_database+.
+ #
+ # +new_value+ The current value, after type casting.
+ def changed_in_place?(raw_old_value, new_value)
false
end
@@ -89,14 +93,17 @@ module ActiveRecord
private
- def type_cast(value)
+ # Convenience method. If you don't need separate behavior for
+ # Value#type_cast_from_database and Value#type_cast_from_user, you can override
+ # this method instead. The default behavior of both methods is to call
+ # this one. See also Value#cast_value.
+ def type_cast(value) # :doc:
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+.
+ # behavior for user and database inputs. Called by Value#type_cast for
+ # values except +nil+.
def cast_value(value) # :doc:
value
end
diff --git a/activerecord/lib/active_record/type_caster/connection.rb b/activerecord/lib/active_record/type_caster/connection.rb
index 9e4a130b40..3878270770 100644
--- a/activerecord/lib/active_record/type_caster/connection.rb
+++ b/activerecord/lib/active_record/type_caster/connection.rb
@@ -1,34 +1,29 @@
module ActiveRecord
module TypeCaster
class Connection
- def initialize(connection, table_name)
- @connection = connection
+ def initialize(klass, table_name)
+ @klass = klass
@table_name = table_name
end
def type_cast_for_database(attribute_name, value)
return value if value.is_a?(Arel::Nodes::BindParam)
- type = type_for(attribute_name)
- type.type_cast_for_database(value)
+ column = column_for(attribute_name)
+ connection.type_cast_from_column(column, value)
end
protected
- attr_reader :connection, :table_name
+ attr_reader :table_name
+ delegate :connection, to: :@klass
private
- def type_for(attribute_name)
+ def column_for(attribute_name)
if connection.schema_cache.table_exists?(table_name)
- column_for(attribute_name).cast_type
- else
- Type::Value.new
+ connection.schema_cache.columns_hash(table_name)[attribute_name.to_s]
end
end
-
- def column_for(attribute_name)
- connection.schema_cache.columns_hash(table_name)[attribute_name.to_s]
- end
end
end
end
diff --git a/activerecord/lib/active_record/validations/length.rb b/activerecord/lib/active_record/validations/length.rb
index ef5a6cbbe7..5991fbad8e 100644
--- a/activerecord/lib/active_record/validations/length.rb
+++ b/activerecord/lib/active_record/validations/length.rb
@@ -2,11 +2,23 @@ module ActiveRecord
module Validations
class LengthValidator < ActiveModel::Validations::LengthValidator # :nodoc:
def validate_each(record, attribute, association_or_value)
+ return unless should_validate?(record) || associations_are_dirty?(record)
if association_or_value.respond_to?(:loaded?) && association_or_value.loaded?
association_or_value = association_or_value.target.reject(&:marked_for_destruction?)
end
super
end
+
+ def associations_are_dirty?(record)
+ attributes.any? do |attribute|
+ value = record.read_attribute_for_validation(attribute)
+ if value.respond_to?(:loaded?) && value.loaded?
+ value.target.any?(&:marked_for_destruction?)
+ else
+ false
+ end
+ end
+ end
end
module ClassMethods
diff --git a/activerecord/lib/active_record/validations/presence.rb b/activerecord/lib/active_record/validations/presence.rb
index 61b30749d9..75d5bd5a35 100644
--- a/activerecord/lib/active_record/validations/presence.rb
+++ b/activerecord/lib/active_record/validations/presence.rb
@@ -2,6 +2,7 @@ module ActiveRecord
module Validations
class PresenceValidator < ActiveModel::Validations::PresenceValidator # :nodoc:
def validate(record)
+ return unless should_validate?(record)
super
attributes.each do |attribute|
next unless record.class._reflect_on_association(attribute)
diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb
index f52f91e89c..a766d77e88 100644
--- a/activerecord/lib/active_record/validations/uniqueness.rb
+++ b/activerecord/lib/active_record/validations/uniqueness.rb
@@ -11,6 +11,7 @@ module ActiveRecord
end
def validate_each(record, attribute, value)
+ return unless should_validate?(record)
finder_class = find_finder_class_for(record)
table = finder_class.arel_table
value = map_enum_attribute(finder_class, attribute, value)
@@ -59,7 +60,8 @@ module ActiveRecord
end
column = klass.columns_hash[attribute_name]
- value = klass.type_for_attribute(attribute_name).type_cast_for_database(value)
+ cast_type = klass.type_for_attribute(attribute_name)
+ value = cast_type.type_cast_for_database(value)
value = klass.connection.type_cast(value)
if value.is_a?(String) && column.limit
value = value.to_s[0, column.limit]
@@ -67,7 +69,7 @@ module ActiveRecord
value = Arel::Nodes::Quoted.new(value)
- comparison = if !options[:case_sensitive] && value && column.text?
+ comparison = if !options[:case_sensitive] && !value.nil?
# will use SQL LOWER function before comparison, unless it detects a case insensitive collation
klass.connection.case_insensitive_comparison(table, attribute, column, value)
else
diff --git a/activerecord/test/active_record/connection_adapters/fake_adapter.rb b/activerecord/test/active_record/connection_adapters/fake_adapter.rb
index 64cde143a1..49a68fb94c 100644
--- a/activerecord/test/active_record/connection_adapters/fake_adapter.rb
+++ b/activerecord/test/active_record/connection_adapters/fake_adapter.rb
@@ -22,15 +22,14 @@ module ActiveRecord
end
def primary_key(table)
- @primary_keys[table]
+ @primary_keys[table] || "id"
end
def merge_column(table_name, name, sql_type = nil, options = {})
@columns[table_name] << ActiveRecord::ConnectionAdapters::Column.new(
name.to_s,
options[:default],
- lookup_cast_type(sql_type.to_s),
- sql_type.to_s,
+ fetch_type_metadata(sql_type),
options[:null])
end
@@ -38,6 +37,10 @@ module ActiveRecord
@columns[table_name]
end
+ def table_exists?(*)
+ true
+ end
+
def active?
true
end
diff --git a/activerecord/test/cases/adapter_test.rb b/activerecord/test/cases/adapter_test.rb
index 99e3d7021d..ecf1368a91 100644
--- a/activerecord/test/cases/adapter_test.rb
+++ b/activerecord/test/cases/adapter_test.rb
@@ -193,7 +193,7 @@ module ActiveRecord
author = Author.create!(name: 'john')
Post.create!(author: author, title: 'foo', body: 'bar')
query = author.posts.where(title: 'foo').select(:title)
- assert_equal({"title" => "foo"}, @connection.select_one(query.arel, nil, query.bind_values))
+ assert_equal({"title" => "foo"}, @connection.select_one(query.arel, nil, query.bound_attributes))
assert_equal({"title" => "foo"}, @connection.select_one(query))
assert @connection.select_all(query).is_a?(ActiveRecord::Result)
assert_equal "foo", @connection.select_value(query)
@@ -203,7 +203,7 @@ module ActiveRecord
def test_select_methods_passing_a_relation
Post.create!(title: 'foo', body: 'bar')
query = Post.where(title: 'foo').select(:title)
- assert_equal({"title" => "foo"}, @connection.select_one(query.arel, nil, query.bind_values))
+ assert_equal({"title" => "foo"}, @connection.select_one(query.arel, nil, query.bound_attributes))
assert_equal({"title" => "foo"}, @connection.select_one(query))
assert @connection.select_all(query).is_a?(ActiveRecord::Result)
assert_equal "foo", @connection.select_value(query)
diff --git a/activerecord/test/cases/adapters/mysql/connection_test.rb b/activerecord/test/cases/adapters/mysql/connection_test.rb
index ce01b16362..4762ef43b5 100644
--- a/activerecord/test/cases/adapters/mysql/connection_test.rb
+++ b/activerecord/test/cases/adapters/mysql/connection_test.rb
@@ -94,7 +94,7 @@ class MysqlConnectionTest < ActiveRecord::TestCase
with_example_table do
@connection.exec_query('INSERT INTO ex (id, data) VALUES (1, "foo")')
result = @connection.exec_query(
- 'SELECT id, data FROM ex WHERE id = ?', nil, [[nil, 1]])
+ 'SELECT id, data FROM ex WHERE id = ?', nil, [ActiveRecord::Relation::QueryAttribute.new("id", 1, ActiveRecord::Type::Value.new)])
assert_equal 1, result.rows.length
assert_equal 2, result.columns.length
@@ -106,10 +106,10 @@ class MysqlConnectionTest < ActiveRecord::TestCase
def test_exec_typecasts_bind_vals
with_example_table do
@connection.exec_query('INSERT INTO ex (id, data) VALUES (1, "foo")')
- column = @connection.columns('ex').find { |col| col.name == 'id' }
+ bind = ActiveRecord::Relation::QueryAttribute.new("id", "1-fuu", ActiveRecord::Type::Integer.new)
result = @connection.exec_query(
- 'SELECT id, data FROM ex WHERE id = ?', nil, [[column, '1-fuu']])
+ 'SELECT id, data FROM ex WHERE id = ?', nil, [bind])
assert_equal 1, result.rows.length
assert_equal 2, result.columns.length
diff --git a/activerecord/test/cases/adapters/mysql/datetime_test.rb b/activerecord/test/cases/adapters/mysql/datetime_test.rb
deleted file mode 100644
index ae00f4e131..0000000000
--- a/activerecord/test/cases/adapters/mysql/datetime_test.rb
+++ /dev/null
@@ -1,87 +0,0 @@
-require 'cases/helper'
-
-if mysql_56?
- class DateTimeTest < ActiveRecord::TestCase
- self.use_transactional_fixtures = false
-
- class Foo < ActiveRecord::Base; end
-
- def test_default_datetime_precision
- ActiveRecord::Base.connection.create_table(:foos, force: true)
- ActiveRecord::Base.connection.add_column :foos, :created_at, :datetime
- ActiveRecord::Base.connection.add_column :foos, :updated_at, :datetime
- assert_nil activerecord_column_option('foos', 'created_at', 'precision')
- end
-
- def test_datetime_data_type_with_precision
- ActiveRecord::Base.connection.create_table(:foos, force: true)
- ActiveRecord::Base.connection.add_column :foos, :created_at, :datetime, precision: 1
- ActiveRecord::Base.connection.add_column :foos, :updated_at, :datetime, precision: 5
- assert_equal 1, activerecord_column_option('foos', 'created_at', 'precision')
- assert_equal 5, activerecord_column_option('foos', 'updated_at', 'precision')
- end
-
- def test_timestamps_helper_with_custom_precision
- ActiveRecord::Base.connection.create_table(:foos, force: true) do |t|
- t.timestamps null: true, precision: 4
- end
- assert_equal 4, activerecord_column_option('foos', 'created_at', 'precision')
- assert_equal 4, activerecord_column_option('foos', 'updated_at', 'precision')
- end
-
- def test_passing_precision_to_datetime_does_not_set_limit
- ActiveRecord::Base.connection.create_table(:foos, force: true) do |t|
- t.timestamps null: true, precision: 4
- end
- assert_nil activerecord_column_option('foos', 'created_at', 'limit')
- assert_nil activerecord_column_option('foos', 'updated_at', 'limit')
- end
-
- def test_invalid_datetime_precision_raises_error
- assert_raises ActiveRecord::ActiveRecordError do
- ActiveRecord::Base.connection.create_table(:foos, force: true) do |t|
- t.timestamps null: true, precision: 7
- end
- end
- end
-
- def test_mysql_agrees_with_activerecord_about_precision
- ActiveRecord::Base.connection.create_table(:foos, force: true) do |t|
- t.timestamps null: true, precision: 4
- end
- assert_equal 4, mysql_datetime_precision('foos', 'created_at')
- assert_equal 4, mysql_datetime_precision('foos', 'updated_at')
- end
-
- def test_formatting_datetime_according_to_precision
- ActiveRecord::Base.connection.create_table(:foos, force: true) do |t|
- t.datetime :created_at, precision: 0
- t.datetime :updated_at, precision: 4
- end
- date = ::Time.utc(2014, 8, 17, 12, 30, 0, 999999)
- Foo.create!(created_at: date, updated_at: date)
- assert foo = Foo.find_by(created_at: date)
- assert_equal date.to_s, foo.created_at.to_s
- assert_equal date.to_s, foo.updated_at.to_s
- assert_equal 000000, foo.created_at.usec
- assert_equal 999900, foo.updated_at.usec
- end
-
- private
-
- def mysql_datetime_precision(table_name, column_name)
- results = ActiveRecord::Base.connection.exec_query("SELECT column_name, datetime_precision FROM information_schema.columns WHERE table_name = '#{table_name}'")
- result = results.find do |result_hash|
- result_hash["column_name"] == column_name
- end
- result && result["datetime_precision"]
- end
-
- def activerecord_column_option(tablename, column_name, option)
- result = ActiveRecord::Base.connection.columns(tablename).find do |column|
- column.name == column_name
- end
- result && result.send(option)
- end
- end
-end
diff --git a/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb b/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb
index 85db8f4614..7d0bd24ba7 100644
--- a/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb
+++ b/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb
@@ -1,4 +1,3 @@
-# encoding: utf-8
require "cases/helper"
require 'support/ddl_helper'
@@ -129,10 +128,10 @@ module ActiveRecord
private
def insert(ctx, data, table='ex')
- binds = data.map { |name, value|
- [ctx.columns(table).find { |x| x.name == name }, value]
+ binds = data.map { |name, value|
+ Relation::QueryAttribute.new(name, value, Type::Value.new)
}
- columns = binds.map(&:first).map(&:name)
+ columns = binds.map(&:name)
sql = "INSERT INTO #{table} (#{columns.join(", ")})
VALUES (#{(['?'] * columns.length).join(', ')})"
diff --git a/activerecord/test/cases/adapters/mysql2/connection_test.rb b/activerecord/test/cases/adapters/mysql2/connection_test.rb
index d261e2db55..ff8ce93248 100644
--- a/activerecord/test/cases/adapters/mysql2/connection_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/connection_test.rb
@@ -122,11 +122,4 @@ class MysqlConnectionTest < ActiveRecord::TestCase
ensure
@connection.execute "DROP TABLE `bar_baz`"
end
-
- if mysql_56?
- def test_quote_time_usec
- assert_equal "'1970-01-01 00:00:00.000000'", @connection.quote(Time.at(0))
- assert_equal "'1970-01-01 00:00:00.000000'", @connection.quote(Time.at(0).to_datetime)
- end
- end
end
diff --git a/activerecord/test/cases/adapters/mysql2/datetime_test.rb b/activerecord/test/cases/adapters/mysql2/datetime_test.rb
deleted file mode 100644
index ae00f4e131..0000000000
--- a/activerecord/test/cases/adapters/mysql2/datetime_test.rb
+++ /dev/null
@@ -1,87 +0,0 @@
-require 'cases/helper'
-
-if mysql_56?
- class DateTimeTest < ActiveRecord::TestCase
- self.use_transactional_fixtures = false
-
- class Foo < ActiveRecord::Base; end
-
- def test_default_datetime_precision
- ActiveRecord::Base.connection.create_table(:foos, force: true)
- ActiveRecord::Base.connection.add_column :foos, :created_at, :datetime
- ActiveRecord::Base.connection.add_column :foos, :updated_at, :datetime
- assert_nil activerecord_column_option('foos', 'created_at', 'precision')
- end
-
- def test_datetime_data_type_with_precision
- ActiveRecord::Base.connection.create_table(:foos, force: true)
- ActiveRecord::Base.connection.add_column :foos, :created_at, :datetime, precision: 1
- ActiveRecord::Base.connection.add_column :foos, :updated_at, :datetime, precision: 5
- assert_equal 1, activerecord_column_option('foos', 'created_at', 'precision')
- assert_equal 5, activerecord_column_option('foos', 'updated_at', 'precision')
- end
-
- def test_timestamps_helper_with_custom_precision
- ActiveRecord::Base.connection.create_table(:foos, force: true) do |t|
- t.timestamps null: true, precision: 4
- end
- assert_equal 4, activerecord_column_option('foos', 'created_at', 'precision')
- assert_equal 4, activerecord_column_option('foos', 'updated_at', 'precision')
- end
-
- def test_passing_precision_to_datetime_does_not_set_limit
- ActiveRecord::Base.connection.create_table(:foos, force: true) do |t|
- t.timestamps null: true, precision: 4
- end
- assert_nil activerecord_column_option('foos', 'created_at', 'limit')
- assert_nil activerecord_column_option('foos', 'updated_at', 'limit')
- end
-
- def test_invalid_datetime_precision_raises_error
- assert_raises ActiveRecord::ActiveRecordError do
- ActiveRecord::Base.connection.create_table(:foos, force: true) do |t|
- t.timestamps null: true, precision: 7
- end
- end
- end
-
- def test_mysql_agrees_with_activerecord_about_precision
- ActiveRecord::Base.connection.create_table(:foos, force: true) do |t|
- t.timestamps null: true, precision: 4
- end
- assert_equal 4, mysql_datetime_precision('foos', 'created_at')
- assert_equal 4, mysql_datetime_precision('foos', 'updated_at')
- end
-
- def test_formatting_datetime_according_to_precision
- ActiveRecord::Base.connection.create_table(:foos, force: true) do |t|
- t.datetime :created_at, precision: 0
- t.datetime :updated_at, precision: 4
- end
- date = ::Time.utc(2014, 8, 17, 12, 30, 0, 999999)
- Foo.create!(created_at: date, updated_at: date)
- assert foo = Foo.find_by(created_at: date)
- assert_equal date.to_s, foo.created_at.to_s
- assert_equal date.to_s, foo.updated_at.to_s
- assert_equal 000000, foo.created_at.usec
- assert_equal 999900, foo.updated_at.usec
- end
-
- private
-
- def mysql_datetime_precision(table_name, column_name)
- results = ActiveRecord::Base.connection.exec_query("SELECT column_name, datetime_precision FROM information_schema.columns WHERE table_name = '#{table_name}'")
- result = results.find do |result_hash|
- result_hash["column_name"] == column_name
- end
- result && result["datetime_precision"]
- end
-
- def activerecord_column_option(tablename, column_name, option)
- result = ActiveRecord::Base.connection.columns(tablename).find do |column|
- column.name == column_name
- end
- result && result.send(option)
- end
- end
-end
diff --git a/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb b/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb
index 9c49599d34..271b570eb5 100644
--- a/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb
@@ -16,23 +16,31 @@ module ActiveRecord
def test_initializes_schema_migrations_for_encoding_utf8mb4
smtn = ActiveRecord::Migrator.schema_migrations_table_name
- connection.drop_table(smtn) if connection.table_exists?(smtn)
+ connection.drop_table smtn, if_exists: true
- config = connection.instance_variable_get(:@config)
- original_encoding = config[:encoding]
+ database_name = connection.current_database
+ database_info = connection.select_one("SELECT * FROM information_schema.schemata WHERE schema_name = '#{database_name}'")
+
+ original_charset = database_info["DEFAULT_CHARACTER_SET_NAME"]
+ original_collation = database_info["DEFAULT_COLLATION_NAME"]
+
+ execute("ALTER DATABASE #{database_name} DEFAULT CHARACTER SET utf8mb4")
- config[:encoding] = 'utf8mb4'
connection.initialize_schema_migrations_table
assert connection.column_exists?(smtn, :version, :string, limit: Mysql2Adapter::MAX_INDEX_LENGTH_FOR_UTF8MB4)
ensure
- config[:encoding] = original_encoding
+ execute("ALTER DATABASE #{database_name} DEFAULT CHARACTER SET #{original_charset} COLLATE #{original_collation}")
end
private
def connection
@connection ||= ActiveRecord::Base.connection
end
+
+ def execute(sql)
+ connection.execute(sql)
+ end
end
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/array_test.rb b/activerecord/test/cases/adapters/postgresql/array_test.rb
index 77055f5b7a..3d5b7e5137 100644
--- a/activerecord/test/cases/adapters/postgresql/array_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/array_test.rb
@@ -1,4 +1,3 @@
-# encoding: utf-8
require "cases/helper"
require 'support/schema_dumping_helper'
@@ -25,6 +24,7 @@ class PostgresqlArrayTest < ActiveRecord::TestCase
end
end
@column = PgArray.columns_hash['tags']
+ @type = PgArray.type_for_attribute("tags")
end
teardown do
@@ -36,13 +36,11 @@ class PostgresqlArrayTest < ActiveRecord::TestCase
assert_equal :string, @column.type
assert_equal "character varying", @column.sql_type
assert @column.array?
- assert_not @column.number?
- assert_not @column.binary?
+ assert_not @type.binary?
ratings_column = PgArray.columns_hash['ratings']
assert_equal :integer, ratings_column.type
assert ratings_column.array?
- assert_not ratings_column.number?
end
def test_default
@@ -94,9 +92,9 @@ class PostgresqlArrayTest < ActiveRecord::TestCase
end
def test_type_cast_array
- assert_equal(['1', '2', '3'], @column.type_cast_from_database('{1,2,3}'))
- assert_equal([], @column.type_cast_from_database('{}'))
- assert_equal([nil], @column.type_cast_from_database('{NULL}'))
+ assert_equal(['1', '2', '3'], @type.type_cast_from_database('{1,2,3}'))
+ assert_equal([], @type.type_cast_from_database('{}'))
+ assert_equal([nil], @type.type_cast_from_database('{NULL}'))
end
def test_type_cast_integers
@@ -112,8 +110,8 @@ class PostgresqlArrayTest < ActiveRecord::TestCase
def test_schema_dump_with_shorthand
output = dump_table_schema "pg_arrays"
- assert_match %r[t.string\s+"tags",\s+array: true], output
- assert_match %r[t.integer\s+"ratings",\s+array: true], output
+ assert_match %r[t\.string\s+"tags",\s+array: true], output
+ assert_match %r[t\.integer\s+"ratings",\s+array: true], output
end
def test_select_with_strings
@@ -208,7 +206,7 @@ class PostgresqlArrayTest < ActiveRecord::TestCase
x = PgArray.create!(tags: tags)
x.reload
- assert_equal x.tags_before_type_cast, PgArray.columns_hash['tags'].type_cast_for_database(tags)
+ assert_equal x.tags_before_type_cast, PgArray.type_for_attribute('tags').type_cast_for_database(tags)
end
def test_quoting_non_standard_delimiters
diff --git a/activerecord/test/cases/adapters/postgresql/bit_string_test.rb b/activerecord/test/cases/adapters/postgresql/bit_string_test.rb
index f154ba4cdc..6c6b4dc22a 100644
--- a/activerecord/test/cases/adapters/postgresql/bit_string_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/bit_string_test.rb
@@ -1,4 +1,3 @@
-# -*- coding: utf-8 -*-
require "cases/helper"
require 'support/connection_helper'
require 'support/schema_dumping_helper'
@@ -14,6 +13,8 @@ class PostgresqlBitStringTest < ActiveRecord::TestCase
@connection.create_table('postgresql_bit_strings', :force => true) do |t|
t.bit :a_bit, default: "00000011", limit: 8
t.bit_varying :a_bit_varying, default: "0011", limit: 4
+ t.bit :another_bit
+ t.bit_varying :another_bit_varying
end
end
@@ -26,18 +27,20 @@ class PostgresqlBitStringTest < ActiveRecord::TestCase
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?
+
+ type = PostgresqlBitString.type_for_attribute("a_bit")
+ assert_not type.binary?
end
def test_bit_string_varying_column
column = PostgresqlBitString.columns_hash["a_bit_varying"]
assert_equal :bit_varying, column.type
assert_equal "bit varying(4)", column.sql_type
- assert_not column.number?
- assert_not column.binary?
assert_not column.array?
+
+ type = PostgresqlBitString.type_for_attribute("a_bit_varying")
+ assert_not type.binary?
end
def test_default
diff --git a/activerecord/test/cases/adapters/postgresql/bytea_test.rb b/activerecord/test/cases/adapters/postgresql/bytea_test.rb
index aeebec034d..678a476661 100644
--- a/activerecord/test/cases/adapters/postgresql/bytea_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/bytea_test.rb
@@ -1,4 +1,3 @@
-# encoding: utf-8
require "cases/helper"
class PostgresqlByteaTest < ActiveRecord::TestCase
@@ -17,6 +16,7 @@ class PostgresqlByteaTest < ActiveRecord::TestCase
end
end
@column = ByteaDataType.columns_hash['payload']
+ @type = ByteaDataType.type_for_attribute("payload")
end
teardown do
@@ -40,16 +40,16 @@ class PostgresqlByteaTest < ActiveRecord::TestCase
data = "\u001F\x8B"
assert_equal('UTF-8', data.encoding.name)
- assert_equal('ASCII-8BIT', @column.type_cast_from_database(data).encoding.name)
+ assert_equal('ASCII-8BIT', @type.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_from_database(data))
+ assert_equal(data, @type.type_cast_from_database(data))
end
def test_type_case_nil
- assert_equal(nil, @column.type_cast_from_database(nil))
+ assert_equal(nil, @type.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 5a8083f7a7..0ee2a21484 100644
--- a/activerecord/test/cases/adapters/postgresql/citext_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/citext_test.rb
@@ -1,4 +1,3 @@
-# encoding: utf-8
require 'cases/helper'
require 'support/schema_dumping_helper'
@@ -32,9 +31,10 @@ if ActiveRecord::Base.connection.supports_extensions?
column = Citext.columns_hash['cival']
assert_equal :citext, column.type
assert_equal 'citext', column.sql_type
- assert_not column.number?
- assert_not column.binary?
assert_not column.array?
+
+ type = Citext.type_for_attribute('cival')
+ assert_not type.binary?
end
def test_change_table_supports_json
@@ -72,7 +72,7 @@ if ActiveRecord::Base.connection.supports_extensions?
def test_schema_dump_with_shorthand
output = dump_table_schema("citexts")
- assert_match %r[t.citext "cival"], output
+ assert_match %r[t\.citext "cival"], output
end
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/composite_test.rb b/activerecord/test/cases/adapters/postgresql/composite_test.rb
index 24c1969dee..83dfb18e95 100644
--- a/activerecord/test/cases/adapters/postgresql/composite_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/composite_test.rb
@@ -1,4 +1,3 @@
-# -*- coding: utf-8 -*-
require "cases/helper"
require 'support/connection_helper'
@@ -50,9 +49,10 @@ class PostgresqlCompositeTest < ActiveRecord::TestCase
column = PostgresqlComposite.columns_hash["address"]
assert_nil column.type
assert_equal "full_address", column.sql_type
- assert_not column.number?
- assert_not column.binary?
assert_not column.array?
+
+ type = PostgresqlComposite.type_for_attribute("address")
+ assert_not type.binary?
end
def test_composite_mapping
@@ -111,9 +111,10 @@ class PostgresqlCompositeWithCustomOIDTest < ActiveRecord::TestCase
column = PostgresqlComposite.columns_hash["address"]
assert_equal :full_address, column.type
assert_equal "full_address", column.sql_type
- assert_not column.number?
- assert_not column.binary?
assert_not column.array?
+
+ type = PostgresqlComposite.type_for_attribute("address")
+ assert_not type.binary?
end
def test_composite_mapping
diff --git a/activerecord/test/cases/adapters/postgresql/connection_test.rb b/activerecord/test/cases/adapters/postgresql/connection_test.rb
index ab7fd3c6d5..7bf8d12eae 100644
--- a/activerecord/test/cases/adapters/postgresql/connection_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/connection_test.rb
@@ -126,11 +126,11 @@ module ActiveRecord
end
def test_statement_key_is_logged
- bindval = 1
- @connection.exec_query('SELECT $1::integer', 'SQL', [[nil, bindval]])
+ bind = Relation::QueryAttribute.new(nil, 1, Type::Value.new)
+ @connection.exec_query('SELECT $1::integer', 'SQL', [bind])
name = @subscriber.payloads.last[:statement_name]
assert name
- res = @connection.exec_query("EXPLAIN (FORMAT JSON) EXECUTE #{name}(#{bindval})")
+ res = @connection.exec_query("EXPLAIN (FORMAT JSON) EXECUTE #{name}(1)")
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/domain_test.rb b/activerecord/test/cases/adapters/postgresql/domain_test.rb
index ebb04814bb..b7d776b40c 100644
--- a/activerecord/test/cases/adapters/postgresql/domain_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/domain_test.rb
@@ -1,4 +1,3 @@
-# -*- coding: utf-8 -*-
require "cases/helper"
require 'support/connection_helper'
@@ -29,9 +28,10 @@ class PostgresqlDomainTest < ActiveRecord::TestCase
column = PostgresqlDomain.columns_hash["price"]
assert_equal :decimal, column.type
assert_equal "custom_money", column.sql_type
- assert column.number?
- assert_not column.binary?
assert_not column.array?
+
+ type = PostgresqlDomain.type_for_attribute("price")
+ assert_not type.binary?
end
def test_domain_acts_like_basetype
diff --git a/activerecord/test/cases/adapters/postgresql/enum_test.rb b/activerecord/test/cases/adapters/postgresql/enum_test.rb
index 88b3b2cc0e..acb09b0607 100644
--- a/activerecord/test/cases/adapters/postgresql/enum_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/enum_test.rb
@@ -1,4 +1,3 @@
-# -*- coding: utf-8 -*-
require "cases/helper"
require 'support/connection_helper'
@@ -31,9 +30,10 @@ class PostgresqlEnumTest < ActiveRecord::TestCase
column = PostgresqlEnum.columns_hash["current_mood"]
assert_equal :enum, column.type
assert_equal "mood", column.sql_type
- assert_not column.number?
- assert_not column.binary?
assert_not column.array?
+
+ type = PostgresqlEnum.type_for_attribute("current_mood")
+ assert_not type.binary?
end
def test_enum_defaults
diff --git a/activerecord/test/cases/adapters/postgresql/full_text_test.rb b/activerecord/test/cases/adapters/postgresql/full_text_test.rb
index a370a5adc6..81891a90fa 100644
--- a/activerecord/test/cases/adapters/postgresql/full_text_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/full_text_test.rb
@@ -1,4 +1,3 @@
-# encoding: utf-8
require "cases/helper"
require 'support/schema_dumping_helper'
@@ -21,9 +20,10 @@ class PostgresqlFullTextTest < ActiveRecord::TestCase
column = Tsvector.columns_hash["text_vector"]
assert_equal :tsvector, column.type
assert_equal "tsvector", column.sql_type
- assert_not column.number?
- assert_not column.binary?
assert_not column.array?
+
+ type = Tsvector.type_for_attribute("text_vector")
+ assert_not type.binary?
end
def test_update_tsvector
@@ -39,6 +39,6 @@ class PostgresqlFullTextTest < ActiveRecord::TestCase
def test_schema_dump_with_shorthand
output = dump_table_schema("tsvectors")
- assert_match %r{t.tsvector "text_vector"}, output
+ assert_match %r{t\.tsvector "text_vector"}, output
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/geometric_test.rb b/activerecord/test/cases/adapters/postgresql/geometric_test.rb
index ed2bf554bb..4b25381a83 100644
--- a/activerecord/test/cases/adapters/postgresql/geometric_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/geometric_test.rb
@@ -1,4 +1,3 @@
-# -*- coding: utf-8 -*-
require "cases/helper"
require 'support/connection_helper'
require 'support/schema_dumping_helper'
@@ -26,9 +25,10 @@ class PostgresqlPointTest < ActiveRecord::TestCase
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?
+
+ type = PostgresqlPoint.type_for_attribute("x")
+ assert_not type.binary?
end
def test_default
diff --git a/activerecord/test/cases/adapters/postgresql/hstore_test.rb b/activerecord/test/cases/adapters/postgresql/hstore_test.rb
index a0aa10630c..11053a6e38 100644
--- a/activerecord/test/cases/adapters/postgresql/hstore_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/hstore_test.rb
@@ -1,4 +1,3 @@
-# encoding: utf-8
require "cases/helper"
require 'support/schema_dumping_helper'
@@ -29,6 +28,7 @@ if ActiveRecord::Base.connection.supports_extensions?
end
end
@column = Hstore.columns_hash['tags']
+ @type = Hstore.type_for_attribute("tags")
end
teardown do
@@ -54,9 +54,9 @@ if ActiveRecord::Base.connection.supports_extensions?
def test_column
assert_equal :hstore, @column.type
assert_equal "hstore", @column.sql_type
- assert_not @column.number?
- assert_not @column.binary?
assert_not @column.array?
+
+ assert_not @type.binary?
end
def test_default
@@ -110,10 +110,10 @@ if ActiveRecord::Base.connection.supports_extensions?
end
def test_type_cast_hstore
- assert_equal({'1' => '2'}, @column.type_cast_from_database("\"1\"=>\"2\""))
- assert_equal({}, @column.type_cast_from_database(""))
- assert_equal({'key'=>nil}, @column.type_cast_from_database('key => NULL'))
- assert_equal({'c'=>'}','"a"'=>'b "a b'}, @column.type_cast_from_database(%q(c=>"}", "\"a\""=>"b \"a b")))
+ assert_equal({'1' => '2'}, @type.type_cast_from_database("\"1\"=>\"2\""))
+ assert_equal({}, @type.type_cast_from_database(""))
+ assert_equal({'key'=>nil}, @type.type_cast_from_database('key => NULL'))
+ assert_equal({'c'=>'}','"a"'=>'b "a b'}, @type.type_cast_from_database(%q(c=>"}", "\"a\""=>"b \"a b")))
end
def test_with_store_accessors
@@ -165,47 +165,47 @@ if ActiveRecord::Base.connection.supports_extensions?
end
def test_gen1
- assert_equal(%q(" "=>""), @column.cast_type.type_cast_for_database({' '=>''}))
+ assert_equal(%q(" "=>""), @type.type_cast_for_database({' '=>''}))
end
def test_gen2
- assert_equal(%q(","=>""), @column.cast_type.type_cast_for_database({','=>''}))
+ assert_equal(%q(","=>""), @type.type_cast_for_database({','=>''}))
end
def test_gen3
- assert_equal(%q("="=>""), @column.cast_type.type_cast_for_database({'='=>''}))
+ assert_equal(%q("="=>""), @type.type_cast_for_database({'='=>''}))
end
def test_gen4
- assert_equal(%q(">"=>""), @column.cast_type.type_cast_for_database({'>'=>''}))
+ assert_equal(%q(">"=>""), @type.type_cast_for_database({'>'=>''}))
end
def test_parse1
- assert_equal({'a'=>nil,'b'=>nil,'c'=>'NuLl','null'=>'c'}, @column.type_cast_from_database('a=>null,b=>NuLl,c=>"NuLl",null=>c'))
+ assert_equal({'a'=>nil,'b'=>nil,'c'=>'NuLl','null'=>'c'}, @type.type_cast_from_database('a=>null,b=>NuLl,c=>"NuLl",null=>c'))
end
def test_parse2
- assert_equal({" " => " "}, @column.type_cast_from_database("\\ =>\\ "))
+ assert_equal({" " => " "}, @type.type_cast_from_database("\\ =>\\ "))
end
def test_parse3
- assert_equal({"=" => ">"}, @column.type_cast_from_database("==>>"))
+ assert_equal({"=" => ">"}, @type.type_cast_from_database("==>>"))
end
def test_parse4
- assert_equal({"=a"=>"q=w"}, @column.type_cast_from_database('\=a=>q=w'))
+ assert_equal({"=a"=>"q=w"}, @type.type_cast_from_database('\=a=>q=w'))
end
def test_parse5
- assert_equal({"=a"=>"q=w"}, @column.type_cast_from_database('"=a"=>q\=w'))
+ assert_equal({"=a"=>"q=w"}, @type.type_cast_from_database('"=a"=>q\=w'))
end
def test_parse6
- assert_equal({"\"a"=>"q>w"}, @column.type_cast_from_database('"\"a"=>q>w'))
+ assert_equal({"\"a"=>"q>w"}, @type.type_cast_from_database('"\"a"=>q>w'))
end
def test_parse7
- assert_equal({"\"a"=>"q\"w"}, @column.type_cast_from_database('\"a=>q"w'))
+ assert_equal({"\"a"=>"q\"w"}, @type.type_cast_from_database('\"a=>q"w'))
end
def test_rewrite
@@ -317,7 +317,7 @@ if ActiveRecord::Base.connection.supports_extensions?
def test_schema_dump_with_shorthand
output = dump_table_schema("hstores")
- assert_match %r[t.hstore "tags",\s+default: {}], output
+ assert_match %r[t\.hstore "tags",\s+default: {}], output
end
private
diff --git a/activerecord/test/cases/adapters/postgresql/json_test.rb b/activerecord/test/cases/adapters/postgresql/json_test.rb
index 7be7e00463..3b123f979e 100644
--- a/activerecord/test/cases/adapters/postgresql/json_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/json_test.rb
@@ -1,4 +1,4 @@
-# encoding: utf-8
+# -*- coding: utf-8 -*-
require "cases/helper"
require 'support/schema_dumping_helper'
@@ -14,29 +14,28 @@ module PostgresqlJSONSharedTestCases
def setup
@connection = ActiveRecord::Base.connection
begin
- @connection.transaction do
- @connection.create_table('json_data_type') do |t|
- t.public_send column_type, 'payload', default: {} # t.json 'payload', default: {}
- t.public_send column_type, 'settings' # t.json 'settings'
- end
+ @connection.create_table('json_data_type') do |t|
+ t.public_send column_type, 'payload', default: {} # t.json 'payload', default: {}
+ t.public_send column_type, 'settings' # t.json 'settings'
end
rescue ActiveRecord::StatementInvalid
- skip "do not test on PG without json"
+ skip "do not test on PostgreSQL without #{column_type} type."
end
@column = JsonDataType.columns_hash['payload']
end
def teardown
- @connection.execute 'drop table if exists json_data_type'
+ @connection.drop_table :json_data_type, if_exists: true
end
def test_column
column = JsonDataType.columns_hash["payload"]
assert_equal column_type, column.type
assert_equal column_type.to_s, column.sql_type
- assert_not column.number?
- assert_not column.binary?
assert_not column.array?
+
+ type = JsonDataType.type_for_attribute("payload")
+ assert_not type.binary?
end
def test_default
@@ -66,7 +65,7 @@ module PostgresqlJSONSharedTestCases
def test_schema_dumping
output = dump_table_schema("json_data_type")
- assert_match(/t.#{column_type.to_s}\s+"payload",\s+default: {}/, output)
+ assert_match(/t\.#{column_type.to_s}\s+"payload",\s+default: {}/, output)
end
def test_cast_value_on_write
@@ -78,16 +77,16 @@ module PostgresqlJSONSharedTestCases
end
def test_type_cast_json
- column = JsonDataType.columns_hash["payload"]
+ type = JsonDataType.type_for_attribute("payload")
data = "{\"a_key\":\"a_value\"}"
- hash = column.type_cast_from_database(data)
+ hash = type.type_cast_from_database(data)
assert_equal({'a_key' => 'a_value'}, hash)
- assert_equal({'a_key' => 'a_value'}, column.type_cast_from_database(data))
+ assert_equal({'a_key' => 'a_value'}, type.type_cast_from_database(data))
- assert_equal({}, column.type_cast_from_database("{}"))
- assert_equal({'key'=>nil}, column.type_cast_from_database('{"key": null}'))
- assert_equal({'c'=>'}','"a"'=>'b "a b'}, column.type_cast_from_database(%q({"c":"}", "\"a\"":"b \"a b"})))
+ assert_equal({}, type.type_cast_from_database("{}"))
+ assert_equal({'key'=>nil}, type.type_cast_from_database('{"key": null}'))
+ assert_equal({'c'=>'}','"a"'=>'b "a b'}, type.type_cast_from_database(%q({"c":"}", "\"a\"":"b \"a b"})))
end
def test_rewrite
@@ -179,6 +178,14 @@ module PostgresqlJSONSharedTestCases
assert_equal({ 'one' => 'two', 'three' => 'four' }, json.payload)
assert_not json.changed?
end
+
+ def test_assigning_invalid_json
+ json = JsonDataType.new
+
+ json.payload = 'foo'
+
+ assert_nil json.payload
+ end
end
class PostgresqlJSONTest < ActiveRecord::TestCase
diff --git a/activerecord/test/cases/adapters/postgresql/ltree_test.rb b/activerecord/test/cases/adapters/postgresql/ltree_test.rb
index 771a825840..2b3823f9f1 100644
--- a/activerecord/test/cases/adapters/postgresql/ltree_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/ltree_test.rb
@@ -1,4 +1,3 @@
-# encoding: utf-8
require "cases/helper"
require 'support/schema_dumping_helper'
@@ -30,9 +29,10 @@ class PostgresqlLtreeTest < ActiveRecord::TestCase
column = Ltree.columns_hash['path']
assert_equal :ltree, column.type
assert_equal "ltree", column.sql_type
- assert_not column.number?
- assert_not column.binary?
assert_not column.array?
+
+ type = Ltree.type_for_attribute('path')
+ assert_not type.binary?
end
def test_write
@@ -48,6 +48,6 @@ class PostgresqlLtreeTest < ActiveRecord::TestCase
def test_schema_dump_with_shorthand
output = dump_table_schema("ltrees")
- assert_match %r[t.ltree "path"], output
+ assert_match %r[t\.ltree "path"], output
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/money_test.rb b/activerecord/test/cases/adapters/postgresql/money_test.rb
index f3a24eee85..ba9af4be6f 100644
--- a/activerecord/test/cases/adapters/postgresql/money_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/money_test.rb
@@ -1,4 +1,3 @@
-# encoding: utf-8
require "cases/helper"
require 'support/schema_dumping_helper'
@@ -11,8 +10,8 @@ class PostgresqlMoneyTest < ActiveRecord::TestCase
@connection = ActiveRecord::Base.connection
@connection.execute("set lc_monetary = 'C'")
@connection.create_table('postgresql_moneys', force: true) do |t|
- t.column "wealth", "money"
- t.column "depth", "money", default: "150.55"
+ t.money "wealth"
+ t.money "depth", default: "150.55"
end
end
@@ -25,9 +24,10 @@ class PostgresqlMoneyTest < ActiveRecord::TestCase
assert_equal :money, column.type
assert_equal "money", column.sql_type
assert_equal 2, column.scale
- assert column.number?
- assert_not column.binary?
assert_not column.array?
+
+ type = PostgresqlMoney.type_for_attribute("wealth")
+ assert_not type.binary?
end
def test_default
@@ -46,17 +46,17 @@ class PostgresqlMoneyTest < ActiveRecord::TestCase
end
def test_money_type_cast
- column = PostgresqlMoney.columns_hash['wealth']
- assert_equal(12345678.12, column.type_cast_from_user("$12,345,678.12"))
- assert_equal(12345678.12, column.type_cast_from_user("$12.345.678,12"))
- assert_equal(-1.15, column.type_cast_from_user("-$1.15"))
- assert_equal(-2.25, column.type_cast_from_user("($2.25)"))
+ type = PostgresqlMoney.type_for_attribute('wealth')
+ assert_equal(12345678.12, type.type_cast_from_user("$12,345,678.12"))
+ assert_equal(12345678.12, type.type_cast_from_user("$12.345.678,12"))
+ assert_equal(-1.15, type.type_cast_from_user("-$1.15"))
+ assert_equal(-2.25, type.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
+ assert_match %r{t\.money\s+"depth",\s+scale: 2,\s+default: 150\.55$}, output
end
def test_create_and_update_money
diff --git a/activerecord/test/cases/adapters/postgresql/network_test.rb b/activerecord/test/cases/adapters/postgresql/network_test.rb
index daa590f369..4cd2d4d5f3 100644
--- a/activerecord/test/cases/adapters/postgresql/network_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/network_test.rb
@@ -1,4 +1,3 @@
-# encoding: utf-8
require "cases/helper"
require 'support/schema_dumping_helper'
@@ -23,27 +22,30 @@ class PostgresqlNetworkTest < ActiveRecord::TestCase
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?
+
+ type = PostgresqlNetworkAddress.type_for_attribute("cidr_address")
+ assert_not type.binary?
end
def test_inet_column
column = PostgresqlNetworkAddress.columns_hash["inet_address"]
assert_equal :inet, column.type
assert_equal "inet", column.sql_type
- assert_not column.number?
- assert_not column.binary?
assert_not column.array?
+
+ type = PostgresqlNetworkAddress.type_for_attribute("inet_address")
+ assert_not type.binary?
end
def test_macaddr_column
column = PostgresqlNetworkAddress.columns_hash["mac_address"]
assert_equal :macaddr, column.type
assert_equal "macaddr", column.sql_type
- assert_not column.number?
- assert_not column.binary?
assert_not column.array?
+
+ type = PostgresqlNetworkAddress.type_for_attribute("mac_address")
+ assert_not type.binary?
end
def test_network_types
@@ -85,8 +87,8 @@ class PostgresqlNetworkTest < ActiveRecord::TestCase
def test_schema_dump_with_shorthand
output = dump_table_schema("postgresql_network_addresses")
- assert_match %r{t.inet\s+"inet_address",\s+default: "192.168.1.1"}, output
- assert_match %r{t.cidr\s+"cidr_address",\s+default: "192.168.1.0/24"}, output
- assert_match %r{t.macaddr\s+"mac_address",\s+default: "ff:ff:ff:ff:ff:ff"}, output
+ assert_match %r{t\.inet\s+"inet_address",\s+default: "192\.168\.1\.1"}, output
+ assert_match %r{t\.cidr\s+"cidr_address",\s+default: "192\.168\.1\.0/24"}, output
+ assert_match %r{t\.macaddr\s+"mac_address",\s+default: "ff:ff:ff:ff:ff:ff"}, output
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb
index 6bb2b26cd5..564712522d 100644
--- a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb
@@ -1,4 +1,3 @@
-# encoding: utf-8
require "cases/helper"
require 'support/ddl_helper'
require 'support/connection_helper'
@@ -284,7 +283,7 @@ module ActiveRecord
string = @connection.quote('foo')
@connection.exec_query("INSERT INTO ex (id, data) VALUES (1, #{string})")
result = @connection.exec_query(
- 'SELECT id, data FROM ex WHERE id = $1', nil, [[nil, 1]])
+ 'SELECT id, data FROM ex WHERE id = $1', nil, [bind_param(1)])
assert_equal 1, result.rows.length
assert_equal 2, result.columns.length
@@ -298,9 +297,9 @@ module ActiveRecord
string = @connection.quote('foo')
@connection.exec_query("INSERT INTO ex (id, data) VALUES (1, #{string})")
- column = @connection.columns('ex').find { |col| col.name == 'id' }
+ bind = ActiveRecord::Relation::QueryAttribute.new("id", "1-fuu", ActiveRecord::Type::Integer.new)
result = @connection.exec_query(
- 'SELECT id, data FROM ex WHERE id = $1', nil, [[column, '1-fuu']])
+ 'SELECT id, data FROM ex WHERE id = $1', nil, [bind])
assert_equal 1, result.rows.length
assert_equal 2, result.columns.length
@@ -437,10 +436,10 @@ module ActiveRecord
private
def insert(ctx, data)
- binds = data.map { |name, value|
- [ctx.columns('ex').find { |x| x.name == name }, value]
+ binds = data.map { |name, value|
+ bind_param(value, name)
}
- columns = binds.map(&:first).map(&:name)
+ columns = binds.map(&:name)
bind_subs = columns.length.times.map { |x| "$#{x + 1}" }
@@ -457,6 +456,10 @@ module ActiveRecord
def connection_without_insert_returning
ActiveRecord::Base.postgresql_connection(ActiveRecord::Base.configurations['arunit'].merge(:insert_returning => false))
end
+
+ def bind_param(value, name = nil)
+ ActiveRecord::Relation::QueryAttribute.new(name, value, ActiveRecord::Type::Value.new)
+ end
end
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/quoting_test.rb b/activerecord/test/cases/adapters/postgresql/quoting_test.rb
index 894cf1ffa2..60baacf67a 100644
--- a/activerecord/test/cases/adapters/postgresql/quoting_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/quoting_test.rb
@@ -27,11 +27,6 @@ module ActiveRecord
assert_equal "'Infinity'", @conn.quote(infinity)
end
- def test_quote_time_usec
- assert_equal "'1970-01-01 00:00:00.000000'", @conn.quote(Time.at(0))
- assert_equal "'1970-01-01 00:00:00.000000'", @conn.quote(Time.at(0).to_datetime)
- end
-
def test_quote_range
range = "1,2]'; SELECT * FROM users; --".."a"
type = OID::Range.new(Type::Integer.new, :int8range)
diff --git a/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb b/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb
index 99c26c4bf7..6937145439 100644
--- a/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb
@@ -55,7 +55,7 @@ class SchemaAuthorizationTest < ActiveRecord::TestCase
set_session_auth
USERS.each do |u|
set_session_auth u
- assert_equal u, @connection.exec_query("SELECT name FROM #{TABLE_NAME} WHERE id = $1", 'SQL', [[nil, 1]]).first['name']
+ assert_equal u, @connection.exec_query("SELECT name FROM #{TABLE_NAME} WHERE id = $1", 'SQL', [bind_param(1)]).first['name']
set_session_auth
end
end
@@ -67,7 +67,7 @@ class SchemaAuthorizationTest < ActiveRecord::TestCase
USERS.each do |u|
@connection.clear_cache!
set_session_auth u
- assert_equal u, @connection.exec_query("SELECT name FROM #{TABLE_NAME} WHERE id = $1", 'SQL', [[nil, 1]]).first['name']
+ assert_equal u, @connection.exec_query("SELECT name FROM #{TABLE_NAME} WHERE id = $1", 'SQL', [bind_param(1)]).first['name']
set_session_auth
end
end
@@ -111,4 +111,7 @@ class SchemaAuthorizationTest < ActiveRecord::TestCase
@connection.session_auth = auth || 'default'
end
+ def bind_param(value)
+ ActiveRecord::Relation::QueryAttribute.new(nil, value, ActiveRecord::Type::Value.new)
+ end
end
diff --git a/activerecord/test/cases/adapters/postgresql/schema_test.rb b/activerecord/test/cases/adapters/postgresql/schema_test.rb
index e99f1e2867..83e35ad1a1 100644
--- a/activerecord/test/cases/adapters/postgresql/schema_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/schema_test.rb
@@ -151,10 +151,10 @@ class SchemaTest < ActiveRecord::TestCase
def test_schema_change_with_prepared_stmt
altered = false
- @connection.exec_query "select * from developers where id = $1", 'sql', [[nil, 1]]
+ @connection.exec_query "select * from developers where id = $1", 'sql', [bind_param(1)]
@connection.exec_query "alter table developers add column zomg int", 'sql', []
altered = true
- @connection.exec_query "select * from developers where id = $1", 'sql', [[nil, 1]]
+ @connection.exec_query "select * from developers where id = $1", 'sql', [bind_param(1)]
ensure
# We are not using DROP COLUMN IF EXISTS because that syntax is only
# supported by pg 9.X
@@ -435,6 +435,10 @@ class SchemaTest < ActiveRecord::TestCase
assert_equal this_index_column, this_index.columns[0]
assert_equal this_index_name, this_index.name
end
+
+ def bind_param(value)
+ ActiveRecord::Relation::QueryAttribute.new(nil, value, ActiveRecord::Type::Value.new)
+ end
end
class SchemaForeignKeyTest < ActiveRecord::TestCase
@@ -454,7 +458,7 @@ class SchemaForeignKeyTest < ActiveRecord::TestCase
end
@connection.add_foreign_key "wagons", "my_schema.trains", column: "train_id"
output = dump_table_schema "wagons"
- assert_match %r{\s+add_foreign_key "wagons", "my_schema.trains", column: "train_id"$}, output
+ assert_match %r{\s+add_foreign_key "wagons", "my_schema\.trains", column: "train_id"$}, output
ensure
@connection.execute "DROP TABLE IF EXISTS wagons"
@connection.execute "DROP TABLE IF EXISTS my_schema.trains"
diff --git a/activerecord/test/cases/adapters/postgresql/timestamp_test.rb b/activerecord/test/cases/adapters/postgresql/timestamp_test.rb
index eb32c4d2c2..da14063e20 100644
--- a/activerecord/test/cases/adapters/postgresql/timestamp_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/timestamp_test.rb
@@ -70,53 +70,6 @@ class TimestampTest < ActiveRecord::TestCase
assert_equal(-1.0 / 0.0, d.updated_at)
end
- def test_default_datetime_precision
- ActiveRecord::Base.connection.create_table(:foos)
- ActiveRecord::Base.connection.add_column :foos, :created_at, :datetime
- ActiveRecord::Base.connection.add_column :foos, :updated_at, :datetime
- assert_nil activerecord_column_option('foos', 'created_at', 'precision')
- end
-
- def test_timestamp_data_type_with_precision
- ActiveRecord::Base.connection.create_table(:foos)
- ActiveRecord::Base.connection.add_column :foos, :created_at, :datetime, :precision => 0
- ActiveRecord::Base.connection.add_column :foos, :updated_at, :datetime, :precision => 5
- assert_equal 0, activerecord_column_option('foos', 'created_at', 'precision')
- assert_equal 5, activerecord_column_option('foos', 'updated_at', 'precision')
- end
-
- def test_timestamps_helper_with_custom_precision
- ActiveRecord::Base.connection.create_table(:foos) do |t|
- t.timestamps :null => true, :precision => 4
- end
- assert_equal 4, activerecord_column_option('foos', 'created_at', 'precision')
- assert_equal 4, activerecord_column_option('foos', 'updated_at', 'precision')
- end
-
- def test_passing_precision_to_timestamp_does_not_set_limit
- ActiveRecord::Base.connection.create_table(:foos) do |t|
- t.timestamps :null => true, :precision => 4
- end
- assert_nil activerecord_column_option("foos", "created_at", "limit")
- assert_nil activerecord_column_option("foos", "updated_at", "limit")
- end
-
- def test_invalid_timestamp_precision_raises_error
- assert_raises ActiveRecord::ActiveRecordError do
- ActiveRecord::Base.connection.create_table(:foos) do |t|
- t.timestamps :null => true, :precision => 7
- end
- end
- end
-
- def test_postgres_agrees_with_activerecord_about_precision
- ActiveRecord::Base.connection.create_table(:foos) do |t|
- t.timestamps :null => true, :precision => 4
- end
- assert_equal '4', pg_datetime_precision('foos', 'created_at')
- assert_equal '4', pg_datetime_precision('foos', 'updated_at')
- end
-
def test_bc_timestamp
date = Date.new(0) - 1.week
Developer.create!(:name => "aaron", :updated_at => date)
@@ -134,21 +87,4 @@ class TimestampTest < ActiveRecord::TestCase
Developer.create!(:name => "yahagi", :updated_at => date)
assert_equal date, Developer.find_by_name("yahagi").updated_at
end
-
- private
-
- def pg_datetime_precision(table_name, column_name)
- results = ActiveRecord::Base.connection.execute("SELECT column_name, datetime_precision FROM information_schema.columns WHERE table_name ='#{table_name}'")
- result = results.find do |result_hash|
- result_hash["column_name"] == column_name
- end
- result && result["datetime_precision"]
- end
-
- def activerecord_column_option(tablename, column_name, option)
- result = ActiveRecord::Base.connection.columns(tablename).find do |column|
- column.name == column_name
- end
- result && result.send(option)
- end
end
diff --git a/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb b/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb
index c88259d274..4506e874bc 100644
--- a/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb
@@ -18,8 +18,8 @@ class PostgresqlTypeLookupTest < ActiveRecord::TestCase
bigint_array = @connection.type_map.lookup(1016, -1, "bigint[]")
big_array = [123456789123456789]
- assert_raises(RangeError) { int_array.type_cast_from_user(big_array) }
- assert_equal big_array, bigint_array.type_cast_from_user(big_array)
+ assert_raises(RangeError) { int_array.type_cast_for_database(big_array) }
+ assert_equal "{123456789123456789}", bigint_array.type_cast_for_database(big_array)
end
test "range types correctly respect registration of subtypes" do
diff --git a/activerecord/test/cases/adapters/postgresql/uuid_test.rb b/activerecord/test/cases/adapters/postgresql/uuid_test.rb
index 7d2fae69d5..6693843497 100644
--- a/activerecord/test/cases/adapters/postgresql/uuid_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/uuid_test.rb
@@ -1,4 +1,3 @@
-# encoding: utf-8
require "cases/helper"
require 'support/schema_dumping_helper'
@@ -49,9 +48,10 @@ class PostgresqlUUIDTest < ActiveRecord::TestCase
column = UUIDType.columns_hash["guid"]
assert_equal :uuid, column.type
assert_equal "uuid", column.sql_type
- assert_not column.number?
- assert_not column.binary?
assert_not column.array?
+
+ type = UUIDType.type_for_attribute("guid")
+ assert_not type.binary?
end
def test_treat_blank_uuid_as_nil
@@ -114,7 +114,7 @@ class PostgresqlUUIDTest < ActiveRecord::TestCase
def test_schema_dump_with_shorthand
output = dump_table_schema "uuid_data_type"
- assert_match %r{t.uuid "guid"}, output
+ assert_match %r{t\.uuid "guid"}, output
end
def test_uniqueness_validation_ignores_uuid
diff --git a/activerecord/test/cases/adapters/postgresql/xml_test.rb b/activerecord/test/cases/adapters/postgresql/xml_test.rb
index 5aba118518..05b34dcf7d 100644
--- a/activerecord/test/cases/adapters/postgresql/xml_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/xml_test.rb
@@ -1,4 +1,3 @@
-# encoding: utf-8
require 'cases/helper'
require 'support/schema_dumping_helper'
@@ -50,6 +49,6 @@ class PostgresqlXMLTest < ActiveRecord::TestCase
def test_schema_dump_with_shorthand
output = dump_table_schema("xml_data_type")
- assert_match %r{t.xml "payload"}, output
+ assert_match %r{t\.xml "payload"}, output
end
end
diff --git a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb
index 274e358e4a..c1d9b7c273 100644
--- a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb
+++ b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb
@@ -85,7 +85,7 @@ module ActiveRecord
def test_quoting_binary_strings
value = "hello".encode('ascii-8bit')
- type = SQLite3String.new
+ type = Type::String.new
assert_equal "'hello'", @conn.quote(type.type_cast_for_database(value))
end
diff --git a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb
index 029663e7f4..f916d99bcf 100644
--- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb
+++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb
@@ -1,4 +1,3 @@
-# encoding: utf-8
require "cases/helper"
require 'models/owner'
require 'tempfile'
@@ -83,8 +82,7 @@ module ActiveRecord
def test_exec_insert
with_example_table do
- column = @conn.columns('ex').find { |col| col.name == 'number' }
- vals = [[column, 10]]
+ vals = [Relation::QueryAttribute.new("number", 10, Type::Value.new)]
@conn.exec_insert('insert into ex (number) VALUES (?)', 'SQL', vals)
result = @conn.exec_query(
@@ -157,7 +155,7 @@ module ActiveRecord
with_example_table 'id int, data string' do
@conn.exec_query('INSERT INTO ex (id, data) VALUES (1, "foo")')
result = @conn.exec_query(
- 'SELECT id, data FROM ex WHERE id = ?', nil, [[nil, 1]])
+ 'SELECT id, data FROM ex WHERE id = ?', nil, [Relation::QueryAttribute.new(nil, 1, Type::Value.new)])
assert_equal 1, result.rows.length
assert_equal 2, result.columns.length
@@ -169,10 +167,9 @@ module ActiveRecord
def test_exec_query_typecasts_bind_vals
with_example_table 'id int, data string' do
@conn.exec_query('INSERT INTO ex (id, data) VALUES (1, "foo")')
- column = @conn.columns('ex').find { |col| col.name == 'id' }
result = @conn.exec_query(
- 'SELECT id, data FROM ex WHERE id = ?', nil, [[column, '1-fuu']])
+ 'SELECT id, data FROM ex WHERE id = ?', nil, [Relation::QueryAttribute.new("id", "1-fuu", Type::Integer.new)])
assert_equal 1, result.rows.length
assert_equal 2, result.columns.length
diff --git a/activerecord/test/cases/adapters/sqlite3/sqlite3_create_folder_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_create_folder_test.rb
index f545fc2011..deedf67c8e 100644
--- a/activerecord/test/cases/adapters/sqlite3/sqlite3_create_folder_test.rb
+++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_create_folder_test.rb
@@ -1,4 +1,3 @@
-# encoding: utf-8
require "cases/helper"
require 'models/owner'
diff --git a/activerecord/test/cases/associations/association_scope_test.rb b/activerecord/test/cases/associations/association_scope_test.rb
index 3e0032ec73..472e270f8c 100644
--- a/activerecord/test/cases/associations/association_scope_test.rb
+++ b/activerecord/test/cases/associations/association_scope_test.rb
@@ -8,12 +8,7 @@ module ActiveRecord
test 'does not duplicate conditions' do
scope = AssociationScope.scope(Author.new.association(:welcome_posts),
Author.connection)
- wheres = scope.where_values.map(&:right)
- binds = scope.bind_values.map(&:last)
- wheres = scope.where_values.map(&:right).reject { |node|
- Arel::Nodes::BindParam === node
- }
- assert_equal wheres.uniq, wheres
+ binds = scope.where_clause.binds.map(&:value)
assert_equal binds.uniq, binds
end
end
diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb
index 17394cb6f7..a425b3ed88 100644
--- a/activerecord/test/cases/associations/belongs_to_associations_test.rb
+++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb
@@ -122,14 +122,14 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
Firm.create("name" => "Apple")
Client.create("name" => "Citibank", :firm_name => "Apple")
citibank_result = Client.all.merge!(:where => {:name => "Citibank"}, :includes => :firm_with_primary_key).first
- assert citibank_result.association_cache.key?(:firm_with_primary_key)
+ assert citibank_result.association(:firm_with_primary_key).loaded?
end
def test_eager_loading_with_primary_key_as_symbol
Firm.create("name" => "Apple")
Client.create("name" => "Citibank", :firm_name => "Apple")
citibank_result = Client.all.merge!(:where => {:name => "Citibank"}, :includes => :firm_with_primary_key_symbols).first
- assert citibank_result.association_cache.key?(:firm_with_primary_key_symbols)
+ assert citibank_result.association(:firm_with_primary_key_symbols).loaded?
end
def test_creating_the_belonging_object
diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb
index 371635d20a..7d8b933992 100644
--- a/activerecord/test/cases/associations/eager_test.rb
+++ b/activerecord/test/cases/associations/eager_test.rb
@@ -77,9 +77,17 @@ class EagerAssociationTest < ActiveRecord::TestCase
def test_has_many_through_with_order
authors = Author.includes(:favorite_authors).to_a
+ assert authors.count > 0
assert_no_queries { authors.map(&:favorite_authors) }
end
+ def test_eager_loaded_has_one_association_with_references_does_not_run_additional_queries
+ Post.update_all(author_id: nil)
+ authors = Author.includes(:post).references(:post).to_a
+ assert authors.count > 0
+ assert_no_queries { authors.map(&:post) }
+ end
+
def test_with_two_tables_in_from_without_getting_double_quoted
posts = Post.select("posts.*").from("authors, posts").eager_load(:comments).where("posts.author_id = authors.id").order("posts.id").to_a
assert_equal 2, posts.first.comments.size
diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb
index 21a45042fa..fde7b0ea3a 100644
--- a/activerecord/test/cases/associations/has_many_associations_test.rb
+++ b/activerecord/test/cases/associations/has_many_associations_test.rb
@@ -316,16 +316,16 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
# would be convenient), because this would cause that scope to be applied to any callbacks etc.
def test_build_and_create_should_not_happen_within_scope
car = cars(:honda)
- scoped_count = car.foo_bulbs.where_values.count
+ scope = car.foo_bulbs.where_values_hash
bulb = car.foo_bulbs.build
- assert_not_equal scoped_count, bulb.scope_after_initialize.where_values.count
+ assert_not_equal scope, bulb.scope_after_initialize.where_values_hash
bulb = car.foo_bulbs.create
- assert_not_equal scoped_count, bulb.scope_after_initialize.where_values.count
+ assert_not_equal scope, bulb.scope_after_initialize.where_values_hash
bulb = car.foo_bulbs.create!
- assert_not_equal scoped_count, bulb.scope_after_initialize.where_values.count
+ assert_not_equal scope, bulb.scope_after_initialize.where_values_hash
end
def test_no_sql_should_be_fired_if_association_already_loaded
@@ -1307,7 +1307,6 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_nothing_raised { topic.destroy }
end
- uses_transaction :test_dependence_with_transaction_support_on_failure
def test_dependence_with_transaction_support_on_failure
firm = companies(:first_firm)
clients = firm.clients
@@ -1679,6 +1678,82 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal 3, firm.clients.size
end
+ def test_calling_none_should_count_instead_of_loading_association
+ firm = companies(:first_firm)
+ assert_queries(1) do
+ firm.clients.none? # use count query
+ end
+ assert !firm.clients.loaded?
+ end
+
+ def test_calling_none_on_loaded_association_should_not_use_query
+ firm = companies(:first_firm)
+ firm.clients.collect # force load
+ assert_no_queries { assert ! firm.clients.none? }
+ end
+
+ def test_calling_none_should_defer_to_collection_if_using_a_block
+ firm = companies(:first_firm)
+ assert_queries(1) do
+ firm.clients.expects(:size).never
+ firm.clients.none? { true }
+ end
+ assert firm.clients.loaded?
+ end
+
+ def test_calling_none_should_return_true_if_none
+ firm = companies(:another_firm)
+ assert firm.clients_like_ms.none?
+ assert_equal 0, firm.clients_like_ms.size
+ end
+
+ def test_calling_none_should_return_false_if_any
+ firm = companies(:first_firm)
+ assert !firm.limited_clients.none?
+ assert_equal 1, firm.limited_clients.size
+ end
+
+ def test_calling_one_should_count_instead_of_loading_association
+ firm = companies(:first_firm)
+ assert_queries(1) do
+ firm.clients.one? # use count query
+ end
+ assert !firm.clients.loaded?
+ end
+
+ def test_calling_one_on_loaded_association_should_not_use_query
+ firm = companies(:first_firm)
+ firm.clients.collect # force load
+ assert_no_queries { assert ! firm.clients.one? }
+ end
+
+ def test_calling_one_should_defer_to_collection_if_using_a_block
+ firm = companies(:first_firm)
+ assert_queries(1) do
+ firm.clients.expects(:size).never
+ firm.clients.one? { true }
+ end
+ assert firm.clients.loaded?
+ end
+
+ def test_calling_one_should_return_false_if_zero
+ firm = companies(:another_firm)
+ assert ! firm.clients_like_ms.one?
+ assert_equal 0, firm.clients_like_ms.size
+ end
+
+ def test_calling_one_should_return_true_if_one
+ firm = companies(:first_firm)
+ assert firm.limited_clients.one?
+ assert_equal 1, firm.limited_clients.size
+ end
+
+ def test_calling_one_should_return_false_if_more_than_one
+ firm = companies(:first_firm)
+ assert ! firm.clients.one?
+ assert_equal 3, firm.clients.size
+ end
+
def test_joins_with_namespaced_model_should_use_correct_type
old = ActiveRecord::Base.store_full_sti_class
ActiveRecord::Base.store_full_sti_class = true
diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb
index 9b6757e256..3b6a4038cd 100644
--- a/activerecord/test/cases/associations/has_one_associations_test.rb
+++ b/activerecord/test/cases/associations/has_one_associations_test.rb
@@ -237,16 +237,16 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
def test_build_and_create_should_not_happen_within_scope
pirate = pirates(:blackbeard)
- scoped_count = pirate.association(:foo_bulb).scope.where_values.count
+ scope = pirate.association(:foo_bulb).scope.where_values_hash
bulb = pirate.build_foo_bulb
- assert_not_equal scoped_count, bulb.scope_after_initialize.where_values.count
+ assert_not_equal scope, bulb.scope_after_initialize.where_values_hash
bulb = pirate.create_foo_bulb
- assert_not_equal scoped_count, bulb.scope_after_initialize.where_values.count
+ assert_not_equal scope, bulb.scope_after_initialize.where_values_hash
bulb = pirate.create_foo_bulb!
- assert_not_equal scoped_count, bulb.scope_after_initialize.where_values.count
+ assert_not_equal scope, bulb.scope_after_initialize.where_values_hash
end
def test_create_association
@@ -276,7 +276,7 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
def test_create_with_inexistent_foreign_key_failing
firm = Firm.create(name: 'GlobalMegaCorp')
- assert_raises(ActiveRecord::UnknownAttributeError) do
+ assert_raises(ActiveModel::AttributeAssignment::UnknownAttributeError) do
firm.create_account_with_inexistent_foreign_key
end
end
diff --git a/activerecord/test/cases/associations/required_test.rb b/activerecord/test/cases/associations/required_test.rb
index 321fb6c8dd..8b765a2e0c 100644
--- a/activerecord/test/cases/associations/required_test.rb
+++ b/activerecord/test/cases/associations/required_test.rb
@@ -18,8 +18,8 @@ class RequiredAssociationsTest < ActiveRecord::TestCase
end
teardown do
- @connection.drop_table 'parents' if @connection.table_exists? 'parents'
- @connection.drop_table 'children' if @connection.table_exists? 'children'
+ @connection.drop_table 'parents', if_exists: true
+ @connection.drop_table 'children', if_exists: true
end
test "belongs_to associations are not required by default" do
@@ -40,7 +40,7 @@ class RequiredAssociationsTest < ActiveRecord::TestCase
record = model.new
assert_not record.save
- assert_equal ["Parent can't be blank"], record.errors.full_messages
+ assert_equal ["Parent must exist"], record.errors.full_messages
record.parent = Parent.new
assert record.save
@@ -64,12 +64,32 @@ class RequiredAssociationsTest < ActiveRecord::TestCase
record = model.new
assert_not record.save
- assert_equal ["Child can't be blank"], record.errors.full_messages
+ assert_equal ["Child must exist"], record.errors.full_messages
record.child = Child.new
assert record.save
end
+ test "required has_one associations have a correct error message" do
+ model = subclass_of(Parent) do
+ has_one :child, required: true, inverse_of: false,
+ class_name: "RequiredAssociationsTest::Child"
+ end
+
+ record = model.create
+ assert_equal ["Child must exist"], record.errors.full_messages
+ end
+
+ test "required belongs_to associations have a correct error message" do
+ model = subclass_of(Child) do
+ belongs_to :parent, required: true, inverse_of: false,
+ class_name: "RequiredAssociationsTest::Parent"
+ end
+
+ record = model.create
+ assert_equal ["Parent must exist"], record.errors.full_messages
+ end
+
private
def subclass_of(klass, &block)
diff --git a/activerecord/test/cases/associations_test.rb b/activerecord/test/cases/associations_test.rb
index 72963fd56c..de358114ab 100644
--- a/activerecord/test/cases/associations_test.rb
+++ b/activerecord/test/cases/associations_test.rb
@@ -141,7 +141,7 @@ class AssociationsTest < ActiveRecord::TestCase
def test_association_with_references
firm = companies(:first_firm)
- assert_equal ['foo'], firm.association_with_references.references_values
+ assert_includes firm.association_with_references.references_values, 'foo'
end
end
@@ -238,7 +238,7 @@ class AssociationProxyTest < ActiveRecord::TestCase
end
def test_scoped_allows_conditions
- assert developers(:david).projects.merge!(where: 'foo').where_values.include?('foo')
+ assert developers(:david).projects.merge(where: 'foo').to_sql.include?('foo')
end
test "getting a scope from an association" do
diff --git a/activerecord/test/cases/attribute_decorators_test.rb b/activerecord/test/cases/attribute_decorators_test.rb
index 53bd58e22e..0b96319cbd 100644
--- a/activerecord/test/cases/attribute_decorators_test.rb
+++ b/activerecord/test/cases/attribute_decorators_test.rb
@@ -28,7 +28,7 @@ module ActiveRecord
teardown do
return unless @connection
- @connection.drop_table 'attribute_decorators_model' if @connection.table_exists? 'attribute_decorators_model'
+ @connection.drop_table 'attribute_decorators_model', if_exists: true
Model.attribute_type_decorations.clear
Model.reset_column_information
end
@@ -51,7 +51,7 @@ module ActiveRecord
end
test "undecorated columns are not touched" do
- Model.attribute :another_string, Type::String.new, default: 'something or other'
+ Model.attribute :another_string, :string, default: 'something or other'
Model.decorate_attribute_type(:a_string, :test) { |t| StringDecorator.new(t) }
assert_equal 'something or other', Model.new.another_string
@@ -86,7 +86,7 @@ module ActiveRecord
end
test "decorating attributes does not modify parent classes" do
- Model.attribute :another_string, Type::String.new, default: 'whatever'
+ Model.attribute :another_string, :string, default: 'whatever'
Model.decorate_attribute_type(:a_string, :test) { |t| StringDecorator.new(t) }
child_class = Class.new(Model)
child_class.decorate_attribute_type(:another_string, :test) { |t| StringDecorator.new(t) }
@@ -110,7 +110,7 @@ module ActiveRecord
end
test "decorating with a proc" do
- Model.attribute :an_int, Type::Integer.new
+ Model.attribute :an_int, :integer
type_is_integer = proc { |_, type| type.type == :integer }
Model.decorate_matching_attribute_types type_is_integer, :multiplier do |type|
Multiplier.new(type)
diff --git a/activerecord/test/cases/attribute_methods/read_test.rb b/activerecord/test/cases/attribute_methods/read_test.rb
index e38b32d7fc..74e556211b 100644
--- a/activerecord/test/cases/attribute_methods/read_test.rb
+++ b/activerecord/test/cases/attribute_methods/read_test.rb
@@ -17,7 +17,7 @@ module ActiveRecord
include ActiveRecord::AttributeMethods
- def self.column_names
+ def self.attribute_names
%w{ one two three }
end
@@ -25,11 +25,11 @@ module ActiveRecord
end
def self.columns
- column_names.map { FakeColumn.new(name) }
+ attribute_names.map { FakeColumn.new(name) }
end
def self.columns_hash
- Hash[column_names.map { |name|
+ Hash[attribute_names.map { |name|
[name, FakeColumn.new(name)]
}]
end
@@ -39,13 +39,13 @@ module ActiveRecord
def test_define_attribute_methods
instance = @klass.new
- @klass.column_names.each do |name|
+ @klass.attribute_names.each do |name|
assert !instance.methods.map(&:to_s).include?(name)
end
@klass.define_attribute_methods
- @klass.column_names.each do |name|
+ @klass.attribute_names.each do |name|
assert instance.methods.map(&:to_s).include?(name), "#{name} is not defined"
end
end
diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb
index 359503db98..243c90e945 100644
--- a/activerecord/test/cases/attribute_methods_test.rb
+++ b/activerecord/test/cases/attribute_methods_test.rb
@@ -657,7 +657,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase
end
end
- def test_setting_time_zone_aware_attribute_in_current_time_zone
+ def test_setting_time_zone_aware_datetime_in_current_time_zone
utc_time = Time.utc(2008, 1, 1)
in_time_zone "Pacific Time (US & Canada)" do
record = @target.new
@@ -676,6 +676,47 @@ class AttributeMethodsTest < ActiveRecord::TestCase
end
end
+ def test_setting_time_zone_aware_time_in_current_time_zone
+ in_time_zone "Pacific Time (US & Canada)" do
+ record = @target.new
+ time_string = "10:00:00"
+ expected_time = Time.zone.parse("2000-01-01 #{time_string}")
+
+ record.bonus_time = time_string
+ assert_equal expected_time, record.bonus_time
+ assert_equal ActiveSupport::TimeZone["Pacific Time (US & Canada)"], record.bonus_time.time_zone
+
+ record.bonus_time = ''
+ assert_nil record.bonus_time
+ end
+ end
+
+ def test_setting_time_zone_aware_time_with_dst
+ in_time_zone "Pacific Time (US & Canada)" do
+ current_time = Time.zone.local(2014, 06, 15, 10)
+ record = @target.new(bonus_time: current_time)
+ time_before_save = record.bonus_time
+
+ record.save
+ record.reload
+
+ assert_equal time_before_save, record.bonus_time
+ assert_equal ActiveSupport::TimeZone["Pacific Time (US & Canada)"], record.bonus_time.time_zone
+ end
+ end
+
+ def test_removing_time_zone_aware_types
+ with_time_zone_aware_types(:datetime) do
+ in_time_zone "Pacific Time (US & Canada)" do
+ record = @target.new(bonus_time: "10:00:00")
+ expected_time = Time.utc(2000, 01, 01, 10)
+
+ assert_equal expected_time, record.bonus_time
+ assert record.bonus_time.utc?
+ end
+ end
+ end
+
def test_setting_time_zone_conversion_for_attributes_should_write_value_on_class_variable
Topic.skip_time_zone_conversion_for_attributes = [:field_a]
Minimalistic.skip_time_zone_conversion_for_attributes = [:field_b]
@@ -717,12 +758,12 @@ class AttributeMethodsTest < ActiveRecord::TestCase
def test_bulk_update_respects_access_control
privatize("title=(value)")
- assert_raise(ActiveRecord::UnknownAttributeError) { @target.new(:title => "Rants about pants") }
- assert_raise(ActiveRecord::UnknownAttributeError) { @target.new.attributes = { :title => "Ants in pants" } }
+ assert_raise(ActiveModel::AttributeAssignment::UnknownAttributeError) { @target.new(:title => "Rants about pants") }
+ assert_raise(ActiveModel::AttributeAssignment::UnknownAttributeError) { @target.new.attributes = { :title => "Ants in pants" } }
end
def test_bulk_update_raise_unknown_attribute_error
- error = assert_raises(ActiveRecord::UnknownAttributeError) {
+ error = assert_raises(ActiveModel::AttributeAssignment::UnknownAttributeError) {
Topic.new(hello: "world")
}
assert_instance_of Topic, error.record
@@ -896,6 +937,16 @@ class AttributeMethodsTest < ActiveRecord::TestCase
assert model.id_came_from_user?
end
+ def test_accessed_fields
+ model = @target.first
+
+ assert_equal [], model.accessed_fields
+
+ model.title
+
+ assert_equal ["title"], model.accessed_fields
+ end
+
private
def new_topic_like_ar_class(&block)
@@ -908,6 +959,14 @@ class AttributeMethodsTest < ActiveRecord::TestCase
klass
end
+ def with_time_zone_aware_types(*types)
+ old_types = ActiveRecord::Base.time_zone_aware_types
+ ActiveRecord::Base.time_zone_aware_types = types
+ yield
+ ensure
+ ActiveRecord::Base.time_zone_aware_types = old_types
+ end
+
def cached_columns
Topic.columns.map(&:name)
end
diff --git a/activerecord/test/cases/attribute_set_test.rb b/activerecord/test/cases/attribute_set_test.rb
index ba53612d30..112cd2fb14 100644
--- a/activerecord/test/cases/attribute_set_test.rb
+++ b/activerecord/test/cases/attribute_set_test.rb
@@ -65,6 +65,16 @@ module ActiveRecord
assert_equal({ foo: 1, bar: 2.2 }, attributes.to_h)
end
+ test "to_hash maintains order" do
+ builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new)
+ attributes = builder.build_from_database(foo: '2.2', bar: '3.3')
+
+ attributes[:bar]
+ hash = attributes.to_h
+
+ assert_equal [[:foo, 2], [:bar, 3.3]], hash.to_a
+ end
+
test "values_before_type_cast" do
builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Integer.new)
attributes = builder.build_from_database(foo: '1.1', bar: '2.2')
@@ -186,5 +196,16 @@ module ActiveRecord
attributes.freeze
assert_equal({ foo: "1" }, attributes.to_hash)
end
+
+ test "#accessed_attributes returns only attributes which have been read" do
+ builder = AttributeSet::Builder.new(foo: Type::Value.new, bar: Type::Value.new)
+ attributes = builder.build_from_database(foo: "1", bar: "2")
+
+ assert_equal [], attributes.accessed
+
+ attributes.fetch_value(:foo)
+
+ assert_equal [:foo], attributes.accessed
+ end
end
end
diff --git a/activerecord/test/cases/attribute_test.rb b/activerecord/test/cases/attribute_test.rb
index 39a976fcc8..eac73e11e8 100644
--- a/activerecord/test/cases/attribute_test.rb
+++ b/activerecord/test/cases/attribute_test.rb
@@ -169,5 +169,24 @@ module ActiveRecord
second = Attribute.from_user(:foo, 1, Type::Integer.new)
assert_not_equal first, second
end
+
+ test "an attribute has not been read by default" do
+ attribute = Attribute.from_database(:foo, 1, Type::Value.new)
+ assert_not attribute.has_been_read?
+ end
+
+ test "an attribute has been read when its value is calculated" do
+ attribute = Attribute.from_database(:foo, 1, Type::Value.new)
+ attribute.value
+ assert attribute.has_been_read?
+ end
+
+ test "an attribute can not be mutated if it has not been read,
+ and skips expensive calculations" do
+ type_which_raises_from_all_methods = Object.new
+ attribute = Attribute.from_database(:foo, "bar", type_which_raises_from_all_methods)
+
+ assert_not attribute.changed_in_place_from?("bar")
+ end
end
end
diff --git a/activerecord/test/cases/attributes_test.rb b/activerecord/test/cases/attributes_test.rb
index dbe1eb48db..a753e8b74e 100644
--- a/activerecord/test/cases/attributes_test.rb
+++ b/activerecord/test/cases/attributes_test.rb
@@ -1,17 +1,17 @@
require 'cases/helper'
class OverloadedType < ActiveRecord::Base
- attribute :overloaded_float, Type::Integer.new
- attribute :overloaded_string_with_limit, Type::String.new(limit: 50)
- attribute :non_existent_decimal, Type::Decimal.new
- attribute :string_with_default, Type::String.new, default: 'the overloaded default'
+ attribute :overloaded_float, :integer
+ attribute :overloaded_string_with_limit, :string, limit: 50
+ attribute :non_existent_decimal, :decimal
+ attribute :string_with_default, :string, default: 'the overloaded default'
end
class ChildOfOverloadedType < OverloadedType
end
class GrandchildOfOverloadedType < ChildOfOverloadedType
- attribute :overloaded_float, Type::Float.new
+ attribute :overloaded_float, :float
end
class UnoverloadedType < ActiveRecord::Base
@@ -50,15 +50,15 @@ module ActiveRecord
end
test "overloaded properties with limit" do
- assert_equal 50, OverloadedType.columns_hash['overloaded_string_with_limit'].limit
- assert_equal 255, UnoverloadedType.columns_hash['overloaded_string_with_limit'].limit
+ assert_equal 50, OverloadedType.type_for_attribute('overloaded_string_with_limit').limit
+ assert_equal 255, UnoverloadedType.type_for_attribute('overloaded_string_with_limit').limit
end
test "nonexistent attribute" do
data = OverloadedType.new(non_existent_decimal: 1)
assert_equal BigDecimal.new(1), data.non_existent_decimal
- assert_raise ActiveRecord::UnknownAttributeError do
+ assert_raise ActiveModel::AttributeAssignment::UnknownAttributeError do
UnoverloadedType.new(non_existent_decimal: 1)
end
end
@@ -87,29 +87,74 @@ module ActiveRecord
assert_equal 4.4, data.overloaded_float
end
- test "overloading properties does not change column order" do
- column_names = OverloadedType.column_names
- assert_equal %w(id overloaded_float unoverloaded_float overloaded_string_with_limit string_with_default non_existent_decimal), column_names
+ test "overloading properties does not attribute method order" do
+ attribute_names = OverloadedType.attribute_names
+ assert_equal %w(id overloaded_float unoverloaded_float overloaded_string_with_limit string_with_default non_existent_decimal), attribute_names
end
test "caches are cleared" do
klass = Class.new(OverloadedType)
- assert_equal 6, klass.columns.length
- assert_not klass.columns_hash.key?('wibble')
- assert_equal 6, klass.column_types.length
+ assert_equal 6, klass.attribute_types.length
assert_equal 6, klass.column_defaults.length
- assert_not klass.column_names.include?('wibble')
- assert_equal 5, klass.content_columns.length
+ assert_not klass.attribute_types.include?('wibble')
klass.attribute :wibble, Type::Value.new
- assert_equal 7, klass.columns.length
- assert klass.columns_hash.key?('wibble')
- assert_equal 7, klass.column_types.length
+ assert_equal 7, klass.attribute_types.length
assert_equal 7, klass.column_defaults.length
- assert klass.column_names.include?('wibble')
- assert_equal 6, klass.content_columns.length
+ assert klass.attribute_types.include?('wibble')
+ end
+
+ test "the given default value is cast from user" do
+ custom_type = Class.new(Type::Value) do
+ def type_cast_from_user(*)
+ "from user"
+ end
+
+ def type_cast_from_database(*)
+ "from database"
+ end
+ end
+
+ klass = Class.new(OverloadedType) do
+ attribute :wibble, custom_type.new, default: "default"
+ end
+ model = klass.new
+
+ assert_equal "from user", model.wibble
+ end
+
+ if current_adapter?(:PostgreSQLAdapter)
+ test "arrays types can be specified" do
+ klass = Class.new(OverloadedType) do
+ attribute :my_array, :string, limit: 50, array: true
+ attribute :my_int_array, :integer, array: true
+ end
+
+ string_array = ConnectionAdapters::PostgreSQL::OID::Array.new(
+ Type::String.new(limit: 50))
+ int_array = ConnectionAdapters::PostgreSQL::OID::Array.new(
+ Type::Integer.new)
+ refute_equal string_array, int_array
+ assert_equal string_array, klass.type_for_attribute("my_array")
+ assert_equal int_array, klass.type_for_attribute("my_int_array")
+ end
+
+ test "range types can be specified" do
+ klass = Class.new(OverloadedType) do
+ attribute :my_range, :string, limit: 50, range: true
+ attribute :my_int_range, :integer, range: true
+ end
+
+ string_range = ConnectionAdapters::PostgreSQL::OID::Range.new(
+ Type::String.new(limit: 50))
+ int_range = ConnectionAdapters::PostgreSQL::OID::Range.new(
+ Type::Integer.new)
+ refute_equal string_range, int_range
+ assert_equal string_range, klass.type_for_attribute("my_range")
+ assert_equal int_range, klass.type_for_attribute("my_int_range")
+ end
end
end
end
diff --git a/activerecord/test/cases/autosave_association_test.rb b/activerecord/test/cases/autosave_association_test.rb
index 52765881d0..859afc4553 100644
--- a/activerecord/test/cases/autosave_association_test.rb
+++ b/activerecord/test/cases/autosave_association_test.rb
@@ -1061,11 +1061,16 @@ class TestAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCase
end
def test_should_not_ignore_different_error_messages_on_the_same_attribute
+ old_validators = Ship._validators.deep_dup
+ old_callbacks = Ship._validate_callbacks.deep_dup
Ship.validates_format_of :name, :with => /\w/
@pirate.ship.name = ""
@pirate.catchphrase = nil
assert @pirate.invalid?
assert_equal ["can't be blank", "is invalid"], @pirate.errors[:"ship.name"]
+ ensure
+ Ship._validators = old_validators if old_validators
+ Ship._validate_callbacks = old_callbacks if old_callbacks
end
def test_should_still_allow_to_bypass_validations_on_the_associated_model
diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb
index 0debd30e5c..ef1173a2ba 100644
--- a/activerecord/test/cases/base_test.rb
+++ b/activerecord/test/cases/base_test.rb
@@ -1,4 +1,3 @@
-# encoding: utf-8
require "cases/helper"
require 'active_support/concurrency/latch'
@@ -904,8 +903,8 @@ class BasicsTest < ActiveRecord::TestCase
class NumericData < ActiveRecord::Base
self.table_name = 'numeric_data'
- attribute :my_house_population, Type::Integer.new
- attribute :atoms_in_universe, Type::Integer.new
+ attribute :my_house_population, :integer
+ attribute :atoms_in_universe, :integer
end
def test_big_decimal_conditions
diff --git a/activerecord/test/cases/batches_test.rb b/activerecord/test/cases/batches_test.rb
index c12fa03015..c05382598b 100644
--- a/activerecord/test/cases/batches_test.rb
+++ b/activerecord/test/cases/batches_test.rb
@@ -106,6 +106,15 @@ class EachTest < ActiveRecord::TestCase
end
end
+ def test_find_in_batches_should_end_at_the_end_option
+ assert_queries(6) do
+ Post.find_in_batches(batch_size: 1, end_at: 5) do |batch|
+ assert_kind_of Array, batch
+ assert_kind_of Post, batch.first
+ end
+ end
+ end
+
def test_find_in_batches_shouldnt_execute_query_unless_needed
assert_queries(2) do
Post.find_in_batches(:batch_size => @total) {|batch| assert_kind_of Array, batch }
diff --git a/activerecord/test/cases/binary_test.rb b/activerecord/test/cases/binary_test.rb
index ccf2be369d..86dee929bf 100644
--- a/activerecord/test/cases/binary_test.rb
+++ b/activerecord/test/cases/binary_test.rb
@@ -1,4 +1,3 @@
-# encoding: utf-8
require "cases/helper"
# Without using prepared statements, it makes no sense to test
diff --git a/activerecord/test/cases/bind_parameter_test.rb b/activerecord/test/cases/bind_parameter_test.rb
index 66663b3e0e..1e38b97c4a 100644
--- a/activerecord/test/cases/bind_parameter_test.rb
+++ b/activerecord/test/cases/bind_parameter_test.rb
@@ -22,8 +22,8 @@ module ActiveRecord
def setup
super
@connection = ActiveRecord::Base.connection
- @subscriber = LogListener.new
- @pk = Topic.columns_hash[Topic.primary_key]
+ @subscriber = LogListener.new
+ @pk = Topic.columns_hash[Topic.primary_key]
@subscription = ActiveSupport::Notifications.subscribe('sql.active_record', @subscriber)
end
@@ -40,7 +40,7 @@ module ActiveRecord
def test_binds_are_logged
sub = @connection.substitute_at(@pk)
- binds = [[@pk, 1]]
+ binds = [Relation::QueryAttribute.new("id", 1, Type::Value.new)]
sql = "select * from topics where id = #{sub.to_sql}"
@connection.exec_query(sql, 'SQL', binds)
@@ -49,29 +49,17 @@ module ActiveRecord
assert_equal binds, message[4][:binds]
end
- def test_binds_are_logged_after_type_cast
- sub = @connection.substitute_at(@pk)
- binds = [[@pk, "3"]]
- sql = "select * from topics where id = #{sub.to_sql}"
-
- @connection.exec_query(sql, 'SQL', binds)
-
- message = @subscriber.calls.find { |args| args[4][:sql] == sql }
- assert_equal [[@pk, 3]], message[4][:binds]
- end
-
def test_find_one_uses_binds
Topic.find(1)
- binds = [[@pk, 1]]
- message = @subscriber.calls.find { |args| args[4][:binds] == binds }
+ message = @subscriber.calls.find { |args| args[4][:binds].any? { |attr| attr.value == 1 } }
assert message, 'expected a message with binds'
end
- def test_logs_bind_vars
+ def test_logs_bind_vars_after_type_cast
payload = {
:name => 'SQL',
:sql => 'select * from topics where id = ?',
- :binds => [[@pk, 10]]
+ :binds => [Relation::QueryAttribute.new("id", "10", Type::Integer.new)]
}
event = ActiveSupport::Notifications::Event.new(
'foo',
diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb
index 299217214e..f0393aa6b1 100644
--- a/activerecord/test/cases/calculations_test.rb
+++ b/activerecord/test/cases/calculations_test.rb
@@ -15,9 +15,9 @@ require 'models/treasure'
class NumericData < ActiveRecord::Base
self.table_name = 'numeric_data'
- attribute :world_population, Type::Integer.new
- attribute :my_house_population, Type::Integer.new
- attribute :atoms_in_universe, Type::Integer.new
+ attribute :world_population, :integer
+ attribute :my_house_population, :integer
+ attribute :atoms_in_universe, :integer
end
class CalculationsTest < ActiveRecord::TestCase
@@ -631,4 +631,9 @@ class CalculationsTest < ActiveRecord::TestCase
assert_equal({ "has trinket" => part.id }, ShipPart.joins(:trinkets).group("ship_parts.name").sum(:id))
end
+
+ def test_calculation_grouped_by_association_doesnt_error_when_no_records_have_association
+ Client.update_all(client_of: nil)
+ assert_equal({ nil => Client.count }, Client.group(:firm).count)
+ end
end
diff --git a/activerecord/test/cases/callbacks_test.rb b/activerecord/test/cases/callbacks_test.rb
index 670d94dc06..3ae4a6eade 100644
--- a/activerecord/test/cases/callbacks_test.rb
+++ b/activerecord/test/cases/callbacks_test.rb
@@ -288,7 +288,12 @@ class CallbacksTest < ActiveRecord::TestCase
[ :after_save, :string ],
[ :after_save, :proc ],
[ :after_save, :object ],
- [ :after_save, :block ]
+ [ :after_save, :block ],
+ [ :after_commit, :block ],
+ [ :after_commit, :object ],
+ [ :after_commit, :proc ],
+ [ :after_commit, :string ],
+ [ :after_commit, :method ]
], david.history
end
@@ -357,7 +362,12 @@ class CallbacksTest < ActiveRecord::TestCase
[ :after_save, :string ],
[ :after_save, :proc ],
[ :after_save, :object ],
- [ :after_save, :block ]
+ [ :after_save, :block ],
+ [ :after_commit, :block ],
+ [ :after_commit, :object ],
+ [ :after_commit, :proc ],
+ [ :after_commit, :string ],
+ [ :after_commit, :method ]
], david.history
end
@@ -408,7 +418,12 @@ class CallbacksTest < ActiveRecord::TestCase
[ :after_destroy, :string ],
[ :after_destroy, :proc ],
[ :after_destroy, :object ],
- [ :after_destroy, :block ]
+ [ :after_destroy, :block ],
+ [ :after_commit, :block ],
+ [ :after_commit, :object ],
+ [ :after_commit, :proc ],
+ [ :after_commit, :string ],
+ [ :after_commit, :method ]
], david.history
end
diff --git a/activerecord/test/cases/column_definition_test.rb b/activerecord/test/cases/column_definition_test.rb
index bcfd66b4bf..14b95ecab1 100644
--- a/activerecord/test/cases/column_definition_test.rb
+++ b/activerecord/test/cases/column_definition_test.rb
@@ -14,7 +14,7 @@ module ActiveRecord
# Avoid column definitions in create table statements like:
# `title` varchar(255) DEFAULT NULL
def test_should_not_include_default_clause_when_default_is_null
- column = Column.new("title", nil, Type::String.new(limit: 20))
+ column = Column.new("title", nil, SqlTypeMetadata.new(limit: 20))
column_def = ColumnDefinition.new(
column.name, "string",
column.limit, column.precision, column.scale, column.default, column.null)
@@ -22,7 +22,7 @@ module ActiveRecord
end
def test_should_include_default_clause_when_default_is_present
- column = Column.new("title", "Hello", Type::String.new(limit: 20))
+ column = Column.new("title", "Hello", SqlTypeMetadata.new(limit: 20))
column_def = ColumnDefinition.new(
column.name, "string",
column.limit, column.precision, column.scale, column.default, column.null)
@@ -30,94 +30,53 @@ module ActiveRecord
end
def test_should_specify_not_null_if_null_option_is_false
- column = Column.new("title", "Hello", Type::String.new(limit: 20), "varchar(20)", false)
+ type_metadata = SqlTypeMetadata.new(limit: 20)
+ column = Column.new("title", "Hello", type_metadata, false)
column_def = ColumnDefinition.new(
column.name, "string",
column.limit, column.precision, column.scale, column.default, column.null)
assert_equal %Q{title varchar(20) DEFAULT 'Hello' NOT NULL}, @viz.accept(column_def)
end
- if current_adapter?(:MysqlAdapter)
+ if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
def test_should_set_default_for_mysql_binary_data_types
- binary_column = MysqlAdapter::Column.new("title", "a", Type::Binary.new, "binary(1)")
+ type = SqlTypeMetadata.new(type: :binary, sql_type: "binary(1)")
+ binary_column = AbstractMysqlAdapter::Column.new("title", "a", type)
assert_equal "a", binary_column.default
- varbinary_column = MysqlAdapter::Column.new("title", "a", Type::Binary.new, "varbinary(1)")
+ type = SqlTypeMetadata.new(type: :binary, sql_type: "varbinary")
+ varbinary_column = AbstractMysqlAdapter::Column.new("title", "a", type)
assert_equal "a", varbinary_column.default
end
def test_should_not_set_default_for_blob_and_text_data_types
assert_raise ArgumentError do
- MysqlAdapter::Column.new("title", "a", Type::Binary.new, "blob")
+ AbstractMysqlAdapter::Column.new("title", "a", SqlTypeMetadata.new(sql_type: "blob"))
end
+ text_type = AbstractMysqlAdapter::MysqlTypeMetadata.new(
+ SqlTypeMetadata.new(type: :text))
assert_raise ArgumentError do
- MysqlAdapter::Column.new("title", "Hello", Type::Text.new)
+ AbstractMysqlAdapter::Column.new("title", "Hello", text_type)
end
- text_column = MysqlAdapter::Column.new("title", nil, Type::Text.new)
+ text_column = AbstractMysqlAdapter::Column.new("title", nil, text_type)
assert_equal nil, text_column.default
- not_null_text_column = MysqlAdapter::Column.new("title", nil, Type::Text.new, "text", false)
+ not_null_text_column = AbstractMysqlAdapter::Column.new("title", nil, text_type, false)
assert_equal "", not_null_text_column.default
end
def test_has_default_should_return_false_for_blob_and_text_data_types
- blob_column = MysqlAdapter::Column.new("title", nil, Type::Binary.new, "blob")
+ binary_type = SqlTypeMetadata.new(sql_type: "blob")
+ blob_column = AbstractMysqlAdapter::Column.new("title", nil, binary_type)
assert !blob_column.has_default?
- text_column = MysqlAdapter::Column.new("title", nil, Type::Text.new)
+ text_type = SqlTypeMetadata.new(type: :text)
+ text_column = AbstractMysqlAdapter::Column.new("title", nil, text_type)
assert !text_column.has_default?
end
end
-
- if current_adapter?(:Mysql2Adapter)
- def test_should_set_default_for_mysql_binary_data_types
- binary_column = Mysql2Adapter::Column.new("title", "a", Type::Binary.new, "binary(1)")
- assert_equal "a", binary_column.default
-
- varbinary_column = Mysql2Adapter::Column.new("title", "a", Type::Binary.new, "varbinary(1)")
- assert_equal "a", varbinary_column.default
- end
-
- def test_should_not_set_default_for_blob_and_text_data_types
- assert_raise ArgumentError do
- Mysql2Adapter::Column.new("title", "a", Type::Binary.new, "blob")
- end
-
- assert_raise ArgumentError do
- Mysql2Adapter::Column.new("title", "Hello", Type::Text.new)
- end
-
- text_column = Mysql2Adapter::Column.new("title", nil, Type::Text.new)
- assert_equal nil, text_column.default
-
- not_null_text_column = Mysql2Adapter::Column.new("title", nil, Type::Text.new, "text", false)
- assert_equal "", not_null_text_column.default
- end
-
- def test_has_default_should_return_false_for_blob_and_text_data_types
- blob_column = Mysql2Adapter::Column.new("title", nil, Type::Binary.new, "blob")
- assert !blob_column.has_default?
-
- text_column = Mysql2Adapter::Column.new("title", nil, Type::Text.new)
- assert !text_column.has_default?
- end
- end
-
- if current_adapter?(:PostgreSQLAdapter)
- def test_bigint_column_should_map_to_integer
- oid = PostgreSQLAdapter::OID::Integer.new
- bigint_column = PostgreSQLColumn.new('number', nil, oid, "bigint")
- assert_equal :integer, bigint_column.type
- end
-
- def test_smallint_column_should_map_to_integer
- oid = PostgreSQLAdapter::OID::Integer.new
- smallint_column = PostgreSQLColumn.new('number', nil, oid, "smallint")
- assert_equal :integer, smallint_column.type
- end
- end
end
end
end
diff --git a/activerecord/test/cases/connection_adapters/type_lookup_test.rb b/activerecord/test/cases/connection_adapters/type_lookup_test.rb
index d5c1dc1e5d..ac2e0053b8 100644
--- a/activerecord/test/cases/connection_adapters/type_lookup_test.rb
+++ b/activerecord/test/cases/connection_adapters/type_lookup_test.rb
@@ -79,6 +79,11 @@ module ActiveRecord
assert_lookup_type :integer, 'bigint'
end
+ def test_bigint_limit
+ cast_type = @connection.type_map.lookup("bigint")
+ assert_equal 8, cast_type.limit
+ 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|
diff --git a/activerecord/test/cases/core_test.rb b/activerecord/test/cases/core_test.rb
index 715d92af99..3cb98832c5 100644
--- a/activerecord/test/cases/core_test.rb
+++ b/activerecord/test/cases/core_test.rb
@@ -98,4 +98,15 @@ class CoreTest < ActiveRecord::TestCase
assert actual.start_with?(expected.split('XXXXXX').first)
assert actual.end_with?(expected.split('XXXXXX').last)
end
+
+ def test_pretty_print_overridden_by_inspect
+ subtopic = Class.new(Topic) do
+ def inspect
+ "inspecting topic"
+ end
+ end
+ actual = ''
+ PP.pp(subtopic.new, StringIO.new(actual))
+ assert_equal "inspecting topic\n", actual
+ end
end
diff --git a/activerecord/test/cases/counter_cache_test.rb b/activerecord/test/cases/counter_cache_test.rb
index 07a182070b..1f5055b2a2 100644
--- a/activerecord/test/cases/counter_cache_test.rb
+++ b/activerecord/test/cases/counter_cache_test.rb
@@ -180,4 +180,22 @@ class CounterCacheTest < ActiveRecord::TestCase
SpecialTopic.reset_counters(special.id, :lightweight_special_replies)
end
end
+
+ test "counters are updated both in memory and in the database on create" do
+ car = Car.new(engines_count: 0)
+ car.engines = [Engine.new, Engine.new]
+ car.save!
+
+ assert_equal 2, car.engines_count
+ assert_equal 2, car.reload.engines_count
+ end
+
+ test "counter caches are updated in memory when the default value is nil" do
+ car = Car.new(engines_count: nil)
+ car.engines = [Engine.new, Engine.new]
+ car.save!
+
+ assert_equal 2, car.engines_count
+ assert_equal 2, car.reload.engines_count
+ end
end
diff --git a/activerecord/test/cases/date_time_precision_test.rb b/activerecord/test/cases/date_time_precision_test.rb
new file mode 100644
index 0000000000..720f10a9d3
--- /dev/null
+++ b/activerecord/test/cases/date_time_precision_test.rb
@@ -0,0 +1,102 @@
+require 'cases/helper'
+require 'support/schema_dumping_helper'
+
+if ActiveRecord::Base.connection.supports_datetime_with_precision?
+class DateTimePrecisionTest < ActiveRecord::TestCase
+ include SchemaDumpingHelper
+ self.use_transactional_fixtures = false
+
+ class Foo < ActiveRecord::Base; end
+
+ setup do
+ @connection = ActiveRecord::Base.connection
+ end
+
+ teardown do
+ @connection.drop_table :foos, if_exists: true
+ end
+
+ def test_datetime_data_type_with_precision
+ @connection.create_table(:foos, force: true)
+ @connection.add_column :foos, :created_at, :datetime, precision: 0
+ @connection.add_column :foos, :updated_at, :datetime, precision: 5
+ assert_equal 0, activerecord_column_option('foos', 'created_at', 'precision')
+ assert_equal 5, activerecord_column_option('foos', 'updated_at', 'precision')
+ end
+
+ def test_timestamps_helper_with_custom_precision
+ @connection.create_table(:foos, force: true) do |t|
+ t.timestamps precision: 4
+ end
+ assert_equal 4, activerecord_column_option('foos', 'created_at', 'precision')
+ assert_equal 4, activerecord_column_option('foos', 'updated_at', 'precision')
+ end
+
+ def test_passing_precision_to_datetime_does_not_set_limit
+ @connection.create_table(:foos, force: true) do |t|
+ t.timestamps precision: 4
+ end
+ assert_nil activerecord_column_option('foos', 'created_at', 'limit')
+ assert_nil activerecord_column_option('foos', 'updated_at', 'limit')
+ end
+
+ def test_invalid_datetime_precision_raises_error
+ assert_raises ActiveRecord::ActiveRecordError do
+ @connection.create_table(:foos, force: true) do |t|
+ t.timestamps precision: 7
+ end
+ end
+ end
+
+ def test_database_agrees_with_activerecord_about_precision
+ @connection.create_table(:foos, force: true) do |t|
+ t.timestamps precision: 4
+ end
+ assert_equal 4, database_datetime_precision('foos', 'created_at')
+ assert_equal 4, database_datetime_precision('foos', 'updated_at')
+ end
+
+ def test_formatting_datetime_according_to_precision
+ @connection.create_table(:foos, force: true) do |t|
+ t.datetime :created_at, precision: 0
+ t.datetime :updated_at, precision: 4
+ end
+ date = ::Time.utc(2014, 8, 17, 12, 30, 0, 999999)
+ Foo.create!(created_at: date, updated_at: date)
+ assert foo = Foo.find_by(created_at: date)
+ assert_equal 1, Foo.where(updated_at: date).count
+ assert_equal date.to_s, foo.created_at.to_s
+ assert_equal date.to_s, foo.updated_at.to_s
+ assert_equal 000000, foo.created_at.usec
+ assert_equal 999900, foo.updated_at.usec
+ end
+
+ if current_adapter?(:PostgreSQLAdapter)
+ def test_datetime_precision_with_zero_should_be_dumped
+ @connection.create_table(:foos) do |t|
+ t.timestamps precision: 0
+ end
+ output = dump_table_schema("foos")
+ assert_match %r{t\.datetime\s+"created_at",\s+precision: 0,\s+null: false$}, output
+ assert_match %r{t\.datetime\s+"updated_at",\s+precision: 0,\s+null: false$}, output
+ end
+ end
+
+ private
+
+ def database_datetime_precision(table_name, column_name)
+ results = @connection.exec_query("SELECT column_name, datetime_precision FROM information_schema.columns WHERE table_name = '#{table_name}'")
+ result = results.find do |result_hash|
+ result_hash["column_name"] == column_name
+ end
+ result && result["datetime_precision"].to_i
+ end
+
+ def activerecord_column_option(tablename, column_name, option)
+ result = @connection.columns(tablename).find do |column|
+ column.name == column_name
+ end
+ result && result.send(option)
+ end
+end
+end
diff --git a/activerecord/test/cases/date_time_test.rb b/activerecord/test/cases/date_time_test.rb
index 330232cee2..4cbff564aa 100644
--- a/activerecord/test/cases/date_time_test.rb
+++ b/activerecord/test/cases/date_time_test.rb
@@ -55,7 +55,7 @@ class DateTimeTest < ActiveRecord::TestCase
now = DateTime.now
with_timezone_config default: :local do
task = Task.new starting: now
- assert now, task.starting
+ assert_equal now, task.starting
end
end
end
diff --git a/activerecord/test/cases/defaults_test.rb b/activerecord/test/cases/defaults_test.rb
index e9bc583bf4..b9db0d0123 100644
--- a/activerecord/test/cases/defaults_test.rb
+++ b/activerecord/test/cases/defaults_test.rb
@@ -40,7 +40,7 @@ class DefaultNumbersTest < ActiveRecord::TestCase
end
teardown do
- @connection.drop_table "default_numbers" if @connection.table_exists? 'default_numbers'
+ @connection.drop_table :default_numbers, if_exists: true
end
def test_default_positive_integer
diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb
index ae4a8aab2c..c2573ac72b 100644
--- a/activerecord/test/cases/dirty_test.rb
+++ b/activerecord/test/cases/dirty_test.rb
@@ -711,7 +711,7 @@ class DirtyTest < ActiveRecord::TestCase
test "attribute_will_change! doesn't try to save non-persistable attributes" do
klass = Class.new(ActiveRecord::Base) do
self.table_name = 'people'
- attribute :non_persisted_attribute, ActiveRecord::Type::String.new
+ attribute :non_persisted_attribute, :string
end
record = klass.new(first_name: "Sean")
@@ -721,6 +721,14 @@ class DirtyTest < ActiveRecord::TestCase
assert record.save
end
+ test "mutating and then assigning doesn't remove the change" do
+ pirate = Pirate.create!(catchphrase: "arrrr")
+ pirate.catchphrase << " matey!"
+ pirate.catchphrase = "arrrr matey!"
+
+ assert pirate.catchphrase_changed?(from: "arrrr", to: "arrrr matey!")
+ end
+
private
def with_partial_writes(klass, on = true)
old = klass.partial_writes?
diff --git a/activerecord/test/cases/enum_test.rb b/activerecord/test/cases/enum_test.rb
index 346fcab6ea..e70d492efd 100644
--- a/activerecord/test/cases/enum_test.rb
+++ b/activerecord/test/cases/enum_test.rb
@@ -26,6 +26,49 @@ class EnumTest < ActiveRecord::TestCase
assert_equal @book, Book.unread.first
end
+ test "find via where with values" do
+ proposed, written = Book.statuses[:proposed], Book.statuses[:written]
+
+ assert_equal @book, Book.where(status: proposed).first
+ refute_equal @book, Book.where(status: written).first
+ assert_equal @book, Book.where(status: [proposed]).first
+ refute_equal @book, Book.where(status: [written]).first
+ refute_equal @book, Book.where("status <> ?", proposed).first
+ assert_equal @book, Book.where("status <> ?", written).first
+ end
+
+ test "find via where with symbols" do
+ assert_equal @book, Book.where(status: :proposed).first
+ refute_equal @book, Book.where(status: :written).first
+ assert_equal @book, Book.where(status: [:proposed]).first
+ refute_equal @book, Book.where(status: [:written]).first
+ refute_equal @book, Book.where.not(status: :proposed).first
+ assert_equal @book, Book.where.not(status: :written).first
+ end
+
+ test "find via where with strings" do
+ assert_equal @book, Book.where(status: "proposed").first
+ refute_equal @book, Book.where(status: "written").first
+ assert_equal @book, Book.where(status: ["proposed"]).first
+ refute_equal @book, Book.where(status: ["written"]).first
+ refute_equal @book, Book.where.not(status: "proposed").first
+ assert_equal @book, Book.where.not(status: "written").first
+ end
+
+ test "build from scope" do
+ assert Book.written.build.written?
+ refute Book.written.build.proposed?
+ end
+
+ test "build from where" do
+ assert Book.where(status: Book.statuses[:written]).build.written?
+ refute Book.where(status: Book.statuses[:written]).build.proposed?
+ assert Book.where(status: :written).build.written?
+ refute Book.where(status: :written).build.proposed?
+ assert Book.where(status: "written").build.written?
+ refute Book.where(status: "written").build.proposed?
+ end
+
test "update by declaration" do
@book.written!
assert @book.written?
@@ -129,19 +172,24 @@ class EnumTest < ActiveRecord::TestCase
assert_equal "'unknown' is not a valid status", e.message
end
+ test "NULL values from database should be casted to nil" do
+ Book.where(id: @book.id).update_all("status = NULL")
+ assert_nil @book.reload.status
+ end
+
test "assign nil value" do
@book.status = nil
- assert @book.status.nil?
+ assert_nil @book.status
end
test "assign empty string value" do
@book.status = ''
- assert @book.status.nil?
+ assert_nil @book.status
end
test "assign long empty string value" do
@book.status = ' '
- assert @book.status.nil?
+ assert_nil @book.status
end
test "constant to access the mapping" do
@@ -161,7 +209,11 @@ class EnumTest < ActiveRecord::TestCase
end
test "_before_type_cast returns the enum label (required for form fields)" do
- assert_equal "proposed", @book.status_before_type_cast
+ if @book.status_came_from_user?
+ assert_equal "proposed", @book.status_before_type_cast
+ else
+ assert_equal "proposed", @book.status
+ end
end
test "reserved enum names" do
diff --git a/activerecord/test/cases/explain_test.rb b/activerecord/test/cases/explain_test.rb
index 9d25bdd82a..f1d5511bb8 100644
--- a/activerecord/test/cases/explain_test.rb
+++ b/activerecord/test/cases/explain_test.rb
@@ -28,7 +28,7 @@ if ActiveRecord::Base.connection.supports_explain?
assert_match "SELECT", sql
if binds.any?
assert_equal 1, binds.length
- assert_equal "honda", binds.flatten.last
+ assert_equal "honda", binds.last.value
else
assert_match 'honda', sql
end
diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb
index 07ec08ccf5..f41245dfd2 100644
--- a/activerecord/test/cases/fixtures_test.rb
+++ b/activerecord/test/cases/fixtures_test.rb
@@ -672,7 +672,8 @@ class FasterFixturesTest < ActiveRecord::TestCase
end
class FoxyFixturesTest < ActiveRecord::TestCase
- fixtures :parrots, :parrots_pirates, :pirates, :treasures, :mateys, :ships, :computers, :developers, :"admin/accounts", :"admin/users"
+ fixtures :parrots, :parrots_pirates, :pirates, :treasures, :mateys, :ships, :computers,
+ :developers, :"admin/accounts", :"admin/users", :live_parrots, :dead_parrots
if ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'
require 'models/uuid_parent'
@@ -812,6 +813,12 @@ class FoxyFixturesTest < ActiveRecord::TestCase
assert_equal pirates(:blackbeard), parrots(:polly).killer
end
+ def test_supports_sti_with_respective_files
+ assert_kind_of LiveParrot, live_parrots(:dusty)
+ assert_kind_of DeadParrot, dead_parrots(:deadbird)
+ assert_equal pirates(:blackbeard), dead_parrots(:deadbird).killer
+ end
+
def test_namespaced_models
assert admin_accounts(:signals37).users.include?(admin_users(:david))
assert_equal 2, admin_accounts(:signals37).users.size
@@ -834,9 +841,9 @@ class CustomNameForFixtureOrModelTest < ActiveRecord::TestCase
set_fixture_class :randomly_named_a9 =>
ClassNameThatDoesNotFollowCONVENTIONS,
:'admin/randomly_named_a9' =>
- Admin::ClassNameThatDoesNotFollowCONVENTIONS,
+ Admin::ClassNameThatDoesNotFollowCONVENTIONS1,
'admin/randomly_named_b0' =>
- Admin::ClassNameThatDoesNotFollowCONVENTIONS
+ Admin::ClassNameThatDoesNotFollowCONVENTIONS2
fixtures :randomly_named_a9, 'admin/randomly_named_a9',
:'admin/randomly_named_b0'
@@ -847,15 +854,15 @@ class CustomNameForFixtureOrModelTest < ActiveRecord::TestCase
end
def test_named_accessor_for_randomly_named_namespaced_fixture_and_class
- assert_kind_of Admin::ClassNameThatDoesNotFollowCONVENTIONS,
+ assert_kind_of Admin::ClassNameThatDoesNotFollowCONVENTIONS1,
admin_randomly_named_a9(:first_instance)
- assert_kind_of Admin::ClassNameThatDoesNotFollowCONVENTIONS,
+ assert_kind_of Admin::ClassNameThatDoesNotFollowCONVENTIONS2,
admin_randomly_named_b0(:second_instance)
end
def test_table_name_is_defined_in_the_model
- assert_equal 'randomly_named_table', ActiveRecord::FixtureSet::all_loaded_fixtures["admin/randomly_named_a9"].table_name
- assert_equal 'randomly_named_table', Admin::ClassNameThatDoesNotFollowCONVENTIONS.table_name
+ assert_equal 'randomly_named_table2', ActiveRecord::FixtureSet::all_loaded_fixtures["admin/randomly_named_a9"].table_name
+ assert_equal 'randomly_named_table2', Admin::ClassNameThatDoesNotFollowCONVENTIONS1.table_name
end
end
diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb
index 925491acbd..f1f927852c 100644
--- a/activerecord/test/cases/helper.rb
+++ b/activerecord/test/cases/helper.rb
@@ -30,6 +30,9 @@ ARTest.connect
# Quote "type" if it's a reserved word for the current connection.
QUOTED_TYPE = ActiveRecord::Base.connection.quote_column_name('type')
+# FIXME: Remove this when the deprecation cycle on TZ aware types by default ends.
+ActiveRecord::Base.time_zone_aware_types << :time
+
def current_adapter?(*types)
types.any? do |type|
ActiveRecord::ConnectionAdapters.const_defined?(type) &&
@@ -121,7 +124,7 @@ def enable_extension!(extension, connection)
return connection.reconnect! if connection.extension_enabled?(extension)
connection.enable_extension extension
- connection.commit_db_transaction
+ connection.commit_db_transaction if connection.transaction_open?
connection.reconnect!
end
diff --git a/activerecord/test/cases/inheritance_test.rb b/activerecord/test/cases/inheritance_test.rb
index fe6323ab02..6b18bfe84f 100644
--- a/activerecord/test/cases/inheritance_test.rb
+++ b/activerecord/test/cases/inheritance_test.rb
@@ -294,12 +294,12 @@ class InheritanceTest < ActiveRecord::TestCase
def test_eager_load_belongs_to_something_inherited
account = Account.all.merge!(:includes => :firm).find(1)
- assert account.association_cache.key?(:firm), "nil proves eager load failed"
+ assert account.association(:firm).loaded?, "association was not eager loaded"
end
def test_alt_eager_loading
cabbage = RedCabbage.all.merge!(:includes => :seller).find(4)
- assert cabbage.association_cache.key?(:seller), "nil proves eager load failed"
+ assert cabbage.association(:seller).loaded?, "association was not eager loaded"
end
def test_eager_load_belongs_to_primary_key_quoting
diff --git a/activerecord/test/cases/integration_test.rb b/activerecord/test/cases/integration_test.rb
index 0021988083..018b7b0d8f 100644
--- a/activerecord/test/cases/integration_test.rb
+++ b/activerecord/test/cases/integration_test.rb
@@ -1,4 +1,3 @@
-# encoding: utf-8
require 'cases/helper'
require 'models/company'
diff --git a/activerecord/test/cases/log_subscriber_test.rb b/activerecord/test/cases/log_subscriber_test.rb
index 97c0350911..4192d12ff4 100644
--- a/activerecord/test/cases/log_subscriber_test.rb
+++ b/activerecord/test/cases/log_subscriber_test.rb
@@ -63,14 +63,6 @@ class LogSubscriberTest < ActiveRecord::TestCase
assert_match(/ruby rails/, logger.debugs.first)
end
- def test_ignore_binds_payload_with_nil_column
- event = Struct.new(:duration, :payload)
-
- logger = TestDebugLogSubscriber.new
- logger.sql(event.new(0, sql: 'hi mom!', binds: [[nil, 1]]))
- assert_equal 1, logger.debugs.length
- end
-
def test_basic_query_logging
Developer.all.load
wait
diff --git a/activerecord/test/cases/migration/change_schema_test.rb b/activerecord/test/cases/migration/change_schema_test.rb
index b3129a8984..3cd4073a38 100644
--- a/activerecord/test/cases/migration/change_schema_test.rb
+++ b/activerecord/test/cases/migration/change_schema_test.rb
@@ -68,8 +68,8 @@ module ActiveRecord
five = columns.detect { |c| c.name == "five" } unless mysql
assert_equal "hello", one.default
- assert_equal true, two.type_cast_from_database(two.default)
- assert_equal false, three.type_cast_from_database(three.default)
+ assert_equal true, connection.lookup_cast_type_from_column(two).type_cast_from_database(two.default)
+ assert_equal false, connection.lookup_cast_type_from_column(three).type_cast_from_database(three.default)
assert_equal '1', four.default
assert_equal "hello", five.default unless mysql
end
@@ -403,6 +403,17 @@ module ActiveRecord
end
end
+ def test_drop_table_if_exists
+ connection.create_table(:testings)
+ assert connection.table_exists?(:testings)
+ connection.drop_table(:testings, if_exists: true)
+ assert_not connection.table_exists?(:testings)
+ end
+
+ def test_drop_table_if_exists_nothing_raised
+ assert_nothing_raised { connection.drop_table(:nonexistent, if_exists: true) }
+ end
+
private
def testing_table_with_only_foo_attribute
connection.create_table :testings, :id => false do |t|
@@ -426,7 +437,7 @@ module ActiveRecord
teardown do
[:wagons, :trains].each do |table|
- @connection.drop_table(table) if @connection.table_exists?(table)
+ @connection.drop_table table, if_exists: true
end
end
diff --git a/activerecord/test/cases/migration/column_positioning_test.rb b/activerecord/test/cases/migration/column_positioning_test.rb
index 62186e13a5..4637970ce0 100644
--- a/activerecord/test/cases/migration/column_positioning_test.rb
+++ b/activerecord/test/cases/migration/column_positioning_test.rb
@@ -3,7 +3,7 @@ require 'cases/helper'
module ActiveRecord
class Migration
class ColumnPositioningTest < ActiveRecord::TestCase
- attr_reader :connection, :table_name
+ attr_reader :connection
alias :conn :connection
def setup
diff --git a/activerecord/test/cases/migration/columns_test.rb b/activerecord/test/cases/migration/columns_test.rb
index e6aa901814..6f65288ac0 100644
--- a/activerecord/test/cases/migration/columns_test.rb
+++ b/activerecord/test/cases/migration/columns_test.rb
@@ -65,7 +65,7 @@ module ActiveRecord
if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
def test_mysql_rename_column_preserves_auto_increment
rename_column "test_models", "id", "id_test"
- assert_equal "auto_increment", connection.columns("test_models").find { |c| c.name == "id_test" }.extra
+ assert connection.columns("test_models").find { |c| c.name == "id_test" }.auto_increment?
TestModel.reset_column_information
ensure
rename_column "test_models", "id_test", "id"
@@ -196,7 +196,7 @@ module ActiveRecord
old_columns = connection.columns(TestModel.table_name)
assert old_columns.find { |c|
- default = c.type_cast_from_database(c.default)
+ default = connection.lookup_cast_type_from_column(c).type_cast_from_database(c.default)
c.name == 'approved' && c.type == :boolean && default == true
}
@@ -204,11 +204,11 @@ module ActiveRecord
new_columns = connection.columns(TestModel.table_name)
assert_not new_columns.find { |c|
- default = c.type_cast_from_database(c.default)
+ default = connection.lookup_cast_type_from_column(c).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)
+ default = connection.lookup_cast_type_from_column(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
diff --git a/activerecord/test/cases/migration/command_recorder_test.rb b/activerecord/test/cases/migration/command_recorder_test.rb
index 8cba777fe2..3844b1a92e 100644
--- a/activerecord/test/cases/migration/command_recorder_test.rb
+++ b/activerecord/test/cases/migration/command_recorder_test.rb
@@ -256,6 +256,11 @@ module ActiveRecord
assert_equal [:add_reference, [:table, :taggable, { polymorphic: true }], nil], add
end
+ def test_invert_remove_reference_with_index_and_foreign_key
+ add = @recorder.inverse_of :remove_reference, [:table, :taggable, { index: true, foreign_key: true }]
+ assert_equal [:add_reference, [:table, :taggable, { index: true, foreign_key: true }], nil], add
+ end
+
def test_invert_remove_belongs_to_alias
add = @recorder.inverse_of :remove_belongs_to, [:table, :user]
assert_equal [:add_reference, [:table, :user], nil], add
diff --git a/activerecord/test/cases/migration/foreign_key_test.rb b/activerecord/test/cases/migration/foreign_key_test.rb
index f8b1bf8c9d..b2f2d077eb 100644
--- a/activerecord/test/cases/migration/foreign_key_test.rb
+++ b/activerecord/test/cases/migration/foreign_key_test.rb
@@ -8,6 +8,7 @@ module ActiveRecord
class ForeignKeyTest < ActiveRecord::TestCase
include DdlHelper
include SchemaDumpingHelper
+ include ActiveSupport::Testing::Stream
class Rocket < ActiveRecord::Base
end
@@ -29,8 +30,8 @@ module ActiveRecord
teardown do
if defined?(@connection)
- @connection.drop_table "astronauts" if @connection.table_exists? 'astronauts'
- @connection.drop_table "rockets" if @connection.table_exists? 'rockets'
+ @connection.drop_table "astronauts", if_exists: true
+ @connection.drop_table "rockets", if_exists: true
end
end
@@ -57,7 +58,7 @@ module ActiveRecord
assert_equal "rockets", fk.to_table
assert_equal "rocket_id", fk.column
assert_equal "id", fk.primary_key
- assert_match(/^fk_rails_.{10}$/, fk.name)
+ assert_equal("fk_rails_78146ddd2e", fk.name)
end
def test_add_foreign_key_with_column
@@ -71,7 +72,7 @@ module ActiveRecord
assert_equal "rockets", fk.to_table
assert_equal "rocket_id", fk.column
assert_equal "id", fk.primary_key
- assert_match(/^fk_rails_.{10}$/, fk.name)
+ assert_equal("fk_rails_78146ddd2e", fk.name)
end
def test_add_foreign_key_with_non_standard_primary_key
@@ -221,17 +222,6 @@ module ActiveRecord
silence_stream($stdout) { migration.migrate(:down) }
end
- private
-
- def silence_stream(stream)
- old_stream = stream.dup
- stream.reopen(IO::NULL)
- stream.sync = true
- yield
- ensure
- stream.reopen(old_stream)
- old_stream.close
- end
end
end
end
diff --git a/activerecord/test/cases/migration/references_foreign_key_test.rb b/activerecord/test/cases/migration/references_foreign_key_test.rb
index bba8897a0d..17ac72a109 100644
--- a/activerecord/test/cases/migration/references_foreign_key_test.rb
+++ b/activerecord/test/cases/migration/references_foreign_key_test.rb
@@ -10,8 +10,8 @@ module ActiveRecord
end
teardown do
- @connection.execute("drop table if exists testings")
- @connection.execute("drop table if exists testing_parents")
+ @connection.drop_table "testings", if_exists: true
+ @connection.drop_table "testing_parents", if_exists: true
end
test "foreign keys can be created with the table" do
@@ -95,6 +95,16 @@ module ActiveRecord
end
end
end
+
+ test "foreign key column can be removed" do
+ @connection.create_table :testings do |t|
+ t.references :testing_parent, index: true, foreign_key: true
+ end
+
+ assert_difference "@connection.foreign_keys('testings').size", -1 do
+ @connection.remove_reference :testings, :testing_parent, foreign_key: true
+ end
+ end
end
end
end
diff --git a/activerecord/test/cases/migration/rename_table_test.rb b/activerecord/test/cases/migration/rename_table_test.rb
index a018bac43d..3eef308428 100644
--- a/activerecord/test/cases/migration/rename_table_test.rb
+++ b/activerecord/test/cases/migration/rename_table_test.rb
@@ -86,8 +86,8 @@ module ActiveRecord
assert connection.table_exists? :felines
ensure
disable_extension!('uuid-ossp', connection)
- connection.drop_table :cats if connection.table_exists? :cats
- connection.drop_table :felines if connection.table_exists? :felines
+ connection.drop_table :cats, if_exists: true
+ connection.drop_table :felines, if_exists: true
end
end
end
diff --git a/activerecord/test/cases/migration/table_and_index_test.rb b/activerecord/test/cases/migration/table_and_index_test.rb
index 8fd770abd1..24cba84a09 100644
--- a/activerecord/test/cases/migration/table_and_index_test.rb
+++ b/activerecord/test/cases/migration/table_and_index_test.rb
@@ -6,11 +6,11 @@ module ActiveRecord
def test_add_schema_info_respects_prefix_and_suffix
conn = ActiveRecord::Base.connection
- conn.drop_table(ActiveRecord::Migrator.schema_migrations_table_name) if conn.table_exists?(ActiveRecord::Migrator.schema_migrations_table_name)
+ conn.drop_table(ActiveRecord::Migrator.schema_migrations_table_name, if_exists: true)
# Use shorter prefix and suffix as in Oracle database identifier cannot be larger than 30 characters
ActiveRecord::Base.table_name_prefix = 'p_'
ActiveRecord::Base.table_name_suffix = '_s'
- conn.drop_table(ActiveRecord::Migrator.schema_migrations_table_name) if conn.table_exists?(ActiveRecord::Migrator.schema_migrations_table_name)
+ conn.drop_table(ActiveRecord::Migrator.schema_migrations_table_name, if_exists: true)
conn.initialize_schema_migrations_table
diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb
index 7c2d3e81d6..5f9fd5d527 100644
--- a/activerecord/test/cases/migration_test.rb
+++ b/activerecord/test/cases/migration_test.rb
@@ -14,9 +14,9 @@ require MIGRATIONS_ROOT + "/decimal/1_give_me_big_numbers"
class BigNumber < ActiveRecord::Base
unless current_adapter?(:PostgreSQLAdapter, :SQLite3Adapter)
- attribute :value_of_e, Type::Integer.new
+ attribute :value_of_e, :integer
end
- attribute :my_house_population, Type::Integer.new
+ attribute :my_house_population, :integer
end
class Reminder < ActiveRecord::Base; end
@@ -90,7 +90,7 @@ class MigrationTest < ActiveRecord::TestCase
end
def test_migration_detection_without_schema_migration_table
- ActiveRecord::Base.connection.drop_table('schema_migrations') if ActiveRecord::Base.connection.table_exists?('schema_migrations')
+ ActiveRecord::Base.connection.drop_table 'schema_migrations', if_exists: true
migrations_path = MIGRATIONS_ROOT + "/valid"
old_path = ActiveRecord::Migrator.migrations_paths
@@ -119,10 +119,6 @@ class MigrationTest < ActiveRecord::TestCase
end
def test_create_table_with_force_true_does_not_drop_nonexisting_table
- if Person.connection.table_exists?(:testings2)
- Person.connection.drop_table :testings2
- end
-
# using a copy as we need the drop_table method to
# continue to work for the ensure block of the test
temp_conn = Person.connection.dup
@@ -133,7 +129,7 @@ class MigrationTest < ActiveRecord::TestCase
t.column :foo, :string
end
ensure
- Person.connection.drop_table :testings2 rescue nil
+ Person.connection.drop_table :testings2, if_exists: true
end
def connection
@@ -162,6 +158,7 @@ class MigrationTest < ActiveRecord::TestCase
assert !BigNumber.table_exists?
GiveMeBigNumbers.up
+ BigNumber.reset_column_information
assert BigNumber.create(
:bank_balance => 1586.43,
@@ -397,6 +394,7 @@ class MigrationTest < ActiveRecord::TestCase
Thing.reset_table_name
Thing.reset_sequence_name
WeNeedThings.up
+ Thing.reset_column_information
assert Thing.create("content" => "hello world")
assert_equal "hello world", Thing.first.content
@@ -416,6 +414,7 @@ class MigrationTest < ActiveRecord::TestCase
ActiveRecord::Base.table_name_suffix = '_suffix'
Reminder.reset_table_name
Reminder.reset_sequence_name
+ Reminder.reset_column_information
WeNeedReminders.up
assert Reminder.create("content" => "hello world", "remind_at" => Time.now)
assert_equal "hello world", Reminder.first.content
@@ -427,8 +426,6 @@ class MigrationTest < ActiveRecord::TestCase
end
def test_create_table_with_binary_column
- Person.connection.drop_table :binary_testings rescue nil
-
assert_nothing_raised {
Person.connection.create_table :binary_testings do |t|
t.column "data", :binary, :null => false
@@ -440,7 +437,7 @@ class MigrationTest < ActiveRecord::TestCase
assert_nil data_column.default
- Person.connection.drop_table :binary_testings rescue nil
+ Person.connection.drop_table :binary_testings, if_exists: true
end
unless mysql_enforcing_gtid_consistency?
@@ -718,6 +715,8 @@ if ActiveRecord::Base.connection.supports_bulk_alter?
end
class CopyMigrationsTest < ActiveRecord::TestCase
+ include ActiveSupport::Testing::Stream
+
def setup
end
@@ -927,23 +926,4 @@ class CopyMigrationsTest < ActiveRecord::TestCase
ActiveRecord::Base.logger = old
end
- private
-
- def quietly
- silence_stream(STDOUT) do
- silence_stream(STDERR) do
- yield
- end
- end
- end
-
- def silence_stream(stream)
- old_stream = stream.dup
- stream.reopen(IO::NULL)
- stream.sync = true
- yield
- ensure
- stream.reopen(old_stream)
- old_stream.close
- end
end
diff --git a/activerecord/test/cases/multiparameter_attributes_test.rb b/activerecord/test/cases/multiparameter_attributes_test.rb
index 14d4ef457d..ae18573126 100644
--- a/activerecord/test/cases/multiparameter_attributes_test.rb
+++ b/activerecord/test/cases/multiparameter_attributes_test.rb
@@ -199,6 +199,7 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase
def test_multiparameter_attributes_on_time_with_time_zone_aware_attributes
with_timezone_config default: :utc, aware_attributes: true, zone: -28800 do
+ Topic.reset_column_information
attributes = {
"written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24",
"written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00"
@@ -209,6 +210,8 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase
assert_equal Time.utc(2004, 6, 24, 16, 24, 0), topic.written_on.time
assert_equal Time.zone, topic.written_on.time_zone
end
+ ensure
+ Topic.reset_column_information
end
def test_multiparameter_attributes_on_time_with_time_zone_aware_attributes_false
@@ -227,6 +230,7 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase
def test_multiparameter_attributes_on_time_with_skip_time_zone_conversion_for_attributes
with_timezone_config default: :utc, aware_attributes: true, zone: -28800 do
Topic.skip_time_zone_conversion_for_attributes = [:written_on]
+ Topic.reset_column_information
attributes = {
"written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24",
"written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00"
@@ -238,21 +242,25 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase
end
ensure
Topic.skip_time_zone_conversion_for_attributes = []
+ Topic.reset_column_information
end
# Oracle does not have a TIME datatype.
unless current_adapter?(:OracleAdapter)
def test_multiparameter_attributes_on_time_only_column_with_time_zone_aware_attributes_does_not_do_time_zone_conversion
with_timezone_config default: :utc, aware_attributes: true, zone: -28800 do
+ Topic.reset_column_information
attributes = {
"bonus_time(1i)" => "2000", "bonus_time(2i)" => "1", "bonus_time(3i)" => "1",
"bonus_time(4i)" => "16", "bonus_time(5i)" => "24"
}
topic = Topic.find(1)
topic.attributes = attributes
- assert_equal Time.utc(2000, 1, 1, 16, 24, 0), topic.bonus_time
- assert topic.bonus_time.utc?
+ assert_equal Time.zone.local(2000, 1, 1, 16, 24, 0), topic.bonus_time
+ assert_not topic.bonus_time.utc?
end
+ ensure
+ Topic.reset_column_information
end
end
diff --git a/activerecord/test/cases/multiple_db_test.rb b/activerecord/test/cases/multiple_db_test.rb
index 15c60d5562..f9bc266e84 100644
--- a/activerecord/test/cases/multiple_db_test.rb
+++ b/activerecord/test/cases/multiple_db_test.rb
@@ -93,14 +93,14 @@ class MultipleDbTest < ActiveRecord::TestCase
assert_not_equal Entrant.arel_engine.connection, Course.arel_engine.connection
end
- def test_count_on_custom_connection
- ActiveRecord::Base.remove_connection
- assert_equal 1, College.count
- ensure
- ActiveRecord::Base.establish_connection :arunit
- end
-
unless in_memory_db?
+ def test_count_on_custom_connection
+ ActiveRecord::Base.remove_connection
+ assert_equal 1, College.count
+ ensure
+ ActiveRecord::Base.establish_connection :arunit
+ end
+
def test_associations_should_work_when_model_has_no_connection
begin
ActiveRecord::Base.remove_connection
diff --git a/activerecord/test/cases/nested_attributes_test.rb b/activerecord/test/cases/nested_attributes_test.rb
index 5c7e8a65d2..198cd6f341 100644
--- a/activerecord/test/cases/nested_attributes_test.rb
+++ b/activerecord/test/cases/nested_attributes_test.rb
@@ -672,7 +672,7 @@ module NestedAttributesOnACollectionAssociationTests
end
def test_should_not_assign_destroy_key_to_a_record
- assert_nothing_raised ActiveRecord::UnknownAttributeError do
+ assert_nothing_raised ActiveModel::AttributeAssignment::UnknownAttributeError do
@pirate.send(association_setter, { 'foo' => { '_destroy' => '0' }})
end
end
@@ -1037,4 +1037,21 @@ class TestHasManyAutosaveAssociationWhichItselfHasAutosaveAssociations < ActiveR
ShipPart.create!(:ship => @ship, :name => "Stern")
assert_no_queries { @ship.valid? }
end
+
+ test "circular references do not perform unnecessary queries" do
+ ship = Ship.new(name: "The Black Rock")
+ part = ship.parts.build(name: "Stern")
+ ship.treasures.build(looter: part)
+
+ assert_queries 3 do
+ ship.save!
+ end
+ end
+
+ test "nested singular associations are validated" do
+ part = ShipPart.new(name: "Stern", ship_attributes: { name: nil })
+
+ assert_not part.valid?
+ assert_equal ["Ship name can't be blank"], part.errors.full_messages
+ end
end
diff --git a/activerecord/test/cases/persistence_test.rb b/activerecord/test/cases/persistence_test.rb
index d6816041bc..2803ad2de0 100644
--- a/activerecord/test/cases/persistence_test.rb
+++ b/activerecord/test/cases/persistence_test.rb
@@ -356,6 +356,16 @@ class PersistenceTest < ActiveRecord::TestCase
assert_equal("David", topic_reloaded.author_name)
end
+ def test_update_attribute_does_not_run_sql_if_attribute_is_not_changed
+ klass = Class.new(Topic) do
+ def self.name; 'Topic'; end
+ end
+ topic = klass.create(title: 'Another New Topic')
+ assert_queries(0) do
+ topic.update_attribute(:title, 'Another New Topic')
+ end
+ end
+
def test_delete
topic = Topic.find(1)
assert_equal topic, topic.delete, 'topic.delete did not return self'
diff --git a/activerecord/test/cases/primary_keys_test.rb b/activerecord/test/cases/primary_keys_test.rb
index 751eccc015..4b668f84dd 100644
--- a/activerecord/test/cases/primary_keys_test.rb
+++ b/activerecord/test/cases/primary_keys_test.rb
@@ -210,7 +210,7 @@ class PrimaryKeyAnyTypeTest < ActiveRecord::TestCase
end
teardown do
- @connection.execute("DROP TABLE IF EXISTS barcodes")
+ @connection.drop_table(:barcodes) if @connection.table_exists? :barcodes
end
def test_any_type_primary_key
diff --git a/activerecord/test/cases/quoting_test.rb b/activerecord/test/cases/quoting_test.rb
index ad09340518..6d91f96bf6 100644
--- a/activerecord/test/cases/quoting_test.rb
+++ b/activerecord/test/cases/quoting_test.rb
@@ -46,28 +46,28 @@ module ActiveRecord
def test_quoted_time_utc
with_timezone_config default: :utc do
- t = Time.now
+ t = Time.now.change(usec: 0)
assert_equal t.getutc.to_s(:db), @quoter.quoted_date(t)
end
end
def test_quoted_time_local
with_timezone_config default: :local do
- t = Time.now
+ t = Time.now.change(usec: 0)
assert_equal t.getlocal.to_s(:db), @quoter.quoted_date(t)
end
end
def test_quoted_time_crazy
with_timezone_config default: :asdfasdf do
- t = Time.now
+ t = Time.now.change(usec: 0)
assert_equal t.getlocal.to_s(:db), @quoter.quoted_date(t)
end
end
def test_quoted_datetime_utc
with_timezone_config default: :utc do
- t = DateTime.now
+ t = Time.now.change(usec: 0).to_datetime
assert_equal t.getutc.to_s(:db), @quoter.quoted_date(t)
end
end
@@ -76,7 +76,7 @@ module ActiveRecord
# DateTime doesn't define getlocal, so make sure it does nothing
def test_quoted_datetime_local
with_timezone_config default: :local do
- t = DateTime.now
+ t = Time.now.change(usec: 0).to_datetime
assert_equal t.to_s(:db), @quoter.quoted_date(t)
end
end
diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb
index a2252a836f..b0e40c7145 100644
--- a/activerecord/test/cases/reflection_test.rb
+++ b/activerecord/test/cases/reflection_test.rb
@@ -86,18 +86,15 @@ class ReflectionTest < ActiveRecord::TestCase
assert_equal "attribute_that_doesnt_exist", column.name
assert_equal nil, column.sql_type
assert_equal nil, column.type
- assert_not column.number?
- assert_not column.text?
- assert_not column.binary?
end
- def test_non_existent_columns_are_identity_types
- column = @first.column_for_attribute("attribute_that_doesnt_exist")
+ def test_non_existent_types_are_identity_types
+ type = @first.type_for_attribute("attribute_that_doesnt_exist")
object = Object.new
- assert_equal object, column.type_cast_from_database(object)
- assert_equal object, column.type_cast_from_user(object)
- assert_equal object, column.type_cast_for_database(object)
+ assert_equal object, type.type_cast_from_database(object)
+ assert_equal object, type.type_cast_from_user(object)
+ assert_equal object, type.type_cast_for_database(object)
end
def test_reflection_klass_for_nested_class_name
diff --git a/activerecord/test/cases/relation/merging_test.rb b/activerecord/test/cases/relation/merging_test.rb
index eb76ef6328..0a2e874e4f 100644
--- a/activerecord/test/cases/relation/merging_test.rb
+++ b/activerecord/test/cases/relation/merging_test.rb
@@ -82,26 +82,15 @@ class RelationMergingTest < ActiveRecord::TestCase
left = Post.where(title: "omg").where(comments_count: 1)
right = Post.where(title: "wtf").where(title: "bbq")
- expected = [left.bind_values[1]] + right.bind_values
+ expected = [left.bound_attributes[1]] + right.bound_attributes
merged = left.merge(right)
- assert_equal expected, merged.bind_values
+ assert_equal expected, merged.bound_attributes
assert !merged.to_sql.include?("omg")
assert merged.to_sql.include?("wtf")
assert merged.to_sql.include?("bbq")
end
- def test_merging_keeps_lhs_bind_parameters
- column = Post.columns_hash['id']
- binds = [[column, 20]]
-
- right = Post.where(id: 20)
- left = Post.where(id: 10)
-
- merged = left.merge(right)
- assert_equal binds, merged.bind_values
- end
-
def test_merging_reorders_bind_params
post = Post.first
right = Post.where(id: 1)
diff --git a/activerecord/test/cases/relation/mutation_test.rb b/activerecord/test/cases/relation/mutation_test.rb
index 2443f10269..45ead08bd5 100644
--- a/activerecord/test/cases/relation/mutation_test.rb
+++ b/activerecord/test/cases/relation/mutation_test.rb
@@ -81,7 +81,7 @@ module ActiveRecord
assert_equal [], relation.extending_values
end
- (Relation::SINGLE_VALUE_METHODS - [:from, :lock, :reordering, :reverse_order, :create_with]).each do |method|
+ (Relation::SINGLE_VALUE_METHODS - [:lock, :reordering, :reverse_order, :create_with]).each do |method|
test "##{method}!" do
assert relation.public_send("#{method}!", :foo).equal?(relation)
assert_equal :foo, relation.public_send("#{method}_value")
@@ -90,7 +90,7 @@ module ActiveRecord
test '#from!' do
assert relation.from!('foo').equal?(relation)
- assert_equal ['foo', nil], relation.from_value
+ assert_equal 'foo', relation.from_clause.value
end
test '#lock!' do
@@ -136,12 +136,12 @@ module ActiveRecord
end
test 'test_merge!' do
- assert relation.merge!(where: :foo).equal?(relation)
- assert_equal [:foo], relation.where_values
+ assert relation.merge!(select: :foo).equal?(relation)
+ assert_equal [:foo], relation.select_values
end
test 'merge with a proc' do
- assert_equal [:foo], relation.merge(-> { where(:foo) }).where_values
+ assert_equal [:foo], relation.merge(-> { select(:foo) }).select_values
end
test 'none!' do
diff --git a/activerecord/test/cases/relation/or_test.rb b/activerecord/test/cases/relation/or_test.rb
new file mode 100644
index 0000000000..2006fc9611
--- /dev/null
+++ b/activerecord/test/cases/relation/or_test.rb
@@ -0,0 +1,84 @@
+require "cases/helper"
+require 'models/post'
+
+module ActiveRecord
+ class OrTest < ActiveRecord::TestCase
+ fixtures :posts
+
+ def test_or_with_relation
+ expected = Post.where('id = 1 or id = 2').to_a
+ assert_equal expected, Post.where('id = 1').or(Post.where('id = 2')).to_a
+ end
+
+ def test_or_identity
+ expected = Post.where('id = 1').to_a
+ assert_equal expected, Post.where('id = 1').or(Post.where('id = 1')).to_a
+ end
+
+ def test_or_with_null_left
+ expected = Post.where('id = 1').to_a
+ assert_equal expected, Post.none.or(Post.where('id = 1')).to_a
+ end
+
+ def test_or_with_null_right
+ expected = Post.where('id = 1').to_a
+ assert_equal expected, Post.where('id = 1').or(Post.none).to_a
+ end
+
+ def test_or_with_bind_params
+ assert_equal Post.find([1, 2]), Post.where(id: 1).or(Post.where(id: 2)).to_a
+ end
+
+ def test_or_with_null_both
+ expected = Post.none.to_a
+ assert_equal expected, Post.none.or(Post.none).to_a
+ end
+
+ def test_or_without_left_where
+ expected = Post.all
+ assert_equal expected, Post.or(Post.where('id = 1')).to_a
+ end
+
+ def test_or_without_right_where
+ expected = Post.all
+ assert_equal expected, Post.where('id = 1').or(Post.all).to_a
+ end
+
+ def test_or_preserves_other_querying_methods
+ expected = Post.where('id = 1 or id = 2 or id = 3').order('body asc').to_a
+ partial = Post.order('body asc')
+ assert_equal expected, partial.where('id = 1').or(partial.where(:id => [2, 3])).to_a
+ assert_equal expected, Post.order('body asc').where('id = 1').or(Post.order('body asc').where(:id => [2, 3])).to_a
+ end
+
+ def test_or_with_incompatible_relations
+ assert_raises ArgumentError do
+ Post.order('body asc').where('id = 1').or(Post.order('id desc').where(:id => [2, 3])).to_a
+ end
+ end
+
+ def test_or_when_grouping
+ groups = Post.where('id < 10').group('body').select('body, COUNT(*) AS c')
+ expected = groups.having("COUNT(*) > 1 OR body like 'Such%'").to_a.map {|o| [o.body, o.c] }
+ assert_equal expected, groups.having('COUNT(*) > 1').or(groups.having("body like 'Such%'")).to_a.map {|o| [o.body, o.c] }
+ end
+
+ def test_or_with_named_scope
+ expected = Post.where("id = 1 or body LIKE '\%a\%'").to_a
+ assert_equal expected, Post.where('id = 1').or(Post.containing_the_letter_a)
+ end
+
+ def test_or_inside_named_scope
+ expected = Post.where("body LIKE '\%a\%' OR title LIKE ?", "%'%").order('id DESC').to_a
+ assert_equal expected, Post.order(id: :desc).typographically_interesting
+ end
+
+ def test_or_on_loaded_relation
+ expected = Post.where('id = 1 or id = 2').to_a
+ p = Post.where('id = 1')
+ p.load
+ assert_equal p.loaded?, true
+ assert_equal expected, p.or(Post.where('id = 2')).to_a
+ end
+ end
+end
diff --git a/activerecord/test/cases/relation/predicate_builder_test.rb b/activerecord/test/cases/relation/predicate_builder_test.rb
index 0cc081fced..8f62014622 100644
--- a/activerecord/test/cases/relation/predicate_builder_test.rb
+++ b/activerecord/test/cases/relation/predicate_builder_test.rb
@@ -8,7 +8,7 @@ module ActiveRecord
Arel::Nodes::InfixOperation.new('~', column, Arel.sql(value.source))
end)
- assert_match %r{["`]topics["`].["`]title["`] ~ rails}i, Topic.where(title: /rails/).to_sql
+ assert_match %r{["`]topics["`]\.["`]title["`] ~ rails}i, Topic.where(title: /rails/).to_sql
ensure
Topic.reset_column_information
end
diff --git a/activerecord/test/cases/relation/where_chain_test.rb b/activerecord/test/cases/relation/where_chain_test.rb
index 619055f1e7..27bbd80f79 100644
--- a/activerecord/test/cases/relation/where_chain_test.rb
+++ b/activerecord/test/cases/relation/where_chain_test.rb
@@ -11,22 +11,11 @@ module ActiveRecord
@name = 'title'
end
- def test_not_eq
+ def test_not_inverts_where_clause
relation = Post.where.not(title: 'hello')
+ expected_where_clause = Post.where(title: 'hello').where_clause.invert
- assert_equal 1, relation.where_values.length
-
- value = relation.where_values.first
- bind = relation.bind_values.first
-
- assert_bound_ast value, Post.arel_table[@name], Arel::Nodes::NotEqual
- assert_equal 'hello', bind.last
- end
-
- def test_not_null
- expected = Post.arel_table[@name].not_eq(nil)
- relation = Post.where.not(title: nil)
- assert_equal([expected], relation.where_values)
+ assert_equal expected_where_clause, relation.where_clause
end
def test_not_with_nil
@@ -35,146 +24,81 @@ module ActiveRecord
end
end
- def test_not_in
- expected = Post.arel_table[@name].not_in(%w[hello goodbye])
- relation = Post.where.not(title: %w[hello goodbye])
- assert_equal([expected], relation.where_values)
- end
-
def test_association_not_eq
- expected = Comment.arel_table[@name].not_eq('hello')
+ expected = Arel::Nodes::Grouping.new(Comment.arel_table[@name].not_eq(Arel::Nodes::BindParam.new))
relation = Post.joins(:comments).where.not(comments: {title: 'hello'})
- assert_equal(expected.to_sql, relation.where_values.first.to_sql)
+ assert_equal(expected.to_sql, relation.where_clause.ast.to_sql)
end
def test_not_eq_with_preceding_where
relation = Post.where(title: 'hello').where.not(title: 'world')
+ expected_where_clause =
+ Post.where(title: 'hello').where_clause +
+ Post.where(title: 'world').where_clause.invert
- value = relation.where_values.first
- bind = relation.bind_values.first
- assert_bound_ast value, Post.arel_table[@name], Arel::Nodes::Equality
- assert_equal 'hello', bind.last
-
- value = relation.where_values.last
- bind = relation.bind_values.last
- assert_bound_ast value, Post.arel_table[@name], Arel::Nodes::NotEqual
- assert_equal 'world', bind.last
+ assert_equal expected_where_clause, relation.where_clause
end
def test_not_eq_with_succeeding_where
relation = Post.where.not(title: 'hello').where(title: 'world')
+ expected_where_clause =
+ Post.where(title: 'hello').where_clause.invert +
+ Post.where(title: 'world').where_clause
- value = relation.where_values.first
- bind = relation.bind_values.first
- assert_bound_ast value, Post.arel_table[@name], Arel::Nodes::NotEqual
- assert_equal 'hello', bind.last
-
- value = relation.where_values.last
- bind = relation.bind_values.last
- assert_bound_ast value, Post.arel_table[@name], Arel::Nodes::Equality
- assert_equal 'world', bind.last
- end
-
- def test_not_eq_with_string_parameter
- expected = Arel::Nodes::Not.new("title = 'hello'")
- relation = Post.where.not("title = 'hello'")
- assert_equal([expected], relation.where_values)
- end
-
- def test_not_eq_with_array_parameter
- expected = Arel::Nodes::Not.new("title = 'hello'")
- relation = Post.where.not(['title = ?', 'hello'])
- assert_equal([expected], relation.where_values)
+ assert_equal expected_where_clause, relation.where_clause
end
def test_chaining_multiple
relation = Post.where.not(author_id: [1, 2]).where.not(title: 'ruby on rails')
+ expected_where_clause =
+ Post.where(author_id: [1, 2]).where_clause.invert +
+ Post.where(title: 'ruby on rails').where_clause.invert
- expected = Post.arel_table['author_id'].not_in([1, 2])
- assert_equal(expected, relation.where_values[0])
-
- value = relation.where_values[1]
- bind = relation.bind_values.first
-
- assert_bound_ast value, Post.arel_table[@name], Arel::Nodes::NotEqual
- assert_equal 'ruby on rails', bind.last
+ assert_equal expected_where_clause, relation.where_clause
end
def test_rewhere_with_one_condition
relation = Post.where(title: 'hello').where(title: 'world').rewhere(title: 'alone')
+ expected = Post.where(title: 'alone')
- assert_equal 1, relation.where_values.size
- value = relation.where_values.first
- bind = relation.bind_values.first
- assert_bound_ast value, Post.arel_table[@name], Arel::Nodes::Equality
- assert_equal 'alone', bind.last
+ assert_equal expected.where_clause, relation.where_clause
end
def test_rewhere_with_multiple_overwriting_conditions
relation = Post.where(title: 'hello').where(body: 'world').rewhere(title: 'alone', body: 'again')
+ expected = Post.where(title: 'alone', body: 'again')
- assert_equal 2, relation.where_values.size
-
- value = relation.where_values.first
- bind = relation.bind_values.first
- assert_bound_ast value, Post.arel_table['title'], Arel::Nodes::Equality
- assert_equal 'alone', bind.last
-
- value = relation.where_values[1]
- bind = relation.bind_values[1]
- assert_bound_ast value, Post.arel_table['body'], Arel::Nodes::Equality
- assert_equal 'again', bind.last
- end
-
- def assert_bound_ast value, table, type
- assert_equal table, value.left
- assert_kind_of type, value
- assert_kind_of Arel::Nodes::BindParam, value.right
+ assert_equal expected.where_clause, relation.where_clause
end
def test_rewhere_with_one_overwriting_condition_and_one_unrelated
relation = Post.where(title: 'hello').where(body: 'world').rewhere(title: 'alone')
+ expected = Post.where(body: 'world', title: 'alone')
- assert_equal 2, relation.where_values.size
-
- value = relation.where_values.first
- bind = relation.bind_values.first
-
- assert_bound_ast value, Post.arel_table['body'], Arel::Nodes::Equality
- assert_equal 'world', bind.last
-
- value = relation.where_values.second
- bind = relation.bind_values.second
-
- assert_bound_ast value, Post.arel_table['title'], Arel::Nodes::Equality
- assert_equal 'alone', bind.last
+ assert_equal expected.where_clause, relation.where_clause
end
def test_rewhere_with_range
relation = Post.where(comments_count: 1..3).rewhere(comments_count: 3..5)
- assert_equal 1, relation.where_values.size
assert_equal Post.where(comments_count: 3..5), relation
end
def test_rewhere_with_infinite_upper_bound_range
relation = Post.where(comments_count: 1..Float::INFINITY).rewhere(comments_count: 3..5)
- assert_equal 1, relation.where_values.size
assert_equal Post.where(comments_count: 3..5), relation
end
def test_rewhere_with_infinite_lower_bound_range
relation = Post.where(comments_count: -Float::INFINITY..1).rewhere(comments_count: 3..5)
- assert_equal 1, relation.where_values.size
assert_equal Post.where(comments_count: 3..5), relation
end
def test_rewhere_with_infinite_range
relation = Post.where(comments_count: -Float::INFINITY..Float::INFINITY).rewhere(comments_count: 3..5)
- assert_equal 1, relation.where_values.size
assert_equal Post.where(comments_count: 3..5), relation
end
end
diff --git a/activerecord/test/cases/relation/where_clause_test.rb b/activerecord/test/cases/relation/where_clause_test.rb
new file mode 100644
index 0000000000..c20ed94d90
--- /dev/null
+++ b/activerecord/test/cases/relation/where_clause_test.rb
@@ -0,0 +1,182 @@
+require "cases/helper"
+
+class ActiveRecord::Relation
+ class WhereClauseTest < ActiveRecord::TestCase
+ test "+ combines two where clauses" do
+ first_clause = WhereClause.new([table["id"].eq(bind_param)], [["id", 1]])
+ second_clause = WhereClause.new([table["name"].eq(bind_param)], [["name", "Sean"]])
+ combined = WhereClause.new(
+ [table["id"].eq(bind_param), table["name"].eq(bind_param)],
+ [["id", 1], ["name", "Sean"]],
+ )
+
+ assert_equal combined, first_clause + second_clause
+ end
+
+ test "+ is associative, but not commutative" do
+ a = WhereClause.new(["a"], ["bind a"])
+ b = WhereClause.new(["b"], ["bind b"])
+ c = WhereClause.new(["c"], ["bind c"])
+
+ assert_equal a + (b + c), (a + b) + c
+ assert_not_equal a + b, b + a
+ end
+
+ test "an empty where clause is the identity value for +" do
+ clause = WhereClause.new([table["id"].eq(bind_param)], [["id", 1]])
+
+ assert_equal clause, clause + WhereClause.empty
+ end
+
+ test "merge combines two where clauses" do
+ a = WhereClause.new([table["id"].eq(1)], [])
+ b = WhereClause.new([table["name"].eq("Sean")], [])
+ expected = WhereClause.new([table["id"].eq(1), table["name"].eq("Sean")], [])
+
+ assert_equal expected, a.merge(b)
+ end
+
+ test "merge keeps the right side, when two equality clauses reference the same column" do
+ a = WhereClause.new([table["id"].eq(1), table["name"].eq("Sean")], [])
+ b = WhereClause.new([table["name"].eq("Jim")], [])
+ expected = WhereClause.new([table["id"].eq(1), table["name"].eq("Jim")], [])
+
+ assert_equal expected, a.merge(b)
+ end
+
+ test "merge removes bind parameters matching overlapping equality clauses" do
+ a = WhereClause.new(
+ [table["id"].eq(bind_param), table["name"].eq(bind_param)],
+ [attribute("id", 1), attribute("name", "Sean")],
+ )
+ b = WhereClause.new(
+ [table["name"].eq(bind_param)],
+ [attribute("name", "Jim")]
+ )
+ expected = WhereClause.new(
+ [table["id"].eq(bind_param), table["name"].eq(bind_param)],
+ [attribute("id", 1), attribute("name", "Jim")],
+ )
+
+ assert_equal expected, a.merge(b)
+ end
+
+ test "merge allows for columns with the same name from different tables" do
+ skip "This is not possible as of 4.2, and the binds do not yet contain sufficient information for this to happen"
+ # We might be able to change the implementation to remove conflicts by index, rather than column name
+ end
+
+ test "a clause knows if it is empty" do
+ assert WhereClause.empty.empty?
+ assert_not WhereClause.new(["anything"], []).empty?
+ end
+
+ test "invert cannot handle nil" do
+ where_clause = WhereClause.new([nil], [])
+
+ assert_raises ArgumentError do
+ where_clause.invert
+ end
+ end
+
+ test "invert replaces each part of the predicate with its inverse" do
+ random_object = Object.new
+ original = WhereClause.new([
+ table["id"].in([1, 2, 3]),
+ table["id"].eq(1),
+ "sql literal",
+ random_object
+ ], [])
+ expected = WhereClause.new([
+ table["id"].not_in([1, 2, 3]),
+ table["id"].not_eq(1),
+ Arel::Nodes::Not.new(Arel::Nodes::SqlLiteral.new("sql literal")),
+ Arel::Nodes::Not.new(random_object)
+ ], [])
+
+ assert_equal expected, original.invert
+ end
+
+ test "accept removes binary predicates referencing a given column" do
+ where_clause = WhereClause.new([
+ table["id"].in([1, 2, 3]),
+ table["name"].eq(bind_param),
+ table["age"].gteq(bind_param),
+ ], [
+ attribute("name", "Sean"),
+ attribute("age", 30),
+ ])
+ expected = WhereClause.new([table["age"].gteq(bind_param)], [attribute("age", 30)])
+
+ assert_equal expected, where_clause.except("id", "name")
+ end
+
+ test "ast groups its predicates with AND" do
+ predicates = [
+ table["id"].in([1, 2, 3]),
+ table["name"].eq(bind_param),
+ ]
+ where_clause = WhereClause.new(predicates, [])
+ expected = Arel::Nodes::And.new(predicates)
+
+ assert_equal expected, where_clause.ast
+ end
+
+ test "ast wraps any SQL literals in parenthesis" do
+ random_object = Object.new
+ where_clause = WhereClause.new([
+ table["id"].in([1, 2, 3]),
+ "foo = bar",
+ random_object,
+ ], [])
+ expected = Arel::Nodes::And.new([
+ table["id"].in([1, 2, 3]),
+ Arel::Nodes::Grouping.new(Arel.sql("foo = bar")),
+ Arel::Nodes::Grouping.new(random_object),
+ ])
+
+ assert_equal expected, where_clause.ast
+ end
+
+ test "ast removes any empty strings" do
+ where_clause = WhereClause.new([table["id"].in([1, 2, 3])], [])
+ where_clause_with_empty = WhereClause.new([table["id"].in([1, 2, 3]), ''], [])
+
+ assert_equal where_clause.ast, where_clause_with_empty.ast
+ end
+
+ test "or joins the two clauses using OR" do
+ where_clause = WhereClause.new([table["id"].eq(bind_param)], [attribute("id", 1)])
+ other_clause = WhereClause.new([table["name"].eq(bind_param)], [attribute("name", "Sean")])
+ expected_ast =
+ Arel::Nodes::Grouping.new(
+ Arel::Nodes::Or.new(table["id"].eq(bind_param), table["name"].eq(bind_param))
+ )
+ expected_binds = where_clause.binds + other_clause.binds
+
+ assert_equal expected_ast.to_sql, where_clause.or(other_clause).ast.to_sql
+ assert_equal expected_binds, where_clause.or(other_clause).binds
+ end
+
+ test "or returns an empty where clause when either side is empty" do
+ where_clause = WhereClause.new([table["id"].eq(bind_param)], [attribute("id", 1)])
+
+ assert_equal WhereClause.empty, where_clause.or(WhereClause.empty)
+ assert_equal WhereClause.empty, WhereClause.empty.or(where_clause)
+ end
+
+ private
+
+ def table
+ Arel::Table.new("table")
+ end
+
+ def bind_param
+ Arel::Nodes::BindParam.new
+ end
+
+ def attribute(name, value)
+ ActiveRecord::Attribute.with_cast_value(name, value, ActiveRecord::Type::Value.new)
+ end
+ end
+end
diff --git a/activerecord/test/cases/relation/where_test.rb b/activerecord/test/cases/relation/where_test.rb
index b0573579da..6af31017d6 100644
--- a/activerecord/test/cases/relation/where_test.rb
+++ b/activerecord/test/cases/relation/where_test.rb
@@ -5,6 +5,7 @@ require "models/cake_designer"
require "models/chef"
require "models/comment"
require "models/edge"
+require "models/essay"
require "models/post"
require "models/price_estimate"
require "models/topic"
@@ -13,7 +14,7 @@ require "models/vertex"
module ActiveRecord
class WhereTest < ActiveRecord::TestCase
- fixtures :posts, :edges, :authors, :binaries
+ fixtures :posts, :edges, :authors, :binaries, :essays
def test_where_copies_bind_params
author = authors(:david)
@@ -28,6 +29,14 @@ module ActiveRecord
}
end
+ def test_where_copies_bind_params_in_the_right_order
+ author = authors(:david)
+ posts = author.posts.where.not(id: 1)
+ joined = Post.where(id: posts, title: posts.first.title)
+
+ assert_equal joined, [posts.first]
+ end
+
def test_where_copies_arel_bind_params
chef = Chef.create!
CakeDesigner.create!(chef: chef)
@@ -232,5 +241,40 @@ module ActiveRecord
count = Binary.where(:data => 0).count
assert_equal 0, count
end
+
+ def test_where_on_association_with_custom_primary_key
+ author = authors(:david)
+ essay = Essay.where(writer: author).first
+
+ assert_equal essays(:david_modest_proposal), essay
+ end
+
+ def test_where_on_association_with_custom_primary_key_with_relation
+ author = authors(:david)
+ essay = Essay.where(writer: Author.where(id: author.id)).first
+
+ assert_equal essays(:david_modest_proposal), essay
+ end
+
+ def test_where_on_association_with_relation_performs_subselect_not_two_queries
+ author = authors(:david)
+
+ assert_queries(1) do
+ Essay.where(writer: Author.where(id: author.id)).to_a
+ end
+ end
+
+ def test_where_on_association_with_custom_primary_key_with_array_of_base
+ author = authors(:david)
+ essay = Essay.where(writer: [author]).first
+
+ assert_equal essays(:david_modest_proposal), essay
+ end
+
+ def test_where_on_association_with_custom_primary_key_with_array_of_ids
+ essay = Essay.where(writer: ["David"]).first
+
+ assert_equal essays(:david_modest_proposal), essay
+ end
end
end
diff --git a/activerecord/test/cases/relation_test.rb b/activerecord/test/cases/relation_test.rb
index f7cb471984..c9c05b75a6 100644
--- a/activerecord/test/cases/relation_test.rb
+++ b/activerecord/test/cases/relation_test.rb
@@ -156,35 +156,45 @@ module ActiveRecord
relation = Relation.new(FakeKlass, :b, nil)
relation = relation.merge where: :lol, readonly: true
- assert_equal [:lol], relation.where_values
+ assert_equal Relation::WhereClause.new([:lol], []), relation.where_clause
assert_equal true, relation.readonly_value
end
test 'merging an empty hash into a relation' do
- assert_equal [], Relation.new(FakeKlass, :b, nil).merge({}).where_values
+ assert_equal Relation::WhereClause.empty, Relation.new(FakeKlass, :b, nil).merge({}).where_clause
end
test 'merging a hash with unknown keys raises' do
assert_raises(ArgumentError) { Relation::HashMerger.new(nil, omg: 'lol') }
end
+ test 'merging nil or false raises' do
+ relation = Relation.new(FakeKlass, :b, nil)
+
+ e = assert_raises(ArgumentError) do
+ relation = relation.merge nil
+ end
+
+ assert_equal 'invalid argument: nil.', e.message
+
+ e = assert_raises(ArgumentError) do
+ relation = relation.merge false
+ end
+
+ assert_equal 'invalid argument: false.', e.message
+ end
+
test '#values returns a dup of the values' do
relation = Relation.new(FakeKlass, :b, nil).where! :foo
values = relation.values
values[:where] = nil
- assert_not_nil relation.where_values
+ assert_not_nil relation.where_clause
end
test 'relations can be created with a values hash' do
- relation = Relation.new(FakeKlass, :b, nil, where: [:foo])
- assert_equal [:foo], relation.where_values
- end
-
- test 'merging a single where value' do
- relation = Relation.new(FakeKlass, :b, nil)
- relation.merge!(where: :foo)
- assert_equal [:foo], relation.where_values
+ relation = Relation.new(FakeKlass, :b, nil, select: [:foo])
+ assert_equal [:foo], relation.select_values
end
test 'merging a hash interpolates conditions' do
@@ -197,7 +207,7 @@ module ActiveRecord
relation = Relation.new(klass, :b, nil)
relation.merge!(where: ['foo = ?', 'bar'])
- assert_equal ['foo = bar'], relation.where_values
+ assert_equal Relation::WhereClause.new(['foo = bar'], []), relation.where_clause
end
def test_merging_readonly_false
diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb
index 9631ea79be..dec3a37f42 100644
--- a/activerecord/test/cases/relations_test.rb
+++ b/activerecord/test/cases/relations_test.rb
@@ -16,6 +16,7 @@ require 'models/engine'
require 'models/tyre'
require 'models/minivan'
require 'models/aircraft'
+require "models/possession"
class RelationTest < ActiveRecord::TestCase
@@ -39,15 +40,6 @@ class RelationTest < ActiveRecord::TestCase
assert_equal van, Minivan.where(:minivan_id => [van]).to_a.first
end
- def test_bind_values
- relation = Post.all
- assert_equal [], relation.bind_values
-
- relation2 = relation.bind 'foo'
- assert_equal %w{ foo }, relation2.bind_values
- assert_equal [], relation.bind_values
- end
-
def test_two_scopes_with_includes_should_not_drop_any_include
# heat habtm cache
car = Car.incl_engines.incl_tyres.first
@@ -350,7 +342,9 @@ class RelationTest < ActiveRecord::TestCase
assert_equal 0, Developer.none.size
assert_equal 0, Developer.none.count
assert_equal true, Developer.none.empty?
+ assert_equal true, Developer.none.none?
assert_equal false, Developer.none.any?
+ assert_equal false, Developer.none.one?
assert_equal false, Developer.none.many?
end
end
@@ -857,6 +851,12 @@ class RelationTest < ActiveRecord::TestCase
assert ! fake.exists?(authors(:david).id)
end
+ def test_exists_uses_existing_scope
+ post = authors(:david).posts.first
+ authors = Author.includes(:posts).where(name: "David", posts: { id: post.id })
+ assert authors.exists?(authors(:david).id)
+ end
+
def test_last
authors = Author.all
assert_equal authors(:bob), authors.last
@@ -1104,6 +1104,38 @@ class RelationTest < ActiveRecord::TestCase
assert ! posts.limit(1).many?
end
+ def test_none?
+ posts = Post.all
+ assert_queries(1) do
+ assert ! posts.none? # Uses COUNT()
+ end
+
+ assert ! posts.loaded?
+
+ assert_queries(1) do
+ assert posts.none? {|p| p.id < 0 }
+ assert ! posts.none? {|p| p.id == 1 }
+ end
+
+ assert posts.loaded?
+ end
+
+ def test_one
+ posts = Post.all
+ assert_queries(1) do
+ assert ! posts.one? # Uses COUNT()
+ end
+
+ assert ! posts.loaded?
+
+ assert_queries(1) do
+ assert ! posts.one? {|p| p.id < 3 }
+ assert posts.one? {|p| p.id == 1 }
+ end
+
+ assert posts.loaded?
+ end
+
def test_build
posts = Post.all
@@ -1462,10 +1494,31 @@ class RelationTest < ActiveRecord::TestCase
def test_doesnt_add_having_values_if_options_are_blank
scope = Post.having('')
- assert_equal [], scope.having_values
+ assert scope.having_clause.empty?
scope = Post.having([])
- assert_equal [], scope.having_values
+ assert scope.having_clause.empty?
+ end
+
+ def test_having_with_binds_for_both_where_and_having
+ post = Post.first
+ having_then_where = Post.having(id: post.id).where(title: post.title).group(:id)
+ where_then_having = Post.where(title: post.title).having(id: post.id).group(:id)
+
+ assert_equal [post], having_then_where
+ assert_equal [post], where_then_having
+ end
+
+ def test_multiple_where_and_having_clauses
+ post = Post.first
+ having_then_where = Post.having(id: post.id).where(title: post.title)
+ .having(id: post.id).where(title: post.title).group(:id)
+
+ assert_equal [post], having_then_where
+ end
+
+ def test_grouping_by_column_with_reserved_name
+ assert_equal [], Possession.select(:where).group(:where).to_a
end
def test_references_triggers_eager_loading
@@ -1744,14 +1797,13 @@ class RelationTest < ActiveRecord::TestCase
end
def test_merging_keeps_lhs_bind_parameters
- column = Post.columns_hash['id']
- binds = [[column, 20]]
+ binds = [ActiveRecord::Relation::QueryAttribute.new("id", 20, Post.type_for_attribute("id"))]
right = Post.where(id: 20)
left = Post.where(id: 10)
merged = left.merge(right)
- assert_equal binds, merged.bind_values
+ assert_equal binds, merged.bound_attributes
end
def test_merging_reorders_bind_params
diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb
index b52c66356c..bafc9fa81b 100644
--- a/activerecord/test/cases/schema_dumper_test.rb
+++ b/activerecord/test/cases/schema_dumper_test.rb
@@ -73,10 +73,10 @@ class SchemaDumperTest < ActiveRecord::TestCase
next if column_set.empty?
lengths = column_set.map do |column|
- if match = column.match(/t\.(?:integer|decimal|float|datetime|timestamp|time|date|text|binary|string|boolean|uuid|point)\s+"/)
+ if match = column.match(/t\.(?:integer|decimal|float|datetime|timestamp|time|date|text|binary|string|boolean|xml|uuid|point)\s+"/)
match[0].length
end
- end
+ end.compact
assert_equal 1, lengths.uniq.length
end
@@ -204,25 +204,25 @@ class SchemaDumperTest < ActiveRecord::TestCase
if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
def test_schema_dump_should_add_default_value_for_mysql_text_field
output = standard_dump
- assert_match %r{t.text\s+"body",\s+limit: 65535,\s+null: false$}, output
+ assert_match %r{t\.text\s+"body",\s+limit: 65535,\s+null: false$}, output
end
def test_schema_dump_includes_length_for_mysql_binary_fields
output = standard_dump
- assert_match %r{t.binary\s+"var_binary",\s+limit: 255$}, output
- assert_match %r{t.binary\s+"var_binary_large",\s+limit: 4095$}, output
+ assert_match %r{t\.binary\s+"var_binary",\s+limit: 255$}, output
+ assert_match %r{t\.binary\s+"var_binary_large",\s+limit: 4095$}, output
end
def test_schema_dump_includes_length_for_mysql_blob_and_text_fields
output = standard_dump
- assert_match %r{t.binary\s+"tiny_blob",\s+limit: 255$}, output
- assert_match %r{t.binary\s+"normal_blob",\s+limit: 65535$}, output
- assert_match %r{t.binary\s+"medium_blob",\s+limit: 16777215$}, output
- assert_match %r{t.binary\s+"long_blob",\s+limit: 4294967295$}, output
- assert_match %r{t.text\s+"tiny_text",\s+limit: 255$}, output
- assert_match %r{t.text\s+"normal_text",\s+limit: 65535$}, output
- assert_match %r{t.text\s+"medium_text",\s+limit: 16777215$}, output
- assert_match %r{t.text\s+"long_text",\s+limit: 4294967295$}, output
+ assert_match %r{t\.binary\s+"tiny_blob",\s+limit: 255$}, output
+ assert_match %r{t\.binary\s+"normal_blob",\s+limit: 65535$}, output
+ assert_match %r{t\.binary\s+"medium_blob",\s+limit: 16777215$}, output
+ assert_match %r{t\.binary\s+"long_blob",\s+limit: 4294967295$}, output
+ assert_match %r{t\.text\s+"tiny_text",\s+limit: 255$}, output
+ assert_match %r{t\.text\s+"normal_text",\s+limit: 65535$}, output
+ assert_match %r{t\.text\s+"medium_text",\s+limit: 16777215$}, output
+ assert_match %r{t\.text\s+"long_text",\s+limit: 4294967295$}, output
end
def test_schema_dumps_index_type
@@ -235,19 +235,19 @@ class SchemaDumperTest < ActiveRecord::TestCase
if mysql_56?
def test_schema_dump_includes_datetime_precision
output = standard_dump
- assert_match %r{t.datetime\s+"written_on",\s+precision: 6$}, output
+ assert_match %r{t\.datetime\s+"written_on",\s+precision: 6$}, output
end
end
def test_schema_dump_includes_decimal_options
output = dump_all_table_schema([/^[^n]/])
- assert_match %r{precision: 3,[[:space:]]+scale: 2,[[:space:]]+default: 2.78}, output
+ assert_match %r{precision: 3,[[:space:]]+scale: 2,[[:space:]]+default: 2\.78}, output
end
if current_adapter?(:PostgreSQLAdapter)
def test_schema_dump_includes_bigint_default
output = standard_dump
- assert_match %r{t.integer\s+"bigint_default",\s+limit: 8,\s+default: 0}, output
+ assert_match %r{t\.integer\s+"bigint_default",\s+limit: 8,\s+default: 0}, output
end
if ActiveRecord::Base.connection.supports_extensions?
@@ -271,11 +271,11 @@ 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}, output
+ assert_match %r{t\.integer\s+"atoms_in_universe",\s+precision: 38}, output
elsif current_adapter?(:FbAdapter)
- assert_match %r{t.integer\s+"atoms_in_universe",\s+precision: 18}, output
+ assert_match %r{t\.integer\s+"atoms_in_universe",\s+precision: 18}, output
else
- assert_match %r{t.decimal\s+"atoms_in_universe",\s+precision: 55}, output
+ assert_match %r{t\.decimal\s+"atoms_in_universe",\s+precision: 55}, output
end
end
@@ -284,7 +284,7 @@ class SchemaDumperTest < ActiveRecord::TestCase
match = output.match(%r{create_table "goofy_string_id"(.*)do.*\n(.*)\n})
assert_not_nil(match, "goofy_string_id table not found")
assert_match %r(id: false), match[1], "no table id not preserved"
- assert_match %r{t.string\s+"id",.*?null: false$}, match[2], "non-primary key id column not preserved"
+ assert_match %r{t\.string\s+"id",.*?null: false$}, match[2], "non-primary key id column not preserved"
end
def test_schema_dump_keeps_id_false_when_id_is_false_and_unique_not_null_column_added
diff --git a/activerecord/test/cases/scoping/default_scoping_test.rb b/activerecord/test/cases/scoping/default_scoping_test.rb
index 0738df1b54..4137b20c4a 100644
--- a/activerecord/test/cases/scoping/default_scoping_test.rb
+++ b/activerecord/test/cases/scoping/default_scoping_test.rb
@@ -284,8 +284,8 @@ class DefaultScopingTest < ActiveRecord::TestCase
def test_unscope_merging
merged = Developer.where(name: "Jamis").merge(Developer.unscope(:where))
- assert merged.where_values.empty?
- assert !merged.where(name: "Jon").where_values.empty?
+ assert merged.where_clause.empty?
+ assert !merged.where(name: "Jon").where_clause.empty?
end
def test_order_in_default_scope_should_not_prevail
@@ -426,19 +426,19 @@ class DefaultScopingTest < ActiveRecord::TestCase
test "additional conditions are ANDed with the default scope" do
scope = DeveloperCalledJamis.where(name: "David")
- assert_equal 2, scope.where_values.length
+ assert_equal 2, scope.where_clause.ast.children.length
assert_equal [], scope.to_a
end
test "additional conditions in a scope are ANDed with the default scope" do
scope = DeveloperCalledJamis.david
- assert_equal 2, scope.where_values.length
+ assert_equal 2, scope.where_clause.ast.children.length
assert_equal [], scope.to_a
end
test "a scope can remove the condition from the default scope" do
scope = DeveloperCalledJamis.david2
- assert_equal 1, scope.where_values.length
- assert_equal Developer.where(name: "David").map(&:id), scope.map(&:id)
+ assert_equal 1, scope.where_clause.ast.children.length
+ assert_equal Developer.where(name: "David"), scope
end
end
diff --git a/activerecord/test/cases/scoping/named_scoping_test.rb b/activerecord/test/cases/scoping/named_scoping_test.rb
index 41f3449828..8cd94ebcc2 100644
--- a/activerecord/test/cases/scoping/named_scoping_test.rb
+++ b/activerecord/test/cases/scoping/named_scoping_test.rb
@@ -380,8 +380,8 @@ class NamedScopingTest < ActiveRecord::TestCase
end
def test_should_not_duplicates_where_values
- where_values = Topic.where("1=1").scope_with_lambda.where_values
- assert_equal ["1=1"], where_values
+ relation = Topic.where("1=1")
+ assert_equal relation.where_clause, relation.scope_with_lambda.where_clause
end
def test_chaining_with_duplicate_joins
diff --git a/activerecord/test/cases/scoping/relation_scoping_test.rb b/activerecord/test/cases/scoping/relation_scoping_test.rb
index d7bcbf6203..4bfffbe9c6 100644
--- a/activerecord/test/cases/scoping/relation_scoping_test.rb
+++ b/activerecord/test/cases/scoping/relation_scoping_test.rb
@@ -184,7 +184,7 @@ class RelationScopingTest < ActiveRecord::TestCase
rescue
end
- assert !Developer.all.where_values.include?("name = 'Jamis'")
+ assert_not Developer.all.to_sql.include?("name = 'Jamis'"), "scope was not restored"
end
def test_default_scope_filters_on_joins
@@ -208,6 +208,12 @@ class RelationScopingTest < ActiveRecord::TestCase
assert_equal [], DeveloperFilteredOnJoins.all
assert_not_equal [], Developer.all
end
+
+ def test_current_scope_does_not_pollute_other_subclasses
+ Post.none.scoping do
+ assert StiPost.all.any?
+ end
+ end
end
class NestedRelationScopingTest < ActiveRecord::TestCase
diff --git a/activerecord/test/cases/secure_token_test.rb b/activerecord/test/cases/secure_token_test.rb
index 3f7455d12d..e731443fc2 100644
--- a/activerecord/test/cases/secure_token_test.rb
+++ b/activerecord/test/cases/secure_token_test.rb
@@ -22,4 +22,11 @@ class SecureTokenTest < ActiveRecord::TestCase
assert_not_equal @user.token, old_token
assert_not_equal @user.auth_token, old_auth_token
end
+
+ def test_token_value_not_overwritten_when_present
+ @user.token = "custom-secure-token"
+ @user.save
+
+ assert_equal @user.token, "custom-secure-token"
+ end
end
diff --git a/activerecord/test/cases/test_case.rb b/activerecord/test/cases/test_case.rb
index 5ba17359f0..e0b01ae8e0 100644
--- a/activerecord/test/cases/test_case.rb
+++ b/activerecord/test/cases/test_case.rb
@@ -1,10 +1,13 @@
require 'active_support/test_case'
+require 'active_support/testing/stream'
module ActiveRecord
# = Active Record Test Case
#
# Defines some test assertions to test against SQL queries.
class TestCase < ActiveSupport::TestCase #:nodoc:
+ include ActiveSupport::Testing::Stream
+
def teardown
SQLCounter.clear_log
end
@@ -13,23 +16,6 @@ module ActiveRecord
assert_equal expected.to_s, actual.to_s, message
end
- def capture(stream)
- stream = stream.to_s
- captured_stream = Tempfile.new(stream)
- stream_io = eval("$#{stream}")
- origin_stream = stream_io.dup
- stream_io.reopen(captured_stream)
-
- yield
-
- stream_io.rewind
- return captured_stream.read
- ensure
- captured_stream.close
- captured_stream.unlink
- stream_io.reopen(origin_stream)
- end
-
def capture_sql
SQLCounter.clear_log
yield
diff --git a/activerecord/test/cases/time_precision_test.rb b/activerecord/test/cases/time_precision_test.rb
new file mode 100644
index 0000000000..23422fd50a
--- /dev/null
+++ b/activerecord/test/cases/time_precision_test.rb
@@ -0,0 +1,65 @@
+require 'cases/helper'
+
+if ActiveRecord::Base.connection.supports_datetime_with_precision?
+class TimePrecisionTest < ActiveRecord::TestCase
+ setup do
+ @connection = ActiveRecord::Base.connection
+ end
+
+ teardown do
+ @connection.drop_table :foos, if_exists: true
+ end
+
+ def test_time_data_type_with_precision
+ @connection.create_table(:foos, force: true)
+ @connection.add_column :foos, :start, :time, precision: 3
+ @connection.add_column :foos, :finish, :time, precision: 6
+ assert_equal 3, activerecord_column_option('foos', 'start', 'precision')
+ assert_equal 6, activerecord_column_option('foos', 'finish', 'precision')
+ end
+
+ def test_passing_precision_to_time_does_not_set_limit
+ @connection.create_table(:foos, force: true) do |t|
+ t.time :start, precision: 3
+ t.time :finish, precision: 6
+ end
+ assert_nil activerecord_column_option('foos', 'start', 'limit')
+ assert_nil activerecord_column_option('foos', 'finish', 'limit')
+ end
+
+ def test_invalid_time_precision_raises_error
+ assert_raises ActiveRecord::ActiveRecordError do
+ @connection.create_table(:foos, force: true) do |t|
+ t.time :start, precision: 7
+ t.time :finish, precision: 7
+ end
+ end
+ end
+
+ def test_database_agrees_with_activerecord_about_precision
+ @connection.create_table(:foos, force: true) do |t|
+ t.time :start, precision: 2
+ t.time :finish, precision: 4
+ end
+ assert_equal 2, database_datetime_precision('foos', 'start')
+ assert_equal 4, database_datetime_precision('foos', 'finish')
+ end
+
+ private
+
+ def database_datetime_precision(table_name, column_name)
+ results = @connection.exec_query("SELECT column_name, datetime_precision FROM information_schema.columns WHERE table_name = '#{table_name}'")
+ result = results.find do |result_hash|
+ result_hash["column_name"] == column_name
+ end
+ result && result["datetime_precision"].to_i
+ end
+
+ def activerecord_column_option(tablename, column_name, option)
+ result = @connection.columns(tablename).find do |column|
+ column.name == column_name
+ end
+ result && result.send(option)
+ end
+end
+end
diff --git a/activerecord/test/cases/transaction_callbacks_test.rb b/activerecord/test/cases/transaction_callbacks_test.rb
index 185fc22e98..f185cda263 100644
--- a/activerecord/test/cases/transaction_callbacks_test.rb
+++ b/activerecord/test/cases/transaction_callbacks_test.rb
@@ -4,7 +4,6 @@ require 'models/pet'
require 'models/topic'
class TransactionCallbacksTest < ActiveRecord::TestCase
- self.use_transactional_fixtures = false
fixtures :topics, :owners, :pets
class ReplyWithCallbacks < ActiveRecord::Base
@@ -200,21 +199,21 @@ class TransactionCallbacksTest < ActiveRecord::TestCase
end
def test_call_after_rollback_when_commit_fails
- @first.class.connection.singleton_class.send(:alias_method, :real_method_commit_db_transaction, :commit_db_transaction)
- begin
- @first.class.connection.singleton_class.class_eval do
- def commit_db_transaction; raise "boom!"; end
- end
+ @first.after_commit_block { |r| r.history << :after_commit }
+ @first.after_rollback_block { |r| r.history << :after_rollback }
- @first.after_commit_block{|r| r.history << :after_commit}
- @first.after_rollback_block{|r| r.history << :after_rollback}
+ assert_raises RuntimeError do
+ @first.transaction do
+ tx = @first.class.connection.transaction_manager.current_transaction
+ def tx.commit
+ raise
+ end
- assert !@first.save rescue nil
- assert_equal [:after_rollback], @first.history
- ensure
- @first.class.connection.singleton_class.send(:remove_method, :commit_db_transaction)
- @first.class.connection.singleton_class.send(:alias_method, :commit_db_transaction, :real_method_commit_db_transaction)
+ @first.save
+ end
end
+
+ assert_equal [:after_rollback], @first.history
end
def test_only_call_after_rollback_on_records_rolled_back_to_a_savepoint
diff --git a/activerecord/test/cases/type/integer_test.rb b/activerecord/test/cases/type/integer_test.rb
index ff956b7680..1e836f2142 100644
--- a/activerecord/test/cases/type/integer_test.rb
+++ b/activerecord/test/cases/type/integer_test.rb
@@ -60,55 +60,68 @@ module ActiveRecord
test "values below int min value are out of range" do
assert_raises(::RangeError) do
- Integer.new.type_cast_from_user("-2147483649")
+ Integer.new.type_cast_for_database(-2147483649)
end
end
test "values above int max value are out of range" do
assert_raises(::RangeError) do
- Integer.new.type_cast_from_user("2147483648")
+ Integer.new.type_cast_for_database(2147483648)
end
end
test "very small numbers are out of range" do
assert_raises(::RangeError) do
- Integer.new.type_cast_from_user("-9999999999999999999999999999999")
+ Integer.new.type_cast_for_database(-9999999999999999999999999999999)
end
end
test "very large numbers are out of range" do
assert_raises(::RangeError) do
- Integer.new.type_cast_from_user("9999999999999999999999999999999")
+ Integer.new.type_cast_for_database(9999999999999999999999999999999)
end
end
test "normal numbers are in range" do
type = Integer.new
- assert_equal(0, type.type_cast_from_user("0"))
- assert_equal(-1, type.type_cast_from_user("-1"))
- assert_equal(1, type.type_cast_from_user("1"))
+ assert_equal(0, type.type_cast_for_database(0))
+ assert_equal(-1, type.type_cast_for_database(-1))
+ assert_equal(1, type.type_cast_for_database(1))
end
test "int max value is in range" do
- assert_equal(2147483647, Integer.new.type_cast_from_user("2147483647"))
+ assert_equal(2147483647, Integer.new.type_cast_for_database(2147483647))
end
test "int min value is in range" do
- assert_equal(-2147483648, Integer.new.type_cast_from_user("-2147483648"))
+ assert_equal(-2147483648, Integer.new.type_cast_for_database(-2147483648))
end
test "columns with a larger limit have larger ranges" do
type = Integer.new(limit: 8)
- assert_equal(9223372036854775807, type.type_cast_from_user("9223372036854775807"))
- assert_equal(-9223372036854775808, type.type_cast_from_user("-9223372036854775808"))
+ assert_equal(9223372036854775807, type.type_cast_for_database(9223372036854775807))
+ assert_equal(-9223372036854775808, type.type_cast_for_database(-9223372036854775808))
assert_raises(::RangeError) do
- type.type_cast_from_user("-9999999999999999999999999999999")
+ type.type_cast_for_database(-9999999999999999999999999999999)
end
assert_raises(::RangeError) do
- type.type_cast_from_user("9999999999999999999999999999999")
+ type.type_cast_for_database(9999999999999999999999999999999)
end
end
+
+ test "values which are out of range can be re-assigned" do
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = 'posts'
+ attribute :foo, :integer
+ end
+ model = klass.new
+
+ model.foo = 2147483648
+ model.foo = 1
+
+ assert_equal 1, model.foo
+ end
end
end
end
diff --git a/activerecord/test/cases/type/unsigned_integer_test.rb b/activerecord/test/cases/type/unsigned_integer_test.rb
index b6f673109e..4b8e2fb518 100644
--- a/activerecord/test/cases/type/unsigned_integer_test.rb
+++ b/activerecord/test/cases/type/unsigned_integer_test.rb
@@ -4,12 +4,12 @@ module ActiveRecord
module Type
class UnsignedIntegerTest < ActiveRecord::TestCase
test "unsigned int max value is in range" do
- assert_equal(4294967295, UnsignedInteger.new.type_cast_from_user("4294967295"))
+ assert_equal(4294967295, UnsignedInteger.new.type_cast_for_database(4294967295))
end
test "minus value is out of range" do
assert_raises(::RangeError) do
- UnsignedInteger.new.type_cast_from_user("-1")
+ UnsignedInteger.new.type_cast_for_database(-1)
end
end
end
diff --git a/activerecord/test/cases/types_test.rb b/activerecord/test/cases/types_test.rb
index 73e92addfe..34b6f2e8a5 100644
--- a/activerecord/test/cases/types_test.rb
+++ b/activerecord/test/cases/types_test.rb
@@ -108,14 +108,21 @@ module ActiveRecord
assert_not_equal Type::Value.new(precision: 1), Type::Value.new(precision: 2)
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
+ def test_attributes_which_are_invalid_for_database_can_still_be_reassigned
+ type_which_cannot_go_to_the_database = Type::Value.new
+ def type_which_cannot_go_to_the_database.type_cast_for_database(*)
+ raise
+ end
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = 'posts'
+ attribute :foo, type_which_cannot_go_to_the_database
end
+ model = klass.new
+
+ model.foo = "foo"
+ model.foo = "bar"
+
+ assert_equal "bar", model.foo
end
end
end
diff --git a/activerecord/test/cases/validations/association_validation_test.rb b/activerecord/test/cases/validations/association_validation_test.rb
index e4edc437e6..bff5ffa65e 100644
--- a/activerecord/test/cases/validations/association_validation_test.rb
+++ b/activerecord/test/cases/validations/association_validation_test.rb
@@ -50,7 +50,7 @@ class AssociationValidationTest < ActiveRecord::TestCase
Topic.validates_presence_of :content
r = Reply.create("title" => "A reply", "content" => "with content!")
r.topic = Topic.create("title" => "uhohuhoh")
- assert !r.valid?
+ assert_not_operator r, :valid?
assert_equal ["This string contains 'single' and \"double\" quotes"], r.errors[:topic]
end
@@ -82,5 +82,4 @@ class AssociationValidationTest < ActiveRecord::TestCase
assert interest.valid?, "Expected interest to be valid, but was not. Interest should have a man object associated"
end
end
-
end
diff --git a/activerecord/test/cases/validations/length_validation_test.rb b/activerecord/test/cases/validations/length_validation_test.rb
index 2c0e282761..952e1681a7 100644
--- a/activerecord/test/cases/validations/length_validation_test.rb
+++ b/activerecord/test/cases/validations/length_validation_test.rb
@@ -1,4 +1,3 @@
-# -*- coding: utf-8 -*-
require "cases/helper"
require 'models/owner'
require 'models/pet'
@@ -37,7 +36,7 @@ class LengthValidationTest < ActiveRecord::TestCase
def test_validates_size_of_association_utf8
repair_validations Owner do
- assert_nothing_raised { Owner.validates_size_of :pets, :minimum => 1 }
+ Owner.validates_size_of :pets, :minimum => 1
o = Owner.new('name' => 'あいうえおかきくけこ')
assert !o.save
assert o.errors[:pets].any?
@@ -46,8 +45,8 @@ class LengthValidationTest < ActiveRecord::TestCase
end
end
- def test_validates_size_of_reprects_records_marked_for_destruction
- assert_nothing_raised { Owner.validates_size_of :pets, minimum: 1 }
+ def test_validates_size_of_respects_records_marked_for_destruction
+ Owner.validates_size_of :pets, minimum: 1
owner = Owner.new
assert_not owner.save
assert owner.errors[:pets].any?
@@ -62,4 +61,17 @@ class LengthValidationTest < ActiveRecord::TestCase
assert_equal pet_count, Pet.count
end
+ def test_does_not_validate_length_of_if_parent_record_is_validate_false
+ Owner.validates_length_of :name, minimum: 1
+ owner = Owner.new
+ owner.save!(validate: false)
+ assert owner.persisted?
+
+ pet = Pet.new(owner_id: owner.id)
+ pet.save!
+
+ assert_equal owner.pets.size, 1
+ assert owner.valid?
+ assert pet.valid?
+ end
end
diff --git a/activerecord/test/cases/validations/presence_validation_test.rb b/activerecord/test/cases/validations/presence_validation_test.rb
index 4f38849131..6f8ad06ab6 100644
--- a/activerecord/test/cases/validations/presence_validation_test.rb
+++ b/activerecord/test/cases/validations/presence_validation_test.rb
@@ -1,4 +1,3 @@
-# encoding: utf-8
require "cases/helper"
require 'models/man'
require 'models/face'
@@ -65,4 +64,20 @@ class PresenceValidationTest < ActiveRecord::TestCase
assert_nothing_raised { s.valid? }
end
+
+ def test_does_not_validate_presence_of_if_parent_record_is_validate_false
+ repair_validations(Interest) do
+ Interest.validates_presence_of(:topic)
+ interest = Interest.new
+ interest.save!(validate: false)
+ assert interest.persisted?
+
+ man = Man.new(interest_ids: [interest.id])
+ man.save!
+
+ assert_equal man.interests.size, 1
+ assert interest.valid?
+ assert man.valid?
+ end
+ end
end
diff --git a/activerecord/test/cases/validations/uniqueness_validation_test.rb b/activerecord/test/cases/validations/uniqueness_validation_test.rb
index 524f59876e..062bc733f9 100644
--- a/activerecord/test/cases/validations/uniqueness_validation_test.rb
+++ b/activerecord/test/cases/validations/uniqueness_validation_test.rb
@@ -1,4 +1,3 @@
-# encoding: utf-8
require "cases/helper"
require 'models/topic'
require 'models/reply'
@@ -386,4 +385,21 @@ class UniquenessValidationTest < ActiveRecord::TestCase
topic = TopicWithUniqEvent.new
assert topic.valid?
end
+
+ def test_does_not_validate_uniqueness_of_if_parent_record_is_validate_false
+ Reply.validates_uniqueness_of(:content)
+
+ Reply.create!(content: "Topic Title")
+
+ reply = Reply.new(content: "Topic Title")
+ reply.save!(validate: false)
+ assert reply.persisted?
+
+ topic = Topic.new(reply_ids: [reply.id])
+ topic.save!
+
+ assert_equal topic.replies.size, 1
+ assert reply.valid?
+ assert topic.valid?
+ end
end
diff --git a/activerecord/test/cases/validations_test.rb b/activerecord/test/cases/validations_test.rb
index 959c58aa85..f4f316f393 100644
--- a/activerecord/test/cases/validations_test.rb
+++ b/activerecord/test/cases/validations_test.rb
@@ -1,4 +1,3 @@
-# encoding: utf-8
require "cases/helper"
require 'models/topic'
require 'models/reply'
@@ -151,7 +150,7 @@ class ValidationsTest < ActiveRecord::TestCase
def test_numericality_validation_with_mutation
Topic.class_eval do
- attribute :wibble, ActiveRecord::Type::String.new
+ attribute :wibble, :string
validates_numericality_of :wibble, only_integer: true
end
diff --git a/activerecord/test/cases/xml_serialization_test.rb b/activerecord/test/cases/xml_serialization_test.rb
index c34e7d5a30..b30b50f597 100644
--- a/activerecord/test/cases/xml_serialization_test.rb
+++ b/activerecord/test/cases/xml_serialization_test.rb
@@ -19,7 +19,7 @@ class XmlSerializationTest < ActiveRecord::TestCase
def test_should_serialize_default_root_with_namespace
@xml = Contact.new.to_xml :namespace=>"http://xml.rubyonrails.org/contact"
- assert_match %r{^<contact xmlns="http://xml.rubyonrails.org/contact">}, @xml
+ assert_match %r{^<contact xmlns="http://xml\.rubyonrails\.org/contact">}, @xml
assert_match %r{</contact>$}, @xml
end
diff --git a/activerecord/test/fixtures/dead_parrots.yml b/activerecord/test/fixtures/dead_parrots.yml
new file mode 100644
index 0000000000..da5529dd27
--- /dev/null
+++ b/activerecord/test/fixtures/dead_parrots.yml
@@ -0,0 +1,5 @@
+deadbird:
+ name: "Dusty DeadBird"
+ treasures: [ruby, sapphire]
+ parrot_sti_class: DeadParrot
+ killer: blackbeard
diff --git a/activerecord/test/fixtures/live_parrots.yml b/activerecord/test/fixtures/live_parrots.yml
new file mode 100644
index 0000000000..95b2078da7
--- /dev/null
+++ b/activerecord/test/fixtures/live_parrots.yml
@@ -0,0 +1,4 @@
+dusty:
+ name: "Dusty Bluebird"
+ treasures: [ruby, sapphire]
+ parrot_sti_class: LiveParrot
diff --git a/activerecord/test/models/admin/randomly_named_c1.rb b/activerecord/test/models/admin/randomly_named_c1.rb
index 2f81d5b831..b64ae7fc41 100644
--- a/activerecord/test/models/admin/randomly_named_c1.rb
+++ b/activerecord/test/models/admin/randomly_named_c1.rb
@@ -1,3 +1,7 @@
-class Admin::ClassNameThatDoesNotFollowCONVENTIONS < ActiveRecord::Base
- self.table_name = :randomly_named_table
+class Admin::ClassNameThatDoesNotFollowCONVENTIONS1 < ActiveRecord::Base
+ self.table_name = :randomly_named_table2
+end
+
+class Admin::ClassNameThatDoesNotFollowCONVENTIONS2 < ActiveRecord::Base
+ self.table_name = :randomly_named_table3
end
diff --git a/activerecord/test/models/car.rb b/activerecord/test/models/car.rb
index db0f93f63b..81263b79d1 100644
--- a/activerecord/test/models/car.rb
+++ b/activerecord/test/models/car.rb
@@ -8,7 +8,7 @@ class Car < ActiveRecord::Base
has_one :bulb
has_many :tyres
- has_many :engines, :dependent => :destroy
+ has_many :engines, :dependent => :destroy, inverse_of: :my_car
has_many :wheels, :as => :wheelable, :dependent => :destroy
scope :incl_tyres, -> { includes(:tyres) }
diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb
index 7b637c9e3f..052b1c9690 100644
--- a/activerecord/test/models/post.rb
+++ b/activerecord/test/models/post.rb
@@ -18,6 +18,7 @@ class Post < ActiveRecord::Base
end
scope :containing_the_letter_a, -> { where("body LIKE '%a%'") }
+ scope :titled_with_an_apostrophe, -> { where("title LIKE '%''%'") }
scope :ranked_by_comments, -> { order("comments_count DESC") }
scope :limit_by, lambda {|l| limit(l) }
@@ -43,6 +44,8 @@ class Post < ActiveRecord::Base
scope :tagged_with, ->(id) { joins(:taggings).where(taggings: { tag_id: id }) }
scope :tagged_with_comment, ->(comment) { joins(:taggings).where(taggings: { comment: comment }) }
+ scope :typographically_interesting, -> { containing_the_letter_a.or(titled_with_an_apostrophe) }
+
has_many :comments do
def find_most_recent
order("id DESC").first
diff --git a/activerecord/test/models/randomly_named_c1.rb b/activerecord/test/models/randomly_named_c1.rb
index 18a86c4989..d4be1e13b4 100644
--- a/activerecord/test/models/randomly_named_c1.rb
+++ b/activerecord/test/models/randomly_named_c1.rb
@@ -1,3 +1,3 @@
class ClassNameThatDoesNotFollowCONVENTIONS < ActiveRecord::Base
- self.table_name = :randomly_named_table
+ self.table_name = :randomly_named_table1
end
diff --git a/activerecord/test/models/ship.rb b/activerecord/test/models/ship.rb
index c2f6d492d8..312caef604 100644
--- a/activerecord/test/models/ship.rb
+++ b/activerecord/test/models/ship.rb
@@ -4,6 +4,7 @@ class Ship < ActiveRecord::Base
belongs_to :pirate
belongs_to :update_only_pirate, :class_name => 'Pirate'
has_many :parts, :class_name => 'ShipPart'
+ has_many :treasures
accepts_nested_attributes_for :parts, :allow_destroy => true
accepts_nested_attributes_for :pirate, :allow_destroy => true, :reject_if => proc(&:empty?)
diff --git a/activerecord/test/models/ship_part.rb b/activerecord/test/models/ship_part.rb
index b6a8a506b4..05c65f8a4a 100644
--- a/activerecord/test/models/ship_part.rb
+++ b/activerecord/test/models/ship_part.rb
@@ -2,6 +2,7 @@ class ShipPart < ActiveRecord::Base
belongs_to :ship
has_many :trinkets, :class_name => "Treasure", :as => :looter
accepts_nested_attributes_for :trinkets, :allow_destroy => true
+ accepts_nested_attributes_for :ship
validates_presence_of :name
-end \ No newline at end of file
+end
diff --git a/activerecord/test/models/treasure.rb b/activerecord/test/models/treasure.rb
index a69d3fd3df..ffc65466d5 100644
--- a/activerecord/test/models/treasure.rb
+++ b/activerecord/test/models/treasure.rb
@@ -1,6 +1,7 @@
class Treasure < ActiveRecord::Base
has_and_belongs_to_many :parrots
belongs_to :looter, :polymorphic => true
+ belongs_to :ship
has_many :price_estimates, :as => :estimate_of
has_and_belongs_to_many :rich_people, join_table: 'peoples_treasures', validate: false
diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb
index 21b23d8e0c..a7d90e3f89 100644
--- a/activerecord/test/schema/schema.rb
+++ b/activerecord/test/schema/schema.rb
@@ -1,4 +1,3 @@
-# encoding: utf-8
ActiveRecord::Schema.define do
def except(adapter_names_to_exclude)
@@ -623,7 +622,17 @@ ActiveRecord::Schema.define do
t.string :type
end
- create_table :randomly_named_table, force: true do |t|
+ create_table :randomly_named_table1, force: true do |t|
+ t.string :some_attribute
+ t.integer :another_attribute
+ end
+
+ create_table :randomly_named_table2, force: true do |t|
+ t.string :some_attribute
+ t.integer :another_attribute
+ end
+
+ create_table :randomly_named_table3, force: true do |t|
t.string :some_attribute
t.integer :another_attribute
end
@@ -772,6 +781,7 @@ ActiveRecord::Schema.define do
t.column :type, :string
t.column :looter_id, :integer
t.column :looter_type, :string
+ t.belongs_to :ship
end
create_table :tyres, force: true do |t|