aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record
diff options
context:
space:
mode:
Diffstat (limited to 'activerecord/lib/active_record')
-rw-r--r--activerecord/lib/active_record/aggregations.rb4
-rw-r--r--activerecord/lib/active_record/associations.rb192
-rw-r--r--activerecord/lib/active_record/associations/alias_tracker.rb3
-rw-r--r--activerecord/lib/active_record/associations/association.rb17
-rw-r--r--activerecord/lib/active_record/associations/association_scope.rb1
-rw-r--r--activerecord/lib/active_record/associations/belongs_to_association.rb2
-rw-r--r--activerecord/lib/active_record/associations/builder/belongs_to.rb16
-rw-r--r--activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb10
-rw-r--r--activerecord/lib/active_record/associations/collection_association.rb21
-rw-r--r--activerecord/lib/active_record/associations/collection_proxy.rb60
-rw-r--r--activerecord/lib/active_record/associations/has_many_association.rb75
-rw-r--r--activerecord/lib/active_record/associations/has_many_through_association.rb4
-rw-r--r--activerecord/lib/active_record/associations/has_one_association.rb15
-rw-r--r--activerecord/lib/active_record/associations/preloader.rb10
-rw-r--r--activerecord/lib/active_record/associations/preloader/association.rb2
-rw-r--r--activerecord/lib/active_record/associations/singular_association.rb17
-rw-r--r--activerecord/lib/active_record/associations/through_association.rb18
-rw-r--r--activerecord/lib/active_record/attribute.rb55
-rw-r--r--activerecord/lib/active_record/attribute/user_provided_default.rb23
-rw-r--r--activerecord/lib/active_record/attribute_assignment.rb3
-rw-r--r--activerecord/lib/active_record/attribute_methods.rb46
-rw-r--r--activerecord/lib/active_record/attribute_methods/dirty.rb132
-rw-r--r--activerecord/lib/active_record/attribute_methods/query.rb2
-rw-r--r--activerecord/lib/active_record/attribute_methods/read.rb6
-rw-r--r--activerecord/lib/active_record/attribute_methods/serialization.rb12
-rw-r--r--activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb2
-rw-r--r--activerecord/lib/active_record/attribute_methods/write.rb4
-rw-r--r--activerecord/lib/active_record/attribute_mutation_tracker.rb70
-rw-r--r--activerecord/lib/active_record/attribute_set.rb13
-rw-r--r--activerecord/lib/active_record/attribute_set/builder.rb10
-rw-r--r--activerecord/lib/active_record/attributes.rb20
-rw-r--r--activerecord/lib/active_record/autosave_association.rb13
-rw-r--r--activerecord/lib/active_record/base.rb3
-rw-r--r--activerecord/lib/active_record/callbacks.rb19
-rw-r--r--activerecord/lib/active_record/coders/yaml_column.rb17
-rw-r--r--activerecord/lib/active_record/collection_cache_key.rb31
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb499
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb4
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb19
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/quoting.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb51
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb55
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb40
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb169
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_adapter.rb41
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb457
-rw-r--r--activerecord/lib/active_record/connection_adapters/column.rb16
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb82
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql_adapter.rb60
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/column.rb3
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid.rb1
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb5
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb18
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb27
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/rails_5_1_point.rb50
-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/quoting.rb8
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb24
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb173
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb55
-rw-r--r--activerecord/lib/active_record/connection_adapters/schema_cache.rb57
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3/schema_creation.rb15
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb131
-rw-r--r--activerecord/lib/active_record/connection_adapters/statement_pool.rb39
-rw-r--r--activerecord/lib/active_record/connection_handling.rb4
-rw-r--r--activerecord/lib/active_record/core.rb27
-rw-r--r--activerecord/lib/active_record/enum.rb91
-rw-r--r--activerecord/lib/active_record/errors.rb48
-rw-r--r--activerecord/lib/active_record/explain_subscriber.rb2
-rw-r--r--activerecord/lib/active_record/fixture_set/file.rb23
-rw-r--r--activerecord/lib/active_record/fixtures.rb55
-rw-r--r--activerecord/lib/active_record/inheritance.rb16
-rw-r--r--activerecord/lib/active_record/integration.rb2
-rw-r--r--activerecord/lib/active_record/locale/en.yml4
-rw-r--r--activerecord/lib/active_record/locking/optimistic.rb4
-rw-r--r--activerecord/lib/active_record/locking/pessimistic.rb2
-rw-r--r--activerecord/lib/active_record/log_subscriber.rb39
-rw-r--r--activerecord/lib/active_record/migration.rb182
-rw-r--r--activerecord/lib/active_record/migration/command_recorder.rb77
-rw-r--r--activerecord/lib/active_record/model_schema.rb38
-rw-r--r--activerecord/lib/active_record/nested_attributes.rb24
-rw-r--r--activerecord/lib/active_record/null_relation.rb2
-rw-r--r--activerecord/lib/active_record/persistence.rb25
-rw-r--r--activerecord/lib/active_record/querying.rb2
-rw-r--r--activerecord/lib/active_record/railtie.rb15
-rw-r--r--activerecord/lib/active_record/railties/databases.rake64
-rw-r--r--activerecord/lib/active_record/reflection.rb128
-rw-r--r--activerecord/lib/active_record/relation.rb99
-rw-r--r--activerecord/lib/active_record/relation/batches.rb98
-rw-r--r--activerecord/lib/active_record/relation/batches/batch_enumerator.rb67
-rw-r--r--activerecord/lib/active_record/relation/calculations.rb15
-rw-r--r--activerecord/lib/active_record/relation/delegation.rb4
-rw-r--r--activerecord/lib/active_record/relation/finder_methods.rb40
-rw-r--r--activerecord/lib/active_record/relation/merger.rb37
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder.rb12
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb4
-rw-r--r--activerecord/lib/active_record/relation/query_methods.rb45
-rw-r--r--activerecord/lib/active_record/relation/where_clause.rb2
-rw-r--r--activerecord/lib/active_record/relation/where_clause_factory.rb1
-rw-r--r--activerecord/lib/active_record/sanitization.rb85
-rw-r--r--activerecord/lib/active_record/schema.rb36
-rw-r--r--activerecord/lib/active_record/schema_dumper.rb38
-rw-r--r--activerecord/lib/active_record/schema_migration.rb7
-rw-r--r--activerecord/lib/active_record/scoping/default.rb13
-rw-r--r--activerecord/lib/active_record/secure_token.rb2
-rw-r--r--activerecord/lib/active_record/serialization.rb2
-rw-r--r--activerecord/lib/active_record/serializers/xml_serializer.rb193
-rw-r--r--activerecord/lib/active_record/suppressor.rb3
-rw-r--r--activerecord/lib/active_record/table_metadata.rb14
-rw-r--r--activerecord/lib/active_record/tasks/database_tasks.rb8
-rw-r--r--activerecord/lib/active_record/tasks/mysql_database_tasks.rb48
-rw-r--r--activerecord/lib/active_record/tasks/postgresql_database_tasks.rb28
-rw-r--r--activerecord/lib/active_record/timestamp.rb17
-rw-r--r--activerecord/lib/active_record/transactions.rb26
-rw-r--r--activerecord/lib/active_record/type.rb32
-rw-r--r--activerecord/lib/active_record/type/adapter_specific_registry.rb36
-rw-r--r--activerecord/lib/active_record/type/big_integer.rb13
-rw-r--r--activerecord/lib/active_record/type/binary.rb50
-rw-r--r--activerecord/lib/active_record/type/boolean.rb19
-rw-r--r--activerecord/lib/active_record/type/date.rb46
-rw-r--r--activerecord/lib/active_record/type/date_time.rb41
-rw-r--r--activerecord/lib/active_record/type/decimal.rb48
-rw-r--r--activerecord/lib/active_record/type/decimal_without_scale.rb11
-rw-r--r--activerecord/lib/active_record/type/float.rb25
-rw-r--r--activerecord/lib/active_record/type/helpers.rb4
-rw-r--r--activerecord/lib/active_record/type/helpers/accepts_multiparameter_time.rb30
-rw-r--r--activerecord/lib/active_record/type/helpers/mutable.rb18
-rw-r--r--activerecord/lib/active_record/type/helpers/numeric.rb34
-rw-r--r--activerecord/lib/active_record/type/helpers/time_value.rb58
-rw-r--r--activerecord/lib/active_record/type/integer.rb63
-rw-r--r--activerecord/lib/active_record/type/internal/abstract_json.rb33
-rw-r--r--activerecord/lib/active_record/type/internal/timezone.rb15
-rw-r--r--activerecord/lib/active_record/type/serialized.rb10
-rw-r--r--activerecord/lib/active_record/type/string.rb36
-rw-r--r--activerecord/lib/active_record/type/text.rb11
-rw-r--r--activerecord/lib/active_record/type/time.rb40
-rw-r--r--activerecord/lib/active_record/type/type_map.rb8
-rw-r--r--activerecord/lib/active_record/type/unsigned_integer.rb15
-rw-r--r--activerecord/lib/active_record/type/value.rb104
-rw-r--r--activerecord/lib/active_record/type_caster/connection.rb2
-rw-r--r--activerecord/lib/active_record/validations.rb21
-rw-r--r--activerecord/lib/active_record/validations/absence.rb24
-rw-r--r--activerecord/lib/active_record/validations/length.rb5
-rw-r--r--activerecord/lib/active_record/validations/presence.rb14
-rw-r--r--activerecord/lib/active_record/validations/uniqueness.rb8
148 files changed, 3302 insertions, 2447 deletions
diff --git a/activerecord/lib/active_record/aggregations.rb b/activerecord/lib/active_record/aggregations.rb
index 3d497a30fb..a2aea63bdd 100644
--- a/activerecord/lib/active_record/aggregations.rb
+++ b/activerecord/lib/active_record/aggregations.rb
@@ -25,7 +25,7 @@ module ActiveRecord
end
# Active Record implements aggregation through a macro-like class method called +composed_of+
- # for representing attributes as value objects. It expresses relationships like "Account [is]
+ # for representing attributes as value objects. It expresses relationships like "Account [is]
# composed of Money [among other things]" or "Person [is] composed of [an] address". Each call
# to the macro adds a description of how the value objects are created from the attributes of
# the entity object (when the entity is initialized either as a new object or from finding an
@@ -142,7 +142,7 @@ module ActiveRecord
# converted to an instance of value class if necessary.
#
# For example, the NetworkResource model has +network_address+ and +cidr_range+ attributes that should be
- # aggregated using the NetAddr::CIDR value class (http://www.ruby-doc.org/gems/docs/n/netaddr-1.5.0/NetAddr/CIDR.html).
+ # aggregated using the NetAddr::CIDR value class (http://www.rubydoc.info/gems/netaddr/1.5.0/NetAddr/CIDR).
# The constructor for the value class is called +create+ and it expects a CIDR address string as a parameter.
# New values can be assigned to the value object using either another NetAddr::CIDR object, a string
# or an array. The <tt>:constructor</tt> and <tt>:converter</tt> options can be used to meet
diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb
index c5c2178ee2..cf3e63b4a3 100644
--- a/activerecord/lib/active_record/associations.rb
+++ b/activerecord/lib/active_record/associations.rb
@@ -5,95 +5,167 @@ require 'active_record/errors'
module ActiveRecord
class AssociationNotFoundError < ConfigurationError #:nodoc:
- def initialize(record, association_name)
- super("Association named '#{association_name}' was not found on #{record.class.name}; perhaps you misspelled it?")
+ def initialize(record = nil, association_name = nil)
+ if record && association_name
+ super("Association named '#{association_name}' was not found on #{record.class.name}; perhaps you misspelled it?")
+ else
+ super("Association was not found.")
+ end
end
end
class InverseOfAssociationNotFoundError < ActiveRecordError #:nodoc:
- def initialize(reflection, associated_class = nil)
- super("Could not find the inverse association for #{reflection.name} (#{reflection.options[:inverse_of].inspect} in #{associated_class.nil? ? reflection.class_name : associated_class.name})")
+ def initialize(reflection = nil, associated_class = nil)
+ if reflection
+ super("Could not find the inverse association for #{reflection.name} (#{reflection.options[:inverse_of].inspect} in #{associated_class.nil? ? reflection.class_name : associated_class.name})")
+ else
+ super("Could not find the inverse association.")
+ end
end
end
class HasManyThroughAssociationNotFoundError < ActiveRecordError #:nodoc:
- def initialize(owner_class_name, reflection)
- super("Could not find the association #{reflection.options[:through].inspect} in model #{owner_class_name}")
+ def initialize(owner_class_name = nil, reflection = nil)
+ if owner_class_name && reflection
+ super("Could not find the association #{reflection.options[:through].inspect} in model #{owner_class_name}")
+ else
+ super("Could not find the association.")
+ end
end
end
class HasManyThroughAssociationPolymorphicSourceError < ActiveRecordError #:nodoc:
- def initialize(owner_class_name, reflection, source_reflection)
- super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' on the polymorphic object '#{source_reflection.class_name}##{source_reflection.name}' without 'source_type'. Try adding 'source_type: \"#{reflection.name.to_s.classify}\"' to 'has_many :through' definition.")
+ def initialize(owner_class_name = nil, reflection = nil, source_reflection = nil)
+ if owner_class_name && reflection && source_reflection
+ super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' on the polymorphic object '#{source_reflection.class_name}##{source_reflection.name}' without 'source_type'. Try adding 'source_type: \"#{reflection.name.to_s.classify}\"' to 'has_many :through' definition.")
+ else
+ super("Cannot have a has_many :through association.")
+ end
end
end
class HasManyThroughAssociationPolymorphicThroughError < ActiveRecordError #:nodoc:
- def initialize(owner_class_name, reflection)
- super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' which goes through the polymorphic association '#{owner_class_name}##{reflection.through_reflection.name}'.")
+ def initialize(owner_class_name = nil, reflection = nil)
+ if owner_class_name && reflection
+ super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' which goes through the polymorphic association '#{owner_class_name}##{reflection.through_reflection.name}'.")
+ else
+ super("Cannot have a has_many :through association.")
+ end
end
end
class HasManyThroughAssociationPointlessSourceTypeError < ActiveRecordError #:nodoc:
- def initialize(owner_class_name, reflection, source_reflection)
- super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' with a :source_type option if the '#{reflection.through_reflection.class_name}##{source_reflection.name}' is not polymorphic. Try removing :source_type on your association.")
+ def initialize(owner_class_name = nil, reflection = nil, source_reflection = nil)
+ if owner_class_name && reflection && source_reflection
+ super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' with a :source_type option if the '#{reflection.through_reflection.class_name}##{source_reflection.name}' is not polymorphic. Try removing :source_type on your association.")
+ else
+ super("Cannot have a has_many :through association.")
+ end
end
end
class HasOneThroughCantAssociateThroughCollection < ActiveRecordError #:nodoc:
- def initialize(owner_class_name, reflection, through_reflection)
- super("Cannot have a has_one :through association '#{owner_class_name}##{reflection.name}' where the :through association '#{owner_class_name}##{through_reflection.name}' is a collection. Specify a has_one or belongs_to association in the :through option instead.")
+ def initialize(owner_class_name = nil, reflection = nil, through_reflection = nil)
+ if owner_class_name && reflection && through_reflection
+ super("Cannot have a has_one :through association '#{owner_class_name}##{reflection.name}' where the :through association '#{owner_class_name}##{through_reflection.name}' is a collection. Specify a has_one or belongs_to association in the :through option instead.")
+ else
+ super("Cannot have a has_one :through association.")
+ end
end
end
class HasOneAssociationPolymorphicThroughError < ActiveRecordError #:nodoc:
- def initialize(owner_class_name, reflection)
- super("Cannot have a has_one :through association '#{owner_class_name}##{reflection.name}' which goes through the polymorphic association '#{owner_class_name}##{reflection.through_reflection.name}'.")
+ def initialize(owner_class_name = nil, reflection = nil)
+ if owner_class_name && reflection
+ super("Cannot have a has_one :through association '#{owner_class_name}##{reflection.name}' which goes through the polymorphic association '#{owner_class_name}##{reflection.through_reflection.name}'.")
+ else
+ super("Cannot have a has_one :through association.")
+ end
end
end
class HasManyThroughSourceAssociationNotFoundError < ActiveRecordError #:nodoc:
- def initialize(reflection)
- through_reflection = reflection.through_reflection
- source_reflection_names = reflection.source_reflection_names
- source_associations = reflection.through_reflection.klass._reflections.keys
- super("Could not find the source association(s) #{source_reflection_names.collect(&:inspect).to_sentence(:two_words_connector => ' or ', :last_word_connector => ', or ', :locale => :en)} in model #{through_reflection.klass}. Try 'has_many #{reflection.name.inspect}, :through => #{through_reflection.name.inspect}, :source => <name>'. Is it one of #{source_associations.to_sentence(:two_words_connector => ' or ', :last_word_connector => ', or ', :locale => :en)}?")
+ def initialize(reflection = nil)
+ if reflection
+ through_reflection = reflection.through_reflection
+ source_reflection_names = reflection.source_reflection_names
+ source_associations = reflection.through_reflection.klass._reflections.keys
+ super("Could not find the source association(s) #{source_reflection_names.collect(&:inspect).to_sentence(:two_words_connector => ' or ', :last_word_connector => ', or ', :locale => :en)} in model #{through_reflection.klass}. Try 'has_many #{reflection.name.inspect}, :through => #{through_reflection.name.inspect}, :source => <name>'. Is it one of #{source_associations.to_sentence(:two_words_connector => ' or ', :last_word_connector => ', or ', :locale => :en)}?")
+ else
+ super("Could not find the source association(s).")
+ end
end
end
- class HasManyThroughCantAssociateThroughHasOneOrManyReflection < ActiveRecordError #:nodoc:
- def initialize(owner, reflection)
- super("Cannot modify association '#{owner.class.name}##{reflection.name}' because the source reflection class '#{reflection.source_reflection.class_name}' is associated to '#{reflection.through_reflection.class_name}' via :#{reflection.source_reflection.macro}.")
+ class ThroughCantAssociateThroughHasOneOrManyReflection < ActiveRecordError #:nodoc:
+ def initialize(owner = nil, reflection = nil)
+ if owner && reflection
+ super("Cannot modify association '#{owner.class.name}##{reflection.name}' because the source reflection class '#{reflection.source_reflection.class_name}' is associated to '#{reflection.through_reflection.class_name}' via :#{reflection.source_reflection.macro}.")
+ else
+ super("Cannot modify association.")
+ end
end
end
+ class HasManyThroughCantAssociateThroughHasOneOrManyReflection < ThroughCantAssociateThroughHasOneOrManyReflection #:nodoc:
+ end
+
+ class HasOneThroughCantAssociateThroughHasOneOrManyReflection < ThroughCantAssociateThroughHasOneOrManyReflection #:nodoc:
+ end
+
class HasManyThroughCantAssociateNewRecords < ActiveRecordError #:nodoc:
- def initialize(owner, reflection)
- super("Cannot associate new records through '#{owner.class.name}##{reflection.name}' on '#{reflection.source_reflection.class_name rescue nil}##{reflection.source_reflection.name rescue nil}'. Both records must have an id in order to create the has_many :through record associating them.")
+ def initialize(owner = nil, reflection = nil)
+ if owner && reflection
+ super("Cannot associate new records through '#{owner.class.name}##{reflection.name}' on '#{reflection.source_reflection.class_name rescue nil}##{reflection.source_reflection.name rescue nil}'. Both records must have an id in order to create the has_many :through record associating them.")
+ else
+ super("Cannot associate new records.")
+ end
end
end
class HasManyThroughCantDissociateNewRecords < ActiveRecordError #:nodoc:
- def initialize(owner, reflection)
- super("Cannot dissociate new records through '#{owner.class.name}##{reflection.name}' on '#{reflection.source_reflection.class_name rescue nil}##{reflection.source_reflection.name rescue nil}'. Both records must have an id in order to delete the has_many :through record associating them.")
+ def initialize(owner = nil, reflection = nil)
+ if owner && reflection
+ super("Cannot dissociate new records through '#{owner.class.name}##{reflection.name}' on '#{reflection.source_reflection.class_name rescue nil}##{reflection.source_reflection.name rescue nil}'. Both records must have an id in order to delete the has_many :through record associating them.")
+ else
+ super("Cannot dissociate new records.")
+ end
end
end
- class HasManyThroughNestedAssociationsAreReadonly < ActiveRecordError #:nodoc:
- def initialize(owner, reflection)
- super("Cannot modify association '#{owner.class.name}##{reflection.name}' because it goes through more than one other association.")
+ class ThroughNestedAssociationsAreReadonly < ActiveRecordError #:nodoc:
+ def initialize(owner = nil, reflection = nil)
+ if owner && reflection
+ super("Cannot modify association '#{owner.class.name}##{reflection.name}' because it goes through more than one other association.")
+ else
+ super("Through nested associations are read-only.")
+ end
end
end
+ class HasManyThroughNestedAssociationsAreReadonly < ThroughNestedAssociationsAreReadonly #:nodoc:
+ end
+
+ class HasOneThroughNestedAssociationsAreReadonly < ThroughNestedAssociationsAreReadonly #:nodoc:
+ end
+
class EagerLoadPolymorphicError < ActiveRecordError #:nodoc:
- def initialize(reflection)
- super("Cannot eagerly load the polymorphic association #{reflection.name.inspect}")
+ def initialize(reflection = nil)
+ if reflection
+ super("Cannot eagerly load the polymorphic association #{reflection.name.inspect}")
+ else
+ super("Eager load polymorphic error.")
+ end
end
end
class ReadOnlyAssociation < ActiveRecordError #:nodoc:
- def initialize(reflection)
- super("Cannot add to a has_many :through association. Try adding to #{reflection.through_reflection.name.inspect}.")
+ def initialize(reflection = nil)
+ if reflection
+ super("Cannot add to a has_many :through association. Try adding to #{reflection.through_reflection.name.inspect}.")
+ else
+ super("Read-only reflection error.")
+ end
end
end
@@ -101,8 +173,12 @@ module ActiveRecord
# (has_many, has_one) when there is at least 1 child associated instance.
# ex: if @project.tasks.size > 0, DeleteRestrictionError will be raised when trying to destroy @project
class DeleteRestrictionError < ActiveRecordError #:nodoc:
- def initialize(name)
- super("Cannot delete record because of dependent #{name}")
+ def initialize(name = nil)
+ if name
+ super("Cannot delete record because of dependent #{name}")
+ else
+ super("Delete restriction error.")
+ end
end
end
@@ -185,7 +261,7 @@ module ActiveRecord
super
end
- # Returns the specified association instance if it responds to :loaded?, nil otherwise.
+ # Returns the specified association instance if it exists, nil otherwise.
def association_instance_get(name)
@association_cache[name]
end
@@ -266,7 +342,6 @@ module ActiveRecord
# others.find(*args) | X | X | X
# others.exists? | X | X | X
# others.distinct | X | X | X
- # others.uniq | X | X | X
# others.reset | X | X | X
#
# === Overriding generated methods
@@ -285,7 +360,7 @@ module ActiveRecord
# end
#
# If your model class is <tt>Project</tt>, the module is
- # named <tt>Project::GeneratedFeatureMethods</tt>. The GeneratedFeatureMethods module is
+ # named <tt>Project::GeneratedAssociationMethods</tt>. The GeneratedAssociationMethods module is
# included in the model class immediately after the (anonymous) generated attributes methods
# module, meaning an association will override the methods for an attribute with the same name.
#
@@ -371,14 +446,14 @@ module ActiveRecord
# The tables for these classes could look something like:
#
# CREATE TABLE users (
- # id int(11) NOT NULL auto_increment,
- # account_id int(11) default NULL,
+ # id int NOT NULL auto_increment,
+ # account_id int default NULL,
# name varchar default NULL,
# PRIMARY KEY (id)
# )
#
# CREATE TABLE accounts (
- # id int(11) NOT NULL auto_increment,
+ # id int NOT NULL auto_increment,
# name varchar default NULL,
# PRIMARY KEY (id)
# )
@@ -608,10 +683,10 @@ module ActiveRecord
# @tag = @post.tags.build name: "ruby"
# @tag.save
#
- # The last line ought to save the through record (a <tt>Taggable</tt>). This will only work if the
+ # The last line ought to save the through record (a <tt>Tagging</tt>). This will only work if the
# <tt>:inverse_of</tt> is set:
#
- # class Taggable < ActiveRecord::Base
+ # class Tagging < ActiveRecord::Base
# belongs_to :post
# belongs_to :tag, inverse_of: :taggings
# end
@@ -632,7 +707,7 @@ module ActiveRecord
# You can turn off the automatic detection of inverse associations by setting
# the <tt>:inverse_of</tt> option to <tt>false</tt> like so:
#
- # class Taggable < ActiveRecord::Base
+ # class Tagging < ActiveRecord::Base
# belongs_to :tag, inverse_of: false
# end
#
@@ -956,20 +1031,16 @@ module ActiveRecord
# The +traps+ association on +Dungeon+ and the +dungeon+ association on +Trap+ are
# the inverse of each other and the inverse of the +dungeon+ association on +EvilWizard+
# is the +evil_wizard+ association on +Dungeon+ (and vice-versa). By default,
- # Active Record doesn't know anything about these inverse relationships and so no object
- # loading optimization is possible. For example:
+ # Active Record can guess the inverse of the association based on the name
+ # of the class. The result is the following:
#
# d = Dungeon.first
# t = d.traps.first
- # d.level == t.dungeon.level # => true
- # d.level = 10
- # d.level == t.dungeon.level # => false
+ # d.object_id == t.dungeon.object_id # => true
#
# The +Dungeon+ instances +d+ and <tt>t.dungeon</tt> in the above example refer to
- # the same object data from the database, but are actually different in-memory copies
- # of that data. Specifying the <tt>:inverse_of</tt> option on associations lets you tell
- # Active Record about inverse relationships and it will optimise object loading. For
- # example, if we changed our model definitions to:
+ # the same in-memory instance since the association matches the name of the class.
+ # The result would be the same if we added +:inverse_of+ to our model definitions:
#
# class Dungeon < ActiveRecord::Base
# has_many :traps, inverse_of: :dungeon
@@ -984,15 +1055,14 @@ module ActiveRecord
# belongs_to :dungeon, inverse_of: :evil_wizard
# end
#
- # Then, from our code snippet above, +d+ and <tt>t.dungeon</tt> are actually the same
- # in-memory instance and our final <tt>d.level == t.dungeon.level</tt> will return +true+.
- #
# There are limitations to <tt>:inverse_of</tt> support:
#
# * does not work with <tt>:through</tt> associations.
# * does not work with <tt>:polymorphic</tt> associations.
# * for +belongs_to+ associations +has_many+ inverse associations are ignored.
#
+ # For more information, see the documentation for the +:inverse_of+ option.
+ #
# == Deleting from associations
#
# === Dependent associations
@@ -1722,7 +1792,7 @@ module ActiveRecord
Builder::HasMany.define_callbacks self, middle_reflection
Reflection.add_reflection self, middle_reflection.name, middle_reflection
- middle_reflection.parent_reflection = [name.to_s, habtm_reflection]
+ middle_reflection.parent_reflection = habtm_reflection
include Module.new {
class_eval <<-RUBY, __FILE__, __LINE__ + 1
@@ -1738,12 +1808,12 @@ module ActiveRecord
hm_options[:through] = middle_reflection.name
hm_options[:source] = join_model.right_reflection.name
- [:before_add, :after_add, :before_remove, :after_remove, :autosave, :validate, :join_table, :class_name].each do |k|
+ [:before_add, :after_add, :before_remove, :after_remove, :autosave, :validate, :join_table, :class_name, :extend].each do |k|
hm_options[k] = options[k] if options.key? k
end
has_many name, scope, hm_options, &extension
- self._reflections[name.to_s].parent_reflection = [name.to_s, habtm_reflection]
+ self._reflections[name.to_s].parent_reflection = habtm_reflection
end
end
end
diff --git a/activerecord/lib/active_record/associations/alias_tracker.rb b/activerecord/lib/active_record/associations/alias_tracker.rb
index 2b7e4f28c5..021bc32237 100644
--- a/activerecord/lib/active_record/associations/alias_tracker.rb
+++ b/activerecord/lib/active_record/associations/alias_tracker.rb
@@ -2,8 +2,7 @@ require 'active_support/core_ext/string/conversions'
module ActiveRecord
module Associations
- # Keeps track of table aliases for ActiveRecord::Associations::ClassMethods::JoinDependency and
- # ActiveRecord::Associations::ThroughAssociationScope
+ # Keeps track of table aliases for ActiveRecord::Associations::JoinDependency
class AliasTracker # :nodoc:
attr_reader :aliases
diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb
index 930f678ae8..c7b396f3d4 100644
--- a/activerecord/lib/active_record/associations/association.rb
+++ b/activerecord/lib/active_record/associations/association.rb
@@ -211,9 +211,12 @@ module ActiveRecord
# the kind of the class of the associated objects. Meant to be used as
# a sanity check when you are about to assign an associated record.
def raise_on_type_mismatch!(record)
- unless record.is_a?(reflection.klass) || record.is_a?(reflection.class_name.constantize)
- message = "#{reflection.class_name}(##{reflection.klass.object_id}) expected, got #{record.class}(##{record.class.object_id})"
- raise ActiveRecord::AssociationTypeMismatch, message
+ unless record.is_a?(reflection.klass)
+ fresh_class = reflection.class_name.safe_constantize
+ unless fresh_class && record.is_a?(fresh_class)
+ message = "#{reflection.class_name}(##{reflection.klass.object_id}) expected, got #{record.class}(##{record.class.object_id})"
+ raise ActiveRecord::AssociationTypeMismatch, message
+ end
end
end
@@ -248,6 +251,14 @@ module ActiveRecord
initialize_attributes(record)
end
end
+
+ # Returns true if statement cache should be skipped on the association reader.
+ def skip_statement_cache?
+ reflection.scope_chain.any?(&:any?) ||
+ scope.eager_loading? ||
+ klass.scope_attributes? ||
+ reflection.source_reflection.active_record.default_scopes.any?
+ end
end
end
end
diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb
index 2416167834..a140dc239c 100644
--- a/activerecord/lib/active_record/associations/association_scope.rb
+++ b/activerecord/lib/active_record/associations/association_scope.rb
@@ -149,6 +149,7 @@ module ActiveRecord
scope.where_clause += item.where_clause
scope.order_values |= item.order_values
+ scope.unscope!(*item.unscope_values)
end
reflection = reflection.next
diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb
index 265a65c4c1..260a0c6a2d 100644
--- a/activerecord/lib/active_record/associations/belongs_to_association.rb
+++ b/activerecord/lib/active_record/associations/belongs_to_association.rb
@@ -107,7 +107,7 @@ module ActiveRecord
end
def stale_state
- result = owner._read_attribute(reflection.foreign_key)
+ result = owner._read_attribute(reflection.foreign_key) { |n| owner.send(:missing_attribute, n, caller) }
result && result.to_s
end
end
diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb
index 97eb007f62..6e4a53f7fb 100644
--- a/activerecord/lib/active_record/associations/builder/belongs_to.rb
+++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb
@@ -33,16 +33,24 @@ module ActiveRecord::Associations::Builder
if (@_after_create_counter_called ||= false)
@_after_create_counter_called = false
- elsif attribute_changed?(foreign_key) && !new_record? && reflection.constructable?
- model = reflection.klass
+ elsif attribute_changed?(foreign_key) && !new_record?
+ if reflection.polymorphic?
+ model = attribute(reflection.foreign_type).try(:constantize)
+ model_was = attribute_was(reflection.foreign_type).try(:constantize)
+ else
+ model = reflection.klass
+ model_was = reflection.klass
+ end
+
foreign_key_was = attribute_was foreign_key
foreign_key = attribute foreign_key
if foreign_key && model.respond_to?(:increment_counter)
model.increment_counter(cache_column, foreign_key)
end
- if foreign_key_was && model.respond_to?(:decrement_counter)
- model.decrement_counter(cache_column, foreign_key_was)
+
+ if foreign_key_was && model_was.respond_to?(:decrement_counter)
+ model_was.decrement_counter(cache_column, foreign_key_was)
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 ffd9c9d6fc..b18d99d54e 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
@@ -46,7 +46,7 @@ module ActiveRecord::Associations::Builder
join_model = Class.new(ActiveRecord::Base) {
class << self;
- attr_accessor :class_resolver
+ attr_accessor :left_model
attr_accessor :name
attr_accessor :table_name_resolver
attr_accessor :left_reflection
@@ -58,7 +58,7 @@ module ActiveRecord::Associations::Builder
end
def self.compute_type(class_name)
- class_resolver.compute_type class_name
+ left_model.compute_type class_name
end
def self.add_left_association(name, options)
@@ -72,11 +72,15 @@ module ActiveRecord::Associations::Builder
self.right_reflection = _reflect_on_association(rhs_name)
end
+ def self.retrieve_connection
+ left_model.retrieve_connection
+ end
+
}
join_model.name = "HABTM_#{association_name.to_s.camelize}"
join_model.table_name_resolver = habtm
- join_model.class_resolver = lhs_model
+ join_model.left_model = lhs_model
join_model.add_left_association :left_side, anonymous_class: lhs_model
join_model.add_right_association association_name, belongs_to_options(options)
diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb
index 6caadb4ce8..256df3ca11 100644
--- a/activerecord/lib/active_record/associations/collection_association.rb
+++ b/activerecord/lib/active_record/associations/collection_association.rb
@@ -1,3 +1,5 @@
+require "active_support/deprecation"
+
module ActiveRecord
module Associations
# = Active Record Association Collection
@@ -28,6 +30,12 @@ module ActiveRecord
# Implements the reader method, e.g. foo.items for Foo.has_many :items
def reader(force_reload = false)
if force_reload
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ Passing an argument to force an association to reload is now
+ deprecated and will be removed in Rails 5.1. Please call `reload`
+ on the result collection proxy instead.
+ MSG
+
klass.uncached { reload }
elsif stale_target?
reload
@@ -54,8 +62,10 @@ module ActiveRecord
record.send(reflection.association_primary_key)
end
else
- column = "#{reflection.quoted_table_name}.#{reflection.association_primary_key}"
- scope.pluck(column)
+ @association_ids ||= (
+ column = "#{reflection.quoted_table_name}.#{reflection.association_primary_key}"
+ scope.pluck(column)
+ )
end
end
@@ -432,12 +442,7 @@ module ActiveRecord
private
def get_records
- if reflection.scope_chain.any?(&:any?) ||
- scope.eager_loading? ||
- klass.scope_attributes?
-
- return scope.to_a
- end
+ return scope.to_a if skip_statement_cache?
conn = klass.connection
sc = reflection.association_scope_cache(conn, owner) do
diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb
index 685c3a5f17..9994b72158 100644
--- a/activerecord/lib/active_record/associations/collection_proxy.rb
+++ b/activerecord/lib/active_record/associations/collection_proxy.rb
@@ -127,7 +127,7 @@ module ActiveRecord
# # ]
#
# person.pets.find(1) # => #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>
- # person.pets.find(4) # => ActiveRecord::RecordNotFound: Couldn't find Pet with id=4
+ # person.pets.find(4) # => ActiveRecord::RecordNotFound: Couldn't find Pet with 'id'=4
#
# person.pets.find(2) { |pet| pet.name.downcase! }
# # => #<Pet id: 2, name: "fancy-fancy", person_id: 1>
@@ -227,6 +227,31 @@ module ActiveRecord
@association.last(*args)
end
+ # Gives a record (or N records if a parameter is supplied) from the collection
+ # using the same rules as <tt>ActiveRecord::Base.take</tt>.
+ #
+ # class Person < ActiveRecord::Base
+ # has_many :pets
+ # end
+ #
+ # person.pets
+ # # => [
+ # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
+ # # #<Pet id: 2, name: "Spook", person_id: 1>,
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
+ # # ]
+ #
+ # person.pets.take # => #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>
+ #
+ # person.pets.take(2)
+ # # => [
+ # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
+ # # #<Pet id: 2, name: "Spook", person_id: 1>
+ # # ]
+ #
+ # another_person_without.pets # => []
+ # another_person_without.pets.take # => nil
+ # another_person_without.pets.take(2) # => []
def take(n = nil)
@association.take(n)
end
@@ -418,7 +443,7 @@ module ActiveRecord
# person.pets.delete_all
#
# Pet.find(1, 2, 3)
- # # => ActiveRecord::RecordNotFound
+ # # => ActiveRecord::RecordNotFound: Couldn't find all Pets with 'id': (1, 2, 3)
#
# If it is set to <tt>:delete_all</tt>, all the objects are deleted
# *without* calling their +destroy+ method.
@@ -438,7 +463,7 @@ module ActiveRecord
# person.pets.delete_all
#
# Pet.find(1, 2, 3)
- # # => ActiveRecord::RecordNotFound
+ # # => ActiveRecord::RecordNotFound: Couldn't find all Pets with 'id': (1, 2, 3)
def delete_all(dependent = nil)
@association.delete_all(dependent)
end
@@ -470,15 +495,16 @@ module ActiveRecord
@association.destroy_all
end
- # Deletes the +records+ supplied and removes them from the collection. For
- # +has_many+ associations, the deletion is done according to the strategy
- # specified by the <tt>:dependent</tt> option. Returns an array with the
+ # Deletes the +records+ supplied from the collection according to the strategy
+ # specified by the +:dependent+ option. If no +:dependent+ option is given,
+ # then it will follow the default strategy. Returns an array with the
# deleted records.
#
- # If no <tt>:dependent</tt> option is given, then it will follow the default
- # strategy. The default strategy is <tt>:nullify</tt>. This sets the foreign
- # keys to <tt>NULL</tt>. For, +has_many+ <tt>:through</tt>, the default
- # strategy is +delete_all+.
+ # For +has_many :through+ associations, the default deletion strategy is
+ # +:delete_all+.
+ #
+ # For +has_many+ associations, the default deletion strategy is +:nullify+.
+ # This sets the foreign keys to +NULL+.
#
# class Person < ActiveRecord::Base
# has_many :pets # dependent: :nullify option by default
@@ -531,7 +557,7 @@ module ActiveRecord
# # => [#<Pet id: 2, name: "Spook", person_id: 1>]
#
# Pet.find(1, 3)
- # # => ActiveRecord::RecordNotFound: Couldn't find all Pets with IDs (1, 3)
+ # # => ActiveRecord::RecordNotFound: Couldn't find all Pets with 'id': (1, 3)
#
# If it is set to <tt>:delete_all</tt>, all the +records+ are deleted
# *without* calling their +destroy+ method.
@@ -559,7 +585,7 @@ module ActiveRecord
# # ]
#
# Pet.find(1)
- # # => ActiveRecord::RecordNotFound: Couldn't find Pet with id=1
+ # # => ActiveRecord::RecordNotFound: Couldn't find Pet with 'id'=1
#
# You can pass +Fixnum+ or +String+ values, it finds the records
# responding to the +id+ and executes delete on them.
@@ -623,7 +649,7 @@ module ActiveRecord
# person.pets.size # => 0
# person.pets # => []
#
- # Pet.find(1, 2, 3) # => ActiveRecord::RecordNotFound: Couldn't find all Pets with IDs (1, 2, 3)
+ # Pet.find(1, 2, 3) # => ActiveRecord::RecordNotFound: Couldn't find all Pets with 'id': (1, 2, 3)
#
# You can pass +Fixnum+ or +String+ values, it finds the records
# responding to the +id+ and then deletes them from the database.
@@ -655,7 +681,7 @@ module ActiveRecord
# person.pets.size # => 0
# person.pets # => []
#
- # Pet.find(4, 5, 6) # => ActiveRecord::RecordNotFound: Couldn't find all Pets with IDs (4, 5, 6)
+ # Pet.find(4, 5, 6) # => ActiveRecord::RecordNotFound: Couldn't find all Pets with 'id': (4, 5, 6)
def destroy(*records)
@association.destroy(*records)
end
@@ -780,7 +806,7 @@ module ActiveRecord
# person.pets.any? # => false
#
# person.pets << Pet.new(name: 'Snoop')
- # person.pets.count # => 0
+ # person.pets.count # => 1
# person.pets.any? # => true
#
# You can also pass a +block+ to define criteria. The behavior
@@ -855,7 +881,7 @@ module ActiveRecord
!!@association.include?(record)
end
- def arel
+ def arel #:nodoc:
scope.arel
end
@@ -970,7 +996,7 @@ module ActiveRecord
alias_method :append, :<<
def prepend(*args)
- raise NoMethodError, "prepend on association is not defined. Please use << or append"
+ raise NoMethodError, "prepend on association is not defined. Please use <<, push or append"
end
# Equivalent to +delete_all+. The difference is that returns +self+, instead
diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb
index ca27c9fdde..7da20d8eea 100644
--- a/activerecord/lib/active_record/associations/has_many_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_association.rb
@@ -15,8 +15,15 @@ module ActiveRecord
when :restrict_with_error
unless empty?
- record = klass.human_attribute_name(reflection.name).downcase
- owner.errors.add(:base, :"restrict_dependent_destroy.many", record: record)
+ record = owner.class.human_attribute_name(reflection.name).downcase
+ message = owner.errors.generate_message(:base, :'restrict_dependent_destroy.many', record: record, raise: true) rescue nil
+ if message
+ ActiveSupport::Deprecation.warn(<<-MESSAGE.squish)
+ The error key `:'restrict_dependent_destroy.many'` has been deprecated and will be removed in Rails 5.1.
+ Please use `:'restrict_dependent_destroy.has_many'` instead.
+ MESSAGE
+ end
+ owner.errors.add(:base, message || :'restrict_dependent_destroy.has_many', record: record)
throw(:abort)
end
@@ -43,7 +50,7 @@ module ActiveRecord
end
def empty?
- if has_cached_counter?
+ if reflection.has_cached_counter?
size.zero?
else
super
@@ -66,8 +73,8 @@ module ActiveRecord
# If the collection is empty the target is set to an empty array and
# the loaded flag is set to true as well.
def count_records
- count = if has_cached_counter?
- owner._read_attribute cached_counter_attribute_name
+ count = if reflection.has_cached_counter?
+ owner._read_attribute reflection.counter_cache_column
else
scope.count
end
@@ -80,70 +87,26 @@ module ActiveRecord
[association_scope.limit_value, count].compact.min
end
- def has_cached_counter?(reflection = reflection())
- owner.attribute_present?(cached_counter_attribute_name(reflection))
- end
-
- def cached_counter_attribute_name(reflection = reflection())
- if reflection.options[:counter_cache]
- reflection.options[:counter_cache].to_s
- else
- "#{reflection.name}_count"
- end
- 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)
+ if reflection.has_cached_counter?
+ owner.class.update_counters(owner.id, reflection.counter_cache_column => difference)
end
end
def update_counter_in_memory(difference, reflection = reflection())
- if counter_must_be_updated_by_has_many?(reflection)
- counter = cached_counter_attribute_name(reflection)
+ if reflection.counter_must_be_updated_by_has_many?
+ counter = reflection.counter_cache_column
+ owner[counter] ||= 0
owner[counter] += difference
- owner.send(:clear_attribute_changes, counter) # eww
+ owner.send(:clear_attribute_change, counter) # eww
end
end
- # This shit is nasty. We need to avoid the following situation:
- #
- # * An associated record is deleted via record.destroy
- # * Hence the callbacks run, and they find a belongs_to on the record with a
- # :counter_cache options which points back at our owner. So they update the
- # counter cache.
- # * In which case, we must make sure to *not* update the counter cache, or else
- # it will be decremented twice.
- #
- # Hence this method.
- def inverse_which_updates_counter_cache(reflection = reflection())
- counter_name = cached_counter_attribute_name(reflection)
- inverse_which_updates_counter_named(counter_name, reflection)
- end
- alias inverse_updates_counter_cache? inverse_which_updates_counter_cache
-
- def inverse_which_updates_counter_named(counter_name, reflection)
- reflection.klass._reflections.values.find { |inverse_reflection|
- inverse_reflection.belongs_to? &&
- inverse_reflection.counter_cache_column == counter_name
- }
- end
-
- def inverse_updates_counter_in_memory?(reflection)
- inverse = inverse_which_updates_counter_cache(reflection)
- inverse && inverse == reflection.inverse_of
- end
-
- def counter_must_be_updated_by_has_many?(reflection)
- !inverse_updates_counter_in_memory?(reflection) && has_cached_counter?(reflection)
- end
-
def delete_count(method, scope)
if method == :delete_all
scope.delete_all
@@ -161,7 +124,7 @@ module ActiveRecord
def delete_records(records, method)
if method == :destroy
records.each(&:destroy!)
- update_counter(-records.length) unless inverse_updates_counter_cache?
+ update_counter(-records.length) unless reflection.inverse_updates_counter_cache?
else
scope = self.scope.where(reflection.klass.primary_key => records)
update_counter(-delete_count(method, scope))
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 cd79266952..deb0f8c9f5 100644
--- a/activerecord/lib/active_record/associations/has_many_through_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_through_association.rb
@@ -110,7 +110,7 @@ module ActiveRecord
def update_through_counter?(method)
case method
when :destroy
- !inverse_updates_counter_cache?(through_reflection)
+ !through_reflection.inverse_updates_counter_cache?
when :nullify
false
else
@@ -133,7 +133,7 @@ module ActiveRecord
if scope.klass.primary_key
count = scope.destroy_all.length
else
- scope.each { |record| record.run_callbacks :destroy }
+ scope.each(&:_run_destroy_callbacks)
arel = scope.arel
diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb
index 41a75b820e..0fe9b2e81b 100644
--- a/activerecord/lib/active_record/associations/has_one_association.rb
+++ b/activerecord/lib/active_record/associations/has_one_association.rb
@@ -1,5 +1,5 @@
module ActiveRecord
- # = Active Record Belongs To Has One Association
+ # = Active Record Has One Association
module Associations
class HasOneAssociation < SingularAssociation #:nodoc:
include ForeignAssociation
@@ -11,8 +11,15 @@ module ActiveRecord
when :restrict_with_error
if load_target
- record = klass.human_attribute_name(reflection.name).downcase
- owner.errors.add(:base, :"restrict_dependent_destroy.one", record: record)
+ record = owner.class.human_attribute_name(reflection.name).downcase
+ message = owner.errors.generate_message(:base, :'restrict_dependent_destroy.one', record: record, raise: true) rescue nil
+ if message
+ ActiveSupport::Deprecation.warn(<<-MESSAGE.squish)
+ The error key `:'restrict_dependent_destroy.one'` has been deprecated and will be removed in Rails 5.1.
+ Please use `:'restrict_dependent_destroy.has_one'` instead.
+ MESSAGE
+ end
+ owner.errors.add(:base, message || :'restrict_dependent_destroy.has_one', record: record)
throw(:abort)
end
@@ -58,7 +65,7 @@ module ActiveRecord
when :destroy
target.destroy
when :nullify
- target.update_columns(reflection.foreign_key => nil)
+ target.update_columns(reflection.foreign_key => nil) if target.persisted?
end
end
end
diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb
index 97f4bd3811..3992a240b9 100644
--- a/activerecord/lib/active_record/associations/preloader.rb
+++ b/activerecord/lib/active_record/associations/preloader.rb
@@ -10,13 +10,13 @@ module ActiveRecord
# end
#
# class Book < ActiveRecord::Base
- # # columns: title, sales
+ # # columns: title, sales, author_id
# end
#
# When you load an author with all associated books Active Record will make
# multiple queries like this:
#
- # Author.includes(:books).where(:name => ['bell hooks', 'Homer').to_a
+ # Author.includes(:books).where(name: ['bell hooks', 'Homer']).to_a
#
# => SELECT `authors`.* FROM `authors` WHERE `name` IN ('bell hooks', 'Homer')
# => SELECT `books`.* FROM `books` WHERE `author_id` IN (2, 5)
@@ -116,7 +116,7 @@ module ActiveRecord
when String
preloaders_for_one(association.to_sym, records, scope)
else
- raise ArgumentError, "#{association.inspect} was not recognised for preload"
+ raise ArgumentError, "#{association.inspect} was not recognized for preload"
end
end
@@ -160,7 +160,7 @@ module ActiveRecord
h
end
- class AlreadyLoaded
+ class AlreadyLoaded # :nodoc:
attr_reader :owners, :reflection
def initialize(klass, owners, reflection, preload_scope)
@@ -175,7 +175,7 @@ module ActiveRecord
end
end
- class NullPreloader
+ class NullPreloader # :nodoc:
def self.new(klass, owners, reflection, preload_scope); self; end
def self.run(preloader); end
def self.preloaded_records; []; end
diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb
index 1dc8bff193..92792a7a15 100644
--- a/activerecord/lib/active_record/associations/preloader/association.rb
+++ b/activerecord/lib/active_record/associations/preloader/association.rb
@@ -154,7 +154,7 @@ module ActiveRecord
scope.where!(klass.table_name => { reflection.type => model.base_class.sti_name })
end
- scope.unscope_values = Array(values[:unscope])
+ scope.unscope_values = Array(values[:unscope]) + Array(preload_values[:unscope])
klass.default_scoped.merge(scope)
end
end
diff --git a/activerecord/lib/active_record/associations/singular_association.rb b/activerecord/lib/active_record/associations/singular_association.rb
index 58d0f7d65d..03cb8cb8c3 100644
--- a/activerecord/lib/active_record/associations/singular_association.rb
+++ b/activerecord/lib/active_record/associations/singular_association.rb
@@ -1,9 +1,17 @@
+require "active_support/deprecation"
+
module ActiveRecord
module Associations
class SingularAssociation < Association #:nodoc:
# Implements the reader method, e.g. foo.bar for Foo.has_one :bar
def reader(force_reload = false)
- if force_reload
+ if force_reload && klass
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ Passing an argument to force an association to reload is now
+ deprecated and will be removed in Rails 5.1. Please call `reload`
+ on the parent object instead.
+ MSG
+
klass.uncached { reload }
elsif !loaded? || stale_target?
reload
@@ -39,12 +47,7 @@ module ActiveRecord
end
def get_records
- if reflection.scope_chain.any?(&:any?) ||
- scope.eager_loading? ||
- klass.scope_attributes?
-
- return scope.limit(1).to_a
- end
+ return scope.limit(1).to_a if skip_statement_cache?
conn = klass.connection
sc = reflection.association_scope_cache(conn, owner) do
diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb
index af1bce523c..d0ec3e8015 100644
--- a/activerecord/lib/active_record/associations/through_association.rb
+++ b/activerecord/lib/active_record/associations/through_association.rb
@@ -15,12 +15,6 @@ module ActiveRecord
scope = super
reflection.chain.drop(1).each do |reflection|
relation = reflection.klass.all
-
- reflection_scope = reflection.scope
- if reflection_scope && reflection_scope.arity.zero?
- relation = relation.merge(reflection_scope)
- end
-
scope.merge!(
relation.except(:select, :create_with, :includes, :preload, :joins, :eager_load)
)
@@ -82,13 +76,21 @@ module ActiveRecord
def ensure_mutable
unless source_reflection.belongs_to?
- raise HasManyThroughCantAssociateThroughHasOneOrManyReflection.new(owner, reflection)
+ if reflection.has_one?
+ raise HasOneThroughCantAssociateThroughHasOneOrManyReflection.new(owner, reflection)
+ else
+ raise HasManyThroughCantAssociateThroughHasOneOrManyReflection.new(owner, reflection)
+ end
end
end
def ensure_not_nested
if reflection.nested?
- raise HasManyThroughNestedAssociationsAreReadonly.new(owner, reflection)
+ if reflection.has_one?
+ raise HasOneThroughNestedAssociationsAreReadonly.new(owner, reflection)
+ else
+ raise HasManyThroughNestedAssociationsAreReadonly.new(owner, reflection)
+ end
end
end
diff --git a/activerecord/lib/active_record/attribute.rb b/activerecord/lib/active_record/attribute.rb
index 73dd3fa041..3c4c8f10ec 100644
--- a/activerecord/lib/active_record/attribute.rb
+++ b/activerecord/lib/active_record/attribute.rb
@@ -5,8 +5,8 @@ module ActiveRecord
FromDatabase.new(name, value, type)
end
- def from_user(name, value, type)
- FromUser.new(name, value, type)
+ def from_user(name, value, type, original_attribute = nil)
+ FromUser.new(name, value, type, original_attribute)
end
def with_cast_value(name, value, type)
@@ -26,36 +26,46 @@ module ActiveRecord
# This method should not be called directly.
# Use #from_database or #from_user
- def initialize(name, value_before_type_cast, type)
+ def initialize(name, value_before_type_cast, type, original_attribute = nil)
@name = name
@value_before_type_cast = value_before_type_cast
@type = type
+ @original_attribute = original_attribute
end
def value
# `defined?` is cheaper than `||=` when we get back falsy values
- @value = original_value unless defined?(@value)
+ @value = type_cast(value_before_type_cast) unless defined?(@value)
@value
end
def original_value
- type_cast(value_before_type_cast)
+ if assigned?
+ original_attribute.original_value
+ else
+ type_cast(value_before_type_cast)
+ end
end
def value_for_database
type.serialize(value)
end
- def changed_from?(old_value)
- type.changed?(old_value, value, value_before_type_cast)
+ def changed?
+ changed_from_assignment? || changed_in_place?
+ end
+
+ def changed_in_place?
+ has_been_read? && type.changed_in_place?(original_value_for_database, value)
end
- def changed_in_place_from?(old_value)
- has_been_read? && type.changed_in_place?(old_value, value)
+ def forgetting_assignment
+ with_value_from_database(value_for_database)
end
def with_value_from_user(value)
- self.class.from_user(name, value, type)
+ type.assert_valid_value(value)
+ self.class.from_user(name, value, type, self)
end
def with_value_from_database(value)
@@ -67,7 +77,7 @@ module ActiveRecord
end
def with_type(type)
- self.class.new(name, value_before_type_cast, type)
+ self.class.new(name, value_before_type_cast, type, original_attribute)
end
def type_cast(*)
@@ -100,16 +110,39 @@ module ActiveRecord
protected
+ attr_reader :original_attribute
+ alias_method :assigned?, :original_attribute
+
def initialize_dup(other)
if defined?(@value) && @value.duplicable?
@value = @value.dup
end
end
+ def changed_from_assignment?
+ assigned? && type.changed?(original_value, value, value_before_type_cast)
+ end
+
+ def original_value_for_database
+ if assigned?
+ original_attribute.original_value_for_database
+ else
+ _original_value_for_database
+ end
+ end
+
+ def _original_value_for_database
+ value_for_database
+ end
+
class FromDatabase < Attribute # :nodoc:
def type_cast(value)
type.deserialize(value)
end
+
+ def _original_value_for_database
+ value_before_type_cast
+ end
end
class FromUser < Attribute # :nodoc:
diff --git a/activerecord/lib/active_record/attribute/user_provided_default.rb b/activerecord/lib/active_record/attribute/user_provided_default.rb
new file mode 100644
index 0000000000..fb8ad9163e
--- /dev/null
+++ b/activerecord/lib/active_record/attribute/user_provided_default.rb
@@ -0,0 +1,23 @@
+require 'active_record/attribute'
+
+module ActiveRecord
+ class Attribute # :nodoc:
+ class UserProvidedDefault < FromUser
+ def initialize(name, value, type, database_default)
+ super(name, value, type, database_default)
+ end
+
+ def type_cast(value)
+ if value.is_a?(Proc)
+ super(value.call)
+ else
+ super
+ end
+ end
+
+ def with_type(type)
+ self.class.new(name, value_before_type_cast, type, original_attribute)
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/attribute_assignment.rb b/activerecord/lib/active_record/attribute_assignment.rb
index cc265e2af6..45fdcaa1cd 100644
--- a/activerecord/lib/active_record/attribute_assignment.rb
+++ b/activerecord/lib/active_record/attribute_assignment.rb
@@ -29,7 +29,8 @@ module ActiveRecord
assign_multiparameter_attributes(multi_parameter_attributes) unless multi_parameter_attributes.empty?
end
- # Re-raise with the ActiveRecord constant in case of an error
+ # Tries to assign given value to given attribute.
+ # In case of an error, re-raises with the ActiveRecord constant.
def _assign_attribute(k, v) # :nodoc:
super
rescue ActiveModel::UnknownAttributeError
diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb
index 9d58a19304..ca6ba18fe0 100644
--- a/activerecord/lib/active_record/attribute_methods.rb
+++ b/activerecord/lib/active_record/attribute_methods.rb
@@ -1,7 +1,7 @@
require 'active_support/core_ext/enumerable'
require 'active_support/core_ext/string/filters'
require 'mutex_m'
-require 'thread_safe'
+require 'concurrent'
module ActiveRecord
# = Active Record Attribute Methods
@@ -37,12 +37,12 @@ module ActiveRecord
class AttributeMethodCache
def initialize
@module = Module.new
- @method_cache = ThreadSafe::Cache.new
+ @method_cache = Concurrent::Map.new
end
def [](name)
@method_cache.compute_if_absent(name) do
- safe_name = name.unpack('h*').first
+ safe_name = name.unpack('h*'.freeze).first
temp_method = "__temp__#{safe_name}"
ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name
@module.module_eval method_body(temp_method, safe_name), __FILE__, __LINE__
@@ -96,7 +96,7 @@ module ActiveRecord
end
end
- # Raises a <tt>ActiveRecord::DangerousAttributeError</tt> exception when an
+ # Raises an <tt>ActiveRecord::DangerousAttributeError</tt> exception when an
# \Active \Record method is defined in the model, otherwise +false+.
#
# class Person < ActiveRecord::Base
@@ -106,7 +106,7 @@ module ActiveRecord
# end
#
# Person.instance_method_already_implemented?(:save)
- # # => ActiveRecord::DangerousAttributeError: save is defined by ActiveRecord
+ # # => ActiveRecord::DangerousAttributeError: save is defined by Active Record. Check to make sure that you don't have an attribute or method with the same name.
#
# Person.instance_method_already_implemented?(:name)
# # => false
@@ -230,7 +230,15 @@ module ActiveRecord
# person.respond_to(:nothing) # => false
def respond_to?(name, include_private = false)
return false unless super
- name = name.to_s
+
+ case name
+ when :to_partial_path
+ name = "to_partial_path".freeze
+ when :to_model
+ name = "to_model".freeze
+ else
+ name = name.to_s
+ end
# If the result is true then check for the select case.
# For queries selecting a subset of columns, return false for unselected columns.
@@ -377,27 +385,27 @@ module ActiveRecord
#
# For example:
#
- # class PostsController < ActionController::Base
- # after_action :print_accessed_fields, only: :index
+ # class PostsController < ActionController::Base
+ # after_action :print_accessed_fields, only: :index
#
- # def index
- # @posts = Post.all
- # end
+ # def index
+ # @posts = Post.all
+ # end
#
- # private
+ # private
#
- # def print_accessed_fields
- # p @posts.first.accessed_fields
+ # def print_accessed_fields
+ # p @posts.first.accessed_fields
+ # end
# end
- # end
#
# Which allows you to quickly change your code to:
#
- # class PostsController < ActionController::Base
- # def index
- # @posts = Post.select(:id, :title, :author_id, :updated_at)
+ # class PostsController < ActionController::Base
+ # def index
+ # @posts = Post.select(:id, :title, :author_id, :updated_at)
+ # end
# end
- # end
def accessed_fields
@attributes.accessed
end
diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb
index 7ba907f786..0bcfa5f00d 100644
--- a/activerecord/lib/active_record/attribute_methods/dirty.rb
+++ b/activerecord/lib/active_record/attribute_methods/dirty.rb
@@ -1,4 +1,5 @@
require 'active_support/core_ext/module/attribute_accessors'
+require 'active_record/attribute_mutation_tracker'
module ActiveRecord
module AttributeMethods
@@ -34,23 +35,43 @@ module ActiveRecord
# <tt>reload</tt> the record and clears changed attributes.
def reload(*)
super.tap do
- clear_changes_information
+ @mutation_tracker = nil
+ @previous_mutation_tracker = nil
+ @changed_attributes = HashWithIndifferentAccess.new
end
end
def initialize_dup(other) # :nodoc:
super
- calculate_changes_from_defaults
+ @attributes = self.class._default_attributes.map do |attr|
+ attr.with_value_from_user(@attributes.fetch_value(attr.name))
+ end
+ @mutation_tracker = nil
end
def changes_applied
- super
- store_original_raw_attributes
+ @previous_mutation_tracker = mutation_tracker
+ @changed_attributes = HashWithIndifferentAccess.new
+ store_original_attributes
end
def clear_changes_information
+ @previous_mutation_tracker = nil
+ @changed_attributes = HashWithIndifferentAccess.new
+ store_original_attributes
+ end
+
+ def raw_write_attribute(attr_name, *)
+ result = super
+ clear_attribute_change(attr_name)
+ result
+ end
+
+ def clear_attribute_changes(attr_names)
super
- original_raw_attributes.clear
+ attr_names.each do |attr_name|
+ clear_attribute_change(attr_name)
+ end
end
def changed_attributes
@@ -59,7 +80,7 @@ module ActiveRecord
if defined?(@cached_changed_attributes)
@cached_changed_attributes
else
- super.reverse_merge(attributes_changed_in_place).freeze
+ super.reverse_merge(mutation_tracker.changed_values).freeze
end
end
@@ -69,58 +90,29 @@ module ActiveRecord
end
end
+ def previous_changes
+ previous_mutation_tracker.changes
+ end
+
def attribute_changed_in_place?(attr_name)
- old_value = original_raw_attribute(attr_name)
- @attributes[attr_name].changed_in_place_from?(old_value)
+ mutation_tracker.changed_in_place?(attr_name)
end
private
- def changes_include?(attr_name)
- super || attribute_changed_in_place?(attr_name)
- end
-
- def calculate_changes_from_defaults
- @changed_attributes = nil
- self.class.column_defaults.each do |attr, orig_value|
- set_attribute_was(attr, orig_value) if _field_changed?(attr, orig_value)
+ def mutation_tracker
+ unless defined?(@mutation_tracker)
+ @mutation_tracker = nil
end
+ @mutation_tracker ||= AttributeMutationTracker.new(@attributes)
end
- # Wrap write_attribute to remember original attribute value.
- def write_attribute(attr, value)
- attr = attr.to_s
-
- old_value = old_attribute_value(attr)
-
- 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_by_setter?(attr)
- clear_attribute_changes(attr) unless _field_changed?(attr, old_value)
- else
- set_attribute_was(attr, old_value) if _field_changed?(attr, old_value)
- end
+ def changes_include?(attr_name)
+ super || mutation_tracker.changed?(attr_name)
end
- def old_attribute_value(attr)
- if attribute_changed?(attr)
- changed_attributes[attr]
- else
- clone_attribute_value(:_read_attribute, attr)
- end
+ def clear_attribute_change(attr_name)
+ mutation_tracker.forget_change(attr_name)
end
def _update_record(*)
@@ -135,48 +127,24 @@ module ActiveRecord
changed & self.class.column_names
end
- def _field_changed?(attr, old_value)
- @attributes[attr].changed_from?(old_value)
- end
-
- def attributes_changed_in_place
- changed_in_place.each_with_object({}) do |attr_name, h|
- orig = @attributes[attr_name].original_value
- h[attr_name] = orig
- end
+ def store_original_attributes
+ @attributes = @attributes.map(&:forgetting_assignment)
+ @mutation_tracker = nil
end
- def changed_in_place
- self.class.attribute_names.select do |attr_name|
- attribute_changed_in_place?(attr_name)
- end
- 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 rescue nil
- end
-
- def store_original_raw_attributes
- attribute_names.each do |attr|
- store_original_raw_attribute(attr)
- end
+ def previous_mutation_tracker
+ @previous_mutation_tracker ||= NullMutationTracker.instance
end
def cache_changed_attributes
@cached_changed_attributes = changed_attributes
yield
ensure
- remove_instance_variable(:@cached_changed_attributes)
+ clear_changed_attributes_cache
+ end
+
+ def clear_changed_attributes_cache
+ remove_instance_variable(:@cached_changed_attributes) if defined?(@cached_changed_attributes)
end
end
end
diff --git a/activerecord/lib/active_record/attribute_methods/query.rb b/activerecord/lib/active_record/attribute_methods/query.rb
index 553122a5fc..10498f4322 100644
--- a/activerecord/lib/active_record/attribute_methods/query.rb
+++ b/activerecord/lib/active_record/attribute_methods/query.rb
@@ -19,7 +19,7 @@ module ActiveRecord
if Numeric === value || value !~ /[^0-9]/
!value.to_i.zero?
else
- return false if ActiveRecord::ConnectionAdapters::Column::FALSE_VALUES.include?(value)
+ return false if ActiveModel::Type::Boolean::FALSE_VALUES.include?(value)
!value.blank?
end
elsif value.respond_to?(:zero?)
diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb
index 0d989c2eca..5197e21fa4 100644
--- a/activerecord/lib/active_record/attribute_methods/read.rb
+++ b/activerecord/lib/active_record/attribute_methods/read.rb
@@ -37,7 +37,7 @@ module ActiveRecord
protected
def define_method_attribute(name)
- safe_name = name.unpack('h*').first
+ safe_name = name.unpack('h*'.freeze).first
temp_method = "__temp__#{safe_name}"
ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name
@@ -56,14 +56,12 @@ module ActiveRecord
end
end
- ID = 'id'.freeze
-
# 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, &block)
name = attr_name.to_s
- name = self.class.primary_key if name == ID
+ name = self.class.primary_key if name == 'id'.freeze
_read_attribute(name, &block)
end
diff --git a/activerecord/lib/active_record/attribute_methods/serialization.rb b/activerecord/lib/active_record/attribute_methods/serialization.rb
index d0d8a968c5..60eecab0d0 100644
--- a/activerecord/lib/active_record/attribute_methods/serialization.rb
+++ b/activerecord/lib/active_record/attribute_methods/serialization.rb
@@ -11,6 +11,18 @@ module ActiveRecord
# serialized object must be of that class on assignment and retrieval.
# Otherwise <tt>SerializationTypeMismatch</tt> will be raised.
#
+ # Empty objects as <tt>{}</tt>, in the case of +Hash+, or <tt>[]</tt>, in the case of
+ # +Array+, will always be persisted as null.
+ #
+ # Keep in mind that database adapters handle certain serialization tasks
+ # for you. For instance: +json+ and +jsonb+ types in PostgreSQL will be
+ # converted between JSON object/array syntax and Ruby +Hash+ or +Array+
+ # objects transparently. There is no need to use +serialize+ in this
+ # case.
+ #
+ # For more complex cases, such as conversion to or from your application
+ # domain objects, consider using the ActiveRecord::Attributes API.
+ #
# ==== Parameters
#
# * +attr_name+ - The field name that should be serialized.
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 f9beb43e4b..9e693b6aee 100644
--- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
+++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
@@ -13,7 +13,7 @@ module ActiveRecord
set_time_zone_without_conversion(super)
elsif value.respond_to?(:in_time_zone)
begin
- user_input_in_time_zone(value) || super
+ super(user_input_in_time_zone(value)) || super
rescue ArgumentError
nil
end
diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb
index ab017c7b54..bbf2a51a0e 100644
--- a/activerecord/lib/active_record/attribute_methods/write.rb
+++ b/activerecord/lib/active_record/attribute_methods/write.rb
@@ -24,7 +24,7 @@ module ActiveRecord
protected
def define_method_attribute=(name)
- safe_name = name.unpack('h*').first
+ safe_name = name.unpack('h*'.freeze).first
ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name
generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
@@ -45,7 +45,7 @@ module ActiveRecord
write_attribute_with_type_cast(attr_name, value, true)
end
- def raw_write_attribute(attr_name, value)
+ def raw_write_attribute(attr_name, value) # :nodoc:
write_attribute_with_type_cast(attr_name, value, false)
end
diff --git a/activerecord/lib/active_record/attribute_mutation_tracker.rb b/activerecord/lib/active_record/attribute_mutation_tracker.rb
new file mode 100644
index 0000000000..0133b4d0be
--- /dev/null
+++ b/activerecord/lib/active_record/attribute_mutation_tracker.rb
@@ -0,0 +1,70 @@
+module ActiveRecord
+ class AttributeMutationTracker # :nodoc:
+ def initialize(attributes)
+ @attributes = attributes
+ end
+
+ def changed_values
+ attr_names.each_with_object({}.with_indifferent_access) do |attr_name, result|
+ if changed?(attr_name)
+ result[attr_name] = attributes[attr_name].original_value
+ end
+ end
+ end
+
+ def changes
+ attr_names.each_with_object({}.with_indifferent_access) do |attr_name, result|
+ if changed?(attr_name)
+ result[attr_name] = [attributes[attr_name].original_value, attributes.fetch_value(attr_name)]
+ end
+ end
+ end
+
+ def changed?(attr_name)
+ attr_name = attr_name.to_s
+ attributes[attr_name].changed?
+ end
+
+ def changed_in_place?(attr_name)
+ attributes[attr_name].changed_in_place?
+ end
+
+ def forget_change(attr_name)
+ attr_name = attr_name.to_s
+ attributes[attr_name] = attributes[attr_name].forgetting_assignment
+ end
+
+ protected
+
+ attr_reader :attributes
+
+ private
+
+ def attr_names
+ attributes.keys
+ end
+ end
+
+ class NullMutationTracker # :nodoc:
+ include Singleton
+
+ def changed_values
+ {}
+ end
+
+ def changes
+ {}
+ end
+
+ def changed?(*)
+ false
+ end
+
+ def changed_in_place?(*)
+ false
+ end
+
+ def forget_change(*)
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/attribute_set.rb b/activerecord/lib/active_record/attribute_set.rb
index 013a7d0e01..026b3cf014 100644
--- a/activerecord/lib/active_record/attribute_set.rb
+++ b/activerecord/lib/active_record/attribute_set.rb
@@ -60,8 +60,14 @@ module ActiveRecord
super
end
+ def deep_dup
+ dup.tap do |copy|
+ copy.instance_variable_set(:@attributes, attributes.deep_dup)
+ end
+ end
+
def initialize_dup(_)
- @attributes = attributes.deep_dup
+ @attributes = attributes.dup
super
end
@@ -80,6 +86,11 @@ module ActiveRecord
attributes.select { |_, attr| attr.has_been_read? }.keys
end
+ def map(&block)
+ new_attributes = attributes.transform_values(&block)
+ AttributeSet.new(new_attributes)
+ end
+
protected
attr_reader :attributes
diff --git a/activerecord/lib/active_record/attribute_set/builder.rb b/activerecord/lib/active_record/attribute_set/builder.rb
index e85777c335..f974b7a876 100644
--- a/activerecord/lib/active_record/attribute_set/builder.rb
+++ b/activerecord/lib/active_record/attribute_set/builder.rb
@@ -1,3 +1,5 @@
+require 'active_record/attribute'
+
module ActiveRecord
class AttributeSet # :nodoc:
class Builder # :nodoc:
@@ -45,8 +47,14 @@ module ActiveRecord
delegate_hash[key] = value
end
+ def deep_dup
+ dup.tap do |copy|
+ copy.instance_variable_set(:@delegate_hash, delegate_hash.transform_values(&:dup))
+ end
+ end
+
def initialize_dup(_)
- @delegate_hash = delegate_hash.transform_values(&:dup)
+ @delegate_hash = Hash[delegate_hash]
super
end
diff --git a/activerecord/lib/active_record/attributes.rb b/activerecord/lib/active_record/attributes.rb
index 50339b6f69..8b2c4c7170 100644
--- a/activerecord/lib/active_record/attributes.rb
+++ b/activerecord/lib/active_record/attributes.rb
@@ -1,3 +1,5 @@
+require 'active_record/attribute/user_provided_default'
+
module ActiveRecord
# See ActiveRecord::Attributes::ClassMethods for documentation
module Attributes
@@ -80,6 +82,14 @@ module ActiveRecord
#
# StoreListing.new.my_string # => "new default"
#
+ # class Product < ActiveRecord::Base
+ # attribute :my_default_proc, :datetime, default: -> { Time.now }
+ # end
+ #
+ # Product.new.my_default_proc # => 2015-05-30 11:04:48 -0600
+ # sleep 1
+ # Product.new.my_default_proc # => 2015-05-30 11:04:49 -0600
+ #
# Attributes do not need to be backed by a database column.
#
# class MyModel < ActiveRecord::Base
@@ -202,7 +212,8 @@ module ActiveRecord
#
# +default+ The default value to use when no value is provided. If this option
# is not passed, the previous default value (if any) will be used.
- # Otherwise, the default will be +nil+.
+ # Otherwise, the default will be +nil+. A proc can also be passed, and
+ # will be called once each time a new value is needed.
#
# +user_provided_default+ Whether the default value should be cast using
# +cast+ or +deserialize+.
@@ -236,7 +247,12 @@ module ActiveRecord
if value == NO_DEFAULT_PROVIDED
default_attribute = _default_attributes[name].with_type(type)
elsif from_user
- default_attribute = Attribute.from_user(name, value, type)
+ default_attribute = Attribute::UserProvidedDefault.new(
+ name,
+ value,
+ type,
+ _default_attributes[name],
+ )
else
default_attribute = Attribute.from_database(name, value, type)
end
diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb
index 0792d19c3e..d0de42d27c 100644
--- a/activerecord/lib/active_record/autosave_association.rb
+++ b/activerecord/lib/active_record/autosave_association.rb
@@ -222,6 +222,7 @@ module ActiveRecord
true
end
validate validation_method
+ after_validation :_ensure_no_duplicate_errors
end
end
end
@@ -233,7 +234,7 @@ module ActiveRecord
super
end
- # Marks this record to be destroyed as part of the parents save transaction.
+ # Marks this record to be destroyed as part of the parent's save transaction.
# This does _not_ actually destroy the record instantly, rather child record will be destroyed
# when <tt>parent.save</tt> is called.
#
@@ -242,7 +243,7 @@ module ActiveRecord
@marked_for_destruction = true
end
- # Returns whether or not this record will be destroyed as part of the parents save transaction.
+ # Returns whether or not this record will be destroyed as part of the parent's save transaction.
#
# Only useful if the <tt>:autosave</tt> option on the parent is enabled for this associated model.
def marked_for_destruction?
@@ -324,7 +325,7 @@ module ActiveRecord
# the parent, <tt>self</tt>, if it wasn't. Skips any <tt>:autosave</tt>
# enabled records if they're marked_for_destruction? or destroyed.
def association_valid?(reflection, record)
- return true if record.destroyed? || record.marked_for_destruction?
+ return true if record.destroyed? || (reflection.options[:autosave] && record.marked_for_destruction?)
validation_context = self.validation_context unless [:create, :update].include?(self.validation_context)
unless valid = record.valid?(validation_context)
@@ -456,5 +457,11 @@ module ActiveRecord
end
end
end
+
+ def _ensure_no_duplicate_errors
+ errors.messages.each_key do |attribute|
+ errors[attribute].uniq!
+ end
+ end
end
end
diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb
index 9c5b7d937d..4b66d8cd36 100644
--- a/activerecord/lib/active_record/base.rb
+++ b/activerecord/lib/active_record/base.rb
@@ -1,11 +1,9 @@
require 'yaml'
-require 'set'
require 'active_support/benchmarkable'
require 'active_support/dependencies'
require 'active_support/descendants_tracker'
require 'active_support/time'
require 'active_support/core_ext/module/attribute_accessors'
-require 'active_support/core_ext/class/delegating_attributes'
require 'active_support/core_ext/array/extract_options'
require 'active_support/core_ext/hash/deep_merge'
require 'active_support/core_ext/hash/slice'
@@ -281,6 +279,7 @@ module ActiveRecord #:nodoc:
extend Explain
extend Enum
extend Delegation::DelegateCache
+ extend CollectionCacheKey
include Core
include Persistence
diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb
index 2fcba8e309..ccdbebbc77 100644
--- a/activerecord/lib/active_record/callbacks.rb
+++ b/activerecord/lib/active_record/callbacks.rb
@@ -194,7 +194,7 @@ module ActiveRecord
#
# If the +before_validation+ callback throws +:abort+, the process will be
# aborted and <tt>Base#save</tt> will return +false+. If Base#save! is called it will raise a
- # ActiveRecord::RecordInvalid exception. Nothing will be appended to the errors object.
+ # <tt>ActiveRecord::RecordInvalid</tt> exception. Nothing will be appended to the errors object.
#
# == Canceling callbacks
#
@@ -206,7 +206,8 @@ module ActiveRecord
# == Ordering callbacks
#
# Sometimes the code needs that the callbacks execute in a specific order. For example, a +before_destroy+
- # callback (+log_children+ in this case) should be executed before the children get destroyed by the +dependent: destroy+ option.
+ # callback (+log_children+ in this case) should be executed before the children get destroyed by the
+ # <tt>dependent: destroy</tt> option.
#
# Let's look at the code below:
#
@@ -289,24 +290,28 @@ module ActiveRecord
end
def destroy #:nodoc:
- run_callbacks(:destroy) { super }
+ _run_destroy_callbacks { super }
+ rescue RecordNotDestroyed => e
+ @_association_destroy_exception = e
+ false
end
def touch(*) #:nodoc:
- run_callbacks(:touch) { super }
+ _run_touch_callbacks { super }
end
private
+
def create_or_update(*) #:nodoc:
- run_callbacks(:save) { super }
+ _run_save_callbacks { super }
end
def _create_record #:nodoc:
- run_callbacks(:create) { super }
+ _run_create_callbacks { super }
end
def _update_record(*) #:nodoc:
- run_callbacks(:update) { super }
+ _run_update_callbacks { super }
end
end
end
diff --git a/activerecord/lib/active_record/coders/yaml_column.rb b/activerecord/lib/active_record/coders/yaml_column.rb
index 9ea22ed798..2456b8ad8c 100644
--- a/activerecord/lib/active_record/coders/yaml_column.rb
+++ b/activerecord/lib/active_record/coders/yaml_column.rb
@@ -14,10 +14,7 @@ module ActiveRecord
def dump(obj)
return if obj.nil?
- unless obj.is_a?(object_class)
- raise SerializationTypeMismatch,
- "Attribute was supposed to be a #{object_class}, but was a #{obj.class}. -- #{obj.inspect}"
- end
+ assert_valid_value(obj)
YAML.dump obj
end
@@ -26,15 +23,19 @@ module ActiveRecord
return yaml unless yaml.is_a?(String) && yaml =~ /^---/
obj = YAML.load(yaml)
- unless obj.is_a?(object_class) || obj.nil?
- raise SerializationTypeMismatch,
- "Attribute was supposed to be a #{object_class}, but was a #{obj.class}"
- end
+ assert_valid_value(obj)
obj ||= object_class.new if object_class != Object
obj
end
+ def assert_valid_value(obj)
+ unless obj.nil? || obj.is_a?(object_class)
+ raise SerializationTypeMismatch,
+ "Attribute was supposed to be a #{object_class}, but was a #{obj.class}. -- #{obj.inspect}"
+ end
+ end
+
private
def check_arity_of_constructor
diff --git a/activerecord/lib/active_record/collection_cache_key.rb b/activerecord/lib/active_record/collection_cache_key.rb
new file mode 100644
index 0000000000..3c4ca3d116
--- /dev/null
+++ b/activerecord/lib/active_record/collection_cache_key.rb
@@ -0,0 +1,31 @@
+module ActiveRecord
+ module CollectionCacheKey
+
+ def collection_cache_key(collection = all, timestamp_column = :updated_at) # :nodoc:
+ query_signature = Digest::MD5.hexdigest(collection.to_sql)
+ key = "#{collection.model_name.cache_key}/query-#{query_signature}"
+
+ if collection.loaded?
+ size = collection.size
+ timestamp = collection.max_by(&timestamp_column).public_send(timestamp_column)
+ else
+ column_type = type_for_attribute(timestamp_column.to_s)
+ column = "#{connection.quote_table_name(collection.table_name)}.#{connection.quote_column_name(timestamp_column)}"
+
+ query = collection
+ .select("COUNT(*) AS size", "MAX(#{column}) AS timestamp")
+ .unscope(:order)
+ result = connection.select_one(query)
+
+ size = result["size"]
+ timestamp = column_type.deserialize(result["timestamp"])
+ end
+
+ if timestamp
+ "#{key}-#{size}-#{timestamp.utc.to_s(cache_timestamp_format)}"
+ else
+ "#{key}-#{size}"
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
index 8c50f3d1a3..b579bc1e93 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
@@ -1,7 +1,6 @@
require 'thread'
-require 'thread_safe'
+require 'concurrent'
require 'monitor'
-require 'set'
module ActiveRecord
# Raised when a connection could not be obtained within the connection
@@ -10,6 +9,12 @@ module ActiveRecord
class ConnectionTimeoutError < ConnectionNotEstablished
end
+ # Raised when a pool was unable to get ahold of all its connections
+ # to perform a "group" action such as +ConnectionPool#disconnect!+
+ # or +ConnectionPool#clear_reloadable_connections!+.
+ class ExclusiveConnectionTimeoutError < ConnectionTimeoutError
+ end
+
module ConnectionAdapters
# Connection pool base class for managing Active Record database
# connections.
@@ -63,6 +68,15 @@ module ActiveRecord
# connection at the end of a thread or a thread dies unexpectedly.
# Regardless of this setting, the Reaper will be invoked before every
# blocking wait. (Default nil, which means don't schedule the Reaper).
+ #
+ #--
+ # Synchronization policy:
+ # * all public methods can be called outside +synchronize+
+ # * access to these i-vars needs to be in +synchronize+:
+ # * @connections
+ # * @now_connecting
+ # * private methods that require being called in a +synchronize+ blocks
+ # are now explicitly documented
class ConnectionPool
# Threadsafe, fair, FIFO queue. Meant to be used by ConnectionPool
# with which it shares a Monitor. But could be a generic Queue.
@@ -129,17 +143,15 @@ module ActiveRecord
# - ConnectionTimeoutError if +timeout+ is given and no element
# becomes available within +timeout+ seconds,
def poll(timeout = nil)
- synchronize do
- if timeout
- no_wait_poll || wait_poll(timeout)
- else
- no_wait_poll
- end
- end
+ synchronize { internal_poll(timeout) }
end
private
+ def internal_poll(timeout)
+ no_wait_poll || (timeout && wait_poll(timeout))
+ end
+
def synchronize(&block)
@lock.synchronize(&block)
end
@@ -193,6 +205,80 @@ module ActiveRecord
end
end
+ # Adds the ability to turn a basic fair FIFO queue into one
+ # biased to some thread.
+ module BiasableQueue # :nodoc:
+ class BiasedConditionVariable # :nodoc:
+ # semantics of condition variables guarantee that +broadcast+, +broadcast_on_biased+,
+ # +signal+ and +wait+ methods are only called while holding a lock
+ def initialize(lock, other_cond, preferred_thread)
+ @real_cond = lock.new_cond
+ @other_cond = other_cond
+ @preferred_thread = preferred_thread
+ @num_waiting_on_real_cond = 0
+ end
+
+ def broadcast
+ broadcast_on_biased
+ @other_cond.broadcast
+ end
+
+ def broadcast_on_biased
+ @num_waiting_on_real_cond = 0
+ @real_cond.broadcast
+ end
+
+ def signal
+ if @num_waiting_on_real_cond > 0
+ @num_waiting_on_real_cond -= 1
+ @real_cond
+ else
+ @other_cond
+ end.signal
+ end
+
+ def wait(timeout)
+ if Thread.current == @preferred_thread
+ @num_waiting_on_real_cond += 1
+ @real_cond
+ else
+ @other_cond
+ end.wait(timeout)
+ end
+ end
+
+ def with_a_bias_for(thread)
+ previous_cond = nil
+ new_cond = nil
+ synchronize do
+ previous_cond = @cond
+ @cond = new_cond = BiasedConditionVariable.new(@lock, @cond, thread)
+ end
+ yield
+ ensure
+ synchronize do
+ @cond = previous_cond if previous_cond
+ new_cond.broadcast_on_biased if new_cond # wake up any remaining sleepers
+ end
+ end
+ end
+
+ # Connections must be leased while holding the main pool mutex. This is
+ # an internal subclass that also +.leases+ returned connections while
+ # still in queue's critical section (queue synchronizes with the same
+ # +@lock+ as the main pool) so that a returned connection is already
+ # leased and there is no need to re-enter synchronized block.
+ class ConnectionLeasingQueue < Queue # :nodoc:
+ include BiasableQueue
+
+ private
+ def internal_poll(timeout)
+ conn = super
+ conn.lease if conn
+ conn
+ end
+ end
+
# Every +frequency+ seconds, the reaper will call +reap+ on +pool+.
# A reaper instantiated with a nil frequency will never reap the
# connection pool.
@@ -220,7 +306,7 @@ module ActiveRecord
include MonitorMixin
- attr_accessor :automatic_reconnect, :checkout_timeout
+ attr_accessor :automatic_reconnect, :checkout_timeout, :schema_cache
attr_reader :spec, :connections, :size, :reaper
# Creates a new ConnectionPool object. +spec+ is a ConnectionSpecification
@@ -241,56 +327,75 @@ module ActiveRecord
# default max pool size to 5
@size = (spec.config[:pool] && spec.config[:pool].to_i) || 5
- # The cache of reserved connections mapped to threads
- @reserved_connections = ThreadSafe::Cache.new(:initial_capacity => @size)
+ # The cache of threads mapped to reserved connections, the sole purpose
+ # of the cache is to speed-up +connection+ method, it is not the authoritative
+ # registry of which thread owns which connection, that is tracked by
+ # +connection.owner+ attr on each +connection+ instance.
+ # The invariant works like this: if there is mapping of +thread => conn+,
+ # then that +thread+ does indeed own that +conn+, however an absence of a such
+ # mapping does not mean that the +thread+ doesn't own the said connection, in
+ # that case +conn.owner+ attr should be consulted.
+ # Access and modification of +@thread_cached_conns+ does not require
+ # synchronization.
+ @thread_cached_conns = Concurrent::Map.new(:initial_capacity => @size)
@connections = []
@automatic_reconnect = true
- @available = Queue.new self
+ # Connection pool allows for concurrent (outside the main `synchronize` section)
+ # establishment of new connections. This variable tracks the number of threads
+ # currently in the process of independently establishing connections to the DB.
+ @now_connecting = 0
+
+ # A boolean toggle that allows/disallows new connections.
+ @new_cons_enabled = true
+
+ @available = ConnectionLeasingQueue.new self
end
# Retrieve the connection associated with the current thread, or call
# #checkout to obtain one if necessary.
#
# #connection can be called any number of times; the connection is
- # held in a hash keyed by the thread id.
+ # held in a cache keyed by a thread.
def connection
- # this is correctly done double-checked locking
- # (ThreadSafe::Cache's lookups have volatile semantics)
- @reserved_connections[current_connection_id] || synchronize do
- @reserved_connections[current_connection_id] ||= checkout
- end
+ @thread_cached_conns[connection_cache_key(Thread.current)] ||= checkout
end
# Is there an open connection that is being used for the current thread?
+ #
+ # This method only works for connections that have been obtained through
+ # #connection or #with_connection methods, connections obtained through
+ # #checkout will not be detected by #active_connection?
def active_connection?
- synchronize do
- @reserved_connections.fetch(current_connection_id) {
- return false
- }.in_use?
- end
+ @thread_cached_conns[connection_cache_key(Thread.current)]
end
# Signal that the thread is finished with the current connection.
# #release_connection releases the connection-thread association
# and returns the connection to the pool.
- def release_connection(with_id = current_connection_id)
- synchronize do
- conn = @reserved_connections.delete(with_id)
- checkin conn if conn
+ #
+ # This method only works for connections that have been obtained through
+ # #connection or #with_connection methods, connections obtained through
+ # #checkout will not be automatically released.
+ def release_connection(owner_thread = Thread.current)
+ if conn = @thread_cached_conns.delete(connection_cache_key(owner_thread))
+ checkin conn
end
end
- # If a connection already exists yield it to the block. If no connection
+ # If a connection obtained through #connection or #with_connection methods
+ # already exists yield it to the block. If no such connection
# exists checkout a connection, yield it to the block, and checkin the
# connection when finished.
def with_connection
- connection_id = current_connection_id
- fresh_connection = true unless active_connection?
- yield connection
+ unless conn = @thread_cached_conns[connection_cache_key(Thread.current)]
+ conn = connection
+ fresh_connection = true
+ end
+ yield conn
ensure
- release_connection(connection_id) if fresh_connection
+ release_connection if fresh_connection
end
# Returns true if a connection has already been opened.
@@ -299,32 +404,81 @@ module ActiveRecord
end
# Disconnects all connections in the pool, and clears the pool.
- def disconnect!
- synchronize do
- @reserved_connections.clear
- @connections.each do |conn|
- checkin conn
- conn.disconnect!
+ #
+ # Raises:
+ # - +ExclusiveConnectionTimeoutError+ if unable to gain ownership of all
+ # connections in the pool within a timeout interval (default duration is
+ # +spec.config[:checkout_timeout] * 2+ seconds).
+ def disconnect(raise_on_acquisition_timeout = true)
+ with_exclusively_acquired_all_connections(raise_on_acquisition_timeout) do
+ synchronize do
+ @connections.each do |conn|
+ checkin conn
+ conn.disconnect!
+ end
+ @connections = []
+ @available.clear
end
- @connections = []
- @available.clear
end
end
- # Clears the cache which maps classes.
- def clear_reloadable_connections!
- synchronize do
- @reserved_connections.clear
- @connections.each do |conn|
- checkin conn
- conn.disconnect! if conn.requires_reloading?
- end
- @connections.delete_if(&:requires_reloading?)
- @available.clear
- @connections.each do |conn|
- @available.add conn
+ # Disconnects all connections in the pool, and clears the pool.
+ #
+ # The pool first tries to gain ownership of all connections, if unable to
+ # do so within a timeout interval (default duration is
+ # +spec.config[:checkout_timeout] * 2+ seconds), the pool is forcefully
+ # disconnected without any regard for other connection owning threads.
+ def disconnect!
+ disconnect(false)
+ end
+
+ # Clears the cache which maps classes and re-connects connections that
+ # require reloading.
+ #
+ # Raises:
+ # - +ExclusiveConnectionTimeoutError+ if unable to gain ownership of all
+ # connections in the pool within a timeout interval (default duration is
+ # +spec.config[:checkout_timeout] * 2+ seconds).
+ def clear_reloadable_connections(raise_on_acquisition_timeout = true)
+ num_new_conns_required = 0
+
+ with_exclusively_acquired_all_connections(raise_on_acquisition_timeout) do
+ synchronize do
+ @connections.each do |conn|
+ checkin conn
+ conn.disconnect! if conn.requires_reloading?
+ end
+ @connections.delete_if(&:requires_reloading?)
+
+ @available.clear
+
+ if @connections.size < @size
+ # because of the pruning done by this method, we might be running
+ # low on connections, while threads stuck in queue are helpless
+ # (not being able to establish new connections for themselves),
+ # see also more detailed explanation in +remove+
+ num_new_conns_required = num_waiting_in_queue - @connections.size
+ end
+
+ @connections.each do |conn|
+ @available.add conn
+ end
end
end
+
+ bulk_make_new_connections(num_new_conns_required) if num_new_conns_required > 0
+ end
+
+ # Clears the cache which maps classes and re-connects connections that
+ # require reloading.
+ #
+ # The pool first tries to gain ownership of all connections, if unable to
+ # do so within a timeout interval (default duration is
+ # +spec.config[:checkout_timeout] * 2+ seconds), the pool forcefully
+ # clears the cache and reloads connections without any regard for other
+ # connection owning threads.
+ def clear_reloadable_connections!
+ clear_reloadable_connections(false)
end
# Check-out a database connection from the pool, indicating that you want
@@ -341,12 +495,8 @@ module ActiveRecord
#
# Raises:
# - ConnectionTimeoutError: no connection can be obtained from the pool.
- def checkout
- synchronize do
- conn = acquire_connection
- conn.lease
- checkout_and_verify(conn)
- end
+ def checkout(checkout_timeout = @checkout_timeout)
+ checkout_and_verify(acquire_connection(checkout_timeout))
end
# Check-in a database connection back into the pool, indicating that you
@@ -356,14 +506,12 @@ module ActiveRecord
# calling +checkout+ on this pool.
def checkin(conn)
synchronize do
- owner = conn.owner
+ remove_connection_from_thread_cache conn
- conn.run_callbacks :checkin do
+ conn._run_checkin_callbacks do
conn.expire
end
- release conn, owner
-
@available.add conn
end
end
@@ -371,14 +519,32 @@ module ActiveRecord
# Remove a connection from the connection pool. The connection will
# remain open and active but will no longer be managed by this pool.
def remove(conn)
+ needs_new_connection = false
+
synchronize do
+ remove_connection_from_thread_cache conn
+
@connections.delete conn
@available.delete conn
- release conn, conn.owner
-
- @available.add checkout_new_connection if @available.any_waiting?
+ # @available.any_waiting? => true means that prior to removing this
+ # conn, the pool was at its max size (@connections.size == @size)
+ # this would mean that any threads stuck waiting in the queue wouldn't
+ # know they could checkout_new_connection, so let's do it for them.
+ # Because condition-wait loop is encapsulated in the Queue class
+ # (that in turn is oblivious to ConnectionPool implementation), threads
+ # that are "stuck" there are helpless, they have no way of creating
+ # new connections and are completely reliant on us feeding available
+ # connections into the Queue.
+ needs_new_connection = @available.any_waiting?
end
+
+ # This is intentionally done outside of the synchronized section as we
+ # would like not to hold the main mutex while checking out new connections,
+ # thus there is some chance that needs_new_connection information is now
+ # stale, we can live with that (bulk_make_new_connections will make
+ # sure not to exceed the pool's @size limit).
+ bulk_make_new_connections(1) if needs_new_connection
end
# Recover lost connections for the pool. A lost connection can occur if
@@ -403,7 +569,118 @@ module ActiveRecord
end
end
+ def num_waiting_in_queue # :nodoc:
+ @available.num_waiting
+ end
+
private
+ #--
+ # this is unfortunately not concurrent
+ def bulk_make_new_connections(num_new_conns_needed)
+ num_new_conns_needed.times do
+ # try_to_checkout_new_connection will not exceed pool's @size limit
+ if new_conn = try_to_checkout_new_connection
+ # make the new_conn available to the starving threads stuck @available Queue
+ checkin(new_conn)
+ end
+ end
+ end
+
+ #--
+ # From the discussion on GitHub:
+ # https://github.com/rails/rails/pull/14938#commitcomment-6601951
+ # This hook-in method allows for easier monkey-patching fixes needed by
+ # JRuby users that use Fibers.
+ def connection_cache_key(thread)
+ thread
+ end
+
+ # Take control of all existing connections so a "group" action such as
+ # reload/disconnect can be performed safely. It is no longer enough to
+ # wrap it in +synchronize+ because some pool's actions are allowed
+ # to be performed outside of the main +synchronize+ block.
+ def with_exclusively_acquired_all_connections(raise_on_acquisition_timeout = true)
+ with_new_connections_blocked do
+ attempt_to_checkout_all_existing_connections(raise_on_acquisition_timeout)
+ yield
+ end
+ end
+
+ def attempt_to_checkout_all_existing_connections(raise_on_acquisition_timeout = true)
+ collected_conns = synchronize do
+ # account for our own connections
+ @connections.select {|conn| conn.owner == Thread.current}
+ end
+
+ newly_checked_out = []
+ timeout_time = Time.now + (@checkout_timeout * 2)
+
+ @available.with_a_bias_for(Thread.current) do
+ while true
+ synchronize do
+ return if collected_conns.size == @connections.size && @now_connecting == 0
+ remaining_timeout = timeout_time - Time.now
+ remaining_timeout = 0 if remaining_timeout < 0
+ conn = checkout_for_exclusive_access(remaining_timeout)
+ collected_conns << conn
+ newly_checked_out << conn
+ end
+ end
+ end
+ rescue ExclusiveConnectionTimeoutError
+ # `raise_on_acquisition_timeout == false` means we are directed to ignore any
+ # timeouts and are expected to just give up: we've obtained as many connections
+ # as possible, note that in a case like that we don't return any of the
+ # `newly_checked_out` connections.
+
+ if raise_on_acquisition_timeout
+ release_newly_checked_out = true
+ raise
+ end
+ rescue Exception # if something else went wrong
+ # this can't be a "naked" rescue, because we have should return conns
+ # even for non-StandardErrors
+ release_newly_checked_out = true
+ raise
+ ensure
+ if release_newly_checked_out && newly_checked_out
+ # releasing only those conns that were checked out in this method, conns
+ # checked outside this method (before it was called) are not for us to release
+ newly_checked_out.each {|conn| checkin(conn)}
+ end
+ end
+
+ #--
+ # Must be called in a synchronize block.
+ def checkout_for_exclusive_access(checkout_timeout)
+ checkout(checkout_timeout)
+ rescue ConnectionTimeoutError
+ # this block can't be easily moved into attempt_to_checkout_all_existing_connections's
+ # rescue block, because doing so would put it outside of synchronize section, without
+ # being in a critical section thread_report might become inaccurate
+ msg = "could not obtain ownership of all database connections in #{checkout_timeout} seconds"
+
+ thread_report = []
+ @connections.each do |conn|
+ unless conn.owner == Thread.current
+ thread_report << "#{conn} is owned by #{conn.owner}"
+ end
+ end
+
+ msg << " (#{thread_report.join(', ')})" if thread_report.any?
+
+ raise ExclusiveConnectionTimeoutError, msg
+ end
+
+ def with_new_connections_blocked
+ previous_value = nil
+ synchronize do
+ previous_value, @new_cons_enabled = @new_cons_enabled, false
+ end
+ yield
+ ensure
+ synchronize { @new_cons_enabled = previous_value }
+ end
# Acquire a connection by one of 1) immediately removing one
# from the queue of available connections, 2) creating a new
@@ -412,44 +689,82 @@ module ActiveRecord
#
# Raises:
# - ConnectionTimeoutError if a connection could not be acquired
- def acquire_connection
- if conn = @available.poll
+ #
+ #--
+ # Implementation detail: the connection returned by +acquire_connection+
+ # will already be "+connection.lease+ -ed" to the current thread.
+ def acquire_connection(checkout_timeout)
+ # NOTE: we rely on `@available.poll` and `try_to_checkout_new_connection` to
+ # `conn.lease` the returned connection (and to do this in a `synchronized`
+ # section), this is not the cleanest implementation, as ideally we would
+ # `synchronize { conn.lease }` in this method, but by leaving it to `@available.poll`
+ # and `try_to_checkout_new_connection` we can piggyback on `synchronize` sections
+ # of the said methods and avoid an additional `synchronize` overhead.
+ if conn = @available.poll || try_to_checkout_new_connection
conn
- elsif @connections.size < @size
- checkout_new_connection
else
reap
- @available.poll(@checkout_timeout)
+ @available.poll(checkout_timeout)
end
end
- def release(conn, owner)
- thread_id = owner.object_id
-
- if @reserved_connections[thread_id] == conn
- @reserved_connections.delete thread_id
- end
+ #--
+ # if owner_thread param is omitted, this must be called in synchronize block
+ def remove_connection_from_thread_cache(conn, owner_thread = conn.owner)
+ @thread_cached_conns.delete_pair(connection_cache_key(owner_thread), conn)
end
+ alias_method :release, :remove_connection_from_thread_cache
def new_connection
- Base.send(spec.adapter_method, spec.config)
+ Base.send(spec.adapter_method, spec.config).tap do |conn|
+ conn.schema_cache = schema_cache.dup if schema_cache
+ end
+ end
+
+ # If the pool is not at a +@size+ limit, establish new connection. Connecting
+ # to the DB is done outside main synchronized section.
+ #--
+ # Implementation constraint: a newly established connection returned by this
+ # method must be in the +.leased+ state.
+ def try_to_checkout_new_connection
+ # first in synchronized section check if establishing new conns is allowed
+ # and increment @now_connecting, to prevent overstepping this pool's @size
+ # constraint
+ do_checkout = synchronize do
+ if @new_cons_enabled && (@connections.size + @now_connecting) < @size
+ @now_connecting += 1
+ end
+ end
+ if do_checkout
+ begin
+ # if successfully incremented @now_connecting establish new connection
+ # outside of synchronized section
+ conn = checkout_new_connection
+ ensure
+ synchronize do
+ if conn
+ adopt_connection(conn)
+ # returned conn needs to be already leased
+ conn.lease
+ end
+ @now_connecting -= 1
+ end
+ end
+ end
end
- def current_connection_id #:nodoc:
- Base.connection_id ||= Thread.current.object_id
+ def adopt_connection(conn)
+ conn.pool = self
+ @connections << conn
end
def checkout_new_connection
raise ConnectionNotEstablished unless @automatic_reconnect
-
- c = new_connection
- c.pool = self
- @connections << c
- c
+ new_connection
end
def checkout_and_verify(c)
- c.run_callbacks :checkout do
+ c._run_checkout_callbacks do
c.verify!
end
c
@@ -509,11 +824,11 @@ module ActiveRecord
# These caches are keyed by klass.name, NOT klass. Keying them by klass
# alone would lead to memory leaks in development mode as all previous
# instances of the class would stay in memory.
- @owner_to_pool = ThreadSafe::Cache.new(:initial_capacity => 2) do |h,k|
- h[k] = ThreadSafe::Cache.new(:initial_capacity => 2)
+ @owner_to_pool = Concurrent::Map.new(:initial_capacity => 2) do |h,k|
+ h[k] = Concurrent::Map.new(:initial_capacity => 2)
end
- @class_to_pool = ThreadSafe::Cache.new(:initial_capacity => 2) do |h,k|
- h[k] = ThreadSafe::Cache.new
+ @class_to_pool = Concurrent::Map.new(:initial_capacity => 2) do |h,k|
+ h[k] = Concurrent::Map.new
end
end
@@ -618,7 +933,9 @@ module ActiveRecord
# A connection was established in an ancestor process that must have
# subsequently forked. We can't reuse the connection, but we can copy
# the specification and establish a new connection with it.
- establish_connection owner, ancestor_pool.spec
+ establish_connection(owner, ancestor_pool.spec).tap do |pool|
+ pool.schema_cache = ancestor_pool.schema_cache if ancestor_pool.schema_cache
+ end
else
owner_to_pool[owner.name] = nil
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb
index c0a2111571..30b2fca2ca 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb
@@ -18,9 +18,9 @@ module ActiveRecord
end
# Returns the maximum allowed length for an index name. This
- # limit is enforced by rails and Is less than or equal to
+ # limit is enforced by \Rails and is less than or equal to
# <tt>index_name_length</tt>. The gap between
- # <tt>index_name_length</tt> is to allow internal rails
+ # <tt>index_name_length</tt> is to allow internal \Rails
# operations to use prefixes in temporary operations.
def allowed_index_name_length
index_name_length
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 42c794c828..107806cd93 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
@@ -40,8 +40,9 @@ module ActiveRecord
# Returns a single value from a record
def select_value(arel, name = nil, binds = [])
- if result = select_one(arel, name, binds)
- result.values.first
+ arel, binds = binds_from_relation arel, binds
+ if result = select_rows(to_sql(arel, binds), name, binds).first
+ result.first
end
end
@@ -136,7 +137,7 @@ module ActiveRecord
#
# In order to get around this problem, #transaction will emulate the effect
# of nested transactions, by using savepoints:
- # http://dev.mysql.com/doc/refman/5.6/en/savepoint.html
+ # http://dev.mysql.com/doc/refman/5.7/en/savepoint.html
# Savepoints are supported by MySQL and PostgreSQL. SQLite3 version >= '3.6.8'
# supports savepoints.
#
@@ -188,8 +189,8 @@ module ActiveRecord
# You should consult the documentation for your database to understand the
# semantics of these different levels:
#
- # * http://www.postgresql.org/docs/9.1/static/transaction-iso.html
- # * https://dev.mysql.com/doc/refman/5.6/en/set-transaction.html
+ # * http://www.postgresql.org/docs/current/static/transaction-iso.html
+ # * https://dev.mysql.com/doc/refman/5.7/en/set-transaction.html
#
# An <tt>ActiveRecord::TransactionIsolationError</tt> will be raised if:
#
@@ -288,8 +289,12 @@ module ActiveRecord
columns = schema_cache.columns_hash(table_name)
binds = fixture.map do |name, value|
- type = lookup_cast_type_from_column(columns[name])
- Relation::QueryAttribute.new(name, value, type)
+ if column = columns[name]
+ type = lookup_cast_type_from_column(column)
+ Relation::QueryAttribute.new(name, value, type)
+ else
+ raise Fixture::FixtureError, %(table "#{table_name}" has no column named "#{name}".)
+ end
end
key_list = fixture.keys.map { |name| quote_column_name(name) }
value_list = prepare_binds_for_database(binds).map do |value|
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
index 91c7298983..2c7409b2dc 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
@@ -123,6 +123,8 @@ module ActiveRecord
'f'
end
+ # Quote date/time values for use in SQL input. Includes microseconds
+ # if the value is a Time responding to usec.
def quoted_date(value)
if value.acts_like?(:time)
zone_conversion_method = ActiveRecord::Base.default_timezone == :utc ? :getutc : :getlocal
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 f754df93b6..0ba4d94e3c 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb
@@ -14,18 +14,16 @@ module ActiveRecord
send m, o
end
- def visit_AddColumn(o)
- "ADD #{accept(o)}"
- end
-
- delegate :quote_column_name, :quote_table_name, :quote_default_expression, :type_to_sql, to: :@conn
- private :quote_column_name, :quote_table_name, :quote_default_expression, :type_to_sql
+ delegate :quote_column_name, :quote_table_name, :quote_default_expression, :type_to_sql,
+ :options_include_default?, :supports_indexes_in_create?, :supports_foreign_keys?, :foreign_key_options, to: :@conn
+ private :quote_column_name, :quote_table_name, :quote_default_expression, :type_to_sql,
+ :options_include_default?, :supports_indexes_in_create?, :supports_foreign_keys?, :foreign_key_options
private
def visit_AlterTable(o)
sql = "ALTER TABLE #{quote_table_name(o.name)} "
- sql << o.adds.map { |col| visit_AddColumn col }.join(' ')
+ sql << o.adds.map { |col| accept 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
@@ -37,18 +35,37 @@ module ActiveRecord
column_sql
end
+ def visit_AddColumnDefinition(o)
+ "ADD #{accept(o.column)}"
+ end
+
def visit_TableDefinition(o)
- create_sql = "CREATE#{' TEMPORARY' if o.temporary} TABLE "
- create_sql << "#{quote_table_name(o.name)} "
- create_sql << "(#{o.columns.map { |c| accept c }.join(', ')}) " unless o.as
+ create_sql = "CREATE#{' TEMPORARY' if o.temporary} TABLE #{quote_table_name(o.name)} "
+
+ statements = o.columns.map { |c| accept c }
+ statements << accept(o.primary_keys) if o.primary_keys
+
+ if supports_indexes_in_create?
+ statements.concat(o.indexes.map { |column_name, options| index_in_create(o.name, column_name, options) })
+ end
+
+ if supports_foreign_keys?
+ statements.concat(o.foreign_keys.map { |to_table, options| foreign_key_in_create(o.name, to_table, options) })
+ end
+
+ create_sql << "(#{statements.join(', ')}) " if statements.present?
create_sql << "#{o.options}"
create_sql << " AS #{@conn.to_sql(o.as)}" if o.as
create_sql
end
- def visit_AddForeignKey(o)
+ def visit_PrimaryKeyDefinition(o)
+ "PRIMARY KEY (#{o.name.join(', ')})"
+ end
+
+ def visit_ForeignKeyDefinition(o)
sql = <<-SQL.strip_heredoc
- ADD CONSTRAINT #{quote_column_name(o.name)}
+ 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
@@ -57,6 +74,10 @@ module ActiveRecord
sql
end
+ def visit_AddForeignKey(o)
+ "ADD #{accept(o)}"
+ end
+
def visit_DropForeignKey(name)
"DROP CONSTRAINT #{quote_column_name(name)}"
end
@@ -70,6 +91,7 @@ module ActiveRecord
column_options[:after] = o.after
column_options[:auto_increment] = o.auto_increment
column_options[:primary_key] = o.primary_key
+ column_options[:collation] = o.collation
column_options
end
@@ -88,8 +110,9 @@ module ActiveRecord
sql
end
- def options_include_default?(options)
- options.include?(:default) && !(options[:null] == false && options[:default].nil?)
+ def foreign_key_in_create(from_table, to_table, options)
+ options = foreign_key_options(from_table, to_table, options)
+ accept ForeignKeyDefinition.new(from_table, to_table, options)
end
def action_sql(action, dependency)
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 cb83d0022c..10329de5f4 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
@@ -1,8 +1,3 @@
-require 'date'
-require 'set'
-require 'bigdecimal'
-require 'bigdecimal/util'
-
module ActiveRecord
module ConnectionAdapters #:nodoc:
# Abstract representation of an index definition on a table. Instances of
@@ -15,16 +10,22 @@ module ActiveRecord
# are typically created by methods in TableDefinition, and added to the
# +columns+ attribute of said TableDefinition object, in order to be used
# for generating a number of table creation or table changing SQL statements.
- class ColumnDefinition < Struct.new(:name, :type, :limit, :precision, :scale, :default, :null, :first, :after, :auto_increment, :primary_key, :sql_type) #:nodoc:
+ class ColumnDefinition < Struct.new(:name, :type, :limit, :precision, :scale, :default, :null, :first, :after, :auto_increment, :primary_key, :collation, :sql_type) #:nodoc:
def primary_key?
primary_key || type.to_sym == :primary_key
end
end
+ class AddColumnDefinition < Struct.new(:column) # :nodoc:
+ end
+
class ChangeColumnDefinition < Struct.new(:column, :name) #:nodoc:
end
+ class PrimaryKeyDefinition < Struct.new(:name) # :nodoc:
+ end
+
class ForeignKeyDefinition < Struct.new(:from_table, :to_table, :options) #:nodoc:
def name
options[:name]
@@ -209,6 +210,7 @@ module ActiveRecord
@columns_hash = {}
@indexes = {}
@foreign_keys = {}
+ @primary_keys = nil
@native = types
@temporary = temporary
@options = options
@@ -216,6 +218,12 @@ module ActiveRecord
@name = name
end
+ def primary_keys(name = nil) # :nodoc:
+ @primary_keys = PrimaryKeyDefinition.new(name) if name
+ @primary_keys
+ end
+
+ # Returns an array of ColumnDefinition objects for the columns of the table.
def columns; @columns_hash.values; end
# Returns a ColumnDefinition for the column with name +name+.
@@ -227,7 +235,7 @@ module ActiveRecord
# The +type+ parameter is normally one of the migrations native types,
# which is one of the following:
# <tt>:primary_key</tt>, <tt>:string</tt>, <tt>:text</tt>,
- # <tt>:integer</tt>, <tt>:float</tt>, <tt>:decimal</tt>,
+ # <tt>:integer</tt>, <tt>:bigint</tt>, <tt>:float</tt>, <tt>:decimal</tt>,
# <tt>:datetime</tt>, <tt>:time</tt>, <tt>:date</tt>,
# <tt>:binary</tt>, <tt>:boolean</tt>.
#
@@ -237,8 +245,8 @@ module ActiveRecord
#
# Available options are (none of these exists by default):
# * <tt>:limit</tt> -
- # Requests a maximum column length. This is number of characters for <tt>:string</tt> and
- # <tt>:text</tt> columns and number of bytes for <tt>:binary</tt> and <tt>:integer</tt> columns.
+ # Requests a maximum column length. This is number of characters for a <tt>:string</tt> column
+ # and number of bytes for <tt>:text</tt>, <tt>:binary</tt> and <tt>:integer</tt> columns.
# * <tt>:default</tt> -
# The column's default value. Use nil for NULL.
# * <tt>:null</tt> -
@@ -359,6 +367,7 @@ module ActiveRecord
def column(name, type, options = {})
name = name.to_s
type = type.to_sym
+ options = options.dup
if @columns_hash[name] && @columns_hash[name].primary_key?
raise ArgumentError, "you can't redefine the primary key column '#{name}'. To define a custom primary key, pass { id: false } to create_table."
@@ -370,6 +379,8 @@ module ActiveRecord
self
end
+ # remove the column +name+ from the table.
+ # remove_column(:account_id)
def remove_column(name)
@columns_hash.delete name.to_s
end
@@ -399,17 +410,12 @@ module ActiveRecord
column(:updated_at, :datetime, options)
end
- # Adds a reference. Optionally adds a +type+ column, if the
- # +:polymorphic+ option is provided. +references+ and +belongs_to+
- # are interchangeable. The reference column will be an +integer+ by default,
- # the +:type+ option can be used to specify a different type. A foreign
- # key will be created if the +:foreign_key+ option is passed.
+ # Adds a reference.
#
# t.references(:user)
- # t.references(:user, type: "string")
- # t.belongs_to(:supplier, polymorphic: true)
+ # t.belongs_to(:supplier, foreign_key: true)
#
- # See SchemaStatements#add_reference
+ # See SchemaStatements#add_reference for details of the options you can use.
def references(*args, **options)
args.each do |col|
ReferenceDefinition.new(col, **options).add_to(self)
@@ -433,6 +439,7 @@ module ActiveRecord
column.after = options[:after]
column.auto_increment = options[:auto_increment]
column.primary_key = type == :primary_key || options[:primary_key]
+ column.collation = options[:collation]
column
end
@@ -475,7 +482,7 @@ module ActiveRecord
def add_column(name, type, options)
name = name.to_s
type = type.to_sym
- @adds << @td.new_column_definition(name, type, options)
+ @adds << AddColumnDefinition.new(@td.new_column_definition(name, type, options))
end
end
@@ -597,10 +604,11 @@ module ActiveRecord
#
# t.change_default(:qualification, 'new')
# t.change_default(:authorized, 1)
+ # t.change_default(:status, from: nil, to: "draft")
#
# See SchemaStatements#change_column_default
- def change_default(column_name, default)
- @base.change_column_default(name, column_name, default)
+ def change_default(column_name, default_or_changes)
+ @base.change_column_default(name, column_name, default_or_changes)
end
# Removes the column(s) from the table definition.
@@ -642,15 +650,12 @@ module ActiveRecord
@base.rename_column(name, column_name, new_column_name)
end
- # Adds a reference. Optionally adds a +type+ column, if
- # <tt>:polymorphic</tt> option is provided.
+ # Adds a reference.
#
# t.references(:user)
- # t.references(:user, type: "string")
- # t.belongs_to(:supplier, polymorphic: true)
# t.belongs_to(:supplier, foreign_key: true)
#
- # See SchemaStatements#add_reference
+ # See SchemaStatements#add_reference for details of the options you can use.
def references(*args)
options = args.extract_options!
args.each do |ref_name|
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb
index 999cb0ec5a..a2f58364a4 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb
@@ -18,7 +18,7 @@ module ActiveRecord
spec.merge!(prepare_column_options(column).delete_if { |key, _| [:name, :type].include?(key) })
end
- # This can be overridden on a Adapter level basis to support other
+ # This can be overridden on an Adapter level basis to support other
# extended datatypes (Example: Adding an array option in the
# PostgreSQLAdapter)
def prepare_column_options(column)
@@ -27,20 +27,31 @@ module ActiveRecord
spec[:type] = schema_type(column)
spec[:null] = 'false' unless column.null
- limit = column.limit || native_database_types[column.type][:limit]
- spec[:limit] = limit.inspect if limit
- spec[:precision] = column.precision.inspect if column.precision
- spec[:scale] = column.scale.inspect if column.scale
+ if limit = schema_limit(column)
+ spec[:limit] = limit
+ end
+
+ if precision = schema_precision(column)
+ spec[:precision] = precision
+ end
+
+ if scale = schema_scale(column)
+ spec[:scale] = scale
+ end
default = schema_default(column) if column.has_default?
spec[:default] = default unless default.nil?
+ if collation = schema_collation(column)
+ spec[:collation] = collation
+ end
+
spec
end
# Lists the valid migration options
def migration_keys
- [:name, :limit, :precision, :scale, :default, :null]
+ [:name, :limit, :precision, :scale, :default, :null, :collation]
end
private
@@ -49,6 +60,19 @@ module ActiveRecord
column.type.to_s
end
+ def schema_limit(column)
+ limit = column.limit || native_database_types[column.type][:limit]
+ limit.inspect if limit
+ end
+
+ def schema_precision(column)
+ column.precision.inspect if column.precision
+ end
+
+ def schema_scale(column)
+ column.scale.inspect if column.scale
+ end
+
def schema_default(column)
type = lookup_cast_type_from_column(column)
default = type.deserialize(column.default)
@@ -56,6 +80,10 @@ module ActiveRecord
type.type_cast_for_schema(default)
end
end
+
+ def schema_collation(column)
+ column.collation.inspect if column.collation
+ 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 ecb4868c13..7e53f8958a 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
@@ -14,11 +14,34 @@ module ActiveRecord
{}
end
+ def table_options(table_name)
+ nil
+ end
+
# Truncates a table alias according to the limits of the current adapter.
def table_alias_for(table_name)
table_name[0...table_alias_length].tr('.', '_')
end
+ # Returns the relation names useable to back Active Record models.
+ # For most adapters this means all #tables and #views.
+ def data_sources
+ tables | views
+ end
+
+ # Checks to see if the data source +name+ exists on the database.
+ #
+ # data_source_exists?(:ebooks)
+ #
+ def data_source_exists?(name)
+ data_sources.include?(name.to_s)
+ end
+
+ # Returns an array of table names defined in the database.
+ def tables(name = nil)
+ raise NotImplementedError, "#tables is not implemented"
+ end
+
# Checks to see if the table +table_name+ exists on the database.
#
# table_exists?(:developers)
@@ -27,6 +50,19 @@ module ActiveRecord
tables.include?(table_name.to_s)
end
+ # Returns an array of view names defined in the database.
+ def views
+ raise NotImplementedError, "#views is not implemented"
+ end
+
+ # Checks to see if the view +view_name+ exists on the database.
+ #
+ # view_exists?(:ebooks)
+ #
+ def view_exists?(view_name)
+ views.include?(view_name.to_s)
+ end
+
# Returns an array of indexes for the given table.
# def indexes(table_name, name = nil) end
@@ -84,6 +120,12 @@ module ActiveRecord
(!options.key?(:null) || c.null == options[:null]) }
end
+ # Returns just a table's primary key
+ def primary_key(table_name)
+ pks = primary_keys(table_name)
+ pks.first if pks.one?
+ end
+
# Creates a new table with the name +table_name+. +table_name+ may either
# be a String or a Symbol.
#
@@ -120,6 +162,8 @@ module ActiveRecord
# [<tt>:id</tt>]
# Whether to automatically add a primary key column. Defaults to true.
# Join tables for +has_and_belongs_to_many+ should set it to false.
+ #
+ # A Symbol can be used to specify the type of the generated primary key column.
# [<tt>:primary_key</tt>]
# The name of the primary key, if one is to be added automatically.
# Defaults to +id+. If <tt>:id</tt> is false this option is ignored.
@@ -147,7 +191,7 @@ module ActiveRecord
# generates:
#
# CREATE TABLE suppliers (
- # id int(11) DEFAULT NULL auto_increment PRIMARY KEY
+ # id int auto_increment PRIMARY KEY
# ) ENGINE=InnoDB DEFAULT CHARSET=utf8
#
# ====== Rename the primary key column
@@ -159,10 +203,23 @@ module ActiveRecord
# generates:
#
# CREATE TABLE objects (
- # guid int(11) DEFAULT NULL auto_increment PRIMARY KEY,
+ # guid int auto_increment PRIMARY KEY,
# name varchar(80)
# )
#
+ # ====== Change the primary key column type
+ #
+ # create_table(:tags, id: :string) do |t|
+ # t.column :label, :string
+ # end
+ #
+ # generates:
+ #
+ # CREATE TABLE tags (
+ # id varchar PRIMARY KEY,
+ # label varchar
+ # )
+ #
# ====== Do not add a primary key column
#
# create_table(:categories_suppliers, id: false) do |t|
@@ -196,7 +253,11 @@ module ActiveRecord
Base.get_primary_key table_name.to_s.singularize
end
- td.primary_key pk, options.fetch(:id, :primary_key), options
+ if pk.is_a?(Array)
+ td.primary_keys pk
+ else
+ td.primary_key pk, options.fetch(:id, :primary_key), options
+ end
end
yield td if block_given?
@@ -213,10 +274,6 @@ module ActiveRecord
end
end
- td.foreign_keys.each_pair do |other_table_name, foreign_key_options|
- add_foreign_key(table_name, other_table_name, foreign_key_options)
- end
-
result
end
@@ -296,7 +353,7 @@ module ActiveRecord
# [<tt>:bulk</tt>]
# Set this to true to make this a bulk alter query, such as
#
- # ALTER TABLE `users` ADD COLUMN age INT(11), ADD COLUMN birthdate DATETIME ...
+ # ALTER TABLE `users` ADD COLUMN age INT, ADD COLUMN birthdate DATETIME ...
#
# Defaults to false.
#
@@ -441,11 +498,16 @@ module ActiveRecord
#
# change_column_default(:users, :email, nil)
#
- def change_column_default(table_name, column_name, default)
+ # Passing a hash containing +:from+ and +:to+ will make this change
+ # reversible in migration:
+ #
+ # change_column_default(:posts, :state, from: nil, to: "draft")
+ #
+ def change_column_default(table_name, column_name, default_or_changes)
raise NotImplementedError, "change_column_default is not implemented"
end
- # Sets or removes a +NOT NULL+ constraint on a column. The +null+ flag
+ # Sets or removes a <tt>NOT NULL</tt> constraint on a column. The +null+ flag
# indicates whether the value can be +NULL+. For example
#
# change_column_null(:users, :nickname, false)
@@ -457,7 +519,7 @@ module ActiveRecord
# allows them to be +NULL+ (drops the constraint).
#
# The method accepts an optional fourth argument to replace existing
- # +NULL+s with some other value. Use that one when enabling the
+ # <tt>NULL</tt>s with some other value. Use that one when enabling the
# constraint if needed, since otherwise those rows would not be valid.
#
# Please note the fourth argument does not set a column's default.
@@ -511,6 +573,8 @@ module ActiveRecord
#
# CREATE INDEX by_name ON accounts(name(10))
#
+ # ====== Creating an index with specific key lengths for multiple keys
+ #
# add_index(:accounts, [:name, :surname], name: 'by_name_surname', length: {name: 10, surname: 15})
#
# generates:
@@ -568,7 +632,7 @@ module ActiveRecord
#
# Removes the +index_accounts_on_column+ in the +accounts+ table.
#
- # remove_index :accounts, :column
+ # remove_index :accounts, :branch_id
#
# Removes the index named +index_accounts_on_branch_id+ in the +accounts+ table.
#
@@ -583,10 +647,7 @@ module ActiveRecord
# remove_index :accounts, name: :by_branch_party
#
def remove_index(table_name, options = {})
- remove_index!(table_name, index_name_for_remove(table_name, options))
- end
-
- def remove_index!(table_name, index_name) #:nodoc:
+ index_name = index_name_for_remove(table_name, options)
execute "DROP INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)}"
end
@@ -630,11 +691,21 @@ module ActiveRecord
indexes(table_name).detect { |i| i.name == index_name }
end
- # Adds a reference. Optionally adds a +type+ column, if <tt>:polymorphic</tt> option is provided.
- # The reference column is an +integer+ by default, the <tt>:type</tt> option can be used to specify
- # a different type.
+ # Adds a reference. The reference column is an integer by default,
+ # the <tt>:type</tt> option can be used to specify a different type.
+ # Optionally adds a +_type+ column, if <tt>:polymorphic</tt> option is provided.
# <tt>add_reference</tt> and <tt>add_belongs_to</tt> are acceptable.
#
+ # The +options+ hash can include the following keys:
+ # [<tt>:type</tt>]
+ # The reference column type. Defaults to +:integer+.
+ # [<tt>:index</tt>]
+ # Add an appropriate index. Defaults to false.
+ # [<tt>:foreign_key</tt>]
+ # Add an appropriate foreign key. Defaults to false.
+ # [<tt>:polymorphic</tt>]
+ # Whether an additional +_type+ column should be added. Defaults to false.
+ #
# ====== Create a user_id integer column
#
# add_reference(:products, :user)
@@ -643,10 +714,6 @@ module ActiveRecord
#
# add_reference(:products, :user, type: :string)
#
- # ====== Create a supplier_id and supplier_type columns
- #
- # add_belongs_to(:products, :supplier, polymorphic: true)
- #
# ====== Create supplier_id, supplier_type columns and appropriate index
#
# add_reference(:products, :supplier, polymorphic: true, index: true)
@@ -731,28 +798,23 @@ module ActiveRecord
# [<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+
+ # 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+
+ # 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]
- }
+ options = foreign_key_options(from_table, to_table, options)
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 given foreign key from the table. Any option parameters provided
+ # will be used to re-add the foreign key in case of a migration rollback.
+ # It is recommended that you provide any options used when creating the foreign
+ # key so that the migration can be reverted properly.
#
# Removes the foreign key on +accounts.branch_id+.
#
@@ -766,6 +828,7 @@ module ActiveRecord
#
# remove_foreign_key :accounts, name: :special_fk_name
#
+ # The +options+ hash accepts the same keys as SchemaStatements#add_foreign_key.
def remove_foreign_key(from_table, options_or_to_table = {})
return unless supports_foreign_keys?
@@ -803,7 +866,17 @@ module ActiveRecord
end
def foreign_key_column_for(table_name) # :nodoc:
- "#{table_name.to_s.singularize}_id"
+ prefix = Base.table_name_prefix
+ suffix = Base.table_name_suffix
+ name = table_name.to_s =~ /#{prefix}(.+)#{suffix}/ ? $1 : table_name.to_s
+ "#{name.singularize}_id"
+ end
+
+ def foreign_key_options(from_table, to_table, options) # :nodoc:
+ options = options.dup
+ options[:column] ||= foreign_key_column_for(to_table)
+ options[:name] ||= foreign_key_name(from_table, options)
+ options
end
def dump_schema_information #:nodoc:
@@ -820,7 +893,7 @@ module ActiveRecord
ActiveRecord::SchemaMigration.create_table
end
- def assume_migrated_upto_version(version, migrations_paths = ActiveRecord::Migrator.migrations_paths)
+ def assume_migrated_upto_version(version, migrations_paths)
migrations_paths = Array(migrations_paths)
version = version.to_i
sm_table = quote_table_name(ActiveRecord::Migrator.schema_migrations_table_name)
@@ -915,13 +988,13 @@ module ActiveRecord
def add_index_options(table_name, column_name, options = {}) #:nodoc:
column_names = Array(column_name)
- index_name = index_name(table_name, column: column_names)
options.assert_valid_keys(:unique, :order, :name, :where, :length, :internal, :using, :algorithm, :type)
- index_type = options[:unique] ? "UNIQUE" : ""
index_type = options[:type].to_s if options.key?(:type)
+ index_type ||= options[:unique] ? "UNIQUE" : ""
index_name = options[:name].to_s if options.key?(:name)
+ index_name ||= index_name(table_name, column: column_names)
max_index_length = options.fetch(:internal, false) ? index_name_length : allowed_index_name_length
if options.key?(:algorithm)
@@ -947,6 +1020,10 @@ module ActiveRecord
[index_name, index_type, index_columns, index_options, algorithm, using]
end
+ def options_include_default?(options)
+ options.include?(:default) && !(options[:null] == false && options[:default].nil?)
+ end
+
protected
def add_index_sort_order(option_strings, column_names, options = {})
if options.is_a?(Hash) && order = options[:order]
@@ -973,10 +1050,6 @@ module ActiveRecord
column_names.map {|name| quote_column_name(name) + option_strings[name]}
end
- def options_include_default?(options)
- options.include?(:default) && !(options[:null] == false && options[:default].nil?)
- end
-
def index_name_for_remove(table_name, options = {})
index_name = index_name(table_name, options)
@@ -1034,11 +1107,19 @@ module ActiveRecord
end
end
- def validate_index_length!(table_name, new_name)
+ def validate_index_length!(table_name, new_name) # :nodoc:
if new_name.length > allowed_index_name_length
raise ArgumentError, "Index name '#{new_name}' on table '#{table_name}' is too long; the limit is #{allowed_index_name_length} characters"
end
end
+
+ def extract_new_default_value(default_or_changes)
+ if default_or_changes.is_a?(Hash) && default_or_changes.has_key?(:from) && default_or_changes.has_key?(:to)
+ default_or_changes[:to]
+ else
+ default_or_changes
+ 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 ae42e8ef8d..ed14c781c6 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
@@ -1,13 +1,9 @@
-require 'date'
-require 'bigdecimal'
-require 'bigdecimal/util'
require 'active_record/type'
require 'active_support/core_ext/benchmark'
require 'active_record/connection_adapters/schema_cache'
require 'active_record/connection_adapters/sql_type_metadata'
require 'active_record/connection_adapters/abstract/schema_dumper'
require 'active_record/connection_adapters/abstract/schema_creation'
-require 'monitor'
require 'arel/collectors/bind'
require 'arel/collectors/sql_string'
@@ -70,7 +66,6 @@ module ActiveRecord
include DatabaseLimits
include QueryCache
include ActiveSupport::Callbacks
- include MonitorMixin
include ColumnDumper
SIMPLE_INT = /\A\d+\z/
@@ -112,6 +107,18 @@ module ActiveRecord
@prepared_statements = false
end
+ class Version
+ include Comparable
+
+ def initialize(version_string)
+ @version = version_string.split('.').map(&:to_i)
+ end
+
+ def <=>(version_string)
+ @version <=> version_string.split('.').map(&:to_i)
+ end
+ end
+
class BindCollector < Arel::Collectors::Bind
def compile(bvs, conn)
casted_binds = conn.prepare_binds_for_database(bvs)
@@ -141,12 +148,20 @@ module ActiveRecord
SchemaCreation.new self
end
+ # this method must only be called while holding connection pool's mutex
def lease
- synchronize do
- unless in_use?
- @owner = Thread.current
+ if in_use?
+ msg = 'Cannot lease connection, '
+ if @owner == Thread.current
+ msg << 'it is already leased by the current thread.'
+ else
+ msg << "it is already in use by a different thread: #{@owner}. " <<
+ "Current thread: #{Thread.current}."
end
+ raise ActiveRecordError, msg
end
+
+ @owner = Thread.current
end
def schema_cache=(cache)
@@ -154,6 +169,7 @@ module ActiveRecord
@schema_cache = cache
end
+ # this method must only be called while holding connection pool's mutex
def expire
@owner = nil
end
@@ -250,6 +266,11 @@ module ActiveRecord
false
end
+ # Does this adapter support json data type?
+ def supports_json?
+ false
+ end
+
# This is meant to be implemented by the adapters that support extensions
def disable_extension(name)
end
@@ -387,8 +408,8 @@ module ActiveRecord
end
end
- def new_column(name, default, sql_type_metadata = nil, null = true)
- Column.new(name, default, sql_type_metadata, null)
+ def new_column(name, default, sql_type_metadata = nil, null = true, default_function = nil, collation = nil)
+ Column.new(name, default, sql_type_metadata, null, default_function, collation)
end
def lookup_cast_type(sql_type) # :nodoc:
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 76aee452ca..cd8097d1b3 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
@@ -1,4 +1,3 @@
-require 'arel/visitors/bind_visitor'
require 'active_support/core_ext/string/strip'
module ActiveRecord
@@ -11,10 +10,30 @@ module ActiveRecord
options[:auto_increment] = true if type == :bigint
super
end
+
+ def json(*args, **options)
+ args.each { |name| column(name, :json, options) }
+ end
+
+ def unsigned_integer(*args, **options)
+ args.each { |name| column(name, :unsigned_integer, options) }
+ end
+
+ def unsigned_bigint(*args, **options)
+ args.each { |name| column(name, :unsigned_bigint, options) }
+ end
+
+ def unsigned_float(*args, **options)
+ args.each { |name| column(name, :unsigned_float, options) }
+ end
+
+ def unsigned_decimal(*args, **options)
+ args.each { |name| column(name, :unsigned_decimal, options) }
+ end
end
class ColumnDefinition < ActiveRecord::ConnectionAdapters::ColumnDefinition
- attr_accessor :charset, :collation
+ attr_accessor :charset, :unsigned
end
class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition
@@ -26,9 +45,12 @@ module ActiveRecord
when :primary_key
column.type = :integer
column.auto_increment = true
+ when /\Aunsigned_(?<type>.+)\z/
+ column.type = $~[:type].to_sym
+ column.unsigned = true
end
+ column.unsigned ||= options[:unsigned]
column.charset = options[:charset]
- column.collation = options[:collation]
column
end
@@ -44,27 +66,19 @@ module ActiveRecord
end
class SchemaCreation < AbstractAdapter::SchemaCreation
- def visit_AddColumn(o)
- add_column_position!(super, column_options(o))
- end
-
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)} "
-
- statements = o.columns.map { |c| accept c }
- statements.concat(o.indexes.map { |column_name, options| index_in_create(name, column_name, options) })
+ def visit_ColumnDefinition(o)
+ o.sql_type = type_to_sql(o.type, o.limit, o.precision, o.scale, o.unsigned)
+ super
+ end
- create_sql << "(#{statements.join(', ')}) " if statements.present?
- create_sql << "#{o.options}"
- create_sql << " AS #{@conn.to_sql(o.as)}" if o.as
- create_sql
+ def visit_AddColumnDefinition(o)
+ add_column_position!(super, column_options(o.column))
end
def visit_ChangeColumnDefinition(o)
@@ -75,7 +89,6 @@ module ActiveRecord
def column_options(o)
column_options = super
column_options[:charset] = o.charset
- column_options[:collation] = o.collation
column_options
end
@@ -99,8 +112,8 @@ module ActiveRecord
end
def index_in_create(table_name, column_name, options)
- index_name, index_type, index_columns, index_options, index_algorithm, index_using = @conn.add_index_options(table_name, column_name, options)
- "#{index_type} INDEX #{quote_column_name(index_name)} #{index_using} (#{index_columns})#{index_options} #{index_algorithm}"
+ index_name, index_type, index_columns, _, _, index_using = @conn.add_index_options(table_name, column_name, options)
+ "#{index_type} INDEX #{quote_column_name(index_name)} #{index_using} (#{index_columns}) "
end
end
@@ -116,6 +129,7 @@ module ActiveRecord
spec = {}
if column.auto_increment?
spec[:id] = ':bigint' if column.bigint?
+ spec[:unsigned] = 'true' if column.unsigned?
return if spec.empty?
else
spec[:id] = column.type.inspect
@@ -126,22 +140,36 @@ module ActiveRecord
def prepare_column_options(column)
spec = super
- spec.delete(:precision) if /time/ === column.sql_type && column.precision == 0
- spec.delete(:limit) if :boolean === column.type
+ spec[:unsigned] = 'true' if column.unsigned?
+ spec
+ end
+
+ def migration_keys
+ super + [:unsigned]
+ end
+
+ private
+
+ def schema_limit(column)
+ super unless column.type == :boolean
+ end
+
+ def schema_precision(column)
+ super unless /time/ === column.sql_type && column.precision == 0
+ end
+
+ def schema_collation(column)
if column.collation && table_name = column.instance_variable_get(:@table_name)
@collation_cache ||= {}
@collation_cache[table_name] ||= select_one("SHOW TABLE STATUS LIKE '#{table_name}'")["Collation"]
- spec[:collation] = column.collation.inspect if column.collation != @collation_cache[table_name]
+ column.collation.inspect if column.collation != @collation_cache[table_name]
end
- spec
end
- def migration_keys
- super + [:collation]
- end
+ public
class Column < ConnectionAdapters::Column # :nodoc:
- delegate :strict, :collation, :extra, to: :sql_type_metadata, allow_nil: true
+ delegate :strict, :extra, to: :sql_type_metadata, allow_nil: true
def initialize(*)
super
@@ -166,6 +194,10 @@ module ActiveRecord
sql_type =~ /blob/i || type == :text
end
+ def unsigned?
+ /unsigned/ === sql_type
+ end
+
def case_sensitive?
collation && !collation.match(/_ci$/)
end
@@ -195,12 +227,11 @@ module ActiveRecord
end
class MysqlTypeMetadata < DelegateClass(SqlTypeMetadata) # :nodoc:
- attr_reader :collation, :extra, :strict
+ attr_reader :extra, :strict
- def initialize(type_metadata, collation: "", extra: "", strict: false)
+ def initialize(type_metadata, extra: "", strict: false)
super(type_metadata)
@type_metadata = type_metadata
- @collation = collation
@extra = extra
@strict = strict
end
@@ -218,7 +249,7 @@ module ActiveRecord
protected
def attributes_for_hash
- [self.class, @type_metadata, collation, extra, strict]
+ [self.class, @type_metadata, extra, strict]
end
end
@@ -242,17 +273,19 @@ module ActiveRecord
QUOTED_TRUE, QUOTED_FALSE = '1', '0'
NATIVE_DATABASE_TYPES = {
- :primary_key => "int(11) auto_increment PRIMARY KEY",
- :string => { :name => "varchar", :limit => 255 },
- :text => { :name => "text" },
- :integer => { :name => "int", :limit => 4 },
- :float => { :name => "float" },
- :decimal => { :name => "decimal" },
- :datetime => { :name => "datetime" },
- :time => { :name => "time" },
- :date => { :name => "date" },
- :binary => { :name => "blob" },
- :boolean => { :name => "tinyint", :limit => 1 }
+ primary_key: "int auto_increment PRIMARY KEY",
+ string: { name: "varchar", limit: 255 },
+ text: { name: "text" },
+ integer: { name: "int", limit: 4 },
+ float: { name: "float" },
+ decimal: { name: "decimal" },
+ datetime: { name: "datetime" },
+ time: { name: "time" },
+ date: { name: "date" },
+ binary: { name: "blob" },
+ boolean: { name: "tinyint", limit: 1 },
+ bigint: { name: "bigint" },
+ json: { name: "json" },
}
INDEX_TYPES = [:fulltext, :spatial]
@@ -307,7 +340,11 @@ module ActiveRecord
#
# http://bugs.mysql.com/bug.php?id=39170
def supports_transaction_isolation?
- version[0] >= 5
+ version >= '5.0.0'
+ end
+
+ def supports_explain?
+ true
end
def supports_indexes_in_create?
@@ -319,11 +356,11 @@ module ActiveRecord
end
def supports_views?
- version[0] >= 5
+ version >= '5.0.0'
end
def supports_datetime_with_precision?
- (version[0] == 5 && version[1] >= 6) || version[0] >= 6
+ version >= '5.6.4'
end
def native_database_types
@@ -342,8 +379,8 @@ module ActiveRecord
raise NotImplementedError
end
- def new_column(field, default, sql_type_metadata = nil, null = true) # :nodoc:
- Column.new(field, default, sql_type_metadata, null)
+ def new_column(field, default, sql_type_metadata = nil, null = true, default_function = nil, collation = nil) # :nodoc:
+ Column.new(field, default, sql_type_metadata, null, default_function, collation)
end
# Must return the MySQL error number from the exception, if the exception has an
@@ -386,6 +423,14 @@ module ActiveRecord
0
end
+ def quoted_date(value)
+ if supports_datetime_with_precision?
+ super
+ else
+ super.sub(/\.\d{6}\z/, '')
+ end
+ end
+
# REFERENTIAL INTEGRITY ====================================
def disable_referential_integrity #:nodoc:
@@ -403,6 +448,80 @@ module ActiveRecord
# DATABASE STATEMENTS ======================================
#++
+ def explain(arel, binds = [])
+ sql = "EXPLAIN #{to_sql(arel, binds)}"
+ start = Time.now
+ result = exec_query(sql, 'EXPLAIN', binds)
+ elapsed = Time.now - start
+
+ ExplainPrettyPrinter.new.pp(result, elapsed)
+ end
+
+ class ExplainPrettyPrinter # :nodoc:
+ # Pretty prints the result of an EXPLAIN in a way that resembles the output of the
+ # MySQL shell:
+ #
+ # +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+
+ # | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+ # +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+
+ # | 1 | SIMPLE | users | const | PRIMARY | PRIMARY | 4 | const | 1 | |
+ # | 1 | SIMPLE | posts | ALL | NULL | NULL | NULL | NULL | 1 | Using where |
+ # +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+
+ # 2 rows in set (0.00 sec)
+ #
+ # This is an exercise in Ruby hyperrealism :).
+ def pp(result, elapsed)
+ widths = compute_column_widths(result)
+ separator = build_separator(widths)
+
+ pp = []
+
+ pp << separator
+ pp << build_cells(result.columns, widths)
+ pp << separator
+
+ result.rows.each do |row|
+ pp << build_cells(row, widths)
+ end
+
+ pp << separator
+ pp << build_footer(result.rows.length, elapsed)
+
+ pp.join("\n") + "\n"
+ end
+
+ private
+
+ def compute_column_widths(result)
+ [].tap do |widths|
+ result.columns.each_with_index do |column, i|
+ cells_in_column = [column] + result.rows.map {|r| r[i].nil? ? 'NULL' : r[i].to_s}
+ widths << cells_in_column.map(&:length).max
+ end
+ end
+ end
+
+ def build_separator(widths)
+ padding = 1
+ '+' + widths.map {|w| '-' * (w + (padding*2))}.join('+') + '+'
+ end
+
+ def build_cells(items, widths)
+ cells = []
+ items.each_with_index do |item, i|
+ item = 'NULL' if item.nil?
+ justifier = item.is_a?(Numeric) ? 'rjust' : 'ljust'
+ cells << item.to_s.send(justifier, widths[i])
+ end
+ '| ' + cells.join(' | ') + ' |'
+ end
+
+ def build_footer(nrows, elapsed)
+ rows_label = nrows == 1 ? 'row' : 'rows'
+ "#{nrows} #{rows_label} in set (%.2f sec)" % elapsed
+ end
+ end
+
def clear_cache!
super
reload_type_map
@@ -506,33 +625,42 @@ module ActiveRecord
show_variable 'collation_database'
end
- def tables(name = nil, database = nil, like = nil) #:nodoc:
- sql = "SHOW TABLES "
- sql << "IN #{quote_table_name(database)} " if database
- sql << "LIKE #{quote(like)}" if like
-
- execute_and_free(sql, 'SCHEMA') do |result|
- result.collect(&:first)
- end
+ def tables(name = nil) # :nodoc:
+ select_values("SHOW FULL TABLES", 'SCHEMA')
end
+ alias data_sources tables
def truncate(table_name, name = nil)
execute "TRUNCATE TABLE #{quote_table_name(table_name)}", name
end
- def table_exists?(name)
- return false unless name.present?
- return true if tables(nil, nil, name).any?
+ def table_exists?(table_name)
+ return false unless table_name.present?
- name = name.to_s
- schema, table = name.split('.', 2)
+ schema, name = table_name.to_s.split('.', 2)
+ schema, name = @config[:database], schema unless name # A table was provided without a schema
- unless table # A table was provided without a schema
- table = schema
- schema = nil
- end
+ sql = "SELECT table_name FROM information_schema.tables "
+ sql << "WHERE table_schema = #{quote(schema)} AND table_name = #{quote(name)}"
+
+ select_values(sql, 'SCHEMA').any?
+ end
+ alias data_source_exists? table_exists?
+
+ def views # :nodoc:
+ select_values("SHOW FULL TABLES WHERE table_type = 'VIEW'", 'SCHEMA')
+ end
+
+ def view_exists?(view_name) # :nodoc:
+ return false unless view_name.present?
+
+ schema, name = view_name.to_s.split('.', 2)
+ schema, name = @config[:database], schema unless name # A view was provided without a schema
+
+ sql = "SELECT table_name FROM information_schema.tables WHERE table_type = 'VIEW'"
+ sql << " AND table_schema = #{quote(schema)} AND table_name = #{quote(name)}"
- tables(nil, schema, table).any?
+ select_values(sql, 'SCHEMA').any?
end
# Returns an array of indexes for the given table.
@@ -566,8 +694,8 @@ module ActiveRecord
each_hash(result).map do |field|
field_name = set_field_encoding(field[:Field])
sql_type = field[:Type]
- type_metadata = fetch_type_metadata(sql_type, field[:Collation], field[:Extra])
- new_column(field_name, field[:Default], type_metadata, field[:Null] == "YES")
+ type_metadata = fetch_type_metadata(sql_type, field[:Extra])
+ new_column(field_name, field[:Default], type_metadata, field[:Null] == "YES", nil, field[:Collation])
end
end
end
@@ -629,12 +757,13 @@ module ActiveRecord
end
end
- def change_column_default(table_name, column_name, default) #:nodoc:
+ def change_column_default(table_name, column_name, default_or_changes) #:nodoc:
+ default = extract_new_default_value(default_or_changes)
column = column_for(table_name, column_name)
change_column table_name, column_name, column.sql_type, :default => default
end
- def change_column_null(table_name, column_name, null, default = nil)
+ def change_column_null(table_name, column_name, null, default = nil) #:nodoc:
column = column_for(table_name, column_name)
unless null || default.nil?
@@ -654,8 +783,8 @@ module ActiveRecord
end
def add_index(table_name, column_name, options = {}) #:nodoc:
- index_name, index_type, index_columns, index_options, index_algorithm, index_using = add_index_options(table_name, column_name, options)
- execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} #{index_using} ON #{quote_table_name(table_name)} (#{index_columns})#{index_options} #{index_algorithm}"
+ index_name, index_type, index_columns, _, index_algorithm, index_using = add_index_options(table_name, column_name, options)
+ execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} #{index_using} ON #{quote_table_name(table_name)} (#{index_columns}) #{index_algorithm}"
end
def foreign_keys(table_name)
@@ -670,7 +799,7 @@ module ActiveRecord
AND fk.table_name = '#{table_name}'
SQL
- create_table_info = select_one("SHOW CREATE TABLE #{quote_table_name(table_name)}")["Create Table"]
+ create_table_info = create_table_info(table_name)
fk_info.map do |row|
options = {
@@ -686,61 +815,62 @@ module ActiveRecord
end
end
+ def table_options(table_name)
+ create_table_info = create_table_info(table_name)
+
+ # strip create_definitions and partition_options
+ raw_table_options = create_table_info.sub(/\A.*\n\) /m, '').sub(/\n\/\*!.*\*\/\n\z/m, '').strip
+
+ # strip AUTO_INCREMENT
+ raw_table_options.sub(/(ENGINE=\w+)(?: AUTO_INCREMENT=\d+)/, '\1')
+ 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
+ def type_to_sql(type, limit = nil, precision = nil, scale = nil, unsigned = nil)
+ sql = case type.to_s
when 'binary'
- case limit
- when 0..0xfff; "varbinary(#{limit})"
- when nil; "blob"
- when 0x1000..0xffffffff; "blob(#{limit})"
- else raise(ActiveRecordError, "No binary type has character length #{limit}")
- end
+ binary_to_sql(limit)
when 'integer'
- case limit
- when 1; 'tinyint'
- when 2; 'smallint'
- when 3; 'mediumint'
- when nil, 4, 11; 'int(11)' # compatibility with MySQL default
- when 5..8; 'bigint'
- else raise(ActiveRecordError, "No integer type has byte size #{limit}")
- end
+ integer_to_sql(limit)
when 'text'
- case limit
- when 0..0xff; 'tinytext'
- when nil, 0x100..0xffff; 'text'
- when 0x10000..0xffffff; 'mediumtext'
- when 0x1000000..0xffffffff; 'longtext'
- else raise(ActiveRecordError, "No text type has character length #{limit}")
- end
+ text_to_sql(limit)
else
- super
+ super(type, limit, precision, scale)
end
+
+ sql << ' unsigned' if unsigned && type != :primary_key
+ sql
end
# SHOW VARIABLES LIKE 'name'
def show_variable(name)
- variables = select_all("SHOW VARIABLES LIKE '#{name}'", 'SCHEMA')
+ variables = select_all("select @@#{name} as 'Value'", 'SCHEMA')
variables.first['Value'] unless variables.empty?
+ rescue ActiveRecord::StatementInvalid
+ nil
end
# Returns a table's primary key and belonging sequence.
def pk_and_sequence_for(table)
- execute_and_free("SHOW CREATE TABLE #{quote_table_name(table)}", 'SCHEMA') do |result|
- create_table = each_hash(result).first[:"Create Table"]
- if create_table.to_s =~ /PRIMARY KEY\s+(?:USING\s+\w+\s+)?\((.+)\)/
- keys = $1.split(",").map { |key| key.delete('`"') }
- keys.length == 1 ? [keys.first, nil] : nil
- else
- nil
- end
+ if pk = primary_key(table)
+ [ pk, nil ]
end
end
- # Returns just a table's primary key
- def primary_key(table)
- pk_and_sequence = pk_and_sequence_for(table)
- pk_and_sequence && pk_and_sequence.first
+ def primary_keys(table_name) # :nodoc:
+ raise ArgumentError unless table_name.present?
+
+ schema, name = table_name.to_s.split('.', 2)
+ schema, name = @config[:database], schema unless name # A table was provided without a schema
+
+ select_values(<<-SQL.strip_heredoc, 'SCHEMA')
+ SELECT column_name
+ FROM information_schema.key_column_usage
+ WHERE constraint_name = 'PRIMARY'
+ AND table_schema = #{quote(schema)}
+ AND table_name = #{quote(name)}
+ ORDER BY ordinal_position
+ SQL
end
def case_sensitive_modifier(node, table_attribute)
@@ -789,6 +919,7 @@ module ActiveRecord
m.register_type %r(longblob)i, Type::Binary.new(limit: 2**32 - 1)
m.register_type %r(^float)i, Type::Float.new(limit: 24)
m.register_type %r(^double)i, Type::Float.new(limit: 53)
+ m.register_type %r(^json)i, MysqlJson.new
register_integer_type m, %r(^bigint)i, limit: 8
register_integer_type m, %r(^int)i, limit: 4
@@ -797,7 +928,6 @@ module ActiveRecord
register_integer_type m, %r(^tinyint)i, limit: 1
m.alias_type %r(tinyint\(1\))i, 'boolean' if emulate_booleans
- m.alias_type %r(set)i, 'varchar'
m.alias_type %r(year)i, 'integer'
m.alias_type %r(bit)i, 'binary'
@@ -806,6 +936,12 @@ module ActiveRecord
.split(',').map{|enum| enum.strip.length - 2}.max
MysqlString.new(limit: limit)
end
+
+ m.register_type(%r(^set)i) do |sql_type|
+ limit = sql_type[/^set\((.+)\)/i, 1]
+ .split(',').map{|set| set.strip.length - 1}.sum - 1
+ MysqlString.new(limit: limit)
+ end
end
def register_integer_type(mapping, key, options) # :nodoc:
@@ -826,21 +962,8 @@ module ActiveRecord
end
end
- def fetch_type_metadata(sql_type, collation = "", extra = "")
- MysqlTypeMetadata.new(super(sql_type), collation: collation, extra: extra, strict: strict_mode?)
- end
-
- # MySQL is too stupid to create a temporary table for use subquery, so we have
- # to give it some prompting in the form of a subsubquery. Ugh!
- def subquery_for(key, select)
- subsubselect = select.clone
- subsubselect.projections = [key]
-
- subselect = Arel::SelectManager.new(select.engine)
- subselect.project Arel.sql(key.name)
- # Materialized subquery by adding distinct
- # to work with MySQL 5.7.6 which sets optimizer_switch='derived_merge=on'
- subselect.from subsubselect.distinct.as('__active_record_temp')
+ def fetch_type_metadata(sql_type, extra = "")
+ MysqlTypeMetadata.new(super(sql_type), extra: extra, strict: strict_mode?)
end
def add_index_length(option_strings, column_names, options = {})
@@ -882,7 +1005,7 @@ module ActiveRecord
def add_column_sql(table_name, column_name, type, options = {})
td = create_table_definition(table_name)
cd = td.new_column_definition(column_name, type, options)
- schema_creation.visit_AddColumn cd
+ schema_creation.accept(AddColumnDefinition.new(cd))
end
def change_column_sql(table_name, column_name, type, options = {})
@@ -924,8 +1047,9 @@ module ActiveRecord
end
def add_index_sql(table_name, column_name, options = {})
- index_name, index_type, index_columns = add_index_options(table_name, column_name, options)
- "ADD #{index_type} INDEX #{index_name} (#{index_columns})"
+ index_name, index_type, index_columns, _, index_algorithm, index_using = add_index_options(table_name, column_name, options)
+ index_algorithm[0, 0] = ", " if index_algorithm.present?
+ "ADD #{index_type} INDEX #{quote_column_name(index_name)} #{index_using} (#{index_columns})#{index_algorithm}"
end
def remove_index_sql(table_name, options = {})
@@ -943,8 +1067,21 @@ module ActiveRecord
private
+ # MySQL is too stupid to create a temporary table for use subquery, so we have
+ # to give it some prompting in the form of a subsubquery. Ugh!
+ def subquery_for(key, select)
+ subsubselect = select.clone
+ subsubselect.projections = [key]
+
+ subselect = Arel::SelectManager.new(select.engine)
+ subselect.project Arel.sql(key.name)
+ # Materialized subquery by adding distinct
+ # to work with MySQL 5.7.6 which sets optimizer_switch='derived_merge=on'
+ subselect.from subsubselect.distinct.as('__active_record_temp')
+ end
+
def version
- @version ||= full_version.scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map(&:to_i)
+ @version ||= Version.new(full_version.match(/^\d+\.\d+\.\d+/)[0])
end
def mariadb?
@@ -952,7 +1089,7 @@ module ActiveRecord
end
def supports_rename_index?
- mariadb? ? false : (version[0] == 5 && version[1] >= 7) || version[0] >= 6
+ mariadb? ? false : version >= '5.7.6'
end
def configure_connection
@@ -966,15 +1103,17 @@ module ActiveRecord
wait_timeout = 2147483 unless wait_timeout.is_a?(Fixnum)
variables['wait_timeout'] = self.class.type_cast_config_to_integer(wait_timeout)
+ defaults = [':default', :default].to_set
+
# Make MySQL reject illegal values rather than truncating or blanking them, see
- # http://dev.mysql.com/doc/refman/5.6/en/sql-mode.html#sqlmode_strict_all_tables
+ # http://dev.mysql.com/doc/refman/5.7/en/sql-mode.html#sqlmode_strict_all_tables
# If the user has provided another value for sql_mode, don't replace it.
- unless variables.has_key?('sql_mode')
+ unless variables.has_key?('sql_mode') || defaults.include?(@config[:strict])
variables['sql_mode'] = strict_mode? ? 'STRICT_ALL_TABLES' : ''
end
# NAMES does not have an equals sign, see
- # http://dev.mysql.com/doc/refman/5.6/en/set-statement.html#id944430
+ # http://dev.mysql.com/doc/refman/5.7/en/set-statement.html#id944430
# (trailing comma because variable_assignments will always have content)
if @config[:encoding]
encoding = "NAMES #{@config[:encoding]}"
@@ -984,7 +1123,7 @@ module ActiveRecord
# Gather up all of the SET variables...
variable_assignments = variables.map do |k, v|
- if v == ':default' || v == :default
+ if defaults.include?(v)
"@@SESSION.#{k} = DEFAULT" # Sets the value to the global or compile default
elsif !v.nil?
"@@SESSION.#{k} = #{quote(v)}"
@@ -1005,10 +1144,54 @@ module ActiveRecord
end
end
+ def create_table_info(table_name) # :nodoc:
+ @create_table_info_cache = {}
+ @create_table_info_cache[table_name] ||= select_one("SHOW CREATE TABLE #{quote_table_name(table_name)}")["Create Table"]
+ end
+
def create_table_definition(name, temporary = false, options = nil, as = nil) # :nodoc:
TableDefinition.new(native_database_types, name, temporary, options, as)
end
+ def binary_to_sql(limit) # :nodoc:
+ case limit
+ when 0..0xfff; "varbinary(#{limit})"
+ when nil; "blob"
+ when 0x1000..0xffffffff; "blob(#{limit})"
+ else raise(ActiveRecordError, "No binary type has byte length #{limit}")
+ end
+ end
+
+ def integer_to_sql(limit) # :nodoc:
+ case limit
+ when 1; 'tinyint'
+ when 2; 'smallint'
+ when 3; 'mediumint'
+ when nil, 4; 'int'
+ when 5..8; 'bigint'
+ when 11; 'int(11)' # backward compatibility with Rails 2.0
+ else raise(ActiveRecordError, "No integer type has byte size #{limit}")
+ end
+ end
+
+ def text_to_sql(limit) # :nodoc:
+ case limit
+ when 0..0xff; 'tinytext'
+ when nil, 0x100..0xffff; 'text'
+ when 0x10000..0xffffff; 'mediumtext'
+ when 0x1000000..0xffffffff; 'longtext'
+ else raise(ActiveRecordError, "No text type has byte length #{limit}")
+ end
+ end
+
+ class MysqlJson < Type::Internal::AbstractJson # :nodoc:
+ def changed_in_place?(raw_old_value, new_value)
+ # Normalization is required because MySQL JSON data format includes
+ # the space between the elements.
+ super(serialize(deserialize(raw_old_value)), new_value)
+ end
+ end
+
class MysqlString < Type::String # :nodoc:
def serialize(value)
case value
@@ -1029,8 +1212,12 @@ module ActiveRecord
end
end
+ ActiveRecord::Type.register(:json, MysqlJson, adapter: :mysql)
+ ActiveRecord::Type.register(:json, MysqlJson, adapter: :mysql2)
ActiveRecord::Type.register(:string, MysqlString, adapter: :mysql)
ActiveRecord::Type.register(:string, MysqlString, adapter: :mysql2)
+ ActiveRecord::Type.register(:unsigned_integer, Type::UnsignedInteger, adapter: :mysql)
+ ActiveRecord::Type.register(:unsigned_integer, Type::UnsignedInteger, adapter: :mysql2)
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb
index f4dda5154e..5e31efec4a 100644
--- a/activerecord/lib/active_record/connection_adapters/column.rb
+++ b/activerecord/lib/active_record/connection_adapters/column.rb
@@ -5,29 +5,23 @@ module ActiveRecord
module ConnectionAdapters
# An abstract definition of a column in a table.
class Column
- FALSE_VALUES = [false, 0, '0', 'f', 'F', 'false', 'FALSE', 'off', 'OFF'].to_set
-
- module Format
- ISO_DATE = /\A(\d{4})-(\d\d)-(\d\d)\z/
- ISO_DATETIME = /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)(\.\d+)?\z/
- end
-
- attr_reader :name, :null, :sql_type_metadata, :default, :default_function
+ attr_reader :name, :null, :sql_type_metadata, :default, :default_function, :collation
delegate :precision, :scale, :limit, :type, :sql_type, to: :sql_type_metadata, allow_nil: true
# Instantiates a new column in the table.
#
- # +name+ is the column's name, such as <tt>supplier_id</tt> in <tt>supplier_id int(11)</tt>.
+ # +name+ is the column's name, such as <tt>supplier_id</tt> in <tt>supplier_id int</tt>.
# +default+ is the type-casted default value, such as +new+ in <tt>sales_stage varchar(20) default 'new'</tt>.
# +sql_type_metadata+ is various information about the type of the column
# +null+ determines if this column allows +NULL+ values.
- def initialize(name, default, sql_type_metadata = nil, null = true, default_function = nil)
+ def initialize(name, default, sql_type_metadata = nil, null = true, default_function = nil, collation = nil)
@name = name
@sql_type_metadata = sql_type_metadata
@null = null
@default = default
@default_function = default_function
+ @collation = collation
@table_name = nil
end
@@ -60,7 +54,7 @@ module ActiveRecord
protected
def attributes_for_hash
- [self.class, name, default, sql_type_metadata, null, default_function]
+ [self.class, name, default, sql_type_metadata, null, default_function, collation]
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
index 21631be25c..4461722bb4 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
@@ -1,6 +1,6 @@
require 'active_record/connection_adapters/abstract_mysql_adapter'
-gem 'mysql2', '~> 0.3.13'
+gem 'mysql2', '>= 0.3.18', '< 0.5'
require 'mysql2'
module ActiveRecord
@@ -37,8 +37,8 @@ module ActiveRecord
configure_connection
end
- def supports_explain?
- true
+ def supports_json?
+ version >= '5.7.8'
end
# HELPER METHODS ===========================================
@@ -95,80 +95,6 @@ module ActiveRecord
# DATABASE STATEMENTS ======================================
#++
- def explain(arel, binds = [])
- sql = "EXPLAIN #{to_sql(arel, binds.dup)}"
- start = Time.now
- result = exec_query(sql, 'EXPLAIN', binds)
- elapsed = Time.now - start
-
- ExplainPrettyPrinter.new.pp(result, elapsed)
- end
-
- class ExplainPrettyPrinter # :nodoc:
- # Pretty prints the result of a EXPLAIN in a way that resembles the output of the
- # MySQL shell:
- #
- # +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+
- # | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
- # +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+
- # | 1 | SIMPLE | users | const | PRIMARY | PRIMARY | 4 | const | 1 | |
- # | 1 | SIMPLE | posts | ALL | NULL | NULL | NULL | NULL | 1 | Using where |
- # +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+
- # 2 rows in set (0.00 sec)
- #
- # This is an exercise in Ruby hyperrealism :).
- def pp(result, elapsed)
- widths = compute_column_widths(result)
- separator = build_separator(widths)
-
- pp = []
-
- pp << separator
- pp << build_cells(result.columns, widths)
- pp << separator
-
- result.rows.each do |row|
- pp << build_cells(row, widths)
- end
-
- pp << separator
- pp << build_footer(result.rows.length, elapsed)
-
- pp.join("\n") + "\n"
- end
-
- private
-
- def compute_column_widths(result)
- [].tap do |widths|
- result.columns.each_with_index do |column, i|
- cells_in_column = [column] + result.rows.map {|r| r[i].nil? ? 'NULL' : r[i].to_s}
- widths << cells_in_column.map(&:length).max
- end
- end
- end
-
- def build_separator(widths)
- padding = 1
- '+' + widths.map {|w| '-' * (w + (padding*2))}.join('+') + '+'
- end
-
- def build_cells(items, widths)
- cells = []
- items.each_with_index do |item, i|
- item = 'NULL' if item.nil?
- justifier = item.is_a?(Numeric) ? 'rjust' : 'ljust'
- cells << item.to_s.send(justifier, widths[i])
- end
- '| ' + cells.join(' | ') + ' |'
- end
-
- def build_footer(nrows, elapsed)
- rows_label = nrows == 1 ? 'row' : 'rows'
- "#{nrows} #{rows_label} in set (%.2f sec)" % elapsed
- end
- end
-
# FIXME: re-enable the following once a "better" query_cache solution is in core
#
# The overrides below perform much better than the originals in AbstractAdapter
@@ -254,7 +180,7 @@ module ActiveRecord
end
def full_version
- @full_version ||= @connection.info[:version]
+ @full_version ||= @connection.server_info[:version]
end
def set_field_encoding field_name
diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
index 45b935f1d6..b3894481cc 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
@@ -5,8 +5,10 @@ require 'active_support/core_ext/hash/keys'
gem 'mysql', '~> 2.9'
require 'mysql'
-class Mysql
+class Mysql # :nodoc: all
class Time
+ # Used for casting DateTime fields to a MySQL friendly Time.
+ # This was documented in 48498da0dfed5239ea1eafb243ce47d7e3ce9e8e
def to_date
Date.new(year, month, day)
end
@@ -56,9 +58,9 @@ module ActiveRecord
# * <tt>:password</tt> - Defaults to nothing.
# * <tt>:database</tt> - The name of the database. No default, must be provided.
# * <tt>:encoding</tt> - (Optional) Sets the client encoding by executing "SET NAMES <encoding>" after connection.
- # * <tt>:reconnect</tt> - Defaults to false (See MySQL documentation: http://dev.mysql.com/doc/refman/5.6/en/auto-reconnect.html).
- # * <tt>:strict</tt> - Defaults to true. Enable STRICT_ALL_TABLES. (See MySQL documentation: http://dev.mysql.com/doc/refman/5.6/en/sql-mode.html)
- # * <tt>:variables</tt> - (Optional) A hash session variables to send as <tt>SET @@SESSION.key = value</tt> on each database connection. Use the value +:default+ to set a variable to its DEFAULT value. (See MySQL documentation: http://dev.mysql.com/doc/refman/5.6/en/set-statement.html).
+ # * <tt>:reconnect</tt> - Defaults to false (See MySQL documentation: http://dev.mysql.com/doc/refman/5.7/en/auto-reconnect.html).
+ # * <tt>:strict</tt> - Defaults to true. Enable STRICT_ALL_TABLES. (See MySQL documentation: http://dev.mysql.com/doc/refman/5.7/en/sql-mode.html)
+ # * <tt>:variables</tt> - (Optional) A hash session variables to send as <tt>SET @@SESSION.key = value</tt> on each database connection. Use the value +:default+ to set a variable to its DEFAULT value. (See MySQL documentation: http://dev.mysql.com/doc/refman/5.7/en/set-statement.html).
# * <tt>:sslca</tt> - Necessary to use MySQL with an SSL connection.
# * <tt>:sslkey</tt> - Necessary to use MySQL with an SSL connection.
# * <tt>:sslcert</tt> - Necessary to use MySQL with an SSL connection.
@@ -69,41 +71,16 @@ module ActiveRecord
ADAPTER_NAME = 'MySQL'.freeze
class StatementPool < ConnectionAdapters::StatementPool
- def initialize(connection, max = 1000)
- super
- @cache = Hash.new { |h,pid| h[pid] = {} }
- end
-
- def each(&block); cache.each(&block); end
- def key?(key); cache.key?(key); end
- def [](key); cache[key]; end
- def length; cache.length; end
- def delete(key); cache.delete(key); end
-
- def []=(sql, key)
- while @max <= cache.size
- cache.shift.last[:stmt].close
- end
- cache[sql] = key
- end
-
- def clear
- cache.each_value do |hash|
- hash[:stmt].close
- end
- cache.clear
- end
-
private
- def cache
- @cache[Process.pid]
+
+ def dealloc(stmt)
+ stmt[:stmt].close
end
end
def initialize(connection, logger, connection_options, config)
super
- @statements = StatementPool.new(@connection,
- self.class.type_cast_config_to_integer(config.fetch(:statement_limit) { 1000 }))
+ @statements = StatementPool.new(self.class.type_cast_config_to_integer(config.fetch(:statement_limit) { 1000 }))
@client_encoding = nil
connect
end
@@ -184,6 +161,14 @@ module ActiveRecord
# DATABASE STATEMENTS ======================================
#++
+ def select_all(arel, name = nil, binds = [])
+ if ExplainRegistry.collect? && prepared_statements
+ unprepared_statement { super }
+ else
+ super
+ end
+ end
+
def select_rows(sql, name = nil, binds = [])
@connection.query_with_result = true
rows = exec_query(sql, name, binds).rows
@@ -245,7 +230,7 @@ module ActiveRecord
return @client_encoding if @client_encoding
result = exec_query(
- "SHOW VARIABLES WHERE Variable_name = 'character_set_client'",
+ "select @@character_set_client",
'SCHEMA')
@client_encoding = ENCODINGS[result.rows.last.last]
end
@@ -414,8 +399,11 @@ module ActiveRecord
# 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
- @statements.delete sql
+ if binds.empty?
+ stmt.close
+ else
+ @statements.delete sql
+ end
raise e
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb
index be13ead120..bfa03fa136 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb
@@ -8,7 +8,8 @@ module ActiveRecord
def serial?
return unless default_function
- %r{\Anextval\('(?<table_name>.+)_#{name}_seq'::regclass\)\z} === default_function
+ table_name = @table_name || '(?<table_name>.+)'
+ %r{\Anextval\('"?#{table_name}_#{name}_seq"?'::regclass\)\z} === default_function
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb
index 11d3f5301a..43e18ebb2b 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb
@@ -8,7 +8,7 @@ module ActiveRecord
end
class ExplainPrettyPrinter # :nodoc:
- # Pretty prints the result of a EXPLAIN in a way that resembles the output of the
+ # Pretty prints the result of an EXPLAIN in a way that resembles the output of the
# PostgreSQL shell:
#
# QUERY PLAN
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb
index 92349e2f9b..68752cdd80 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb
@@ -12,6 +12,7 @@ require 'active_record/connection_adapters/postgresql/oid/json'
require 'active_record/connection_adapters/postgresql/oid/jsonb'
require 'active_record/connection_adapters/postgresql/oid/money'
require 'active_record/connection_adapters/postgresql/oid/point'
+require 'active_record/connection_adapters/postgresql/oid/rails_5_1_point'
require 'active_record/connection_adapters/postgresql/oid/range'
require 'active_record/connection_adapters/postgresql/oid/specialized_string'
require 'active_record/connection_adapters/postgresql/oid/uuid'
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 3de794f797..25961a9869 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb
@@ -45,6 +45,11 @@ module ActiveRecord
delimiter == other.delimiter
end
+ def type_cast_for_schema(value)
+ return super unless value.is_a?(::Array)
+ "[" + value.map { |v| subtype.type_cast_for_schema(v) }.join(", ") + "]"
+ end
+
private
def type_cast_array(value, method)
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 2c04c46131..424769f765 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
@@ -4,18 +4,14 @@ module ActiveRecord
module OID # :nodoc:
class DateTime < Type::DateTime # :nodoc:
def cast_value(value)
- if value.is_a?(::String)
- case value
- when 'infinity' then ::Float::INFINITY
- when '-infinity' then -::Float::INFINITY
- when / BC$/
- astronomical_year = format("%04d", -value[/^\d+/].to_i + 1)
- super(value.sub(/ BC$/, "").sub(/^\d+/, astronomical_year))
- else
- super
- end
+ case value
+ when 'infinity' then ::Float::INFINITY
+ when '-infinity' then -::Float::INFINITY
+ when / BC$/
+ astronomical_year = format("%04d", -value[/^\d+/].to_i + 1)
+ super(value.sub(/ BC$/, "").sub(/^\d+/, astronomical_year))
else
- value
+ super
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb
index 8e1256baad..dbc879ffd4 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb
@@ -2,32 +2,7 @@ module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module OID # :nodoc:
- class Json < Type::Value # :nodoc:
- include Type::Helpers::Mutable
-
- def type
- :json
- end
-
- def deserialize(value)
- if value.is_a?(::String)
- ::ActiveSupport::JSON.decode(value) rescue nil
- else
- value
- end
- end
-
- def serialize(value)
- if value.is_a?(::Array) || value.is_a?(::Hash)
- ::ActiveSupport::JSON.encode(value)
- else
- value
- end
- end
-
- def accessor
- ActiveRecord::Store::StringKeyedHashAccessor
- end
+ class Json < Type::Internal::AbstractJson
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb
index afc9383f91..87391b5dc7 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb
@@ -9,7 +9,7 @@ module ActiveRecord
def changed_in_place?(raw_old_value, new_value)
# Postgres does not preserve insignificant whitespaces when
- # roundtripping jsonb columns. This causes some false positives for
+ # round-tripping jsonb columns. This causes some false positives for
# the comparison here. Therefore, we need to parse and re-dump the
# raw value here to ensure the insignificant whitespaces are
# consistent with our encoder's output.
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/rails_5_1_point.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/rails_5_1_point.rb
new file mode 100644
index 0000000000..7427a25ad5
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/rails_5_1_point.rb
@@ -0,0 +1,50 @@
+module ActiveRecord
+ Point = Struct.new(:x, :y)
+
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Rails51Point < Type::Value # :nodoc:
+ include Type::Helpers::Mutable
+
+ def type
+ :point
+ end
+
+ def cast(value)
+ case value
+ when ::String
+ if value[0] == '(' && value[-1] == ')'
+ value = value[1...-1]
+ end
+ x, y = value.split(",")
+ build_point(x, y)
+ when ::Array
+ build_point(*value)
+ else
+ value
+ end
+ end
+
+ def serialize(value)
+ if value.is_a?(ActiveRecord::Point)
+ "(#{number_for_point(value.x)},#{number_for_point(value.y)})"
+ else
+ super
+ end
+ end
+
+ private
+
+ def number_for_point(number)
+ number.to_s.gsub(/\.0$/, '')
+ end
+
+ def build_point(x, y)
+ ActiveRecord::Point.new(Float(x), Float(y))
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb
index 191c828e60..6155e53632 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
@@ -36,7 +36,7 @@ module ActiveRecord
WHERE
t.typname IN (%s)
OR t.typtype IN (%s)
- OR t.typinput::varchar = 'array_in'
+ OR t.typinput = 'array_in(cstring,oid,integer)'::regprocedure
OR t.typelem != 0
SQL
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
index b7755c4593..d5879ea7df 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
@@ -31,6 +31,11 @@ module ActiveRecord
Utils.extract_schema_qualified_name(name.to_s).quoted
end
+ # Quotes schema names for use in SQL queries.
+ def quote_schema_name(name)
+ PGconn.quote_ident(name)
+ end
+
def quote_table_name_for_assignment(table, attr)
quote_column_name(attr)
end
@@ -40,8 +45,7 @@ module ActiveRecord
PGconn.quote_ident(name.to_s)
end
- # Quote date/time values for use in SQL input. Includes microseconds
- # if the value is a Time responding to usec.
+ # Quote date/time values for use in SQL input.
def quoted_date(value) #:nodoc:
if value.year <= 0
bce_year = format("%04d", -value.year + 1)
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb
index 022dbdfa27..6399bddbee 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb
@@ -103,6 +103,30 @@ module ActiveRecord
args.each { |name| column(name, :point, options) }
end
+ def line(*args, **options)
+ args.each { |name| column(name, :line, options) }
+ end
+
+ def lseg(*args, **options)
+ args.each { |name| column(name, :lseg, options) }
+ end
+
+ def box(*args, **options)
+ args.each { |name| column(name, :box, options) }
+ end
+
+ def path(*args, **options)
+ args.each { |name| column(name, :path, options) }
+ end
+
+ def polygon(*args, **options)
+ args.each { |name| column(name, :polygon, options) }
+ end
+
+ def circle(*args, **options)
+ args.each { |name| column(name, :circle, options) }
+ end
+
def serial(*args, **options)
args.each { |name| column(name, :serial, options) }
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 168180cfd3..aaf5b2898b 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
@@ -8,6 +8,13 @@ module ActiveRecord
o.sql_type = type_to_sql(o.type, o.limit, o.precision, o.scale, o.array)
super
end
+
+ def add_column_options!(sql, options)
+ if options[:collation]
+ sql << " COLLATE \"#{options[:collation]}\""
+ end
+ super
+ end
end
module SchemaStatements
@@ -61,12 +68,18 @@ module ActiveRecord
execute "DROP DATABASE IF EXISTS #{quote_table_name(name)}"
end
- # Returns the list of all tables in the schema search path or a specified schema.
+ # Returns the list of all tables in the schema search path.
def tables(name = nil)
- query(<<-SQL, 'SCHEMA').map { |row| row[0] }
- SELECT tablename
- FROM pg_tables
- WHERE schemaname = ANY (current_schemas(false))
+ select_values("SELECT tablename FROM pg_tables WHERE schemaname = ANY(current_schemas(false))", 'SCHEMA')
+ end
+
+ def data_sources # :nodoc
+ select_values(<<-SQL, 'SCHEMA')
+ SELECT c.relname
+ FROM pg_class c
+ LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
+ WHERE c.relkind IN ('r', 'v','m') -- (r)elation/table, (v)iew, (m)aterialized view
+ AND n.nspname = ANY (current_schemas(false))
SQL
end
@@ -77,7 +90,7 @@ module ActiveRecord
name = Utils.extract_schema_qualified_name(name.to_s)
return false unless name.identifier
- exec_query(<<-SQL, 'SCHEMA').rows.first[0].to_i > 0
+ select_value(<<-SQL, 'SCHEMA').to_i > 0
SELECT COUNT(*)
FROM pg_class c
LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
@@ -86,6 +99,31 @@ module ActiveRecord
AND n.nspname = #{name.schema ? "'#{name.schema}'" : 'ANY (current_schemas(false))'}
SQL
end
+ alias data_source_exists? table_exists?
+
+ def views # :nodoc:
+ select_values(<<-SQL, 'SCHEMA')
+ SELECT c.relname
+ FROM pg_class c
+ LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
+ WHERE c.relkind IN ('v','m') -- (v)iew, (m)aterialized view
+ AND n.nspname = ANY (current_schemas(false))
+ SQL
+ end
+
+ def view_exists?(view_name) # :nodoc:
+ name = Utils.extract_schema_qualified_name(view_name.to_s)
+ return false unless name.identifier
+
+ select_values(<<-SQL, 'SCHEMA').any?
+ SELECT c.relname
+ FROM pg_class c
+ LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
+ WHERE c.relkind IN ('v','m') -- (v)iew, (m)aterialized view
+ AND c.relname = '#{name.identifier}'
+ AND n.nspname = #{name.schema ? "'#{name.schema}'" : 'ANY (current_schemas(false))'}
+ SQL
+ end
def drop_table(table_name, options = {}) # :nodoc:
execute "DROP TABLE#{' IF EXISTS' if options[:if_exists]} #{quote_table_name(table_name)}#{' CASCADE' if options[:force] == :cascade}"
@@ -93,23 +131,24 @@ module ActiveRecord
# Returns true if schema exists.
def schema_exists?(name)
- exec_query(<<-SQL, 'SCHEMA').rows.first[0].to_i > 0
- SELECT COUNT(*)
- FROM pg_namespace
- WHERE nspname = '#{name}'
- SQL
+ select_value("SELECT COUNT(*) FROM pg_namespace WHERE nspname = '#{name}'", 'SCHEMA').to_i > 0
end
+ # Verifies existence of an index with a given name.
def index_name_exists?(table_name, index_name, default)
- exec_query(<<-SQL, 'SCHEMA').rows.first[0].to_i > 0
+ table = Utils.extract_schema_qualified_name(table_name.to_s)
+ index = Utils.extract_schema_qualified_name(index_name.to_s)
+
+ select_value(<<-SQL, 'SCHEMA').to_i > 0
SELECT COUNT(*)
FROM pg_class t
INNER JOIN pg_index d ON t.oid = d.indrelid
INNER JOIN pg_class i ON d.indexrelid = i.oid
+ LEFT JOIN pg_namespace n ON n.oid = i.relnamespace
WHERE i.relkind = 'i'
- AND i.relname = '#{index_name}'
- AND t.relname = '#{table_name}'
- AND i.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname = ANY (current_schemas(false)) )
+ AND i.relname = '#{index.identifier}'
+ AND t.relname = '#{table.identifier}'
+ AND n.nspname = #{index.schema ? "'#{index.schema}'" : 'ANY (current_schemas(false))'}
SQL
end
@@ -158,55 +197,48 @@ module ActiveRecord
# Returns the list of all column definitions for a table.
def columns(table_name)
# Limit, precision, and scale are all handled by the superclass.
- column_definitions(table_name).map do |column_name, type, default, notnull, oid, fmod|
+ column_definitions(table_name).map do |column_name, type, default, notnull, oid, fmod, collation|
oid = oid.to_i
fmod = fmod.to_i
type_metadata = fetch_type_metadata(column_name, type, oid, fmod)
default_value = extract_value_from_default(default)
default_function = extract_default_function(default_value, default)
- new_column(column_name, default_value, type_metadata, !notnull, default_function)
+ new_column(column_name, default_value, type_metadata, !notnull, default_function, collation)
end
end
- def new_column(name, default, sql_type_metadata = nil, null = true, default_function = nil) # :nodoc:
- PostgreSQLColumn.new(name, default, sql_type_metadata, null, default_function)
+ def new_column(name, default, sql_type_metadata = nil, null = true, default_function = nil, collation = nil) # :nodoc:
+ PostgreSQLColumn.new(name, default, sql_type_metadata, null, default_function, collation)
end
# Returns the current database name.
def current_database
- query('select current_database()', 'SCHEMA')[0][0]
+ select_value('select current_database()', 'SCHEMA')
end
# Returns the current schema name.
def current_schema
- query('SELECT current_schema', 'SCHEMA')[0][0]
+ select_value('SELECT current_schema', 'SCHEMA')
end
# Returns the current database encoding format.
def encoding
- query(<<-end_sql, 'SCHEMA')[0][0]
- SELECT pg_encoding_to_char(pg_database.encoding) FROM pg_database
- WHERE pg_database.datname LIKE '#{current_database}'
- end_sql
+ select_value("SELECT pg_encoding_to_char(encoding) FROM pg_database WHERE datname LIKE '#{current_database}'", 'SCHEMA')
end
# Returns the current database collation.
def collation
- query(<<-end_sql, 'SCHEMA')[0][0]
- SELECT pg_database.datcollate FROM pg_database WHERE pg_database.datname LIKE '#{current_database}'
- end_sql
+ select_value("SELECT datcollate FROM pg_database WHERE datname LIKE '#{current_database}'", 'SCHEMA')
end
# Returns the current database ctype.
def ctype
- query(<<-end_sql, 'SCHEMA')[0][0]
- SELECT pg_database.datctype FROM pg_database WHERE pg_database.datname LIKE '#{current_database}'
- end_sql
+ select_value("SELECT datctype FROM pg_database WHERE datname LIKE '#{current_database}'", 'SCHEMA')
end
# Returns an array of schema names.
def schema_names
- query(<<-SQL, 'SCHEMA').flatten
+ select_values(<<-SQL, 'SCHEMA')
SELECT nspname
FROM pg_namespace
WHERE nspname !~ '^pg_.*'
@@ -217,12 +249,12 @@ module ActiveRecord
# Creates a schema for the given schema name.
def create_schema schema_name
- execute "CREATE SCHEMA #{schema_name}"
+ execute "CREATE SCHEMA #{quote_schema_name(schema_name)}"
end
# Drops the schema for the given schema name.
- def drop_schema schema_name
- execute "DROP SCHEMA #{schema_name} CASCADE"
+ def drop_schema(schema_name, options = {})
+ execute "DROP SCHEMA#{' IF EXISTS' if options[:if_exists]} #{quote_schema_name(schema_name)} CASCADE"
end
# Sets the schema search path to a string of comma-separated schema names.
@@ -239,12 +271,12 @@ module ActiveRecord
# Returns the active schema search path.
def schema_search_path
- @schema_search_path ||= query('SHOW search_path', 'SCHEMA')[0][0]
+ @schema_search_path ||= select_value('SHOW search_path', 'SCHEMA')
end
# Returns the current client message level.
def client_min_messages
- query('SHOW client_min_messages', 'SCHEMA')[0][0]
+ select_value('SHOW client_min_messages', 'SCHEMA')
end
# Set the client message level.
@@ -262,10 +294,7 @@ module ActiveRecord
end
def serial_sequence(table, column)
- result = exec_query(<<-eosql, 'SCHEMA')
- SELECT pg_get_serial_sequence('#{table}', '#{column}')
- eosql
- result.rows.first.first
+ select_value("SELECT pg_get_serial_sequence('#{table}', '#{column}')", 'SCHEMA')
end
# Sets the sequence of a table's primary key to the specified value.
@@ -276,9 +305,7 @@ module ActiveRecord
if sequence
quoted_sequence = quote_table_name(sequence)
- select_value <<-end_sql, 'SCHEMA'
- SELECT setval('#{quoted_sequence}', #{value})
- end_sql
+ select_value("SELECT setval('#{quoted_sequence}', #{value})", 'SCHEMA')
else
@logger.warn "#{table} has primary key #{pk} with no default sequence" if @logger
end
@@ -301,7 +328,7 @@ module ActiveRecord
if pk && sequence
quoted_sequence = quote_table_name(sequence)
- select_value <<-end_sql, 'SCHEMA'
+ select_value(<<-end_sql, 'SCHEMA')
SELECT setval('#{quoted_sequence}', (SELECT COALESCE(MAX(#{quote_column_name pk})+(SELECT increment_by FROM #{quoted_sequence}), (SELECT min_value FROM #{quoted_sequence})) FROM #{quote_table_name(table)}), false)
end_sql
end
@@ -361,17 +388,19 @@ module ActiveRecord
nil
end
- # Returns just a table's primary key
- def primary_key(table)
- pks = exec_query(<<-end_sql, 'SCHEMA').rows
- SELECT attr.attname
- FROM pg_attribute attr
- INNER JOIN pg_constraint cons ON attr.attrelid = cons.conrelid AND attr.attnum = any(cons.conkey)
- WHERE cons.contype = 'p'
- AND cons.conrelid = '#{quote_table_name(table)}'::regclass
- end_sql
- return nil unless pks.count == 1
- pks[0][0]
+ def primary_keys(table_name) # :nodoc:
+ select_values(<<-SQL.strip_heredoc, 'SCHEMA')
+ WITH pk_constraint AS (
+ SELECT conrelid, unnest(conkey) AS connum FROM pg_constraint
+ WHERE contype = 'p'
+ AND conrelid = '#{quote_table_name(table_name)}'::regclass
+ ), cons AS (
+ SELECT conrelid, connum, row_number() OVER() AS rownum FROM pk_constraint
+ )
+ SELECT attr.attname FROM pg_attribute attr
+ INNER JOIN cons ON attr.attrelid = cons.conrelid AND attr.attnum = cons.connum
+ ORDER BY cons.rownum
+ SQL
end
# Renames a table.
@@ -388,27 +417,27 @@ module ActiveRecord
new_seq = "#{new_name}_#{pk}_seq"
idx = "#{table_name}_pkey"
new_idx = "#{new_name}_pkey"
- execute "ALTER TABLE #{quote_table_name(seq)} RENAME TO #{quote_table_name(new_seq)}"
+ execute "ALTER TABLE #{seq.quoted} RENAME TO #{quote_table_name(new_seq)}"
execute "ALTER INDEX #{quote_table_name(idx)} RENAME TO #{quote_table_name(new_idx)}"
end
rename_table_indexes(table_name, new_name)
end
- # Adds a new column to the named table.
- # See TableDefinition#column for details of the options you can use.
- def add_column(table_name, column_name, type, options = {})
+ def add_column(table_name, column_name, type, options = {}) #:nodoc:
clear_cache!
super
end
- # Changes the column of a table.
- def change_column(table_name, column_name, type, options = {})
+ def change_column(table_name, column_name, type, options = {}) #:nodoc:
clear_cache!
quoted_table_name = quote_table_name(table_name)
quoted_column_name = quote_column_name(column_name)
sql_type = type_to_sql(type, options[:limit], options[:precision], options[:scale], options[:array])
sql = "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quoted_column_name} TYPE #{sql_type}"
+ if options[:collation]
+ sql << " COLLATE \"#{options[:collation]}\""
+ end
if options[:using]
sql << " USING #{options[:using]}"
elsif options[:cast_as]
@@ -422,11 +451,12 @@ module ActiveRecord
end
# Changes the default value of a table column.
- def change_column_default(table_name, column_name, default) # :nodoc:
+ def change_column_default(table_name, column_name, default_or_changes) # :nodoc:
clear_cache!
column = column_for(table_name, column_name)
return unless column
+ default = extract_new_default_value(default_or_changes)
alter_column_query = "ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} %s"
if default.nil?
# <tt>DEFAULT NULL</tt> results in the same behavior as <tt>DROP DEFAULT</tt>. However, PostgreSQL will
@@ -437,7 +467,7 @@ module ActiveRecord
end
end
- def change_column_null(table_name, column_name, null, default = nil)
+ def change_column_null(table_name, column_name, null, default = nil) #:nodoc:
clear_cache!
unless null || default.nil?
column = column_for(table_name, column_name)
@@ -447,7 +477,7 @@ module ActiveRecord
end
# Renames a column in a table.
- def rename_column(table_name, column_name, new_column_name)
+ def rename_column(table_name, column_name, new_column_name) #:nodoc:
clear_cache!
execute "ALTER TABLE #{quote_table_name(table_name)} RENAME COLUMN #{quote_column_name(column_name)} TO #{quote_column_name(new_column_name)}"
rename_column_indexes(table_name, column_name, new_column_name)
@@ -458,10 +488,19 @@ module ActiveRecord
execute "CREATE #{index_type} INDEX #{index_algorithm} #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} #{index_using} (#{index_columns})#{index_options}"
end
- def remove_index!(table_name, index_name) #:nodoc:
- execute "DROP INDEX #{quote_table_name(index_name)}"
+ def remove_index(table_name, options = {}) #:nodoc:
+ index_name = index_name_for_remove(table_name, options)
+ algorithm =
+ if Hash === options && options.key?(:algorithm)
+ index_algorithms.fetch(options[:algorithm]) do
+ raise ArgumentError.new("Algorithm must be one of the following: #{index_algorithms.keys.map(&:inspect).join(', ')}")
+ end
+ end
+ execute "DROP INDEX #{algorithm} #{quote_table_name(index_name)}"
end
+ # Renames an index of a table. Raises error if length of new
+ # index name is greater than allowed limit.
def rename_index(table_name, old_name, new_name)
validate_index_length!(table_name, new_name)
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb b/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb
index 58715978f7..b2c49989a4 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb
@@ -12,7 +12,7 @@ module ActiveRecord
end
def sql_type
- super.gsub(/\[\]$/, "")
+ super.gsub(/\[\]$/, "".freeze)
end
def ==(other)
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
index 332ac9d88c..25dfda9ef8 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
@@ -14,8 +14,6 @@ require "active_record/connection_adapters/postgresql/type_metadata"
require "active_record/connection_adapters/postgresql/utils"
require "active_record/connection_adapters/statement_pool"
-require 'arel/visitors/bind_visitor'
-
require 'ipaddr'
module ActiveRecord
@@ -68,11 +66,11 @@ module ActiveRecord
# defaults to true.
#
# Any further options are used as connection parameters to libpq. See
- # http://www.postgresql.org/docs/9.1/static/libpq-connect.html for the
+ # http://www.postgresql.org/docs/current/static/libpq-connect.html for the
# list of parameters.
#
# In addition, default connection parameters of libpq can be set per environment variables.
- # See http://www.postgresql.org/docs/9.1/static/libpq-envars.html .
+ # See http://www.postgresql.org/docs/current/static/libpq-envars.html .
class PostgreSQLAdapter < AbstractAdapter
ADAPTER_NAME = 'PostgreSQL'.freeze
@@ -203,52 +201,31 @@ module ActiveRecord
true
end
+ def supports_json?
+ postgresql_version >= 90200
+ end
+
def index_algorithms
{ concurrently: 'CONCURRENTLY' }
end
class StatementPool < ConnectionAdapters::StatementPool
def initialize(connection, max)
- super
+ super(max)
+ @connection = connection
@counter = 0
- @cache = Hash.new { |h,pid| h[pid] = {} }
end
- def each(&block); cache.each(&block); end
- def key?(key); cache.key?(key); end
- def [](key); cache[key]; end
- def length; cache.length; end
-
def next_key
"a#{@counter + 1}"
end
def []=(sql, key)
- while @max <= cache.size
- dealloc(cache.shift.last)
- end
- @counter += 1
- cache[sql] = key
- end
-
- def clear
- cache.each_value do |stmt_key|
- dealloc stmt_key
- end
- cache.clear
- end
-
- def delete(sql_key)
- dealloc cache[sql_key]
- cache.delete sql_key
+ super.tap { @counter += 1 }
end
private
- def cache
- @cache[Process.pid]
- end
-
def dealloc(key)
@connection.query "DEALLOCATE #{key}" if connection_active?
end
@@ -452,7 +429,7 @@ module ActiveRecord
@connection.server_version
end
- # See http://www.postgresql.org/docs/9.1/static/errcodes-appendix.html
+ # See http://www.postgresql.org/docs/current/static/errcodes-appendix.html
FOREIGN_KEY_VIOLATION = "23503"
UNIQUE_VIOLATION = "23505"
@@ -726,7 +703,7 @@ module ActiveRecord
end
# SET statements from :variables config hash
- # http://www.postgresql.org/docs/8.3/static/sql-set.html
+ # http://www.postgresql.org/docs/current/static/sql-set.html
variables = @config[:variables] || {}
variables.map do |k, v|
if v == ':default' || v == :default
@@ -770,9 +747,11 @@ module ActiveRecord
# - format_type includes the column size constraint, e.g. varchar(50)
# - ::regclass is a function that gives the id for a table name
def column_definitions(table_name) # :nodoc:
- exec_query(<<-end_sql, 'SCHEMA').rows
+ query(<<-end_sql, 'SCHEMA')
SELECT a.attname, format_type(a.atttypid, a.atttypmod),
- pg_get_expr(d.adbin, d.adrelid), a.attnotnull, a.atttypid, a.atttypmod
+ pg_get_expr(d.adbin, d.adrelid), a.attnotnull, a.atttypid, a.atttypmod,
+ (SELECT c.collname FROM pg_collation c, pg_type t
+ WHERE c.oid = a.attcollation AND t.oid = a.atttypid AND a.attcollation <> t.typcollation)
FROM pg_attribute a LEFT JOIN pg_attrdef d
ON a.attrelid = d.adrelid AND a.attnum = d.adnum
WHERE a.attrelid = '#{quote_table_name(table_name)}'::regclass
@@ -782,7 +761,7 @@ module ActiveRecord
end
def extract_table_ref_from_insert_sql(sql) # :nodoc:
- sql[/into\s+([^\(]*).*values\s*\(/im]
+ sql[/into\s("[A-Za-z0-9_."\[\]\s]+"|[A-Za-z0-9_."\[\]]+)\s*/im]
$1.strip if $1
end
@@ -864,6 +843,8 @@ module ActiveRecord
ActiveRecord::Type.register(:jsonb, OID::Jsonb, adapter: :postgresql)
ActiveRecord::Type.register(:money, OID::Money, adapter: :postgresql)
ActiveRecord::Type.register(:point, OID::Point, adapter: :postgresql)
+ ActiveRecord::Type.register(:legacy_point, OID::Point, adapter: :postgresql)
+ ActiveRecord::Type.register(:rails_5_1_point, OID::Rails51Point, adapter: :postgresql)
ActiveRecord::Type.register(:uuid, OID::Uuid, adapter: :postgresql)
ActiveRecord::Type.register(:vector, OID::Vector, adapter: :postgresql)
ActiveRecord::Type.register(:xml, OID::Xml, adapter: :postgresql)
diff --git a/activerecord/lib/active_record/connection_adapters/schema_cache.rb b/activerecord/lib/active_record/connection_adapters/schema_cache.rb
index 37ff4e4613..eee142378c 100644
--- a/activerecord/lib/active_record/connection_adapters/schema_cache.rb
+++ b/activerecord/lib/active_record/connection_adapters/schema_cache.rb
@@ -10,33 +10,46 @@ module ActiveRecord
@columns = {}
@columns_hash = {}
@primary_keys = {}
- @tables = {}
+ @data_sources = {}
+ end
+
+ def initialize_dup(other)
+ super
+ @columns = @columns.dup
+ @columns_hash = @columns_hash.dup
+ @primary_keys = @primary_keys.dup
+ @data_sources = @data_sources.dup
end
def primary_keys(table_name)
- @primary_keys[table_name] ||= table_exists?(table_name) ? connection.primary_key(table_name) : nil
+ @primary_keys[table_name] ||= data_source_exists?(table_name) ? connection.primary_key(table_name) : nil
end
# A cached lookup for table existence.
- def table_exists?(name)
- prepare_tables if @tables.empty?
- return @tables[name] if @tables.key? name
+ def data_source_exists?(name)
+ prepare_data_sources if @data_sources.empty?
+ return @data_sources[name] if @data_sources.key? name
- @tables[name] = connection.table_exists?(name)
+ @data_sources[name] = connection.data_source_exists?(name)
end
+ alias table_exists? data_source_exists?
+ deprecate :table_exists? => "use #data_source_exists? instead"
+
# Add internal cache for table with +table_name+.
def add(table_name)
- if table_exists?(table_name)
+ if data_source_exists?(table_name)
primary_keys(table_name)
columns(table_name)
columns_hash(table_name)
end
end
- def tables(name)
- @tables[name]
+ def data_sources(name)
+ @data_sources[name]
end
+ alias tables data_sources
+ deprecate :tables => "use #data_sources instead"
# Get the columns for a table
def columns(table_name)
@@ -56,36 +69,38 @@ module ActiveRecord
@columns.clear
@columns_hash.clear
@primary_keys.clear
- @tables.clear
+ @data_sources.clear
@version = nil
end
def size
- [@columns, @columns_hash, @primary_keys, @tables].map(&:size).inject :+
+ [@columns, @columns_hash, @primary_keys, @data_sources].map(&:size).inject :+
end
- # Clear out internal caches for table with +table_name+.
- def clear_table_cache!(table_name)
- @columns.delete table_name
- @columns_hash.delete table_name
- @primary_keys.delete table_name
- @tables.delete table_name
+ # Clear out internal caches for the data source +name+.
+ def clear_data_source_cache!(name)
+ @columns.delete name
+ @columns_hash.delete name
+ @primary_keys.delete name
+ @data_sources.delete name
end
+ alias clear_table_cache! clear_data_source_cache!
+ deprecate :clear_table_cache! => "use #clear_data_source_cache! instead"
def marshal_dump
# if we get current version during initialization, it happens stack over flow.
@version = ActiveRecord::Migrator.current_version
- [@version, @columns, @columns_hash, @primary_keys, @tables]
+ [@version, @columns, @columns_hash, @primary_keys, @data_sources]
end
def marshal_load(array)
- @version, @columns, @columns_hash, @primary_keys, @tables = array
+ @version, @columns, @columns_hash, @primary_keys, @data_sources = array
end
private
- def prepare_tables
- connection.tables.each { |table| @tables[table] = true }
+ def prepare_data_sources
+ connection.data_sources.each { |source| @data_sources[source] = true }
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_creation.rb
new file mode 100644
index 0000000000..fe1dcbd710
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_creation.rb
@@ -0,0 +1,15 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module SQLite3
+ class SchemaCreation < AbstractAdapter::SchemaCreation
+ private
+ def add_column_options!(sql, options)
+ if options[:collation]
+ sql << " COLLATE \"#{options[:collation]}\""
+ end
+ super
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
index 7e184dd510..505a71720f 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
@@ -1,6 +1,6 @@
require 'active_record/connection_adapters/abstract_adapter'
require 'active_record/connection_adapters/statement_pool'
-require 'arel/visitors/bind_visitor'
+require 'active_record/connection_adapters/sqlite3/schema_creation'
gem 'sqlite3', '~> 1.3.6'
require 'sqlite3'
@@ -65,59 +65,23 @@ module ActiveRecord
boolean: { name: "boolean" }
}
- class Version
- include Comparable
-
- def initialize(version_string)
- @version = version_string.split('.').map(&:to_i)
- end
-
- def <=>(version_string)
- @version <=> version_string.split('.').map(&:to_i)
- end
- end
-
class StatementPool < ConnectionAdapters::StatementPool
- def initialize(connection, max)
- super
- @cache = Hash.new { |h,pid| h[pid] = {} }
- end
-
- def each(&block); cache.each(&block); end
- def key?(key); cache.key?(key); end
- def [](key); cache[key]; end
- def length; cache.length; end
-
- def []=(sql, key)
- while @max <= cache.size
- dealloc(cache.shift.last[:stmt])
- end
- cache[sql] = key
- end
-
- def clear
- cache.each_value do |hash|
- dealloc hash[:stmt]
- end
- cache.clear
- end
-
private
- def cache
- @cache[$$]
- end
def dealloc(stmt)
- stmt.close unless stmt.closed?
+ stmt[:stmt].close unless stmt[:stmt].closed?
end
end
+ def schema_creation # :nodoc:
+ SQLite3::SchemaCreation.new self
+ end
+
def initialize(connection, logger, connection_options, config)
super(connection, logger)
@active = nil
- @statements = StatementPool.new(@connection,
- self.class.type_cast_config_to_integer(config.fetch(:statement_limit) { 1000 }))
+ @statements = StatementPool.new(self.class.type_cast_config_to_integer(config.fetch(:statement_limit) { 1000 }))
@config = config
@visitor = Arel::Visitors::SQLite.new self
@@ -254,7 +218,7 @@ module ActiveRecord
end
class ExplainPrettyPrinter
- # Pretty prints the result of a EXPLAIN QUERY PLAN in a way that resembles
+ # Pretty prints the result of an EXPLAIN QUERY PLAN in a way that resembles
# the output of the SQLite shell:
#
# 0|0|0|SEARCH TABLE users USING INTEGER PRIMARY KEY (rowid=?) (~1 rows)
@@ -355,10 +319,25 @@ module ActiveRecord
row['name']
end
end
+ alias data_sources tables
def table_exists?(table_name)
table_name && tables(nil, table_name).any?
end
+ alias data_source_exists? table_exists?
+
+ def views # :nodoc:
+ select_values("SELECT name FROM sqlite_master WHERE type = 'view' AND name <> 'sqlite_sequence'", 'SCHEMA')
+ end
+
+ def view_exists?(view_name) # :nodoc:
+ return false unless view_name.present?
+
+ sql = "SELECT name FROM sqlite_master WHERE type = 'view' AND name <> 'sqlite_sequence'"
+ sql << " AND name = #{quote(view_name)}"
+
+ select_values(sql, 'SCHEMA').any?
+ end
# Returns an array of +Column+ objects for the table specified by +table_name+.
def columns(table_name) #:nodoc:
@@ -372,9 +351,10 @@ module ActiveRecord
field["dflt_value"] = $1.gsub('""', '"')
end
+ collation = field['collation']
sql_type = field['type']
type_metadata = fetch_type_metadata(sql_type)
- new_column(field['name'], field['dflt_value'], type_metadata, field['notnull'].to_i == 0)
+ new_column(field['name'], field['dflt_value'], type_metadata, field['notnull'].to_i == 0, nil, collation)
end
end
@@ -403,13 +383,13 @@ module ActiveRecord
end
end
- def primary_key(table_name) #:nodoc:
+ def primary_keys(table_name) # :nodoc:
pks = table_structure(table_name).select { |f| f['pk'] > 0 }
- return nil unless pks.count == 1
- pks[0]['name']
+ pks.sort_by { |f| f['pk'] }.map { |f| f['name'] }
end
- def remove_index!(table_name, index_name) #:nodoc:
+ def remove_index(table_name, options = {}) #:nodoc:
+ index_name = index_name_for_remove(table_name, options)
exec_query "DROP INDEX #{quote_column_name(index_name)}"
end
@@ -444,13 +424,15 @@ module ActiveRecord
end
end
- def change_column_default(table_name, column_name, default) #:nodoc:
+ def change_column_default(table_name, column_name, default_or_changes) #:nodoc:
+ default = extract_new_default_value(default_or_changes)
+
alter_table(table_name) do |definition|
definition[column_name].default = default
end
end
- def change_column_null(table_name, column_name, null, default = nil)
+ def change_column_null(table_name, column_name, null, default = nil) #:nodoc:
unless null || default.nil?
exec_query("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL")
end
@@ -469,6 +451,7 @@ module ActiveRecord
self.null = options[:null] if options.include?(:null)
self.precision = options[:precision] if options.include?(:precision)
self.scale = options[:scale] if options.include?(:scale)
+ self.collation = options[:collation] if options.include?(:collation)
end
end
end
@@ -482,9 +465,9 @@ module ActiveRecord
protected
def table_structure(table_name)
- structure = exec_query("PRAGMA table_info(#{quote_table_name(table_name)})", 'SCHEMA').to_hash
+ structure = exec_query("PRAGMA table_info(#{quote_table_name(table_name)})", 'SCHEMA')
raise(ActiveRecord::StatementInvalid, "Could not find table '#{table_name}'") if structure.empty?
- structure
+ table_structure_with_collation(table_name, structure)
end
def alter_table(table_name, options = {}) #:nodoc:
@@ -519,7 +502,7 @@ module ActiveRecord
@definition.column(column_name, column.type,
:limit => column.limit, :default => column.default,
:precision => column.precision, :scale => column.scale,
- :null => column.null)
+ :null => column.null, collation: column.collation)
end
yield @definition if block_given?
end
@@ -581,6 +564,46 @@ module ActiveRecord
super
end
end
+
+ private
+ COLLATE_REGEX = /.*\"(\w+)\".*collate\s+\"(\w+)\".*/i.freeze
+
+ def table_structure_with_collation(table_name, basic_structure)
+ collation_hash = {}
+ sql = "SELECT sql FROM
+ (SELECT * FROM sqlite_master UNION ALL
+ SELECT * FROM sqlite_temp_master)
+ WHERE type='table' and name='#{ table_name }' \;"
+
+ # Result will have following sample string
+ # CREATE TABLE "users" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
+ # "password_digest" varchar COLLATE "NOCASE");
+ result = exec_query(sql, 'SCHEMA').first
+
+ if result
+ # Splitting with left parantheses and picking up last will return all
+ # columns separated with comma(,).
+ columns_string = result["sql"].split('(').last
+
+ columns_string.split(',').each do |column_string|
+ # This regex will match the column name and collation type and will save
+ # the value in $1 and $2 respectively.
+ collation_hash[$1] = $2 if (COLLATE_REGEX =~ column_string)
+ end
+
+ basic_structure.map! do |column|
+ column_name = column['name']
+
+ if collation_hash.has_key? column_name
+ column['collation'] = collation_hash[column_name]
+ end
+
+ column
+ end
+ else
+ basic_structure.to_hash
+ end
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/statement_pool.rb b/activerecord/lib/active_record/connection_adapters/statement_pool.rb
index c6b1bc8b5b..57463dd749 100644
--- a/activerecord/lib/active_record/connection_adapters/statement_pool.rb
+++ b/activerecord/lib/active_record/connection_adapters/statement_pool.rb
@@ -3,36 +3,53 @@ module ActiveRecord
class StatementPool
include Enumerable
- def initialize(connection, max = 1000)
- @connection = connection
- @max = max
+ def initialize(max = 1000)
+ @cache = Hash.new { |h,pid| h[pid] = {} }
+ @max = max
end
- def each
- raise NotImplementedError
+ def each(&block)
+ cache.each(&block)
end
def key?(key)
- raise NotImplementedError
+ cache.key?(key)
end
def [](key)
- raise NotImplementedError
+ cache[key]
end
def length
- raise NotImplementedError
+ cache.length
end
- def []=(sql, key)
- raise NotImplementedError
+ def []=(sql, stmt)
+ while @max <= cache.size
+ dealloc(cache.shift.last)
+ end
+ cache[sql] = stmt
end
def clear
- raise NotImplementedError
+ cache.each_value do |stmt|
+ dealloc stmt
+ end
+ cache.clear
end
def delete(key)
+ dealloc cache[key]
+ cache.delete(key)
+ end
+
+ private
+
+ def cache
+ @cache[Process.pid]
+ end
+
+ def dealloc(stmt)
raise NotImplementedError
end
end
diff --git a/activerecord/lib/active_record/connection_handling.rb b/activerecord/lib/active_record/connection_handling.rb
index 24f5849e45..2fc5e410f9 100644
--- a/activerecord/lib/active_record/connection_handling.rb
+++ b/activerecord/lib/active_record/connection_handling.rb
@@ -42,7 +42,7 @@ module ActiveRecord
#
# ActiveRecord::Base.establish_connection(:production)
#
- # The exceptions AdapterNotSpecified, AdapterNotFound and ArgumentError
+ # The exceptions +AdapterNotSpecified+, +AdapterNotFound+ and +ArgumentError+
# may be returned on an error.
def establish_connection(spec = nil)
spec ||= DEFAULT_ENV.call.to_sym
@@ -88,7 +88,7 @@ module ActiveRecord
end
def connection_id
- ActiveRecord::RuntimeRegistry.connection_id
+ ActiveRecord::RuntimeRegistry.connection_id ||= Thread.current.object_id
end
def connection_id=(connection_id)
diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb
index 1ad910c4bc..894d18b79e 100644
--- a/activerecord/lib/active_record/core.rb
+++ b/activerecord/lib/active_record/core.rb
@@ -88,9 +88,10 @@ module ActiveRecord
##
# :singleton-method:
# Specifies which database schemas to dump when calling db:structure:dump.
- # If :schema_search_path (the default), it will dumps any schemas listed in schema_search_path.
- # Use :all to always dumps all schemas regardless of the schema_search_path.
- # A string of comma separated schemas can also be used to pass a custom list of schemas.
+ # If the value is :schema_search_path (the default), any schemas listed in
+ # schema_search_path are dumped. Use :all to dump all schemas regardless
+ # of schema_search_path, or a string of comma separated schemas for a
+ # custom list.
mattr_accessor :dump_schemas, instance_writer: false
self.dump_schemas = :schema_search_path
@@ -161,11 +162,13 @@ module ActiveRecord
}
record = statement.execute([id], self, connection).first
unless record
- raise RecordNotFound, "Couldn't find #{name} with '#{primary_key}'=#{id}"
+ raise RecordNotFound.new("Couldn't find #{name} with '#{primary_key}'=#{id}",
+ name, primary_key, id)
end
record
rescue RangeError
- raise RecordNotFound, "Couldn't find #{name} with an out of range value for '#{primary_key}'"
+ raise RecordNotFound.new("Couldn't find #{name} with an out of range value for '#{primary_key}'",
+ name, primary_key)
end
def find_by(*args) # :nodoc:
@@ -198,7 +201,7 @@ module ActiveRecord
end
def find_by!(*args) # :nodoc:
- find_by(*args) or raise RecordNotFound.new("Couldn't find #{name}")
+ find_by(*args) or raise RecordNotFound.new("Couldn't find #{name}", name)
end
def initialize_generated_modules # :nodoc:
@@ -293,7 +296,7 @@ module ActiveRecord
# # Instantiates a single new object
# User.new(first_name: 'Jamie')
def initialize(attributes = nil)
- @attributes = self.class._default_attributes.dup
+ @attributes = self.class._default_attributes.deep_dup
self.class.define_attribute_methods
init_internals
@@ -302,7 +305,7 @@ module ActiveRecord
assign_attributes(attributes) if attributes
yield self if block_given?
- run_callbacks :initialize
+ _run_initialize_callbacks
end
# Initialize an empty model object from +coder+. +coder+ should be
@@ -329,8 +332,8 @@ module ActiveRecord
self.class.define_attribute_methods
- run_callbacks :find
- run_callbacks :initialize
+ _run_find_callbacks
+ _run_initialize_callbacks
self
end
@@ -363,10 +366,10 @@ module ActiveRecord
##
def initialize_dup(other) # :nodoc:
- @attributes = @attributes.dup
+ @attributes = @attributes.deep_dup
@attributes.reset(self.class.primary_key)
- run_callbacks(:initialize)
+ _run_initialize_callbacks
@new_record = true
@destroyed = false
diff --git a/activerecord/lib/active_record/enum.rb b/activerecord/lib/active_record/enum.rb
index ea88983917..10b5fcab24 100644
--- a/activerecord/lib/active_record/enum.rb
+++ b/activerecord/lib/active_record/enum.rb
@@ -18,10 +18,9 @@ module ActiveRecord
# conversation.archived? # => true
# conversation.status # => "archived"
#
- # # conversation.update! status: 1
+ # # conversation.status = 1
# conversation.status = "archived"
#
- # # conversation.update! status: nil
# conversation.status = nil
# conversation.status.nil? # => true
# conversation.status # => nil
@@ -32,7 +31,7 @@ module ActiveRecord
# Conversation.active
# Conversation.archived
#
- # Of course, you can also query them directly if the scopes doesn't fit your
+ # Of course, you can also query them directly if the scopes don't fit your
# needs:
#
# Conversation.where(status: [:active, :archived])
@@ -75,6 +74,24 @@ module ActiveRecord
#
# Conversation.where("status <> ?", Conversation.statuses[:archived])
#
+ # You can use the +:_prefix+ or +:_suffix+ options when you need to define
+ # multiple enums with same values. If the passed value is +true+, the methods
+ # are prefixed/suffixed with the name of the enum. It is also possible to
+ # supply a custom value:
+ #
+ # class Conversation < ActiveRecord::Base
+ # enum status: [:active, :archived], _suffix: true
+ # enum comments_status: [:active, :inactive], _prefix: :comments
+ # end
+ #
+ # With the above example, the bang and predicate methods along with the
+ # associated scopes are now prefixed and/or suffixed accordingly:
+ #
+ # conversation.active_status!
+ # conversation.archived_status? # => false
+ #
+ # conversation.comments_inactive!
+ # conversation.comments_active? # => false
module Enum
def self.extended(base) # :nodoc:
@@ -101,7 +118,7 @@ module ActiveRecord
elsif mapping.has_value?(value)
mapping.key(value)
else
- raise ArgumentError, "'#{value}' is not a valid #{name}"
+ assert_valid_value(value)
end
end
@@ -114,6 +131,12 @@ module ActiveRecord
mapping.fetch(value, value)
end
+ def assert_valid_value(value)
+ unless value.blank? || mapping.has_key?(value) || mapping.has_value?(value)
+ raise ArgumentError, "'#{value}' is not a valid #{name}"
+ end
+ end
+
protected
attr_reader :name, :mapping
@@ -121,6 +144,8 @@ module ActiveRecord
def enum(definitions)
klass = self
+ enum_prefix = definitions.delete(:_prefix)
+ enum_suffix = definitions.delete(:_suffix)
definitions.each do |name, values|
# statuses = { }
enum_values = ActiveSupport::HashWithIndifferentAccess.new
@@ -138,19 +163,31 @@ module ActiveRecord
_enum_methods_module.module_eval do
pairs = values.respond_to?(:each_pair) ? values.each_pair : values.each_with_index
pairs.each do |value, i|
+ if enum_prefix == true
+ prefix = "#{name}_"
+ elsif enum_prefix
+ prefix = "#{enum_prefix}_"
+ end
+ if enum_suffix == true
+ suffix = "_#{name}"
+ elsif enum_suffix
+ suffix = "_#{enum_suffix}"
+ end
+
+ value_method_name = "#{prefix}#{value}#{suffix}"
enum_values[value] = i
# def active?() status == 0 end
- klass.send(:detect_enum_conflict!, name, "#{value}?")
- define_method("#{value}?") { self[name] == value.to_s }
+ klass.send(:detect_enum_conflict!, name, "#{value_method_name}?")
+ define_method("#{value_method_name}?") { self[name] == value.to_s }
# def active!() update! status: :active end
- klass.send(:detect_enum_conflict!, name, "#{value}!")
- define_method("#{value}!") { update! name => value }
+ klass.send(:detect_enum_conflict!, name, "#{value_method_name}!")
+ define_method("#{value_method_name}!") { update! name => value }
# scope :active, -> { where status: 0 }
- klass.send(:detect_enum_conflict!, name, value, true)
- klass.scope value, -> { klass.where name => value }
+ klass.send(:detect_enum_conflict!, name, value_method_name, true)
+ klass.scope value_method_name, -> { klass.where name => value }
end
end
defined_enums[name.to_s] = enum_values
@@ -173,30 +210,22 @@ module ActiveRecord
def detect_enum_conflict!(enum_name, method_name, klass_method = false)
if klass_method && dangerous_class_method?(method_name)
- raise ArgumentError, ENUM_CONFLICT_MESSAGE % {
- enum: enum_name,
- klass: self.name,
- type: 'class',
- method: method_name,
- source: 'Active Record'
- }
+ raise_conflict_error(enum_name, method_name, type: 'class')
elsif !klass_method && dangerous_attribute_method?(method_name)
- raise ArgumentError, ENUM_CONFLICT_MESSAGE % {
- enum: enum_name,
- klass: self.name,
- type: 'instance',
- method: method_name,
- source: 'Active Record'
- }
+ raise_conflict_error(enum_name, method_name)
elsif !klass_method && method_defined_within?(method_name, _enum_methods_module, Module)
- raise ArgumentError, ENUM_CONFLICT_MESSAGE % {
- enum: enum_name,
- klass: self.name,
- type: 'instance',
- method: method_name,
- source: 'another enum'
- }
+ raise_conflict_error(enum_name, method_name, source: 'another enum')
end
end
+
+ def raise_conflict_error(enum_name, method_name, type: 'instance', source: 'Active Record')
+ raise ArgumentError, ENUM_CONFLICT_MESSAGE % {
+ enum: enum_name,
+ klass: self.name,
+ type: type,
+ method: method_name,
+ source: source
+ }
+ end
end
end
diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb
index 98aee77557..6721fe144f 100644
--- a/activerecord/lib/active_record/errors.rb
+++ b/activerecord/lib/active_record/errors.rb
@@ -47,6 +47,15 @@ module ActiveRecord
# Raised when Active Record cannot find record by given id or set of ids.
class RecordNotFound < ActiveRecordError
+ attr_reader :model, :primary_key, :id
+
+ def initialize(message = nil, model = nil, primary_key = nil, id = nil)
+ @primary_key = primary_key
+ @model = model
+ @id = id
+
+ super(message)
+ end
end
# Raised by ActiveRecord::Base.save! and ActiveRecord::Base.create! methods when record cannot be
@@ -54,7 +63,7 @@ module ActiveRecord
class RecordNotSaved < ActiveRecordError
attr_reader :record
- def initialize(message, record = nil)
+ def initialize(message = nil, record = nil)
@record = record
super(message)
end
@@ -71,9 +80,9 @@ module ActiveRecord
class RecordNotDestroyed < ActiveRecordError
attr_reader :record
- def initialize(record)
+ def initialize(message = nil, record = nil)
@record = record
- super()
+ super(message)
end
end
@@ -83,9 +92,9 @@ module ActiveRecord
class StatementInvalid < ActiveRecordError
attr_reader :original_exception
- def initialize(message, original_exception = nil)
- super(message)
+ def initialize(message = nil, original_exception = nil)
@original_exception = original_exception
+ super(message)
end
end
@@ -125,10 +134,14 @@ module ActiveRecord
class StaleObjectError < ActiveRecordError
attr_reader :record, :attempted_action
- def initialize(record, attempted_action)
- super("Attempted to #{attempted_action} a stale object: #{record.class.name}")
- @record = record
- @attempted_action = attempted_action
+ def initialize(record = nil, attempted_action = nil)
+ if record && attempted_action
+ @record = record
+ @attempted_action = attempted_action
+ super("Attempted to #{attempted_action} a stale object: #{record.class.name}.")
+ else
+ super("Stale object error.")
+ end
end
end
@@ -187,7 +200,7 @@ module ActiveRecord
class AttributeAssignmentError < ActiveRecordError
attr_reader :exception, :attribute
- def initialize(message, exception, attribute)
+ def initialize(message = nil, exception = nil, attribute = nil)
super(message)
@exception = exception
@attribute = attribute
@@ -200,7 +213,7 @@ module ActiveRecord
class MultiparameterAssignmentErrors < ActiveRecordError
attr_reader :errors
- def initialize(errors)
+ def initialize(errors = nil)
@errors = errors
end
end
@@ -209,11 +222,16 @@ module ActiveRecord
class UnknownPrimaryKey < ActiveRecordError
attr_reader :model
- def initialize(model)
- super("Unknown primary key for table #{model.table_name} in model #{model}.")
- @model = model
+ def initialize(model = nil, description = nil)
+ if model
+ message = "Unknown primary key for table #{model.table_name} in model #{model}."
+ message += "\n#{description}" if description
+ @model = model
+ super(message)
+ else
+ super("Unknown primary key.")
+ end
end
-
end
# Raised when a relation cannot be mutated because it's already loaded.
diff --git a/activerecord/lib/active_record/explain_subscriber.rb b/activerecord/lib/active_record/explain_subscriber.rb
index 6a49936644..9adabd7819 100644
--- a/activerecord/lib/active_record/explain_subscriber.rb
+++ b/activerecord/lib/active_record/explain_subscriber.rb
@@ -19,7 +19,7 @@ module ActiveRecord
# On the other hand, we want to monitor the performance of our real database
# queries, not the performance of the access to the query cache.
IGNORED_PAYLOADS = %w(SCHEMA EXPLAIN CACHE)
- EXPLAINED_SQLS = /\A\s*(select|update|delete|insert)\b/i
+ EXPLAINED_SQLS = /\A\s*(with|select|update|delete|insert)\b/i
def ignore_payload?(payload)
payload[:exception] || IGNORED_PAYLOADS.include?(payload[:name]) || payload[:sql] !~ EXPLAINED_SQLS
end
diff --git a/activerecord/lib/active_record/fixture_set/file.rb b/activerecord/lib/active_record/fixture_set/file.rb
index 8132310c91..f969556c50 100644
--- a/activerecord/lib/active_record/fixture_set/file.rb
+++ b/activerecord/lib/active_record/fixture_set/file.rb
@@ -17,24 +17,39 @@ module ActiveRecord
def initialize(file)
@file = file
- @rows = nil
end
def each(&block)
rows.each(&block)
end
+ def model_class
+ config_row['model_class']
+ end
private
def rows
- return @rows if @rows
+ @rows ||= raw_rows.reject { |fixture_name, _| fixture_name == '_fixture' }
+ end
+
+ def config_row
+ @config_row ||= begin
+ row = raw_rows.find { |fixture_name, _| fixture_name == '_fixture' }
+ if row
+ row.last
+ else
+ {'model_class': nil}
+ end
+ end
+ end
- begin
+ def raw_rows
+ @raw_rows ||= begin
data = YAML.load(render(IO.read(@file)))
+ data ? validate(data).to_a : []
rescue ArgumentError, Psych::SyntaxError => error
raise Fixture::FormatError, "a YAML error occurred parsing #{@file}. Please note that YAML must be consistently indented using spaces. Tabs are not allowed. Please have a look at http://www.yaml.org/faq.html\nThe exact error was:\n #{error.class}: #{error}", error.backtrace
end
- @rows = data ? validate(data).to_a : []
end
def render(content)
diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb
index 2c1771dd6c..59df3c78f3 100644
--- a/activerecord/lib/active_record/fixtures.rb
+++ b/activerecord/lib/active_record/fixtures.rb
@@ -110,7 +110,7 @@ module ActiveRecord
# <% 1.upto(1000) do |i| %>
# fix_<%= i %>:
# id: <%= i %>
- # name: guy_<%= 1 %>
+ # name: guy_<%= i %>
# <% end %>
#
# This will create 1000 very simple fixtures.
@@ -395,6 +395,20 @@ module ActiveRecord
# <<: *DEFAULTS
#
# Any fixture labeled "DEFAULTS" is safely ignored.
+ #
+ # == Configure the fixture model class
+ #
+ # It's possible to set the fixture's model class directly in the YAML file.
+ # This is helpful when fixtures are loaded outside tests and
+ # +set_fixture_class+ is not available (e.g.
+ # when running <tt>rake db:fixtures:load</tt>).
+ #
+ # _fixture:
+ # model_class: User
+ # david:
+ # name: David
+ #
+ # Any fixtures labeled "_fixture" are safely ignored.
class FixtureSet
#--
# An instance of FixtureSet is normally stored in a single YAML file and
@@ -578,21 +592,16 @@ module ActiveRecord
@name = name
@path = path
@config = config
- @model_class = nil
- if class_name.is_a?(Class) # TODO: Should be an AR::Base type class, or any?
- @model_class = class_name
- else
- @model_class = class_name.safe_constantize if class_name
- end
+ self.model_class = class_name
+
+ @fixtures = read_fixture_files(path)
@connection = connection
@table_name = ( model_class.respond_to?(:table_name) ?
model_class.table_name :
self.class.default_fixture_table_name(name, config) )
-
- @fixtures = read_fixture_files path, @model_class
end
def [](x)
@@ -615,7 +624,6 @@ module ActiveRecord
# a list of rows to insert to that table.
def table_rows
now = config.default_timezone == :utc ? Time.now.utc : Time.now
- now = now.to_s(:db)
# allow a standard key to be used for doing defaults in YAML
fixtures.delete('DEFAULTS')
@@ -644,6 +652,13 @@ module ActiveRecord
row[primary_key_name] = ActiveRecord::FixtureSet.identify(label, primary_key_type)
end
+ # Resolve enums
+ model_class.defined_enums.each do |name, values|
+ if row.include?(name)
+ row[name] = values.fetch(row[name], row[name])
+ end
+ end
+
# If STI is used, find the correct subclass for association reflection
reflection_class =
if row.include?(inheritance_column_name)
@@ -664,7 +679,7 @@ module ActiveRecord
row[association.foreign_type] = $1
end
- fk_type = association.active_record.type_for_attribute(fk_name).type
+ fk_type = reflection_class.type_for_attribute(fk_name).type
row[fk_name] = ActiveRecord::FixtureSet.identify(value, fk_type)
end
when :has_many
@@ -755,13 +770,25 @@ module ActiveRecord
@column_names ||= @connection.columns(@table_name).collect(&:name)
end
- def read_fixture_files(path, model_class)
+ def model_class=(class_name)
+ if class_name.is_a?(Class) # TODO: Should be an AR::Base type class, or any?
+ @model_class = class_name
+ else
+ @model_class = class_name.safe_constantize if class_name
+ end
+ end
+
+ # Loads the fixtures from the YAML file at +path+.
+ # If the file sets the +model_class+ and current instance value is not set,
+ # it uses the file value.
+ def read_fixture_files(path)
yaml_files = Dir["#{path}/{**,*}/*.yml"].select { |f|
::File.file?(f)
} + [yaml_file_path(path)]
yaml_files.each_with_object({}) do |file, fixtures|
FixtureSet::File.open(file) do |fh|
+ self.model_class ||= fh.model_class if fh.model_class
fh.each do |fixture_name, row|
fixtures[fixture_name] = ActiveRecord::Fixture.new(row, model_class)
end
@@ -821,12 +848,12 @@ module ActiveRecord
module TestFixtures
extend ActiveSupport::Concern
- def before_setup
+ def before_setup # :nodoc:
setup_fixtures
super
end
- def after_teardown
+ def after_teardown # :nodoc:
super
teardown_fixtures
end
diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb
index 24098f72dc..c26842014d 100644
--- a/activerecord/lib/active_record/inheritance.rb
+++ b/activerecord/lib/active_record/inheritance.rb
@@ -82,7 +82,7 @@ module ActiveRecord
# Returns the class descending directly from ActiveRecord::Base, or
# an abstract class, if any, in the inheritance hierarchy.
#
- # If A extends AR::Base, A.base_class will return A. If B descends from A
+ # If A extends ActiveRecord::Base, A.base_class will return A. If B descends from A
# through some arbitrarily deep hierarchy, B.base_class will return A.
#
# If B < A and C < B and if A is an abstract_class then both B.base_class
@@ -198,14 +198,16 @@ module ActiveRecord
def subclass_from_attributes(attrs)
subclass_name = attrs.with_indifferent_access[inheritance_column]
- if subclass_name.present? && subclass_name != self.name
- subclass = subclass_name.safe_constantize
+ if subclass_name.present?
+ subclass = find_sti_class(subclass_name)
- unless descendants.include?(subclass)
- raise ActiveRecord::SubclassNotFound.new("Invalid single-table inheritance type: #{subclass_name} is not a subclass of #{name}")
- end
+ if subclass.name != self.name
+ unless descendants.include?(subclass)
+ raise ActiveRecord::SubclassNotFound.new("Invalid single-table inheritance type: #{subclass.name} is not a subclass of #{name}")
+ end
- subclass
+ subclass
+ end
end
end
end
diff --git a/activerecord/lib/active_record/integration.rb b/activerecord/lib/active_record/integration.rb
index 15b2f65dcb..370c1562c9 100644
--- a/activerecord/lib/active_record/integration.rb
+++ b/activerecord/lib/active_record/integration.rb
@@ -84,7 +84,7 @@ module ActiveRecord
# Values longer than 20 characters will be truncated. The value
# is truncated word by word.
#
- # user = User.find_by(name: 'David HeinemeierHansson')
+ # user = User.find_by(name: 'David Heinemeier Hansson')
# user.id # => 125
# user_path(user) # => "/users/125-david"
#
diff --git a/activerecord/lib/active_record/locale/en.yml b/activerecord/lib/active_record/locale/en.yml
index 8a3c27e6da..0b35027b2b 100644
--- a/activerecord/lib/active_record/locale/en.yml
+++ b/activerecord/lib/active_record/locale/en.yml
@@ -16,8 +16,8 @@ en:
messages:
record_invalid: "Validation failed: %{errors}"
restrict_dependent_destroy:
- one: "Cannot delete record because a dependent %{record} exists"
- many: "Cannot delete record because dependent %{record} exist"
+ has_one: "Cannot delete record because a dependent %{record} exists"
+ has_many: "Cannot delete record because dependent %{record} exist"
# Append your own errors here or at the model/attributes scope.
# You can define own errors for models or model attributes.
diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb
index a09437b4b0..2336d23a1c 100644
--- a/activerecord/lib/active_record/locking/optimistic.rb
+++ b/activerecord/lib/active_record/locking/optimistic.rb
@@ -22,7 +22,7 @@ module ActiveRecord
# p1.save
#
# p2.first_name = "should fail"
- # p2.save # Raises a ActiveRecord::StaleObjectError
+ # p2.save # Raises an ActiveRecord::StaleObjectError
#
# Optimistic locking will also check for stale data when objects are destroyed. Example:
#
@@ -32,7 +32,7 @@ module ActiveRecord
# p1.first_name = "Michael"
# p1.save
#
- # p2.destroy # Raises a ActiveRecord::StaleObjectError
+ # p2.destroy # Raises an ActiveRecord::StaleObjectError
#
# You're then responsible for dealing with the conflict by rescuing the exception and either rolling back, merging,
# or otherwise apply the business logic needed to resolve the conflict.
diff --git a/activerecord/lib/active_record/locking/pessimistic.rb b/activerecord/lib/active_record/locking/pessimistic.rb
index 3d95c54ef3..8ecdf76b72 100644
--- a/activerecord/lib/active_record/locking/pessimistic.rb
+++ b/activerecord/lib/active_record/locking/pessimistic.rb
@@ -51,7 +51,7 @@ module ActiveRecord
# end
#
# Database-specific information on row locking:
- # MySQL: http://dev.mysql.com/doc/refman/5.6/en/innodb-locking-reads.html
+ # MySQL: http://dev.mysql.com/doc/refman/5.7/en/innodb-locking-reads.html
# PostgreSQL: http://www.postgresql.org/docs/current/interactive/sql-select.html#SQL-FOR-UPDATE-SHARE
module Pessimistic
# Obtain a row lock on this record. Reloads the record to obtain the requested
diff --git a/activerecord/lib/active_record/log_subscriber.rb b/activerecord/lib/active_record/log_subscriber.rb
index af816a278e..b63caa4473 100644
--- a/activerecord/lib/active_record/log_subscriber.rb
+++ b/activerecord/lib/active_record/log_subscriber.rb
@@ -47,18 +47,41 @@ module ActiveRecord
binds = " " + payload[:binds].map { |attr| render_bind(attr) }.inspect
end
- if odd?
- name = color(name, CYAN, true)
- sql = color(sql, nil, true)
- else
- name = color(name, MAGENTA, true)
- end
+ name = colorize_payload_name(name, payload[:name])
+ sql = color(sql, sql_color(sql), true)
debug " #{name} #{sql}#{binds}"
end
- def odd?
- @odd = !@odd
+ private
+
+ def colorize_payload_name(name, payload_name)
+ if payload_name.blank? || payload_name == "SQL" # SQL vs Model Load/Exists
+ color(name, MAGENTA, true)
+ else
+ color(name, CYAN, true)
+ end
+ end
+
+ def sql_color(sql)
+ case sql
+ when /\A\s*rollback/mi
+ RED
+ when /\s*.*?select .*for update/mi, /\A\s*lock/mi
+ WHITE
+ when /\A\s*select/i
+ BLUE
+ when /\A\s*insert/i
+ GREEN
+ when /\A\s*update/i
+ YELLOW
+ when /\A\s*delete/i
+ RED
+ when /transaction\s*\Z/i
+ CYAN
+ else
+ MAGENTA
+ end
end
def logger
diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb
index a83b90a95f..90b8a29869 100644
--- a/activerecord/lib/active_record/migration.rb
+++ b/activerecord/lib/active_record/migration.rb
@@ -9,40 +9,128 @@ module ActiveRecord
end
end
- # Exception that can be raised to stop migrations from going backwards.
+ # Exception that can be raised to stop migrations from being rolled back.
+ # For example the following migration is not reversible.
+ # Rolling back this migration will raise an ActiveRecord::IrreversibleMigration error.
+ #
+ # class IrreversibleMigrationExample < ActiveRecord::Migration
+ # def change
+ # create_table :distributors do |t|
+ # t.string :zipcode
+ # end
+ #
+ # execute <<-SQL
+ # ALTER TABLE distributors
+ # ADD CONSTRAINT zipchk
+ # CHECK (char_length(zipcode) = 5) NO INHERIT;
+ # SQL
+ # end
+ # end
+ #
+ # There are two ways to mitigate this problem.
+ #
+ # 1. Define <tt>#up</tt> and <tt>#down</tt> methods instead of <tt>#change</tt>:
+ #
+ # class ReversibleMigrationExample < ActiveRecord::Migration
+ # def up
+ # create_table :distributors do |t|
+ # t.string :zipcode
+ # end
+ #
+ # execute <<-SQL
+ # ALTER TABLE distributors
+ # ADD CONSTRAINT zipchk
+ # CHECK (char_length(zipcode) = 5) NO INHERIT;
+ # SQL
+ # end
+ #
+ # def down
+ # execute <<-SQL
+ # ALTER TABLE distributors
+ # DROP CONSTRAINT zipchk
+ # SQL
+ #
+ # drop_table :distributors
+ # end
+ # end
+ #
+ # 2. Use the #reversible method in <tt>#change</tt> method:
+ #
+ # class ReversibleMigrationExample < ActiveRecord::Migration
+ # def change
+ # create_table :distributors do |t|
+ # t.string :zipcode
+ # end
+ #
+ # reversible do |dir|
+ # dir.up do
+ # execute <<-SQL
+ # ALTER TABLE distributors
+ # ADD CONSTRAINT zipchk
+ # CHECK (char_length(zipcode) = 5) NO INHERIT;
+ # SQL
+ # end
+ #
+ # dir.down do
+ # execute <<-SQL
+ # ALTER TABLE distributors
+ # DROP CONSTRAINT zipchk
+ # SQL
+ # end
+ # end
+ # end
+ # end
class IrreversibleMigration < MigrationError
end
class DuplicateMigrationVersionError < MigrationError#:nodoc:
- def initialize(version)
- super("Multiple migrations have the version number #{version}")
+ def initialize(version = nil)
+ if version
+ super("Multiple migrations have the version number #{version}.")
+ else
+ super("Duplicate migration version error.")
+ end
end
end
class DuplicateMigrationNameError < MigrationError#:nodoc:
- def initialize(name)
- super("Multiple migrations have the name #{name}")
+ def initialize(name = nil)
+ if name
+ super("Multiple migrations have the name #{name}.")
+ else
+ super("Duplicate migration name.")
+ end
end
end
class UnknownMigrationVersionError < MigrationError #:nodoc:
- def initialize(version)
- super("No migration with version number #{version}")
+ def initialize(version = nil)
+ if version
+ super("No migration with version number #{version}.")
+ else
+ super("Unknown migration version.")
+ end
end
end
class IllegalMigrationNameError < MigrationError#:nodoc:
- def initialize(name)
- super("Illegal name for migration file: #{name}\n\t(only lower case letters, numbers, and '_' allowed)")
+ def initialize(name = nil)
+ if name
+ super("Illegal name for migration file: #{name}\n\t(only lower case letters, numbers, and '_' allowed).")
+ else
+ super("Illegal name for migration.")
+ end
end
end
class PendingMigrationError < MigrationError#:nodoc:
- def initialize
- if defined?(Rails.env)
- super("Migrations are pending. To resolve this issue, run:\n\n\tbin/rake db:migrate RAILS_ENV=#{::Rails.env}")
+ def initialize(message = nil)
+ if !message && defined?(Rails.env)
+ super("Migrations are pending. To resolve this issue, run:\n\n\tbin/rake db:migrate RAILS_ENV=#{::Rails.env}.")
+ elsif !message
+ super("Migrations are pending. To resolve this issue, run:\n\n\tbin/rake db:migrate.")
else
- super("Migrations are pending. To resolve this issue, run:\n\n\tbin/rake db:migrate")
+ super
end
end
end
@@ -138,10 +226,13 @@ module ActiveRecord
# <tt>:name</tt>, <tt>:unique</tt> (e.g.
# <tt>{ name: 'users_name_index', unique: true }</tt>) and <tt>:order</tt>
# (e.g. <tt>{ order: { name: :desc } }</tt>).
- # * <tt>remove_index(table_name, column: column_name)</tt>: Removes the index
- # specified by +column_name+.
+ # * <tt>remove_index(table_name, column: column_names)</tt>: Removes the index
+ # specified by +column_names+.
# * <tt>remove_index(table_name, name: index_name)</tt>: Removes the index
# specified by +index_name+.
+ # * <tt>add_reference(:table_name, :reference_name)</tt>: Adds a new column
+ # +reference_name_id+ by default an integer. See
+ # ActiveRecord::ConnectionAdapters::SchemaStatements#add_reference for details.
#
# == Irreversible transformations
#
@@ -165,7 +256,7 @@ module ActiveRecord
#
# rails generate migration add_fieldname_to_tablename fieldname:string
#
- # This will generate the file <tt>timestamp_add_fieldname_to_tablename</tt>, which will look like this:
+ # This will generate the file <tt>timestamp_add_fieldname_to_tablename.rb</tt>, which will look like this:
# class AddFieldnameToTablename < ActiveRecord::Migration
# def change
# add_column :tablenames, :fieldname, :string
@@ -275,21 +366,6 @@ module ActiveRecord
# The phrase "Updating salaries..." would then be printed, along with the
# benchmark for the block when the block completes.
#
- # == About the schema_migrations table
- #
- # Rails versions 2.0 and prior used to create a table called
- # <tt>schema_info</tt> when using migrations. This table contained the
- # version of the schema as of the last applied migration.
- #
- # Starting with Rails 2.1, the <tt>schema_info</tt> table is
- # (automatically) replaced by the <tt>schema_migrations</tt> table, which
- # contains the version numbers of all the migrations applied.
- #
- # As a result, it is now possible to add migration files that are numbered
- # lower than the current schema version: when migrating up, those
- # never-applied "interleaved" migrations will be automatically applied, and
- # when migrating down, never-applied "interleaved" migrations will be skipped.
- #
# == Timestamped Migrations
#
# By default, Rails generates migrations that look like:
@@ -307,9 +383,8 @@ module ActiveRecord
#
# == Reversible Migrations
#
- # Starting with Rails 3.1, you will be able to define reversible migrations.
# Reversible migrations are migrations that know how to go +down+ for you.
- # You simply supply the +up+ logic, and the Migration system will figure out
+ # You simply supply the +up+ logic, and the Migration system figures out
# how to execute the down commands for you.
#
# To define a reversible migration, define the +change+ method in your
@@ -389,6 +464,7 @@ module ActiveRecord
attr_accessor :delegate # :nodoc:
attr_accessor :disable_ddl_transaction # :nodoc:
+ # Raises <tt>ActiveRecord::PendingMigrationError</tt> error if any migrations are pending.
def check_pending!(connection = Base.connection)
raise ActiveRecord::PendingMigrationError if ActiveRecord::Migrator.needs_migration?(connection)
end
@@ -421,7 +497,10 @@ module ActiveRecord
new.migrate direction
end
- # Disable DDL transactions for this migration.
+ # Disable the transaction wrapping this migration.
+ # You can still create your own transactions even after calling #disable_ddl_transaction!
+ #
+ # For more details read the {"Transactional Migrations" section above}[rdoc-ref:Migration].
def disable_ddl_transaction!
@disable_ddl_transaction = true
end
@@ -468,7 +547,7 @@ module ActiveRecord
# Or equivalently, if +TenderloveMigration+ is defined as in the
# documentation for Migration:
#
- # require_relative '2012121212_tenderlove_migration'
+ # require_relative '20121212123456_tenderlove_migration'
#
# class FixupTLMigration < ActiveRecord::Migration
# def change
@@ -484,13 +563,13 @@ module ActiveRecord
def revert(*migration_classes)
run(*migration_classes.reverse, revert: true) unless migration_classes.empty?
if block_given?
- if @connection.respond_to? :revert
- @connection.revert { yield }
+ if connection.respond_to? :revert
+ connection.revert { yield }
else
- recorder = CommandRecorder.new(@connection)
+ recorder = CommandRecorder.new(connection)
@connection = recorder
suppress_messages do
- @connection.revert { yield }
+ connection.revert { yield }
end
@connection = recorder.delegate
recorder.commands.each do |cmd, args, block|
@@ -501,7 +580,7 @@ module ActiveRecord
end
def reverting?
- @connection.respond_to?(:reverting) && @connection.reverting
+ connection.respond_to?(:reverting) && connection.reverting
end
class ReversibleBlockHelper < Struct.new(:reverting) # :nodoc:
@@ -558,7 +637,7 @@ module ActiveRecord
revert { run(*migration_classes, direction: dir, revert: true) }
else
migration_classes.each do |migration_class|
- migration_class.new.exec_migration(@connection, dir)
+ migration_class.new.exec_migration(connection, dir)
end
end
end
@@ -650,10 +729,11 @@ module ActiveRecord
arg_list = arguments.map(&:inspect) * ', '
say_with_time "#{method}(#{arg_list})" do
- unless @connection.respond_to? :revert
+ 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)
- if [:rename_table, :add_foreign_key].include?(method)
+ if [:rename_table, :add_foreign_key].include?(method) ||
+ (method == :remove_foreign_key && !arguments.second.is_a?(Hash))
arguments[1] = proper_table_name(arguments.second, table_name_options)
end
end
@@ -728,7 +808,9 @@ module ActiveRecord
end
end
- def table_name_options(config = ActiveRecord::Base)
+ # Builds a hash for use in ActiveRecord::Migration#proper_table_name using
+ # the Active Record object's table_name prefix and suffix
+ def table_name_options(config = ActiveRecord::Base) #:nodoc:
{
table_name_prefix: config.table_name_prefix,
table_name_suffix: config.table_name_suffix
@@ -820,7 +902,7 @@ module ActiveRecord
new(:up, migrations, target_version).migrate
end
- def down(migrations_paths, target_version = nil, &block)
+ def down(migrations_paths, target_version = nil)
migrations = migrations(migrations_paths)
migrations.select! { |m| yield m } if block_given?
@@ -859,24 +941,16 @@ module ActiveRecord
migrations(migrations_paths).any?
end
- def last_version
- last_migration.version
- end
-
def last_migration #:nodoc:
migrations(migrations_paths).last || NullMigration.new
end
def migrations_paths
@migrations_paths ||= ['db/migrate']
- # just to not break things if someone uses: migration_path = some_string
+ # just to not break things if someone uses: migrations_path = some_string
Array(@migrations_paths)
end
- def migrations_path
- migrations_paths.first
- end
-
def migrations(paths)
paths = Array(paths)
diff --git a/activerecord/lib/active_record/migration/command_recorder.rb b/activerecord/lib/active_record/migration/command_recorder.rb
index 36256415df..0fa665c7e0 100644
--- a/activerecord/lib/active_record/migration/command_recorder.rb
+++ b/activerecord/lib/active_record/migration/command_recorder.rb
@@ -5,15 +5,36 @@ module ActiveRecord
# knows how to invert the following commands:
#
# * add_column
+ # * add_foreign_key
# * add_index
+ # * add_reference
# * add_timestamps
- # * create_table
+ # * change_column
+ # * change_column_default (must supply a :from and :to option)
+ # * change_column_null
# * create_join_table
+ # * create_table
+ # * disable_extension
+ # * drop_join_table
+ # * drop_table (must supply a block)
+ # * enable_extension
+ # * remove_column (must supply a type)
+ # * remove_columns (must specify at least one column name or more)
+ # * remove_foreign_key (must supply a second table)
+ # * remove_index
+ # * remove_reference
# * remove_timestamps
# * rename_column
# * rename_index
# * rename_table
class CommandRecorder
+ ReversibleAndIrreversibleMethods = [:create_table, :create_join_table, :rename_table, :add_column, :remove_column,
+ :rename_index, :rename_column, :add_index, :remove_index, :add_timestamps, :remove_timestamps,
+ :change_column_default, :add_reference, :remove_reference, :transaction,
+ :drop_join_table, :drop_table, :execute_block, :enable_extension, :disable_extension,
+ :change_column, :execute, :remove_columns, :change_column_null,
+ :add_foreign_key, :remove_foreign_key
+ ]
include JoinTable
attr_accessor :commands, :delegate, :reverting
@@ -41,7 +62,7 @@ module ActiveRecord
@reverting = !@reverting
end
- # record +command+. +command+ should be a method name and arguments.
+ # Record +command+. +command+ should be a method name and arguments.
# For example:
#
# recorder.record(:method_name, [:arg1, :arg2])
@@ -62,7 +83,12 @@ module ActiveRecord
# invert the +command+.
def inverse_of(command, args, &block)
method = :"invert_#{command}"
- raise IrreversibleMigration unless respond_to?(method, true)
+ raise IrreversibleMigration, <<-MSG.strip_heredoc unless respond_to?(method, true)
+ This migration uses #{command}, which is not automatically reversible.
+ To make the migration reversible you can either:
+ 1. Define #up and #down methods in place of the #change method.
+ 2. Use the #reversible method to define reversible behavior.
+ MSG
send(method, args, &block)
end
@@ -70,14 +96,7 @@ module ActiveRecord
super || delegate.respond_to?(*args)
end
- [:create_table, :create_join_table, :rename_table, :add_column, :remove_column,
- :rename_index, :rename_column, :add_index, :remove_index, :add_timestamps, :remove_timestamps,
- :change_column_default, :add_reference, :remove_reference, :transaction,
- :drop_join_table, :drop_table, :execute_block, :enable_extension,
- :change_column, :execute, :remove_columns, :change_column_null,
- :add_foreign_key, :remove_foreign_key
- # irreversible methods need to be here too
- ].each do |method|
+ ReversibleAndIrreversibleMethods.each do |method|
class_eval <<-EOV, __FILE__, __LINE__ + 1
def #{method}(*args, &block) # def create_table(*args, &block)
record(:"#{method}", args, &block) # record(:create_table, args, &block)
@@ -151,19 +170,31 @@ module ActiveRecord
end
def invert_remove_index(args)
- table, options = *args
-
- unless options && options.is_a?(Hash) && options[:column]
- raise ActiveRecord::IrreversibleMigration, "remove_index is only reversible if given a :column option."
+ table, options_or_column = *args
+ if (options = options_or_column).is_a?(Hash)
+ unless options[:column]
+ raise ActiveRecord::IrreversibleMigration, "remove_index is only reversible if given a :column option."
+ end
+ options = options.dup
+ [:add_index, [table, options.delete(:column), options]]
+ elsif (column = options_or_column).present?
+ [:add_index, [table, column]]
end
-
- options = options.dup
- [:add_index, [table, options.delete(:column), options]]
end
alias :invert_add_belongs_to :invert_add_reference
alias :invert_remove_belongs_to :invert_remove_reference
+ def invert_change_column_default(args)
+ table, column, options = *args
+
+ unless options && options.is_a?(Hash) && options.has_key?(:from) && options.has_key?(:to)
+ raise ActiveRecord::IrreversibleMigration, "change_column_default is only reversible if given a :from and :to option."
+ end
+
+ [:change_column_default, [table, column, from: options[:to], to: options[:from]]]
+ end
+
def invert_change_column_null(args)
args[2] = !args[2]
[:change_column_null, args]
@@ -184,6 +215,16 @@ module ActiveRecord
[:remove_foreign_key, [from_table, options]]
end
+ def invert_remove_foreign_key(args)
+ from_table, to_table, remove_options = args
+ raise ActiveRecord::IrreversibleMigration, "remove_foreign_key is only reversible if given a second table" if to_table.nil? || to_table.is_a?(Hash)
+
+ reversed_args = [from_table, to_table]
+ reversed_args << remove_options if remove_options
+
+ [:add_foreign_key, reversed_args]
+ 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 3674f672cb..a9bd094a66 100644
--- a/activerecord/lib/active_record/model_schema.rb
+++ b/activerecord/lib/active_record/model_schema.rb
@@ -50,6 +50,13 @@ module ActiveRecord
class_attribute :pluralize_table_names, instance_writer: false
self.pluralize_table_names = true
+ ##
+ # :singleton-method:
+ # Accessor for the list of columns names the model should ignore. Ignored columns won't have attribute
+ # accessors defined, and won't be referenced in SQL queries.
+ class_attribute :ignored_columns, instance_accessor: false
+ self.ignored_columns = [].freeze
+
self.inheritance_column = 'type'
delegate :type_for_attribute, to: :class
@@ -213,7 +220,7 @@ module ActiveRecord
# Indicates whether the table associated with this class exists
def table_exists?
- connection.schema_cache.table_exists?(table_name)
+ connection.schema_cache.data_source_exists?(table_name)
end
def attributes_builder # :nodoc:
@@ -240,7 +247,7 @@ module ActiveRecord
end
# Returns a hash where the keys are column names and the values are
- # default values when instantiating the AR object for this table.
+ # default values when instantiating the Active Record object for this table.
def column_defaults
load_schema
_default_attributes.to_hash
@@ -290,7 +297,7 @@ module ActiveRecord
def reset_column_information
connection.clear_cache!
undefine_attribute_methods
- connection.schema_cache.clear_table_cache!(table_name)
+ connection.schema_cache.clear_data_source_cache!(table_name)
reload_schema_from_cache
end
@@ -308,8 +315,9 @@ module ActiveRecord
end
def load_schema!
- @columns_hash = connection.schema_cache.columns_hash(table_name)
+ @columns_hash = connection.schema_cache.columns_hash(table_name).except(*ignored_columns)
@columns_hash.each do |name, column|
+ warn_if_deprecated_type(column)
define_attribute(
name,
connection.lookup_cast_type_from_column(column),
@@ -356,6 +364,28 @@ module ActiveRecord
base.table_name
end
end
+
+ def warn_if_deprecated_type(column)
+ return if attributes_to_define_after_schema_loads.key?(column.name)
+ if column.respond_to?(:oid) && column.sql_type.start_with?("point")
+ if column.array?
+ array_arguments = ", array: true"
+ else
+ array_arguments = ""
+ end
+ ActiveSupport::Deprecation.warn(<<-WARNING.strip_heredoc)
+ The behavior of the `:point` type will be changing in Rails 5.1 to
+ return a `Point` object, rather than an `Array`. If you'd like to
+ keep the old behavior, you can add this line to #{self.name}:
+
+ attribute :#{column.name}, :legacy_point#{array_arguments}
+
+ If you'd like the new behavior today, you can add this line:
+
+ attribute :#{column.name}, :rails_5_1_point#{array_arguments}
+ WARNING
+ end
+ end
end
end
end
diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb
index 084ef397a8..c5a1488588 100644
--- a/activerecord/lib/active_record/nested_attributes.rb
+++ b/activerecord/lib/active_record/nested_attributes.rb
@@ -147,8 +147,8 @@ module ActiveRecord
# has_many :posts
# accepts_nested_attributes_for :posts, reject_if: :reject_posts
#
- # def reject_posts(attributed)
- # attributed['title'].blank?
+ # def reject_posts(attributes)
+ # attributes['title'].blank?
# end
# end
#
@@ -166,6 +166,11 @@ module ActiveRecord
# member.posts.first.title # => '[UPDATED] An, as of yet, undisclosed awesome Ruby documentation browser!'
# member.posts.second.title # => '[UPDATED] other post'
#
+ # However, the above applies if the parent model is being updated as well.
+ # For example, If you wanted to create a +member+ named _joe_ and wanted to
+ # update the +posts+ at the same time, that would give an
+ # ActiveRecord::RecordNotFound error.
+ #
# By default the associated records are protected from being destroyed. If
# you want to destroy any of the associated records through the attributes
# hash, you have to enable it first using the <tt>:allow_destroy</tt>
@@ -208,7 +213,7 @@ module ActiveRecord
#
# Passing attributes for an associated collection in the form of a hash
# of hashes can be used with hashes generated from HTTP/HTML parameters,
- # where there maybe no natural way to submit an array of hashes.
+ # where there may be no natural way to submit an array of hashes.
#
# === Saving
#
@@ -381,6 +386,9 @@ module ActiveRecord
# then the existing record will be marked for destruction.
def assign_nested_attributes_for_one_to_one_association(association_name, attributes)
options = self.nested_attributes_options[association_name]
+ if attributes.respond_to?(:permitted?)
+ attributes = attributes.to_h
+ end
attributes = attributes.with_indifferent_access
existing_record = send(association_name)
@@ -437,6 +445,9 @@ module ActiveRecord
# ])
def assign_nested_attributes_for_collection_association(association_name, attributes_collection)
options = self.nested_attributes_options[association_name]
+ if attributes_collection.respond_to?(:permitted?)
+ attributes_collection = attributes_collection.to_h
+ end
unless attributes_collection.is_a?(Hash) || attributes_collection.is_a?(Array)
raise ArgumentError, "Hash or Array expected, got #{attributes_collection.class.name} (#{attributes_collection.inspect})"
@@ -463,6 +474,9 @@ module ActiveRecord
end
attributes_collection.each do |attributes|
+ if attributes.respond_to?(:permitted?)
+ attributes = attributes.to_h
+ end
attributes = attributes.with_indifferent_access
if attributes['id'].blank?
@@ -547,7 +561,9 @@ module ActiveRecord
end
def raise_nested_attributes_record_not_found!(association_name, record_id)
- raise RecordNotFound, "Couldn't find #{self.class._reflect_on_association(association_name).klass.name} with ID=#{record_id} for #{self.class.name} with ID=#{id}"
+ model = self.class._reflect_on_association(association_name).klass.name
+ raise RecordNotFound.new("Couldn't find #{model} with ID=#{record_id} for #{self.class.name} with ID=#{id}",
+ model, 'id', record_id)
end
end
end
diff --git a/activerecord/lib/active_record/null_relation.rb b/activerecord/lib/active_record/null_relation.rb
index 74894d0c37..0b500346bc 100644
--- a/activerecord/lib/active_record/null_relation.rb
+++ b/activerecord/lib/active_record/null_relation.rb
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-
module ActiveRecord
module NullRelation # :nodoc:
def exec_queries
diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb
index ae1c326d95..3f02f73a5a 100644
--- a/activerecord/lib/active_record/persistence.rb
+++ b/activerecord/lib/active_record/persistence.rb
@@ -148,7 +148,7 @@ module ActiveRecord
# Attributes marked as readonly are silently ignored if the record is
# being updated.
def save!(*args)
- create_or_update(*args) || raise(RecordNotSaved.new(nil, self))
+ create_or_update(*args) || raise(RecordNotSaved.new("Failed to save the record", self))
end
# Deletes the record in the database and freezes this instance to
@@ -193,7 +193,7 @@ module ActiveRecord
# and #destroy! raises ActiveRecord::RecordNotDestroyed.
# See ActiveRecord::Callbacks for further details.
def destroy!
- destroy || raise(ActiveRecord::RecordNotDestroyed, self)
+ destroy || _raise_record_not_destroyed
end
# Returns an instance of the specified +klass+ with the attributes of the
@@ -205,12 +205,14 @@ module ActiveRecord
# instance using the companies/company partial instead of clients/client.
#
# Note: The new instance will share a link to the same attributes as the original class.
- # So any change to the attributes in either instance will affect the other.
+ # Therefore the sti column value will still be the same.
+ # Any change to the attributes on either instance will affect both instances.
+ # If you want to change the sti column as well, use +becomes!+ instead.
def becomes(klass)
became = klass.new
became.instance_variable_set("@attributes", @attributes)
- changed_attributes = @changed_attributes if defined?(@changed_attributes)
- became.instance_variable_set("@changed_attributes", changed_attributes || {})
+ became.instance_variable_set("@mutation_tracker", @mutation_tracker) if defined?(@mutation_tracker)
+ became.instance_variable_set("@changed_attributes", attributes_changed_by_setter)
became.instance_variable_set("@new_record", new_record?)
became.instance_variable_set("@destroyed", destroyed?)
became.instance_variable_set("@errors", errors)
@@ -380,7 +382,7 @@ module ActiveRecord
# # => #<Account id: 1, email: 'account@example.com'>
#
# Attributes are reloaded from the database, and caches busted, in
- # particular the associations cache.
+ # particular the associations cache and the QueryCache.
#
# If the record no longer exists in the database <tt>ActiveRecord::RecordNotFound</tt>
# is raised. Otherwise, in addition to the in-place modification the method
@@ -416,6 +418,8 @@ module ActiveRecord
# end
#
def reload(options = nil)
+ self.class.connection.clear_query_cache
+
fresh_object =
if options && options[:lock]
self.class.unscoped { self.class.lock(options[:lock]).find(id) }
@@ -428,7 +432,7 @@ module ActiveRecord
self
end
- # Saves the record with the updated_at/on attributes set to the current time
+ # Saves the record with the updated_at/on attributes set to the current time
# or the time specified.
# Please note that no validation is performed and only the +after_touch+,
# +after_commit+ and +after_rollback+ callbacks are executed.
@@ -545,5 +549,12 @@ module ActiveRecord
def verify_readonly_attribute(name)
raise ActiveRecordError, "#{name} is marked as readonly" if self.class.readonly_attributes.include?(name)
end
+
+ def _raise_record_not_destroyed
+ @_association_destroy_exception ||= nil
+ raise @_association_destroy_exception || RecordNotDestroyed.new("Failed to destroy the record", self)
+ ensure
+ @_association_destroy_exception = nil
+ end
end
end
diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb
index 4e597590e9..87a1988f2f 100644
--- a/activerecord/lib/active_record/querying.rb
+++ b/activerecord/lib/active_record/querying.rb
@@ -6,7 +6,7 @@ module ActiveRecord
delegate :find_or_create_by, :find_or_create_by!, :find_or_initialize_by, to: :all
delegate :find_by, :find_by!, to: :all
delegate :destroy, :destroy_all, :delete, :delete_all, :update, :update_all, to: :all
- delegate :find_each, :find_in_batches, to: :all
+ delegate :find_each, :find_in_batches, :in_batches, to: :all
delegate :select, :group, :order, :except, :reorder, :limit, :offset, :joins, :or,
:where, :rewhere, :preload, :eager_load, :includes, :from, :lock, :readonly,
:having, :create_with, :uniq, :distinct, :references, :none, :unscope, to: :all
diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb
index 7e907beec0..6dd54f9262 100644
--- a/activerecord/lib/active_record/railtie.rb
+++ b/activerecord/lib/active_record/railtie.rb
@@ -16,11 +16,11 @@ module ActiveRecord
config.app_generators.orm :active_record, :migration => true,
:timestamps => true
- config.app_middleware.insert_after "::ActionDispatch::Callbacks",
- "ActiveRecord::QueryCache"
+ config.app_middleware.insert_after ::ActionDispatch::Callbacks,
+ ActiveRecord::QueryCache
- config.app_middleware.insert_after "::ActionDispatch::Callbacks",
- "ActiveRecord::ConnectionAdapters::ConnectionManagement"
+ config.app_middleware.insert_after ::ActionDispatch::Callbacks,
+ ActiveRecord::ConnectionAdapters::ConnectionManagement
config.action_dispatch.rescue_responses.merge!(
'ActiveRecord::RecordNotFound' => :not_found,
@@ -78,8 +78,8 @@ module ActiveRecord
initializer "active_record.migration_error" do
if config.active_record.delete(:migration_error) == :page_load
- config.app_middleware.insert_after "::ActionDispatch::Callbacks",
- "ActiveRecord::Migration::CheckPending"
+ config.app_middleware.insert_after ::ActionDispatch::Callbacks,
+ ActiveRecord::Migration::CheckPending
end
end
@@ -93,6 +93,7 @@ module ActiveRecord
cache = Marshal.load File.binread filename
if cache.version == ActiveRecord::Migrator.current_version
self.connection.schema_cache = cache
+ self.connection_pool.schema_cache = cache.dup
else
warn "Ignoring db/schema_cache.dump because it has expired. The current schema version is #{ActiveRecord::Migrator.current_version}, but the one in the cache is #{cache.version}."
end
@@ -155,8 +156,8 @@ end_warning
ActiveSupport.on_load(:active_record) do
ActionDispatch::Reloader.send(hook) do
if ActiveRecord::Base.connected?
- ActiveRecord::Base.clear_reloadable_connections!
ActiveRecord::Base.clear_cache!
+ ActiveRecord::Base.clear_reloadable_connections!
end
end
end
diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake
index 2591e7492d..d940ac244a 100644
--- a/activerecord/lib/active_record/railties/databases.rake
+++ b/activerecord/lib/active_record/railties/databases.rake
@@ -12,7 +12,7 @@ db_namespace = namespace :db do
end
end
- desc 'Creates the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:create:all to create all databases in the config). Without RAILS_ENV it defaults to creating the development and test databases.'
+ desc 'Creates the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:create:all to create all databases in the config). Without RAILS_ENV, it defaults to creating the development and test databases.'
task :create => [:load_config] do
ActiveRecord::Tasks::DatabaseTasks.create_current
end
@@ -23,7 +23,7 @@ db_namespace = namespace :db do
end
end
- desc 'Drops the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:drop:all to drop all databases in the config). Without RAILS_ENV it defaults to dropping the development and test databases.'
+ desc 'Drops the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:drop:all to drop all databases in the config). Without RAILS_ENV, it defaults to dropping the development and test databases.'
task :drop => [:load_config] do
ActiveRecord::Tasks::DatabaseTasks.drop_current
end
@@ -42,15 +42,18 @@ db_namespace = namespace :db do
desc "Migrate the database (options: VERSION=x, VERBOSE=false, SCOPE=blog)."
task :migrate => [:environment, :load_config] do
ActiveRecord::Tasks::DatabaseTasks.migrate
- db_namespace['_dump'].invoke if ActiveRecord::Base.dump_schema_after_migration
+ db_namespace['_dump'].invoke
end
+ # IMPORTANT: This task won't dump the schema if ActiveRecord::Base.dump_schema_after_migration is set to false
task :_dump do
- case ActiveRecord::Base.schema_format
- when :ruby then db_namespace["schema:dump"].invoke
- when :sql then db_namespace["structure:dump"].invoke
- else
- raise "unknown schema format #{ActiveRecord::Base.schema_format}"
+ if ActiveRecord::Base.dump_schema_after_migration
+ case ActiveRecord::Base.schema_format
+ when :ruby then db_namespace["schema:dump"].invoke
+ when :sql then db_namespace["structure:dump"].invoke
+ else
+ raise "unknown schema format #{ActiveRecord::Base.schema_format}"
+ end
end
# Allow this task to be called as many times as required. An example is the
# migrate:redo task, which calls other two internally that depend on this one.
@@ -76,7 +79,7 @@ db_namespace = namespace :db do
task :up => [:environment, :load_config] do
version = ENV['VERSION'] ? ENV['VERSION'].to_i : nil
raise 'VERSION is required' unless version
- ActiveRecord::Migrator.run(:up, ActiveRecord::Migrator.migrations_paths, version)
+ ActiveRecord::Migrator.run(:up, ActiveRecord::Tasks::DatabaseTasks.migrations_paths, version)
db_namespace['_dump'].invoke
end
@@ -84,7 +87,7 @@ db_namespace = namespace :db do
task :down => [:environment, :load_config] do
version = ENV['VERSION'] ? ENV['VERSION'].to_i : nil
raise 'VERSION is required - To go down one migration, run db:rollback' unless version
- ActiveRecord::Migrator.run(:down, ActiveRecord::Migrator.migrations_paths, version)
+ ActiveRecord::Migrator.run(:down, ActiveRecord::Tasks::DatabaseTasks.migrations_paths, version)
db_namespace['_dump'].invoke
end
@@ -96,7 +99,7 @@ db_namespace = namespace :db do
db_list = ActiveRecord::SchemaMigration.normalized_versions
file_list =
- ActiveRecord::Migrator.migrations_paths.flat_map do |path|
+ ActiveRecord::Tasks::DatabaseTasks.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)
@@ -122,22 +125,19 @@ db_namespace = namespace :db do
desc 'Rolls the schema back to the previous version (specify steps w/ STEP=n).'
task :rollback => [:environment, :load_config] do
step = ENV['STEP'] ? ENV['STEP'].to_i : 1
- ActiveRecord::Migrator.rollback(ActiveRecord::Migrator.migrations_paths, step)
+ ActiveRecord::Migrator.rollback(ActiveRecord::Tasks::DatabaseTasks.migrations_paths, step)
db_namespace['_dump'].invoke
end
# desc 'Pushes the schema to the next version (specify steps w/ STEP=n).'
task :forward => [:environment, :load_config] do
step = ENV['STEP'] ? ENV['STEP'].to_i : 1
- ActiveRecord::Migrator.forward(ActiveRecord::Migrator.migrations_paths, step)
+ ActiveRecord::Migrator.forward(ActiveRecord::Tasks::DatabaseTasks.migrations_paths, step)
db_namespace['_dump'].invoke
end
# desc 'Drops and recreates the database from db/schema.rb for the current environment and loads the seeds.'
- task :reset => [:environment, :load_config] do
- db_namespace["drop"].invoke
- db_namespace["setup"].invoke
- end
+ task :reset => [ 'db:drop', 'db:setup' ]
# desc "Retrieves the charset for the current environment's database"
task :charset => [:environment, :load_config] do
@@ -159,8 +159,8 @@ db_namespace = namespace :db do
end
# desc "Raises an error if there are pending migrations"
- task :abort_if_pending_migrations => :environment do
- pending_migrations = ActiveRecord::Migrator.open(ActiveRecord::Migrator.migrations_paths).pending_migrations
+ task :abort_if_pending_migrations => [:environment, :load_config] do
+ pending_migrations = ActiveRecord::Migrator.open(ActiveRecord::Tasks::DatabaseTasks.migrations_paths).pending_migrations
if pending_migrations.any?
puts "You have #{pending_migrations.size} pending #{pending_migrations.size > 1 ? 'migrations:' : 'migration:'}"
@@ -171,17 +171,17 @@ db_namespace = namespace :db do
end
end
- desc 'Create the database, load the schema, and initialize with the seed data (use db:reset to also drop the database first)'
+ desc 'Creates the database, loads the schema, and initializes with the seed data (use db:reset to also drop the database first)'
task :setup => ['db:schema:load_if_ruby', 'db:structure:load_if_sql', :seed]
- desc 'Load the seed data from db/seeds.rb'
+ desc 'Loads the seed data from db/seeds.rb'
task :seed do
db_namespace['abort_if_pending_migrations'].invoke
ActiveRecord::Tasks::DatabaseTasks.load_seed
end
namespace :fixtures do
- desc "Load fixtures into the current environment's database. Load specific fixtures using FIXTURES=x,y. Load from subdirectory in test/fixtures using FIXTURES_DIR=z. Specify an alternative path (eg. spec/fixtures) using FIXTURES_PATH=spec/fixtures."
+ desc "Loads fixtures into the current environment's database. Load specific fixtures using FIXTURES=x,y. Load from subdirectory in test/fixtures using FIXTURES_DIR=z. Specify an alternative path (eg. spec/fixtures) using FIXTURES_PATH=spec/fixtures."
task :load => [:environment, :load_config] do
require 'active_record/fixtures'
@@ -229,7 +229,7 @@ db_namespace = namespace :db do
end
namespace :schema do
- desc 'Create a db/schema.rb file that is portable against any DB supported by AR'
+ desc 'Creates a db/schema.rb file that is portable against any DB supported by Active Record'
task :dump => [:environment, :load_config] do
require 'active_record/schema_dumper'
filename = ENV['SCHEMA'] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, 'schema.rb')
@@ -239,8 +239,8 @@ db_namespace = namespace :db do
db_namespace['schema:dump'].reenable
end
- desc 'Load a schema.rb file into the database'
- task :load => [:load_config] do
+ desc 'Loads a schema.rb file into the database'
+ task :load => [:environment, :load_config] do
ActiveRecord::Tasks::DatabaseTasks.load_schema_current(:ruby, ENV['SCHEMA'])
end
@@ -249,17 +249,17 @@ db_namespace = namespace :db do
end
namespace :cache do
- desc 'Create a db/schema_cache.dump file.'
+ desc 'Creates a db/schema_cache.dump file.'
task :dump => [:environment, :load_config] do
con = ActiveRecord::Base.connection
filename = File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "schema_cache.dump")
con.schema_cache.clear!
- con.tables.each { |table| con.schema_cache.add(table) }
+ con.data_sources.each { |table| con.schema_cache.add(table) }
open(filename, 'wb') { |f| f.write(Marshal.dump(con.schema_cache)) }
end
- desc 'Clear a db/schema_cache.dump file.'
+ desc 'Clears a db/schema_cache.dump file.'
task :clear => [:environment, :load_config] do
filename = File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "schema_cache.dump")
FileUtils.rm(filename) if File.exist?(filename)
@@ -269,7 +269,7 @@ db_namespace = namespace :db do
end
namespace :structure do
- desc 'Dump the database structure to db/structure.sql. Specify another file with SCHEMA=db/my_structure.sql'
+ desc 'Dumps the database structure to db/structure.sql. Specify another file with SCHEMA=db/my_structure.sql'
task :dump => [:environment, :load_config] do
filename = ENV['SCHEMA'] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "structure.sql")
current_config = ActiveRecord::Tasks::DatabaseTasks.current_config
@@ -285,7 +285,7 @@ db_namespace = namespace :db do
db_namespace['structure:dump'].reenable
end
- desc "Recreate the databases from the structure.sql file"
+ desc "Recreates the databases from the structure.sql file"
task :load => [:load_config] do
ActiveRecord::Tasks::DatabaseTasks.load_schema_current(:sql, ENV['SCHEMA'])
end
@@ -353,7 +353,7 @@ db_namespace = namespace :db do
ActiveRecord::Tasks::DatabaseTasks.purge ActiveRecord::Base.configurations['test']
end
- # desc 'Check for pending migrations and load the test schema'
+ # desc 'Load the test schema'
task :prepare => %w(environment load_config) do
unless ActiveRecord::Base.configurations.blank?
db_namespace['test:load'].invoke
@@ -384,7 +384,7 @@ namespace :railties do
puts "Copied migration #{migration.basename} from #{name}"
end
- ActiveRecord::Migration.copy(ActiveRecord::Migrator.migrations_paths.first, railties,
+ ActiveRecord::Migration.copy(ActiveRecord::Tasks::DatabaseTasks.migrations_paths.first, railties,
:on_skip => on_skip, :on_copy => on_copy)
end
end
diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb
index 1b0ae2c942..5b9d45d871 100644
--- a/activerecord/lib/active_record/reflection.rb
+++ b/activerecord/lib/active_record/reflection.rb
@@ -32,6 +32,7 @@ module ActiveRecord
end
def self.add_reflection(ar, name, reflection)
+ ar.clear_reflections_cache
ar._reflections = ar._reflections.merge(name.to_s => reflection)
end
@@ -61,22 +62,27 @@ module ActiveRecord
aggregate_reflections[aggregation.to_s]
end
- # Returns a Hash of name of the reflection as the key and a AssociationReflection as the value.
+ # Returns a Hash of name of the reflection as the key and an AssociationReflection as the value.
#
# Account.reflections # => {"balance" => AggregateReflection}
#
- # @api public
def reflections
- ref = {}
- _reflections.each do |name, reflection|
- parent_name, parent_reflection = reflection.parent_reflection
- if parent_name
- ref[parent_name] = parent_reflection
- else
- ref[name] = reflection
+ @__reflections ||= begin
+ ref = {}
+
+ _reflections.each do |name, reflection|
+ parent_reflection = reflection.parent_reflection
+
+ if parent_reflection
+ parent_name = parent_reflection.name
+ ref[parent_name.to_s] = parent_reflection
+ else
+ ref[name] = reflection
+ end
end
+
+ ref
end
- ref
end
# Returns an array of AssociationReflection objects for all the
@@ -89,10 +95,10 @@ module ActiveRecord
# Account.reflect_on_all_associations # returns an array of all associations
# Account.reflect_on_all_associations(:has_many) # returns an array of all has_many associations
#
- # @api public
def reflect_on_all_associations(macro = nil)
association_reflections = reflections.values
- macro ? association_reflections.select { |reflection| reflection.macro == macro } : association_reflections
+ association_reflections.select! { |reflection| reflection.macro == macro } if macro
+ association_reflections
end
# Returns the AssociationReflection object for the +association+ (use the symbol).
@@ -100,22 +106,22 @@ module ActiveRecord
# Account.reflect_on_association(:owner) # returns the owner AssociationReflection
# Invoice.reflect_on_association(:line_items).macro # returns :has_many
#
- # @api public
def reflect_on_association(association)
reflections[association.to_s]
end
- # @api private
def _reflect_on_association(association) #:nodoc:
_reflections[association.to_s]
end
# Returns an array of AssociationReflection objects for all associations which have <tt>:autosave</tt> enabled.
- #
- # @api public
def reflect_on_all_autosave_associations
reflections.values.select { |reflection| reflection.options[:autosave] }
end
+
+ def clear_reflections_cache # :nodoc:
+ @__reflections = nil
+ end
end
# Holds all the methods that are shared between MacroReflection, AssociationReflection
@@ -157,6 +163,68 @@ module ActiveRecord
scope_chain.flatten
end
+ def counter_cache_column
+ if belongs_to?
+ if options[:counter_cache] == true
+ "#{active_record.name.demodulize.underscore.pluralize}_count"
+ elsif options[:counter_cache]
+ options[:counter_cache].to_s
+ end
+ else
+ options[:counter_cache] ? options[:counter_cache].to_s : "#{name}_count"
+ end
+ end
+
+ def inverse_of
+ return unless inverse_name
+
+ @inverse_of ||= klass._reflect_on_association inverse_name
+ end
+
+ def check_validity_of_inverse!
+ unless polymorphic?
+ if has_inverse? && inverse_of.nil?
+ raise InverseOfAssociationNotFoundError.new(self)
+ end
+ end
+ end
+
+ # This shit is nasty. We need to avoid the following situation:
+ #
+ # * An associated record is deleted via record.destroy
+ # * Hence the callbacks run, and they find a belongs_to on the record with a
+ # :counter_cache options which points back at our owner. So they update the
+ # counter cache.
+ # * In which case, we must make sure to *not* update the counter cache, or else
+ # it will be decremented twice.
+ #
+ # Hence this method.
+ def inverse_which_updates_counter_cache
+ return @inverse_which_updates_counter_cache if defined?(@inverse_which_updates_counter_cache)
+ @inverse_which_updates_counter_cache = klass.reflect_on_all_associations(:belongs_to).find do |inverse|
+ inverse.counter_cache_column == counter_cache_column
+ end
+ end
+ alias inverse_updates_counter_cache? inverse_which_updates_counter_cache
+
+ def inverse_updates_counter_in_memory?
+ inverse_of && inverse_which_updates_counter_cache == inverse_of
+ end
+
+ # Returns whether a counter cache should be used for this association.
+ #
+ # The counter_cache option must be given on either the owner or inverse
+ # association, and the column must be present on the owner.
+ def has_cached_counter?
+ options[:counter_cache] ||
+ inverse_which_updates_counter_cache && inverse_which_updates_counter_cache.options[:counter_cache] &&
+ !!active_record.columns_hash[counter_cache_column]
+ end
+
+ def counter_must_be_updated_by_has_many?
+ !inverse_updates_counter_in_memory? && has_cached_counter?
+ end
+
def alias_candidate(name)
"#{plural_name}_#{name}"
end
@@ -204,7 +272,7 @@ module ActiveRecord
def autosave=(autosave)
@automatic_inverse_of = false
@options[:autosave] = autosave
- _, parent_reflection = self.parent_reflection
+ parent_reflection = self.parent_reflection
if parent_reflection
parent_reflection.autosave = autosave
end
@@ -272,7 +340,7 @@ module ActiveRecord
end
attr_reader :type, :foreign_type
- attr_accessor :parent_reflection # [:name, Reflection]
+ attr_accessor :parent_reflection # Reflection
def initialize(name, scope, options, active_record)
super
@@ -319,26 +387,10 @@ module ActiveRecord
@active_record_primary_key ||= options[:primary_key] || primary_key(active_record)
end
- def counter_cache_column
- if options[:counter_cache] == true
- "#{active_record.name.demodulize.underscore.pluralize}_count"
- elsif options[:counter_cache]
- options[:counter_cache].to_s
- end
- end
-
def check_validity!
check_validity_of_inverse!
end
- def check_validity_of_inverse!
- unless polymorphic?
- if has_inverse? && inverse_of.nil?
- raise InverseOfAssociationNotFoundError.new(self)
- end
- end
- end
-
def check_preloadable!
return unless scope
@@ -390,12 +442,6 @@ module ActiveRecord
inverse_name
end
- def inverse_of
- return unless inverse_name
-
- @inverse_of ||= klass._reflect_on_association inverse_name
- end
-
def polymorphic_inverse_of(associated_class)
if has_inverse?
if inverse_relationship = associated_class._reflect_on_association(options[:inverse_of])
@@ -878,6 +924,8 @@ module ActiveRecord
klass.primary_key || raise(UnknownPrimaryKey.new(klass))
end
+ def inverse_name; delegate_reflection.send(:inverse_name); end
+
private
def derive_class_name
# get the class_name of the belongs_to association of the through reflection
diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb
index 85648a7f8f..36cdeed489 100644
--- a/activerecord/lib/active_record/relation.rb
+++ b/activerecord/lib/active_record/relation.rb
@@ -1,4 +1,3 @@
-# -*- coding: utf-8 -*-
require "arel/collectors/bind"
module ActiveRecord
@@ -9,12 +8,13 @@ module ActiveRecord
:extending, :unscope]
SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :reordering,
- :reverse_order, :distinct, :create_with, :uniq]
+ :reverse_order, :distinct, :create_with]
CLAUSE_METHODS = [:where, :having, :from]
INVALID_METHODS_FOR_DELETE_ALL = [:limit, :distinct, :offset, :group, :having]
VALUE_METHODS = MULTI_VALUE_METHODS + SINGLE_VALUE_METHODS + CLAUSE_METHODS
+ include Enumerable
include FinderMethods, Calculations, SpawnMethods, QueryMethods, Batches, Explain, Delegation
attr_reader :table, :klass, :loaded, :predicate_builder
@@ -275,38 +275,52 @@ module ActiveRecord
# Returns true if there are no records.
def none?
- if block_given?
- to_a.none? { |*block_args| yield(*block_args) }
- else
- empty?
- end
+ return super if block_given?
+ empty?
end
# Returns true if there are any records.
def any?
- if block_given?
- to_a.any? { |*block_args| yield(*block_args) }
- else
- !empty?
- end
+ return super if block_given?
+ !empty?
end
# Returns true if there is exactly one record.
def one?
- if block_given?
- to_a.one? { |*block_args| yield(*block_args) }
- else
- limit_value ? to_a.one? : size == 1
- end
+ return super if block_given?
+ limit_value ? to_a.one? : size == 1
end
# Returns true if there is more than one record.
def many?
- if block_given?
- to_a.many? { |*block_args| yield(*block_args) }
- else
- limit_value ? to_a.many? : size > 1
- end
+ return super if block_given?
+ limit_value ? to_a.many? : size > 1
+ end
+
+ # Returns a cache key that can be used to identify the records fetched by
+ # this query. The cache key is built with a fingerprint of the sql query,
+ # the number of records matched by the query and a timestamp of the last
+ # updated record. When a new record comes to match the query, or any of
+ # the existing records is updated or deleted, the cache key changes.
+ #
+ # Product.where("name like ?", "%Cosmic Encounter%").cache_key
+ # => "products/query-1850ab3d302391b85b8693e941286659-1-20150714212553907087000"
+ #
+ # If the collection is loaded, the method will iterate through the records
+ # to generate the timestamp, otherwise it will trigger one SQL query like:
+ #
+ # SELECT COUNT(*), MAX("products"."updated_at") FROM "products" WHERE (name like '%Cosmic Encounter%')
+ #
+ # You can also pass a custom timestamp column to fetch the timestamp of the
+ # last updated record.
+ #
+ # Product.where("name like ?", "%Game%").cache_key(:last_reviewed_at)
+ #
+ # You can customize the strategy to generate the key on a per model basis
+ # overriding ActiveRecord::Base#collection_cache_key.
+ def cache_key(timestamp_column = :updated_at)
+ @cache_keys ||= {}
+ @cache_keys[timestamp_column] ||= @klass.collection_cache_key(self, timestamp_column)
end
# Scope all queries to the current scope.
@@ -386,11 +400,11 @@ module ActiveRecord
# people = Person.where(group: 'expert')
# people.update(group: 'masters')
#
- # Note: Updating a large number of records will run a
- # UPDATE query for each record, which may cause a performance
- # issue. So if it is not needed to run callbacks for each update, it is
- # preferred to use <tt>update_all</tt> for updating all records using
- # a single query.
+ # Note: Updating a large number of records will run an
+ # UPDATE query for each record, which may cause a performance
+ # issue. So if it is not needed to run callbacks for each update, it is
+ # preferred to use <tt>update_all</tt> for updating all records using
+ # a single query.
def update(id = :all, attributes)
if id.is_a?(Array)
id.map.with_index { |one_id, idx| update(one_id, attributes[idx]) }
@@ -403,7 +417,7 @@ module ActiveRecord
end
end
- # Destroys the records matching +conditions+ by instantiating each
+ # Destroys the records by instantiating each
# record and calling its +destroy+ method. Each object's callbacks are
# executed (including <tt>:dependent</tt> association options). Returns the
# collection of objects that were destroyed; each will be frozen, to
@@ -416,20 +430,15 @@ module ActiveRecord
# rows quickly, without concern for their associations or callbacks, use
# +delete_all+ instead.
#
- # ==== Parameters
- #
- # * +conditions+ - A string, array, or hash that specifies which records
- # to destroy. If omitted, all records are destroyed. See the
- # Conditions section in the introduction to ActiveRecord::Base for
- # more information.
- #
# ==== Examples
#
- # Person.destroy_all("last_login < '2004-04-04'")
- # Person.destroy_all(status: "inactive")
# Person.where(age: 0..18).destroy_all
def destroy_all(conditions = nil)
if conditions
+ ActiveSupport::Deprecation.warn(<<-MESSAGE.squish)
+ Passing conditions to destroy_all is deprecated and will be removed in Rails 5.1.
+ To achieve the same use where(conditions).destroy_all
+ MESSAGE
where(conditions).destroy_all
else
to_a.each(&:destroy).tap { reset }
@@ -463,15 +472,13 @@ module ActiveRecord
end
end
- # Deletes the records matching +conditions+ without instantiating the records
+ # Deletes the records without instantiating the records
# first, and hence not calling the +destroy+ method nor invoking callbacks. This
# is a single SQL DELETE statement that goes straight to the database, much more
# efficient than +destroy_all+. Be careful with relations though, in particular
# <tt>:dependent</tt> rules defined on associations are not honored. Returns the
# number of rows affected.
#
- # Post.delete_all("person_id = 5 AND (category = 'Something' OR category = 'Else')")
- # Post.delete_all(["person_id = ? AND (category = ? OR category = ?)", 5, 'Something', 'Else'])
# Post.where(person_id: 5).where(category: ['Something', 'Else']).delete_all
#
# Both calls delete the affected posts all at once with a single DELETE statement.
@@ -497,6 +504,10 @@ module ActiveRecord
end
if conditions
+ ActiveSupport::Deprecation.warn(<<-MESSAGE.squish)
+ Passing conditions to delete_all is deprecated and will be removed in Rails 5.1.
+ To achieve the same use where(conditions).delete_all
+ MESSAGE
where(conditions).delete_all
else
stmt = Arel::DeleteManager.new
@@ -618,6 +629,7 @@ module ActiveRecord
def uniq_value
distinct_value
end
+ deprecate uniq_value: :distinct_value
# Compares two relations for equality.
def ==(other)
@@ -651,6 +663,13 @@ module ActiveRecord
"#<#{self.class.name} [#{entries.join(', ')}]>"
end
+ protected
+
+ def load_records(records)
+ @records = records
+ @loaded = true
+ end
+
private
def exec_queries
diff --git a/activerecord/lib/active_record/relation/batches.rb b/activerecord/lib/active_record/relation/batches.rb
index e07580a563..beb8fa511c 100644
--- a/activerecord/lib/active_record/relation/batches.rb
+++ b/activerecord/lib/active_record/relation/batches.rb
@@ -1,3 +1,5 @@
+require "active_record/relation/batches/batch_enumerator"
+
module ActiveRecord
module Batches
# Looping through a collection of records from the database
@@ -122,24 +124,102 @@ module ActiveRecord
end
end
+ in_batches(of: batch_size, begin_at: begin_at, end_at: end_at, load: true) do |batch|
+ yield batch.to_a
+ end
+ end
+
+ # Yields ActiveRecord::Relation objects to work with a batch of records.
+ #
+ # Person.where("age > 21").in_batches do |relation|
+ # relation.delete_all
+ # sleep(10) # Throttle the delete queries
+ # end
+ #
+ # If you do not provide a block to #in_batches, it will return a
+ # BatchEnumerator which is enumerable.
+ #
+ # Person.in_batches.with_index do |relation, batch_index|
+ # puts "Processing relation ##{batch_index}"
+ # relation.each { |relation| relation.delete_all }
+ # end
+ #
+ # Examples of calling methods on the returned BatchEnumerator object:
+ #
+ # Person.in_batches.delete_all
+ # Person.in_batches.update_all(awesome: true)
+ # Person.in_batches.each_record(&:party_all_night!)
+ #
+ # ==== Options
+ # * <tt>:of</tt> - Specifies the size of the batch. Default to 1000.
+ # * <tt>:load</tt> - Specifies if the relation should be loaded. Default to false.
+ # * <tt>:begin_at</tt> - Specifies the primary key value to start from, inclusive of the value.
+ # * <tt>:end_at</tt> - Specifies the primary key value to end at, inclusive of the value.
+ #
+ # This is especially useful if you want to work with the
+ # ActiveRecord::Relation object instead of the array of records, or if
+ # you want multiple workers dealing with the same processing queue. You can
+ # make worker 1 handle all the records between id 0 and 10,000 and worker 2
+ # handle from 10,000 and beyond (by setting the +:begin_at+ and +:end_at+
+ # option on each worker).
+ #
+ # # Let's process the next 2000 records
+ # Person.in_batches(of: 2000, begin_at: 2000).update_all(awesome: true)
+ #
+ # An example of calling where query method on the relation:
+ #
+ # Person.in_batches.each do |relation|
+ # relation.update_all('age = age + 1')
+ # relation.where('age > 21').update_all(should_party: true)
+ # relation.where('age <= 21').delete_all
+ # end
+ #
+ # NOTE: If you are going to iterate through each record, you should call
+ # #each_record on the yielded BatchEnumerator:
+ #
+ # Person.in_batches.each_record(&:party_all_night!)
+ #
+ # NOTE: It's not possible to set the order. That is automatically set to
+ # ascending on the primary key ("id ASC") to make the batch ordering
+ # consistent. Therefore the primary key must be orderable, e.g an integer
+ # or a string.
+ #
+ # NOTE: You can't set the limit either, that's used to control the batch
+ # sizes.
+ def in_batches(of: 1000, begin_at: nil, end_at: nil, load: false)
+ relation = self
+ unless block_given?
+ return BatchEnumerator.new(of: of, begin_at: begin_at, end_at: end_at, relation: self)
+ end
+
if logger && (arel.orders.present? || arel.taken.present?)
logger.warn("Scoped order and limit are ignored, it's forced to be batch order and batch size")
end
- relation = relation.reorder(batch_order).limit(batch_size)
+ relation = relation.reorder(batch_order).limit(of)
relation = apply_limits(relation, begin_at, end_at)
- records = relation.to_a
+ batch_relation = relation
+
+ loop do
+ if load
+ records = batch_relation.to_a
+ ids = records.map(&:id)
+ yielded_relation = self.where(primary_key => ids)
+ yielded_relation.load_records(records)
+ else
+ ids = batch_relation.pluck(primary_key)
+ yielded_relation = self.where(primary_key => ids)
+ end
- while records.any?
- records_size = records.size
- primary_key_offset = records.last.id
- raise "Primary key not included in the custom select clause" unless primary_key_offset
+ break if ids.empty?
- yield records
+ primary_key_offset = ids.last
+ raise ArgumentError.new("Primary key not included in the custom select clause") unless primary_key_offset
- break if records_size < batch_size
+ yield yielded_relation
- records = relation.where(table[primary_key].gt(primary_key_offset)).to_a
+ break if ids.length < of
+ batch_relation = relation.where(table[primary_key].gt(primary_key_offset))
end
end
diff --git a/activerecord/lib/active_record/relation/batches/batch_enumerator.rb b/activerecord/lib/active_record/relation/batches/batch_enumerator.rb
new file mode 100644
index 0000000000..153aae9584
--- /dev/null
+++ b/activerecord/lib/active_record/relation/batches/batch_enumerator.rb
@@ -0,0 +1,67 @@
+module ActiveRecord
+ module Batches
+ class BatchEnumerator
+ include Enumerable
+
+ def initialize(of: 1000, begin_at: nil, end_at: nil, relation:) #:nodoc:
+ @of = of
+ @relation = relation
+ @begin_at = begin_at
+ @end_at = end_at
+ end
+
+ # Looping through a collection of records from the database (using the
+ # +all+ method, for example) is very inefficient since it will try to
+ # instantiate all the objects at once.
+ #
+ # In that case, batch processing methods allow you to work with the
+ # records in batches, thereby greatly reducing memory consumption.
+ #
+ # Person.in_batches.each_record do |person|
+ # person.do_awesome_stuff
+ # end
+ #
+ # Person.where("age > 21").in_batches(of: 10).each_record do |person|
+ # person.party_all_night!
+ # end
+ #
+ # If you do not provide a block to #each_record, it will return an Enumerator
+ # for chaining with other methods:
+ #
+ # Person.in_batches.each_record.with_index do |person, index|
+ # person.award_trophy(index + 1)
+ # end
+ def each_record
+ return to_enum(:each_record) unless block_given?
+
+ @relation.to_enum(:in_batches, of: @of, begin_at: @begin_at, end_at: @end_at, load: true).each do |relation|
+ relation.to_a.each { |record| yield record }
+ end
+ end
+
+ # Delegates #delete_all, #update_all, #destroy_all methods to each batch.
+ #
+ # People.in_batches.delete_all
+ # People.in_batches.destroy_all('age < 10')
+ # People.in_batches.update_all('age = age + 1')
+ [:delete_all, :update_all, :destroy_all].each do |method|
+ define_method(method) do |*args, &block|
+ @relation.to_enum(:in_batches, of: @of, begin_at: @begin_at, end_at: @end_at, load: false).each do |relation|
+ relation.send(method, *args, &block)
+ end
+ end
+ end
+
+ # Yields an ActiveRecord::Relation object for each batch of records.
+ #
+ # Person.in_batches.each do |relation|
+ # relation.update_all(awesome: true)
+ # end
+ def each
+ enum = @relation.to_enum(:in_batches, of: @of, begin_at: @begin_at, end_at: @end_at, load: false)
+ return enum.each { |relation| yield relation } if block_given?
+ enum
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb
index 402b317d9c..69f39e5ba9 100644
--- a/activerecord/lib/active_record/relation/calculations.rb
+++ b/activerecord/lib/active_record/relation/calculations.rb
@@ -70,8 +70,9 @@ module ActiveRecord
# +calculate+ for examples with options.
#
# Person.sum(:age) # => 4562
- def sum(*args)
- calculate(:sum, *args)
+ def sum(column_name = nil, &block)
+ return super(&block) if block_given?
+ calculate(:sum, column_name)
end
# This calculates aggregate values in the given column. Methods for count, sum, average,
@@ -138,7 +139,7 @@ module ActiveRecord
# # SELECT people.id, people.name FROM people
# # => [[1, 'David'], [2, 'Jeremy'], [3, 'Jose']]
#
- # Person.pluck('DISTINCT role')
+ # Person.distinct.pluck(:role)
# # SELECT DISTINCT role FROM people
# # => ['admin', 'member', 'guest']
#
@@ -161,6 +162,10 @@ module ActiveRecord
end
end
+ if loaded? && (column_names - @klass.column_names).empty?
+ return @records.pluck(*column_names)
+ end
+
if has_include?(column_names.first)
construct_relation_for_association_calculations.pluck(*column_names)
else
@@ -190,7 +195,8 @@ module ActiveRecord
def perform_calculation(operation, column_name)
operation = operation.to_s.downcase
- # If #count is used with #distinct / #uniq it is considered distinct. (eg. relation.distinct.count)
+ # If #count is used with #distinct (i.e. `relation.distinct.count`) it is
+ # considered distinct.
distinct = self.distinct_value
if operation == "count"
@@ -332,7 +338,6 @@ module ActiveRecord
# column_alias_for("sum(id)") # => "sum_id"
# column_alias_for("count(distinct users.id)") # => "count_distinct_users_id"
# column_alias_for("count(*)") # => "count_all"
- # column_alias_for("count", "id") # => "count_id"
def column_alias_for(keys)
if keys.respond_to? :name
keys = "#{keys.relation.name}.#{keys.name}"
diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb
index d4a8823cfe..d75ec72b1a 100644
--- a/activerecord/lib/active_record/relation/delegation.rb
+++ b/activerecord/lib/active_record/relation/delegation.rb
@@ -18,7 +18,7 @@ module ActiveRecord
delegate = Class.new(klass) {
include ClassSpecificRelation
}
- const_set klass.name.gsub('::', '_'), delegate
+ const_set klass.name.gsub('::'.freeze, '_'.freeze), delegate
cache[klass] = delegate
end
end
@@ -39,7 +39,7 @@ module ActiveRecord
BLACKLISTED_ARRAY_METHODS = [
:compact!, :flatten!, :reject!, :reverse!, :rotate!, :map!,
:shuffle!, :slice!, :sort!, :sort_by!, :delete_if,
- :keep_if, :pop, :shift, :delete_at, :compact, :select!
+ :keep_if, :pop, :shift, :delete_at, :select!
].to_set # :nodoc:
delegate :to_xml, :to_yaml, :length, :collect, :map, :each, :all?, :include?, :to_ary, :join, to: :to_a
diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb
index 6a3a56f1cc..009b2bad57 100644
--- a/activerecord/lib/active_record/relation/finder_methods.rb
+++ b/activerecord/lib/active_record/relation/finder_methods.rb
@@ -46,7 +46,7 @@ module ActiveRecord
# # returns the first item or returns a new instance (requires you call .save to persist against the database).
#
# Person.where(name: 'Spartacus', rating: 4).first_or_create
- # # returns the first item or creates it and returns it, available since Rails 3.2.1.
+ # # returns the first item or creates it and returns it.
#
# ==== Alternatives for +find+
#
@@ -57,16 +57,13 @@ module ActiveRecord
# # returns a chainable list of instances with only the mentioned fields.
#
# Person.where(name: 'Spartacus', rating: 4).ids
- # # returns an Array of ids, available since Rails 3.2.1.
+ # # returns an Array of ids.
#
# Person.where(name: 'Spartacus', rating: 4).pluck(:field1, :field2)
- # # returns an Array of the required fields, available since Rails 3.1.
+ # # returns an Array of the required fields.
def find(*args)
- if block_given?
- to_a.find(*args) { |*block_args| yield(*block_args) }
- else
- find_with_ids(*args)
- end
+ return super if block_given?
+ find_with_ids(*args)
end
# Finds the first record matching the specified conditions. There
@@ -77,18 +74,19 @@ module ActiveRecord
#
# Post.find_by name: 'Spartacus', rating: 4
# Post.find_by "published_at < ?", 2.weeks.ago
- def find_by(*args)
- where(*args).take
+ def find_by(arg, *args)
+ where(arg, *args).take
rescue RangeError
nil
end
# Like <tt>find_by</tt>, except that if no record is found, raises
# an <tt>ActiveRecord::RecordNotFound</tt> error.
- def find_by!(*args)
- where(*args).take!
+ def find_by!(arg, *args)
+ where(arg, *args).take!
rescue RangeError
- raise RecordNotFound, "Couldn't find #{@klass.name} with an out of range value"
+ raise RecordNotFound.new("Couldn't find #{@klass.name} with an out of range value",
+ @klass.name)
end
# Gives a record (or N records if a parameter is supplied) without any implied
@@ -111,23 +109,11 @@ module ActiveRecord
# Find the first record (or first N records if a parameter is supplied).
# If no order is defined it will order by primary key.
#
- # Person.first # returns the first object fetched by SELECT * FROM people
+ # Person.first # returns the first object fetched by SELECT * FROM people ORDER BY people.id LIMIT 1
# Person.where(["user_name = ?", user_name]).first
# Person.where(["user_name = :u", { u: user_name }]).first
# Person.order("created_on DESC").offset(5).first
- # Person.first(3) # returns the first three objects fetched by SELECT * FROM people LIMIT 3
- #
- # ==== Rails 3
- #
- # Person.first # SELECT "people".* FROM "people" LIMIT 1
- #
- # NOTE: Rails 3 may not order this query by the primary key and the order
- # will depend on the database implementation. In order to ensure that behavior,
- # use <tt>User.order(:id).first</tt> instead.
- #
- # ==== Rails 4
- #
- # Person.first # SELECT "people".* FROM "people" ORDER BY "people"."id" ASC LIMIT 1
+ # Person.first(3) # returns the first three objects fetched by SELECT * FROM people ORDER BY people.id LIMIT 3
#
def first(limit = nil)
if limit
diff --git a/activerecord/lib/active_record/relation/merger.rb b/activerecord/lib/active_record/relation/merger.rb
index 65b607ff1c..cb971eb255 100644
--- a/activerecord/lib/active_record/relation/merger.rb
+++ b/activerecord/lib/active_record/relation/merger.rb
@@ -1,5 +1,4 @@
require 'active_support/core_ext/hash/keys'
-require "set"
module ActiveRecord
class Relation
@@ -51,7 +50,7 @@ module ActiveRecord
NORMAL_VALUES = Relation::VALUE_METHODS -
Relation::CLAUSE_METHODS -
- [:joins, :order, :reverse_order, :lock, :create_with, :reordering] # :nodoc:
+ [:includes, :preload, :joins, :order, :reverse_order, :lock, :create_with, :reordering] # :nodoc:
def normal_values
NORMAL_VALUES
@@ -76,6 +75,7 @@ module ActiveRecord
merge_multi_values
merge_single_values
merge_clauses
+ merge_preloads
merge_joins
relation
@@ -83,6 +83,27 @@ module ActiveRecord
private
+ def merge_preloads
+ return if other.preload_values.empty? && other.includes_values.empty?
+
+ if other.klass == relation.klass
+ relation.preload!(*other.preload_values) unless other.preload_values.empty?
+ relation.includes!(other.includes_values) unless other.includes_values.empty?
+ else
+ reflection = relation.klass.reflect_on_all_associations.find do |r|
+ r.class_name == other.klass.name
+ end || return
+
+ unless other.preload_values.empty?
+ relation.preload! reflection.name => other.preload_values
+ end
+
+ unless other.includes_values.empty?
+ relation.includes! reflection.name => other.includes_values
+ end
+ end
+ end
+
def merge_joins
return if other.joins_values.blank?
@@ -127,11 +148,15 @@ module ActiveRecord
end
end
+ CLAUSE_METHOD_NAMES = CLAUSE_METHODS.map do |name|
+ ["#{name}_clause", "#{name}_clause="]
+ end
+
def merge_clauses
- CLAUSE_METHODS.each do |name|
- clause = relation.send("#{name}_clause")
- other_clause = other.send("#{name}_clause")
- relation.send("#{name}_clause=", clause.merge(other_clause))
+ CLAUSE_METHOD_NAMES.each do |(reader, writer)|
+ clause = relation.send(reader)
+ other_clause = other.send(reader)
+ relation.send(writer, clause.merge(other_clause))
end
end
end
diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb
index 43e9afe853..39e7b42629 100644
--- a/activerecord/lib/active_record/relation/predicate_builder.rb
+++ b/activerecord/lib/active_record/relation/predicate_builder.rb
@@ -24,12 +24,12 @@ module ActiveRecord
end
def build_from_hash(attributes)
- attributes = convert_dot_notation_to_hash(attributes.stringify_keys)
+ attributes = convert_dot_notation_to_hash(attributes)
expand_from_hash(attributes)
end
def create_binds(attributes)
- attributes = convert_dot_notation_to_hash(attributes.stringify_keys)
+ attributes = convert_dot_notation_to_hash(attributes)
create_binds_for_hash(attributes)
end
@@ -52,7 +52,7 @@ module ActiveRecord
key
else
key = key.to_s
- key.split('.').first if key.include?('.')
+ key.split('.'.freeze).first if key.include?('.'.freeze)
end
end.compact
end
@@ -67,7 +67,7 @@ module ActiveRecord
# Arel::Nodes::And.new([range.start, range.end])
# )
# end
- # ActiveRecord::PredicateBuilder.register_handler(MyCustomDateRange, handler)
+ # ActiveRecord::PredicateBuilder.new("users").register_handler(MyCustomDateRange, handler)
def register_handler(klass, handler)
@handlers.unshift([klass, handler])
end
@@ -123,10 +123,10 @@ module ActiveRecord
end
def convert_dot_notation_to_hash(attributes)
- dot_notation = attributes.keys.select { |s| s.include?(".") }
+ dot_notation = attributes.keys.select { |s| s.include?(".".freeze) }
dot_notation.each do |key|
- table_name, column_name = key.split(".")
+ table_name, column_name = key.split(".".freeze)
value = attributes.delete(key)
attributes[table_name] ||= {}
diff --git a/activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb
index 159889d3b8..e81be63cd3 100644
--- a/activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb
+++ b/activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb
@@ -10,10 +10,10 @@ module ActiveRecord
table = value.associated_table
if value.base_class
- queries[table.association_foreign_type] = value.base_class.name
+ queries[table.association_foreign_type.to_s] = value.base_class.name
end
- queries[table.association_foreign_key] = value.ids
+ queries[table.association_foreign_key.to_s] = value.ids
predicate_builder.build_from_hash(queries)
end
diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb
index 69ce5cdc2a..ccb0ab18ae 100644
--- a/activerecord/lib/active_record/relation/query_methods.rb
+++ b/activerecord/lib/active_record/relation/query_methods.rb
@@ -242,18 +242,15 @@ module ActiveRecord
# Model.select(:field).first.other_field
# # => ActiveModel::MissingAttributeError: missing attribute: other_field
def select(*fields)
- if block_given?
- to_a.select { |*block_args| yield(*block_args) }
- else
- raise ArgumentError, 'Call this with at least one field' if fields.empty?
- spawn._select!(*fields)
- end
+ return super if block_given?
+ raise ArgumentError, 'Call this with at least one field' if fields.empty?
+ spawn._select!(*fields)
end
def _select!(*fields) # :nodoc:
fields.flatten!
fields.map! do |field|
- klass.attribute_alias?(field) ? klass.attribute_alias(field) : field
+ klass.attribute_alias?(field) ? klass.attribute_alias(field).to_sym : field
end
self.select_values += fields
self
@@ -267,7 +264,7 @@ module ActiveRecord
# Returns an array with distinct records based on the +group+ attribute:
#
# User.select([:id, :name])
- # => [#<User id: 1, name: "Oscar">, #<User id: 2, name: "Oscar">, #<User id: 3, name: "Foo">
+ # => [#<User id: 1, name: "Oscar">, #<User id: 2, name: "Oscar">, #<User id: 3, name: "Foo">]
#
# User.group(:name)
# => [#<User id: 3, name: "Foo", ...>, #<User id: 2, name: "Oscar", ...>]
@@ -551,7 +548,7 @@ module ActiveRecord
# If the condition is any blank-ish object, then #where is a no-op and returns
# the current relation.
def where(opts = :chain, *rest)
- if opts == :chain
+ if :chain == opts
WhereChain.new(spawn)
elsif opts.blank?
self
@@ -561,11 +558,8 @@ module ActiveRecord
end
def where!(opts, *rest) # :nodoc:
- if Hash === opts
- opts = sanitize_forbidden_attributes(opts)
- references!(PredicateBuilder.references(opts))
- end
-
+ opts = sanitize_forbidden_attributes(opts)
+ references!(PredicateBuilder.references(opts)) if Hash === opts
self.where_clause += where_clause_factory.build(opts, rest)
self
end
@@ -587,7 +581,7 @@ module ActiveRecord
#
# The two relations must be structurally compatible: they must be scoping the same model, and
# they must differ only by +where+ (if no +group+ has been defined) or +having+ (if a +group+ is
- # present). Neither relation may have a +limit+, +offset+, or +uniq+ set.
+ # present). Neither relation may have a +limit+, +offset+, or +distinct+ set.
#
# Post.where("id = 1").or(Post.where("id = 2"))
# # SELECT `posts`.* FROM `posts` WHERE (('id = 1' OR 'id = 2'))
@@ -622,6 +616,7 @@ module ActiveRecord
end
def having!(opts, *rest) # :nodoc:
+ opts = sanitize_forbidden_attributes(opts)
references!(PredicateBuilder.references(opts)) if Hash === opts
self.having_clause += having_clause_factory.build(opts, rest)
@@ -716,7 +711,7 @@ module ActiveRecord
#
# users = User.readonly
# users.first.save
- # => ActiveRecord::ReadOnlyRecord: ActiveRecord::ReadOnlyRecord
+ # => ActiveRecord::ReadOnlyRecord: User is marked as readonly
def readonly(value = true)
spawn.readonly!(value)
end
@@ -790,6 +785,7 @@ module ActiveRecord
spawn.distinct!(value)
end
alias uniq distinct
+ deprecate uniq: :distinct
# Like #distinct, but modifies relation in place.
def distinct!(value = true) # :nodoc:
@@ -797,6 +793,7 @@ module ActiveRecord
self
end
alias uniq! distinct!
+ deprecate uniq!: :distinct!
# Used to extend a scope with additional methods, either through
# a module or through a block provided.
@@ -999,15 +996,13 @@ module ActiveRecord
end
def arel_columns(columns)
- if from_clause.value
- columns
- else
- columns.map do |field|
- if (Symbol === field || String === field) && columns_hash.key?(field.to_s)
- arel_table[field]
- else
- field
- end
+ columns.map do |field|
+ if (Symbol === field || String === field) && columns_hash.key?(field.to_s) && !from_clause.value
+ arel_table[field]
+ elsif Symbol === field
+ connection.quote_table_name(field.to_s)
+ else
+ field
end
end
end
diff --git a/activerecord/lib/active_record/relation/where_clause.rb b/activerecord/lib/active_record/relation/where_clause.rb
index f9b9e640ec..1f000b3f0f 100644
--- a/activerecord/lib/active_record/relation/where_clause.rb
+++ b/activerecord/lib/active_record/relation/where_clause.rb
@@ -135,7 +135,7 @@ module ActiveRecord
def predicates_except(columns)
predicates.reject do |node|
case node
- when Arel::Nodes::Between, Arel::Nodes::In, Arel::Nodes::NotIn, Arel::Nodes::Equality, Arel::Nodes::NotEqual, Arel::Nodes::LessThanOrEqual, Arel::Nodes::GreaterThanOrEqual
+ when Arel::Nodes::Between, Arel::Nodes::In, Arel::Nodes::NotIn, Arel::Nodes::Equality, Arel::Nodes::NotEqual, Arel::Nodes::LessThan, Arel::Nodes::LessThanOrEqual, Arel::Nodes::GreaterThan, Arel::Nodes::GreaterThanOrEqual
subrelation = (node.left.kind_of?(Arel::Attributes::Attribute) ? node.left : node.right)
columns.include?(subrelation.name.to_s)
end
diff --git a/activerecord/lib/active_record/relation/where_clause_factory.rb b/activerecord/lib/active_record/relation/where_clause_factory.rb
index 0430922be3..23eaab4699 100644
--- a/activerecord/lib/active_record/relation/where_clause_factory.rb
+++ b/activerecord/lib/active_record/relation/where_clause_factory.rb
@@ -15,6 +15,7 @@ module ActiveRecord
when Hash
attributes = predicate_builder.resolve_column_aliases(opts)
attributes = klass.send(:expand_hash_conditions_for_aggregates, attributes)
+ attributes.stringify_keys!
attributes, binds = predicate_builder.create_binds(attributes)
diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb
index c7f55ebaa1..7f6664ea50 100644
--- a/activerecord/lib/active_record/sanitization.rb
+++ b/activerecord/lib/active_record/sanitization.rb
@@ -4,24 +4,29 @@ module ActiveRecord
module ClassMethods
# Used to sanitize objects before they're used in an SQL SELECT statement. Delegates to <tt>connection.quote</tt>.
- def sanitize(object) #:nodoc:
+ def sanitize(object) # :nodoc:
connection.quote(object)
end
alias_method :quote_value, :sanitize
protected
- # Accepts an array, hash, or string of SQL conditions and sanitizes
+ # Accepts an array or string of SQL conditions and sanitizes
# them into a valid SQL fragment for a WHERE clause.
- # ["name='%s' and group_id='%s'", "foo'bar", 4] returns "name='foo''bar' and group_id='4'"
- # { name: "foo'bar", group_id: 4 } returns "name='foo''bar' and group_id='4'"
- # "name='foo''bar' and group_id='4'" returns "name='foo''bar' and group_id='4'"
- def sanitize_sql_for_conditions(condition, table_name = self.table_name)
+ #
+ # sanitize_sql_for_conditions(["name=? and group_id=?", "foo'bar", 4])
+ # # => "name='foo''bar' and group_id=4"
+ #
+ # sanitize_sql_for_conditions(["name='%s' and group_id='%s'", "foo'bar", 4])
+ # # => "name='foo''bar' and group_id='4'"
+ #
+ # sanitize_sql_for_conditions("name='foo''bar' and group_id='4'")
+ # # => "name='foo''bar' and group_id='4'"
+ def sanitize_sql_for_conditions(condition)
return nil if condition.blank?
case condition
when Array; sanitize_sql_array(condition)
- when Hash; sanitize_sql_hash_for_conditions(condition, table_name)
else condition
end
end
@@ -30,7 +35,15 @@ module ActiveRecord
# Accepts an array, hash, or string of SQL conditions and sanitizes
# them into a valid SQL fragment for a SET clause.
- # { name: nil, group_id: 4 } returns "name = NULL , group_id='4'"
+ #
+ # sanitize_sql_for_assignment(["name=? and group_id=?", nil, 4])
+ # # => "name=NULL and group_id=4"
+ #
+ # Post.send(:sanitize_sql_for_assignment, { name: nil, group_id: 4 })
+ # # => "`posts`.`name` = NULL, `posts`.`group_id` = 4"
+ #
+ # sanitize_sql_for_assignment("name=NULL and group_id='4'")
+ # # => "name=NULL and group_id='4'"
def sanitize_sql_for_assignment(assignments, default_table_name = self.table_name)
case assignments
when Array; sanitize_sql_array(assignments)
@@ -42,14 +55,18 @@ module ActiveRecord
# Accepts a hash of SQL conditions and replaces those attributes
# that correspond to a +composed_of+ relationship with their expanded
# aggregate attribute values.
+ #
# Given:
- # class Person < ActiveRecord::Base
- # composed_of :address, class_name: "Address",
- # mapping: [%w(address_street street), %w(address_city city)]
- # end
+ #
+ # class Person < ActiveRecord::Base
+ # composed_of :address, class_name: "Address",
+ # mapping: [%w(address_street street), %w(address_city city)]
+ # end
+ #
# Then:
- # { address: Address.new("813 abc st.", "chicago") }
- # # => { address_street: "813 abc st.", address_city: "chicago" }
+ #
+ # { address: Address.new("813 abc st.", "chicago") }
+ # # => { address_street: "813 abc st.", address_city: "chicago" }
def expand_hash_conditions_for_aggregates(attrs)
expanded_attrs = {}
attrs.each do |attr, value|
@@ -70,8 +87,9 @@ module ActiveRecord
end
# Sanitizes a hash of attribute/value pairs into SQL conditions for a SET clause.
- # { status: nil, group_id: 1 }
- # # => "status = NULL , group_id = 1"
+ #
+ # sanitize_sql_hash_for_assignment({ status: nil, group_id: 1 }, "posts")
+ # # => "`posts`.`status` = NULL, `posts`.`group_id` = 1"
def sanitize_sql_hash_for_assignment(attrs, table)
c = connection
attrs.map do |attr, value|
@@ -81,7 +99,19 @@ module ActiveRecord
end
# 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 "%"
+ # LIKE statement. This method uses +escape_character+ to escape all occurrences of "\", "_" and "%".
+ #
+ # sanitize_sql_like("100%")
+ # # => "100\\%"
+ #
+ # sanitize_sql_like("snake_cased_string")
+ # # => "snake\\_cased\\_string"
+ #
+ # sanitize_sql_like("100%", "!")
+ # # => "100!%"
+ #
+ # sanitize_sql_like("snake_cased_string", "!")
+ # # => "snake!_cased!_string"
def sanitize_sql_like(string, escape_character = "\\")
pattern = Regexp.union(escape_character, "%", "_")
string.gsub(pattern) { |x| [escape_character, x].join }
@@ -89,7 +119,12 @@ module ActiveRecord
# Accepts an array of conditions. The array has each value
# sanitized and interpolated into the SQL statement.
- # ["name='%s' and group_id='%s'", "foo'bar", 4] returns "name='foo''bar' and group_id='4'"
+ #
+ # sanitize_sql_array(["name=? and group_id=?", "foo'bar", 4])
+ # # => "name='foo''bar' and group_id=4"
+ #
+ # sanitize_sql_array(["name='%s' and group_id='%s'", "foo'bar", 4])
+ # # => "name='foo''bar' and group_id='4'"
def sanitize_sql_array(ary)
statement, *values = ary
if values.first.is_a?(Hash) && statement =~ /:\w+/
@@ -103,7 +138,7 @@ module ActiveRecord
end
end
- def replace_bind_variables(statement, values) #:nodoc:
+ def replace_bind_variables(statement, values) # :nodoc:
raise_if_bind_arity_mismatch(statement, statement.count('?'), values.size)
bound = values.dup
c = connection
@@ -112,7 +147,7 @@ module ActiveRecord
end
end
- def replace_bind_variable(value, c = connection) #:nodoc:
+ def replace_bind_variable(value, c = connection) # :nodoc:
if ActiveRecord::Relation === value
value.to_sql
else
@@ -120,10 +155,10 @@ module ActiveRecord
end
end
- def replace_named_bind_variables(statement, bind_vars) #:nodoc:
- statement.gsub(/(:?):([a-zA-Z]\w*)/) do
+ def replace_named_bind_variables(statement, bind_vars) # :nodoc:
+ statement.gsub(/(:?):([a-zA-Z]\w*)/) do |match|
if $1 == ':' # skip postgresql casts
- $& # return the whole match
+ match # return the whole match
elsif bind_vars.include?(match = $2.to_sym)
replace_bind_variable(bind_vars[match])
else
@@ -132,7 +167,7 @@ module ActiveRecord
end
end
- def quote_bound_value(value, c = connection) #:nodoc:
+ def quote_bound_value(value, c = connection) # :nodoc:
if value.respond_to?(:map) && !value.acts_like?(:string)
if value.respond_to?(:empty?) && value.empty?
c.quote(nil)
@@ -144,7 +179,7 @@ module ActiveRecord
end
end
- def raise_if_bind_arity_mismatch(statement, expected, provided) #:nodoc:
+ def raise_if_bind_arity_mismatch(statement, expected, provided) # :nodoc:
unless expected == provided
raise PreparedStatementInvalid, "wrong number of bind variables (#{provided} for #{expected}) in: #{statement}"
end
diff --git a/activerecord/lib/active_record/schema.rb b/activerecord/lib/active_record/schema.rb
index 0a5546a760..c1a42dc629 100644
--- a/activerecord/lib/active_record/schema.rb
+++ b/activerecord/lib/active_record/schema.rb
@@ -28,24 +28,6 @@ module ActiveRecord
# ActiveRecord::Schema is only supported by database adapters that also
# support migrations, the two features being very similar.
class Schema < Migration
-
- # Returns the migrations paths.
- #
- # ActiveRecord::Schema.new.migrations_paths
- # # => ["db/migrate"] # Rails migration path by default.
- def migrations_paths
- ActiveRecord::Migrator.migrations_paths
- end
-
- def define(info, &block) # :nodoc:
- instance_eval(&block)
-
- unless info[:version].blank?
- initialize_schema_migrations_table
- connection.assume_migrated_upto_version(info[:version], migrations_paths)
- end
- end
-
# Eval the given block. All methods available to the current connection
# adapter are available within the block, so you can easily use the
# database definition DSL to build up your schema (+create_table+,
@@ -60,5 +42,23 @@ module ActiveRecord
def self.define(info={}, &block)
new.define(info, &block)
end
+
+ def define(info, &block) # :nodoc:
+ instance_eval(&block)
+
+ if info[:version].present?
+ initialize_schema_migrations_table
+ connection.assume_migrated_upto_version(info[:version], migrations_paths)
+ end
+ end
+
+ private
+ # Returns the migrations paths.
+ #
+ # ActiveRecord::Schema.new.migrations_paths
+ # # => ["db/migrate"] # Rails migration path by default.
+ def migrations_paths # :nodoc:
+ ActiveRecord::Migrator.migrations_paths
+ end
end
end
diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb
index eaeaf0321b..2362dae9fc 100644
--- a/activerecord/lib/active_record/schema_dumper.rb
+++ b/activerecord/lib/active_record/schema_dumper.rb
@@ -1,5 +1,4 @@
require 'stringio'
-require 'active_support/core_ext/big_decimal'
module ActiveRecord
# = Active Record Schema Dumper
@@ -90,7 +89,7 @@ HEADER
end
def tables(stream)
- sorted_tables = @connection.tables.sort
+ sorted_tables = @connection.data_sources.sort - @connection.views
sorted_tables.each do |table_name|
table(table_name, stream) unless ignored?(table_name)
@@ -113,24 +112,35 @@ HEADER
tbl = StringIO.new
# first dump primary key column
- pk = @connection.primary_key(table)
+ if @connection.respond_to?(:primary_keys)
+ pk = @connection.primary_keys(table)
+ pk = pk.first unless pk.size > 1
+ else
+ pk = @connection.primary_key(table)
+ end
tbl.print " create_table #{remove_prefix_and_suffix(table).inspect}"
- pkcol = columns.detect { |c| c.name == pk }
- if pkcol
- if pk != 'id'
- tbl.print %Q(, primary_key: "#{pk}")
- end
+
+ case pk
+ when String
+ tbl.print ", primary_key: #{pk.inspect}" unless pk == 'id'
+ pkcol = columns.detect { |c| c.name == pk }
pkcolspec = @connection.column_spec_for_primary_key(pkcol)
if pkcolspec
pkcolspec.each do |key, value|
tbl.print ", #{key}: #{value}"
end
end
+ when Array
+ tbl.print ", primary_key: #{pk.inspect}"
else
tbl.print ", id: false"
end
tbl.print ", force: :cascade"
+
+ table_options = @connection.table_options(table)
+ tbl.print ", options: #{table_options.inspect}" unless table_options.blank?
+
tbl.puts " do |t|"
# then dump all non-primary key columns
@@ -168,11 +178,11 @@ HEADER
tbl.puts
end
+ indexes(table, tbl)
+
tbl.puts " end"
tbl.puts
- indexes(table, tbl)
-
tbl.rewind
stream.print tbl.read
rescue => e
@@ -188,8 +198,7 @@ HEADER
if (indexes = @connection.indexes(table)).any?
add_index_statements = indexes.map do |index|
statement_parts = [
- "add_index #{remove_prefix_and_suffix(index.table).inspect}",
- index.columns.inspect,
+ "t.index #{index.columns.inspect}",
"name: #{index.name.inspect}",
]
statement_parts << 'unique: true' if index.unique
@@ -203,11 +212,10 @@ HEADER
statement_parts << "using: #{index.using.inspect}" if index.using
statement_parts << "type: #{index.type.inspect}" if index.type
- " #{statement_parts.join(', ')}"
+ " #{statement_parts.join(', ')}"
end
stream.puts add_index_statements.sort.join("\n")
- stream.puts
end
end
@@ -246,7 +254,7 @@ HEADER
end
def ignored?(table_name)
- ['schema_migrations', ignore_tables].flatten.any? do |ignored|
+ [ActiveRecord::Base.schema_migrations_table_name, ignore_tables].flatten.any? do |ignored|
ignored === remove_prefix_and_suffix(table_name)
end
end
diff --git a/activerecord/lib/active_record/schema_migration.rb b/activerecord/lib/active_record/schema_migration.rb
index b5038104ac..b384529e75 100644
--- a/activerecord/lib/active_record/schema_migration.rb
+++ b/activerecord/lib/active_record/schema_migration.rb
@@ -1,9 +1,12 @@
require 'active_record/scoping/default'
require 'active_record/scoping/named'
-require 'active_record/base'
module ActiveRecord
- class SchemaMigration < ActiveRecord::Base
+ # This class is used to create a table that keeps track of which migrations
+ # have been applied to a given database. When a migration is run, its schema
+ # number is inserted in to the `SchemaMigration.table_name` so it doesn't need
+ # to be executed the next time.
+ class SchemaMigration < ActiveRecord::Base # :nodoc:
class << self
def primary_key
nil
diff --git a/activerecord/lib/active_record/scoping/default.rb b/activerecord/lib/active_record/scoping/default.rb
index 3590b8846e..fac566e12b 100644
--- a/activerecord/lib/active_record/scoping/default.rb
+++ b/activerecord/lib/active_record/scoping/default.rb
@@ -6,8 +6,10 @@ module ActiveRecord
included do
# Stores the default scope for the class.
class_attribute :default_scopes, instance_writer: false, instance_predicate: false
+ class_attribute :default_scope_override, instance_predicate: false
self.default_scopes = []
+ self.default_scope_override = nil
end
module ClassMethods
@@ -99,11 +101,18 @@ module ActiveRecord
self.default_scopes += [scope]
end
- def build_default_scope(base_rel = relation) # :nodoc:
- if !Base.is_a?(method(:default_scope).owner)
+ def build_default_scope(base_rel = nil) # :nodoc:
+ return if abstract_class?
+
+ if self.default_scope_override.nil?
+ self.default_scope_override = !Base.is_a?(method(:default_scope).owner)
+ end
+
+ if self.default_scope_override
# The user has defined their own default scope method, so call that
evaluate_default_scope { default_scope }
elsif default_scopes.any?
+ base_rel ||= relation
evaluate_default_scope do
default_scopes.inject(base_rel) do |default_scope, scope|
default_scope.merge(base_rel.scoping { scope.call })
diff --git a/activerecord/lib/active_record/secure_token.rb b/activerecord/lib/active_record/secure_token.rb
index a3023a0cb4..ca11853da7 100644
--- a/activerecord/lib/active_record/secure_token.rb
+++ b/activerecord/lib/active_record/secure_token.rb
@@ -13,7 +13,7 @@ module ActiveRecord
#
# user = User.new
# user.save
- # user.token # => "4kUgL2pdQMSCQtjE"
+ # user.token # => "pX27zsMN2ViQKta1bGfLmVJE"
# user.auth_token # => "77TMHrHJFvFDwodq8w7Ev2m7"
# user.regenerate_token # => true
# user.regenerate_auth_token # => true
diff --git a/activerecord/lib/active_record/serialization.rb b/activerecord/lib/active_record/serialization.rb
index 48c12dcf9f..23dc6465af 100644
--- a/activerecord/lib/active_record/serialization.rb
+++ b/activerecord/lib/active_record/serialization.rb
@@ -18,5 +18,3 @@ module ActiveRecord #:nodoc:
end
end
end
-
-require 'active_record/serializers/xml_serializer'
diff --git a/activerecord/lib/active_record/serializers/xml_serializer.rb b/activerecord/lib/active_record/serializers/xml_serializer.rb
deleted file mode 100644
index 89b7e0be82..0000000000
--- a/activerecord/lib/active_record/serializers/xml_serializer.rb
+++ /dev/null
@@ -1,193 +0,0 @@
-require 'active_support/core_ext/hash/conversions'
-
-module ActiveRecord #:nodoc:
- module Serialization
- include ActiveModel::Serializers::Xml
-
- # Builds an XML document to represent the model. Some configuration is
- # available through +options+. However more complicated cases should
- # override ActiveRecord::Base#to_xml.
- #
- # By default the generated XML document will include the processing
- # instruction and all the object's attributes. For example:
- #
- # <?xml version="1.0" encoding="UTF-8"?>
- # <topic>
- # <title>The First Topic</title>
- # <author-name>David</author-name>
- # <id type="integer">1</id>
- # <approved type="boolean">false</approved>
- # <replies-count type="integer">0</replies-count>
- # <bonus-time type="dateTime">2000-01-01T08:28:00+12:00</bonus-time>
- # <written-on type="dateTime">2003-07-16T09:28:00+1200</written-on>
- # <content>Have a nice day</content>
- # <author-email-address>david@loudthinking.com</author-email-address>
- # <parent-id></parent-id>
- # <last-read type="date">2004-04-15</last-read>
- # </topic>
- #
- # This behavior can be controlled with <tt>:only</tt>, <tt>:except</tt>,
- # <tt>:skip_instruct</tt>, <tt>:skip_types</tt>, <tt>:dasherize</tt> and <tt>:camelize</tt> .
- # The <tt>:only</tt> and <tt>:except</tt> options are the same as for the
- # +attributes+ method. The default is to dasherize all column names, but you
- # can disable this setting <tt>:dasherize</tt> to +false+. Setting <tt>:camelize</tt>
- # to +true+ will camelize all column names - this also overrides <tt>:dasherize</tt>.
- # To not have the column type included in the XML output set <tt>:skip_types</tt> to +true+.
- #
- # For instance:
- #
- # topic.to_xml(skip_instruct: true, except: [ :id, :bonus_time, :written_on, :replies_count ])
- #
- # <topic>
- # <title>The First Topic</title>
- # <author-name>David</author-name>
- # <approved type="boolean">false</approved>
- # <content>Have a nice day</content>
- # <author-email-address>david@loudthinking.com</author-email-address>
- # <parent-id></parent-id>
- # <last-read type="date">2004-04-15</last-read>
- # </topic>
- #
- # To include first level associations use <tt>:include</tt>:
- #
- # firm.to_xml include: [ :account, :clients ]
- #
- # <?xml version="1.0" encoding="UTF-8"?>
- # <firm>
- # <id type="integer">1</id>
- # <rating type="integer">1</rating>
- # <name>37signals</name>
- # <clients type="array">
- # <client>
- # <rating type="integer">1</rating>
- # <name>Summit</name>
- # </client>
- # <client>
- # <rating type="integer">1</rating>
- # <name>Microsoft</name>
- # </client>
- # </clients>
- # <account>
- # <id type="integer">1</id>
- # <credit-limit type="integer">50</credit-limit>
- # </account>
- # </firm>
- #
- # Additionally, the record being serialized will be passed to a Proc's second
- # parameter. This allows for ad hoc additions to the resultant document that
- # incorporate the context of the record being serialized. And by leveraging the
- # closure created by a Proc, to_xml can be used to add elements that normally fall
- # outside of the scope of the model -- for example, generating and appending URLs
- # associated with models.
- #
- # proc = Proc.new { |options, record| options[:builder].tag!('name-reverse', record.name.reverse) }
- # firm.to_xml procs: [ proc ]
- #
- # <firm>
- # # ... normal attributes as shown above ...
- # <name-reverse>slangis73</name-reverse>
- # </firm>
- #
- # To include deeper levels of associations pass a hash like this:
- #
- # firm.to_xml include: {account: {}, clients: {include: :address}}
- # <?xml version="1.0" encoding="UTF-8"?>
- # <firm>
- # <id type="integer">1</id>
- # <rating type="integer">1</rating>
- # <name>37signals</name>
- # <clients type="array">
- # <client>
- # <rating type="integer">1</rating>
- # <name>Summit</name>
- # <address>
- # ...
- # </address>
- # </client>
- # <client>
- # <rating type="integer">1</rating>
- # <name>Microsoft</name>
- # <address>
- # ...
- # </address>
- # </client>
- # </clients>
- # <account>
- # <id type="integer">1</id>
- # <credit-limit type="integer">50</credit-limit>
- # </account>
- # </firm>
- #
- # To include any methods on the model being called use <tt>:methods</tt>:
- #
- # firm.to_xml methods: [ :calculated_earnings, :real_earnings ]
- #
- # <firm>
- # # ... normal attributes as shown above ...
- # <calculated-earnings>100000000000000000</calculated-earnings>
- # <real-earnings>5</real-earnings>
- # </firm>
- #
- # To call any additional Procs use <tt>:procs</tt>. The Procs are passed a
- # modified version of the options hash that was given to +to_xml+:
- #
- # proc = Proc.new { |options| options[:builder].tag!('abc', 'def') }
- # firm.to_xml procs: [ proc ]
- #
- # <firm>
- # # ... normal attributes as shown above ...
- # <abc>def</abc>
- # </firm>
- #
- # Alternatively, you can yield the builder object as part of the +to_xml+ call:
- #
- # firm.to_xml do |xml|
- # xml.creator do
- # xml.first_name "David"
- # xml.last_name "Heinemeier Hansson"
- # end
- # end
- #
- # <firm>
- # # ... normal attributes as shown above ...
- # <creator>
- # <first_name>David</first_name>
- # <last_name>Heinemeier Hansson</last_name>
- # </creator>
- # </firm>
- #
- # As noted above, you may override +to_xml+ in your ActiveRecord::Base
- # subclasses to have complete control about what's generated. The general
- # form of doing this is:
- #
- # class IHaveMyOwnXML < ActiveRecord::Base
- # def to_xml(options = {})
- # require 'builder'
- # options[:indent] ||= 2
- # xml = options[:builder] ||= ::Builder::XmlMarkup.new(indent: options[:indent])
- # xml.instruct! unless options[:skip_instruct]
- # xml.level_one do
- # xml.tag!(:second_level, 'content')
- # end
- # end
- # end
- def to_xml(options = {}, &block)
- XmlSerializer.new(self, options).serialize(&block)
- end
- end
-
- class XmlSerializer < ActiveModel::Serializers::Xml::Serializer #:nodoc:
- class Attribute < ActiveModel::Serializers::Xml::Serializer::Attribute #:nodoc:
- def compute_type
- klass = @serializable.class
- cast_type = klass.type_for_attribute(name)
-
- type = ActiveSupport::XmlMini::TYPE_NAMES[value.class.name] || cast_type.type
-
- { :text => :string,
- :time => :datetime }[type] || type
- end
- protected :compute_type
- end
- end
-end
diff --git a/activerecord/lib/active_record/suppressor.rb b/activerecord/lib/active_record/suppressor.rb
index b0b86865fd..b3644bf569 100644
--- a/activerecord/lib/active_record/suppressor.rb
+++ b/activerecord/lib/active_record/suppressor.rb
@@ -37,8 +37,7 @@ module ActiveRecord
end
end
- # Ignore saving events if we're in suppression mode.
- def save!(*args) # :nodoc:
+ def create_or_update(*args) # :nodoc:
SuppressorRegistry.suppressed[self.class.name] ? true : super
end
end
diff --git a/activerecord/lib/active_record/table_metadata.rb b/activerecord/lib/active_record/table_metadata.rb
index 3dd6321a97..f9bb1cf5e0 100644
--- a/activerecord/lib/active_record/table_metadata.rb
+++ b/activerecord/lib/active_record/table_metadata.rb
@@ -10,13 +10,15 @@ module ActiveRecord
end
def resolve_column_aliases(hash)
- hash = hash.dup
- hash.keys.grep(Symbol) do |key|
- if klass.attribute_alias? key
- hash[klass.attribute_alias(key)] = hash.delete key
+ # This method is a hot spot, so for now, use Hash[] to dup the hash.
+ # https://bugs.ruby-lang.org/issues/7166
+ new_hash = Hash[hash]
+ hash.each do |key, _|
+ if (key.is_a?(Symbol)) && klass.attribute_alias?(key)
+ new_hash[klass.attribute_alias(key)] = new_hash.delete(key)
end
end
- hash
+ new_hash
end
def arel_attribute(column_name)
@@ -41,7 +43,7 @@ module ActiveRecord
association = klass._reflect_on_association(table_name)
if association && !association.polymorphic?
association_klass = association.klass
- arel_table = association_klass.arel_table
+ arel_table = association_klass.arel_table.alias(table_name)
else
type_caster = TypeCaster::Connection.new(klass, table_name)
association_klass = nil
diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb
index 683741768b..f1141a8613 100644
--- a/activerecord/lib/active_record/tasks/database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/database_tasks.rb
@@ -134,7 +134,7 @@ module ActiveRecord
version = ENV["VERSION"] ? ENV["VERSION"].to_i : nil
scope = ENV['SCOPE']
verbose_was, Migration.verbose = Migration.verbose, verbose
- Migrator.migrate(Migrator.migrations_paths, version) do |migration|
+ Migrator.migrate(migrations_paths, version) do |migration|
scope.blank? || scope == migration.scope
end
ensure
@@ -221,12 +221,6 @@ module ActiveRecord
end
end
- def load_schema_current_if_exists(format = ActiveRecord::Base.schema_format, file = nil, environment = env)
- if File.exist?(file || schema_file(format))
- load_schema_current(format, file, environment)
- end
- end
-
def load_schema_current(format = ActiveRecord::Base.schema_format, file = nil, environment = env)
each_current_configuration(environment) { |configuration|
load_schema configuration, format, file
diff --git a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb
index eafbb2c249..8929aa85c8 100644
--- a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb
@@ -23,7 +23,7 @@ module ActiveRecord
end
rescue error_class => error
if error.respond_to?(:errno) && error.errno == ACCESS_DENIED_ERROR
- $stdout.print error.error
+ $stdout.print error.message
establish_connection root_configuration_without_database
connection.create_database configuration['database'], creation_options
if configuration['username'] != 'root'
@@ -56,21 +56,21 @@ module ActiveRecord
end
def structure_dump(filename)
- args = prepare_command_options('mysqldump')
+ args = prepare_command_options
args.concat(["--result-file", "#{filename}"])
args.concat(["--no-data"])
+ args.concat(["--routines"])
args.concat(["#{configuration['database']}"])
- unless Kernel.system(*args)
- $stderr.puts "Could not dump the database structure. "\
- "Make sure `mysqldump` is in your PATH and check the command output for warnings."
- end
+
+ run_cmd('mysqldump', args, 'dumping')
end
def structure_load(filename)
- args = prepare_command_options('mysql')
+ args = prepare_command_options
args.concat(['--execute', %{SET FOREIGN_KEY_CHECKS = 0; SOURCE #{filename}; SET FOREIGN_KEY_CHECKS = 1}])
args.concat(["--database", "#{configuration['database']}"])
- Kernel.system(*args)
+
+ run_cmd('mysql', args, 'loading')
end
private
@@ -129,17 +129,33 @@ IDENTIFIED BY '#{configuration['password']}' WITH GRANT OPTION;
$stdin.gets.strip
end
- def prepare_command_options(command)
- args = [command]
- args.concat(['--user', configuration['username']]) if configuration['username']
- args << "--password=#{configuration['password']}" if configuration['password']
- args.concat(['--default-character-set', configuration['encoding']]) if configuration['encoding']
- configuration.slice('host', 'port', 'socket').each do |k, v|
- args.concat([ "--#{k}", v.to_s ]) if v
- end
+ def prepare_command_options
+ args = {
+ 'host' => '--host',
+ 'port' => '--port',
+ 'socket' => '--socket',
+ 'username' => '--user',
+ 'password' => '--password',
+ 'encoding' => '--default-character-set',
+ 'sslca' => '--ssl-ca',
+ 'sslcert' => '--ssl-cert',
+ 'sslcapath' => '--ssl-capath',
+ 'sslcipher' => '--ssh-cipher',
+ 'sslkey' => '--ssl-key'
+ }.map { |opt, arg| "#{arg}=#{configuration[opt]}" if configuration[opt] }.compact
args
end
+
+ def run_cmd(cmd, args, action)
+ fail run_cmd_error(cmd, args, action) unless Kernel.system(cmd, *args)
+ end
+
+ def run_cmd_error(cmd, args, action)
+ msg = "failed to execute: `#{cmd}`\n"
+ msg << "Please check the output above for any errors and make sure that `#{cmd}` is installed in your PATH and has proper permissions.\n\n"
+ msg
+ end
end
end
end
diff --git a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb
index d7da95c8a9..55f839444b 100644
--- a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb
@@ -1,5 +1,3 @@
-require 'shellwords'
-
module ActiveRecord
module Tasks # :nodoc:
class PostgreSQLDatabaseTasks # :nodoc:
@@ -55,19 +53,22 @@ module ActiveRecord
when String
ActiveRecord::Base.dump_schemas
end
+
+ args = ['-i', '-s', '-x', '-O', '-f', filename]
unless search_path.blank?
- search_path = search_path.split(",").map{|search_path_part| "--schema=#{Shellwords.escape(search_path_part.strip)}" }.join(" ")
+ args << search_path.split(',').map do |part|
+ "--schema=#{part.strip}"
+ end.join(' ')
end
-
- command = "pg_dump -i -s -x -O -f #{Shellwords.escape(filename)} #{search_path} #{Shellwords.escape(configuration['database'])}"
- raise 'Error dumping database' unless Kernel.system(command)
-
+ args << configuration['database']
+ run_cmd('pg_dump', args, 'dumping')
File.open(filename, "a") { |f| f << "SET search_path TO #{connection.schema_search_path};\n\n" }
end
def structure_load(filename)
set_psql_env
- Kernel.system("psql -X -q -f #{Shellwords.escape(filename)} #{configuration['database']}")
+ args = [ '-q', '-f', filename, configuration['database'] ]
+ run_cmd('psql', args, 'loading' )
end
private
@@ -93,6 +94,17 @@ module ActiveRecord
ENV['PGPASSWORD'] = configuration['password'].to_s if configuration['password']
ENV['PGUSER'] = configuration['username'].to_s if configuration['username']
end
+
+ def run_cmd(cmd, args, action)
+ fail run_cmd_error(cmd, args, action) unless Kernel.system(cmd, *args)
+ end
+
+ def run_cmd_error(cmd, args, action)
+ msg = "failed to execute:\n"
+ msg << "#{cmd} #{args.join(' ')}\n\n"
+ msg << "Please check the output above for any errors and make sure that `#{cmd}` is installed in your PATH and has proper permissions.\n\n"
+ msg
+ end
end
end
end
diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb
index 20e4235788..e759475cfb 100644
--- a/activerecord/lib/active_record/timestamp.rb
+++ b/activerecord/lib/active_record/timestamp.rb
@@ -15,14 +15,21 @@ module ActiveRecord
#
# == Time Zone aware attributes
#
- # By default, ActiveRecord::Base keeps all the datetime columns time zone aware by executing following code.
+ # Active Record keeps all the <tt>datetime</tt> and <tt>time</tt> columns
+ # time-zone aware. By default, these values are stored in the database as UTC
+ # and converted back to the current Time.zone when pulled from the database.
#
- # config.active_record.time_zone_aware_attributes = true
+ # This feature can be turned off completely by setting:
#
- # This feature can easily be turned off by assigning value <tt>false</tt> .
+ # config.active_record.time_zone_aware_attributes = false
#
- # If your attributes are time zone aware and you desire to skip time zone conversion to the current Time.zone
- # when reading certain attributes then you can do following:
+ # You can also specify that only <tt>datetime</tt> columns should be time-zone
+ # aware (while <tt>time</tt> should not) by setting:
+ #
+ # ActiveRecord::Base.time_zone_aware_types = [:datetime]
+ #
+ # Finally, you can indicate specific attributes of a model for which time zone
+ # conversion should not applied, for instance by setting:
#
# class Topic < ActiveRecord::Base
# self.skip_time_zone_conversion_for_attributes = [:written_on]
diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb
index 311dacb449..1a2988ea77 100644
--- a/activerecord/lib/active_record/transactions.rb
+++ b/activerecord/lib/active_record/transactions.rb
@@ -11,6 +11,7 @@ module ActiveRecord
:before_commit_without_transaction_enrollment,
:commit_without_transaction_enrollment,
:rollback_without_transaction_enrollment,
+ terminator: deprecated_false_terminator,
scope: [:kind, :name]
end
@@ -167,7 +168,7 @@ module ActiveRecord
# writing, the only database that we're aware of that supports true nested
# transactions, is MS-SQL. Because of this, Active Record emulates nested
# transactions by using savepoints on MySQL and PostgreSQL. See
- # http://dev.mysql.com/doc/refman/5.6/en/savepoint.html
+ # http://dev.mysql.com/doc/refman/5.7/en/savepoint.html
# for more information about savepoints.
#
# === Callbacks
@@ -196,17 +197,16 @@ module ActiveRecord
# automatically released. The following example demonstrates the problem:
#
# Model.connection.transaction do # BEGIN
- # Model.connection.transaction(requires_new: true) do # CREATE SAVEPOINT active_record_1
+ # Model.connection.transaction(requires_new: true) do # CREATE SAVEPOINT active_record_1
# Model.connection.create_table(...) # active_record_1 now automatically released
- # end # RELEASE savepoint active_record_1
+ # end # RELEASE SAVEPOINT active_record_1
# # ^^^^ BOOM! database error!
# end
#
# Note that "TRUNCATE" is also a MySQL DDL statement!
module ClassMethods
- # See ActiveRecord::Transactions::ClassMethods for detailed documentation.
+ # See the ConnectionAdapters::DatabaseStatements#transaction API docs.
def transaction(options = {}, &block)
- # See the ConnectionAdapters::DatabaseStatements#transaction API docs.
connection.transaction(options, &block)
end
@@ -319,8 +319,8 @@ module ActiveRecord
end
def before_committed! # :nodoc:
- run_callbacks :before_commit_without_transaction_enrollment
- run_callbacks :before_commit
+ _run_before_commit_without_transaction_enrollment_callbacks
+ _run_before_commit_callbacks
end
# Call the +after_commit+ callbacks.
@@ -329,8 +329,8 @@ module ActiveRecord
# but call it after the commit of a destroyed object.
def committed!(should_run_callbacks: true) #:nodoc:
if should_run_callbacks && destroyed? || persisted?
- run_callbacks :commit_without_transaction_enrollment
- run_callbacks :commit
+ _run_commit_without_transaction_enrollment_callbacks
+ _run_commit_callbacks
end
ensure
force_clear_transaction_record_state
@@ -340,8 +340,8 @@ module ActiveRecord
# state should be rolled back to the beginning or just to the last savepoint.
def rolledback!(force_restore_state: false, should_run_callbacks: true) #:nodoc:
if should_run_callbacks
- run_callbacks :rollback
- run_callbacks :rollback_without_transaction_enrollment
+ _run_rollback_callbacks
+ _run_rollback_without_transaction_enrollment_callbacks
end
ensure
restore_transaction_record_state(force_restore_state)
@@ -380,6 +380,10 @@ module ActiveRecord
raise ActiveRecord::Rollback unless status
end
status
+ ensure
+ if @transaction_state && @transaction_state.committed?
+ clear_transaction_record_state
+ end
end
protected
diff --git a/activerecord/lib/active_record/type.rb b/activerecord/lib/active_record/type.rb
index 2c0cda69d0..74dfe88349 100644
--- a/activerecord/lib/active_record/type.rb
+++ b/activerecord/lib/active_record/type.rb
@@ -1,22 +1,15 @@
-require 'active_record/type/helpers'
-require 'active_record/type/value'
+require 'active_model/type'
+
+require 'active_record/type/internal/abstract_json'
+require 'active_record/type/internal/timezone'
-require 'active_record/type/big_integer'
-require 'active_record/type/binary'
-require 'active_record/type/boolean'
require 'active_record/type/date'
require 'active_record/type/date_time'
-require 'active_record/type/decimal'
-require 'active_record/type/decimal_without_scale'
-require 'active_record/type/float'
-require 'active_record/type/integer'
-require 'active_record/type/serialized'
-require 'active_record/type/string'
-require 'active_record/type/text'
require 'active_record/type/time'
-require 'active_record/type/unsigned_integer'
+require 'active_record/type/serialized'
require 'active_record/type/adapter_specific_registry'
+
require 'active_record/type/type_map'
require 'active_record/type/hash_lookup_type_map'
@@ -51,6 +44,19 @@ module ActiveRecord
end
end
+ Helpers = ActiveModel::Type::Helpers
+ BigInteger = ActiveModel::Type::BigInteger
+ Binary = ActiveModel::Type::Binary
+ Boolean = ActiveModel::Type::Boolean
+ Decimal = ActiveModel::Type::Decimal
+ DecimalWithoutScale = ActiveModel::Type::DecimalWithoutScale
+ Float = ActiveModel::Type::Float
+ Integer = ActiveModel::Type::Integer
+ String = ActiveModel::Type::String
+ Text = ActiveModel::Type::Text
+ UnsignedInteger = ActiveModel::Type::UnsignedInteger
+ Value = ActiveModel::Type::Value
+
register(:big_integer, Type::BigInteger, override: false)
register(:binary, Type::Binary, override: false)
register(:boolean, Type::Boolean, override: false)
diff --git a/activerecord/lib/active_record/type/adapter_specific_registry.rb b/activerecord/lib/active_record/type/adapter_specific_registry.rb
index 5f71b3cb94..d440eac619 100644
--- a/activerecord/lib/active_record/type/adapter_specific_registry.rb
+++ b/activerecord/lib/active_record/type/adapter_specific_registry.rb
@@ -1,35 +1,24 @@
+require 'active_model/type/registry'
+
module ActiveRecord
# :stopdoc:
module Type
- class AdapterSpecificRegistry
- def initialize
- @registrations = []
- end
-
- def register(type_name, klass = nil, **options, &block)
- block ||= proc { |_, *args| klass.new(*args) }
- registrations << Registration.new(type_name, block, **options)
+ class AdapterSpecificRegistry < ActiveModel::Type::Registry
+ def add_modifier(options, klass, **args)
+ registrations << DecorationRegistration.new(options, klass, **args)
end
- def lookup(symbol, *args)
- registration = registrations
- .select { |r| r.matches?(symbol, *args) }
- .max
+ private
- if registration
- registration.call(self, symbol, *args)
- else
- raise ArgumentError, "Unknown type #{symbol.inspect}"
- end
+ def registration_klass
+ Registration
end
- def add_modifier(options, klass, **args)
- registrations << DecorationRegistration.new(options, klass, **args)
+ def find_registration(symbol, *args)
+ registrations
+ .select { |registration| registration.matches?(symbol, *args) }
+ .max
end
-
- protected
-
- attr_reader :registrations
end
class Registration
@@ -137,6 +126,5 @@ module ActiveRecord
class TypeConflictError < StandardError
end
-
# :startdoc:
end
diff --git a/activerecord/lib/active_record/type/big_integer.rb b/activerecord/lib/active_record/type/big_integer.rb
deleted file mode 100644
index 0c72d8914f..0000000000
--- a/activerecord/lib/active_record/type/big_integer.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-require 'active_record/type/integer'
-
-module ActiveRecord
- module Type
- class BigInteger < Integer # :nodoc:
- private
-
- def max_value
- ::Float::INFINITY
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/type/binary.rb b/activerecord/lib/active_record/type/binary.rb
deleted file mode 100644
index 0baf8c63ad..0000000000
--- a/activerecord/lib/active_record/type/binary.rb
+++ /dev/null
@@ -1,50 +0,0 @@
-module ActiveRecord
- module Type
- class Binary < Value # :nodoc:
- def type
- :binary
- end
-
- def binary?
- true
- end
-
- def cast(value)
- if value.is_a?(Data)
- value.to_s
- else
- super
- end
- end
-
- def serialize(value)
- return if value.nil?
- Data.new(super)
- end
-
- def changed_in_place?(raw_old_value, value)
- old_value = deserialize(raw_old_value)
- old_value != value
- end
-
- class Data # :nodoc:
- def initialize(value)
- @value = value.to_s
- end
-
- def to_s
- @value
- end
- alias_method :to_str, :to_s
-
- def hex
- @value.unpack('H*')[0]
- end
-
- def ==(other)
- other == to_s || super
- end
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/type/boolean.rb b/activerecord/lib/active_record/type/boolean.rb
deleted file mode 100644
index f6a75512fd..0000000000
--- a/activerecord/lib/active_record/type/boolean.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-module ActiveRecord
- module Type
- class Boolean < Value # :nodoc:
- def type
- :boolean
- end
-
- private
-
- def cast_value(value)
- if value == ''
- nil
- else
- !ConnectionAdapters::Column::FALSE_VALUES.include?(value)
- end
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/type/date.rb b/activerecord/lib/active_record/type/date.rb
index 3ceab59ebb..ccafed054e 100644
--- a/activerecord/lib/active_record/type/date.rb
+++ b/activerecord/lib/active_record/type/date.rb
@@ -1,49 +1,7 @@
module ActiveRecord
module Type
- class Date < Value # :nodoc:
- include Helpers::AcceptsMultiparameterTime.new
-
- def type
- :date
- end
-
- def type_cast_for_schema(value)
- "'#{value.to_s(:db)}'"
- end
-
- private
-
- def cast_value(value)
- if value.is_a?(::String)
- return if value.empty?
- fast_string_to_date(value) || fallback_string_to_date(value)
- elsif value.respond_to?(:to_date)
- value.to_date
- else
- value
- end
- end
-
- def fast_string_to_date(string)
- if string =~ ConnectionAdapters::Column::Format::ISO_DATE
- new_date $1.to_i, $2.to_i, $3.to_i
- end
- end
-
- def fallback_string_to_date(string)
- new_date(*::Date._parse(string, false).values_at(:year, :mon, :mday))
- end
-
- def new_date(year, mon, mday)
- if year && year != 0
- ::Date.new(year, mon, mday) rescue nil
- end
- end
-
- def value_from_multiparameter_assignment(*)
- time = super
- time && time.to_date
- end
+ class Date < ActiveModel::Type::Date
+ include Internal::Timezone
end
end
end
diff --git a/activerecord/lib/active_record/type/date_time.rb b/activerecord/lib/active_record/type/date_time.rb
index a5199959b9..1fb9380ecd 100644
--- a/activerecord/lib/active_record/type/date_time.rb
+++ b/activerecord/lib/active_record/type/date_time.rb
@@ -1,44 +1,7 @@
module ActiveRecord
module Type
- class DateTime < Value # :nodoc:
- include Helpers::TimeValue
- include Helpers::AcceptsMultiparameterTime.new(
- defaults: { 4 => 0, 5 => 0 }
- )
-
- def type
- :datetime
- end
-
- private
-
- def cast_value(string)
- return string unless string.is_a?(::String)
- return if string.empty?
-
- fast_string_to_time(string) || fallback_string_to_time(string)
- end
-
- # '0.123456' -> 123456
- # '1.123456' -> 123456
- def microseconds(time)
- time[:sec_fraction] ? (time[:sec_fraction] * 1_000_000).to_i : 0
- end
-
- def fallback_string_to_time(string)
- time_hash = ::Date._parse(string)
- time_hash[:sec_fraction] = microseconds(time_hash)
-
- new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction, :offset))
- end
-
- def value_from_multiparameter_assignment(values_hash)
- missing_parameter = (1..3).detect { |key| !values_hash.key?(key) }
- if missing_parameter
- raise ArgumentError, missing_parameter
- end
- super
- end
+ class DateTime < ActiveModel::Type::DateTime
+ include Internal::Timezone
end
end
end
diff --git a/activerecord/lib/active_record/type/decimal.rb b/activerecord/lib/active_record/type/decimal.rb
deleted file mode 100644
index 867b5f75c7..0000000000
--- a/activerecord/lib/active_record/type/decimal.rb
+++ /dev/null
@@ -1,48 +0,0 @@
-module ActiveRecord
- module Type
- class Decimal < Value # :nodoc:
- include Helpers::Numeric
-
- def type
- :decimal
- end
-
- def type_cast_for_schema(value)
- value.to_s
- end
-
- private
-
- def cast_value(value)
- case value
- when ::Float
- convert_float_to_big_decimal(value)
- when ::Numeric, ::String
- BigDecimal(value, precision.to_i)
- else
- if value.respond_to?(:to_d)
- value.to_d
- else
- cast_value(value.to_s)
- end
- end
- end
-
- def convert_float_to_big_decimal(value)
- if precision
- BigDecimal(value, float_precision)
- else
- value.to_d
- end
- end
-
- def float_precision
- if precision.to_i > ::Float::DIG + 1
- ::Float::DIG + 1
- else
- precision.to_i
- end
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/type/decimal_without_scale.rb b/activerecord/lib/active_record/type/decimal_without_scale.rb
deleted file mode 100644
index ff5559e300..0000000000
--- a/activerecord/lib/active_record/type/decimal_without_scale.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-require 'active_record/type/big_integer'
-
-module ActiveRecord
- module Type
- class DecimalWithoutScale < BigInteger # :nodoc:
- def type
- :decimal
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/type/float.rb b/activerecord/lib/active_record/type/float.rb
deleted file mode 100644
index d88482b85d..0000000000
--- a/activerecord/lib/active_record/type/float.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-module ActiveRecord
- module Type
- class Float < Value # :nodoc:
- include Helpers::Numeric
-
- def type
- :float
- end
-
- alias serialize cast
-
- private
-
- def cast_value(value)
- case value
- when ::Float then value
- when "Infinity" then ::Float::INFINITY
- when "-Infinity" then -::Float::INFINITY
- when "NaN" then ::Float::NAN
- else value.to_f
- end
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/type/helpers.rb b/activerecord/lib/active_record/type/helpers.rb
deleted file mode 100644
index 634d417d13..0000000000
--- a/activerecord/lib/active_record/type/helpers.rb
+++ /dev/null
@@ -1,4 +0,0 @@
-require 'active_record/type/helpers/accepts_multiparameter_time'
-require 'active_record/type/helpers/numeric'
-require 'active_record/type/helpers/mutable'
-require 'active_record/type/helpers/time_value'
diff --git a/activerecord/lib/active_record/type/helpers/accepts_multiparameter_time.rb b/activerecord/lib/active_record/type/helpers/accepts_multiparameter_time.rb
deleted file mode 100644
index be571fc1c7..0000000000
--- a/activerecord/lib/active_record/type/helpers/accepts_multiparameter_time.rb
+++ /dev/null
@@ -1,30 +0,0 @@
-module ActiveRecord
- module Type
- module Helpers
- class AcceptsMultiparameterTime < Module # :nodoc:
- def initialize(defaults: {})
- define_method(:cast) do |value|
- if value.is_a?(Hash)
- value_from_multiparameter_assignment(value)
- else
- super(value)
- end
- end
-
- define_method(:value_from_multiparameter_assignment) do |values_hash|
- defaults.each do |k, v|
- values_hash[k] ||= v
- end
- return unless values_hash[1] && values_hash[2] && values_hash[3]
- values = values_hash.sort.map(&:last)
- ::Time.send(
- ActiveRecord::Base.default_timezone,
- *values
- )
- end
- private :value_from_multiparameter_assignment
- end
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/type/helpers/mutable.rb b/activerecord/lib/active_record/type/helpers/mutable.rb
deleted file mode 100644
index 88a9099277..0000000000
--- a/activerecord/lib/active_record/type/helpers/mutable.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-module ActiveRecord
- module Type
- module Helpers
- module Mutable # :nodoc:
- def cast(value)
- deserialize(serialize(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 != serialize(new_value)
- end
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/type/helpers/numeric.rb b/activerecord/lib/active_record/type/helpers/numeric.rb
deleted file mode 100644
index a755a02a59..0000000000
--- a/activerecord/lib/active_record/type/helpers/numeric.rb
+++ /dev/null
@@ -1,34 +0,0 @@
-module ActiveRecord
- module Type
- module Helpers
- module Numeric # :nodoc:
- def cast(value)
- value = case value
- when true then 1
- when false then 0
- when ::String then value.presence
- else value
- end
- super(value)
- end
-
- def changed?(old_value, _new_value, new_value_before_type_cast) # :nodoc:
- super || number_to_non_number?(old_value, new_value_before_type_cast)
- end
-
- private
-
- def number_to_non_number?(old_value, new_value_before_type_cast)
- old_value != nil && non_numeric_string?(new_value_before_type_cast)
- end
-
- def non_numeric_string?(value)
- # 'wibble'.to_i will give zero, we want to make sure
- # that we aren't marking int zero to string zero as
- # changed.
- value.to_s !~ /\A-?\d+\.?\d*\z/
- end
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/type/helpers/time_value.rb b/activerecord/lib/active_record/type/helpers/time_value.rb
deleted file mode 100644
index 7eb41557cb..0000000000
--- a/activerecord/lib/active_record/type/helpers/time_value.rb
+++ /dev/null
@@ -1,58 +0,0 @@
-module ActiveRecord
- module Type
- module Helpers
- module TimeValue # :nodoc:
- def serialize(value)
- if precision && value.respond_to?(:usec)
- number_of_insignificant_digits = 6 - precision
- round_power = 10 ** number_of_insignificant_digits
- value = value.change(usec: value.usec / round_power * round_power)
- end
-
- if value.acts_like?(:time)
- zone_conversion_method = ActiveRecord::Base.default_timezone == :utc ? :getutc : :getlocal
-
- if value.respond_to?(zone_conversion_method)
- value = value.send(zone_conversion_method)
- end
- end
-
- value
- end
-
- def type_cast_for_schema(value)
- "'#{value.to_s(:db)}'"
- end
-
- def user_input_in_time_zone(value)
- value.in_time_zone
- end
-
- private
-
- def new_time(year, mon, mday, hour, min, sec, microsec, offset = nil)
- # Treat 0000-00-00 00:00:00 as nil.
- return if year.nil? || (year == 0 && mon == 0 && mday == 0)
-
- if offset
- time = ::Time.utc(year, mon, mday, hour, min, sec, microsec) rescue nil
- return unless time
-
- time -= offset
- Base.default_timezone == :utc ? time : time.getlocal
- else
- ::Time.public_send(Base.default_timezone, year, mon, mday, hour, min, sec, microsec) rescue nil
- end
- end
-
- # Doesn't handle time zones.
- def fast_string_to_time(string)
- if string =~ ConnectionAdapters::Column::Format::ISO_DATETIME
- microsec = ($7.to_r * 1_000_000).to_i
- new_time $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, microsec
- end
- end
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/type/integer.rb b/activerecord/lib/active_record/type/integer.rb
deleted file mode 100644
index 2a1b04ac7f..0000000000
--- a/activerecord/lib/active_record/type/integer.rb
+++ /dev/null
@@ -1,63 +0,0 @@
-module ActiveRecord
- module Type
- class Integer < Value # :nodoc:
- include Helpers::Numeric
-
- # Column storage size in bytes.
- # 4 bytes means a MySQL int or Postgres integer as opposed to smallint etc.
- DEFAULT_LIMIT = 4
-
- def initialize(*)
- super
- @range = min_value...max_value
- end
-
- def type
- :integer
- end
-
- def deserialize(value)
- return if value.nil?
- value.to_i
- end
-
- def serialize(value)
- result = cast(value)
- if result
- ensure_in_range(result)
- end
- result
- end
-
- protected
-
- attr_reader :range
-
- private
-
- def cast_value(value)
- case value
- when true then 1
- when false then 0
- else
- value.to_i rescue nil
- end
- end
-
- def ensure_in_range(value)
- unless range.cover?(value)
- raise RangeError, "#{value} is out of range for #{self.class} with limit #{limit || DEFAULT_LIMIT}"
- end
- end
-
- def max_value
- limit = self.limit || DEFAULT_LIMIT
- 1 << (limit * 8 - 1) # 8 bits per byte with one bit for sign
- end
-
- def min_value
- -max_value
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/type/internal/abstract_json.rb b/activerecord/lib/active_record/type/internal/abstract_json.rb
new file mode 100644
index 0000000000..097d1bd363
--- /dev/null
+++ b/activerecord/lib/active_record/type/internal/abstract_json.rb
@@ -0,0 +1,33 @@
+module ActiveRecord
+ module Type
+ module Internal # :nodoc:
+ class AbstractJson < ActiveModel::Type::Value # :nodoc:
+ include ActiveModel::Type::Helpers::Mutable
+
+ def type
+ :json
+ end
+
+ def deserialize(value)
+ if value.is_a?(::String)
+ ::ActiveSupport::JSON.decode(value) rescue nil
+ else
+ value
+ end
+ end
+
+ def serialize(value)
+ if value.is_a?(::Array) || value.is_a?(::Hash)
+ ::ActiveSupport::JSON.encode(value)
+ else
+ value
+ end
+ end
+
+ def accessor
+ ActiveRecord::Store::StringKeyedHashAccessor
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/internal/timezone.rb b/activerecord/lib/active_record/type/internal/timezone.rb
new file mode 100644
index 0000000000..947e06158a
--- /dev/null
+++ b/activerecord/lib/active_record/type/internal/timezone.rb
@@ -0,0 +1,15 @@
+module ActiveRecord
+ module Type
+ module Internal
+ module Timezone
+ def is_utc?
+ ActiveRecord::Base.default_timezone == :utc
+ end
+
+ def default_timezone
+ ActiveRecord::Base.default_timezone
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/serialized.rb b/activerecord/lib/active_record/type/serialized.rb
index ea3e0d6a45..4ff0740cfb 100644
--- a/activerecord/lib/active_record/type/serialized.rb
+++ b/activerecord/lib/active_record/type/serialized.rb
@@ -1,7 +1,7 @@
module ActiveRecord
module Type
- class Serialized < DelegateClass(Type::Value) # :nodoc:
- include Helpers::Mutable
+ class Serialized < DelegateClass(ActiveModel::Type::Value) # :nodoc:
+ include ActiveModel::Type::Helpers::Mutable
attr_reader :subtype, :coder
@@ -41,6 +41,12 @@ module ActiveRecord
ActiveRecord::Store::IndifferentHashAccessor
end
+ def assert_valid_value(value)
+ if coder.respond_to?(:assert_valid_value)
+ coder.assert_valid_value(value)
+ end
+ end
+
private
def default_value?(value)
diff --git a/activerecord/lib/active_record/type/string.rb b/activerecord/lib/active_record/type/string.rb
deleted file mode 100644
index 2662b7e874..0000000000
--- a/activerecord/lib/active_record/type/string.rb
+++ /dev/null
@@ -1,36 +0,0 @@
-module ActiveRecord
- module Type
- class String < Value # :nodoc:
- def type
- :string
- end
-
- def changed_in_place?(raw_old_value, new_value)
- if new_value.is_a?(::String)
- raw_old_value != new_value
- end
- end
-
- def serialize(value)
- case value
- when ::Numeric, ActiveSupport::Duration then value.to_s
- when ::String then ::String.new(value)
- when true then "t"
- when false then "f"
- else super
- end
- end
-
- private
-
- def cast_value(value)
- case value
- when true then "t"
- when false then "f"
- # String.new is slightly faster than dup
- else ::String.new(value.to_s)
- end
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/type/text.rb b/activerecord/lib/active_record/type/text.rb
deleted file mode 100644
index 26f980f060..0000000000
--- a/activerecord/lib/active_record/type/text.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-require 'active_record/type/string'
-
-module ActiveRecord
- module Type
- class Text < String # :nodoc:
- def type
- :text
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/type/time.rb b/activerecord/lib/active_record/type/time.rb
index 19a10021bc..70988d84ff 100644
--- a/activerecord/lib/active_record/type/time.rb
+++ b/activerecord/lib/active_record/type/time.rb
@@ -1,42 +1,8 @@
module ActiveRecord
module Type
- class Time < Value # :nodoc:
- include Helpers::TimeValue
- include Helpers::AcceptsMultiparameterTime.new(
- defaults: { 1 => 1970, 2 => 1, 3 => 1, 4 => 0, 5 => 0 }
- )
-
- def type
- :time
- end
-
- def user_input_in_time_zone(value)
- return unless value.present?
-
- case value
- when ::String
- value = "2000-01-01 #{value}"
- when ::Time
- value = value.change(year: 2000, day: 1, month: 1)
- end
-
- super(value)
- end
-
- private
-
- def cast_value(value)
- return value unless value.is_a?(::String)
- return if value.empty?
-
- dummy_time_value = "2000-01-01 #{value}"
-
- fast_string_to_time(dummy_time_value) || begin
- time_hash = ::Date._parse(dummy_time_value)
- return if time_hash[:hour].nil?
- new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction))
- end
- end
+ class Time < ActiveModel::Type::Time
+ include Internal::Timezone
end
end
end
+
diff --git a/activerecord/lib/active_record/type/type_map.rb b/activerecord/lib/active_record/type/type_map.rb
index 09f5ba6b74..81d7ed39bb 100644
--- a/activerecord/lib/active_record/type/type_map.rb
+++ b/activerecord/lib/active_record/type/type_map.rb
@@ -1,12 +1,12 @@
-require 'thread_safe'
+require 'concurrent'
module ActiveRecord
module Type
class TypeMap # :nodoc:
def initialize
@mapping = {}
- @cache = ThreadSafe::Cache.new do |h, key|
- h.fetch_or_store(key, ThreadSafe::Cache.new)
+ @cache = Concurrent::Map.new do |h, key|
+ h.fetch_or_store(key, Concurrent::Map.new)
end
end
@@ -57,7 +57,7 @@ module ActiveRecord
end
def default_value
- @default_value ||= Value.new
+ @default_value ||= ActiveModel::Type::Value.new
end
end
end
diff --git a/activerecord/lib/active_record/type/unsigned_integer.rb b/activerecord/lib/active_record/type/unsigned_integer.rb
deleted file mode 100644
index ed3e527483..0000000000
--- a/activerecord/lib/active_record/type/unsigned_integer.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-module ActiveRecord
- module Type
- class UnsignedInteger < Integer # :nodoc:
- private
-
- def max_value
- super * 2
- end
-
- def min_value
- 0
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/type/value.rb b/activerecord/lib/active_record/type/value.rb
deleted file mode 100644
index 6b9d147ecc..0000000000
--- a/activerecord/lib/active_record/type/value.rb
+++ /dev/null
@@ -1,104 +0,0 @@
-module ActiveRecord
- module Type
- class Value
- attr_reader :precision, :scale, :limit
-
- def initialize(precision: nil, limit: nil, scale: nil)
- @precision = precision
- @scale = scale
- @limit = limit
- end
-
- def type # :nodoc:
- end
-
- # Converts a value from database input to the appropriate ruby type. The
- # return value of this method will be returned from
- # ActiveRecord::AttributeMethods::Read#read_attribute. The default
- # implementation just calls Value#cast.
- #
- # +value+ The raw input, as provided from the database.
- def deserialize(value)
- 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 a ruby object passed to a setter.
- # There is currently no way to differentiate between which source it came
- # from.
- #
- # The return value of this method will be returned from
- # ActiveRecord::AttributeMethods::Read#read_attribute. See also:
- # Value#cast_value.
- #
- # +value+ The raw input, as provided to the attribute setter.
- def cast(value)
- cast_value(value) unless value.nil?
- end
-
- # Casts 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 serialize(value)
- value
- end
-
- # Type casts a value for schema dumping. This method is private, as we are
- # hoping to remove it entirely.
- def type_cast_for_schema(value) # :nodoc:
- value.inspect
- end
-
- # These predicates are not documented, as I need to look further into
- # their use, and see if they can be removed entirely.
- def binary? # :nodoc:
- false
- end
-
- # Determines whether a value has changed for dirty checking. +old_value+
- # and +new_value+ will always be type-cast. Types should not need to
- # override this method.
- def changed?(old_value, new_value, _new_value_before_type_cast)
- old_value != new_value
- end
-
- # Determines whether the mutable value has been modified since it was
- # read. Returns +false+ by default. If your type returns an object
- # which could be mutated, you should override this method. You will need
- # to either:
- #
- # - pass +new_value+ to Value#serialize and compare it to
- # +raw_old_value+
- #
- # or
- #
- # - pass +raw_old_value+ to Value#deserialize and compare it to
- # +new_value+
- #
- # +raw_old_value+ The original value, before being passed to
- # +deserialize+.
- #
- # +new_value+ The current value, after type casting.
- def changed_in_place?(raw_old_value, new_value)
- false
- end
-
- def ==(other)
- self.class == other.class &&
- precision == other.precision &&
- scale == other.scale &&
- limit == other.limit
- end
-
- private
-
- # Convenience method for types which do not need separate type casting
- # behavior for user and database inputs. Called by Value#cast for
- # values except +nil+.
- def cast_value(value) # :doc:
- value
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/type_caster/connection.rb b/activerecord/lib/active_record/type_caster/connection.rb
index 3878270770..868d08ed44 100644
--- a/activerecord/lib/active_record/type_caster/connection.rb
+++ b/activerecord/lib/active_record/type_caster/connection.rb
@@ -20,7 +20,7 @@ module ActiveRecord
private
def column_for(attribute_name)
- if connection.schema_cache.table_exists?(table_name)
+ if connection.schema_cache.data_source_exists?(table_name)
connection.schema_cache.columns_hash(table_name)[attribute_name.to_s]
end
end
diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb
index e227212827..4113ca4561 100644
--- a/activerecord/lib/active_record/validations.rb
+++ b/activerecord/lib/active_record/validations.rb
@@ -12,10 +12,16 @@ module ActiveRecord
class RecordInvalid < ActiveRecordError
attr_reader :record
- def initialize(record)
- @record = record
- errors = @record.errors.full_messages.join(", ")
- super(I18n.t(:"#{@record.class.i18n_scope}.errors.messages.record_invalid", :errors => errors, :default => :"errors.messages.record_invalid"))
+ def initialize(record = nil)
+ if record
+ @record = record
+ errors = @record.errors.full_messages.join(", ")
+ message = I18n.t(:"#{@record.class.i18n_scope}.errors.messages.record_invalid", errors: errors, default: :"errors.messages.record_invalid")
+ else
+ message = "Record invalid"
+ end
+
+ super(message)
end
end
@@ -54,7 +60,7 @@ module ActiveRecord
# 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 valid?(context = nil)
- context ||= (new_record? ? :create : :update)
+ context ||= default_validation_context
output = super(context)
errors.empty? && output
end
@@ -63,6 +69,10 @@ module ActiveRecord
protected
+ def default_validation_context
+ new_record? ? :create : :update
+ end
+
def raise_validation_error
raise(RecordInvalid.new(self))
end
@@ -76,4 +86,5 @@ end
require "active_record/validations/associated"
require "active_record/validations/uniqueness"
require "active_record/validations/presence"
+require "active_record/validations/absence"
require "active_record/validations/length"
diff --git a/activerecord/lib/active_record/validations/absence.rb b/activerecord/lib/active_record/validations/absence.rb
new file mode 100644
index 0000000000..2e19e6dc5c
--- /dev/null
+++ b/activerecord/lib/active_record/validations/absence.rb
@@ -0,0 +1,24 @@
+module ActiveRecord
+ module Validations
+ class AbsenceValidator < ActiveModel::Validations::AbsenceValidator # :nodoc:
+ def validate_each(record, attribute, association_or_value)
+ return unless should_validate?(record)
+ if record.class._reflect_on_association(attribute)
+ association_or_value = Array.wrap(association_or_value).reject(&:marked_for_destruction?)
+ end
+ super
+ end
+ end
+
+ module ClassMethods
+ # Validates that the specified attributes are not present (as defined by
+ # Object#present?). If the attribute is an association, the associated object
+ # is considered absent if it was marked for destruction.
+ #
+ # See ActiveModel::Validations::HelperMethods.validates_absence_of for more information.
+ def validates_absence_of(*attr_names)
+ validates_with AbsenceValidator, _merge_attributes(attr_names)
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/validations/length.rb b/activerecord/lib/active_record/validations/length.rb
index 5991fbad8e..69e048eef1 100644
--- a/activerecord/lib/active_record/validations/length.rb
+++ b/activerecord/lib/active_record/validations/length.rb
@@ -22,7 +22,10 @@ module ActiveRecord
end
module ClassMethods
- # See <tt>ActiveModel::Validation::LengthValidator</tt> for more information.
+ # Validates that the specified attributes match the length restrictions supplied.
+ # If the attribute is an association, records that are marked for destruction are not counted.
+ #
+ # See ActiveModel::Validations::HelperMethods.validates_length_of for more information.
def validates_length_of(*attr_names)
validates_with LengthValidator, _merge_attributes(attr_names)
end
diff --git a/activerecord/lib/active_record/validations/presence.rb b/activerecord/lib/active_record/validations/presence.rb
index a9b791397b..23a3985d35 100644
--- a/activerecord/lib/active_record/validations/presence.rb
+++ b/activerecord/lib/active_record/validations/presence.rb
@@ -1,18 +1,12 @@
module ActiveRecord
module Validations
class PresenceValidator < ActiveModel::Validations::PresenceValidator # :nodoc:
- def validate(record)
+ def validate_each(record, attribute, association_or_value)
return unless should_validate?(record)
- super
- attributes.each do |attribute|
- next unless record.class._reflect_on_association(attribute)
- associated_records = Array.wrap(record.send(attribute))
-
- # Superclass validates presence. Ensure present records aren't about to be destroyed.
- if associated_records.present? && associated_records.all?(&:marked_for_destruction?)
- record.errors.add(attribute, :blank, options)
- end
+ if record.class._reflect_on_association(attribute)
+ association_or_value = Array.wrap(association_or_value).reject(&:marked_for_destruction?)
end
+ super
end
end
diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb
index 5106f4e127..5706bbd903 100644
--- a/activerecord/lib/active_record/validations/uniqueness.rb
+++ b/activerecord/lib/active_record/validations/uniqueness.rb
@@ -17,7 +17,13 @@ module ActiveRecord
value = map_enum_attribute(finder_class, attribute, value)
relation = build_relation(finder_class, table, attribute, value)
- relation = relation.where.not(finder_class.primary_key => record.id) if record.persisted?
+ if record.persisted? && finder_class.primary_key.to_s != attribute.to_s
+ if finder_class.primary_key
+ relation = relation.where.not(finder_class.primary_key => record.id)
+ else
+ raise UnknownPrimaryKey.new(finder_class, "Can not validate uniqueness for persisted record without primary key.")
+ end
+ end
relation = scope_relation(record, table, relation)
relation = relation.merge(options[:conditions]) if options[:conditions]