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.rb3
-rw-r--r--activerecord/lib/active_record/association_relation.rb18
-rw-r--r--activerecord/lib/active_record/associations.rb120
-rw-r--r--activerecord/lib/active_record/associations/association.rb49
-rw-r--r--activerecord/lib/active_record/associations/association_scope.rb30
-rw-r--r--activerecord/lib/active_record/associations/belongs_to_association.rb59
-rw-r--r--activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb7
-rw-r--r--activerecord/lib/active_record/associations/builder/association.rb132
-rw-r--r--activerecord/lib/active_record/associations/builder/belongs_to.rb149
-rw-r--r--activerecord/lib/active_record/associations/builder/collection_association.rb92
-rw-r--r--activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb132
-rw-r--r--activerecord/lib/active_record/associations/builder/has_many.rb4
-rw-r--r--activerecord/lib/active_record/associations/builder/has_one.rb12
-rw-r--r--activerecord/lib/active_record/associations/builder/singular_association.rb15
-rw-r--r--activerecord/lib/active_record/associations/collection_association.rb173
-rw-r--r--activerecord/lib/active_record/associations/collection_proxy.rb37
-rw-r--r--activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb65
-rw-r--r--activerecord/lib/active_record/associations/has_many_association.rb16
-rw-r--r--activerecord/lib/active_record/associations/has_many_through_association.rb18
-rw-r--r--activerecord/lib/active_record/associations/has_one_association.rb15
-rw-r--r--activerecord/lib/active_record/associations/join_dependency.rb372
-rw-r--r--activerecord/lib/active_record/associations/join_dependency/join_association.rb144
-rw-r--r--activerecord/lib/active_record/associations/join_dependency/join_base.rb16
-rw-r--r--activerecord/lib/active_record/associations/join_dependency/join_part.rb66
-rw-r--r--activerecord/lib/active_record/associations/join_helper.rb13
-rw-r--r--activerecord/lib/active_record/associations/preloader.rb119
-rw-r--r--activerecord/lib/active_record/associations/preloader/association.rb66
-rw-r--r--activerecord/lib/active_record/associations/preloader/collection_association.rb4
-rw-r--r--activerecord/lib/active_record/associations/preloader/has_and_belongs_to_many.rb60
-rw-r--r--activerecord/lib/active_record/associations/preloader/has_many_through.rb2
-rw-r--r--activerecord/lib/active_record/associations/preloader/singular_association.rb6
-rw-r--r--activerecord/lib/active_record/associations/preloader/through_association.rb84
-rw-r--r--activerecord/lib/active_record/associations/singular_association.rb5
-rw-r--r--activerecord/lib/active_record/associations/through_association.rb4
-rw-r--r--activerecord/lib/active_record/attribute_assignment.rb7
-rw-r--r--activerecord/lib/active_record/attribute_methods.rb136
-rw-r--r--activerecord/lib/active_record/attribute_methods/before_type_cast.rb3
-rw-r--r--activerecord/lib/active_record/attribute_methods/dirty.rb26
-rw-r--r--activerecord/lib/active_record/attribute_methods/read.rb95
-rw-r--r--activerecord/lib/active_record/attribute_methods/serialization.rb37
-rw-r--r--activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb2
-rw-r--r--activerecord/lib/active_record/attribute_methods/write.rb49
-rw-r--r--activerecord/lib/active_record/autosave_association.rb417
-rw-r--r--activerecord/lib/active_record/base.rb6
-rw-r--r--activerecord/lib/active_record/callbacks.rb11
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb17
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb39
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb25
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/quoting.rb6
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/savepoints.rb21
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb84
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb15
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb116
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_adapter.rb126
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb112
-rw-r--r--activerecord/lib/active_record/connection_adapters/column.rb73
-rw-r--r--activerecord/lib/active_record/connection_adapters/connection_specification.rb240
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb23
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql_adapter.rb61
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb37
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/cast.rb35
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb64
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid.rb46
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb35
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb23
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb194
-rw-r--r--activerecord/lib/active_record/connection_adapters/schema_cache.rb37
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb107
-rw-r--r--activerecord/lib/active_record/connection_handling.rb15
-rw-r--r--activerecord/lib/active_record/core.rb95
-rw-r--r--activerecord/lib/active_record/counter_cache.rb14
-rw-r--r--activerecord/lib/active_record/dynamic_matchers.rb11
-rw-r--r--activerecord/lib/active_record/enum.rb116
-rw-r--r--activerecord/lib/active_record/errors.rb44
-rw-r--r--activerecord/lib/active_record/explain.rb15
-rw-r--r--activerecord/lib/active_record/explain_registry.rb30
-rw-r--r--activerecord/lib/active_record/explain_subscriber.rb7
-rw-r--r--activerecord/lib/active_record/fixture_set/file.rb3
-rw-r--r--activerecord/lib/active_record/fixtures.rb247
-rw-r--r--activerecord/lib/active_record/inheritance.rb18
-rw-r--r--activerecord/lib/active_record/integration.rb55
-rw-r--r--activerecord/lib/active_record/locking/optimistic.rb9
-rw-r--r--activerecord/lib/active_record/locking/pessimistic.rb8
-rw-r--r--activerecord/lib/active_record/log_subscriber.rb21
-rw-r--r--activerecord/lib/active_record/migration.rb178
-rw-r--r--activerecord/lib/active_record/migration/command_recorder.rb8
-rw-r--r--activerecord/lib/active_record/model_schema.rb65
-rw-r--r--activerecord/lib/active_record/nested_attributes.rb57
-rw-r--r--activerecord/lib/active_record/no_touching.rb52
-rw-r--r--activerecord/lib/active_record/null_relation.rb18
-rw-r--r--activerecord/lib/active_record/persistence.rb94
-rw-r--r--activerecord/lib/active_record/querying.rb26
-rw-r--r--activerecord/lib/active_record/railtie.rb85
-rw-r--r--activerecord/lib/active_record/railties/databases.rake112
-rw-r--r--activerecord/lib/active_record/readonly_attributes.rb6
-rw-r--r--activerecord/lib/active_record/reflection.rb267
-rw-r--r--activerecord/lib/active_record/relation.rb209
-rw-r--r--activerecord/lib/active_record/relation/batches.rb77
-rw-r--r--activerecord/lib/active_record/relation/calculations.rb84
-rw-r--r--activerecord/lib/active_record/relation/delegation.rb99
-rw-r--r--activerecord/lib/active_record/relation/finder_methods.rb192
-rw-r--r--activerecord/lib/active_record/relation/merger.rb114
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder.rb95
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder/array_handler.rb29
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder/relation_handler.rb13
-rw-r--r--activerecord/lib/active_record/relation/query_methods.rb225
-rw-r--r--activerecord/lib/active_record/relation/spawn_methods.rb3
-rw-r--r--activerecord/lib/active_record/result.rb67
-rw-r--r--activerecord/lib/active_record/runtime_registry.rb22
-rw-r--r--activerecord/lib/active_record/sanitization.rb30
-rw-r--r--activerecord/lib/active_record/schema.rb2
-rw-r--r--activerecord/lib/active_record/schema_dumper.rb27
-rw-r--r--activerecord/lib/active_record/schema_migration.rb42
-rw-r--r--activerecord/lib/active_record/scoping.rb58
-rw-r--r--activerecord/lib/active_record/scoping/default.rb23
-rw-r--r--activerecord/lib/active_record/scoping/named.rb37
-rw-r--r--activerecord/lib/active_record/statement_cache.rb26
-rw-r--r--activerecord/lib/active_record/store.rb68
-rw-r--r--activerecord/lib/active_record/tasks/database_tasks.rb125
-rw-r--r--activerecord/lib/active_record/tasks/firebird_database_tasks.rb56
-rw-r--r--activerecord/lib/active_record/tasks/mysql_database_tasks.rb3
-rw-r--r--activerecord/lib/active_record/tasks/oracle_database_tasks.rb45
-rw-r--r--activerecord/lib/active_record/tasks/postgresql_database_tasks.rb2
-rw-r--r--activerecord/lib/active_record/tasks/sqlite_database_tasks.rb2
-rw-r--r--activerecord/lib/active_record/tasks/sqlserver_database_tasks.rb48
-rw-r--r--activerecord/lib/active_record/test_case.rb96
-rw-r--r--activerecord/lib/active_record/timestamp.rb8
-rw-r--r--activerecord/lib/active_record/transactions.rb32
-rw-r--r--activerecord/lib/active_record/validations/associated.rb6
-rw-r--r--activerecord/lib/active_record/validations/presence.rb2
-rw-r--r--activerecord/lib/active_record/validations/uniqueness.rb29
-rw-r--r--activerecord/lib/active_record/version.rb2
132 files changed, 4775 insertions, 3331 deletions
diff --git a/activerecord/lib/active_record/aggregations.rb b/activerecord/lib/active_record/aggregations.rb
index 9d1c12ec62..0d5313956b 100644
--- a/activerecord/lib/active_record/aggregations.rb
+++ b/activerecord/lib/active_record/aggregations.rb
@@ -223,7 +223,8 @@ module ActiveRecord
reader_method(name, class_name, mapping, allow_nil, constructor)
writer_method(name, class_name, mapping, allow_nil, converter)
- create_reflection(:composed_of, part_id, nil, options, self)
+ reflection = ActiveRecord::Reflection.create(:composed_of, part_id, nil, options, self)
+ Reflection.add_aggregate_reflection self, part_id, reflection
end
private
diff --git a/activerecord/lib/active_record/association_relation.rb b/activerecord/lib/active_record/association_relation.rb
new file mode 100644
index 0000000000..20516bba0c
--- /dev/null
+++ b/activerecord/lib/active_record/association_relation.rb
@@ -0,0 +1,18 @@
+module ActiveRecord
+ class AssociationRelation < Relation
+ def initialize(klass, table, association)
+ super(klass, table)
+ @association = association
+ end
+
+ def proxy_association
+ @association
+ end
+
+ private
+
+ def exec_queries
+ super.each { |r| @association.set_inverse_instance r }
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb
index 3c92e379f1..74e2774626 100644
--- a/activerecord/lib/active_record/associations.rb
+++ b/activerecord/lib/active_record/associations.rb
@@ -73,12 +73,6 @@ module ActiveRecord
end
end
- class HasAndBelongsToManyAssociationForeignKeyNeeded < ActiveRecordError #:nodoc:
- def initialize(reflection)
- super("Cannot create self referential has_and_belongs_to_many association on '#{reflection.class_name rescue nil}##{reflection.name rescue nil}'. :association_foreign_key cannot be the same as the :foreign_key.")
- end
- end
-
class EagerLoadPolymorphicError < ActiveRecordError #:nodoc:
def initialize(reflection)
super("Can not eagerly load the polymorphic association #{reflection.name.inspect}")
@@ -114,7 +108,6 @@ module ActiveRecord
autoload :BelongsToAssociation, 'active_record/associations/belongs_to_association'
autoload :BelongsToPolymorphicAssociation, 'active_record/associations/belongs_to_polymorphic_association'
- autoload :HasAndBelongsToManyAssociation, 'active_record/associations/has_and_belongs_to_many_association'
autoload :HasManyAssociation, 'active_record/associations/has_many_association'
autoload :HasManyThroughAssociation, 'active_record/associations/has_many_through_association'
autoload :HasOneAssociation, 'active_record/associations/has_one_association'
@@ -164,7 +157,7 @@ module ActiveRecord
private
# Returns the specified association instance if it responds to :loaded?, nil otherwise.
def association_instance_get(name)
- @association_cache[name.to_sym]
+ @association_cache[name]
end
# Set the specified association instance.
@@ -172,7 +165,7 @@ module ActiveRecord
@association_cache[name] = association
end
- # Associations are a set of macro-like class methods for tying objects together through
+ # \Associations are a set of macro-like class methods for tying objects together through
# foreign keys. They express relationships like "Project has one Project Manager"
# or "Project belongs to a Portfolio". Each macro adds a number of methods to the
# class which are specialized according to the collection or association symbol and the
@@ -365,11 +358,11 @@ module ActiveRecord
# there is some special behavior you should be aware of, mostly involving the saving of
# associated objects.
#
- # You can set the :autosave option on a <tt>has_one</tt>, <tt>belongs_to</tt>,
+ # You can set the <tt>:autosave</tt> option on a <tt>has_one</tt>, <tt>belongs_to</tt>,
# <tt>has_many</tt>, or <tt>has_and_belongs_to_many</tt> association. Setting it
# to +true+ will _always_ save the members, whereas setting it to +false+ will
- # _never_ save the members. More details about :autosave option is available at
- # autosave_association.rb .
+ # _never_ save the members. More details about <tt>:autosave</tt> option is available at
+ # AutosaveAssociation.
#
# === One-to-one associations
#
@@ -402,7 +395,7 @@ module ActiveRecord
#
# == Customizing the query
#
- # Associations are built from <tt>Relation</tt>s, and you can use the <tt>Relation</tt> syntax
+ # \Associations are built from <tt>Relation</tt>s, and you can use the <tt>Relation</tt> syntax
# to customize them. For example, to add a condition:
#
# class Blog < ActiveRecord::Base
@@ -568,6 +561,8 @@ module ActiveRecord
# @group.avatars << Avatar.new # this would work if User belonged_to Avatar rather than the other way around
# @group.avatars.delete(@group.avatars.last) # so would this
#
+ # == Setting Inverses
+ #
# If you are using a +belongs_to+ on the join model, it is a good idea to set the
# <tt>:inverse_of</tt> option on the +belongs_to+, which will mean that the following example
# works correctly (where <tt>tags</tt> is a +has_many+ <tt>:through</tt> association):
@@ -584,7 +579,27 @@ module ActiveRecord
# belongs_to :tag, inverse_of: :taggings
# end
#
- # == Nested Associations
+ # If you do not set the <tt>:inverse_of</tt> record, the association will
+ # do its best to match itself up with the correct inverse. Automatic
+ # inverse detection only works on <tt>has_many</tt>, <tt>has_one</tt>, and
+ # <tt>belongs_to</tt> associations.
+ #
+ # Extra options on the associations, as defined in the
+ # <tt>AssociationReflection::INVALID_AUTOMATIC_INVERSE_OPTIONS</tt> constant, will
+ # also prevent the association's inverse from being found automatically.
+ #
+ # The automatic guessing of the inverse association uses a heuristic based
+ # on the name of the class, so it may not work for all associations,
+ # especially the ones with non-standard names.
+ #
+ # You can turn off the automatic detection of inverse associations by setting
+ # the <tt>:inverse_of</tt> option to <tt>false</tt> like so:
+ #
+ # class Taggable < ActiveRecord::Base
+ # belongs_to :tag, inverse_of: false
+ # end
+ #
+ # == Nested \Associations
#
# You can actually specify *any* association with the <tt>:through</tt> option, including an
# association which has a <tt>:through</tt> option itself. For example:
@@ -627,7 +642,7 @@ module ActiveRecord
# add a <tt>Commenter</tt> in the example above, there would be no way to tell how to set up the
# intermediate <tt>Post</tt> and <tt>Comment</tt> objects.
#
- # == Polymorphic Associations
+ # == Polymorphic \Associations
#
# Polymorphic associations on models are not restricted on what types of models they
# can be associated with. Rather, they specify an interface that a +has_many+ association
@@ -789,7 +804,7 @@ module ActiveRecord
# For example if all the addressables are either of class Person or Company then a total
# of 3 queries will be executed. The list of addressable types to load is determined on
# the back of the addresses loaded. This is not supported if Active Record has to fallback
- # to the previous implementation of eager loading and will raise ActiveRecord::EagerLoadPolymorphicError.
+ # to the previous implementation of eager loading and will raise <tt>ActiveRecord::EagerLoadPolymorphicError</tt>.
# The reason is that the parent model's type is a column value so its corresponding table
# name cannot be put in the +FROM+/+JOIN+ clauses of that query.
#
@@ -1024,7 +1039,7 @@ module ActiveRecord
# An empty array is returned if none are found.
# [collection<<(object, ...)]
# Adds one or more objects to the collection by setting their foreign keys to the collection's primary key.
- # Note that this operation instantly fires update sql without waiting for the save or update call on the
+ # Note that this operation instantly fires update SQL without waiting for the save or update call on the
# parent object, unless the parent object is a new record.
# [collection.delete(object, ...)]
# Removes one or more objects from the collection by setting their foreign keys to +NULL+.
@@ -1060,10 +1075,10 @@ module ActiveRecord
# [collection.size]
# Returns the number of associated objects.
# [collection.find(...)]
- # Finds an associated object according to the same rules as ActiveRecord::Base.find.
+ # Finds an associated object according to the same rules as <tt>ActiveRecord::Base.find</tt>.
# [collection.exists?(...)]
# Checks whether an associated object with the given conditions exists.
- # Uses the same rules as ActiveRecord::Base.exists?.
+ # Uses the same rules as <tt>ActiveRecord::Base.exists?</tt>.
# [collection.build(attributes = {}, ...)]
# Returns one or more new objects of the collection type that have been instantiated
# with +attributes+ and linked to this object through a foreign key, but have not yet
@@ -1073,13 +1088,16 @@ module ActiveRecord
# with +attributes+, linked to this object through a foreign key, and that has already
# been saved (if it passed the validation). *Note*: This only works if the base model
# already exists in the DB, not if it is a new (unsaved) record!
+ # [collection.create!(attributes = {})]
+ # Does the same as <tt>collection.create</tt>, but raises <tt>ActiveRecord::RecordInvalid</tt>
+ # if the record is invalid.
#
# (*Note*: +collection+ is replaced with the symbol passed as the first argument, so
# <tt>has_many :clients</tt> would add among others <tt>clients.empty?</tt>.)
#
# === Example
#
- # Example: A Firm class declares <tt>has_many :clients</tt>, which will add:
+ # A <tt>Firm</tt> class declares <tt>has_many :clients</tt>, which will add:
# * <tt>Firm#clients</tt> (similar to <tt>Client.where(firm_id: id)</tt>)
# * <tt>Firm#clients<<</tt>
# * <tt>Firm#clients.delete</tt>
@@ -1094,6 +1112,7 @@ module ActiveRecord
# * <tt>Firm#clients.exists?(name: 'ACME')</tt> (similar to <tt>Client.exists?(name: 'ACME', firm_id: firm.id)</tt>)
# * <tt>Firm#clients.build</tt> (similar to <tt>Client.new("firm_id" => id)</tt>)
# * <tt>Firm#clients.create</tt> (similar to <tt>c = Client.new("firm_id" => id); c.save; c</tt>)
+ # * <tt>Firm#clients.create!</tt> (similar to <tt>c = Client.new("firm_id" => id); c.save!</tt>)
# The declaration can also include an options hash to specialize the behavior of the association.
#
# === Options
@@ -1112,8 +1131,8 @@ module ActiveRecord
# Controls what happens to the associated objects when
# their owner is destroyed. Note that these are implemented as
# callbacks, and Rails executes callbacks in order. Therefore, other
- # similar callbacks may affect the :dependent behavior, and the
- # :dependent behavior may affect other callbacks.
+ # similar callbacks may affect the <tt>:dependent</tt> behavior, and the
+ # <tt>:dependent</tt> behavior may affect other callbacks.
#
# * <tt>:destroy</tt> causes all the associated objects to also be destroyed.
# * <tt>:delete_all</tt> causes all the associated objects to be deleted directly from the database (so callbacks will not be executed).
@@ -1159,8 +1178,8 @@ module ActiveRecord
# If true, always save the associated objects or destroy them if marked for destruction,
# when saving the parent object. If false, never save or destroy the associated objects.
# By default, only save associated objects that are new records. This option is implemented as a
- # before_save callback. Because callbacks are run in the order they are defined, associated objects
- # may need to be explicitly saved in any user-defined before_save callbacks.
+ # +before_save+ callback. Because callbacks are run in the order they are defined, associated objects
+ # may need to be explicitly saved in any user-defined +before_save+ callbacks.
#
# Note that <tt>accepts_nested_attributes_for</tt> sets <tt>:autosave</tt> to <tt>true</tt>.
# [:inverse_of]
@@ -1179,13 +1198,14 @@ module ActiveRecord
# has_many :reports, -> { readonly }
# has_many :subscribers, through: :subscriptions, source: :user
def has_many(name, scope = nil, options = {}, &extension)
- Builder::HasMany.build(self, name, scope, options, &extension)
+ reflection = Builder::HasMany.build(self, name, scope, options, &extension)
+ Reflection.add_reflection self, name, reflection
end
# Specifies a one-to-one association with another class. This method should only be used
# if the other class contains the foreign key. If the current class contains the foreign key,
# then you should use +belongs_to+ instead. See also ActiveRecord::Associations::ClassMethods's overview
- # on when to use has_one and when to use belongs_to.
+ # on when to use +has_one+ and when to use +belongs_to+.
#
# The following methods for retrieval and query of a single associated object will be added:
#
@@ -1282,7 +1302,8 @@ module ActiveRecord
# has_one :club, through: :membership
# has_one :primary_address, -> { where primary: true }, through: :addressables, source: :addressable
def has_one(name, scope = nil, options = {})
- Builder::HasOne.build(self, name, scope, options)
+ reflection = Builder::HasOne.build(self, name, scope, options)
+ Reflection.add_reflection self, name, reflection
end
# Specifies a one-to-one association with another class. This method should only be used
@@ -1353,7 +1374,7 @@ module ActiveRecord
# class is created and decremented when it's destroyed. This requires that a column
# named <tt>#{table_name}_count</tt> (such as +comments_count+ for a belonging Comment class)
# is used on the associate class (such as a Post class) - that is the migration for
- # <tt>#{table_name}_count</tt> is created on the associate class (such that Post.comments_count will
+ # <tt>#{table_name}_count</tt> is created on the associate class (such that <tt>Post.comments_count</tt> will
# return the count cached, see note below). You can also specify a custom counter
# cache column by providing a column name instead of a +true+/+false+ value to this
# option (e.g., <tt>counter_cache: :my_custom_counter</tt>.)
@@ -1394,7 +1415,8 @@ module ActiveRecord
# belongs_to :company, touch: true
# belongs_to :company, touch: :employees_last_updated_at
def belongs_to(name, scope = nil, options = {})
- Builder::BelongsTo.build(self, name, scope, options)
+ reflection = Builder::BelongsTo.build(self, name, scope, options)
+ Reflection.add_reflection self, name, reflection
end
# Specifies a many-to-many relationship with another class. This associates two classes via an
@@ -1435,7 +1457,7 @@ module ActiveRecord
# [collection<<(object, ...)]
# Adds one or more objects to the collection by creating associations in the join table
# (<tt>collection.push</tt> and <tt>collection.concat</tt> are aliases to this method).
- # Note that this operation instantly fires update sql without waiting for the save or update call on the
+ # Note that this operation instantly fires update SQL without waiting for the save or update call on the
# parent object, unless the parent object is a new record.
# [collection.delete(object, ...)]
# Removes one or more objects from the collection by removing their associations from the join table.
@@ -1458,10 +1480,10 @@ module ActiveRecord
# [collection.find(id)]
# Finds an associated object responding to the +id+ and that
# meets the condition that it has to be associated with this object.
- # Uses the same rules as ActiveRecord::Base.find.
+ # Uses the same rules as <tt>ActiveRecord::Base.find</tt>.
# [collection.exists?(...)]
# Checks whether an associated object with the given conditions exists.
- # Uses the same rules as ActiveRecord::Base.exists?.
+ # Uses the same rules as <tt>ActiveRecord::Base.exists?</tt>.
# [collection.build(attributes = {})]
# Returns a new object of the collection type that has been instantiated
# with +attributes+ and linked to this object through the join table, but has not yet been saved.
@@ -1531,7 +1553,39 @@ module ActiveRecord
# has_and_belongs_to_many :categories, join_table: "prods_cats"
# has_and_belongs_to_many :categories, -> { readonly }
def has_and_belongs_to_many(name, scope = nil, options = {}, &extension)
- Builder::HasAndBelongsToMany.build(self, name, scope, options, &extension)
+ if scope.is_a?(Hash)
+ options = scope
+ scope = nil
+ end
+
+ builder = Builder::HasAndBelongsToMany.new name, self, options
+
+ join_model = builder.through_model
+
+ middle_reflection = builder.middle_reflection join_model
+
+ Builder::HasMany.define_callbacks self, middle_reflection
+ Reflection.add_reflection self, middle_reflection.name, middle_reflection
+
+ include Module.new {
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
+ def destroy_associations
+ association(:#{middle_reflection.name}).delete_all(:delete_all)
+ association(:#{name}).reset
+ super
+ end
+ RUBY
+ }
+
+ hm_options = {}
+ hm_options[:through] = middle_reflection.name
+ hm_options[:source] = join_model.right_reflection.name
+
+ [:before_add, :after_add, :before_remove, :after_remove].each do |k|
+ hm_options[k] = options[k] if options.key? k
+ end
+
+ has_many name, scope, hm_options, &extension
end
end
end
diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb
index 729ef8c55a..67ea489b22 100644
--- a/activerecord/lib/active_record/associations/association.rb
+++ b/activerecord/lib/active_record/associations/association.rb
@@ -13,11 +13,11 @@ module ActiveRecord
# BelongsToAssociation
# BelongsToPolymorphicAssociation
# CollectionAssociation
- # HasAndBelongsToManyAssociation
# HasManyAssociation
# HasManyThroughAssociation + ThroughAssociation
class Association #:nodoc:
attr_reader :owner, :target, :reflection
+ attr_accessor :inversed
delegate :options, :to => :reflection
@@ -30,7 +30,7 @@ module ActiveRecord
reset_scope
end
- # Returns the name of the table of the related class:
+ # Returns the name of the table of the associated class:
#
# post.comments.aliased_table_name # => "comments"
#
@@ -43,6 +43,7 @@ module ActiveRecord
@loaded = false
@target = nil
@stale_state = nil
+ @inversed = false
end
# Reloads the \target and returns +self+ on success.
@@ -60,18 +61,19 @@ module ActiveRecord
# Asserts the \target has been loaded setting the \loaded flag to +true+.
def loaded!
- @loaded = true
+ @loaded = true
@stale_state = stale_state
+ @inversed = false
end
# The target is stale if the target no longer points to the record(s) that the
# relevant foreign_key(s) refers to. If stale, the association accessor method
# on the owner will reload the target. It's up to subclasses to implement the
- # state_state method if relevant.
+ # stale_state method if relevant.
#
# Note that if the target has not been loaded, it is not considered stale.
def stale_target?
- loaded? && @stale_state != stale_state
+ !inversed && loaded? && @stale_state != stale_state
end
# Sets the target of this association to <tt>\target</tt>, and the \loaded flag to +true+.
@@ -84,11 +86,6 @@ module ActiveRecord
target_scope.merge(association_scope)
end
- def scoped
- ActiveSupport::Deprecation.warn "#scoped is deprecated. use #scope instead."
- scope
- end
-
# The scope for this association.
#
# Note that the association_scope is merged into the target_scope only when the
@@ -107,10 +104,12 @@ module ActiveRecord
# Set the inverse association, if possible
def set_inverse_instance(record)
- if record && invertible_for?(record)
+ if invertible_for?(record)
inverse = record.association(inverse_reflection_for(record).name)
inverse.target = owner
+ inverse.inversed = true
end
+ record
end
# Returns the class of the target. belongs_to polymorphic overrides this to look at the
@@ -122,7 +121,7 @@ module ActiveRecord
# Can be overridden (i.e. in ThroughAssociation) to merge in other scopes (i.e. the
# through association's scope)
def target_scope
- klass.all
+ AssociationRelation.create(klass, klass.arel_table, self).merge!(klass.all)
end
# Loads the \target if needed and returns it.
@@ -164,6 +163,13 @@ module ActiveRecord
@reflection = @owner.class.reflect_on_association(reflection_name)
end
+ def initialize_attributes(record) #:nodoc:
+ skip_assign = [reflection.foreign_key, reflection.type].compact
+ attributes = create_scope.except(*(record.changed - skip_assign))
+ record.assign_attributes(attributes)
+ set_inverse_instance(record)
+ end
+
private
def find_target?
@@ -189,13 +195,14 @@ module ActiveRecord
creation_attributes.each { |key, value| record[key] = value }
end
- # Should be true if there is a foreign key present on the owner which
+ # Returns true if there is a foreign key present on the owner which
# references the target. This is used to determine whether we can load
# the target if the owner is currently a new record (and therefore
- # without a key).
+ # without a key). If the owner is a new record then foreign_key must
+ # be present in order to load target.
#
# Currently implemented by belongs_to (vanilla and polymorphic) and
- # has_one/has_many :through associations which go through a belongs_to
+ # has_one/has_many :through associations which go through a belongs_to.
def foreign_key_present?
false
end
@@ -220,7 +227,12 @@ module ActiveRecord
# Returns true if inverse association on the given record needs to be set.
# This method is redefined by subclasses.
def invertible_for?(record)
- inverse_reflection_for(record)
+ foreign_key_for?(record) && inverse_reflection_for(record)
+ end
+
+ # Returns true if record contains the foreign_key
+ def foreign_key_for?(record)
+ record.attributes.has_key? reflection.foreign_key
end
# This should be implemented to return the values of the relevant key(s) on the owner,
@@ -233,10 +245,7 @@ module ActiveRecord
def build_record(attributes)
reflection.build_association(attributes) do |record|
- skip_assign = [reflection.foreign_key, reflection.type].compact
- attributes = create_scope.except(*(record.changed - skip_assign))
- record.assign_attributes(attributes)
- set_inverse_instance(record)
+ initialize_attributes(record)
end
end
end
diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb
index aa5551fe0c..17f056e764 100644
--- a/activerecord/lib/active_record/associations/association_scope.rb
+++ b/activerecord/lib/active_record/associations/association_scope.rb
@@ -44,18 +44,6 @@ module ActiveRecord
chain.each_with_index do |reflection, i|
table, foreign_table = tables.shift, tables.first
- if reflection.source_macro == :has_and_belongs_to_many
- join_table = tables.shift
-
- scope = scope.joins(join(
- join_table,
- table[reflection.association_primary_key].
- eq(join_table[reflection.association_foreign_key])
- ))
-
- table, foreign_table = join_table, tables.first
- end
-
if reflection.source_macro == :belongs_to
if reflection.options[:polymorphic]
key = reflection.association_primary_key(self.klass)
@@ -82,24 +70,30 @@ module ActiveRecord
constraint = table[key].eq(foreign_table[foreign_key])
if reflection.type
- type = chain[i + 1].klass.base_class.name
- constraint = constraint.and(table[reflection.type].eq(type))
+ value = chain[i + 1].klass.base_class.name
+ bind_val = bind scope, table.table_name, reflection.type.to_s, value
+ scope = scope.where(table[reflection.type].eq(bind_val))
end
scope = scope.joins(join(foreign_table, constraint))
end
+ is_first_chain = i == 0
+ klass = is_first_chain ? self.klass : reflection.klass
+
# Exclude the scope of the association itself, because that
# was already merged in the #scope method.
scope_chain[i].each do |scope_chain_item|
- klass = i == 0 ? self.klass : reflection.klass
item = eval_scope(klass, scope_chain_item)
if scope_chain_item == self.reflection.scope
- scope.merge! item.except(:where, :includes)
+ scope.merge! item.except(:where, :includes, :bind)
+ end
+
+ if is_first_chain
+ scope.includes! item.includes_values
end
- scope.includes! item.includes_values
scope.where_values += item.where_values
scope.order_values |= item.order_values
end
@@ -119,7 +113,7 @@ module ActiveRecord
# the owner
klass.table_name
else
- reflection.table_name
+ super
end
end
diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb
index 8eec4f56af..8272a5584c 100644
--- a/activerecord/lib/active_record/associations/belongs_to_association.rb
+++ b/activerecord/lib/active_record/associations/belongs_to_association.rb
@@ -8,13 +8,16 @@ module ActiveRecord
end
def replace(record)
- raise_on_type_mismatch!(record) if record
-
- update_counters(record)
- replace_keys(record)
- set_inverse_instance(record)
-
- @updated = true if record
+ if record
+ raise_on_type_mismatch!(record)
+ update_counters(record)
+ replace_keys(record)
+ set_inverse_instance(record)
+ @updated = true
+ else
+ decrement_counters
+ remove_keys
+ end
self.target = record
end
@@ -34,35 +37,41 @@ module ActiveRecord
!loaded? && foreign_key_present? && klass
end
- def update_counters(record)
+ def with_cache_name
counter_cache_name = reflection.counter_cache_column
+ return unless counter_cache_name && owner.persisted?
+ yield counter_cache_name
+ end
+
+ def update_counters(record)
+ with_cache_name do |name|
+ return unless different_target? record
+ record.class.increment_counter(name, record.id)
+ decrement_counter name
+ end
+ end
- if counter_cache_name && owner.persisted? && different_target?(record)
- if record
- record.class.increment_counter(counter_cache_name, record.id)
- end
+ def decrement_counters
+ with_cache_name { |name| decrement_counter name }
+ end
- if foreign_key_present?
- klass.decrement_counter(counter_cache_name, target_id)
- end
+ def decrement_counter counter_cache_name
+ if foreign_key_present?
+ klass.decrement_counter(counter_cache_name, target_id)
end
end
# Checks whether record is different to the current target, without loading it
def different_target?(record)
- if record.nil?
- owner[reflection.foreign_key]
- else
- record.id != owner[reflection.foreign_key]
- end
+ record.id != owner[reflection.foreign_key]
end
def replace_keys(record)
- if record
- owner[reflection.foreign_key] = record[reflection.association_primary_key(record.class)]
- else
- owner[reflection.foreign_key] = nil
- end
+ owner[reflection.foreign_key] = record[reflection.association_primary_key(record.class)]
+ end
+
+ def remove_keys
+ owner[reflection.foreign_key] = nil
end
def foreign_key_present?
diff --git a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb
index eae5eed3a1..b710cf6bdb 100644
--- a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb
+++ b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb
@@ -11,7 +11,12 @@ module ActiveRecord
def replace_keys(record)
super
- owner[reflection.foreign_type] = record && record.class.base_class.name
+ owner[reflection.foreign_type] = record.class.base_class.name
+ end
+
+ def remove_keys
+ super
+ owner[reflection.foreign_type] = nil
end
def different_target?(record)
diff --git a/activerecord/lib/active_record/associations/builder/association.rb b/activerecord/lib/active_record/associations/builder/association.rb
index 5c37f42794..3911d1b520 100644
--- a/activerecord/lib/active_record/associations/builder/association.rb
+++ b/activerecord/lib/active_record/associations/builder/association.rb
@@ -1,50 +1,66 @@
+require 'active_support/core_ext/module/attribute_accessors'
+
+# This is the parent Association class which defines the variables
+# used by all associations.
+#
+# The hierarchy is defined as follows:
+# Association
+# - SingularAssociation
+# - BelongsToAssociation
+# - HasOneAssociation
+# - CollectionAssociation
+# - HasManyAssociation
+
module ActiveRecord::Associations::Builder
class Association #:nodoc:
class << self
+ attr_accessor :extensions
+ # TODO: This class accessor is needed to make activerecord-deprecated_finders work.
+ # We can move it to a constant in 5.0.
attr_accessor :valid_options
end
+ self.extensions = []
- self.valid_options = [:class_name, :foreign_key, :validate]
+ self.valid_options = [:class_name, :class, :foreign_key, :validate]
- attr_reader :model, :name, :scope, :options, :reflection
+ attr_reader :name, :scope, :options
- def self.build(*args, &block)
- new(*args, &block).build
+ def self.build(model, name, scope, options, &block)
+ builder = create_builder model, name, scope, options, &block
+ reflection = builder.build(model)
+ define_accessors model, reflection
+ define_callbacks model, reflection
+ builder.define_extensions model
+ reflection
end
- def initialize(model, name, scope, options)
+ def self.create_builder(model, name, scope, options, &block)
raise ArgumentError, "association names must be a Symbol" unless name.kind_of?(Symbol)
- @model = model
- @name = name
+ new(model, name, scope, options, &block)
+ end
+ def initialize(model, name, scope, options)
+ # TODO: Move this to create_builder as soon we drop support to activerecord-deprecated_finders.
if scope.is_a?(Hash)
- @scope = nil
- @options = scope
- else
- @scope = scope
- @options = options
+ options = scope
+ scope = nil
end
- if @scope && @scope.arity == 0
- prev_scope = @scope
- @scope = proc { instance_exec(&prev_scope) }
- end
- end
+ # TODO: Remove this model argument as soon we drop support to activerecord-deprecated_finders.
+ @name = name
+ @scope = scope
+ @options = options
- def mixin
- @model.generated_feature_methods
- end
+ validate_options
- include Module.new { def build; end }
+ if scope && scope.arity == 0
+ @scope = proc { instance_exec(&scope) }
+ end
+ end
- def build
- validate_options
- define_accessors
- configure_dependency if options[:dependent]
- @reflection = model.create_reflection(macro, name, scope, options, model)
- super # provides an extension point
- @reflection
+ def build(model)
+ ActiveRecord::Reflection.create(macro, name, scope, options, model)
end
def macro
@@ -52,19 +68,37 @@ module ActiveRecord::Associations::Builder
end
def valid_options
- Association.valid_options
+ Association.valid_options + Association.extensions.flat_map(&:valid_options)
end
def validate_options
options.assert_valid_keys(valid_options)
end
- def define_accessors
- define_readers
- define_writers
+ def define_extensions(model)
end
- def define_readers
+ def self.define_callbacks(model, reflection)
+ add_before_destroy_callbacks(model, reflection) if reflection.options[:dependent]
+ Association.extensions.each do |extension|
+ extension.build model, reflection
+ end
+ end
+
+ # Defines the setter and getter methods for the association
+ # class Post < ActiveRecord::Base
+ # has_many :comments
+ # end
+ #
+ # Post.first.comments and Post.first.comments= methods are defined by this method...
+ def self.define_accessors(model, reflection)
+ mixin = model.generated_association_methods
+ name = reflection.name
+ define_readers(mixin, name)
+ define_writers(mixin, name)
+ end
+
+ def self.define_readers(mixin, name)
mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
def #{name}(*args)
association(:#{name}).reader(*args)
@@ -72,7 +106,7 @@ module ActiveRecord::Associations::Builder
CODE
end
- def define_writers
+ def self.define_writers(mixin, name)
mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
def #{name}=(value)
association(:#{name}).writer(value)
@@ -80,29 +114,19 @@ module ActiveRecord::Associations::Builder
CODE
end
- def configure_dependency
- unless valid_dependent_options.include? options[:dependent]
- raise ArgumentError, "The :dependent option must be one of #{valid_dependent_options}, but is :#{options[:dependent]}"
- end
-
- if options[:dependent] == :restrict
- ActiveSupport::Deprecation.warn(
- "The :restrict option is deprecated. Please use :restrict_with_exception instead, which " \
- "provides the same functionality."
- )
- end
+ def self.valid_dependent_options
+ raise NotImplementedError
+ end
- mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
- def #{macro}_dependent_for_#{name}
- association(:#{name}).handle_dependency
- end
- CODE
+ private
- model.before_destroy "#{macro}_dependent_for_#{name}"
- end
+ def self.add_before_destroy_callbacks(model, reflection)
+ unless valid_dependent_options.include? reflection.options[:dependent]
+ raise ArgumentError, "The :dependent option must be one of #{valid_dependent_options}, but is :#{reflection.options[:dependent]}"
+ end
- def valid_dependent_options
- raise NotImplementedError
+ name = reflection.name
+ model.before_destroy lambda { |o| o.association(name).handle_dependency }
end
end
end
diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb
index 3ba6a71366..5ccaa55a32 100644
--- a/activerecord/lib/active_record/associations/builder/belongs_to.rb
+++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb
@@ -5,93 +5,134 @@ module ActiveRecord::Associations::Builder
end
def valid_options
- super + [:foreign_type, :polymorphic, :touch]
+ super + [:foreign_type, :polymorphic, :touch, :counter_cache]
end
- def constructable?
- !options[:polymorphic]
+ def self.valid_dependent_options
+ [:destroy, :delete]
end
- def build
- reflection = super
- add_counter_cache_callbacks(reflection) if options[:counter_cache]
- add_touch_callbacks(reflection) if options[:touch]
- reflection
+ def self.define_callbacks(model, reflection)
+ super
+ add_counter_cache_callbacks(model, reflection) if reflection.options[:counter_cache]
+ add_touch_callbacks(model, reflection) if reflection.options[:touch]
end
- def add_counter_cache_callbacks(reflection)
- cache_column = reflection.counter_cache_column
- foreign_key = reflection.foreign_key
+ def self.define_accessors(mixin, reflection)
+ super
+ add_counter_cache_methods mixin
+ end
+
+ private
+
+ def self.add_counter_cache_methods(mixin)
+ return if mixin.method_defined? :belongs_to_counter_cache_after_create
- mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
- def belongs_to_counter_cache_after_create_for_#{name}
- record = #{name}
- record.class.increment_counter(:#{cache_column}, record.id) unless record.nil?
- @_after_create_counter_called = true
+ mixin.class_eval do
+ def belongs_to_counter_cache_after_create(reflection)
+ if record = send(reflection.name)
+ cache_column = reflection.counter_cache_column
+ record.class.increment_counter(cache_column, record.id)
+ @_after_create_counter_called = true
+ end
end
- def belongs_to_counter_cache_before_destroy_for_#{name}
- unless destroyed_by_association && destroyed_by_association.foreign_key.to_sym == #{foreign_key.to_sym.inspect}
- record = #{name}
- record.class.decrement_counter(:#{cache_column}, record.id) unless record.nil?
+ def belongs_to_counter_cache_before_destroy(reflection)
+ foreign_key = reflection.foreign_key.to_sym
+ unless destroyed_by_association && destroyed_by_association.foreign_key.to_sym == foreign_key
+ record = send reflection.name
+ if record && !self.destroyed?
+ cache_column = reflection.counter_cache_column
+ record.class.decrement_counter(cache_column, record.id)
+ end
end
end
- def belongs_to_counter_cache_after_update_for_#{name}
+ def belongs_to_counter_cache_after_update(reflection)
+ foreign_key = reflection.foreign_key
+ cache_column = reflection.counter_cache_column
+
if (@_after_create_counter_called ||= false)
@_after_create_counter_called = false
- elsif self.#{foreign_key}_changed? && !new_record? && defined?(#{name.to_s.camelize})
- model = #{name.to_s.camelize}
- foreign_key_was = self.#{foreign_key}_was
- foreign_key = self.#{foreign_key}
+ elsif attribute_changed?(foreign_key) && !new_record? && reflection.constructable?
+ model = reflection.klass
+ foreign_key_was = attribute_was foreign_key
+ foreign_key = attribute foreign_key
if foreign_key && model.respond_to?(:increment_counter)
- model.increment_counter(:#{cache_column}, foreign_key)
+ 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)
+ model.decrement_counter(cache_column, foreign_key_was)
end
end
end
- CODE
+ end
+ end
+
+ def self.add_counter_cache_callbacks(model, reflection)
+ cache_column = reflection.counter_cache_column
- model.after_create "belongs_to_counter_cache_after_create_for_#{name}"
- model.before_destroy "belongs_to_counter_cache_before_destroy_for_#{name}"
- model.after_update "belongs_to_counter_cache_after_update_for_#{name}"
+ model.after_create lambda { |record|
+ record.belongs_to_counter_cache_after_create(reflection)
+ }
+
+ model.before_destroy lambda { |record|
+ record.belongs_to_counter_cache_before_destroy(reflection)
+ }
+
+ model.after_update lambda { |record|
+ record.belongs_to_counter_cache_after_update(reflection)
+ }
klass = reflection.class_name.safe_constantize
klass.attr_readonly cache_column if klass && klass.respond_to?(:attr_readonly)
end
- def add_touch_callbacks(reflection)
- mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
- def belongs_to_touch_after_save_or_destroy_for_#{name}
- foreign_key_field = #{reflection.foreign_key.inspect}
- old_foreign_id = attribute_was(foreign_key_field)
-
- if old_foreign_id
- reflection_klass = #{reflection.klass}
- old_record = reflection_klass.find_by(reflection_klass.primary_key => old_foreign_id)
+ def self.touch_record(o, foreign_key, name, touch) # :nodoc:
+ old_foreign_id = o.changed_attributes[foreign_key]
- if old_record
- old_record.touch #{options[:touch].inspect if options[:touch] != true}
- end
- end
+ if old_foreign_id
+ association = o.association(name)
+ reflection = association.reflection
+ if reflection.polymorphic?
+ klass = o.public_send("#{reflection.foreign_type}_was").constantize
+ else
+ klass = association.klass
+ end
+ old_record = klass.find_by(klass.primary_key => old_foreign_id)
- record = #{name}
- unless record.nil? || record.new_record?
- record.touch #{options[:touch].inspect if options[:touch] != true}
+ if old_record
+ if touch != true
+ old_record.touch touch
+ else
+ old_record.touch
end
end
- CODE
-
- model.after_save "belongs_to_touch_after_save_or_destroy_for_#{name}"
- model.after_touch "belongs_to_touch_after_save_or_destroy_for_#{name}"
- model.after_destroy "belongs_to_touch_after_save_or_destroy_for_#{name}"
+ end
+
+ record = o.send name
+ if record && record.persisted?
+ if touch != true
+ record.touch touch
+ else
+ record.touch
+ end
+ end
end
- def valid_dependent_options
- [:destroy, :delete]
+ def self.add_touch_callbacks(model, reflection)
+ foreign_key = reflection.foreign_key
+ n = reflection.name
+ touch = reflection.options[:touch]
+
+ callback = lambda { |record|
+ BelongsTo.touch_record(record, foreign_key, n, touch)
+ }
+
+ model.after_save callback
+ model.after_touch callback
+ model.after_destroy callback
end
end
end
diff --git a/activerecord/lib/active_record/associations/builder/collection_association.rb b/activerecord/lib/active_record/associations/builder/collection_association.rb
index fdead16761..bc15a49996 100644
--- a/activerecord/lib/active_record/associations/builder/collection_association.rb
+++ b/activerecord/lib/active_record/associations/builder/collection_association.rb
@@ -1,3 +1,5 @@
+# This class is inherited by the has_many and has_many_and_belongs_to_many association classes
+
require 'active_record/associations'
module ActiveRecord::Associations::Builder
@@ -6,67 +8,57 @@ module ActiveRecord::Associations::Builder
CALLBACKS = [:before_add, :after_add, :before_remove, :after_remove]
def valid_options
- super + [:table_name, :finder_sql, :counter_sql, :before_add,
+ super + [:table_name, :before_add,
:after_add, :before_remove, :after_remove, :extend]
end
- attr_reader :block_extension, :extension_module
-
- def initialize(*args, &extension)
- super(*args)
- @block_extension = extension
- end
+ attr_reader :block_extension
- def build
- show_deprecation_warnings
- wrap_block_extension
- reflection = super
- CALLBACKS.each { |callback_name| define_callback(callback_name) }
- reflection
+ def initialize(model, name, scope, options)
+ super
+ @mod = nil
+ if block_given?
+ @mod = Module.new(&Proc.new)
+ @scope = wrap_scope @scope, @mod
+ end
end
- def writable?
- true
+ def self.define_callbacks(model, reflection)
+ super
+ name = reflection.name
+ options = reflection.options
+ CALLBACKS.each { |callback_name|
+ define_callback(model, callback_name, name, options)
+ }
end
- def show_deprecation_warnings
- [:finder_sql, :counter_sql].each do |name|
- if options.include? name
- ActiveSupport::Deprecation.warn("The :#{name} association option is deprecated. Please find an alternative (such as using scopes).")
- end
+ def define_extensions(model)
+ if @mod
+ extension_module_name = "#{model.name.demodulize}#{name.to_s.camelize}AssociationExtension"
+ model.parent.const_set(extension_module_name, @mod)
end
end
- def wrap_block_extension
- if block_extension
- @extension_module = mod = Module.new(&block_extension)
- silence_warnings do
- model.parent.const_set(extension_module_name, mod)
- end
-
- prev_scope = @scope
+ def self.define_callback(model, callback_name, name, options)
+ full_callback_name = "#{callback_name}_for_#{name}"
- if prev_scope
- @scope = proc { |owner| instance_exec(owner, &prev_scope).extending(mod) }
+ # TODO : why do i need method_defined? I think its because of the inheritance chain
+ model.class_attribute full_callback_name unless model.method_defined?(full_callback_name)
+ callbacks = Array(options[callback_name.to_sym]).map do |callback|
+ case callback
+ when Symbol
+ ->(method, owner, record) { owner.send(callback, record) }
+ when Proc
+ ->(method, owner, record) { callback.call(owner, record) }
else
- @scope = proc { extending(mod) }
+ ->(method, owner, record) { callback.send(method, owner, record) }
end
end
+ model.send "#{full_callback_name}=", callbacks
end
- def extension_module_name
- @extension_module_name ||= "#{model.name.demodulize}#{name.to_s.camelize}AssociationExtension"
- end
-
- def define_callback(callback_name)
- full_callback_name = "#{callback_name}_for_#{name}"
-
- # TODO : why do i need method_defined? I think its because of the inheritance chain
- model.class_attribute full_callback_name.to_sym unless model.method_defined?(full_callback_name)
- model.send("#{full_callback_name}=", Array(options[callback_name.to_sym]))
- end
-
- def define_readers
+ # Defines the setter and getter methods for the collection_singular_ids.
+ def self.define_readers(mixin, name)
super
mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
@@ -76,7 +68,7 @@ module ActiveRecord::Associations::Builder
CODE
end
- def define_writers
+ def self.define_writers(mixin, name)
super
mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
@@ -85,5 +77,15 @@ module ActiveRecord::Associations::Builder
end
CODE
end
+
+ private
+
+ def wrap_scope(scope, mod)
+ if scope
+ proc { |owner| instance_exec(owner, &scope).extending(mod) }
+ else
+ proc { extending(mod) }
+ end
+ end
end
end
diff --git a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb
index bdac02b5bf..e472277374 100644
--- a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb
+++ b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb
@@ -1,39 +1,121 @@
module ActiveRecord::Associations::Builder
- class HasAndBelongsToMany < CollectionAssociation #:nodoc:
- def macro
- :has_and_belongs_to_many
- end
+ class HasAndBelongsToMany # :nodoc:
+ class JoinTableResolver
+ KnownTable = Struct.new :join_table
+
+ class KnownClass
+ def initialize(lhs_class, rhs_class_name)
+ @lhs_class = lhs_class
+ @rhs_class_name = rhs_class_name
+ @join_table = nil
+ end
+
+ def join_table
+ @join_table ||= [@lhs_class.table_name, klass.table_name].sort.join("\0").gsub(/^(.*_)(.+)\0\1(.+)/, '\1\2_\3').gsub("\0", "_")
+ end
+
+ private
+ def klass; @rhs_class_name.constantize; end
+ end
- def valid_options
- super + [:join_table, :association_foreign_key, :delete_sql, :insert_sql]
+ def self.build(lhs_class, name, options)
+ if options[:join_table]
+ KnownTable.new options[:join_table].to_s
+ else
+ class_name = options.fetch(:class_name) {
+ name.to_s.camelize.singularize
+ }
+ KnownClass.new lhs_class, class_name
+ end
+ end
end
- def build
- reflection = super
- define_destroy_hook
- reflection
+ attr_reader :lhs_model, :association_name, :options
+
+ def initialize(association_name, lhs_model, options)
+ @association_name = association_name
+ @lhs_model = lhs_model
+ @options = options
end
- def show_deprecation_warnings
- super
+ def through_model
+ habtm = JoinTableResolver.build lhs_model, association_name, options
+
+ join_model = Class.new(ActiveRecord::Base) {
+ class << self;
+ attr_accessor :class_resolver
+ attr_accessor :name
+ attr_accessor :table_name_resolver
+ attr_accessor :left_reflection
+ attr_accessor :right_reflection
+ end
+
+ def self.table_name
+ table_name_resolver.join_table
+ end
+
+ def self.compute_type(class_name)
+ class_resolver.compute_type class_name
+ end
- [:delete_sql, :insert_sql].each do |name|
- if options.include? name
- ActiveSupport::Deprecation.warn("The :#{name} association option is deprecated. Please find an alternative (such as using has_many :through).")
+ def self.add_left_association(name, options)
+ belongs_to name, options
+ self.left_reflection = reflect_on_association(name)
end
+
+ def self.add_right_association(name, options)
+ rhs_name = name.to_s.singularize.to_sym
+ belongs_to rhs_name, options
+ self.right_reflection = reflect_on_association(rhs_name)
+ end
+
+ }
+
+ join_model.name = "HABTM_#{association_name.to_s.camelize}"
+ join_model.table_name_resolver = habtm
+ join_model.class_resolver = lhs_model
+
+ join_model.add_left_association :left_side, class: lhs_model
+ join_model.add_right_association association_name, belongs_to_options(options)
+ join_model
+ end
+
+ def middle_reflection(join_model)
+ middle_name = [lhs_model.name.downcase.pluralize,
+ association_name].join('_').gsub(/::/, '_').to_sym
+ middle_options = middle_options join_model
+ hm_builder = HasMany.create_builder(lhs_model,
+ middle_name,
+ nil,
+ middle_options)
+ hm_builder.build lhs_model
+ end
+
+ private
+
+ def middle_options(join_model)
+ middle_options = {}
+ middle_options[:class] = join_model
+ middle_options[:source] = join_model.left_reflection.name
+ if options.key? :foreign_key
+ middle_options[:foreign_key] = options[:foreign_key]
end
+ middle_options
end
- def define_destroy_hook
- name = self.name
- model.send(:include, Module.new {
- class_eval <<-RUBY, __FILE__, __LINE__ + 1
- def destroy_associations
- association(:#{name}).delete_all
- super
- end
- RUBY
- })
+ def belongs_to_options(options)
+ rhs_options = {}
+
+ if options.key? :class_name
+ rhs_options[:foreign_key] = options[:class_name].foreign_key
+ rhs_options[:class_name] = options[:class_name]
+ end
+
+ if options.key? :association_foreign_key
+ rhs_options[:foreign_key] = options[:association_foreign_key]
+ end
+
+ rhs_options
end
end
end
diff --git a/activerecord/lib/active_record/associations/builder/has_many.rb b/activerecord/lib/active_record/associations/builder/has_many.rb
index 0d1bdd21ee..7909b93622 100644
--- a/activerecord/lib/active_record/associations/builder/has_many.rb
+++ b/activerecord/lib/active_record/associations/builder/has_many.rb
@@ -8,8 +8,8 @@ module ActiveRecord::Associations::Builder
super + [:primary_key, :dependent, :as, :through, :source, :source_type, :inverse_of, :counter_cache]
end
- def valid_dependent_options
- [:destroy, :delete_all, :nullify, :restrict, :restrict_with_error, :restrict_with_exception]
+ def self.valid_dependent_options
+ [:destroy, :delete_all, :nullify, :restrict_with_error, :restrict_with_exception]
end
end
end
diff --git a/activerecord/lib/active_record/associations/builder/has_one.rb b/activerecord/lib/active_record/associations/builder/has_one.rb
index 0da564f402..f359efd496 100644
--- a/activerecord/lib/active_record/associations/builder/has_one.rb
+++ b/activerecord/lib/active_record/associations/builder/has_one.rb
@@ -10,16 +10,14 @@ module ActiveRecord::Associations::Builder
valid
end
- def constructable?
- !options[:through]
+ def self.valid_dependent_options
+ [:destroy, :delete, :nullify, :restrict_with_error, :restrict_with_exception]
end
- def configure_dependency
- super unless options[:through]
- end
+ private
- def valid_dependent_options
- [:destroy, :delete, :nullify, :restrict, :restrict_with_error, :restrict_with_exception]
+ def self.add_before_destroy_callbacks(model, reflection)
+ super unless reflection.options[:through]
end
end
end
diff --git a/activerecord/lib/active_record/associations/builder/singular_association.rb b/activerecord/lib/active_record/associations/builder/singular_association.rb
index 6a5830e57f..e655c389a6 100644
--- a/activerecord/lib/active_record/associations/builder/singular_association.rb
+++ b/activerecord/lib/active_record/associations/builder/singular_association.rb
@@ -1,19 +1,18 @@
+# This class is inherited by the has_one and belongs_to association classes
+
module ActiveRecord::Associations::Builder
class SingularAssociation < Association #:nodoc:
def valid_options
- super + [:remote, :dependent, :counter_cache, :primary_key, :inverse_of]
- end
-
- def constructable?
- true
+ super + [:remote, :dependent, :primary_key, :inverse_of]
end
- def define_accessors
+ def self.define_accessors(model, reflection)
super
- define_constructors if constructable?
+ define_constructors(model.generated_association_methods, reflection.name) if reflection.constructable?
end
- def define_constructors
+ # Defines the (build|create)_association methods for belongs_to or has_one association
+ def self.define_constructors(mixin, name)
mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
def build_#{name}(*args, &block)
association(:#{name}).build(*args, &block)
diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb
index 5b08d07425..52531a3520 100644
--- a/activerecord/lib/active_record/associations/collection_association.rb
+++ b/activerecord/lib/active_record/associations/collection_association.rb
@@ -4,10 +4,9 @@ module ActiveRecord
#
# CollectionAssociation is an abstract class that provides common stuff to
# ease the implementation of association proxies that represent
- # collections. See the class hierarchy in AssociationProxy.
+ # collections. See the class hierarchy in Association.
#
# CollectionAssociation:
- # HasAndBelongsToManyAssociation => has_and_belongs_to_many
# HasManyAssociation => has_many
# HasManyThroughAssociation + ThroughAssociation => has_many :through
#
@@ -34,7 +33,7 @@ module ActiveRecord
reload
end
- @proxy ||= CollectionProxy.new(klass, self)
+ @proxy ||= CollectionProxy.create(klass, self)
end
# Implements the writer method, e.g. foo.items= for Foo.has_many :items
@@ -44,7 +43,7 @@ module ActiveRecord
# Implements the ids reader method, e.g. foo.item_ids for Foo.has_many :items
def ids_reader
- if loaded? || options[:finder_sql]
+ if loaded?
load_target.map do |record|
record.send(reflection.association_primary_key)
end
@@ -79,17 +78,14 @@ module ActiveRecord
if block_given?
load_target.find(*args) { |*block_args| yield(*block_args) }
else
- if options[:finder_sql]
- find_by_scan(*args)
- elsif options[:inverse_of]
- args = args.flatten
- raise RecordNotFound, "Couldn't find #{scope.klass.name} without an ID" if args.blank?
-
+ if options[:inverse_of] && loaded?
+ args_flatten = args.flatten
+ raise RecordNotFound, "Couldn't find #{scope.klass.name} without an ID" if args_flatten.blank?
result = find_by_scan(*args)
result_size = Array(result).size
- if !result || result_size != args.size
- scope.raise_record_not_found_exception!(args, result_size, args.size)
+ if !result || result_size != args_flatten.size
+ scope.raise_record_not_found_exception!(args_flatten, result_size, args_flatten.size)
else
result
end
@@ -153,11 +149,33 @@ module ActiveRecord
end
end
- # Remove all records from this association.
+ # Removes all records from the association without calling callbacks
+ # on the associated records. It honors the `:dependent` option. However
+ # if the `:dependent` value is `:destroy` then in that case the `:delete_all`
+ # deletion strategy for the association is applied.
+ #
+ # You can force a particular deletion strategy by passing a parameter.
+ #
+ # Example:
+ #
+ # @author.books.delete_all(:nullify)
+ # @author.books.delete_all(:delete_all)
#
# See delete for more info.
- def delete_all
- delete(:all).tap do
+ def delete_all(dependent = nil)
+ if dependent.present? && ![:nullify, :delete_all].include?(dependent)
+ raise ArgumentError, "Valid values are :nullify or :delete_all"
+ end
+
+ dependent = if dependent.present?
+ dependent
+ elsif options[:dependent] == :destroy
+ :delete_all
+ else
+ options[:dependent]
+ end
+
+ delete(:all, dependent: dependent).tap do
reset
loaded!
end
@@ -173,36 +191,29 @@ module ActiveRecord
end
end
- # Count all records using SQL. If the +:counter_sql+ or +:finder_sql+ option is set for the
- # association, it will be used for the query. Otherwise, construct options and pass them with
+ # Count all records using SQL. Construct options and pass them with
# scope to the target class's +count+.
def count(column_name = nil, count_options = {})
+ # TODO: Remove count_options argument as soon we remove support to
+ # activerecord-deprecated_finders.
column_name, count_options = nil, column_name if column_name.is_a?(Hash)
- if options[:counter_sql] || options[:finder_sql]
- unless count_options.blank?
- raise ArgumentError, "If finder_sql/counter_sql is used then options cannot be passed"
- end
-
- reflection.klass.count_by_sql(custom_counter_sql)
- else
- relation = scope
- if association_scope.distinct_value
- # This is needed because 'SELECT count(DISTINCT *)..' is not valid SQL.
- column_name ||= reflection.klass.primary_key
- relation = relation.distinct
- end
+ relation = scope
+ if association_scope.distinct_value
+ # This is needed because 'SELECT count(DISTINCT *)..' is not valid SQL.
+ column_name ||= reflection.klass.primary_key
+ relation = relation.distinct
+ end
- value = relation.count(column_name)
+ value = relation.count(column_name)
- limit = options[:limit]
- offset = options[:offset]
+ limit = options[:limit]
+ offset = options[:offset]
- if limit || offset
- [ [value - offset.to_i, 0].max, limit.to_i ].min
- else
- value
- end
+ if limit || offset
+ [ [value - offset.to_i, 0].max, limit.to_i ].min
+ else
+ value
end
end
@@ -214,18 +225,10 @@ module ActiveRecord
# are actually removed from the database, that depends precisely on
# +delete_records+. They are in any case removed from the collection.
def delete(*records)
- dependent = options[:dependent]
+ _options = records.extract_options!
+ dependent = _options[:dependent] || options[:dependent]
if records.first == :all
-
- if dependent && dependent == :destroy
- message = 'In Rails 4.1 delete_all on associations would not fire callbacks. ' \
- 'It means if the :dependent option is :destroy then the associated ' \
- 'records would be deleted without loading and invoking callbacks.'
-
- ActiveRecord::Base.logger ? ActiveRecord::Base.logger.warn(message) : $stderr.puts(message)
- end
-
if loaded? || dependent == :destroy
delete_or_destroy(load_target, dependent)
else
@@ -237,11 +240,11 @@ module ActiveRecord
end
end
- # Destroy +records+ and remove them from this association calling
- # +before_remove+ and +after_remove+ callbacks.
+ # Deletes the +records+ and removes them from this association calling
+ # +before_remove+ , +after_remove+ , +before_destroy+ and +after_destroy+ callbacks.
#
- # Note that this method will _always_ remove records from the database
- # ignoring the +:dependent+ option.
+ # Note that this method removes records from the database ignoring the
+ # +:dependent+ option.
def destroy(*records)
records = find(records) if records.any? { |record| record.kind_of?(Fixnum) || record.kind_of?(String) }
delete_or_destroy(records, :destroy)
@@ -285,14 +288,14 @@ module ActiveRecord
# Returns true if the collection is empty.
#
- # If the collection has been loaded or the <tt>:counter_sql</tt> option
- # is provided, it is equivalent to <tt>collection.size.zero?</tt>. If the
+ # If the collection has been loaded
+ # it is equivalent to <tt>collection.size.zero?</tt>. If the
# collection has not been loaded, it is equivalent to
# <tt>collection.exists?</tt>. If the collection has not already been
# loaded and you are going to fetch the records anyway it is better to
# check <tt>collection.length.zero?</tt>.
def empty?
- if loaded? || options[:counter_sql]
+ if loaded?
size.zero?
else
@target.blank? && !scope.exists?
@@ -345,7 +348,6 @@ module ActiveRecord
if record.new_record?
include_in_memory?(record)
else
- load_target if options[:finder_sql]
loaded? ? target.include?(record) : scope.exists?(record)
end
else
@@ -362,8 +364,8 @@ module ActiveRecord
target
end
- def add_to_target(record)
- callback(:before_add, record)
+ def add_to_target(record, skip_callbacks = false)
+ callback(:before_add, record) unless skip_callbacks
yield(record) if block_given?
if association_scope.distinct_value && index = @target.index(record)
@@ -372,7 +374,7 @@ module ActiveRecord
@target << record
end
- callback(:after_add, record)
+ callback(:after_add, record) unless skip_callbacks
set_inverse_instance(record)
record
@@ -390,31 +392,8 @@ module ActiveRecord
private
- def custom_counter_sql
- if options[:counter_sql]
- interpolate(options[:counter_sql])
- else
- # replace the SELECT clause with COUNT(SELECTS), preserving any hints within /* ... */
- interpolate(options[:finder_sql]).sub(/SELECT\b(\/\*.*?\*\/ )?(.*)\bFROM\b/im) do
- count_with = $2.to_s
- count_with = '*' if count_with.blank? || count_with =~ /,/ || count_with =~ /\.\*/
- "SELECT #{$1}COUNT(#{count_with}) FROM"
- end
- end
- end
-
- def custom_finder_sql
- interpolate(options[:finder_sql])
- end
-
def find_target
- records =
- if options[:finder_sql]
- reflection.klass.find_by_sql(custom_finder_sql)
- else
- scope.to_a
- end
-
+ records = scope.to_a
records.each { |record| set_inverse_instance(record) }
records
end
@@ -529,20 +508,13 @@ module ActiveRecord
def callback(method, record)
callbacks_for(method).each do |callback|
- case callback
- when Symbol
- owner.send(callback, record)
- when Proc
- callback.call(owner, record)
- else
- callback.send(method, owner, record)
- end
+ callback.call(method, owner, record)
end
end
def callbacks_for(callback_name)
full_callback_name = "#{callback_name}_for_#{reflection.name}"
- owner.class.send(full_callback_name.to_sym) || []
+ owner.class.send(full_callback_name)
end
# Should we deal with assoc.first or assoc.last by issuing an independent query to
@@ -553,24 +525,21 @@ module ActiveRecord
# Otherwise, go to the database only if none of the following are true:
# * target already loaded
# * owner is new record
- # * custom :finder_sql exists
# * target contains new or changed record(s)
- # * the first arg is an integer (which indicates the number of records to be returned)
def fetch_first_or_last_using_find?(args)
if args.first.is_a?(Hash)
true
else
!(loaded? ||
owner.new_record? ||
- options[:finder_sql] ||
- target.any? { |record| record.new_record? || record.changed? } ||
- args.first.kind_of?(Integer))
+ target.any? { |record| record.new_record? || record.changed? })
end
end
def include_in_memory?(record)
if reflection.is_a?(ActiveRecord::Reflection::ThroughReflection)
- owner.send(reflection.through_reflection.name).any? { |source|
+ assoc = owner.association(reflection.through_reflection.name)
+ assoc.reader.any? { |source|
target = source.send(reflection.source_reflection.name)
target.respond_to?(:include?) ? target.include?(record) : target == record
} || target.include?(record)
@@ -579,18 +548,18 @@ module ActiveRecord
end
end
- # If using a custom finder_sql or if the :inverse_of option has been
+ # If the :inverse_of option has been
# specified, then #find scans the entire collection.
def find_by_scan(*args)
expects_array = args.first.kind_of?(Array)
- ids = args.flatten.compact.map{ |arg| arg.to_i }.uniq
+ ids = args.flatten.compact.map{ |arg| arg.to_s }.uniq
if ids.size == 1
id = ids.first
- record = load_target.detect { |r| id == r.id }
+ record = load_target.detect { |r| id == r.id.to_s }
expects_array ? [ record ] : record
else
- load_target.select { |r| ids.include?(r.id) }
+ load_target.select { |r| ids.include?(r.id.to_s) }
end
end
diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb
index ef2acfce89..a28cc2993b 100644
--- a/activerecord/lib/active_record/associations/collection_proxy.rb
+++ b/activerecord/lib/active_record/associations/collection_proxy.rb
@@ -33,7 +33,6 @@ module ActiveRecord
def initialize(klass, association) #:nodoc:
@association = association
super klass, klass.arel_table
- self.default_scoped = true
merge! association.scope(nullify: false)
end
@@ -92,7 +91,7 @@ module ActiveRecord
# # => ActiveModel::MissingAttributeError: missing attribute: person_id
#
# *Second:* You can pass a block so it can be used just like Array#select.
- # This build an array of objects from the database for the scope,
+ # This builds an array of objects from the database for the scope,
# converting them into an array and iterating through them using
# Array#select.
#
@@ -282,7 +281,7 @@ module ActiveRecord
# so method calls may be chained.
#
# class Person < ActiveRecord::Base
- # pets :has_many
+ # has_many :pets
# end
#
# person.pets.size # => 0
@@ -304,7 +303,7 @@ module ActiveRecord
@association.concat(*records)
end
- # Replace this collection with +other_array+. This will perform a diff
+ # Replaces this collection with +other_array+. This will perform a diff
# and delete/add only records that have changed.
#
# class Person < ActiveRecord::Base
@@ -418,13 +417,13 @@ module ActiveRecord
#
# Pet.find(1, 2, 3)
# # => ActiveRecord::RecordNotFound
- def delete_all
- @association.delete_all
+ def delete_all(dependent = nil)
+ @association.delete_all(dependent)
end
- # Deletes the records of the collection directly from the database.
- # This will _always_ remove the records ignoring the +:dependent+
- # option.
+ # Deletes the records of the collection directly from the database
+ # ignoring the +:dependent+ option. It invokes +before_remove+,
+ # +after_remove+ , +before_destroy+ and +after_destroy+ callbacks.
#
# class Person < ActiveRecord::Base
# has_many :pets
@@ -671,6 +670,8 @@ module ActiveRecord
# # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
# # ]
def count(column_name = nil, options = {})
+ # TODO: Remove options argument as soon we remove support to
+ # activerecord-deprecated_finders.
@association.count(column_name, options)
end
@@ -727,7 +728,7 @@ module ActiveRecord
end
# Returns +true+ if the collection is empty. If the collection has been
- # loaded or the <tt>:counter_sql</tt> option is provided, it is equivalent
+ # loaded it is equivalent
# to <tt>collection.size.zero?</tt>. If the collection has not been loaded,
# it is equivalent to <tt>collection.exists?</tt>. If the collection has
# not already been loaded and you are going to fetch the records anyway it
@@ -788,12 +789,12 @@ module ActiveRecord
# has_many :pets
# end
#
- # person.pets.count #=> 1
- # person.pets.many? #=> false
+ # person.pets.count # => 1
+ # person.pets.many? # => false
#
# person.pets << Pet.new(name: 'Snoopy')
- # person.pets.count #=> 2
- # person.pets.many? #=> true
+ # person.pets.count # => 2
+ # person.pets.many? # => true
#
# You can also pass a block to define criteria. The
# behavior is the same, it returns true if the collection
@@ -830,7 +831,7 @@ module ActiveRecord
# person.pets.include?(Pet.find(20)) # => true
# person.pets.include?(Pet.find(21)) # => false
def include?(record)
- @association.include?(record)
+ !!@association.include?(record)
end
def proxy_association
@@ -847,12 +848,8 @@ module ActiveRecord
# Returns a <tt>Relation</tt> object for the records in this association
def scope
- @association.scope.tap do |scope|
- scope.proxy_association = @association
- end
+ @association.scope
end
-
- # :nodoc:
alias spawn scope
# Equivalent to <tt>Array#==</tt>. Returns +true+ if the two arrays
diff --git a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb
deleted file mode 100644
index bb3e3db379..0000000000
--- a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb
+++ /dev/null
@@ -1,65 +0,0 @@
-module ActiveRecord
- # = Active Record Has And Belongs To Many Association
- module Associations
- class HasAndBelongsToManyAssociation < CollectionAssociation #:nodoc:
- attr_reader :join_table
-
- def initialize(owner, reflection)
- @join_table = Arel::Table.new(reflection.join_table)
- super
- end
-
- def insert_record(record, validate = true, raise = false)
- if record.new_record?
- if raise
- record.save!(:validate => validate)
- else
- return unless record.save(:validate => validate)
- end
- end
-
- if options[:insert_sql]
- owner.connection.insert(interpolate(options[:insert_sql], record))
- else
- stmt = join_table.compile_insert(
- join_table[reflection.foreign_key] => owner.id,
- join_table[reflection.association_foreign_key] => record.id
- )
-
- owner.class.connection.insert stmt
- end
-
- record
- end
-
- private
-
- def count_records
- load_target.size
- end
-
- def delete_records(records, method)
- if sql = options[:delete_sql]
- records = load_target if records == :all
- records.each { |record| owner.class.connection.delete(interpolate(sql, record)) }
- else
- relation = join_table
- condition = relation[reflection.foreign_key].eq(owner.id)
-
- unless records == :all
- condition = condition.and(
- relation[reflection.association_foreign_key]
- .in(records.map { |x| x.id }.compact)
- )
- end
-
- owner.class.connection.delete(relation.where(condition).compile_delete)
- end
- end
-
- def invertible_for?(record)
- false
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb
index 29fae809da..72e0891702 100644
--- a/activerecord/lib/active_record/associations/has_many_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_association.rb
@@ -9,7 +9,7 @@ module ActiveRecord
def handle_dependency
case options[:dependent]
- when :restrict, :restrict_with_exception
+ when :restrict_with_exception
raise ActiveRecord::DeleteRestrictionError.new(reflection.name) unless empty?
when :restrict_with_error
@@ -32,6 +32,7 @@ module ActiveRecord
def insert_record(record, validate = true, raise = false)
set_owner_attributes(record)
+ set_inverse_instance(record)
if raise
record.save!(:validate => validate)
@@ -58,8 +59,6 @@ module ActiveRecord
def count_records
count = if has_cached_counter?
owner.send(:read_attribute, cached_counter_attribute_name)
- elsif options[:counter_sql] || options[:finder_sql]
- reflection.klass.count_by_sql(custom_counter_sql)
else
scope.count
end
@@ -109,14 +108,13 @@ module ActiveRecord
# Deletes the records according to the <tt>:dependent</tt> option.
def delete_records(records, method)
if method == :destroy
- records.each { |r| r.destroy }
+ records.each(&:destroy!)
update_counter(-records.length) unless inverse_updates_counter_cache?
else
if records == :all
scope = self.scope
else
- keys = records.map { |r| r[reflection.association_primary_key] }
- scope = self.scope.where(reflection.association_primary_key => keys)
+ scope = self.scope.where(reflection.klass.primary_key => records)
end
if method == :delete_all
@@ -128,7 +126,11 @@ module ActiveRecord
end
def foreign_key_present?
- owner.attribute_present?(reflection.association_primary_key)
+ if reflection.klass.primary_key
+ owner.attribute_present?(reflection.association_primary_key)
+ else
+ false
+ end
end
end
end
diff --git a/activerecord/lib/active_record/associations/has_many_through_association.rb b/activerecord/lib/active_record/associations/has_many_through_association.rb
index a74dd1cdab..31b8d27892 100644
--- a/activerecord/lib/active_record/associations/has_many_through_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_through_association.rb
@@ -140,7 +140,21 @@ module ActiveRecord
case method
when :destroy
- count = scope.destroy_all.length
+ if scope.klass.primary_key
+ count = scope.destroy_all.length
+ else
+ scope.to_a.each do |record|
+ record.run_callbacks :destroy
+ end
+
+ arel = scope.arel
+
+ stmt = Arel::DeleteManager.new arel.engine
+ stmt.from scope.klass.arel_table
+ stmt.wheres = arel.constraints
+
+ count = scope.klass.connection.delete(stmt, 'SQL', scope.bind_values)
+ end
when :nullify
count = scope.update_all(source_reflection.foreign_key => nil)
else
@@ -149,7 +163,7 @@ module ActiveRecord
delete_through_records(records)
- if source_reflection.options[:counter_cache]
+ if source_reflection.options[:counter_cache] && method != :destroy
counter = source_reflection.counter_cache_column
klass.decrement_counter counter, records.map(&:id)
end
diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb
index 98bd010f70..944caacab6 100644
--- a/activerecord/lib/active_record/associations/has_one_association.rb
+++ b/activerecord/lib/active_record/associations/has_one_association.rb
@@ -6,7 +6,7 @@ module ActiveRecord
def handle_dependency
case options[:dependent]
- when :restrict, :restrict_with_exception
+ when :restrict_with_exception
raise ActiveRecord::DeleteRestrictionError.new(reflection.name) if load_target
when :restrict_with_error
@@ -25,17 +25,20 @@ module ActiveRecord
raise_on_type_mismatch!(record) if record
load_target
- # If target and record are nil, or target is equal to record,
- # we don't need to have transaction.
- if (target || record) && target != record
+ return self.target if !(target || record)
+
+ assigning_another_record = target != record
+ if assigning_another_record || record.changed?
+ save &&= owner.persisted?
+
transaction_if(save) do
- remove_target!(options[:dependent]) if target && !target.destroyed?
+ remove_target!(options[:dependent]) if target && !target.destroyed? && assigning_another_record
if record
set_owner_attributes(record)
set_inverse_instance(record)
- if owner.persisted? && save && !record.save
+ if save && !record.save
nullify_owner_attributes(record)
set_owner_attributes(target) if target
raise RecordNotSaved, "Failed to save the new associated #{reflection.name}."
diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb
index 57fa6a8fc9..295dccf34e 100644
--- a/activerecord/lib/active_record/associations/join_dependency.rb
+++ b/activerecord/lib/active_record/associations/join_dependency.rb
@@ -1,213 +1,273 @@
module ActiveRecord
module Associations
class JoinDependency # :nodoc:
- autoload :JoinPart, 'active_record/associations/join_dependency/join_part'
autoload :JoinBase, 'active_record/associations/join_dependency/join_base'
autoload :JoinAssociation, 'active_record/associations/join_dependency/join_association'
- attr_reader :join_parts, :reflections, :alias_tracker, :active_record
+ class Aliases # :nodoc:
+ def initialize(tables)
+ @tables = tables
+ @alias_cache = tables.each_with_object({}) { |table,h|
+ h[table.node] = table.columns.each_with_object({}) { |column,i|
+ i[column.name] = column.alias
+ }
+ }
+ @name_and_alias_cache = tables.each_with_object({}) { |table,h|
+ h[table.node] = table.columns.map { |column|
+ [column.name, column.alias]
+ }
+ }
+ end
- def initialize(base, associations, joins)
- @active_record = base
- @table_joins = joins
- @join_parts = [JoinBase.new(base)]
- @associations = {}
- @reflections = []
- @alias_tracker = AliasTracker.new(base.connection, joins)
- @alias_tracker.aliased_name_for(base.table_name) # Updates the count for base.table_name to 1
- build(associations)
- end
+ def columns
+ @tables.flat_map { |t| t.column_aliases }
+ end
- def graft(*associations)
- associations.each do |association|
- join_associations.detect {|a| association == a} ||
- build(association.reflection.name, association.find_parent_in(self) || join_base, association.join_type)
+ # An array of [column_name, alias] pairs for the table
+ def column_aliases(node)
+ @name_and_alias_cache[node]
end
- self
- end
- def join_associations
- join_parts.last(join_parts.length - 1)
- end
+ def column_alias(node, column)
+ @alias_cache[node][column]
+ end
- def join_base
- join_parts.first
- end
+ class Table < Struct.new(:node, :columns)
+ def table
+ Arel::Nodes::TableAlias.new node.table, node.aliased_table_name
+ end
- def columns
- join_parts.collect { |join_part|
- table = join_part.aliased_table
- join_part.column_names_with_alias.collect{ |column_name, aliased_name|
- table[column_name].as Arel.sql(aliased_name)
- }
- }.flatten
+ def column_aliases
+ t = table
+ columns.map { |column| t[column.name].as Arel.sql column.alias }
+ end
+ end
+ Column = Struct.new(:name, :alias)
end
- def instantiate(rows)
- primary_key = join_base.aliased_primary_key
- parents = {}
-
- records = rows.map { |model|
- primary_id = model[primary_key]
- parent = parents[primary_id] ||= join_base.instantiate(model)
- construct(parent, @associations, join_associations, model)
- parent
- }.uniq
+ attr_reader :alias_tracker, :base_klass, :join_root
- remove_duplicate_results!(active_record, records, @associations)
- records
+ def self.make_tree(associations)
+ hash = {}
+ walk_tree associations, hash
+ hash
end
- def remove_duplicate_results!(base, records, associations)
+ def self.walk_tree(associations, hash)
case associations
when Symbol, String
- reflection = base.reflections[associations]
- remove_uniq_by_reflection(reflection, records)
+ hash[associations.to_sym] ||= {}
when Array
- associations.each do |association|
- remove_duplicate_results!(base, records, association)
+ associations.each do |assoc|
+ walk_tree assoc, hash
end
when Hash
- associations.each_key do |name|
- reflection = base.reflections[name]
- remove_uniq_by_reflection(reflection, records)
-
- parent_records = []
- records.each do |record|
- if descendant = record.send(reflection.name)
- if reflection.collection?
- parent_records.concat descendant.target.uniq
- else
- parent_records << descendant
- end
- end
- end
-
- remove_duplicate_results!(reflection.klass, parent_records, associations[name]) unless parent_records.empty?
+ associations.each do |k,v|
+ cache = hash[k] ||= {}
+ walk_tree v, cache
end
+ else
+ raise ConfigurationError, associations.inspect
end
end
- protected
+ # base is the base class on which operation is taking place.
+ # associations is the list of associations which are joined using hash, symbol or array.
+ # joins is the list of all string join commands and arel nodes.
+ #
+ # Example :
+ #
+ # class Physician < ActiveRecord::Base
+ # has_many :appointments
+ # has_many :patients, through: :appointments
+ # end
+ #
+ # If I execute `@physician.patients.to_a` then
+ # base # => Physician
+ # associations # => []
+ # joins # => [#<Arel::Nodes::InnerJoin: ...]
+ #
+ # However if I execute `Physician.joins(:appointments).to_a` then
+ # base # => Physician
+ # associations # => [:appointments]
+ # joins # => []
+ #
+ def initialize(base, associations, joins)
+ @alias_tracker = AliasTracker.new(base.connection, joins)
+ @alias_tracker.aliased_name_for(base.table_name) # Updates the count for base.table_name to 1
+ tree = self.class.make_tree associations
+ @join_root = JoinBase.new base, build(tree, base)
+ @join_root.children.each { |child| construct_tables! @join_root, child }
+ end
- def cache_joined_association(association)
- associations = []
- parent = association.parent
- while parent != join_base
- associations.unshift(parent.reflection.name)
- parent = parent.parent
- end
- ref = @associations
- associations.each do |key|
- ref = ref[key]
- end
- ref[association.reflection.name] ||= {}
+ def reflections
+ join_root.drop(1).map!(&:reflection)
end
- def build(associations, parent = nil, join_type = Arel::InnerJoin)
- parent ||= join_parts.last
- case associations
- when Symbol, String
- reflection = parent.reflections[associations.intern] or
- raise ConfigurationError, "Association named '#{ associations }' was not found on #{ parent.active_record.name }; perhaps you misspelled it?"
- unless join_association = find_join_association(reflection, parent)
- @reflections << reflection
- join_association = build_join_association(reflection, parent)
- join_association.join_type = join_type
- @join_parts << join_association
- cache_joined_association(join_association)
- end
- join_association
- when Array
- associations.each do |association|
- build(association, parent, join_type)
- end
- when Hash
- associations.keys.sort_by { |a| a.to_s }.each do |name|
- join_association = build(name, parent, join_type)
- build(associations[name], join_association, join_type)
+ def join_constraints(outer_joins)
+ joins = join_root.children.flat_map { |child|
+ make_inner_joins join_root, child
+ }
+
+ joins.concat outer_joins.flat_map { |oj|
+ if join_root.match? oj.join_root
+ walk join_root, oj.join_root
+ else
+ oj.join_root.children.flat_map { |child|
+ make_outer_joins oj.join_root, child
+ }
end
- else
- raise ConfigurationError, associations.inspect
- end
+ }
end
- def find_join_association(name_or_reflection, parent)
- if String === name_or_reflection
- name_or_reflection = name_or_reflection.to_sym
- end
+ def aliases
+ Aliases.new join_root.each_with_index.map { |join_part,i|
+ columns = join_part.column_names.each_with_index.map { |column_name,j|
+ Aliases::Column.new column_name, "t#{i}_r#{j}"
+ }
+ Aliases::Table.new(join_part, columns)
+ }
+ end
+
+ def instantiate(result_set, aliases)
+ primary_key = aliases.column_alias(join_root, join_root.primary_key)
+ type_caster = result_set.column_type primary_key
+
+ seen = Hash.new { |h,parent_klass|
+ h[parent_klass] = Hash.new { |i,parent_id|
+ i[parent_id] = Hash.new { |j,child_klass| j[child_klass] = {} }
+ }
+ }
+
+ model_cache = Hash.new { |h,klass| h[klass] = {} }
+ parents = model_cache[join_root]
+ column_aliases = aliases.column_aliases join_root
- join_associations.detect { |j|
- j.reflection == name_or_reflection && j.parent == parent
+ result_set.each { |row_hash|
+ primary_id = type_caster.type_cast row_hash[primary_key]
+ parent = parents[primary_id] ||= join_root.instantiate(row_hash, column_aliases)
+ construct(parent, join_root, row_hash, result_set, seen, model_cache, aliases)
}
+
+ parents.values
end
- def remove_uniq_by_reflection(reflection, records)
- if reflection && reflection.collection?
- records.each { |record| record.send(reflection.name).target.uniq! }
- end
+ private
+
+ def make_constraints(parent, child, tables, join_type)
+ chain = child.reflection.chain
+ foreign_table = parent.table
+ foreign_klass = parent.base_klass
+ child.join_constraints(foreign_table, foreign_klass, child, join_type, tables, child.reflection.scope_chain, chain)
end
- def build_join_association(reflection, parent)
- JoinAssociation.new(reflection, self, parent)
+ def make_outer_joins(parent, child)
+ tables = table_aliases_for(parent, child)
+ join_type = Arel::OuterJoin
+ joins = make_constraints parent, child, tables, join_type
+
+ joins.concat child.children.flat_map { |c| make_outer_joins(child, c) }
end
- def construct(parent, associations, join_parts, row)
- case associations
- when Symbol, String
- name = associations.to_s
+ def make_inner_joins(parent, child)
+ tables = child.tables
+ join_type = Arel::InnerJoin
+ joins = make_constraints parent, child, tables, join_type
- join_part = join_parts.detect { |j|
- j.reflection.name.to_s == name &&
- j.parent_table_name == parent.class.table_name }
+ joins.concat child.children.flat_map { |c| make_inner_joins(child, c) }
+ end
- raise(ConfigurationError, "No such association") unless join_part
+ def table_aliases_for(parent, node)
+ node.reflection.chain.map { |reflection|
+ alias_tracker.aliased_table_for(
+ reflection.table_name,
+ table_alias_for(reflection, parent, reflection != node.reflection)
+ )
+ }
+ end
- join_parts.delete(join_part)
- construct_association(parent, join_part, row)
- when Array
- associations.each do |association|
- construct(parent, association, join_parts, row)
- end
- when Hash
- associations.sort_by { |k,_| k.to_s }.each do |association_name, assoc|
- association = construct(parent, association_name, join_parts, row)
- construct(association, assoc, join_parts, row) if association
+ def construct_tables!(parent, node)
+ node.tables = table_aliases_for(parent, node)
+ node.children.each { |child| construct_tables! node, child }
+ end
+
+ def table_alias_for(reflection, parent, join)
+ name = "#{reflection.plural_name}_#{parent.table_name}"
+ name << "_join" if join
+ name
+ end
+
+ def walk(left, right)
+ intersection, missing = right.children.map { |node1|
+ [left.children.find { |node2| node1.match? node2 }, node1]
+ }.partition(&:first)
+
+ ojs = missing.flat_map { |_,n| make_outer_joins left, n }
+ intersection.flat_map { |l,r| walk l, r }.concat ojs
+ end
+
+ def find_reflection(klass, name)
+ klass.reflect_on_association(name) or
+ raise ConfigurationError, "Association named '#{ name }' was not found on #{ klass.name }; perhaps you misspelled it?"
+ end
+
+ def build(associations, base_klass)
+ associations.map do |name, right|
+ reflection = find_reflection base_klass, name
+ reflection.check_validity!
+
+ if reflection.options[:polymorphic]
+ raise EagerLoadPolymorphicError.new(reflection)
end
- else
- raise ConfigurationError, associations.inspect
+
+ JoinAssociation.new reflection, build(right, reflection.klass)
end
end
- def construct_association(record, join_part, row)
- return if record.id.to_s != join_part.parent.record_id(row).to_s
+ def construct(ar_parent, parent, row, rs, seen, model_cache, aliases)
+ primary_id = ar_parent.id
- macro = join_part.reflection.macro
- if macro == :has_one
- return record.association(join_part.reflection.name).target if record.association_cache.key?(join_part.reflection.name)
- association = join_part.instantiate(row) unless row[join_part.aliased_primary_key].nil?
- set_target_and_inverse(join_part, association, record)
- else
- association = join_part.instantiate(row) unless row[join_part.aliased_primary_key].nil?
- case macro
- when :has_many, :has_and_belongs_to_many
- other = record.association(join_part.reflection.name)
+ parent.children.each do |node|
+ if node.reflection.collection?
+ other = ar_parent.association(node.reflection.name)
other.loaded!
- other.target.push(association) if association
- other.set_inverse_instance(association)
- when :belongs_to
- set_target_and_inverse(join_part, association, record)
else
- raise ConfigurationError, "unknown macro: #{join_part.reflection.macro}"
+ if ar_parent.association_cache.key?(node.reflection.name)
+ model = ar_parent.association(node.reflection.name).target
+ construct(model, node, row, rs, seen, model_cache, aliases)
+ next
+ end
+ end
+
+ key = aliases.column_alias(node, node.primary_key)
+ id = row[key]
+ next if id.nil?
+
+ model = seen[parent.base_klass][primary_id][node.base_klass][id]
+
+ if model
+ construct(model, node, row, rs, seen, model_cache, aliases)
+ else
+ model = construct_model(ar_parent, node, row, model_cache, id, aliases)
+ seen[parent.base_klass][primary_id][node.base_klass][id] = model
+ construct(model, node, row, rs, seen, model_cache, aliases)
end
end
- association
end
- def set_target_and_inverse(join_part, association, record)
- other = record.association(join_part.reflection.name)
- other.target = association
- other.set_inverse_instance(association)
+ def construct_model(record, node, row, model_cache, id, aliases)
+ model = model_cache[node][id] ||= node.instantiate(row,
+ aliases.column_aliases(node))
+ other = record.association(node.reflection.name)
+
+ if node.reflection.collection?
+ other.target.push(model)
+ else
+ other.target = model
+ end
+
+ other.set_inverse_instance(model)
+ model
end
end
end
diff --git a/activerecord/lib/active_record/associations/join_dependency/join_association.rb b/activerecord/lib/active_record/associations/join_dependency/join_association.rb
index a332034cb0..84e18684d8 100644
--- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb
+++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb
@@ -1,153 +1,117 @@
+require 'active_record/associations/join_dependency/join_part'
+
module ActiveRecord
module Associations
class JoinDependency # :nodoc:
class JoinAssociation < JoinPart # :nodoc:
- include JoinHelper
-
# The reflection of the association represented
attr_reader :reflection
- # The JoinDependency object which this JoinAssociation exists within. This is mainly
- # relevant for generating aliases which do not conflict with other joins which are
- # part of the query.
- attr_reader :join_dependency
-
- # A JoinBase instance representing the active record we are joining onto.
- # (So in Author.has_many :posts, the Author would be that base record.)
- attr_reader :parent
-
- # What type of join will be generated, either Arel::InnerJoin (default) or Arel::OuterJoin
- attr_accessor :join_type
-
- # These implement abstract methods from the superclass
- attr_reader :aliased_prefix
-
- attr_reader :tables
-
- delegate :options, :through_reflection, :source_reflection, :chain, :to => :reflection
- delegate :table, :table_name, :to => :parent, :prefix => :parent
- delegate :alias_tracker, :to => :join_dependency
+ attr_accessor :tables
- alias :alias_suffix :parent_table_name
-
- def initialize(reflection, join_dependency, parent = nil)
- reflection.check_validity!
-
- if reflection.options[:polymorphic]
- raise EagerLoadPolymorphicError.new(reflection)
- end
-
- super(reflection.klass)
+ def initialize(reflection, children)
+ super(reflection.klass, children)
@reflection = reflection
- @join_dependency = join_dependency
- @parent = parent
- @join_type = Arel::InnerJoin
- @aliased_prefix = "t#{ join_dependency.join_parts.size }"
- @tables = construct_tables.reverse
+ @tables = nil
end
- def ==(other)
- other.class == self.class &&
- other.reflection == reflection &&
- other.parent == parent
+ def match?(other)
+ return true if self == other
+ super && reflection == other.reflection
end
- def find_parent_in(other_join_dependency)
- other_join_dependency.join_parts.detect do |join_part|
- parent == join_part
- end
- end
+ def join_constraints(foreign_table, foreign_klass, node, join_type, tables, scope_chain, chain)
+ joins = []
+ tables = tables.reverse
- def join_to(manager)
- tables = @tables.dup
- foreign_table = parent_table
- foreign_klass = parent.active_record
+ scope_chain_iter = scope_chain.reverse_each
# The chain starts with the target table, but we want to end with it here (makes
# more sense in this context), so we reverse
- chain.reverse.each_with_index do |reflection, i|
+ chain.reverse_each do |reflection|
table = tables.shift
+ klass = reflection.klass
case reflection.source_macro
when :belongs_to
key = reflection.association_primary_key
foreign_key = reflection.foreign_key
- when :has_and_belongs_to_many
- # Join the join table first...
- manager.from(join(
- table,
- table[reflection.foreign_key].
- eq(foreign_table[reflection.active_record_primary_key])
- ))
-
- foreign_table, table = table, tables.shift
-
- key = reflection.association_primary_key
- foreign_key = reflection.association_foreign_key
else
key = reflection.foreign_key
foreign_key = reflection.active_record_primary_key
end
- constraint = build_constraint(reflection, table, key, foreign_table, foreign_key)
+ constraint = build_constraint(klass, table, key, foreign_table, foreign_key)
- scope_chain_items = scope_chain[i]
+ scope_chain_items = scope_chain_iter.next.map do |item|
+ if item.is_a?(Relation)
+ item
+ else
+ ActiveRecord::Relation.create(klass, table).instance_exec(node, &item)
+ end
+ end
if reflection.type
- scope_chain_items += [
- ActiveRecord::Relation.new(reflection.klass, table)
+ scope_chain_items <<
+ ActiveRecord::Relation.create(klass, table)
.where(reflection.type => foreign_klass.base_class.name)
- ]
end
- scope_chain_items.each do |item|
- unless item.is_a?(Relation)
- item = ActiveRecord::Relation.new(reflection.klass, table).instance_exec(self, &item)
- end
+ scope_chain_items.concat [klass.send(:build_default_scope)].compact
- constraint = constraint.and(item.arel.constraints) unless item.arel.constraints.empty?
+ rel = scope_chain_items.inject(scope_chain_items.shift) do |left, right|
+ left.merge right
end
- manager.from(join(table, constraint))
+ if rel && !rel.arel.constraints.empty?
+ constraint = constraint.and rel.arel.constraints
+ end
+
+ joins << table.create_join(table, table.create_on(constraint), join_type)
# The current table in this iteration becomes the foreign table in the next
- foreign_table, foreign_klass = table, reflection.klass
+ foreign_table, foreign_klass = table, klass
end
- manager
+ joins
end
- def build_constraint(reflection, table, key, foreign_table, foreign_key)
+ # Builds equality condition.
+ #
+ # Example:
+ #
+ # class Physician < ActiveRecord::Base
+ # has_many :appointments
+ # end
+ #
+ # If I execute `Physician.joins(:appointments).to_a` then
+ # reflection # => #<ActiveRecord::Reflection::AssociationReflection @macro=:has_many ...>
+ # table # => #<Arel::Table @name="appointments" ...>
+ # key # => physician_id
+ # foreign_table # => #<Arel::Table @name="physicians" ...>
+ # foreign_key # => id
+ #
+ def build_constraint(klass, table, key, foreign_table, foreign_key)
constraint = table[key].eq(foreign_table[foreign_key])
- if reflection.klass.finder_needs_type_condition?
+ if klass.finder_needs_type_condition?
constraint = table.create_and([
constraint,
- reflection.klass.send(:type_condition, table)
+ klass.send(:type_condition, table)
])
end
constraint
end
- def join_relation(joining_relation)
- self.join_type = Arel::OuterJoin
- joining_relation.joins(self)
- end
-
def table
- tables.last
+ tables.first
end
def aliased_table_name
table.table_alias || table.name
end
-
- def scope_chain
- @scope_chain ||= reflection.scope_chain.reverse
- end
-
end
end
end
diff --git a/activerecord/lib/active_record/associations/join_dependency/join_base.rb b/activerecord/lib/active_record/associations/join_dependency/join_base.rb
index 3920e84976..3a26c25737 100644
--- a/activerecord/lib/active_record/associations/join_dependency/join_base.rb
+++ b/activerecord/lib/active_record/associations/join_dependency/join_base.rb
@@ -1,22 +1,20 @@
+require 'active_record/associations/join_dependency/join_part'
+
module ActiveRecord
module Associations
class JoinDependency # :nodoc:
class JoinBase < JoinPart # :nodoc:
- def ==(other)
- other.class == self.class &&
- other.active_record == active_record
- end
-
- def aliased_prefix
- "t0"
+ def match?(other)
+ return true if self == other
+ super && base_klass == other.base_klass
end
def table
- Arel::Table.new(table_name, arel_engine)
+ base_klass.arel_table
end
def aliased_table_name
- active_record.table_name
+ base_klass.table_name
end
end
end
diff --git a/activerecord/lib/active_record/associations/join_dependency/join_part.rb b/activerecord/lib/active_record/associations/join_dependency/join_part.rb
index 5604687b57..91e1c6a9d7 100644
--- a/activerecord/lib/active_record/associations/join_dependency/join_part.rb
+++ b/activerecord/lib/active_record/associations/join_dependency/join_part.rb
@@ -1,41 +1,43 @@
module ActiveRecord
module Associations
class JoinDependency # :nodoc:
- # A JoinPart represents a part of a JoinDependency. It is an abstract class, inherited
+ # A JoinPart represents a part of a JoinDependency. It is inherited
# by JoinBase and JoinAssociation. A JoinBase represents the Active Record which
# everything else is being joined onto. A JoinAssociation represents an association which
# is joining to the base. A JoinAssociation may result in more than one actual join
# operations (for example a has_and_belongs_to_many JoinAssociation would result in
# two; one for the join table and one for the target table).
class JoinPart # :nodoc:
+ include Enumerable
+
# The Active Record class which this join part is associated 'about'; for a JoinBase
# this is the actual base model, for a JoinAssociation this is the target model of the
# association.
- attr_reader :active_record
+ attr_reader :base_klass, :children
- delegate :table_name, :column_names, :primary_key, :reflections, :arel_engine, :to => :active_record
+ delegate :table_name, :column_names, :primary_key, :to => :base_klass
- def initialize(active_record)
- @active_record = active_record
- @cached_record = {}
+ def initialize(base_klass, children)
+ @base_klass = base_klass
@column_names_with_alias = nil
+ @children = children
end
- def aliased_table
- Arel::Nodes::TableAlias.new table, aliased_table_name
+ def name
+ reflection.name
end
- def ==(other)
- raise NotImplementedError
+ def match?(other)
+ self.class == other.class
end
- # An Arel::Table for the active_record
- def table
- raise NotImplementedError
+ def each(&block)
+ yield self
+ children.each { |child| child.each(&block) }
end
- # The prefix to be used when aliasing columns in the active_record's table
- def aliased_prefix
+ # An Arel::Table for the active_record
+ def table
raise NotImplementedError
end
@@ -44,33 +46,25 @@ module ActiveRecord
raise NotImplementedError
end
- # The alias for the primary key of the active_record's table
- def aliased_primary_key
- "#{aliased_prefix}_r0"
- end
+ def extract_record(row, column_names_with_alias)
+ # This code is performance critical as it is called per row.
+ # see: https://github.com/rails/rails/pull/12185
+ hash = {}
- # An array of [column_name, alias] pairs for the table
- def column_names_with_alias
- unless @column_names_with_alias
- @column_names_with_alias = []
+ index = 0
+ length = column_names_with_alias.length
- ([primary_key] + (column_names - [primary_key])).compact.each_with_index do |column_name, i|
- @column_names_with_alias << [column_name, "#{aliased_prefix}_r#{i}"]
- end
+ while index < length
+ column_name, alias_name = column_names_with_alias[index]
+ hash[column_name] = row[alias_name]
+ index += 1
end
- @column_names_with_alias
- end
-
- def extract_record(row)
- Hash[column_names_with_alias.map{|cn, an| [cn, row[an]]}]
- end
- def record_id(row)
- row[aliased_primary_key]
+ hash
end
- def instantiate(row)
- @cached_record[record_id(row)] ||= active_record.instantiate(extract_record(row))
+ def instantiate(row, aliases)
+ base_klass.instantiate(extract_record(row, aliases))
end
end
end
diff --git a/activerecord/lib/active_record/associations/join_helper.rb b/activerecord/lib/active_record/associations/join_helper.rb
index 5a41b40c8f..f345d16841 100644
--- a/activerecord/lib/active_record/associations/join_helper.rb
+++ b/activerecord/lib/active_record/associations/join_helper.rb
@@ -10,21 +10,12 @@ module ActiveRecord
private
def construct_tables
- tables = []
- chain.each do |reflection|
- tables << alias_tracker.aliased_table_for(
+ chain.map do |reflection|
+ alias_tracker.aliased_table_for(
table_name_for(reflection),
table_alias_for(reflection, reflection != self.reflection)
)
-
- if reflection.source_macro == :has_and_belongs_to_many
- tables << alias_tracker.aliased_table_for(
- (reflection.source_reflection || reflection).join_table,
- table_alias_for(reflection, true)
- )
- end
end
- tables
end
def table_name_for(reflection)
diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb
index 82bf426b22..83637a0409 100644
--- a/activerecord/lib/active_record/associations/preloader.rb
+++ b/activerecord/lib/active_record/associations/preloader.rb
@@ -42,12 +42,9 @@ module ActiveRecord
autoload :HasManyThrough, 'active_record/associations/preloader/has_many_through'
autoload :HasOne, 'active_record/associations/preloader/has_one'
autoload :HasOneThrough, 'active_record/associations/preloader/has_one_through'
- autoload :HasAndBelongsToMany, 'active_record/associations/preloader/has_and_belongs_to_many'
autoload :BelongsTo, 'active_record/associations/preloader/belongs_to'
end
- attr_reader :records, :associations, :preload_scope, :model
-
# Eager loads the named associations for the given Active Record record(s).
#
# In this description, 'association name' shall refer to the name passed
@@ -82,38 +79,47 @@ module ActiveRecord
# [ :books, :author ]
# { author: :avatar }
# [ :books, { author: :avatar } ]
- def initialize(records, associations, preload_scope = nil)
- @records = Array.wrap(records).compact.uniq
- @associations = Array.wrap(associations)
- @preload_scope = preload_scope || Relation.new(nil, nil)
- end
- def run
- unless records.empty?
- associations.each { |association| preload(association) }
+ NULL_RELATION = Struct.new(:values).new({})
+
+ def preload(records, associations, preload_scope = nil)
+ records = Array.wrap(records).compact.uniq
+ associations = Array.wrap(associations)
+ preload_scope = preload_scope || NULL_RELATION
+
+ if records.empty?
+ []
+ else
+ associations.flat_map { |association|
+ preloaders_on association, records, preload_scope
+ }
end
end
private
- def preload(association)
+ def preloaders_on(association, records, scope)
case association
when Hash
- preload_hash(association)
+ preloaders_for_hash(association, records, scope)
when Symbol
- preload_one(association)
+ preloaders_for_one(association, records, scope)
when String
- preload_one(association.to_sym)
+ preloaders_for_one(association.to_sym, records, scope)
else
raise ArgumentError, "#{association.inspect} was not recognised for preload"
end
end
- def preload_hash(association)
- association.each do |parent, child|
- Preloader.new(records, parent, preload_scope).run
- Preloader.new(records.map { |record| record.send(parent) }.flatten, child).run
- end
+ def preloaders_for_hash(association, records, scope)
+ parent, child = association.to_a.first # hash should only be of length 1
+
+ loaders = preloaders_for_one parent, records, scope
+
+ recs = loaders.flat_map(&:preloaded_records).uniq
+ loaders.concat Array.wrap(child).flat_map { |assoc|
+ preloaders_on assoc, recs, scope
+ }
end
# Not all records have the same class, so group then preload group on the reflection
@@ -123,52 +129,81 @@ module ActiveRecord
# Additionally, polymorphic belongs_to associations can have multiple associated
# classes, depending on the polymorphic_type field. So we group by the classes as
# well.
- def preload_one(association)
- grouped_records(association).each do |reflection, klasses|
- klasses.each do |klass, records|
- preloader_for(reflection).new(klass, records, reflection, preload_scope).run
+ def preloaders_for_one(association, records, scope)
+ grouped_records(association, records).flat_map do |reflection, klasses|
+ klasses.map do |rhs_klass, rs|
+ loader = preloader_for(reflection, rs, rhs_klass).new(rhs_klass, rs, reflection, scope)
+ loader.run self
+ loader
end
end
end
- def grouped_records(association)
- Hash[
- records_by_reflection(association).map do |reflection, records|
- [reflection, records.group_by { |record| association_klass(reflection, record) }]
- end
- ]
+ def grouped_records(association, records)
+ reflection_records = records_by_reflection(association, records)
+
+ reflection_records.each_with_object({}) do |(reflection, r_records),h|
+ h[reflection] = r_records.group_by { |record|
+ association_klass(reflection, record)
+ }
+ end
end
- def records_by_reflection(association)
+ def records_by_reflection(association, records)
records.group_by do |record|
- reflection = record.class.reflections[association]
+ reflection = record.class.reflect_on_association(association)
- unless reflection
- raise ActiveRecord::ConfigurationError, "Association named '#{association}' was not found; " \
- "perhaps you misspelled it?"
- end
-
- reflection
+ reflection || raise_config_error(record, association)
end
end
+ def raise_config_error(record, association)
+ raise ActiveRecord::ConfigurationError,
+ "Association named '#{association}' was not found on #{record.class.name}; " \
+ "perhaps you misspelled it?"
+ end
+
def association_klass(reflection, record)
if reflection.macro == :belongs_to && reflection.options[:polymorphic]
- klass = record.send(reflection.foreign_type)
+ klass = record.read_attribute(reflection.foreign_type.to_s)
klass && klass.constantize
else
reflection.klass
end
end
- def preloader_for(reflection)
+ class AlreadyLoaded
+ attr_reader :owners, :reflection
+
+ def initialize(klass, owners, reflection, preload_scope)
+ @owners = owners
+ @reflection = reflection
+ end
+
+ def run(preloader); end
+
+ def preloaded_records
+ owners.flat_map { |owner| owner.association(reflection.name).target }
+ end
+ end
+
+ class NullPreloader
+ def self.new(klass, owners, reflection, preload_scope); self; end
+ def self.run(preloader); end
+ end
+
+ def preloader_for(reflection, owners, rhs_klass)
+ return NullPreloader unless rhs_klass
+
+ if owners.first.association(reflection.name).loaded?
+ return AlreadyLoaded
+ end
+
case reflection.macro
when :has_many
reflection.options[:through] ? HasManyThrough : HasMany
when :has_one
reflection.options[:through] ? HasOneThrough : HasOne
- when :has_and_belongs_to_many
- HasAndBelongsToMany
when :belongs_to
BelongsTo
end
diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb
index 82588905c6..69b65982b3 100644
--- a/activerecord/lib/active_record/associations/preloader/association.rb
+++ b/activerecord/lib/active_record/associations/preloader/association.rb
@@ -3,6 +3,7 @@ module ActiveRecord
class Preloader
class Association #:nodoc:
attr_reader :owners, :reflection, :preload_scope, :model, :klass
+ attr_reader :preloaded_records
def initialize(klass, owners, reflection, preload_scope)
@klass = klass
@@ -12,15 +13,14 @@ module ActiveRecord
@model = owners.first && owners.first.class
@scope = nil
@owners_by_key = nil
+ @preloaded_records = []
end
- def run
- unless owners.first.association(reflection.name).loaded?
- preload
- end
+ def run(preloader)
+ preload(preloader)
end
- def preload
+ def preload(preloader)
raise NotImplementedError
end
@@ -29,6 +29,10 @@ module ActiveRecord
end
def records_for(ids)
+ query_scope(ids)
+ end
+
+ def query_scope(ids)
scope.where(association_key.in(ids))
end
@@ -52,12 +56,9 @@ module ActiveRecord
raise NotImplementedError
end
- # We're converting to a string here because postgres will return the aliased association
- # key in a habtm as a string (for whatever reason)
def owners_by_key
@owners_by_key ||= owners.group_by do |owner|
- key = owner[owner_key_name]
- key && key.to_s
+ owner[owner_key_name]
end
end
@@ -67,38 +68,47 @@ module ActiveRecord
private
- def associated_records_by_owner
+ def associated_records_by_owner(preloader)
owners_map = owners_by_key
owner_keys = owners_map.keys.compact
- if klass.nil? || owner_keys.empty?
- records = []
- else
+ # Each record may have multiple owners, and vice-versa
+ records_by_owner = owners.each_with_object({}) do |owner,h|
+ h[owner] = []
+ end
+
+ if owner_keys.any?
# Some databases impose a limit on the number of ids in a list (in Oracle it's 1000)
# Make several smaller queries if necessary or make one query if the adapter supports it
sliced = owner_keys.each_slice(klass.connection.in_clause_length || owner_keys.size)
- records = sliced.map { |slice| records_for(slice).to_a }.flatten
- end
-
- # Each record may have multiple owners, and vice-versa
- records_by_owner = Hash[owners.map { |owner| [owner, []] }]
- records.each do |record|
- owner_key = record[association_key_name].to_s
- owners_map[owner_key].each do |owner|
- records_by_owner[owner] << record
+ records = load_slices sliced
+ records.each do |record, owner_key|
+ owners_map[owner_key].each do |owner|
+ records_by_owner[owner] << record
+ end
end
end
+
records_by_owner
end
+ def load_slices(slices)
+ @preloaded_records = slices.flat_map { |slice|
+ records_for(slice)
+ }
+
+ @preloaded_records.map { |record|
+ [record, record[association_key_name]]
+ }
+ end
+
def reflection_scope
@reflection_scope ||= reflection.scope ? klass.unscoped.instance_exec(nil, &reflection.scope) : klass.unscoped
end
def build_scope
scope = klass.unscoped
- scope.default_scoped = true
values = reflection_scope.values
preload_values = preload_scope.values
@@ -109,11 +119,19 @@ module ActiveRecord
scope.select! preload_values[:select] || values[:select] || table[Arel.star]
scope.includes! preload_values[:includes] || values[:includes]
+ if preload_values.key? :order
+ scope.order! preload_values[:order]
+ else
+ if values.key? :order
+ scope.order! values[:order]
+ end
+ end
+
if options[:as]
scope.where!(klass.table_name => { reflection.type => model.base_class.sti_name })
end
- scope
+ klass.default_scoped.merge(scope)
end
end
end
diff --git a/activerecord/lib/active_record/associations/preloader/collection_association.rb b/activerecord/lib/active_record/associations/preloader/collection_association.rb
index e6cd35e7a1..5adffcd831 100644
--- a/activerecord/lib/active_record/associations/preloader/collection_association.rb
+++ b/activerecord/lib/active_record/associations/preloader/collection_association.rb
@@ -9,8 +9,8 @@ module ActiveRecord
super.order(preload_scope.values[:order] || reflection_scope.values[:order])
end
- def preload
- associated_records_by_owner.each do |owner, records|
+ def preload(preloader)
+ associated_records_by_owner(preloader).each do |owner, records|
association = owner.association(reflection.name)
association.loaded!
association.target.concat(records)
diff --git a/activerecord/lib/active_record/associations/preloader/has_and_belongs_to_many.rb b/activerecord/lib/active_record/associations/preloader/has_and_belongs_to_many.rb
deleted file mode 100644
index 9a3fada380..0000000000
--- a/activerecord/lib/active_record/associations/preloader/has_and_belongs_to_many.rb
+++ /dev/null
@@ -1,60 +0,0 @@
-module ActiveRecord
- module Associations
- class Preloader
- class HasAndBelongsToMany < CollectionAssociation #:nodoc:
- attr_reader :join_table
-
- def initialize(klass, records, reflection, preload_options)
- super
- @join_table = Arel::Table.new(reflection.join_table).alias('t0')
- end
-
- # Unlike the other associations, we want to get a raw array of rows so that we can
- # access the aliased column on the join table
- def records_for(ids)
- scope = super
- klass.connection.select_all(scope.arel, 'SQL', scope.bind_values)
- end
-
- def owner_key_name
- reflection.active_record_primary_key
- end
-
- def association_key_name
- 'ar_association_key_name'
- end
-
- def association_key
- join_table[reflection.foreign_key]
- end
-
- private
-
- # Once we have used the join table column (in super), we manually instantiate the
- # actual records, ensuring that we don't create more than one instances of the same
- # record
- def associated_records_by_owner
- records = {}
- super.each_value do |rows|
- rows.map! { |row| records[row[klass.primary_key]] ||= klass.instantiate(row) }
- end
- end
-
- def build_scope
- super.joins(join).select(join_select)
- end
-
- def join_select
- association_key.as(Arel.sql(association_key_name))
- end
-
- def join
- condition = table[reflection.association_primary_key].eq(
- join_table[reflection.association_foreign_key])
-
- table.create_join(join_table, table.create_on(condition))
- end
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/associations/preloader/has_many_through.rb b/activerecord/lib/active_record/associations/preloader/has_many_through.rb
index 157b627ad5..7b37b5942d 100644
--- a/activerecord/lib/active_record/associations/preloader/has_many_through.rb
+++ b/activerecord/lib/active_record/associations/preloader/has_many_through.rb
@@ -4,7 +4,7 @@ module ActiveRecord
class HasManyThrough < CollectionAssociation #:nodoc:
include ThroughAssociation
- def associated_records_by_owner
+ def associated_records_by_owner(preloader)
records_by_owner = super
if reflection_scope.distinct_value
diff --git a/activerecord/lib/active_record/associations/preloader/singular_association.rb b/activerecord/lib/active_record/associations/preloader/singular_association.rb
index 44e804d785..f60647a81e 100644
--- a/activerecord/lib/active_record/associations/preloader/singular_association.rb
+++ b/activerecord/lib/active_record/associations/preloader/singular_association.rb
@@ -5,13 +5,13 @@ module ActiveRecord
private
- def preload
- associated_records_by_owner.each do |owner, associated_records|
+ def preload(preloader)
+ associated_records_by_owner(preloader).each do |owner, associated_records|
record = associated_records.first
association = owner.association(reflection.name)
association.target = record
- association.set_inverse_instance(record)
+ association.set_inverse_instance(record) if record
end
end
diff --git a/activerecord/lib/active_record/associations/preloader/through_association.rb b/activerecord/lib/active_record/associations/preloader/through_association.rb
index c4b50ab306..2a8530af62 100644
--- a/activerecord/lib/active_record/associations/preloader/through_association.rb
+++ b/activerecord/lib/active_record/associations/preloader/through_association.rb
@@ -2,7 +2,6 @@ module ActiveRecord
module Associations
class Preloader
module ThroughAssociation #:nodoc:
-
def through_reflection
reflection.through_reflection
end
@@ -11,51 +10,84 @@ module ActiveRecord
reflection.source_reflection
end
- def associated_records_by_owner
- through_records = through_records_by_owner
+ def associated_records_by_owner(preloader)
+ preloader.preload(owners,
+ through_reflection.name,
+ through_scope)
- Preloader.new(through_records.values.flatten, source_reflection.name, reflection_scope).run
+ through_records = owners.map do |owner|
+ association = owner.association through_reflection.name
- through_records.each do |owner, records|
- records.map! { |r| r.send(source_reflection.name) }.flatten!
- records.compact!
+ [owner, Array(association.reader)]
end
- end
- private
+ reset_association owners, through_reflection.name
+
+ middle_records = through_records.map { |(_,rec)| rec }.flatten
+
+ preloaders = preloader.preload(middle_records,
+ source_reflection.name,
+ reflection_scope)
- def through_records_by_owner
- Preloader.new(owners, through_reflection.name, through_scope).run
+ @preloaded_records = preloaders.flat_map(&:preloaded_records)
+
+ middle_to_pl = preloaders.each_with_object({}) do |pl,h|
+ pl.owners.each { |middle|
+ h[middle] = pl
+ }
+ end
+
+ record_offset = {}
+ @preloaded_records.each_with_index do |record,i|
+ record_offset[record] = i
+ end
- Hash[owners.map do |owner|
- through_records = Array.wrap(owner.send(through_reflection.name))
+ through_records.each_with_object({}) { |(lhs,center),records_by_owner|
+ pl_to_middle = center.group_by { |record| middle_to_pl[record] }
- # Dont cache the association - we would only be caching a subset
- if (through_scope != through_reflection.klass.unscoped) ||
- (reflection.options[:source_type] && through_reflection.collection?)
- owner.association(through_reflection.name).reset
+ records_by_owner[lhs] = pl_to_middle.flat_map do |pl, middles|
+ rhs_records = middles.flat_map { |r|
+ association = r.association source_reflection.name
+
+ association.reader
+ }.compact
+
+ rhs_records.sort_by { |rhs| record_offset[rhs] }
end
+ }
+ end
+
+ private
+
+ def reset_association(owners, association_name)
+ should_reset = (through_scope != through_reflection.klass.unscoped) ||
+ (reflection.options[:source_type] && through_reflection.collection?)
- [owner, through_records]
- end]
+ # Dont cache the association - we would only be caching a subset
+ if should_reset
+ owners.each { |owner|
+ owner.association(association_name).reset
+ }
+ end
end
+
def through_scope
- through_scope = through_reflection.klass.unscoped
+ scope = through_reflection.klass.unscoped
if options[:source_type]
- through_scope.where! reflection.foreign_type => options[:source_type]
+ scope.where! reflection.foreign_type => options[:source_type]
else
unless reflection_scope.where_values.empty?
- through_scope.includes_values = Array(reflection_scope.values[:includes] || options[:source])
- through_scope.where_values = reflection_scope.values[:where]
+ scope.includes_values = Array(reflection_scope.values[:includes] || options[:source])
+ scope.where_values = reflection_scope.values[:where]
end
- through_scope.references! reflection_scope.values[:references]
- through_scope.order! reflection_scope.values[:order] if through_scope.eager_loading?
+ scope.references! reflection_scope.values[:references]
+ scope.order! reflection_scope.values[:order] if scope.eager_loading?
end
- through_scope
+ scope
end
end
end
diff --git a/activerecord/lib/active_record/associations/singular_association.rb b/activerecord/lib/active_record/associations/singular_association.rb
index 10238555f0..e4500af5b2 100644
--- a/activerecord/lib/active_record/associations/singular_association.rb
+++ b/activerecord/lib/active_record/associations/singular_association.rb
@@ -39,10 +39,11 @@ module ActiveRecord
end
def find_target
- scope.first.tap { |record| set_inverse_instance(record) }
+ if record = scope.first
+ set_inverse_instance record
+ end
end
- # Implemented by subclasses
def replace(record)
raise NotImplementedError, "Subclasses must implement a replace(record) method"
end
diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb
index 35f29b37a2..ba7d2a3782 100644
--- a/activerecord/lib/active_record/associations/through_association.rb
+++ b/activerecord/lib/active_record/associations/through_association.rb
@@ -13,9 +13,9 @@ module ActiveRecord
# 2. To get the type conditions for any STI models in the chain
def target_scope
scope = super
- chain[1..-1].each do |reflection|
+ chain.drop(1).each do |reflection|
scope.merge!(
- reflection.klass.all.with_default_scope.
+ reflection.klass.all.
except(:select, :create_with, :includes, :preload, :joins, :eager_load)
)
end
diff --git a/activerecord/lib/active_record/attribute_assignment.rb b/activerecord/lib/active_record/attribute_assignment.rb
index e536f5ebcc..30fa2c8ba5 100644
--- a/activerecord/lib/active_record/attribute_assignment.rb
+++ b/activerecord/lib/active_record/attribute_assignment.rb
@@ -1,8 +1,8 @@
+require 'active_model/forbidden_attributes_protection'
module ActiveRecord
module AttributeAssignment
extend ActiveSupport::Concern
- include ActiveModel::DeprecatedMassAssignmentSecurity
include ActiveModel::ForbiddenAttributesProtection
# Allows you to set all the attributes by passing in a hash of attributes with
@@ -12,6 +12,9 @@ module ActiveRecord
# of this method is +false+ an <tt>ActiveModel::ForbiddenAttributesError</tt>
# exception is raised.
def assign_attributes(new_attributes)
+ if !new_attributes.respond_to?(:stringify_keys)
+ raise ArgumentError, "When assigning attributes, you must pass a hash as an argument."
+ end
return if new_attributes.blank?
attributes = new_attributes.stringify_keys
@@ -44,7 +47,7 @@ module ActiveRecord
if respond_to?("#{k}=")
raise
else
- raise UnknownAttributeError, "unknown attribute: #{k}"
+ raise UnknownAttributeError.new(self, k)
end
end
diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb
index 22405c5d74..73761520f7 100644
--- a/activerecord/lib/active_record/attribute_methods.rb
+++ b/activerecord/lib/active_record/attribute_methods.rb
@@ -1,4 +1,6 @@
require 'active_support/core_ext/enumerable'
+require 'mutex_m'
+require 'thread_safe'
module ActiveRecord
# = Active Record Attribute Methods
@@ -7,6 +9,7 @@ module ActiveRecord
include ActiveModel::AttributeMethods
included do
+ initialize_generated_modules
include Read
include Write
include BeforeTypeCast
@@ -17,27 +20,66 @@ module ActiveRecord
include Serialization
end
+ AttrNames = Module.new {
+ def self.set_name_cache(name, value)
+ const_name = "ATTR_#{name}"
+ unless const_defined? const_name
+ const_set const_name, value.dup.freeze
+ end
+ end
+ }
+
+ class AttributeMethodCache
+ def initialize
+ @module = Module.new
+ @method_cache = ThreadSafe::Cache.new
+ end
+
+ def [](name)
+ @method_cache.compute_if_absent(name) do
+ safe_name = name.unpack('h*').first
+ temp_method = "__temp__#{safe_name}"
+ ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name
+ @module.module_eval method_body(temp_method, safe_name), __FILE__, __LINE__
+ @module.instance_method temp_method
+ end
+ end
+
+ private
+ def method_body; raise NotImplementedError; end
+ end
+
module ClassMethods
+ def inherited(child_class) #:nodoc:
+ child_class.initialize_generated_modules
+ super
+ end
+
+ def initialize_generated_modules # :nodoc:
+ @generated_attribute_methods = Module.new { extend Mutex_m }
+ @attribute_methods_generated = false
+ include @generated_attribute_methods
+ end
+
# Generates all the attribute related methods for columns in the database
# accessors, mutators and query methods.
def define_attribute_methods # :nodoc:
# Use a mutex; we don't want two thread simultaneously trying to define
# attribute methods.
- @attribute_methods_mutex.synchronize do
- return if attribute_methods_generated?
+ generated_attribute_methods.synchronize do
+ return false if @attribute_methods_generated
superclass.define_attribute_methods unless self == base_class
super(column_names)
@attribute_methods_generated = true
end
- end
-
- def attribute_methods_generated? # :nodoc:
- @attribute_methods_generated ||= false
+ true
end
def undefine_attribute_methods # :nodoc:
- super if attribute_methods_generated?
- @attribute_methods_generated = false
+ generated_attribute_methods.synchronize do
+ super if @attribute_methods_generated
+ @attribute_methods_generated = false
+ end
end
# Raises a <tt>ActiveRecord::DangerousAttributeError</tt> exception when an
@@ -56,7 +98,7 @@ module ActiveRecord
# # => false
def instance_method_already_implemented?(method_name)
if dangerous_attribute_method?(method_name)
- raise DangerousAttributeError, "#{method_name} is defined by ActiveRecord"
+ raise DangerousAttributeError, "#{method_name} is defined by Active Record"
end
if superclass == Base
@@ -86,6 +128,16 @@ module ActiveRecord
end
end
+ def find_generated_attribute_method(method_name) # :nodoc:
+ klass = self
+ until klass == Base
+ gen_methods = klass.generated_attribute_methods
+ return gen_methods.instance_method(method_name) if method_defined_within?(method_name, gen_methods, Object)
+ klass = klass.superclass
+ end
+ nil
+ end
+
# Returns +true+ if +attribute+ is an attribute method and table exists,
# +false+ otherwise.
#
@@ -119,33 +171,21 @@ module ActiveRecord
# If we haven't generated any methods yet, generate them, then
# see if we've created the method we're looking for.
def method_missing(method, *args, &block) # :nodoc:
- unless self.class.attribute_methods_generated?
- self.class.define_attribute_methods
-
- if respond_to_without_attributes?(method)
- send(method, *args, &block)
+ self.class.define_attribute_methods
+ if respond_to_without_attributes?(method)
+ # make sure to invoke the correct attribute method, as we might have gotten here via a `super`
+ # call in a overwritten attribute method
+ if attribute_method = self.class.find_generated_attribute_method(method)
+ # this is probably horribly slow, but should only happen at most once for a given AR class
+ attribute_method.bind(self).call(*args, &block)
else
- super
+ send(method, *args, &block)
end
else
super
end
end
- def attribute_missing(match, *args, &block) # :nodoc:
- if self.class.columns_hash[match.attr_name]
- ActiveSupport::Deprecation.warn(
- "The method `#{match.method_name}', matching the attribute `#{match.attr_name}' has " \
- "dispatched through method_missing. This shouldn't happen, because `#{match.attr_name}' " \
- "is a column of the table. If this error has happened through normal usage of Active " \
- "Record (rather than through your own code or external libraries), please report it as " \
- "a bug."
- )
- end
-
- super
- end
-
# A Person object with a name attribute can ask <tt>person.respond_to?(:name)</tt>,
# <tt>person.respond_to?(:name=)</tt>, and <tt>person.respond_to?(:name?)</tt>
# which will all return +true+. It also define the attribute methods if they have
@@ -163,8 +203,22 @@ module ActiveRecord
# person.respond_to('age?') # => true
# person.respond_to(:nothing) # => false
def respond_to?(name, include_private = false)
- self.class.define_attribute_methods unless self.class.attribute_methods_generated?
- super
+ name = name.to_s
+ self.class.define_attribute_methods
+ result = super
+
+ # If the result is false the answer is false.
+ return false unless result
+
+ # If the result is true then check for the select case.
+ # For queries selecting a subset of columns, return false for unselected columns.
+ # We check defined?(@attributes) not to issue warnings if called on objects that
+ # have been allocated but not yet initialized.
+ if defined?(@attributes) && @attributes.any? && self.class.column_names.include?(name)
+ return has_attribute?(name)
+ end
+
+ return true
end
# Returns +true+ if the given attribute is in the attributes hash, otherwise +false+.
@@ -208,24 +262,31 @@ module ActiveRecord
# Returns an <tt>#inspect</tt>-like string for the value of the
# attribute +attr_name+. String attributes are truncated upto 50
- # characters, and Date and Time attributes are returned in the
- # <tt>:db</tt> format. Other attributes return the value of
- # <tt>#inspect</tt> without modification.
+ # characters, Date and Time attributes are returned in the
+ # <tt>:db</tt> format, Array attributes are truncated upto 10 values.
+ # Other attributes return the value of <tt>#inspect</tt> without
+ # modification.
#
# person = Person.create!(name: 'David Heinemeier Hansson ' * 3)
#
# person.attribute_for_inspect(:name)
- # # => "\"David Heinemeier Hansson David Heinemeier Hansson D...\""
+ # # => "\"David Heinemeier Hansson David Heinemeier Hansson ...\""
#
# person.attribute_for_inspect(:created_at)
# # => "\"2012-10-22 00:15:07\""
+ #
+ # person.attribute_for_inspect(:tag_ids)
+ # # => "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]"
def attribute_for_inspect(attr_name)
value = read_attribute(attr_name)
if value.is_a?(String) && value.length > 50
- "#{value[0..50]}...".inspect
+ "#{value[0, 50]}...".inspect
elsif value.is_a?(Date) || value.is_a?(Time)
%("#{value.to_s(:db)}")
+ elsif value.is_a?(Array) && value.size > 10
+ inspected = value.first(10).inspect
+ %(#{inspected[0...-1]}, ...])
else
value.inspect
end
@@ -269,7 +330,7 @@ module ActiveRecord
end
# Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example,
- # "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)). It raises
+ # "2004-12-12" in a date column is cast to a date object, like Date.new(2004, 12, 12)). It raises
# <tt>ActiveModel::MissingAttributeError</tt> if the identified attribute is missing.
#
# Alias for the <tt>read_attribute</tt> method.
@@ -328,6 +389,7 @@ module ActiveRecord
end
def attribute_method?(attr_name) # :nodoc:
+ # We check defined? because Syck calls respond_to? before actually calling initialize.
defined?(@attributes) && @attributes.include?(attr_name)
end
diff --git a/activerecord/lib/active_record/attribute_methods/before_type_cast.rb b/activerecord/lib/active_record/attribute_methods/before_type_cast.rb
index a23baeaced..f596a8b02e 100644
--- a/activerecord/lib/active_record/attribute_methods/before_type_cast.rb
+++ b/activerecord/lib/active_record/attribute_methods/before_type_cast.rb
@@ -41,8 +41,9 @@ module ActiveRecord
# task.read_attribute_before_type_cast('id') # => '1'
# task.read_attribute('completed_on') # => Sun, 21 Oct 2012
# task.read_attribute_before_type_cast('completed_on') # => "2012-10-21"
+ # task.read_attribute_before_type_cast(:completed_on) # => "2012-10-21"
def read_attribute_before_type_cast(attr_name)
- @attributes[attr_name]
+ @attributes[attr_name.to_s]
end
# Returns a hash of attributes before typecasting and deserialization.
diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb
index 6315dd9549..19e81abba5 100644
--- a/activerecord/lib/active_record/attribute_methods/dirty.rb
+++ b/activerecord/lib/active_record/attribute_methods/dirty.rb
@@ -14,24 +14,12 @@ module ActiveRecord
class_attribute :partial_writes, instance_writer: false
self.partial_writes = true
-
- def self.partial_updates=(v); self.partial_writes = v; end
- def self.partial_updates?; partial_writes?; end
- def self.partial_updates; partial_writes; end
-
- ActiveSupport::Deprecation.deprecate_methods(
- singleton_class,
- :partial_updates= => :partial_writes=,
- :partial_updates? => :partial_writes?,
- :partial_updates => :partial_writes
- )
end
# Attempts to +save+ the record and clears changed attributes if successful.
def save(*)
if status = super
- @previously_changed = changes
- @changed_attributes.clear
+ changes_applied
end
status
end
@@ -39,16 +27,14 @@ module ActiveRecord
# Attempts to <tt>save!</tt> the record and clears changed attributes if successful.
def save!(*)
super.tap do
- @previously_changed = changes
- @changed_attributes.clear
+ changes_applied
end
end
# <tt>reload</tt> the record and clears changed attributes.
def reload(*)
super.tap do
- @previously_changed.clear
- @changed_attributes.clear
+ reset_changes
end
end
@@ -59,11 +45,11 @@ module ActiveRecord
# The attribute already has an unsaved change.
if attribute_changed?(attr)
- old = @changed_attributes[attr]
- @changed_attributes.delete(attr) unless _field_changed?(attr, old, value)
+ old = changed_attributes[attr]
+ changed_attributes.delete(attr) unless _field_changed?(attr, old, value)
else
old = clone_attribute_value(:read_attribute, attr)
- @changed_attributes[attr] = old if _field_changed?(attr, old, value)
+ changed_attributes[attr] = old if _field_changed?(attr, old, value)
end
# Carry on.
diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb
index 506f5d75f9..d01e9aea59 100644
--- a/activerecord/lib/active_record/attribute_methods/read.rb
+++ b/activerecord/lib/active_record/attribute_methods/read.rb
@@ -1,6 +1,38 @@
+require 'active_support/core_ext/module/method_transplanting'
+
module ActiveRecord
module AttributeMethods
module Read
+ ReaderMethodCache = Class.new(AttributeMethodCache) {
+ private
+ # We want to generate the methods via module_eval rather than
+ # define_method, because define_method is slower on dispatch.
+ # Evaluating many similar methods may use more memory as the instruction
+ # sequences are duplicated and cached (in MRI). define_method may
+ # be slower on dispatch, but if you're careful about the closure
+ # created, then define_method will consume much less memory.
+ #
+ # But sometimes the database might return columns with
+ # characters that are not allowed in normal method names (like
+ # 'my_column(omg)'. So to work around this we first define with
+ # the __temp__ identifier, and then use alias method to rename
+ # it to what we want.
+ #
+ # We are also defining a constant to hold the frozen string of
+ # the attribute name. Using a constant means that we do not have
+ # to allocate an object on each call to the attribute method.
+ # Making it frozen means that it doesn't get duped when used to
+ # key the @attributes_cache in read_attribute.
+ def method_body(method_name, const_name)
+ <<-EOMETHOD
+ def #{method_name}
+ name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{const_name}
+ read_attribute(name) { |n| missing_attribute(n, caller) }
+ end
+ EOMETHOD
+ end
+ }.new
+
extend ActiveSupport::Concern
ATTRIBUTE_TYPES_CACHED_BY_DEFAULT = [:datetime, :timestamp, :time, :date]
@@ -32,30 +64,30 @@ module ActiveRecord
protected
- # We want to generate the methods via module_eval rather than
- # define_method, because define_method is slower on dispatch and
- # uses more memory (because it creates a closure).
- #
- # But sometimes the database might return columns with
- # characters that are not allowed in normal method names (like
- # 'my_column(omg)'. So to work around this we first define with
- # the __temp__ identifier, and then use alias method to rename
- # it to what we want.
- #
- # We are also defining a constant to hold the frozen string of
- # the attribute name. Using a constant means that we do not have
- # to allocate an object on each call to the attribute method.
- # Making it frozen means that it doesn't get duped when used to
- # key the @attributes_cache in read_attribute.
- def define_method_attribute(name)
- safe_name = name.unpack('h*').first
- generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
- def __temp__#{safe_name}
- read_attribute(AttrNames::ATTR_#{safe_name}) { |n| missing_attribute(n, caller) }
+ if Module.methods_transplantable?
+ def define_method_attribute(name)
+ method = ReaderMethodCache[name]
+ generated_attribute_methods.module_eval { define_method name, method }
+ end
+ else
+ def define_method_attribute(name)
+ safe_name = name.unpack('h*').first
+ temp_method = "__temp__#{safe_name}"
+
+ ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name
+
+ generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
+ def #{temp_method}
+ name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{safe_name}
+ read_attribute(name) { |n| missing_attribute(n, caller) }
+ end
+ STR
+
+ generated_attribute_methods.module_eval do
+ alias_method name, temp_method
+ undef_method temp_method
end
- alias_method #{name.inspect}, :__temp__#{safe_name}
- undef_method :__temp__#{safe_name}
- STR
+ end
end
private
@@ -70,20 +102,21 @@ module ActiveRecord
end
# Returns the value of the attribute identified by <tt>attr_name</tt> after
- # it has been typecast (for example, "2004-12-12" in a data column is cast
+ # it has been typecast (for example, "2004-12-12" in a date column is cast
# to a date object, like Date.new(2004, 12, 12)).
def read_attribute(attr_name)
# If it's cached, just return it
# We use #[] first as a perf optimization for non-nil values. See https://gist.github.com/jonleighton/3552829.
name = attr_name.to_s
@attributes_cache[name] || @attributes_cache.fetch(name) {
- column = @columns_hash.fetch(name) {
- return @attributes.fetch(name) {
- if name == 'id' && self.class.primary_key != name
- read_attribute(self.class.primary_key)
- end
- }
- }
+ column = @column_types_override[name] if @column_types_override
+ column ||= @column_types[name]
+
+ return @attributes.fetch(name) {
+ if name == 'id' && self.class.primary_key != name
+ read_attribute(self.class.primary_key)
+ end
+ } unless column
value = @attributes.fetch(name) {
return block_given? ? yield(name) : nil
diff --git a/activerecord/lib/active_record/attribute_methods/serialization.rb b/activerecord/lib/active_record/attribute_methods/serialization.rb
index 25d62fdb85..d484659190 100644
--- a/activerecord/lib/active_record/attribute_methods/serialization.rb
+++ b/activerecord/lib/active_record/attribute_methods/serialization.rb
@@ -11,6 +11,12 @@ module ActiveRecord
end
module ClassMethods
+ ##
+ # :method: serialized_attributes
+ #
+ # Returns a hash of all the attributes that have been specified for
+ # serialization as keys and their class restriction as values.
+
# If you have an attribute that needs to be saved to the database as an
# object, and retrieved as the same object, then specify the name of that
# attribute using this method and it will be handled automatically. The
@@ -18,6 +24,9 @@ module ActiveRecord
# serialized object must be of that class on retrieval or
# <tt>SerializationTypeMismatch</tt> will be raised.
#
+ # A notable side effect of serialized attributes is that the model will
+ # be updated on every save, even if it is not dirty.
+ #
# ==== Parameters
#
# * +attr_name+ - The field name that should be serialized.
@@ -44,38 +53,40 @@ module ActiveRecord
end
end
- def serialized_attributes
- message = "Instance level serialized_attributes method is deprecated, please use class level method."
- ActiveSupport::Deprecation.warn message
- defined?(@serialized_attributes) ? @serialized_attributes : self.class.serialized_attributes
- end
-
class Type # :nodoc:
def initialize(column)
@column = column
end
def type_cast(value)
- value.unserialized_value
+ if value.state == :serialized
+ value.unserialized_value @column.type_cast value.value
+ else
+ value.unserialized_value
+ end
end
def type
@column.type
end
+
+ def accessor
+ ActiveRecord::Store::IndifferentHashAccessor
+ end
end
class Attribute < Struct.new(:coder, :value, :state) # :nodoc:
- def unserialized_value
- state == :serialized ? unserialize : value
+ def unserialized_value(v = value)
+ state == :serialized ? unserialize(v) : value
end
def serialized_value
state == :unserialized ? serialize : value
end
- def unserialize
+ def unserialize(v)
self.state = :unserialized
- self.value = coder.load(value)
+ self.value = coder.load(v)
end
def serialize
@@ -86,10 +97,10 @@ module ActiveRecord
# This is only added to the model when serialize is called, which
# ensures we do not make things slower when serialization is not used.
- module Behavior #:nodoc:
+ module Behavior # :nodoc:
extend ActiveSupport::Concern
- module ClassMethods
+ module ClassMethods # :nodoc:
def initialize_attributes(attributes, options = {})
serialized = (options.delete(:serialized) { true }) ? :serialized : :unserialized
super(attributes, options)
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 41b5a6e926..f168282ea3 100644
--- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
+++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
@@ -51,7 +51,7 @@ module ActiveRecord
def create_time_zone_conversion_attribute?(name, column)
time_zone_aware_attributes &&
!self.skip_time_zone_conversion_for_attributes.include?(name.to_sym) &&
- [:datetime, :timestamp].include?(column.type)
+ (:datetime == column.type || :timestamp == column.type)
end
end
end
diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb
index cd33494cc3..c853fc0917 100644
--- a/activerecord/lib/active_record/attribute_methods/write.rb
+++ b/activerecord/lib/active_record/attribute_methods/write.rb
@@ -1,6 +1,21 @@
+require 'active_support/core_ext/module/method_transplanting'
+
module ActiveRecord
module AttributeMethods
module Write
+ WriterMethodCache = Class.new(AttributeMethodCache) {
+ private
+
+ def method_body(method_name, const_name)
+ <<-EOMETHOD
+ def #{method_name}(value)
+ name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{const_name}
+ write_attribute(name, value)
+ end
+ EOMETHOD
+ end
+ }.new
+
extend ActiveSupport::Concern
included do
@@ -10,17 +25,29 @@ module ActiveRecord
module ClassMethods
protected
- # See define_method_attribute in read.rb for an explanation of
- # this code.
- def define_method_attribute=(name)
- safe_name = name.unpack('h*').first
- generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
- def __temp__#{safe_name}=(value)
- write_attribute(AttrNames::ATTR_#{safe_name}, value)
- end
- alias_method #{(name + '=').inspect}, :__temp__#{safe_name}=
- undef_method :__temp__#{safe_name}=
- STR
+ if Module.methods_transplantable?
+ # See define_method_attribute in read.rb for an explanation of
+ # this code.
+ def define_method_attribute=(name)
+ method = WriterMethodCache[name]
+ generated_attribute_methods.module_eval {
+ define_method "#{name}=", method
+ }
+ end
+ else
+ def define_method_attribute=(name)
+ safe_name = name.unpack('h*').first
+ ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name
+
+ generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
+ def __temp__#{safe_name}=(value)
+ name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{safe_name}
+ write_attribute(name, value)
+ end
+ alias_method #{(name + '=').inspect}, :__temp__#{safe_name}=
+ undef_method :__temp__#{safe_name}=
+ STR
+ end
end
end
diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb
index 44323ce9db..e9622ca0c1 100644
--- a/activerecord/lib/active_record/autosave_association.rb
+++ b/activerecord/lib/active_record/autosave_association.rb
@@ -17,7 +17,8 @@ module ActiveRecord
# be destroyed directly. They will however still be marked for destruction.
#
# Note that <tt>autosave: false</tt> is not same as not declaring <tt>:autosave</tt>.
- # When the <tt>:autosave</tt> option is not present new associations are saved.
+ # When the <tt>:autosave</tt> option is not present then new association records are
+ # saved but the updated association records are not saved.
#
# == Validation
#
@@ -126,87 +127,87 @@ module ActiveRecord
extend ActiveSupport::Concern
module AssociationBuilderExtension #:nodoc:
- def build
+ def self.build(model, reflection)
model.send(:add_autosave_association_callbacks, reflection)
- super
+ end
+
+ def self.valid_options
+ [ :autosave ]
end
end
included do
- Associations::Builder::Association.class_eval do
- self.valid_options << :autosave
- include AssociationBuilderExtension
- end
+ Associations::Builder::Association.extensions << AssociationBuilderExtension
end
module ClassMethods
private
- def define_non_cyclic_method(name, reflection, &block)
- define_method(name) do |*args|
- result = true; @_already_called ||= {}
- # Loop prevention for validation of associations
- unless @_already_called[[name, reflection.name]]
- begin
- @_already_called[[name, reflection.name]]=true
- result = instance_eval(&block)
- ensure
- @_already_called[[name, reflection.name]]=false
+ def define_non_cyclic_method(name, &block)
+ define_method(name) do |*args|
+ result = true; @_already_called ||= {}
+ # Loop prevention for validation of associations
+ unless @_already_called[name]
+ begin
+ @_already_called[name]=true
+ result = instance_eval(&block)
+ ensure
+ @_already_called[name]=false
+ end
end
- end
- result
+ result
+ end
end
- end
- # Adds validation and save callbacks for the association as specified by
- # the +reflection+.
- #
- # For performance reasons, we don't check whether to validate at runtime.
- # However the validation and callback methods are lazy and those methods
- # get created when they are invoked for the very first time. However,
- # this can change, for instance, when using nested attributes, which is
- # called _after_ the association has been defined. Since we don't want
- # the callbacks to get defined multiple times, there are guards that
- # check if the save or validation methods have already been defined
- # before actually defining them.
- def add_autosave_association_callbacks(reflection)
- save_method = :"autosave_associated_records_for_#{reflection.name}"
- validation_method = :"validate_associated_records_for_#{reflection.name}"
- collection = reflection.collection?
-
- unless method_defined?(save_method)
- if collection
- before_save :before_save_collection_association
-
- define_non_cyclic_method(save_method, reflection) { save_collection_association(reflection) }
- # Doesn't use after_save as that would save associations added in after_create/after_update twice
- after_create save_method
- after_update save_method
- elsif reflection.macro == :has_one
- define_method(save_method) { save_has_one_association(reflection) }
- # Configures two callbacks instead of a single after_save so that
- # the model may rely on their execution order relative to its
- # own callbacks.
- #
- # For example, given that after_creates run before after_saves, if
- # we configured instead an after_save there would be no way to fire
- # a custom after_create callback after the child association gets
- # created.
- after_create save_method
- after_update save_method
- else
- define_non_cyclic_method(save_method, reflection) { save_belongs_to_association(reflection) }
- before_save save_method
+ # Adds validation and save callbacks for the association as specified by
+ # the +reflection+.
+ #
+ # For performance reasons, we don't check whether to validate at runtime.
+ # However the validation and callback methods are lazy and those methods
+ # get created when they are invoked for the very first time. However,
+ # this can change, for instance, when using nested attributes, which is
+ # called _after_ the association has been defined. Since we don't want
+ # the callbacks to get defined multiple times, there are guards that
+ # check if the save or validation methods have already been defined
+ # before actually defining them.
+ def add_autosave_association_callbacks(reflection)
+ save_method = :"autosave_associated_records_for_#{reflection.name}"
+ validation_method = :"validate_associated_records_for_#{reflection.name}"
+ collection = reflection.collection?
+
+ unless method_defined?(save_method)
+ if collection
+ before_save :before_save_collection_association
+
+ define_non_cyclic_method(save_method) { save_collection_association(reflection) }
+ # Doesn't use after_save as that would save associations added in after_create/after_update twice
+ after_create save_method
+ after_update save_method
+ elsif reflection.macro == :has_one
+ define_method(save_method) { save_has_one_association(reflection) }
+ # Configures two callbacks instead of a single after_save so that
+ # the model may rely on their execution order relative to its
+ # own callbacks.
+ #
+ # For example, given that after_creates run before after_saves, if
+ # we configured instead an after_save there would be no way to fire
+ # a custom after_create callback after the child association gets
+ # created.
+ after_create save_method
+ after_update save_method
+ else
+ define_non_cyclic_method(save_method) { save_belongs_to_association(reflection) }
+ before_save save_method
+ end
end
- end
- if reflection.validate? && !method_defined?(validation_method)
- method = (collection ? :validate_collection_association : :validate_single_association)
- define_non_cyclic_method(validation_method, reflection) { send(method, reflection) }
- validate validation_method
+ if reflection.validate? && !method_defined?(validation_method)
+ method = (collection ? :validate_collection_association : :validate_single_association)
+ define_non_cyclic_method(validation_method) { send(method, reflection) }
+ validate validation_method
+ end
end
- end
end
# Reloads the attributes of the object as usual and clears <tt>marked_for_destruction</tt> flag.
@@ -253,173 +254,179 @@ module ActiveRecord
private
- # Returns the record for an association collection that should be validated
- # or saved. If +autosave+ is +false+ only new records will be returned,
- # unless the parent is/was a new record itself.
- def associated_records_to_validate_or_save(association, new_record, autosave)
- if new_record
- association && association.target
- elsif autosave
- association.target.find_all { |record| record.changed_for_autosave? }
- else
- association.target.find_all { |record| record.new_record? }
+ # Returns the record for an association collection that should be validated
+ # or saved. If +autosave+ is +false+ only new records will be returned,
+ # unless the parent is/was a new record itself.
+ def associated_records_to_validate_or_save(association, new_record, autosave)
+ if new_record
+ association && association.target
+ elsif autosave
+ association.target.find_all { |record| record.changed_for_autosave? }
+ else
+ association.target.find_all { |record| record.new_record? }
+ end
end
- end
- # go through nested autosave associations that are loaded in memory (without loading
- # any new ones), and return true if is changed for autosave
- def nested_records_changed_for_autosave?
- self.class.reflect_on_all_autosave_associations.any? do |reflection|
- association = association_instance_get(reflection.name)
- association && Array.wrap(association.target).any? { |a| a.changed_for_autosave? }
+ # go through nested autosave associations that are loaded in memory (without loading
+ # any new ones), and return true if is changed for autosave
+ def nested_records_changed_for_autosave?
+ self.class.reflect_on_all_autosave_associations.any? do |reflection|
+ association = association_instance_get(reflection.name)
+ association && Array.wrap(association.target).any? { |a| a.changed_for_autosave? }
+ end
end
- end
- # Validate the association if <tt>:validate</tt> or <tt>:autosave</tt> is
- # turned on for the association.
- def validate_single_association(reflection)
- association = association_instance_get(reflection.name)
- record = association && association.reader
- association_valid?(reflection, record) if record
- end
+ # Validate the association if <tt>:validate</tt> or <tt>:autosave</tt> is
+ # turned on for the association.
+ def validate_single_association(reflection)
+ association = association_instance_get(reflection.name)
+ record = association && association.reader
+ association_valid?(reflection, record) if record
+ end
- # Validate the associated records if <tt>:validate</tt> or
- # <tt>:autosave</tt> is turned on for the association specified by
- # +reflection+.
- def validate_collection_association(reflection)
- if association = association_instance_get(reflection.name)
- if records = associated_records_to_validate_or_save(association, new_record?, reflection.options[:autosave])
- records.each { |record| association_valid?(reflection, record) }
+ # Validate the associated records if <tt>:validate</tt> or
+ # <tt>:autosave</tt> is turned on for the association specified by
+ # +reflection+.
+ def validate_collection_association(reflection)
+ if association = association_instance_get(reflection.name)
+ if records = associated_records_to_validate_or_save(association, new_record?, reflection.options[:autosave])
+ records.each { |record| association_valid?(reflection, record) }
+ end
end
end
- end
- # Returns whether or not the association is valid and applies any errors to
- # the parent, <tt>self</tt>, if it wasn't. Skips any <tt>:autosave</tt>
- # enabled records if they're marked_for_destruction? or destroyed.
- def association_valid?(reflection, record)
- return true if record.destroyed? || record.marked_for_destruction?
-
- unless valid = record.valid?(validation_context)
- if reflection.options[:autosave]
- record.errors.each do |attribute, message|
- attribute = "#{reflection.name}.#{attribute}"
- errors[attribute] << message
- errors[attribute].uniq!
+ # Returns whether or not the association is valid and applies any errors to
+ # the parent, <tt>self</tt>, if it wasn't. Skips any <tt>:autosave</tt>
+ # enabled records if they're marked_for_destruction? or destroyed.
+ def association_valid?(reflection, record)
+ return true if record.destroyed? || record.marked_for_destruction?
+
+ unless valid = record.valid?
+ if reflection.options[:autosave]
+ record.errors.each do |attribute, message|
+ attribute = "#{reflection.name}.#{attribute}"
+ errors[attribute] << message
+ errors[attribute].uniq!
+ end
+ else
+ errors.add(reflection.name)
end
- else
- errors.add(reflection.name)
end
+ valid
end
- valid
- end
- # Is used as a before_save callback to check while saving a collection
- # association whether or not the parent was a new record before saving.
- def before_save_collection_association
- @new_record_before_save = new_record?
- true
- end
+ # Is used as a before_save callback to check while saving a collection
+ # association whether or not the parent was a new record before saving.
+ def before_save_collection_association
+ @new_record_before_save = new_record?
+ true
+ end
- # Saves any new associated records, or all loaded autosave associations if
- # <tt>:autosave</tt> is enabled on the association.
- #
- # In addition, it destroys all children that were marked for destruction
- # with mark_for_destruction.
- #
- # This all happens inside a transaction, _if_ the Transactions module is included into
- # ActiveRecord::Base after the AutosaveAssociation module, which it does by default.
- def save_collection_association(reflection)
- if association = association_instance_get(reflection.name)
- autosave = reflection.options[:autosave]
-
- if records = associated_records_to_validate_or_save(association, @new_record_before_save, autosave)
- records_to_destroy = []
- records.each do |record|
- next if record.destroyed?
-
- saved = true
-
- if autosave && record.marked_for_destruction?
- records_to_destroy << record
- elsif autosave != false && (@new_record_before_save || record.new_record?)
- if autosave
- saved = association.insert_record(record, false)
- else
- association.insert_record(record) unless reflection.nested?
- end
- elsif autosave
- saved = record.save(:validate => false)
+ # Saves any new associated records, or all loaded autosave associations if
+ # <tt>:autosave</tt> is enabled on the association.
+ #
+ # In addition, it destroys all children that were marked for destruction
+ # with mark_for_destruction.
+ #
+ # This all happens inside a transaction, _if_ the Transactions module is included into
+ # ActiveRecord::Base after the AutosaveAssociation module, which it does by default.
+ def save_collection_association(reflection)
+ if association = association_instance_get(reflection.name)
+ autosave = reflection.options[:autosave]
+
+ if records = associated_records_to_validate_or_save(association, @new_record_before_save, autosave)
+
+ if autosave
+ records_to_destroy = records.select(&:marked_for_destruction?)
+ records_to_destroy.each { |record| association.destroy(record) }
+ records -= records_to_destroy
end
- raise ActiveRecord::Rollback unless saved
- end
+ records.each do |record|
+ next if record.destroyed?
+
+ saved = true
- records_to_destroy.each do |record|
- association.destroy(record)
+ if autosave != false && (@new_record_before_save || record.new_record?)
+ if autosave
+ saved = association.insert_record(record, false)
+ else
+ association.insert_record(record) unless reflection.nested?
+ end
+ elsif autosave
+ saved = record.save(:validate => false)
+ end
+
+ raise ActiveRecord::Rollback unless saved
+ end
end
- end
- # reconstruct the scope now that we know the owner's id
- association.reset_scope if association.respond_to?(:reset_scope)
+ # reconstruct the scope now that we know the owner's id
+ association.reset_scope if association.respond_to?(:reset_scope)
+ end
end
- end
- # Saves the associated record if it's new or <tt>:autosave</tt> is enabled
- # on the association.
- #
- # In addition, it will destroy the association if it was marked for
- # destruction with mark_for_destruction.
- #
- # This all happens inside a transaction, _if_ the Transactions module is included into
- # ActiveRecord::Base after the AutosaveAssociation module, which it does by default.
- def save_has_one_association(reflection)
- association = association_instance_get(reflection.name)
- record = association && association.load_target
- if record && !record.destroyed?
- autosave = reflection.options[:autosave]
-
- if autosave && record.marked_for_destruction?
- record.destroy
- else
- key = reflection.options[:primary_key] ? send(reflection.options[:primary_key]) : id
- if autosave != false && (new_record? || record.new_record? || record[reflection.foreign_key] != key || autosave)
- unless reflection.through_reflection
- record[reflection.foreign_key] = key
- end
+ # Saves the associated record if it's new or <tt>:autosave</tt> is enabled
+ # on the association.
+ #
+ # In addition, it will destroy the association if it was marked for
+ # destruction with mark_for_destruction.
+ #
+ # This all happens inside a transaction, _if_ the Transactions module is included into
+ # ActiveRecord::Base after the AutosaveAssociation module, which it does by default.
+ def save_has_one_association(reflection)
+ association = association_instance_get(reflection.name)
+ record = association && association.load_target
+ if record && !record.destroyed?
+ autosave = reflection.options[:autosave]
+
+ if autosave && record.marked_for_destruction?
+ record.destroy
+ else
+ key = reflection.options[:primary_key] ? send(reflection.options[:primary_key]) : id
+ if autosave != false && (autosave || new_record? || record_changed?(reflection, record, key))
+
+ unless reflection.through_reflection
+ record[reflection.foreign_key] = key
+ end
- saved = record.save(:validate => !autosave)
- raise ActiveRecord::Rollback if !saved && autosave
- saved
+ saved = record.save(:validate => !autosave)
+ raise ActiveRecord::Rollback if !saved && autosave
+ saved
+ end
end
end
end
- end
- # Saves the associated record if it's new or <tt>:autosave</tt> is enabled.
- #
- # In addition, it will destroy the association if it was marked for destruction.
- def save_belongs_to_association(reflection)
- association = association_instance_get(reflection.name)
- record = association && association.load_target
- if record && !record.destroyed?
- autosave = reflection.options[:autosave]
-
- if autosave && record.marked_for_destruction?
- self[reflection.foreign_key] = nil
- record.destroy
- elsif autosave != false
- saved = record.save(:validate => !autosave) if record.new_record? || (autosave && record.changed_for_autosave?)
-
- if association.updated?
- association_id = record.send(reflection.options[:primary_key] || :id)
- self[reflection.foreign_key] = association_id
- association.loaded!
- end
+ # If the record is new or it has changed, returns true.
+ def record_changed?(reflection, record, key)
+ record.new_record? || record[reflection.foreign_key] != key || record.attribute_changed?(reflection.foreign_key)
+ end
+
+ # Saves the associated record if it's new or <tt>:autosave</tt> is enabled.
+ #
+ # In addition, it will destroy the association if it was marked for destruction.
+ def save_belongs_to_association(reflection)
+ association = association_instance_get(reflection.name)
+ record = association && association.load_target
+ if record && !record.destroyed?
+ autosave = reflection.options[:autosave]
+
+ if autosave && record.marked_for_destruction?
+ self[reflection.foreign_key] = nil
+ record.destroy
+ elsif autosave != false
+ saved = record.save(:validate => !autosave) if record.new_record? || (autosave && record.changed_for_autosave?)
+
+ if association.updated?
+ association_id = record.send(reflection.options[:primary_key] || :id)
+ self[reflection.foreign_key] = association_id
+ association.loaded!
+ end
- saved if autosave
+ saved if autosave
+ end
end
end
- end
end
end
diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb
index b06add096f..1d3ec75aa1 100644
--- a/activerecord/lib/active_record/base.rb
+++ b/activerecord/lib/active_record/base.rb
@@ -4,7 +4,7 @@ require 'active_support/benchmarkable'
require 'active_support/dependencies'
require 'active_support/descendants_tracker'
require 'active_support/time'
-require 'active_support/core_ext/class/attribute_accessors'
+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'
@@ -18,6 +18,7 @@ require 'arel'
require 'active_record/errors'
require 'active_record/log_subscriber'
require 'active_record/explain_subscriber'
+require 'active_record/relation/delegation'
module ActiveRecord #:nodoc:
# = Active Record
@@ -290,8 +291,11 @@ module ActiveRecord #:nodoc:
extend Translation
extend DynamicMatchers
extend Explain
+ extend Enum
+ extend Delegation::DelegateCache
include Persistence
+ include NoTouching
include ReadonlyAttributes
include ModelSchema
include Inheritance
diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb
index 22226b2f4f..35f19f0bc0 100644
--- a/activerecord/lib/active_record/callbacks.rb
+++ b/activerecord/lib/active_record/callbacks.rb
@@ -23,11 +23,14 @@ module ActiveRecord
# Check out <tt>ActiveRecord::Transactions</tt> for more details about <tt>after_commit</tt> and
# <tt>after_rollback</tt>.
#
+ # Additionally, an <tt>after_touch</tt> callback is triggered whenever an
+ # object is touched.
+ #
# Lastly an <tt>after_find</tt> and <tt>after_initialize</tt> callback is triggered for each object that
# is found and instantiated by a finder, with <tt>after_initialize</tt> being triggered after new objects
# are instantiated as well.
#
- # That's a total of twelve callbacks, which gives you immense power to react and prepare for each state in the
+ # There are nineteen callbacks in total, which give you immense power to react and prepare for each state in the
# Active Record life cycle. The sequence for calling <tt>Base#save</tt> for an existing record is similar,
# except that each <tt>_create</tt> callback is replaced by the corresponding <tt>_update</tt> callback.
#
@@ -83,7 +86,7 @@ module ActiveRecord
#
# In that case, <tt>Reply#destroy</tt> would only run +destroy_readers+ and _not_ +destroy_author+.
# So, use the callback macros when you want to ensure that a certain callback is called for the entire
- # hierarchy, and use the regular overwriteable methods when you want to leave it up to each descendant
+ # hierarchy, and use the regular overwritable methods when you want to leave it up to each descendant
# to decide whether they want to call +super+ and trigger the inherited callbacks.
#
# *IMPORTANT:* In order for inheritance to work for the callback queues, you must specify the
@@ -125,7 +128,7 @@ module ActiveRecord
# record.credit_card_number = decrypt(record.credit_card_number)
# end
#
- # alias_method :after_find, :after_save
+ # alias_method :after_initialize, :after_save
#
# private
# def encrypt(value)
@@ -160,7 +163,7 @@ module ActiveRecord
# record.send("#{@attribute}=", decrypt(record.send("#{@attribute}")))
# end
#
- # alias_method :after_find, :after_save
+ # alias_method :after_initialize, :after_save
#
# private
# def encrypt(value)
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 bf2f945448..cebe741daa 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
@@ -86,7 +86,7 @@ module ActiveRecord
end
end
- # Return the number of threads currently waiting on this
+ # Returns the number of threads currently waiting on this
# queue.
def num_waiting
synchronize do
@@ -253,14 +253,6 @@ module ActiveRecord
@available = Queue.new self
end
- # Hack for tests to be able to add connections. Do not call outside of tests
- def insert_connection_for_test!(c) #:nodoc:
- synchronize do
- @connections << c
- @available.add c
- end
- end
-
# Retrieve the connection associated with the current thread, or call
# #checkout to obtain one if necessary.
#
@@ -340,11 +332,6 @@ module ActiveRecord
end
end
- def clear_stale_cached_connections! # :nodoc:
- reap
- end
- deprecate :clear_stale_cached_connections! => "Please use #reap instead"
-
# Check-out a database connection from the pool, indicating that you want
# to use it. You should call #checkin when you no longer need this.
#
@@ -637,7 +624,7 @@ module ActiveRecord
end
response
- rescue
+ rescue Exception
ActiveRecord::Base.clear_active_connections! unless testing
raise
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
index c64b542286..e196df079b 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
@@ -18,8 +18,7 @@ module ActiveRecord
end
end
- # Returns an array of record hashes with the column names as keys and
- # column values as values.
+ # Returns an ActiveRecord::Result instance.
def select_all(arel, name = nil, binds = [])
select(to_sql(arel, binds), name, binds)
end
@@ -27,8 +26,7 @@ module ActiveRecord
# Returns a record hash with the column names as keys and column values
# as values.
def select_one(arel, name = nil, binds = [])
- result = select_all(arel, name, binds)
- result.first if result
+ select_all(arel, name, binds).first
end
# Returns a single value from a record
@@ -41,8 +39,8 @@ module ActiveRecord
# Returns an array of the values of the first column in a select:
# select_values("SELECT id FROM companies LIMIT 3") => [1,2,3]
def select_values(arel, name = nil)
- result = select_rows(to_sql(arel, []), name)
- result.map { |v| v[0] }
+ select_rows(to_sql(arel, []), name)
+ .map { |v| v[0] }
end
# Returns an array of arrays containing the field values.
@@ -288,6 +286,10 @@ module ActiveRecord
# Inserts the given fixture into the table. Overridden in adapters that require
# something beyond a simple insert (eg. Oracle).
def insert_fixture(fixture, table_name)
+ execute fixture_sql(fixture, table_name), 'Fixture Insert'
+ end
+
+ def fixture_sql(fixture, table_name)
columns = schema_cache.columns_hash(table_name)
key_list = []
@@ -296,17 +298,13 @@ module ActiveRecord
quote(value, columns[name])
end
- execute "INSERT INTO #{quote_table_name(table_name)} (#{key_list.join(', ')}) VALUES (#{value_list.join(', ')})", 'Fixture Insert'
+ "INSERT INTO #{quote_table_name(table_name)} (#{key_list.join(', ')}) VALUES (#{value_list.join(', ')})"
end
def empty_insert_statement_value
"DEFAULT VALUES"
end
- def case_sensitive_equality_operator
- "="
- end
-
def limited_update_conditions(where_sql, quoted_table_name, quoted_primary_key)
"WHERE #{quoted_primary_key} IN (SELECT #{quoted_primary_key} FROM #{quoted_table_name} #{where_sql})"
end
@@ -348,15 +346,14 @@ module ActiveRecord
protected
- # Return a subquery for the given key using the join information.
+ # Returns a subquery for the given key using the join information.
def subquery_for(key, select)
subselect = select.clone
subselect.projections = [key]
subselect
end
- # Returns an array of record hashes with the column names as keys and
- # column values as values.
+ # Returns an ActiveRecord::Result instance.
def select(sql, name = nil, binds = [])
end
undef_method :select
@@ -377,14 +374,14 @@ module ActiveRecord
update_sql(sql, name)
end
- def sql_for_insert(sql, pk, id_value, sequence_name, binds)
- [sql, binds]
- end
+ def sql_for_insert(sql, pk, id_value, sequence_name, binds)
+ [sql, binds]
+ end
- def last_inserted_id(result)
- row = result.rows.first
- row && row.first
- end
+ def last_inserted_id(result)
+ row = result.rows.first
+ row && row.first
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb
index 41e07fbda9..adc23a6674 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb
@@ -9,10 +9,10 @@ module ActiveRecord
def dirties_query_cache(base, *method_names)
method_names.each do |method_name|
base.class_eval <<-end_code, __FILE__, __LINE__ + 1
- def #{method_name}(*) # def update_with_query_dirty(*)
- clear_query_cache if @query_cache_enabled # clear_query_cache if @query_cache_enabled
- super # super
- end # end
+ def #{method_name}(*)
+ clear_query_cache if @query_cache_enabled
+ super
+ end
end_code
end
end
@@ -20,13 +20,19 @@ module ActiveRecord
attr_reader :query_cache, :query_cache_enabled
+ def initialize(*)
+ super
+ @query_cache = Hash.new { |h,sql| h[sql] = {} }
+ @query_cache_enabled = false
+ end
+
# Enable the query cache within the block.
def cache
old, @query_cache_enabled = @query_cache_enabled, true
yield
ensure
- clear_query_cache
@query_cache_enabled = old
+ clear_query_cache unless @query_cache_enabled
end
def enable_query_cache!
@@ -75,14 +81,7 @@ module ActiveRecord
else
@query_cache[sql][binds] = yield
end
-
- # FIXME: we should guarantee that all cached items are Result
- # objects. Then we can avoid this conditional
- if ActiveRecord::Result === result
- result.dup
- else
- result.collect { |row| row.dup }
- end
+ result.dup
end
# If arel is locked this is a SELECT ... FOR UPDATE or somesuch. Such
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
index d18b9c991f..75501852ed 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
@@ -15,7 +15,6 @@ module ActiveRecord
return "'#{quote_string(value)}'" unless column
case column.type
- when :binary then "'#{quote_string(column.string_to_binary(value))}'"
when :integer then value.to_i.to_s
when :float then value.to_f.to_s
else
@@ -44,7 +43,9 @@ module ActiveRecord
# SQLite does not understand dates, so this method will convert a Date
# to a String.
def type_cast(value, column)
- return value.id if value.respond_to?(:quoted_id)
+ if value.respond_to?(:quoted_id) && value.respond_to?(:id)
+ return value.id
+ end
case value
when String, ActiveSupport::Multibyte::Chars
@@ -52,7 +53,6 @@ module ActiveRecord
return value unless column
case column.type
- when :binary then value
when :integer then value.to_i
when :float then value.to_f
else
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/savepoints.rb b/activerecord/lib/active_record/connection_adapters/abstract/savepoints.rb
new file mode 100644
index 0000000000..25c17ce971
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/abstract/savepoints.rb
@@ -0,0 +1,21 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module Savepoints #:nodoc:
+ def supports_savepoints?
+ true
+ end
+
+ def create_savepoint(name = current_savepoint_name)
+ execute("SAVEPOINT #{name}")
+ end
+
+ def rollback_to_savepoint(name = current_savepoint_name)
+ execute("ROLLBACK TO SAVEPOINT #{name}")
+ end
+
+ def release_savepoint(name = current_savepoint_name)
+ execute("RELEASE SAVEPOINT #{name}")
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb
new file mode 100644
index 0000000000..a51691bfa8
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb
@@ -0,0 +1,84 @@
+module ActiveRecord
+ module ConnectionAdapters
+ class AbstractAdapter
+ class SchemaCreation # :nodoc:
+ def initialize(conn)
+ @conn = conn
+ @cache = {}
+ end
+
+ def accept(o)
+ m = @cache[o.class] ||= "visit_#{o.class.name.split('::').last}"
+ send m, o
+ end
+
+ def visit_AddColumn(o)
+ sql_type = type_to_sql(o.type.to_sym, o.limit, o.precision, o.scale)
+ sql = "ADD #{quote_column_name(o.name)} #{sql_type}"
+ add_column_options!(sql, column_options(o))
+ end
+
+ private
+
+ def visit_AlterTable(o)
+ sql = "ALTER TABLE #{quote_table_name(o.name)} "
+ sql << o.adds.map { |col| visit_AddColumn col }.join(' ')
+ end
+
+ def visit_ColumnDefinition(o)
+ sql_type = type_to_sql(o.type.to_sym, o.limit, o.precision, o.scale)
+ column_sql = "#{quote_column_name(o.name)} #{sql_type}"
+ add_column_options!(column_sql, column_options(o)) unless o.primary_key?
+ column_sql
+ end
+
+ def visit_TableDefinition(o)
+ create_sql = "CREATE#{' TEMPORARY' if o.temporary} TABLE "
+ create_sql << "#{quote_table_name(o.name)} "
+ create_sql << "(#{o.columns.map { |c| accept c }.join(', ')}) " unless o.as
+ create_sql << "#{o.options}"
+ create_sql << " AS #{@conn.to_sql(o.as)}" if o.as
+ create_sql
+ end
+
+ def column_options(o)
+ column_options = {}
+ column_options[:null] = o.null unless o.null.nil?
+ column_options[:default] = o.default unless o.default.nil?
+ column_options[:column] = o
+ column_options[:first] = o.first
+ column_options[:after] = o.after
+ column_options
+ end
+
+ def quote_column_name(name)
+ @conn.quote_column_name name
+ end
+
+ def quote_table_name(name)
+ @conn.quote_table_name name
+ end
+
+ def type_to_sql(type, limit, precision, scale)
+ @conn.type_to_sql type.to_sym, limit, precision, scale
+ end
+
+ def add_column_options!(sql, options)
+ sql << " DEFAULT #{@conn.quote(options[:default], options[:column])}" if options_include_default?(options)
+ # must explicitly check for :null to allow change_column to work on migrations
+ if options[:null] == false
+ sql << " NOT NULL"
+ end
+ if options[:auto_increment] == true
+ sql << " AUTO_INCREMENT"
+ end
+ sql
+ end
+
+ def options_include_default?(options)
+ options.include?(:default) && !(options[:null] == false && options[:default].nil?)
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
index 566550cbe2..c39bf15e83 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
@@ -16,15 +16,15 @@ module ActiveRecord
# +columns+ attribute of said TableDefinition object, in order to be used
# for generating a number of table creation or table changing SQL statements.
class ColumnDefinition < Struct.new(:name, :type, :limit, :precision, :scale, :default, :null, :first, :after, :primary_key) #:nodoc:
- def string_to_binary(value)
- value
- end
def primary_key?
primary_key || type.to_sym == :primary_key
end
end
+ class ChangeColumnDefinition < Struct.new(:column, :type, :options) #:nodoc:
+ end
+
# Represents the schema of an SQL table in an abstract way. This class
# provides methods for manipulating the schema representation.
#
@@ -49,14 +49,15 @@ module ActiveRecord
# An array of ColumnDefinition objects, representing the column changes
# that have been defined.
attr_accessor :indexes
- attr_reader :name, :temporary, :options
+ attr_reader :name, :temporary, :options, :as
- def initialize(types, name, temporary, options)
+ def initialize(types, name, temporary, options, as = nil)
@columns_hash = {}
@indexes = {}
@native = types
@temporary = temporary
@options = options
+ @as = as
@name = name
end
@@ -65,8 +66,7 @@ module ActiveRecord
# Appends a primary key definition to the table definition.
# Can be called multiple times, but this is probably not a good idea.
def primary_key(name, type = :primary_key, options = {})
- options[:primary_key] = true
- column(name, type, options)
+ column(name, type, options.merge(:primary_key => true))
end
# Returns a ColumnDefinition for the column with name +name+.
@@ -270,6 +270,7 @@ module ActiveRecord
end
column.limit = limit
+ column.array = options[:array] if column.respond_to?(:array)
column.precision = options[:precision]
column.scale = options[:scale]
column.default = options[:default]
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 9c0c4e3ef0..00383bad3b 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
@@ -131,6 +131,9 @@ module ActiveRecord
# [<tt>:force</tt>]
# Set to true to drop the table before creating it.
# Defaults to false.
+ # [<tt>:as</tt>]
+ # SQL to use to generate the table. When this option is used, the block is
+ # ignored, as are the <tt>:id</tt> and <tt>:primary_key</tt> options.
#
# ====== Add a backend specific option to the generated SQL (MySQL)
#
@@ -169,19 +172,31 @@ module ActiveRecord
# supplier_id int
# )
#
+ # ====== Create a temporary table based on a query
+ #
+ # create_table(:long_query, temporary: true,
+ # as: "SELECT * FROM orders INNER JOIN line_items ON order_id=orders.id")
+ #
+ # generates:
+ #
+ # CREATE TEMPORARY TABLE long_query AS
+ # SELECT * FROM orders INNER JOIN line_items ON order_id=orders.id
+ #
# See also TableDefinition#column for details on how to create columns.
def create_table(table_name, options = {})
- td = create_table_definition table_name, options[:temporary], options[:options]
+ td = create_table_definition table_name, options[:temporary], options[:options], options[:as]
- unless options[:id] == false
- pk = options.fetch(:primary_key) {
- Base.get_primary_key table_name.to_s.singularize
- }
+ if !options[:as]
+ unless options[:id] == false
+ pk = options.fetch(:primary_key) {
+ Base.get_primary_key table_name.to_s.singularize
+ }
- td.primary_key pk, options.fetch(:id, :primary_key), options
- end
+ td.primary_key pk, options.fetch(:id, :primary_key), options
+ end
- yield td if block_given?
+ yield td if block_given?
+ end
if options[:force] && table_exists?(table_name)
drop_table(table_name, options)
@@ -214,8 +229,8 @@ module ActiveRecord
# its block form to do so yourself:
#
# create_join_table :products, :categories do |t|
- # t.index :products
- # t.index :categories
+ # t.index :product_id
+ # t.index :category_id
# end
#
# ====== Add a backend specific option to the generated SQL (MySQL)
@@ -558,8 +573,8 @@ module ActiveRecord
# this is a naive implementation; some DBs may support this more efficiently (Postgres, for instance)
old_index_def = indexes(table_name).detect { |i| i.name == old_name }
return unless old_index_def
- remove_index(table_name, :name => old_name)
- add_index(table_name, old_index_def.columns, :name => new_name, :unique => old_index_def.unique)
+ add_index(table_name, old_index_def.columns, name: new_name, unique: old_index_def.unique)
+ remove_index(table_name, name: old_name)
end
def index_name(table_name, options) #:nodoc:
@@ -606,7 +621,7 @@ module ActiveRecord
index_options = options.delete(:index)
add_column(table_name, "#{ref_name}_id", :integer, options)
add_column(table_name, "#{ref_name}_type", :string, polymorphic.is_a?(Hash) ? polymorphic : options) if polymorphic
- add_index(table_name, polymorphic ? %w[id type].map{ |t| "#{ref_name}_#{t}" } : "#{ref_name}_id", index_options.is_a?(Hash) ? index_options : nil) if index_options
+ add_index(table_name, polymorphic ? %w[id type].map{ |t| "#{ref_name}_#{t}" } : "#{ref_name}_id", index_options.is_a?(Hash) ? index_options : {}) if index_options
end
alias :add_belongs_to :add_reference
@@ -690,28 +705,17 @@ module ActiveRecord
column_type_sql
else
- type
+ type.to_s
end
end
- def add_column_options!(sql, options) #:nodoc:
- sql << " DEFAULT #{quote(options[:default], options[:column])}" if options_include_default?(options)
- # must explicitly check for :null to allow change_column to work on migrations
- if options[:null] == false
- sql << " NOT NULL"
- end
- if options[:auto_increment] == true
- sql << " AUTO_INCREMENT"
- end
- end
-
- # SELECT DISTINCT clause for a given set of columns and a given ORDER BY clause.
- # Both PostgreSQL and Oracle overrides this for custom DISTINCT syntax.
- #
- # distinct("posts.id", "posts.created_at desc")
+ # Given a set of columns and an ORDER BY clause, returns the columns for a SELECT DISTINCT.
+ # Both PostgreSQL and Oracle overrides this for custom DISTINCT syntax - they
+ # require the order columns appear in the SELECT.
#
- def distinct(columns, order_by)
- "DISTINCT #{columns}"
+ # columns_for_distinct("posts.id", ["posts.created_at desc"])
+ def columns_for_distinct(columns, orders) # :nodoc:
+ columns
end
# Adds timestamps (+created_at+ and +updated_at+) columns to the named table.
@@ -766,37 +770,23 @@ module ActiveRecord
column_names = Array(column_name)
index_name = index_name(table_name, column: column_names)
- if Hash === options # legacy support, since this param was a string
- options.assert_valid_keys(:unique, :order, :name, :where, :length, :internal, :using, :algorithm, :type)
-
- index_type = options[:unique] ? "UNIQUE" : ""
- index_type = options[:type].to_s if options.key?(:type)
- index_name = options[:name].to_s if options.key?(:name)
- max_index_length = options.fetch(:internal, false) ? index_name_length : allowed_index_name_length
+ options.assert_valid_keys(:unique, :order, :name, :where, :length, :internal, :using, :algorithm, :type)
- if options.key?(:algorithm)
- algorithm = index_algorithms.fetch(options[:algorithm]) {
- raise ArgumentError.new("Algorithm must be one of the following: #{index_algorithms.keys.map(&:inspect).join(', ')}")
- }
- end
-
- using = "USING #{options[:using]}" if options[:using].present?
+ index_type = options[:unique] ? "UNIQUE" : ""
+ index_type = options[:type].to_s if options.key?(:type)
+ index_name = options[:name].to_s if options.key?(:name)
+ max_index_length = options.fetch(:internal, false) ? index_name_length : allowed_index_name_length
- if supports_partial_index?
- index_options = options[:where] ? " WHERE #{options[:where]}" : ""
- end
- else
- if options
- message = "Passing a string as third argument of `add_index` is deprecated and will" +
- " be removed in Rails 4.1." +
- " Use add_index(#{table_name.inspect}, #{column_name.inspect}, unique: true) instead"
+ if options.key?(:algorithm)
+ algorithm = index_algorithms.fetch(options[:algorithm]) {
+ raise ArgumentError.new("Algorithm must be one of the following: #{index_algorithms.keys.map(&:inspect).join(', ')}")
+ }
+ end
- ActiveSupport::Deprecation.warn message
- end
+ using = "USING #{options[:using]}" if options[:using].present?
- index_type = options
- max_index_length = allowed_index_name_length
- algorithm = using = nil
+ if supports_partial_index?
+ index_options = options[:where] ? " WHERE #{options[:where]}" : ""
end
if index_name.length > max_index_length
@@ -828,12 +818,6 @@ module ActiveRecord
index_name
end
- def columns_for_remove(table_name, *column_names)
- ActiveSupport::Deprecation.warn("columns_for_remove is deprecated and will be removed in the future")
- raise ArgumentError.new("You must specify at least one column name. Example: remove_columns(:people, :first_name)") if column_names.blank?
- column_names.map {|column_name| quote_column_name(column_name) }
- end
-
def rename_table_indexes(table_name, new_name)
indexes(new_name).each do |index|
generated_index_name = index_name(table_name, column: index.columns)
@@ -857,8 +841,8 @@ module ActiveRecord
end
private
- def create_table_definition(name, temporary, options)
- TableDefinition.new native_database_types, name, temporary, options
+ def create_table_definition(name, temporary, options, as = nil)
+ TableDefinition.new native_database_types, name, temporary, options, as
end
def create_alter_table(name)
diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
index 1138b10a1b..8aa1ce5c04 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
@@ -4,6 +4,7 @@ require 'bigdecimal/util'
require 'active_support/core_ext/benchmark'
require 'active_record/connection_adapters/schema_cache'
require 'active_record/connection_adapters/abstract/schema_dumper'
+require 'active_record/connection_adapters/abstract/schema_creation'
require 'monitor'
module ActiveRecord
@@ -16,6 +17,7 @@ module ActiveRecord
autoload_at 'active_record/connection_adapters/abstract/schema_definitions' do
autoload :IndexDefinition
autoload :ColumnDefinition
+ autoload :ChangeColumnDefinition
autoload :TableDefinition
autoload :Table
autoload :AlterTable
@@ -33,12 +35,14 @@ module ActiveRecord
autoload :Quoting
autoload :ConnectionPool
autoload :QueryCache
+ autoload :Savepoints
end
autoload_at 'active_record/connection_adapters/abstract/transaction' do
autoload :ClosedTransaction
autoload :RealTransaction
autoload :SavepointTransaction
+ autoload :TransactionState
end
# Active Record supports multiple database systems. AbstractAdapter and
@@ -95,81 +99,15 @@ module ActiveRecord
@last_use = false
@logger = logger
@pool = pool
- @query_cache = Hash.new { |h,sql| h[sql] = {} }
- @query_cache_enabled = false
@schema_cache = SchemaCache.new self
@visitor = nil
+ @prepared_statements = false
end
def valid_type?(type)
true
end
- class SchemaCreation
- def initialize(conn)
- @conn = conn
- @cache = {}
- end
-
- def accept(o)
- m = @cache[o.class] ||= "visit_#{o.class.name.split('::').last}"
- send m, o
- end
-
- private
-
- def visit_AlterTable(o)
- sql = "ALTER TABLE #{quote_table_name(o.name)} "
- sql << o.adds.map { |col| visit_AddColumn col }.join(' ')
- end
-
- def visit_AddColumn(o)
- sql_type = type_to_sql(o.type.to_sym, o.limit, o.precision, o.scale)
- sql = "ADD #{quote_column_name(o.name)} #{sql_type}"
- add_column_options!(sql, column_options(o))
- end
-
- def visit_ColumnDefinition(o)
- sql_type = type_to_sql(o.type.to_sym, o.limit, o.precision, o.scale)
- column_sql = "#{quote_column_name(o.name)} #{sql_type}"
- add_column_options!(column_sql, column_options(o)) unless o.primary_key?
- column_sql
- end
-
- def visit_TableDefinition(o)
- create_sql = "CREATE#{' TEMPORARY' if o.temporary} TABLE "
- create_sql << "#{quote_table_name(o.name)} ("
- create_sql << o.columns.map { |c| accept c }.join(', ')
- create_sql << ") #{o.options}"
- create_sql
- end
-
- def column_options(o)
- column_options = {}
- column_options[:null] = o.null unless o.null.nil?
- column_options[:default] = o.default unless o.default.nil?
- column_options[:column] = o
- column_options
- end
-
- def quote_column_name(name)
- @conn.quote_column_name name
- end
-
- def quote_table_name(name)
- @conn.quote_table_name name
- end
-
- def type_to_sql(type, limit, precision, scale)
- @conn.type_to_sql type.to_sym, limit, precision, scale
- end
-
- def add_column_options!(column_sql, column_options)
- @conn.add_column_options! column_sql, column_options
- column_sql
- end
- end
-
def schema_creation
SchemaCreation.new self
end
@@ -197,10 +135,11 @@ module ActiveRecord
end
def unprepared_statement
- old, @visitor = @visitor, unprepared_visitor
+ old_prepared_statements, @prepared_statements = @prepared_statements, false
+ old_visitor, @visitor = @visitor, unprepared_visitor
yield
ensure
- @visitor = old
+ @visitor, @prepared_statements = old_visitor, old_prepared_statements
end
# Returns the human-readable name of the adapter. Use mixed case - one
@@ -280,6 +219,14 @@ module ActiveRecord
false
end
+ # This is meant to be implemented by the adapters that support extensions
+ def disable_extension(name)
+ end
+
+ # This is meant to be implemented by the adapters that support extensions
+ def enable_extension(name)
+ end
+
# A list of extensions, to be filled in by adapters that support them. At
# the moment only postgresql does.
def extensions
@@ -294,8 +241,8 @@ module ActiveRecord
# QUOTING ==================================================
- # Returns a bind substitution value given a +column+ and list of current
- # +binds+.
+ # Returns a bind substitution value given a bind +index+ and +column+
+ # NOTE: The column param is currently being used by the sqlserver-adapter
def substitute_at(column, index)
Arel::Nodes::BindParam.new '?'
end
@@ -374,27 +321,13 @@ module ActiveRecord
@transaction.number
end
- def increment_open_transactions
- ActiveSupport::Deprecation.warn "#increment_open_transactions is deprecated and has no effect"
- end
-
- def decrement_open_transactions
- ActiveSupport::Deprecation.warn "#decrement_open_transactions is deprecated and has no effect"
- end
-
- def transaction_joinable=(joinable)
- message = "#transaction_joinable= is deprecated. Please pass the :joinable option to #begin_transaction instead."
- ActiveSupport::Deprecation.warn message
- @transaction.joinable = joinable
+ def create_savepoint(name = nil)
end
- def create_savepoint
+ def rollback_to_savepoint(name = nil)
end
- def rollback_to_savepoint
- end
-
- def release_savepoint
+ def release_savepoint(name = nil)
end
def case_sensitive_modifier(node)
@@ -416,13 +349,14 @@ module ActiveRecord
protected
- def log(sql, name = "SQL", binds = [])
+ def log(sql, name = "SQL", binds = [], statement_name = nil)
@instrumenter.instrument(
"sql.active_record",
- :sql => sql,
- :name => name,
- :connection_id => object_id,
- :binds => binds) { yield }
+ :sql => sql,
+ :name => name,
+ :connection_id => object_id,
+ :statement_name => statement_name,
+ :binds => binds) { yield }
rescue => e
message = "#{e.class.name}: #{e.message}: #{sql}"
@logger.error message if @logger
@@ -433,7 +367,11 @@ module ActiveRecord
def translate_exception(exception, message)
# override in derived class
- ActiveRecord::StatementInvalid.new(message)
+ ActiveRecord::StatementInvalid.new(message, exception)
+ end
+
+ def without_prepared_statement?(binds)
+ !@prepared_statements || binds.empty?
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
index 94d9efe521..7768c24967 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
@@ -3,18 +3,29 @@ require 'arel/visitors/bind_visitor'
module ActiveRecord
module ConnectionAdapters
class AbstractMysqlAdapter < AbstractAdapter
+ include Savepoints
+
class SchemaCreation < AbstractAdapter::SchemaCreation
- private
def visit_AddColumn(o)
- add_column_position!(super, o)
+ add_column_position!(super, column_options(o))
end
- def add_column_position!(sql, column)
- if column.first
+ private
+ def visit_ChangeColumnDefinition(o)
+ column = o.column
+ options = o.options
+ sql_type = type_to_sql(o.type, options[:limit], options[:precision], options[:scale])
+ change_column_sql = "CHANGE #{quote_column_name(column.name)} #{quote_column_name(options[:name])} #{sql_type}"
+ add_column_options!(change_column_sql, options)
+ add_column_position!(change_column_sql, options)
+ end
+
+ def add_column_position!(sql, options)
+ if options[:first]
sql << " FIRST"
- elsif column.after
- sql << " AFTER #{quote_column_name(column.after)}"
+ elsif options[:after]
+ sql << " AFTER #{quote_column_name(options[:after])}"
end
sql
end
@@ -137,7 +148,7 @@ module ActiveRecord
QUOTED_TRUE, QUOTED_FALSE = '1', '0'
NATIVE_DATABASE_TYPES = {
- :primary_key => "int(11) DEFAULT NULL auto_increment PRIMARY KEY",
+ :primary_key => "int(11) auto_increment PRIMARY KEY",
:string => { :name => "varchar", :limit => 255 },
:text => { :name => "text" },
:integer => { :name => "int", :limit => 4 },
@@ -165,6 +176,7 @@ module ActiveRecord
@quoted_column_names, @quoted_table_names = {}, {}
if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true })
+ @prepared_statements = true
@visitor = Arel::Visitors::MySQL.new self
else
@visitor = unprepared_visitor
@@ -184,11 +196,6 @@ module ActiveRecord
true
end
- # Returns true, since this connection adapter supports savepoints.
- def supports_savepoints?
- true
- end
-
def supports_bulk_alter? #:nodoc:
true
end
@@ -199,6 +206,17 @@ module ActiveRecord
true
end
+ def type_cast(value, column)
+ case value
+ when TrueClass
+ 1
+ when FalseClass
+ 0
+ else
+ super
+ end
+ end
+
# MySQL 4 technically support transaction isolation, but it is affected by a bug
# where the transaction level gets persisted for the whole session:
#
@@ -237,8 +255,8 @@ module ActiveRecord
# QUOTING ==================================================
def quote(value, column = nil)
- if value.kind_of?(String) && column && column.type == :binary && column.class.respond_to?(:string_to_binary)
- s = column.class.string_to_binary(value).unpack("H*")[0]
+ if value.kind_of?(String) && column && column.type == :binary
+ s = value.unpack("H*")[0]
"x'#{s}'"
elsif value.kind_of?(BigDecimal)
value.to_s("F")
@@ -265,7 +283,7 @@ module ActiveRecord
# REFERENTIAL INTEGRITY ====================================
- def disable_referential_integrity(&block) #:nodoc:
+ def disable_referential_integrity #:nodoc:
old = select_value("SELECT @@FOREIGN_KEY_CHECKS")
begin
@@ -285,12 +303,6 @@ module ActiveRecord
else
log(sql, name) { @connection.query(sql) }
end
- rescue ActiveRecord::StatementInvalid => exception
- if exception.message.split(":").first =~ /Packets out of order/
- raise ActiveRecord::StatementInvalid, "'Packets out of order' error was received from the database. Please update your mysql bindings (gem install mysql) and read http://dev.mysql.com/doc/mysql/en/password-hashing.html for more information. If you're on Windows, use the Instant Rails installer to get the updated mysql bindings."
- else
- raise
- end
end
# MysqlAdapter has to free a result after using it, so we use this method to write
@@ -307,39 +319,19 @@ module ActiveRecord
def begin_db_transaction
execute "BEGIN"
- rescue
- # Transactions aren't supported
end
def begin_isolated_db_transaction(isolation)
execute "SET TRANSACTION ISOLATION LEVEL #{transaction_isolation_levels.fetch(isolation)}"
begin_db_transaction
- rescue
- # Transactions aren't supported
end
def commit_db_transaction #:nodoc:
execute "COMMIT"
- rescue
- # Transactions aren't supported
end
def rollback_db_transaction #:nodoc:
execute "ROLLBACK"
- rescue
- # Transactions aren't supported
- end
-
- def create_savepoint
- execute("SAVEPOINT #{current_savepoint_name}")
- end
-
- def rollback_to_savepoint
- execute("ROLLBACK TO SAVEPOINT #{current_savepoint_name}")
- end
-
- def release_savepoint
- execute("RELEASE SAVEPOINT #{current_savepoint_name}")
end
# In the simple case, MySQL allows us to place JOINs directly into the UPDATE
@@ -364,7 +356,9 @@ module ActiveRecord
# and creates it again using the provided +options+.
def recreate_database(name, options = {})
drop_database(name)
- create_database(name, options)
+ sql = create_database(name, options)
+ reconnect!
+ sql
end
# Create a new MySQL database with optional <tt>:charset</tt> and <tt>:collation</tt>.
@@ -458,7 +452,8 @@ module ActiveRecord
sql = "SHOW FULL FIELDS FROM #{quote_table_name(table_name)}"
execute_and_free(sql, 'SCHEMA') do |result|
each_hash(result).map do |field|
- new_column(field[:Field], field[:Default], field[:Type], field[:Null] == "YES", field[:Collation], field[:Extra])
+ field_name = set_field_encoding(field[:Field])
+ new_column(field_name, field[:Default], field[:Type], field[:Null] == "YES", field[:Collation], field[:Extra])
end
end
end
@@ -491,6 +486,18 @@ module ActiveRecord
rename_table_indexes(table_name, new_name)
end
+ def drop_table(table_name, options = {})
+ execute "DROP#{' TEMPORARY' if options[:temporary]} TABLE #{quote_table_name(table_name)}"
+ end
+
+ def rename_index(table_name, old_name, new_name)
+ if (version[0] == 5 && version[1] >= 7) || version[0] >= 6
+ execute "ALTER TABLE #{quote_table_name(table_name)} RENAME INDEX #{quote_table_name(old_name)} TO #{quote_table_name(new_name)}"
+ else
+ super
+ end
+ end
+
def change_column_default(table_name, column_name, default)
column = column_for(table_name, column_name)
change_column table_name, column_name, column.sql_type, :default => default
@@ -659,10 +666,9 @@ module ActiveRecord
end
def add_column_sql(table_name, column_name, type, options = {})
- add_column_sql = "ADD #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
- add_column_options!(add_column_sql, options)
- add_column_position!(add_column_sql, options)
- add_column_sql
+ td = create_table_definition table_name, options[:temporary], options[:options]
+ cd = td.new_column_definition(column_name, type, options)
+ schema_creation.visit_AddColumn cd
end
def change_column_sql(table_name, column_name, type, options = {})
@@ -676,14 +682,12 @@ module ActiveRecord
options[:null] = column.null
end
- change_column_sql = "CHANGE #{quote_column_name(column_name)} #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
- add_column_options!(change_column_sql, options)
- add_column_position!(change_column_sql, options)
- change_column_sql
+ options[:name] = column.name
+ schema_creation.accept ChangeColumnDefinition.new column, type, options
end
def rename_column_sql(table_name, column_name, new_column_name)
- options = {}
+ options = { name: new_column_name }
if column = columns(table_name).find { |c| c.name == column_name.to_s }
options[:default] = column.default
@@ -694,9 +698,7 @@ module ActiveRecord
end
current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'", 'SCHEMA')["Type"]
- rename_column_sql = "CHANGE #{quote_column_name(column_name)} #{quote_column_name(new_column_name)} #{current_type}"
- add_column_options!(rename_column_sql, options)
- rename_column_sql
+ schema_creation.accept ChangeColumnDefinition.new column, current_type, options
end
def remove_column_sql(table_name, column_name, type = nil, options = {})
diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb
index 609ccc2ed2..f2fbd5a8f2 100644
--- a/activerecord/lib/active_record/connection_adapters/column.rb
+++ b/activerecord/lib/active_record/connection_adapters/column.rb
@@ -13,7 +13,7 @@ module ActiveRecord
ISO_DATETIME = /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)(\.\d+)?\z/
end
- attr_reader :name, :default, :type, :limit, :null, :sql_type, :precision, :scale
+ attr_reader :name, :default, :type, :limit, :null, :sql_type, :precision, :scale, :default_function
attr_accessor :primary, :coder
alias :encoded? :coder
@@ -27,16 +27,17 @@ module ActiveRecord
# It will be mapped to one of the standard Rails SQL types in the <tt>type</tt> attribute.
# +null+ determines if this column allows +NULL+ values.
def initialize(name, default, sql_type = nil, null = true)
- @name = name
- @sql_type = sql_type
- @null = null
- @limit = extract_limit(sql_type)
- @precision = extract_precision(sql_type)
- @scale = extract_scale(sql_type)
- @type = simplified_type(sql_type)
- @default = extract_default(default)
- @primary = nil
- @coder = nil
+ @name = name
+ @sql_type = sql_type
+ @null = null
+ @limit = extract_limit(sql_type)
+ @precision = extract_precision(sql_type)
+ @scale = extract_scale(sql_type)
+ @type = simplified_type(sql_type)
+ @default = extract_default(default)
+ @default_function = nil
+ @primary = nil
+ @coder = nil
end
# Returns +true+ if the column is either of type string or text.
@@ -107,30 +108,6 @@ module ActiveRecord
end
end
- def type_cast_code(var_name)
- message = "Column#type_cast_code is deprecated in favor of using Column#type_cast only, " \
- "and it is going to be removed in future Rails versions."
- ActiveSupport::Deprecation.warn message
-
- klass = self.class.name
-
- case type
- when :string, :text then var_name
- when :integer then "#{klass}.value_to_integer(#{var_name})"
- when :float then "#{var_name}.to_f"
- when :decimal then "#{klass}.value_to_decimal(#{var_name})"
- when :datetime, :timestamp then "#{klass}.string_to_time(#{var_name})"
- when :time then "#{klass}.string_to_dummy_time(#{var_name})"
- when :date then "#{klass}.value_to_date(#{var_name})"
- when :binary then "#{klass}.binary_to_string(#{var_name})"
- when :boolean then "#{klass}.value_to_boolean(#{var_name})"
- when :hstore then "#{klass}.string_to_hstore(#{var_name})"
- when :inet, :cidr then "#{klass}.string_to_cidr(#{var_name})"
- when :json then "#{klass}.string_to_json(#{var_name})"
- else var_name
- end
- end
-
# Returns the human name of the column name.
#
# ===== Examples
@@ -143,17 +120,7 @@ module ActiveRecord
type_cast(default)
end
- # Used to convert from Strings to BLOBs
- def string_to_binary(value)
- self.class.string_to_binary(value)
- end
-
class << self
- # Used to convert from Strings to BLOBs
- def string_to_binary(value)
- value
- end
-
# Used to convert from BLOBs to Strings
def binary_to_string(value)
value
@@ -237,11 +204,19 @@ module ActiveRecord
end
end
- def new_time(year, mon, mday, hour, min, sec, microsec)
+ def new_time(year, mon, mday, hour, min, sec, microsec, offset = nil)
# Treat 0000-00-00 00:00:00 as nil.
return nil if year.nil? || (year == 0 && mon == 0 && mday == 0)
- Time.send(Base.default_timezone, year, mon, mday, hour, min, sec, microsec) rescue nil
+ if offset
+ time = Time.utc(year, mon, mday, hour, min, sec, microsec) rescue nil
+ return nil unless time
+
+ time -= offset
+ Base.default_timezone == :utc ? time : time.getlocal
+ else
+ Time.public_send(Base.default_timezone, year, mon, mday, hour, min, sec, microsec) rescue nil
+ end
end
def fast_string_to_date(string)
@@ -266,7 +241,7 @@ module ActiveRecord
time_hash = Date._parse(string)
time_hash[:sec_fraction] = microseconds(time_hash)
- new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction))
+ new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction, :offset))
end
end
@@ -306,7 +281,7 @@ module ActiveRecord
:text
when /blob/i, /binary/i
:binary
- when /char/i, /string/i
+ when /char/i
:string
when /boolean/i
:boolean
diff --git a/activerecord/lib/active_record/connection_adapters/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/connection_specification.rb
index 8bad7d0cf5..049768effc 100644
--- a/activerecord/lib/active_record/connection_adapters/connection_specification.rb
+++ b/activerecord/lib/active_record/connection_adapters/connection_specification.rb
@@ -13,41 +13,137 @@ module ActiveRecord
@config = original.config.dup
end
- ##
- # Builds a ConnectionSpecification from user input
- class Resolver # :nodoc:
- attr_reader :config, :klass, :configurations
-
- def initialize(config, configurations)
- @config = config
- @configurations = configurations
+ # Expands a connection string into a hash.
+ class ConnectionUrlResolver # :nodoc:
+
+ # == Example
+ #
+ # url = "postgresql://foo:bar@localhost:9000/foo_test?pool=5&timeout=3000"
+ # ConnectionUrlResolver.new(url).to_hash
+ # # => {
+ # "adapter" => "postgresql",
+ # "host" => "localhost",
+ # "port" => 9000,
+ # "database" => "foo_test",
+ # "username" => "foo",
+ # "password" => "bar",
+ # "pool" => "5",
+ # "timeout" => "3000"
+ # }
+ def initialize(url)
+ raise "Database URL cannot be empty" if url.blank?
+ @uri = URI.parse(url)
+ @adapter = @uri.scheme
+ @adapter = "postgresql" if @adapter == "postgres"
+ @query = @uri.query || ''
end
- def spec
- case config
- when nil
- raise AdapterNotSpecified unless defined?(Rails.env)
- resolve_string_connection Rails.env
- when Symbol, String
- resolve_string_connection config.to_s
- when Hash
- resolve_hash_connection config
- end
+ # Converts the given URL to a full connection hash.
+ def to_hash
+ config = raw_config.reject { |_,value| value.blank? }
+ config.map { |key,value| config[key] = uri_parser.unescape(value) if value.is_a? String }
+ config
end
private
- def resolve_string_connection(spec) # :nodoc:
- hash = configurations.fetch(spec) do |k|
- connection_url_to_hash(k)
+
+ def uri
+ @uri
+ end
+
+ def uri_parser
+ @uri_parser ||= URI::Parser.new
+ end
+
+ # Converts the query parameters of the URI into a hash.
+ #
+ # "localhost?pool=5&reap_frequency=2"
+ # # => { "pool" => "5", "reap_frequency" => "2" }
+ #
+ # returns empty hash if no query present.
+ #
+ # "localhost"
+ # # => {}
+ def query_hash
+ Hash[@query.split("&").map { |pair| pair.split("=") }]
+ end
+
+ def raw_config
+ query_hash.merge({
+ "adapter" => @adapter,
+ "username" => uri.user,
+ "password" => uri.password,
+ "port" => uri.port,
+ "database" => database,
+ "host" => uri.host })
+ end
+
+ # Returns name of the database.
+ # Sqlite3 expects this to be a full path or `:memory`.
+ def database
+ if @adapter == 'sqlite3'
+ if '/:memory:' == uri.path
+ ':memory:'
+ else
+ uri.path
+ end
+ else
+ uri.path.sub(%r{^/},"")
end
+ end
+ end
- raise(AdapterNotSpecified, "#{spec} database is not configured") unless hash
+ ##
+ # Builds a ConnectionSpecification from user input.
+ class Resolver # :nodoc:
+ attr_reader :configurations
- resolve_hash_connection hash
+ # Accepts a hash two layers deep, keys on the first layer represent
+ # environments such as "production". Keys must be strings.
+ def initialize(configurations)
+ @configurations = configurations
+ end
+
+ # Returns a hash with database connection information.
+ #
+ # == Examples
+ #
+ # Full hash Configuration.
+ #
+ # configurations = { "production" => { "host" => "localhost", "database" => "foo", "adapter" => "sqlite3" } }
+ # Resolver.new(configurations).resolve(:production)
+ # # => {host: "localhost", database: "foo", adapter: "sqlite3"}
+ #
+ # Initialized with URL configuration strings.
+ #
+ # configurations = { "production" => "postgresql://localhost/foo" }
+ # Resolver.new(configurations).resolve(:production)
+ # # => { "host" => "localhost", "database" => "foo", "adapter" => "postgresql" }
+ #
+ def resolve(config)
+ if config
+ resolve_connection config
+ elsif defined?(Rails.env)
+ resolve_env_connection Rails.env.to_sym
+ else
+ raise AdapterNotSpecified
+ end
end
- def resolve_hash_connection(spec) # :nodoc:
- spec = spec.symbolize_keys
+ # Returns an instance of ConnectionSpecification for a given adapter.
+ # Accepts a hash one layer deep that contains all connection information.
+ #
+ # == Example
+ #
+ # config = { "production" => { "host" => "localhost", "database" => "foo", "adapter" => "sqlite3" } }
+ # spec = Resolver.new(config).spec(:production)
+ # spec.adapter_method
+ # # => "sqlite3"
+ # spec.config
+ # # => { "host" => "localhost", "database" => "foo", "adapter" => "sqlite3" }
+ #
+ def spec(config)
+ spec = resolve(config).symbolize_keys
raise(AdapterNotSpecified, "database configuration does not specify adapter") unless spec.key?(:adapter)
@@ -55,41 +151,93 @@ module ActiveRecord
begin
require path_to_adapter
rescue Gem::LoadError => e
- raise Gem::LoadError, "Specified '#{spec[:adapter]}' for database adapter, but the gem is not loaded. Add `gem '#{e.name}'` to your Gemfile."
+ raise Gem::LoadError, "Specified '#{spec[:adapter]}' for database adapter, but the gem is not loaded. Add `gem '#{e.name}'` to your Gemfile (and ensure its version is at the minimum required by ActiveRecord)."
rescue LoadError => e
raise LoadError, "Could not load '#{path_to_adapter}'. Make sure that the adapter in config/database.yml is valid. If you use an adapter other than 'mysql', 'mysql2', 'postgresql' or 'sqlite3' add the necessary adapter gem to the Gemfile.", e.backtrace
end
adapter_method = "#{spec[:adapter]}_connection"
-
ConnectionSpecification.new(spec, adapter_method)
end
- def connection_url_to_hash(url) # :nodoc:
- config = URI.parse url
- adapter = config.scheme
- adapter = "postgresql" if adapter == "postgres"
- spec = { :adapter => adapter,
- :username => config.user,
- :password => config.password,
- :port => config.port,
- :database => config.path.sub(%r{^/},""),
- :host => config.host }
-
- spec.reject!{ |_,value| value.blank? }
-
- uri_parser = URI::Parser.new
-
- spec.map { |key,value| spec[key] = uri_parser.unescape(value) if value.is_a?(String) }
+ private
- if config.query
- options = Hash[config.query.split("&").map{ |pair| pair.split("=") }].symbolize_keys
+ # Returns fully resolved connection, accepts hash, string or symbol.
+ # Always returns a hash.
+ #
+ # == Examples
+ #
+ # Symbol representing current environment.
+ #
+ # Resolver.new("production" => {}).resolve_connection(:production)
+ # # => {}
+ #
+ # One layer deep hash of connection values.
+ #
+ # Resolver.new({}).resolve_connection("adapter" => "sqlite3")
+ # # => { "adapter" => "sqlite3" }
+ #
+ # Connection URL.
+ #
+ # Resolver.new({}).resolve_connection("postgresql://localhost/foo")
+ # # => { "host" => "localhost", "database" => "foo", "adapter" => "postgresql" }
+ #
+ def resolve_connection(spec)
+ case spec
+ when Symbol, String
+ resolve_env_connection spec
+ when Hash
+ resolve_hash_connection spec
+ end
+ end
- spec.merge!(options)
+ # Takes the environment such as `:production` or `:development`.
+ # This requires that the @configurations was initialized with a key that
+ # matches.
+ #
+ #
+ # Resolver.new("production" => {}).resolve_env_connection(:production)
+ # # => {}
+ #
+ # Takes a connection URL.
+ #
+ # Resolver.new({}).resolve_env_connection("postgresql://localhost/foo")
+ # # => { "host" => "localhost", "database" => "foo", "adapter" => "postgresql" }
+ #
+ def resolve_env_connection(spec)
+ # Rails has historically accepted a string to mean either
+ # an environment key or a URL spec, so we have deprecated
+ # this ambiguous behaviour and in the future this function
+ # can be removed in favor of resolve_string_connection and
+ # resolve_symbol_connection.
+ if config = configurations[spec.to_s]
+ if spec.is_a?(String)
+ ActiveSupport::Deprecation.warn "Passing a string to ActiveRecord::Base.establish_connection " \
+ "for a configuration lookup is deprecated, please pass a symbol (#{spec.to_sym.inspect}) instead"
+ end
+ resolve_connection(config)
+ elsif spec.is_a?(String)
+ resolve_string_connection(spec)
+ else
+ raise(AdapterNotSpecified, "#{spec} database is not configured")
end
+ end
+ # Accepts a hash. Expands the "url" key that contains a
+ # URL database connection to a full connection
+ # hash and merges with the rest of the hash.
+ # Connection details inside of the "url" key win any merge conflicts
+ def resolve_hash_connection(spec)
+ if url = spec.delete("url")
+ connection_hash = resolve_string_connection(url)
+ spec.merge!(connection_hash)
+ end
spec
end
+
+ def resolve_string_connection(url)
+ ConnectionUrlResolver.new(url).to_hash
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
index 530a27d099..6d8e994654 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.10'
+gem 'mysql2', '~> 0.3.13'
require 'mysql2'
module ActiveRecord
@@ -18,6 +18,12 @@ module ActiveRecord
client = Mysql2::Client.new(config)
options = [config[:host], config[:username], config[:password], config[:database], config[:port], config[:socket], 0]
ConnectionAdapters::Mysql2Adapter.new(client, logger, options, config)
+ rescue Mysql2::Error => error
+ if error.message.include?("Unknown database")
+ raise ActiveRecord::NoDatabaseError.new(error.message)
+ else
+ raise error
+ end
end
end
@@ -213,9 +219,11 @@ module ActiveRecord
# Executes the SQL statement in the context of this connection.
def execute(sql, name = nil)
- # make sure we carry over any changes to ActiveRecord::Base.default_timezone that have been
- # made since we established the connection
- @connection.query_options[:database_timezone] = ActiveRecord::Base.default_timezone
+ if @connection
+ # make sure we carry over any changes to ActiveRecord::Base.default_timezone that have been
+ # made since we established the connection
+ @connection.query_options[:database_timezone] = ActiveRecord::Base.default_timezone
+ end
super
end
@@ -227,8 +235,7 @@ module ActiveRecord
alias exec_without_stmt exec_query
- # Returns an array of record hashes with the column names as keys and
- # column values as values.
+ # Returns an ActiveRecord::Result instance.
def select(sql, name = nil, binds = [])
exec_query(sql, name)
end
@@ -268,6 +275,10 @@ module ActiveRecord
def version
@version ||= @connection.info[:version].scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map { |v| v.to_i }
end
+
+ def set_field_encoding field_name
+ field_name
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
index 09ba2e0d4a..7dbaa272a3 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
@@ -34,6 +34,12 @@ module ActiveRecord
default_flags |= Mysql::CLIENT_FOUND_ROWS if Mysql.const_defined?(:CLIENT_FOUND_ROWS)
options = [host, username, password, database, port, socket, default_flags]
ConnectionAdapters::MysqlAdapter.new(mysql, logger, options, config)
+ rescue Mysql::Error => error
+ if error.message.include?("Unknown database")
+ raise ActiveRecord::NoDatabaseError.new(error.message)
+ else
+ raise error
+ end
end
end
@@ -160,12 +166,6 @@ module ActiveRecord
# QUOTING ==================================================
- def type_cast(value, column)
- return super unless value == true || value == false
-
- value ? 1 : 0
- end
-
def quote_string(string) #:nodoc:
@connection.quote(string)
end
@@ -279,11 +279,7 @@ module ActiveRecord
end
def exec_query(sql, name = 'SQL', binds = [])
- # If the configuration sets prepared_statements:false, binds will
- # always be empty, since the bind variables will have been already
- # substituted and removed from binds by BindVisitor, so this will
- # effectively disable prepared statement usage completely.
- if binds.empty?
+ if without_prepared_statement?(binds)
result_set, affected_rows = exec_without_stmt(sql, name)
else
result_set, affected_rows = exec_stmt(sql, name, binds)
@@ -383,7 +379,7 @@ module ActiveRecord
TYPES = {}
- # Register an MySQL +type_id+ with a typcasting object in
+ # Register an MySQL +type_id+ with a typecasting object in
# +type+.
def self.register_type(type_id, type)
TYPES[type_id] = type
@@ -393,6 +389,14 @@ module ActiveRecord
TYPES[new] = TYPES[old]
end
+ def self.find_type(field)
+ if field.type == Mysql::Field::TYPE_TINY && field.length > 1
+ TYPES[Mysql::Field::TYPE_LONG]
+ else
+ TYPES.fetch(field.type) { Fields::Identity.new }
+ end
+ end
+
register_type Mysql::Field::TYPE_TINY, Fields::Boolean.new
register_type Mysql::Field::TYPE_LONG, Fields::Integer.new
alias_type Mysql::Field::TYPE_LONGLONG, Mysql::Field::TYPE_LONG
@@ -421,16 +425,19 @@ module ActiveRecord
if result
types = {}
+ fields = []
result.fetch_fields.each { |field|
+ field_name = field.name
+ fields << field_name
+
if field.decimals > 0
- types[field.name] = Fields::Decimal.new
+ types[field_name] = Fields::Decimal.new
else
- types[field.name] = Fields::TYPES.fetch(field.type) {
- Fields::Identity.new
- }
+ types[field_name] = Fields.find_type field
end
}
- result_set = ActiveRecord::Result.new(types.keys, result.to_a, types)
+
+ result_set = ActiveRecord::Result.new(fields, result.to_a, types)
result.free
else
result_set = ActiveRecord::Result.new([], [])
@@ -466,15 +473,17 @@ module ActiveRecord
def begin_db_transaction #:nodoc:
exec_query "BEGIN"
- rescue Mysql::Error
- # Transactions aren't supported
end
private
def exec_stmt(sql, name, binds)
cache = {}
- log(sql, name, binds) do
+ type_casted_binds = binds.map { |col, val|
+ [col, type_cast(val, col)]
+ }
+
+ log(sql, name, type_casted_binds) do
if binds.empty?
stmt = @connection.prepare(sql)
else
@@ -485,7 +494,7 @@ module ActiveRecord
end
begin
- stmt.execute(*binds.map { |col, val| type_cast(val, col) })
+ stmt.execute(*type_casted_binds.map { |_, val| val })
rescue Mysql::Error => e
# Older versions of MySQL leave the prepared statement in a bad
# place when an error occurs. To support older mysql versions, we
@@ -501,12 +510,12 @@ module ActiveRecord
cols = cache[:cols] ||= metadata.fetch_fields.map { |field|
field.name
}
+ metadata.free
end
result_set = ActiveRecord::Result.new(cols, stmt.to_a) if cols
affected_rows = stmt.affected_rows
- stmt.result_metadata.free if cols
stmt.free_result
stmt.close if binds.empty?
@@ -553,6 +562,14 @@ module ActiveRecord
def version
@version ||= @connection.server_info.scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map { |v| v.to_i }
end
+
+ def set_field_encoding field_name
+ field_name.force_encoding(client_encoding)
+ if internal_enc = Encoding.default_internal
+ field_name = field_name.encode!(internal_enc)
+ end
+ field_name
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb b/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb
index b7d24f2bb3..20de8d1982 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb
@@ -2,6 +2,13 @@ module ActiveRecord
module ConnectionAdapters
class PostgreSQLColumn < Column
module ArrayParser
+
+ DOUBLE_QUOTE = '"'
+ BACKSLASH = "\\"
+ COMMA = ','
+ BRACKET_OPEN = '{'
+ BRACKET_CLOSE = '}'
+
private
# Loads pg_array_parser if available. String parsing can be
# performed quicker by a native extension, which will not create
@@ -12,18 +19,18 @@ module ActiveRecord
include PgArrayParser
rescue LoadError
def parse_pg_array(string)
- parse_data(string, 0)
+ parse_data(string)
end
end
- def parse_data(string, index)
- local_index = index
+ def parse_data(string)
+ local_index = 0
array = []
while(local_index < string.length)
case string[local_index]
- when '{'
+ when BRACKET_OPEN
local_index,array = parse_array_contents(array, string, local_index + 1)
- when '}'
+ when BRACKET_CLOSE
return array
end
local_index += 1
@@ -33,9 +40,9 @@ module ActiveRecord
end
def parse_array_contents(array, string, index)
- is_escaping = false
- is_quoted = false
- was_quoted = false
+ is_escaping = false
+ is_quoted = false
+ was_quoted = false
current_item = ''
local_index = index
@@ -47,29 +54,29 @@ module ActiveRecord
else
if is_quoted
case token
- when '"'
+ when DOUBLE_QUOTE
is_quoted = false
was_quoted = true
- when "\\"
+ when BACKSLASH
is_escaping = true
else
current_item << token
end
else
case token
- when "\\"
+ when BACKSLASH
is_escaping = true
- when ','
+ when COMMA
add_item_to_array(array, current_item, was_quoted)
current_item = ''
was_quoted = false
- when '"'
+ when DOUBLE_QUOTE
is_quoted = true
- when '{'
+ when BRACKET_OPEN
internal_items = []
local_index,internal_items = parse_array_contents(internal_items, string, local_index + 1)
array.push(internal_items)
- when '}'
+ when BRACKET_CLOSE
add_item_to_array(array, current_item, was_quoted)
return local_index,array
else
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb b/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb
index 14ef07a75e..35ce881302 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb
@@ -17,8 +17,8 @@ module ActiveRecord
return string unless String === string
case string
- when 'infinity'; 1.0 / 0.0
- when '-infinity'; -1.0 / 0.0
+ when 'infinity'; Float::INFINITY
+ when '-infinity'; -Float::INFINITY
when / BC$/
super("-" + string.sub(/ BC$/, ""))
else
@@ -26,6 +26,15 @@ module ActiveRecord
end
end
+ def string_to_bit(value)
+ case value
+ when /^0x/i
+ value[2..-1].hex.to_s(2) # Hexadecimal notation
+ else
+ value # Bit-string notation
+ end
+ end
+
def hstore_to_string(object)
if Hash === object
object.map { |k,v|
@@ -51,14 +60,14 @@ module ActiveRecord
end
def json_to_string(object)
- if Hash === object
+ if Hash === object || Array === object
ActiveSupport::JSON.encode(object)
else
object
end
end
- def array_to_string(value, column, adapter, should_be_quoted = false)
+ def array_to_string(value, column, adapter)
casted_values = value.map do |val|
if String === val
if val == "NULL"
@@ -91,7 +100,11 @@ module ActiveRecord
if string.nil?
nil
elsif String === string
- IPAddr.new(string)
+ begin
+ IPAddr.new(string)
+ rescue ArgumentError
+ nil
+ end
else
string
end
@@ -106,7 +119,7 @@ module ActiveRecord
end
def string_to_array(string, oid)
- parse_pg_array(string).map{|val| oid.type_cast val}
+ parse_pg_array(string).map {|val| type_cast_array(oid, val)}
end
private
@@ -131,12 +144,20 @@ module ActiveRecord
def quote_and_escape(value)
case value
- when "NULL"
+ when "NULL", Numeric
value
else
"\"#{value.gsub(/"/,"\\\"")}\""
end
end
+
+ def type_cast_array(oid, value)
+ if ::Array === value
+ value.map {|item| type_cast_array(oid, item)}
+ else
+ oid.type_cast value
+ end
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb
index 9b5170f657..f349c37724 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb
@@ -134,34 +134,31 @@ module ActiveRecord
end
def exec_query(sql, name = 'SQL', binds = [])
- log(sql, name, binds) do
- result = binds.empty? ? exec_no_cache(sql, binds) :
- exec_cache(sql, binds)
-
- types = {}
- result.fields.each_with_index do |fname, i|
- ftype = result.ftype i
- fmod = result.fmod i
- types[fname] = OID::TYPE_MAP.fetch(ftype, fmod) { |oid, mod|
- warn "unknown OID: #{fname}(#{oid}) (#{sql})"
- OID::Identity.new
- }
- end
-
- ret = ActiveRecord::Result.new(result.fields, result.values, types)
- result.clear
- return ret
+ result = without_prepared_statement?(binds) ? exec_no_cache(sql, name, binds) :
+ exec_cache(sql, name, binds)
+
+ types = {}
+ fields = result.fields
+ fields.each_with_index do |fname, i|
+ ftype = result.ftype i
+ fmod = result.fmod i
+ types[fname] = type_map.fetch(ftype, fmod) { |oid, mod|
+ warn "unknown OID: #{fname}(#{oid}) (#{sql})"
+ OID::Identity.new
+ }
end
+
+ ret = ActiveRecord::Result.new(fields, result.values, types)
+ result.clear
+ return ret
end
def exec_delete(sql, name = 'SQL', binds = [])
- log(sql, name, binds) do
- result = binds.empty? ? exec_no_cache(sql, binds) :
- exec_cache(sql, binds)
- affected = result.cmd_tuples
- result.clear
- affected
- end
+ result = without_prepared_statement?(binds) ? exec_no_cache(sql, name, binds) :
+ exec_cache(sql, name, binds)
+ affected = result.cmd_tuples
+ result.clear
+ affected
end
alias :exec_update :exec_delete
@@ -217,25 +214,6 @@ module ActiveRecord
def rollback_db_transaction
execute "ROLLBACK"
end
-
- def outside_transaction?
- message = "#outside_transaction? is deprecated. This method was only really used " \
- "internally, but you can use #transaction_open? instead."
- ActiveSupport::Deprecation.warn message
- @connection.transaction_status == PGconn::PQTRANS_IDLE
- end
-
- def create_savepoint
- execute("SAVEPOINT #{current_savepoint_name}")
- end
-
- def rollback_to_savepoint
- execute("ROLLBACK TO SAVEPOINT #{current_savepoint_name}")
- end
-
- def release_savepoint
- execute("RELEASE SAVEPOINT #{current_savepoint_name}")
- end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb
index 51f377dfd7..fae260a921 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb
@@ -6,20 +6,27 @@ module ActiveRecord
module OID
class Type
def type; end
+ end
- def type_cast_for_write(value)
+ class Identity < Type
+ def type_cast(value)
value
end
end
- class Identity < Type
+ class Bit < Type
def type_cast(value)
- value
+ if String === value
+ ConnectionAdapters::PostgreSQLColumn.string_to_bit value
+ else
+ value
+ end
end
end
class Bytea < Type
def type_cast(value)
+ return if value.nil?
PGconn.unescape_bytea value
end
end
@@ -27,12 +34,17 @@ module ActiveRecord
class Money < Type
def type_cast(value)
return if value.nil?
+ return value unless String === value
# Because money output is formatted according to the locale, there are two
# cases to consider (note the decimal separators):
# (1) $12,345,678.12
# (2) $12.345.678,12
+ # Negative values are represented as follows:
+ # (3) -$2.55
+ # (4) ($2.55)
+ value.sub!(/^\((.+)\)$/, '-\1') # (4)
case value
when /^-?\D+[\d,]+\.\d{2}$/ # (1)
value.gsub!(/[^-\d.]/, '')
@@ -213,11 +225,19 @@ module ActiveRecord
end
class Hstore < Type
+ def type_cast_for_write(value)
+ ConnectionAdapters::PostgreSQLColumn.hstore_to_string value
+ end
+
def type_cast(value)
return if value.nil?
ConnectionAdapters::PostgreSQLColumn.string_to_hstore value
end
+
+ def accessor
+ ActiveRecord::Store::StringKeyedHashAccessor
+ end
end
class Cidr < Type
@@ -229,11 +249,19 @@ module ActiveRecord
end
class Json < Type
+ def type_cast_for_write(value)
+ ConnectionAdapters::PostgreSQLColumn.json_to_string value
+ end
+
def type_cast(value)
return if value.nil?
ConnectionAdapters::PostgreSQLColumn.string_to_json value
end
+
+ def accessor
+ ActiveRecord::Store::StringKeyedHashAccessor
+ end
end
class TypeMap
@@ -273,17 +301,15 @@ module ActiveRecord
end
end
- TYPE_MAP = TypeMap.new # :nodoc:
-
- # When the PG adapter connects, the pg_type table is queried. The
+ # When the PG adapter connects, the pg_type table is queried. The
# key of this hash maps to the `typname` column from the table.
- # TYPE_MAP is then dynamically built with oids as the key and type
+ # type_map is then dynamically built with oids as the key and type
# objects as values.
NAMES = Hash.new { |h,k| # :nodoc:
h[k] = OID::Identity.new
}
- # Register an OID type named +name+ with a typcasting object in
+ # Register an OID type named +name+ with a typecasting object in
# +type+. +name+ should correspond to the `typname` column in
# the `pg_type` table.
def self.register_type(name, type)
@@ -322,14 +348,14 @@ module ActiveRecord
# FIXME: why are we keeping these types as strings?
alias_type 'tsvector', 'text'
alias_type 'interval', 'text'
- alias_type 'bit', 'text'
- alias_type 'varbit', 'text'
alias_type 'macaddr', 'text'
alias_type 'uuid', 'text'
register_type 'money', OID::Money.new
register_type 'bytea', OID::Bytea.new
register_type 'bool', OID::Boolean.new
+ register_type 'bit', OID::Bit.new
+ register_type 'varbit', OID::Bit.new
register_type 'float4', OID::Float.new
alias_type 'float8', 'float4'
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
index 6329733abc..c1f978a081 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
@@ -30,9 +30,10 @@ module ActiveRecord
when Array
case sql_type
when 'point' then super(PostgreSQLColumn.point_to_string(value))
+ when 'json' then super(PostgreSQLColumn.json_to_string(value))
else
if column.array
- "'#{PostgreSQLColumn.array_to_string(value, column, self)}'"
+ "'#{PostgreSQLColumn.array_to_string(value, column, self).gsub(/'/, "''")}'"
else
super
end
@@ -85,8 +86,11 @@ module ActiveRecord
case value
when Range
- return super(value, column) unless /range$/ =~ column.sql_type
- PostgreSQLColumn.range_to_string(value)
+ if /range$/ =~ column.sql_type
+ PostgreSQLColumn.range_to_string(value)
+ else
+ super(value, column)
+ end
when NilClass
if column.array && array_member
'NULL'
@@ -98,13 +102,23 @@ module ActiveRecord
when Array
case column.sql_type
when 'point' then PostgreSQLColumn.point_to_string(value)
+ when 'json' then PostgreSQLColumn.json_to_string(value)
else
- return super(value, column) unless column.array
- PostgreSQLColumn.array_to_string(value, column, self)
+ if column.array
+ PostgreSQLColumn.array_to_string(value, column, self)
+ else
+ super(value, column)
+ end
end
when String
- return super(value, column) unless 'bytea' == column.sql_type
- { :value => value, :format => 1 }
+ if 'bytea' == column.sql_type
+ # Return a bind param hash with format as binary.
+ # See http://deveiate.org/code/pg/PGconn.html#method-i-exec_prepared-doc
+ # for more information
+ { value: value, format: 1 }
+ else
+ super(value, column)
+ end
when Hash
case column.sql_type
when 'hstore' then PostgreSQLColumn.hstore_to_string(value)
@@ -112,8 +126,11 @@ module ActiveRecord
else super(value, column)
end
when IPAddr
- return super(value, column) unless ['inet','cidr'].include? column.sql_type
- PostgreSQLColumn.cidr_to_string(value)
+ if %w(inet cidr).include? column.sql_type
+ PostgreSQLColumn.cidr_to_string(value)
+ else
+ super(value, column)
+ end
else
super(value, column)
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 d9b807bba4..571257f6dd 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
@@ -46,7 +46,7 @@ module ActiveRecord
end
# Create a new PostgreSQL database. Options include <tt>:owner</tt>, <tt>:template</tt>,
- # <tt>:encoding</tt>, <tt>:collation</tt>, <tt>:ctype</tt>,
+ # <tt>:encoding</tt> (defaults to utf8), <tt>:collation</tt>, <tt>:ctype</tt>,
# <tt>:tablespace</tt>, and <tt>:connection_limit</tt> (note that MySQL uses
# <tt>:charset</tt> while PostgreSQL uses <tt>:encoding</tt>).
#
@@ -172,7 +172,7 @@ module ActiveRecord
def columns(table_name)
# Limit, precision, and scale are all handled by the superclass.
column_definitions(table_name).map do |column_name, type, default, notnull, oid, fmod|
- oid = OID::TYPE_MAP.fetch(oid.to_i, fmod.to_i) {
+ oid = type_map.fetch(oid.to_i, fmod.to_i) {
OID::Identity.new
}
PostgreSQLColumn.new(column_name, default, oid, type, notnull == 'f')
@@ -321,6 +321,7 @@ module ActiveRecord
result = query(<<-end_sql, 'SCHEMA')[0]
SELECT attr.attname,
CASE
+ WHEN pg_get_expr(def.adbin, def.adrelid) !~* 'nextval' THEN NULL
WHEN split_part(pg_get_expr(def.adbin, def.adrelid), '''', 2) ~ '.' THEN
substr(split_part(pg_get_expr(def.adbin, def.adrelid), '''', 2),
strpos(split_part(pg_get_expr(def.adbin, def.adrelid), '''', 2), '.')+1)
@@ -332,7 +333,7 @@ module ActiveRecord
JOIN pg_constraint cons ON (conrelid = adrelid AND adnum = conkey[1])
WHERE t.oid = '#{quote_table_name(table)}'::regclass
AND cons.contype = 'p'
- AND pg_get_expr(def.adbin, def.adrelid) ~* 'nextval'
+ AND pg_get_expr(def.adbin, def.adrelid) ~* 'nextval|uuid_generate'
end_sql
end
@@ -383,8 +384,9 @@ module ActiveRecord
def change_column(table_name, column_name, type, options = {})
clear_cache!
quoted_table_name = quote_table_name(table_name)
-
- execute "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quote_column_name(column_name)} TYPE #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
+ sql_type = type_to_sql(type, options[:limit], options[:precision], options[:scale])
+ sql_type << "[]" if options[:array]
+ execute "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quote_column_name(column_name)} TYPE #{sql_type}"
change_column_default(table_name, column_name, options[:default]) if options_include_default?(options)
change_column_null(table_name, column_name, options[:null], options[:default]) if options.key?(:null)
@@ -466,22 +468,17 @@ module ActiveRecord
end
end
- # Returns a SELECT DISTINCT clause for a given set of columns and a given ORDER BY clause.
- #
# PostgreSQL requires the ORDER BY columns in the select list for distinct queries, and
# requires that the ORDER BY include the distinct column.
- #
- # distinct("posts.id", ["posts.created_at desc"])
- # # => "DISTINCT posts.id, posts.created_at AS alias_0"
- def distinct(columns, orders) #:nodoc:
- order_columns = orders.map{ |s|
+ def columns_for_distinct(columns, orders) #:nodoc:
+ order_columns = orders.reject(&:blank?).map{ |s|
# Convert Arel node to string
s = s.to_sql unless s.is_a?(String)
# Remove any ASC/DESC modifiers
s.gsub(/\s+(ASC|DESC)\s*(NULLS\s+(FIRST|LAST)\s*)?/i, '')
}.reject(&:blank?).map.with_index { |column, i| "#{column} AS alias_#{i}" }
- [super].concat(order_columns).join(', ')
+ [super, *order_columns].join(', ')
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
index bf403c3ae0..11a5eba464 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
@@ -20,8 +20,8 @@ module ActiveRecord
VALID_CONN_PARAMS = [:host, :hostaddr, :port, :dbname, :user, :password, :connect_timeout,
:client_encoding, :options, :application_name, :fallback_application_name,
:keepalives, :keepalives_idle, :keepalives_interval, :keepalives_count,
- :tty, :sslmode, :requiressl, :sslcert, :sslkey, :sslrootcert, :sslcrl,
- :requirepeer, :krbsrvname, :gsslib, :service]
+ :tty, :sslmode, :requiressl, :sslcompression, :sslcert, :sslkey,
+ :sslrootcert, :sslcrl, :requirepeer, :krbsrvname, :gsslib, :service]
# Establishes a connection to the database that's used by all Active Record objects
def postgresql_connection(config)
@@ -46,16 +46,28 @@ module ActiveRecord
# PostgreSQL-specific extensions to column definitions in a table.
class PostgreSQLColumn < Column #:nodoc:
attr_accessor :array
- # Instantiates a new PostgreSQL column definition in a table.
+
def initialize(name, default, oid_type, sql_type = nil, null = true)
@oid_type = oid_type
+ default_value = self.class.extract_value_from_default(default)
+
if sql_type =~ /\[\]$/
@array = true
- super(name, self.class.extract_value_from_default(default), sql_type[0..sql_type.length - 3], null)
+ super(name, default_value, sql_type[0..sql_type.length - 3], null)
else
@array = false
- super(name, self.class.extract_value_from_default(default), sql_type, null)
+ super(name, default_value, sql_type, null)
end
+
+ @default_function = default if has_default_function?(default_value, default)
+ end
+
+ def number?
+ !array && super
+ end
+
+ def text?
+ !array && super
end
# :stopdoc:
@@ -84,7 +96,7 @@ module ActiveRecord
$1
# Character types
when /\A\(?'(.*)'::.*\b(?:character varying|bpchar|text)\z/m
- $1
+ $1.gsub(/''/, "'")
# Binary data types
when /\A'(.*)'::bytea\z/m
$1
@@ -129,6 +141,14 @@ module ActiveRecord
end
end
+ def type_cast_for_write(value)
+ if @oid_type.respond_to?(:type_cast_for_write)
+ @oid_type.type_cast_for_write(value)
+ else
+ super
+ end
+ end
+
def type_cast(value)
return if value.nil?
return super if encoded?
@@ -136,8 +156,16 @@ module ActiveRecord
@oid_type.type_cast value
end
+ def accessor
+ @oid_type.accessor
+ end
+
private
+ def has_default_function?(default_value, default)
+ !default_value && (%r{\w+\(.*\)} === default)
+ end
+
def extract_limit(sql_type)
case sql_type
when /^bigint/i; 8
@@ -330,9 +358,37 @@ module ActiveRecord
class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition
include ColumnMethods
+ # Defines the primary key field.
+ # Use of the native PostgreSQL UUID type is supported, and can be used
+ # by defining your tables as such:
+ #
+ # create_table :stuffs, id: :uuid do |t|
+ # t.string :content
+ # t.timestamps
+ # end
+ #
+ # By default, this will use the +uuid_generate_v4()+ function from the
+ # +uuid-ossp+ extension, which MUST be enabled on your database. To enable
+ # the +uuid-ossp+ extension, you can use the +enable_extension+ method in your
+ # migrations. To use a UUID primary key without +uuid-ossp+ enabled, you can
+ # set the +:default+ option to +nil+:
+ #
+ # create_table :stuffs, id: false do |t|
+ # t.primary_key :id, :uuid, default: nil
+ # t.uuid :foo_id
+ # t.timestamps
+ # end
+ #
+ # You may also pass a different UUID generation function from +uuid-ossp+
+ # or another library.
+ #
+ # Note that setting the UUID primary key default value to +nil+ will
+ # require you to assure that you always provide a UUID value before saving
+ # a record (as primary keys cannot be +nil+). This might be done via the
+ # +SecureRandom.uuid+ method and a +before_save+ callback, for instance.
def primary_key(name, type = :primary_key, options = {})
return super unless type == :uuid
- options[:default] ||= 'uuid_generate_v4()'
+ options[:default] = options.fetch(:default, 'uuid_generate_v4()')
options[:primary_key] = true
column name, type, options
end
@@ -345,15 +401,11 @@ module ActiveRecord
self
end
- def xml(options = {})
- column(args[0], :text, options)
- end
-
private
- def create_column_definition(name, type)
- ColumnDefinition.new name, type
- end
+ def create_column_definition(name, type)
+ ColumnDefinition.new name, type
+ end
end
class Table < ActiveRecord::ConnectionAdapters::Table
@@ -396,6 +448,7 @@ module ActiveRecord
include ReferentialIntegrity
include SchemaStatements
include DatabaseStatements
+ include Savepoints
# Returns 'PostgreSQL' as adapter name for identification purposes.
def adapter_name
@@ -407,6 +460,7 @@ module ActiveRecord
def prepare_column_options(column, types)
spec = super
spec[:array] = 'true' if column.respond_to?(:array) && column.array
+ spec[:default] = "\"#{column.default_function}\"" if column.default_function
spec
end
@@ -499,6 +553,7 @@ module ActiveRecord
super(connection, logger)
if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true })
+ @prepared_statements = true
@visitor = Arel::Visitors::PostgreSQL.new self
else
@visitor = unprepared_visitor
@@ -518,7 +573,8 @@ module ActiveRecord
raise "Your version of PostgreSQL (#{postgresql_version}) is too old, please upgrade!"
end
- initialize_type_map
+ @type_map = OID::TypeMap.new
+ initialize_type_map(type_map)
@local_tz = execute('SHOW TIME ZONE', 'SCHEMA').first["TimeZone"]
@use_insert_returning = @config.key?(:insert_returning) ? self.class.type_cast_config_to_boolean(@config[:insert_returning]) : true
end
@@ -584,19 +640,13 @@ module ActiveRecord
true
end
- # Returns true, since this connection adapter supports savepoints.
- def supports_savepoints?
- true
- end
-
- # Returns true.
def supports_explain?
true
end
- # Returns true if pg > 9.2
+ # Returns true if pg > 9.1
def supports_extensions?
- postgresql_version >= 90200
+ postgresql_version >= 90100
end
# Range datatypes weren't introduced until PostgreSQL 9.2
@@ -618,9 +668,9 @@ module ActiveRecord
def extension_enabled?(name)
if supports_extensions?
- res = exec_query "SELECT EXISTS(SELECT * FROM pg_available_extensions WHERE name = '#{name}' AND installed_version IS NOT NULL)",
+ res = exec_query "SELECT EXISTS(SELECT * FROM pg_available_extensions WHERE name = '#{name}' AND installed_version IS NOT NULL) as enabled",
'SCHEMA'
- res.column_types['exists'].type_cast res.rows.first.first
+ res.column_types['enabled'].type_cast res.rows.first.first
end
end
@@ -697,65 +747,85 @@ module ActiveRecord
private
+ def type_map
+ @type_map
+ end
+
def reload_type_map
- OID::TYPE_MAP.clear
- initialize_type_map
+ type_map.clear
+ initialize_type_map(type_map)
end
- def initialize_type_map
+ def add_oid(row, records_by_oid, type_map)
+ return type_map if type_map.key? row['type_elem'].to_i
+
+ if OID.registered_type? row['typname']
+ # this composite type is explicitly registered
+ vector = OID::NAMES[row['typname']]
+ else
+ # use the default for composite types
+ unless type_map.key? row['typelem'].to_i
+ add_oid records_by_oid[row['typelem']], records_by_oid, type_map
+ end
+
+ vector = OID::Vector.new row['typdelim'], type_map[row['typelem'].to_i]
+ end
+
+ type_map[row['oid'].to_i] = vector
+ type_map
+ end
+
+ def initialize_type_map(type_map)
result = execute('SELECT oid, typname, typelem, typdelim, typinput FROM pg_type', 'SCHEMA')
leaves, nodes = result.partition { |row| row['typelem'] == '0' }
# populate the leaf nodes
leaves.find_all { |row| OID.registered_type? row['typname'] }.each do |row|
- OID::TYPE_MAP[row['oid'].to_i] = OID::NAMES[row['typname']]
+ type_map[row['oid'].to_i] = OID::NAMES[row['typname']]
end
+ records_by_oid = result.group_by { |row| row['oid'] }
+
arrays, nodes = nodes.partition { |row| row['typinput'] == 'array_in' }
# populate composite types
- nodes.find_all { |row| OID::TYPE_MAP.key? row['typelem'].to_i }.each do |row|
- if OID.registered_type? row['typname']
- # this composite type is explicitly registered
- vector = OID::NAMES[row['typname']]
- else
- # use the default for composite types
- vector = OID::Vector.new row['typdelim'], OID::TYPE_MAP[row['typelem'].to_i]
- end
-
- OID::TYPE_MAP[row['oid'].to_i] = vector
+ nodes.each do |row|
+ add_oid row, records_by_oid, type_map
end
# populate array types
- arrays.find_all { |row| OID::TYPE_MAP.key? row['typelem'].to_i }.each do |row|
- array = OID::Array.new OID::TYPE_MAP[row['typelem'].to_i]
- OID::TYPE_MAP[row['oid'].to_i] = array
+ arrays.find_all { |row| type_map.key? row['typelem'].to_i }.each do |row|
+ array = OID::Array.new type_map[row['typelem'].to_i]
+ type_map[row['oid'].to_i] = array
end
end
FEATURE_NOT_SUPPORTED = "0A000" # :nodoc:
- def exec_no_cache(sql, binds)
- @connection.async_exec(sql)
+ def exec_no_cache(sql, name, binds)
+ log(sql, name, binds) { @connection.async_exec(sql) }
end
- def exec_cache(sql, binds)
- stmt_key = prepare_statement sql
+ def exec_cache(sql, name, binds)
+ stmt_key = prepare_statement(sql)
+ type_casted_binds = binds.map { |col, val|
+ [col, type_cast(val, col)]
+ }
+
+ log(sql, name, type_casted_binds, stmt_key) do
+ @connection.send_query_prepared(stmt_key, type_casted_binds.map { |_, val| val })
+ @connection.block
+ @connection.get_last_result
+ end
+ rescue ActiveRecord::StatementInvalid => e
+ pgerror = e.original_exception
- # Clear the queue
- @connection.get_last_result
- @connection.send_query_prepared(stmt_key, binds.map { |col, val|
- type_cast(val, col)
- })
- @connection.block
- @connection.get_last_result
- rescue PGError => e
# Get the PG code for the failure. Annoyingly, the code for
# prepared statements whose return value may have changed is
# FEATURE_NOT_SUPPORTED. Check here for more details:
# http://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/utils/cache/plancache.c#l573
begin
- code = e.result.result_error_field(PGresult::PG_DIAG_SQLSTATE)
+ code = pgerror.result.result_error_field(PGresult::PG_DIAG_SQLSTATE)
rescue
raise e
end
@@ -780,6 +850,8 @@ module ActiveRecord
unless @statements.key? sql_key
nextkey = @statements.next_key
@connection.prepare nextkey, sql
+ # Clear the queue
+ @connection.get_last_result
@statements[sql_key] = nextkey
end
@statements[sql_key]
@@ -801,6 +873,12 @@ module ActiveRecord
PostgreSQLColumn.money_precision = (postgresql_version >= 80300) ? 19 : 10
configure_connection
+ rescue ::PG::Error => error
+ if error.message.include?("does not exist")
+ raise ActiveRecord::NoDatabaseError.new(error.message)
+ else
+ raise error
+ end
end
# Configures the encoding, verbosity, schema search path, and time zone of the connection.
@@ -905,12 +983,12 @@ module ActiveRecord
end
def extract_table_ref_from_insert_sql(sql)
- sql[/into\s+([^\(]*).*values\s*\(/i]
+ sql[/into\s+([^\(]*).*values\s*\(/im]
$1.strip if $1
end
- def create_table_definition(name, temporary, options)
- TableDefinition.new native_database_types, name, temporary, options
+ def create_table_definition(name, temporary, options, as = nil)
+ TableDefinition.new native_database_types, name, temporary, options, as
end
def update_table_definition(table_name, base)
diff --git a/activerecord/lib/active_record/connection_adapters/schema_cache.rb b/activerecord/lib/active_record/connection_adapters/schema_cache.rb
index 1d7a22e831..e5c9f6f54a 100644
--- a/activerecord/lib/active_record/connection_adapters/schema_cache.rb
+++ b/activerecord/lib/active_record/connection_adapters/schema_cache.rb
@@ -1,4 +1,3 @@
-require 'active_support/deprecation/reporting'
module ActiveRecord
module ConnectionAdapters
@@ -16,13 +15,8 @@ module ActiveRecord
prepare_default_proc
end
- def primary_keys(table_name = nil)
- if table_name
- @primary_keys[table_name]
- else
- ActiveSupport::Deprecation.warn('call primary_keys with a table name!')
- @primary_keys.dup
- end
+ def primary_keys(table_name)
+ @primary_keys[table_name]
end
# A cached lookup for table existence.
@@ -41,34 +35,19 @@ module ActiveRecord
end
end
- def tables(name = nil)
- if name
- @tables[name]
- else
- ActiveSupport::Deprecation.warn('call tables with a name!')
- @tables.dup
- end
+ def tables(name)
+ @tables[name]
end
# Get the columns for a table
- def columns(table = nil)
- if table
- @columns[table]
- else
- ActiveSupport::Deprecation.warn('call columns with a table name!')
- @columns.dup
- end
+ def columns(table)
+ @columns[table]
end
# Get the columns for a table as a hash, key is the column name
# value is the column object.
- def columns_hash(table = nil)
- if table
- @columns_hash[table]
- else
- ActiveSupport::Deprecation.warn('call columns_hash with a table name!')
- @columns_hash.dup
- end
+ def columns_hash(table)
+ @columns_hash[table]
end
# Clears out internal caches
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
index 7d940fe1c9..92bb70ba53 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
@@ -17,18 +17,26 @@ module ActiveRecord
# Allow database path relative to Rails.root, but only if
# the database path is not the special path that tells
# Sqlite to build a database only in memory.
- if defined?(Rails.root) && ':memory:' != config[:database]
- config[:database] = File.expand_path(config[:database], Rails.root)
+ if ':memory:' != config[:database]
+ config[:database] = File.expand_path(config[:database], Rails.root) if defined?(Rails.root)
+ dirname = File.dirname(config[:database])
+ Dir.mkdir(dirname) unless File.directory?(dirname)
end
db = SQLite3::Database.new(
- config[:database],
+ config[:database].to_s,
:results_as_hash => true
)
db.busy_timeout(ConnectionAdapters::SQLite3Adapter.type_cast_config_to_integer(config[:timeout])) if config[:timeout]
ConnectionAdapters::SQLite3Adapter.new(db, logger, config)
+ rescue Errno::ENOENT => error
+ if error.message.include?("No such file or directory")
+ raise ActiveRecord::NoDatabaseError.new(error.message)
+ else
+ raise error
+ end
end
end
@@ -51,6 +59,23 @@ module ActiveRecord
#
# * <tt>:database</tt> - Path to the database file.
class SQLite3Adapter < AbstractAdapter
+ include Savepoints
+
+ NATIVE_DATABASE_TYPES = {
+ primary_key: 'INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL',
+ string: { name: "varchar", limit: 255 },
+ text: { name: "text" },
+ integer: { name: "integer" },
+ float: { name: "float" },
+ decimal: { name: "decimal" },
+ datetime: { name: "datetime" },
+ timestamp: { name: "datetime" },
+ time: { name: "time" },
+ date: { name: "date" },
+ binary: { name: "blob" },
+ boolean: { name: "boolean" }
+ }
+
class Version
include Comparable
@@ -111,6 +136,7 @@ module ActiveRecord
@config = config
if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true })
+ @prepared_statements = true
@visitor = Arel::Visitors::SQLite.new self
else
@visitor = unprepared_visitor
@@ -121,14 +147,12 @@ module ActiveRecord
'SQLite'
end
- # Returns true
def supports_ddl_transactions?
true
end
- # Returns true if SQLite version is '3.6.8' or greater, false otherwise.
def supports_savepoints?
- sqlite_version >= '3.6.8'
+ true
end
# Returns true, since this connection adapter supports prepared statement
@@ -142,7 +166,6 @@ module ActiveRecord
true
end
- # Returns true.
def supports_primary_key? #:nodoc:
true
end
@@ -151,7 +174,6 @@ module ActiveRecord
true
end
- # Returns true
def supports_add_column?
true
end
@@ -173,16 +195,6 @@ module ActiveRecord
@statements.clear
end
- # Returns true
- def supports_count_distinct? #:nodoc:
- true
- end
-
- # Returns true
- def supports_autoincrement? #:nodoc:
- true
- end
-
def supports_index_sort_order?
true
end
@@ -195,20 +207,7 @@ module ActiveRecord
end
def native_database_types #:nodoc:
- {
- :primary_key => default_primary_key_type,
- :string => { :name => "varchar", :limit => 255 },
- :text => { :name => "text" },
- :integer => { :name => "integer" },
- :float => { :name => "float" },
- :decimal => { :name => "decimal" },
- :datetime => { :name => "datetime" },
- :timestamp => { :name => "datetime" },
- :time => { :name => "time" },
- :date => { :name => "date" },
- :binary => { :name => "blob" },
- :boolean => { :name => "boolean" }
- }
+ NATIVE_DATABASE_TYPES
end
# Returns the current database encoding format as a string, eg: 'UTF-8'
@@ -216,7 +215,6 @@ module ActiveRecord
@connection.encoding.to_s
end
- # Returns true.
def supports_explain?
true
end
@@ -224,8 +222,8 @@ module ActiveRecord
# QUOTING ==================================================
def quote(value, column = nil)
- if value.kind_of?(String) && column && column.type == :binary && column.class.respond_to?(:string_to_binary)
- s = column.class.string_to_binary(value).unpack("H*")[0]
+ if value.kind_of?(String) && column && column.type == :binary
+ s = value.unpack("H*")[0]
"x'#{s}'"
else
super
@@ -289,10 +287,13 @@ module ActiveRecord
end
def exec_query(sql, name = nil, binds = [])
- log(sql, name, binds) do
+ type_casted_binds = binds.map { |col, val|
+ [col, type_cast(val, col)]
+ }
- # Don't cache statements without bind values
- if binds.empty?
+ log(sql, name, type_casted_binds) do
+ # Don't cache statements if they are not prepared
+ if without_prepared_statement?(binds)
stmt = @connection.prepare(sql)
cols = stmt.columns
records = stmt.to_a
@@ -305,9 +306,7 @@ module ActiveRecord
stmt = cache[:stmt]
cols = cache[:cols] ||= stmt.columns
stmt.reset!
- stmt.bind_params binds.map { |col, val|
- type_cast(val, col)
- }
+ stmt.bind_params type_casted_binds.map { |_, val| val }
end
ActiveRecord::Result.new(cols, stmt.to_a)
@@ -348,18 +347,6 @@ module ActiveRecord
exec_query(sql, name).rows
end
- def create_savepoint
- execute("SAVEPOINT #{current_savepoint_name}")
- end
-
- def rollback_to_savepoint
- execute("ROLLBACK TO SAVEPOINT #{current_savepoint_name}")
- end
-
- def release_savepoint
- execute("RELEASE SAVEPOINT #{current_savepoint_name}")
- end
-
def begin_db_transaction #:nodoc:
log('begin transaction',nil) { @connection.transaction }
end
@@ -603,17 +590,13 @@ module ActiveRecord
@sqlite_version ||= SQLite3Adapter::Version.new(select_value('select sqlite_version(*)'))
end
- def default_primary_key_type
- if supports_autoincrement?
- 'INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL'
- else
- 'INTEGER PRIMARY KEY NOT NULL'
- end
- end
-
def translate_exception(exception, message)
case exception.message
- when /column(s)? .* (is|are) not unique/
+ # SQLite 3.8.2 returns a newly formatted error message:
+ # UNIQUE constraint failed: *table_name*.*column_name*
+ # Older versions of SQLite return:
+ # column *column_name* is not unique
+ when /column(s)? .* (is|are) not unique/, /UNIQUE constraint failed: .*/
RecordNotUnique.new(message, exception)
else
super
diff --git a/activerecord/lib/active_record/connection_handling.rb b/activerecord/lib/active_record/connection_handling.rb
index 1e03414c29..c4afadbd9b 100644
--- a/activerecord/lib/active_record/connection_handling.rb
+++ b/activerecord/lib/active_record/connection_handling.rb
@@ -32,11 +32,18 @@ module ActiveRecord
# "postgres://myuser:mypass@localhost/somedatabase"
# )
#
+ # In case <tt>ActiveRecord::Base.configurations</tt> is set (Rails
+ # automatically loads the contents of config/database.yml into it),
+ # a symbol can also be given as argument, representing a key in the
+ # configuration hash:
+ #
+ # ActiveRecord::Base.establish_connection(:production)
+ #
# The exceptions AdapterNotSpecified, AdapterNotFound and ArgumentError
# may be returned on an error.
def establish_connection(spec = ENV["DATABASE_URL"])
- resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new spec, configurations
- spec = resolver.spec
+ resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new configurations
+ spec = resolver.spec(spec)
unless respond_to?(spec.adapter_method)
raise AdapterNotFound, "database configuration specifies nonexistent #{spec.config[:adapter]} adapter"
@@ -54,11 +61,11 @@ module ActiveRecord
end
def connection_id
- Thread.current['ActiveRecord::Base.connection_id']
+ ActiveRecord::RuntimeRegistry.connection_id
end
def connection_id=(connection_id)
- Thread.current['ActiveRecord::Base.connection_id'] = connection_id
+ ActiveRecord::RuntimeRegistry.connection_id = connection_id
end
# Returns the configuration of the associated connection as a hash:
diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb
index afa1766615..18ee77f6fe 100644
--- a/activerecord/lib/active_record/core.rb
+++ b/activerecord/lib/active_record/core.rb
@@ -69,49 +69,37 @@ module ActiveRecord
mattr_accessor :timestamped_migrations, instance_writer: false
self.timestamped_migrations = true
- ##
- # :singleton-method:
- # Disable implicit join references. This feature was deprecated with Rails 4.
- # If you don't make use of implicit references but still see deprecation warnings
- # you can disable the feature entirely. This will be the default with Rails 4.1.
- mattr_accessor :disable_implicit_join_references, instance_writer: false
- self.disable_implicit_join_references = false
+ # :nodoc:
+ mattr_accessor :maintain_test_schema, instance_accessor: false
+
+ def self.disable_implicit_join_references=(value)
+ ActiveSupport::Deprecation.warn("Implicit join references were removed with Rails 4.1." \
+ "Make sure to remove this configuration because it does nothing.")
+ end
class_attribute :default_connection_handler, instance_writer: false
def self.connection_handler
- Thread.current[:active_record_connection_handler] || self.default_connection_handler
+ ActiveRecord::RuntimeRegistry.connection_handler || default_connection_handler
end
def self.connection_handler=(handler)
- Thread.current[:active_record_connection_handler] = handler
+ ActiveRecord::RuntimeRegistry.connection_handler = handler
end
self.default_connection_handler = ConnectionAdapters::ConnectionHandler.new
end
module ClassMethods
- def inherited(child_class) #:nodoc:
- child_class.initialize_generated_modules
- super
- end
-
def initialize_generated_modules
- @attribute_methods_mutex = Mutex.new
-
- # force attribute methods to be higher in inheritance hierarchy than other generated methods
- generated_attribute_methods.const_set(:AttrNames, Module.new {
- def self.const_missing(name)
- const_set(name, [name.to_s.sub(/ATTR_/, '')].pack('h*').freeze)
- end
- })
+ super
- generated_feature_methods
+ generated_association_methods
end
- def generated_feature_methods
- @generated_feature_methods ||= begin
- mod = const_set(:GeneratedFeatureMethods, Module.new)
+ def generated_association_methods
+ @generated_association_methods ||= begin
+ mod = const_set(:GeneratedAssociationMethods, Module.new)
include mod
mod
end
@@ -123,6 +111,8 @@ module ActiveRecord
super
elsif abstract_class?
"#{super}(abstract)"
+ elsif !connected?
+ "#{super} (call '#{super}.connection' to establish a connection)"
elsif table_exists?
attr_list = columns.map { |c| "#{c.name}: #{c.type}" } * ', '
"#{super}(#{attr_list})"
@@ -139,7 +129,7 @@ module ActiveRecord
# Returns an instance of <tt>Arel::Table</tt> loaded with the current table name.
#
# class Post < ActiveRecord::Base
- # scope :published_and_commented, published.and(self.arel_table[:comments_count].gt(0))
+ # scope :published_and_commented, -> { published.and(self.arel_table[:comments_count].gt(0)) }
# end
def arel_table
@arel_table ||= Arel::Table.new(table_name, arel_engine)
@@ -147,19 +137,18 @@ module ActiveRecord
# Returns the Arel engine.
def arel_engine
- @arel_engine ||= begin
+ @arel_engine ||=
if Base == self || connection_handler.retrieve_connection_pool(self)
self
else
superclass.arel_engine
end
- end
end
private
def relation #:nodoc:
- relation = Relation.new(self, arel_table)
+ relation = Relation.create(self, arel_table)
if finder_needs_type_condition?
relation.where(type_condition).create_with(inheritance_column.to_sym => sti_name)
@@ -177,19 +166,22 @@ module ActiveRecord
# ==== Example:
# # Instantiates a single new object
# User.new(first_name: 'Jamie')
- def initialize(attributes = nil)
+ def initialize(attributes = nil, options = {})
defaults = self.class.column_defaults.dup
defaults.each { |k, v| defaults[k] = v.dup if v.duplicable? }
@attributes = self.class.initialize_attributes(defaults)
- @columns_hash = self.class.column_types.dup
+ @column_types_override = nil
+ @column_types = self.class.column_types
init_internals
init_changed_attributes
ensure_proper_type
populate_with_current_scope_attributes
- assign_attributes(attributes) if attributes
+ # +options+ argument is only needed to make protected_attributes gem easier to hook.
+ # Remove it when we drop support to this gem.
+ init_attributes(attributes, options) if attributes
yield self if block_given?
run_callbacks :initialize unless _initialize_callbacks.empty?
@@ -207,7 +199,8 @@ module ActiveRecord
# post.title # => 'hello world'
def init_with(coder)
@attributes = self.class.initialize_attributes(coder['attributes'])
- @columns_hash = self.class.column_types.merge(coder['column_types'] || {})
+ @column_types_override = coder['column_types']
+ @column_types = self.class.column_types
init_internals
@@ -296,7 +289,7 @@ module ActiveRecord
def ==(comparison_object)
super ||
comparison_object.instance_of?(self.class) &&
- id.present? &&
+ id &&
comparison_object.id == id
end
alias :eql? :==
@@ -307,9 +300,11 @@ module ActiveRecord
id.hash
end
- # Freeze the attributes hash such that associations are still accessible, even on destroyed records.
+ # Clone and freeze the attributes hash such that associations are still
+ # accessible, even on destroyed records, but cloned models will not be
+ # frozen.
def freeze
- @attributes.freeze
+ @attributes = @attributes.clone.freeze
self
end
@@ -322,6 +317,8 @@ module ActiveRecord
def <=>(other_object)
if other_object.is_a?(self.class)
self.to_key <=> other_object.to_key
+ else
+ super
end
end
@@ -336,21 +333,15 @@ module ActiveRecord
@readonly = true
end
- # Returns the connection currently associated with the class. This can
- # also be used to "borrow" the connection to do database work that isn't
- # easily done without going straight to SQL.
- def connection
- ActiveSupport::Deprecation.warn("#connection is deprecated in favour of accessing it via the class")
- self.class.connection
- end
-
def connection_handler
self.class.connection_handler
end
# Returns the contents of the record as a nicely formatted string.
def inspect
- inspection = if @attributes
+ # We check defined?(@attributes) not to issue warnings if the object is
+ # allocated but not initialized.
+ inspection = if defined?(@attributes) && @attributes
self.class.column_names.collect { |name|
if has_attribute?(name)
"#{name}: #{attribute_for_inspect(name)}"
@@ -364,7 +355,7 @@ module ActiveRecord
# Returns a hash of the given methods with their names as keys and returned values as values.
def slice(*methods)
- Hash[methods.map { |method| [method, public_send(method)] }].with_indifferent_access
+ Hash[methods.map! { |method| [method, public_send(method)] }].with_indifferent_access
end
def set_transaction_state(state) # :nodoc:
@@ -434,8 +425,6 @@ module ActiveRecord
@aggregation_cache = {}
@association_cache = {}
@attributes_cache = {}
- @previously_changed = {}
- @changed_attributes = {}
@readonly = false
@destroyed = false
@marked_for_destruction = false
@@ -452,8 +441,14 @@ module ActiveRecord
# optimistic locking) won't get written unless they get marked as changed
self.class.columns.each do |c|
attr, orig_value = c.name, c.default
- @changed_attributes[attr] = orig_value if _field_changed?(attr, orig_value, @attributes[attr])
+ changed_attributes[attr] = orig_value if _field_changed?(attr, orig_value, @attributes[attr])
end
end
+
+ # This method is needed to make protected_attributes gem easier to hook.
+ # Remove it when we drop support to this gem.
+ def init_attributes(attributes, options)
+ assign_attributes(attributes)
+ end
end
end
diff --git a/activerecord/lib/active_record/counter_cache.rb b/activerecord/lib/active_record/counter_cache.rb
index 81cca37939..dcbdf75627 100644
--- a/activerecord/lib/active_record/counter_cache.rb
+++ b/activerecord/lib/active_record/counter_cache.rb
@@ -35,7 +35,7 @@ module ActiveRecord
stmt = unscoped.where(arel_table[primary_key].eq(object.id)).arel.compile_update({
arel_table[counter_name] => object.send(association).count
- })
+ }, primary_key)
connection.update stmt
end
return true
@@ -50,7 +50,7 @@ module ActiveRecord
# ==== Parameters
#
# * +id+ - The id of the object you wish to update a counter on or an Array of ids.
- # * +counters+ - An Array of Hashes containing the names of the fields
+ # * +counters+ - A Hash containing the names of the fields
# to update as keys and the amount to update the field by as values.
#
# ==== Examples
@@ -77,15 +77,15 @@ module ActiveRecord
"#{quoted_column} = COALESCE(#{quoted_column}, 0) #{operator} #{value.abs}"
end
- where(primary_key => id).update_all updates.join(', ')
+ unscoped.where(primary_key => id).update_all updates.join(', ')
end
# Increment a numeric field by one, via a direct SQL update.
#
- # This method is used primarily for maintaining counter_cache columns used to
- # store aggregate values. For example, a DiscussionBoard may cache posts_count
- # and comments_count to avoid running an SQL query to calculate the number of
- # posts and comments there are each time it is displayed.
+ # This method is used primarily for maintaining counter_cache columns that are
+ # used to store aggregate values. For example, a DiscussionBoard may cache
+ # posts_count and comments_count to avoid running an SQL query to calculate the
+ # number of posts and comments there are, each time it is displayed.
#
# ==== Parameters
#
diff --git a/activerecord/lib/active_record/dynamic_matchers.rb b/activerecord/lib/active_record/dynamic_matchers.rb
index 3bac31c6aa..5caab09038 100644
--- a/activerecord/lib/active_record/dynamic_matchers.rb
+++ b/activerecord/lib/active_record/dynamic_matchers.rb
@@ -35,7 +35,7 @@ module ActiveRecord
end
def pattern
- /^#{prefix}_([_a-zA-Z]\w*)#{suffix}$/
+ @pattern ||= /\A#{prefix}_([_a-zA-Z]\w*)#{suffix}\Z/
end
def prefix
@@ -84,13 +84,18 @@ module ActiveRecord
"#{finder}(#{attributes_hash})"
end
+ # The parameters in the signature may have reserved Ruby words, in order
+ # to prevent errors, we start each param name with `_`.
+ #
# Extended in activerecord-deprecated_finders
def signature
- attribute_names.join(', ')
+ attribute_names.map { |name| "_#{name}" }.join(', ')
end
+ # Given that the parameters starts with `_`, the finder needs to use the
+ # same parameter name.
def attributes_hash
- "{" + attribute_names.map { |name| ":#{name} => #{name}" }.join(',') + "}"
+ "{" + attribute_names.map { |name| ":#{name} => _#{name}" }.join(',') + "}"
end
def finder
diff --git a/activerecord/lib/active_record/enum.rb b/activerecord/lib/active_record/enum.rb
new file mode 100644
index 0000000000..c43b311223
--- /dev/null
+++ b/activerecord/lib/active_record/enum.rb
@@ -0,0 +1,116 @@
+module ActiveRecord
+ # Declare an enum attribute where the values map to integers in the database,
+ # but can be queried by name. Example:
+ #
+ # class Conversation < ActiveRecord::Base
+ # enum status: [ :active, :archived ]
+ # end
+ #
+ # # conversation.update! status: 0
+ # conversation.active!
+ # conversation.active? # => true
+ # conversation.status # => "active"
+ #
+ # # conversation.update! status: 1
+ # conversation.archived!
+ # conversation.archived? # => true
+ # conversation.status # => "archived"
+ #
+ # # conversation.update! status: 1
+ # conversation.status = "archived"
+ #
+ # # conversation.update! status: nil
+ # conversation.status = nil
+ # conversation.status.nil? # => true
+ # conversation.status # => nil
+ #
+ # Scopes based on the allowed values of the enum field will be provided
+ # as well. With the above example, it will create an +active+ and +archived+
+ # scope.
+ #
+ # You can set the default value from the database declaration, like:
+ #
+ # create_table :conversations do |t|
+ # t.column :status, :integer, default: 0
+ # end
+ #
+ # Good practice is to let the first declared status be the default.
+ #
+ # Finally, it's also possible to explicitly map the relation between attribute and
+ # database integer with a +Hash+:
+ #
+ # class Conversation < ActiveRecord::Base
+ # enum status: { active: 0, archived: 1 }
+ # end
+ #
+ # Note that when an +Array+ is used, the implicit mapping from the values to database
+ # integers is derived from the order the values appear in the array. In the example,
+ # <tt>:active</tt> is mapped to +0+ as it's the first element, and <tt>:archived</tt>
+ # is mapped to +1+. In general, the +i+-th element is mapped to <tt>i-1</tt> in the
+ # database.
+ #
+ # Therefore, once a value is added to the enum array, its position in the array must
+ # be maintained, and new values should only be added to the end of the array. To
+ # remove unused values, the explicit +Hash+ syntax should be used.
+ #
+ # In rare circumstances you might need to access the mapping directly.
+ # The mappings are exposed through a constant with the attributes name:
+ #
+ # Conversation::STATUS # => { "active" => 0, "archived" => 1 }
+ #
+ # Use that constant when you need to know the ordinal value of an enum:
+ #
+ # Conversation.where("status <> ?", Conversation::STATUS[:archived])
+ module Enum
+ def enum(definitions)
+ klass = self
+ definitions.each do |name, values|
+ # STATUS = { }
+ enum_values = _enum_methods_module.const_set name.to_s.upcase, ActiveSupport::HashWithIndifferentAccess.new
+ name = name.to_sym
+
+ _enum_methods_module.module_eval do
+ # def status=(value) self[:status] = STATUS[value] end
+ define_method("#{name}=") { |value|
+ if enum_values.has_key?(value) || value.blank?
+ self[name] = enum_values[value]
+ elsif enum_values.has_value?(value)
+ # Assigning a value directly is not a end-user fetaure, hence it's not documented.
+ # This is used internally to make building objects from the generated scopes work
+ # as expected, i.e. +Conversation.archived.build.archived?+ should be true.
+ self[name] = value
+ else
+ raise ArgumentError, "'#{value}' is not a valid #{name}"
+ end
+ }
+
+ # def status() STATUS.key self[:status] end
+ define_method(name) { enum_values.key self[name] }
+
+ pairs = values.respond_to?(:each_pair) ? values.each_pair : values.each_with_index
+ pairs.each do |value, i|
+ enum_values[value] = i
+
+ # scope :active, -> { where status: 0 }
+ klass.scope value, -> { klass.where name => i }
+
+ # def active?() status == 0 end
+ define_method("#{value}?") { self[name] == i }
+
+ # def active!() update! status: :active end
+ define_method("#{value}!") { update! name => value }
+ end
+ end
+ end
+ end
+
+ private
+ def _enum_methods_module
+ @_enum_methods_module ||= begin
+ mod = Module.new
+ include mod
+ mod
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb
index c615d59725..7f6228131f 100644
--- a/activerecord/lib/active_record/errors.rb
+++ b/activerecord/lib/active_record/errors.rb
@@ -57,26 +57,23 @@ module ActiveRecord
class RecordNotDestroyed < ActiveRecordError
end
- # Raised when SQL statement cannot be executed by the database (for example, it's often the case for
- # MySQL when Ruby driver used is too old).
+ # Superclass for all database execution errors.
+ #
+ # Wraps the underlying database error as +original_exception+.
class StatementInvalid < ActiveRecordError
- end
-
- # Raised when SQL statement is invalid and the application gets a blank result.
- class ThrowResult < ActiveRecordError
- end
-
- # Parent class for all specific exceptions which wrap database driver exceptions
- # provides access to the original exception also.
- class WrappedDatabaseException < StatementInvalid
attr_reader :original_exception
- def initialize(message, original_exception)
+ def initialize(message, original_exception = nil)
super(message)
@original_exception = original_exception
end
end
+ # Defunct wrapper class kept for compatibility.
+ # +StatementInvalid+ wraps the original exception now.
+ class WrappedDatabaseException < StatementInvalid
+ end
+
# Raised when a record cannot be inserted because it would violate a uniqueness constraint.
class RecordNotUnique < WrappedDatabaseException
end
@@ -97,6 +94,18 @@ module ActiveRecord
class PreparedStatementInvalid < ActiveRecordError
end
+ # Raised when a given database does not exist
+ class NoDatabaseError < ActiveRecordError
+ def initialize(message)
+ super extend_message(message)
+ end
+
+ # can be over written to add additional error information.
+ def extend_message(message)
+ message
+ end
+ end
+
# Raised on attempt to save stale record. Record is stale when it's being saved in another query after
# instantiation, for example, when two users edit the same wiki page and one starts editing and saves
# the page before the other.
@@ -158,6 +167,15 @@ module ActiveRecord
# Raised when unknown attributes are supplied via mass assignment.
class UnknownAttributeError < NoMethodError
+
+ attr_reader :record, :attribute
+
+ def initialize(record, attribute)
+ @record = record
+ @attribute = attribute.to_s
+ super("unknown attribute: #{attribute}")
+ end
+
end
# Raised when an error occurred while doing a mass assignment to an attribute through the
@@ -182,7 +200,7 @@ module ActiveRecord
end
end
- # Raised when a primary key is needed, but there is not one specified in the schema or model.
+ # Raised when a primary key is needed, but not specified in the schema or model.
class UnknownPrimaryKey < ActiveRecordError
attr_reader :model
diff --git a/activerecord/lib/active_record/explain.rb b/activerecord/lib/active_record/explain.rb
index 15736575a2..e65dab07ba 100644
--- a/activerecord/lib/active_record/explain.rb
+++ b/activerecord/lib/active_record/explain.rb
@@ -1,22 +1,22 @@
require 'active_support/lazy_load_hooks'
+require 'active_record/explain_registry'
module ActiveRecord
module Explain
- # Relation#explain needs to be able to collect the queries.
+ # Executes the block with the collect flag enabled. Queries are collected
+ # asynchronously by the subscriber and returned.
def collecting_queries_for_explain # :nodoc:
- current = Thread.current
- original, current[:available_queries_for_explain] = current[:available_queries_for_explain], []
+ ExplainRegistry.collect = true
yield
- return current[:available_queries_for_explain]
+ ExplainRegistry.queries
ensure
- # Note that the return value above does not depend on this assignment.
- current[:available_queries_for_explain] = original
+ ExplainRegistry.reset
end
# Makes the adapter execute EXPLAIN for the tuples of queries and bindings.
# Returns a formatted string ready to be logged.
def exec_explain(queries) # :nodoc:
- str = queries && queries.map do |sql, bind|
+ str = queries.map do |sql, bind|
[].tap do |msg|
msg << "EXPLAIN for: #{sql}"
unless bind.empty?
@@ -31,6 +31,7 @@ module ActiveRecord
def str.inspect
self
end
+
str
end
end
diff --git a/activerecord/lib/active_record/explain_registry.rb b/activerecord/lib/active_record/explain_registry.rb
new file mode 100644
index 0000000000..f5cd57e075
--- /dev/null
+++ b/activerecord/lib/active_record/explain_registry.rb
@@ -0,0 +1,30 @@
+require 'active_support/per_thread_registry'
+
+module ActiveRecord
+ # This is a thread locals registry for EXPLAIN. For example
+ #
+ # ActiveRecord::ExplainRegistry.queries
+ #
+ # returns the collected queries local to the current thread.
+ #
+ # See the documentation of <tt>ActiveSupport::PerThreadRegistry</tt>
+ # for further details.
+ class ExplainRegistry # :nodoc:
+ extend ActiveSupport::PerThreadRegistry
+
+ attr_accessor :queries, :collect
+
+ def initialize
+ reset
+ end
+
+ def collect?
+ @collect
+ end
+
+ def reset
+ @collect = false
+ @queries = []
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/explain_subscriber.rb b/activerecord/lib/active_record/explain_subscriber.rb
index 0f927496fb..6a49936644 100644
--- a/activerecord/lib/active_record/explain_subscriber.rb
+++ b/activerecord/lib/active_record/explain_subscriber.rb
@@ -1,4 +1,5 @@
require 'active_support/notifications'
+require 'active_record/explain_registry'
module ActiveRecord
class ExplainSubscriber # :nodoc:
@@ -7,8 +8,8 @@ module ActiveRecord
end
def finish(name, id, payload)
- if queries = Thread.current[:available_queries_for_explain]
- queries << payload.values_at(:sql, :binds) unless ignore_payload?(payload)
+ if ExplainRegistry.collect? && !ignore_payload?(payload)
+ ExplainRegistry.queries << payload.values_at(:sql, :binds)
end
end
@@ -18,7 +19,7 @@ module ActiveRecord
# On the other hand, we want to monitor the performance of our real database
# queries, not the performance of the access to the query cache.
IGNORED_PAYLOADS = %w(SCHEMA EXPLAIN CACHE)
- EXPLAINED_SQLS = /\A\s*(select|update|delete|insert)/i
+ EXPLAINED_SQLS = /\A\s*(select|update|delete|insert)\b/i
def ignore_payload?(payload)
payload[:exception] || IGNORED_PAYLOADS.include?(payload[:name]) || payload[:sql] !~ EXPLAINED_SQLS
end
diff --git a/activerecord/lib/active_record/fixture_set/file.rb b/activerecord/lib/active_record/fixture_set/file.rb
index fbd7a4d891..8132310c91 100644
--- a/activerecord/lib/active_record/fixture_set/file.rb
+++ b/activerecord/lib/active_record/fixture_set/file.rb
@@ -38,7 +38,8 @@ module ActiveRecord
end
def render(content)
- ERB.new(content).result
+ context = ActiveRecord::FixtureSet::RenderContext.create_subclass.new
+ ERB.new(content).result(context.get_binding)
end
# Validate our unmarshalled data.
diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb
index 45dc26f0ed..a7a54483bc 100644
--- a/activerecord/lib/active_record/fixtures.rb
+++ b/activerecord/lib/active_record/fixtures.rb
@@ -119,6 +119,23 @@ module ActiveRecord
# perhaps you should reexamine whether your application is properly testable. Hence, dynamic values
# in fixtures are to be considered a code smell.
#
+ # Helper methods defined in a fixture will not be available in other fixtures, to prevent against
+ # unwanted inter-test dependencies. Methods used by multiple fixtures should be defined in a module
+ # that is included in <tt>ActiveRecord::FixtureSet.context_class</tt>.
+ #
+ # - define a helper method in `test_helper.rb`
+ # class FixtureFileHelpers
+ # def file_sha(path)
+ # Digest::SHA2.hexdigest(File.read(Rails.root.join('test/fixtures', path)))
+ # end
+ # end
+ # ActiveRecord::FixtureSet.context_class.send :include, FixtureFileHelpers
+ #
+ # - use the helper method in a fixture
+ # photo:
+ # name: kitten.png
+ # sha: <%= file_sha 'files/kitten.png' %>
+ #
# = Transactional Fixtures
#
# Test cases can use begin+rollback to isolate their changes to the database instead of having to
@@ -379,22 +396,16 @@ module ActiveRecord
@@all_cached_fixtures = Hash.new { |h,k| h[k] = {} }
- def self.find_table_name(fixture_set_name) # :nodoc:
- ActiveSupport::Deprecation.warn(
- "ActiveRecord::Fixtures.find_table_name is deprecated and shall be removed from future releases. Use ActiveRecord::Fixtures.default_fixture_model_name instead.")
- default_fixture_model_name(fixture_set_name)
- end
-
- def self.default_fixture_model_name(fixture_set_name) # :nodoc:
- ActiveRecord::Base.pluralize_table_names ?
+ def self.default_fixture_model_name(fixture_set_name, config = ActiveRecord::Base) # :nodoc:
+ config.pluralize_table_names ?
fixture_set_name.singularize.camelize :
fixture_set_name.camelize
end
- def self.default_fixture_table_name(fixture_set_name) # :nodoc:
- "#{ ActiveRecord::Base.table_name_prefix }"\
+ def self.default_fixture_table_name(fixture_set_name, config = ActiveRecord::Base) # :nodoc:
+ "#{ config.table_name_prefix }"\
"#{ fixture_set_name.tr('/', '_') }"\
- "#{ ActiveRecord::Base.table_name_suffix }".to_sym
+ "#{ config.table_name_suffix }".to_sym
end
def self.reset_cache
@@ -442,9 +453,47 @@ module ActiveRecord
cattr_accessor :all_loaded_fixtures
self.all_loaded_fixtures = {}
- def self.create_fixtures(fixtures_directory, fixture_set_names, class_names = {})
+ class ClassCache
+ def initialize(class_names, config)
+ @class_names = class_names.stringify_keys
+ @config = config
+
+ # Remove string values that aren't constants or subclasses of AR
+ @class_names.delete_if { |k,klass|
+ unless klass.is_a? Class
+ klass = klass.safe_constantize
+ ActiveSupport::Deprecation.warn("The ability to pass in strings as a class name will be removed in Rails 4.2, consider using the class itself instead.")
+ end
+ !insert_class(@class_names, k, klass)
+ }
+ end
+
+ def [](fs_name)
+ @class_names.fetch(fs_name) {
+ klass = default_fixture_model(fs_name, @config).safe_constantize
+ insert_class(@class_names, fs_name, klass)
+ }
+ end
+
+ private
+
+ def insert_class(class_names, name, klass)
+ # We only want to deal with AR objects.
+ if klass && klass < ActiveRecord::Base
+ class_names[name] = klass
+ else
+ class_names[name] = nil
+ end
+ end
+
+ def default_fixture_model(fs_name, config)
+ ActiveRecord::FixtureSet.default_fixture_model_name(fs_name, config)
+ end
+ end
+
+ def self.create_fixtures(fixtures_directory, fixture_set_names, class_names = {}, config = ActiveRecord::Base)
fixture_set_names = Array(fixture_set_names).map(&:to_s)
- class_names = class_names.stringify_keys
+ class_names = ClassCache.new class_names, config
# FIXME: Apparently JK uses this.
connection = block_given? ? yield : ActiveRecord::Base.connection
@@ -458,10 +507,12 @@ module ActiveRecord
fixtures_map = {}
fixture_sets = files_to_read.map do |fs_name|
+ klass = class_names[fs_name]
+ conn = klass ? klass.connection : connection
fixtures_map[fs_name] = new( # ActiveRecord::FixtureSet.new
- connection,
+ conn,
fs_name,
- class_names[fs_name] || default_fixture_model_name(fs_name),
+ klass,
::File.join(fixtures_directory, fs_name))
end
@@ -470,16 +521,8 @@ module ActiveRecord
connection.transaction(:requires_new => true) do
fixture_sets.each do |fs|
conn = fs.model_class.respond_to?(:connection) ? fs.model_class.connection : connection
- table_rows = fs.table_rows
-
- table_rows.keys.each do |table|
- conn.delete "DELETE FROM #{conn.quote_table_name(table)}", 'Fixture Delete'
- end
-
- table_rows.each do |fixture_set_name, rows|
- rows.each do |row|
- conn.insert_fixture(row, fixture_set_name)
- end
+ fs.fixture_sql(conn).each do |stmt|
+ conn.execute stmt
end
end
@@ -503,27 +546,36 @@ module ActiveRecord
Zlib.crc32(label.to_s) % MAX_ID
end
- attr_reader :table_name, :name, :fixtures, :model_class
+ # Superclass for the evaluation contexts used by ERB fixtures.
+ def self.context_class
+ @context_class ||= Class.new
+ end
+
+ attr_reader :table_name, :name, :fixtures, :model_class, :config
- def initialize(connection, name, class_name, path)
- @fixtures = {} # Ordered hash
+ def initialize(connection, name, class_name, path, config = ActiveRecord::Base)
@name = name
@path = path
+ @config = config
+ @model_class = nil
+
+ if class_name.is_a?(String)
+ ActiveSupport::Deprecation.warn("The ability to pass in strings as a class name will be removed in Rails 4.2, consider using the class itself instead.")
+ end
if class_name.is_a?(Class) # TODO: Should be an AR::Base type class, or any?
@model_class = class_name
else
- @model_class = class_name.constantize rescue nil
+ @model_class = class_name.safe_constantize if class_name
end
- @connection = ( model_class.respond_to?(:connection) ?
- model_class.connection : connection )
+ @connection = connection
@table_name = ( model_class.respond_to?(:table_name) ?
model_class.table_name :
- self.class.default_fixture_table_name(name) )
+ self.class.default_fixture_table_name(name, config) )
- read_fixture_files
+ @fixtures = read_fixture_files path, @model_class
end
def [](x)
@@ -542,10 +594,20 @@ module ActiveRecord
fixtures.size
end
- # Return a hash of rows to be inserted. The key is the table, the value is
+ def fixture_sql(conn)
+ table_rows = self.table_rows
+
+ table_rows.keys.map { |table|
+ "DELETE FROM #{conn.quote_table_name(table)}"
+ }.concat table_rows.flat_map { |fixture_set_name, rows|
+ rows.map { |row| conn.fixture_sql(row, fixture_set_name) }
+ }
+ end
+
+ # Returns a hash of rows to be inserted. The key is the table, the value is
# a list of rows to insert to that table.
def table_rows
- now = ActiveRecord::Base.default_timezone == :utc ? Time.now.utc : Time.now
+ now = config.default_timezone == :utc ? Time.now.utc : Time.now
now = now.to_s(:db)
# allow a standard key to be used for doing defaults in YAML
@@ -557,7 +619,7 @@ module ActiveRecord
rows[table_name] = fixtures.map do |label, fixture|
row = fixture.to_hash
- if model_class && model_class < ActiveRecord::Base
+ if model_class
# fill in timestamp columns if they aren't specified and the model is set to record_timestamps
if model_class.record_timestamps
timestamp_column_names.each do |c_name|
@@ -567,7 +629,7 @@ module ActiveRecord
# interpolate the fixture label
row.each do |key, value|
- row[key] = label if value == "$LABEL"
+ row[key] = label if "$LABEL" == value
end
# generate a primary key if necessary
@@ -597,14 +659,9 @@ module ActiveRecord
row[fk_name] = ActiveRecord::FixtureSet.identify(value)
end
- when :has_and_belongs_to_many
- if (targets = row.delete(association.name.to_s))
- targets = targets.is_a?(Array) ? targets : targets.split(/\s*,\s*/)
- table_name = association.join_table
- rows[table_name].concat targets.map { |target|
- { association.foreign_key => row[primary_key_name],
- association.association_foreign_key => ActiveRecord::FixtureSet.identify(target) }
- }
+ when :has_many
+ if association.options[:through]
+ add_join_records(rows, row, HasManyThroughProxy.new(association))
end
end
end
@@ -615,11 +672,50 @@ module ActiveRecord
rows
end
+ class ReflectionProxy # :nodoc:
+ def initialize(association)
+ @association = association
+ end
+
+ def join_table
+ @association.join_table
+ end
+
+ def name
+ @association.name
+ end
+ end
+
+ class HasManyThroughProxy < ReflectionProxy # :nodoc:
+ def rhs_key
+ @association.foreign_key
+ end
+
+ def lhs_key
+ @association.through_reflection.foreign_key
+ end
+ end
+
private
def primary_key_name
@primary_key_name ||= model_class && model_class.primary_key
end
+ def add_join_records(rows, row, association)
+ # This is the case when the join table has no fixtures file
+ if (targets = row.delete(association.name.to_s))
+ table_name = association.join_table
+ lhs_key = association.lhs_key
+ rhs_key = association.rhs_key
+
+ targets = targets.is_a?(Array) ? targets : targets.split(/\s*,\s*/)
+ rows[table_name].concat targets.map { |target|
+ { lhs_key => row[primary_key_name],
+ rhs_key => ActiveRecord::FixtureSet.identify(target) }
+ }
+ end
+ end
+
def has_primary_key_column?
@has_primary_key_column ||= primary_key_name &&
model_class.columns.any? { |c| c.name == primary_key_name }
@@ -638,12 +734,12 @@ module ActiveRecord
@column_names ||= @connection.columns(@table_name).collect { |c| c.name }
end
- def read_fixture_files
- yaml_files = Dir["#{@path}/**/*.yml"].select { |f|
+ def read_fixture_files(path, model_class)
+ yaml_files = Dir["#{path}/{**,*}/*.yml"].select { |f|
::File.file?(f)
- } + [yaml_file_path]
+ } + [yaml_file_path(path)]
- yaml_files.each do |file|
+ yaml_files.each_with_object({}) do |file, fixtures|
FixtureSet::File.open(file) do |fh|
fh.each do |fixture_name, row|
fixtures[fixture_name] = ActiveRecord::Fixture.new(row, model_class)
@@ -652,8 +748,8 @@ module ActiveRecord
end
end
- def yaml_file_path
- "#{@path}.yml"
+ def yaml_file_path(path)
+ "#{path}.yml"
end
end
@@ -725,14 +821,16 @@ module ActiveRecord
class_attribute :use_transactional_fixtures
class_attribute :use_instantiated_fixtures # true, false, or :no_instances
class_attribute :pre_loaded_fixtures
+ class_attribute :config
self.fixture_table_names = []
self.use_transactional_fixtures = true
self.use_instantiated_fixtures = false
self.pre_loaded_fixtures = false
+ self.config = ActiveRecord::Base
self.fixture_class_names = Hash.new do |h, fixture_set_name|
- h[fixture_set_name] = ActiveRecord::FixtureSet.default_fixture_model_name(fixture_set_name)
+ h[fixture_set_name] = ActiveRecord::FixtureSet.default_fixture_model_name(fixture_set_name, self.config)
end
end
@@ -745,27 +843,20 @@ module ActiveRecord
# 'namespaced/fixture' => Another::Model
#
# The keys must be the fixture names, that coincide with the short paths to the fixture files.
- #--
- # It is also possible to pass the class name instead of the class:
- # set_fixture_class 'some_fixture' => 'SomeModel'
- # I think this option is redundant, i propose to deprecate it.
- # Isn't it easier to always pass the class itself?
- # (2011-12-20 alexeymuranov)
- #++
def set_fixture_class(class_names = {})
self.fixture_class_names = self.fixture_class_names.merge(class_names.stringify_keys)
end
def fixtures(*fixture_set_names)
if fixture_set_names.first == :all
- fixture_set_names = Dir["#{fixture_path}/**/*.{yml}"]
+ fixture_set_names = Dir["#{fixture_path}/{**,*}/*.{yml}"]
fixture_set_names.map! { |f| f[(fixture_path.to_s.size + 1)..-5] }
else
fixture_set_names = fixture_set_names.flatten.map { |n| n.to_s }
end
self.fixture_table_names |= fixture_set_names
- require_fixture_classes(fixture_set_names)
+ require_fixture_classes(fixture_set_names, self.config)
setup_fixture_accessors(fixture_set_names)
end
@@ -780,7 +871,7 @@ module ActiveRecord
end
end
- def require_fixture_classes(fixture_set_names = nil)
+ def require_fixture_classes(fixture_set_names = nil, config = ActiveRecord::Base)
if fixture_set_names
fixture_set_names = fixture_set_names.map { |n| n.to_s }
else
@@ -788,7 +879,7 @@ module ActiveRecord
end
fixture_set_names.each do |file_name|
- file_name = file_name.singularize if ActiveRecord::Base.pluralize_table_names
+ file_name = file_name.singularize if config.pluralize_table_names
try_to_load_dependency(file_name)
end
end
@@ -840,9 +931,7 @@ module ActiveRecord
!self.class.uses_transaction?(method_name)
end
- def setup_fixtures
- return if ActiveRecord::Base.configurations.blank?
-
+ def setup_fixtures(config = ActiveRecord::Base)
if pre_loaded_fixtures && !use_transactional_fixtures
raise RuntimeError, 'pre_loaded_fixtures requires use_transactional_fixtures'
end
@@ -856,7 +945,7 @@ module ActiveRecord
if @@already_loaded_fixtures[self.class]
@loaded_fixtures = @@already_loaded_fixtures[self.class]
else
- @loaded_fixtures = load_fixtures
+ @loaded_fixtures = load_fixtures(config)
@@already_loaded_fixtures[self.class] = @loaded_fixtures
end
@fixture_connections = enlist_fixture_connections
@@ -867,16 +956,14 @@ module ActiveRecord
else
ActiveRecord::FixtureSet.reset_cache
@@already_loaded_fixtures[self.class] = nil
- @loaded_fixtures = load_fixtures
+ @loaded_fixtures = load_fixtures(config)
end
# Instantiate fixtures for every test if requested.
- instantiate_fixtures if use_instantiated_fixtures
+ instantiate_fixtures(config) if use_instantiated_fixtures
end
def teardown_fixtures
- return if ActiveRecord::Base.configurations.blank?
-
# Rollback changes if a transaction is active.
if run_in_transaction?
@fixture_connections.each do |connection|
@@ -895,19 +982,19 @@ module ActiveRecord
end
private
- def load_fixtures
- fixtures = ActiveRecord::FixtureSet.create_fixtures(fixture_path, fixture_table_names, fixture_class_names)
+ def load_fixtures(config)
+ fixtures = ActiveRecord::FixtureSet.create_fixtures(fixture_path, fixture_table_names, fixture_class_names, config)
Hash[fixtures.map { |f| [f.name, f] }]
end
# for pre_loaded_fixtures, only require the classes once. huge speed improvement
@@required_fixture_classes = false
- def instantiate_fixtures
+ def instantiate_fixtures(config)
if pre_loaded_fixtures
raise RuntimeError, 'Load fixtures before instantiating them.' if ActiveRecord::FixtureSet.all_loaded_fixtures.empty?
unless @@required_fixture_classes
- self.class.require_fixture_classes ActiveRecord::FixtureSet.all_loaded_fixtures.keys
+ self.class.require_fixture_classes ActiveRecord::FixtureSet.all_loaded_fixtures.keys, config
@@required_fixture_classes = true
end
ActiveRecord::FixtureSet.instantiate_all_loaded_fixtures(self, load_instances?)
@@ -924,3 +1011,13 @@ module ActiveRecord
end
end
end
+
+class ActiveRecord::FixtureSet::RenderContext # :nodoc:
+ def self.create_subclass
+ Class.new ActiveRecord::FixtureSet.context_class do
+ def get_binding
+ binding()
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb
index 8df76c7f5f..f144d84946 100644
--- a/activerecord/lib/active_record/inheritance.rb
+++ b/activerecord/lib/active_record/inheritance.rb
@@ -5,7 +5,7 @@ module ActiveRecord
extend ActiveSupport::Concern
included do
- # Determine whether to store the full constant name including namespace when using STI
+ # Determines whether to store the full constant name including namespace when using STI.
class_attribute :store_full_sti_class, instance_writer: false
self.store_full_sti_class = true
end
@@ -13,7 +13,7 @@ module ActiveRecord
module ClassMethods
# Determines if one of the attributes passed in is the inheritance column,
# and if the inheritance column is attr accessible, it initializes an
- # instance of the given subclass instead of the base class
+ # instance of the given subclass instead of the base class.
def new(*args, &block)
if abstract_class? || self == Base
raise NotImplementedError, "#{self} is an abstract class and can not be instantiated."
@@ -27,7 +27,8 @@ module ActiveRecord
super
end
- # True if this isn't a concrete subclass needing a STI type condition.
+ # Returns +true+ if this does not need STI type condition. Returns
+ # +false+ if STI type condition needs to be applied.
def descends_from_active_record?
if self == Base
false
@@ -44,10 +45,12 @@ module ActiveRecord
end
def symbolized_base_class
+ ActiveSupport::Deprecation.warn("ActiveRecord::Base.symbolized_base_class is deprecated and will be removed without replacement.")
@symbolized_base_class ||= base_class.to_s.to_sym
end
def symbolized_sti_name
+ ActiveSupport::Deprecation.warn("ActiveRecord::Base.symbolized_sti_name is deprecated and will be removed without replacement.")
@symbolized_sti_name ||= sti_name.present? ? sti_name.to_sym : symbolized_base_class
end
@@ -116,9 +119,10 @@ module ActiveRecord
begin
constant = ActiveSupport::Dependencies.constantize(candidate)
return constant if candidate == constant.to_s
- rescue NameError => e
- # We don't want to swallow NoMethodError < NameError errors
- raise e unless e.instance_of?(NameError)
+ # We don't want to swallow NoMethodError < NameError errors
+ rescue NoMethodError
+ raise
+ rescue NameError
end
end
@@ -158,7 +162,7 @@ module ActiveRecord
end
def type_condition(table = arel_table)
- sti_column = table[inheritance_column.to_sym]
+ sti_column = table[inheritance_column]
sti_names = ([self] + descendants).map { |model| model.sti_name }
sti_column.in(sti_names)
diff --git a/activerecord/lib/active_record/integration.rb b/activerecord/lib/active_record/integration.rb
index 2589b2f3da..31e2518540 100644
--- a/activerecord/lib/active_record/integration.rb
+++ b/activerecord/lib/active_record/integration.rb
@@ -1,3 +1,5 @@
+require 'active_support/core_ext/string/filters'
+
module ActiveRecord
module Integration
extend ActiveSupport::Concern
@@ -45,10 +47,19 @@ module ActiveRecord
# Product.new.cache_key # => "products/new"
# Product.find(5).cache_key # => "products/5" (updated_at not available)
# Person.find(5).cache_key # => "people/5-20071224150000" (updated_at available)
- def cache_key
+ #
+ # You can also pass a list of named timestamps, and the newest in the list will be
+ # used to generate the key:
+ #
+ # Person.find(5).cache_key(:updated_at, :last_reviewed_at)
+ def cache_key(*timestamp_names)
case
when new_record?
"#{self.class.model_name.cache_key}/new"
+ when timestamp_names.any?
+ timestamp = max_updated_column_timestamp(timestamp_names)
+ timestamp = timestamp.utc.to_s(cache_timestamp_format)
+ "#{self.class.model_name.cache_key}/#{id}-#{timestamp}"
when timestamp = max_updated_column_timestamp
timestamp = timestamp.utc.to_s(cache_timestamp_format)
"#{self.class.model_name.cache_key}/#{id}-#{timestamp}"
@@ -56,5 +67,47 @@ module ActiveRecord
"#{self.class.model_name.cache_key}/#{id}"
end
end
+
+ module ClassMethods
+ # Defines your model's +to_param+ method to generate "pretty" URLs
+ # using +method_name+, which can be any attribute or method that
+ # responds to +to_s+.
+ #
+ # class User < ActiveRecord::Base
+ # to_param :name
+ # end
+ #
+ # user = User.find_by(name: 'Fancy Pants')
+ # user.id # => 123
+ # user_path(user) # => "/users/123-fancy-pants"
+ #
+ # Values longer than 20 characters will be truncated. The value
+ # is truncated word by word.
+ #
+ # user = User.find_by(name: 'David HeinemeierHansson')
+ # user.id # => 125
+ # user_path(user) # => "/users/125-david"
+ #
+ # Because the generated param begins with the record's +id+, it is
+ # suitable for passing to +find+. In a controller, for example:
+ #
+ # params[:id] # => "123-fancy-pants"
+ # User.find(params[:id]).id # => 123
+ def to_param(method_name = nil)
+ if method_name.nil?
+ super()
+ else
+ define_method :to_param do
+ if (default = super()) &&
+ (result = send(method_name).to_s).present? &&
+ (param = result.squish.truncate(20, separator: /\s/, omission: nil).parameterize).present?
+ "#{default}-#{param}"
+ else
+ default
+ end
+ end
+ end
+ end
+ end
end
end
diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb
index 209de78898..6f54729b3c 100644
--- a/activerecord/lib/active_record/locking/optimistic.rb
+++ b/activerecord/lib/active_record/locking/optimistic.rb
@@ -82,9 +82,12 @@ module ActiveRecord
stmt = relation.where(
relation.table[self.class.primary_key].eq(id).and(
- relation.table[lock_col].eq(self.class.quote_value(previous_lock_value))
+ relation.table[lock_col].eq(self.class.quote_value(previous_lock_value, column_for_attribute(lock_col)))
)
- ).arel.compile_update(arel_attributes_with_values_for_update(attribute_names))
+ ).arel.compile_update(
+ arel_attributes_with_values_for_update(attribute_names),
+ self.class.primary_key
+ )
affected_rows = self.class.connection.update stmt
@@ -138,6 +141,7 @@ module ActiveRecord
# Set the column to use for optimistic locking. Defaults to +lock_version+.
def locking_column=(value)
+ @column_defaults = nil
@locking_column = value.to_s
end
@@ -149,6 +153,7 @@ module ActiveRecord
# Quote the column name used for optimistic locking.
def quoted_locking_column
+ ActiveSupport::Deprecation.warn "ActiveRecord::Base.quoted_locking_column is deprecated and will be removed in Rails 4.2 or later."
connection.quote_column_name(locking_column)
end
diff --git a/activerecord/lib/active_record/locking/pessimistic.rb b/activerecord/lib/active_record/locking/pessimistic.rb
index 8e4ddcac82..ff7102d35b 100644
--- a/activerecord/lib/active_record/locking/pessimistic.rb
+++ b/activerecord/lib/active_record/locking/pessimistic.rb
@@ -3,12 +3,12 @@ module ActiveRecord
# Locking::Pessimistic provides support for row-level locking using
# SELECT ... FOR UPDATE and other lock types.
#
- # Pass <tt>lock: true</tt> to <tt>ActiveRecord::Base.find</tt> to obtain an exclusive
+ # Chain <tt>ActiveRecord::Base#find</tt> to <tt>ActiveRecord::QueryMethods#lock</tt> to obtain an exclusive
# lock on the selected rows:
# # select * from accounts where id=1 for update
- # Account.find(1, lock: true)
+ # Account.lock.find(1)
#
- # Pass <tt>lock: 'some locking clause'</tt> to give a database-specific locking clause
+ # Call <tt>lock('some locking clause')</tt> to use a database-specific locking clause
# of your own such as 'LOCK IN SHARE MODE' or 'FOR UPDATE NOWAIT'. Example:
#
# Account.transaction do
@@ -64,7 +64,7 @@ module ActiveRecord
end
# Wraps the passed block in a transaction, locking the object
- # before yielding. You pass can the SQL locking clause
+ # before yielding. You can pass the SQL locking clause
# as argument (see <tt>lock!</tt>).
def with_lock(lock = true)
transaction do
diff --git a/activerecord/lib/active_record/log_subscriber.rb b/activerecord/lib/active_record/log_subscriber.rb
index c1ba524c84..654ef21b07 100644
--- a/activerecord/lib/active_record/log_subscriber.rb
+++ b/activerecord/lib/active_record/log_subscriber.rb
@@ -3,11 +3,11 @@ module ActiveRecord
IGNORE_PAYLOAD_NAMES = ["SCHEMA", "EXPLAIN"]
def self.runtime=(value)
- Thread.current[:active_record_sql_runtime] = value
+ ActiveRecord::RuntimeRegistry.sql_runtime = value
end
def self.runtime
- Thread.current[:active_record_sql_runtime] ||= 0
+ ActiveRecord::RuntimeRegistry.sql_runtime ||= 0
end
def self.reset_runtime
@@ -17,12 +17,14 @@ module ActiveRecord
def initialize
super
- @odd_or_even = false
+ @odd = false
end
def render_bind(column, value)
if column
if column.binary?
+ # This specifically deals with the PG adapter that casts bytea columns into a Hash.
+ value = value[:value] if value.is_a?(Hash)
value = "<#{value.bytesize} bytes of binary data>"
end
@@ -41,7 +43,7 @@ module ActiveRecord
return if IGNORE_PAYLOAD_NAMES.include?(payload[:name])
name = "#{payload[:name]} (#{event.duration.round(1)}ms)"
- sql = payload[:sql].squeeze(' ')
+ sql = payload[:sql]
binds = nil
unless (payload[:binds] || []).empty?
@@ -60,17 +62,8 @@ module ActiveRecord
debug " #{name} #{sql}#{binds}"
end
- def identity(event)
- return unless logger.debug?
-
- name = color(event.payload[:name], odd? ? CYAN : MAGENTA, true)
- line = odd? ? color(event.payload[:line], nil, true) : event.payload[:line]
-
- debug " #{name} #{line}"
- end
-
def odd?
- @odd_or_even = !@odd_or_even
+ @odd = !@odd
end
def logger
diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb
index d3edcf3cdb..fc84b907a5 100644
--- a/activerecord/lib/active_record/migration.rb
+++ b/activerecord/lib/active_record/migration.rb
@@ -1,38 +1,49 @@
-require "active_support/core_ext/class/attribute_accessors"
+require "active_support/core_ext/module/attribute_accessors"
require 'set'
module ActiveRecord
+ class MigrationError < ActiveRecordError#:nodoc:
+ def initialize(message = nil)
+ message = "\n\n#{message}\n\n" if message
+ super
+ end
+ end
+
# Exception that can be raised to stop migrations from going backwards.
- class IrreversibleMigration < ActiveRecordError
+ class IrreversibleMigration < MigrationError
end
- class DuplicateMigrationVersionError < ActiveRecordError#:nodoc:
+ class DuplicateMigrationVersionError < MigrationError#:nodoc:
def initialize(version)
super("Multiple migrations have the version number #{version}")
end
end
- class DuplicateMigrationNameError < ActiveRecordError#:nodoc:
+ class DuplicateMigrationNameError < MigrationError#:nodoc:
def initialize(name)
super("Multiple migrations have the name #{name}")
end
end
- class UnknownMigrationVersionError < ActiveRecordError #:nodoc:
+ class UnknownMigrationVersionError < MigrationError #:nodoc:
def initialize(version)
super("No migration with version number #{version}")
end
end
- class IllegalMigrationNameError < ActiveRecordError#:nodoc:
+ class IllegalMigrationNameError < MigrationError#:nodoc:
def initialize(name)
super("Illegal name for migration file: #{name}\n\t(only lower case letters, numbers, and '_' allowed)")
end
end
- class PendingMigrationError < ActiveRecordError#:nodoc:
+ class PendingMigrationError < MigrationError#:nodoc:
def initialize
- super("Migrations are pending; run 'rake db:migrate RAILS_ENV=#{Rails.env}' to resolve this issue.")
+ if defined?(Rails)
+ super("Migrations are pending. To resolve this issue, run:\n\n\tbin/rake db:migrate RAILS_ENV=#{::Rails.env}")
+ else
+ super("Migrations are pending. To resolve this issue, run:\n\n\tbin/rake db:migrate")
+ end
end
end
@@ -120,8 +131,8 @@ module ActiveRecord
# a column but keeps the type and content.
# * <tt>change_column(table_name, column_name, type, options)</tt>: Changes
# the column to a different type using the same parameters as add_column.
- # * <tt>remove_column(table_name, column_names)</tt>: Removes the column listed in
- # +column_names+ from the table called +table_name+.
+ # * <tt>remove_column(table_name, column_name, type, options)</tt>: Removes the column
+ # named +column_name+ from the table called +table_name+.
# * <tt>add_index(table_name, column_names, options)</tt>: Adds a new index
# with the name of the column. Other options include
# <tt>:name</tt>, <tt>:unique</tt> (e.g.
@@ -357,11 +368,14 @@ module ActiveRecord
class CheckPending
def initialize(app)
@app = app
+ @last_check = 0
end
def call(env)
- ActiveRecord::Base.logger.silence do
+ mtime = ActiveRecord::Migrator.last_migration.mtime.to_i
+ if @last_check < mtime
ActiveRecord::Migration.check_pending!
+ @last_check = mtime
end
@app.call(env)
end
@@ -370,23 +384,36 @@ module ActiveRecord
class << self
attr_accessor :delegate # :nodoc:
attr_accessor :disable_ddl_transaction # :nodoc:
- end
- def self.check_pending!
- raise ActiveRecord::PendingMigrationError if ActiveRecord::Migrator.needs_migration?
- end
+ def check_pending!
+ raise ActiveRecord::PendingMigrationError if ActiveRecord::Migrator.needs_migration?
+ end
- def self.method_missing(name, *args, &block) # :nodoc:
- (delegate || superclass.delegate).send(name, *args, &block)
- end
+ def load_schema_if_pending!
+ if ActiveRecord::Migrator.needs_migration?
+ ActiveRecord::Tasks::DatabaseTasks.load_schema
+ check_pending!
+ end
+ end
- def self.migrate(direction)
- new.migrate direction
- end
+ def maintain_test_schema! # :nodoc:
+ if ActiveRecord::Base.maintain_test_schema
+ suppress_messages { load_schema_if_pending! }
+ end
+ end
+
+ def method_missing(name, *args, &block) # :nodoc:
+ (delegate || superclass.delegate).send(name, *args, &block)
+ end
+
+ def migrate(direction)
+ new.migrate direction
+ end
- # Disable DDL transactions for this migration.
- def self.disable_ddl_transaction!
- @disable_ddl_transaction = true
+ # Disable DDL transactions for this migration.
+ def disable_ddl_transaction!
+ @disable_ddl_transaction = true
+ end
end
def disable_ddl_transaction # :nodoc:
@@ -466,7 +493,7 @@ module ActiveRecord
@connection.respond_to?(:reverting) && @connection.reverting
end
- class ReversibleBlockHelper < Struct.new(:reverting)
+ class ReversibleBlockHelper < Struct.new(:reverting) # :nodoc:
def up
yield unless reverting
end
@@ -614,8 +641,8 @@ module ActiveRecord
say_with_time "#{method}(#{arg_list})" do
unless @connection.respond_to? :revert
unless arguments.empty? || method == :execute
- arguments[0] = Migrator.proper_table_name(arguments.first)
- arguments[1] = Migrator.proper_table_name(arguments.second) if method == :rename_table
+ arguments[0] = proper_table_name(arguments.first, table_name_options)
+ arguments[1] = proper_table_name(arguments.second, table_name_options) if method == :rename_table
end
end
return super unless connection.respond_to?(method)
@@ -626,7 +653,7 @@ module ActiveRecord
def copy(destination, sources, options = {})
copied = []
- FileUtils.mkdir_p(destination) unless File.exists?(destination)
+ FileUtils.mkdir_p(destination) unless File.exist?(destination)
destination_migrations = ActiveRecord::Migrator.migrations(destination)
last = destination_migrations.last
@@ -668,6 +695,18 @@ module ActiveRecord
copied
end
+ # Finds the correct table name given an Active Record object.
+ # Uses the Active Record object's own table_name, or pre/suffix from the
+ # options passed in.
+ def proper_table_name(name, options = {})
+ if name.respond_to? :table_name
+ name.table_name
+ else
+ "#{options[:table_name_prefix]}#{name}#{options[:table_name_suffix]}"
+ end
+ end
+
+ # Determines the version number of the next migration.
def next_migration_number(number)
if ActiveRecord::Base.timestamped_migrations
[Time.now.utc.strftime("%Y%m%d%H%M%S"), "%.14d" % number].max
@@ -676,6 +715,13 @@ module ActiveRecord
end
end
+ def table_name_options(config = ActiveRecord::Base)
+ {
+ table_name_prefix: config.table_name_prefix,
+ table_name_suffix: config.table_name_suffix
+ }
+ end
+
private
def execute_block
if connection.respond_to? :execute_block
@@ -699,6 +745,10 @@ module ActiveRecord
File.basename(filename)
end
+ def mtime
+ File.mtime filename
+ end
+
delegate :migrate, :announce, :write, :disable_ddl_transaction, to: :migration
private
@@ -714,6 +764,16 @@ module ActiveRecord
end
+ class NullMigration < MigrationProxy #:nodoc:
+ def initialize
+ super(nil, 0, nil, nil)
+ end
+
+ def mtime
+ 0
+ end
+ end
+
class Migrator#:nodoc:
class << self
attr_writer :migrations_paths
@@ -784,15 +844,23 @@ module ActiveRecord
end
def last_version
- migrations(migrations_paths).last.try(:version)||0
+ last_migration.version
+ end
+
+ def last_migration #:nodoc:
+ migrations(migrations_paths).last || NullMigration.new
end
- def proper_table_name(name)
- # Use the Active Record objects own table_name, or pre/suffix from ActiveRecord::Base if name is a symbol/string
+ def proper_table_name(name, options = {})
+ ActiveSupport::Deprecation.warn "ActiveRecord::Migrator.proper_table_name is deprecated and will be removed in Rails 4.2. Use the proper_table_name instance method on ActiveRecord::Migration instead"
+ options = {
+ table_name_prefix: ActiveRecord::Base.table_name_prefix,
+ table_name_suffix: ActiveRecord::Base.table_name_suffix
+ }.merge(options)
if name.respond_to? :table_name
name.table_name
else
- "#{ActiveRecord::Base.table_name_prefix}#{name}#{ActiveRecord::Base.table_name_suffix}"
+ "#{options[:table_name_prefix]}#{name}#{options[:table_name_suffix]}"
end
end
@@ -844,17 +912,11 @@ module ActiveRecord
@direction = direction
@target_version = target_version
@migrated_versions = nil
-
- if Array(migrations).grep(String).empty?
- @migrations = migrations
- else
- ActiveSupport::Deprecation.warn "instantiate this class with a list of migrations"
- @migrations = self.class.migrations(migrations)
- end
+ @migrations = migrations
validate(@migrations)
- ActiveRecord::SchemaMigration.create_table
+ Base.connection.initialize_schema_migrations_table
end
def current_version
@@ -867,11 +929,15 @@ module ActiveRecord
alias :current :current_migration
def run
- target = migrations.detect { |m| m.version == @target_version }
- raise UnknownMigrationVersionError.new(@target_version) if target.nil?
- unless (up? && migrated.include?(target.version.to_i)) || (down? && !migrated.include?(target.version.to_i))
- target.migrate(@direction)
- record_version_state_after_migrating(target.version)
+ migration = migrations.detect { |m| m.version == @target_version }
+ raise UnknownMigrationVersionError.new(@target_version) if migration.nil?
+ unless (up? && migrated.include?(migration.version.to_i)) || (down? && !migrated.include?(migration.version.to_i))
+ begin
+ execute_migration_in_transaction(migration, @direction)
+ rescue => e
+ canceled_msg = use_transaction?(migration) ? ", this migration was canceled" : ""
+ raise StandardError, "An error has occurred#{canceled_msg}:\n\n#{e}", e.backtrace
+ end
end
end
@@ -880,22 +946,11 @@ module ActiveRecord
raise UnknownMigrationVersionError.new(@target_version)
end
- running = runnable
-
- if block_given?
- message = "block argument to migrate is deprecated, please filter migrations before constructing the migrator"
- ActiveSupport::Deprecation.warn message
- running.select! { |m| yield m }
- end
-
- running.each do |migration|
+ runnable.each do |migration|
Base.logger.info "Migrating to #{migration.name} (#{migration.version})" if Base.logger
begin
- ddl_transaction(migration) do
- migration.migrate(@direction)
- record_version_state_after_migrating(migration.version)
- end
+ execute_migration_in_transaction(migration, @direction)
rescue => e
canceled_msg = use_transaction?(migration) ? "this and " : ""
raise StandardError, "An error has occurred, #{canceled_msg}all later migrations canceled:\n\n#{e}", e.backtrace
@@ -932,6 +987,13 @@ module ActiveRecord
migrated.include?(migration.version.to_i)
end
+ def execute_migration_in_transaction(migration, direction)
+ ddl_transaction(migration) do
+ migration.migrate(direction)
+ record_version_state_after_migrating(migration.version)
+ end
+ end
+
def target
migrations.detect { |m| m.version == @target_version }
end
diff --git a/activerecord/lib/active_record/migration/command_recorder.rb b/activerecord/lib/active_record/migration/command_recorder.rb
index 79c55045ba..01c73be849 100644
--- a/activerecord/lib/active_record/migration/command_recorder.rb
+++ b/activerecord/lib/active_record/migration/command_recorder.rb
@@ -73,7 +73,7 @@ module ActiveRecord
[: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,
+ :drop_join_table, :drop_table, :execute_block, :enable_extension,
:change_column, :execute, :remove_columns, # irreversible methods need to be here too
].each do |method|
class_eval <<-EOV, __FILE__, __LINE__ + 1
@@ -100,6 +100,7 @@ module ActiveRecord
add_column: :remove_column,
add_timestamps: :remove_timestamps,
add_reference: :remove_reference,
+ enable_extension: :disable_extension
}.each do |cmd, inv|
[[inv, cmd], [cmd, inv]].uniq.each do |method, inverse|
class_eval <<-EOV, __FILE__, __LINE__ + 1
@@ -144,7 +145,10 @@ module ActiveRecord
def invert_remove_index(args)
table, options = *args
- raise ActiveRecord::IrreversibleMigration, "remove_index is only reversible if given a :column option." unless options && options[:column]
+
+ unless options && options.is_a?(Hash) && 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]]
diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb
index ac2d2f2712..dc5ff02882 100644
--- a/activerecord/lib/active_record/model_schema.rb
+++ b/activerecord/lib/active_record/model_schema.rb
@@ -34,6 +34,12 @@ module ActiveRecord
##
# :singleton-method:
+ # Accessor for the name of the schema migrations table. By default, the value is "schema_migrations"
+ class_attribute :schema_migrations_table_name, instance_accessor: false
+ self.schema_migrations_table_name = "schema_migrations"
+
+ ##
+ # :singleton-method:
# Indicates whether table names should be the pluralized versions of the corresponding class names.
# If true, the default table name for a Product class will be +products+. If false, it would just be +product+.
# See table_name for the full rules on table/class naming. This is true, by default.
@@ -124,7 +130,7 @@ module ActiveRecord
@quoted_table_name = nil
@arel_table = nil
@sequence_name = nil unless defined?(@explicit_sequence_name) && @explicit_sequence_name
- @relation = Relation.new(self, arel_table)
+ @relation = Relation.create(self, arel_table)
end
# Returns a quoted version of the table name, used to construct SQL statements.
@@ -224,13 +230,20 @@ module ActiveRecord
def decorate_columns(columns_hash) # :nodoc:
return if columns_hash.empty?
- columns_hash.each do |name, col|
- if serialized_attributes.key?(name)
- columns_hash[name] = AttributeMethods::Serialization::Type.new(col)
- end
- if create_time_zone_conversion_attribute?(name, col)
- columns_hash[name] = AttributeMethods::TimeZoneConversion::Type.new(col)
- end
+ @serialized_column_names ||= self.columns_hash.keys.find_all do |name|
+ serialized_attributes.key?(name)
+ end
+
+ @serialized_column_names.each do |name|
+ columns_hash[name] = AttributeMethods::Serialization::Type.new(columns_hash[name])
+ end
+
+ @time_zone_column_names ||= self.columns_hash.find_all do |name, col|
+ create_time_zone_conversion_attribute?(name, col)
+ end.map!(&:first)
+
+ @time_zone_column_names.each do |name|
+ columns_hash[name] = AttributeMethods::TimeZoneConversion::Type.new(columns_hash[name])
end
columns_hash
@@ -253,19 +266,6 @@ module ActiveRecord
@content_columns ||= columns.reject { |c| c.primary || c.name =~ /(_id|_count)$/ || c.name == inheritance_column }
end
- # Returns a hash of all the methods added to query each of the columns in the table with the name of the method as the key
- # and true as the value. This makes it possible to do O(1) lookups in respond_to? to check if a given method for attribute
- # is available.
- def column_methods_hash #:nodoc:
- @dynamic_methods_hash ||= column_names.each_with_object(Hash.new(false)) do |attr, methods|
- attr_name = attr.to_s
- methods[attr.to_sym] = attr_name
- methods["#{attr}=".to_sym] = attr_name
- methods["#{attr}?".to_sym] = attr_name
- methods["#{attr}_before_type_cast".to_sym] = attr_name
- end
- end
-
# Resets all the cached information about columns, which will cause them
# to be reloaded on the next request.
#
@@ -297,16 +297,19 @@ module ActiveRecord
undefine_attribute_methods
connection.schema_cache.clear_table_cache!(table_name) if table_exists?
- @arel_engine = nil
- @column_defaults = nil
- @column_names = nil
- @columns = nil
- @columns_hash = nil
- @column_types = nil
- @content_columns = nil
- @dynamic_methods_hash = nil
- @inheritance_column = nil unless defined?(@explicit_inheritance_column) && @explicit_inheritance_column
- @relation = nil
+ @arel_engine = nil
+ @column_defaults = nil
+ @column_names = nil
+ @columns = nil
+ @columns_hash = nil
+ @column_types = nil
+ @content_columns = nil
+ @dynamic_methods_hash = nil
+ @inheritance_column = nil unless defined?(@explicit_inheritance_column) && @explicit_inheritance_column
+ @relation = nil
+ @serialized_column_names = nil
+ @time_zone_column_names = nil
+ @cached_time_zone = nil
end
# This is a hook for use by modules that need to do extra stuff to
diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb
index d607f49e2b..9d92e747d4 100644
--- a/activerecord/lib/active_record/nested_attributes.rb
+++ b/activerecord/lib/active_record/nested_attributes.rb
@@ -229,6 +229,27 @@ module ActiveRecord
# belongs_to :member, inverse_of: :posts
# validates_presence_of :member
# end
+ #
+ # Note that if you do not specify the <tt>inverse_of</tt> option, then
+ # Active Record will try to automatically guess the inverse association
+ # based on heuristics.
+ #
+ # For one-to-one nested associations, if you build the new (in-memory)
+ # child object yourself before assignment, then this module will not
+ # overwrite it, e.g.:
+ #
+ # class Member < ActiveRecord::Base
+ # has_one :avatar
+ # accepts_nested_attributes_for :avatar
+ #
+ # def avatar
+ # super || build_avatar(width: 200)
+ # end
+ # end
+ #
+ # member = Member.new
+ # member.avatar_attributes = {icon: 'sad'}
+ # member.avatar.width # => 200
module ClassMethods
REJECT_ALL_BLANK_PROC = proc { |attributes| attributes.all? { |key, value| key == '_destroy' || value.blank? } }
@@ -285,7 +306,7 @@ module ActiveRecord
attr_names.each do |association_name|
if reflection = reflect_on_association(association_name)
- reflection.options[:autosave] = true
+ reflection.autosave = true
add_autosave_association_callbacks(reflection)
nested_attributes_options = self.nested_attributes_options.dup
@@ -314,7 +335,7 @@ module ActiveRecord
# the helper methods defined below. Makes it seem like the nested
# associations are just regular associations.
def generate_association_writer(association_name, type)
- generated_feature_methods.module_eval <<-eoruby, __FILE__, __LINE__ + 1
+ generated_association_methods.module_eval <<-eoruby, __FILE__, __LINE__ + 1
if method_defined?(:#{association_name}_attributes=)
remove_method(:#{association_name}_attributes=)
end
@@ -356,20 +377,28 @@ module ActiveRecord
def assign_nested_attributes_for_one_to_one_association(association_name, attributes)
options = self.nested_attributes_options[association_name]
attributes = attributes.with_indifferent_access
+ existing_record = send(association_name)
- if (options[:update_only] || !attributes['id'].blank?) && (record = send(association_name)) &&
- (options[:update_only] || record.id.to_s == attributes['id'].to_s)
- assign_to_or_mark_for_destruction(record, attributes, options[:allow_destroy]) unless call_reject_if(association_name, attributes)
+ if (options[:update_only] || !attributes['id'].blank?) && existing_record &&
+ (options[:update_only] || existing_record.id.to_s == attributes['id'].to_s)
+ assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy]) unless call_reject_if(association_name, attributes)
elsif attributes['id'].present?
raise_nested_attributes_record_not_found!(association_name, attributes['id'])
elsif !reject_new_record?(association_name, attributes)
- method = "build_#{association_name}"
- if respond_to?(method)
- send(method, attributes.except(*UNASSIGNABLE_KEYS))
+ assignable_attributes = attributes.except(*UNASSIGNABLE_KEYS)
+
+ if existing_record && existing_record.new_record?
+ existing_record.assign_attributes(assignable_attributes)
+ association(association_name).initialize_attributes(existing_record)
else
- raise ArgumentError, "Cannot build association `#{association_name}'. Are you trying to build a polymorphic one-to-one association?"
+ method = "build_#{association_name}"
+ if respond_to?(method)
+ send(method, assignable_attributes)
+ else
+ raise ArgumentError, "Cannot build association `#{association_name}'. Are you trying to build a polymorphic one-to-one association?"
+ end
end
end
end
@@ -436,19 +465,17 @@ module ActiveRecord
association.build(attributes.except(*UNASSIGNABLE_KEYS))
end
elsif existing_record = existing_records.detect { |record| record.id.to_s == attributes['id'].to_s }
- unless association.loaded? || call_reject_if(association_name, attributes)
+ unless call_reject_if(association_name, attributes)
# Make sure we are operating on the actual object which is in the association's
# proxy_target array (either by finding it, or adding it if not found)
- target_record = association.target.detect { |record| record == existing_record }
-
+ # Take into account that the proxy_target may have changed due to callbacks
+ target_record = association.target.detect { |record| record.id.to_s == attributes['id'].to_s }
if target_record
existing_record = target_record
else
- association.add_to_target(existing_record)
+ association.add_to_target(existing_record, :skip_callbacks)
end
- end
- if !call_reject_if(association_name, attributes)
assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy])
end
else
diff --git a/activerecord/lib/active_record/no_touching.rb b/activerecord/lib/active_record/no_touching.rb
new file mode 100644
index 0000000000..dbf4564ae5
--- /dev/null
+++ b/activerecord/lib/active_record/no_touching.rb
@@ -0,0 +1,52 @@
+module ActiveRecord
+ # = Active Record No Touching
+ module NoTouching
+ extend ActiveSupport::Concern
+
+ module ClassMethods
+ # Lets you selectively disable calls to `touch` for the
+ # duration of a block.
+ #
+ # ==== Examples
+ # ActiveRecord::Base.no_touching do
+ # Project.first.touch # does nothing
+ # Message.first.touch # does nothing
+ # end
+ #
+ # Project.no_touching do
+ # Project.first.touch # does nothing
+ # Message.first.touch # works, but does not touch the associated project
+ # end
+ #
+ def no_touching(&block)
+ NoTouching.apply_to(self, &block)
+ end
+ end
+
+ class << self
+ def apply_to(klass) #:nodoc:
+ klasses.push(klass)
+ yield
+ ensure
+ klasses.pop
+ end
+
+ def applied_to?(klass) #:nodoc:
+ klasses.any? { |k| k >= klass }
+ end
+
+ private
+ def klasses
+ Thread.current[:no_touching_classes] ||= []
+ end
+ end
+
+ def no_touching?
+ NoTouching.applied_to?(self.class)
+ end
+
+ def touch(*)
+ super unless no_touching?
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/null_relation.rb b/activerecord/lib/active_record/null_relation.rb
index 711fc8b883..5b255c3fe5 100644
--- a/activerecord/lib/active_record/null_relation.rb
+++ b/activerecord/lib/active_record/null_relation.rb
@@ -6,7 +6,7 @@ module ActiveRecord
@records = []
end
- def pluck(_column_name)
+ def pluck(*column_names)
[]
end
@@ -39,11 +39,7 @@ module ActiveRecord
end
def to_sql
- @to_sql ||= ""
- end
-
- def where_values_hash
- {}
+ ""
end
def count(*)
@@ -54,8 +50,14 @@ module ActiveRecord
0
end
- def calculate(_operation, _column_name, _options = {})
- nil
+ def calculate(operation, _column_name, _options = {})
+ # TODO: Remove _options argument as soon we remove support to
+ # activerecord-deprecated_finders.
+ if operation == :count
+ 0
+ else
+ nil
+ end
end
def exists?(_id = false)
diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb
index 42cece3ad0..b94ef18fd5 100644
--- a/activerecord/lib/active_record/persistence.rb
+++ b/activerecord/lib/active_record/persistence.rb
@@ -10,9 +10,6 @@ module ActiveRecord
# The +attributes+ parameter can be either a Hash or an Array of Hashes. These Hashes describe the
# attributes on the objects that are to be created.
#
- # +create+ respects mass-assignment security and accepts either +:as+ or +:without_protection+ options
- # in the +options+ parameter.
- #
# ==== Examples
# # Create a single new object
# User.create(first_name: 'Jamie')
@@ -51,7 +48,7 @@ module ActiveRecord
# how this "single-table" inheritance mapping is implemented.
def instantiate(record, column_types = {})
klass = discriminate_class_for_record(record)
- column_types = klass.decorate_columns(column_types)
+ column_types = klass.decorate_columns(column_types.dup)
klass.allocate.init_with('attributes' => record, 'column_types' => column_types)
end
@@ -99,6 +96,9 @@ module ActiveRecord
# <tt>before_*</tt> callbacks return +false+ the action is cancelled and
# +save+ returns +false+. See ActiveRecord::Callbacks for further
# details.
+ #
+ # Attributes marked as readonly are silently ignored if the record is
+ # being updated.
def save(*)
create_or_update
rescue ActiveRecord::RecordInvalid
@@ -118,6 +118,9 @@ module ActiveRecord
# the <tt>before_*</tt> callbacks return +false+ the action is cancelled
# and <tt>save!</tt> raises ActiveRecord::RecordNotSaved. See
# ActiveRecord::Callbacks for further details.
+ #
+ # Attributes marked as readonly are silently ignored if the record is
+ # being updated.
def save!(*)
create_or_update || raise(RecordNotSaved)
end
@@ -178,6 +181,7 @@ module ActiveRecord
became = klass.new
became.instance_variable_set("@attributes", @attributes)
became.instance_variable_set("@attributes_cache", @attributes_cache)
+ became.instance_variable_set("@changed_attributes", @changed_attributes) if defined?(@changed_attributes)
became.instance_variable_set("@new_record", new_record?)
became.instance_variable_set("@destroyed", destroyed?)
became.instance_variable_set("@errors", errors)
@@ -204,6 +208,8 @@ module ActiveRecord
# * updated_at/updated_on column is updated if that column is available.
# * Updates all the attributes that are dirty in this object.
#
+ # This method raises an +ActiveRecord::ActiveRecordError+ if the
+ # attribute is marked as readonly.
def update_attribute(name, value)
name = name.to_s
verify_readonly_attribute(name)
@@ -325,10 +331,54 @@ module ActiveRecord
toggle(attribute).update_attribute(attribute, self[attribute])
end
- # Reloads the attributes of this object from the database.
- # The optional options argument is passed to find when reloading so you
- # may do e.g. record.reload(lock: true) to reload the same record with
- # an exclusive row lock.
+ # Reloads the record from the database.
+ #
+ # This method finds record by its primary key (which could be assigned manually) and
+ # modifies the receiver in-place:
+ #
+ # account = Account.new
+ # # => #<Account id: nil, email: nil>
+ # account.id = 1
+ # account.reload
+ # # Account Load (1.2ms) SELECT "accounts".* FROM "accounts" WHERE "accounts"."id" = $1 LIMIT 1 [["id", 1]]
+ # # => #<Account id: 1, email: 'account@example.com'>
+ #
+ # Attributes are reloaded from the database, and caches busted, in
+ # particular the associations cache.
+ #
+ # If the record no longer exists in the database <tt>ActiveRecord::RecordNotFound</tt>
+ # is raised. Otherwise, in addition to the in-place modification the method
+ # returns +self+ for convenience.
+ #
+ # The optional <tt>:lock</tt> flag option allows you to lock the reloaded record:
+ #
+ # reload(lock: true) # reload with pessimistic locking
+ #
+ # Reloading is commonly used in test suites to test something is actually
+ # written to the database, or when some action modifies the corresponding
+ # row in the database but not the object in memory:
+ #
+ # assert account.deposit!(25)
+ # assert_equal 25, account.credit # check it is updated in memory
+ # assert_equal 25, account.reload.credit # check it is also persisted
+ #
+ # Another common use case is optimistic locking handling:
+ #
+ # def with_optimistic_retry
+ # begin
+ # yield
+ # rescue ActiveRecord::StaleObjectError
+ # begin
+ # # Reload lock_version in particular.
+ # reload
+ # rescue ActiveRecord::RecordNotFound
+ # # If the record is gone there is nothing to do.
+ # else
+ # retry
+ # end
+ # end
+ # end
+ #
def reload(options = nil)
clear_aggregation_cache
clear_association_cache
@@ -341,14 +391,16 @@ module ActiveRecord
end
@attributes.update(fresh_object.instance_variable_get('@attributes'))
- @columns_hash = fresh_object.instance_variable_get('@columns_hash')
- @attributes_cache = {}
+ @column_types = self.class.column_types
+ @column_types_override = fresh_object.instance_variable_get('@column_types_override')
+ @attributes_cache = {}
self
end
# Saves the record with the updated_at/on attributes set to the current time.
- # Please note that no validation is performed and no callbacks are executed.
+ # Please note that no validation is performed and only the +after_touch+
+ # callback is executed.
# If an attribute name is passed, that attribute is updated along with
# updated_at/on attributes.
#
@@ -391,7 +443,7 @@ module ActiveRecord
changes[self.class.locking_column] = increment_lock if locking_enabled?
- @changed_attributes.except!(*changes.keys)
+ changed_attributes.except!(*changes.keys)
primary_key = self.class.primary_key
self.class.unscoped.where(primary_key => self[primary_key]).update_all(changes) == 1
end
@@ -428,23 +480,11 @@ module ActiveRecord
# Updates the associated record with values matching those of the instance attributes.
# Returns the number of affected rows.
def update_record(attribute_names = @attributes.keys)
- attributes_with_values = arel_attributes_with_values_for_update(attribute_names)
- if attributes_with_values.empty?
+ attributes_values = arel_attributes_with_values_for_update(attribute_names)
+ if attributes_values.empty?
0
else
- klass = self.class
- column_hash = klass.connection.schema_cache.columns_hash klass.table_name
- db_columns_with_values = attributes_with_values.map { |attr,value|
- real_column = column_hash[attr.name]
- [real_column, value]
- }
- bind_attrs = attributes_with_values.dup
- bind_attrs.keys.each_with_index do |column, i|
- real_column = db_columns_with_values[i].first
- bind_attrs[column] = klass.connection.substitute_at(real_column, i)
- end
- stmt = klass.unscoped.where(klass.arel_table[klass.primary_key].eq(id_was || id)).arel.compile_update(bind_attrs)
- klass.connection.update stmt, 'SQL', db_columns_with_values
+ self.class.unscoped.update_record attributes_values, id, id_was
end
end
diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb
index 902fd90c54..fd4c973504 100644
--- a/activerecord/lib/active_record/querying.rb
+++ b/activerecord/lib/active_record/querying.rb
@@ -1,20 +1,21 @@
module ActiveRecord
module Querying
- delegate :find, :take, :take!, :first, :first!, :last, :last!, :exists?, :any?, :many?, :to => :all
- delegate :first_or_create, :first_or_create!, :first_or_initialize, :to => :all
- delegate :find_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, :take, :take!, :first, :first!, :last, :last!, :exists?, :any?, :many?, to: :all
+ delegate :first_or_create, :first_or_create!, :first_or_initialize, to: :all
+ delegate :find_or_create_by, :find_or_create_by!, :find_or_initialize_by, to: :all
+ delegate :find_by, :find_by!, to: :all
+ delegate :destroy, :destroy_all, :delete, :delete_all, :update, :update_all, to: :all
+ delegate :find_each, :find_in_batches, to: :all
delegate :select, :group, :order, :except, :reorder, :limit, :offset, :joins,
- :where, :preload, :eager_load, :includes, :from, :lock, :readonly,
- :having, :create_with, :uniq, :distinct, :references, :none, :to => :all
- delegate :count, :average, :minimum, :maximum, :sum, :calculate, :pluck, :ids, :to => :all
+ :where, :rewhere, :preload, :eager_load, :includes, :from, :lock, :readonly,
+ :having, :create_with, :uniq, :distinct, :references, :none, :unscope, to: :all
+ delegate :count, :average, :minimum, :maximum, :sum, :calculate, to: :all
+ delegate :pluck, :ids, to: :all
# Executes a custom SQL query against your database and returns all the results. The results will
# be returned as an array with columns requested encapsulated as attributes of the model you call
# this method from. If you call <tt>Product.find_by_sql</tt> then the results will be returned in
- # a Product object with the attributes you specified in the SQL query.
+ # a +Product+ object with the attributes you specified in the SQL query.
#
# If you call a complicated SQL query which spans multiple tables the columns specified by the
# SELECT will be attributes of the model, whether or not they are columns of the corresponding
@@ -29,9 +30,10 @@ module ActiveRecord
# Post.find_by_sql "SELECT p.title, c.author FROM posts p, comments c WHERE p.id = c.post_id"
# # => [#<Post:0x36bff9c @attributes={"title"=>"Ruby Meetup", "first_name"=>"Quentin"}>, ...]
#
- # # You can use the same string replacement techniques as you can with ActiveRecord#find
+ # You can use the same string replacement techniques as you can with <tt>ActiveRecord::QueryMethods#where</tt>:
+ #
# Post.find_by_sql ["SELECT title FROM posts WHERE author = ? AND created > ?", author_id, start_date]
- # # => [#<Post:0x36bff9c @attributes={"title"=>"The Cheap Man Buys Twice"}>, ...]
+ # Post.find_by_sql ["SELECT body FROM comments WHERE author = :user_id OR approved_by = :user_id", { :user_id => user_id }]
def find_by_sql(sql, binds = [])
result_set = connection.select_all(sanitize_sql(sql), "#{name} Load", binds)
column_types = {}
diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb
index 99117b74c5..ec85b3c843 100644
--- a/activerecord/lib/active_record/railtie.rb
+++ b/activerecord/lib/active_record/railtie.rb
@@ -31,11 +31,37 @@ module ActiveRecord
config.active_record.use_schema_cache_dump = true
+ config.active_record.maintain_test_schema = true
config.eager_load_namespaces << ActiveRecord
rake_tasks do
require "active_record/base"
+
+ namespace :db do
+ task :load_config do
+ configuration = if ENV["DATABASE_URL"]
+ { Rails.env => ENV["DATABASE_URL"] }
+ else
+ Rails.application.config.database_configuration || {}
+ end
+
+ resolver = ActiveRecord::ConnectionAdapters::ConnectionSpecification::Resolver.new(configuration)
+
+ configuration.each do |key, value|
+ configuration[key] = resolver.resolve(value) if value
+ end
+
+ ActiveRecord::Tasks::DatabaseTasks.database_configuration = configuration
+
+ if defined?(ENGINE_PATH) && engine = Rails::Engine.find(ENGINE_PATH)
+ if engine.paths['db/migrate'].existent
+ ActiveRecord::Tasks::DatabaseTasks.migrations_paths += engine.paths['db/migrate'].to_a
+ end
+ end
+ end
+ end
+
load "active_record/railties/databases.rake"
end
@@ -92,56 +118,6 @@ module ActiveRecord
initializer "active_record.set_configs" do |app|
ActiveSupport.on_load(:active_record) do
- begin
- old_behavior, ActiveSupport::Deprecation.behavior = ActiveSupport::Deprecation.behavior, :stderr
- whitelist_attributes = app.config.active_record.delete(:whitelist_attributes)
-
- if respond_to?(:mass_assignment_sanitizer=)
- mass_assignment_sanitizer = nil
- else
- mass_assignment_sanitizer = app.config.active_record.delete(:mass_assignment_sanitizer)
- end
-
- unless whitelist_attributes.nil? && mass_assignment_sanitizer.nil?
- ActiveSupport::Deprecation.warn <<-EOF.strip_heredoc, []
- Model based mass assignment security has been extracted
- out of Rails into a gem. Please use the new recommended protection model for
- params or add `protected_attributes` to your Gemfile to use the old one.
-
- To disable this message remove the `whitelist_attributes` option from your
- `config/application.rb` file and any `mass_assignment_sanitizer` options
- from your `config/environments/*.rb` files.
-
- See http://guides.rubyonrails.org/security.html#mass-assignment for more information.
- EOF
- end
-
- unless app.config.active_record.delete(:auto_explain_threshold_in_seconds).nil?
- ActiveSupport::Deprecation.warn <<-EOF.strip_heredoc, []
- The Active Record auto explain feature has been removed.
-
- To disable this message remove the `active_record.auto_explain_threshold_in_seconds`
- option from the `config/environments/*.rb` config file.
-
- See http://guides.rubyonrails.org/4_0_release_notes.html for more information.
- EOF
- end
-
- unless app.config.active_record.delete(:observers).nil?
- ActiveSupport::Deprecation.warn <<-EOF.strip_heredoc, []
- Active Record Observers has been extracted out of Rails into a gem.
- Please use callbacks or add `rails-observers` to your Gemfile to use observers.
-
- To disable this message remove the `observers` option from your
- `config/application.rb` or from your initializers.
-
- See http://guides.rubyonrails.org/4_0_release_notes.html for more information.
- EOF
- end
- ensure
- ActiveSupport::Deprecation.behavior = old_behavior
- end
-
app.config.active_record.each do |k,v|
send "#{k}=", v
end
@@ -152,6 +128,15 @@ module ActiveRecord
# and then establishes the connection.
initializer "active_record.initialize_database" do |app|
ActiveSupport.on_load(:active_record) do
+
+ class ActiveRecord::NoDatabaseError
+ remove_possible_method :extend_message
+ def extend_message(message)
+ message << "Run `$ bin/rake db:create db:migrate` to create your database"
+ message
+ end
+ end
+
self.configurations = app.config.database_configuration || {}
establish_connection
end
diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake
index 93bbeea5a3..25d42bbf2d 100644
--- a/activerecord/lib/active_record/railties/databases.rake
+++ b/activerecord/lib/active_record/railties/databases.rake
@@ -2,14 +2,8 @@ require 'active_record'
db_namespace = namespace :db do
task :load_config do
- ActiveRecord::Base.configurations = Rails.application.config.database_configuration || {}
- ActiveRecord::Migrator.migrations_paths = Rails.application.paths['db/migrate'].to_a
-
- if defined?(ENGINE_PATH) && engine = Rails::Engine.find(ENGINE_PATH)
- if engine.paths['db/migrate'].existent
- ActiveRecord::Migrator.migrations_paths += engine.paths['db/migrate'].to_a
- end
- end
+ ActiveRecord::Base.configurations = ActiveRecord::Tasks::DatabaseTasks.database_configuration || {}
+ ActiveRecord::Migrator.migrations_paths = ActiveRecord::Tasks::DatabaseTasks.migrations_paths
end
namespace :create do
@@ -18,13 +12,9 @@ db_namespace = namespace :db do
end
end
- desc 'Create the database from DATABASE_URL or config/database.yml for the current Rails.env (use db:create:all to create all dbs in the config)'
+ desc 'Create the database from DATABASE_URL or config/database.yml for the current Rails.env (use db:create:all to create all databases in the config)'
task :create => [:load_config] do
- if ENV['DATABASE_URL']
- ActiveRecord::Tasks::DatabaseTasks.create_database_url
- else
- ActiveRecord::Tasks::DatabaseTasks.create_current
- end
+ ActiveRecord::Tasks::DatabaseTasks.create_current
end
namespace :drop do
@@ -35,11 +25,7 @@ db_namespace = namespace :db do
desc 'Drops the database using DATABASE_URL or the current Rails.env (use db:drop:all to drop all databases)'
task :drop => [:load_config] do
- if ENV['DATABASE_URL']
- ActiveRecord::Tasks::DatabaseTasks.drop_database_url
- else
- ActiveRecord::Tasks::DatabaseTasks.drop_current
- end
+ ActiveRecord::Tasks::DatabaseTasks.drop_current
end
desc "Migrate the database (options: VERSION=x, VERBOSE=false, SCOPE=blog)."
@@ -156,7 +142,7 @@ db_namespace = namespace :db do
begin
puts ActiveRecord::Tasks::DatabaseTasks.collation_current
rescue NoMethodError
- $stderr.puts 'Sorry, your database adapter is not supported yet, feel free to submit a patch'
+ $stderr.puts 'Sorry, your database adapter is not supported yet. Feel free to submit a patch.'
end
end
@@ -170,7 +156,7 @@ db_namespace = namespace :db do
pending_migrations = ActiveRecord::Migrator.open(ActiveRecord::Migrator.migrations_paths).pending_migrations
if pending_migrations.any?
- puts "You have #{pending_migrations.size} pending migrations:"
+ puts "You have #{pending_migrations.size} pending #{pending_migrations.size > 1 ? 'migrations:' : 'migration:'}"
pending_migrations.each do |pending_migration|
puts ' %4d %s' % [pending_migration.version, pending_migration.name]
end
@@ -178,13 +164,13 @@ db_namespace = namespace :db do
end
end
- desc 'Create the database, load the schema, and initialize with the seed data (use db:reset to also drop the db first)'
+ desc 'Create the database, load the schema, and initialize with the seed data (use db:reset to also drop the database first)'
task :setup => ['db:schema:load_if_ruby', 'db:structure:load_if_sql', :seed]
desc 'Load the seed data from db/seeds.rb'
task :seed do
db_namespace['abort_if_pending_migrations'].invoke
- Rails.application.load_seed
+ ActiveRecord::Tasks::DatabaseTasks.load_seed
end
namespace :fixtures do
@@ -192,7 +178,12 @@ db_namespace = namespace :db do
task :load => [:environment, :load_config] do
require 'active_record/fixtures'
- base_dir = File.join [Rails.root, ENV['FIXTURES_PATH'] || %w{test fixtures}].flatten
+ base_dir = if ENV['FIXTURES_PATH']
+ File.join [Rails.root, ENV['FIXTURES_PATH'] || %w{test fixtures}].flatten
+ else
+ ActiveRecord::Tasks::DatabaseTasks.fixtures_path
+ end
+
fixtures_dir = File.join [base_dir, ENV['FIXTURES_DIR']].compact
(ENV['FIXTURES'] ? ENV['FIXTURES'].split(/,/) : Dir["#{fixtures_dir}/**/*.yml"].map {|f| f[(fixtures_dir.size + 1)..-5] }).each do |fixture_file|
@@ -209,7 +200,13 @@ db_namespace = namespace :db do
puts %Q(The fixture ID for "#{label}" is #{ActiveRecord::FixtureSet.identify(label)}.) if label
- base_dir = ENV['FIXTURES_PATH'] ? File.join(Rails.root, ENV['FIXTURES_PATH']) : File.join(Rails.root, 'test', 'fixtures')
+ base_dir = if ENV['FIXTURES_PATH']
+ File.join [Rails.root, ENV['FIXTURES_PATH'] || %w{test fixtures}].flatten
+ else
+ ActiveRecord::Tasks::DatabaseTasks.fixtures_path
+ end
+
+
Dir["#{base_dir}/**/*.yml"].each do |file|
if data = YAML::load(ERB.new(IO.read(file)).result)
data.keys.each do |key|
@@ -225,10 +222,10 @@ db_namespace = namespace :db do
end
namespace :schema do
- desc 'Create a db/schema.rb file that can be portably used against any DB supported by AR'
+ desc 'Create a db/schema.rb file that is portable against any DB supported by AR'
task :dump => [:environment, :load_config] do
require 'active_record/schema_dumper'
- filename = ENV['SCHEMA'] || "#{Rails.root}/db/schema.rb"
+ filename = ENV['SCHEMA'] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, 'schema.rb')
File.open(filename, "w:utf-8") do |file|
ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, file)
end
@@ -237,12 +234,7 @@ db_namespace = namespace :db do
desc 'Load a schema.rb file into the database'
task :load => [:environment, :load_config] do
- file = ENV['SCHEMA'] || "#{Rails.root}/db/schema.rb"
- if File.exists?(file)
- load(file)
- else
- abort %{#{file} doesn't exist yet. Run `rake db:migrate` to create it then try again. If you do not intend to use a database, you should instead alter #{Rails.root}/config/application.rb to limit the frameworks that will be loaded}
- end
+ ActiveRecord::Tasks::DatabaseTasks.load_schema(:ruby, ENV['SCHEMA'])
end
task :load_if_ruby => ['db:create', :environment] do
@@ -253,7 +245,7 @@ db_namespace = namespace :db do
desc 'Create a db/schema_cache.dump file.'
task :dump => [:environment, :load_config] do
con = ActiveRecord::Base.connection
- filename = File.join(Rails.application.config.paths["db"].first, "schema_cache.dump")
+ filename = File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "schema_cache.dump")
con.schema_cache.clear!
con.tables.each { |table| con.schema_cache.add(table) }
@@ -262,8 +254,8 @@ db_namespace = namespace :db do
desc 'Clear a db/schema_cache.dump file.'
task :clear => [:environment, :load_config] do
- filename = File.join(Rails.application.config.paths["db"].first, "schema_cache.dump")
- FileUtils.rm(filename) if File.exists?(filename)
+ filename = File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "schema_cache.dump")
+ FileUtils.rm(filename) if File.exist?(filename)
end
end
@@ -272,13 +264,14 @@ db_namespace = namespace :db do
namespace :structure do
desc 'Dump the database structure to db/structure.sql. Specify another file with DB_STRUCTURE=db/my_structure.sql'
task :dump => [:environment, :load_config] do
- filename = ENV['DB_STRUCTURE'] || File.join(Rails.root, "db", "structure.sql")
+ filename = ENV['DB_STRUCTURE'] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "structure.sql")
current_config = ActiveRecord::Tasks::DatabaseTasks.current_config
ActiveRecord::Tasks::DatabaseTasks.structure_dump(current_config, filename)
if ActiveRecord::Base.connection.supports_migrations?
File.open(filename, "a") do |f|
f.puts ActiveRecord::Base.connection.dump_schema_information
+ f.print "\n"
end
end
db_namespace['structure:dump'].reenable
@@ -286,9 +279,7 @@ db_namespace = namespace :db do
# desc "Recreate the databases from the structure.sql file"
task :load => [:environment, :load_config] do
- filename = ENV['DB_STRUCTURE'] || File.join(Rails.root, "db", "structure.sql")
- current_config = ActiveRecord::Tasks::DatabaseTasks.current_config
- ActiveRecord::Tasks::DatabaseTasks.structure_load(current_config, filename)
+ ActiveRecord::Tasks::DatabaseTasks.load_schema(:sql, ENV['DB_STRUCTURE'])
end
task :load_if_sql => ['db:create', :environment] do
@@ -298,8 +289,15 @@ db_namespace = namespace :db do
namespace :test do
+ task :deprecated do
+ Rake.application.top_level_tasks.grep(/^db:test:/).each do |task|
+ $stderr.puts "WARNING: #{task} is deprecated. The Rails test helper now maintains " \
+ "your test schema automatically, see the release notes for details."
+ end
+ end
+
# desc "Recreate the test database from the current schema"
- task :load => 'db:test:purge' do
+ task :load => %w(db:test:deprecated db:test:purge) do
case ActiveRecord::Base.schema_format
when :ruby
db_namespace["test:load_schema"].invoke
@@ -309,14 +307,21 @@ db_namespace = namespace :db do
end
# desc "Recreate the test database from an existent schema.rb file"
- task :load_schema => 'db:test:purge' do
- ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations['test'])
- ActiveRecord::Schema.verbose = false
- db_namespace["schema:load"].invoke
+ task :load_schema => %w(db:test:deprecated db:test:purge) do
+ begin
+ should_reconnect = ActiveRecord::Base.connection_pool.active_connection?
+ ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations['test'])
+ ActiveRecord::Schema.verbose = false
+ db_namespace["schema:load"].invoke
+ ensure
+ if should_reconnect
+ ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations[ActiveRecord::Tasks::DatabaseTasks.env])
+ end
+ end
end
# desc "Recreate the test database from an existent structure.sql file"
- task :load_structure => 'db:test:purge' do
+ task :load_structure => %w(db:test:deprecated db:test:purge) do
begin
ActiveRecord::Tasks::DatabaseTasks.current_config(:config => ActiveRecord::Base.configurations['test'])
db_namespace["structure:load"].invoke
@@ -326,7 +331,7 @@ db_namespace = namespace :db do
end
# desc "Recreate the test database from a fresh schema"
- task :clone do
+ task :clone => %w(db:test:deprecated environment) do
case ActiveRecord::Base.schema_format
when :ruby
db_namespace["test:clone_schema"].invoke
@@ -336,18 +341,18 @@ db_namespace = namespace :db do
end
# desc "Recreate the test database from a fresh schema.rb file"
- task :clone_schema => ["db:schema:dump", "db:test:load_schema"]
+ task :clone_schema => %w(db:test:deprecated db:schema:dump db:test:load_schema)
# desc "Recreate the test database from a fresh structure.sql file"
- task :clone_structure => [ "db:structure:dump", "db:test:load_structure" ]
+ task :clone_structure => %w(db:structure:dump db:test:load_structure)
# desc "Empty the test database"
- task :purge => [:environment, :load_config] do
+ task :purge => %w(db:test:deprecated environment load_config) do
ActiveRecord::Tasks::DatabaseTasks.purge ActiveRecord::Base.configurations['test']
end
# desc 'Check for pending migrations and load the test schema'
- task :prepare do
+ task :prepare => %w(db:test:deprecated environment load_config) do
unless ActiveRecord::Base.configurations.blank?
db_namespace['test:load'].invoke
end
@@ -373,7 +378,7 @@ namespace :railties do
puts "NOTE: Migration #{migration.basename} from #{name} has been skipped. Migration with the same name already exists."
end
- on_copy = Proc.new do |name, migration, old_path|
+ on_copy = Proc.new do |name, migration|
puts "Copied migration #{migration.basename} from #{name}"
end
@@ -382,6 +387,3 @@ namespace :railties do
end
end
end
-
-task 'test:prepare' => ['db:test:prepare', 'db:test:load_schema', 'db:abort_if_pending_migrations']
-
diff --git a/activerecord/lib/active_record/readonly_attributes.rb b/activerecord/lib/active_record/readonly_attributes.rb
index 8499bb16e7..b3ddfd63d4 100644
--- a/activerecord/lib/active_record/readonly_attributes.rb
+++ b/activerecord/lib/active_record/readonly_attributes.rb
@@ -20,11 +20,5 @@ module ActiveRecord
self._attr_readonly
end
end
-
- def _attr_readonly
- message = "Instance level _attr_readonly method is deprecated, please use class level method."
- ActiveSupport::Deprecation.warn message
- defined?(@_attr_readonly) ? @_attr_readonly : self.class._attr_readonly
- end
end
end
diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb
index 9403273db0..bce7766501 100644
--- a/activerecord/lib/active_record/reflection.rb
+++ b/activerecord/lib/active_record/reflection.rb
@@ -5,10 +5,31 @@ module ActiveRecord
included do
class_attribute :reflections
+ class_attribute :aggregate_reflections
self.reflections = {}
+ self.aggregate_reflections = {}
end
- # Reflection enables to interrogate Active Record classes and objects
+ def self.create(macro, name, scope, options, ar)
+ case macro
+ when :has_many, :belongs_to, :has_one
+ klass = options[:through] ? ThroughReflection : AssociationReflection
+ when :composed_of
+ klass = AggregateReflection
+ end
+
+ klass.new(macro, name, scope, options, ar)
+ end
+
+ def self.add_reflection(ar, name, reflection)
+ ar.reflections = ar.reflections.merge(name => reflection)
+ end
+
+ def self.add_aggregate_reflection(ar, name, reflection)
+ ar.aggregate_reflections = ar.aggregate_reflections.merge(name => reflection)
+ end
+
+ # \Reflection enables to interrogate Active Record classes and objects
# about their associations and aggregations. This information can,
# for example, be used in a form builder that takes an Active Record object
# and creates input fields for all of the attributes depending on their type
@@ -17,22 +38,9 @@ module ActiveRecord
# MacroReflection class has info for AggregateReflection and AssociationReflection
# classes.
module ClassMethods
- def create_reflection(macro, name, scope, options, active_record)
- case macro
- when :has_many, :belongs_to, :has_one, :has_and_belongs_to_many
- klass = options[:through] ? ThroughReflection : AssociationReflection
- reflection = klass.new(macro, name, scope, options, active_record)
- when :composed_of
- reflection = AggregateReflection.new(macro, name, scope, options, active_record)
- end
-
- self.reflections = self.reflections.merge(name => reflection)
- reflection
- end
-
# Returns an array of AggregateReflection objects for all the aggregations in the class.
def reflect_on_all_aggregations
- reflections.values.grep(AggregateReflection)
+ aggregate_reflections.values
end
# Returns the AggregateReflection object for the named +aggregation+ (use the symbol).
@@ -40,8 +48,7 @@ module ActiveRecord
# Account.reflect_on_aggregation(:balance) # => the balance AggregateReflection
#
def reflect_on_aggregation(aggregation)
- reflection = reflections[aggregation]
- reflection if reflection.is_a?(AggregateReflection)
+ aggregate_reflections[aggregation]
end
# Returns an array of AssociationReflection objects for all the
@@ -55,7 +62,7 @@ module ActiveRecord
# Account.reflect_on_all_associations(:has_many) # returns an array of all has_many associations
#
def reflect_on_all_associations(macro = nil)
- association_reflections = reflections.values.grep(AssociationReflection)
+ association_reflections = reflections.values
macro ? association_reflections.select { |reflection| reflection.macro == macro } : association_reflections
end
@@ -65,8 +72,7 @@ module ActiveRecord
# Invoice.reflect_on_association(:line_items).macro # returns :has_many
#
def reflect_on_association(association)
- reflection = reflections[association]
- reflection if reflection.is_a?(AssociationReflection)
+ reflections[association]
end
# Returns an array of AssociationReflection objects for all associations which have <tt>:autosave</tt> enabled.
@@ -75,8 +81,13 @@ module ActiveRecord
end
end
- # Abstract base class for AggregateReflection and AssociationReflection. Objects of
+ # Base class for AggregateReflection and AssociationReflection. Objects of
# AggregateReflection and AssociationReflection are returned by the Reflection::ClassMethods.
+ #
+ # MacroReflection
+ # AggregateReflection
+ # AssociationReflection
+ # ThroughReflection
class MacroReflection
# Returns the name of the macro.
#
@@ -95,7 +106,7 @@ module ActiveRecord
# Returns the hash of options used for the macro.
#
# <tt>composed_of :balance, class_name: 'Money'</tt> returns <tt>{ class_name: "Money" }</tt>
- # <tt>has_many :clients</tt> returns +{}+
+ # <tt>has_many :clients</tt> returns <tt>{}</tt>
attr_reader :options
attr_reader :active_record
@@ -108,10 +119,16 @@ module ActiveRecord
@scope = scope
@options = options
@active_record = active_record
+ @klass = options[:class]
@plural_name = active_record.pluralize_table_names ?
name.to_s.pluralize : name.to_s
end
+ def autosave=(autosave)
+ @automatic_inverse_of = false
+ @options[:autosave] = autosave
+ end
+
# Returns the class for the macro.
#
# <tt>composed_of :balance, class_name: 'Money'</tt> returns the Money class
@@ -173,9 +190,15 @@ module ActiveRecord
@klass ||= active_record.send(:compute_type, class_name)
end
- def initialize(*args)
+ attr_reader :type, :foreign_type
+
+ def initialize(macro, name, scope, options, active_record)
super
- @collection = [:has_many, :has_and_belongs_to_many].include?(macro)
+ @collection = :has_many == macro
+ @automatic_inverse_of = nil
+ @type = options[:as] && "#{options[:as]}_type"
+ @foreign_type = options[:foreign_type] || "#{name}_type"
+ @constructable = calculate_constructable(macro, options)
end
# Returns a new, unsaved instance of the associated class. +attributes+ will
@@ -184,12 +207,16 @@ module ActiveRecord
klass.new(attributes, &block)
end
+ def constructable? # :nodoc:
+ @constructable
+ end
+
def table_name
- @table_name ||= klass.table_name
+ klass.table_name
end
def quoted_table_name
- @quoted_table_name ||= klass.quoted_table_name
+ klass.quoted_table_name
end
def join_table
@@ -200,16 +227,8 @@ module ActiveRecord
@foreign_key ||= options[:foreign_key] || derive_foreign_key
end
- def foreign_type
- @foreign_type ||= options[:foreign_type] || "#{name}_type"
- end
-
- def type
- @type ||= options[:as] && "#{options[:as]}_type"
- end
-
def primary_key_column
- @primary_key_column ||= klass.columns.find { |c| c.name == klass.primary_key }
+ klass.columns_hash[klass.primary_key]
end
def association_foreign_key
@@ -233,20 +252,8 @@ module ActiveRecord
end
end
- def columns(tbl_name)
- @columns ||= klass.connection.columns(tbl_name)
- end
-
- def reset_column_information
- @columns = nil
- end
-
def check_validity!
check_validity_of_inverse!
-
- if has_and_belongs_to_many? && association_foreign_key == foreign_key
- raise HasAndBelongsToManyAssociationForeignKeyNeeded.new(self)
- end
end
def check_validity_of_inverse!
@@ -262,7 +269,7 @@ module ActiveRecord
end
def source_reflection
- nil
+ self
end
# A chain of reflections from this one back to the owner. For more see the explanation in
@@ -284,13 +291,13 @@ module ActiveRecord
alias :source_macro :macro
def has_inverse?
- @options[:inverse_of]
+ inverse_name
end
def inverse_of
- if has_inverse?
- @inverse_of ||= klass.reflect_on_association(options[:inverse_of])
- end
+ return unless inverse_name
+
+ @inverse_of ||= klass.reflect_on_association inverse_name
end
def polymorphic_inverse_of(associated_class)
@@ -328,10 +335,6 @@ module ActiveRecord
macro == :belongs_to
end
- def has_and_belongs_to_many?
- macro == :has_and_belongs_to_many
- end
-
def association_class
case macro
when :belongs_to
@@ -340,8 +343,6 @@ module ActiveRecord
else
Associations::BelongsToAssociation
end
- when :has_and_belongs_to_many
- Associations::HasAndBelongsToManyAssociation
when :has_many
if options[:through]
Associations::HasManyThroughAssociation
@@ -361,7 +362,92 @@ module ActiveRecord
options.key? :polymorphic
end
+ VALID_AUTOMATIC_INVERSE_MACROS = [:has_many, :has_one, :belongs_to]
+ INVALID_AUTOMATIC_INVERSE_OPTIONS = [:conditions, :through, :polymorphic, :foreign_key]
+
+ protected
+
+ def actual_source_reflection # FIXME: this is a horrible name
+ self
+ end
+
private
+
+ def calculate_constructable(macro, options)
+ case macro
+ when :belongs_to
+ !options[:polymorphic]
+ when :has_one
+ !options[:through]
+ else
+ true
+ end
+ end
+
+ # Attempts to find the inverse association name automatically.
+ # If it cannot find a suitable inverse association name, it returns
+ # nil.
+ def inverse_name
+ options.fetch(:inverse_of) do
+ if @automatic_inverse_of == false
+ nil
+ else
+ @automatic_inverse_of ||= automatic_inverse_of
+ end
+ end
+ end
+
+ # returns either nil or the inverse association name that it finds.
+ def automatic_inverse_of
+ if can_find_inverse_of_automatically?(self)
+ inverse_name = ActiveSupport::Inflector.underscore(active_record.name).to_sym
+
+ begin
+ reflection = klass.reflect_on_association(inverse_name)
+ rescue NameError
+ # Give up: we couldn't compute the klass type so we won't be able
+ # to find any associations either.
+ reflection = false
+ end
+
+ if valid_inverse_reflection?(reflection)
+ return inverse_name
+ end
+ end
+
+ false
+ end
+
+ # Checks if the inverse reflection that is returned from the
+ # +automatic_inverse_of+ method is a valid reflection. We must
+ # make sure that the reflection's active_record name matches up
+ # with the current reflection's klass name.
+ #
+ # Note: klass will always be valid because when there's a NameError
+ # from calling +klass+, +reflection+ will already be set to false.
+ def valid_inverse_reflection?(reflection)
+ reflection &&
+ klass.name == reflection.active_record.name &&
+ can_find_inverse_of_automatically?(reflection)
+ end
+
+ # Checks to see if the reflection doesn't have any options that prevent
+ # us from being able to guess the inverse automatically. First, the
+ # <tt>inverse_of</tt> option cannot be set to false. Second, we must
+ # have <tt>has_many</tt>, <tt>has_one</tt>, <tt>belongs_to</tt> associations.
+ # Third, we must not have options such as <tt>:polymorphic</tt> or
+ # <tt>:foreign_key</tt> which prevent us from correctly guessing the
+ # inverse association.
+ #
+ # Anything with a scope can additionally ruin our attempt at finding an
+ # inverse, so we exclude reflections with scopes.
+ def can_find_inverse_of_automatically?(reflection)
+ reflection.options[:inverse_of] != false &&
+ VALID_AUTOMATIC_INVERSE_MACROS.include?(reflection.macro) &&
+ !INVALID_AUTOMATIC_INVERSE_OPTIONS.any? { |opt| reflection.options[opt] } &&
+ !reflection.scope
+ end
+
def derive_class_name
class_name = name.to_s.camelize
class_name = class_name.singularize if collection?
@@ -393,7 +479,12 @@ module ActiveRecord
delegate :foreign_key, :foreign_type, :association_foreign_key,
:active_record_primary_key, :type, :to => :source_reflection
- # Gets the source of the through reflection. It checks both a singularized
+ def initialize(macro, name, scope, options, active_record)
+ super
+ @source_reflection_name = options[:source]
+ end
+
+ # Returns the source of the through reflection. It checks both a singularized
# and pluralized form for <tt>:belongs_to</tt> or <tt>:has_many</tt>.
#
# class Post < ActiveRecord::Base
@@ -407,12 +498,11 @@ module ActiveRecord
# end
#
# tags_reflection = Post.reflect_on_association(:tags)
- #
- # taggings_reflection = tags_reflection.source_reflection
+ # tags_reflection.source_reflection
# # => <ActiveRecord::Reflection::AssociationReflection: @macro=:belongs_to, @name=:tag, @active_record=Tagging, @plural_name="tags">
#
def source_reflection
- @source_reflection ||= source_reflection_names.collect { |name| through_reflection.klass.reflect_on_association(name) }.compact.first
+ through_reflection.klass.reflect_on_association(source_reflection_name)
end
# Returns the AssociationReflection object specified in the <tt>:through</tt> option
@@ -424,10 +514,11 @@ module ActiveRecord
# end
#
# tags_reflection = Post.reflect_on_association(:tags)
- # taggings_reflection = tags_reflection.through_reflection
+ # tags_reflection.through_reflection
+ # # => <ActiveRecord::Reflection::AssociationReflection: @macro=:has_many, @name=:taggings, @active_record=Post, @plural_name="taggings">
#
def through_reflection
- @through_reflection ||= active_record.reflect_on_association(options[:through])
+ active_record.reflect_on_association(options[:through])
end
# Returns an array of reflections which are involved in this association. Each item in the
@@ -449,7 +540,9 @@ module ActiveRecord
#
def chain
@chain ||= begin
- chain = source_reflection.chain + through_reflection.chain
+ a = source_reflection.chain
+ b = through_reflection.chain
+ chain = a + b
chain[0] = self # Use self so we don't lose the information from :source_type
chain
end
@@ -481,7 +574,7 @@ module ActiveRecord
# Add to it the scope from this reflection (if any)
scope_chain.first << scope if scope
- through_scope_chain = through_reflection.scope_chain
+ through_scope_chain = through_reflection.scope_chain.map(&:dup)
if options[:source_type]
through_scope_chain.first <<
@@ -500,7 +593,7 @@ module ActiveRecord
# A through association is nested if there would be more than one join table
def nested?
- chain.length > 2 || through_reflection.macro == :has_and_belongs_to_many
+ chain.length > 2
end
# We want to use the klass from this reflection, rather than just delegate straight to
@@ -509,12 +602,7 @@ module ActiveRecord
def association_primary_key(klass = nil)
# Get the "actual" source reflection if the immediate source reflection has a
# source reflection itself
- source_reflection = self.source_reflection
- while source_reflection.source_reflection
- source_reflection = source_reflection.source_reflection
- end
-
- source_reflection.options[:primary_key] || primary_key(klass || self.klass)
+ actual_source_reflection.options[:primary_key] || primary_key(klass || self.klass)
end
# Gets an array of possible <tt>:through</tt> source reflection names in both singular and plural form.
@@ -529,7 +617,32 @@ module ActiveRecord
# # => [:tag, :tags]
#
def source_reflection_names
- @source_reflection_names ||= (options[:source] ? [options[:source]] : [name.to_s.singularize, name]).collect { |n| n.to_sym }
+ (options[:source] ? [options[:source]] : [name.to_s.singularize, name]).collect { |n| n.to_sym }.uniq
+ end
+
+ def source_reflection_name # :nodoc:
+ return @source_reflection_name if @source_reflection_name
+
+ names = [name.to_s.singularize, name].collect { |n| n.to_sym }.uniq
+ names = names.find_all { |n|
+ through_reflection.klass.reflect_on_association(n)
+ }
+
+ if names.length > 1
+ example_options = options.dup
+ example_options[:source] = source_reflection_names.first
+ ActiveSupport::Deprecation.warn <<-eowarn
+Ambiguous source reflection for through association. Please specify a :source
+directive on your declaration like:
+
+ class #{active_record.name} < ActiveRecord::Base
+ #{macro} :#{name}, #{example_options}
+ end
+
+ eowarn
+ end
+
+ @source_reflection_name = names.first
end
def source_options
@@ -568,6 +681,12 @@ module ActiveRecord
check_validity_of_inverse!
end
+ protected
+
+ def actual_source_reflection # FIXME: this is a horrible name
+ source_reflection.actual_source_reflection
+ end
+
private
def derive_class_name
# get the class_name of the belongs_to association of the through reflection
diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb
index 56462d355b..745c6cf349 100644
--- a/activerecord/lib/active_record/relation.rb
+++ b/activerecord/lib/active_record/relation.rb
@@ -7,7 +7,7 @@ module ActiveRecord
MULTI_VALUE_METHODS = [:includes, :eager_load, :preload, :select, :group,
:order, :joins, :where, :having, :bind, :references,
- :extending]
+ :extending, :unscope]
SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :from, :reordering,
:reverse_order, :distinct, :create_with, :uniq]
@@ -17,18 +17,14 @@ module ActiveRecord
include FinderMethods, Calculations, SpawnMethods, QueryMethods, Batches, Explain, Delegation
attr_reader :table, :klass, :loaded
- attr_accessor :default_scoped, :proxy_association
alias :model :klass
alias :loaded? :loaded
- alias :default_scoped? :default_scoped
def initialize(klass, table, values = {})
- @klass = klass
- @table = table
- @values = values
- @implicit_readonly = nil
- @loaded = false
- @default_scoped = false
+ @klass = klass
+ @table = table
+ @values = values
+ @loaded = false
end
def initialize_copy(other)
@@ -39,7 +35,7 @@ module ActiveRecord
reset
end
- def insert(values)
+ def insert(values) # :nodoc:
primary_key_value = nil
if primary_key && Hash === values
@@ -56,16 +52,7 @@ module ActiveRecord
im = arel.create_insert
im.into @table
- conn = @klass.connection
-
- substitutes = values.sort_by { |arel_attr,_| arel_attr.name }
- binds = substitutes.map do |arel_attr, value|
- [@klass.columns_hash[arel_attr.name], value]
- end
-
- substitutes.each_with_index do |tuple, i|
- tuple[1] = conn.substitute_at(binds[i][0], i)
- end
+ substitutes, binds = substitute_values values
if values.empty? # empty insert
im.values = Arel.sql(connection.empty_insert_statement_value)
@@ -73,7 +60,7 @@ module ActiveRecord
im.insert substitutes
end
- conn.insert(
+ @klass.connection.insert(
im,
'SQL',
primary_key,
@@ -82,6 +69,29 @@ module ActiveRecord
binds)
end
+ def update_record(values, id, id_was) # :nodoc:
+ substitutes, binds = substitute_values values
+ um = @klass.unscoped.where(@klass.arel_table[@klass.primary_key].eq(id_was || id)).arel.compile_update(substitutes, @klass.primary_key)
+
+ @klass.connection.update(
+ um,
+ 'SQL',
+ binds)
+ end
+
+ def substitute_values(values) # :nodoc:
+ substitutes = values.sort_by { |arel_attr,_| arel_attr.name }
+ binds = substitutes.map do |arel_attr, value|
+ [@klass.columns_hash[arel_attr.name], value]
+ end
+
+ substitutes.each_with_index do |tuple, i|
+ tuple[1] = @klass.connection.substitute_at(binds[i][0], i)
+ end
+
+ [substitutes, binds]
+ end
+
# Initializes new record from relation while maintaining the current
# scope.
#
@@ -141,34 +151,58 @@ module ActiveRecord
first || new(attributes, &block)
end
- # Finds the first record with the given attributes, or creates a record with the attributes
- # if one is not found.
+ # Finds the first record with the given attributes, or creates a record
+ # with the attributes if one is not found:
#
- # ==== Examples
- # # Find the first user named Penélope or create a new one.
+ # # Find the first user named "Penélope" or create a new one.
# User.find_or_create_by(first_name: 'Penélope')
- # # => <User id: 1, first_name: 'Penélope', last_name: nil>
+ # # => #<User id: 1, first_name: "Penélope", last_name: nil>
#
- # # Find the first user named Penélope or create a new one.
+ # # Find the first user named "Penélope" or create a new one.
# # We already have one so the existing record will be returned.
# User.find_or_create_by(first_name: 'Penélope')
- # # => <User id: 1, first_name: 'Penélope', last_name: nil>
+ # # => #<User id: 1, first_name: "Penélope", last_name: nil>
#
- # # Find the first user named Scarlett or create a new one with a particular last name.
+ # # Find the first user named "Scarlett" or create a new one with
+ # # a particular last name.
# User.create_with(last_name: 'Johansson').find_or_create_by(first_name: 'Scarlett')
- # # => <User id: 2, first_name: 'Scarlett', last_name: 'Johansson'>
+ # # => #<User id: 2, first_name: "Scarlett", last_name: "Johansson">
#
- # # Find the first user named Scarlett or create a new one with a different last name.
- # # We already have one so the existing record will be returned.
+ # This method accepts a block, which is passed down to +create+. The last example
+ # above can be alternatively written this way:
+ #
+ # # Find the first user named "Scarlett" or create a new one with a
+ # # different last name.
# User.find_or_create_by(first_name: 'Scarlett') do |user|
- # user.last_name = "O'Hara"
+ # user.last_name = 'Johansson'
# end
- # # => <User id: 2, first_name: 'Scarlett', last_name: 'Johansson'>
+ # # => #<User id: 2, first_name: "Scarlett", last_name: "Johansson">
+ #
+ # This method always returns a record, but if creation was attempted and
+ # failed due to validation errors it won't be persisted, you get what
+ # +create+ returns in such situation.
+ #
+ # Please note *this method is not atomic*, it runs first a SELECT, and if
+ # there are no results an INSERT is attempted. If there are other threads
+ # or processes there is a race condition between both calls and it could
+ # be the case that you end up with two similar records.
+ #
+ # Whether that is a problem or not depends on the logic of the
+ # application, but in the particular case in which rows have a UNIQUE
+ # constraint an exception may be raised, just retry:
+ #
+ # begin
+ # CreditAccount.find_or_create_by(user_id: user.id)
+ # rescue ActiveRecord::RecordNotUnique
+ # retry
+ # end
+ #
def find_or_create_by(attributes, &block)
find_by(attributes) || create(attributes, &block)
end
- # Like <tt>find_or_create_by</tt>, but calls <tt>create!</tt> so an exception is raised if the created record is invalid.
+ # Like <tt>find_or_create_by</tt>, but calls <tt>create!</tt> so an exception
+ # is raised if the created record is invalid.
def find_or_create_by!(attributes, &block)
find_by(attributes) || create!(attributes, &block)
end
@@ -210,8 +244,13 @@ module ActiveRecord
def empty?
return @records.empty? if loaded?
- c = count
- c.respond_to?(:zero?) ? c.zero? : c.empty?
+ if limit_value == 0
+ true
+ else
+ # FIXME: This count is not compatible with #select('authors.*') or other select narrows
+ c = count
+ c.respond_to?(:zero?) ? c.zero? : c.empty?
+ end
end
# Returns true if there are any records.
@@ -276,7 +315,7 @@ module ActiveRecord
stmt.table(table)
stmt.key = table[primary_key]
- if with_default_scope.joins_values.any?
+ if joins_values.any?
@klass.connection.join_to_update(stmt, arel)
else
stmt.take(arel.limit)
@@ -401,7 +440,7 @@ module ActiveRecord
stmt = Arel::DeleteManager.new(arel.engine)
stmt.from(table)
- if with_default_scope.joins_values.any?
+ if joins_values.any?
@klass.connection.join_to_delete(stmt, arel, table[primary_key])
else
stmt.wheres = arel.constraints
@@ -467,7 +506,21 @@ module ActiveRecord
# User.where(name: 'Oscar').to_sql
# # => SELECT "users".* FROM "users" WHERE "users"."name" = 'Oscar'
def to_sql
- @to_sql ||= klass.connection.to_sql(arel, bind_values.dup)
+ @to_sql ||= begin
+ relation = self
+ connection = klass.connection
+ visitor = connection.visitor
+
+ if eager_loading?
+ find_with_associations { |rel| relation = rel }
+ end
+
+ ast = relation.arel.ast
+ binds = relation.bind_values.dup
+ visitor.accept(ast) do
+ connection.quote(*binds.shift.reverse)
+ end
+ end
end
# Returns a hash of where conditions.
@@ -475,11 +528,12 @@ module ActiveRecord
# User.where(name: 'Oscar').where_values_hash
# # => {name: "Oscar"}
def where_values_hash
- equalities = with_default_scope.where_values.grep(Arel::Nodes::Equality).find_all { |node|
+ equalities = where_values.grep(Arel::Nodes::Equality).find_all { |node|
node.left.relation.name == table_name
}
binds = Hash[bind_values.find_all(&:first).map { |column, v| [column.name, v] }]
+ binds.merge!(Hash[bind_values.find_all(&:first).map { |column, v| [column.name, v] }])
Hash[equalities.map { |where|
name = where.left.name
@@ -526,16 +580,6 @@ module ActiveRecord
q.pp(self.to_a)
end
- def with_default_scope #:nodoc:
- if default_scoped? && default_scope = klass.send(:build_default_scope)
- default_scope = default_scope.merge(self)
- default_scope.default_scoped = false
- default_scope
- else
- self
- end
- end
-
# Returns true if relation is blank.
def blank?
to_a.blank?
@@ -555,34 +599,24 @@ module ActiveRecord
private
def exec_queries
- default_scoped = with_default_scope
+ @records = eager_loading? ? find_with_associations : @klass.find_by_sql(arel, bind_values)
- if default_scoped.equal?(self)
- @records = eager_loading? ? find_with_associations : @klass.find_by_sql(arel, bind_values)
-
- preload = preload_values
- preload += includes_values unless eager_loading?
- preload.each do |associations|
- ActiveRecord::Associations::Preloader.new(@records, associations).run
- end
-
- # @readonly_value is true only if set explicitly. @implicit_readonly is true if there
- # are JOINS and no explicit SELECT.
- readonly = readonly_value.nil? ? @implicit_readonly : readonly_value
- @records.each { |record| record.readonly! } if readonly
- else
- @records = default_scoped.to_a
+ preload = preload_values
+ preload += includes_values unless eager_loading?
+ preloader = ActiveRecord::Associations::Preloader.new
+ preload.each do |associations|
+ preloader.preload @records, associations
end
+ @records.each { |record| record.readonly! } if readonly_value
+
@loaded = true
@records
end
def references_eager_loaded_tables?
joined_tables = arel.join_sources.map do |join|
- if join.is_a?(Arel::Nodes::StringJoin)
- tables_in_string(join.left)
- else
+ unless join.is_a?(Arel::Nodes::StringJoin)
[join.left.table_name, join.left.table_alias]
end
end
@@ -591,41 +625,8 @@ module ActiveRecord
# always convert table names to downcase as in Oracle quoted table names are in uppercase
joined_tables = joined_tables.flatten.compact.map { |t| t.downcase }.uniq
- string_tables = tables_in_string(to_sql)
- if (references_values - joined_tables).any?
- true
- elsif !ActiveRecord::Base.disable_implicit_join_references &&
- (string_tables - joined_tables).any?
- ActiveSupport::Deprecation.warn(
- "It looks like you are eager loading table(s) (one of: #{string_tables.join(', ')}) " \
- "that are referenced in a string SQL snippet. For example: \n" \
- "\n" \
- " Post.includes(:comments).where(\"comments.title = 'foo'\")\n" \
- "\n" \
- "Currently, Active Record recognizes the table in the string, and knows to JOIN the " \
- "comments table to the query, rather than loading comments in a separate query. " \
- "However, doing this without writing a full-blown SQL parser is inherently flawed. " \
- "Since we don't want to write an SQL parser, we are removing this functionality. " \
- "From now on, you must explicitly tell Active Record when you are referencing a table " \
- "from a string:\n" \
- "\n" \
- " Post.includes(:comments).where(\"comments.title = 'foo'\").references(:comments)\n" \
- "\n" \
- "If you don't rely on implicit join references you can disable the feature entirely " \
- "by setting `config.active_record.disable_implicit_join_references = true`."
- )
- true
- else
- false
- end
- end
-
- def tables_in_string(string)
- return [] if string.blank?
- # always convert table names to downcase as in Oracle quoted table names are in uppercase
- # ignore raw_sql_ that is used by Oracle adapter as alias for limit/offset subqueries
- string.scan(/([a-zA-Z_][.\w]+).?\./).flatten.map{ |s| s.downcase }.uniq - ['raw_sql_']
+ (references_values - joined_tables).any?
end
end
end
diff --git a/activerecord/lib/active_record/relation/batches.rb b/activerecord/lib/active_record/relation/batches.rb
index b921f2eddb..49b01909c6 100644
--- a/activerecord/lib/active_record/relation/batches.rb
+++ b/activerecord/lib/active_record/relation/batches.rb
@@ -11,7 +11,7 @@ module ActiveRecord
# The #find_each method uses #find_in_batches with a batch size of 1000 (or as
# specified by the +:batch_size+ option).
#
- # Person.all.find_each do |person|
+ # Person.find_each do |person|
# person.do_awesome_stuff
# end
#
@@ -19,47 +19,78 @@ module ActiveRecord
# person.party_all_night!
# end
#
- # You can also pass the +:start+ option to specify
- # an offset to control the starting point.
- def find_each(options = {})
- find_in_batches(options) do |records|
- records.each { |record| yield record }
- end
- end
-
- # Yields each batch of records that was found by the find +options+ as
- # an array. The size of each batch is set by the +:batch_size+
- # option; the default is 1000.
+ # If you do not provide a block to #find_each, it will return an Enumerator
+ # for chaining with other methods:
+ #
+ # Person.find_each.with_index do |person, index|
+ # person.award_trophy(index + 1)
+ # end
+ #
+ # ==== Options
+ # * <tt>:batch_size</tt> - Specifies the size of the batch. Default to 1000.
+ # * <tt>:start</tt> - Specifies the starting point for the batch processing.
+ # This is especially useful if you want multiple workers dealing with
+ # the same processing queue. You can make worker 1 handle all the records
+ # between id 0 and 10,000 and worker 2 handle from 10,000 and beyond
+ # (by setting the +:start+ option on that worker).
#
- # You can control the starting point for the batch processing by
- # supplying the +:start+ option. This is especially useful if you
- # want multiple workers dealing with the same processing queue. You can
- # make worker 1 handle all the records between id 0 and 10,000 and
- # worker 2 handle from 10,000 and beyond (by setting the +:start+
- # option on that worker).
+ # # Let's process for a batch of 2000 records, skipping the first 2000 rows
+ # Person.find_each(start: 2000, batch_size: 2000) do |person|
+ # person.party_all_night!
+ # end
#
- # It's not possible to set the order. That is automatically set to
+ # NOTE: It's not possible to set the order. That is automatically set to
# ascending on the primary key ("id ASC") to make the batch ordering
# work. This also means that this method only works with integer-based
- # primary keys. You can't set the limit either, that's used to control
+ # primary keys.
+ #
+ # NOTE: You can't set the limit either, that's used to control
# the batch sizes.
+ def find_each(options = {})
+ if block_given?
+ find_in_batches(options) do |records|
+ records.each { |record| yield record }
+ end
+ else
+ enum_for :find_each, options
+ end
+ end
+
+ # Yields each batch of records that was found by the find +options+ as
+ # an array.
#
# Person.where("age > 21").find_in_batches do |group|
# sleep(50) # Make sure it doesn't get too crowded in there!
# group.each { |person| person.party_all_night! }
# end
#
+ # ==== Options
+ # * <tt>:batch_size</tt> - Specifies the size of the batch. Default to 1000.
+ # * <tt>:start</tt> - Specifies the starting point for the batch processing.
+ # This is especially useful if you want multiple workers dealing with
+ # the same processing queue. You can make worker 1 handle all the records
+ # between id 0 and 10,000 and worker 2 handle from 10,000 and beyond
+ # (by setting the +:start+ option on that worker).
+ #
# # Let's process the next 2000 records
- # Person.all.find_in_batches(start: 2000, batch_size: 2000) do |group|
+ # Person.find_in_batches(start: 2000, batch_size: 2000) do |group|
# group.each { |person| person.party_all_night! }
# end
+ #
+ # NOTE: It's not possible to set the order. That is automatically set to
+ # ascending on the primary key ("id ASC") to make the batch ordering
+ # work. This also means that this method only works with integer-based
+ # primary keys.
+ #
+ # NOTE: You can't set the limit either, that's used to control
+ # the batch sizes.
def find_in_batches(options = {})
options.assert_valid_keys(:start, :batch_size)
relation = self
- unless arel.orders.blank? && arel.taken.blank?
- ActiveRecord::Base.logger.warn("Scoped order and limit are ignored, it's forced to be batch order and batch size")
+ if logger && (arel.orders.present? || arel.taken.present?)
+ logger.warn("Scoped order and limit are ignored, it's forced to be batch order and batch size")
end
start = options.delete(:start)
diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb
index 64e1ff9a6a..45ffb99868 100644
--- a/activerecord/lib/active_record/relation/calculations.rb
+++ b/activerecord/lib/active_record/relation/calculations.rb
@@ -20,6 +20,8 @@ module ActiveRecord
# Person.group(:city).count
# # => { 'Rome' => 5, 'Paris' => 3 }
def count(column_name = nil, options = {})
+ # TODO: Remove options argument as soon we remove support to
+ # activerecord-deprecated_finders.
column_name, options = nil, column_name if column_name.is_a?(Hash)
calculate(:count, column_name, options)
end
@@ -27,8 +29,10 @@ module ActiveRecord
# Calculates the average value on a given column. Returns +nil+ if there's
# no row. See +calculate+ for examples with options.
#
- # Person.average('age') # => 35.8
+ # Person.average(:age) # => 35.8
def average(column_name, options = {})
+ # TODO: Remove options argument as soon we remove support to
+ # activerecord-deprecated_finders.
calculate(:average, column_name, options)
end
@@ -36,8 +40,10 @@ module ActiveRecord
# with the same data type of the column, or +nil+ if there's no row. See
# +calculate+ for examples with options.
#
- # Person.minimum('age') # => 7
+ # Person.minimum(:age) # => 7
def minimum(column_name, options = {})
+ # TODO: Remove options argument as soon we remove support to
+ # activerecord-deprecated_finders.
calculate(:minimum, column_name, options)
end
@@ -45,8 +51,10 @@ module ActiveRecord
# with the same data type of the column, or +nil+ if there's no row. See
# +calculate+ for examples with options.
#
- # Person.maximum('age') # => 93
+ # Person.maximum(:age) # => 93
def maximum(column_name, options = {})
+ # TODO: Remove options argument as soon we remove support to
+ # activerecord-deprecated_finders.
calculate(:maximum, column_name, options)
end
@@ -54,17 +62,9 @@ module ActiveRecord
# with the same data type of the column, 0 if there's no row. See
# +calculate+ for examples with options.
#
- # Person.sum('age') # => 4562
+ # Person.sum(:age) # => 4562
def sum(*args)
- if block_given?
- ActiveSupport::Deprecation.warn(
- "Calling #sum with a block is deprecated and will be removed in Rails 4.1. " \
- "If you want to perform sum calculation over the array of elements, use `to_a.sum(&block)`."
- )
- self.to_a.sum(*args) {|*block_args| yield(*block_args)}
- else
- calculate(:sum, *args)
- end
+ calculate(:sum, *args)
end
# This calculates aggregate values in the given column. Methods for count, sum, average,
@@ -99,19 +99,17 @@ module ActiveRecord
#
# Person.sum("2 * age")
def calculate(operation, column_name, options = {})
- relation = with_default_scope
+ # TODO: Remove options argument as soon we remove support to
+ # activerecord-deprecated_finders.
+ if column_name.is_a?(Symbol) && attribute_alias?(column_name)
+ column_name = attribute_alias(column_name)
+ end
- if relation.equal?(self)
- if has_include?(column_name)
- construct_relation_for_association_calculations.calculate(operation, column_name, options)
- else
- perform_calculation(operation, column_name, options)
- end
+ if has_include?(column_name)
+ construct_relation_for_association_calculations.calculate(operation, column_name, options)
else
- relation.calculate(operation, column_name, options)
+ perform_calculation(operation, column_name, options)
end
- rescue ThrowResult
- 0
end
# Use <tt>pluck</tt> as a shortcut to select one or more attributes without
@@ -149,10 +147,10 @@ module ActiveRecord
#
def pluck(*column_names)
column_names.map! do |column_name|
- if column_name.is_a?(Symbol) && self.column_names.include?(column_name.to_s)
- "#{connection.quote_table_name(table_name)}.#{connection.quote_column_name(column_name)}"
+ if column_name.is_a?(Symbol) && attribute_alias?(column_name)
+ attribute_alias(column_name)
else
- column_name
+ column_name.to_s
end
end
@@ -160,22 +158,20 @@ module ActiveRecord
construct_relation_for_association_calculations.pluck(*column_names)
else
relation = spawn
- relation.select_values = column_names
+ relation.select_values = column_names.map { |cn|
+ columns_hash.key?(cn) ? arel_table[cn] : cn
+ }
result = klass.connection.select_all(relation.arel, nil, bind_values)
columns = result.columns.map do |key|
klass.column_types.fetch(key) {
- result.column_types.fetch(key) {
- Class.new { def type_cast(v); v; end }.new
- }
+ result.column_types.fetch(key) { result.identity_type }
}
end
result = result.map do |attributes|
values = klass.initialize_attributes(attributes).values
- columns.zip(values).map do |column, value|
- column.type_cast(value)
- end
+ columns.zip(values).map { |column, value| column.type_cast value }
end
columns.one? ? result.map!(&:first) : result
end
@@ -196,26 +192,22 @@ module ActiveRecord
end
def perform_calculation(operation, column_name, options = {})
+ # TODO: Remove options argument as soon we remove support to
+ # activerecord-deprecated_finders.
operation = operation.to_s.downcase
# If #count is used with #distinct / #uniq it is considered distinct. (eg. relation.distinct.count)
distinct = self.distinct_value
- if options.has_key?(:distinct)
- ActiveSupport::Deprecation.warn "The :distinct option for `Relation#count` is deprecated. " \
- "Please use `Relation#distinct` instead. (eg. `relation.distinct.count`)"
- distinct = options[:distinct]
- end
if operation == "count"
- column_name ||= (select_for_count || :all)
+ column_name ||= select_for_count
unless arel.ast.grep(Arel::Nodes::OuterJoin).empty?
distinct = true
end
column_name = primary_key if column_name == :all && distinct
-
- distinct = nil if column_name =~ /\s*DISTINCT\s+/i
+ distinct = nil if column_name =~ /\s*DISTINCT[\s(]+/i
end
if group_values.any?
@@ -332,7 +324,9 @@ module ActiveRecord
}
key = key.first if key.size == 1
key = key_records[key] if associated
- [key, type_cast_calculated_value(row[aggregate_alias], column_for(column_name), operation)]
+
+ column_type = calculated_data.column_types.fetch(aggregate_alias) { column_for(column_name) }
+ [key, type_cast_calculated_value(row[aggregate_alias], column_type, operation)]
end]
end
@@ -376,10 +370,12 @@ module ActiveRecord
column ? column.type_cast(value) : value
end
+ # TODO: refactor to allow non-string `select_values` (eg. Arel nodes).
def select_for_count
if select_values.present?
- select = select_values.join(", ")
- select if select !~ /[,*]/
+ select_values.join(", ")
+ else
+ :all
end
end
diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb
index 1b6239eb38..21beed332f 100644
--- a/activerecord/lib/active_record/relation/delegation.rb
+++ b/activerecord/lib/active_record/relation/delegation.rb
@@ -1,8 +1,35 @@
-require 'thread'
-require 'thread_safe'
+require 'set'
+require 'active_support/concern'
+require 'active_support/deprecation'
module ActiveRecord
module Delegation # :nodoc:
+ module DelegateCache
+ def relation_delegate_class(klass) # :nodoc:
+ @relation_delegate_cache[klass]
+ end
+
+ def initialize_relation_delegate_cache # :nodoc:
+ @relation_delegate_cache = cache = {}
+ [
+ ActiveRecord::Relation,
+ ActiveRecord::Associations::CollectionProxy,
+ ActiveRecord::AssociationRelation
+ ].each do |klass|
+ delegate = Class.new(klass) {
+ include ClassSpecificRelation
+ }
+ const_set klass.name.gsub('::', '_'), delegate
+ cache[klass] = delegate
+ end
+ end
+
+ def inherited(child_class)
+ child_class.initialize_relation_delegate_cache
+ super
+ end
+ end
+
extend ActiveSupport::Concern
# This module creates compiled delegation methods dynamically at runtime, which makes
@@ -10,18 +37,25 @@ module ActiveRecord
# may vary depending on the klass of a relation, so we create a subclass of Relation
# for each different klass, and the delegations are compiled into that subclass only.
- delegate :to_xml, :to_yaml, :length, :collect, :map, :each, :all?, :include?, :to_ary, :to => :to_a
+ BLACKLISTED_ARRAY_METHODS = [
+ :compact!, :flatten!, :reject!, :reverse!, :rotate!, :map!,
+ :shuffle!, :slice!, :sort!, :sort_by!, :delete_if,
+ :keep_if, :pop, :shift, :delete_at, :compact
+ ].to_set # :nodoc:
+
+ delegate :to_xml, :to_yaml, :length, :collect, :map, :each, :all?, :include?, :to_ary, to: :to_a
+
delegate :table_name, :quoted_table_name, :primary_key, :quoted_primary_key,
:connection, :columns_hash, :to => :klass
- module ClassSpecificRelation
+ module ClassSpecificRelation # :nodoc:
extend ActiveSupport::Concern
included do
@delegation_mutex = Mutex.new
end
- module ClassMethods
+ module ClassMethods # :nodoc:
def name
superclass.name
end
@@ -38,7 +72,7 @@ module ActiveRecord
RUBY
else
define_method method do |*args, &block|
- scoping { @klass.send(method, *args, &block) }
+ scoping { @klass.public_send(method, *args, &block) }
end
end
end
@@ -57,66 +91,47 @@ module ActiveRecord
def method_missing(method, *args, &block)
if @klass.respond_to?(method)
self.class.delegate_to_scoped_klass(method)
- scoping { @klass.send(method, *args, &block) }
- elsif Array.method_defined?(method)
- self.class.delegate method, :to => :to_a
- to_a.send(method, *args, &block)
+ scoping { @klass.public_send(method, *args, &block) }
elsif arel.respond_to?(method)
self.class.delegate method, :to => :arel
- arel.send(method, *args, &block)
+ arel.public_send(method, *args, &block)
else
super
end
end
end
- module ClassMethods
- @@subclasses = ThreadSafe::Cache.new(:initial_capacity => 2)
-
- def new(klass, *args)
- relation = relation_class_for(klass).allocate
- relation.__send__(:initialize, klass, *args)
- relation
- end
-
- # This doesn't have to be thread-safe. relation_class_for guarantees that this will only be
- # called exactly once for a given const name.
- def const_missing(name)
- const_set(name, Class.new(self) { include ClassSpecificRelation })
+ module ClassMethods # :nodoc:
+ def create(klass, *args)
+ relation_class_for(klass).new(klass, *args)
end
private
- # Cache the constants in @@subclasses because looking them up via const_get
- # make instantiation significantly slower.
+
def relation_class_for(klass)
- if klass && (klass_name = klass.name)
- my_cache = @@subclasses.compute_if_absent(self) { ThreadSafe::Cache.new }
- # This hash is keyed by klass.name to avoid memory leaks in development mode
- my_cache.compute_if_absent(klass_name) do
- # Cache#compute_if_absent guarantees that the block will only executed once for the given klass_name
- const_get("#{name.gsub('::', '_')}_#{klass_name.gsub('::', '_')}", false)
- end
- else
- ActiveRecord::Relation
- end
+ klass.relation_delegate_class(self)
end
end
def respond_to?(method, include_private = false)
- super || Array.method_defined?(method) ||
- @klass.respond_to?(method, include_private) ||
+ super || @klass.respond_to?(method, include_private) ||
+ array_delegable?(method) ||
arel.respond_to?(method, include_private)
end
protected
+ def array_delegable?(method)
+ Array.method_defined?(method) && BLACKLISTED_ARRAY_METHODS.exclude?(method)
+ end
+
def method_missing(method, *args, &block)
if @klass.respond_to?(method)
- scoping { @klass.send(method, *args, &block) }
- elsif Array.method_defined?(method)
- to_a.send(method, *args, &block)
+ scoping { @klass.public_send(method, *args, &block) }
+ elsif array_delegable?(method)
+ to_a.public_send(method, *args, &block)
elsif arel.respond_to?(method)
- arel.send(method, *args, &block)
+ arel.public_send(method, *args, &block)
else
super
end
diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb
index 72e9272cd7..3963f2b3e0 100644
--- a/activerecord/lib/active_record/relation/finder_methods.rb
+++ b/activerecord/lib/active_record/relation/finder_methods.rb
@@ -1,19 +1,24 @@
module ActiveRecord
module FinderMethods
+ ONE_AS_ONE = '1 AS one'
+
# Find by id - This can either be a specific id (1), a list of ids (1, 5, 6), or an array of ids ([5, 6, 10]).
# If no record can be found for all of the listed ids, then RecordNotFound will be raised. If the primary key
# is an integer, find by id coerces its arguments using +to_i+.
#
- # Person.find(1) # returns the object for ID = 1
- # Person.find("1") # returns the object for ID = 1
- # Person.find(1, 2, 6) # returns an array for objects with IDs in (1, 2, 6)
- # Person.find([7, 17]) # returns an array for objects with IDs in (7, 17)
- # Person.find([1]) # returns an array for the object with ID = 1
+ # Person.find(1) # returns the object for ID = 1
+ # Person.find("1") # returns the object for ID = 1
+ # Person.find("31-sarah") # returns the object for ID = 31
+ # Person.find(1, 2, 6) # returns an array for objects with IDs in (1, 2, 6)
+ # Person.find([7, 17]) # returns an array for objects with IDs in (7, 17)
+ # Person.find([1]) # returns an array for the object with ID = 1
# Person.where("administrator = 1").order("created_on DESC").find(1)
#
- # Note that returned records may not be in the same order as the ids you
- # provide since database rows are unordered. Give an explicit <tt>order</tt>
- # to ensure the results are sorted.
+ # <tt>ActiveRecord::RecordNotFound</tt> will be raised if one or more ids are not found.
+ #
+ # NOTE: The returned records may not be in the same order as the ids you
+ # provide since database rows are unordered. You'd need to provide an explicit <tt>order</tt>
+ # option if you want the results are sorted.
#
# ==== Find with lock
#
@@ -28,6 +33,34 @@ module ActiveRecord
# person.visits += 1
# person.save!
# end
+ #
+ # ==== Variations of +find+
+ #
+ # Person.where(name: 'Spartacus', rating: 4)
+ # # returns a chainable list (which can be empty).
+ #
+ # Person.find_by(name: 'Spartacus', rating: 4)
+ # # returns the first item or nil.
+ #
+ # Person.where(name: 'Spartacus', rating: 4).first_or_initialize
+ # # returns the first item or returns a new instance (requires you call .save to persist against the database).
+ #
+ # Person.where(name: 'Spartacus', rating: 4).first_or_create
+ # # returns the first item or creates it and returns it, available since Rails 3.2.1.
+ #
+ # ==== Alternatives for +find+
+ #
+ # Person.where(name: 'Spartacus', rating: 4).exists?(conditions = :none)
+ # # returns a boolean indicating if any record with the given conditions exist.
+ #
+ # Person.where(name: 'Spartacus', rating: 4).select("field1, field2, field3")
+ # # returns a chainable list of instances with only the mentioned fields.
+ #
+ # Person.where(name: 'Spartacus', rating: 4).ids
+ # # returns an Array of ids, available since Rails 3.2.1.
+ #
+ # Person.where(name: 'Spartacus', rating: 4).pluck(:field1, :field2)
+ # # returns an Array of the required fields, available since Rails 3.1.
def find(*args)
if block_given?
to_a.find { |*block_args| yield(*block_args) }
@@ -79,13 +112,22 @@ module ActiveRecord
# Person.where(["user_name = :u", { u: user_name }]).first
# Person.order("created_on DESC").offset(5).first
# Person.first(3) # returns the first three objects fetched by SELECT * FROM people LIMIT 3
+ #
+ # ==== Rails 3
+ #
+ # Person.first # SELECT "people".* FROM "people" LIMIT 1
+ #
+ # NOTE: Rails 3 may not order this query by the primary key and the order
+ # will depend on the database implementation. In order to ensure that behavior,
+ # use <tt>User.order(:id).first</tt> instead.
+ #
+ # ==== Rails 4
+ #
+ # Person.first # SELECT "people".* FROM "people" ORDER BY "people"."id" ASC LIMIT 1
+ #
def first(limit = nil)
if limit
- if order_values.empty? && primary_key
- order(arel_table[primary_key].asc).limit(limit).to_a
- else
- limit(limit).to_a
- end
+ find_first_with_limit(limit)
else
find_first
end
@@ -130,21 +172,21 @@ module ActiveRecord
last or raise RecordNotFound
end
- # Returns truthy if a record exists in the table that matches the +id+ or
- # conditions given, or falsy otherwise. The argument can take six forms:
+ # Returns +true+ if a record exists in the table that matches the +id+ or
+ # conditions given, or +false+ otherwise. The argument can take six forms:
#
# * Integer - Finds the record with this primary key.
# * String - Finds the record with a primary key corresponding to this
# string (such as <tt>'5'</tt>).
# * Array - Finds the record that matches these +find+-style conditions
- # (such as <tt>['color = ?', 'red']</tt>).
+ # (such as <tt>['name LIKE ?', "%#{query}%"]</tt>).
# * Hash - Finds the record that matches these +find+-style conditions
- # (such as <tt>{color: 'red'}</tt>).
+ # (such as <tt>{name: 'David'}</tt>).
# * +false+ - Returns always +false+.
# * No args - Returns +false+ if the table is empty, +true+ otherwise.
#
- # For more information about specifying conditions as a Hash or Array,
- # see the Conditions section in the introduction to ActiveRecord::Base.
+ # For more information about specifying conditions as a hash or array,
+ # see the Conditions section in the introduction to <tt>ActiveRecord::Base</tt>.
#
# Note: You can't pass in a condition as a string (like <tt>name =
# 'Jamie'</tt>), since it would be sanitized and then queried against
@@ -160,9 +202,10 @@ module ActiveRecord
conditions = conditions.id if Base === conditions
return false if !conditions
- join_dependency = construct_join_dependency_for_association_find
- relation = construct_relation_for_association_find(join_dependency)
- relation = relation.except(:select, :order).select("1 AS one").limit(1)
+ relation = apply_join_dependency(self, construct_join_dependency)
+ return false if ActiveRecord::NullRelation === relation
+
+ relation = relation.except(:select, :order).select(ONE_AS_ONE).limit(1)
case conditions
when Array, Hash
@@ -171,9 +214,7 @@ module ActiveRecord
relation = relation.where(table[primary_key].eq(conditions)) if conditions != :none
end
- connection.select_value(relation, "#{name} Exists", relation.bind_values)
- rescue ThrowResult
- false
+ connection.select_value(relation, "#{name} Exists", relation.bind_values) ? true : false
end
# This method is called whenever no records are found with either a single
@@ -198,64 +239,70 @@ module ActiveRecord
raise RecordNotFound, error
end
- protected
+ private
def find_with_associations
- join_dependency = construct_join_dependency_for_association_find
- relation = construct_relation_for_association_find(join_dependency)
- rows = connection.select_all(relation, 'SQL', relation.bind_values.dup)
- join_dependency.instantiate(rows)
- rescue ThrowResult
- []
- end
+ join_dependency = construct_join_dependency
- def construct_join_dependency_for_association_find
- including = (eager_load_values + includes_values).uniq
- ActiveRecord::Associations::JoinDependency.new(@klass, including, [])
+ aliases = join_dependency.aliases
+ relation = select aliases.columns
+ relation = apply_join_dependency(relation, join_dependency)
+
+ if block_given?
+ yield relation
+ else
+ if ActiveRecord::NullRelation === relation
+ []
+ else
+ rows = connection.select_all(relation.arel, 'SQL', relation.bind_values.dup)
+ join_dependency.instantiate(rows, aliases)
+ end
+ end
end
- def construct_relation_for_association_calculations
- including = (eager_load_values + includes_values).uniq
- join_dependency = ActiveRecord::Associations::JoinDependency.new(@klass, including, arel.froms.first)
- relation = except(:includes, :eager_load, :preload)
- apply_join_dependency(relation, join_dependency)
+ def construct_join_dependency(joins = [])
+ including = eager_load_values + includes_values
+ ActiveRecord::Associations::JoinDependency.new(@klass, including, joins)
end
- def construct_relation_for_association_find(join_dependency)
- relation = except(:includes, :eager_load, :preload, :select).select(join_dependency.columns)
- apply_join_dependency(relation, join_dependency)
+ def construct_relation_for_association_calculations
+ apply_join_dependency(self, construct_join_dependency(arel.froms.first))
end
def apply_join_dependency(relation, join_dependency)
- join_dependency.join_associations.each do |association|
- relation = association.join_relation(relation)
- end
-
- limitable_reflections = using_limitable_reflections?(join_dependency.reflections)
+ relation = relation.except(:includes, :eager_load, :preload)
+ relation = relation.joins join_dependency
- if !limitable_reflections && relation.limit_value
- limited_id_condition = construct_limited_ids_condition(relation.except(:select))
- relation = relation.where(limited_id_condition)
+ if using_limitable_reflections?(join_dependency.reflections)
+ relation
+ else
+ if relation.limit_value
+ limited_ids = limited_ids_for(relation)
+ limited_ids.empty? ? relation.none! : relation.where!(table[primary_key].in(limited_ids))
+ end
+ relation.except(:limit, :offset)
end
-
- relation = relation.except(:limit, :offset) unless limitable_reflections
-
- relation
end
- def construct_limited_ids_condition(relation)
- orders = relation.order_values.map { |val| val.presence }.compact
- values = @klass.connection.distinct("#{quoted_table_name}.#{primary_key}", orders)
+ def limited_ids_for(relation)
+ values = @klass.connection.columns_for_distinct(
+ "#{quoted_table_name}.#{quoted_primary_key}", relation.order_values)
- relation = relation.dup.select(values)
+ relation = relation.except(:select).select(values).distinct!
id_rows = @klass.connection.select_all(relation.arel, 'SQL', relation.bind_values)
- ids_array = id_rows.map {|row| row[primary_key]}
+ id_rows.map {|row| row[primary_key]}
+ end
- ids_array.empty? ? raise(ThrowResult) : table[primary_key].in(ids_array)
+ def using_limitable_reflections?(reflections)
+ reflections.none? { |r| r.collection? }
end
+ protected
+
def find_with_ids(*ids)
+ raise UnknownPrimaryKey.new(@klass) if primary_key.nil?
+
expects_array = ids.first.kind_of?(Array)
return ids.first if expects_array && ids.first.empty?
@@ -320,12 +367,15 @@ module ActiveRecord
if loaded?
@records.first
else
- @first ||=
- if with_default_scope.order_values.empty? && primary_key
- order(arel_table[primary_key].asc).limit(1).to_a.first
- else
- limit(1).to_a.first
- end
+ @first ||= find_first_with_limit(1).first
+ end
+ end
+
+ def find_first_with_limit(limit)
+ if order_values.empty? && primary_key
+ order(arel_table[primary_key].asc).limit(limit).to_a
+ else
+ limit(limit).to_a
end
end
@@ -334,16 +384,12 @@ module ActiveRecord
@records.last
else
@last ||=
- if offset_value || limit_value
+ if limit_value
to_a.last
else
reverse_order.limit(1).to_a.first
end
end
end
-
- def using_limitable_reflections?(reflections)
- reflections.none? { |r| r.collection? }
- end
end
end
diff --git a/activerecord/lib/active_record/relation/merger.rb b/activerecord/lib/active_record/relation/merger.rb
index eb23e92fb8..182b9ed89c 100644
--- a/activerecord/lib/active_record/relation/merger.rb
+++ b/activerecord/lib/active_record/relation/merger.rb
@@ -22,7 +22,7 @@ module ActiveRecord
# build a relation to merge in rather than directly merging
# the values.
def other
- other = Relation.new(relation.klass, relation.table)
+ other = Relation.create(relation.klass, relation.table)
hash.each { |k, v|
if k == :joins
if Hash === v
@@ -39,20 +39,17 @@ module ActiveRecord
end
class Merger # :nodoc:
- attr_reader :relation, :values
+ attr_reader :relation, :values, :other
def initialize(relation, other)
- if other.default_scoped? && other.klass != relation.klass
- other = other.with_default_scope
- end
-
@relation = relation
@values = other.values
+ @other = other
end
NORMAL_VALUES = Relation::SINGLE_VALUE_METHODS +
Relation::MULTI_VALUE_METHODS -
- [:where, :order, :bind, :reverse_order, :lock, :create_with, :reordering, :from] # :nodoc:
+ [:joins, :where, :order, :bind, :reverse_order, :lock, :create_with, :reordering, :from] # :nodoc:
def normal_values
NORMAL_VALUES
@@ -61,26 +58,78 @@ module ActiveRecord
def merge
normal_values.each do |name|
value = values[name]
- relation.send("#{name}!", *value) unless value.blank?
+ # The unless clause is here mostly for performance reasons (since the `send` call might be moderately
+ # expensive), most of the time the value is going to be `nil` or `.blank?`, the only catch is that
+ # `false.blank?` returns `true`, so there needs to be an extra check so that explicit `false` values
+ # don't fall through the cracks.
+ relation.send("#{name}!", *value) unless value.nil? || (value.blank? && false != value)
end
merge_multi_values
merge_single_values
+ merge_joins
relation
end
private
+ def merge_joins
+ return if values[:joins].blank?
+
+ if other.klass == relation.klass
+ relation.joins!(*values[:joins])
+ else
+ joins_dependency, rest = values[:joins].partition do |join|
+ case join
+ when Hash, Symbol, Array
+ true
+ else
+ false
+ end
+ end
+
+ join_dependency = ActiveRecord::Associations::JoinDependency.new(other.klass,
+ joins_dependency,
+ [])
+ relation.joins! rest
+
+ @relation = relation.joins join_dependency
+ end
+ end
+
def merge_multi_values
- relation.where_values = merged_wheres
- relation.bind_values = merged_binds
+ lhs_wheres = relation.where_values
+ rhs_wheres = values[:where] || []
+
+ lhs_binds = relation.bind_values
+ rhs_binds = values[:bind] || []
+
+ removed, kept = partition_overwrites(lhs_wheres, rhs_wheres)
+
+ where_values = kept + rhs_wheres
+ bind_values = filter_binds(lhs_binds, removed) + rhs_binds
+
+ conn = relation.klass.connection
+ bv_index = 0
+ where_values.map! do |node|
+ if Arel::Nodes::Equality === node && Arel::Nodes::BindParam === node.right
+ substitute = conn.substitute_at(bind_values[bv_index].first, bv_index)
+ bv_index += 1
+ Arel::Nodes::Equality.new(node.left, substitute)
+ else
+ node
+ end
+ end
+
+ relation.where_values = where_values
+ relation.bind_values = bind_values
if values[:reordering]
# override any order specified in the original relation
relation.reorder! values[:order]
elsif values[:order]
- # merge in order_values from r
+ # merge in order_values from relation
relation.order! values[:order]
end
@@ -97,35 +146,28 @@ module ActiveRecord
end
end
- def merged_binds
- if values[:bind]
- (relation.bind_values + values[:bind]).uniq(&:first)
- else
- relation.bind_values
- end
- end
+ def filter_binds(lhs_binds, removed_wheres)
+ return lhs_binds if removed_wheres.empty?
- def merged_wheres
- values[:where] ||= []
+ set = Set.new removed_wheres.map { |x| x.left.name }
+ lhs_binds.dup.delete_if { |col,_| set.include? col.name }
+ end
- if values[:where].empty? || relation.where_values.empty?
- relation.where_values + values[:where]
- else
- # Remove equalities from the existing relation with a LHS which is
- # present in the relation being merged in.
+ # Remove equalities from the existing relation with a LHS which is
+ # present in the relation being merged in.
+ # returns [things_to_remove, things_to_keep]
+ def partition_overwrites(lhs_wheres, rhs_wheres)
+ if lhs_wheres.empty? || rhs_wheres.empty?
+ return [[], lhs_wheres]
+ end
- seen = Set.new
- values[:where].each { |w|
- if w.respond_to?(:operator) && w.operator == :==
- seen << w.left
- end
- }
+ nodes = rhs_wheres.find_all do |w|
+ w.respond_to?(:operator) && w.operator == :==
+ end
+ seen = Set.new(nodes) { |node| node.left }
- relation.where_values.reject { |w|
- w.respond_to?(:operator) &&
- w.operator == :== &&
- seen.include?(w.left)
- } + values[:where]
+ lhs_wheres.partition do |w|
+ w.respond_to?(:operator) && w.operator == :== && seen.include?(w.left)
end
end
end
diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb
index f44d46d15b..1252af7635 100644
--- a/activerecord/lib/active_record/relation/predicate_builder.rb
+++ b/activerecord/lib/active_record/relation/predicate_builder.rb
@@ -1,5 +1,20 @@
module ActiveRecord
class PredicateBuilder # :nodoc:
+ @handlers = []
+
+ autoload :RelationHandler, 'active_record/relation/predicate_builder/relation_handler'
+ autoload :ArrayHandler, 'active_record/relation/predicate_builder/array_handler'
+
+ def self.resolve_column_aliases(klass, hash)
+ hash = hash.dup
+ hash.keys.grep(Symbol) do |key|
+ if klass.attribute_alias? key
+ hash[klass.attribute_alias(key)] = hash.delete key
+ end
+ end
+ hash
+ end
+
def self.build_from_hash(klass, attributes, default_table)
queries = []
@@ -40,9 +55,9 @@ module ActiveRecord
#
# For polymorphic relationships, find the foreign key and type:
# PriceEstimate.where(estimate_of: treasure)
- if klass && value.class < Base && reflection = klass.reflect_on_association(column.to_sym)
- if reflection.polymorphic?
- queries << build(table[reflection.foreign_type], value.class.base_class)
+ if klass && reflection = klass.reflect_on_association(column.to_sym)
+ if reflection.polymorphic? && base_class = polymorphic_base_class_from_value(value)
+ queries << build(table[reflection.foreign_type], base_class)
end
column = reflection.foreign_key
@@ -52,6 +67,18 @@ module ActiveRecord
queries
end
+ def self.polymorphic_base_class_from_value(value)
+ case value
+ when Relation
+ value.klass.base_class
+ when Array
+ val = value.compact.first
+ val.class.base_class if val.is_a?(Base)
+ when Base
+ value.class.base_class
+ end
+ end
+
def self.references(attributes)
attributes.map do |key, value|
if value.is_a?(Hash)
@@ -63,44 +90,36 @@ module ActiveRecord
end.compact
end
+ # Define how a class is converted to Arel nodes when passed to +where+.
+ # The handler can be any object that responds to +call+, and will be used
+ # for any value that +===+ the class given. For example:
+ #
+ # MyCustomDateRange = Struct.new(:start, :end)
+ # handler = proc do |column, range|
+ # Arel::Nodes::Between.new(column,
+ # Arel::Nodes::And.new([range.start, range.end])
+ # )
+ # end
+ # ActiveRecord::PredicateBuilder.register_handler(MyCustomDateRange, handler)
+ def self.register_handler(klass, handler)
+ @handlers.unshift([klass, handler])
+ end
+
+ register_handler(BasicObject, ->(attribute, value) { attribute.eq(value) })
+ # FIXME: I think we need to deprecate this behavior
+ register_handler(Class, ->(attribute, value) { attribute.eq(value.name) })
+ register_handler(Base, ->(attribute, value) { attribute.eq(value.id) })
+ register_handler(Range, ->(attribute, value) { attribute.in(value) })
+ register_handler(Relation, RelationHandler.new)
+ register_handler(Array, ArrayHandler.new)
+
private
def self.build(attribute, value)
- case value
- when Array
- values = value.to_a.map {|x| x.is_a?(Base) ? x.id : x}
- ranges, values = values.partition {|v| v.is_a?(Range)}
-
- values_predicate = if values.include?(nil)
- values = values.compact
-
- case values.length
- when 0
- attribute.eq(nil)
- when 1
- attribute.eq(values.first).or(attribute.eq(nil))
- else
- attribute.in(values).or(attribute.eq(nil))
- end
- else
- attribute.in(values)
- end
+ handler_for(value).call(attribute, value)
+ end
- array_predicates = ranges.map { |range| attribute.in(range) }
- array_predicates << values_predicate
- array_predicates.inject { |composite, predicate| composite.or(predicate) }
- when ActiveRecord::Relation
- value = value.select(value.klass.arel_table[value.klass.primary_key]) if value.select_values.empty?
- attribute.in(value.arel.ast)
- when Range
- attribute.in(value)
- when ActiveRecord::Base
- attribute.eq(value.id)
- when Class
- # FIXME: I think we need to deprecate this behavior
- attribute.eq(value.name)
- else
- attribute.eq(value)
- end
+ def self.handler_for(object)
+ @handlers.detect { |klass, _| klass === object }.last
end
end
end
diff --git a/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb
new file mode 100644
index 0000000000..2f6c34ac08
--- /dev/null
+++ b/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb
@@ -0,0 +1,29 @@
+module ActiveRecord
+ class PredicateBuilder
+ class ArrayHandler # :nodoc:
+ def call(attribute, value)
+ values = value.map { |x| x.is_a?(Base) ? x.id : x }
+ ranges, values = values.partition { |v| v.is_a?(Range) }
+
+ values_predicate = if values.include?(nil)
+ values = values.compact
+
+ case values.length
+ when 0
+ attribute.eq(nil)
+ when 1
+ attribute.eq(values.first).or(attribute.eq(nil))
+ else
+ attribute.in(values).or(attribute.eq(nil))
+ end
+ else
+ attribute.in(values)
+ end
+
+ array_predicates = ranges.map { |range| attribute.in(range) }
+ array_predicates << values_predicate
+ array_predicates.inject { |composite, predicate| composite.or(predicate) }
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/relation/predicate_builder/relation_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/relation_handler.rb
new file mode 100644
index 0000000000..618fa3cdd9
--- /dev/null
+++ b/activerecord/lib/active_record/relation/predicate_builder/relation_handler.rb
@@ -0,0 +1,13 @@
+module ActiveRecord
+ class PredicateBuilder
+ class RelationHandler # :nodoc:
+ def call(attribute, value)
+ if value.select_values.empty?
+ value = value.select(value.klass.arel_table[value.klass.primary_key])
+ end
+
+ attribute.in(value.arel.ast)
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb
index 257221174b..78da6a83ec 100644
--- a/activerecord/lib/active_record/relation/query_methods.rb
+++ b/activerecord/lib/active_record/relation/query_methods.rb
@@ -34,10 +34,11 @@ module ActiveRecord
#
# User.where.not(name: "Jon", role: "admin")
# # SELECT * FROM users WHERE name != 'Jon' AND role != 'admin'
- #
def not(opts, *rest)
where_value = @scope.send(:build_where, opts, rest).map do |rel|
case rel
+ when NilClass
+ raise ArgumentError, 'Invalid argument for .where.not(), got nil.'
when Arel::Nodes::In
Arel::Nodes::NotIn.new(rel.left, rel.right)
when Arel::Nodes::Equality
@@ -101,6 +102,14 @@ module ActiveRecord
# firing an additional query. This will often result in a
# performance improvement over a simple +join+.
#
+ # You can also specify multiple relationships, like this:
+ #
+ # users = User.includes(:address, :friends)
+ #
+ # Loading nested relationships is possible using a Hash:
+ #
+ # users = User.includes(:address, friends: [:address, :followers])
+ #
# === conditions
#
# If you want to add conditions to your included models you'll have
@@ -112,14 +121,15 @@ module ActiveRecord
#
# User.includes(:posts).where('posts.name = ?', 'example').references(:posts)
def includes(*args)
- check_if_method_has_arguments!("includes", args)
+ check_if_method_has_arguments!(:includes, args)
spawn.includes!(*args)
end
def includes!(*args) # :nodoc:
- args.reject! {|a| a.blank? }
+ args.reject!(&:blank?)
+ args.flatten!
- self.includes_values = (includes_values + args).flatten.uniq
+ self.includes_values |= args
self
end
@@ -130,7 +140,7 @@ module ActiveRecord
# FROM "users" LEFT OUTER JOIN "posts" ON "posts"."user_id" =
# "users"."id"
def eager_load(*args)
- check_if_method_has_arguments!("eager_load", args)
+ check_if_method_has_arguments!(:eager_load, args)
spawn.eager_load!(*args)
end
@@ -144,7 +154,7 @@ module ActiveRecord
# User.preload(:posts)
# => SELECT "posts".* FROM "posts" WHERE "posts"."user_id" IN (1, 2, 3)
def preload(*args)
- check_if_method_has_arguments!("preload", args)
+ check_if_method_has_arguments!(:preload, args)
spawn.preload!(*args)
end
@@ -162,14 +172,15 @@ module ActiveRecord
# User.includes(:posts).where("posts.name = 'foo'").references(:posts)
# # => Query now knows the string references posts, so adds a JOIN
def references(*args)
- check_if_method_has_arguments!("references", args)
+ check_if_method_has_arguments!(:references, args)
spawn.references!(*args)
end
def references!(*args) # :nodoc:
args.flatten!
+ args.map!(&:to_s)
- self.references_values = (references_values + args.map!(&:to_s)).uniq
+ self.references_values |= args
self
end
@@ -222,7 +233,9 @@ module ActiveRecord
end
def select!(*fields) # :nodoc:
- self.select_values += fields.flatten
+ fields.flatten!
+
+ self.select_values += fields
self
end
@@ -242,7 +255,7 @@ module ActiveRecord
# User.group('name AS grouped_name, age')
# => [#<User id: 3, name: "Foo", age: 21, ...>, #<User id: 2, name: "Oscar", age: 21, ...>, #<User id: 5, name: "Foo", age: 23, ...>]
def group(*args)
- check_if_method_has_arguments!("group", args)
+ check_if_method_has_arguments!(:group, args)
spawn.group!(*args)
end
@@ -273,24 +286,14 @@ module ActiveRecord
# User.order(:name, email: :desc)
# => SELECT "users".* FROM "users" ORDER BY "users"."name" ASC, "users"."email" DESC
def order(*args)
- check_if_method_has_arguments!("order", args)
+ check_if_method_has_arguments!(:order, args)
spawn.order!(*args)
end
def order!(*args) # :nodoc:
- args.flatten!
- validate_order_args args
-
- references = args.reject { |arg| Arel::Node === arg }
- references.map! { |arg| arg =~ /^([a-zA-Z]\w*)\.(\w+)/ && $1 }.compact!
- references!(references) if references.any?
+ preprocess_order_args(args)
- # if a symbol is given we prepend the quoted table name
- args = args.map { |arg|
- arg.is_a?(Symbol) ? "#{quoted_table_name}.#{arg} ASC" : arg
- }
-
- self.order_values = args + self.order_values
+ self.order_values += args
self
end
@@ -302,15 +305,14 @@ module ActiveRecord
#
# User.order('email DESC').reorder('id ASC').order('name ASC')
#
- # generates a query with 'ORDER BY name ASC, id ASC'.
+ # generates a query with 'ORDER BY id ASC, name ASC'.
def reorder(*args)
- check_if_method_has_arguments!("reorder", args)
+ check_if_method_has_arguments!(:reorder, args)
spawn.reorder!(*args)
end
def reorder!(*args) # :nodoc:
- args.flatten!
- validate_order_args args
+ preprocess_order_args(args)
self.reordering_value = true
self.order_values = args
@@ -341,20 +343,27 @@ module ActiveRecord
# User.where(name: "John", active: true).unscope(where: :name)
# == User.where(active: true)
#
- # Note that this method is more generalized than ActiveRecord::SpawnMethods#except
- # because #except will only affect a particular relation's values. It won't wipe
- # the order, grouping, etc. when that relation is merged. For example:
+ # This method is similar to <tt>except</tt>, but unlike
+ # <tt>except</tt>, it persists across merges:
#
- # Post.comments.except(:order)
+ # User.order('email').merge(User.except(:order))
+ # == User.order('email')
+ #
+ # User.order('email').merge(User.unscope(:order))
+ # == User.all
+ #
+ # This means it can be used in association definitions:
+ #
+ # has_many :comments, -> { unscope where: :trashed }
#
- # will still have an order if it comes from the default_scope on Comment.
def unscope(*args)
- check_if_method_has_arguments!("unscope", args)
+ check_if_method_has_arguments!(:unscope, args)
spawn.unscope!(*args)
end
- def unscope!(*args)
+ def unscope!(*args) # :nodoc:
args.flatten!
+ self.unscope_values += args
args.each do |scope|
case scope
@@ -388,8 +397,12 @@ module ActiveRecord
# User.joins("LEFT JOIN bookmarks ON bookmarks.bookmarkable_type = 'Post' AND bookmarks.user_id = users.id")
# => SELECT "users".* FROM "users" LEFT JOIN bookmarks ON bookmarks.bookmarkable_type = 'Post' AND bookmarks.user_id = users.id
def joins(*args)
- check_if_method_has_arguments!("joins", args)
- spawn.joins!(*args.compact.flatten)
+ check_if_method_has_arguments!(:joins, args)
+
+ args.compact!
+ args.flatten!
+
+ spawn.joins!(*args)
end
def joins!(*args) # :nodoc:
@@ -416,7 +429,7 @@ module ActiveRecord
# === string
#
# A single string, without additional arguments, is passed to the query
- # constructor as a SQL fragment, and used in the where clause of the query.
+ # constructor as an SQL fragment, and used in the where clause of the query.
#
# Client.where("orders_count = '2'")
# # SELECT * from clients where orders_count = '2';
@@ -546,13 +559,24 @@ module ActiveRecord
end
end
+ # Allows you to change a previously set where condition for a given attribute, instead of appending to that condition.
+ #
+ # Post.where(trashed: true).where(trashed: false) # => WHERE `trashed` = 1 AND `trashed` = 0
+ # Post.where(trashed: true).rewhere(trashed: false) # => WHERE `trashed` = 0
+ # Post.where(active: true).where(trashed: true).rewhere(trashed: false) # => WHERE `active` = 1 AND `trashed` = 0
+ #
+ # This is short-hand for unscope(where: conditions.keys).where(conditions). Note that unlike reorder, we're only unscoping
+ # the named conditions -- not the entire where statement.
+ def rewhere(conditions)
+ unscope(where: conditions.keys).where(conditions)
+ end
+
# Allows to specify a HAVING clause. Note that you can't use HAVING
# without also specifying a GROUP clause.
#
# Order.having('SUM(price) > 30').group('user_id')
def having(opts, *rest)
opts.blank? ? self : spawn.having!(opts, *rest)
- spawn.having!(opts, *rest)
end
def having!(opts, *rest) # :nodoc:
@@ -609,12 +633,11 @@ module ActiveRecord
self
end
- # Returns a chainable relation with zero records, specifically an
- # instance of the <tt>ActiveRecord::NullRelation</tt> class.
+ # Returns a chainable relation with zero records.
#
- # The returned <tt>ActiveRecord::NullRelation</tt> inherits from Relation and implements the
- # Null Object pattern. It is an object with defined null behavior and always returns an empty
- # array of records without querying the database.
+ # The returned relation implements the Null Object pattern. It is an
+ # object with defined null behavior and always returns an empty array of
+ # records without querying the database.
#
# Any subsequent condition chained to the returned relation will continue
# generating an empty relation and will not fire any query to the database.
@@ -634,7 +657,7 @@ module ActiveRecord
# when 'Reviewer'
# Post.published
# when 'Bad User'
- # Post.none # => returning [] instead breaks the previous code
+ # Post.none # It can't be chained if [] is returned.
# end
# end
#
@@ -686,7 +709,7 @@ module ActiveRecord
# Specifies table from which the records will be fetched. For example:
#
# Topic.select('title').from('posts')
- # #=> SELECT title FROM posts
+ # # => SELECT title FROM posts
#
# Can accept other relation objects. For example:
#
@@ -772,9 +795,10 @@ module ActiveRecord
end
def extending!(*modules, &block) # :nodoc:
- modules << Module.new(&block) if block_given?
+ modules << Module.new(&block) if block
+ modules.flatten!
- self.extending_values += modules.flatten
+ self.extending_values += modules
extend(*extending_values) if extending_values.any?
self
@@ -794,23 +818,23 @@ module ActiveRecord
# Returns the Arel object associated with the relation.
def arel
- @arel ||= with_default_scope.build_arel
+ @arel ||= build_arel
end
# Like #arel, but ignores the default scope of the model.
def build_arel
arel = Arel::SelectManager.new(table.engine, table)
- build_joins(arel, joins_values) unless joins_values.empty?
+ build_joins(arel, joins_values.flatten) unless joins_values.empty?
collapse_wheres(arel, (where_values - ['']).uniq)
- arel.having(*having_values.uniq.reject{|h| h.blank?}) unless having_values.empty?
+ arel.having(*having_values.uniq.reject(&:blank?)) unless having_values.empty?
arel.take(connection.sanitize_limit(limit_value)) if limit_value
arel.skip(offset_value.to_i) if offset_value
- arel.group(*group_values.uniq.reject{|g| g.blank?}) unless group_values.empty?
+ arel.group(*group_values.uniq.reject(&:blank?)) unless group_values.empty?
build_order(arel)
@@ -849,7 +873,7 @@ module ActiveRecord
where_values.reject! do |rel|
case rel
- when Arel::Nodes::In, Arel::Nodes::Equality
+ when Arel::Nodes::In, Arel::Nodes::NotIn, Arel::Nodes::Equality, Arel::Nodes::NotEqual
subrelation = (rel.left.kind_of?(Arel::Attributes::Attribute) ? rel.left : rel.right)
subrelation.name.to_sym == target_value_sym
else
@@ -859,13 +883,11 @@ module ActiveRecord
end
def custom_join_ast(table, joins)
- joins = joins.reject { |join| join.blank? }
+ joins = joins.reject(&:blank?)
return [] if joins.empty?
- @implicit_readonly = true
-
- joins.map do |join|
+ joins.map! do |join|
case join
when Array
join = Arel.sql(join.join(' ')) if array_of_strings?(join)
@@ -877,21 +899,28 @@ module ActiveRecord
end
def collapse_wheres(arel, wheres)
- equalities = wheres.grep(Arel::Nodes::Equality)
-
- arel.where(Arel::Nodes::And.new(equalities)) unless equalities.empty?
-
- (wheres - equalities).each do |where|
+ predicates = wheres.map do |where|
+ next where if ::Arel::Nodes::Equality === where
where = Arel.sql(where) if String === where
- arel.where(Arel::Nodes::Grouping.new(where))
+ Arel::Nodes::Grouping.new(where)
end
+
+ arel.where(Arel::Nodes::And.new(predicates)) if predicates.present?
end
def build_where(opts, other = [])
case opts
when String, Array
+ #TODO: Remove duplication with: /activerecord/lib/active_record/sanitization.rb:113
+ values = Hash === other.first ? other.first.values : other
+
+ values.grep(ActiveRecord::Relation) do |rel|
+ self.bind_values += rel.bind_values
+ end
+
[@klass.send(:sanitize_sql, other.empty? ? opts : ([opts] + other))]
when Hash
+ opts = PredicateBuilder.resolve_column_aliases(klass, opts)
attributes = @klass.send(:expand_hash_conditions_for_aggregates, opts)
attributes.values.grep(ActiveRecord::Relation) do |rel|
@@ -909,6 +938,7 @@ module ActiveRecord
case opts
when Relation
name ||= 'subquery'
+ self.bind_values = opts.bind_values + self.bind_values
opts.arel.as(name.to_s)
else
opts
@@ -922,7 +952,7 @@ module ActiveRecord
:string_join
when Hash, Symbol, Array
:association_join
- when ActiveRecord::Associations::JoinDependency::JoinAssociation
+ when ActiveRecord::Associations::JoinDependency
:stashed_join
when Arel::Nodes::Join
:join_node
@@ -934,9 +964,7 @@ module ActiveRecord
association_joins = buckets[:association_join] || []
stashed_association_joins = buckets[:stashed_join] || []
join_nodes = (buckets[:join_node] || []).uniq
- string_joins = (buckets[:string_join] || []).map { |x|
- x.strip
- }.uniq
+ string_joins = (buckets[:string_join] || []).map(&:strip).uniq
join_list = join_nodes + custom_join_ast(manager, string_joins)
@@ -946,24 +974,20 @@ module ActiveRecord
join_list
)
- join_dependency.graft(*stashed_association_joins)
-
- @implicit_readonly = true unless association_joins.empty? && stashed_association_joins.empty?
+ joins = join_dependency.join_constraints stashed_association_joins
- # FIXME: refactor this to build an AST
- join_dependency.join_associations.each do |association|
- association.join_to(manager)
- end
+ joins.each { |join| manager.from(join) }
- manager.join_sources.concat join_list
+ manager.join_sources.concat(join_list)
manager
end
def build_select(arel, selects)
- unless selects.empty?
- @implicit_readonly = false
+ if !selects.empty?
arel.project(*selects)
+ elsif from_value
+ arel.project(Arel.star)
else
arel.project(@klass.arel_table[Arel.star])
end
@@ -977,16 +1001,10 @@ module ActiveRecord
when Arel::Nodes::Ordering
o.reverse
when String
- o.to_s.split(',').collect do |s|
+ o.to_s.split(',').map! do |s|
s.strip!
s.gsub!(/\sasc\Z/i, ' DESC') || s.gsub!(/\sdesc\Z/i, ' ASC') || s.concat(' DESC')
end
- when Symbol
- { o => :desc }
- when Hash
- o.each_with_object({}) do |(field, dir), memo|
- memo[field] = (dir == :asc ? :desc : :asc )
- end
else
o
end
@@ -994,35 +1012,48 @@ module ActiveRecord
end
def array_of_strings?(o)
- o.is_a?(Array) && o.all?{|obj| obj.is_a?(String)}
+ o.is_a?(Array) && o.all? { |obj| obj.is_a?(String) }
end
def build_order(arel)
- orders = order_values
+ orders = order_values.uniq
+ orders.reject!(&:blank?)
orders = reverse_sql_order(orders) if reverse_order_value
- orders = orders.uniq.reject(&:blank?).flat_map do |order|
- case order
- when Symbol
- table[order].asc
- when Hash
- order.map { |field, dir| table[field].send(dir) }
- else
- order
- end
- end
-
arel.order(*orders) unless orders.empty?
end
def validate_order_args(args)
- args.select { |a| Hash === a }.each do |h|
+ args.grep(Hash) do |h|
unless (h.values - [:asc, :desc]).empty?
raise ArgumentError, 'Direction should be :asc or :desc'
end
end
end
+ def preprocess_order_args(order_args)
+ order_args.flatten!
+ validate_order_args(order_args)
+
+ references = order_args.grep(String)
+ references.map! { |arg| arg =~ /^([a-zA-Z]\w*)\.(\w+)/ && $1 }.compact!
+ references!(references) if references.any?
+
+ # if a symbol is given we prepend the quoted table name
+ order_args.map! do |arg|
+ case arg
+ when Symbol
+ table[arg].asc
+ when Hash
+ arg.map { |field, dir|
+ table[field].send(dir)
+ }
+ else
+ arg
+ end
+ end.flatten!
+ end
+
# Checks to make sure that the arguments are not blank. Note that if some
# blank-like object were initially passed into the query method, then this
# method will not raise an error.
diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb
index de784f9f57..2552cbd234 100644
--- a/activerecord/lib/active_record/relation/spawn_methods.rb
+++ b/activerecord/lib/active_record/relation/spawn_methods.rb
@@ -64,8 +64,7 @@ module ActiveRecord
private
def relation_with(values) # :nodoc:
- result = Relation.new(klass, table, values)
- result.default_scoped = default_scoped
+ result = Relation.create(klass, table, values)
result.extend(*extending_values) if extending_values.any?
result
end
diff --git a/activerecord/lib/active_record/result.rb b/activerecord/lib/active_record/result.rb
index bea195e9b8..469451e2f4 100644
--- a/activerecord/lib/active_record/result.rb
+++ b/activerecord/lib/active_record/result.rb
@@ -3,11 +3,36 @@ module ActiveRecord
# This class encapsulates a Result returned from calling +exec_query+ on any
# database connection adapter. For example:
#
- # x = ActiveRecord::Base.connection.exec_query('SELECT * FROM foo')
- # x # => #<ActiveRecord::Result:0xdeadbeef>
+ # result = ActiveRecord::Base.connection.exec_query('SELECT id, title, body FROM posts')
+ # result # => #<ActiveRecord::Result:0xdeadbeef>
+ #
+ # # Get the column names of the result:
+ # result.columns
+ # # => ["id", "title", "body"]
+ #
+ # # Get the record values of the result:
+ # result.rows
+ # # => [[1, "title_1", "body_1"],
+ # [2, "title_2", "body_2"],
+ # ...
+ # ]
+ #
+ # # Get an array of hashes representing the result (column => value):
+ # result.to_hash
+ # # => [{"id" => 1, "title" => "title_1", "body" => "body_1"},
+ # {"id" => 2, "title" => "title_2", "body" => "body_2"},
+ # ...
+ # ]
+ #
+ # # ActiveRecord::Result also includes Enumerable.
+ # result.each do |row|
+ # puts row['title'] + " " + row['body']
+ # end
class Result
include Enumerable
+ IDENTITY_TYPE = Class.new { def type_cast(v); v; end }.new # :nodoc:
+
attr_reader :columns, :rows, :column_types
def initialize(columns, rows, column_types = {})
@@ -17,8 +42,20 @@ module ActiveRecord
@column_types = column_types
end
+ def identity_type # :nodoc:
+ IDENTITY_TYPE
+ end
+
+ def column_type(name)
+ @column_types[name] || identity_type
+ end
+
def each
- hash_rows.each { |row| yield row }
+ if block_given?
+ hash_rows.each { |row| yield row }
+ else
+ hash_rows.to_enum
+ end
end
def to_hash
@@ -46,12 +83,14 @@ module ActiveRecord
end
def initialize_copy(other)
- @columns = columns.dup
- @rows = rows.dup
- @hash_rows = nil
+ @columns = columns.dup
+ @rows = rows.dup
+ @column_types = column_types.dup
+ @hash_rows = nil
end
private
+
def hash_rows
@hash_rows ||=
begin
@@ -59,7 +98,21 @@ module ActiveRecord
# used as keys in ActiveRecord::Base's @attributes hash
columns = @columns.map { |c| c.dup.freeze }
@rows.map { |row|
- Hash[columns.zip(row)]
+ # In the past we used Hash[columns.zip(row)]
+ # though elegant, the verbose way is much more efficient
+ # both time and memory wise cause it avoids a big array allocation
+ # this method is called a lot and needs to be micro optimised
+ hash = {}
+
+ index = 0
+ length = columns.length
+
+ while index < length
+ hash[columns[index]] = row[index]
+ index += 1
+ end
+
+ hash
}
end
end
diff --git a/activerecord/lib/active_record/runtime_registry.rb b/activerecord/lib/active_record/runtime_registry.rb
new file mode 100644
index 0000000000..9d605b826a
--- /dev/null
+++ b/activerecord/lib/active_record/runtime_registry.rb
@@ -0,0 +1,22 @@
+require 'active_support/per_thread_registry'
+
+module ActiveRecord
+ # This is a thread locals registry for Active Record. For example:
+ #
+ # ActiveRecord::RuntimeRegistry.connection_handler
+ #
+ # returns the connection handler local to the current thread.
+ #
+ # See the documentation of <tt>ActiveSupport::PerThreadRegistry</tt>
+ # for further details.
+ class RuntimeRegistry # :nodoc:
+ extend ActiveSupport::PerThreadRegistry
+
+ attr_accessor :connection_handler, :sql_runtime, :connection_id
+
+ [:connection_handler, :sql_runtime, :connection_id].each do |val|
+ class_eval %{ def self.#{val}; instance.#{val}; end }, __FILE__, __LINE__
+ class_eval %{ def self.#{val}=(x); instance.#{val}=x; end }, __FILE__, __LINE__
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb
index 3c5b871e99..dacaec26b7 100644
--- a/activerecord/lib/active_record/sanitization.rb
+++ b/activerecord/lib/active_record/sanitization.rb
@@ -3,8 +3,8 @@ module ActiveRecord
extend ActiveSupport::Concern
module ClassMethods
- def quote_value(value, column = nil) #:nodoc:
- connection.quote(value,column)
+ def quote_value(value, column) #:nodoc:
+ connection.quote(value, column)
end
# Used to sanitize objects before they're used in an SQL SELECT statement. Delegates to <tt>connection.quote</tt>.
@@ -86,10 +86,11 @@ module ActiveRecord
# { address: Address.new("123 abc st.", "chicago") }
# # => "address_street='123 abc st.' and address_city='chicago'"
def sanitize_sql_hash_for_conditions(attrs, default_table_name = self.table_name)
+ attrs = PredicateBuilder.resolve_column_aliases self, attrs
attrs = expand_hash_conditions_for_aggregates(attrs)
table = Arel::Table.new(table_name, arel_engine).alias(default_table_name)
- PredicateBuilder.build_from_hash(self.class, attrs, table).map { |b|
+ PredicateBuilder.build_from_hash(self, attrs, table).map { |b|
connection.visitor.accept b
}.join(' AND ')
end
@@ -99,8 +100,9 @@ module ActiveRecord
# { status: nil, group_id: 1 }
# # => "status = NULL , group_id = 1"
def sanitize_sql_hash_for_assignment(attrs, table)
+ c = connection
attrs.map do |attr, value|
- "#{connection.quote_table_name_for_assignment(table, attr)} = #{quote_bound_value(value)}"
+ "#{c.quote_table_name_for_assignment(table, attr)} = #{quote_bound_value(value, c, columns_hash[attr.to_s])}"
end.join(', ')
end
@@ -126,7 +128,17 @@ module ActiveRecord
raise_if_bind_arity_mismatch(statement, statement.count('?'), values.size)
bound = values.dup
c = connection
- statement.gsub('?') { quote_bound_value(bound.shift, c) }
+ statement.gsub('?') do
+ replace_bind_variable(bound.shift, c)
+ end
+ end
+
+ def replace_bind_variable(value, c = connection) #:nodoc:
+ if ActiveRecord::Relation === value
+ value.to_sql
+ else
+ quote_bound_value(value, c)
+ end
end
def replace_named_bind_variables(statement, bind_vars) #:nodoc:
@@ -134,15 +146,17 @@ module ActiveRecord
if $1 == ':' # skip postgresql casts
$& # return the whole match
elsif bind_vars.include?(match = $2.to_sym)
- quote_bound_value(bind_vars[match])
+ replace_bind_variable(bind_vars[match])
else
raise PreparedStatementInvalid, "missing value for :#{match} in #{statement}"
end
end
end
- def quote_bound_value(value, c = connection) #:nodoc:
- if value.respond_to?(:map) && !value.acts_like?(:string)
+ def quote_bound_value(value, c = connection, column = nil) #:nodoc:
+ if column
+ c.quote(value, column)
+ elsif value.respond_to?(:map) && !value.acts_like?(:string)
if value.respond_to?(:empty?) && value.empty?
c.quote(nil)
else
diff --git a/activerecord/lib/active_record/schema.rb b/activerecord/lib/active_record/schema.rb
index 3259dbbd80..4bfd0167a4 100644
--- a/activerecord/lib/active_record/schema.rb
+++ b/activerecord/lib/active_record/schema.rb
@@ -43,7 +43,7 @@ module ActiveRecord
unless info[:version].blank?
initialize_schema_migrations_table
- assume_migrated_upto_version(info[:version], migrations_paths)
+ connection.assume_migrated_upto_version(info[:version], migrations_paths)
end
end
diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb
index 10c6d272cd..e055d571ab 100644
--- a/activerecord/lib/active_record/schema_dumper.rb
+++ b/activerecord/lib/active_record/schema_dumper.rb
@@ -17,9 +17,19 @@ module ActiveRecord
cattr_accessor :ignore_tables
@@ignore_tables = []
- def self.dump(connection=ActiveRecord::Base.connection, stream=STDOUT)
- new(connection).dump(stream)
- stream
+ class << self
+ def dump(connection=ActiveRecord::Base.connection, stream=STDOUT, config = ActiveRecord::Base)
+ new(connection, generate_options(config)).dump(stream)
+ stream
+ end
+
+ private
+ def generate_options(config)
+ {
+ table_name_prefix: config.table_name_prefix,
+ table_name_suffix: config.table_name_suffix
+ }
+ end
end
def dump(stream)
@@ -32,10 +42,11 @@ module ActiveRecord
private
- def initialize(connection)
+ def initialize(connection, options = {})
@connection = connection
@types = @connection.native_database_types
@version = Migrator::current_version rescue nil
+ @options = options
end
def header(stream)
@@ -106,9 +117,13 @@ HEADER
end
tbl.print " create_table #{remove_prefix_and_suffix(table).inspect}"
- if columns.detect { |c| c.name == pk }
+ pkcol = columns.detect { |c| c.name == pk }
+ if pkcol
if pk != 'id'
tbl.print %Q(, primary_key: "#{pk}")
+ elsif pkcol.sql_type == 'uuid'
+ tbl.print ", id: :uuid"
+ tbl.print %Q(, default: "#{pkcol.default_function}") if pkcol.default_function
end
else
tbl.print ", id: false"
@@ -198,7 +213,7 @@ HEADER
end
def remove_prefix_and_suffix(table)
- table.gsub(/^(#{ActiveRecord::Base.table_name_prefix})(.+)(#{ActiveRecord::Base.table_name_suffix})$/, "\\2")
+ table.gsub(/^(#{@options[:table_name_prefix]})(.+)(#{@options[:table_name_suffix]})$/, "\\2")
end
end
end
diff --git a/activerecord/lib/active_record/schema_migration.rb b/activerecord/lib/active_record/schema_migration.rb
index 6077144265..a9d164e366 100644
--- a/activerecord/lib/active_record/schema_migration.rb
+++ b/activerecord/lib/active_record/schema_migration.rb
@@ -4,31 +4,37 @@ require 'active_record/base'
module ActiveRecord
class SchemaMigration < ActiveRecord::Base
+ class << self
- def self.table_name
- "#{Base.table_name_prefix}schema_migrations#{Base.table_name_suffix}"
- end
+ def table_name
+ "#{table_name_prefix}#{ActiveRecord::Base.schema_migrations_table_name}#{table_name_suffix}"
+ end
- def self.index_name
- "#{Base.table_name_prefix}unique_schema_migrations#{Base.table_name_suffix}"
- end
+ def index_name
+ "#{table_name_prefix}unique_#{ActiveRecord::Base.schema_migrations_table_name}#{table_name_suffix}"
+ end
- def self.create_table(limit=nil)
- unless connection.table_exists?(table_name)
- version_options = {null: false}
- version_options[:limit] = limit if limit
+ def table_exists?
+ connection.table_exists?(table_name)
+ end
+
+ def create_table(limit=nil)
+ unless table_exists?
+ version_options = {null: false}
+ version_options[:limit] = limit if limit
- connection.create_table(table_name, id: false) do |t|
- t.column :version, :string, version_options
+ connection.create_table(table_name, id: false) do |t|
+ t.column :version, :string, version_options
+ end
+ connection.add_index table_name, :version, unique: true, name: index_name
end
- connection.add_index table_name, :version, unique: true, name: index_name
end
- end
- def self.drop_table
- if connection.table_exists?(table_name)
- connection.remove_index table_name, name: index_name
- connection.drop_table(table_name)
+ def drop_table
+ if table_exists?
+ connection.remove_index table_name, name: index_name
+ connection.drop_table(table_name)
+ end
end
end
diff --git a/activerecord/lib/active_record/scoping.rb b/activerecord/lib/active_record/scoping.rb
index 886182f534..0cf3d59985 100644
--- a/activerecord/lib/active_record/scoping.rb
+++ b/activerecord/lib/active_record/scoping.rb
@@ -1,3 +1,5 @@
+require 'active_support/per_thread_registry'
+
module ActiveRecord
module Scoping
extend ActiveSupport::Concern
@@ -9,11 +11,11 @@ module ActiveRecord
module ClassMethods
def current_scope #:nodoc:
- Thread.current["#{base_class}_current_scope"]
+ ScopeRegistry.value_for(:current_scope, base_class.to_s)
end
def current_scope=(scope) #:nodoc:
- Thread.current["#{base_class}_current_scope"] = scope
+ ScopeRegistry.set_value_for(:current_scope, base_class.to_s, scope)
end
end
@@ -24,5 +26,57 @@ module ActiveRecord
send("#{att}=", value) if respond_to?("#{att}=")
end
end
+
+ # This class stores the +:current_scope+ and +:ignore_default_scope+ values
+ # for different classes. The registry is stored as a thread local, which is
+ # accessed through +ScopeRegistry.current+.
+ #
+ # This class allows you to store and get the scope values on different
+ # classes and different types of scopes. For example, if you are attempting
+ # to get the current_scope for the +Board+ model, then you would use the
+ # following code:
+ #
+ # registry = ActiveRecord::Scoping::ScopeRegistry
+ # registry.set_value_for(:current_scope, "Board", some_new_scope)
+ #
+ # Now when you run:
+ #
+ # registry.value_for(:current_scope, "Board")
+ #
+ # You will obtain whatever was defined in +some_new_scope+. The +value_for+
+ # and +set_value_for+ methods are delegated to the current +ScopeRegistry+
+ # object, so the above example code can also be called as:
+ #
+ # ActiveRecord::Scoping::ScopeRegistry.set_value_for(:current_scope,
+ # "Board", some_new_scope)
+ class ScopeRegistry # :nodoc:
+ extend ActiveSupport::PerThreadRegistry
+
+ VALID_SCOPE_TYPES = [:current_scope, :ignore_default_scope]
+
+ def initialize
+ @registry = Hash.new { |hash, key| hash[key] = {} }
+ end
+
+ # Obtains the value for a given +scope_name+ and +variable_name+.
+ def value_for(scope_type, variable_name)
+ raise_invalid_scope_type!(scope_type)
+ @registry[scope_type][variable_name]
+ end
+
+ # Sets the +value+ for a given +scope_type+ and +variable_name+.
+ def set_value_for(scope_type, variable_name, value)
+ raise_invalid_scope_type!(scope_type)
+ @registry[scope_type][variable_name] = value
+ end
+
+ private
+
+ def raise_invalid_scope_type!(scope_type)
+ if !VALID_SCOPE_TYPES.include?(scope_type)
+ raise ArgumentError, "Invalid scope type '#{scope_type}' sent to the registry. Scope types must be included in VALID_SCOPE_TYPES"
+ end
+ end
+ end
end
end
diff --git a/activerecord/lib/active_record/scoping/default.rb b/activerecord/lib/active_record/scoping/default.rb
index 8a90528ce8..01fec31544 100644
--- a/activerecord/lib/active_record/scoping/default.rb
+++ b/activerecord/lib/active_record/scoping/default.rb
@@ -8,14 +8,6 @@ module ActiveRecord
class_attribute :default_scopes, instance_writer: false, instance_predicate: false
self.default_scopes = []
-
- def self.default_scopes?
- ActiveSupport::Deprecation.warn(
- "#default_scopes? is deprecated. Do something like #default_scopes.empty? instead."
- )
-
- !!self.default_scopes
- end
end
module ClassMethods
@@ -91,12 +83,11 @@ module ActiveRecord
scope = Proc.new if block_given?
if scope.is_a?(Relation) || !scope.respond_to?(:call)
- ActiveSupport::Deprecation.warn(
- "Calling #default_scope without a block is deprecated. For example instead " \
+ raise ArgumentError,
+ "Support for calling #default_scope without a block is removed. For example instead " \
"of `default_scope where(color: 'red')`, please use " \
"`default_scope { where(color: 'red') }`. (Alternatively you can just redefine " \
"self.default_scope.)"
- )
end
self.default_scopes += [scope]
@@ -109,22 +100,18 @@ module ActiveRecord
elsif default_scopes.any?
evaluate_default_scope do
default_scopes.inject(relation) do |default_scope, scope|
- if !scope.is_a?(Relation) && scope.respond_to?(:call)
- default_scope.merge(unscoped { scope.call })
- else
- default_scope.merge(scope)
- end
+ default_scope.merge(unscoped { scope.call })
end
end
end
end
def ignore_default_scope? # :nodoc:
- Thread.current["#{self}_ignore_default_scope"]
+ ScopeRegistry.value_for(:ignore_default_scope, self)
end
def ignore_default_scope=(ignore) # :nodoc:
- Thread.current["#{self}_ignore_default_scope"] = ignore
+ ScopeRegistry.set_value_for(:ignore_default_scope, self, ignore)
end
# The ignore_default_scope flag is used to prevent an infinite recursion
diff --git a/activerecord/lib/active_record/scoping/named.rb b/activerecord/lib/active_record/scoping/named.rb
index da73bead32..2a5718f388 100644
--- a/activerecord/lib/active_record/scoping/named.rb
+++ b/activerecord/lib/active_record/scoping/named.rb
@@ -25,22 +25,18 @@ module ActiveRecord
if current_scope
current_scope.clone
else
- scope = relation
- scope.default_scoped = true
- scope
+ default_scoped
end
end
+ def default_scoped # :nodoc:
+ relation.merge(build_default_scope)
+ end
+
# Collects attributes from scopes that should be applied when creating
# an AR instance for the particular class this is called on.
def scope_attributes # :nodoc:
- if current_scope
- current_scope.scope_for_create
- else
- scope = relation
- scope.default_scoped = true
- scope.scope_for_create
- end
+ all.scope_for_create
end
# Are there default attributes associated with this scope?
@@ -145,26 +141,9 @@ module ActiveRecord
def scope(name, body, &block)
extension = Module.new(&block) if block
- # Check body.is_a?(Relation) to prevent the relation actually being
- # loaded by respond_to?
- if body.is_a?(Relation) || !body.respond_to?(:call)
- ActiveSupport::Deprecation.warn(
- "Using #scope without passing a callable object is deprecated. For " \
- "example `scope :red, where(color: 'red')` should be changed to " \
- "`scope :red, -> { where(color: 'red') }`. There are numerous gotchas " \
- "in the former usage and it makes the implementation more complicated " \
- "and buggy. (If you prefer, you can just define a class method named " \
- "`self.red`.)"
- )
- end
-
singleton_class.send(:define_method, name) do |*args|
- if body.respond_to?(:call)
- scope = all.scoping { body.call(*args) }
- scope = scope.extending(extension) if extension
- else
- scope = body
- end
+ scope = all.scoping { body.call(*args) }
+ scope = scope.extending(extension) if extension
scope || all
end
diff --git a/activerecord/lib/active_record/statement_cache.rb b/activerecord/lib/active_record/statement_cache.rb
new file mode 100644
index 0000000000..dd4ee0c4a0
--- /dev/null
+++ b/activerecord/lib/active_record/statement_cache.rb
@@ -0,0 +1,26 @@
+module ActiveRecord
+
+ # Statement cache is used to cache a single statement in order to avoid creating the AST again.
+ # Initializing the cache is done by passing the statement in the initialization block:
+ #
+ # cache = ActiveRecord::StatementCache.new do
+ # Book.where(name: "my book").limit(100)
+ # end
+ #
+ # The cached statement is executed by using the +execute+ method:
+ #
+ # cache.execute
+ #
+ # The relation returned by the block is cached, and for each +execute+ call the cached relation gets duped.
+ # Database is queried when +to_a+ is called on the relation.
+ class StatementCache
+ def initialize
+ @relation = yield
+ raise ArgumentError.new("Statement cannot be nil") if @relation.nil?
+ end
+
+ def execute
+ @relation.dup.to_a
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/store.rb b/activerecord/lib/active_record/store.rb
index a610f479f2..186a737561 100644
--- a/activerecord/lib/active_record/store.rb
+++ b/activerecord/lib/active_record/store.rb
@@ -15,6 +15,11 @@ module ActiveRecord
# You can set custom coder to encode/decode your serialized attributes to/from different formats.
# JSON, YAML, Marshal are supported out of the box. Generally it can be any wrapper that provides +load+ and +dump+.
#
+ # NOTE - If you are using PostgreSQL specific columns like +hstore+ or +json+ there is no need for
+ # the serialization provided by +store+. Simply use +store_accessor+ instead to generate
+ # the accessor methods. Be aware that these columns use a string keyed hash and do not allow access
+ # using a symbol.
+ #
# Examples:
#
# class User < ActiveRecord::Base
@@ -86,6 +91,9 @@ module ActiveRecord
end
end
+ # assign new store attribute and create new hash to ensure that each class in the hierarchy
+ # has its own hash of stored attributes.
+ self.stored_attributes = {} if self.stored_attributes.blank?
self.stored_attributes[store_attribute] ||= []
self.stored_attributes[store_attribute] |= keys
end
@@ -101,26 +109,58 @@ module ActiveRecord
protected
def read_store_attribute(store_attribute, key)
- attribute = initialize_store_attribute(store_attribute)
- attribute[key]
+ accessor = store_accessor_for(store_attribute)
+ accessor.read(self, store_attribute, key)
end
def write_store_attribute(store_attribute, key, value)
- attribute = initialize_store_attribute(store_attribute)
- if value != attribute[key]
- send :"#{store_attribute}_will_change!"
- attribute[key] = value
- end
+ accessor = store_accessor_for(store_attribute)
+ accessor.write(self, store_attribute, key, value)
end
private
- def initialize_store_attribute(store_attribute)
- attribute = send(store_attribute)
- unless attribute.is_a?(ActiveSupport::HashWithIndifferentAccess)
- attribute = IndifferentCoder.as_indifferent_hash(attribute)
- send :"#{store_attribute}=", attribute
+ def store_accessor_for(store_attribute)
+ @column_types[store_attribute.to_s].accessor
+ end
+
+ class HashAccessor
+ def self.read(object, attribute, key)
+ prepare(object, attribute)
+ object.public_send(attribute)[key]
+ end
+
+ def self.write(object, attribute, key, value)
+ prepare(object, attribute)
+ if value != read(object, attribute, key)
+ object.public_send :"#{attribute}_will_change!"
+ object.public_send(attribute)[key] = value
+ end
+ end
+
+ def self.prepare(object, attribute)
+ object.public_send :"#{attribute}=", {} unless object.send(attribute)
+ end
+ end
+
+ class StringKeyedHashAccessor < HashAccessor
+ def self.read(object, attribute, key)
+ super object, attribute, key.to_s
+ end
+
+ def self.write(object, attribute, key, value)
+ super object, attribute, key.to_s, value
+ end
+ end
+
+ class IndifferentHashAccessor < ActiveRecord::Store::HashAccessor
+ def self.prepare(object, store_attribute)
+ attribute = object.send(store_attribute)
+ unless attribute.is_a?(ActiveSupport::HashWithIndifferentAccess)
+ attribute = IndifferentCoder.as_indifferent_hash(attribute)
+ object.send :"#{store_attribute}=", attribute
+ end
+ attribute
end
- attribute
end
class IndifferentCoder # :nodoc:
@@ -138,7 +178,7 @@ module ActiveRecord
end
def load(yaml)
- self.class.as_indifferent_hash @coder.load(yaml)
+ self.class.as_indifferent_hash(@coder.load(yaml))
end
def self.as_indifferent_hash(obj)
diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb
index 36133bab4c..afd493011e 100644
--- a/activerecord/lib/active_record/tasks/database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/database_tasks.rb
@@ -3,10 +3,41 @@ module ActiveRecord
class DatabaseAlreadyExists < StandardError; end # :nodoc:
class DatabaseNotSupported < StandardError; end # :nodoc:
- module DatabaseTasks # :nodoc:
+ # <tt>ActiveRecord::Tasks::DatabaseTasks</tt> is a utility class, which encapsulates
+ # logic behind common tasks used to manage database and migrations.
+ #
+ # The tasks defined here are used in rake tasks provided by Active Record.
+ #
+ # In order to use DatabaseTasks, a few config values need to be set. All the needed
+ # config values are set by Rails already, so it's necessary to do it only if you
+ # want to change the defaults or when you want to use Active Record outside of Rails
+ # (in such case after configuring the database tasks, you can also use the rake tasks
+ # defined in Active Record).
+ #
+ #
+ # The possible config values are:
+ #
+ # * +env+: current environment (like Rails.env).
+ # * +database_configuration+: configuration of your databases (as in +config/database.yml+).
+ # * +db_dir+: your +db+ directory.
+ # * +fixtures_path+: a path to fixtures directory.
+ # * +migrations_paths+: a list of paths to directories with migrations.
+ # * +seed_loader+: an object which will load seeds, it needs to respond to the +load_seed+ method.
+ # * +root+: a path to the root of the application.
+ #
+ # Example usage of +DatabaseTasks+ outside Rails could look as such:
+ #
+ # include ActiveRecord::Tasks
+ # DatabaseTasks.database_configuration = YAML.load(File.read('my_database_config.yml'))
+ # DatabaseTasks.db_dir = 'db'
+ # # other settings...
+ #
+ # DatabaseTasks.create_current('production')
+ module DatabaseTasks
extend self
- attr_writer :current_config
+ attr_writer :current_config, :db_dir, :migrations_paths, :fixtures_path, :root, :env, :seed_loader
+ attr_accessor :database_configuration
LOCAL_HOSTS = ['127.0.0.1', 'localhost']
@@ -19,20 +50,36 @@ module ActiveRecord
register_task(/postgresql/, ActiveRecord::Tasks::PostgreSQLDatabaseTasks)
register_task(/sqlite/, ActiveRecord::Tasks::SQLiteDatabaseTasks)
- register_task(/firebird/, ActiveRecord::Tasks::FirebirdDatabaseTasks)
- register_task(/sqlserver/, ActiveRecord::Tasks::SqlserverDatabaseTasks)
- register_task(/(oci|oracle)/, ActiveRecord::Tasks::OracleDatabaseTasks)
+ def db_dir
+ @db_dir ||= Rails.application.config.paths["db"].first
+ end
+
+ def migrations_paths
+ @migrations_paths ||= Rails.application.paths['db/migrate'].to_a
+ end
+
+ def fixtures_path
+ @fixtures_path ||= File.join(root, 'test', 'fixtures')
+ end
+
+ def root
+ @root ||= Rails.root
+ end
+
+ def env
+ @env ||= Rails.env
+ end
+
+ def seed_loader
+ @seed_loader ||= Rails.application
+ end
def current_config(options = {})
- options.reverse_merge! :env => Rails.env
+ options.reverse_merge! :env => env
if options.has_key?(:config)
@current_config = options[:config]
else
- @current_config ||= if ENV['DATABASE_URL']
- database_url_config
- else
- ActiveRecord::Base.configurations[options[:env]]
- end
+ @current_config ||= ActiveRecord::Base.configurations[options[:env]]
end
end
@@ -50,15 +97,11 @@ module ActiveRecord
each_local_configuration { |configuration| create configuration }
end
- def create_current(environment = Rails.env)
+ def create_current(environment = env)
each_current_configuration(environment) { |configuration|
create configuration
}
- ActiveRecord::Base.establish_connection environment
- end
-
- def create_database_url
- create database_url_config
+ ActiveRecord::Base.establish_connection(environment.to_sym)
end
def drop(*arguments)
@@ -73,17 +116,13 @@ module ActiveRecord
each_local_configuration { |configuration| drop configuration }
end
- def drop_current(environment = Rails.env)
+ def drop_current(environment = env)
each_current_configuration(environment) { |configuration|
drop configuration
}
end
- def drop_database_url
- drop database_url_config
- end
-
- def charset_current(environment = Rails.env)
+ def charset_current(environment = env)
charset ActiveRecord::Base.configurations[environment]
end
@@ -92,7 +131,7 @@ module ActiveRecord
class_for_adapter(configuration['adapter']).new(*arguments).charset
end
- def collation_current(environment = Rails.env)
+ def collation_current(environment = env)
collation ActiveRecord::Base.configurations[environment]
end
@@ -117,13 +156,41 @@ module ActiveRecord
class_for_adapter(configuration['adapter']).new(*arguments).structure_load(filename)
end
- private
+ def load_schema(format = ActiveRecord::Base.schema_format, file = nil)
+ case format
+ when :ruby
+ file ||= File.join(db_dir, "schema.rb")
+ check_schema_file(file)
+ load(file)
+ when :sql
+ file ||= File.join(db_dir, "structure.sql")
+ check_schema_file(file)
+ structure_load(current_config, file)
+ else
+ raise ArgumentError, "unknown format #{format.inspect}"
+ end
+ end
- def database_url_config
- @database_url_config ||=
- ConnectionAdapters::ConnectionSpecification::Resolver.new(ENV["DATABASE_URL"], {}).spec.config.stringify_keys
+ def check_schema_file(filename)
+ unless File.exist?(filename)
+ message = %{#{filename} doesn't exist yet. Run `rake db:migrate` to create it, then try again.}
+ message << %{ If you do not intend to use a database, you should instead alter #{Rails.root}/config/application.rb to limit the frameworks that will be loaded.} if defined?(::Rails)
+ Kernel.abort message
+ end
end
+ def load_seed
+ if seed_loader
+ seed_loader.load_seed
+ else
+ raise "You tried to load seed data, but no seed loader is specified. Please specify seed " +
+ "loader with ActiveRecord::Tasks::DatabaseTasks.seed_loader = your_seed_loader\n" +
+ "Seed loader should respond to load_seed method"
+ end
+ end
+
+ private
+
def class_for_adapter(adapter)
key = @tasks.keys.detect { |pattern| adapter[pattern] }
unless key
@@ -134,7 +201,7 @@ module ActiveRecord
def each_current_configuration(environment)
environments = [environment]
- environments << 'test' if environment.development?
+ environments << 'test' if environment == 'development'
configurations = ActiveRecord::Base.configurations.values_at(*environments)
configurations.compact.each do |configuration|
diff --git a/activerecord/lib/active_record/tasks/firebird_database_tasks.rb b/activerecord/lib/active_record/tasks/firebird_database_tasks.rb
deleted file mode 100644
index 98014a38ea..0000000000
--- a/activerecord/lib/active_record/tasks/firebird_database_tasks.rb
+++ /dev/null
@@ -1,56 +0,0 @@
-module ActiveRecord
- module Tasks # :nodoc:
- class FirebirdDatabaseTasks # :nodoc:
- delegate :connection, :establish_connection, to: ActiveRecord::Base
-
- def initialize(configuration)
- ActiveSupport::Deprecation.warn "This database tasks were deprecated, because this tasks should be served by the 3rd party adapter."
- @configuration = configuration
- end
-
- def create
- $stderr.puts 'sorry, your database adapter is not supported yet, feel free to submit a patch'
- end
-
- def drop
- $stderr.puts 'sorry, your database adapter is not supported yet, feel free to submit a patch'
- end
-
- def purge
- establish_connection(:test)
- connection.recreate_database!
- end
-
- def charset
- $stderr.puts 'sorry, your database adapter is not supported yet, feel free to submit a patch'
- end
-
- def structure_dump(filename)
- set_firebird_env(configuration)
- db_string = firebird_db_string(configuration)
- Kernel.system "isql -a #{db_string} > #{filename}"
- end
-
- def structure_load(filename)
- set_firebird_env(configuration)
- db_string = firebird_db_string(configuration)
- Kernel.system "isql -i #{filename} #{db_string}"
- end
-
- private
-
- def set_firebird_env(config)
- ENV['ISC_USER'] = config['username'].to_s if config['username']
- ENV['ISC_PASSWORD'] = config['password'].to_s if config['password']
- end
-
- def firebird_db_string(config)
- FireRuby::Database.db_string_for(config.symbolize_keys)
- end
-
- def configuration
- @configuration
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb
index 50569d2462..c755831e6d 100644
--- a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb
@@ -134,8 +134,9 @@ IDENTIFIED BY '#{configuration['password']}' WITH GRANT OPTION;
args << "--password=#{configuration['password']}" if configuration['password']
args.concat(['--default-character-set', configuration['encoding']]) if configuration['encoding']
configuration.slice('host', 'port', 'socket').each do |k, v|
- args.concat([ "--#{k}", v ]) if v
+ args.concat([ "--#{k}", v.to_s ]) if v
end
+
args
end
end
diff --git a/activerecord/lib/active_record/tasks/oracle_database_tasks.rb b/activerecord/lib/active_record/tasks/oracle_database_tasks.rb
deleted file mode 100644
index de3aa50e5e..0000000000
--- a/activerecord/lib/active_record/tasks/oracle_database_tasks.rb
+++ /dev/null
@@ -1,45 +0,0 @@
-module ActiveRecord
- module Tasks # :nodoc:
- class OracleDatabaseTasks # :nodoc:
- delegate :connection, :establish_connection, to: ActiveRecord::Base
-
- def initialize(configuration)
- ActiveSupport::Deprecation.warn "This database tasks were deprecated, because this tasks should be served by the 3rd party adapter."
- @configuration = configuration
- end
-
- def create
- $stderr.puts 'sorry, your database adapter is not supported yet, feel free to submit a patch'
- end
-
- def drop
- $stderr.puts 'sorry, your database adapter is not supported yet, feel free to submit a patch'
- end
-
- def purge
- establish_connection(:test)
- connection.structure_drop.split(";\n\n").each { |ddl| connection.execute(ddl) }
- end
-
- def charset
- $stderr.puts 'sorry, your database adapter is not supported yet, feel free to submit a patch'
- end
-
- def structure_dump(filename)
- establish_connection(configuration)
- File.open(filename, "w:utf-8") { |f| f << connection.structure_dump }
- end
-
- def structure_load(filename)
- establish_connection(configuration)
- IO.read(filename).split(";\n\n").each { |ddl| connection.execute(ddl) }
- end
-
- private
-
- def configuration
- @configuration
- 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 0b1b030516..3d02ee07d0 100644
--- a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb
@@ -59,7 +59,7 @@ module ActiveRecord
def structure_load(filename)
set_psql_env
- Kernel.system("psql -f #{filename} #{configuration['database']}")
+ Kernel.system("psql -q -f #{Shellwords.escape(filename)} #{configuration['database']}")
end
private
diff --git a/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb b/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb
index de8b16627e..5688931db2 100644
--- a/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb
@@ -3,7 +3,7 @@ module ActiveRecord
class SQLiteDatabaseTasks # :nodoc:
delegate :connection, :establish_connection, to: ActiveRecord::Base
- def initialize(configuration, root = Rails.root)
+ def initialize(configuration, root = ActiveRecord::Tasks::DatabaseTasks.root)
@configuration, @root = configuration, root
end
diff --git a/activerecord/lib/active_record/tasks/sqlserver_database_tasks.rb b/activerecord/lib/active_record/tasks/sqlserver_database_tasks.rb
deleted file mode 100644
index c718ee03a8..0000000000
--- a/activerecord/lib/active_record/tasks/sqlserver_database_tasks.rb
+++ /dev/null
@@ -1,48 +0,0 @@
-require 'shellwords'
-
-module ActiveRecord
- module Tasks # :nodoc:
- class SqlserverDatabaseTasks # :nodoc:
- delegate :connection, :establish_connection, to: ActiveRecord::Base
-
- def initialize(configuration)
- ActiveSupport::Deprecation.warn "This database tasks were deprecated, because this tasks should be served by the 3rd party adapter."
- @configuration = configuration
- end
-
- def create
- $stderr.puts 'sorry, your database adapter is not supported yet, feel free to submit a patch'
- end
-
- def drop
- $stderr.puts 'sorry, your database adapter is not supported yet, feel free to submit a patch'
- end
-
- def purge
- test = configuration.deep_dup
- test_database = test['database']
- test['database'] = 'master'
- establish_connection(test)
- connection.recreate_database!(test_database)
- end
-
- def charset
- $stderr.puts 'sorry, your database adapter is not supported yet, feel free to submit a patch'
- end
-
- def structure_dump(filename)
- Kernel.system("smoscript -s #{configuration['host']} -d #{configuration['database']} -u #{configuration['username']} -p #{configuration['password']} -f #{filename} -A -U")
- end
-
- def structure_load(filename)
- Kernel.system("sqlcmd -S #{configuration['host']} -d #{configuration['database']} -U #{configuration['username']} -P #{configuration['password']} -i #{filename}")
- end
-
- private
-
- def configuration
- @configuration
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/test_case.rb b/activerecord/lib/active_record/test_case.rb
deleted file mode 100644
index e9142481a3..0000000000
--- a/activerecord/lib/active_record/test_case.rb
+++ /dev/null
@@ -1,96 +0,0 @@
-require 'active_support/test_case'
-
-ActiveSupport::Deprecation.warn('ActiveRecord::TestCase is deprecated, please use ActiveSupport::TestCase')
-module ActiveRecord
- # = Active Record Test Case
- #
- # Defines some test assertions to test against SQL queries.
- class TestCase < ActiveSupport::TestCase #:nodoc:
- def teardown
- SQLCounter.clear_log
- end
-
- def assert_date_from_db(expected, actual, message = nil)
- # SybaseAdapter doesn't have a separate column type just for dates,
- # so the time is in the string and incorrectly formatted
- if current_adapter?(:SybaseAdapter)
- assert_equal expected.to_s, actual.to_date.to_s, message
- else
- assert_equal expected.to_s, actual.to_s, message
- end
- end
-
- def assert_sql(*patterns_to_match)
- SQLCounter.clear_log
- yield
- SQLCounter.log_all
- ensure
- failed_patterns = []
- patterns_to_match.each do |pattern|
- failed_patterns << pattern unless SQLCounter.log_all.any?{ |sql| pattern === sql }
- end
- assert failed_patterns.empty?, "Query pattern(s) #{failed_patterns.map{ |p| p.inspect }.join(', ')} not found.#{SQLCounter.log.size == 0 ? '' : "\nQueries:\n#{SQLCounter.log.join("\n")}"}"
- end
-
- def assert_queries(num = 1, options = {})
- ignore_none = options.fetch(:ignore_none) { num == :any }
- SQLCounter.clear_log
- yield
- ensure
- the_log = ignore_none ? SQLCounter.log_all : SQLCounter.log
- if num == :any
- assert_operator the_log.size, :>=, 1, "1 or more queries expected, but none were executed."
- else
- mesg = "#{the_log.size} instead of #{num} queries were executed.#{the_log.size == 0 ? '' : "\nQueries:\n#{the_log.join("\n")}"}"
- assert_equal num, the_log.size, mesg
- end
- end
-
- def assert_no_queries(&block)
- assert_queries(0, :ignore_none => true, &block)
- end
-
- end
-
- class SQLCounter
- class << self
- attr_accessor :ignored_sql, :log, :log_all
- def clear_log; self.log = []; self.log_all = []; end
- end
-
- self.clear_log
-
- self.ignored_sql = [/^PRAGMA/, /^SELECT currval/, /^SELECT CAST/, /^SELECT @@IDENTITY/, /^SELECT @@ROWCOUNT/, /^SAVEPOINT/, /^ROLLBACK TO SAVEPOINT/, /^RELEASE SAVEPOINT/, /^SHOW max_identifier_length/, /^BEGIN/, /^COMMIT/]
-
- # FIXME: this needs to be refactored so specific database can add their own
- # ignored SQL, or better yet, use a different notification for the queries
- # instead examining the SQL content.
- oracle_ignored = [/^select .*nextval/i, /^SAVEPOINT/, /^ROLLBACK TO/, /^\s*select .* from all_triggers/im]
- mysql_ignored = [/^SHOW TABLES/i, /^SHOW FULL FIELDS/]
- postgresql_ignored = [/^\s*select\b.*\bfrom\b.*pg_namespace\b/im, /^\s*select\b.*\battname\b.*\bfrom\b.*\bpg_attribute\b/im, /^SHOW search_path/i]
- sqlite3_ignored = [/^\s*SELECT name\b.*\bFROM sqlite_master/im]
-
- [oracle_ignored, mysql_ignored, postgresql_ignored, sqlite3_ignored].each do |db_ignored_sql|
- ignored_sql.concat db_ignored_sql
- end
-
- attr_reader :ignore
-
- def initialize(ignore = Regexp.union(self.class.ignored_sql))
- @ignore = ignore
- end
-
- def call(name, start, finish, message_id, values)
- sql = values[:sql]
-
- # FIXME: this seems bad. we should probably have a better way to indicate
- # the query was cached
- return if 'CACHE' == values[:name]
-
- self.class.log_all << sql
- self.class.log << sql unless ignore =~ sql
- end
- end
-
- ActiveSupport::Notifications.subscribe('sql.active_record', SQLCounter.new)
-end
diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb
index ae99cff35e..e0541b7681 100644
--- a/activerecord/lib/active_record/timestamp.rb
+++ b/activerecord/lib/active_record/timestamp.rb
@@ -10,9 +10,9 @@ module ActiveRecord
#
# config.active_record.record_timestamps = false
#
- # Timestamps are in the local timezone by default but you can use UTC by setting:
+ # Timestamps are in UTC by default but you can use the local timezone by setting:
#
- # config.active_record.default_timezone = :utc
+ # config.active_record.default_timezone = :local
#
# == Time Zone aware attributes
#
@@ -98,8 +98,8 @@ module ActiveRecord
timestamp_attributes_for_create + timestamp_attributes_for_update
end
- def max_updated_column_timestamp
- if (timestamps = timestamp_attributes_for_update.map { |attr| self[attr] }.compact).present?
+ def max_updated_column_timestamp(timestamp_names = timestamp_attributes_for_update)
+ if (timestamps = timestamp_names.map { |attr| self[attr] }.compact).present?
timestamps.map { |ts| ts.to_time }.max
end
end
diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb
index 4dbd668fcf..45313b5e75 100644
--- a/activerecord/lib/active_record/transactions.rb
+++ b/activerecord/lib/active_record/transactions.rb
@@ -10,7 +10,9 @@ module ActiveRecord
end
included do
- define_callbacks :commit, :rollback, :terminator => "result == false", :scope => [:kind, :name]
+ define_callbacks :commit, :rollback,
+ terminator: ->(_, result) { result == false },
+ scope: [:kind, :name]
end
# = Active Record Transactions
@@ -218,8 +220,8 @@ module ActiveRecord
# after_commit :do_bar, on: :update
# after_commit :do_baz, on: :destroy
#
- # after_commit :do_foo_bar, :on [:create, :update]
- # after_commit :do_bar_baz, :on [:update, :destroy]
+ # after_commit :do_foo_bar, on: [:create, :update]
+ # after_commit :do_bar_baz, on: [:update, :destroy]
#
# Note that transactional fixtures do not play well with this feature. Please
# use the +test_after_commit+ gem to have these hooks fired in tests.
@@ -243,7 +245,7 @@ module ActiveRecord
if options.is_a?(Hash) && options[:on]
assert_valid_transaction_action(options[:on])
options[:if] = Array(options[:if])
- fire_on = Array(options[:on]).map(&:to_sym)
+ fire_on = Array(options[:on])
options[:if] << "transaction_include_any_action?(#{fire_on})"
end
end
@@ -286,25 +288,26 @@ module ActiveRecord
clear_transaction_record_state
end
- # Call the after_commit callbacks
+ # Call the +after_commit+ callbacks.
#
# Ensure that it is not called if the object was never persisted (failed create),
- # but call it after the commit of a destroyed object
+ # but call it after the commit of a destroyed object.
def committed! #:nodoc:
run_callbacks :commit if destroyed? || persisted?
ensure
clear_transaction_record_state
end
- # Call the after rollback callbacks. The restore_state argument indicates if the record
+ # Call the +after_rollback+ callbacks. The +force_restore_state+ argument indicates if the record
# state should be rolled back to the beginning or just to the last savepoint.
def rolledback!(force_restore_state = false) #:nodoc:
run_callbacks :rollback
ensure
restore_transaction_record_state(force_restore_state)
+ clear_transaction_record_state
end
- # Add the record to the current transaction so that the :after_rollback and :after_commit callbacks
+ # Add the record to the current transaction so that the +after_rollback+ and +after_commit+ callbacks
# can be called.
def add_to_transaction
if self.class.connection.add_transaction_record(self)
@@ -339,8 +342,12 @@ module ActiveRecord
# Save the new record state and id of a record so it can be restored later if a transaction fails.
def remember_transaction_record_state #:nodoc:
@_start_transaction_state[:id] = id if has_attribute?(self.class.primary_key)
- @_start_transaction_state[:new_record] = @new_record
- @_start_transaction_state[:destroyed] = @destroyed
+ unless @_start_transaction_state.include?(:new_record)
+ @_start_transaction_state[:new_record] = @new_record
+ end
+ unless @_start_transaction_state.include?(:destroyed)
+ @_start_transaction_state[:destroyed] = @destroyed
+ end
@_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) + 1
@_start_transaction_state[:frozen?] = @attributes.frozen?
end
@@ -354,8 +361,8 @@ module ActiveRecord
# Restore the new record state and id of a record that was previously saved by a call to save_record_state.
def restore_transaction_record_state(force = false) #:nodoc:
unless @_start_transaction_state.empty?
- @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1
- if @_start_transaction_state[:level] < 1 || force
+ transaction_level = (@_start_transaction_state[:level] || 0) - 1
+ if transaction_level < 1 || force
restore_state = @_start_transaction_state
was_frozen = restore_state[:frozen?]
@attributes = @attributes.dup if @attributes.frozen?
@@ -368,7 +375,6 @@ module ActiveRecord
@attributes_cache.delete(self.class.primary_key)
end
@attributes.freeze if was_frozen
- @_start_transaction_state.clear
end
end
end
diff --git a/activerecord/lib/active_record/validations/associated.rb b/activerecord/lib/active_record/validations/associated.rb
index 7f1972ccf9..b4785d3ba4 100644
--- a/activerecord/lib/active_record/validations/associated.rb
+++ b/activerecord/lib/active_record/validations/associated.rb
@@ -2,15 +2,15 @@ module ActiveRecord
module Validations
class AssociatedValidator < ActiveModel::EachValidator #:nodoc:
def validate_each(record, attribute, value)
- if Array.wrap(value).reject {|r| r.marked_for_destruction? || r.valid?(record.validation_context) }.any?
+ if Array.wrap(value).reject {|r| r.marked_for_destruction? || r.valid?}.any?
record.errors.add(attribute, :invalid, options.merge(:value => value))
end
end
end
module ClassMethods
- # Validates whether the associated object or objects are all valid
- # themselves. Works with any kind of association.
+ # Validates whether the associated object or objects are all valid.
+ # Works with any kind of association.
#
# class Book < ActiveRecord::Base
# has_many :pages
diff --git a/activerecord/lib/active_record/validations/presence.rb b/activerecord/lib/active_record/validations/presence.rb
index 6b14c39686..9a19483da3 100644
--- a/activerecord/lib/active_record/validations/presence.rb
+++ b/activerecord/lib/active_record/validations/presence.rb
@@ -5,7 +5,7 @@ module ActiveRecord
super
attributes.each do |attribute|
next unless record.class.reflect_on_association(attribute)
- associated_records = Array(record.send(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? { |r| r.marked_for_destruction? }
diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb
index a705d8c2c4..7ebe9dfec0 100644
--- a/activerecord/lib/active_record/validations/uniqueness.rb
+++ b/activerecord/lib/active_record/validations/uniqueness.rb
@@ -7,11 +7,7 @@ module ActiveRecord
"Pass a callable instead: `conditions: -> { where(approved: true) }`"
end
super({ case_sensitive: true }.merge!(options))
- end
-
- # Unfortunately, we have to tie Uniqueness validators to a class.
- def setup(klass)
- @klass = klass
+ @klass = options[:class]
end
def validate_each(record, attribute, value)
@@ -34,7 +30,6 @@ module ActiveRecord
end
protected
-
# The check for an existing value should be run from a class that
# isn't abstract. This means working down from the current class
# (self), to the first non-abstract class. Since classes don't know
@@ -53,10 +48,18 @@ module ActiveRecord
def build_relation(klass, table, attribute, value) #:nodoc:
if reflection = klass.reflect_on_association(attribute)
attribute = reflection.foreign_key
- value = value.attributes[reflection.primary_key_column.name]
+ value = value.attributes[reflection.primary_key_column.name] unless value.nil?
+ end
+
+ attribute_name = attribute.to_s
+
+ # the attribute may be an aliased attribute
+ if klass.attribute_aliases[attribute_name]
+ attribute = klass.attribute_aliases[attribute_name]
+ attribute_name = attribute.to_s
end
- column = klass.columns_hash[attribute.to_s]
+ column = klass.columns_hash[attribute_name]
value = klass.connection.type_cast(value, column)
value = value.to_s[0, column.limit] if value && column.limit && column.text?
@@ -171,11 +174,11 @@ module ActiveRecord
# WHERE title = 'My Post' |
# |
# | # User 2 does the same thing and also
- # | # infers that his title is unique.
+ # | # infers that their title is unique.
# | SELECT * FROM comments
# | WHERE title = 'My Post'
# |
- # # User 1 inserts his comment. |
+ # # User 1 inserts their comment. |
# INSERT INTO comments |
# (title, content) VALUES |
# ('My Post', 'hi!') |
@@ -201,9 +204,9 @@ module ActiveRecord
# exception. You can either choose to let this error propagate (which
# will result in the default Rails exception page being shown), or you
# can catch it and restart the transaction (e.g. by telling the user
- # that the title already exists, and asking him to re-enter the title).
- # This technique is also known as optimistic concurrency control:
- # http://en.wikipedia.org/wiki/Optimistic_concurrency_control.
+ # that the title already exists, and asking them to re-enter the title).
+ # This technique is also known as
+ # {optimistic concurrency control}[http://en.wikipedia.org/wiki/Optimistic_concurrency_control].
#
# The bundled ActiveRecord::ConnectionAdapters distinguish unique index
# constraint errors from other types of database errors by throwing an
diff --git a/activerecord/lib/active_record/version.rb b/activerecord/lib/active_record/version.rb
index f2b041ad97..863c3ebe4d 100644
--- a/activerecord/lib/active_record/version.rb
+++ b/activerecord/lib/active_record/version.rb
@@ -1,7 +1,7 @@
module ActiveRecord
# Returns the version of the currently loaded ActiveRecord as a Gem::Version
def self.version
- Gem::Version.new "4.0.0.beta1"
+ Gem::Version.new "4.1.0.beta1"
end
module VERSION #:nodoc: