aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord
diff options
context:
space:
mode:
Diffstat (limited to 'activerecord')
-rw-r--r--activerecord/CHANGELOG.md205
-rw-r--r--activerecord/lib/active_record.rb2
-rw-r--r--activerecord/lib/active_record/associations.rb16
-rw-r--r--activerecord/lib/active_record/associations/association_scope.rb15
-rw-r--r--activerecord/lib/active_record/associations/builder/association.rb19
-rw-r--r--activerecord/lib/active_record/associations/builder/belongs_to.rb5
-rw-r--r--activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb13
-rw-r--r--activerecord/lib/active_record/associations/builder/has_one.rb2
-rw-r--r--activerecord/lib/active_record/associations/builder/singular_association.rb9
-rw-r--r--activerecord/lib/active_record/associations/collection_association.rb4
-rw-r--r--activerecord/lib/active_record/associations/has_many_association.rb34
-rw-r--r--activerecord/lib/active_record/associations/has_many_through_association.rb31
-rw-r--r--activerecord/lib/active_record/associations/join_dependency.rb4
-rw-r--r--activerecord/lib/active_record/associations/preloader/association.rb8
-rw-r--r--activerecord/lib/active_record/associations/through_association.rb16
-rw-r--r--activerecord/lib/active_record/attribute.rb120
-rw-r--r--activerecord/lib/active_record/attribute_assignment.rb14
-rw-r--r--activerecord/lib/active_record/attribute_decorators.rb52
-rw-r--r--activerecord/lib/active_record/attribute_methods.rb77
-rw-r--r--activerecord/lib/active_record/attribute_methods/before_type_cast.rb4
-rw-r--r--activerecord/lib/active_record/attribute_methods/dirty.rb89
-rw-r--r--activerecord/lib/active_record/attribute_methods/primary_key.rb13
-rw-r--r--activerecord/lib/active_record/attribute_methods/read.rb69
-rw-r--r--activerecord/lib/active_record/attribute_methods/serialization.rb42
-rw-r--r--activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb21
-rw-r--r--activerecord/lib/active_record/attribute_methods/write.rb18
-rw-r--r--activerecord/lib/active_record/attribute_set.rb77
-rw-r--r--activerecord/lib/active_record/attribute_set/builder.rb32
-rw-r--r--activerecord/lib/active_record/attributes.rb12
-rw-r--r--activerecord/lib/active_record/autosave_association.rb5
-rw-r--r--activerecord/lib/active_record/base.rb27
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb6
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb6
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/quoting.rb20
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb35
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb45
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb14
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb117
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_adapter.rb9
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb68
-rw-r--r--activerecord/lib/active_record/connection_adapters/column.rb17
-rw-r--r--activerecord/lib/active_record/connection_adapters/connection_specification.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql_adapter.rb6
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/cast.rb117
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/column.rb20
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid.rb1
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb86
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb28
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/bit_varying.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb7
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb2
-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/decimal.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/float.rb3
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb44
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/inet.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/infinity.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/integer.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb20
-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.rb25
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb54
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb6
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/time.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/vector.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/xml.rb32
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb103
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb50
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb22
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb42
-rw-r--r--activerecord/lib/active_record/core.rb45
-rw-r--r--activerecord/lib/active_record/counter_cache.rb2
-rw-r--r--activerecord/lib/active_record/errors.rb36
-rw-r--r--activerecord/lib/active_record/fixtures.rb30
-rw-r--r--activerecord/lib/active_record/inheritance.rb31
-rw-r--r--activerecord/lib/active_record/locking/optimistic.rb48
-rw-r--r--activerecord/lib/active_record/migration.rb20
-rw-r--r--activerecord/lib/active_record/migration/command_recorder.rb19
-rw-r--r--activerecord/lib/active_record/model_schema.rb38
-rw-r--r--activerecord/lib/active_record/persistence.rb16
-rw-r--r--activerecord/lib/active_record/query_cache.rb5
-rw-r--r--activerecord/lib/active_record/querying.rb9
-rw-r--r--activerecord/lib/active_record/railties/databases.rake69
-rw-r--r--activerecord/lib/active_record/reflection.rb179
-rw-r--r--activerecord/lib/active_record/relation/calculations.rb45
-rw-r--r--activerecord/lib/active_record/result.rb25
-rw-r--r--activerecord/lib/active_record/sanitization.rb2
-rw-r--r--activerecord/lib/active_record/schema_dumper.rb43
-rw-r--r--activerecord/lib/active_record/schema_migration.rb4
-rw-r--r--activerecord/lib/active_record/serializers/xml_serializer.rb6
-rw-r--r--activerecord/lib/active_record/store.rb10
-rw-r--r--activerecord/lib/active_record/tasks/database_tasks.rb21
-rw-r--r--activerecord/lib/active_record/tasks/mysql_database_tasks.rb2
-rw-r--r--activerecord/lib/active_record/timestamp.rb8
-rw-r--r--activerecord/lib/active_record/type.rb1
-rw-r--r--activerecord/lib/active_record/type/binary.rb4
-rw-r--r--activerecord/lib/active_record/type/date_time.rb10
-rw-r--r--activerecord/lib/active_record/type/mutable.rb16
-rw-r--r--activerecord/lib/active_record/type/numeric.rb18
-rw-r--r--activerecord/lib/active_record/type/serialized.rb21
-rw-r--r--activerecord/lib/active_record/type/string.rb19
-rw-r--r--activerecord/lib/active_record/type/value.rb72
-rw-r--r--activerecord/lib/active_record/validations.rb18
-rw-r--r--activerecord/lib/active_record/validations/uniqueness.rb8
-rw-r--r--activerecord/test/cases/adapters/mysql2/boolean_test.rb4
-rw-r--r--activerecord/test/cases/adapters/postgresql/array_test.rb80
-rw-r--r--activerecord/test/cases/adapters/postgresql/bit_string_test.rb8
-rw-r--r--activerecord/test/cases/adapters/postgresql/citext_test.rb1
-rw-r--r--activerecord/test/cases/adapters/postgresql/composite_test.rb4
-rw-r--r--activerecord/test/cases/adapters/postgresql/domain_test.rb1
-rw-r--r--activerecord/test/cases/adapters/postgresql/enum_test.rb4
-rw-r--r--activerecord/test/cases/adapters/postgresql/full_text_test.rb1
-rw-r--r--activerecord/test/cases/adapters/postgresql/geometric_test.rb18
-rw-r--r--activerecord/test/cases/adapters/postgresql/hstore_test.rb40
-rw-r--r--activerecord/test/cases/adapters/postgresql/infinity_test.rb44
-rw-r--r--activerecord/test/cases/adapters/postgresql/json_test.rb29
-rw-r--r--activerecord/test/cases/adapters/postgresql/ltree_test.rb1
-rw-r--r--activerecord/test/cases/adapters/postgresql/money_test.rb28
-rw-r--r--activerecord/test/cases/adapters/postgresql/network_test.rb3
-rw-r--r--activerecord/test/cases/adapters/postgresql/quoting_test.rb11
-rw-r--r--activerecord/test/cases/adapters/postgresql/range_test.rb19
-rw-r--r--activerecord/test/cases/adapters/postgresql/type_lookup_test.rb15
-rw-r--r--activerecord/test/cases/adapters/postgresql/uuid_test.rb39
-rw-r--r--activerecord/test/cases/adapters/postgresql/xml_test.rb15
-rw-r--r--activerecord/test/cases/adapters/sqlite3/quoting_test.rb11
-rw-r--r--activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb11
-rw-r--r--activerecord/test/cases/associations/belongs_to_associations_test.rb15
-rw-r--r--activerecord/test/cases/associations/cascaded_eager_loading_test.rb4
-rw-r--r--activerecord/test/cases/associations/deprecated_counter_cache_on_has_many_through_test.rb26
-rw-r--r--activerecord/test/cases/associations/eager_test.rb29
-rw-r--r--activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb11
-rw-r--r--activerecord/test/cases/associations/has_many_associations_test.rb38
-rw-r--r--activerecord/test/cases/associations/has_many_through_associations_test.rb50
-rw-r--r--activerecord/test/cases/associations/has_one_through_associations_test.rb14
-rw-r--r--activerecord/test/cases/associations/inverse_associations_test.rb11
-rw-r--r--activerecord/test/cases/associations/join_model_test.rb25
-rw-r--r--activerecord/test/cases/associations/nested_through_associations_test.rb1
-rw-r--r--activerecord/test/cases/associations/required_test.rb82
-rw-r--r--activerecord/test/cases/attribute_decorators_test.rb25
-rw-r--r--activerecord/test/cases/attribute_methods/read_test.rb1
-rw-r--r--activerecord/test/cases/attribute_methods_test.rb88
-rw-r--r--activerecord/test/cases/attribute_set_test.rb165
-rw-r--r--activerecord/test/cases/attribute_test.rb142
-rw-r--r--activerecord/test/cases/base_test.rb14
-rw-r--r--activerecord/test/cases/binary_test.rb2
-rw-r--r--activerecord/test/cases/calculations_test.rb12
-rw-r--r--activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb12
-rw-r--r--activerecord/test/cases/connection_adapters/schema_cache_test.rb4
-rw-r--r--activerecord/test/cases/counter_cache_test.rb10
-rw-r--r--activerecord/test/cases/defaults_test.rb2
-rw-r--r--activerecord/test/cases/dirty_test.rb19
-rw-r--r--activerecord/test/cases/dup_test.rb12
-rw-r--r--activerecord/test/cases/finder_test.rb8
-rw-r--r--activerecord/test/cases/fixtures_test.rb6
-rw-r--r--activerecord/test/cases/locking_test.rb7
-rw-r--r--activerecord/test/cases/migration/change_schema_test.rb14
-rw-r--r--activerecord/test/cases/migration/column_attributes_test.rb2
-rw-r--r--activerecord/test/cases/migration/columns_test.rb17
-rw-r--r--activerecord/test/cases/migration/command_recorder_test.rb42
-rw-r--r--activerecord/test/cases/migration/foreign_key_test.rb242
-rw-r--r--activerecord/test/cases/migration/logger_test.rb2
-rw-r--r--activerecord/test/cases/migration/pending_migrations_test.rb53
-rw-r--r--activerecord/test/cases/migration/rename_table_test.rb10
-rw-r--r--activerecord/test/cases/migration_test.rb2
-rw-r--r--activerecord/test/cases/persistence_test.rb46
-rw-r--r--activerecord/test/cases/primary_keys_test.rb12
-rw-r--r--activerecord/test/cases/quoting_test.rb36
-rw-r--r--activerecord/test/cases/reflection_test.rb118
-rw-r--r--activerecord/test/cases/relation/where_test.rb33
-rw-r--r--activerecord/test/cases/relation_test.rb28
-rw-r--r--activerecord/test/cases/result_test.rb44
-rw-r--r--activerecord/test/cases/schema_dumper_test.rb21
-rw-r--r--activerecord/test/cases/serialization_test.rb8
-rw-r--r--activerecord/test/cases/serialized_attribute_test.rb12
-rw-r--r--activerecord/test/cases/store_test.rb1
-rw-r--r--activerecord/test/cases/tasks/database_tasks_test.rb36
-rw-r--r--activerecord/test/cases/type/string_test.rb36
-rw-r--r--activerecord/test/cases/types_test.rb25
-rw-r--r--activerecord/test/cases/validations_test.rb14
-rw-r--r--activerecord/test/cases/yaml_serialization_test.rb15
-rw-r--r--activerecord/test/fixtures/fk_test_has_pk.yml2
-rw-r--r--activerecord/test/fixtures/posts.yml2
-rw-r--r--activerecord/test/fixtures/topics.yml10
-rw-r--r--activerecord/test/models/club.rb7
-rw-r--r--activerecord/test/models/contact.rb1
-rw-r--r--activerecord/test/models/face.rb2
-rw-r--r--activerecord/test/models/man.rb1
-rw-r--r--activerecord/test/models/post.rb2
-rw-r--r--activerecord/test/models/publisher/article.rb1
-rw-r--r--activerecord/test/models/tagging.rb2
-rw-r--r--activerecord/test/schema/schema.rb16
-rw-r--r--activerecord/test/schema/sqlite_specific_schema.rb6
-rw-r--r--activerecord/test/support/ddl_helper.rb4
197 files changed, 3773 insertions, 1362 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md
index f6f7de7fbc..1ba01d3618 100644
--- a/activerecord/CHANGELOG.md
+++ b/activerecord/CHANGELOG.md
@@ -1,3 +1,199 @@
+* Fix the schema dump generated for tables without constraints and with
+ primary key with default value of custom PostgreSQL function result.
+
+ Fixes #16111
+
+ *Andrey Novikov*
+
+* Fix the SQL generated when a `delete_all` is run on an association to not
+ produce an `IN` statements.
+
+ Before:
+
+ UPDATE "categorizations" SET "category_id" = NULL WHERE
+ "categorizations"."category_id" = 1 AND "categorizations"."id" IN (1, 2)
+
+ After:
+
+ UPDATE "categorizations" SET "category_id" = NULL WHERE
+ "categorizations"."category_id" = 1
+
+ *Eileen M. Uchitelle, Aaron Patterson*
+
+* Avoid type casting boolean and ActiveSupport::Duration values to numeric
+ values for string columns. Otherwise, in some database, the string column
+ values will be coerced to a numeric allowing false or 0.seconds match any
+ string starting with a non-digit.
+
+ Example:
+
+ App.where(apikey: false) # => SELECT * FROM users WHERE apikey = '0'
+
+ *Dylan Thacker-Smith*
+
+* Add a `:required` option to singular associations, providing a nicer
+ API for presence validations on associations.
+
+ *Sean Griffin*
+
+* Fixed error in `reset_counters` when associations have `select` scope.
+ (Call to `count` generates invalid SQL.)
+
+ *Cade Truitt*
+
+* After a successful `reload`, `new_record?` is always false.
+
+ Fixes #12101.
+
+ *Matthew Draper*
+
+* PostgreSQL renaming table doesn't attempt to rename non existent sequences.
+
+ *Abdelkader Boudih*
+
+* Move 'dependent: :destroy' handling for 'belongs_to'
+ from 'before_destroy' to 'after_destroy' callback chain
+
+ Fix #12380.
+
+ *Ivan Antropov*
+
+* Detect in-place modifications on String attributes.
+
+ Before this change user have to mark the attribute as changed to it be persisted
+ in the database. Now it is not required anymore.
+
+ Before:
+
+ user = User.first
+ user.name << ' Griffin'
+ user.name_will_change!
+ user.save
+ user.reload.name # => "Sean Griffin"
+
+ After:
+
+ user = User.first
+ user.name << ' Griffin'
+ user.save
+ user.reload.name # => "Sean Griffin"
+
+ *Sean Griffin*
+
+* Add `ActiveRecord::Base#validate!` that raises `RecordInvalid` if the record
+ is invalid.
+
+ *Bogdan Gusiev*, *Marc Schütz*
+
+* Support for adding and removing foreign keys. Foreign keys are now
+ a part of `schema.rb`. This is supported by Mysql2Adapter, MysqlAdapter
+ and PostgreSQLAdapter.
+
+ Many thanks to *Matthew Higgins* for laying the foundation with his work on
+ [foreigner](https://github.com/matthuhiggins/foreigner).
+
+ Example:
+
+ # within your migrations:
+ add_foreign_key :articles, :authors
+ remove_foreign_key :articles, :authors
+
+ *Yves Senn*
+
+* Fix subtle bugs regarding attribute assignment on models with no primary
+ key. `'id'` will no longer be part of the attributes hash.
+
+ *Sean Griffin*
+
+* Deprecate automatic counter caches on `has_many :through`. The behavior was
+ broken and inconsistent.
+
+ *Sean Griffin*
+
+* `preload` preserves readonly flag for associations.
+
+ See #15853.
+
+ *Yves Senn*
+
+* Assume numeric types have changed if they were assigned to a value that
+ would fail numericality validation, regardless of the old value. Previously
+ this would only occur if the old value was 0.
+
+ Example:
+
+ model = Model.create!(number: 5)
+ model.number = '5wibble'
+ model.number_changed? # => true
+
+ Fixes #14731.
+
+ *Sean Griffin*
+
+* `reload` no longer merges with the existing attributes.
+ The attribute hash is fully replaced. The record is put into the same state
+ as it would be with `Model.find(model.id)`.
+
+ *Sean Griffin*
+
+* The object returned from `select_all` must respond to `column_types`.
+ If this is not the case a `NoMethodError` is raised.
+
+ *Sean Griffin*
+
+* `has_many :through` associations will no longer save the through record
+ twice when added in an `after_create` callback defined before the
+ associations.
+
+ Fixes #3798.
+
+ *Sean Griffin*
+
+* Detect in-place modifications of PG array types
+
+ *Sean Griffin*
+
+* Add `bin/rake db:purge` task to empty the current database.
+
+ *Yves Senn*
+
+* Deprecate `serialized_attributes` without replacement.
+
+ *Sean Griffin*
+
+* Correctly extract IPv6 addresses from `DATABASE_URI`: the square brackets
+ are part of the URI structure, not the actual host.
+
+ Fixes #15705.
+
+ *Andy Bakun*, *Aaron Stone*
+
+* Ensure both parent IDs are set on join records when both sides of a
+ through association are new.
+
+ *Sean Griffin*
+
+* `ActiveRecord::Dirty` now detects in-place changes to mutable values.
+ Serialized attributes on ActiveRecord models will no longer save when
+ unchanged. Fixes #8328.
+
+ *Sean Griffin*
+
+* Pluck now works when selecting columns from different tables with the same
+ name.
+
+ Fixes #15649
+
+ *Sean Griffin*
+
+* Remove `cache_attributes` and friends. All attributes are cached.
+
+ *Sean Griffin*
+
+* Remove deprecated method `ActiveRecord::Base.quoted_locking_column`.
+
+ *Akshay Vishnoi*
+
* `ActiveRecord::FinderMethods.find` with block can handle proc parameter as
`Enumerable#find` does.
@@ -45,7 +241,8 @@
*Lauro Caetano*, *Carlos Antonio da Silva*
-* Return a null column from `column_for_attribute` when no column exists.
+* Deprecate returning `nil` from `column_for_attribute` when no column exists.
+ It will return a null object in Rails 5.0
*Sean Griffin*
@@ -378,7 +575,7 @@
*Eric Chahin*
-* `sanitize_sql_like` helper method to escape a string for safe use in a SQL
+* `sanitize_sql_like` helper method to escape a string for safe use in an SQL
LIKE statement.
Example:
@@ -414,7 +611,7 @@
*Lauro Caetano*
* Calling `delete_all` on an unloaded `CollectionProxy` no longer
- generates a SQL statement containing each id of the collection:
+ generates an SQL statement containing each id of the collection:
Before:
@@ -681,7 +878,7 @@
*Vilius Luneckas* *Ahmed AbouElhamayed*
* `before_add` callbacks are fired before the record is saved on
- `has_and_belongs_to_many` assocations *and* on `has_many :through`
+ `has_and_belongs_to_many` associations *and* on `has_many :through`
associations. Before this change, `before_add` callbacks would be fired
before the record was saved on `has_and_belongs_to_many` associations, but
*not* on `has_many :through` associations.
diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb
index f856c482d6..17b00bbaea 100644
--- a/activerecord/lib/active_record.rb
+++ b/activerecord/lib/active_record.rb
@@ -27,10 +27,12 @@ require 'active_model'
require 'arel'
require 'active_record/version'
+require 'active_record/attribute_set'
module ActiveRecord
extend ActiveSupport::Autoload
+ autoload :Attribute
autoload :Base
autoload :Callbacks
autoload :Core
diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb
index 6222bfe903..7ec7eb1b24 100644
--- a/activerecord/lib/active_record/associations.rb
+++ b/activerecord/lib/active_record/associations.rb
@@ -717,9 +717,9 @@ module ActiveRecord
# == Eager loading of associations
#
# Eager loading is a way to find objects of a certain class and a number of named associations.
- # This is one of the easiest ways of to prevent the dreaded 1+N problem in which fetching 100
+ # This is one of the easiest ways of to prevent the dreaded N+1 problem in which fetching 100
# posts that each need to display their author triggers 101 database queries. Through the
- # use of eager loading, the 101 queries can be reduced to 2.
+ # use of eager loading, the number of queries will be reduced from 101 to 2.
#
# class Post < ActiveRecord::Base
# belongs_to :author
@@ -1309,6 +1309,10 @@ module ActiveRecord
# that is the inverse of this <tt>has_one</tt> association. Does not work in combination
# with <tt>:through</tt> or <tt>:as</tt> options.
# See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail.
+ # [:required]
+ # When set to +true+, the association will also have its presence validated.
+ # This will validate the association itself, not the id. You can use
+ # +:inverse_of+ to avoid an extra query during validation.
#
# Option examples:
# has_one :credit_card, dependent: :destroy # destroys the associated credit card
@@ -1320,6 +1324,7 @@ module ActiveRecord
# has_one :boss, readonly: :true
# has_one :club, through: :membership
# has_one :primary_address, -> { where primary: true }, through: :addressables, source: :addressable
+ # has_one :credit_card, required: true
def has_one(name, scope = nil, options = {})
reflection = Builder::HasOne.build(self, name, scope, options)
Reflection.add_reflection self, name, reflection
@@ -1421,6 +1426,10 @@ module ActiveRecord
# object that is the inverse of this <tt>belongs_to</tt> association. Does not work in
# combination with the <tt>:polymorphic</tt> options.
# See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail.
+ # [:required]
+ # When set to +true+, the association will also have its presence validated.
+ # This will validate the association itself, not the id. You can use
+ # +:inverse_of+ to avoid an extra query during validation.
#
# Option examples:
# belongs_to :firm, foreign_key: "client_of"
@@ -1433,6 +1442,7 @@ module ActiveRecord
# belongs_to :post, counter_cache: true
# belongs_to :company, touch: true
# belongs_to :company, touch: :employees_last_updated_at
+ # belongs_to :company, required: true
def belongs_to(name, scope = nil, options = {})
reflection = Builder::BelongsTo.build(self, name, scope, options)
Reflection.add_reflection self, name, reflection
@@ -1577,7 +1587,7 @@ module ActiveRecord
scope = nil
end
- habtm_reflection = ActiveRecord::Reflection::HasAndBelongsToManyReflection.new(:has_and_belongs_to_many, name, scope, options, self)
+ habtm_reflection = ActiveRecord::Reflection::HasAndBelongsToManyReflection.new(name, scope, options, self)
builder = Builder::HasAndBelongsToMany.new name, self, options
diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb
index 31108cc1aa..519d4d8651 100644
--- a/activerecord/lib/active_record/associations/association_scope.rb
+++ b/activerecord/lib/active_record/associations/association_scope.rb
@@ -105,18 +105,9 @@ module ActiveRecord
chain.each_with_index do |reflection, i|
table, foreign_table = tables.shift, tables.first
- if reflection.source_macro == :belongs_to
- if reflection.polymorphic?
- key = reflection.association_primary_key(assoc_klass)
- else
- key = reflection.association_primary_key
- end
-
- foreign_key = reflection.foreign_key
- else
- key = reflection.foreign_key
- foreign_key = reflection.active_record_primary_key
- end
+ join_keys = reflection.join_keys(assoc_klass)
+ key = join_keys.key
+ foreign_key = join_keys.foreign_key
if reflection == chain.last
bind_val = bind scope, table.table_name, key.to_s, owner[foreign_key], tracker
diff --git a/activerecord/lib/active_record/associations/builder/association.rb b/activerecord/lib/active_record/associations/builder/association.rb
index f085fd1cfd..947d61ee7b 100644
--- a/activerecord/lib/active_record/associations/builder/association.rb
+++ b/activerecord/lib/active_record/associations/builder/association.rb
@@ -36,6 +36,7 @@ module ActiveRecord::Associations::Builder
reflection = builder.build(model)
define_accessors model, reflection
define_callbacks model, reflection
+ define_validations model, reflection
builder.define_extensions model
reflection
end
@@ -85,7 +86,11 @@ module ActiveRecord::Associations::Builder
end
def self.define_callbacks(model, reflection)
- add_before_destroy_callbacks(model, reflection) if reflection.options[:dependent]
+ if dependent = reflection.options[:dependent]
+ check_dependent_options(dependent)
+ add_destroy_callbacks(model, reflection)
+ end
+
Association.extensions.each do |extension|
extension.build model, reflection
end
@@ -120,17 +125,23 @@ module ActiveRecord::Associations::Builder
CODE
end
+ def self.define_validations(model, reflection)
+ # noop
+ end
+
def self.valid_dependent_options
raise NotImplementedError
end
private
- def self.add_before_destroy_callbacks(model, reflection)
- unless valid_dependent_options.include? reflection.options[:dependent]
- raise ArgumentError, "The :dependent option must be one of #{valid_dependent_options}, but is :#{reflection.options[:dependent]}"
+ def self.check_dependent_options(dependent)
+ unless valid_dependent_options.include? dependent
+ raise ArgumentError, "The :dependent option must be one of #{valid_dependent_options}, but is :#{dependent}"
end
+ end
+ def self.add_destroy_callbacks(model, reflection)
name = reflection.name
model.before_destroy lambda { |o| o.association(name).handle_dependency }
end
diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb
index 3998aca23e..954ea3878a 100644
--- a/activerecord/lib/active_record/associations/builder/belongs_to.rb
+++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb
@@ -107,5 +107,10 @@ module ActiveRecord::Associations::Builder
model.after_touch callback
model.after_destroy callback
end
+
+ def self.add_destroy_callbacks(model, reflection)
+ name = reflection.name
+ model.after_destroy lambda { |o| o.association(name).handle_dependency }
+ end
end
end
diff --git a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb
index 0ad5206980..34a555dfd4 100644
--- a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb
+++ b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb
@@ -15,7 +15,10 @@ module ActiveRecord::Associations::Builder
end
private
- def klass; @rhs_class_name.constantize; end
+
+ def klass
+ @lhs_class.send(:compute_type, @rhs_class_name)
+ end
end
def self.build(lhs_class, name, options)
@@ -23,13 +26,7 @@ module ActiveRecord::Associations::Builder
KnownTable.new options[:join_table].to_s
else
class_name = options.fetch(:class_name) {
- model_name = name.to_s.camelize.singularize
-
- if lhs_class.parent_name
- model_name.prepend("#{lhs_class.parent_name}::")
- end
-
- model_name
+ name.to_s.camelize.singularize
}
KnownClass.new lhs_class, class_name
end
diff --git a/activerecord/lib/active_record/associations/builder/has_one.rb b/activerecord/lib/active_record/associations/builder/has_one.rb
index a1f4f51664..c194c8ae9a 100644
--- a/activerecord/lib/active_record/associations/builder/has_one.rb
+++ b/activerecord/lib/active_record/associations/builder/has_one.rb
@@ -16,7 +16,7 @@ module ActiveRecord::Associations::Builder
private
- def self.add_before_destroy_callbacks(model, reflection)
+ def self.add_destroy_callbacks(model, reflection)
super unless reflection.options[:through]
end
end
diff --git a/activerecord/lib/active_record/associations/builder/singular_association.rb b/activerecord/lib/active_record/associations/builder/singular_association.rb
index e655c389a6..17f36acf40 100644
--- a/activerecord/lib/active_record/associations/builder/singular_association.rb
+++ b/activerecord/lib/active_record/associations/builder/singular_association.rb
@@ -3,7 +3,7 @@
module ActiveRecord::Associations::Builder
class SingularAssociation < Association #:nodoc:
def valid_options
- super + [:remote, :dependent, :primary_key, :inverse_of]
+ super + [:remote, :dependent, :primary_key, :inverse_of, :required]
end
def self.define_accessors(model, reflection)
@@ -27,5 +27,12 @@ module ActiveRecord::Associations::Builder
end
CODE
end
+
+ def self.define_validations(model, reflection)
+ super
+ if reflection.options[:required]
+ model.validates_presence_of reflection.name
+ end
+ end
end
end
diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb
index 306588ac66..065a2cff01 100644
--- a/activerecord/lib/active_record/associations/collection_association.rb
+++ b/activerecord/lib/active_record/associations/collection_association.rb
@@ -55,9 +55,9 @@ module ActiveRecord
# Implements the ids writer method, e.g. foo.item_ids= for Foo.has_many :items
def ids_writer(ids)
- pk_column = reflection.primary_key_column
+ pk_type = reflection.primary_key_type
ids = Array(ids).reject { |id| id.blank? }
- ids.map! { |i| pk_column.type_cast_from_user(i) }
+ ids.map! { |i| pk_type.type_cast_from_user(i) }
replace(klass.find(ids).index_by { |r| r.id }.values_at(*ids))
end
diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb
index 3ffc015975..2a97d0ed31 100644
--- a/activerecord/lib/active_record/associations/has_many_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_association.rb
@@ -88,9 +88,20 @@ module ActiveRecord
end
def update_counter(difference, reflection = reflection())
+ update_counter_in_database(difference, reflection)
+ update_counter_in_memory(difference, reflection)
+ end
+
+ def update_counter_in_database(difference, reflection = reflection())
if has_cached_counter?(reflection)
counter = cached_counter_attribute_name(reflection)
owner.class.update_counters(owner.id, counter => difference)
+ end
+ end
+
+ def update_counter_in_memory(difference, reflection = reflection())
+ if has_cached_counter?(reflection)
+ counter = cached_counter_attribute_name(reflection)
owner[counter] += difference
owner.changed_attributes.delete(counter) # eww
end
@@ -108,6 +119,10 @@ module ActiveRecord
# Hence this method.
def inverse_updates_counter_cache?(reflection = reflection())
counter_name = cached_counter_attribute_name(reflection)
+ inverse_updates_counter_named?(counter_name, reflection)
+ end
+
+ def inverse_updates_counter_named?(counter_name, reflection = reflection())
reflection.klass._reflections.values.any? { |inverse_reflection|
:belongs_to == inverse_reflection.macro &&
inverse_reflection.counter_cache_column == counter_name
@@ -145,6 +160,25 @@ module ActiveRecord
false
end
end
+
+ def concat_records(records, *)
+ update_counter_if_success(super, records.length)
+ end
+
+ def _create_record(attributes, *)
+ if attributes.is_a?(Array)
+ super
+ else
+ update_counter_if_success(super, 1)
+ end
+ end
+
+ def update_counter_if_success(saved_successfully, difference)
+ if saved_successfully
+ update_counter_in_memory(difference)
+ end
+ saved_successfully
+ end
end
end
end
diff --git a/activerecord/lib/active_record/associations/has_many_through_association.rb b/activerecord/lib/active_record/associations/has_many_through_association.rb
index da9b125fd6..007e3bc555 100644
--- a/activerecord/lib/active_record/associations/has_many_through_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_through_association.rb
@@ -63,7 +63,15 @@ module ActiveRecord
end
save_through_record(record)
- update_counter(1)
+ if has_cached_counter? && !through_reflection_updates_counter_cache?
+ ActiveSupport::Deprecation.warn(<<-MESSAGE.strip_heredoc)
+ Automatic updating of counter caches on through associations has been
+ deprecated, and will be removed in Rails 5.0. Instead, please set the
+ appropriate counter_cache options on the has_many and belongs_to for
+ your associations to #{through_reflection.name}.
+ MESSAGE
+ update_counter_in_database(1)
+ end
record
end
@@ -82,14 +90,20 @@ module ActiveRecord
@through_records[record.object_id] ||= begin
ensure_mutable
- through_record = through_association.build through_scope_attributes
+ through_record = through_association.build(*options_for_through_record)
through_record.send("#{source_reflection.name}=", record)
through_record
end
end
+ def options_for_through_record
+ [through_scope_attributes]
+ end
+
def through_scope_attributes
- scope.where_values_hash(through_association.reflection.name.to_s).except!(through_association.reflection.foreign_key)
+ scope.where_values_hash(through_association.reflection.name.to_s).
+ except!(through_association.reflection.foreign_key,
+ through_association.reflection.klass.inheritance_column)
end
def save_through_record(record)
@@ -180,7 +194,11 @@ module ActiveRecord
def through_records_for(record)
attributes = construct_join_attributes(record)
candidates = Array.wrap(through_association.target)
- candidates.find_all { |c| c.attributes.slice(*attributes.keys) == attributes }
+ candidates.find_all do |c|
+ attributes.all? do |key, value|
+ c.public_send(key) == value
+ end
+ end
end
def delete_through_records(records)
@@ -208,6 +226,11 @@ module ActiveRecord
def invertible_for?(record)
false
end
+
+ def through_reflection_updates_counter_cache?
+ counter_name = cached_counter_attribute_name
+ inverse_updates_counter_named?(counter_name, through_reflection)
+ end
end
end
end
diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb
index fbb4551b22..ec5c189cd3 100644
--- a/activerecord/lib/active_record/associations/join_dependency.rb
+++ b/activerecord/lib/active_record/associations/join_dependency.rb
@@ -131,7 +131,6 @@ module ActiveRecord
def instantiate(result_set, aliases)
primary_key = aliases.column_alias(join_root, join_root.primary_key)
- type_caster = result_set.column_type primary_key
seen = Hash.new { |h,parent_klass|
h[parent_klass] = Hash.new { |i,parent_id|
@@ -144,8 +143,7 @@ module ActiveRecord
column_aliases = aliases.column_aliases join_root
result_set.each { |row_hash|
- primary_id = type_caster.type_cast_from_database row_hash[primary_key]
- parent = parents[primary_id] ||= join_root.instantiate(row_hash, column_aliases)
+ parent = parents[row_hash[primary_key]] ||= join_root.instantiate(row_hash, column_aliases)
construct(parent, join_root, row_hash, result_set, seen, model_cache, aliases)
}
diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb
index 33c8619359..c0639742be 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)
@@ -151,6 +151,10 @@ module ActiveRecord
end
end
+ if preload_values[:readonly] || values[:readonly]
+ scope.readonly!
+ end
+
if options[:as]
scope.where!(klass.table_name => { reflection.type => model.base_class.sti_name })
end
diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb
index fcf3b219d4..f00fef8b9e 100644
--- a/activerecord/lib/active_record/associations/through_association.rb
+++ b/activerecord/lib/active_record/associations/through_association.rb
@@ -41,12 +41,16 @@ module ActiveRecord
def construct_join_attributes(*records)
ensure_mutable
- join_attributes = {
- source_reflection.foreign_key =>
- records.map { |record|
- record.send(source_reflection.association_primary_key(reflection.klass))
- }
- }
+ if source_reflection.association_primary_key(reflection.klass) == reflection.klass.primary_key
+ join_attributes = { source_reflection.name => records }
+ else
+ join_attributes = {
+ source_reflection.foreign_key =>
+ records.map { |record|
+ record.send(source_reflection.association_primary_key(reflection.klass))
+ }
+ }
+ end
if options[:source_type]
join_attributes[source_reflection.foreign_type] =
diff --git a/activerecord/lib/active_record/attribute.rb b/activerecord/lib/active_record/attribute.rb
new file mode 100644
index 0000000000..6d38224830
--- /dev/null
+++ b/activerecord/lib/active_record/attribute.rb
@@ -0,0 +1,120 @@
+module ActiveRecord
+ class Attribute # :nodoc:
+ class << self
+ def from_database(name, value, type)
+ FromDatabase.new(name, value, type)
+ end
+
+ def from_user(name, value, type)
+ FromUser.new(name, value, type)
+ end
+
+ def null(name)
+ Null.new(name)
+ end
+
+ def uninitialized(name, type)
+ Uninitialized.new(name, type)
+ end
+ end
+
+ attr_reader :name, :value_before_type_cast, :type
+
+ # This method should not be called directly.
+ # Use #from_database or #from_user
+ def initialize(name, value_before_type_cast, type)
+ @name = name
+ @value_before_type_cast = value_before_type_cast
+ @type = type
+ end
+
+ def value
+ # `defined?` is cheaper than `||=` when we get back falsy values
+ @value = type_cast(value_before_type_cast) unless defined?(@value)
+ @value
+ end
+
+ def value_for_database
+ type.type_cast_for_database(value)
+ end
+
+ def changed_from?(old_value)
+ type.changed?(old_value, value, value_before_type_cast)
+ end
+
+ def changed_in_place_from?(old_value)
+ type.changed_in_place?(old_value, value)
+ end
+
+ def with_value_from_user(value)
+ self.class.from_user(name, value, type)
+ end
+
+ def with_value_from_database(value)
+ self.class.from_database(name, value, type)
+ end
+
+ def type_cast
+ raise NotImplementedError
+ end
+
+ def initialized?
+ true
+ end
+
+ protected
+
+ def initialize_dup(other)
+ if defined?(@value) && @value.duplicable?
+ @value = @value.dup
+ end
+ end
+
+ class FromDatabase < Attribute # :nodoc:
+ def type_cast(value)
+ type.type_cast_from_database(value)
+ end
+ end
+
+ class FromUser < Attribute # :nodoc:
+ def type_cast(value)
+ type.type_cast_from_user(value)
+ end
+ end
+
+ class Null < Attribute # :nodoc:
+ def initialize(name)
+ super(name, nil, Type::Value.new)
+ end
+
+ def value
+ nil
+ end
+
+ def with_value_from_database(value)
+ raise ActiveModel::MissingAttributeError, "can't write unknown attribute `#{name}`"
+ end
+ alias_method :with_value_from_user, :with_value_from_database
+ end
+
+ class Uninitialized < Attribute # :nodoc:
+ def initialize(name, type)
+ super(name, nil, type)
+ end
+
+ def value
+ if block_given?
+ yield name
+ end
+ end
+
+ def value_for_database
+ end
+
+ def initialized?
+ false
+ end
+ end
+ private_constant :FromDatabase, :FromUser, :Null, :Uninitialized
+ end
+end
diff --git a/activerecord/lib/active_record/attribute_assignment.rb b/activerecord/lib/active_record/attribute_assignment.rb
index 40e2918777..2887db3bf7 100644
--- a/activerecord/lib/active_record/attribute_assignment.rb
+++ b/activerecord/lib/active_record/attribute_assignment.rb
@@ -115,7 +115,7 @@ module ActiveRecord
end
class MultiparameterAttribute #:nodoc:
- attr_reader :object, :name, :values, :column
+ attr_reader :object, :name, :values, :cast_type
def initialize(object, name, values)
@object = object
@@ -126,22 +126,22 @@ module ActiveRecord
def read_value
return if values.values.compact.empty?
- @column = object.column_for_attribute(name)
- klass = column ? column.klass : nil
+ @cast_type = object.type_for_attribute(name)
+ klass = cast_type.klass
if klass == Time
read_time
elsif klass == Date
read_date
else
- read_other(klass)
+ read_other
end
end
private
def instantiate_time_object(set_values)
- if object.class.send(:create_time_zone_conversion_attribute?, name, column)
+ if object.class.send(:create_time_zone_conversion_attribute?, name, cast_type)
Time.zone.local(*set_values)
else
Time.send(object.class.default_timezone, *set_values)
@@ -151,7 +151,7 @@ module ActiveRecord
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 column.type == :time
+ if cast_type.type == :time
# if the column is a time set the values to their defaults as January 1, 1970, but only if they're nil
{ 1 => 1970, 2 => 1, 3 => 1 }.each do |key,value|
values[key] ||= value
@@ -181,7 +181,7 @@ module ActiveRecord
end
end
- def read_other(klass)
+ def read_other
max_position = extract_max_param
positions = (1..max_position)
validate_required_parameters!(positions)
diff --git a/activerecord/lib/active_record/attribute_decorators.rb b/activerecord/lib/active_record/attribute_decorators.rb
index 596161f81d..5b96623b6e 100644
--- a/activerecord/lib/active_record/attribute_decorators.rb
+++ b/activerecord/lib/active_record/attribute_decorators.rb
@@ -4,31 +4,63 @@ module ActiveRecord
included do
class_attribute :attribute_type_decorations, instance_accessor: false # :internal:
- self.attribute_type_decorations = Hash.new({})
+ self.attribute_type_decorations = TypeDecorator.new
end
- module ClassMethods
+ module ClassMethods # :nodoc:
def decorate_attribute_type(column_name, decorator_name, &block)
+ matcher = ->(name, _) { name == column_name.to_s }
+ key = "_#{column_name}_#{decorator_name}"
+ decorate_matching_attribute_types(matcher, key, &block)
+ end
+
+ def decorate_matching_attribute_types(matcher, decorator_name, &block)
clear_caches_calculated_from_columns
- column_name = column_name.to_s
+ decorator_name = decorator_name.to_s
# Create new hashes so we don't modify parent classes
- decorations_for_column = attribute_type_decorations[column_name]
- new_decorations = decorations_for_column.merge(decorator_name.to_s => block)
- self.attribute_type_decorations = attribute_type_decorations.merge(column_name => new_decorations)
+ self.attribute_type_decorations = attribute_type_decorations.merge(decorator_name => [matcher, block])
end
private
def add_user_provided_columns(*)
super.map do |column|
- decorations = attribute_type_decorations[column.name].values
- decorated_type = decorations.inject(column.cast_type) do |type, block|
- block.call(type)
- end
+ decorated_type = attribute_type_decorations.apply(column.name, column.cast_type)
column.with_type(decorated_type)
end
end
end
+
+ class TypeDecorator # :nodoc:
+ delegate :clear, to: :@decorations
+
+ def initialize(decorations = {})
+ @decorations = decorations
+ end
+
+ def merge(*args)
+ TypeDecorator.new(@decorations.merge(*args))
+ end
+
+ def apply(name, type)
+ decorations = decorators_for(name, type)
+ decorations.inject(type) do |new_type, block|
+ block.call(new_type)
+ end
+ end
+
+ private
+
+ def decorators_for(name, type)
+ matching(name, type).map(&:last)
+ end
+
+ def matching(name, type)
+ @decorations.values.select do |(matcher, _)|
+ matcher.call(name, type)
+ end
+ end
+ end
end
end
diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb
index e626227e7e..cbe5cf202a 100644
--- a/activerecord/lib/active_record/attribute_methods.rb
+++ b/activerecord/lib/active_record/attribute_methods.rb
@@ -73,7 +73,7 @@ module ActiveRecord
# accessors, mutators and query methods.
def define_attribute_methods # :nodoc:
return false if @attribute_methods_generated
- # Use a mutex; we don't want two thread simultaneously trying to define
+ # Use a mutex; we don't want two threads simultaneously trying to define
# attribute methods.
generated_attribute_methods.synchronize do
return false if @attribute_methods_generated
@@ -156,16 +156,6 @@ module ActiveRecord
end
end
- def find_generated_attribute_method(method_name) # :nodoc:
- klass = self
- until klass == Base
- gen_methods = klass.generated_attribute_methods
- return gen_methods.instance_method(method_name) if method_defined_within?(method_name, gen_methods, Object)
- klass = klass.superclass
- end
- nil
- end
-
# Returns +true+ if +attribute+ is an attribute method and table exists,
# +false+ otherwise.
#
@@ -196,8 +186,7 @@ module ActiveRecord
end
# Returns the column object for the named attribute.
- # Returns a +ActiveRecord::ConnectionAdapters::NullColumn+ if the
- # named attribute does not exist.
+ # Returns nil if the named attribute does not exist.
#
# class Person < ActiveRecord::Base
# end
@@ -207,31 +196,17 @@ module ActiveRecord
# # => #<ActiveRecord::ConnectionAdapters::SQLite3Column:0x007ff4ab083980 @name="name", @sql_type="varchar(255)", @null=true, ...>
#
# person.column_for_attribute(:nothing)
- # # => #<ActiveRecord::ConnectionAdapters::NullColumn:0xXXX @name=nil, @sql_type=nil, @cast_type=#<Type::Value>, ...>
+ # # => nil
def column_for_attribute(name)
- name = name.to_s
- columns_hash.fetch(name) do
- ConnectionAdapters::NullColumn.new(name)
+ column = columns_hash[name.to_s]
+ if column.nil?
+ ActiveSupport::Deprecation.warn(<<-MESSAGE.strip_heredoc)
+ `column_for_attribute` will return a null object for non-existent columns
+ in Rails 5.0. Use `has_attribute?` if you need to check for an
+ attribute's existence.
+ MESSAGE
end
- end
- end
-
- # If we haven't generated any methods yet, generate them, then
- # see if we've created the method we're looking for.
- def method_missing(method, *args, &block) # :nodoc:
- self.class.define_attribute_methods
- if respond_to_without_attributes?(method)
- # make sure to invoke the correct attribute method, as we might have gotten here via a `super`
- # call in a overwritten attribute method
- if attribute_method = self.class.find_generated_attribute_method(method)
- # this is probably horribly slow, but should only happen at most once for a given AR class
- attribute_method.bind(self).call(*args, &block)
- else
- return super unless respond_to_missing?(method, true)
- send(method, *args, &block)
- end
- else
- super
+ column
end
end
@@ -252,18 +227,14 @@ module ActiveRecord
# person.respond_to('age?') # => true
# person.respond_to(:nothing) # => false
def respond_to?(name, include_private = false)
+ return false unless super
name = name.to_s
- self.class.define_attribute_methods
- result = super
-
- # If the result is false the answer is false.
- return false unless result
# If the result is true then check for the select case.
# For queries selecting a subset of columns, return false for unselected columns.
- # We check defined?(@raw_attributes) not to issue warnings if called on objects that
+ # We check defined?(@attributes) not to issue warnings if called on objects that
# have been allocated but not yet initialized.
- if defined?(@raw_attributes) && @raw_attributes.any? && self.class.column_names.include?(name)
+ if defined?(@attributes) && self.class.column_names.include?(name)
return has_attribute?(name)
end
@@ -280,7 +251,7 @@ module ActiveRecord
# person.has_attribute?('age') # => true
# person.has_attribute?(:nothing) # => false
def has_attribute?(attr_name)
- @raw_attributes.has_key?(attr_name.to_s)
+ @attributes.include?(attr_name.to_s)
end
# Returns an array of names for the attributes available on this object.
@@ -292,7 +263,7 @@ module ActiveRecord
# person.attribute_names
# # => ["id", "created_at", "updated_at", "name", "age"]
def attribute_names
- @raw_attributes.keys
+ @attributes.keys
end
# Returns a hash of all the attributes with their names as keys and the values of the attributes as values.
@@ -304,9 +275,7 @@ module ActiveRecord
# person.attributes
# # => {"id"=>3, "created_at"=>Sun, 21 Oct 2012 04:53:04, "updated_at"=>Sun, 21 Oct 2012 04:53:04, "name"=>"Francesco", "age"=>22}
def attributes
- attribute_names.each_with_object({}) { |name, attrs|
- attrs[name] = read_attribute(name)
- }
+ @attributes.to_hash
end
# Returns an <tt>#inspect</tt>-like string for the value of the
@@ -400,13 +369,6 @@ module ActiveRecord
protected
- def clone_attributes(reader_method = :read_attribute, attributes = {}) # :nodoc:
- attribute_names.each do |name|
- attributes[name] = clone_attribute_value(reader_method, name)
- end
- attributes
- end
-
def clone_attribute_value(reader_method, attribute_name) # :nodoc:
value = send(reader_method, attribute_name)
value.duplicable? ? value.clone : value
@@ -424,7 +386,7 @@ module ActiveRecord
def attribute_method?(attr_name) # :nodoc:
# We check defined? because Syck calls respond_to? before actually calling initialize.
- defined?(@raw_attributes) && @raw_attributes.include?(attr_name)
+ defined?(@attributes) && @attributes.include?(attr_name)
end
private
@@ -465,9 +427,6 @@ module ActiveRecord
end
def typecasted_attribute_value(name)
- # FIXME: we need @attributes to be used consistently.
- # If the values stored in @attributes were already typecasted, this code
- # could be simplified
read_attribute(name)
end
end
diff --git a/activerecord/lib/active_record/attribute_methods/before_type_cast.rb b/activerecord/lib/active_record/attribute_methods/before_type_cast.rb
index 4365f5a1a1..fd61febd57 100644
--- a/activerecord/lib/active_record/attribute_methods/before_type_cast.rb
+++ b/activerecord/lib/active_record/attribute_methods/before_type_cast.rb
@@ -43,7 +43,7 @@ module ActiveRecord
# task.read_attribute_before_type_cast('completed_on') # => "2012-10-21"
# task.read_attribute_before_type_cast(:completed_on) # => "2012-10-21"
def read_attribute_before_type_cast(attr_name)
- @raw_attributes[attr_name.to_s]
+ @attributes[attr_name.to_s].value_before_type_cast
end
# Returns a hash of attributes before typecasting and deserialization.
@@ -57,7 +57,7 @@ module ActiveRecord
# task.attributes_before_type_cast
# # => {"id"=>nil, "title"=>nil, "is_done"=>true, "completed_on"=>"2012-10-21", "created_at"=>nil, "updated_at"=>nil}
def attributes_before_type_cast
- @raw_attributes
+ @attributes.values_before_type_cast
end
private
diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb
index be438aba95..e1a86fd3aa 100644
--- a/activerecord/lib/active_record/attribute_methods/dirty.rb
+++ b/activerecord/lib/active_record/attribute_methods/dirty.rb
@@ -38,23 +38,42 @@ module ActiveRecord
end
end
- def initialize_dup(other) # :nodoc:
- super
- init_changed_attributes
- end
+ def initialize_dup(other) # :nodoc:
+ super
+ calculate_changes_from_defaults
+ end
+
+ def changed?
+ super || changed_in_place.any?
+ end
- private
- def initialize_internals_callback
+ def changed
+ super | changed_in_place
+ end
+
+ def attribute_changed?(attr_name, options = {})
+ result = super
+ # We can't change "from" something in place. Only setters can define
+ # "from" and "to"
+ result ||= changed_in_place?(attr_name) unless options.key?(:from)
+ result
+ end
+
+ def changes_applied
+ super
+ store_original_raw_attributes
+ end
+
+ def reset_changes
super
- init_changed_attributes
+ original_raw_attributes.clear
end
- def init_changed_attributes
+ private
+
+ def calculate_changes_from_defaults
@changed_attributes = nil
- # Intentionally avoid using #column_defaults since overridden defaults (as is done in
- # optimistic locking) won't get written unless they get marked as changed
- self.class.columns.each do |c|
- attr, orig_value = c.name, c.default
+ self.class.column_defaults.each do |attr, orig_value|
changed_attributes[attr] = orig_value if _field_changed?(attr, orig_value)
end
end
@@ -65,11 +84,20 @@ module ActiveRecord
old_value = old_attribute_value(attr)
- result = super(attr, value)
+ result = super
+ store_original_raw_attribute(attr)
save_changed_attribute(attr, old_value)
result
end
+ def raw_write_attribute(attr, value)
+ attr = attr.to_s
+
+ result = super
+ original_raw_attributes[attr] = value
+ result
+ end
+
def save_changed_attribute(attr, old_value)
if attribute_changed?(attr)
changed_attributes.delete(attr) unless _field_changed?(attr, old_value)
@@ -101,9 +129,38 @@ module ActiveRecord
end
def _field_changed?(attr, old_value)
- new_value = read_attribute(attr)
- raw_value = read_attribute_before_type_cast(attr)
- column_for_attribute(attr).changed?(old_value, new_value, raw_value)
+ @attributes[attr].changed_from?(old_value)
+ end
+
+ def changed_in_place
+ self.class.attribute_names.select do |attr_name|
+ changed_in_place?(attr_name)
+ end
+ end
+
+ def changed_in_place?(attr_name)
+ old_value = original_raw_attribute(attr_name)
+ @attributes[attr_name].changed_in_place_from?(old_value)
+ end
+
+ def original_raw_attribute(attr_name)
+ original_raw_attributes.fetch(attr_name) do
+ read_attribute_before_type_cast(attr_name)
+ end
+ end
+
+ def original_raw_attributes
+ @original_raw_attributes ||= {}
+ end
+
+ def store_original_raw_attribute(attr_name)
+ original_raw_attributes[attr_name] = @attributes[attr_name].value_for_database
+ end
+
+ def store_original_raw_attributes
+ attribute_names.each do |attr|
+ store_original_raw_attribute(attr)
+ end
end
end
end
diff --git a/activerecord/lib/active_record/attribute_methods/primary_key.rb b/activerecord/lib/active_record/attribute_methods/primary_key.rb
index 931209b07b..cadad60ddd 100644
--- a/activerecord/lib/active_record/attribute_methods/primary_key.rb
+++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb
@@ -15,8 +15,10 @@ module ActiveRecord
# Returns the primary key value.
def id
- sync_with_transaction_state
- read_attribute(self.class.primary_key)
+ if pk = self.class.primary_key
+ sync_with_transaction_state
+ read_attribute(pk)
+ end
end
# Sets the primary key value.
@@ -81,12 +83,9 @@ module ActiveRecord
end
def get_primary_key(base_name) #:nodoc:
- return 'id' if base_name.blank?
-
- case primary_key_prefix_type
- when :table_name
+ if base_name && primary_key_prefix_type == :table_name
base_name.foreign_key(false)
- when :table_name_with_underscore
+ elsif base_name && primary_key_prefix_type == :table_name_with_underscore
base_name.foreign_key
else
if ActiveRecord::Base != self && table_exists?
diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb
index a354cd7503..10869dfc1e 100644
--- a/activerecord/lib/active_record/attribute_methods/read.rb
+++ b/activerecord/lib/active_record/attribute_methods/read.rb
@@ -35,35 +35,22 @@ module ActiveRecord
extend ActiveSupport::Concern
- ATTRIBUTE_TYPES_CACHED_BY_DEFAULT = [:datetime, :time, :date]
-
- included do
- class_attribute :attribute_types_cached_by_default, instance_writer: false
- self.attribute_types_cached_by_default = ATTRIBUTE_TYPES_CACHED_BY_DEFAULT
- end
-
module ClassMethods
- # +cache_attributes+ allows you to declare which converted attribute
- # values should be cached. Usually caching only pays off for attributes
- # with expensive conversion methods, like time related columns (e.g.
- # +created_at+, +updated_at+).
- def cache_attributes(*attribute_names)
- cached_attributes.merge attribute_names.map { |attr| attr.to_s }
+ [:cache_attributes, :cached_attributes, :cache_attribute?].each do |method_name|
+ define_method method_name do |*|
+ cached_attributes_deprecation_warning(method_name)
+ true
+ end
end
- # Returns the attributes which are cached. By default time related columns
- # with datatype <tt>:datetime, :time, :date</tt> are cached.
- def cached_attributes
- @cached_attributes ||= columns.select { |c| cacheable_column?(c) }.map { |col| col.name }.to_set
- end
+ protected
- # Returns +true+ if the provided attribute is being cached.
- def cache_attribute?(attr_name)
- cached_attributes.include?(attr_name)
+ def cached_attributes_deprecation_warning(method_name)
+ ActiveSupport::Deprecation.warn(<<-MESSAGE.strip_heredoc)
+ Calling `#{method_name}` is no longer necessary. All attributes are cached.
+ MESSAGE
end
- protected
-
if Module.methods_transplantable?
def define_method_attribute(name)
method = ReaderMethodCache[name]
@@ -89,45 +76,15 @@ module ActiveRecord
end
end
end
-
- private
-
- def cacheable_column?(column)
- if attribute_types_cached_by_default == ATTRIBUTE_TYPES_CACHED_BY_DEFAULT
- true
- else
- attribute_types_cached_by_default.include?(column.type)
- end
- end
end
# Returns the value of the attribute identified by <tt>attr_name</tt> after
# it has been typecast (for example, "2004-12-12" in a date column is cast
# to a date object, like Date.new(2004, 12, 12)).
- def read_attribute(attr_name)
- # If it's cached, just return it
- # We use #[] first as a perf optimization for non-nil values. See https://gist.github.com/jonleighton/3552829.
+ def read_attribute(attr_name, &block)
name = attr_name.to_s
- @attributes[name] || @attributes.fetch(name) {
- column = @column_types_override[name] if @column_types_override
- column ||= @column_types[name]
-
- return @raw_attributes.fetch(name) {
- if name == 'id' && self.class.primary_key != name
- read_attribute(self.class.primary_key)
- end
- } unless column
-
- value = @raw_attributes.fetch(name) {
- return block_given? ? yield(name) : nil
- }
-
- if self.class.cache_attribute?(name)
- @attributes[name] = column.type_cast_from_database(value)
- else
- column.type_cast_from_database value
- end
- }
+ name = self.class.primary_key if name == 'id'
+ @attributes.fetch_value(name, &block)
end
private
diff --git a/activerecord/lib/active_record/attribute_methods/serialization.rb b/activerecord/lib/active_record/attribute_methods/serialization.rb
index 60debb7d18..734d94865a 100644
--- a/activerecord/lib/active_record/attribute_methods/serialization.rb
+++ b/activerecord/lib/active_record/attribute_methods/serialization.rb
@@ -3,20 +3,7 @@ module ActiveRecord
module Serialization
extend ActiveSupport::Concern
- included do
- # Returns a hash of all the attributes that have been specified for
- # serialization as keys and their class restriction as values.
- class_attribute :serialized_attributes, instance_accessor: false
- self.serialized_attributes = {}
- end
-
module ClassMethods
- ##
- # :method: serialized_attributes
- #
- # Returns a hash of all the attributes that have been specified for
- # serialization as keys and their class restriction as values.
-
# If you have an attribute that needs to be saved to the database as an
# object, and retrieved as the same object, then specify the name of that
# attribute using this method and it will be handled automatically. The
@@ -50,8 +37,6 @@ module ActiveRecord
# serialize :preferences, Hash
# end
def serialize(attr_name, class_name_or_coder = Object)
- include Behavior
-
coder = if [:load, :dump].all? { |x| class_name_or_coder.respond_to?(x) }
class_name_or_coder
else
@@ -61,25 +46,18 @@ module ActiveRecord
decorate_attribute_type(attr_name, :serialize) do |type|
Type::Serialized.new(type, coder)
end
-
- # merge new serialized attribute and create new hash to ensure that each class in inheritance hierarchy
- # has its own hash of own serialized attributes
- self.serialized_attributes = serialized_attributes.merge(attr_name.to_s => coder)
- end
- end
-
-
- # This is only added to the model when serialize is called, which
- # ensures we do not make things slower when serialization is not used.
- module Behavior # :nodoc:
- extend ActiveSupport::Concern
-
- def should_record_timestamps?
- super || (self.record_timestamps && (attributes.keys & self.class.serialized_attributes.keys).present?)
end
- def keys_for_partial_write
- super | (attributes.keys & self.class.serialized_attributes.keys)
+ def serialized_attributes
+ ActiveSupport::Deprecation.warn(<<-WARNING.strip_heredoc)
+ `serialized_attributes` is deprecated without replacement, and will
+ be removed in Rails 5.0.
+ WARNING
+ @serialized_attributes ||= Hash[
+ columns.select { |t| t.cast_type.is_a?(Type::Serialized) }.map { |c|
+ [c.name, c.cast_type.coder]
+ }
+ ]
end
end
end
diff --git a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
index abad949ef4..f439bd1ffe 100644
--- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
+++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
@@ -1,7 +1,7 @@
module ActiveRecord
module AttributeMethods
module TimeZoneConversion
- class Type < SimpleDelegator # :nodoc:
+ class TimeZoneConverter < SimpleDelegator # :nodoc:
def type_cast_from_database(value)
convert_time_to_time_zone(super)
end
@@ -37,10 +37,25 @@ module ActiveRecord
module ClassMethods
private
- def create_time_zone_conversion_attribute?(name, column)
+
+ def inherited(subclass)
+ # We need to apply this decorator here, rather than on module inclusion. The closure
+ # created by the matcher would otherwise evaluate for `ActiveRecord::Base`, not the
+ # sub class being decorated. As such, changes to `time_zone_aware_attributes`, or
+ # `skip_time_zone_conversion_for_attributes` would not be picked up.
+ subclass.class_eval do
+ matcher = ->(name, type) { create_time_zone_conversion_attribute?(name, type) }
+ decorate_matching_attribute_types(matcher, :_time_zone_conversion) do |type|
+ TimeZoneConverter.new(type)
+ end
+ end
+ super
+ end
+
+ def create_time_zone_conversion_attribute?(name, cast_type)
time_zone_aware_attributes &&
!self.skip_time_zone_conversion_for_attributes.include?(name.to_sym) &&
- (:datetime == column.type)
+ (:datetime == cast_type.type)
end
end
end
diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb
index b72a6219b0..b3c8209a74 100644
--- a/activerecord/lib/active_record/attribute_methods/write.rb
+++ b/activerecord/lib/active_record/attribute_methods/write.rb
@@ -69,22 +69,14 @@ module ActiveRecord
def write_attribute_with_type_cast(attr_name, value, should_type_cast)
attr_name = attr_name.to_s
attr_name = self.class.primary_key if attr_name == 'id' && self.class.primary_key
- @attributes.delete(attr_name)
- column = type_for_attribute(attr_name)
- unless has_attribute?(attr_name) || self.class.columns_hash.key?(attr_name)
- raise ActiveModel::MissingAttributeError, "can't write unknown attribute `#{attr_name}'"
- end
-
- # If we're dealing with a binary column, write the data to the cache
- # so we don't attempt to typecast multiple times.
- if column.binary?
- @attributes[attr_name] = value
- elsif should_type_cast
- @attributes[attr_name] = column.type_cast_from_user(value)
+ if should_type_cast
+ @attributes.write_from_user(attr_name, value)
+ else
+ @attributes.write_from_database(attr_name, value)
end
- @raw_attributes[attr_name] = value
+ value
end
end
end
diff --git a/activerecord/lib/active_record/attribute_set.rb b/activerecord/lib/active_record/attribute_set.rb
new file mode 100644
index 0000000000..64df6f6358
--- /dev/null
+++ b/activerecord/lib/active_record/attribute_set.rb
@@ -0,0 +1,77 @@
+require 'active_record/attribute_set/builder'
+
+module ActiveRecord
+ class AttributeSet # :nodoc:
+ delegate :keys, to: :initialized_attributes
+
+ def initialize(attributes)
+ @attributes = attributes
+ end
+
+ def [](name)
+ attributes[name] || Attribute.null(name)
+ end
+
+ def values_before_type_cast
+ attributes.transform_values(&:value_before_type_cast)
+ end
+
+ def to_hash
+ initialized_attributes.transform_values(&:value)
+ end
+ alias_method :to_h, :to_hash
+
+ def include?(name)
+ attributes.include?(name) && self[name].initialized?
+ end
+
+ def fetch_value(name, &block)
+ self[name].value(&block)
+ end
+
+ def write_from_database(name, value)
+ attributes[name] = self[name].with_value_from_database(value)
+ end
+
+ def write_from_user(name, value)
+ attributes[name] = self[name].with_value_from_user(value)
+ end
+
+ def freeze
+ @attributes.freeze
+ super
+ end
+
+ def initialize_dup(_)
+ @attributes = attributes.transform_values(&:dup)
+ super
+ end
+
+ def initialize_clone(_)
+ @attributes = attributes.clone
+ super
+ end
+
+ def reset(key)
+ if include?(key)
+ write_from_database(key, nil)
+ end
+ end
+
+ def ensure_initialized(key)
+ unless self[key].initialized?
+ write_from_database(key, nil)
+ end
+ end
+
+ protected
+
+ attr_reader :attributes
+
+ private
+
+ def initialized_attributes
+ attributes.select { |_, attr| attr.initialized? }
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/attribute_set/builder.rb b/activerecord/lib/active_record/attribute_set/builder.rb
new file mode 100644
index 0000000000..1e146a07da
--- /dev/null
+++ b/activerecord/lib/active_record/attribute_set/builder.rb
@@ -0,0 +1,32 @@
+module ActiveRecord
+ class AttributeSet # :nodoc:
+ class Builder # :nodoc:
+ attr_reader :types
+
+ def initialize(types)
+ @types = types
+ end
+
+ def build_from_database(values = {}, additional_types = {})
+ attributes = build_attributes_from_values(values, additional_types)
+ add_uninitialized_attributes(attributes)
+ AttributeSet.new(attributes)
+ end
+
+ private
+
+ def build_attributes_from_values(values, additional_types)
+ values.each_with_object({}) do |(name, value), hash|
+ type = additional_types.fetch(name, types[name])
+ hash[name] = Attribute.from_database(name, value, type)
+ end
+ end
+
+ def add_uninitialized_attributes(attributes)
+ types.except(*attributes.keys).each do |name, type|
+ attributes[name] = Attribute.uninitialized(name, type)
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/attributes.rb b/activerecord/lib/active_record/attributes.rb
index 9c80121d70..890a1314d9 100644
--- a/activerecord/lib/active_record/attributes.rb
+++ b/activerecord/lib/active_record/attributes.rb
@@ -9,7 +9,7 @@ module ActiveRecord
self.user_provided_columns = {}
end
- module ClassMethods
+ 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.
@@ -27,7 +27,7 @@ module ActiveRecord
#
# ==== Examples
#
- # The type detected by Active Record can be overriden.
+ # The type detected by Active Record can be overridden.
#
# # db/schema.rb
# create_table :store_listings, force: true do |t|
@@ -109,13 +109,13 @@ module ActiveRecord
end
def clear_caches_calculated_from_columns
+ @attributes_builder = nil
+ @column_names = nil
+ @column_types = nil
@columns = nil
@columns_hash = nil
- @column_types = nil
- @column_defaults = nil
- @raw_column_defaults = nil
- @column_names = nil
@content_columns = nil
+ @default_attributes = nil
end
end
end
diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb
index b3c3e26c9f..dd92e29199 100644
--- a/activerecord/lib/active_record/autosave_association.rb
+++ b/activerecord/lib/active_record/autosave_association.rb
@@ -184,9 +184,7 @@ module ActiveRecord
before_save :before_save_collection_association
define_non_cyclic_method(save_method) { save_collection_association(reflection) }
- # Doesn't use after_save as that would save associations added in after_create/after_update twice
- after_create save_method
- after_update save_method
+ after_save save_method
elsif reflection.has_one?
define_method(save_method) { save_has_one_association(reflection) } unless method_defined?(save_method)
# Configures two callbacks instead of a single after_save so that
@@ -364,6 +362,7 @@ module ActiveRecord
raise ActiveRecord::Rollback unless saved
end
+ @new_record_before_save = false
end
# reconstruct the scope now that we know the owner's id
diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb
index e4d0abb8ef..64cc5b68cc 100644
--- a/activerecord/lib/active_record/base.rb
+++ b/activerecord/lib/active_record/base.rb
@@ -9,6 +9,7 @@ require 'active_support/core_ext/class/delegating_attributes'
require 'active_support/core_ext/array/extract_options'
require 'active_support/core_ext/hash/deep_merge'
require 'active_support/core_ext/hash/slice'
+require 'active_support/core_ext/hash/transform_values'
require 'active_support/core_ext/string/behavior'
require 'active_support/core_ext/kernel/singleton_class'
require 'active_support/core_ext/module/introspection'
@@ -219,25 +220,9 @@ module ActiveRecord #:nodoc:
#
# == Single table inheritance
#
- # Active Record allows inheritance by storing the name of the class in a column that by
- # default is named "type" (can be changed by overwriting <tt>Base.inheritance_column</tt>).
- # This means that an inheritance looking like this:
- #
- # class Company < ActiveRecord::Base; end
- # class Firm < Company; end
- # class Client < Company; end
- # class PriorityClient < Client; end
- #
- # When you do <tt>Firm.create(name: "37signals")</tt>, this record will be saved in
- # the companies table with type = "Firm". You can then fetch this row again using
- # <tt>Company.where(name: '37signals').first</tt> and it will return a Firm object.
- #
- # If you don't have a type column defined in your table, single-table inheritance won't
- # be triggered. In that case, it'll work just like normal subclasses with no special magic
- # for differentiating between them or reloading the right type with find.
- #
- # Note, all the attributes for all the cases are kept in the same table. Read more:
- # http://www.martinfowler.com/eaaCatalog/singleTableInheritance.html
+ # Active Record allows inheritance by storing the name of the class in a
+ # column that is named "type" by default. See ActiveRecord::Inheritance for
+ # more details.
#
# == Connection to multiple databases in different models
#
@@ -308,6 +293,8 @@ module ActiveRecord #:nodoc:
include Integration
include Validations
include CounterCache
+ include Attributes
+ include AttributeDecorators
include Locking::Optimistic
include Locking::Pessimistic
include AttributeMethods
@@ -323,8 +310,6 @@ module ActiveRecord #:nodoc:
include Reflection
include Serialization
include Store
- include Attributes
- include AttributeDecorators
end
ActiveSupport.run_load_hooks(:active_record, Base)
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
index db80c0faee..cb75070e3a 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
@@ -364,7 +364,7 @@ module ActiveRecord
conn.expire
end
- release conn, owner
+ release owner
@available.add conn
end
@@ -377,7 +377,7 @@ module ActiveRecord
@connections.delete conn
@available.delete conn
- release conn, conn.owner
+ release conn.owner
@available.add checkout_new_connection if @available.any_waiting?
end
@@ -425,7 +425,7 @@ module ActiveRecord
end
end
- def release(conn, owner)
+ def release(owner)
thread_id = owner.object_id
@reserved_connections.delete thread_id
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
index 7ff5001796..e8ce00d92b 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
@@ -193,7 +193,7 @@ module ActiveRecord
# * You are creating a nested (savepoint) transaction
#
# The mysql, mysql2 and postgresql adapters support setting the transaction
- # isolation level. However, support is disabled for mysql versions below 5,
+ # isolation level. However, support is disabled for MySQL versions below 5,
# because they are affected by a bug[http://bugs.mysql.com/bug.php?id=39170]
# which means the isolation level gets persisted outside the transaction.
def transaction(options = {})
@@ -338,8 +338,8 @@ module ActiveRecord
end
# The default strategy for an UPDATE with joins is to use a subquery. This doesn't work
- # on mysql (even when aliasing the tables), but mysql allows using JOIN directly in
- # an UPDATE statement, so in the mysql adapters we redefine this to do that.
+ # on MySQL (even when aliasing the tables), but MySQL allows using JOIN directly in
+ # an UPDATE statement, so in the MySQL adapters we redefine this to do that.
def join_to_update(update, select) #:nodoc:
key = update.key
subselect = subquery_for(key, select)
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
index 04ae67234f..eb88845913 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
@@ -9,13 +9,8 @@ module ActiveRecord
# records are quoted as their primary key
return value.quoted_id if value.respond_to?(:quoted_id)
- # FIXME: The only case we get an object other than nil or a real column
- # is `SchemaStatements#add_column` with a PG array that has a non-empty default
- # value. Is this really the only case? Are we missing tests for other types?
- # We should have a real column object passed (or nil) here, and check for that
- # instead
- if column.respond_to?(:type_cast_for_database)
- value = column.type_cast_for_database(value)
+ if column
+ value = column.cast_type.type_cast_for_database(value)
end
_quote(value)
@@ -29,13 +24,8 @@ module ActiveRecord
return value.id
end
- # FIXME: The only case we get an object other than nil or a real column
- # is `SchemaStatements#add_column` with a PG array that has a non-empty default
- # value. Is this really the only case? Are we missing tests for other types?
- # We should have a real column object passed (or nil) here, and check for that
- # instead
- if column.respond_to?(:type_cast_for_database)
- value = column.type_cast_for_database(value)
+ if column
+ value = column.cast_type.type_cast_for_database(value)
end
_type_cast(value)
@@ -66,7 +56,7 @@ module ActiveRecord
# This works for mysql and mysql2 where table.column can be used to
# resolve ambiguity.
#
- # We override this in the sqlite and postgresql adapters to use only
+ # We override this in the sqlite3 and postgresql adapters to use only
# the column name (as per syntax requirements).
def quote_table_name_for_assignment(table, attr)
quote_table_name("#{table}.#{attr}")
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb
index 47fe501752..adad6cd542 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb
@@ -23,6 +23,8 @@ module ActiveRecord
def visit_AlterTable(o)
sql = "ALTER TABLE #{quote_table_name(o.name)} "
sql << o.adds.map { |col| visit_AddColumn col }.join(' ')
+ sql << o.foreign_key_adds.map { |fk| visit_AddForeignKey fk }.join(' ')
+ sql << o.foreign_key_drops.map { |fk| visit_DropForeignKey fk }.join(' ')
end
def visit_ColumnDefinition(o)
@@ -41,6 +43,21 @@ module ActiveRecord
create_sql
end
+ def visit_AddForeignKey(o)
+ sql = <<-SQL.strip_heredoc
+ ADD CONSTRAINT #{quote_column_name(o.name)}
+ FOREIGN KEY (#{quote_column_name(o.column)})
+ REFERENCES #{quote_table_name(o.to_table)} (#{quote_column_name(o.primary_key)})
+ SQL
+ sql << " #{action_sql('DELETE', o.on_delete)}" if o.on_delete
+ sql << " #{action_sql('UPDATE', o.on_update)}" if o.on_update
+ sql
+ end
+
+ def visit_DropForeignKey(name)
+ "DROP CONSTRAINT #{quote_column_name(name)}"
+ end
+
def column_options(o)
column_options = {}
column_options[:null] = o.null unless o.null.nil?
@@ -77,6 +94,7 @@ module ActiveRecord
def quote_value(value, column)
column.sql_type ||= type_to_sql(column.type, column.limit, column.precision, column.scale)
+ column.cast_type ||= type_for_column(column)
@conn.quote(value, column)
end
@@ -84,6 +102,23 @@ module ActiveRecord
def options_include_default?(options)
options.include?(:default) && !(options[:null] == false && options[:default].nil?)
end
+
+ def action_sql(action, dependency)
+ case dependency
+ when :nullify then "ON #{action} SET NULL"
+ when :cascade then "ON #{action} CASCADE"
+ when :restrict then "ON #{action} RESTRICT"
+ else
+ raise ArgumentError, <<-MSG.strip_heredoc
+ '#{dependency}' is not supported for :on_update or :on_delete.
+ Supported values are: :nullify, :cascade, :restrict
+ MSG
+ end
+ end
+
+ def type_for_column(column)
+ @conn.lookup_cast_type(column.sql_type)
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
index a9b3e9cfb9..98e6795f10 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
@@ -15,7 +15,7 @@ module ActiveRecord
# are typically created by methods in TableDefinition, and added to the
# +columns+ attribute of said TableDefinition object, in order to be used
# for generating a number of table creation or table changing SQL statements.
- class ColumnDefinition < Struct.new(:name, :type, :limit, :precision, :scale, :default, :null, :first, :after, :primary_key, :sql_type) #:nodoc:
+ class ColumnDefinition < Struct.new(:name, :type, :limit, :precision, :scale, :default, :null, :first, :after, :primary_key, :sql_type, :cast_type) #:nodoc:
def primary_key?
primary_key || type.to_sym == :primary_key
@@ -25,6 +25,37 @@ module ActiveRecord
class ChangeColumnDefinition < Struct.new(:column, :type, :options) #:nodoc:
end
+ class ForeignKeyDefinition < Struct.new(:from_table, :to_table, :options) #:nodoc:
+ def name
+ options[:name]
+ end
+
+ def column
+ options[:column]
+ end
+
+ def primary_key
+ options[:primary_key] || default_primary_key
+ end
+
+ def on_delete
+ options[:on_delete]
+ end
+
+ def on_update
+ options[:on_update]
+ end
+
+ def custom_primary_key?
+ options[:primary_key] != default_primary_key
+ end
+
+ private
+ def default_primary_key
+ "id"
+ end
+ end
+
# Represents the schema of an SQL table in an abstract way. This class
# provides methods for manipulating the schema representation.
#
@@ -303,14 +334,26 @@ module ActiveRecord
class AlterTable # :nodoc:
attr_reader :adds
+ attr_reader :foreign_key_adds
+ attr_reader :foreign_key_drops
def initialize(td)
@td = td
@adds = []
+ @foreign_key_adds = []
+ @foreign_key_drops = []
end
def name; @td.name; end
+ def add_foreign_key(to_table, options)
+ @foreign_key_adds << ForeignKeyDefinition.new(name, to_table, options)
+ end
+
+ def drop_foreign_key(name)
+ @foreign_key_drops << name
+ end
+
def add_column(name, type, options)
name = name.to_s
type = type.to_sym
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 d3e172927d..9bd0401e40 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb
@@ -1,5 +1,3 @@
-require 'ipaddr'
-
module ActiveRecord
module ConnectionAdapters # :nodoc:
# The goal of this module is to move Adapter specific column
@@ -25,7 +23,8 @@ module ActiveRecord
spec[:precision] = column.precision.inspect if column.precision
spec[:scale] = column.scale.inspect if column.scale
spec[:null] = 'false' unless column.null
- spec[:default] = column.type_cast_for_schema(column.default) if column.has_default?
+ spec[:default] = schema_default(column) if column.has_default?
+ spec.delete(:default) if spec[:default].nil?
spec
end
@@ -33,6 +32,15 @@ module ActiveRecord
def migration_keys
[:name, :limit, :precision, :scale, :default, :null]
end
+
+ private
+
+ def schema_default(column)
+ default = column.type_cast_from_database(column.default)
+ unless default.nil?
+ column.type_cast_for_schema(default)
+ end
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
index ffa6af6d99..5814c2b711 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
@@ -642,6 +642,115 @@ module ActiveRecord
end
alias :remove_belongs_to :remove_reference
+ # Returns an array of foreign keys for the given table.
+ # The foreign keys are represented as +ForeignKeyDefinition+ objects.
+ def foreign_keys(table_name)
+ raise NotImplementedError, "foreign_keys is not implemented"
+ end
+
+ # Adds a new foreign key. +from_table+ is the table with the key column,
+ # +to_table+ contains the referenced primary key.
+ #
+ # The foreign key will be named after the following pattern: <tt>fk_rails_<identifier></tt>.
+ # +identifier+ is a 10 character long random string. A custom name can be specified with
+ # the <tt>:name</tt> option.
+ #
+ # ====== Creating a simple foreign key
+ #
+ # add_foreign_key :articles, :authors
+ #
+ # generates:
+ #
+ # ALTER TABLE "articles" ADD CONSTRAINT articles_author_id_fk FOREIGN KEY ("author_id") REFERENCES "authors" ("id")
+ #
+ # ====== Creating a foreign key on a specific column
+ #
+ # add_foreign_key :articles, :users, column: :author_id, primary_key: "lng_id"
+ #
+ # generates:
+ #
+ # ALTER TABLE "articles" ADD CONSTRAINT fk_rails_58ca3d3a82 FOREIGN KEY ("author_id") REFERENCES "users" ("lng_id")
+ #
+ # ====== Creating a cascading foreign key
+ #
+ # add_foreign_key :articles, :authors, on_delete: :cascade
+ #
+ # generates:
+ #
+ # ALTER TABLE "articles" ADD CONSTRAINT articles_author_id_fk FOREIGN KEY ("author_id") REFERENCES "authors" ("id") ON DELETE CASCADE
+ #
+ # The +options+ hash can include the following keys:
+ # [<tt>:column</tt>]
+ # The foreign key column name on +from_table+. Defaults to <tt>to_table.singularize + "_id"</tt>
+ # [<tt>:primary_key</tt>]
+ # The primary key column name on +to_table+. Defaults to +id+.
+ # [<tt>:name</tt>]
+ # The constraint name. Defaults to <tt>fk_rails_<identifier></tt>.
+ # [<tt>:on_delete</tt>]
+ # Action that happens <tt>ON DELETE</tt>. Valid values are +:nullify+, +:cascade:+ and +:restrict+
+ # [<tt>:on_update</tt>]
+ # Action that happens <tt>ON UPDATE</tt>. Valid values are +:nullify+, +:cascade:+ and +:restrict+
+ def add_foreign_key(from_table, to_table, options = {})
+ return unless supports_foreign_keys?
+
+ options[:column] ||= foreign_key_column_for(to_table)
+
+ options = {
+ column: options[:column],
+ primary_key: options[:primary_key],
+ name: foreign_key_name(from_table, options),
+ on_delete: options[:on_delete],
+ on_update: options[:on_update]
+ }
+ at = create_alter_table from_table
+ at.add_foreign_key to_table, options
+
+ execute schema_creation.accept(at)
+ end
+
+ # Removes the given foreign key from the table.
+ #
+ # Removes the foreign key on +accounts.branch_id+.
+ #
+ # remove_foreign_key :accounts, :branches
+ #
+ # Removes the foreign key on +accounts.owner_id+.
+ #
+ # remove_foreign_key :accounts, column: :owner_id
+ #
+ # Removes the foreign key named +special_fk_name+ on the +accounts+ table.
+ #
+ # remove_foreign_key :accounts, name: :special_fk_name
+ #
+ def remove_foreign_key(from_table, options_or_to_table = {})
+ return unless supports_foreign_keys?
+
+ if options_or_to_table.is_a?(Hash)
+ options = options_or_to_table
+ else
+ options = { column: foreign_key_column_for(options_or_to_table) }
+ end
+
+ fk_name_to_delete = options.fetch(:name) do
+ fk_to_delete = foreign_keys(from_table).detect {|fk| fk.column == options[:column] }
+
+ if fk_to_delete
+ fk_to_delete.name
+ else
+ raise ArgumentError, "Table '#{from_table}' has no foreign key on column '#{options[:column]}'"
+ end
+ end
+
+ at = create_alter_table from_table
+ at.drop_foreign_key fk_name_to_delete
+
+ execute schema_creation.accept(at)
+ end
+
+ def foreign_key_column_for(table_name) # :nodoc:
+ "#{table_name.to_s.singularize}_id"
+ end
+
def dump_schema_information #:nodoc:
sm_table = ActiveRecord::Migrator.schema_migrations_table_name
@@ -788,7 +897,7 @@ module ActiveRecord
return option_strings
end
- # Overridden by the mysql adapter for supporting index lengths
+ # Overridden by the MySQL adapter for supporting index lengths
def quoted_columns_for_index(column_names, options = {})
option_strings = Hash[column_names.map {|name| [name, '']}]
@@ -852,6 +961,12 @@ module ActiveRecord
def create_alter_table(name)
AlterTable.new create_table_definition(name, false, {})
end
+
+ def foreign_key_name(table_name, options) # :nodoc:
+ options.fetch(:name) do
+ "fk_rails_#{SecureRandom.hex(5)}"
+ end
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
index cc494a7f40..f8c054eb69 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
@@ -233,6 +233,11 @@ module ActiveRecord
false
end
+ # Does this adapter support creating foreign key constraints?
+ def supports_foreign_keys?
+ false
+ end
+
# This is meant to be implemented by the adapters that support extensions
def disable_extension(name)
end
@@ -370,12 +375,12 @@ module ActiveRecord
Column.new(name, default, cast_type, sql_type, null)
end
- protected
-
def lookup_cast_type(sql_type) # :nodoc:
type_map.lookup(sql_type)
end
+ protected
+
def initialize_type_map(m) # :nodoc:
register_class_with_limit m, %r(boolean)i, Type::Boolean
register_class_with_limit m, %r(char)i, Type::String
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 200b773172..ccb957d2c8 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
@@ -12,6 +12,10 @@ module ActiveRecord
private
+ def visit_DropForeignKey(name)
+ "DROP FOREIGN KEY #{name}"
+ end
+
def visit_TableDefinition(o)
name = o.name
create_sql = "CREATE#{' TEMPORARY' if o.temporary} TABLE #{quote_table_name(name)} "
@@ -62,20 +66,19 @@ module ActiveRecord
@extra = extra
super(name, default, cast_type, sql_type, null)
assert_valid_default(default)
+ extract_default
end
- def default
- @default ||= if blob_or_text_column?
- null || strict ? nil : ''
- elsif missing_default_forged_as_empty_string?(@original_default)
- nil
- else
- super
+ def extract_default
+ if blob_or_text_column?
+ @default = null || strict ? nil : ''
+ elsif missing_default_forged_as_empty_string?(@default)
+ @default = nil
end
end
def has_default?
- return false if blob_or_text_column? #mysql forbids defaults on blob and text columns
+ return false if blob_or_text_column? # MySQL forbids defaults on blob and text columns
super
end
@@ -193,6 +196,10 @@ module ActiveRecord
true
end
+ def supports_foreign_keys?
+ true
+ end
+
def native_database_types
NATIVE_DATABASE_TYPES
end
@@ -213,7 +220,7 @@ module ActiveRecord
Column.new(field, default, cast_type, sql_type, null, collation, strict_mode?, extra)
end
- # Must return the Mysql error number from the exception, if the exception has an
+ # Must return the MySQL error number from the exception, if the exception has an
# error number.
def error_number(exception) # :nodoc:
raise NotImplementedError
@@ -382,7 +389,7 @@ module ActiveRecord
end
def table_exists?(name)
- return false unless name
+ return false unless name.present?
return true if tables(nil, nil, name).any?
name = name.to_s
@@ -502,6 +509,34 @@ module ActiveRecord
execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} #{index_using} ON #{quote_table_name(table_name)} (#{index_columns})#{index_options} #{index_algorithm}"
end
+ def foreign_keys(table_name)
+ fk_info = select_all <<-SQL.strip_heredoc
+ SELECT fk.referenced_table_name as 'to_table'
+ ,fk.referenced_column_name as 'primary_key'
+ ,fk.column_name as 'column'
+ ,fk.constraint_name as 'name'
+ FROM information_schema.key_column_usage fk
+ WHERE fk.referenced_column_name is not null
+ AND fk.table_schema = '#{@config[:database]}'
+ AND fk.table_name = '#{table_name}'
+ SQL
+
+ create_table_info = select_one("SHOW CREATE TABLE #{quote_table_name(table_name)}")["Create Table"]
+
+ fk_info.map do |row|
+ options = {
+ column: row['column'],
+ name: row['name'],
+ primary_key: row['primary_key']
+ }
+
+ options[:on_update] = extract_foreign_key_action(create_table_info, row['name'], "UPDATE")
+ options[:on_delete] = extract_foreign_key_action(create_table_info, row['name'], "DELETE")
+
+ ForeignKeyDefinition.new(table_name, row['to_table'], options)
+ end
+ end
+
# Maps logical Rails types to MySQL-specific data types.
def type_to_sql(type, limit = nil, precision = nil, scale = nil)
case type.to_s
@@ -758,8 +793,8 @@ module ActiveRecord
# Make MySQL reject illegal values rather than truncating or blanking them, see
# http://dev.mysql.com/doc/refman/5.0/en/server-sql-mode.html#sqlmode_strict_all_tables
# If the user has provided another value for sql_mode, don't replace it.
- if strict_mode? && !variables.has_key?('sql_mode')
- variables['sql_mode'] = 'STRICT_ALL_TABLES'
+ unless variables.has_key?('sql_mode')
+ variables['sql_mode'] = strict_mode? ? 'STRICT_ALL_TABLES' : ''
end
# NAMES does not have an equals sign, see
@@ -780,6 +815,15 @@ module ActiveRecord
# ...and send them all in one query
@connection.query "SET #{encoding} #{variable_assignments}"
end
+
+ def extract_foreign_key_action(structure, name, action) # :nodoc:
+ if structure =~ /CONSTRAINT #{quote_column_name(name)} FOREIGN KEY .* REFERENCES .* ON #{action} (CASCADE|SET NULL|RESTRICT)/
+ case $1
+ when 'CASCADE'; :cascade
+ when 'SET NULL'; :nullify
+ end
+ end
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb
index 72c6990ba5..1f1e2c46f4 100644
--- a/activerecord/lib/active_record/connection_adapters/column.rb
+++ b/activerecord/lib/active_record/connection_adapters/column.rb
@@ -13,10 +13,10 @@ 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_function
+ attr_reader :name, :cast_type, :null, :sql_type, :default, :default_function
delegate :type, :precision, :scale, :limit, :klass, :accessor,
- :text?, :number?, :binary?, :serialized?, :changed?,
+ :text?, :number?, :binary?, :changed?,
:type_cast_from_user, :type_cast_from_database, :type_cast_for_database,
:type_cast_for_schema,
to: :cast_type
@@ -35,7 +35,7 @@ module ActiveRecord
@cast_type = cast_type
@sql_type = sql_type
@null = null
- @original_default = default
+ @default = default
@default_function = nil
end
@@ -51,23 +51,12 @@ module ActiveRecord
Base.human_attribute_name(@name)
end
- def default
- @default ||= type_cast_from_database(@original_default)
- end
-
def with_type(type)
dup.tap do |clone|
- clone.instance_variable_set('@default', nil)
clone.instance_variable_set('@cast_type', type)
end
end
end
-
- class NullColumn < Column
- def initialize(name)
- super name, nil, Type::Value.new
- end
- end
end
# :startdoc:
end
diff --git a/activerecord/lib/active_record/connection_adapters/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/connection_specification.rb
index b79d1a4458..2fcb085ab2 100644
--- a/activerecord/lib/active_record/connection_adapters/connection_specification.rb
+++ b/activerecord/lib/active_record/connection_adapters/connection_specification.rb
@@ -85,7 +85,7 @@ module ActiveRecord
"password" => uri.password,
"port" => uri.port,
"database" => database_from_path,
- "host" => uri.host })
+ "host" => uri.hostname })
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
index 909bba8c7d..ad07a46e51 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
@@ -263,7 +263,7 @@ module ActiveRecord
end
module Fields # :nodoc:
- class DateTime < Type::DateTime
+ class DateTime < Type::DateTime # :nodoc:
def cast_value(value)
if Mysql::Time === value
new_time(
@@ -280,7 +280,7 @@ module ActiveRecord
end
end
- class Time < Type::Time
+ class Time < Type::Time # :nodoc:
def cast_value(value)
if Mysql::Time === value
new_time(
@@ -409,7 +409,7 @@ module ActiveRecord
stmt.execute(*type_casted_binds.map { |_, val| val })
rescue Mysql::Error => e
# Older versions of MySQL leave the prepared statement in a bad
- # place when an error occurs. To support older mysql versions, we
+ # place when an error occurs. To support older MySQL versions, we
# need to close the statement and delete the statement from the
# cache.
stmt.close
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb b/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb
deleted file mode 100644
index 666d1cf6e3..0000000000
--- a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb
+++ /dev/null
@@ -1,117 +0,0 @@
-module ActiveRecord
- module ConnectionAdapters
- module PostgreSQL
- module Cast # :nodoc:
- def point_to_string(point) # :nodoc:
- "(#{point[0]},#{point[1]})"
- end
-
- def hstore_to_string(object, array_member = false) # :nodoc:
- if Hash === object
- string = object.map { |k, v| "#{escape_hstore(k)}=>#{escape_hstore(v)}" }.join(',')
- string = escape_hstore(string) if array_member
- string
- else
- object
- end
- end
-
- def string_to_hstore(string) # :nodoc:
- if string.nil?
- nil
- elsif String === string
- Hash[string.scan(HstorePair).map { |k, v|
- v = v.upcase == 'NULL' ? nil : v.gsub(/\A"(.*)"\Z/m,'\1').gsub(/\\(.)/, '\1')
- k = k.gsub(/\A"(.*)"\Z/m,'\1').gsub(/\\(.)/, '\1')
- [k, v]
- }]
- else
- string
- end
- end
-
- def json_to_string(object) # :nodoc:
- if Hash === object || Array === object
- ActiveSupport::JSON.encode(object)
- else
- object
- end
- end
-
- def array_to_string(value, column, adapter) # :nodoc:
- casted_values = value.map do |val|
- if String === val
- if val == "NULL"
- "\"#{val}\""
- else
- quote_and_escape(adapter.type_cast(val, column, true))
- end
- else
- adapter.type_cast(val, column, true)
- end
- end
- "{#{casted_values.join(',')}}"
- end
-
- def range_to_string(object) # :nodoc:
- from = object.begin.respond_to?(:infinite?) && object.begin.infinite? ? '' : object.begin
- to = object.end.respond_to?(:infinite?) && object.end.infinite? ? '' : object.end
- "[#{from},#{to}#{object.exclude_end? ? ')' : ']'}"
- end
-
- def string_to_json(string) # :nodoc:
- if String === string
- ActiveSupport::JSON.decode(string)
- else
- string
- end
- end
-
- def string_to_array(string, oid) # :nodoc:
- parse_pg_array(string).map {|val| type_cast_array(oid, val)}
- end
-
- private
-
- HstorePair = begin
- quoted_string = /"[^"\\]*(?:\\.[^"\\]*)*"/
- unquoted_string = /(?:\\.|[^\s,])[^\s=,\\]*(?:\\.[^\s=,\\]*|=[^,>])*/
- /(#{quoted_string}|#{unquoted_string})\s*=>\s*(#{quoted_string}|#{unquoted_string})/
- end
-
- def escape_hstore(value)
- if value.nil?
- 'NULL'
- else
- if value == ""
- '""'
- else
- '"%s"' % value.to_s.gsub(/(["\\])/, '\\\\\1')
- end
- end
- end
-
- ARRAY_ESCAPE = "\\" * 2 * 2 # escape the backslash twice for PG arrays
-
- def quote_and_escape(value)
- case value
- when "NULL", Numeric
- value
- else
- value = value.gsub(/\\/, ARRAY_ESCAPE)
- value.gsub!(/"/,"\\\"")
- "\"#{value}\""
- end
- end
-
- def type_cast_array(oid, value)
- if ::Array === value
- value.map {|item| type_cast_array(oid, item)}
- else
- oid.type_cast_from_database value
- end
- end
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb
index a579746815..37e5c3859c 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb
@@ -1,5 +1,3 @@
-require 'active_record/connection_adapters/postgresql/cast'
-
module ActiveRecord
module ConnectionAdapters
# PostgreSQL-specific extensions to column definitions in a table.
@@ -17,24 +15,6 @@ module ActiveRecord
@default_function = default_function
end
-
- # :stopdoc:
- class << self
- include PostgreSQL::Cast
-
- # Loads pg_array_parser if available. String parsing can be
- # performed quicker by a native extension, which will not create
- # a large amount of Ruby objects that will need to be garbage
- # collected. pg_array_parser has a C and Java extension
- begin
- require 'pg_array_parser'
- include PgArrayParser
- rescue LoadError
- require 'active_record/connection_adapters/postgresql/array_parser'
- include PostgreSQL::ArrayParser
- end
- end
- # :startdoc:
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb
index 33a98b4fcb..d05ce61330 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb
@@ -21,6 +21,7 @@ require 'active_record/connection_adapters/postgresql/oid/specialized_string'
require 'active_record/connection_adapters/postgresql/oid/time'
require 'active_record/connection_adapters/postgresql/oid/uuid'
require 'active_record/connection_adapters/postgresql/oid/vector'
+require 'active_record/connection_adapters/postgresql/oid/xml'
require 'active_record/connection_adapters/postgresql/oid/type_map_initializer'
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 0e9dcd8c0c..cd5efe2bb8 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb
@@ -2,21 +2,93 @@ module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module OID # :nodoc:
- class Array < Type::Value
- attr_reader :subtype
+ class Array < Type::Value # :nodoc:
+ include Type::Mutable
+
+ # Loads pg_array_parser if available. String parsing can be
+ # performed quicker by a native extension, which will not create
+ # a large amount of Ruby objects that will need to be garbage
+ # collected. pg_array_parser has a C and Java extension
+ begin
+ require 'pg_array_parser'
+ include PgArrayParser
+ rescue LoadError
+ require 'active_record/connection_adapters/postgresql/array_parser'
+ include PostgreSQL::ArrayParser
+ end
+
+ attr_reader :subtype, :delimiter
delegate :type, to: :subtype
- def initialize(subtype)
+ def initialize(subtype, delimiter = ',')
@subtype = subtype
+ @delimiter = delimiter
+ end
+
+ def type_cast_from_database(value)
+ if value.is_a?(::String)
+ type_cast_array(parse_pg_array(value), :type_cast_from_database)
+ else
+ super
+ end
+ end
+
+ def type_cast_from_user(value)
+ type_cast_array(value, :type_cast_from_user)
+ end
+
+ def type_cast_for_database(value)
+ if value.is_a?(::Array)
+ cast_value_for_database(value)
+ else
+ super
+ end
+ end
+
+ private
+
+ def type_cast_array(value, method)
+ if value.is_a?(::Array)
+ value.map { |item| type_cast_array(item, method) }
+ else
+ @subtype.public_send(method, value)
+ end
end
- def type_cast(value)
- if ::String === value
- ConnectionAdapters::PostgreSQLColumn.string_to_array value, @subtype
+ def cast_value_for_database(value)
+ if value.is_a?(::Array)
+ casted_values = value.map { |item| cast_value_for_database(item) }
+ "{#{casted_values.join(delimiter)}}"
else
- value
+ quote_and_escape(subtype.type_cast_for_database(value))
end
end
+
+ ARRAY_ESCAPE = "\\" * 2 * 2 # escape the backslash twice for PG arrays
+
+ def quote_and_escape(value)
+ case value
+ when ::String
+ if string_requires_quoting?(value)
+ value = value.gsub(/\\/, ARRAY_ESCAPE)
+ value.gsub!(/"/,"\\\"")
+ %("#{value}")
+ else
+ value
+ end
+ when nil then "NULL"
+ else value
+ end
+ end
+
+ # See http://www.postgresql.org/docs/9.2/static/arrays.html#ARRAYS-IO
+ # for a list of all cases in which strings will be quoted.
+ def string_requires_quoting?(string)
+ string.empty? ||
+ string == "NULL" ||
+ string =~ /[\{\}"\\\s]/ ||
+ string.include?(delimiter)
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb
index 3073f8ff30..1dbb40ca1d 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb
@@ -2,7 +2,7 @@ module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module OID # :nodoc:
- class Bit < Type::Value
+ class Bit < Type::Value # :nodoc:
def type
:bit
end
@@ -19,6 +19,32 @@ module ActiveRecord
value
end
end
+
+ def type_cast_for_database(value)
+ Data.new(super) if value
+ end
+
+ class Data
+ def initialize(value)
+ @value = value
+ end
+
+ def to_s
+ value
+ end
+
+ def binary?
+ /\A[01]*\Z/ === value
+ end
+
+ def hex?
+ /\A[0-9A-F]*\Z/i === value
+ end
+
+ protected
+
+ attr_reader :value
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit_varying.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit_varying.rb
index 054af285bb..4c21097d48 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit_varying.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit_varying.rb
@@ -2,7 +2,7 @@ module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module OID # :nodoc:
- class BitVarying < OID::Bit
+ class BitVarying < OID::Bit # :nodoc:
def type
:bit_varying
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb
index 36c53d8732..997613d7be 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb
@@ -2,9 +2,10 @@ module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module OID # :nodoc:
- class Bytea < Type::Binary
- def cast_value(value)
- PGconn.unescape_bytea value
+ class Bytea < Type::Binary # :nodoc:
+ def type_cast_from_database(value)
+ return if value.nil?
+ PGconn.unescape_bytea(super)
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb
index 534961a414..a53b4ee8e2 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb
@@ -2,7 +2,7 @@ module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module OID # :nodoc:
- class Cidr < Type::Value
+ class Cidr < Type::Value # :nodoc:
def type
:cidr
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb
index 3c30ad5fec..1d8d264530 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb
@@ -2,7 +2,7 @@ module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module OID # :nodoc:
- class Date < Type::Date
+ class Date < Type::Date # :nodoc:
include Infinity
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 34e2276dd1..b9e7894e5c 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
@@ -2,7 +2,7 @@ module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module OID # :nodoc:
- class DateTime < Type::DateTime
+ class DateTime < Type::DateTime # :nodoc:
include Infinity
def cast_value(value)
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/decimal.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/decimal.rb
index ed4b8911d9..43d22c8daf 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/decimal.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/decimal.rb
@@ -2,7 +2,7 @@ module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module OID # :nodoc:
- class Decimal < Type::Decimal
+ class Decimal < Type::Decimal # :nodoc:
def infinity(options = {})
BigDecimal.new("Infinity") * (options[:negative] ? -1 : 1)
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb
index 5fed8b0f89..77d5038efd 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb
@@ -2,7 +2,7 @@ module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module OID # :nodoc:
- class Enum < Type::Value
+ class Enum < Type::Value # :nodoc:
def type
:enum
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/float.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/float.rb
index 77dd97e140..78ef94b912 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/float.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/float.rb
@@ -2,11 +2,12 @@ module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module OID # :nodoc:
- class Float < Type::Float
+ 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
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 88de816d4f..be4525c94f 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb
@@ -2,26 +2,56 @@ module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module OID # :nodoc:
- class Hstore < Type::Value
+ class Hstore < Type::Value # :nodoc:
+ include Type::Mutable
+
def type
:hstore
end
- def type_cast_from_user(value)
- type_cast_from_database(type_cast_for_database(value))
- end
-
def type_cast_from_database(value)
- ConnectionAdapters::PostgreSQLColumn.string_to_hstore(value)
+ if value.is_a?(::String)
+ ::Hash[value.scan(HstorePair).map { |k, v|
+ v = v.upcase == 'NULL' ? nil : v.gsub(/\A"(.*)"\Z/m,'\1').gsub(/\\(.)/, '\1')
+ k = k.gsub(/\A"(.*)"\Z/m,'\1').gsub(/\\(.)/, '\1')
+ [k, v]
+ }]
+ else
+ value
+ end
end
def type_cast_for_database(value)
- ConnectionAdapters::PostgreSQLColumn.hstore_to_string(value)
+ if value.is_a?(::Hash)
+ value.map { |k, v| "#{escape_hstore(k)}=>#{escape_hstore(v)}" }.join(', ')
+ else
+ value
+ end
end
def accessor
ActiveRecord::Store::StringKeyedHashAccessor
end
+
+ private
+
+ HstorePair = begin
+ quoted_string = /"[^"\\]*(?:\\.[^"\\]*)*"/
+ unquoted_string = /(?:\\.|[^\s,])[^\s=,\\]*(?:\\.[^\s=,\\]*|=[^,>])*/
+ /(#{quoted_string}|#{unquoted_string})\s*=>\s*(#{quoted_string}|#{unquoted_string})/
+ end
+
+ def escape_hstore(value)
+ if value.nil?
+ 'NULL'
+ else
+ if value == ""
+ '""'
+ else
+ '"%s"' % value.to_s.gsub(/(["\\])/, '\\\\\1')
+ end
+ end
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/inet.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/inet.rb
index 7ed8f5f031..96486fa65b 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/inet.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/inet.rb
@@ -2,7 +2,7 @@ module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module OID # :nodoc:
- class Inet < Cidr
+ class Inet < Cidr # :nodoc:
def type
:inet
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/infinity.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/infinity.rb
index d438ffa140..e47780399a 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/infinity.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/infinity.rb
@@ -2,7 +2,7 @@ module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module OID # :nodoc:
- module Infinity
+ module Infinity # :nodoc:
def infinity(options = {})
options[:negative] ? -::Float::INFINITY : ::Float::INFINITY
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
index 388d3dd9ed..59abdc0009 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/integer.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/integer.rb
@@ -2,7 +2,7 @@ module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module OID # :nodoc:
- class Integer < Type::Integer
+ class Integer < Type::Integer # :nodoc:
include Infinity
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 b4fed1bcab..e12ddd9901 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb
@@ -2,21 +2,27 @@ module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module OID # :nodoc:
- class Json < Type::Value
+ class Json < Type::Value # :nodoc:
+ include Type::Mutable
+
def type
:json
end
- def type_cast_from_user(value)
- type_cast_from_database(type_cast_for_database(value))
- end
-
def type_cast_from_database(value)
- ConnectionAdapters::PostgreSQLColumn.string_to_json(value)
+ if value.is_a?(::String)
+ ::ActiveSupport::JSON.decode(value)
+ else
+ super
+ end
end
def type_cast_for_database(value)
- ConnectionAdapters::PostgreSQLColumn.json_to_string(value)
+ if value.is_a?(::Array) || value.is_a?(::Hash)
+ ::ActiveSupport::JSON.encode(value)
+ else
+ super
+ end
end
def accessor
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 d25eb256c2..df890c2ed6 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb
@@ -2,7 +2,7 @@ module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module OID # :nodoc:
- class Money < Type::Decimal
+ class Money < Type::Decimal # :nodoc:
include Infinity
class_attribute :precision
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 9007bfb178..bac8b01d6b 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb
@@ -2,21 +2,40 @@ module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module OID # :nodoc:
- class Point < Type::Value
+ class Point < Type::Value # :nodoc:
+ include Type::Mutable
+
def type
:point
end
def type_cast(value)
- if ::String === value
+ case value
+ when ::String
if value[0] == '(' && value[-1] == ')'
value = value[1...-1]
end
- value.split(',').map{ |v| Float(v) }
+ type_cast(value.split(','))
+ when ::Array
+ value.map { |v| Float(v) }
else
value
end
end
+
+ def type_cast_for_database(value)
+ if value.is_a?(::Array)
+ "(#{number_for_point(value[0])},#{number_for_point(value[1])})"
+ else
+ super
+ end
+ end
+
+ private
+
+ def number_for_point(number)
+ number.to_s.gsub(/\.0$/, '')
+ end
end
end
end
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 c289ba8980..ae967d5167 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb
@@ -2,7 +2,7 @@ module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module OID # :nodoc:
- class Range < Type::Value
+ class Range < Type::Value # :nodoc:
attr_reader :subtype, :type
def initialize(subtype, type)
@@ -10,28 +10,10 @@ module ActiveRecord
@type = type
end
- def extract_bounds(value)
- from, to = value[1..-2].split(',')
- {
- from: (value[1] == ',' || from == '-infinity') ? @subtype.infinity(negative: true) : from,
- to: (value[-2] == ',' || to == 'infinity') ? @subtype.infinity : to,
- exclude_start: (value[0] == '('),
- exclude_end: (value[-1] == ')')
- }
- end
-
- def infinity?(value)
- value.respond_to?(:infinite?) && value.infinite?
- end
-
def type_cast_for_schema(value)
value.inspect.gsub('Infinity', '::Float::INFINITY')
end
- def type_cast_single(value)
- infinity?(value) ? value : @subtype.type_cast_from_database(value)
- end
-
def cast_value(value)
return if value == 'empty'
return value if value.is_a?(::Range)
@@ -53,6 +35,40 @@ This is not reliable and will be removed in the future.
end
::Range.new(from, to, extracted[:exclude_end])
end
+
+ def type_cast_for_database(value)
+ if value.is_a?(::Range)
+ from = type_cast_single_for_database(value.begin)
+ to = type_cast_single_for_database(value.end)
+ "[#{from},#{to}#{value.exclude_end? ? ')' : ']'}"
+ else
+ super
+ end
+ end
+
+ private
+
+ def type_cast_single(value)
+ infinity?(value) ? value : @subtype.type_cast_from_database(value)
+ end
+
+ def type_cast_single_for_database(value)
+ infinity?(value) ? '' : @subtype.type_cast_for_database(value)
+ end
+
+ def extract_bounds(value)
+ from, to = value[1..-2].split(',')
+ {
+ from: (value[1] == ',' || from == '-infinity') ? @subtype.infinity(negative: true) : from,
+ to: (value[-2] == ',' || to == 'infinity') ? @subtype.infinity : to,
+ exclude_start: (value[0] == '('),
+ exclude_end: (value[-1] == ')')
+ }
+ end
+
+ def infinity?(value)
+ value.respond_to?(:infinite?) && value.infinite?
+ end
end
end
end
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 7b1ca16bc4..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
@@ -2,16 +2,12 @@ module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module OID # :nodoc:
- class SpecializedString < Type::String
+ class SpecializedString < Type::String # :nodoc:
attr_reader :type
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
index ea1f599b0f..8f0246eddb 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/time.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/time.rb
@@ -2,7 +2,7 @@ module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module OID # :nodoc:
- class Time < Type::Time
+ class Time < Type::Time # :nodoc:
include Infinity
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb
index 28f7a4eafb..e396ff4a1e 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb
@@ -40,7 +40,7 @@ module ActiveRecord
def register_array_type(row)
if subtype = @store.lookup(row['typelem'].to_i)
- register row['oid'], OID::Array.new(subtype)
+ register row['oid'], OID::Array.new(subtype, row['typdelim'])
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb
index 0ed5491887..89728b0fe2 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb
@@ -2,7 +2,7 @@ module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module OID # :nodoc:
- class Uuid < Type::Value
+ class Uuid < Type::Value # :nodoc:
def type
:uuid
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/vector.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/vector.rb
index 2f7d1be197..de4187b028 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/vector.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/vector.rb
@@ -2,7 +2,7 @@ module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module OID # :nodoc:
- class Vector < Type::Value
+ class Vector < Type::Value # :nodoc:
attr_reader :delim, :subtype
# +delim+ corresponds to the `typdelim` column in the pg_types
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/xml.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/xml.rb
new file mode 100644
index 0000000000..7323f12763
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/xml.rb
@@ -0,0 +1,32 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Xml < Type::String # :nodoc:
+ def type
+ :xml
+ end
+
+ def text?
+ false
+ end
+
+ def type_cast_for_database(value)
+ return unless value
+ Data.new(super)
+ end
+
+ class Data # :nodoc:
+ def initialize(value)
+ @value = value
+ end
+
+ def to_s
+ @value
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
index 3cf40e6cd4..cf5c8d288e 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
@@ -18,103 +18,18 @@ module ActiveRecord
def quote(value, column = nil) #:nodoc:
return super unless column
- sql_type = type_to_sql(column.type, column.limit, column.precision, column.scale)
-
case value
- when Range
- if /range$/ =~ sql_type
- "'#{PostgreSQLColumn.range_to_string(value)}'::#{sql_type}"
- else
- super
- end
- when Array
- case sql_type
- when 'point' then super(PostgreSQLColumn.point_to_string(value))
- when 'json' then super(PostgreSQLColumn.json_to_string(value))
- else
- if column.array
- "'#{PostgreSQLColumn.array_to_string(value, column, self).gsub(/'/, "''")}'"
- else
- super
- end
- end
- when Hash
- case sql_type
- when 'hstore' then super(PostgreSQLColumn.hstore_to_string(value), column)
- when 'json' then super(PostgreSQLColumn.json_to_string(value), column)
- else super
- end
when Float
- if value.infinite? && column.type == :datetime
- "'#{value.to_s.downcase}'"
- elsif value.infinite? || value.nan?
+ if value.infinite? || value.nan?
"'#{value.to_s}'"
else
super
end
- when Numeric
- if sql_type == 'money' || [:string, :text].include?(column.type)
- # Not truly string input, so doesn't require (or allow) escape string syntax.
- "'#{value}'"
- else
- super
- end
- when String
- case sql_type
- when 'xml' then "xml '#{quote_string(value)}'"
- when /^bit/
- case value
- when /^[01]*$/ then "B'#{value}'" # Bit-string notation
- when /^[0-9A-F]*$/i then "X'#{value}'" # Hexadecimal notation
- end
- else
- super
- end
else
super
end
end
- def type_cast(value, column, array_member = false)
- return super(value, column) unless column
-
- case value
- when Range
- if /range$/ =~ column.sql_type
- PostgreSQLColumn.range_to_string(value)
- else
- super(value, column)
- end
- when NilClass
- if column.array && array_member
- 'NULL'
- elsif column.array
- value
- else
- super(value, column)
- end
- when Array
- case column.sql_type
- when 'point' then PostgreSQLColumn.point_to_string(value)
- when 'json' then PostgreSQLColumn.json_to_string(value)
- else
- if column.array
- PostgreSQLColumn.array_to_string(value, column, self)
- else
- super(value, column)
- end
- end
- when Hash
- case column.sql_type
- when 'hstore' then PostgreSQLColumn.hstore_to_string(value, array_member)
- when 'json' then PostgreSQLColumn.json_to_string(value)
- else super(value, column)
- end
- else
- super(value, column)
- end
- end
-
# Quotes strings for use in SQL input.
def quote_string(s) #:nodoc:
@connection.escape(s)
@@ -168,19 +83,31 @@ module ActiveRecord
private
def _quote(value)
- if value.is_a?(Type::Binary::Data)
+ case value
+ when Type::Binary::Data
"'#{escape_bytea(value.to_s)}'"
+ when OID::Xml::Data
+ "xml '#{quote_string(value.to_s)}'"
+ when OID::Bit::Data
+ if value.binary?
+ "B'#{value}'"
+ elsif value.hex?
+ "X'#{value}'"
+ end
else
super
end
end
def _type_cast(value)
- if value.is_a?(Type::Binary::Data)
+ case value
+ when Type::Binary::Data
# Return a bind param hash with format as binary.
# See http://deveiate.org/code/pg/PGconn.html#method-i-exec_prepared-doc
# for more information
{ value: value.to_s, format: 1 }
+ when OID::Xml::Data, OID::Bit::Data
+ value.to_s
else
super
end
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 b2aeb3a058..e09672d239 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
@@ -31,6 +31,14 @@ module ActiveRecord
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
@@ -375,8 +383,8 @@ module ActiveRecord
end
# Renames a table.
- # Also renames a table's primary key sequence if the sequence name matches the
- # Active Record default.
+ # Also renames a table's primary key sequence if the sequence name exists and
+ # matches the Active Record default.
#
# Example:
# rename_table('octopuses', 'octopi')
@@ -384,7 +392,7 @@ module ActiveRecord
clear_cache!
execute "ALTER TABLE #{quote_table_name(table_name)} RENAME TO #{quote_table_name(new_name)}"
pk, seq = pk_and_sequence_for(new_name)
- if seq.identifier == "#{table_name}_#{pk}_seq"
+ if seq && seq.identifier == "#{table_name}_#{pk}_seq"
new_seq = "#{new_name}_#{pk}_seq"
execute "ALTER TABLE #{quote_table_name(seq)} RENAME TO #{quote_table_name(new_seq)}"
end
@@ -448,6 +456,42 @@ module ActiveRecord
execute "ALTER INDEX #{quote_column_name(old_name)} RENAME TO #{quote_table_name(new_name)}"
end
+ def foreign_keys(table_name)
+ fk_info = select_all <<-SQL.strip_heredoc
+ SELECT t2.relname AS to_table, a1.attname AS column, a2.attname AS primary_key, c.conname AS name, c.confupdtype AS on_update, c.confdeltype AS on_delete
+ FROM pg_constraint c
+ JOIN pg_class t1 ON c.conrelid = t1.oid
+ JOIN pg_class t2 ON c.confrelid = t2.oid
+ JOIN pg_attribute a1 ON a1.attnum = c.conkey[1] AND a1.attrelid = t1.oid
+ JOIN pg_attribute a2 ON a2.attnum = c.confkey[1] AND a2.attrelid = t2.oid
+ JOIN pg_namespace t3 ON c.connamespace = t3.oid
+ WHERE c.contype = 'f'
+ AND t1.relname = #{quote(table_name)}
+ AND t3.nspname = ANY (current_schemas(false))
+ ORDER BY c.conname
+ SQL
+
+ fk_info.map do |row|
+ options = {
+ column: row['column'],
+ name: row['name'],
+ primary_key: row['primary_key']
+ }
+
+ options[:on_delete] = extract_foreign_key_action(row['on_delete'])
+ options[:on_update] = extract_foreign_key_action(row['on_update'])
+ ForeignKeyDefinition.new(table_name, row['to_table'], options)
+ end
+ end
+
+ def extract_foreign_key_action(specifier) # :nodoc:
+ case specifier
+ when 'c'; :cascade
+ when 'n'; :nullify
+ when 'r'; :restrict
+ end
+ end
+
def index_name_length
63
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
index 71b05cdbae..f660fc41cf 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
@@ -159,6 +159,10 @@ module ActiveRecord
true
end
+ def supports_foreign_keys?
+ true
+ end
+
def index_algorithms
{ concurrently: 'CONCURRENTLY' }
end
@@ -306,10 +310,6 @@ module ActiveRecord
self.client_min_messages = old
end
- def supports_insert_with_returning?
- true
- end
-
def supports_ddl_transactions?
true
end
@@ -348,14 +348,13 @@ module ActiveRecord
if supports_extensions?
res = exec_query "SELECT EXISTS(SELECT * FROM pg_available_extensions WHERE name = '#{name}' AND installed_version IS NOT NULL) as enabled",
'SCHEMA'
- res.column_types['enabled'].type_cast_from_database res.rows.first.first
+ res.cast_values.first
end
end
def extensions
if supports_extensions?
- res = exec_query "SELECT extname from pg_extension", "SCHEMA"
- res.rows.map { |r| res.column_types['extname'].type_cast_from_database r.first }
+ exec_query("SELECT extname from pg_extension", "SCHEMA").cast_values
else
super
end
@@ -384,6 +383,11 @@ module ActiveRecord
PostgreSQL::Table.new(table_name, base)
end
+ def lookup_cast_type(sql_type) # :nodoc:
+ oid = execute("SELECT #{quote(sql_type)}::regtype::oid", "SCHEMA").first['oid'].to_i
+ super(oid)
+ end
+
protected
# Returns the version of the connected PostgreSQL server.
@@ -450,7 +454,7 @@ module ActiveRecord
m.register_type 'cidr', OID::Cidr.new
m.register_type 'inet', OID::Inet.new
m.register_type 'uuid', OID::Uuid.new
- m.register_type 'xml', OID::SpecializedString.new(:xml)
+ m.register_type 'xml', OID::Xml.new
m.register_type 'tsvector', OID::SpecializedString.new(:tsvector)
m.register_type 'macaddr', OID::SpecializedString.new(:macaddr)
m.register_type 'citext', OID::SpecializedString.new(:citext)
@@ -565,7 +569,7 @@ module ActiveRecord
end
def exec_no_cache(sql, name, binds)
- log(sql, name, binds) { @connection.async_exec(sql) }
+ log(sql, name, binds) { @connection.async_exec(sql, []) }
end
def exec_cache(sql, name, binds)
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
index e6163771e8..bf96acad4a 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
@@ -14,9 +14,9 @@ module ActiveRecord
raise ArgumentError, "No database file specified. Missing argument: database"
end
- # Allow database path relative to Rails.root, but only if
- # the database path is not the special path that tells
- # Sqlite to build a database only in memory.
+ # Allow database path relative to Rails.root, but only if the database
+ # path is not the special path that tells sqlite to build a database only
+ # in memory.
if ':memory:' != config[:database]
config[:database] = File.expand_path(config[:database], Rails.root) if defined?(Rails.root)
dirname = File.dirname(config[:database])
@@ -50,6 +50,16 @@ module ActiveRecord
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).
#
@@ -220,13 +230,23 @@ module ActiveRecord
# QUOTING ==================================================
def _quote(value) # :nodoc:
- if value.is_a?(Type::Binary::Data)
+ case value
+ when Type::Binary::Data
"x'#{value.hex}'"
else
super
end
end
+ def _type_cast(value) # :nodoc:
+ case value
+ when BigDecimal
+ value.to_f
+ else
+ super
+ end
+ end
+
def quote_string(s) #:nodoc:
@connection.class.quote(s)
end
@@ -249,19 +269,6 @@ module ActiveRecord
end
end
- def type_cast(value, column) # :nodoc:
- return value.to_f if BigDecimal === value
- return super unless String === value
- return super unless column && value
-
- value = super
- if column.type == :string && value.encoding == Encoding::ASCII_8BIT
- logger.error "Binary data inserted for `string` type on column `#{column.name}`" if logger
- value = value.encode Encoding::UTF_8
- end
- value
- end
-
# DATABASE STATEMENTS ======================================
def explain(arel, binds = [])
@@ -503,6 +510,7 @@ module ActiveRecord
def initialize_type_map(m)
super
m.register_type(/binary/i, SQLite3Binary.new)
+ register_class_with_limit m, %r(char)i, SQLite3String
end
def select(sql, name = nil, binds = []) #:nodoc:
diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb
index 79388f53b5..b11c4f804f 100644
--- a/activerecord/lib/active_record/core.rb
+++ b/activerecord/lib/active_record/core.rb
@@ -16,7 +16,6 @@ module ActiveRecord
mattr_accessor :logger, instance_writer: false
##
- # :singleton-method:
# Contains the database configuration - as is typically stored in config/database.yml -
# as a Hash.
#
@@ -249,16 +248,12 @@ module ActiveRecord
# # Instantiates a single new object
# User.new(first_name: 'Jamie')
def initialize(attributes = nil, options = {})
- defaults = self.class.raw_column_defaults.dup
- defaults.each { |k, v| defaults[k] = v.dup if v.duplicable? }
-
- @raw_attributes = defaults
- @column_types_override = nil
- @column_types = self.class.column_types
+ @attributes = self.class.default_attributes.dup
init_internals
initialize_internals_callback
+ self.class.define_attribute_methods
# +options+ argument is only needed to make protected_attributes gem easier to hook.
# Remove it when we drop support to this gem.
init_attributes(attributes, options) if attributes
@@ -278,13 +273,10 @@ module ActiveRecord
# post.init_with('attributes' => { 'title' => 'hello world' })
# post.title # => 'hello world'
def init_with(coder)
- @raw_attributes = coder['raw_attributes']
- @column_types_override = coder['column_types']
- @column_types = self.class.column_types
+ @attributes = coder['attributes']
init_internals
- @attributes = coder['attributes'] if coder['attributes']
@new_record = coder['new_record']
self.class.define_attribute_methods
@@ -323,12 +315,8 @@ module ActiveRecord
##
def initialize_dup(other) # :nodoc:
- cloned_attributes = other.clone_attributes(:read_attribute_before_type_cast)
-
- @raw_attributes = cloned_attributes
- @raw_attributes[self.class.primary_key] = nil
- @attributes = other.clone_attributes(:read_attribute)
- @attributes[self.class.primary_key] = nil
+ @attributes = @attributes.dup
+ @attributes.reset(self.class.primary_key)
run_callbacks(:initialize) unless _initialize_callbacks.empty?
@@ -354,7 +342,8 @@ module ActiveRecord
# Post.new.encode_with(coder)
# coder # => {"attributes" => {"id" => nil, ... }}
def encode_with(coder)
- coder['raw_attributes'] = @raw_attributes
+ # FIXME: Remove this when we better serialize attributes
+ coder['raw_attributes'] = attributes_before_type_cast
coder['attributes'] = @attributes
coder['new_record'] = new_record?
end
@@ -379,20 +368,24 @@ module ActiveRecord
# Delegates to id in order to allow two records of the same type and id to work with something like:
# [ Person.find(1), Person.find(2), Person.find(3) ] & [ Person.find(1), Person.find(4) ] # => [ Person.find(1) ]
def hash
- id.hash
+ if id
+ id.hash
+ else
+ super
+ end
end
# Clone and freeze the attributes hash such that associations are still
# accessible, even on destroyed records, but cloned models will not be
# frozen.
def freeze
- @raw_attributes = @raw_attributes.clone.freeze
+ @attributes = @attributes.clone.freeze
self
end
# Returns +true+ if the attributes hash has been frozen.
def frozen?
- @raw_attributes.frozen?
+ @attributes.frozen?
end
# Allows sort on objects
@@ -421,9 +414,9 @@ module ActiveRecord
# Returns the contents of the record as a nicely formatted string.
def inspect
- # We check defined?(@raw_attributes) not to issue warnings if the object is
+ # We check defined?(@attributes) not to issue warnings if the object is
# allocated but not initialized.
- inspection = if defined?(@raw_attributes) && @raw_attributes
+ inspection = if defined?(@attributes) && @attributes
self.class.column_names.collect { |name|
if has_attribute?(name)
"#{name}: #{attribute_for_inspect(name)}"
@@ -521,12 +514,10 @@ module ActiveRecord
end
def init_internals
- pk = self.class.primary_key
- @raw_attributes[pk] = nil unless @raw_attributes.key?(pk)
+ @attributes.ensure_initialized(self.class.primary_key)
@aggregation_cache = {}
@association_cache = {}
- @attributes = {}
@readonly = false
@destroyed = false
@marked_for_destruction = false
@@ -549,7 +540,7 @@ module ActiveRecord
def thaw
if frozen?
- @raw_attributes = @raw_attributes.dup
+ @attributes = @attributes.dup
end
end
end
diff --git a/activerecord/lib/active_record/counter_cache.rb b/activerecord/lib/active_record/counter_cache.rb
index 05c4b13016..a33c7c64a7 100644
--- a/activerecord/lib/active_record/counter_cache.rb
+++ b/activerecord/lib/active_record/counter_cache.rb
@@ -38,7 +38,7 @@ module ActiveRecord
counter_name = reflection.counter_cache_column
stmt = unscoped.where(arel_table[primary_key].eq(object.id)).arel.compile_update({
- arel_table[counter_name] => object.send(counter_association).count
+ arel_table[counter_name] => object.send(counter_association).count(:all)
}, primary_key)
connection.update stmt
end
diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb
index 2ccb1b0702..52c70977ef 100644
--- a/activerecord/lib/active_record/errors.rb
+++ b/activerecord/lib/active_record/errors.rb
@@ -30,17 +30,18 @@ module ActiveRecord
class SerializationTypeMismatch < ActiveRecordError
end
- # Raised when adapter not specified on connection (or configuration file <tt>config/database.yml</tt>
- # misses adapter field).
+ # Raised when adapter not specified on connection (or configuration file
+ # +config/database.yml+ misses adapter field).
class AdapterNotSpecified < ActiveRecordError
end
- # Raised when Active Record cannot find database adapter specified in <tt>config/database.yml</tt> or programmatically.
+ # Raised when Active Record cannot find database adapter specified in
+ # +config/database.yml+ or programmatically.
class AdapterNotFound < ActiveRecordError
end
- # Raised when connection to the database could not been established (for example when <tt>connection=</tt>
- # is given a nil object).
+ # Raised when connection to the database could not been established (for
+ # example when +connection=+ is given a nil object).
class ConnectionNotEstablished < ActiveRecordError
end
@@ -82,19 +83,17 @@ module ActiveRecord
class InvalidForeignKey < WrappedDatabaseException
end
- # Raised when number of bind variables in statement given to <tt>:condition</tt> key (for example,
- # when using +find+ method)
- # does not match number of expected variables.
+ # Raised when number of bind variables in statement given to +:condition+ key
+ # (for example, when using +find+ method) does not match number of expected
+ # values supplied.
#
- # For example, in
+ # For example, when there are two placeholders with only one value supplied:
#
# Location.where("lat = ? AND lng = ?", 53.7362)
- #
- # two placeholders are given but only one variable to fill them.
class PreparedStatementInvalid < ActiveRecordError
end
- # Raised when a given database does not exist
+ # Raised when a given database does not exist.
class NoDatabaseError < StatementInvalid
end
@@ -102,7 +101,8 @@ module ActiveRecord
# instantiation, for example, when two users edit the same wiki page and one starts editing and saves
# the page before the other.
#
- # Read more about optimistic locking in ActiveRecord::Locking module RDoc.
+ # Read more about optimistic locking in ActiveRecord::Locking module
+ # documentation.
class StaleObjectError < ActiveRecordError
attr_reader :record, :attempted_action
@@ -114,8 +114,9 @@ module ActiveRecord
end
- # Raised when association is being configured improperly or
- # user tries to use offset and limit together with has_many or has_and_belongs_to_many associations.
+ # Raised when association is being configured improperly or user tries to use
+ # offset and limit together with +has_many+ or +has_and_belongs_to_many+
+ # associations.
class ConfigurationError < ActiveRecordError
end
@@ -153,7 +154,8 @@ module ActiveRecord
class Rollback < ActiveRecordError
end
- # Raised when attribute has a name reserved by Active Record (when attribute has name of one of Active Record instance methods).
+ # Raised when attribute has a name reserved by Active Record (when attribute
+ # has name of one of Active Record instance methods).
class DangerousAttributeError < ActiveRecordError
end
@@ -171,7 +173,7 @@ module ActiveRecord
end
# Raised when an error occurred while doing a mass assignment to an attribute through the
- # <tt>attributes=</tt> method. The exception has an +attribute+ property that is the name of the
+ # +attributes=+ method. The exception has an +attribute+ property that is the name of the
# offending attribute.
class AttributeAssignmentError < ActiveRecordError
attr_reader :exception, :attribute
diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb
index 6ba667b996..4cd5f92207 100644
--- a/activerecord/lib/active_record/fixtures.rb
+++ b/activerecord/lib/active_record/fixtures.rb
@@ -15,9 +15,10 @@ module ActiveRecord
# They are stored in YAML files, one file per model, which are placed in the directory
# appointed by <tt>ActiveSupport::TestCase.fixture_path=(path)</tt> (this is automatically
# configured for Rails, so you can just put your files in <tt><your-rails-app>/test/fixtures/</tt>).
- # The fixture file ends with the <tt>.yml</tt> file extension (Rails example:
- # <tt><your-rails-app>/test/fixtures/web_sites.yml</tt>). The format of a fixture file looks
- # like this:
+ # The fixture file ends with the +.yml+ file extension, for example:
+ # <tt><your-rails-app>/test/fixtures/web_sites.yml</tt>).
+ #
+ # The format of a fixture file looks like this:
#
# rubyonrails:
# id: 1
@@ -61,8 +62,8 @@ module ActiveRecord
# end
# end
#
- # By default, <tt>test_helper.rb</tt> will load all of your fixtures into your test database,
- # so this test will succeed.
+ # By default, +test_helper.rb+ will load all of your fixtures into your test
+ # database, so this test will succeed.
#
# The testing environment will automatically load the all fixtures into the database before each
# test. To ensure consistent data, the environment deletes the fixtures before running the load.
@@ -375,8 +376,8 @@ module ActiveRecord
# == Support for YAML defaults
#
# You can set and reuse defaults in your fixtures YAML file.
- # This is the same technique used in the <tt>database.yml</tt> file
- # to specify defaults:
+ # This is the same technique used in the +database.yml+ file to specify
+ # defaults:
#
# DEFAULTS: &DEFAULTS
# created_on: <%= 3.weeks.ago.to_s(:db) %>
@@ -392,7 +393,8 @@ module ActiveRecord
# Any fixture labeled "DEFAULTS" is safely ignored.
class FixtureSet
#--
- # An instance of FixtureSet is normally stored in a single YAML file and possibly in a folder with the same name.
+ # An instance of FixtureSet is normally stored in a single YAML file and
+ # possibly in a folder with the same name.
#++
MAX_ID = 2 ** 30 - 1
@@ -462,13 +464,7 @@ module ActiveRecord
@config = config
# Remove string values that aren't constants or subclasses of AR
- @class_names.delete_if { |k,klass|
- unless klass.is_a? Class
- klass = klass.safe_constantize
- ActiveSupport::Deprecation.warn("The ability to pass in strings as a class name to `set_fixture_class` will be removed in Rails 4.2. Use the class itself instead.")
- end
- !insert_class(@class_names, k, klass)
- }
+ @class_names.delete_if { |klass_name, klass| !insert_class(@class_names, klass_name, klass) }
end
def [](fs_name)
@@ -574,10 +570,6 @@ module ActiveRecord
@config = config
@model_class = nil
- if class_name.is_a?(String)
- ActiveSupport::Deprecation.warn("The ability to pass in strings as a class name to `FixtureSet.new` will be removed in Rails 4.2. Use the class itself instead.")
- end
-
if class_name.is_a?(Class) # TODO: Should be an AR::Base type class, or any?
@model_class = class_name
else
diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb
index 08fc91c9df..f6c265a6d6 100644
--- a/activerecord/lib/active_record/inheritance.rb
+++ b/activerecord/lib/active_record/inheritance.rb
@@ -1,6 +1,37 @@
require 'active_support/core_ext/hash/indifferent_access'
module ActiveRecord
+ # == Single table inheritance
+ #
+ # Active Record allows inheritance by storing the name of the class in a column that by
+ # default is named "type" (can be changed by overwriting <tt>Base.inheritance_column</tt>).
+ # This means that an inheritance looking like this:
+ #
+ # class Company < ActiveRecord::Base; end
+ # class Firm < Company; end
+ # class Client < Company; end
+ # class PriorityClient < Client; end
+ #
+ # When you do <tt>Firm.create(name: "37signals")</tt>, this record will be saved in
+ # the companies table with type = "Firm". You can then fetch this row again using
+ # <tt>Company.where(name: '37signals').first</tt> and it will return a Firm object.
+ #
+ # Be aware that because the type column is an attribute on the record every new
+ # subclass will instantly be marked as dirty and the type column will be included
+ # in the list of changed attributes on the record. This is different from non
+ # STI classes:
+ #
+ # Company.new.changed? # => false
+ # Firm.new.changed? # => true
+ # Firm.new.changes # => {"type"=>["","Firm"]}
+ #
+ # If you don't have a type column defined in your table, single-table inheritance won't
+ # be triggered. In that case, it'll work just like normal subclasses with no special magic
+ # for differentiating between them or reloading the right type with find.
+ #
+ # Note, all the attributes for all the cases are kept in the same table. Read more:
+ # http://www.martinfowler.com/eaaCatalog/singleTableInheritance.html
+ #
module Inheritance
extend ActiveSupport::Concern
diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb
index f7ceff7469..52eeb8ae1f 100644
--- a/activerecord/lib/active_record/locking/optimistic.rb
+++ b/activerecord/lib/active_record/locking/optimistic.rb
@@ -141,7 +141,7 @@ module ActiveRecord
# Set the column to use for optimistic locking. Defaults to +lock_version+.
def locking_column=(value)
- @column_defaults = nil
+ clear_caches_calculated_from_columns
@locking_column = value.to_s
end
@@ -151,12 +151,6 @@ module ActiveRecord
@locking_column
end
- # Quote the column name used for optimistic locking.
- def quoted_locking_column
- ActiveSupport::Deprecation.warn "ActiveRecord::Base.quoted_locking_column is deprecated and will be removed in Rails 4.2 or later."
- connection.quote_column_name(locking_column)
- end
-
# Reset the column used for optimistic locking back to the +lock_version+ default.
def reset_locking_column
self.locking_column = DEFAULT_LOCKING_COLUMN
@@ -169,18 +163,42 @@ module ActiveRecord
super
end
- def column_defaults
- @column_defaults ||= begin
- defaults = super
-
- if defaults.key?(locking_column) && lock_optimistically
- defaults[locking_column] ||= 0
+ private
+
+ # We need to apply this decorator here, rather than on module inclusion. The closure
+ # created by the matcher would otherwise evaluate for `ActiveRecord::Base`, not the
+ # sub class being decorated. As such, changes to `lock_optimistically`, or
+ # `locking_column` would not be picked up.
+ def inherited(subclass)
+ subclass.class_eval do
+ is_lock_column = ->(name, _) { lock_optimistically && name == locking_column }
+ decorate_matching_attribute_types(is_lock_column, :_optimistic_locking) do |type|
+ LockingType.new(type)
end
-
- defaults
end
+ super
end
end
end
+
+ class LockingType < SimpleDelegator # :nodoc:
+ def type_cast_from_database(value)
+ # `nil` *should* be changed to 0
+ super.to_i
+ end
+
+ def changed?(old_value, *)
+ # Ensure we save if the default was `nil`
+ super || old_value == 0
+ end
+
+ def init_with(coder)
+ __setobj__(coder['subtype'])
+ end
+
+ def encode_with(coder)
+ coder['subtype'] = __getobj__
+ end
+ end
end
end
diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb
index 481e5c17e4..e94b6ae9eb 100644
--- a/activerecord/lib/active_record/migration.rb
+++ b/activerecord/lib/active_record/migration.rb
@@ -372,13 +372,21 @@ module ActiveRecord
end
def call(env)
- mtime = ActiveRecord::Migrator.last_migration.mtime.to_i
- if @last_check < mtime
- ActiveRecord::Migration.check_pending!
- @last_check = mtime
+ if connection.supports_migrations?
+ mtime = ActiveRecord::Migrator.last_migration.mtime.to_i
+ if @last_check < mtime
+ ActiveRecord::Migration.check_pending!(connection)
+ @last_check = mtime
+ end
end
@app.call(env)
end
+
+ private
+
+ def connection
+ ActiveRecord::Base.connection
+ end
end
class << self
@@ -642,7 +650,9 @@ module ActiveRecord
unless @connection.respond_to? :revert
unless arguments.empty? || [:execute, :enable_extension, :disable_extension].include?(method)
arguments[0] = proper_table_name(arguments.first, table_name_options)
- arguments[1] = proper_table_name(arguments.second, table_name_options) if method == :rename_table
+ if [:rename_table, :add_foreign_key].include?(method)
+ arguments[1] = proper_table_name(arguments.second, table_name_options)
+ end
end
end
return super unless connection.respond_to?(method)
diff --git a/activerecord/lib/active_record/migration/command_recorder.rb b/activerecord/lib/active_record/migration/command_recorder.rb
index c44d8c1665..f833caaab6 100644
--- a/activerecord/lib/active_record/migration/command_recorder.rb
+++ b/activerecord/lib/active_record/migration/command_recorder.rb
@@ -74,7 +74,9 @@ module ActiveRecord
:rename_index, :rename_column, :add_index, :remove_index, :add_timestamps, :remove_timestamps,
:change_column_default, :add_reference, :remove_reference, :transaction,
:drop_join_table, :drop_table, :execute_block, :enable_extension,
- :change_column, :execute, :remove_columns, :change_column_null # irreversible methods need to be here too
+ :change_column, :execute, :remove_columns, :change_column_null,
+ :add_foreign_key, :remove_foreign_key
+ # irreversible methods need to be here too
].each do |method|
class_eval <<-EOV, __FILE__, __LINE__ + 1
def #{method}(*args, &block) # def create_table(*args, &block)
@@ -167,6 +169,21 @@ module ActiveRecord
[:change_column_null, args]
end
+ def invert_add_foreign_key(args)
+ from_table, to_table, add_options = args
+ add_options ||= {}
+
+ if add_options[:name]
+ options = { name: add_options[:name] }
+ elsif add_options[:column]
+ options = { column: add_options[:column] }
+ else
+ options = to_table
+ end
+
+ [:remove_foreign_key, [from_table, options]]
+ end
+
# Forwards any missing method call to the \target.
def method_missing(method, *args, &block)
if @delegate.respond_to?(method)
diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb
index f96f77f696..092c3b4fb7 100644
--- a/activerecord/lib/active_record/model_schema.rb
+++ b/activerecord/lib/active_record/model_schema.rb
@@ -219,40 +219,29 @@ module ActiveRecord
connection.schema_cache.table_exists?(table_name)
end
- def column_types # :nodoc:
- @column_types ||= decorate_columns(columns_hash.dup)
+ def attributes_builder # :nodoc:
+ @attributes_builder ||= AttributeSet::Builder.new(column_types)
end
- def type_for_attribute(attr_name) # :nodoc:
- column_types.fetch(attr_name) { column_for_attribute(attr_name) }
- end
-
- def decorate_columns(columns_hash) # :nodoc:
- return if columns_hash.empty?
-
- @time_zone_column_names ||= self.columns_hash.find_all do |name, col|
- create_time_zone_conversion_attribute?(name, col)
- end.map!(&:first)
-
- @time_zone_column_names.each do |name|
- columns_hash[name] = AttributeMethods::TimeZoneConversion::Type.new(columns_hash[name])
+ def column_types # :nodoc:
+ @column_types ||= columns_hash.transform_values(&:cast_type).tap do |h|
+ h.default = Type::Value.new
end
+ end
- columns_hash
+ def type_for_attribute(attr_name) # :nodoc:
+ column_types[attr_name]
end
# Returns a hash where the keys are column names and the values are
# default values when instantiating the AR object for this table.
def column_defaults
- @column_defaults ||= Hash[columns.map { |c| [c.name, c.default] }]
+ default_attributes.to_hash
end
- # Returns a hash where the keys are the column names and the values
- # are the default values suitable for use in `@raw_attriubtes`
- def raw_column_defaults # :nodoc:
- @raw_column_defauts ||= Hash[column_defaults.map { |name, default|
- [name, columns_hash[name].type_cast_for_database(default)]
- }]
+ def default_attributes # :nodoc:
+ @default_attributes ||= attributes_builder.build_from_database(
+ columns_hash.transform_values(&:default))
end
# Returns an array of column names as strings.
@@ -298,11 +287,10 @@ module ActiveRecord
connection.schema_cache.clear_table_cache!(table_name) if table_exists?
@arel_engine = nil
- @column_defaults = nil
- @raw_column_defauts = nil
@column_names = nil
@column_types = nil
@content_columns = nil
+ @default_attributes = nil
@dynamic_methods_hash = nil
@inheritance_column = nil unless defined?(@explicit_inheritance_column) && @explicit_inheritance_column
@relation = nil
diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb
index 525289c270..96e44c2f59 100644
--- a/activerecord/lib/active_record/persistence.rb
+++ b/activerecord/lib/active_record/persistence.rb
@@ -48,12 +48,8 @@ module ActiveRecord
# how this "single-table" inheritance mapping is implemented.
def instantiate(attributes, column_types = {})
klass = discriminate_class_for_record(attributes)
- column_types = klass.decorate_columns(column_types.dup)
- klass.allocate.init_with(
- 'raw_attributes' => attributes,
- 'column_types' => column_types,
- 'new_record' => false,
- )
+ attributes = klass.attributes_builder.build_from_database(attributes, column_types)
+ klass.allocate.init_with('attributes' => attributes, 'new_record' => false)
end
private
@@ -183,7 +179,6 @@ module ActiveRecord
# So any change to the attributes in either instance will affect the other.
def becomes(klass)
became = klass.new
- became.instance_variable_set("@raw_attributes", @raw_attributes)
became.instance_variable_set("@attributes", @attributes)
became.instance_variable_set("@changed_attributes", @changed_attributes) if defined?(@changed_attributes)
became.instance_variable_set("@new_record", new_record?)
@@ -400,11 +395,8 @@ module ActiveRecord
self.class.unscoped { self.class.find(id) }
end
- @raw_attributes.update(fresh_object.instance_variable_get('@raw_attributes'))
-
- @column_types = self.class.column_types
- @column_types_override = fresh_object.instance_variable_get('@column_types_override')
- @attributes = {}
+ @attributes = fresh_object.instance_variable_get('@attributes')
+ @new_record = false
self
end
diff --git a/activerecord/lib/active_record/query_cache.rb b/activerecord/lib/active_record/query_cache.rb
index df8654e5c1..16ad942912 100644
--- a/activerecord/lib/active_record/query_cache.rb
+++ b/activerecord/lib/active_record/query_cache.rb
@@ -29,9 +29,10 @@ module ActiveRecord
end
def call(env)
- enabled = ActiveRecord::Base.connection.query_cache_enabled
+ connection = ActiveRecord::Base.connection
+ enabled = connection.query_cache_enabled
connection_id = ActiveRecord::Base.connection_id
- ActiveRecord::Base.connection.enable_query_cache!
+ connection.enable_query_cache!
response = @app.call(env)
response[2] = Rack::BodyProxy.new(response[2]) do
diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb
index 1fe54cea3f..a9ddd9141f 100644
--- a/activerecord/lib/active_record/querying.rb
+++ b/activerecord/lib/active_record/querying.rb
@@ -37,14 +37,7 @@ module ActiveRecord
# Post.find_by_sql ["SELECT body FROM comments WHERE author = :user_id OR approved_by = :user_id", { :user_id => user_id }]
def find_by_sql(sql, binds = [])
result_set = connection.select_all(sanitize_sql(sql), "#{name} Load", binds)
- column_types = {}
-
- if result_set.respond_to? :column_types
- column_types = result_set.column_types.merge(columns_hash)
- else
- ActiveSupport::Deprecation.warn "the object returned from `select_all` must respond to `column_types`"
- end
-
+ column_types = result_set.column_types.except(*columns_hash.keys)
result_set.map { |record| instantiate(record, column_types) }
end
diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake
index 6dca206f2a..fa94df7a52 100644
--- a/activerecord/lib/active_record/railties/databases.rake
+++ b/activerecord/lib/active_record/railties/databases.rake
@@ -28,6 +28,17 @@ db_namespace = namespace :db do
ActiveRecord::Tasks::DatabaseTasks.drop_current
end
+ namespace :purge do
+ task :all => :load_config do
+ ActiveRecord::Tasks::DatabaseTasks.purge_all
+ end
+ end
+
+ # desc "Empty the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:drop:all to drop all databases in the config). Without RAILS_ENV it defaults to purging the development and test databases."
+ task :purge => [:load_config] do
+ ActiveRecord::Tasks::DatabaseTasks.purge_current
+ end
+
desc "Migrate the database (options: VERSION=x, VERBOSE=false, SCOPE=blog)."
task :migrate => [:environment, :load_config] do
ActiveRecord::Migration.verbose = ENV["VERBOSE"] ? ENV["VERBOSE"] == "true" : true
@@ -82,22 +93,21 @@ db_namespace = namespace :db do
desc 'Display status of migrations'
task :status => [:environment, :load_config] do
- unless ActiveRecord::Base.connection.table_exists?(ActiveRecord::Migrator.schema_migrations_table_name)
+ unless ActiveRecord::SchemaMigration.table_exists?
abort 'Schema migrations table does not exist yet.'
end
- db_list = ActiveRecord::Base.connection.select_values("SELECT version FROM #{ActiveRecord::Migrator.schema_migrations_table_name}")
- db_list.map! { |version| ActiveRecord::SchemaMigration.normalize_migration_number(version) }
- file_list = []
- ActiveRecord::Migrator.migrations_paths.each do |path|
- Dir.foreach(path) do |file|
- # match "20091231235959_some_name.rb" and "001_some_name.rb" pattern
- if match_data = /^(\d{3,})_(.+)\.rb$/.match(file)
- version = ActiveRecord::SchemaMigration.normalize_migration_number(match_data[1])
- status = db_list.delete(version) ? 'up' : 'down'
- file_list << [status, version, match_data[2].humanize]
+ db_list = ActiveRecord::SchemaMigration.normalized_versions
+
+ file_list =
+ ActiveRecord::Migrator.migrations_paths.flat_map do |path|
+ # match "20091231235959_some_name.rb" and "001_some_name.rb" pattern
+ Dir.foreach(path).grep(/^(\d{3,})_(.+)\.rb$/) do
+ version = ActiveRecord::SchemaMigration.normalize_migration_number($1)
+ status = db_list.delete(version) ? 'up' : 'down'
+ [status, version, $2.humanize]
+ end
end
- end
- end
+
db_list.map! do |version|
['up', version, '********** NO FILE **********']
end
@@ -105,8 +115,8 @@ db_namespace = namespace :db do
puts "\ndatabase: #{ActiveRecord::Base.connection_config[:database]}\n\n"
puts "#{'Status'.center(8)} #{'Migration ID'.ljust(14)} Migration Name"
puts "-" * 50
- (db_list + file_list).sort_by {|migration| migration[1]}.each do |migration|
- puts "#{migration[0].center(8)} #{migration[1].ljust(14)} #{migration[2]}"
+ (db_list + file_list).sort_by { |_, version, _| version }.each do |status, version, name|
+ puts "#{status.center(8)} #{version.ljust(14)} #{name}"
end
puts
end
@@ -178,17 +188,21 @@ db_namespace = namespace :db do
task :load => [:environment, :load_config] do
require 'active_record/fixtures'
- base_dir = if ENV['FIXTURES_PATH']
- File.join [Rails.root, ENV['FIXTURES_PATH'] || %w{test fixtures}].flatten
- else
- ActiveRecord::Tasks::DatabaseTasks.fixtures_path
- end
+ base_dir = ActiveRecord::Tasks::DatabaseTasks.fixtures_path
- fixtures_dir = File.join [base_dir, ENV['FIXTURES_DIR']].compact
+ fixtures_dir = if ENV['FIXTURES_DIR']
+ File.join base_dir, ENV['FIXTURES_DIR']
+ else
+ base_dir
+ end
- (ENV['FIXTURES'] ? ENV['FIXTURES'].split(',') : Dir["#{fixtures_dir}/**/*.yml"].map {|f| f[(fixtures_dir.size + 1)..-5] }).each do |fixture_file|
- ActiveRecord::FixtureSet.create_fixtures(fixtures_dir, fixture_file)
- end
+ fixture_files = if ENV['FIXTURES']
+ ENV['FIXTURES'].split(',')
+ else
+ Pathname.glob("#{fixtures_dir}/**/*.yml").map {|f| f.basename.sub_ext('').to_s }
+ end
+
+ ActiveRecord::FixtureSet.create_fixtures(fixtures_dir, fixture_files)
end
# desc "Search for a fixture given a LABEL or ID. Specify an alternative path (eg. spec/fixtures) using FIXTURES_PATH=spec/fixtures."
@@ -200,12 +214,7 @@ db_namespace = namespace :db do
puts %Q(The fixture ID for "#{label}" is #{ActiveRecord::FixtureSet.identify(label)}.) if label
- base_dir = if ENV['FIXTURES_PATH']
- File.join [Rails.root, ENV['FIXTURES_PATH'] || %w{test fixtures}].flatten
- else
- ActiveRecord::Tasks::DatabaseTasks.fixtures_path
- end
-
+ base_dir = ActiveRecord::Tasks::DatabaseTasks.fixtures_path
Dir["#{base_dir}/**/*.yml"].each do |file|
if data = YAML::load(ERB.new(IO.read(file)).result)
diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb
index 24f3de019d..12208aecf7 100644
--- a/activerecord/lib/active_record/reflection.rb
+++ b/activerecord/lib/active_record/reflection.rb
@@ -13,14 +13,21 @@ module ActiveRecord
end
def self.create(macro, name, scope, options, ar)
- case macro
- when :has_many, :belongs_to, :has_one
- klass = options[:through] ? ThroughReflection : AssociationReflection
- when :composed_of
- klass = AggregateReflection
- end
-
- klass.new(macro, name, scope, options, ar)
+ klass = case macro
+ when :composed_of
+ AggregateReflection
+ when :has_many
+ HasManyReflection
+ when :has_one
+ HasOneReflection
+ when :belongs_to
+ BelongsToReflection
+ else
+ raise "Unsupported Macro: #{macro}"
+ end
+
+ reflection = klass.new(name, scope, options, ar)
+ options[:through] ? ThroughReflection.new(reflection) : reflection
end
def self.add_reflection(ar, name, reflection)
@@ -110,6 +117,52 @@ module ActiveRecord
end
end
+ # Holds all the methods that are shared between MacroReflection, AssociationReflection
+ # and ThroughReflection
+ class AbstractReflection # :nodoc:
+ def table_name
+ klass.table_name
+ end
+
+ # Returns a new, unsaved instance of the associated class. +attributes+ will
+ # be passed to the class's constructor.
+ def build_association(attributes, &block)
+ klass.new(attributes, &block)
+ end
+
+ def quoted_table_name
+ klass.quoted_table_name
+ end
+
+ def primary_key_type
+ klass.type_for_attribute(klass.primary_key)
+ end
+
+ # Returns the class name for the macro.
+ #
+ # <tt>composed_of :balance, class_name: 'Money'</tt> returns <tt>'Money'</tt>
+ # <tt>has_many :clients</tt> returns <tt>'Client'</tt>
+ def class_name
+ @class_name ||= (options[:class_name] || derive_class_name).to_s
+ end
+
+ JoinKeys = Struct.new(:key, :foreign_key) # :nodoc:
+
+ def join_keys(assoc_klass)
+ if source_macro == :belongs_to
+ if polymorphic?
+ reflection_key = association_primary_key(assoc_klass)
+ else
+ reflection_key = association_primary_key
+ end
+ reflection_foreign_key = foreign_key
+ else
+ reflection_foreign_key = active_record_primary_key
+ reflection_key = foreign_key
+ end
+ JoinKeys.new(reflection_key, reflection_foreign_key)
+ end
+ end
# Base class for AggregateReflection and AssociationReflection. Objects of
# AggregateReflection and AssociationReflection are returned by the Reflection::ClassMethods.
#
@@ -117,7 +170,7 @@ module ActiveRecord
# AggregateReflection
# AssociationReflection
# ThroughReflection
- class MacroReflection
+ class MacroReflection < AbstractReflection
# Returns the name of the macro.
#
# <tt>composed_of :balance, class_name: 'Money'</tt> returns <tt>:balance</tt>
@@ -142,8 +195,7 @@ module ActiveRecord
attr_reader :plural_name # :nodoc:
- def initialize(macro, name, scope, options, active_record)
- @macro = macro
+ def initialize(name, scope, options, active_record)
@name = name
@scope = scope
@options = options
@@ -167,15 +219,11 @@ module ActiveRecord
# <tt>composed_of :balance, class_name: 'Money'</tt> returns the Money class
# <tt>has_many :clients</tt> returns the Client class
def klass
- @klass ||= class_name.constantize
+ @klass ||= compute_class(class_name)
end
- # Returns the class name for the macro.
- #
- # <tt>composed_of :balance, class_name: 'Money'</tt> returns <tt>'Money'</tt>
- # <tt>has_many :clients</tt> returns <tt>'Client'</tt>
- def class_name
- @class_name ||= (options[:class_name] || derive_class_name).to_s
+ def compute_class(name)
+ name.constantize
end
# Returns +true+ if +self+ and +other_aggregation+ have the same +name+ attribute, +active_record+ attribute,
@@ -220,15 +268,18 @@ module ActiveRecord
# a new association object. Use +build_association+ or +create_association+
# instead. This allows plugins to hook into association object creation.
def klass
- @klass ||= active_record.send(:compute_type, class_name)
+ @klass ||= compute_class(class_name)
+ end
+
+ def compute_class(name)
+ active_record.send(:compute_type, name)
end
attr_reader :type, :foreign_type
attr_accessor :parent_reflection # [:name, Reflection]
- def initialize(macro, name, scope, options, active_record)
+ def initialize(name, scope, options, active_record)
super
- @collection = macro == :has_many
@automatic_inverse_of = nil
@type = options[:as] && "#{options[:as]}_type"
@foreign_type = options[:foreign_type] || "#{name}_type"
@@ -247,24 +298,10 @@ module ActiveRecord
}
end
- # Returns a new, unsaved instance of the associated class. +attributes+ will
- # be passed to the class's constructor.
- def build_association(attributes, &block)
- klass.new(attributes, &block)
- end
-
def constructable? # :nodoc:
@constructable
end
- def table_name
- klass.table_name
- end
-
- def quoted_table_name
- klass.quoted_table_name
- end
-
def join_table
@join_table ||= options[:join_table] || derive_join_table
end
@@ -273,10 +310,6 @@ module ActiveRecord
@foreign_key ||= options[:foreign_key] || derive_foreign_key
end
- def primary_key_column
- klass.columns_hash[klass.primary_key]
- end
-
def association_foreign_key
@association_foreign_key ||= options[:association_foreign_key] || class_name.foreign_key
end
@@ -379,7 +412,7 @@ Joining, Preloading and eager loading of these associations is deprecated and wi
# association. Returns +true+ if the +macro+ is either +has_many+ or
# +has_and_belongs_to_many+, +false+ otherwise.
def collection?
- @collection
+ false
end
# Returns whether or not the association should be validated as part of
@@ -400,6 +433,7 @@ Joining, Preloading and eager loading of these associations is deprecated and wi
macro == :belongs_to
end
+ # Returns +true+ if +self+ is a +has_one+ reflection.
def has_one?
macro == :has_one
end
@@ -469,7 +503,7 @@ Joining, Preloading and eager loading of these associations is deprecated and wi
# returns either nil or the inverse association name that it finds.
def automatic_inverse_of
if can_find_inverse_of_automatically?(self)
- inverse_name = ActiveSupport::Inflector.underscore(active_record.name).to_sym
+ inverse_name = ActiveSupport::Inflector.underscore(options[:as] || active_record.name).to_sym
begin
reflection = klass._reflect_on_association(inverse_name)
@@ -542,22 +576,57 @@ Joining, Preloading and eager loading of these associations is deprecated and wi
end
end
+ class HasManyReflection < AssociationReflection #:nodoc:
+ def initialize(name, scope, options, active_record)
+ @macro = :has_many
+ super(name, scope, options, active_record)
+ end
+
+ def collection?
+ true
+ end
+ end
+
+ class HasOneReflection < AssociationReflection #:nodoc:
+ def initialize(name, scope, options, active_record)
+ @macro = :has_one
+ super(name, scope, options, active_record)
+ end
+ end
+
+ class BelongsToReflection < AssociationReflection #:nodoc:
+ def initialize(name, scope, options, active_record)
+ @macro = :belongs_to
+ super(name, scope, options, active_record)
+ end
+ end
+
class HasAndBelongsToManyReflection < AssociationReflection #:nodoc:
- def initialize(macro, name, scope, options, active_record)
+ def initialize(name, scope, options, active_record)
+ @macro = :has_and_belongs_to_many
super
- @collection = true
+ end
+
+ def collection?
+ true
end
end
# Holds all the meta-data about a :through association as it was specified
# in the Active Record class.
- class ThroughReflection < AssociationReflection #:nodoc:
+ class ThroughReflection < AbstractReflection #:nodoc:
+ attr_reader :delegate_reflection
delegate :foreign_key, :foreign_type, :association_foreign_key,
:active_record_primary_key, :type, :to => :source_reflection
- def initialize(macro, name, scope, options, active_record)
- super
- @source_reflection_name = options[:source]
+ def initialize(delegate_reflection)
+ @delegate_reflection = delegate_reflection
+ @klass = delegate_reflection.options[:class]
+ @source_reflection_name = delegate_reflection.options[:source]
+ end
+
+ def klass
+ @klass ||= delegate_reflection.compute_class(class_name)
end
# Returns the source of the through reflection. It checks both a singularized
@@ -759,15 +828,25 @@ directive on your declaration like:
protected
- def actual_source_reflection # FIXME: this is a horrible name
- source_reflection.actual_source_reflection
- end
+ def actual_source_reflection # FIXME: this is a horrible name
+ source_reflection.send(:actual_source_reflection)
+ end
+
+ def primary_key(klass)
+ klass.primary_key || raise(UnknownPrimaryKey.new(klass))
+ end
private
def derive_class_name
# get the class_name of the belongs_to association of the through reflection
options[:source_type] || source_reflection.class_name
end
+
+ delegate_methods = AssociationReflection.public_instance_methods -
+ public_instance_methods
+
+ delegate(*delegate_methods, to: :delegate_reflection)
+
end
end
end
diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb
index 38970a66ae..90e99957f6 100644
--- a/activerecord/lib/active_record/relation/calculations.rb
+++ b/activerecord/lib/active_record/relation/calculations.rb
@@ -19,14 +19,22 @@ module ActiveRecord
#
# Person.group(:city).count
# # => { 'Rome' => 5, 'Paris' => 3 }
- #
+ #
+ # If +count+ is used with +group+ for multiple columns, it returns a Hash whose
+ # keys are an array containing the individual values of each column and the value
+ # of each key would be the +count+.
+ #
+ # Article.group(:status, :category).count
+ # # => {["draft", "business"]=>10, ["draft", "technology"]=>4,
+ # ["published", "business"]=>0, ["published", "technology"]=>2}
+ #
# If +count+ is used with +select+, it will count the selected columns:
#
# Person.select(:age).count
# # => counts the number of different age values
#
# Note: not all valid +select+ expressions are valid +count+ expressions. The specifics differ
- # between databases. In invalid cases, an error from the databsae is thrown.
+ # between databases. In invalid cases, an error from the database is thrown.
def count(column_name = nil, options = {})
# TODO: Remove options argument as soon we remove support to
# activerecord-deprecated_finders.
@@ -170,18 +178,7 @@ module ActiveRecord
columns_hash.key?(cn) ? arel_table[cn] : cn
}
result = klass.connection.select_all(relation.arel, nil, bind_values)
- columns = result.columns.map do |key|
- klass.column_types.fetch(key) {
- result.column_types.fetch(key) { result.identity_type }
- }
- end
-
- result = result.map do |attributes|
- values = attributes.values
-
- columns.zip(values).map { |column, value| column.type_cast_from_database value }
- end
- columns.one? ? result.map!(&:first) : result
+ result.cast_values(klass.column_types)
end
end
@@ -267,7 +264,7 @@ module ActiveRecord
row = result.first
value = row && row.values.first
column = result.column_types.fetch(column_alias) do
- column_for(column_name)
+ type_for(column_name)
end
type_cast_calculated_value(value, column, operation)
@@ -330,14 +327,14 @@ module ActiveRecord
Hash[calculated_data.map do |row|
key = group_columns.map { |aliaz, col_name|
column = calculated_data.column_types.fetch(aliaz) do
- column_for(col_name)
+ type_for(col_name)
end
type_cast_calculated_value(row[aliaz], column)
}
key = key.first if key.size == 1
key = key_records[key] if associated
- column_type = calculated_data.column_types.fetch(aggregate_alias) { column_for(column_name) }
+ column_type = calculated_data.column_types.fetch(aggregate_alias) { type_for(column_name) }
[key, type_cast_calculated_value(row[aggregate_alias], column_type, operation)]
end]
end
@@ -364,24 +361,20 @@ module ActiveRecord
@klass.connection.table_alias_for(table_name)
end
- def column_for(field)
+ def type_for(field)
field_name = field.respond_to?(:name) ? field.name.to_s : field.to_s.split('.').last
- @klass.columns_hash[field_name]
+ @klass.type_for_attribute(field_name)
end
- def type_cast_calculated_value(value, column, operation = nil)
+ def type_cast_calculated_value(value, type, operation = nil)
case operation
when 'count' then value.to_i
- when 'sum' then type_cast_using_column(value || 0, column)
+ when 'sum' then type.type_cast_from_database(value || 0)
when 'average' then value.respond_to?(:to_d) ? value.to_d : value
- else type_cast_using_column(value, column)
+ else type.type_cast_from_database(value)
end
end
- def type_cast_using_column(value, column)
- column ? column.type_cast_from_database(value) : value
- end
-
# TODO: refactor to allow non-string `select_values` (eg. Arel nodes).
def select_for_count
if select_values.present?
diff --git a/activerecord/lib/active_record/result.rb b/activerecord/lib/active_record/result.rb
index ae53f66d7a..8405fdaeb9 100644
--- a/activerecord/lib/active_record/result.rb
+++ b/activerecord/lib/active_record/result.rb
@@ -42,14 +42,6 @@ module ActiveRecord
@column_types = column_types
end
- def identity_type # :nodoc:
- IDENTITY_TYPE
- end
-
- def column_type(name)
- @column_types[name] || identity_type
- end
-
def each
if block_given?
hash_rows.each { |row| yield row }
@@ -82,6 +74,15 @@ module ActiveRecord
hash_rows.last
end
+ def cast_values(type_overrides = {}) # :nodoc:
+ types = columns.map { |name| column_type(name, type_overrides) }
+ result = rows.map do |values|
+ types.zip(values).map { |type, value| type.type_cast_from_database(value) }
+ end
+
+ columns.one? ? result.map!(&:first) : result
+ end
+
def initialize_copy(other)
@columns = columns.dup
@rows = rows.dup
@@ -91,11 +92,17 @@ module ActiveRecord
private
+ def column_type(name, type_overrides = {})
+ type_overrides.fetch(name) do
+ column_types.fetch(name, IDENTITY_TYPE)
+ end
+ end
+
def hash_rows
@hash_rows ||=
begin
# We freeze the strings to prevent them getting duped when
- # used as keys in ActiveRecord::Base's @raw_attributes hash
+ # used as keys in ActiveRecord::Base's @attributes hash
columns = @columns.map { |c| c.dup.freeze }
@rows.map { |row|
# In the past we used Hash[columns.zip(row)]
diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb
index 1aa93ffbb3..ff70cbed0f 100644
--- a/activerecord/lib/active_record/sanitization.rb
+++ b/activerecord/lib/active_record/sanitization.rb
@@ -107,7 +107,7 @@ module ActiveRecord
end.join(', ')
end
- # Sanitizes a +string+ so that it is safe to use within a sql
+ # Sanitizes a +string+ so that it is safe to use within an SQL
# LIKE statement. This method uses +escape_character+ to escape all occurrences of "\", "_" and "%"
def sanitize_sql_like(string, escape_character = "\\")
pattern = Regexp.union(escape_character, "%", "_")
diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb
index e055d571ab..a94364bde1 100644
--- a/activerecord/lib/active_record/schema_dumper.rb
+++ b/activerecord/lib/active_record/schema_dumper.rb
@@ -91,7 +91,8 @@ HEADER
end
def tables(stream)
- @connection.tables.sort.each do |tbl|
+ sorted_tables = @connection.tables.sort
+ sorted_tables.each do |tbl|
next if ['schema_migrations', ignore_tables].flatten.any? do |ignored|
case ignored
when String; remove_prefix_and_suffix(tbl) == ignored
@@ -102,6 +103,13 @@ HEADER
end
table(tbl, stream)
end
+
+ # dump foreign keys at the end to make sure all dependent tables exist.
+ if @connection.supports_foreign_keys?
+ sorted_tables.each do |tbl|
+ foreign_keys(tbl, stream)
+ end
+ end
end
def table(table, stream)
@@ -112,7 +120,8 @@ HEADER
# first dump primary key column
if @connection.respond_to?(:pk_and_sequence_for)
pk, _ = @connection.pk_and_sequence_for(table)
- elsif @connection.respond_to?(:primary_key)
+ end
+ if !pk && @connection.respond_to?(:primary_key)
pk = @connection.primary_key(table)
end
@@ -212,6 +221,36 @@ HEADER
end
end
+ def foreign_keys(table, stream)
+ if (foreign_keys = @connection.foreign_keys(table)).any?
+ add_foreign_key_statements = foreign_keys.map do |foreign_key|
+ parts = [
+ 'add_foreign_key ' + remove_prefix_and_suffix(foreign_key.from_table).inspect,
+ remove_prefix_and_suffix(foreign_key.to_table).inspect,
+ ]
+
+ if foreign_key.column != @connection.foreign_key_column_for(foreign_key.to_table)
+ parts << ('column: ' + foreign_key.column.inspect)
+ end
+
+ if foreign_key.custom_primary_key?
+ parts << ('primary_key: ' + foreign_key.primary_key.inspect)
+ end
+
+ if foreign_key.name !~ /^fk_rails_[0-9a-f]{10}$/
+ parts << ('name: ' + foreign_key.name.inspect)
+ end
+
+ parts << ('on_update: ' + foreign_key.on_update.inspect) if foreign_key.on_update
+ parts << ('on_delete: ' + foreign_key.on_delete.inspect) if foreign_key.on_delete
+
+ ' ' + parts.join(', ')
+ end
+
+ stream.puts add_foreign_key_statements.sort.join("\n")
+ end
+ end
+
def remove_prefix_and_suffix(table)
table.gsub(/^(#{@options[:table_name_prefix]})(.+)(#{@options[:table_name_suffix]})$/, "\\2")
end
diff --git a/activerecord/lib/active_record/schema_migration.rb b/activerecord/lib/active_record/schema_migration.rb
index 3a004d58c9..b5038104ac 100644
--- a/activerecord/lib/active_record/schema_migration.rb
+++ b/activerecord/lib/active_record/schema_migration.rb
@@ -43,6 +43,10 @@ module ActiveRecord
def normalize_migration_number(number)
"%.3d" % number.to_i
end
+
+ def normalized_versions
+ pluck(:version).map { |v| normalize_migration_number v }
+ end
end
def version
diff --git a/activerecord/lib/active_record/serializers/xml_serializer.rb b/activerecord/lib/active_record/serializers/xml_serializer.rb
index 019fe2218e..c2484d02ed 100644
--- a/activerecord/lib/active_record/serializers/xml_serializer.rb
+++ b/activerecord/lib/active_record/serializers/xml_serializer.rb
@@ -182,11 +182,7 @@ module ActiveRecord #:nodoc:
klass = @serializable.class
column = klass.columns_hash[name] || Type::Value.new
- type = if column.serialized?
- super
- else
- column.type
- end
+ type = ActiveSupport::XmlMini::TYPE_NAMES[value.class.name] || column.type
{ :text => :string,
:time => :datetime }[type] || type
diff --git a/activerecord/lib/active_record/store.rb b/activerecord/lib/active_record/store.rb
index 7014bc6d45..3c291f28e3 100644
--- a/activerecord/lib/active_record/store.rb
+++ b/activerecord/lib/active_record/store.rb
@@ -99,7 +99,7 @@ module ActiveRecord
self.local_stored_attributes[store_attribute] |= keys
end
- def _store_accessors_module
+ def _store_accessors_module # :nodoc:
@_store_accessors_module ||= begin
mod = Module.new
include mod
@@ -129,10 +129,10 @@ module ActiveRecord
private
def store_accessor_for(store_attribute)
- @column_types[store_attribute.to_s].accessor
+ type_for_attribute(store_attribute.to_s).accessor
end
- class HashAccessor
+ class HashAccessor # :nodoc:
def self.read(object, attribute, key)
prepare(object, attribute)
object.public_send(attribute)[key]
@@ -151,7 +151,7 @@ module ActiveRecord
end
end
- class StringKeyedHashAccessor < HashAccessor
+ class StringKeyedHashAccessor < HashAccessor # :nodoc:
def self.read(object, attribute, key)
super object, attribute, key.to_s
end
@@ -161,7 +161,7 @@ module ActiveRecord
end
end
- class IndifferentHashAccessor < ActiveRecord::Store::HashAccessor
+ class IndifferentHashAccessor < ActiveRecord::Store::HashAccessor # :nodoc:
def self.prepare(object, store_attribute)
attribute = object.send(store_attribute)
unless attribute.is_a?(ActiveSupport::HashWithIndifferentAccess)
diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb
index 168b338b97..9bc23b5ec7 100644
--- a/activerecord/lib/active_record/tasks/database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/database_tasks.rb
@@ -6,7 +6,7 @@ module ActiveRecord
# <tt>ActiveRecord::Tasks::DatabaseTasks</tt> is a utility class, which encapsulates
# logic behind common tasks used to manage database and migrations.
#
- # The tasks defined here are used in rake tasks provided by Active Record.
+ # The tasks defined here are used with Rake tasks provided by Active Record.
#
# In order to use DatabaseTasks, a few config values need to be set. All the needed
# config values are set by Rails already, so it's necessary to do it only if you
@@ -14,7 +14,6 @@ module ActiveRecord
# (in such case after configuring the database tasks, you can also use the rake tasks
# defined in Active Record).
#
- #
# The possible config values are:
#
# * +env+: current environment (like Rails.env).
@@ -59,7 +58,11 @@ module ActiveRecord
end
def fixtures_path
- @fixtures_path ||= File.join(root, 'test', 'fixtures')
+ @fixtures_path ||= if ENV['FIXTURES_PATH']
+ File.join(root, ENV['FIXTURES_PATH'])
+ else
+ File.join(root, 'test', 'fixtures')
+ end
end
def root
@@ -144,6 +147,18 @@ module ActiveRecord
class_for_adapter(configuration['adapter']).new(configuration).purge
end
+ def purge_all
+ each_local_configuration { |configuration|
+ purge configuration
+ }
+ end
+
+ def purge_current(environment = env)
+ each_current_configuration(environment) { |configuration|
+ purge configuration
+ }
+ end
+
def structure_dump(*arguments)
configuration = arguments.first
filename = arguments.delete_at 1
diff --git a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb
index c755831e6d..644c4852b9 100644
--- a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb
@@ -124,7 +124,7 @@ IDENTIFIED BY '#{configuration['password']}' WITH GRANT OPTION;
end
def root_password
- $stdout.print "Please provide the root password for your mysql installation\n>"
+ $stdout.print "Please provide the root password for your MySQL installation\n>"
$stdin.gets.strip
end
diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb
index 6c30ccab72..e2e37e7c00 100644
--- a/activerecord/lib/active_record/timestamp.rb
+++ b/activerecord/lib/active_record/timestamp.rb
@@ -99,9 +99,11 @@ module ActiveRecord
end
def max_updated_column_timestamp(timestamp_names = timestamp_attributes_for_update)
- if (timestamps = timestamp_names.map { |attr| self[attr] }.compact).present?
- timestamps.map { |ts| ts.to_time }.max
- end
+ timestamp_names
+ .map { |attr| self[attr] }
+ .compact
+ .map(&:to_time)
+ .max
end
def current_time_from_proper_timezone
diff --git a/activerecord/lib/active_record/type.rb b/activerecord/lib/active_record/type.rb
index e9b827886a..f1384e0bb2 100644
--- a/activerecord/lib/active_record/type.rb
+++ b/activerecord/lib/active_record/type.rb
@@ -1,3 +1,4 @@
+require 'active_record/type/mutable'
require 'active_record/type/numeric'
require 'active_record/type/time_value'
require 'active_record/type/value'
diff --git a/activerecord/lib/active_record/type/binary.rb b/activerecord/lib/active_record/type/binary.rb
index 3bf29b5026..d29ff4e494 100644
--- a/activerecord/lib/active_record/type/binary.rb
+++ b/activerecord/lib/active_record/type/binary.rb
@@ -22,9 +22,9 @@ module ActiveRecord
Data.new(super)
end
- class Data
+ class Data # :nodoc:
def initialize(value)
- @value = value
+ @value = value.to_s
end
def to_s
diff --git a/activerecord/lib/active_record/type/date_time.rb b/activerecord/lib/active_record/type/date_time.rb
index 560d63c101..5f19608a33 100644
--- a/activerecord/lib/active_record/type/date_time.rb
+++ b/activerecord/lib/active_record/type/date_time.rb
@@ -7,6 +7,16 @@ module ActiveRecord
:datetime
end
+ def type_cast_for_database(value)
+ zone_conversion_method = ActiveRecord::Base.default_timezone == :utc ? :getutc : :getlocal
+
+ if value.acts_like?(:time)
+ value.send(zone_conversion_method)
+ else
+ super
+ end
+ end
+
private
def cast_value(string)
diff --git a/activerecord/lib/active_record/type/mutable.rb b/activerecord/lib/active_record/type/mutable.rb
new file mode 100644
index 0000000000..066617ea59
--- /dev/null
+++ b/activerecord/lib/active_record/type/mutable.rb
@@ -0,0 +1,16 @@
+module ActiveRecord
+ module Type
+ module Mutable # :nodoc:
+ def type_cast_from_user(value)
+ type_cast_from_database(type_cast_for_database(value))
+ end
+
+ # +raw_old_value+ will be the `_before_type_cast` version of the
+ # value (likely a string). +new_value+ will be the current, type
+ # cast value.
+ def changed_in_place?(raw_old_value, new_value)
+ raw_old_value != type_cast_for_database(new_value)
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/numeric.rb b/activerecord/lib/active_record/type/numeric.rb
index 137c9e4c99..fa43266504 100644
--- a/activerecord/lib/active_record/type/numeric.rb
+++ b/activerecord/lib/active_record/type/numeric.rb
@@ -16,26 +16,20 @@ module ActiveRecord
end
def changed?(old_value, _new_value, new_value_before_type_cast) # :nodoc:
- # 0 => 'wibble' should mark as changed so numericality validations run
- if nil_or_zero?(old_value) && non_numeric_string?(new_value_before_type_cast)
- # nil => '' should not mark as changed
- old_value != new_value_before_type_cast.presence
- else
- super
- end
+ 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 !~ /\A\d+\.?\d*\z/
- end
-
- def nil_or_zero?(value)
- value.nil? || value == 0
+ value.to_s !~ /\A\d+\.?\d*\z/
end
end
end
diff --git a/activerecord/lib/active_record/type/serialized.rb b/activerecord/lib/active_record/type/serialized.rb
index 9144028576..42bbed7103 100644
--- a/activerecord/lib/active_record/type/serialized.rb
+++ b/activerecord/lib/active_record/type/serialized.rb
@@ -1,6 +1,8 @@
module ActiveRecord
module Type
class Serialized < SimpleDelegator # :nodoc:
+ include Mutable
+
attr_reader :subtype, :coder
def initialize(subtype, coder)
@@ -17,10 +19,6 @@ module ActiveRecord
end
end
- def type_cast_from_user(value)
- type_cast_from_database(type_cast_for_database(value))
- end
-
def type_cast_for_database(value)
return if value.nil?
unless is_default_value?(value)
@@ -28,14 +26,21 @@ module ActiveRecord
end
end
- def serialized?
- true
- end
-
def accessor
ActiveRecord::Store::IndifferentHashAccessor
end
+ def init_with(coder)
+ @subtype = coder['subtype']
+ @coder = coder['coder']
+ __setobj__(@subtype)
+ end
+
+ def encode_with(coder)
+ coder['subtype'] = @subtype
+ coder['coder'] = @coder
+ end
+
private
def is_default_value?(value)
diff --git a/activerecord/lib/active_record/type/string.rb b/activerecord/lib/active_record/type/string.rb
index 3b1554bd5a..150defb106 100644
--- a/activerecord/lib/active_record/type/string.rb
+++ b/activerecord/lib/active_record/type/string.rb
@@ -5,8 +5,20 @@ module ActiveRecord
:string
end
- def text?
- true
+ def changed_in_place?(raw_old_value, new_value)
+ if new_value.is_a?(::String)
+ raw_old_value != new_value
+ end
+ end
+
+ def type_cast_for_database(value)
+ case value
+ when ::Numeric, ActiveSupport::Duration then value.to_s
+ when ::String then ::String.new(value)
+ when true then "1"
+ when false then "0"
+ else super
+ end
end
private
@@ -15,7 +27,8 @@ module ActiveRecord
case value
when true then "1"
when false then "0"
- else value.to_s
+ # String.new is slightly faster than dup
+ else ::String.new(value.to_s)
end
end
end
diff --git a/activerecord/lib/active_record/type/value.rb b/activerecord/lib/active_record/type/value.rb
index efcdab1c0e..e0a783fb45 100644
--- a/activerecord/lib/active_record/type/value.rb
+++ b/activerecord/lib/active_record/type/value.rb
@@ -3,8 +3,8 @@ module ActiveRecord
class Value # :nodoc:
attr_reader :precision, :scale, :limit
- # Valid options are +precision+, +scale+, and +limit+.
- # They are only used when dumping schema.
+ # 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]
@@ -12,65 +12,81 @@ module ActiveRecord
@limit = options[:limit]
end
- # The simplified type that this object represents. Subclasses
- # must override this method.
+ # The simplified type that this object represents. Returns a symbol such
+ # as +:string+ or +:integer+
def type; end
+ # Type casts a string from the database into the appropriate ruby type.
+ # Classes which do not need separate type casting behavior for database
+ # and user provided values should override +cast_value+ instead.
def type_cast_from_database(value)
type_cast(value)
end
+ # Type casts a value from user input (e.g. from a setter). This value may
+ # be a string from the form builder, or an already type cast value
+ # provided manually to a setter.
+ #
+ # Classes which do not need separate type casting behavior for database
+ # and user provided values should override +type_cast+ or +cast_value+
+ # instead.
def type_cast_from_user(value)
type_cast(value)
end
+ # Cast a value from the ruby type to a type that the database knows how
+ # to understand. The returned value from this method should be a
+ # +String+, +Numeric+, +Date+, +Time+, +Symbol+, +true+, +false+, or
+ # +nil+
def type_cast_for_database(value)
value
end
- def type_cast_for_schema(value)
+ # Type cast a value for schema dumping. This method is private, as we are
+ # hoping to remove it entirely.
+ def type_cast_for_schema(value) # :nodoc:
value.inspect
end
- def text?
+ # These predicates are not documented, as I need to look further into
+ # their use, and see if they can be removed entirely.
+ def number? # :nodoc:
false
end
- def number?
- false
- end
-
- def binary?
- false
- end
-
- def serialized?
+ def binary? # :nodoc:
false
end
def klass # :nodoc:
end
- # +old_value+ will always be type-cast.
- # +new_value+ will come straight from the database
- # or from assignment, so it could be anything. Types
- # which cannot typecast arbitrary values should override
- # this method.
- def changed?(old_value, new_value, _new_value_before_type_cast) # :nodoc:
+ # Determines whether a value has changed for dirty checking. +old_value+
+ # and +new_value+ will always be type-cast. Types should not need to
+ # override this method.
+ def changed?(old_value, new_value, _new_value_before_type_cast)
old_value != new_value
end
+ # Determines whether the mutable value has been modified since it was
+ # read. Returns +false+ by default. This method should not need to be
+ # overriden directly. Types which return a mutable value should include
+ # +Type::Mutable+, which will define this method.
+ def changed_in_place?(*)
+ false
+ end
+
private
- # Takes an input from the database, or from attribute setters,
- # and casts it to a type appropriate for this object. This method
- # should not be overriden by subclasses. Instead, override `cast_value`.
- def type_cast(value) # :api: public
+
+ def type_cast(value)
cast_value(value) unless value.nil?
end
- # Responsible for casting values from external sources to the appropriate
- # type. Called by `type_cast` for all values except `nil`.
- def cast_value(value) # :api: public
+ # Convenience method for types which do not need separate type casting
+ # behavior for user and database inputs. Called by
+ # `type_cast_from_database` and `type_cast_from_user` for all values
+ # except `nil`.
+ def cast_value(value) # :doc:
value
end
end
diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb
index 9999624fcf..b4b33804de 100644
--- a/activerecord/lib/active_record/validations.rb
+++ b/activerecord/lib/active_record/validations.rb
@@ -54,7 +54,7 @@ module ActiveRecord
# Attempts to save the record just like Base#save but will raise a +RecordInvalid+
# exception instead of returning +false+ if the record is not valid.
def save!(options={})
- perform_validations(options) ? super : raise(RecordInvalid.new(self))
+ perform_validations(options) ? super : raise_record_invalid
end
# Runs all the validations within the specified context. Returns +true+ if
@@ -75,8 +75,24 @@ module ActiveRecord
alias_method :validate, :valid?
+ # Runs all the validations within the specified context. Returns +true+ if
+ # no errors are found, raises +RecordInvalid+ otherwise.
+ #
+ # If the argument is +false+ (default is +nil+), the context is set to <tt>:create</tt> if
+ # <tt>new_record?</tt> is +true+, and to <tt>:update</tt> if it is not.
+ #
+ # Validations with no <tt>:on</tt> option will run no matter the context. Validations with
+ # some <tt>:on</tt> option will only run in the specified context.
+ def validate!(context = nil)
+ valid?(context) || raise_record_invalid
+ end
+
protected
+ def raise_record_invalid
+ raise(RecordInvalid.new(self))
+ end
+
def perform_validations(options={}) # :nodoc:
options[:validate] == false || valid?(options[:context])
end
diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb
index 2e7b1d7206..2a34969a8c 100644
--- a/activerecord/lib/active_record/validations/uniqueness.rb
+++ b/activerecord/lib/active_record/validations/uniqueness.rb
@@ -48,7 +48,7 @@ module ActiveRecord
def build_relation(klass, table, attribute, value) #:nodoc:
if reflection = klass._reflect_on_association(attribute)
attribute = reflection.foreign_key
- value = value.attributes[reflection.primary_key_column.name] unless value.nil?
+ value = value.attributes[reflection.klass.primary_key] unless value.nil?
end
attribute_name = attribute.to_s
@@ -61,9 +61,11 @@ module ActiveRecord
column = klass.columns_hash[attribute_name]
value = klass.connection.type_cast(value, column)
- value = value.to_s[0, column.limit] if value && column.limit && column.text?
+ if value.is_a?(String) && column.limit
+ value = value.to_s[0, column.limit]
+ end
- if !options[:case_sensitive] && value && column.text?
+ if !options[:case_sensitive] && value.is_a?(String)
# will use SQL LOWER function before comparison, unless it detects a case insensitive collation
klass.connection.case_insensitive_comparison(table, attribute, column, value)
else
diff --git a/activerecord/test/cases/adapters/mysql2/boolean_test.rb b/activerecord/test/cases/adapters/mysql2/boolean_test.rb
index 267aa232d9..f3c711a64b 100644
--- a/activerecord/test/cases/adapters/mysql2/boolean_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/boolean_test.rb
@@ -47,7 +47,7 @@ class Mysql2BooleanTest < ActiveRecord::TestCase
assert_equal "1", attributes["published"]
assert_equal 1, @connection.type_cast(true, boolean_column)
- assert_equal 1, @connection.type_cast(true, string_column)
+ assert_equal "1", @connection.type_cast(true, string_column)
end
test "test type casting without emulated booleans" do
@@ -60,7 +60,7 @@ class Mysql2BooleanTest < ActiveRecord::TestCase
assert_equal "1", attributes["published"]
assert_equal 1, @connection.type_cast(true, boolean_column)
- assert_equal 1, @connection.type_cast(true, string_column)
+ assert_equal "1", @connection.type_cast(true, string_column)
end
test "with booleans stored as 1 and 0" do
diff --git a/activerecord/test/cases/adapters/postgresql/array_test.rb b/activerecord/test/cases/adapters/postgresql/array_test.rb
index 66750deb68..8df1b7d18c 100644
--- a/activerecord/test/cases/adapters/postgresql/array_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/array_test.rb
@@ -3,6 +3,7 @@ require "cases/helper"
class PostgresqlArrayTest < ActiveRecord::TestCase
include InTimeZone
+ OID = ActiveRecord::ConnectionAdapters::PostgreSQL::OID
class PgArray < ActiveRecord::Base
self.table_name = 'pg_arrays'
@@ -10,11 +11,20 @@ class PostgresqlArrayTest < ActiveRecord::TestCase
def setup
@connection = ActiveRecord::Base.connection
+
+ unless @connection.extension_enabled?('hstore')
+ @connection.enable_extension 'hstore'
+ @connection.commit_db_transaction
+ end
+
+ @connection.reconnect!
+
@connection.transaction do
@connection.create_table('pg_arrays') do |t|
t.string 'tags', array: true
t.integer 'ratings', array: true
t.datetime :datetimes, array: true
+ t.hstore :hstores, array: true
end
end
@column = PgArray.columns_hash['tags']
@@ -28,7 +38,6 @@ class PostgresqlArrayTest < ActiveRecord::TestCase
assert_equal :string, @column.type
assert_equal "character varying", @column.sql_type
assert @column.array
- assert_not @column.text?
assert_not @column.number?
assert_not @column.binary?
@@ -41,9 +50,8 @@ class PostgresqlArrayTest < ActiveRecord::TestCase
def test_default
@connection.add_column 'pg_arrays', 'score', :integer, array: true, default: [4, 4, 2]
PgArray.reset_column_information
- column = PgArray.columns_hash["score"]
- assert_equal([4, 4, 2], column.default)
+ assert_equal([4, 4, 2], PgArray.column_defaults['score'])
assert_equal([4, 4, 2], PgArray.new.score)
ensure
PgArray.reset_column_information
@@ -52,9 +60,8 @@ class PostgresqlArrayTest < ActiveRecord::TestCase
def test_default_strings
@connection.add_column 'pg_arrays', 'names', :string, array: true, default: ["foo", "bar"]
PgArray.reset_column_information
- column = PgArray.columns_hash["names"]
- assert_equal(["foo", "bar"], column.default)
+ assert_equal(["foo", "bar"], PgArray.column_defaults['names'])
assert_equal(["foo", "bar"], PgArray.new.names)
ensure
PgArray.reset_column_information
@@ -68,7 +75,7 @@ class PostgresqlArrayTest < ActiveRecord::TestCase
column = PgArray.columns_hash['snippets']
assert_equal :text, column.type
- assert_equal [], column.default
+ assert_equal [], PgArray.column_defaults['snippets']
assert column.array
end
@@ -85,8 +92,7 @@ class PostgresqlArrayTest < ActiveRecord::TestCase
@connection.change_column_default :pg_arrays, :tags, []
PgArray.reset_column_information
- column = PgArray.columns_hash['tags']
- assert_equal [], column.default
+ assert_equal [], PgArray.column_defaults['tags']
end
def test_type_cast_array
@@ -97,8 +103,13 @@ class PostgresqlArrayTest < ActiveRecord::TestCase
def test_type_cast_integers
x = PgArray.new(ratings: ['1', '2'])
- assert x.save!
- assert_equal(['1', '2'], x.ratings)
+
+ assert_equal([1, 2], x.ratings)
+
+ x.save!
+ x.reload
+
+ assert_equal([1, 2], x.ratings)
end
def test_select_with_strings
@@ -180,16 +191,6 @@ class PostgresqlArrayTest < ActiveRecord::TestCase
assert_equal("[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]", record.attribute_for_inspect(:ratings))
end
- def test_update_all
- pg_array = PgArray.create! tags: ["one", "two", "three"]
-
- PgArray.update_all tags: ["four", "five"]
- assert_equal ["four", "five"], pg_array.reload.tags
-
- PgArray.update_all tags: []
- assert_equal [], pg_array.reload.tags
- end
-
def test_escaping
unknown = 'foo\\",bar,baz,\\'
tags = ["hello_#{unknown}"]
@@ -198,6 +199,45 @@ class PostgresqlArrayTest < ActiveRecord::TestCase
assert_equal tags, ar.tags
end
+ def test_string_quoting_rules_match_pg_behavior
+ tags = ["", "one{", "two}", %(three"), "four\\", "five ", "six\t", "seven\n", "eight,", "nine", "ten\r", "NULL"]
+ x = PgArray.create!(tags: tags)
+ x.reload
+
+ assert_equal x.tags_before_type_cast, PgArray.columns_hash['tags'].type_cast_for_database(tags)
+ end
+
+ def test_quoting_non_standard_delimiters
+ strings = ["hello,", "world;"]
+ comma_delim = OID::Array.new(ActiveRecord::Type::String.new, ',')
+ semicolon_delim = OID::Array.new(ActiveRecord::Type::String.new, ';')
+
+ assert_equal %({"hello,",world;}), comma_delim.type_cast_for_database(strings)
+ assert_equal %({hello,;"world;"}), semicolon_delim.type_cast_for_database(strings)
+ end
+
+ def test_mutate_array
+ x = PgArray.create!(tags: %w(one two))
+
+ x.tags << "three"
+ x.save!
+ x.reload
+
+ assert_equal %w(one two three), x.tags
+ assert_not x.changed?
+ end
+
+ def test_mutate_value_in_array
+ x = PgArray.create!(hstores: [{ a: 'a' }, { b: 'b' }])
+
+ x.hstores.first['a'] = 'c'
+ x.save!
+ x.reload
+
+ assert_equal [{ 'a' => 'c' }, { 'b' => 'b' }], x.hstores
+ assert_not x.changed?
+ end
+
def test_datetime_with_timezone_awareness
tz = "Pacific Time (US & Canada)"
diff --git a/activerecord/test/cases/adapters/postgresql/bit_string_test.rb b/activerecord/test/cases/adapters/postgresql/bit_string_test.rb
index 3a9397bc26..72222c01fd 100644
--- a/activerecord/test/cases/adapters/postgresql/bit_string_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/bit_string_test.rb
@@ -26,7 +26,6 @@ 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.text?
assert_not column.number?
assert_not column.binary?
assert_not column.array
@@ -36,19 +35,16 @@ class PostgresqlBitStringTest < ActiveRecord::TestCase
column = PostgresqlBitString.columns_hash["a_bit_varying"]
assert_equal :bit_varying, column.type
assert_equal "bit varying(4)", column.sql_type
- assert_not column.text?
assert_not column.number?
assert_not column.binary?
assert_not column.array
end
def test_default
- column = PostgresqlBitString.columns_hash["a_bit"]
- assert_equal "00000011", column.default
+ assert_equal "00000011", PostgresqlBitString.column_defaults['a_bit']
assert_equal "00000011", PostgresqlBitString.new.a_bit
- column = PostgresqlBitString.columns_hash["a_bit_varying"]
- assert_equal "0011", column.default
+ assert_equal "0011", PostgresqlBitString.column_defaults['a_bit_varying']
assert_equal "0011", PostgresqlBitString.new.a_bit_varying
end
diff --git a/activerecord/test/cases/adapters/postgresql/citext_test.rb b/activerecord/test/cases/adapters/postgresql/citext_test.rb
index 90e837d426..2acb64f81c 100644
--- a/activerecord/test/cases/adapters/postgresql/citext_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/citext_test.rb
@@ -35,7 +35,6 @@ if ActiveRecord::Base.connection.supports_extensions?
column = Citext.columns_hash['cival']
assert_equal :citext, column.type
assert_equal 'citext', column.sql_type
- assert_not column.text?
assert_not column.number?
assert_not column.binary?
assert_not column.array
diff --git a/activerecord/test/cases/adapters/postgresql/composite_test.rb b/activerecord/test/cases/adapters/postgresql/composite_test.rb
index 0b48fe9af8..cfab5ca902 100644
--- a/activerecord/test/cases/adapters/postgresql/composite_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/composite_test.rb
@@ -51,7 +51,6 @@ class PostgresqlCompositeTest < ActiveRecord::TestCase
assert_nil column.type
assert_equal "full_address", column.sql_type
assert_not column.number?
- assert_not column.text?
assert_not column.binary?
assert_not column.array
end
@@ -84,7 +83,7 @@ class PostgresqlCompositeWithCustomOIDTest < ActiveRecord::TestCase
class FullAddressType < ActiveRecord::Type::Value
def type; :full_address end
- def type_cast(value)
+ def type_cast_from_database(value)
if value =~ /\("?([^",]*)"?,"?([^",]*)"?\)/
FullAddress.new($1, $2)
end
@@ -113,7 +112,6 @@ class PostgresqlCompositeWithCustomOIDTest < ActiveRecord::TestCase
assert_equal :full_address, column.type
assert_equal "full_address", column.sql_type
assert_not column.number?
- assert_not column.text?
assert_not column.binary?
assert_not column.array
end
diff --git a/activerecord/test/cases/adapters/postgresql/domain_test.rb b/activerecord/test/cases/adapters/postgresql/domain_test.rb
index fd7fdecff1..1500adb42d 100644
--- a/activerecord/test/cases/adapters/postgresql/domain_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/domain_test.rb
@@ -30,7 +30,6 @@ class PostgresqlDomainTest < ActiveRecord::TestCase
assert_equal :decimal, column.type
assert_equal "custom_money", column.sql_type
assert column.number?
- assert_not column.text?
assert_not column.binary?
assert_not column.array
end
diff --git a/activerecord/test/cases/adapters/postgresql/enum_test.rb b/activerecord/test/cases/adapters/postgresql/enum_test.rb
index b809f1a79c..d99c4a292e 100644
--- a/activerecord/test/cases/adapters/postgresql/enum_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/enum_test.rb
@@ -34,7 +34,6 @@ class PostgresqlEnumTest < ActiveRecord::TestCase
assert_equal :enum, column.type
assert_equal "mood", column.sql_type
assert_not column.number?
- assert_not column.text?
assert_not column.binary?
assert_not column.array
end
@@ -42,9 +41,8 @@ class PostgresqlEnumTest < ActiveRecord::TestCase
def test_enum_defaults
@connection.add_column 'postgresql_enums', 'good_mood', :mood, default: 'happy'
PostgresqlEnum.reset_column_information
- column = PostgresqlEnum.columns_hash["good_mood"]
- assert_equal "happy", column.default
+ assert_equal "happy", PostgresqlEnum.column_defaults['good_mood']
assert_equal "happy", PostgresqlEnum.new.good_mood
ensure
PostgresqlEnum.reset_column_information
diff --git a/activerecord/test/cases/adapters/postgresql/full_text_test.rb b/activerecord/test/cases/adapters/postgresql/full_text_test.rb
index ec646de5e9..9dadb177ca 100644
--- a/activerecord/test/cases/adapters/postgresql/full_text_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/full_text_test.rb
@@ -9,7 +9,6 @@ class PostgresqlFullTextTest < ActiveRecord::TestCase
assert_equal :tsvector, column.type
assert_equal "tsvector", column.sql_type
assert_not column.number?
- assert_not column.text?
assert_not column.binary?
assert_not column.array
end
diff --git a/activerecord/test/cases/adapters/postgresql/geometric_test.rb b/activerecord/test/cases/adapters/postgresql/geometric_test.rb
index 2f106ee664..6c0adbbeaa 100644
--- a/activerecord/test/cases/adapters/postgresql/geometric_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/geometric_test.rb
@@ -28,19 +28,16 @@ class PostgresqlPointTest < ActiveRecord::TestCase
column = PostgresqlPoint.columns_hash["x"]
assert_equal :point, column.type
assert_equal "point", column.sql_type
- assert_not column.text?
assert_not column.number?
assert_not column.binary?
assert_not column.array
end
def test_default
- column = PostgresqlPoint.columns_hash["y"]
- assert_equal [12.2, 13.3], column.default
+ assert_equal [12.2, 13.3], PostgresqlPoint.column_defaults['y']
assert_equal [12.2, 13.3], PostgresqlPoint.new.y
- column = PostgresqlPoint.columns_hash["z"]
- assert_equal [14.4, 15.5], column.default
+ assert_equal [14.4, 15.5], PostgresqlPoint.column_defaults['z']
assert_equal [14.4, 15.5], PostgresqlPoint.new.z
end
@@ -61,4 +58,15 @@ class PostgresqlPointTest < ActiveRecord::TestCase
assert record.reload
assert_equal [1.1, 2.2], record.x
end
+
+ def test_mutation
+ p = PostgresqlPoint.create! x: [10, 20]
+
+ p.x[1] = 25
+ p.save!
+ p.reload
+
+ assert_equal [10.0, 25.0], p.x
+ assert_not p.changed?
+ end
end
diff --git a/activerecord/test/cases/adapters/postgresql/hstore_test.rb b/activerecord/test/cases/adapters/postgresql/hstore_test.rb
index a25c9cb5e4..1296eb72c0 100644
--- a/activerecord/test/cases/adapters/postgresql/hstore_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/hstore_test.rb
@@ -56,7 +56,6 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase
assert_equal :hstore, @column.type
assert_equal "hstore", @column.sql_type
assert_not @column.number?
- assert_not @column.text?
assert_not @column.binary?
assert_not @column.array
end
@@ -64,9 +63,8 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase
def test_default
@connection.add_column 'hstores', 'permissions', :hstore, default: '"users"=>"read", "articles"=>"write"'
Hstore.reset_column_information
- column = Hstore.columns_hash["permissions"]
- assert_equal({"users"=>"read", "articles"=>"write"}, column.default)
+ assert_equal({"users"=>"read", "articles"=>"write"}, Hstore.column_defaults['permissions'])
assert_equal({"users"=>"read", "articles"=>"write"}, Hstore.new.permissions)
ensure
Hstore.reset_column_information
@@ -113,13 +111,7 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase
end
def test_type_cast_hstore
- assert @column
-
- data = "\"1\"=>\"2\""
- hash = @column.class.string_to_hstore data
- assert_equal({'1' => '2'}, hash)
- assert_equal({'1' => '2'}, @column.type_cast_from_database(data))
-
+ 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")))
@@ -163,20 +155,30 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase
assert_equal "GMT", y.timezone
end
+ def test_changes_in_place
+ hstore = Hstore.create!(settings: { 'one' => 'two' })
+ hstore.settings['three'] = 'four'
+ hstore.save!
+ hstore.reload
+
+ assert_equal 'four', hstore.settings['three']
+ assert_not hstore.changed?
+ end
+
def test_gen1
- assert_equal(%q(" "=>""), @column.class.hstore_to_string({' '=>''}))
+ assert_equal(%q(" "=>""), @column.cast_type.type_cast_for_database({' '=>''}))
end
def test_gen2
- assert_equal(%q(","=>""), @column.class.hstore_to_string({','=>''}))
+ assert_equal(%q(","=>""), @column.cast_type.type_cast_for_database({','=>''}))
end
def test_gen3
- assert_equal(%q("="=>""), @column.class.hstore_to_string({'='=>''}))
+ assert_equal(%q("="=>""), @column.cast_type.type_cast_for_database({'='=>''}))
end
def test_gen4
- assert_equal(%q(">"=>""), @column.class.hstore_to_string({'>'=>''}))
+ assert_equal(%q(">"=>""), @column.cast_type.type_cast_for_database({'>'=>''}))
end
def test_parse1
@@ -286,16 +288,6 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase
assert_cycle("a\nb" => "c\nd")
end
- def test_update_all
- hstore = Hstore.create! tags: { "one" => "two" }
-
- Hstore.update_all tags: { "three" => "four" }
- assert_equal({ "three" => "four" }, hstore.reload.tags)
-
- Hstore.update_all tags: { }
- assert_equal({ }, hstore.reload.tags)
- end
-
class TagCollection
def initialize(hash); @hash = hash end
def to_hash; @hash end
diff --git a/activerecord/test/cases/adapters/postgresql/infinity_test.rb b/activerecord/test/cases/adapters/postgresql/infinity_test.rb
new file mode 100644
index 0000000000..22e8873333
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/infinity_test.rb
@@ -0,0 +1,44 @@
+require "cases/helper"
+
+class PostgresqlInfinityTest < ActiveRecord::TestCase
+ class PostgresqlInfinity < ActiveRecord::Base
+ end
+
+ setup do
+ @connection = ActiveRecord::Base.connection
+ @connection.create_table(:postgresql_infinities) do |t|
+ t.float :float
+ t.datetime :datetime
+ end
+ end
+
+ teardown do
+ @connection.execute("DROP TABLE IF EXISTS postgresql_infinities")
+ end
+
+ test "type casting infinity on a float column" do
+ record = PostgresqlInfinity.create!(float: Float::INFINITY)
+ record.reload
+ assert_equal Float::INFINITY, record.float
+ end
+
+ test "update_all with infinity on a float column" do
+ record = PostgresqlInfinity.create!
+ PostgresqlInfinity.update_all(float: Float::INFINITY)
+ record.reload
+ assert_equal Float::INFINITY, record.float
+ end
+
+ test "type casting infinity on a datetime column" do
+ record = PostgresqlInfinity.create!(datetime: Float::INFINITY)
+ record.reload
+ assert_equal Float::INFINITY, record.datetime
+ end
+
+ test "update_all with infinity on a datetime column" do
+ record = PostgresqlInfinity.create!
+ PostgresqlInfinity.update_all(datetime: Float::INFINITY)
+ record.reload
+ assert_equal Float::INFINITY, record.datetime
+ end
+end
diff --git a/activerecord/test/cases/adapters/postgresql/json_test.rb b/activerecord/test/cases/adapters/postgresql/json_test.rb
index 3ee8839823..cb3c02fa3a 100644
--- a/activerecord/test/cases/adapters/postgresql/json_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/json_test.rb
@@ -35,7 +35,6 @@ class PostgresqlJSONTest < ActiveRecord::TestCase
assert_equal :json, column.type
assert_equal "json", column.sql_type
assert_not column.number?
- assert_not column.text?
assert_not column.binary?
assert_not column.array
end
@@ -43,9 +42,8 @@ class PostgresqlJSONTest < ActiveRecord::TestCase
def test_default
@connection.add_column 'json_data_type', 'permissions', :json, default: '{"users": "read", "posts": ["read", "write"]}'
JsonDataType.reset_column_information
- column = JsonDataType.columns_hash["permissions"]
- assert_equal({"users"=>"read", "posts"=>["read", "write"]}, column.default)
+ assert_equal({"users"=>"read", "posts"=>["read", "write"]}, JsonDataType.column_defaults['permissions'])
assert_equal({"users"=>"read", "posts"=>["read", "write"]}, JsonDataType.new.permissions)
ensure
JsonDataType.reset_column_information
@@ -78,7 +76,7 @@ class PostgresqlJSONTest < ActiveRecord::TestCase
column = JsonDataType.columns_hash["payload"]
data = "{\"a_key\":\"a_value\"}"
- hash = column.class.string_to_json data
+ hash = column.type_cast_from_database(data)
assert_equal({'a_key' => 'a_value'}, hash)
assert_equal({'a_key' => 'a_value'}, column.type_cast_from_database(data))
@@ -156,13 +154,24 @@ class PostgresqlJSONTest < ActiveRecord::TestCase
assert_equal "320×480", y.resolution
end
- def test_update_all
- json = JsonDataType.create! payload: { "one" => "two" }
+ def test_changes_in_place
+ json = JsonDataType.new
+ assert_not json.changed?
- JsonDataType.update_all payload: { "three" => "four" }
- assert_equal({ "three" => "four" }, json.reload.payload)
+ json.payload = { 'one' => 'two' }
+ assert json.changed?
+ assert json.payload_changed?
- JsonDataType.update_all payload: { }
- assert_equal({ }, json.reload.payload)
+ json.save!
+ assert_not json.changed?
+
+ json.payload['three'] = 'four'
+ assert json.payload_changed?
+
+ json.save!
+ json.reload
+
+ assert_equal({ 'one' => 'two', 'three' => 'four' }, json.payload)
+ assert_not json.changed?
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/ltree_test.rb b/activerecord/test/cases/adapters/postgresql/ltree_test.rb
index ddb7cd658c..889e369bd6 100644
--- a/activerecord/test/cases/adapters/postgresql/ltree_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/ltree_test.rb
@@ -31,7 +31,6 @@ class PostgresqlLtreeTest < ActiveRecord::TestCase
assert_equal :ltree, column.type
assert_equal "ltree", column.sql_type
assert_not column.number?
- assert_not column.text?
assert_not column.binary?
assert_not column.array
end
diff --git a/activerecord/test/cases/adapters/postgresql/money_test.rb b/activerecord/test/cases/adapters/postgresql/money_test.rb
index bdfeedafab..87183174f2 100644
--- a/activerecord/test/cases/adapters/postgresql/money_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/money_test.rb
@@ -26,14 +26,12 @@ class PostgresqlMoneyTest < ActiveRecord::TestCase
assert_equal "money", column.sql_type
assert_equal 2, column.scale
assert column.number?
- assert_not column.text?
assert_not column.binary?
assert_not column.array
end
def test_default
- column = PostgresqlMoney.columns_hash["depth"]
- assert_equal BigDecimal.new("150.55"), column.default
+ assert_equal BigDecimal.new("150.55"), PostgresqlMoney.column_defaults['depth']
assert_equal BigDecimal.new("150.55"), PostgresqlMoney.new.depth
end
@@ -71,4 +69,28 @@ class PostgresqlMoneyTest < ActiveRecord::TestCase
money.reload
assert_equal new_value, money.wealth
end
+
+ def test_update_all_with_money_string
+ money = PostgresqlMoney.create!
+ PostgresqlMoney.update_all(wealth: "987.65")
+ money.reload
+
+ assert_equal 987.65, money.wealth
+ end
+
+ def test_update_all_with_money_big_decimal
+ money = PostgresqlMoney.create!
+ PostgresqlMoney.update_all(wealth: '123.45'.to_d)
+ money.reload
+
+ assert_equal 123.45, money.wealth
+ end
+
+ def test_update_all_with_money_numeric
+ money = PostgresqlMoney.create!
+ PostgresqlMoney.update_all(wealth: 123.45)
+ money.reload
+
+ assert_equal 123.45, money.wealth
+ end
end
diff --git a/activerecord/test/cases/adapters/postgresql/network_test.rb b/activerecord/test/cases/adapters/postgresql/network_test.rb
index 32085cbb17..4f4c1103fa 100644
--- a/activerecord/test/cases/adapters/postgresql/network_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/network_test.rb
@@ -10,7 +10,6 @@ class PostgresqlNetworkTest < ActiveRecord::TestCase
assert_equal :cidr, column.type
assert_equal "cidr", column.sql_type
assert_not column.number?
- assert_not column.text?
assert_not column.binary?
assert_not column.array
end
@@ -20,7 +19,6 @@ class PostgresqlNetworkTest < ActiveRecord::TestCase
assert_equal :inet, column.type
assert_equal "inet", column.sql_type
assert_not column.number?
- assert_not column.text?
assert_not column.binary?
assert_not column.array
end
@@ -30,7 +28,6 @@ class PostgresqlNetworkTest < ActiveRecord::TestCase
assert_equal :macaddr, column.type
assert_equal "macaddr", column.sql_type
assert_not column.number?
- assert_not column.text?
assert_not column.binary?
assert_not column.array
end
diff --git a/activerecord/test/cases/adapters/postgresql/quoting_test.rb b/activerecord/test/cases/adapters/postgresql/quoting_test.rb
index 218c59247e..11d5173d37 100644
--- a/activerecord/test/cases/adapters/postgresql/quoting_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/quoting_test.rb
@@ -57,6 +57,17 @@ module ActiveRecord
assert_equal "'1970-01-01 00:00:00.000000'", @conn.quote(Time.at(0))
assert_equal "'1970-01-01 00:00:00.000000'", @conn.quote(Time.at(0).to_datetime)
end
+
+ def test_quote_range
+ range = "1,2]'; SELECT * FROM users; --".."a"
+ c = PostgreSQLColumn.new(nil, nil, OID::Range.new(Type::Integer.new, :int8range))
+ assert_equal "'[1,0]'", @conn.quote(range, c)
+ end
+
+ def test_quote_bit_string
+ c = PostgreSQLColumn.new(nil, 1, OID::Bit.new)
+ assert_equal nil, @conn.quote("'); SELECT * FROM users; /*\n01\n*/--", c)
+ end
end
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/range_test.rb b/activerecord/test/cases/adapters/postgresql/range_test.rb
index 4d9cfe55f5..d812cd01c4 100644
--- a/activerecord/test/cases/adapters/postgresql/range_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/range_test.rb
@@ -156,7 +156,7 @@ _SQL
assert_equal 0.5..0.7, @first_range.float_range
assert_equal 0.5...0.7, @second_range.float_range
assert_equal 0.5...Float::INFINITY, @third_range.float_range
- assert_equal (-Float::INFINITY...Float::INFINITY), @fourth_range.float_range
+ assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.float_range)
assert_nil @empty_range.float_range
end
@@ -262,6 +262,23 @@ _SQL
assert_raises(ArgumentError) { PostgresqlRange.create!(float_range: "(0.5, 0.7]") }
end
+ def test_update_all_with_ranges
+ PostgresqlRange.create!
+
+ PostgresqlRange.update_all(int8_range: 1..100)
+
+ assert_equal 1...101, PostgresqlRange.first.int8_range
+ end
+
+ def test_ranges_correctly_escape_input
+ range = "-1,2]'; DROP TABLE postgresql_ranges; --".."a"
+ PostgresqlRange.update_all(int8_range: range)
+
+ assert_nothing_raised do
+ PostgresqlRange.first
+ end
+ end
+
private
def assert_equal_round_trip(range, attribute, value)
round_trip(range, attribute, value)
diff --git a/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb b/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb
new file mode 100644
index 0000000000..23817198b1
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb
@@ -0,0 +1,15 @@
+require 'cases/helper'
+
+class PostgresqlTypeLookupTest < ActiveRecord::TestCase
+ setup do
+ @connection = ActiveRecord::Base.connection
+ end
+
+ test "array delimiters are looked up correctly" do
+ box_array = @connection.type_map.lookup(1020)
+ int_array = @connection.type_map.lookup(1007)
+
+ assert_equal ';', box_array.delimiter
+ assert_equal ',', int_array.delimiter
+ end
+end
diff --git a/activerecord/test/cases/adapters/postgresql/uuid_test.rb b/activerecord/test/cases/adapters/postgresql/uuid_test.rb
index 40ed0f64a4..df8faff546 100644
--- a/activerecord/test/cases/adapters/postgresql/uuid_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/uuid_test.rb
@@ -9,15 +9,6 @@ module PostgresqlUUIDHelper
@connection ||= ActiveRecord::Base.connection
end
- def enable_uuid_ossp
- unless connection.extension_enabled?('uuid-ossp')
- connection.enable_extension 'uuid-ossp'
- connection.commit_db_transaction
- end
-
- connection.reconnect!
- end
-
def drop_table(name)
connection.execute "drop table if exists #{name}"
end
@@ -60,7 +51,6 @@ class PostgresqlUUIDTest < ActiveRecord::TestCase
assert_equal :uuid, column.type
assert_equal "uuid", column.sql_type
assert_not column.number?
- assert_not column.text?
assert_not column.binary?
assert_not column.array
end
@@ -91,16 +81,32 @@ class PostgresqlUUIDGenerationTest < ActiveRecord::TestCase
end
setup do
- enable_uuid_ossp
+ enable_uuid_ossp!(connection)
connection.create_table('pg_uuids', id: :uuid, default: 'uuid_generate_v1()') do |t|
t.string 'name'
t.uuid 'other_uuid', default: 'uuid_generate_v4()'
end
+
+ # Create custom PostgreSQL function to generate UUIDs
+ # to test dumping tables which columns have defaults with custom functions
+ connection.execute <<-SQL
+ CREATE OR REPLACE FUNCTION my_uuid_generator() RETURNS uuid
+ AS $$ SELECT * FROM uuid_generate_v4() $$
+ LANGUAGE SQL VOLATILE;
+ SQL
+
+ # Create such a table with custom function as default value generator
+ connection.create_table('pg_uuids_2', id: :uuid, default: 'my_uuid_generator()') do |t|
+ t.string 'name'
+ t.uuid 'other_uuid_2', default: 'my_uuid_generator()'
+ end
end
teardown do
drop_table "pg_uuids"
+ drop_table 'pg_uuids_2'
+ connection.execute 'DROP FUNCTION IF EXISTS my_uuid_generator();'
end
if ActiveRecord::Base.connection.supports_extensions?
@@ -132,6 +138,13 @@ class PostgresqlUUIDGenerationTest < ActiveRecord::TestCase
assert_match(/\bcreate_table "pg_uuids", id: :uuid, default: "uuid_generate_v1\(\)"/, schema.string)
assert_match(/t\.uuid "other_uuid", default: "uuid_generate_v4\(\)"/, schema.string)
end
+
+ def test_schema_dumper_for_uuid_primary_key_with_custom_default
+ schema = StringIO.new
+ ActiveRecord::SchemaDumper.dump(connection, schema)
+ assert_match(/\bcreate_table "pg_uuids_2", id: :uuid, default: "my_uuid_generator\(\)"/, schema.string)
+ assert_match(/t\.uuid "other_uuid_2", default: "my_uuid_generator\(\)"/, schema.string)
+ end
end
end
@@ -139,7 +152,7 @@ class PostgresqlUUIDTestNilDefault < ActiveRecord::TestCase
include PostgresqlUUIDHelper
setup do
- enable_uuid_ossp
+ enable_uuid_ossp!(connection)
connection.create_table('pg_uuids', id: false) do |t|
t.primary_key :id, :uuid, default: nil
@@ -176,7 +189,7 @@ class PostgresqlUUIDTestInverseOf < ActiveRecord::TestCase
end
setup do
- enable_uuid_ossp
+ enable_uuid_ossp!(connection)
connection.transaction do
connection.create_table('pg_uuid_posts', id: :uuid) do |t|
diff --git a/activerecord/test/cases/adapters/postgresql/xml_test.rb b/activerecord/test/cases/adapters/postgresql/xml_test.rb
index 48c6eeb62c..4165dd5ac9 100644
--- a/activerecord/test/cases/adapters/postgresql/xml_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/xml_test.rb
@@ -11,7 +11,7 @@ class PostgresqlXMLTest < ActiveRecord::TestCase
begin
@connection.transaction do
@connection.create_table('xml_data_type') do |t|
- t.xml 'payload', default: {}
+ t.xml 'payload'
end
end
rescue ActiveRecord::StatementInvalid
@@ -32,4 +32,17 @@ class PostgresqlXMLTest < ActiveRecord::TestCase
@connection.execute %q|insert into xml_data_type (payload) VALUES(null)|
assert_nil XmlDataType.first.payload
end
+
+ def test_round_trip
+ data = XmlDataType.new(payload: "<foo>bar</foo>")
+ assert_equal "<foo>bar</foo>", data.payload
+ data.save!
+ assert_equal "<foo>bar</foo>", data.reload.payload
+ end
+
+ def test_update_all
+ data = XmlDataType.create!
+ XmlDataType.update_all(payload: "<bar>baz</bar>")
+ assert_equal "<bar>baz</bar>", data.reload.payload
+ end
end
diff --git a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb
index 8c9a051eea..ac8332e2fa 100644
--- a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb
+++ b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb
@@ -15,10 +15,10 @@ module ActiveRecord
def test_type_cast_binary_encoding_without_logger
@conn.extend(Module.new { def logger; end })
- cast_type = Type::String.new
+ column = Column.new(nil, nil, Type::String.new)
binary = SecureRandom.hex
expected = binary.dup.encode!(Encoding::UTF_8)
- assert_equal expected, @conn.type_cast(binary, cast_type)
+ assert_equal expected, @conn.type_cast(binary, column)
end
def test_type_cast_symbol
@@ -103,6 +103,13 @@ module ActiveRecord
}.new
assert_raise(TypeError) { @conn.type_cast(quoted_id_obj, nil) }
end
+
+ def test_quoting_binary_strings
+ value = "hello".encode('ascii-8bit')
+ column = Column.new(nil, 1, SQLite3String.new)
+
+ assert_equal "'hello'", @conn.quote(value, column)
+ end
end
end
end
diff --git a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb
index e55525177f..b2bf9480dd 100644
--- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb
+++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb
@@ -57,10 +57,11 @@ module ActiveRecord
end
end
- # sqlite databases should be able to support any type and not
- # just the ones mentioned in the native_database_types.
- # Therefore test_invalid column should always return true
- # even if the type is not valid.
+ # sqlite3 databases should be able to support any type and not just the
+ # ones mentioned in the native_database_types.
+ #
+ # Therefore test_invalid column should always return true even if the
+ # type is not valid.
def test_invalid_column
assert @conn.valid_type?(:foobar)
end
@@ -339,7 +340,7 @@ module ActiveRecord
column = @conn.columns('ex').find { |x|
x.name == 'number'
}
- assert_equal 10, column.default
+ assert_equal '10', column.default
end
end
diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb
index 9c92dc1141..25555bd75c 100644
--- a/activerecord/test/cases/associations/belongs_to_associations_test.rb
+++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb
@@ -787,8 +787,8 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
post = posts(:welcome)
comment = comments(:greetings)
- assert_difference lambda { post.reload.taggings_count }, -1 do
- assert_difference 'comment.reload.taggings_count', +1 do
+ assert_difference lambda { post.reload.tags_count }, -1 do
+ assert_difference 'comment.reload.tags_count', +1 do
tagging.taggable = comment
end
end
@@ -935,3 +935,14 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
assert_equal 1, Column.count
end
end
+
+class BelongsToWithForeignKeyTest < ActiveRecord::TestCase
+ fixtures :authors, :author_addresses
+
+ def test_destroy_linked_models
+ address = AuthorAddress.create!
+ author = Author.create! name: "Author", author_address_id: address.id
+
+ author.destroy!
+ end
+end
diff --git a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb
index 71c0609df5..51d8e0523e 100644
--- a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb
+++ b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb
@@ -35,9 +35,9 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase
def test_eager_association_loading_with_hmt_does_not_table_name_collide_when_joining_associations
assert_nothing_raised do
- Author.joins(:posts).eager_load(:comments).where(:posts => {:taggings_count => 1}).to_a
+ Author.joins(:posts).eager_load(:comments).where(:posts => {:tags_count => 1}).to_a
end
- authors = Author.joins(:posts).eager_load(:comments).where(:posts => {:taggings_count => 1}).to_a
+ authors = Author.joins(:posts).eager_load(:comments).where(:posts => {:tags_count => 1}).to_a
assert_equal 1, assert_no_queries { authors.size }
assert_equal 10, assert_no_queries { authors[0].comments.size }
end
diff --git a/activerecord/test/cases/associations/deprecated_counter_cache_on_has_many_through_test.rb b/activerecord/test/cases/associations/deprecated_counter_cache_on_has_many_through_test.rb
new file mode 100644
index 0000000000..48f7ddbe83
--- /dev/null
+++ b/activerecord/test/cases/associations/deprecated_counter_cache_on_has_many_through_test.rb
@@ -0,0 +1,26 @@
+require "cases/helper"
+
+class DeprecatedCounterCacheOnHasManyThroughTest < ActiveRecord::TestCase
+ class Post < ActiveRecord::Base
+ has_many :taggings, as: :taggable
+ has_many :tags, through: :taggings
+ end
+
+ class Tagging < ActiveRecord::Base
+ belongs_to :taggable, polymorphic: true
+ belongs_to :tag
+ end
+
+ class Tag < ActiveRecord::Base
+ end
+
+ test "counter caches are updated in the database if the belongs_to association doesn't specify a counter cache" do
+ post = Post.create!(title: 'Hello', body: 'World!')
+ assert_deprecated { post.tags << Tag.create!(name: 'whatever') }
+
+ assert_equal 1, post.tags.size
+ assert_equal 1, post.tags_count
+ assert_equal 1, post.reload.tags.size
+ assert_equal 1, post.reload.tags_count
+ end
+end
diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb
index 910067666a..21912fdf0f 100644
--- a/activerecord/test/cases/associations/eager_test.rb
+++ b/activerecord/test/cases/associations/eager_test.rb
@@ -1261,4 +1261,33 @@ class EagerAssociationTest < ActiveRecord::TestCase
Author.eager_load(:posts_with_signature).to_a
end
end
+
+ test "preloading readonly association" do
+ # has-one
+ firm = Firm.where(id: "1").preload(:readonly_account).first!
+ assert firm.readonly_account.readonly?
+
+ # has_and_belongs_to_many
+ project = Project.where(id: "2").preload(:readonly_developers).first!
+ assert project.readonly_developers.first.readonly?
+
+ # has-many :through
+ david = Author.where(id: "1").preload(:readonly_comments).first!
+ assert david.readonly_comments.first.readonly?
+ end
+
+ test "eager-loading readonly association" do
+ skip "eager_load does not yet preserve readonly associations"
+ # has-one
+ firm = Firm.where(id: "1").eager_load(:readonly_account).first!
+ assert firm.readonly_account.readonly?
+
+ # has_and_belongs_to_many
+ project = Project.where(id: "2").eager_load(:readonly_developers).first!
+ assert project.readonly_developers.first.readonly?
+
+ # has-many :through
+ david = Author.where(id: "1").eager_load(:readonly_comments).first!
+ assert david.readonly_comments.first.readonly?
+ end
end
diff --git a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb
index 080c499444..cc58a4a1a2 100644
--- a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb
+++ b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb
@@ -860,7 +860,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
assert_equal 'edges', Vertex.reflect_on_association(:sources).join_table
end
- def test_namespaced_habtm
+ def test_has_and_belongs_to_many_in_a_namespaced_model_pointing_to_a_namespaced_model
magazine = Publisher::Magazine.create
article = Publisher::Article.create
magazine.articles << article
@@ -869,6 +869,15 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
assert_includes magazine.articles, article
end
+ def test_has_and_belongs_to_many_in_a_namespaced_model_pointing_to_a_non_namespaced_model
+ article = Publisher::Article.create
+ tag = Tag.create
+ article.tags << tag
+ article.save
+
+ assert_includes article.tags, tag
+ end
+
def test_redefine_habtm
child = SubDeveloper.new("name" => "Aredridel")
child.special_projects << SpecialProject.new("name" => "Special Project")
diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb
index a80be99720..fe961e871c 100644
--- a/activerecord/test/cases/associations/has_many_associations_test.rb
+++ b/activerecord/test/cases/associations/has_many_associations_test.rb
@@ -36,7 +36,7 @@ class HasManyAssociationsTestForReorderWithJoinDependency < ActiveRecord::TestCa
author = authors(:david)
# this can fail on adapters which require ORDER BY expressions to be included in the SELECT expression
# if the reorder clauses are not correctly handled
- assert author.posts_with_comments_sorted_by_comment_id.where('comments.id > 0').reorder('posts.comments_count DESC', 'posts.taggings_count DESC').last
+ assert author.posts_with_comments_sorted_by_comment_id.where('comments.id > 0').reorder('posts.comments_count DESC', 'posts.tags_count DESC').last
end
end
@@ -772,6 +772,36 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal topic.replies.to_a.size, topic.replies_count
end
+ def test_counter_cache_updates_in_memory_after_concat
+ topic = Topic.create title: "Zoom-zoom-zoom"
+
+ topic.replies << Reply.create(title: "re: zoom", content: "speedy quick!")
+ assert_equal 1, topic.replies_count
+ assert_equal 1, topic.replies.size
+ assert_equal 1, topic.reload.replies.size
+ end
+
+ def test_counter_cache_updates_in_memory_after_create
+ topic = Topic.create title: "Zoom-zoom-zoom"
+
+ topic.replies.create!(title: "re: zoom", content: "speedy quick!")
+ assert_equal 1, topic.replies_count
+ assert_equal 1, topic.replies.size
+ assert_equal 1, topic.reload.replies.size
+ end
+
+ def test_counter_cache_updates_in_memory_after_create_with_array
+ topic = Topic.create title: "Zoom-zoom-zoom"
+
+ topic.replies.create!([
+ { title: "re: zoom", content: "speedy quick!" },
+ { title: "re: zoom 2", content: "OMG lol!" },
+ ])
+ assert_equal 2, topic.replies_count
+ assert_equal 2, topic.replies.size
+ assert_equal 2, topic.reload.replies.size
+ end
+
def test_pushing_association_updates_counter_cache
topic = Topic.order("id ASC").first
reply = Reply.create!
@@ -784,14 +814,14 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_deleting_updates_counter_cache_without_dependent_option
post = posts(:welcome)
- assert_difference "post.reload.taggings_count", -1 do
+ assert_difference "post.reload.tags_count", -1 do
post.taggings.delete(post.taggings.first)
end
end
def test_deleting_updates_counter_cache_with_dependent_delete_all
post = posts(:welcome)
- post.update_columns(taggings_with_delete_all_count: post.taggings_count)
+ post.update_columns(taggings_with_delete_all_count: post.tags_count)
assert_difference "post.reload.taggings_with_delete_all_count", -1 do
post.taggings_with_delete_all.delete(post.taggings_with_delete_all.first)
@@ -800,7 +830,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_deleting_updates_counter_cache_with_dependent_destroy
post = posts(:welcome)
- post.update_columns(taggings_with_destroy_count: post.taggings_count)
+ post.update_columns(taggings_with_destroy_count: post.tags_count)
assert_difference "post.reload.taggings_with_destroy_count", -1 do
post.taggings_with_destroy.delete(post.taggings_with_destroy.first)
diff --git a/activerecord/test/cases/associations/has_many_through_associations_test.rb b/activerecord/test/cases/associations/has_many_through_associations_test.rb
index 2e62189e7a..a85e020f0c 100644
--- a/activerecord/test/cases/associations/has_many_through_associations_test.rb
+++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb
@@ -330,6 +330,19 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
assert post.single_people.include?(person)
end
+ def test_both_parent_ids_set_when_saving_new
+ post = Post.new(title: 'Hello', body: 'world')
+ person = Person.new(first_name: 'Sean')
+
+ post.people = [person]
+ post.save
+
+ assert post.id
+ assert person.id
+ assert_equal post.id, post.readers.first.post_id
+ assert_equal person.id, post.readers.first.person_id
+ end
+
def test_delete_association
assert_queries(2){posts(:welcome);people(:michael); }
@@ -476,7 +489,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
post = posts(:welcome)
tag = post.tags.create!(:name => 'doomed')
- assert_difference ['post.reload.taggings_count', 'post.reload.tags_count'], -1 do
+ assert_difference ['post.reload.tags_count'], -1 do
posts(:welcome).tags.delete(tag)
end
end
@@ -486,7 +499,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
tag = post.tags.create!(:name => 'doomed')
post.update_columns(tags_with_destroy_count: post.tags.count)
- assert_difference ['post.reload.taggings_count', 'post.reload.tags_with_destroy_count'], -1 do
+ assert_difference ['post.reload.tags_with_destroy_count'], -1 do
posts(:welcome).tags_with_destroy.delete(tag)
end
end
@@ -496,7 +509,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
tag = post.tags.create!(:name => 'doomed')
post.update_columns(tags_with_nullify_count: post.tags.count)
- assert_no_difference 'post.reload.taggings_count' do
+ assert_no_difference 'post.reload.tags_count' do
assert_difference 'post.reload.tags_with_nullify_count', -1 do
posts(:welcome).tags_with_nullify.delete(tag)
end
@@ -511,14 +524,14 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
tag.tagged_posts = []
post.reload
- assert_equal(post.taggings.count, post.taggings_count)
+ assert_equal(post.taggings.count, post.tags_count)
end
def test_update_counter_caches_on_destroy
post = posts(:welcome)
tag = post.tags.create!(name: 'doomed')
- assert_difference 'post.reload.taggings_count', -1 do
+ assert_difference 'post.reload.tags_count', -1 do
tag.tagged_posts.destroy(post)
end
end
@@ -1126,4 +1139,31 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
assert_equal 2, post.lazy_readers_unscope_skimmers.to_a.size
assert_equal 2, post.lazy_people_unscope_skimmers.to_a.size
end
+
+ def test_has_many_through_add_with_sti_middle_relation
+ club = SuperClub.create!(name: 'Fight Club')
+ member = Member.create!(name: 'Tyler Durden')
+
+ club.members << member
+ assert_equal 1, SuperMembership.where(member_id: member.id, club_id: club.id).count
+ end
+
+ class ClubWithCallbacks < ActiveRecord::Base
+ self.table_name = 'clubs'
+ after_create :add_a_member
+
+ has_many :memberships, inverse_of: :club, foreign_key: :club_id
+ has_many :members, through: :memberships
+
+ def add_a_member
+ members << Member.last
+ end
+ end
+
+ def test_has_many_with_callback_before_association
+ Member.create!
+ club = ClubWithCallbacks.create!
+
+ assert_equal 1, club.reload.memberships.count
+ end
end
diff --git a/activerecord/test/cases/associations/has_one_through_associations_test.rb b/activerecord/test/cases/associations/has_one_through_associations_test.rb
index a2725441b3..089cb0a3a2 100644
--- a/activerecord/test/cases/associations/has_one_through_associations_test.rb
+++ b/activerecord/test/cases/associations/has_one_through_associations_test.rb
@@ -45,6 +45,20 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase
assert_equal clubs(:moustache_club), new_member.club
end
+ def test_creating_association_sets_both_parent_ids_for_new
+ member = Member.new(name: 'Sean Griffin')
+ club = Club.new(name: 'Da Club')
+
+ member.club = club
+
+ member.save!
+
+ assert member.id
+ assert club.id
+ assert_equal member.id, member.current_membership.member_id
+ assert_equal club.id, member.current_membership.club_id
+ end
+
def test_replace_target_record
new_club = Club.create(:name => "Marx Bros")
@member.club = new_club
diff --git a/activerecord/test/cases/associations/inverse_associations_test.rb b/activerecord/test/cases/associations/inverse_associations_test.rb
index a674a39d65..60df4e14dd 100644
--- a/activerecord/test/cases/associations/inverse_associations_test.rb
+++ b/activerecord/test/cases/associations/inverse_associations_test.rb
@@ -100,6 +100,17 @@ class AutomaticInverseFindingTests < ActiveRecord::TestCase
assert_respond_to club_reflection, :has_inverse?
assert !club_reflection.has_inverse?, "A has_many_through association should not find an inverse automatically"
end
+
+ def test_polymorphic_relationships_should_still_not_have_inverses_when_non_polymorphic_relationship_has_the_same_name
+ man_reflection = Man.reflect_on_association(:polymorphic_face_without_inverse)
+ face_reflection = Face.reflect_on_association(:man)
+
+ assert_respond_to face_reflection, :has_inverse?
+ assert face_reflection.has_inverse?, "For this test, the non-polymorphic association must have an inverse"
+
+ assert_respond_to man_reflection, :has_inverse?
+ assert !man_reflection.has_inverse?, "The target of a polymorphic association should not find an inverse automatically"
+ end
end
class InverseAssociationTests < ActiveRecord::TestCase
diff --git a/activerecord/test/cases/associations/join_model_test.rb b/activerecord/test/cases/associations/join_model_test.rb
index aabeea025f..cace7ba142 100644
--- a/activerecord/test/cases/associations/join_model_test.rb
+++ b/activerecord/test/cases/associations/join_model_test.rb
@@ -326,11 +326,11 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
end
def test_belongs_to_polymorphic_with_counter_cache
- assert_equal 1, posts(:welcome)[:taggings_count]
+ assert_equal 1, posts(:welcome)[:tags_count]
tagging = posts(:welcome).taggings.create(:tag => tags(:general))
- assert_equal 2, posts(:welcome, :reload)[:taggings_count]
+ assert_equal 2, posts(:welcome, :reload)[:tags_count]
tagging.destroy
- assert_equal 1, posts(:welcome, :reload)[:taggings_count]
+ assert_equal 1, posts(:welcome, :reload)[:tags_count]
end
def test_unavailable_through_reflection
@@ -489,7 +489,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
message = "Expected a Tag in tags collection, got #{wrong.class}.")
assert_nil( wrong = post_thinking.taggings.detect { |t| t.class != Tagging },
message = "Expected a Tagging in taggings collection, got #{wrong.class}.")
- assert_equal(count + 1, post_thinking.tags.size)
+ assert_equal(count + 1, post_thinking.reload.tags.size)
assert_equal(count + 1, post_thinking.tags(true).size)
assert_kind_of Tag, post_thinking.tags.create!(:name => 'foo')
@@ -497,7 +497,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
message = "Expected a Tag in tags collection, got #{wrong.class}.")
assert_nil( wrong = post_thinking.taggings.detect { |t| t.class != Tagging },
message = "Expected a Tagging in taggings collection, got #{wrong.class}.")
- assert_equal(count + 2, post_thinking.tags.size)
+ assert_equal(count + 2, post_thinking.reload.tags.size)
assert_equal(count + 2, post_thinking.tags(true).size)
assert_nothing_raised { post_thinking.tags.concat(Tag.create!(:name => 'abc'), Tag.create!(:name => 'def')) }
@@ -505,7 +505,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
message = "Expected a Tag in tags collection, got #{wrong.class}.")
assert_nil( wrong = post_thinking.taggings.detect { |t| t.class != Tagging },
message = "Expected a Tagging in taggings collection, got #{wrong.class}.")
- assert_equal(count + 4, post_thinking.tags.size)
+ assert_equal(count + 4, post_thinking.reload.tags.size)
assert_equal(count + 4, post_thinking.tags(true).size)
# Raises if the wrong reflection name is used to set the Edge belongs_to
@@ -554,34 +554,35 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
def test_delete_associate_when_deleting_from_has_many_through
count = posts(:thinking).tags.count
- tags_before = posts(:thinking).tags
+ tags_before = posts(:thinking).tags.sort
tag = Tag.create!(:name => 'doomed')
post_thinking = posts(:thinking)
post_thinking.tags << tag
assert_equal(count + 1, post_thinking.taggings(true).size)
- assert_equal(count + 1, post_thinking.tags(true).size)
+ assert_equal(count + 1, post_thinking.reload.tags(true).size)
+ assert_not_equal(tags_before, post_thinking.tags.sort)
assert_nothing_raised { post_thinking.tags.delete(tag) }
assert_equal(count, post_thinking.tags.size)
assert_equal(count, post_thinking.tags(true).size)
assert_equal(count, post_thinking.taggings(true).size)
- assert_equal(tags_before.sort, post_thinking.tags.sort)
+ assert_equal(tags_before, post_thinking.tags.sort)
end
def test_delete_associate_when_deleting_from_has_many_through_with_multiple_tags
count = posts(:thinking).tags.count
- tags_before = posts(:thinking).tags
+ tags_before = posts(:thinking).tags.sort
doomed = Tag.create!(:name => 'doomed')
doomed2 = Tag.create!(:name => 'doomed2')
quaked = Tag.create!(:name => 'quaked')
post_thinking = posts(:thinking)
post_thinking.tags << doomed << doomed2
- assert_equal(count + 2, post_thinking.tags(true).size)
+ assert_equal(count + 2, post_thinking.reload.tags(true).size)
assert_nothing_raised { post_thinking.tags.delete(doomed, doomed2, quaked) }
assert_equal(count, post_thinking.tags.size)
assert_equal(count, post_thinking.tags(true).size)
- assert_equal(tags_before.sort, post_thinking.tags.sort)
+ assert_equal(tags_before, post_thinking.tags.sort)
end
def test_deleting_junk_from_has_many_through_should_raise_type_mismatch
diff --git a/activerecord/test/cases/associations/nested_through_associations_test.rb b/activerecord/test/cases/associations/nested_through_associations_test.rb
index 8ef351cda8..3720d6d251 100644
--- a/activerecord/test/cases/associations/nested_through_associations_test.rb
+++ b/activerecord/test/cases/associations/nested_through_associations_test.rb
@@ -153,6 +153,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase
end
def test_has_many_through_has_one_with_has_many_through_source_reflection_preload
+ ActiveRecord::Base.connection.table_alias_length # preheat cache
members = assert_queries(4) { Member.includes(:organization_member_details).to_a.sort_by(&:id) }
groucho_details, other_details = member_details(:groucho), member_details(:some_other_guy)
diff --git a/activerecord/test/cases/associations/required_test.rb b/activerecord/test/cases/associations/required_test.rb
new file mode 100644
index 0000000000..a6934a056e
--- /dev/null
+++ b/activerecord/test/cases/associations/required_test.rb
@@ -0,0 +1,82 @@
+require "cases/helper"
+
+class RequiredAssociationsTest < ActiveRecord::TestCase
+ self.use_transactional_fixtures = false
+
+ class Parent < ActiveRecord::Base
+ end
+
+ class Child < ActiveRecord::Base
+ end
+
+ setup do
+ @connection = ActiveRecord::Base.connection
+ @connection.create_table :parents, force: true
+ @connection.create_table :children, force: true do |t|
+ t.belongs_to :parent
+ end
+ end
+
+ teardown do
+ @connection.execute("DROP TABLE IF EXISTS parents")
+ @connection.execute("DROP TABLE IF EXISTS children")
+ end
+
+ test "belongs_to associations are not required by default" do
+ model = subclass_of(Child) do
+ belongs_to :parent, inverse_of: false,
+ class_name: "RequiredAssociationsTest::Parent"
+ end
+
+ assert model.new.save
+ assert model.new(parent: Parent.new).save
+ end
+
+ test "required belongs_to associations have presence validated" do
+ model = subclass_of(Child) do
+ belongs_to :parent, required: true, inverse_of: false,
+ class_name: "RequiredAssociationsTest::Parent"
+ end
+
+ record = model.new
+ assert_not record.save
+ assert_equal ["Parent can't be blank"], record.errors.full_messages
+
+ record.parent = Parent.new
+ assert record.save
+ end
+
+ test "has_one associations are not required by default" do
+ model = subclass_of(Parent) do
+ has_one :child, inverse_of: false,
+ class_name: "RequiredAssociationsTest::Child"
+ end
+
+ assert model.new.save
+ assert model.new(child: Child.new).save
+ end
+
+ test "required has_one associations have presence validated" do
+ model = subclass_of(Parent) do
+ has_one :child, required: true, inverse_of: false,
+ class_name: "RequiredAssociationsTest::Child"
+ end
+
+ record = model.new
+ assert_not record.save
+ assert_equal ["Child can't be blank"], record.errors.full_messages
+
+ record.child = Child.new
+ assert record.save
+ end
+
+ private
+
+ def subclass_of(klass, &block)
+ subclass = Class.new(klass, &block)
+ def subclass.name
+ superclass.name
+ end
+ subclass
+ end
+end
diff --git a/activerecord/test/cases/attribute_decorators_test.rb b/activerecord/test/cases/attribute_decorators_test.rb
index 35393753a2..b352d1a6c2 100644
--- a/activerecord/test/cases/attribute_decorators_test.rb
+++ b/activerecord/test/cases/attribute_decorators_test.rb
@@ -98,17 +98,28 @@ module ActiveRecord
assert_equal 'Hello! decorated!', model.a_string
assert_equal 'whatever', model.another_string
assert_equal 'Hello! decorated! decorated!', child.a_string
- # We are round tripping the default, and we don't undo our decoration
- assert_equal 'whatever decorated! decorated!', child.another_string
+ assert_equal 'whatever decorated!', child.another_string
end
- test "defaults are decorated on the column" do
- Model.attribute :a_string, Type::String.new, default: 'whatever'
- Model.decorate_attribute_type(:a_string, :test) { |t| StringDecorator.new(t) }
+ class Multiplier < SimpleDelegator
+ def type_cast_from_user(value)
+ return if value.nil?
+ value * 2
+ end
+ alias type_cast_from_database type_cast_from_user
+ end
+
+ test "decorating with a proc" do
+ Model.attribute :an_int, Type::Integer.new
+ type_is_integer = proc { |_, type| type.type == :integer }
+ Model.decorate_matching_attribute_types type_is_integer, :multiplier do |type|
+ Multiplier.new(type)
+ end
- column = Model.columns_hash['a_string']
+ model = Model.new(a_string: 'whatever', an_int: 1)
- assert_equal 'whatever decorated!', column.default
+ assert_equal 'whatever', model.a_string
+ assert_equal 2, model.an_int
end
end
end
diff --git a/activerecord/test/cases/attribute_methods/read_test.rb b/activerecord/test/cases/attribute_methods/read_test.rb
index c0659fddef..4741ee8799 100644
--- a/activerecord/test/cases/attribute_methods/read_test.rb
+++ b/activerecord/test/cases/attribute_methods/read_test.rb
@@ -12,6 +12,7 @@ module ActiveRecord
@klass = Class.new do
def self.superclass; Base; end
def self.base_class; self; end
+ def self.decorate_matching_attribute_types(*); end
include ActiveRecord::AttributeMethods
diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb
index 139fe9c04b..2048e0d5ad 100644
--- a/activerecord/test/cases/attribute_methods_test.rb
+++ b/activerecord/test/cases/attribute_methods_test.rb
@@ -253,6 +253,15 @@ class AttributeMethodsTest < ActiveRecord::TestCase
assert_equal @loaded_fixtures['computers']['workstation'].to_hash, Computer.first.attributes
end
+ def test_attributes_without_primary_key
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = 'developers_projects'
+ end
+
+ assert_equal klass.column_names, klass.new.attributes.keys
+ assert_not klass.new.attributes.key?('id')
+ end
+
def test_hashes_not_mangled
new_topic = { :title => "New Topic" }
new_topic_values = { :title => "AnotherTopic" }
@@ -451,10 +460,10 @@ class AttributeMethodsTest < ActiveRecord::TestCase
end
def test_declared_suffixed_attribute_method_affects_respond_to_and_method_missing
- topic = @target.new(:title => 'Budget')
%w(_default _title_default _it! _candidate= able?).each do |suffix|
@target.class_eval "def attribute#{suffix}(*args) args end"
@target.attribute_method_suffix suffix
+ topic = @target.new(:title => 'Budget')
meth = "title#{suffix}"
assert topic.respond_to?(meth)
@@ -465,10 +474,10 @@ class AttributeMethodsTest < ActiveRecord::TestCase
end
def test_declared_affixed_attribute_method_affects_respond_to_and_method_missing
- topic = @target.new(:title => 'Budget')
[['mark_', '_for_update'], ['reset_', '!'], ['default_', '_value?']].each do |prefix, suffix|
@target.class_eval "def #{prefix}attribute#{suffix}(*args) args end"
@target.attribute_method_affix({ :prefix => prefix, :suffix => suffix })
+ topic = @target.new(:title => 'Budget')
meth = "#{prefix}title#{suffix}"
assert topic.respond_to?(meth)
@@ -517,43 +526,17 @@ class AttributeMethodsTest < ActiveRecord::TestCase
end
end
- def test_only_time_related_columns_are_meant_to_be_cached_by_default
- expected = %w(datetime time date).sort
- assert_equal expected, ActiveRecord::Base.attribute_types_cached_by_default.map(&:to_s).sort
- end
-
- def test_declaring_attributes_as_cached_adds_them_to_the_attributes_cached_by_default
- default_attributes = Topic.cached_attributes
- Topic.cache_attributes :replies_count
- expected = default_attributes + ["replies_count"]
- assert_equal expected.sort, Topic.cached_attributes.sort
- Topic.instance_variable_set "@cached_attributes", nil
- end
-
- def test_cacheable_columns_are_actually_cached
- assert_equal cached_columns.sort, Topic.cached_attributes.sort
- end
-
- def test_accessing_cached_attributes_caches_the_converted_values_and_nothing_else
- t = topics(:first)
- cache = t.instance_variable_get "@attributes"
-
- assert_not_nil cache
- assert cache.empty?
+ def test_deprecated_cache_attributes
+ assert_deprecated do
+ Topic.cache_attributes :replies_count
+ end
- all_columns = Topic.columns.map(&:name)
- uncached_columns = all_columns - cached_columns
+ assert_deprecated do
+ Topic.cached_attributes
+ end
- all_columns.each do |attr_name|
- attribute_gets_cached = Topic.cache_attribute?(attr_name)
- val = t.send attr_name unless attr_name == "type"
- if attribute_gets_cached
- assert cached_columns.include?(attr_name)
- assert_equal val, cache[attr_name]
- else
- assert uncached_columns.include?(attr_name)
- assert !cache.include?(attr_name)
- end
+ assert_deprecated do
+ Topic.cache_attribute? :replies_count
end
end
@@ -857,6 +840,37 @@ class AttributeMethodsTest < ActiveRecord::TestCase
end
end
+ def test_attribute_method?
+ assert @target.attribute_method?(:title)
+ assert @target.attribute_method?(:title=)
+ assert_not @target.attribute_method?(:wibble)
+ end
+
+ def test_attribute_method_returns_false_if_table_does_not_exist
+ @target.table_name = 'wibble'
+ assert_not @target.attribute_method?(:title)
+ end
+
+ def test_attribute_names_on_new_record
+ model = @target.new
+
+ assert_equal @target.column_names, model.attribute_names
+ end
+
+ def test_attribute_names_on_queried_record
+ model = @target.last!
+
+ assert_equal @target.column_names, model.attribute_names
+ end
+
+ def test_attribute_names_with_custom_select
+ model = @target.select('id').last!
+
+ assert_equal ['id'], model.attribute_names
+ # Sanity check, make sure other columns exist
+ assert_not_equal ['id'], @target.column_names
+ end
+
private
def new_topic_like_ar_class(&block)
diff --git a/activerecord/test/cases/attribute_set_test.rb b/activerecord/test/cases/attribute_set_test.rb
new file mode 100644
index 0000000000..cdbb11fa32
--- /dev/null
+++ b/activerecord/test/cases/attribute_set_test.rb
@@ -0,0 +1,165 @@
+require 'cases/helper'
+
+module ActiveRecord
+ class AttributeSetTest < ActiveRecord::TestCase
+ test "building a new set from raw attributes" do
+ builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new)
+ attributes = builder.build_from_database(foo: '1.1', bar: '2.2')
+
+ assert_equal 1, attributes[:foo].value
+ assert_equal 2.2, attributes[:bar].value
+ assert_equal :foo, attributes[:foo].name
+ assert_equal :bar, attributes[:bar].name
+ end
+
+ test "building with custom types" do
+ builder = AttributeSet::Builder.new(foo: Type::Float.new)
+ attributes = builder.build_from_database({ foo: '3.3', bar: '4.4' }, { bar: Type::Integer.new })
+
+ assert_equal 3.3, attributes[:foo].value
+ assert_equal 4, attributes[:bar].value
+ end
+
+ test "[] returns a null object" do
+ builder = AttributeSet::Builder.new(foo: Type::Float.new)
+ attributes = builder.build_from_database(foo: '3.3')
+
+ assert_equal '3.3', attributes[:foo].value_before_type_cast
+ assert_equal nil, attributes[:bar].value_before_type_cast
+ assert_equal :bar, attributes[:bar].name
+ end
+
+ test "duping creates a new hash and dups each attribute" do
+ builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::String.new)
+ attributes = builder.build_from_database(foo: 1, bar: 'foo')
+
+ # Ensure the type cast value is cached
+ attributes[:foo].value
+ attributes[:bar].value
+
+ duped = attributes.dup
+ duped.write_from_database(:foo, 2)
+ duped[:bar].value << 'bar'
+
+ assert_equal 1, attributes[:foo].value
+ assert_equal 2, duped[:foo].value
+ assert_equal 'foo', attributes[:bar].value
+ assert_equal 'foobar', duped[:bar].value
+ end
+
+ test "freezing cloned set does not freeze original" do
+ attributes = AttributeSet.new({})
+ clone = attributes.clone
+
+ clone.freeze
+
+ assert clone.frozen?
+ assert_not attributes.frozen?
+ end
+
+ test "to_hash returns a hash of the type cast values" do
+ builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new)
+ attributes = builder.build_from_database(foo: '1.1', bar: '2.2')
+
+ assert_equal({ foo: 1, bar: 2.2 }, attributes.to_hash)
+ assert_equal({ foo: 1, bar: 2.2 }, attributes.to_h)
+ end
+
+ test "values_before_type_cast" do
+ builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Integer.new)
+ attributes = builder.build_from_database(foo: '1.1', bar: '2.2')
+
+ assert_equal({ foo: '1.1', bar: '2.2' }, attributes.values_before_type_cast)
+ end
+
+ test "known columns are built with uninitialized attributes" do
+ attributes = attributes_with_uninitialized_key
+ assert attributes[:foo].initialized?
+ assert_not attributes[:bar].initialized?
+ end
+
+ test "uninitialized attributes are not included in the attributes hash" do
+ attributes = attributes_with_uninitialized_key
+ assert_equal({ foo: 1 }, attributes.to_hash)
+ end
+
+ test "uninitialized attributes are not included in keys" do
+ attributes = attributes_with_uninitialized_key
+ assert_equal [:foo], attributes.keys
+ end
+
+ test "uninitialized attributes return false for include?" do
+ attributes = attributes_with_uninitialized_key
+ assert attributes.include?(:foo)
+ assert_not attributes.include?(:bar)
+ end
+
+ test "unknown attributes return false for include?" do
+ attributes = attributes_with_uninitialized_key
+ assert_not attributes.include?(:wibble)
+ end
+
+ test "fetch_value returns the value for the given initialized attribute" do
+ builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new)
+ attributes = builder.build_from_database(foo: '1.1', bar: '2.2')
+
+ assert_equal 1, attributes.fetch_value(:foo)
+ assert_equal 2.2, attributes.fetch_value(:bar)
+ end
+
+ test "fetch_value returns nil for unknown attributes" do
+ attributes = attributes_with_uninitialized_key
+ assert_nil attributes.fetch_value(:wibble)
+ end
+
+ test "fetch_value uses the given block for uninitialized attributes" do
+ attributes = attributes_with_uninitialized_key
+ value = attributes.fetch_value(:bar) { |n| n.to_s + '!' }
+ assert_equal 'bar!', value
+ end
+
+ test "fetch_value returns nil for uninitialized attributes if no block is given" do
+ attributes = attributes_with_uninitialized_key
+ assert_nil attributes.fetch_value(:bar)
+ end
+
+ class MyType
+ def type_cast_from_user(value)
+ return if value.nil?
+ value + " from user"
+ end
+
+ def type_cast_from_database(value)
+ return if value.nil?
+ value + " from database"
+ end
+ end
+
+ test "write_from_database sets the attribute with database typecasting" do
+ builder = AttributeSet::Builder.new(foo: MyType.new)
+ attributes = builder.build_from_database
+
+ assert_nil attributes.fetch_value(:foo)
+
+ attributes.write_from_database(:foo, "value")
+
+ assert_equal "value from database", attributes.fetch_value(:foo)
+ end
+
+ test "write_from_user sets the attribute with user typecasting" do
+ builder = AttributeSet::Builder.new(foo: MyType.new)
+ attributes = builder.build_from_database
+
+ assert_nil attributes.fetch_value(:foo)
+
+ attributes.write_from_user(:foo, "value")
+
+ assert_equal "value from user", attributes.fetch_value(:foo)
+ end
+
+ def attributes_with_uninitialized_key
+ builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new)
+ builder.build_from_database(foo: '1.1')
+ end
+ end
+end
diff --git a/activerecord/test/cases/attribute_test.rb b/activerecord/test/cases/attribute_test.rb
new file mode 100644
index 0000000000..24452fdec2
--- /dev/null
+++ b/activerecord/test/cases/attribute_test.rb
@@ -0,0 +1,142 @@
+require 'cases/helper'
+require 'minitest/mock'
+
+module ActiveRecord
+ class AttributeTest < ActiveRecord::TestCase
+ setup do
+ @type = MiniTest::Mock.new
+ end
+
+ teardown do
+ assert @type.verify
+ end
+
+ test "from_database + read type casts from database" do
+ @type.expect(:type_cast_from_database, 'type cast from database', ['a value'])
+ attribute = Attribute.from_database(nil, 'a value', @type)
+
+ type_cast_value = attribute.value
+
+ assert_equal 'type cast from database', type_cast_value
+ end
+
+ test "from_user + read type casts from user" do
+ @type.expect(:type_cast_from_user, 'type cast from user', ['a value'])
+ attribute = Attribute.from_user(nil, 'a value', @type)
+
+ type_cast_value = attribute.value
+
+ assert_equal 'type cast from user', type_cast_value
+ end
+
+ test "reading memoizes the value" do
+ @type.expect(:type_cast_from_database, 'from the database', ['whatever'])
+ attribute = Attribute.from_database(nil, 'whatever', @type)
+
+ type_cast_value = attribute.value
+ second_read = attribute.value
+
+ assert_equal 'from the database', type_cast_value
+ assert_same type_cast_value, second_read
+ end
+
+ test "reading memoizes falsy values" do
+ @type.expect(:type_cast_from_database, false, ['whatever'])
+ attribute = Attribute.from_database(nil, 'whatever', @type)
+
+ attribute.value
+ attribute.value
+ end
+
+ test "read_before_typecast returns the given value" do
+ attribute = Attribute.from_database(nil, 'raw value', @type)
+
+ raw_value = attribute.value_before_type_cast
+
+ assert_equal 'raw value', raw_value
+ end
+
+ test "from_database + read_for_database type casts to and from database" do
+ @type.expect(:type_cast_from_database, 'read from database', ['whatever'])
+ @type.expect(:type_cast_for_database, 'ready for database', ['read from database'])
+ attribute = Attribute.from_database(nil, 'whatever', @type)
+
+ type_cast_for_database = attribute.value_for_database
+
+ assert_equal 'ready for database', type_cast_for_database
+ end
+
+ test "from_user + read_for_database type casts from the user to the database" do
+ @type.expect(:type_cast_from_user, 'read from user', ['whatever'])
+ @type.expect(:type_cast_for_database, 'ready for database', ['read from user'])
+ attribute = Attribute.from_user(nil, 'whatever', @type)
+
+ type_cast_for_database = attribute.value_for_database
+
+ assert_equal 'ready for database', type_cast_for_database
+ end
+
+ test "duping dups the value" do
+ @type.expect(:type_cast_from_database, 'type cast', ['a value'])
+ attribute = Attribute.from_database(nil, 'a value', @type)
+
+ value_from_orig = attribute.value
+ value_from_clone = attribute.dup.value
+ value_from_orig << ' foo'
+
+ assert_equal 'type cast foo', value_from_orig
+ assert_equal 'type cast', value_from_clone
+ end
+
+ test "duping does not dup the value if it is not dupable" do
+ @type.expect(:type_cast_from_database, false, ['a value'])
+ attribute = Attribute.from_database(nil, 'a value', @type)
+
+ assert_same attribute.value, attribute.dup.value
+ end
+
+ test "duping does not eagerly type cast if we have not yet type cast" do
+ attribute = Attribute.from_database(nil, 'a value', @type)
+ attribute.dup
+ end
+
+ class MyType
+ def type_cast_from_user(value)
+ value + " from user"
+ end
+
+ def type_cast_from_database(value)
+ value + " from database"
+ end
+ end
+
+ test "with_value_from_user returns a new attribute with the value from the user" do
+ old = Attribute.from_database(nil, "old", MyType.new)
+ new = old.with_value_from_user("new")
+
+ assert_equal "old from database", old.value
+ assert_equal "new from user", new.value
+ end
+
+ test "with_value_from_database returns a new attribute with the value from the database" do
+ old = Attribute.from_user(nil, "old", MyType.new)
+ new = old.with_value_from_database("new")
+
+ assert_equal "old from user", old.value
+ assert_equal "new from database", new.value
+ end
+
+ test "uninitialized attributes yield their name if a block is given to value" do
+ block = proc { |name| name.to_s + "!" }
+ foo = Attribute.uninitialized(:foo, nil)
+ bar = Attribute.uninitialized(:bar, nil)
+
+ assert_equal "foo!", foo.value(&block)
+ assert_equal "bar!", bar.value(&block)
+ end
+
+ test "uninitialized attributes have no value" do
+ assert_nil Attribute.uninitialized(:foo, nil).value
+ end
+ end
+end
diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb
index dc8f25b0e7..8f83cf7cb4 100644
--- a/activerecord/test/cases/base_test.rb
+++ b/activerecord/test/cases/base_test.rb
@@ -1488,15 +1488,14 @@ class BasicsTest < ActiveRecord::TestCase
attrs = topic.attributes.dup
attrs.delete 'id'
- typecast = Class.new {
- def type_cast_from_database value
+ typecast = Class.new(ActiveRecord::Type::Value) {
+ def type_cast value
"t.lo"
end
}
types = { 'author_name' => typecast.new }
- topic = Topic.allocate.init_with 'raw_attributes' => attrs,
- 'column_types' => types
+ topic = Topic.instantiate(attrs, types)
assert_equal 't.lo', topic.author_name
end
@@ -1597,4 +1596,11 @@ class BasicsTest < ActiveRecord::TestCase
assert_equal after_handler, new_handler
assert_equal orig_handler, klass.connection_handler
end
+
+ # Note: This is a performance optimization for Array#uniq and Hash#[] with
+ # AR::Base objects. If the future has made this irrelevant, feel free to
+ # delete this.
+ test "records without an id have unique hashes" do
+ assert_not_equal Post.new.hash, Post.new.hash
+ end
end
diff --git a/activerecord/test/cases/binary_test.rb b/activerecord/test/cases/binary_test.rb
index b41b95309b..ccf2be369d 100644
--- a/activerecord/test/cases/binary_test.rb
+++ b/activerecord/test/cases/binary_test.rb
@@ -21,7 +21,7 @@ unless current_adapter?(:DB2Adapter)
name = binary.name
- # Mysql adapter doesn't properly encode things, so we have to do it
+ # MySQL adapter doesn't properly encode things, so we have to do it
if current_adapter?(:MysqlAdapter)
name.force_encoding(Encoding::UTF_8)
end
diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb
index dfc6a7ec67..319ea9260a 100644
--- a/activerecord/test/cases/calculations_test.rb
+++ b/activerecord/test/cases/calculations_test.rb
@@ -53,11 +53,6 @@ class CalculationsTest < ActiveRecord::TestCase
assert_nil NumericData.average(:bank_balance)
end
- def test_type_cast_calculated_value_should_convert_db_averages_of_fixnum_class_to_decimal
- assert_equal 0, NumericData.all.send(:type_cast_calculated_value, 0, nil, 'avg')
- assert_equal 53.0, NumericData.all.send(:type_cast_calculated_value, 53, nil, 'avg')
- end
-
def test_should_get_maximum_of_field
assert_equal 60, Account.maximum(:credit_limit)
end
@@ -610,4 +605,11 @@ class CalculationsTest < ActiveRecord::TestCase
assert_equal [1,2,3,4,5], taks_relation.pluck(:id)
assert_equal [false, true, true, true, true], taks_relation.pluck(:approved)
end
+
+ def test_pluck_columns_with_same_name
+ expected = [["The First Topic", "The Second Topic of the day"], ["The Third Topic of the day", "The Fourth Topic of the day"]]
+ actual = Topic.joins(:replies)
+ .pluck('topics.title', 'replies_topics.title')
+ assert_equal expected, actual
+ end
end
diff --git a/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb b/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb
index da852aaa02..e1b2804a18 100644
--- a/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb
+++ b/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb
@@ -144,6 +144,18 @@ module ActiveRecord
assert_equal nil, actual[:test]
end
+ def test_database_url_with_ipv6_host_and_port
+ ENV['DATABASE_URL'] = "postgres://[::1]:5454/foo"
+
+ config = {}
+ actual = resolve_config(config)
+ expected = { "adapter" => "postgresql",
+ "database" => "foo",
+ "host" => "::1",
+ "port" => 5454 }
+ assert_equal expected, actual["default_env"]
+ end
+
def test_url_sub_key_with_database_url
ENV['DATABASE_URL'] = "NOT-POSTGRES://localhost/NOT_FOO"
diff --git a/activerecord/test/cases/connection_adapters/schema_cache_test.rb b/activerecord/test/cases/connection_adapters/schema_cache_test.rb
index ecad7c942f..c7531f5418 100644
--- a/activerecord/test/cases/connection_adapters/schema_cache_test.rb
+++ b/activerecord/test/cases/connection_adapters/schema_cache_test.rb
@@ -45,8 +45,8 @@ module ActiveRecord
@cache = Marshal.load(Marshal.dump(@cache))
- assert_equal 12, @cache.columns('posts').size
- assert_equal 12, @cache.columns_hash('posts').size
+ assert_equal 11, @cache.columns('posts').size
+ assert_equal 11, @cache.columns_hash('posts').size
assert @cache.tables('posts')
assert_equal 'id', @cache.primary_keys('posts')
end
diff --git a/activerecord/test/cases/counter_cache_test.rb b/activerecord/test/cases/counter_cache_test.rb
index ab2a749ba8..07a182070b 100644
--- a/activerecord/test/cases/counter_cache_test.rb
+++ b/activerecord/test/cases/counter_cache_test.rb
@@ -19,6 +19,7 @@ class CounterCacheTest < ActiveRecord::TestCase
class ::SpecialTopic < ::Topic
has_many :special_replies, :foreign_key => 'parent_id'
+ has_many :lightweight_special_replies, -> { select('topics.id, topics.title') }, :foreign_key => 'parent_id', :class_name => 'SpecialReply'
end
class ::SpecialReply < ::Reply
@@ -170,4 +171,13 @@ class CounterCacheTest < ActiveRecord::TestCase
end
assert_equal "'Topic' has no association called 'undefined_count'", e.message
end
+
+ test "reset counter works with select declared on association" do
+ special = SpecialTopic.create!(:title => 'Special')
+ SpecialTopic.increment_counter(:replies_count, special.id)
+
+ assert_difference 'special.reload.replies_count', -1 do
+ SpecialTopic.reset_counters(special.id, :lightweight_special_replies)
+ end
+ end
end
diff --git a/activerecord/test/cases/defaults_test.rb b/activerecord/test/cases/defaults_test.rb
index 92144bc802..c089e63128 100644
--- a/activerecord/test/cases/defaults_test.rb
+++ b/activerecord/test/cases/defaults_test.rb
@@ -154,7 +154,7 @@ if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
t.column :omit, :integer, :null => false
end
- assert_equal 0, klass.columns_hash['zero'].default
+ assert_equal '0', klass.columns_hash['zero'].default
assert !klass.columns_hash['zero'].null
# 0 in MySQL 4, nil in 5.
assert [0, nil].include?(klass.columns_hash['omit'].default)
diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb
index 87f24e32b2..ea73c561e9 100644
--- a/activerecord/test/cases/dirty_test.rb
+++ b/activerecord/test/cases/dirty_test.rb
@@ -309,16 +309,14 @@ class DirtyTest < ActiveRecord::TestCase
def test_attribute_will_change!
pirate = Pirate.create!(:catchphrase => 'arr')
- pirate.catchphrase << ' matey'
assert !pirate.catchphrase_changed?
-
assert pirate.catchphrase_will_change!
assert pirate.catchphrase_changed?
- assert_equal ['arr matey', 'arr matey'], pirate.catchphrase_change
+ assert_equal ['arr', 'arr'], pirate.catchphrase_change
- pirate.catchphrase << '!'
+ pirate.catchphrase << ' matey!'
assert pirate.catchphrase_changed?
- assert_equal ['arr matey', 'arr matey!'], pirate.catchphrase_change
+ assert_equal ['arr', 'arr matey!'], pirate.catchphrase_change
end
def test_association_assignment_changes_foreign_key
@@ -445,11 +443,20 @@ class DirtyTest < ActiveRecord::TestCase
def test_save_should_store_serialized_attributes_even_with_partial_writes
with_partial_writes(Topic) do
topic = Topic.create!(:content => {:a => "a"})
+
+ assert_not topic.changed?
+
topic.content[:b] = "b"
- #assert topic.changed? # Known bug, will fail
+
+ assert topic.changed?
+
topic.save!
+
+ assert_not topic.changed?
assert_equal "b", topic.content[:b]
+
topic.reload
+
assert_equal "b", topic.content[:b]
end
end
diff --git a/activerecord/test/cases/dup_test.rb b/activerecord/test/cases/dup_test.rb
index 409d9a82e2..638cffe0e6 100644
--- a/activerecord/test/cases/dup_test.rb
+++ b/activerecord/test/cases/dup_test.rb
@@ -141,5 +141,17 @@ module ActiveRecord
ensure
Topic.default_scopes = prev_default_scopes
end
+
+ def test_dup_without_primary_key
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = 'parrots_pirates'
+ end
+
+ record = klass.create!
+
+ assert_nothing_raised do
+ record.dup
+ end
+ end
end
end
diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb
index 0f52f2c0fc..40e51a0cdc 100644
--- a/activerecord/test/cases/finder_test.rb
+++ b/activerecord/test/cases/finder_test.rb
@@ -34,10 +34,10 @@ class FinderTest < ActiveRecord::TestCase
end
def test_find_with_proc_parameter_and_block
- e = assert_raises(RuntimeError) do
+ exception = assert_raises(RuntimeError) do
Topic.all.find(-> { raise "should happen" }) { |e| e.title == "non-existing-title" }
end
- assert_equal "should happen", e.message
+ assert_equal "should happen", exception.message
assert_nothing_raised(RuntimeError) do
Topic.all.find(-> { raise "should not happen" }) { |e| e.title == topics(:first).title }
@@ -144,8 +144,8 @@ class FinderTest < ActiveRecord::TestCase
def test_exists_with_distinct_association_includes_limit_and_order
author = Author.first
- assert_equal false, author.unique_categorized_posts.includes(:special_comments).order('comments.taggings_count DESC').limit(0).exists?
- assert_equal true, author.unique_categorized_posts.includes(:special_comments).order('comments.taggings_count DESC').limit(1).exists?
+ assert_equal false, author.unique_categorized_posts.includes(:special_comments).order('comments.tags_count DESC').limit(0).exists?
+ assert_equal true, author.unique_categorized_posts.includes(:special_comments).order('comments.tags_count DESC').limit(1).exists?
end
def test_exists_with_empty_table_and_no_args_given
diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb
index 8bbc0af758..042fdaf0bb 100644
--- a/activerecord/test/cases/fixtures_test.rb
+++ b/activerecord/test/cases/fixtures_test.rb
@@ -84,12 +84,6 @@ class FixturesTest < ActiveRecord::TestCase
assert fixtures.detect { |f| f.name == 'collections' }, "no fixtures named 'collections' in #{fixtures.map(&:name).inspect}"
end
- def test_create_symbol_fixtures_is_deprecated
- assert_deprecated do
- ActiveRecord::FixtureSet.create_fixtures(FIXTURES_ROOT, :collections, :collections => 'Course') { Course.connection }
- end
- end
-
def test_attributes
topics = create_fixtures("topics").first
assert_equal("The First Topic", topics["first"]["title"])
diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb
index 93fd3b9605..0c9dff2c25 100644
--- a/activerecord/test/cases/locking_test.rb
+++ b/activerecord/test/cases/locking_test.rb
@@ -273,8 +273,11 @@ class OptimisticLockingTest < ActiveRecord::TestCase
assert RichPerson.connection.select_all("SELECT * FROM peoples_treasures WHERE rich_person_id = 1").empty?
end
- def test_quoted_locking_column_is_deprecated
- assert_deprecated { ActiveRecord::Base.quoted_locking_column }
+ def test_yaml_dumping_with_lock_column
+ t1 = LockWithoutDefault.new
+ t2 = YAML.load(YAML.dump(t1))
+
+ assert_equal t1.attributes, t2.attributes
end
end
diff --git a/activerecord/test/cases/migration/change_schema_test.rb b/activerecord/test/cases/migration/change_schema_test.rb
index 9b26c30d14..c66eaf1ee1 100644
--- a/activerecord/test/cases/migration/change_schema_test.rb
+++ b/activerecord/test/cases/migration/change_schema_test.rb
@@ -68,9 +68,9 @@ module ActiveRecord
five = columns.detect { |c| c.name == "five" } unless mysql
assert_equal "hello", one.default
- assert_equal true, two.default
- assert_equal false, three.default
- assert_equal 1, four.default
+ assert_equal true, two.type_cast_from_database(two.default)
+ assert_equal false, three.type_cast_from_database(three.default)
+ assert_equal '1', four.default
assert_equal "hello", five.default unless mysql
end
@@ -275,7 +275,7 @@ module ActiveRecord
person_klass.connection.add_column "testings", "wealth", :integer, :null => false, :default => 99
person_klass.reset_column_information
- assert_equal 99, person_klass.columns_hash["wealth"].default
+ assert_equal 99, person_klass.column_defaults["wealth"]
assert_equal false, person_klass.columns_hash["wealth"].null
# Oracle needs primary key value from sequence
if current_adapter?(:OracleAdapter)
@@ -287,20 +287,20 @@ module ActiveRecord
# change column default to see that column doesn't lose its not null definition
person_klass.connection.change_column_default "testings", "wealth", 100
person_klass.reset_column_information
- assert_equal 100, person_klass.columns_hash["wealth"].default
+ assert_equal 100, person_klass.column_defaults["wealth"]
assert_equal false, person_klass.columns_hash["wealth"].null
# rename column to see that column doesn't lose its not null and/or default definition
person_klass.connection.rename_column "testings", "wealth", "money"
person_klass.reset_column_information
assert_nil person_klass.columns_hash["wealth"]
- assert_equal 100, person_klass.columns_hash["money"].default
+ assert_equal 100, person_klass.column_defaults["money"]
assert_equal false, person_klass.columns_hash["money"].null
# change column
person_klass.connection.change_column "testings", "money", :integer, :null => false, :default => 1000
person_klass.reset_column_information
- assert_equal 1000, person_klass.columns_hash["money"].default
+ assert_equal 1000, person_klass.column_defaults["money"]
assert_equal false, person_klass.columns_hash["money"].null
# change column, make it nullable and clear default
diff --git a/activerecord/test/cases/migration/column_attributes_test.rb b/activerecord/test/cases/migration/column_attributes_test.rb
index 984d1c2597..93adbdd05b 100644
--- a/activerecord/test/cases/migration/column_attributes_test.rb
+++ b/activerecord/test/cases/migration/column_attributes_test.rb
@@ -62,7 +62,7 @@ module ActiveRecord
# Do a manual insertion
if current_adapter?(:OracleAdapter)
connection.execute "insert into test_models (id, wealth) values (people_seq.nextval, 12345678901234567890.0123456789)"
- elsif current_adapter?(:MysqlAdapter) && Mysql.client_version < 50003 #before mysql 5.0.3 decimals stored as strings
+ elsif current_adapter?(:MysqlAdapter) && Mysql.client_version < 50003 #before MySQL 5.0.3 decimals stored as strings
connection.execute "insert into test_models (wealth) values ('12345678901234567890.0123456789')"
elsif current_adapter?(:PostgreSQLAdapter)
connection.execute "insert into test_models (wealth) values (12345678901234567890.0123456789)"
diff --git a/activerecord/test/cases/migration/columns_test.rb b/activerecord/test/cases/migration/columns_test.rb
index a7c287515d..4e6d7963aa 100644
--- a/activerecord/test/cases/migration/columns_test.rb
+++ b/activerecord/test/cases/migration/columns_test.rb
@@ -53,13 +53,13 @@ module ActiveRecord
add_column 'test_models', 'salary', :integer, :default => 70000
default_before = connection.columns("test_models").find { |c| c.name == "salary" }.default
- assert_equal 70000, default_before
+ assert_equal '70000', default_before
rename_column "test_models", "salary", "annual_salary"
assert TestModel.column_names.include?("annual_salary")
default_after = connection.columns("test_models").find { |c| c.name == "annual_salary" }.default
- assert_equal 70000, default_after
+ assert_equal '70000', default_after
end
if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
@@ -193,14 +193,21 @@ module ActiveRecord
old_columns = connection.columns(TestModel.table_name)
assert old_columns.find { |c|
- c.name == 'approved' && c.type == :boolean && c.default == true
+ default = c.type_cast_from_database(c.default)
+ c.name == 'approved' && c.type == :boolean && default == true
}
change_column :test_models, :approved, :boolean, :default => false
new_columns = connection.columns(TestModel.table_name)
- assert_not new_columns.find { |c| c.name == 'approved' and c.type == :boolean and c.default == true }
- assert new_columns.find { |c| c.name == 'approved' and c.type == :boolean and c.default == false }
+ assert_not new_columns.find { |c|
+ default = c.type_cast_from_database(c.default)
+ c.name == 'approved' and c.type == :boolean and default == true
+ }
+ assert new_columns.find { |c|
+ default = c.type_cast_from_database(c.default)
+ c.name == 'approved' and c.type == :boolean and default == false
+ }
change_column :test_models, :approved, :boolean, :default => true
end
diff --git a/activerecord/test/cases/migration/command_recorder_test.rb b/activerecord/test/cases/migration/command_recorder_test.rb
index a925cf4c05..e955beae1a 100644
--- a/activerecord/test/cases/migration/command_recorder_test.rb
+++ b/activerecord/test/cases/migration/command_recorder_test.rb
@@ -157,6 +157,23 @@ module ActiveRecord
assert_equal [:remove_column, [:table, :column, :type, {}], nil], remove
end
+ def test_invert_change_column
+ assert_raises(ActiveRecord::IrreversibleMigration) do
+ @recorder.inverse_of :change_column, [:table, :column, :type, {}]
+ end
+ end
+
+ def test_invert_change_column_default
+ assert_raises(ActiveRecord::IrreversibleMigration) do
+ @recorder.inverse_of :change_column_default, [:table, :column, 'default_value']
+ end
+ end
+
+ def test_invert_change_column_null
+ add = @recorder.inverse_of :change_column_null, [:table, :column, true]
+ assert_equal [:change_column_null, [:table, :column, false]], add
+ end
+
def test_invert_remove_column
add = @recorder.inverse_of :remove_column, [:table, :column, :type, {}]
assert_equal [:add_column, [:table, :column, :type, {}], nil], add
@@ -253,6 +270,31 @@ module ActiveRecord
enable = @recorder.inverse_of :disable_extension, ['uuid-ossp']
assert_equal [:enable_extension, ['uuid-ossp'], nil], enable
end
+
+ def test_invert_add_foreign_key
+ enable = @recorder.inverse_of :add_foreign_key, [:dogs, :people]
+ assert_equal [:remove_foreign_key, [:dogs, :people]], enable
+ end
+
+ def test_invert_add_foreign_key_with_column
+ enable = @recorder.inverse_of :add_foreign_key, [:dogs, :people, column: "owner_id"]
+ assert_equal [:remove_foreign_key, [:dogs, column: "owner_id"]], enable
+ end
+
+ def test_invert_add_foreign_key_with_column_and_name
+ enable = @recorder.inverse_of :add_foreign_key, [:dogs, :people, column: "owner_id", name: "fk"]
+ assert_equal [:remove_foreign_key, [:dogs, name: "fk"]], enable
+ end
+
+ def test_remove_foreign_key_is_irreversible
+ assert_raises ActiveRecord::IrreversibleMigration do
+ @recorder.inverse_of :remove_foreign_key, [:dogs, column: "owner_id"]
+ end
+
+ assert_raises ActiveRecord::IrreversibleMigration do
+ @recorder.inverse_of :remove_foreign_key, [:dogs, name: "fk"]
+ end
+ end
end
end
end
diff --git a/activerecord/test/cases/migration/foreign_key_test.rb b/activerecord/test/cases/migration/foreign_key_test.rb
new file mode 100644
index 0000000000..c985092b4c
--- /dev/null
+++ b/activerecord/test/cases/migration/foreign_key_test.rb
@@ -0,0 +1,242 @@
+require 'cases/helper'
+require 'support/ddl_helper'
+require 'support/schema_dumping_helper'
+
+if ActiveRecord::Base.connection.supports_foreign_keys?
+module ActiveRecord
+ class Migration
+ class ForeignKeyTest < ActiveRecord::TestCase
+ include DdlHelper
+ include SchemaDumpingHelper
+
+ class Rocket < ActiveRecord::Base
+ end
+
+ class Astronaut < ActiveRecord::Base
+ end
+
+ setup do
+ @connection = ActiveRecord::Base.connection
+ @connection.create_table "rockets" do |t|
+ t.string :name
+ end
+
+ @connection.create_table "astronauts" do |t|
+ t.string :name
+ t.references :rocket
+ end
+ end
+
+ teardown do
+ if defined?(@connection)
+ @connection.execute "DROP TABLE IF EXISTS astronauts"
+ @connection.execute "DROP TABLE IF EXISTS rockets"
+ end
+ end
+
+ def test_foreign_keys
+ foreign_keys = @connection.foreign_keys("fk_test_has_fk")
+ assert_equal 1, foreign_keys.size
+
+ fk = foreign_keys.first
+ assert_equal "fk_test_has_fk", fk.from_table
+ assert_equal "fk_test_has_pk", fk.to_table
+ assert_equal "fk_id", fk.column
+ assert_equal "pk_id", fk.primary_key
+ assert_equal "fk_name", fk.name
+ end
+
+ def test_add_foreign_key_inferes_column
+ @connection.add_foreign_key :astronauts, :rockets
+
+ foreign_keys = @connection.foreign_keys("astronauts")
+ assert_equal 1, foreign_keys.size
+
+ fk = foreign_keys.first
+ assert_equal "astronauts", fk.from_table
+ assert_equal "rockets", fk.to_table
+ assert_equal "rocket_id", fk.column
+ assert_equal "id", fk.primary_key
+ assert_match(/^fk_rails_.{10}$/, fk.name)
+ end
+
+ def test_add_foreign_key_with_column
+ @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id"
+
+ foreign_keys = @connection.foreign_keys("astronauts")
+ assert_equal 1, foreign_keys.size
+
+ fk = foreign_keys.first
+ assert_equal "astronauts", fk.from_table
+ assert_equal "rockets", fk.to_table
+ assert_equal "rocket_id", fk.column
+ assert_equal "id", fk.primary_key
+ assert_match(/^fk_rails_.{10}$/, fk.name)
+ end
+
+ def test_add_foreign_key_with_non_standard_primary_key
+ with_example_table @connection, "space_shuttles", "pk integer PRIMARY KEY" do
+ @connection.add_foreign_key(:astronauts, :space_shuttles,
+ column: "rocket_id", primary_key: "pk", name: "custom_pk")
+
+ foreign_keys = @connection.foreign_keys("astronauts")
+ assert_equal 1, foreign_keys.size
+
+ fk = foreign_keys.first
+ assert_equal "astronauts", fk.from_table
+ assert_equal "space_shuttles", fk.to_table
+ assert_equal "pk", fk.primary_key
+
+ @connection.remove_foreign_key :astronauts, name: "custom_pk"
+ end
+ end
+
+ def test_add_on_delete_restrict_foreign_key
+ @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_delete: :restrict
+
+ foreign_keys = @connection.foreign_keys("astronauts")
+ assert_equal 1, foreign_keys.size
+
+ fk = foreign_keys.first
+ if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
+ # ON DELETE RESTRICT is the default on MySQL
+ assert_equal nil, fk.on_delete
+ else
+ assert_equal :restrict, fk.on_delete
+ end
+ end
+
+ def test_add_on_delete_cascade_foreign_key
+ @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_delete: :cascade
+
+ foreign_keys = @connection.foreign_keys("astronauts")
+ assert_equal 1, foreign_keys.size
+
+ fk = foreign_keys.first
+ assert_equal :cascade, fk.on_delete
+ end
+
+ def test_add_on_delete_nullify_foreign_key
+ @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_delete: :nullify
+
+ foreign_keys = @connection.foreign_keys("astronauts")
+ assert_equal 1, foreign_keys.size
+
+ fk = foreign_keys.first
+ assert_equal :nullify, fk.on_delete
+ end
+
+ def test_on_update_and_on_delete_raises_with_invalid_values
+ assert_raises ArgumentError do
+ @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_delete: :invalid
+ end
+
+ assert_raises ArgumentError do
+ @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_update: :invalid
+ end
+ end
+
+ def test_add_foreign_key_with_on_update
+ @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_update: :nullify
+
+ foreign_keys = @connection.foreign_keys("astronauts")
+ assert_equal 1, foreign_keys.size
+
+ fk = foreign_keys.first
+ assert_equal :nullify, fk.on_update
+ end
+
+ def test_remove_foreign_key_inferes_column
+ @connection.add_foreign_key :astronauts, :rockets
+
+ assert_equal 1, @connection.foreign_keys("astronauts").size
+ @connection.remove_foreign_key :astronauts, :rockets
+ assert_equal [], @connection.foreign_keys("astronauts")
+ end
+
+ def test_remove_foreign_key_by_column
+ @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id"
+
+ assert_equal 1, @connection.foreign_keys("astronauts").size
+ @connection.remove_foreign_key :astronauts, column: "rocket_id"
+ assert_equal [], @connection.foreign_keys("astronauts")
+ end
+
+ def test_remove_foreign_key_by_name
+ @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", name: "fancy_named_fk"
+
+ assert_equal 1, @connection.foreign_keys("astronauts").size
+ @connection.remove_foreign_key :astronauts, name: "fancy_named_fk"
+ assert_equal [], @connection.foreign_keys("astronauts")
+ end
+
+ def test_remove_foreign_non_existing_foreign_key_raises
+ assert_raises ArgumentError do
+ @connection.remove_foreign_key :astronauts, :rockets
+ end
+ end
+
+ def test_schema_dumping
+ @connection.add_foreign_key :astronauts, :rockets
+ output = dump_table_schema "astronauts"
+ assert_match %r{\s+add_foreign_key "astronauts", "rockets"$}, output
+ end
+
+ def test_schema_dumping_with_options
+ output = dump_table_schema "fk_test_has_fk"
+ assert_match %r{\s+add_foreign_key "fk_test_has_fk", "fk_test_has_pk", column: "fk_id", primary_key: "pk_id", name: "fk_name"$}, output
+ end
+
+ def test_schema_dumping_on_delete_and_on_update_options
+ @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_delete: :nullify, on_update: :cascade
+
+ output = dump_table_schema "astronauts"
+ assert_match %r{\s+add_foreign_key "astronauts",.+on_update: :cascade,.+on_delete: :nullify$}, output
+ end
+
+ class CreateCitiesAndHousesMigration < ActiveRecord::Migration
+ def change
+ create_table("cities") { |t| }
+
+ create_table("houses") do |t|
+ t.column :city_id, :integer
+ end
+ add_foreign_key :houses, :cities, column: "city_id"
+ end
+ end
+
+ def test_add_foreign_key_is_reversible
+ migration = CreateCitiesAndHousesMigration.new
+ silence_stream($stdout) { migration.migrate(:up) }
+ assert_equal 1, @connection.foreign_keys("houses").size
+ ensure
+ silence_stream($stdout) { migration.migrate(:down) }
+ end
+ end
+ end
+end
+else
+module ActiveRecord
+ class Migration
+ class NoForeignKeySupportTest < ActiveRecord::TestCase
+ setup do
+ @connection = ActiveRecord::Base.connection
+ end
+
+ def test_add_foreign_key_should_be_noop
+ @connection.add_foreign_key :clubs, :categories
+ end
+
+ def test_remove_foreign_key_should_be_noop
+ @connection.remove_foreign_key :clubs, :categories
+ end
+
+ def test_foreign_keys_should_raise_not_implemented
+ assert_raises NotImplementedError do
+ @connection.foreign_keys("clubs")
+ end
+ end
+ end
+ end
+end
+end
diff --git a/activerecord/test/cases/migration/logger_test.rb b/activerecord/test/cases/migration/logger_test.rb
index 84224e6e4c..319d3e1af3 100644
--- a/activerecord/test/cases/migration/logger_test.rb
+++ b/activerecord/test/cases/migration/logger_test.rb
@@ -3,7 +3,7 @@ require "cases/helper"
module ActiveRecord
class Migration
class LoggerTest < ActiveRecord::TestCase
- # mysql can't roll back ddl changes
+ # MySQL can't roll back ddl changes
self.use_transactional_fixtures = false
Migration = Struct.new(:name, :version) do
diff --git a/activerecord/test/cases/migration/pending_migrations_test.rb b/activerecord/test/cases/migration/pending_migrations_test.rb
new file mode 100644
index 0000000000..517ee695ce
--- /dev/null
+++ b/activerecord/test/cases/migration/pending_migrations_test.rb
@@ -0,0 +1,53 @@
+require 'cases/helper'
+require "minitest/mock"
+
+module ActiveRecord
+ class Migration
+ class PendingMigrationsTest < ActiveRecord::TestCase
+ def setup
+ super
+ @connection = MiniTest::Mock.new
+ @app = MiniTest::Mock.new
+ conn = @connection
+ @pending = Class.new(CheckPending) {
+ define_method(:connection) { conn }
+ }.new(@app)
+ @pending.instance_variable_set :@last_check, -1 # Force checking
+ end
+
+ def teardown
+ assert @connection.verify
+ assert @app.verify
+ super
+ end
+
+ def test_errors_if_pending
+ @connection.expect :supports_migrations?, true
+
+ ActiveRecord::Migrator.stub :needs_migration?, true do
+ assert_raise ActiveRecord::PendingMigrationError do
+ @pending.call(nil)
+ end
+ end
+ end
+
+ def test_checks_if_supported
+ @connection.expect :supports_migrations?, true
+ @app.expect :call, nil, [:foo]
+
+ ActiveRecord::Migrator.stub :needs_migration?, false do
+ @pending.call(:foo)
+ end
+ end
+
+ def test_doesnt_check_if_unsupported
+ @connection.expect :supports_migrations?, false
+ @app.expect :call, nil, [:foo]
+
+ ActiveRecord::Migrator.stub :needs_migration?, true do
+ @pending.call(:foo)
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/migration/rename_table_test.rb b/activerecord/test/cases/migration/rename_table_test.rb
index a52b58c4ac..ba39fb1dec 100644
--- a/activerecord/test/cases/migration/rename_table_test.rb
+++ b/activerecord/test/cases/migration/rename_table_test.rb
@@ -76,6 +76,16 @@ module ActiveRecord
assert_equal ConnectionAdapters::PostgreSQL::Name.new("public", "octopi_#{pk}_seq"), seq
end
+
+ def test_renaming_table_doesnt_attempt_to_rename_non_existent_sequences
+ enable_uuid_ossp!(connection)
+ connection.create_table :cats, id: :uuid
+ assert_nothing_raised { rename_table :cats, :felines }
+ assert connection.table_exists? :felines
+ ensure
+ connection.drop_table :cats if connection.table_exists? :cats
+ connection.drop_table :felines if connection.table_exists? :felines
+ end
end
end
end
diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb
index 9855835e27..6b840e16bb 100644
--- a/activerecord/test/cases/migration_test.rb
+++ b/activerecord/test/cases/migration_test.rb
@@ -567,7 +567,7 @@ if ActiveRecord::Base.connection.supports_bulk_alter?
assert_equal 8, columns.size
[:name, :qualification, :experience].each {|s| assert_equal :string, column(s).type }
- assert_equal 0, column(:age).default
+ assert_equal '0', column(:age).default
end
def test_removing_columns
diff --git a/activerecord/test/cases/persistence_test.rb b/activerecord/test/cases/persistence_test.rb
index 7f5d0d66d3..2170fe6118 100644
--- a/activerecord/test/cases/persistence_test.rb
+++ b/activerecord/test/cases/persistence_test.rb
@@ -20,7 +20,7 @@ require 'models/toy'
require 'rexml/document'
class PersistenceTest < ActiveRecord::TestCase
- fixtures :topics, :companies, :developers, :projects, :computers, :accounts, :minimalistics, 'warehouse-things', :authors, :categorizations, :categories, :posts, :minivans, :pets, :toys
+ fixtures :topics, :companies, :developers, :projects, :computers, :accounts, :minimalistics, 'warehouse-things', :authors, :author_addresses, :categorizations, :categories, :posts, :minivans, :pets, :toys
# Oracle UPDATE does not support ORDER BY
unless current_adapter?(:OracleAdapter)
@@ -250,11 +250,9 @@ class PersistenceTest < ActiveRecord::TestCase
end
def test_create_columns_not_equal_attributes
- topic = Topic.allocate.init_with(
- 'raw_attributes' => {
- 'title' => 'Another New Topic',
- 'does_not_exist' => 'test'
- }
+ topic = Topic.instantiate(
+ 'title' => 'Another New Topic',
+ 'does_not_exist' => 'test'
)
assert_nothing_raised { topic.save }
end
@@ -300,10 +298,7 @@ class PersistenceTest < ActiveRecord::TestCase
topic.title = "Still another topic"
topic.save
- topic_reloaded = Topic.allocate
- topic_reloaded.init_with(
- 'raw_attributes' => topic.attributes.merge('does_not_exist' => 'test')
- )
+ topic_reloaded = Topic.instantiate(topic.attributes.merge('does_not_exist' => 'test'))
topic_reloaded.title = 'A New Topic'
assert_nothing_raised { topic_reloaded.save }
end
@@ -514,14 +509,14 @@ class PersistenceTest < ActiveRecord::TestCase
def test_update_column_should_not_leave_the_object_dirty
topic = Topic.find(1)
- topic.update_column("content", "Have a nice day")
+ topic.update_column("content", "--- Have a nice day\n...\n")
topic.reload
- topic.update_column(:content, "You too")
+ topic.update_column(:content, "--- You too\n...\n")
assert_equal [], topic.changed
topic.reload
- topic.update_column("content", "Have a nice day")
+ topic.update_column("content", "--- Have a nice day\n...\n")
assert_equal [], topic.changed
end
@@ -605,14 +600,14 @@ class PersistenceTest < ActiveRecord::TestCase
def test_update_columns_should_not_leave_the_object_dirty
topic = Topic.find(1)
- topic.update({ "content" => "Have a nice day", :author_name => "Jose" })
+ topic.update({ "content" => "--- Have a nice day\n...\n", :author_name => "Jose" })
topic.reload
- topic.update_columns({ content: "You too", "author_name" => "Sebastian" })
+ topic.update_columns({ content: "--- You too\n...\n", "author_name" => "Sebastian" })
assert_equal [], topic.changed
topic.reload
- topic.update_columns({ content: "Have a nice day", author_name: "Jose" })
+ topic.update_columns({ content: "--- Have a nice day\n...\n", author_name: "Jose" })
assert_equal [], topic.changed
end
@@ -863,4 +858,23 @@ class PersistenceTest < ActiveRecord::TestCase
post.body
end
end
+
+ def test_reload_removes_custom_selects
+ post = Post.select('posts.*, 1 as wibble').last!
+
+ assert_equal 1, post[:wibble]
+ assert_nil post.reload[:wibble]
+ end
+
+ def test_find_via_reload
+ post = Post.new
+
+ assert post.new_record?
+
+ post.id = 1
+ post.reload
+
+ assert_equal "Welcome to the weblog", post.title
+ assert_not post.new_record?
+ end
end
diff --git a/activerecord/test/cases/primary_keys_test.rb b/activerecord/test/cases/primary_keys_test.rb
index f43483d291..b04df7ce43 100644
--- a/activerecord/test/cases/primary_keys_test.rb
+++ b/activerecord/test/cases/primary_keys_test.rb
@@ -134,14 +134,22 @@ class PrimaryKeysTest < ActiveRecord::TestCase
end
def test_primary_key_returns_value_if_it_exists
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = 'developers'
+ end
+
if ActiveRecord::Base.connection.supports_primary_key?
- assert_equal 'id', ActiveRecord::Base.connection.primary_key('developers')
+ assert_equal 'id', klass.primary_key
end
end
def test_primary_key_returns_nil_if_it_does_not_exist
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = 'developers_projects'
+ end
+
if ActiveRecord::Base.connection.supports_primary_key?
- assert_nil ActiveRecord::Base.connection.primary_key('developers_projects')
+ assert_nil klass.primary_key
end
end
diff --git a/activerecord/test/cases/quoting_test.rb b/activerecord/test/cases/quoting_test.rb
index bbd5298da1..1d6ae2f67f 100644
--- a/activerecord/test/cases/quoting_test.rb
+++ b/activerecord/test/cases/quoting_test.rb
@@ -83,69 +83,56 @@ module ActiveRecord
def test_quote_with_quoted_id
assert_equal 1, @quoter.quote(Struct.new(:quoted_id).new(1), nil)
- assert_equal 1, @quoter.quote(Struct.new(:quoted_id).new(1), 'foo')
end
def test_quote_nil
assert_equal 'NULL', @quoter.quote(nil, nil)
- assert_equal 'NULL', @quoter.quote(nil, 'foo')
end
def test_quote_true
assert_equal @quoter.quoted_true, @quoter.quote(true, nil)
- assert_equal '1', @quoter.quote(true, Type::Integer.new)
end
def test_quote_false
assert_equal @quoter.quoted_false, @quoter.quote(false, nil)
- assert_equal '0', @quoter.quote(false, Type::Integer.new)
end
def test_quote_float
float = 1.2
assert_equal float.to_s, @quoter.quote(float, nil)
- assert_equal float.to_s, @quoter.quote(float, Object.new)
end
def test_quote_fixnum
fixnum = 1
assert_equal fixnum.to_s, @quoter.quote(fixnum, nil)
- assert_equal fixnum.to_s, @quoter.quote(fixnum, Object.new)
end
def test_quote_bignum
bignum = 1 << 100
assert_equal bignum.to_s, @quoter.quote(bignum, nil)
- assert_equal bignum.to_s, @quoter.quote(bignum, Object.new)
end
def test_quote_bigdecimal
bigdec = BigDecimal.new((1 << 100).to_s)
assert_equal bigdec.to_s('F'), @quoter.quote(bigdec, nil)
- assert_equal bigdec.to_s('F'), @quoter.quote(bigdec, Object.new)
end
def test_dates_and_times
@quoter.extend(Module.new { def quoted_date(value) 'lol' end })
assert_equal "'lol'", @quoter.quote(Date.today, nil)
- assert_equal "'lol'", @quoter.quote(Date.today, Object.new)
assert_equal "'lol'", @quoter.quote(Time.now, nil)
- assert_equal "'lol'", @quoter.quote(Time.now, Object.new)
assert_equal "'lol'", @quoter.quote(DateTime.now, nil)
- assert_equal "'lol'", @quoter.quote(DateTime.now, Object.new)
end
def test_crazy_object
crazy = Class.new.new
expected = "'#{YAML.dump(crazy)}'"
assert_equal expected, @quoter.quote(crazy, nil)
- assert_equal expected, @quoter.quote(crazy, Object.new)
end
def test_crazy_object_calls_quote_string
crazy = Class.new { def initialize; @lol = 'lo\l' end }.new
assert_match "lo\\\\l", @quoter.quote(crazy, nil)
- assert_match "lo\\\\l", @quoter.quote(crazy, Object.new)
end
def test_quote_string_no_column
@@ -157,25 +144,6 @@ module ActiveRecord
assert_equal "'lo\\\\l'", @quoter.quote(string, nil)
end
- def test_quote_string_int_column
- assert_equal "1", @quoter.quote('1', Type::Integer.new)
- assert_equal "1", @quoter.quote('1.2', Type::Integer.new)
- end
-
- def test_quote_string_float_column
- assert_equal "1.0", @quoter.quote('1', Type::Float.new)
- assert_equal "1.2", @quoter.quote('1.2', Type::Float.new)
- end
-
- def test_quote_as_mb_chars_binary_column
- string = ActiveSupport::Multibyte::Chars.new('lo\l')
- assert_equal "'lo\\\\l'", @quoter.quote(string, Type::Binary.new)
- end
-
- def test_quote_binary_without_string_to_binary
- assert_equal "'lo\\\\l'", @quoter.quote('lo\l', Type::Binary.new)
- end
-
def test_string_with_crazy_column
assert_equal "'lo\\\\l'", @quoter.quote('lo\l')
end
@@ -183,10 +151,6 @@ module ActiveRecord
def test_quote_duration
assert_equal "1800", @quoter.quote(30.minutes)
end
-
- def test_quote_duration_int_column
- assert_equal "7200", @quoter.quote(2.hours, Type::Integer.new)
- end
end
end
end
diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb
index c6e73f78cc..84abaf0291 100644
--- a/activerecord/test/cases/reflection_test.rb
+++ b/activerecord/test/cases/reflection_test.rb
@@ -80,28 +80,14 @@ class ReflectionTest < ActiveRecord::TestCase
assert_equal :integer, @first.column_for_attribute("id").type
end
- def test_non_existent_columns_return_null_object
- column = @first.column_for_attribute("attribute_that_doesnt_exist")
- assert_instance_of ActiveRecord::ConnectionAdapters::NullColumn, column
- 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")
- 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)
+ def test_non_existent_columns_return_nil
+ assert_deprecated do
+ assert_nil @first.column_for_attribute("attribute_that_doesnt_exist")
+ end
end
def test_reflection_klass_for_nested_class_name
- reflection = MacroReflection.new(:company, nil, nil, { :class_name => 'MyApplication::Business::Company' }, ActiveRecord::Base)
+ reflection = ActiveRecord::Reflection.create(:has_many, nil, nil, { :class_name => 'MyApplication::Business::Company' }, ActiveRecord::Base)
assert_nothing_raised do
assert_equal MyApplication::Business::Company, reflection.klass
end
@@ -111,21 +97,21 @@ class ReflectionTest < ActiveRecord::TestCase
ActiveSupport::Inflector.inflections do |inflect|
inflect.irregular 'plural_irregular', 'plurales_irregulares'
end
- reflection = AssociationReflection.new(:has_many, 'plurales_irregulares', nil, {}, ActiveRecord::Base)
+ reflection = ActiveRecord::Reflection.create(:has_many, 'plurales_irregulares', nil, {}, ActiveRecord::Base)
assert_equal 'PluralIrregular', reflection.class_name
end
def test_aggregation_reflection
reflection_for_address = AggregateReflection.new(
- :composed_of, :address, nil, { :mapping => [ %w(address_street street), %w(address_city city), %w(address_country country) ] }, Customer
+ :address, nil, { :mapping => [ %w(address_street street), %w(address_city city), %w(address_country country) ] }, Customer
)
reflection_for_balance = AggregateReflection.new(
- :composed_of, :balance, nil, { :class_name => "Money", :mapping => %w(balance amount) }, Customer
+ :balance, nil, { :class_name => "Money", :mapping => %w(balance amount) }, Customer
)
reflection_for_gps_location = AggregateReflection.new(
- :composed_of, :gps_location, nil, { }, Customer
+ :gps_location, nil, { }, Customer
)
assert Customer.reflect_on_all_aggregations.include?(reflection_for_gps_location)
@@ -149,7 +135,7 @@ class ReflectionTest < ActiveRecord::TestCase
end
def test_has_many_reflection
- reflection_for_clients = AssociationReflection.new(:has_many, :clients, nil, { :order => "id", :dependent => :destroy }, Firm)
+ reflection_for_clients = ActiveRecord::Reflection.create(:has_many, :clients, nil, { :order => "id", :dependent => :destroy }, Firm)
assert_equal reflection_for_clients, Firm.reflect_on_association(:clients)
@@ -161,7 +147,7 @@ class ReflectionTest < ActiveRecord::TestCase
end
def test_has_one_reflection
- reflection_for_account = AssociationReflection.new(:has_one, :account, nil, { :foreign_key => "firm_id", :dependent => :destroy }, Firm)
+ reflection_for_account = ActiveRecord::Reflection.create(:has_one, :account, nil, { :foreign_key => "firm_id", :dependent => :destroy }, Firm)
assert_equal reflection_for_account, Firm.reflect_on_association(:account)
assert_equal Account, Firm.reflect_on_association(:account).klass
@@ -298,12 +284,12 @@ class ReflectionTest < ActiveRecord::TestCase
end
def test_association_primary_key_raises_when_missing_primary_key
- reflection = ActiveRecord::Reflection::AssociationReflection.new(:fuu, :edge, nil, {}, Author)
+ reflection = ActiveRecord::Reflection.create(:has_many, :edge, nil, {}, Author)
assert_raises(ActiveRecord::UnknownPrimaryKey) { reflection.association_primary_key }
through = Class.new(ActiveRecord::Reflection::ThroughReflection) {
define_method(:source_reflection) { reflection }
- }.new(:fuu, :edge, nil, {}, Author)
+ }.new(reflection)
assert_raises(ActiveRecord::UnknownPrimaryKey) { through.association_primary_key }
end
@@ -313,7 +299,7 @@ class ReflectionTest < ActiveRecord::TestCase
end
def test_active_record_primary_key_raises_when_missing_primary_key
- reflection = ActiveRecord::Reflection::AssociationReflection.new(:fuu, :author, nil, {}, Edge)
+ reflection = ActiveRecord::Reflection.create(:has_many, :author, nil, {}, Edge)
assert_raises(ActiveRecord::UnknownPrimaryKey) { reflection.active_record_primary_key }
end
@@ -331,32 +317,28 @@ class ReflectionTest < ActiveRecord::TestCase
end
def test_default_association_validation
- assert AssociationReflection.new(:has_many, :clients, nil, {}, Firm).validate?
+ assert ActiveRecord::Reflection.create(:has_many, :clients, nil, {}, Firm).validate?
- assert !AssociationReflection.new(:has_one, :client, nil, {}, Firm).validate?
- assert !AssociationReflection.new(:belongs_to, :client, nil, {}, Firm).validate?
- assert !AssociationReflection.new(:has_and_belongs_to_many, :clients, nil, {}, Firm).validate?
+ assert !ActiveRecord::Reflection.create(:has_one, :client, nil, {}, Firm).validate?
+ assert !ActiveRecord::Reflection.create(:belongs_to, :client, nil, {}, Firm).validate?
end
def test_always_validate_association_if_explicit
- assert AssociationReflection.new(:has_one, :client, nil, { :validate => true }, Firm).validate?
- assert AssociationReflection.new(:belongs_to, :client, nil, { :validate => true }, Firm).validate?
- assert AssociationReflection.new(:has_many, :clients, nil, { :validate => true }, Firm).validate?
- assert AssociationReflection.new(:has_and_belongs_to_many, :clients, nil, { :validate => true }, Firm).validate?
+ assert ActiveRecord::Reflection.create(:has_one, :client, nil, { :validate => true }, Firm).validate?
+ assert ActiveRecord::Reflection.create(:belongs_to, :client, nil, { :validate => true }, Firm).validate?
+ assert ActiveRecord::Reflection.create(:has_many, :clients, nil, { :validate => true }, Firm).validate?
end
def test_validate_association_if_autosave
- assert AssociationReflection.new(:has_one, :client, nil, { :autosave => true }, Firm).validate?
- assert AssociationReflection.new(:belongs_to, :client, nil, { :autosave => true }, Firm).validate?
- assert AssociationReflection.new(:has_many, :clients, nil, { :autosave => true }, Firm).validate?
- assert AssociationReflection.new(:has_and_belongs_to_many, :clients, nil, { :autosave => true }, Firm).validate?
+ assert ActiveRecord::Reflection.create(:has_one, :client, nil, { :autosave => true }, Firm).validate?
+ assert ActiveRecord::Reflection.create(:belongs_to, :client, nil, { :autosave => true }, Firm).validate?
+ assert ActiveRecord::Reflection.create(:has_many, :clients, nil, { :autosave => true }, Firm).validate?
end
def test_never_validate_association_if_explicit
- assert !AssociationReflection.new(:has_one, :client, nil, { :autosave => true, :validate => false }, Firm).validate?
- assert !AssociationReflection.new(:belongs_to, :client, nil, { :autosave => true, :validate => false }, Firm).validate?
- assert !AssociationReflection.new(:has_many, :clients, nil, { :autosave => true, :validate => false }, Firm).validate?
- assert !AssociationReflection.new(:has_and_belongs_to_many, :clients, nil, { :autosave => true, :validate => false }, Firm).validate?
+ assert !ActiveRecord::Reflection.create(:has_one, :client, nil, { :autosave => true, :validate => false }, Firm).validate?
+ assert !ActiveRecord::Reflection.create(:belongs_to, :client, nil, { :autosave => true, :validate => false }, Firm).validate?
+ assert !ActiveRecord::Reflection.create(:has_many, :clients, nil, { :autosave => true, :validate => false }, Firm).validate?
end
def test_foreign_key
@@ -378,11 +360,11 @@ class ReflectionTest < ActiveRecord::TestCase
category = Struct.new(:table_name, :pluralize_table_names).new('categories', true)
product = Struct.new(:table_name, :pluralize_table_names).new('products', true)
- reflection = AssociationReflection.new(:has_and_belongs_to_many, :categories, nil, {}, product)
+ reflection = ActiveRecord::Reflection.create(:has_many, :categories, nil, {}, product)
reflection.stubs(:klass).returns(category)
assert_equal 'categories_products', reflection.join_table
- reflection = AssociationReflection.new(:has_and_belongs_to_many, :products, nil, {}, category)
+ reflection = ActiveRecord::Reflection.create(:has_many, :products, nil, {}, category)
reflection.stubs(:klass).returns(product)
assert_equal 'categories_products', reflection.join_table
end
@@ -391,11 +373,11 @@ class ReflectionTest < ActiveRecord::TestCase
category = Struct.new(:table_name, :pluralize_table_names).new('catalog_categories', true)
product = Struct.new(:table_name, :pluralize_table_names).new('catalog_products', true)
- reflection = AssociationReflection.new(:has_and_belongs_to_many, :categories, nil, {}, product)
+ reflection = ActiveRecord::Reflection.create(:has_many, :categories, nil, {}, product)
reflection.stubs(:klass).returns(category)
assert_equal 'catalog_categories_products', reflection.join_table
- reflection = AssociationReflection.new(:has_and_belongs_to_many, :products, nil, {}, category)
+ reflection = ActiveRecord::Reflection.create(:has_many, :products, nil, {}, category)
reflection.stubs(:klass).returns(product)
assert_equal 'catalog_categories_products', reflection.join_table
end
@@ -404,11 +386,11 @@ class ReflectionTest < ActiveRecord::TestCase
category = Struct.new(:table_name, :pluralize_table_names).new('catalog_categories', true)
page = Struct.new(:table_name, :pluralize_table_names).new('content_pages', true)
- reflection = AssociationReflection.new(:has_and_belongs_to_many, :categories, nil, {}, page)
+ reflection = ActiveRecord::Reflection.create(:has_many, :categories, nil, {}, page)
reflection.stubs(:klass).returns(category)
assert_equal 'catalog_categories_content_pages', reflection.join_table
- reflection = AssociationReflection.new(:has_and_belongs_to_many, :pages, nil, {}, category)
+ reflection = ActiveRecord::Reflection.create(:has_many, :pages, nil, {}, category)
reflection.stubs(:klass).returns(page)
assert_equal 'catalog_categories_content_pages', reflection.join_table
end
@@ -417,15 +399,47 @@ class ReflectionTest < ActiveRecord::TestCase
category = Struct.new(:table_name, :pluralize_table_names).new('categories', true)
product = Struct.new(:table_name, :pluralize_table_names).new('products', true)
- reflection = AssociationReflection.new(:has_and_belongs_to_many, :categories, nil, { :join_table => 'product_categories' }, product)
+ reflection = ActiveRecord::Reflection.create(:has_many, :categories, nil, { :join_table => 'product_categories' }, product)
reflection.stubs(:klass).returns(category)
assert_equal 'product_categories', reflection.join_table
- reflection = AssociationReflection.new(:has_and_belongs_to_many, :products, nil, { :join_table => 'product_categories' }, category)
+ reflection = ActiveRecord::Reflection.create(:has_many, :products, nil, { :join_table => 'product_categories' }, category)
reflection.stubs(:klass).returns(product)
assert_equal 'product_categories', reflection.join_table
end
+ def test_includes_accepts_symbols
+ hotel = Hotel.create!
+ department = hotel.departments.create!
+ department.chefs.create!
+
+ assert_nothing_raised do
+ assert_equal department.chefs, Hotel.includes([departments: :chefs]).first.chefs
+ end
+ end
+
+ def test_includes_accepts_strings
+ hotel = Hotel.create!
+ department = hotel.departments.create!
+ department.chefs.create!
+
+ assert_nothing_raised do
+ assert_equal department.chefs, Hotel.includes(['departments' => 'chefs']).first.chefs
+ end
+ end
+
+ def test_reflect_on_association_accepts_symbols
+ assert_nothing_raised do
+ assert_equal Hotel.reflect_on_association(:departments).name, :departments
+ end
+ end
+
+ def test_reflect_on_association_accepts_strings
+ assert_nothing_raised do
+ assert_equal Hotel.reflect_on_association("departments").name, :departments
+ end
+ end
+
private
def assert_reflection(klass, association, options)
assert reflection = klass.reflect_on_association(association)
diff --git a/activerecord/test/cases/relation/where_test.rb b/activerecord/test/cases/relation/where_test.rb
index 937f226b1d..a6a36a6fd9 100644
--- a/activerecord/test/cases/relation/where_test.rb
+++ b/activerecord/test/cases/relation/where_test.rb
@@ -6,10 +6,11 @@ require 'models/post'
require 'models/comment'
require 'models/edge'
require 'models/topic'
+require 'models/binary'
module ActiveRecord
class WhereTest < ActiveRecord::TestCase
- fixtures :posts, :edges, :authors
+ fixtures :posts, :edges, :authors, :binaries
def test_where_copies_bind_params
author = authors(:david)
@@ -179,5 +180,35 @@ module ActiveRecord
assert_equal 4, Edge.where(blank).order("sink_id").to_a.size
end
end
+
+ def test_where_with_integer_for_string_column
+ count = Post.where(:title => 0).count
+ assert_equal 0, count
+ end
+
+ def test_where_with_float_for_string_column
+ count = Post.where(:title => 0.0).count
+ assert_equal 0, count
+ end
+
+ def test_where_with_boolean_for_string_column
+ count = Post.where(:title => false).count
+ assert_equal 0, count
+ end
+
+ def test_where_with_decimal_for_string_column
+ count = Post.where(:title => BigDecimal.new(0)).count
+ assert_equal 0, count
+ end
+
+ def test_where_with_duration_for_string_column
+ count = Post.where(:title => 0.seconds).count
+ assert_equal 0, count
+ end
+
+ def test_where_with_integer_for_binary_column
+ count = Binary.where(:data => 0).count
+ assert_equal 0, count
+ end
end
end
diff --git a/activerecord/test/cases/relation_test.rb b/activerecord/test/cases/relation_test.rb
index fb0b906c07..3280945d09 100644
--- a/activerecord/test/cases/relation_test.rb
+++ b/activerecord/test/cases/relation_test.rb
@@ -235,5 +235,33 @@ module ActiveRecord
posts_with_special_comments_with_ratings = Post.group("posts.id").joins(:special_comments).merge(special_comments_with_ratings)
assert_equal 3, authors(:david).posts.merge(posts_with_special_comments_with_ratings).count.length
end
+
+ class EnsureRoundTripTypeCasting < ActiveRecord::Type::Value
+ def type
+ :string
+ end
+
+ def type_cast_from_database(value)
+ raise value unless value == "type cast for database"
+ "type cast from database"
+ end
+
+ def type_cast_for_database(value)
+ raise value unless value == "value from user"
+ "type cast for database"
+ end
+ end
+
+ class UpdateAllTestModel < ActiveRecord::Base
+ self.table_name = 'posts'
+
+ attribute :body, EnsureRoundTripTypeCasting.new
+ end
+
+ def test_update_all_goes_through_normal_type_casting
+ UpdateAllTestModel.update_all(body: "value from user", type: nil) # No STI
+
+ assert_equal "type cast from database", UpdateAllTestModel.first.body
+ end
end
end
diff --git a/activerecord/test/cases/result_test.rb b/activerecord/test/cases/result_test.rb
index 2131b32a0c..d6decafad9 100644
--- a/activerecord/test/cases/result_test.rb
+++ b/activerecord/test/cases/result_test.rb
@@ -10,7 +10,7 @@ module ActiveRecord
])
end
- def test_to_hash_returns_row_hashes
+ test "to_hash returns row_hashes" do
assert_equal [
{'col_1' => 'row 1 col 1', 'col_2' => 'row 1 col 2'},
{'col_1' => 'row 2 col 1', 'col_2' => 'row 2 col 2'},
@@ -18,13 +18,13 @@ module ActiveRecord
], result.to_hash
end
- def test_each_with_block_returns_row_hashes
+ test "each with block returns row hashes" do
result.each do |row|
assert_equal ['col_1', 'col_2'], row.keys
end
end
- def test_each_without_block_returns_an_enumerator
+ test "each without block returns an enumerator" do
result.each.with_index do |row, index|
assert_equal ['col_1', 'col_2'], row.keys
assert_kind_of Integer, index
@@ -32,9 +32,45 @@ module ActiveRecord
end
if Enumerator.method_defined? :size
- def test_each_without_block_returns_a_sized_enumerator
+ test "each without block returns a sized enumerator" do
assert_equal 3, result.each.size
end
end
+
+ test "cast_values returns rows after type casting" do
+ values = [["1.1", "2.2"], ["3.3", "4.4"]]
+ columns = ["col1", "col2"]
+ types = { "col1" => Type::Integer.new, "col2" => Type::Float.new }
+ result = Result.new(columns, values, types)
+
+ assert_equal [[1, 2.2], [3, 4.4]], result.cast_values
+ end
+
+ test "cast_values uses identity type for unknown types" do
+ values = [["1.1", "2.2"], ["3.3", "4.4"]]
+ columns = ["col1", "col2"]
+ types = { "col1" => Type::Integer.new }
+ result = Result.new(columns, values, types)
+
+ assert_equal [[1, "2.2"], [3, "4.4"]], result.cast_values
+ end
+
+ test "cast_values returns single dimensional array if single column" do
+ values = [["1.1"], ["3.3"]]
+ columns = ["col1"]
+ types = { "col1" => Type::Integer.new }
+ result = Result.new(columns, values, types)
+
+ assert_equal [1, 3], result.cast_values
+ end
+
+ test "cast_values can receive types to use instead" do
+ values = [["1.1", "2.2"], ["3.3", "4.4"]]
+ columns = ["col1", "col2"]
+ types = { "col1" => Type::Integer.new, "col2" => Type::Float.new }
+ result = Result.new(columns, values, types)
+
+ assert_equal [[1.1, 2.2], [3.3, 4.4]], result.cast_values("col1" => Type::Float.new)
+ end
end
end
diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb
index e2a18d138d..4e71d04bc0 100644
--- a/activerecord/test/cases/schema_dumper_test.rb
+++ b/activerecord/test/cases/schema_dumper_test.rb
@@ -372,15 +372,28 @@ class SchemaDumperTest < ActiveRecord::TestCase
assert_match %r{create_table "subscribers", id: false}, output
end
+ if ActiveRecord::Base.connection.supports_foreign_keys?
+ def test_foreign_keys_are_dumped_at_the_bottom_to_circumvent_dependency_issues
+ output = standard_dump
+ assert_match(/^\s+add_foreign_key "fk_test_has_fk"[^\n]+\n\s+add_foreign_key "lessons_students"/, output)
+ end
+ end
+
class CreateDogMigration < ActiveRecord::Migration
def up
+ create_table("dog_owners") do |t|
+ end
+
create_table("dogs") do |t|
t.column :name, :string
+ t.column :owner_id, :integer
end
add_index "dogs", [:name]
+ add_foreign_key :dogs, :dog_owners, column: "owner_id" if supports_foreign_keys?
end
def down
drop_table("dogs")
+ drop_table("dog_owners")
end
end
@@ -394,15 +407,19 @@ class SchemaDumperTest < ActiveRecord::TestCase
output = standard_dump
assert_no_match %r{create_table "foo_.+_bar"}, output
- assert_no_match %r{create_index "foo_.+_bar"}, output
+ assert_no_match %r{add_index "foo_.+_bar"}, output
assert_no_match %r{create_table "schema_migrations"}, output
+
+ if ActiveRecord::Base.connection.supports_foreign_keys?
+ assert_no_match %r{add_foreign_key "foo_.+_bar"}, output
+ assert_no_match %r{add_foreign_key "[^"]+", "foo_.+_bar"}, output
+ end
ensure
migration.migrate(:down)
ActiveRecord::Base.table_name_suffix = ActiveRecord::Base.table_name_prefix = ''
$stdout = original
end
-
end
class SchemaDumperDefaultsTest < ActiveRecord::TestCase
diff --git a/activerecord/test/cases/serialization_test.rb b/activerecord/test/cases/serialization_test.rb
index 7dd1f10ce9..3f52e80e11 100644
--- a/activerecord/test/cases/serialization_test.rb
+++ b/activerecord/test/cases/serialization_test.rb
@@ -69,6 +69,14 @@ class SerializationTest < ActiveRecord::TestCase
ActiveRecord::Base.include_root_in_json = original_root_in_json
end
+ def test_read_attribute_for_serialization_with_format_without_method_missing
+ klazz = Class.new(ActiveRecord::Base)
+ klazz.table_name = 'books'
+
+ book = klazz.new
+ assert_nil book.read_attribute_for_serialization(:format)
+ end
+
def test_read_attribute_for_serialization_with_format_after_init
klazz = Class.new(ActiveRecord::Base)
klazz.table_name = 'books'
diff --git a/activerecord/test/cases/serialized_attribute_test.rb b/activerecord/test/cases/serialized_attribute_test.rb
index 7d1c240638..186a1a2ade 100644
--- a/activerecord/test/cases/serialized_attribute_test.rb
+++ b/activerecord/test/cases/serialized_attribute_test.rb
@@ -22,7 +22,9 @@ class SerializedAttributeTest < ActiveRecord::TestCase
end
def test_list_of_serialized_attributes
- assert_equal %w(content), Topic.serialized_attributes.keys
+ assert_deprecated do
+ assert_equal %w(content), Topic.serialized_attributes.keys
+ end
end
def test_serialized_attribute
@@ -36,12 +38,6 @@ class SerializedAttributeTest < ActiveRecord::TestCase
assert_equal(myobj, topic.content)
end
- def test_serialized_attribute_init_with
- topic = Topic.allocate
- topic.init_with('raw_attributes' => { 'content' => '--- foo' })
- assert_equal 'foo', topic.content
- end
-
def test_serialized_attribute_in_base_class
Topic.serialize("content", Hash)
@@ -213,7 +209,7 @@ class SerializedAttributeTest < ActiveRecord::TestCase
t = Topic.create(content: "first")
assert_equal("first", t.content)
- t.update_column(:content, Topic.serialized_attributes["content"].dump("second"))
+ t.update_column(:content, Topic.type_for_attribute('content').type_cast_for_database("second"))
assert_equal("second", t.content)
end
diff --git a/activerecord/test/cases/store_test.rb b/activerecord/test/cases/store_test.rb
index f841b1c983..e9cdf94c99 100644
--- a/activerecord/test/cases/store_test.rb
+++ b/activerecord/test/cases/store_test.rb
@@ -189,7 +189,6 @@ class StoreTest < ActiveRecord::TestCase
assert_equal @john, loaded
second_dump = YAML.dump(loaded)
- assert_equal dumped, second_dump
assert_equal @john, YAML.load(second_dump)
end
end
diff --git a/activerecord/test/cases/tasks/database_tasks_test.rb b/activerecord/test/cases/tasks/database_tasks_test.rb
index bf9e14fa4f..0f48c8d5fc 100644
--- a/activerecord/test/cases/tasks/database_tasks_test.rb
+++ b/activerecord/test/cases/tasks/database_tasks_test.rb
@@ -205,7 +205,7 @@ module ActiveRecord
ActiveRecord::Tasks::DatabaseTasks.drop_all
end
- def test_creates_configurations_with_local_ip
+ def test_drops_configurations_with_local_ip
@configurations[:development].merge!('host' => '127.0.0.1')
ActiveRecord::Tasks::DatabaseTasks.expects(:drop)
@@ -213,7 +213,7 @@ module ActiveRecord
ActiveRecord::Tasks::DatabaseTasks.drop_all
end
- def test_creates_configurations_with_local_host
+ def test_drops_configurations_with_local_host
@configurations[:development].merge!('host' => 'localhost')
ActiveRecord::Tasks::DatabaseTasks.expects(:drop)
@@ -221,7 +221,7 @@ module ActiveRecord
ActiveRecord::Tasks::DatabaseTasks.drop_all
end
- def test_creates_configurations_with_blank_hosts
+ def test_drops_configurations_with_blank_hosts
@configurations[:development].merge!('host' => nil)
ActiveRecord::Tasks::DatabaseTasks.expects(:drop)
@@ -241,7 +241,7 @@ module ActiveRecord
ActiveRecord::Base.stubs(:configurations).returns(@configurations)
end
- def test_creates_current_environment_database
+ def test_drops_current_environment_database
ActiveRecord::Tasks::DatabaseTasks.expects(:drop).
with('database' => 'prod-db')
@@ -285,6 +285,34 @@ module ActiveRecord
end
end
+ class DatabaseTasksPurgeCurrentTest < ActiveRecord::TestCase
+ def test_purges_current_environment_database
+ configurations = {
+ 'development' => {'database' => 'dev-db'},
+ 'test' => {'database' => 'test-db'},
+ 'production' => {'database' => 'prod-db'}
+ }
+ ActiveRecord::Base.stubs(:configurations).returns(configurations)
+
+ ActiveRecord::Tasks::DatabaseTasks.expects(:purge).
+ with('database' => 'prod-db')
+
+ ActiveRecord::Tasks::DatabaseTasks.purge_current('production')
+ end
+ end
+
+ class DatabaseTasksPurgeAllTest < ActiveRecord::TestCase
+ def test_purge_all_local_configurations
+ configurations = {:development => {'database' => 'my-db'}}
+ ActiveRecord::Base.stubs(:configurations).returns(configurations)
+
+ ActiveRecord::Tasks::DatabaseTasks.expects(:purge).
+ with('database' => 'my-db')
+
+ ActiveRecord::Tasks::DatabaseTasks.purge_all
+ end
+ end
+
class DatabaseTasksCharsetTest < ActiveRecord::TestCase
include DatabaseTasksSetupper
diff --git a/activerecord/test/cases/type/string_test.rb b/activerecord/test/cases/type/string_test.rb
new file mode 100644
index 0000000000..420177ed49
--- /dev/null
+++ b/activerecord/test/cases/type/string_test.rb
@@ -0,0 +1,36 @@
+require 'cases/helper'
+
+module ActiveRecord
+ class StringTypeTest < ActiveRecord::TestCase
+ test "type casting" do
+ type = Type::String.new
+ assert_equal "1", type.type_cast_from_user(true)
+ assert_equal "0", type.type_cast_from_user(false)
+ assert_equal "123", type.type_cast_from_user(123)
+ end
+
+ test "values are duped coming out" do
+ s = "foo"
+ type = Type::String.new
+ assert_not_same s, type.type_cast_from_user(s)
+ assert_not_same s, type.type_cast_from_database(s)
+ end
+
+ test "string mutations are detected" do
+ klass = Class.new(Base)
+ klass.table_name = 'authors'
+
+ author = klass.create!(name: 'Sean')
+ assert_not author.changed?
+
+ author.name << ' Griffin'
+ assert author.name_changed?
+
+ author.save!
+ author.reload
+
+ assert_equal 'Sean Griffin', author.name
+ assert_not author.changed?
+ end
+ end
+end
diff --git a/activerecord/test/cases/types_test.rb b/activerecord/test/cases/types_test.rb
index 731f8cfba3..47cf775cb6 100644
--- a/activerecord/test/cases/types_test.rb
+++ b/activerecord/test/cases/types_test.rb
@@ -35,13 +35,6 @@ module ActiveRecord
assert_equal false, type.type_cast_from_user('SOMETHING RANDOM')
end
- def test_type_cast_string
- type = Type::String.new
- assert_equal "1", type.type_cast_from_user(true)
- assert_equal "0", type.type_cast_from_user(false)
- assert_equal "123", type.type_cast_from_user(123)
- end
-
def test_type_cast_integer
type = Type::Integer.new
assert_equal 1, type.type_cast_from_user(1)
@@ -79,11 +72,29 @@ module ActiveRecord
assert_nil type.type_cast_from_user(1.0/0.0)
end
+ def test_changing_integers
+ type = Type::Integer.new
+
+ assert type.changed?(5, 5, '5wibble')
+ assert_not type.changed?(5, 5, '5')
+ assert_not type.changed?(5, 5, '5.0')
+ assert_not type.changed?(nil, nil, nil)
+ end
+
def test_type_cast_float
type = Type::Float.new
assert_equal 1.0, type.type_cast_from_user("1")
end
+ def test_changing_float
+ type = Type::Float.new
+
+ assert type.changed?(5.0, 5.0, '5wibble')
+ assert_not type.changed?(5.0, 5.0, '5')
+ assert_not type.changed?(5.0, 5.0, '5.0')
+ assert_not type.changed?(nil, nil, nil)
+ end
+
def test_type_cast_decimal
type = Type::Decimal.new
assert_equal BigDecimal.new("0"), type.type_cast_from_user(BigDecimal.new("0"))
diff --git a/activerecord/test/cases/validations_test.rb b/activerecord/test/cases/validations_test.rb
index a6e1dc72e5..55804f9576 100644
--- a/activerecord/test/cases/validations_test.rb
+++ b/activerecord/test/cases/validations_test.rb
@@ -78,6 +78,20 @@ class ValidationsTest < ActiveRecord::TestCase
assert_equal r, invalid.record
end
+ def test_validate_with_bang
+ assert_raise(ActiveRecord::RecordInvalid) do
+ WrongReply.new.validate!
+ end
+ end
+
+ def test_validate_with_bang_and_context
+ assert_raise(ActiveRecord::RecordInvalid) do
+ WrongReply.new.validate!(:special_case)
+ end
+ r = WrongReply.new(:title => "Valid title", :author_name => "secret", :content => "Good")
+ assert r.validate!(:special_case)
+ end
+
def test_exception_on_create_bang_many
assert_raise(ActiveRecord::RecordInvalid) do
WrongReply.create!([ { "title" => "OK" }, { "title" => "Wrong Create" }])
diff --git a/activerecord/test/cases/yaml_serialization_test.rb b/activerecord/test/cases/yaml_serialization_test.rb
index d4f8ef5b4d..9f1d110ddb 100644
--- a/activerecord/test/cases/yaml_serialization_test.rb
+++ b/activerecord/test/cases/yaml_serialization_test.rb
@@ -1,8 +1,10 @@
require 'cases/helper'
require 'models/topic'
+require 'models/post'
+require 'models/author'
class YamlSerializationTest < ActiveRecord::TestCase
- fixtures :topics
+ fixtures :topics, :authors, :posts
def test_to_yaml_with_time_with_zone_should_not_raise_exception
with_timezone_config aware_attributes: true, zone: "Pacific Time (US & Canada)" do
@@ -69,4 +71,15 @@ class YamlSerializationTest < ActiveRecord::TestCase
assert_not topic.new_record?, "Loaded records without ID are not new"
assert_not YAML.load(YAML.dump(topic)).new_record?, "Record should not be new after deserialization"
end
+
+ def test_types_of_virtual_columns_are_not_changed_on_round_trip
+ author = Author.select('authors.*, count(posts.id) as posts_count')
+ .joins(:posts)
+ .group('authors.id')
+ .first
+ dumped = YAML.load(YAML.dump(author))
+
+ assert_equal 5, author.posts_count
+ assert_equal 5, dumped.posts_count
+ end
end
diff --git a/activerecord/test/fixtures/fk_test_has_pk.yml b/activerecord/test/fixtures/fk_test_has_pk.yml
index c93952180b..73882bac41 100644
--- a/activerecord/test/fixtures/fk_test_has_pk.yml
+++ b/activerecord/test/fixtures/fk_test_has_pk.yml
@@ -1,2 +1,2 @@
first:
- id: 1 \ No newline at end of file
+ pk_id: 1 \ No newline at end of file
diff --git a/activerecord/test/fixtures/posts.yml b/activerecord/test/fixtures/posts.yml
index 7298096025..86d46f753a 100644
--- a/activerecord/test/fixtures/posts.yml
+++ b/activerecord/test/fixtures/posts.yml
@@ -4,7 +4,6 @@ welcome:
title: Welcome to the weblog
body: Such a lovely day
comments_count: 2
- taggings_count: 1
tags_count: 1
type: Post
@@ -14,7 +13,6 @@ thinking:
title: So I was thinking
body: Like I hopefully always am
comments_count: 1
- taggings_count: 1
tags_count: 1
type: SpecialPost
diff --git a/activerecord/test/fixtures/topics.yml b/activerecord/test/fixtures/topics.yml
index bf049abbf1..4c98b10380 100644
--- a/activerecord/test/fixtures/topics.yml
+++ b/activerecord/test/fixtures/topics.yml
@@ -6,7 +6,7 @@ first:
written_on: 2003-07-16t15:28:11.2233+01:00
last_read: 2004-04-15
bonus_time: 2005-01-30t15:28:00.00+01:00
- content: Have a nice day
+ content: "--- Have a nice day\n...\n"
approved: false
replies_count: 1
@@ -15,7 +15,7 @@ second:
title: The Second Topic of the day
author_name: Mary
written_on: 2004-07-15t15:28:00.0099+01:00
- content: Have a nice day
+ content: "--- Have a nice day\n...\n"
approved: true
replies_count: 0
parent_id: 1
@@ -26,7 +26,7 @@ third:
title: The Third Topic of the day
author_name: Carl
written_on: 2012-08-12t20:24:22.129346+00:00
- content: I'm a troll
+ content: "--- I'm a troll\n...\n"
approved: true
replies_count: 1
@@ -35,7 +35,7 @@ fourth:
title: The Fourth Topic of the day
author_name: Carl
written_on: 2006-07-15t15:28:00.0099+01:00
- content: Why not?
+ content: "--- Why not?\n...\n"
approved: true
type: Reply
parent_id: 3
@@ -45,5 +45,5 @@ fifth:
title: The Fifth Topic of the day
author_name: Jason
written_on: 2013-07-13t12:11:00.0099+01:00
- content: Omakase
+ content: "--- Omakase\n...\n"
approved: true
diff --git a/activerecord/test/models/club.rb b/activerecord/test/models/club.rb
index a762ad4bb5..6ceafe5858 100644
--- a/activerecord/test/models/club.rb
+++ b/activerecord/test/models/club.rb
@@ -14,3 +14,10 @@ class Club < ActiveRecord::Base
"I'm sorry sir, this is a *private* club, not a *pirate* club"
end
end
+
+class SuperClub < ActiveRecord::Base
+ self.table_name = "clubs"
+
+ has_many :memberships, class_name: 'SuperMembership', foreign_key: 'club_id'
+ has_many :members, through: :memberships
+end
diff --git a/activerecord/test/models/contact.rb b/activerecord/test/models/contact.rb
index a1cb8d62b6..3ea17c3abf 100644
--- a/activerecord/test/models/contact.rb
+++ b/activerecord/test/models/contact.rb
@@ -8,6 +8,7 @@ module ContactFakeColumns
table_name => 'id'
}
+ column :id, :integer
column :name, :string
column :age, :integer
column :avatar, :binary
diff --git a/activerecord/test/models/face.rb b/activerecord/test/models/face.rb
index edb75d333f..91e46f83e5 100644
--- a/activerecord/test/models/face.rb
+++ b/activerecord/test/models/face.rb
@@ -1,6 +1,8 @@
class Face < ActiveRecord::Base
belongs_to :man, :inverse_of => :face
belongs_to :polymorphic_man, :polymorphic => true, :inverse_of => :polymorphic_face
+ # Oracle identifier lengh is limited to 30 bytes or less, `polymorphic` renamed `poly`
+ belongs_to :poly_man_without_inverse, :polymorphic => true
# These is a "broken" inverse_of for the purposes of testing
belongs_to :horrible_man, :class_name => 'Man', :inverse_of => :horrible_face
belongs_to :horrible_polymorphic_man, :polymorphic => true, :inverse_of => :horrible_polymorphic_face
diff --git a/activerecord/test/models/man.rb b/activerecord/test/models/man.rb
index f4d127730c..4fbb6b226b 100644
--- a/activerecord/test/models/man.rb
+++ b/activerecord/test/models/man.rb
@@ -1,6 +1,7 @@
class Man < ActiveRecord::Base
has_one :face, :inverse_of => :man
has_one :polymorphic_face, :class_name => 'Face', :as => :polymorphic_man, :inverse_of => :polymorphic_man
+ has_one :polymorphic_face_without_inverse, :class_name => 'Face', :as => :poly_man_without_inverse
has_many :interests, :inverse_of => :man
has_many :polymorphic_interests, :class_name => 'Interest', :as => :polymorphic_man, :inverse_of => :polymorphic_man
# These are "broken" inverse_of associations for the purposes of testing
diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb
index 5f01ab0a82..a29858213b 100644
--- a/activerecord/test/models/post.rb
+++ b/activerecord/test/models/post.rb
@@ -88,7 +88,7 @@ class Post < ActiveRecord::Base
has_and_belongs_to_many :categories
has_and_belongs_to_many :special_categories, :join_table => "categories_posts", :association_foreign_key => 'category_id'
- has_many :taggings, :as => :taggable
+ has_many :taggings, :as => :taggable, :counter_cache => :tags_count
has_many :tags, :through => :taggings do
def add_joins_and_select
select('tags.*, authors.id as author_id')
diff --git a/activerecord/test/models/publisher/article.rb b/activerecord/test/models/publisher/article.rb
index 03a277bbdd..d73a8eb936 100644
--- a/activerecord/test/models/publisher/article.rb
+++ b/activerecord/test/models/publisher/article.rb
@@ -1,3 +1,4 @@
class Publisher::Article < ActiveRecord::Base
has_and_belongs_to_many :magazines
+ has_and_belongs_to_many :tags
end
diff --git a/activerecord/test/models/tagging.rb b/activerecord/test/models/tagging.rb
index f91f2ad2e9..a6c05da26a 100644
--- a/activerecord/test/models/tagging.rb
+++ b/activerecord/test/models/tagging.rb
@@ -8,6 +8,6 @@ class Tagging < ActiveRecord::Base
belongs_to :invalid_tag, :class_name => 'Tag', :foreign_key => 'tag_id'
belongs_to :blue_tag, -> { where :tags => { :name => 'Blue' } }, :class_name => 'Tag', :foreign_key => :tag_id
belongs_to :tag_with_primary_key, :class_name => 'Tag', :foreign_key => :tag_id, :primary_key => :custom_primary_key
- belongs_to :taggable, :polymorphic => true, :counter_cache => true
+ belongs_to :taggable, :polymorphic => true, :counter_cache => :tags_count
has_many :things, :through => :taggable
end
diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb
index 98fd0b6803..146190fc19 100644
--- a/activerecord/test/schema/schema.rb
+++ b/activerecord/test/schema/schema.rb
@@ -62,6 +62,11 @@ ActiveRecord::Schema.define do
t.references :magazine
end
+ create_table :articles_tags, force: true do |t|
+ t.references :article
+ t.references :tag
+ end
+
create_table :audit_logs, force: true do |t|
t.column :message, :string, null: false
t.column :developer_id, :integer, null: false
@@ -79,6 +84,8 @@ ActiveRecord::Schema.define do
create_table :author_addresses, force: true do |t|
end
+ add_foreign_key :authors, :author_addresses
+
create_table :author_favorites, force: true do |t|
t.column :author_id, :integer
t.column :favorite_author_id, :integer
@@ -185,7 +192,7 @@ ActiveRecord::Schema.define do
t.text :body, null: false
end
t.string :type
- t.integer :taggings_count, default: 0
+ t.integer :tags_count, default: 0
t.integer :children_count, default: 0
t.integer :parent_id
t.references :author, polymorphic: true
@@ -564,7 +571,6 @@ ActiveRecord::Schema.define do
end
t.string :type
t.integer :comments_count, default: 0
- t.integer :taggings_count, default: 0
t.integer :taggings_with_delete_all_count, default: 0
t.integer :taggings_with_destroy_count, default: 0
t.integer :tags_count, default: 0
@@ -775,6 +781,8 @@ ActiveRecord::Schema.define do
t.integer :man_id
t.integer :polymorphic_man_id
t.string :polymorphic_man_type
+ t.integer :poly_man_without_inverse_id
+ t.string :poly_man_without_inverse_type
t.integer :horrible_polymorphic_man_id
t.string :horrible_polymorphic_man_type
end
@@ -849,10 +857,10 @@ ActiveRecord::Schema.define do
t.integer :fk_id, null: false
end
- create_table :fk_test_has_pk, force: true do |t|
+ create_table :fk_test_has_pk, force: true, primary_key: "pk_id" do |t|
end
- execute "ALTER TABLE fk_test_has_fk ADD CONSTRAINT fk_name FOREIGN KEY (#{quote_column_name 'fk_id'}) REFERENCES #{quote_table_name 'fk_test_has_pk'} (#{quote_column_name 'id'})"
+ execute "ALTER TABLE fk_test_has_fk ADD CONSTRAINT fk_name FOREIGN KEY (#{quote_column_name 'fk_id'}) REFERENCES #{quote_table_name 'fk_test_has_pk'} (#{quote_column_name 'pk_id'})"
execute "ALTER TABLE lessons_students ADD CONSTRAINT student_id_fk FOREIGN KEY (#{quote_column_name 'student_id'}) REFERENCES #{quote_table_name 'students'} (#{quote_column_name 'id'})"
end
diff --git a/activerecord/test/schema/sqlite_specific_schema.rb b/activerecord/test/schema/sqlite_specific_schema.rb
index b7aff4f47d..b5552c2755 100644
--- a/activerecord/test/schema/sqlite_specific_schema.rb
+++ b/activerecord/test/schema/sqlite_specific_schema.rb
@@ -7,7 +7,7 @@ ActiveRecord::Schema.define do
execute "DROP TABLE fk_test_has_pk" rescue nil
execute <<_SQL
CREATE TABLE 'fk_test_has_pk' (
- 'id' INTEGER NOT NULL PRIMARY KEY
+ 'pk_id' INTEGER NOT NULL PRIMARY KEY
);
_SQL
@@ -16,7 +16,7 @@ _SQL
'id' INTEGER NOT NULL PRIMARY KEY,
'fk_id' INTEGER NOT NULL,
- FOREIGN KEY ('fk_id') REFERENCES 'fk_test_has_pk'('id')
+ FOREIGN KEY ('fk_id') REFERENCES 'fk_test_has_pk'('pk_id')
);
_SQL
-end \ No newline at end of file
+end
diff --git a/activerecord/test/support/ddl_helper.rb b/activerecord/test/support/ddl_helper.rb
index 0107babaaf..43cb235e01 100644
--- a/activerecord/test/support/ddl_helper.rb
+++ b/activerecord/test/support/ddl_helper.rb
@@ -1,8 +1,8 @@
module DdlHelper
def with_example_table(connection, table_name, definition = nil)
- connection.exec_query("CREATE TABLE #{table_name}(#{definition})")
+ connection.execute("CREATE TABLE #{table_name}(#{definition})")
yield
ensure
- connection.exec_query("DROP TABLE #{table_name}")
+ connection.execute("DROP TABLE #{table_name}")
end
end