aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib
diff options
context:
space:
mode:
Diffstat (limited to 'activerecord/lib')
-rw-r--r--activerecord/lib/active_record.rb11
-rw-r--r--activerecord/lib/active_record/association_relation.rb18
-rw-r--r--activerecord/lib/active_record/associations.rb78
-rw-r--r--activerecord/lib/active_record/associations/association.rb26
-rw-r--r--activerecord/lib/active_record/associations/association_scope.rb3
-rw-r--r--activerecord/lib/active_record/associations/builder/association.rb28
-rw-r--r--activerecord/lib/active_record/associations/builder/belongs_to.rb118
-rw-r--r--activerecord/lib/active_record/associations/builder/collection_association.rb4
-rw-r--r--activerecord/lib/active_record/associations/builder/singular_association.rb4
-rw-r--r--activerecord/lib/active_record/associations/collection_association.rb22
-rw-r--r--activerecord/lib/active_record/associations/collection_proxy.rb19
-rw-r--r--activerecord/lib/active_record/associations/has_many_association.rb3
-rw-r--r--activerecord/lib/active_record/associations/has_one_association.rb5
-rw-r--r--activerecord/lib/active_record/associations/join_dependency.rb48
-rw-r--r--activerecord/lib/active_record/associations/join_dependency/join_association.rb40
-rw-r--r--activerecord/lib/active_record/associations/join_dependency/join_base.rb4
-rw-r--r--activerecord/lib/active_record/associations/join_dependency/join_part.rb12
-rw-r--r--activerecord/lib/active_record/associations/preloader/has_many_through.rb8
-rw-r--r--activerecord/lib/active_record/associations/through_association.rb2
-rw-r--r--activerecord/lib/active_record/attribute_assignment.rb3
-rw-r--r--activerecord/lib/active_record/attribute_methods.rb19
-rw-r--r--activerecord/lib/active_record/attribute_methods/before_type_cast.rb3
-rw-r--r--activerecord/lib/active_record/attribute_methods/serialization.rb26
-rw-r--r--activerecord/lib/active_record/autosave_association.rb30
-rw-r--r--activerecord/lib/active_record/base.rb6
-rw-r--r--activerecord/lib/active_record/callbacks.rb2
-rw-r--r--activerecord/lib/active_record/coders/yaml_column.rb20
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb8
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb6
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb81
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_adapter.rb11
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb10
-rw-r--r--activerecord/lib/active_record/connection_adapters/column.rb8
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql_adapter.rb18
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/cast.rb11
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid.rb15
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb4
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb30
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb50
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb8
-rw-r--r--activerecord/lib/active_record/connection_handling.rb14
-rw-r--r--activerecord/lib/active_record/core.rb18
-rw-r--r--activerecord/lib/active_record/errors.rb30
-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/fixtures.rb20
-rw-r--r--activerecord/lib/active_record/inheritance.rb9
-rw-r--r--activerecord/lib/active_record/integration.rb4
-rw-r--r--activerecord/lib/active_record/locking/pessimistic.rb2
-rw-r--r--activerecord/lib/active_record/log_subscriber.rb4
-rw-r--r--activerecord/lib/active_record/migration.rb54
-rw-r--r--activerecord/lib/active_record/migration/command_recorder.rb5
-rw-r--r--activerecord/lib/active_record/nested_attributes.rb112
-rw-r--r--activerecord/lib/active_record/null_relation.rb8
-rw-r--r--activerecord/lib/active_record/persistence.rb70
-rw-r--r--activerecord/lib/active_record/querying.rb9
-rw-r--r--activerecord/lib/active_record/railtie.rb14
-rw-r--r--activerecord/lib/active_record/railties/databases.rake134
-rw-r--r--activerecord/lib/active_record/readonly_attributes.rb6
-rw-r--r--activerecord/lib/active_record/reflection.rb176
-rw-r--r--activerecord/lib/active_record/relation.rb106
-rw-r--r--activerecord/lib/active_record/relation/calculations.rb58
-rw-r--r--activerecord/lib/active_record/relation/delegation.rb6
-rw-r--r--activerecord/lib/active_record/relation/finder_methods.rb106
-rw-r--r--activerecord/lib/active_record/relation/merger.rb84
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder.rb4
-rw-r--r--activerecord/lib/active_record/relation/query_methods.rb18
-rw-r--r--activerecord/lib/active_record/runtime_registry.rb17
-rw-r--r--activerecord/lib/active_record/sanitization.rb2
-rw-r--r--activerecord/lib/active_record/schema.rb2
-rw-r--r--activerecord/lib/active_record/schema_dumper.rb5
-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.rb15
-rw-r--r--activerecord/lib/active_record/scoping/named.rb9
-rw-r--r--activerecord/lib/active_record/statement_cache.rb26
-rw-r--r--activerecord/lib/active_record/tasks/database_tasks.rb73
-rw-r--r--activerecord/lib/active_record/tasks/firebird_database_tasks.rb56
-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/sqlserver_database_tasks.rb48
-rw-r--r--activerecord/lib/active_record/test_case.rb4
-rw-r--r--activerecord/lib/active_record/transactions.rb12
-rw-r--r--activerecord/lib/active_record/validations/associated.rb6
-rw-r--r--activerecord/lib/active_record/validations/uniqueness.rb7
-rw-r--r--activerecord/lib/active_record/version.rb2
-rw-r--r--activerecord/lib/rails/generators/active_record/migration/migration_generator.rb4
-rw-r--r--activerecord/lib/rails/generators/active_record/model/model_generator.rb4
90 files changed, 1649 insertions, 737 deletions
diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb
index c33f03f13f..994cacb4f9 100644
--- a/activerecord/lib/active_record.rb
+++ b/activerecord/lib/active_record.rb
@@ -35,8 +35,8 @@ module ActiveRecord
autoload :Base
autoload :Callbacks
autoload :Core
- autoload :CounterCache
autoload :ConnectionHandling
+ autoload :CounterCache
autoload :DynamicMatchers
autoload :Explain
autoload :Inheritance
@@ -50,12 +50,14 @@ module ActiveRecord
autoload :Querying
autoload :ReadonlyAttributes
autoload :Reflection
+ autoload :RuntimeRegistry
autoload :Sanitization
autoload :Schema
autoload :SchemaDumper
autoload :SchemaMigration
autoload :Scoping
autoload :Serialization
+ autoload :StatementCache
autoload :Store
autoload :Timestamp
autoload :Transactions
@@ -69,11 +71,12 @@ module ActiveRecord
autoload :Aggregations
autoload :Associations
- autoload :AttributeMethods
autoload :AttributeAssignment
+ autoload :AttributeMethods
autoload :AutosaveAssociation
autoload :Relation
+ autoload :AssociationRelation
autoload :NullRelation
autoload_under 'relation' do
@@ -143,6 +146,10 @@ module ActiveRecord
autoload :MySQLDatabaseTasks, 'active_record/tasks/mysql_database_tasks'
autoload :PostgreSQLDatabaseTasks,
'active_record/tasks/postgresql_database_tasks'
+
+ autoload :FirebirdDatabaseTasks, 'active_record/tasks/firebird_database_tasks'
+ autoload :SqlserverDatabaseTasks, 'active_record/tasks/sqlserver_database_tasks'
+ autoload :OracleDatabaseTasks, 'active_record/tasks/oracle_database_tasks'
end
autoload :TestCase
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 eb08f72286..6fd4f3042c 100644
--- a/activerecord/lib/active_record/associations.rb
+++ b/activerecord/lib/active_record/associations.rb
@@ -172,7 +172,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 +365,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 +402,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 +568,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 +586,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 +649,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 +811,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.
#
@@ -988,7 +1010,7 @@ module ActiveRecord
# associated objects themselves. So with +has_and_belongs_to_many+ and +has_many+
# <tt>:through</tt>, the join records will be deleted, but the associated records won't.
#
- # This makes sense if you think about it: if you were to call <tt>post.tags.delete(Tag.find_by_name('food'))</tt>
+ # This makes sense if you think about it: if you were to call <tt>post.tags.delete(Tag.find_by(name: 'food'))</tt>
# you would want the 'food' tag to be unlinked from the post, rather than for the tag itself
# to be removed from the database.
#
@@ -1024,7 +1046,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 +1082,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 +1095,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 +1119,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,14 +1138,14 @@ 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 execute)
+ # * <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).
# * <tt>:nullify</tt> causes the foreign keys to be set to +NULL+. Callbacks are not executed.
- # * <tt>:restrict_with_exception</tt> causes an exception to be raised if there are any associated records
- # * <tt>:restrict_with_error</tt> causes an error to be added to the owner if there are any associated objects
+ # * <tt>:restrict_with_exception</tt> causes an exception to be raised if there are any associated records.
+ # * <tt>:restrict_with_error</tt> causes an error to be added to the owner if there are any associated objects.
#
# If using with the <tt>:through</tt> option, the association on the join model must be
# a +belongs_to+, and the records which get deleted are the join records, rather than
@@ -1159,8 +1185,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]
@@ -1185,7 +1211,7 @@ module ActiveRecord
# 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:
#
@@ -1353,7 +1379,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>.)
@@ -1435,7 +1461,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 +1484,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.
diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb
index 4c4b0f08e5..608a6af16c 100644
--- a/activerecord/lib/active_record/associations/association.rb
+++ b/activerecord/lib/active_record/associations/association.rb
@@ -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"
#
@@ -92,7 +92,7 @@ module ActiveRecord
# The scope for this association.
#
# Note that the association_scope is merged into the target_scope only when the
- # scoped method is called. This is because at that point the call may be surrounded
+ # scope method is called. This is because at that point the call may be surrounded
# by scope.scoping { ... } or with_scope { ... } etc, which affects the scope which
# actually gets built.
def association_scope
@@ -113,7 +113,7 @@ module ActiveRecord
end
end
- # This class of the target. belongs_to polymorphic overrides this to look at the
+ # Returns the class of the target. belongs_to polymorphic overrides this to look at the
# polymorphic_type field on the owner.
def klass
reflection.klass
@@ -122,7 +122,11 @@ 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
+ all = klass.all
+ scope = AssociationRelation.new(klass, klass.arel_table, self)
+ scope.merge! all
+ scope.default_scoped = all.default_scoped?
+ scope
end
# Loads the \target if needed and returns it.
@@ -164,6 +168,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?
@@ -217,7 +228,8 @@ module ActiveRecord
reflection.inverse_of
end
- # Is this association invertible? Can be redefined by subclasses.
+ # 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)
end
@@ -232,9 +244,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)
+ 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 a9525436fb..f1bec5787a 100644
--- a/activerecord/lib/active_record/associations/association_scope.rb
+++ b/activerecord/lib/active_record/associations/association_scope.rb
@@ -96,11 +96,12 @@ module ActiveRecord
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
scope.includes! item.includes_values
scope.where_values += item.where_values
+ scope.order_values |= item.order_values
end
end
diff --git a/activerecord/lib/active_record/associations/builder/association.rb b/activerecord/lib/active_record/associations/builder/association.rb
index 5c37f42794..3254da4677 100644
--- a/activerecord/lib/active_record/associations/builder/association.rb
+++ b/activerecord/lib/active_record/associations/builder/association.rb
@@ -1,3 +1,15 @@
+# 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
+# - HasAndBelongsToManyAssociation
+
module ActiveRecord::Associations::Builder
class Association #:nodoc:
class << self
@@ -58,6 +70,13 @@ module ActiveRecord::Associations::Builder
def validate_options
options.assert_valid_keys(valid_options)
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 define_accessors
define_readers
@@ -92,13 +111,8 @@ module ActiveRecord::Associations::Builder
)
end
- mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
- def #{macro}_dependent_for_#{name}
- association(:#{name}).handle_dependency
- end
- CODE
-
- model.before_destroy "#{macro}_dependent_for_#{name}"
+ n = name
+ model.before_destroy lambda { |o| o.association(n).handle_dependency }
end
def valid_dependent_options
diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb
index 9ac561b997..d4e1a0dda1 100644
--- a/activerecord/lib/active_record/associations/builder/belongs_to.rb
+++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb
@@ -19,68 +19,116 @@ module ActiveRecord::Associations::Builder
reflection
end
- def add_counter_cache_callbacks(reflection)
- cache_column = reflection.counter_cache_column
- foreign_key = reflection.foreign_key
+ def valid_dependent_options
+ [:destroy, :delete]
+ end
+
+ private
- 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
+ def add_counter_cache_methods(mixin)
+ return if mixin.method_defined? :belongs_to_counter_cache_after_create
+
+ mixin.class_eval do
+ def belongs_to_counter_cache_after_create(association, reflection)
+ if record = send(association.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(association, reflection)
+ foreign_key = reflection.foreign_key.to_sym
+ unless destroyed_by_association && destroyed_by_association.foreign_key.to_sym == foreign_key
+ record = send association.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(association, 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? && association.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 add_counter_cache_callbacks(reflection)
+ cache_column = reflection.counter_cache_column
+ add_counter_cache_methods mixin
+ association = self
+
+ model.after_create lambda { |record|
+ record.belongs_to_counter_cache_after_create(association, reflection)
+ }
+
+ model.before_destroy lambda { |record|
+ record.belongs_to_counter_cache_before_destroy(association, reflection)
+ }
- 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_update lambda { |record|
+ record.belongs_to_counter_cache_after_update(association, 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}
- record = #{name}
+ def self.touch_record(o, foreign_key, name, touch) # :nodoc:
+ old_foreign_id = o.attribute_was(foreign_key)
+
+ if old_foreign_id
+ klass = o.association(name).klass
+ old_record = klass.find_by(klass.primary_key => old_foreign_id)
- 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
+ end
- 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}"
+ record = o.send name
+ unless record.nil? || record.new_record?
+ if touch != true
+ record.touch touch
+ else
+ record.touch
+ end
+ end
end
- def valid_dependent_options
- [:destroy, :delete]
+ def add_touch_callbacks(reflection)
+ foreign_key = reflection.foreign_key
+ n = name
+ touch = 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..9c6690b721 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
@@ -66,6 +68,8 @@ module ActiveRecord::Associations::Builder
model.send("#{full_callback_name}=", Array(options[callback_name.to_sym]))
end
+ # Defines the setter and getter methods for the collection_singular_ids.
+
def define_readers
super
diff --git a/activerecord/lib/active_record/associations/builder/singular_association.rb b/activerecord/lib/active_record/associations/builder/singular_association.rb
index 6a5830e57f..76e48e66e5 100644
--- a/activerecord/lib/active_record/associations/builder/singular_association.rb
+++ b/activerecord/lib/active_record/associations/builder/singular_association.rb
@@ -1,3 +1,5 @@
+# This class is inherited by the has_one and belongs_to association classes
+
module ActiveRecord::Associations::Builder
class SingularAssociation < Association #:nodoc:
def valid_options
@@ -13,6 +15,8 @@ module ActiveRecord::Associations::Builder
define_constructors if constructable?
end
+ # Defines the (build|create)_association methods for belongs_to or has_one association
+
def define_constructors
mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
def build_#{name}(*args, &block)
diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb
index 2385c90c1a..efd7ecb97c 100644
--- a/activerecord/lib/active_record/associations/collection_association.rb
+++ b/activerecord/lib/active_record/associations/collection_association.rb
@@ -79,8 +79,20 @@ module ActiveRecord
if block_given?
load_target.find(*args) { |*block_args| yield(*block_args) }
else
- if options[:finder_sql] || options[:inverse_of]
+ 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?
+
+ 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)
+ else
+ result
+ end
else
scope.find(*args)
end
@@ -225,11 +237,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)
diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb
index c2add32aa6..e82c195335 100644
--- a/activerecord/lib/active_record/associations/collection_proxy.rb
+++ b/activerecord/lib/active_record/associations/collection_proxy.rb
@@ -92,7 +92,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.
#
@@ -228,6 +228,7 @@ module ActiveRecord
def build(attributes = {}, &block)
@association.build(attributes, &block)
end
+ alias_method :new, :build
# Returns a new object of the collection type that has been instantiated with
# attributes, linked to this object and that has already been saved (if it
@@ -303,7 +304,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
@@ -421,9 +422,9 @@ module ActiveRecord
@association.delete_all
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
@@ -832,8 +833,6 @@ module ActiveRecord
@association.include?(record)
end
- alias_method :new, :build
-
def proxy_association
@association
end
@@ -848,11 +847,7 @@ module ActiveRecord
# Returns a <tt>Relation</tt> object for the records in this association
def scope
- association = @association
-
- @association.scope.extending! do
- define_method(:proxy_association) { association }
- end
+ @association.scope
end
# :nodoc:
diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb
index 29fae809da..cf8a589496 100644
--- a/activerecord/lib/active_record/associations/has_many_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_association.rb
@@ -115,8 +115,7 @@ module ActiveRecord
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
diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb
index 98bd010f70..920038a543 100644
--- a/activerecord/lib/active_record/associations/has_one_association.rb
+++ b/activerecord/lib/active_record/associations/has_one_association.rb
@@ -25,9 +25,8 @@ 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)
+ if (target != record) || record.changed?
transaction_if(save) do
remove_target!(options[:dependent]) if target && !target.destroyed?
diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb
index f40368cfeb..5aa17e5fbb 100644
--- a/activerecord/lib/active_record/associations/join_dependency.rb
+++ b/activerecord/lib/active_record/associations/join_dependency.rb
@@ -5,10 +5,31 @@ module ActiveRecord
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
-
+ attr_reader :join_parts, :reflections, :alias_tracker, :base_klass
+
+ # 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 commnads 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)
- @active_record = base
+ @base_klass = base
@table_joins = joins
@join_parts = [JoinBase.new(base)]
@associations = {}
@@ -27,13 +48,19 @@ module ActiveRecord
end
def join_associations
- join_parts.last(join_parts.length - 1)
+ join_parts.drop 1
end
def join_base
join_parts.first
end
+ def join_relation(relation)
+ join_associations.inject(relation) do |rel,association|
+ association.join_relation(rel)
+ end
+ end
+
def columns
join_parts.collect { |join_part|
table = join_part.aliased_table
@@ -54,10 +81,12 @@ module ActiveRecord
parent
}.uniq
- remove_duplicate_results!(active_record, records, @associations)
+ remove_duplicate_results!(base_klass, records, @associations)
records
end
+ protected
+
def remove_duplicate_results!(base, records, associations)
case associations
when Symbol, String
@@ -88,8 +117,6 @@ module ActiveRecord
end
end
- protected
-
def cache_joined_association(association)
associations = []
parent = association.parent
@@ -104,12 +131,11 @@ module ActiveRecord
ref[association.reflection.name] ||= {}
end
- def build(associations, parent = nil, join_type = Arel::InnerJoin)
- parent ||= join_parts.last
+ def build(associations, parent = join_parts.last, join_type = Arel::InnerJoin)
case associations
when Symbol, String
- reflection = parent.reflections[associations.to_s.intern] or
- raise ConfigurationError, "Association named '#{ associations }' was not found; perhaps you misspelled it?"
+ reflection = parent.reflections[associations.intern] or
+ raise ConfigurationError, "Association named '#{ associations }' was not found on #{ parent.base_klass.name }; perhaps you misspelled it?"
unless join_association = find_join_association(reflection, parent)
@reflections << reflection
join_association = build_join_association(reflection, parent)
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 0d3b4dbab1..b81aecb4e5 100644
--- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb
+++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb
@@ -55,14 +55,19 @@ module ActiveRecord
def find_parent_in(other_join_dependency)
other_join_dependency.join_parts.detect do |join_part|
- parent == join_part
+ case parent
+ when JoinBase
+ parent.base_klass == join_part.base_klass
+ else
+ parent == join_part
+ end
end
end
- def join_to(relation)
+ def join_to(manager)
tables = @tables.dup
foreign_table = parent_table
- foreign_klass = parent.active_record
+ foreign_klass = parent.base_klass
# The chain starts with the target table, but we want to end with it here (makes
# more sense in this context), so we reverse
@@ -75,7 +80,7 @@ module ActiveRecord
foreign_key = reflection.foreign_key
when :has_and_belongs_to_many
# Join the join table first...
- relation.from(join(
+ manager.from(join(
table,
table[reflection.foreign_key].
eq(foreign_table[reflection.active_record_primary_key])
@@ -101,23 +106,42 @@ module ActiveRecord
]
end
- scope_chain_items.each do |item|
+ constraint = scope_chain_items.inject(constraint) do |chain, item|
unless item.is_a?(Relation)
item = ActiveRecord::Relation.new(reflection.klass, table).instance_exec(self, &item)
end
- constraint = constraint.and(item.arel.constraints) unless item.arel.constraints.empty?
+ if item.arel.constraints.empty?
+ chain
+ else
+ chain.and(item.arel.constraints)
+ end
end
- relation.from(join(table, constraint))
+ manager.from(join(table, constraint))
# The current table in this iteration becomes the foreign table in the next
foreign_table, foreign_klass = table, reflection.klass
end
- relation
+ manager
end
+ # 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(reflection, table, key, foreign_table, foreign_key)
constraint = table[key].eq(foreign_table[foreign_key])
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..a7dacdbfd6 100644
--- a/activerecord/lib/active_record/associations/join_dependency/join_base.rb
+++ b/activerecord/lib/active_record/associations/join_dependency/join_base.rb
@@ -4,7 +4,7 @@ module ActiveRecord
class JoinBase < JoinPart # :nodoc:
def ==(other)
other.class == self.class &&
- other.active_record == active_record
+ other.base_klass == base_klass
end
def aliased_prefix
@@ -16,7 +16,7 @@ module ActiveRecord
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..b534569063 100644
--- a/activerecord/lib/active_record/associations/join_dependency/join_part.rb
+++ b/activerecord/lib/active_record/associations/join_dependency/join_part.rb
@@ -1,7 +1,7 @@
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
@@ -11,12 +11,12 @@ module ActiveRecord
# 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
- delegate :table_name, :column_names, :primary_key, :reflections, :arel_engine, :to => :active_record
+ delegate :table_name, :column_names, :primary_key, :reflections, :arel_engine, :to => :base_klass
- def initialize(active_record)
- @active_record = active_record
+ def initialize(base_klass)
+ @base_klass = base_klass
@cached_record = {}
@column_names_with_alias = nil
end
@@ -70,7 +70,7 @@ module ActiveRecord
end
def instantiate(row)
- @cached_record[record_id(row)] ||= active_record.instantiate(extract_record(row))
+ @cached_record[record_id(row)] ||= base_klass.instantiate(extract_record(row))
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 38bc7ce7da..157b627ad5 100644
--- a/activerecord/lib/active_record/associations/preloader/has_many_through.rb
+++ b/activerecord/lib/active_record/associations/preloader/has_many_through.rb
@@ -5,9 +5,13 @@ module ActiveRecord
include ThroughAssociation
def associated_records_by_owner
- super.each do |owner, records|
- records.uniq! if reflection_scope.distinct_value
+ records_by_owner = super
+
+ if reflection_scope.distinct_value
+ records_by_owner.each_value { |records| records.uniq! }
end
+
+ records_by_owner
end
end
end
diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb
index 43520142bf..35f29b37a2 100644
--- a/activerecord/lib/active_record/associations/through_association.rb
+++ b/activerecord/lib/active_record/associations/through_association.rb
@@ -14,7 +14,7 @@ module ActiveRecord
def target_scope
scope = super
chain[1..-1].each do |reflection|
- scope = scope.merge(
+ scope.merge!(
reflection.klass.all.with_default_scope.
except(:select, :create_with, :includes, :preload, :joins, :eager_load)
)
diff --git a/activerecord/lib/active_record/attribute_assignment.rb b/activerecord/lib/active_record/attribute_assignment.rb
index e536f5ebcc..75377bba57 100644
--- a/activerecord/lib/active_record/attribute_assignment.rb
+++ b/activerecord/lib/active_record/attribute_assignment.rb
@@ -1,3 +1,4 @@
+require 'active_model/forbidden_attributes_protection'
module ActiveRecord
module AttributeAssignment
@@ -44,7 +45,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..609c6e8cab 100644
--- a/activerecord/lib/active_record/attribute_methods.rb
+++ b/activerecord/lib/active_record/attribute_methods.rb
@@ -56,7 +56,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
@@ -163,8 +163,22 @@ module ActiveRecord
# person.respond_to('age?') # => true
# person.respond_to(:nothing) # => false
def respond_to?(name, include_private = false)
+ name = name.to_s
self.class.define_attribute_methods unless self.class.attribute_methods_generated?
- super
+ 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.present? && 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+.
@@ -328,6 +342,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/serialization.rb b/activerecord/lib/active_record/attribute_methods/serialization.rb
index 25d62fdb85..9caf73e627 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
@@ -44,19 +50,13 @@ 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
+ value.unserialized_value @column.type_cast value.value
end
def type
@@ -65,17 +65,17 @@ module ActiveRecord
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 +86,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/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb
index 0df3e57947..a8a1847554 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
#
@@ -62,14 +63,14 @@ module ActiveRecord
# Note that the model is _not_ yet removed from the database:
#
# id = post.author.id
- # Author.find_by_id(id).nil? # => false
+ # Author.find_by(id: id).nil? # => false
#
# post.save
# post.reload.author # => nil
#
# Now it _is_ removed from the database:
#
- # Author.find_by_id(id).nil? # => true
+ # Author.find_by(id: id).nil? # => true
#
# === One-to-many Example
#
@@ -113,14 +114,14 @@ module ActiveRecord
# Note that the model is _not_ yet removed from the database:
#
# id = post.comments.last.id
- # Comment.find_by_id(id).nil? # => false
+ # Comment.find_by(id: id).nil? # => false
#
# post.save
# post.reload.comments.length # => 1
#
# Now it _is_ removed from the database:
#
- # Comment.find_by_id(id).nil? # => true
+ # Comment.find_by(id: id).nil? # => true
module AutosaveAssociation
extend ActiveSupport::Concern
@@ -300,7 +301,7 @@ module ActiveRecord
def association_valid?(reflection, record)
return true if record.destroyed? || record.marked_for_destruction?
- unless valid = record.valid?(validation_context)
+ unless valid = record.valid?
if reflection.options[:autosave]
record.errors.each do |attribute, message|
attribute = "#{reflection.name}.#{attribute}"
@@ -334,15 +335,18 @@ module ActiveRecord
autosave = reflection.options[:autosave]
if records = associated_records_to_validate_or_save(association, @new_record_before_save, autosave)
- records_to_destroy = []
+
+ if autosave
+ records_to_destroy = records.select(&:marked_for_destruction?)
+ records_to_destroy.each { |record| association.destroy(record) }
+ records -= records_to_destroy
+ end
+
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 != false && (@new_record_before_save || record.new_record?)
if autosave
saved = association.insert_record(record, false)
else
@@ -354,10 +358,6 @@ module ActiveRecord
raise ActiveRecord::Rollback unless saved
end
-
- records_to_destroy.each do |record|
- association.destroy(record)
- end
end
# reconstruct the scope now that we know the owner's id
diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb
index e262401da6..b06add096f 100644
--- a/activerecord/lib/active_record/base.rb
+++ b/activerecord/lib/active_record/base.rb
@@ -160,10 +160,10 @@ module ActiveRecord #:nodoc:
#
# == Dynamic attribute-based finders
#
- # Dynamic attribute-based finders are a cleaner way of getting (and/or creating) objects
+ # Dynamic attribute-based finders are a mildly deprecated way of getting (and/or creating) objects
# by simple queries without turning to SQL. They work by appending the name of an attribute
# to <tt>find_by_</tt> like <tt>Person.find_by_user_name</tt>.
- # Instead of writing <tt>Person.where(user_name: user_name).first</tt>, you just do
+ # Instead of writing <tt>Person.find_by(user_name: user_name)</tt>, you can use
# <tt>Person.find_by_user_name(user_name)</tt>.
#
# It's possible to add an exclamation point (!) on the end of the dynamic finders to get them to raise an
@@ -172,7 +172,7 @@ module ActiveRecord #:nodoc:
#
# It's also possible to use multiple attributes in the same find by separating them with "_and_".
#
- # Person.where(user_name: user_name, password: password).first
+ # Person.find_by(user_name: user_name, password: password)
# Person.find_by_user_name_and_password(user_name, password) # with dynamic finder
#
# It's even possible to call these dynamic finder methods on relations and named scopes.
diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb
index 22226b2f4f..e4c484d64b 100644
--- a/activerecord/lib/active_record/callbacks.rb
+++ b/activerecord/lib/active_record/callbacks.rb
@@ -83,7 +83,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
diff --git a/activerecord/lib/active_record/coders/yaml_column.rb b/activerecord/lib/active_record/coders/yaml_column.rb
index 8d22942a06..d3d7396c91 100644
--- a/activerecord/lib/active_record/coders/yaml_column.rb
+++ b/activerecord/lib/active_record/coders/yaml_column.rb
@@ -23,19 +23,15 @@ module ActiveRecord
def load(yaml)
return object_class.new if object_class != Object && yaml.nil?
return yaml unless yaml.is_a?(String) && yaml =~ /^---/
- begin
- obj = YAML.load(yaml)
-
- unless obj.is_a?(object_class) || obj.nil?
- raise SerializationTypeMismatch,
- "Attribute was supposed to be a #{object_class}, but was a #{obj.class}"
- end
- obj ||= object_class.new if object_class != Object
-
- obj
- rescue ArgumentError, Psych::SyntaxError
- yaml
+ obj = YAML.load(yaml)
+
+ unless obj.is_a?(object_class) || obj.nil?
+ raise SerializationTypeMismatch,
+ "Attribute was supposed to be a #{object_class}, but was a #{obj.class}"
end
+ obj ||= object_class.new if object_class != Object
+
+ obj
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
index bf2f945448..816b397fcf 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
@@ -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.
#
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 553bce983a..0be4b5cb19 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
@@ -68,8 +68,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+.
@@ -261,7 +260,7 @@ module ActiveRecord
args.each do |col|
column("#{col}_id", :integer, options)
column("#{col}_type", :string, polymorphic.is_a?(Hash) ? polymorphic : options) if polymorphic
- index(polymorphic ? %w(id type).map { |t| "#{col}_#{t}" } : "#{col}_id", index_options.is_a?(Hash) ? index_options : nil) if index_options
+ index(polymorphic ? %w(id type).map { |t| "#{col}_#{t}" } : "#{col}_id", index_options.is_a?(Hash) ? index_options : {}) if index_options
end
end
alias :belongs_to :references
@@ -273,6 +272,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 742d198096..3ac55a0f11 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
@@ -443,7 +443,7 @@ module ActiveRecord
#
# add_index(:suppliers, :name)
#
- # generates
+ # generates:
#
# CREATE INDEX suppliers_name_index ON suppliers(name)
#
@@ -451,7 +451,7 @@ module ActiveRecord
#
# add_index(:accounts, [:branch_id, :party_id], unique: true)
#
- # generates
+ # generates:
#
# CREATE UNIQUE INDEX accounts_branch_id_party_id_index ON accounts(branch_id, party_id)
#
@@ -459,7 +459,7 @@ module ActiveRecord
#
# add_index(:accounts, [:branch_id, :party_id], unique: true, name: 'by_branch_party')
#
- # generates
+ # generates:
#
# CREATE UNIQUE INDEX by_branch_party ON accounts(branch_id, party_id)
#
@@ -467,13 +467,13 @@ module ActiveRecord
#
# add_index(:accounts, :name, name: 'by_name', length: 10)
#
- # generates
+ # generates:
#
# CREATE INDEX by_name ON accounts(name(10))
#
# add_index(:accounts, [:name, :surname], name: 'by_name_surname', length: {name: 10, surname: 15})
#
- # generates
+ # generates:
#
# CREATE INDEX by_name_surname ON accounts(name(10), surname(15))
#
@@ -483,7 +483,7 @@ module ActiveRecord
#
# add_index(:accounts, [:branch_id, :party_id, :surname], order: {branch_id: :desc, party_id: :asc})
#
- # generates
+ # generates:
#
# CREATE INDEX by_branch_desc_party ON accounts(branch_id DESC, party_id ASC, surname)
#
@@ -493,26 +493,26 @@ module ActiveRecord
#
# add_index(:accounts, [:branch_id, :party_id], unique: true, where: "active")
#
- # generates
+ # generates:
#
# CREATE UNIQUE INDEX index_accounts_on_branch_id_and_party_id ON accounts(branch_id, party_id) WHERE active
#
# ====== Creating an index with a specific method
#
- # add_index(:developers, :name, :using => 'btree')
+ # add_index(:developers, :name, using: 'btree')
#
- # generates
+ # generates:
#
- # CREATE INDEX index_developers_on_name ON developers USING btree (name) -- PostgreSQL
- # CREATE INDEX index_developers_on_name USING btree ON developers (name) -- MySQL
+ # CREATE INDEX index_developers_on_name ON developers USING btree (name) -- PostgreSQL
+ # CREATE INDEX index_developers_on_name USING btree ON developers (name) -- MySQL
#
# Note: only supported by PostgreSQL and MySQL
#
# ====== Creating an index with a specific type
#
- # add_index(:developers, :name, :type => :fulltext)
+ # add_index(:developers, :name, type: :fulltext)
#
- # generates
+ # generates:
#
# CREATE FULLTEXT INDEX index_developers_on_name ON developers (name) -- MySQL
#
@@ -606,7 +606,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
@@ -695,12 +695,21 @@ module ActiveRecord
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")
+ # distinct("posts.id", ["posts.created_at desc"])
#
def distinct(columns, order_by)
- "DISTINCT #{columns}"
+ ActiveSupport::Deprecation.warn("#distinct is deprecated and shall be removed from future releases.")
+ "DISTINCT #{columns_for_distinct(columns, order_by)}"
+ end
+
+ # 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.
+ #
+ # 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.
@@ -755,37 +764,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)
+ 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
+ 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 index_algorithms.key?(options[:algorithm])
- algorithm = index_algorithms[options[:algorithm]]
- elsif options[:algorithm].present?
+ 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?
-
- 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"
+ }
+ 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
diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
index 0b24c042ee..e232cad982 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
@@ -101,6 +101,10 @@ module ActiveRecord
@visitor = nil
end
+ def valid_type?(type)
+ true
+ end
+
class SchemaCreation
def initialize(conn)
@conn = conn
@@ -295,9 +299,8 @@ module ActiveRecord
[]
end
- # A list of index algorithms, to be filled by adapters that
- # support them. MySQL and PostgreSQL has support for them right
- # now.
+ # A list of index algorithms, to be filled by adapters that support them.
+ # MySQL and PostgreSQL have support for them right now.
def index_algorithms
{}
end
@@ -443,7 +446,7 @@ module ActiveRecord
def translate_exception(exception, message)
# override in derived class
- ActiveRecord::StatementInvalid.new(message)
+ ActiveRecord::StatementInvalid.new(message, exception)
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 84bf3acf0e..5b25b26164 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
@@ -91,6 +91,8 @@ module ActiveRecord
def extract_limit(sql_type)
case sql_type
+ when /^enum\((.+)\)/i
+ $1.split(',').map{|enum| enum.strip.length - 2}.max
when /blob|text/i
case sql_type
when /tiny/i
@@ -107,8 +109,6 @@ module ActiveRecord
when /^mediumint/i; 3
when /^smallint/i; 2
when /^tinyint/i; 1
- when /^enum\((.+)\)/i
- $1.split(',').map{|enum| enum.strip.length - 2}.max
else
super
end
@@ -296,7 +296,7 @@ module ActiveRecord
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."
+ raise ActiveRecord::StatementInvalid.new("'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.", exception.original_exception)
else
raise
end
@@ -373,7 +373,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>.
diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb
index a4b3a0c584..609ccc2ed2 100644
--- a/activerecord/lib/active_record/connection_adapters/column.rb
+++ b/activerecord/lib/active_record/connection_adapters/column.rb
@@ -161,7 +161,7 @@ module ActiveRecord
def value_to_date(value)
if value.is_a?(String)
- return nil if value.blank?
+ return nil if value.empty?
fast_string_to_date(value) || fallback_string_to_date(value)
elsif value.respond_to?(:to_date)
value.to_date
@@ -172,14 +172,14 @@ module ActiveRecord
def string_to_time(string)
return string unless string.is_a?(String)
- return nil if string.blank?
+ return nil if string.empty?
fast_string_to_time(string) || fallback_string_to_time(string)
end
def string_to_dummy_time(string)
return string unless string.is_a?(String)
- return nil if string.blank?
+ return nil if string.empty?
dummy_time_string = "2000-01-01 #{string}"
@@ -192,7 +192,7 @@ module ActiveRecord
# convert something to a boolean
def value_to_boolean(value)
- if value.is_a?(String) && value.blank?
+ if value.is_a?(String) && value.empty?
nil
else
TRUE_VALUES.include?(value)
diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
index 6b319fed88..530a27d099 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
@@ -4,7 +4,7 @@ gem 'mysql2', '~> 0.3.10'
require 'mysql2'
module ActiveRecord
- module ConnectionHandling
+ module ConnectionHandling # :nodoc:
# Establishes a connection to the database that's used by all Active Record objects.
def mysql2_connection(config)
config = config.symbolize_keys
diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
index 0e01fe0fdb..1826d88500 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
@@ -16,9 +16,9 @@ class Mysql
end
module ActiveRecord
- module ConnectionHandling
+ module ConnectionHandling # :nodoc:
# Establishes a connection to the database that's used by all Active Record objects.
- def mysql_connection(config) # :nodoc:
+ def mysql_connection(config)
config = config.symbolize_keys
host = config[:host]
port = config[:port]
@@ -383,7 +383,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 +393,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
@@ -425,9 +433,7 @@ module ActiveRecord
if field.decimals > 0
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)
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb b/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb
index 14ef07a75e..a73f0ac57f 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb
@@ -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,7 +60,7 @@ module ActiveRecord
end
def json_to_string(object)
- if Hash === object
+ if Hash === object || Array === object
ActiveSupport::JSON.encode(object)
else
object
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb
index 51f377dfd7..1be116ce10 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb
@@ -18,8 +18,19 @@ module ActiveRecord
end
end
+ class Bit < Type
+ def type_cast(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
@@ -322,14 +333,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..e9daa5d7ff 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
@@ -98,6 +99,7 @@ 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)
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 0168c36abc..a651b6c32e 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
@@ -156,13 +156,15 @@ module ActiveRecord
column_names = columns.values_at(*indkey).compact
- # add info on sort order for columns (only desc order is explicitly specified, asc is the default)
- desc_order_columns = inddef.scan(/(\w+) DESC/).flatten
- orders = desc_order_columns.any? ? Hash[desc_order_columns.map {|order_column| [order_column, :desc]}] : {}
- where = inddef.scan(/WHERE (.+)$/).flatten[0]
- using = inddef.scan(/USING (.+?) /).flatten[0].to_sym
-
- column_names.empty? ? nil : IndexDefinition.new(table_name, index_name, unique, column_names, [], orders, where, nil, using)
+ unless column_names.empty?
+ # add info on sort order for columns (only desc order is explicitly specified, asc is the default)
+ desc_order_columns = inddef.scan(/(\w+) DESC/).flatten
+ orders = desc_order_columns.any? ? Hash[desc_order_columns.map {|order_column| [order_column, :desc]}] : {}
+ where = inddef.scan(/WHERE (.+)$/).flatten[0]
+ using = inddef.scan(/USING (.+?) /).flatten[0].to_sym
+
+ IndexDefinition.new(table_name, index_name, unique, column_names, [], orders, where, nil, using)
+ end
end.compact
end
@@ -319,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)
@@ -330,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
@@ -464,22 +467,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 46e41e6b48..d5a603cadc 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
@@ -16,15 +16,15 @@ require 'pg'
require 'ipaddr'
module ActiveRecord
- module ConnectionHandling
+ module ConnectionHandling # :nodoc:
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) # :nodoc:
+ def postgresql_connection(config)
conn_params = config.symbolize_keys
conn_params.delete_if { |_, v| v.nil? }
@@ -80,7 +80,7 @@ module ActiveRecord
when /\A'(.*)'::(num|date|tstz|ts|int4|int8)range\z/m
$1
# Numeric types
- when /\A\(?(-?\d+(\.\d*)?\)?)\z/
+ when /\A\(?(-?\d+(\.\d*)?\)?(::bigint)?)\z/
$1
# Character types
when /\A\(?'(.*)'::.*\b(?:character varying|bpchar|text)\z/m
@@ -330,9 +330,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
@@ -594,9 +622,9 @@ module ActiveRecord
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 +646,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
@@ -740,7 +768,7 @@ module ActiveRecord
end
def exec_cache(sql, binds)
- stmt_key = prepare_statement sql
+ stmt_key = prepare_statement(sql)
# Clear the queue
@connection.get_last_result
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
index c3bebf7a5e..7d940fe1c9 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
@@ -6,9 +6,9 @@ gem 'sqlite3', '~> 1.3.6'
require 'sqlite3'
module ActiveRecord
- module ConnectionHandling
+ module ConnectionHandling # :nodoc:
# sqlite3 adapter reuses sqlite_connection.
- def sqlite3_connection(config) # :nodoc:
+ def sqlite3_connection(config)
# Require database.
unless config[:database]
raise ArgumentError, "No database file specified. Missing argument: database"
@@ -499,10 +499,6 @@ module ActiveRecord
rename_column_indexes(table_name, column_name, new_column_name)
end
- def valid_type?(type)
- true
- end
-
protected
def select(sql, name = nil, binds = []) #:nodoc:
exec_query(sql, name, binds)
diff --git a/activerecord/lib/active_record/connection_handling.rb b/activerecord/lib/active_record/connection_handling.rb
index d6d998c7be..a1943dfcb0 100644
--- a/activerecord/lib/active_record/connection_handling.rb
+++ b/activerecord/lib/active_record/connection_handling.rb
@@ -15,15 +15,15 @@ module ActiveRecord
# Example for SQLite database:
#
# ActiveRecord::Base.establish_connection(
- # adapter: "sqlite",
- # database: "path/to/dbfile"
+ # adapter: "sqlite",
+ # database: "path/to/dbfile"
# )
#
# Also accepts keys as strings (for parsing from YAML for example):
#
# ActiveRecord::Base.establish_connection(
- # "adapter" => "sqlite",
- # "database" => "path/to/dbfile"
+ # "adapter" => "sqlite",
+ # "database" => "path/to/dbfile"
# )
#
# Or a URL:
@@ -54,11 +54,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:
@@ -79,7 +79,7 @@ module ActiveRecord
connection_handler.retrieve_connection(self)
end
- # Returns true if Active Record is connected.
+ # Returns +true+ if Active Record is connected.
def connected?
connection_handler.connected?(self)
end
diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb
index db5e7b82ca..ba053700f2 100644
--- a/activerecord/lib/active_record/core.rb
+++ b/activerecord/lib/active_record/core.rb
@@ -80,11 +80,11 @@ module ActiveRecord
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
@@ -307,9 +307,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
@@ -344,9 +346,15 @@ module ActiveRecord
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)}"
diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb
index c615d59725..7e38719811 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
@@ -158,6 +155,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
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/fixtures.rb b/activerecord/lib/active_record/fixtures.rb
index c26fc76515..33e19313a0 100644
--- a/activerecord/lib/active_record/fixtures.rb
+++ b/activerecord/lib/active_record/fixtures.rb
@@ -567,7 +567,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
@@ -708,11 +708,18 @@ module ActiveRecord
module TestFixtures
extend ActiveSupport::Concern
- included do
- setup :setup_fixtures
- teardown :teardown_fixtures
+ def before_setup
+ setup_fixtures
+ super
+ end
+
+ def after_teardown
+ super
+ teardown_fixtures
+ end
- class_attribute :fixture_path
+ included do
+ class_attribute :fixture_path, :instance_writer => false
class_attribute :fixture_table_names
class_attribute :fixture_class_names
class_attribute :use_transactional_fixtures
@@ -765,8 +772,7 @@ module ActiveRecord
def try_to_load_dependency(file_name)
require_dependency file_name
rescue LoadError => e
- # Let's hope the developer has included it himself
-
+ # Let's hope the developer has included it
# Let's warn in case this is a subdependency, otherwise
# subdependency error messages are totally cryptic
if ActiveRecord::Base.logger
diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb
index f54865c86e..40976bc29e 100644
--- a/activerecord/lib/active_record/inheritance.rb
+++ b/activerecord/lib/active_record/inheritance.rb
@@ -116,9 +116,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
@@ -174,7 +175,7 @@ module ActiveRecord
if subclass_name.present? && subclass_name != self.name
subclass = subclass_name.safe_constantize
- unless subclasses.include?(subclass)
+ unless descendants.include?(subclass)
raise ActiveRecord::SubclassNotFound.new("Invalid single-table inheritance type: #{subclass_name} is not a subclass of #{name}")
end
diff --git a/activerecord/lib/active_record/integration.rb b/activerecord/lib/active_record/integration.rb
index 48c73d7781..2589b2f3da 100644
--- a/activerecord/lib/active_record/integration.rb
+++ b/activerecord/lib/active_record/integration.rb
@@ -21,7 +21,7 @@ module ActiveRecord
# <tt>resources :users</tt> route. Normally, +user_path+ will
# construct a path with the user object's 'id' in it:
#
- # user = User.find_by_name('Phusion')
+ # user = User.find_by(name: 'Phusion')
# user_path(user) # => "/users/1"
#
# You can override +to_param+ in your model to make +user_path+ construct
@@ -33,7 +33,7 @@ module ActiveRecord
# end
# end
#
- # user = User.find_by_name('Phusion')
+ # user = User.find_by(name: 'Phusion')
# user_path(user) # => "/users/Phusion"
def to_param
# We can't use alias_method here, because method 'id' optimizes itself on the fly.
diff --git a/activerecord/lib/active_record/locking/pessimistic.rb b/activerecord/lib/active_record/locking/pessimistic.rb
index 8e4ddcac82..ddf2afca0c 100644
--- a/activerecord/lib/active_record/locking/pessimistic.rb
+++ b/activerecord/lib/active_record/locking/pessimistic.rb
@@ -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..61e5c120df 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
diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb
index d3edcf3cdb..e96c347f6f 100644
--- a/activerecord/lib/active_record/migration.rb
+++ b/activerecord/lib/active_record/migration.rb
@@ -357,11 +357,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
@@ -466,7 +469,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
@@ -668,6 +671,7 @@ module ActiveRecord
copied
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
@@ -699,6 +703,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 +722,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,7 +802,11 @@ 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)
@@ -867,11 +889,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
@@ -892,10 +918,7 @@ module ActiveRecord
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 +955,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..9782a48055 100644
--- a/activerecord/lib/active_record/migration/command_recorder.rb
+++ b/activerecord/lib/active_record/migration/command_recorder.rb
@@ -144,7 +144,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/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb
index 602ab9e2f4..0e8822d63f 100644
--- a/activerecord/lib/active_record/nested_attributes.rb
+++ b/activerecord/lib/active_record/nested_attributes.rb
@@ -90,8 +90,9 @@ module ActiveRecord
# accepts_nested_attributes_for :posts
# end
#
- # You can now set or update attributes on an associated post model through
- # the attribute hash.
+ # You can now set or update attributes on the associated posts through
+ # an attribute hash for a member: include the key +:posts_attributes+
+ # with an array of hashes of post attributes as a value.
#
# For each hash that does _not_ have an <tt>id</tt> key a new record will
# be instantiated, unless the hash also contains a <tt>_destroy</tt> key
@@ -114,10 +115,10 @@ module ActiveRecord
# hashes if they fail to pass your criteria. For example, the previous
# example could be rewritten as:
#
- # class Member < ActiveRecord::Base
- # has_many :posts
- # accepts_nested_attributes_for :posts, reject_if: proc { |attributes| attributes['title'].blank? }
- # end
+ # class Member < ActiveRecord::Base
+ # has_many :posts
+ # accepts_nested_attributes_for :posts, reject_if: proc { |attributes| attributes['title'].blank? }
+ # end
#
# params = { member: {
# name: 'joe', posts_attributes: [
@@ -134,19 +135,19 @@ module ActiveRecord
#
# Alternatively, :reject_if also accepts a symbol for using methods:
#
- # class Member < ActiveRecord::Base
- # has_many :posts
- # accepts_nested_attributes_for :posts, reject_if: :new_record?
- # end
+ # class Member < ActiveRecord::Base
+ # has_many :posts
+ # accepts_nested_attributes_for :posts, reject_if: :new_record?
+ # end
#
- # class Member < ActiveRecord::Base
- # has_many :posts
- # accepts_nested_attributes_for :posts, reject_if: :reject_posts
+ # class Member < ActiveRecord::Base
+ # has_many :posts
+ # accepts_nested_attributes_for :posts, reject_if: :reject_posts
#
- # def reject_posts(attributed)
- # attributed['title'].blank?
- # end
- # end
+ # def reject_posts(attributed)
+ # attributed['title'].blank?
+ # end
+ # end
#
# If the hash contains an <tt>id</tt> key that matches an already
# associated record, the matching record will be modified:
@@ -183,6 +184,29 @@ module ActiveRecord
# member.save
# member.reload.posts.length # => 1
#
+ # Nested attributes for an associated collection can also be passed in
+ # the form of a hash of hashes instead of an array of hashes:
+ #
+ # Member.create(name: 'joe',
+ # posts_attributes: { first: { title: 'Foo' },
+ # second: { title: 'Bar' } })
+ #
+ # has the same effect as
+ #
+ # Member.create(name: 'joe',
+ # posts_attributes: [ { title: 'Foo' },
+ # { title: 'Bar' } ])
+ #
+ # The keys of the hash which is the value for +:posts_attributes+ are
+ # ignored in this case.
+ # However, it is not allowed to use +'id'+ or +:id+ for one of
+ # such keys, otherwise the hash will be wrapped in an array and
+ # interpreted as an attribute hash for a single post.
+ #
+ # Passing attributes for an associated collection in the form of a hash
+ # of hashes can be used with hashes generated from HTTP/HTML parameters,
+ # where there maybe no natural way to submit an array of hashes.
+ #
# === Saving
#
# All changes to models, including the destruction of those marked for
@@ -205,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? } }
@@ -264,6 +309,11 @@ module ActiveRecord
reflection.options[:autosave] = true
add_autosave_association_callbacks(reflection)
+ # Clear cached values of any inverse associations found in the
+ # reflection and prevent the reflection from finding inverses
+ # automatically in the future.
+ reflection.remove_automatic_inverse_of!
+
nested_attributes_options = self.nested_attributes_options.dup
nested_attributes_options[association_name.to_sym] = options
self.nested_attributes_options = nested_attributes_options
@@ -332,20 +382,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'])
+ 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
@@ -428,7 +486,7 @@ module ActiveRecord
assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy])
end
else
- raise_nested_attributes_record_not_found(association_name, attributes['id'])
+ raise_nested_attributes_record_not_found!(association_name, attributes['id'])
end
end
end
@@ -490,7 +548,7 @@ module ActiveRecord
end
end
- def raise_nested_attributes_record_not_found(association_name, record_id)
+ def raise_nested_attributes_record_not_found!(association_name, record_id)
raise RecordNotFound, "Couldn't find #{self.class.reflect_on_association(association_name).klass.name} with ID=#{record_id} for #{self.class.name} with ID=#{id}"
end
end
diff --git a/activerecord/lib/active_record/null_relation.rb b/activerecord/lib/active_record/null_relation.rb
index 711fc8b883..d166f0dd66 100644
--- a/activerecord/lib/active_record/null_relation.rb
+++ b/activerecord/lib/active_record/null_relation.rb
@@ -39,7 +39,7 @@ module ActiveRecord
end
def to_sql
- @to_sql ||= ""
+ ""
end
def where_values_hash
@@ -55,7 +55,11 @@ module ActiveRecord
end
def calculate(_operation, _column_name, _options = {})
- nil
+ 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..582006ea7d 100644
--- a/activerecord/lib/active_record/persistence.rb
+++ b/activerecord/lib/active_record/persistence.rb
@@ -51,7 +51,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 +99,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 +121,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
@@ -204,6 +210,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 +333,44 @@ 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 modifies the receiver in-place. Attributes are updated, 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 commom 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
@@ -428,23 +470,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..3d85898c41 100644
--- a/activerecord/lib/active_record/querying.rb
+++ b/activerecord/lib/active_record/querying.rb
@@ -8,13 +8,13 @@ module ActiveRecord
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
+ :having, :create_with, :uniq, :distinct, :references, :none, :unscope, :to => :all
delegate :count, :average, :minimum, :maximum, :sum, :calculate, :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 +29,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..31a0ace864 100644
--- a/activerecord/lib/active_record/railtie.rb
+++ b/activerecord/lib/active_record/railtie.rb
@@ -36,6 +36,20 @@ module ActiveRecord
rake_tasks do
require "active_record/base"
+
+ ActiveRecord::Tasks::DatabaseTasks.env = Rails.env
+ ActiveRecord::Tasks::DatabaseTasks.db_dir = Rails.application.config.paths["db"].first
+ ActiveRecord::Tasks::DatabaseTasks.seed_loader = Rails.application
+ ActiveRecord::Tasks::DatabaseTasks.database_configuration = Rails.application.config.database_configuration
+ ActiveRecord::Tasks::DatabaseTasks.migrations_paths = Rails.application.paths['db/migrate'].to_a
+ ActiveRecord::Tasks::DatabaseTasks.fixtures_path = File.join Rails.root, 'test', 'fixtures'
+
+ if defined?(APP_RAKEFILE) && engine = Rails::Engine.find(find_engine_path(APP_RAKEFILE))
+ if engine.paths['db/migrate'].existent
+ ActiveRecord::Tasks::DatabaseTasks.migrations_paths += engine.paths['db/migrate'].to_a
+ end
+ end
+
load "active_record/railties/databases.rake"
end
diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake
index d92e268109..f69e9b2217 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
@@ -156,7 +150,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
@@ -166,11 +160,11 @@ db_namespace = namespace :db do
end
# desc "Raises an error if there are pending migrations"
- task :abort_if_pending_migrations => [:environment, :load_config] do
+ task :abort_if_pending_migrations => :environment 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
@@ -184,7 +178,7 @@ db_namespace = namespace :db do
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 +186,15 @@ 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']
+ STDERR.puts "Using FIXTURES_PATH env variable is deprecated, please use " +
+ "ActiveRecord::Tasks::DatabaseTasks.fixtures_path = '/path/to/fixtures' " +
+ "instead."
+ 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 +211,16 @@ 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']
+ STDERR.puts "Using FIXTURES_PATH env variable is deprecated, please use " +
+ "ActiveRecord::Tasks::DatabaseTasks.fixtures_path = '/path/to/fixtures' " +
+ "instead."
+ 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|
@@ -228,7 +239,7 @@ db_namespace = namespace :db do
desc 'Create a db/schema.rb file that can be portably used 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 +248,9 @@ 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
+ file = ENV['SCHEMA'] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, 'schema.rb')
+ ActiveRecord::Tasks::DatabaseTasks.check_schema_file(file)
+ load(file)
end
task :load_if_ruby => ['db:create', :environment] do
@@ -253,7 +261,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,7 +270,7 @@ 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")
+ filename = File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "schema_cache.dump")
FileUtils.rm(filename) if File.exists?(filename)
end
end
@@ -270,32 +278,11 @@ db_namespace = namespace :db do
end
namespace :structure do
- 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
-
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
- case current_config['adapter']
- when 'oci', 'oracle'
- ActiveRecord::Base.establish_connection(current_config)
- File.open(filename, "w:utf-8") { |f| f << ActiveRecord::Base.connection.structure_dump }
- when 'sqlserver'
- `smoscript -s #{current_config['host']} -d #{current_config['database']} -u #{current_config['username']} -p #{current_config['password']} -f #{filename} -A -U`
- when "firebird"
- set_firebird_env(current_config)
- db_string = firebird_db_string(current_config)
- sh "isql -a #{db_string} > #{filename}"
- else
- ActiveRecord::Tasks::DatabaseTasks.structure_dump(current_config, filename)
- end
+ ActiveRecord::Tasks::DatabaseTasks.structure_dump(current_config, filename)
if ActiveRecord::Base.connection.supports_migrations?
File.open(filename, "a") do |f|
@@ -307,23 +294,10 @@ 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(ActiveRecord::Tasks::DatabaseTasks.db_dir, "structure.sql")
+ ActiveRecord::Tasks::DatabaseTasks.check_schema_file(filename)
current_config = ActiveRecord::Tasks::DatabaseTasks.current_config
- filename = ENV['DB_STRUCTURE'] || File.join(Rails.root, "db", "structure.sql")
- case current_config['adapter']
- when 'sqlserver'
- `sqlcmd -S #{current_config['host']} -d #{current_config['database']} -U #{current_config['username']} -P #{current_config['password']} -i #{filename}`
- when 'oci', 'oracle'
- ActiveRecord::Base.establish_connection(current_config)
- IO.read(filename).split(";\n\n").each do |ddl|
- ActiveRecord::Base.connection.execute(ddl)
- end
- when 'firebird'
- set_firebird_env(current_config)
- db_string = firebird_db_string(current_config)
- sh "isql -i #{filename} #{db_string}"
- else
- ActiveRecord::Tasks::DatabaseTasks.structure_load(current_config, filename)
- end
+ ActiveRecord::Tasks::DatabaseTasks.structure_load(current_config, filename)
end
task :load_if_sql => ['db:create', :environment] do
@@ -345,9 +319,13 @@ db_namespace = namespace :db do
# 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
+ begin
+ ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations['test'])
+ ActiveRecord::Schema.verbose = false
+ db_namespace["schema:load"].invoke
+ ensure
+ ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations[Rails.env])
+ end
end
# desc "Recreate the test database from an existent structure.sql file"
@@ -378,29 +356,11 @@ db_namespace = namespace :db do
# desc "Empty the test database"
task :purge => [:environment, :load_config] do
- abcs = ActiveRecord::Base.configurations
- case abcs['test']['adapter']
- when 'sqlserver'
- test = abcs.deep_dup['test']
- test_database = test['database']
- test['database'] = 'master'
- ActiveRecord::Base.establish_connection(test)
- ActiveRecord::Base.connection.recreate_database!(test_database)
- when "oci", "oracle"
- ActiveRecord::Base.establish_connection(:test)
- ActiveRecord::Base.connection.structure_drop.split(";\n\n").each do |ddl|
- ActiveRecord::Base.connection.execute(ddl)
- end
- when 'firebird'
- ActiveRecord::Base.establish_connection(:test)
- ActiveRecord::Base.connection.recreate_database!
- else
- ActiveRecord::Tasks::DatabaseTasks.purge abcs['test']
- end
+ ActiveRecord::Tasks::DatabaseTasks.purge ActiveRecord::Base.configurations['test']
end
# desc 'Check for pending migrations and load the test schema'
- task :prepare => 'db:abort_if_pending_migrations' do
+ task :prepare => :load_config do
unless ActiveRecord::Base.configurations.blank?
db_namespace['test:load'].invoke
end
@@ -426,7 +386,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
@@ -436,5 +396,5 @@ namespace :railties do
end
end
-task 'test:prepare' => 'db:test:prepare'
+task 'test:prepare' => ['db:test:prepare', 'db:test:load', '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 0995750ecd..2aec73bd5b 100644
--- a/activerecord/lib/active_record/reflection.rb
+++ b/activerecord/lib/active_record/reflection.rb
@@ -5,10 +5,12 @@ 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
+ # \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
@@ -21,18 +23,24 @@ module ActiveRecord
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)
+ klass = AggregateReflection
+ end
+
+ reflection = klass.new(macro, name, scope, options, active_record)
+
+ if klass == AggregateReflection
+ self.aggregate_reflections = self.aggregate_reflections.merge(name => reflection)
+ else
+ self.reflections = self.reflections.merge(name => reflection)
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
@@ -176,6 +187,7 @@ module ActiveRecord
def initialize(*args)
super
@collection = [:has_many, :has_and_belongs_to_many].include?(macro)
+ @automatic_inverse_of = nil
end
# Returns a new, unsaved instance of the associated class. +attributes+ will
@@ -284,15 +296,31 @@ module ActiveRecord
alias :source_macro :macro
def has_inverse?
- @options[:inverse_of]
+ @options[:inverse_of] || find_inverse_of_automatically
end
def inverse_of
- if has_inverse?
- @inverse_of ||= klass.reflect_on_association(options[:inverse_of])
+ @inverse_of ||= if options[:inverse_of]
+ klass.reflect_on_association(options[:inverse_of])
+ else
+ find_inverse_of_automatically
end
end
+ # Clears the cached value of +@inverse_of+ on this object. This will
+ # not remove the :inverse_of option however, so future calls on the
+ # +inverse_of+ will have to recompute the inverse.
+ def clear_inverse_of_cache!
+ @inverse_of = nil
+ end
+
+ # Removes the cached inverse association that was found automatically
+ # and prevents this object from finding the inverse association
+ # automatically in the future.
+ def remove_automatic_inverse_of!
+ @automatic_inverse_of = false
+ end
+
def polymorphic_inverse_of(associated_class)
if has_inverse?
if inverse_relationship = associated_class.reflect_on_association(options[:inverse_of])
@@ -361,7 +389,85 @@ 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]
+
private
+ # Attempts to find the inverse association automatically.
+ # If it cannot find a suitable inverse association, it returns
+ # nil.
+ def find_inverse_of_automatically
+ if @automatic_inverse_of == false
+ nil
+ elsif @automatic_inverse_of.nil?
+ set_automatic_inverse_of
+ else
+ klass.reflect_on_association(@automatic_inverse_of)
+ end
+ end
+
+ # Sets the +@automatic_inverse_of+ instance variable, and returns
+ # either nil or the inverse association that it finds.
+ #
+ # This method caches the inverse association that is found so that
+ # future calls to +find_inverse_of_automatically+ have much less
+ # overhead.
+ def set_automatic_inverse_of
+ if can_find_inverse_of_automatically?(self)
+ inverse_name = active_record.name.downcase.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)
+ @automatic_inverse_of = inverse_name
+ reflection
+ else
+ @automatic_inverse_of = false
+ nil
+ end
+ else
+ @automatic_inverse_of = false
+ nil
+ end
+ end
+
+ # Checks if the inverse reflection that is returned from the
+ # +set_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.try(:name) &&
+ klass.primary_key == reflection.active_record_primary_key &&
+ 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 +499,7 @@ 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
+ # 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
@@ -401,6 +507,15 @@ module ActiveRecord
# has_many :tags, through: :taggings
# end
#
+ # class Tagging < ActiveRecord::Base
+ # belongs_to :post
+ # belongs_to :tag
+ # end
+ #
+ # tags_reflection = Post.reflect_on_association(:tags)
+ # tags_reflection.source_reflection
+ # # => <ActiveRecord::Reflection::AssociationReflection: @macro=:belongs_to, @name=:tag, @active_record=Tagging, @plural_name="tags">
+ #
def source_reflection
@source_reflection ||= source_reflection_names.collect { |name| through_reflection.klass.reflect_on_association(name) }.compact.first
end
@@ -414,10 +529,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
@@ -426,6 +542,17 @@ module ActiveRecord
# The chain is built by recursively calling #chain on the source reflection and the through
# reflection. The base case for the recursion is a normal association, which just returns
# [self] as its #chain.
+ #
+ # class Post < ActiveRecord::Base
+ # has_many :taggings
+ # has_many :tags, through: :taggings
+ # end
+ #
+ # tags_reflection = Post.reflect_on_association(:tags)
+ # tags_reflection.chain
+ # # => [<ActiveRecord::Reflection::ThroughReflection: @macro=:has_many, @name=:tags, @options={:through=>:taggings}, @active_record=Post>,
+ # <ActiveRecord::Reflection::AssociationReflection: @macro=:has_many, @name=:taggings, @options={}, @active_record=Post>]
+ #
def chain
@chain ||= begin
chain = source_reflection.chain + through_reflection.chain
@@ -496,12 +623,19 @@ module ActiveRecord
source_reflection.options[:primary_key] || primary_key(klass || self.klass)
end
- # Gets an array of possible <tt>:through</tt> source reflection names:
+ # Gets an array of possible <tt>:through</tt> source reflection names in both singular and plural form.
+ #
+ # class Post < ActiveRecord::Base
+ # has_many :taggings
+ # has_many :tags, through: :taggings
+ # end
#
- # [:singularized, :pluralized]
+ # tags_reflection = Post.reflect_on_association(:tags)
+ # tags_reflection.source_reflection_names
+ # # => [:tag, :tags]
#
def source_reflection_names
- @source_reflection_names ||= (options[:source] ? [options[:source]] : [name.to_s.singularize, name]).collect { |n| n.to_sym }
+ @source_reflection_names ||= (options[:source] ? [options[:source]] : [name.to_s.singularize, name]).collect { |n| n.to_sym }.uniq
end
def source_options
diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb
index 4d24654015..d37471e9ad 100644
--- a/activerecord/lib/active_record/relation.rb
+++ b/activerecord/lib/active_record/relation.rb
@@ -10,7 +10,7 @@ module ActiveRecord
:extending]
SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :from, :reordering,
- :reverse_order, :distinct, :create_with]
+ :reverse_order, :distinct, :create_with, :uniq]
VALUE_METHODS = MULTI_VALUE_METHODS + SINGLE_VALUE_METHODS
@@ -26,7 +26,6 @@ module ActiveRecord
@klass = klass
@table = table
@values = values
- @implicit_readonly = nil
@loaded = false
@default_scoped = false
end
@@ -39,7 +38,7 @@ module ActiveRecord
reset
end
- def insert(values)
+ def insert(values) # :nodoc:
primary_key_value = nil
if primary_key && Hash === values
@@ -56,16 +55,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 +63,7 @@ module ActiveRecord
im.insert substitutes
end
- conn.insert(
+ @klass.connection.insert(
im,
'SQL',
primary_key,
@@ -82,6 +72,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.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 +154,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,7 +247,7 @@ module ActiveRecord
def empty?
return @records.empty? if loaded?
- c = count
+ c = count(:all)
c.respond_to?(:zero?) ? c.zero? : c.empty?
end
@@ -475,11 +512,13 @@ 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|
+ scope = with_default_scope
+ equalities = scope.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 = Hash[scope.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
@@ -566,10 +605,7 @@ module ActiveRecord
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
+ @records.each { |record| record.readonly! } if readonly_value
else
@records = default_scoped.to_a
end
diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb
index be011b22af..4becf3980d 100644
--- a/activerecord/lib/active_record/relation/calculations.rb
+++ b/activerecord/lib/active_record/relation/calculations.rb
@@ -27,7 +27,7 @@ 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 = {})
calculate(:average, column_name, options)
end
@@ -36,7 +36,7 @@ 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 = {})
calculate(:minimum, column_name, options)
end
@@ -45,7 +45,7 @@ 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 = {})
calculate(:maximum, column_name, options)
end
@@ -54,17 +54,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,
@@ -82,7 +74,7 @@ module ActiveRecord
# puts values["Drake"]
# # => 43
#
- # drake = Family.find_by_last_name('Drake')
+ # drake = Family.find_by(last_name: 'Drake')
# values = Person.group(:family).maximum(:age) # Person belongs_to :family
# puts values[drake]
# # => 43
@@ -101,6 +93,10 @@ module ActiveRecord
def calculate(operation, column_name, options = {})
relation = with_default_scope
+ if column_name.is_a?(Symbol) && attribute_aliases.key?(column_name.to_s)
+ column_name = attribute_aliases[column_name.to_s].to_sym
+ end
+
if relation.equal?(self)
if has_include?(column_name)
construct_relation_for_association_calculations.calculate(operation, column_name, options)
@@ -110,8 +106,6 @@ module ActiveRecord
else
relation.calculate(operation, column_name, options)
end
- rescue ThrowResult
- 0
end
# Use <tt>pluck</tt> as a shortcut to select one or more attributes without
@@ -135,7 +129,7 @@ module ActiveRecord
# # SELECT people.id, people.name FROM people
# # => [[1, 'David'], [2, 'Jeremy'], [3, 'Jose']]
#
- # Person.uniq.pluck(:role)
+ # Person.pluck('DISTINCT role')
# # SELECT DISTINCT role FROM people
# # => ['admin', 'member', 'guest']
#
@@ -149,11 +143,17 @@ 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)}"
- else
- column_name
+ if column_name.is_a?(Symbol)
+ if attribute_aliases.key?(column_name.to_s)
+ column_name = attribute_aliases[column_name.to_s].to_sym
+ end
+
+ if self.columns_hash.key?(column_name.to_s)
+ column_name = "#{connection.quote_table_name(table_name)}.#{connection.quote_column_name(column_name)}"
+ end
end
+
+ column_name
end
if has_include?(column_names.first)
@@ -207,15 +207,18 @@ module ActiveRecord
end
if operation == "count"
- column_name ||= (select_for_count || :all)
+ if select_values.present?
+ column_name ||= select_values.join(", ")
+ else
+ column_name ||= :all
+ end
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?
@@ -376,13 +379,6 @@ module ActiveRecord
column ? column.type_cast(value) : value
end
- def select_for_count
- if select_values.present?
- select = select_values.join(", ")
- select if select !~ /[,*]/
- end
- end
-
def build_count_subquery(relation, column_name, distinct)
column_alias = Arel.sql('count_column')
subquery_alias = Arel.sql('subquery_for_count')
diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb
index 1b6239eb38..8d6740246c 100644
--- a/activerecord/lib/active_record/relation/delegation.rb
+++ b/activerecord/lib/active_record/relation/delegation.rb
@@ -14,14 +14,14 @@ module ActiveRecord
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
@@ -70,7 +70,7 @@ module ActiveRecord
end
end
- module ClassMethods
+ module ClassMethods # :nodoc:
@@subclasses = ThreadSafe::Cache.new(:initial_capacity => 2)
def new(klass, *args)
diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb
index e2685d0478..cbb2803593 100644
--- a/activerecord/lib/active_record/relation/finder_methods.rb
+++ b/activerecord/lib/active_record/relation/finder_methods.rb
@@ -130,8 +130,8 @@ module ActiveRecord
last or raise RecordNotFound
end
- # 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:
+ # 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:
#
# * Integer - Finds the record with this primary key.
# * String - Finds the record with a primary key corresponding to this
@@ -160,8 +160,9 @@ 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 = construct_relation_for_association_find(construct_join_dependency)
+ return false if ActiveRecord::NullRelation === relation
+
relation = relation.except(:select, :order).select("1 AS one").limit(1)
case conditions
@@ -171,66 +172,82 @@ 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
+ relation = relation.with_default_scope
+ connection.select_value(relation.arel, "#{name} Exists", relation.bind_values)
+ end
+
+ # This method is called whenever no records are found with either a single
+ # id or multiple ids and raises a +ActiveRecord::RecordNotFound+ exception.
+ #
+ # The error message is different depending on whether a single id or
+ # multiple ids are provided. If multiple ids are provided, then the number
+ # of results obtained should be provided in the +result_size+ argument and
+ # the expected number of results should be provided in the +expected_size+
+ # argument.
+ def raise_record_not_found_exception!(ids, result_size, expected_size) #:nodoc:
+ conditions = arel.where_sql
+ conditions = " [#{conditions}]" if conditions
+
+ if Array(ids).size == 1
+ error = "Couldn't find #{@klass.name} with #{primary_key}=#{ids}#{conditions}"
+ else
+ error = "Couldn't find all #{@klass.name.pluralize} with IDs "
+ error << "(#{ids.join(", ")})#{conditions} (found #{result_size} results, but was looking for #{expected_size})"
+ end
+
+ raise RecordNotFound, error
end
protected
def find_with_associations
- join_dependency = construct_join_dependency_for_association_find
+ join_dependency = construct_join_dependency
relation = construct_relation_for_association_find(join_dependency)
- rows = connection.select_all(relation, 'SQL', relation.bind_values.dup)
- join_dependency.instantiate(rows)
- rescue ThrowResult
- []
+ if ActiveRecord::NullRelation === relation
+ []
+ else
+ rows = connection.select_all(relation, 'SQL', relation.bind_values.dup)
+ join_dependency.instantiate(rows)
+ end
end
- def construct_join_dependency_for_association_find
+ def construct_join_dependency(joins = [])
including = (eager_load_values + includes_values).uniq
- ActiveRecord::Associations::JoinDependency.new(@klass, including, [])
+ ActiveRecord::Associations::JoinDependency.new(@klass, including, joins)
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)
+ apply_join_dependency(self, construct_join_dependency(arel.froms.first))
end
def construct_relation_for_association_find(join_dependency)
- relation = except(:includes, :eager_load, :preload, :select).select(join_dependency.columns)
+ relation = except(:select).select(join_dependency.columns)
apply_join_dependency(relation, join_dependency)
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 = join_dependency.join_relation(relation)
- 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]}
-
- ids_array.empty? ? raise(ThrowResult) : table[primary_key].in(ids_array)
+ id_rows.map {|row| row[primary_key]}
end
def find_with_ids(*ids)
@@ -259,11 +276,7 @@ module ActiveRecord
relation.bind_values += [[column, id]]
record = relation.take
- unless record
- conditions = arel.where_sql
- conditions = " [#{conditions}]" if conditions
- raise RecordNotFound, "Couldn't find #{@klass.name} with #{primary_key}=#{id}#{conditions}"
- end
+ raise_record_not_found_exception!(id, 0, 1) unless record
record
end
@@ -286,12 +299,7 @@ module ActiveRecord
if result.size == expected_size
result
else
- conditions = arel.where_sql
- conditions = " [#{conditions}]" if conditions
-
- error = "Couldn't find all #{@klass.name.pluralize} with IDs "
- error << "(#{ids.join(", ")})#{conditions} (found #{result.size} results, but was looking for #{expected_size})"
- raise RecordNotFound, error
+ raise_record_not_found_exception!(ids, result.size, expected_size)
end
end
diff --git a/activerecord/lib/active_record/relation/merger.rb b/activerecord/lib/active_record/relation/merger.rb
index eb23e92fb8..c114ea0c0d 100644
--- a/activerecord/lib/active_record/relation/merger.rb
+++ b/activerecord/lib/active_record/relation/merger.rb
@@ -39,7 +39,7 @@ 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
@@ -48,11 +48,12 @@ module ActiveRecord
@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
@@ -66,15 +67,47 @@ module ActiveRecord
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 = join_dependency.join_relation(relation)
+ 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)
+
+ relation.where_values = kept + rhs_wheres
+ relation.bind_values = filter_binds(lhs_binds, removed) + rhs_binds
if values[:reordering]
# override any order specified in the original relation
@@ -97,35 +130,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..b7609c97b5 100644
--- a/activerecord/lib/active_record/relation/predicate_builder.rb
+++ b/activerecord/lib/active_record/relation/predicate_builder.rb
@@ -6,6 +6,10 @@ module ActiveRecord
attributes.each do |column, value|
table = default_table
+ if column.is_a?(Symbol) && klass.attribute_aliases.key?(column.to_s)
+ column = klass.attribute_aliases[column.to_s]
+ end
+
if value.is_a?(Hash)
if value.empty?
queries << '1=0'
diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb
index 257221174b..ca1de2d4dc 100644
--- a/activerecord/lib/active_record/relation/query_methods.rb
+++ b/activerecord/lib/active_record/relation/query_methods.rb
@@ -34,7 +34,6 @@ 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
@@ -341,6 +340,9 @@ module ActiveRecord
# User.where(name: "John", active: true).unscope(where: :name)
# == User.where(active: true)
#
+ # This method is applied before the default_scope is applied. So the conditions
+ # specified in default_scope will not be removed.
+ #
# 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:
@@ -353,7 +355,7 @@ module ActiveRecord
spawn.unscope!(*args)
end
- def unscope!(*args)
+ def unscope!(*args) # :nodoc:
args.flatten!
args.each do |scope|
@@ -552,7 +554,6 @@ module ActiveRecord
# 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:
@@ -863,8 +864,6 @@ module ActiveRecord
return [] if joins.empty?
- @implicit_readonly = true
-
joins.map do |join|
case join
when Array
@@ -934,9 +933,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 { |x| x.strip }.uniq
join_list = join_nodes + custom_join_ast(manager, string_joins)
@@ -948,8 +945,6 @@ module ActiveRecord
join_dependency.graft(*stashed_association_joins)
- @implicit_readonly = true unless association_joins.empty? && stashed_association_joins.empty?
-
# FIXME: refactor this to build an AST
join_dependency.join_associations.each do |association|
association.join_to(manager)
@@ -962,7 +957,6 @@ module ActiveRecord
def build_select(arel, selects)
unless selects.empty?
- @implicit_readonly = false
arel.project(*selects)
else
arel.project(@klass.arel_table[Arel.star])
@@ -1016,7 +1010,7 @@ module ActiveRecord
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
diff --git a/activerecord/lib/active_record/runtime_registry.rb b/activerecord/lib/active_record/runtime_registry.rb
new file mode 100644
index 0000000000..63e6738622
--- /dev/null
+++ b/activerecord/lib/active_record/runtime_registry.rb
@@ -0,0 +1,17 @@
+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
+ end
+end
diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb
index 3c5b871e99..0ed97b66d6 100644
--- a/activerecord/lib/active_record/sanitization.rb
+++ b/activerecord/lib/active_record/sanitization.rb
@@ -89,7 +89,7 @@ module ActiveRecord
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
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..1181cc739e 100644
--- a/activerecord/lib/active_record/schema_dumper.rb
+++ b/activerecord/lib/active_record/schema_dumper.rb
@@ -106,9 +106,12 @@ 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"
end
else
tbl.print ", id: false"
diff --git a/activerecord/lib/active_record/schema_migration.rb b/activerecord/lib/active_record/schema_migration.rb
index 6077144265..fee19b1096 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}schema_migrations#{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_schema_migrations#{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 9746b1c3c2..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["#{self}_current_scope"]
+ ScopeRegistry.value_for(:current_scope, base_class.to_s)
end
def current_scope=(scope) #:nodoc:
- Thread.current["#{self}_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 cde4f29d08..d37d33d552 100644
--- a/activerecord/lib/active_record/scoping/default.rb
+++ b/activerecord/lib/active_record/scoping/default.rb
@@ -5,8 +5,17 @@ module ActiveRecord
included do
# Stores the default scope for the class.
- class_attribute :default_scopes, instance_writer: false
+ 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
@@ -111,11 +120,11 @@ module ActiveRecord
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 12317601b6..da73bead32 100644
--- a/activerecord/lib/active_record/scoping/named.rb
+++ b/activerecord/lib/active_record/scoping/named.rb
@@ -160,13 +160,8 @@ module ActiveRecord
singleton_class.send(:define_method, name) do |*args|
if body.respond_to?(:call)
- scope = extension ? body.call(*args).extending(extension) : body.call(*args)
-
- if scope
- default_scoped = scope.default_scoped
- scope = relation.merge(scope)
- scope.default_scoped = default_scoped
- end
+ scope = all.scoping { body.call(*args) }
+ scope = scope.extending(extension) if extension
else
scope = body
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/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb
index 4fa7cf8a7d..3e8b79c7a0 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.
+ #
+ # 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_accessor :database_configuration, :migrations_paths, :seed_loader, :db_dir,
+ :fixtures_path, :env
LOCAL_HOSTS = ['127.0.0.1', 'localhost']
@@ -15,12 +46,16 @@ module ActiveRecord
@tasks[pattern] = task
end
- register_task(/mysql/, ActiveRecord::Tasks::MySQLDatabaseTasks)
- register_task(/postgresql/, ActiveRecord::Tasks::PostgreSQLDatabaseTasks)
- register_task(/sqlite/, ActiveRecord::Tasks::SQLiteDatabaseTasks)
+ register_task(/mysql/, ActiveRecord::Tasks::MySQLDatabaseTasks)
+ 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 current_config(options = {})
- options.reverse_merge! :env => Rails.env
+ options.reverse_merge! :env => env
if options.has_key?(:config)
@current_config = options[:config]
else
@@ -46,7 +81,7 @@ 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
}
@@ -69,7 +104,7 @@ 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
}
@@ -79,7 +114,7 @@ module ActiveRecord
drop database_url_config
end
- def charset_current(environment = Rails.env)
+ def charset_current(environment = env)
charset ActiveRecord::Base.configurations[environment]
end
@@ -88,7 +123,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
@@ -113,6 +148,24 @@ module ActiveRecord
class_for_adapter(configuration['adapter']).new(*arguments).structure_load(filename)
end
+ def check_schema_file(filename)
+ unless File.exists?(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 database_url_config
@@ -130,7 +183,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
new file mode 100644
index 0000000000..98014a38ea
--- /dev/null
+++ b/activerecord/lib/active_record/tasks/firebird_database_tasks.rb
@@ -0,0 +1,56 @@
+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/oracle_database_tasks.rb b/activerecord/lib/active_record/tasks/oracle_database_tasks.rb
new file mode 100644
index 0000000000..de3aa50e5e
--- /dev/null
+++ b/activerecord/lib/active_record/tasks/oracle_database_tasks.rb
@@ -0,0 +1,45 @@
+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..4413330fab 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 #{filename} #{configuration['database']}")
end
private
diff --git a/activerecord/lib/active_record/tasks/sqlserver_database_tasks.rb b/activerecord/lib/active_record/tasks/sqlserver_database_tasks.rb
new file mode 100644
index 0000000000..c718ee03a8
--- /dev/null
+++ b/activerecord/lib/active_record/tasks/sqlserver_database_tasks.rb
@@ -0,0 +1,48 @@
+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
index e9142481a3..1b4c473bfc 100644
--- a/activerecord/lib/active_record/test_case.rb
+++ b/activerecord/lib/active_record/test_case.rb
@@ -35,8 +35,7 @@ module ActiveRecord
def assert_queries(num = 1, options = {})
ignore_none = options.fetch(:ignore_none) { num == :any }
SQLCounter.clear_log
- yield
- ensure
+ x = yield
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."
@@ -44,6 +43,7 @@ module ActiveRecord
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
+ x
end
def assert_no_queries(&block)
diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb
index 4dbd668fcf..77634b40bb 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
@@ -339,8 +341,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
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/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb
index a705d8c2c4..52e46e1ffe 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
diff --git a/activerecord/lib/active_record/version.rb b/activerecord/lib/active_record/version.rb
index f2b041ad97..de5fd05468 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.beta"
end
module VERSION #:nodoc:
diff --git a/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb b/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb
index b967bb6e0f..3968acba64 100644
--- a/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb
+++ b/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb
@@ -14,6 +14,10 @@ module ActiveRecord
protected
attr_reader :migration_action, :join_tables
+ # sets the default migration template that is being used for the generation of the migration
+ # depending on the arguments which would be sent out in the command line, the migration template
+ # and the table name instance variables are setup.
+
def set_local_assigns!
@migration_template = "migration.rb"
case file_name
diff --git a/activerecord/lib/rails/generators/active_record/model/model_generator.rb b/activerecord/lib/rails/generators/active_record/model/model_generator.rb
index 40e134e626..7e8d68ce69 100644
--- a/activerecord/lib/rails/generators/active_record/model/model_generator.rb
+++ b/activerecord/lib/rails/generators/active_record/model/model_generator.rb
@@ -12,6 +12,9 @@ module ActiveRecord
class_option :parent, :type => :string, :desc => "The parent class for the generated model"
class_option :indexes, :type => :boolean, :default => true, :desc => "Add indexes for references and belongs_to columns"
+
+ # creates the migration file for the model.
+
def create_migration_file
return unless options[:migration] && options[:parent].nil?
attributes.each { |a| a.attr_options.delete(:index) if a.reference? && !a.has_index? } if options[:indexes] == false
@@ -39,6 +42,7 @@ module ActiveRecord
protected
+ # Used by the migration template to determine the parent name of the model
def parent_class_name
options[:parent] || "ActiveRecord::Base"
end