aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record
diff options
context:
space:
mode:
Diffstat (limited to 'activerecord/lib/active_record')
-rw-r--r--activerecord/lib/active_record/aggregations.rb38
-rw-r--r--activerecord/lib/active_record/associations.rb358
-rw-r--r--activerecord/lib/active_record/associations/association.rb57
-rw-r--r--activerecord/lib/active_record/associations/association_scope.rb67
-rw-r--r--activerecord/lib/active_record/associations/belongs_to_association.rb12
-rw-r--r--activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb3
-rw-r--r--activerecord/lib/active_record/associations/builder/association.rb128
-rw-r--r--activerecord/lib/active_record/associations/builder/belongs_to.rb89
-rw-r--r--activerecord/lib/active_record/associations/builder/collection_association.rb90
-rw-r--r--activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb64
-rw-r--r--activerecord/lib/active_record/associations/builder/has_many.rb61
-rw-r--r--activerecord/lib/active_record/associations/builder/has_one.rb57
-rw-r--r--activerecord/lib/active_record/associations/builder/singular_association.rb26
-rw-r--r--activerecord/lib/active_record/associations/collection_association.rb158
-rw-r--r--activerecord/lib/active_record/associations/collection_proxy.rb977
-rw-r--r--activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb23
-rw-r--r--activerecord/lib/active_record/associations/has_many_association.rb34
-rw-r--r--activerecord/lib/active_record/associations/has_many_through_association.rb36
-rw-r--r--activerecord/lib/active_record/associations/has_one_association.rb66
-rw-r--r--activerecord/lib/active_record/associations/join_dependency/join_association.rb33
-rw-r--r--activerecord/lib/active_record/associations/join_dependency/join_part.rb2
-rw-r--r--activerecord/lib/active_record/associations/join_helper.rb2
-rw-r--r--activerecord/lib/active_record/associations/preloader.rb21
-rw-r--r--activerecord/lib/active_record/associations/preloader/association.rb60
-rw-r--r--activerecord/lib/active_record/associations/preloader/collection_association.rb2
-rw-r--r--activerecord/lib/active_record/associations/preloader/has_and_belongs_to_many.rb2
-rw-r--r--activerecord/lib/active_record/associations/preloader/has_many_through.rb2
-rw-r--r--activerecord/lib/active_record/associations/preloader/has_one.rb2
-rw-r--r--activerecord/lib/active_record/associations/preloader/through_association.rb27
-rw-r--r--activerecord/lib/active_record/associations/singular_association.rb20
-rw-r--r--activerecord/lib/active_record/associations/through_association.rb14
-rw-r--r--activerecord/lib/active_record/attribute_assignment.rb287
-rw-r--r--activerecord/lib/active_record/attribute_methods.rb114
-rw-r--r--activerecord/lib/active_record/attribute_methods/dirty.rb38
-rw-r--r--activerecord/lib/active_record/attribute_methods/primary_key.rb16
-rw-r--r--activerecord/lib/active_record/attribute_methods/query.rb4
-rw-r--r--activerecord/lib/active_record/attribute_methods/read.rb85
-rw-r--r--activerecord/lib/active_record/attribute_methods/serialization.rb122
-rw-r--r--activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb32
-rw-r--r--activerecord/lib/active_record/attribute_methods/write.rb4
-rw-r--r--activerecord/lib/active_record/autosave_association.rb62
-rw-r--r--activerecord/lib/active_record/base.rb30
-rw-r--r--activerecord/lib/active_record/callbacks.rb26
-rw-r--r--activerecord/lib/active_record/coders/yaml_column.rb16
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb378
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb204
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb9
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/quoting.rb3
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb82
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb56
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb104
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/transaction.rb156
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_adapter.rb84
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb78
-rw-r--r--activerecord/lib/active_record/connection_adapters/column.rb27
-rw-r--r--activerecord/lib/active_record/connection_adapters/connection_specification.rb4
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb14
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql_adapter.rb241
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb97
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/cast.rb124
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb240
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid.rb40
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb141
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb22
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb446
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb1137
-rw-r--r--activerecord/lib/active_record/connection_adapters/schema_cache.rb64
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb580
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb563
-rw-r--r--activerecord/lib/active_record/connection_handling.rb9
-rw-r--r--activerecord/lib/active_record/core.rb251
-rw-r--r--activerecord/lib/active_record/counter_cache.rb200
-rw-r--r--activerecord/lib/active_record/dynamic_finder_match.rb84
-rw-r--r--activerecord/lib/active_record/dynamic_matchers.rb168
-rw-r--r--activerecord/lib/active_record/dynamic_scope_match.rb27
-rw-r--r--activerecord/lib/active_record/errors.rb17
-rw-r--r--activerecord/lib/active_record/explain.rb22
-rw-r--r--activerecord/lib/active_record/explain_subscriber.rb10
-rw-r--r--activerecord/lib/active_record/fixtures.rb187
-rw-r--r--activerecord/lib/active_record/fixtures/file.rb44
-rw-r--r--activerecord/lib/active_record/identity_map.rb144
-rw-r--r--activerecord/lib/active_record/inheritance.rb79
-rw-r--r--activerecord/lib/active_record/integration.rb2
-rw-r--r--activerecord/lib/active_record/locking/optimistic.rb64
-rw-r--r--activerecord/lib/active_record/migration.rb37
-rw-r--r--activerecord/lib/active_record/migration/command_recorder.rb14
-rw-r--r--activerecord/lib/active_record/migration/join_table.rb6
-rw-r--r--activerecord/lib/active_record/model.rb175
-rw-r--r--activerecord/lib/active_record/model_schema.rb75
-rw-r--r--activerecord/lib/active_record/nested_attributes.rb112
-rw-r--r--activerecord/lib/active_record/null_relation.rb57
-rw-r--r--activerecord/lib/active_record/observer.rb7
-rw-r--r--activerecord/lib/active_record/persistence.rb163
-rw-r--r--activerecord/lib/active_record/query_cache.rb15
-rw-r--r--activerecord/lib/active_record/querying.rb21
-rw-r--r--activerecord/lib/active_record/railtie.rb68
-rw-r--r--activerecord/lib/active_record/railties/console_sandbox.rb2
-rw-r--r--activerecord/lib/active_record/railties/databases.rake391
-rw-r--r--activerecord/lib/active_record/railties/jdbcmysql_error.rb2
-rw-r--r--activerecord/lib/active_record/readonly_attributes.rb9
-rw-r--r--activerecord/lib/active_record/reflection.rb98
-rw-r--r--activerecord/lib/active_record/relation.rb323
-rw-r--r--activerecord/lib/active_record/relation/batches.rb60
-rw-r--r--activerecord/lib/active_record/relation/calculations.rb266
-rw-r--r--activerecord/lib/active_record/relation/delegation.rb13
-rw-r--r--activerecord/lib/active_record/relation/finder_methods.rb298
-rw-r--r--activerecord/lib/active_record/relation/merger.rb119
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder.rb77
-rw-r--r--activerecord/lib/active_record/relation/query_methods.rb603
-rw-r--r--activerecord/lib/active_record/relation/spawn_methods.rb156
-rw-r--r--activerecord/lib/active_record/result.rb17
-rw-r--r--activerecord/lib/active_record/sanitization.rb27
-rw-r--r--activerecord/lib/active_record/schema.rb26
-rw-r--r--activerecord/lib/active_record/schema_dumper.rb65
-rw-r--r--activerecord/lib/active_record/schema_migration.rb12
-rw-r--r--activerecord/lib/active_record/scoping.rb122
-rw-r--r--activerecord/lib/active_record/scoping/default.rb44
-rw-r--r--activerecord/lib/active_record/scoping/named.rb66
-rw-r--r--activerecord/lib/active_record/serialization.rb12
-rw-r--r--activerecord/lib/active_record/serializers/xml_serializer.rb9
-rw-r--r--activerecord/lib/active_record/session_store.rb358
-rw-r--r--activerecord/lib/active_record/store.rb113
-rw-r--r--activerecord/lib/active_record/tasks/database_tasks.rb150
-rw-r--r--activerecord/lib/active_record/tasks/mysql_database_tasks.rb128
-rw-r--r--activerecord/lib/active_record/tasks/postgresql_database_tasks.rb85
-rw-r--r--activerecord/lib/active_record/tasks/sqlite_database_tasks.rb55
-rw-r--r--activerecord/lib/active_record/test_case.rb61
-rw-r--r--activerecord/lib/active_record/timestamp.rb9
-rw-r--r--activerecord/lib/active_record/transactions.rb58
-rw-r--r--activerecord/lib/active_record/validations.rb9
-rw-r--r--activerecord/lib/active_record/validations/associated.rb32
-rw-r--r--activerecord/lib/active_record/validations/presence.rb64
-rw-r--r--activerecord/lib/active_record/validations/uniqueness.rb89
133 files changed, 8308 insertions, 6039 deletions
diff --git a/activerecord/lib/active_record/aggregations.rb b/activerecord/lib/active_record/aggregations.rb
index 5a8addc4e4..3db8e0716b 100644
--- a/activerecord/lib/active_record/aggregations.rb
+++ b/activerecord/lib/active_record/aggregations.rb
@@ -10,9 +10,9 @@ module ActiveRecord
# Active Record implements aggregation through a macro-like class method called +composed_of+
# for representing attributes as value objects. It expresses relationships like "Account [is]
# composed of Money [among other things]" or "Person [is] composed of [an] address". Each call
- # to the macro adds a description of how the value objects are created from the attributes of
- # the entity object (when the entity is initialized either as a new object or from finding an
- # existing object) and how it can be turned back into attributes (when the entity is saved to
+ # to the macro adds a description of how the value objects are created from the attributes of
+ # the entity object (when the entity is initialized either as a new object or from finding an
+ # existing object) and how it can be turned back into attributes (when the entity is saved to
# the database).
#
# class Customer < ActiveRecord::Base
@@ -46,7 +46,7 @@ module ActiveRecord
#
# def <=>(other_money)
# if currency == other_money.currency
- # amount <=> amount
+ # amount <=> other_money.amount
# else
# amount <=> other_money.exchange_to(currency).amount
# end
@@ -71,7 +71,7 @@ module ActiveRecord
# Now it's possible to access attributes from the database through the value objects instead. If
# you choose to name the composition the same as the attribute's name, it will be the only way to
# access that attribute. That's the case with our +balance+ attribute. You interact with the value
- # objects just like you would any other attribute, though:
+ # objects just like you would with any other attribute:
#
# customer.balance = Money.new(20) # sets the Money value object and the attribute
# customer.balance # => Money value object
@@ -86,6 +86,12 @@ module ActiveRecord
# customer.address_street = "Hyancintvej"
# customer.address_city = "Copenhagen"
# customer.address # => Address.new("Hyancintvej", "Copenhagen")
+ #
+ # customer.address_street = "Vesterbrogade"
+ # customer.address # => Address.new("Hyancintvej", "Copenhagen")
+ # customer.clear_aggregation_cache
+ # customer.address # => Address.new("Vesterbrogade", "Copenhagen")
+ #
# customer.address = Address.new("May Street", "Chicago")
# customer.address_street # => "May Street"
# customer.address_city # => "Chicago"
@@ -101,8 +107,8 @@ module ActiveRecord
# ActiveRecord::Base classes are entity objects.
#
# It's also important to treat the value objects as immutable. Don't allow the Money object to have
- # its amount changed after creation. Create a new Money object with the new value instead. This
- # is exemplified by the Money#exchange_to method that returns a new value object instead of changing
+ # its amount changed after creation. Create a new Money object with the new value instead. The
+ # Money#exchange_to method is an example of this. It returns a new value object instead of changing
# its own values. Active Record won't persist value objects that have been changed through means
# other than the writer method.
#
@@ -119,7 +125,7 @@ module ActiveRecord
# option, as arguments. If the value class doesn't support this convention then +composed_of+ allows
# a custom constructor to be specified.
#
- # When a new value is assigned to the value object the default assumption is that the new value
+ # When a new value is assigned to the value object, the default assumption is that the new value
# is an instance of the value class. Specifying a custom converter allows the new value to be automatically
# converted to an instance of value class if necessary.
#
@@ -187,7 +193,8 @@ module ActiveRecord
# * <tt>:converter</tt> - A symbol specifying the name of a class method of <tt>:class_name</tt>
# or a Proc that is called when a new value is assigned to the value object. The converter is
# passed the single value that is used in the assignment and is only called if the new value is
- # not an instance of <tt>:class_name</tt>.
+ # not an instance of <tt>:class_name</tt>. If <tt>:allow_nil</tt> is set to true, the converter
+ # can return nil to skip the assignment.
#
# Option examples:
# composed_of :temperature, :mapping => %w(reading celsius)
@@ -216,7 +223,7 @@ module ActiveRecord
reader_method(name, class_name, mapping, allow_nil, constructor)
writer_method(name, class_name, mapping, allow_nil, converter)
- create_reflection(:composed_of, part_id, options, self)
+ create_reflection(:composed_of, part_id, nil, options, self)
end
private
@@ -235,16 +242,15 @@ module ActiveRecord
def writer_method(name, class_name, mapping, allow_nil, converter)
define_method("#{name}=") do |part|
+ klass = class_name.constantize
+ unless part.is_a?(klass) || converter.nil? || part.nil?
+ part = converter.respond_to?(:call) ? converter.call(part) : klass.send(converter, part)
+ end
+
if part.nil? && allow_nil
mapping.each { |pair| self[pair.first] = nil }
@aggregation_cache[name] = nil
else
- unless part.is_a?(class_name.constantize) || converter.nil?
- part = converter.respond_to?(:call) ?
- converter.call(part) :
- class_name.constantize.send(converter, part)
- end
-
mapping.each { |pair| self[pair.first] = part.send(pair.last) }
@aggregation_cache[name] = part.freeze
end
diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb
index 958821add6..60b7118d7e 100644
--- a/activerecord/lib/active_record/associations.rb
+++ b/activerecord/lib/active_record/associations.rb
@@ -1,9 +1,6 @@
require 'active_support/core_ext/enumerable'
-require 'active_support/core_ext/module/delegation'
-require 'active_support/core_ext/object/blank'
require 'active_support/core_ext/string/conversions'
require 'active_support/core_ext/module/remove_method'
-require 'active_support/core_ext/class/attribute'
module ActiveRecord
class InverseOfAssociationNotFoundError < ActiveRecordError #:nodoc:
@@ -20,7 +17,7 @@ module ActiveRecord
class HasManyThroughAssociationPolymorphicSourceError < ActiveRecordError #:nodoc:
def initialize(owner_class_name, reflection, source_reflection)
- super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' on the polymorphic object '#{source_reflection.class_name}##{source_reflection.name}'.")
+ super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' on the polymorphic object '#{source_reflection.class_name}##{source_reflection.name}' without 'source_type'. Try adding 'source_type: \"#{reflection.name.to_s.classify}\"' to 'has_many :through' definition.")
end
end
@@ -104,6 +101,7 @@ module ActiveRecord
# See ActiveRecord::Associations::ClassMethods for documentation.
module Associations # :nodoc:
+ extend ActiveSupport::Autoload
extend ActiveSupport::Concern
# These classes will be loaded when associations are created.
@@ -133,11 +131,13 @@ module ActiveRecord
autoload :HasAndBelongsToMany, 'active_record/associations/builder/has_and_belongs_to_many'
end
- autoload :Preloader, 'active_record/associations/preloader'
- autoload :JoinDependency, 'active_record/associations/join_dependency'
- autoload :AssociationScope, 'active_record/associations/association_scope'
- autoload :AliasTracker, 'active_record/associations/alias_tracker'
- autoload :JoinHelper, 'active_record/associations/join_helper'
+ eager_autoload do
+ autoload :Preloader, 'active_record/associations/preloader'
+ autoload :JoinDependency, 'active_record/associations/join_dependency'
+ autoload :AssociationScope, 'active_record/associations/association_scope'
+ autoload :AliasTracker, 'active_record/associations/alias_tracker'
+ autoload :JoinHelper, 'active_record/associations/join_helper'
+ end
# Clears out the association cache.
def clear_association_cache #:nodoc:
@@ -195,26 +195,6 @@ module ActiveRecord
# * <tt>Project#categories.empty?, Project#categories.size, Project#categories, Project#categories<<(category1),</tt>
# <tt>Project#categories.delete(category1)</tt>
#
- # === Overriding generated methods
- #
- # Association methods are generated in a module that is included into the model class,
- # which allows you to easily override with your own methods and call the original
- # generated method with +super+. For example:
- #
- # class Car < ActiveRecord::Base
- # belongs_to :owner
- # belongs_to :old_owner
- # def owner=(new_owner)
- # self.old_owner = self.owner
- # super
- # end
- # end
- #
- # If your model class is <tt>Project</tt>, the module is
- # named <tt>Project::GeneratedFeatureMethods</tt>. The GeneratedFeatureMethods module is
- # included in the model class immediately after the (anonymous) generated attributes methods
- # module, meaning an association will override the methods for an attribute with the same name.
- #
# === A word of warning
#
# Don't create associations that have the same name as instance methods of
@@ -262,6 +242,26 @@ module ActiveRecord
# others.uniq | X | X | X
# others.reset | X | X | X
#
+ # === Overriding generated methods
+ #
+ # Association methods are generated in a module that is included into the model class,
+ # which allows you to easily override with your own methods and call the original
+ # generated method with +super+. For example:
+ #
+ # class Car < ActiveRecord::Base
+ # belongs_to :owner
+ # belongs_to :old_owner
+ # def owner=(new_owner)
+ # self.old_owner = self.owner
+ # super
+ # end
+ # end
+ #
+ # If your model class is <tt>Project</tt>, the module is
+ # named <tt>Project::GeneratedFeatureMethods</tt>. The GeneratedFeatureMethods module is
+ # included in the model class immediately after the (anonymous) generated attributes methods
+ # module, meaning an association will override the methods for an attribute with the same name.
+ #
# == Cardinality and associations
#
# Active Record associations can be used to describe one-to-one, one-to-many and many-to-many
@@ -397,7 +397,28 @@ module ActiveRecord
# * All unsaved (<tt>new_record? == true</tt>) members of the collection are automatically
# saved when the parent is saved.
#
- # === Association callbacks
+ # == Customizing the query
+ #
+ # 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
+ # has_many :published_posts, -> { where published: true }, class_name: 'Post'
+ # end
+ #
+ # Inside the <tt>-> { ... }</tt> block you can use all of the usual <tt>Relation</tt> methods.
+ #
+ # === Accessing the owner object
+ #
+ # Sometimes it is useful to have access to the owner object when building the query. The owner
+ # is passed as a parameter to the block. For example, the following association would find all
+ # events that occur on the user's birthday:
+ #
+ # class User < ActiveRecord::Base
+ # has_many :birthday_events, ->(user) { where starts_on: user.birthday }, class_name: 'Event'
+ # end
+ #
+ # == Association callbacks
#
# Similar to the normal callbacks that hook into the life cycle of an Active Record object,
# you can also define callbacks that get triggered when you add an object to or remove an
@@ -424,7 +445,7 @@ module ActiveRecord
# added to the collection. Same with the +before_remove+ callbacks; if an exception is
# thrown the object doesn't get removed.
#
- # === Association extensions
+ # == Association extensions
#
# The proxy objects that control the access to associations can be extended through anonymous
# modules. This is especially beneficial for adding new finders, creators, and other
@@ -454,20 +475,11 @@ module ActiveRecord
# end
#
# class Account < ActiveRecord::Base
- # has_many :people, :extend => FindOrCreateByNameExtension
+ # has_many :people, -> { extending FindOrCreateByNameExtension }
# end
#
# class Company < ActiveRecord::Base
- # has_many :people, :extend => FindOrCreateByNameExtension
- # end
- #
- # If you need to use multiple named extension modules, you can specify an array of modules
- # with the <tt>:extend</tt> option.
- # In the case of name conflicts between methods in the modules, methods in modules later
- # in the array supercede those earlier in the array.
- #
- # class Account < ActiveRecord::Base
- # has_many :people, :extend => [FindOrCreateByNameExtension, FindRecentExtension]
+ # has_many :people, -> { extending FindOrCreateByNameExtension }
# end
#
# Some extensions can only be made to work with knowledge of the association's internals.
@@ -485,7 +497,7 @@ module ActiveRecord
# the same object, allowing you to make calls like <tt>proxy_association.owner</tt> inside
# association extensions.
#
- # === Association Join Models
+ # == Association Join Models
#
# Has Many associations can be configured with the <tt>:through</tt> option to use an
# explicit join model to retrieve the data. This operates similarly to a
@@ -543,7 +555,7 @@ module ActiveRecord
# end
#
# @group = Group.first
- # @group.users.collect { |u| u.avatar }.flatten # select all avatars for all users in the group
+ # @group.users.collect { |u| u.avatar }.compact # select all avatars for all users in the group
# @group.avatars # selects all avatars by going through the User join model.
#
# An important caveat with going through +has_one+ or +has_many+ associations on the
@@ -569,7 +581,7 @@ module ActiveRecord
# belongs_to :tag, :inverse_of => :taggings
# end
#
- # === Nested Associations
+ # == 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:
@@ -612,7 +624,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
@@ -742,7 +754,7 @@ module ActiveRecord
# to include an association which has conditions defined on it:
#
# class Post < ActiveRecord::Base
- # has_many :approved_comments, :class_name => 'Comment', :conditions => ['approved = ?', true]
+ # has_many :approved_comments, -> { where approved: true }, :class_name => 'Comment'
# end
#
# Post.includes(:approved_comments)
@@ -754,14 +766,11 @@ module ActiveRecord
# returning all the associated objects:
#
# class Picture < ActiveRecord::Base
- # has_many :most_recent_comments, :class_name => 'Comment', :order => 'id DESC', :limit => 10
+ # has_many :most_recent_comments, -> { order('id DESC').limit(10) }, :class_name => 'Comment'
# end
#
# Picture.includes(:most_recent_comments).first.most_recent_comments # => returns all associated comments.
#
- # When eager loaded, conditions are interpolated in the context of the model class, not
- # the model instance. Conditions are lazily interpolated before the actual model exists.
- #
# Eager loading is supported with polymorphic associations.
#
# class Address < ActiveRecord::Base
@@ -839,8 +848,8 @@ module ActiveRecord
# module MyApplication
# module Business
# class Firm < ActiveRecord::Base
- # has_many :clients
- # end
+ # has_many :clients
+ # end
#
# class Client < ActiveRecord::Base; end
# end
@@ -938,7 +947,8 @@ module ActiveRecord
#
# The <tt>:dependent</tt> option can have different values which specify how the deletion
# is done. For more information, see the documentation for this option on the different
- # specific association types.
+ # specific association types. When no option is given, the behaviour is to do nothing
+ # with the associated records when destroying a record.
#
# === Delete or destroy?
#
@@ -1077,15 +1087,6 @@ module ActiveRecord
# from the association name. So <tt>has_many :products</tt> will by default be linked
# to the Product class, but if the real class name is SpecialProduct, you'll have to
# specify it with this option.
- # [:conditions]
- # Specify the conditions that the associated objects must meet in order to be included as a +WHERE+
- # SQL fragment, such as <tt>price > 5 AND name LIKE 'B%'</tt>. Record creations from
- # the association are scoped if a hash is used.
- # <tt>has_many :posts, :conditions => {:published => true}</tt> will create published
- # posts with <tt>@blog.posts.create</tt> or <tt>@blog.posts.build</tt>.
- # [:order]
- # Specify the order in which the associated objects are returned as an <tt>ORDER BY</tt> SQL fragment,
- # such as <tt>last_name, first_name DESC</tt>.
# [:foreign_key]
# Specify the foreign key used for the association. By default this is guessed to be the name
# of this class in lower-case and "_id" suffixed. So a Person class that makes a +has_many+
@@ -1093,44 +1094,18 @@ module ActiveRecord
# [:primary_key]
# Specify the method that returns the primary key used for the association. By default this is +id+.
# [:dependent]
- # If set to <tt>:destroy</tt> all the associated objects are destroyed
- # alongside this object by calling their +destroy+ method. If set to <tt>:delete_all</tt> all associated
- # objects are deleted *without* calling their +destroy+ method. If set to <tt>:nullify</tt> all associated
- # objects' foreign keys are set to +NULL+ *without* calling their +save+ callbacks. If set to
- # <tt>:restrict</tt> an error will be added to the object, preventing its deletion, if any associated
- # objects are present.
+ # Controls what happens to the associated objects when
+ # their owner is destroyed:
+ #
+ # * <tt>:destroy</tt> causes all the associated objects to also be destroyed
+ # * <tt>:delete_all</tt> causes all the asssociated objects to be deleted directly from the database (so callbacks will not execute)
+ # * <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
#
# 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
# the associated records.
- #
- # [:finder_sql]
- # Specify a complete SQL statement to fetch the association. This is a good way to go for complex
- # associations that depend on multiple tables. May be supplied as a string or a proc where interpolation is
- # required. Note: When this option is used, +find_in_collection+
- # is _not_ added.
- # [:counter_sql]
- # Specify a complete SQL statement to fetch the size of the association. If <tt>:finder_sql</tt> is
- # specified but not <tt>:counter_sql</tt>, <tt>:counter_sql</tt> will be generated by
- # replacing <tt>SELECT ... FROM</tt> with <tt>SELECT COUNT(*) FROM</tt>.
- # [:extend]
- # Specify a named module for extending the proxy. See "Association extensions".
- # [:include]
- # Specify second-order associations that should be eager loaded when the collection is loaded.
- # [:group]
- # An attribute name by which the result should be grouped. Uses the <tt>GROUP BY</tt> SQL-clause.
- # [:having]
- # Combined with +:group+ this can be used to filter the records that a <tt>GROUP BY</tt>
- # returns. Uses the <tt>HAVING</tt> SQL-clause.
- # [:limit]
- # An integer determining the limit on the number of rows that should be returned.
- # [:offset]
- # An integer determining the offset from where the rows should be fetched. So at 5,
- # it would skip the first 4 rows.
- # [:select]
- # By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed if
- # you, for example, want to do a join but not include the joined columns. Do not forget
- # to include the primary and foreign keys, otherwise it will raise an error.
# [:as]
# Specifies a polymorphic interface (See <tt>belongs_to</tt>).
# [:through]
@@ -1157,16 +1132,16 @@ module ActiveRecord
# [:source_type]
# Specifies type of the source association used by <tt>has_many :through</tt> queries where the source
# association is a polymorphic +belongs_to+.
- # [:uniq]
- # If true, duplicates will be omitted from the collection. Useful in conjunction with <tt>:through</tt>.
- # [:readonly]
- # If true, all the associated objects are readonly through the association.
# [:validate]
# If +false+, don't validate the associated objects when saving the parent object. true by default.
# [:autosave]
# 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.
+ # 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.
+ #
+ # Note that <tt>accepts_nested_attributes_for</tt> sets <tt>:autosave</tt> to <tt>true</tt>.
# [:inverse_of]
# Specifies the name of the <tt>belongs_to</tt> association on the associated object
# that is the inverse of this <tt>has_many</tt> association. Does not work in combination
@@ -1174,24 +1149,16 @@ module ActiveRecord
# See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail.
#
# Option examples:
- # has_many :comments, :order => "posted_on"
- # has_many :comments, :include => :author
- # has_many :people, :class_name => "Person", :conditions => "deleted = 0", :order => "name"
- # has_many :tracks, :order => "position", :dependent => :destroy
- # has_many :comments, :dependent => :nullify
- # has_many :tags, :as => :taggable
- # has_many :reports, :readonly => true
- # has_many :subscribers, :through => :subscriptions, :source => :user
- # has_many :subscribers, :class_name => "Person", :finder_sql => Proc.new {
- # %Q{
- # SELECT DISTINCT *
- # FROM people p, post_subscriptions ps
- # WHERE ps.post_id = #{id} AND ps.person_id = p.id
- # ORDER BY p.first_name
- # }
- # }
- def has_many(name, options = {}, &extension)
- Builder::HasMany.build(self, name, options, &extension)
+ # has_many :comments, -> { order "posted_on" }
+ # has_many :comments, -> { includes :author }
+ # has_many :people, -> { where("deleted = 0").order("name") }, class_name: "Person"
+ # has_many :tracks, -> { order "position" }, dependent: :destroy
+ # has_many :comments, dependent: :nullify
+ # has_many :tags, as: :taggable
+ # has_many :reports, -> { readonly }
+ # has_many :subscribers, through: :subscriptions, source: :user
+ def has_many(name, scope = nil, options = {}, &extension)
+ Builder::HasMany.build(self, name, scope, options, &extension)
end
# Specifies a one-to-one association with another class. This method should only be used
@@ -1239,34 +1206,23 @@ module ActiveRecord
# Specify the class name of the association. Use it only if that name can't be inferred
# from the association name. So <tt>has_one :manager</tt> will by default be linked to the Manager class, but
# if the real class name is Person, you'll have to specify it with this option.
- # [:conditions]
- # Specify the conditions that the associated object must meet in order to be included as a +WHERE+
- # SQL fragment, such as <tt>rank = 5</tt>. Record creation from the association is scoped if a hash
- # is used. <tt>has_one :account, :conditions => {:enabled => true}</tt> will create
- # an enabled account with <tt>@company.create_account</tt> or <tt>@company.build_account</tt>.
- # [:order]
- # Specify the order in which the associated objects are returned as an <tt>ORDER BY</tt> SQL fragment,
- # such as <tt>last_name, first_name DESC</tt>.
# [:dependent]
- # If set to <tt>:destroy</tt>, the associated object is destroyed when this object is. If set to
- # <tt>:delete</tt>, the associated object is deleted *without* calling its destroy method.
- # If set to <tt>:nullify</tt>, the associated object's foreign key is set to +NULL+.
- # If set to <tt>:restrict</tt>, an error will be added to the object, preventing its deletion, if an
- # associated object is present.
+ # Controls what happens to the associated object when
+ # its owner is destroyed:
+ #
+ # * <tt>:destroy</tt> causes the associated object to also be destroyed
+ # * <tt>:delete</tt> causes the asssociated object to be deleted directly from the database (so callbacks will not execute)
+ # * <tt>:nullify</tt> causes the foreign key to be set to +NULL+. Callbacks are not executed.
+ # * <tt>:restrict_with_exception</tt> causes an exception to be raised if there is an associated record
+ # * <tt>:restrict_with_error</tt> causes an error to be added to the owner if there is an associated object
# [:foreign_key]
# Specify the foreign key used for the association. By default this is guessed to be the name
# of this class in lower-case and "_id" suffixed. So a Person class that makes a +has_one+ association
# will use "person_id" as the default <tt>:foreign_key</tt>.
# [:primary_key]
# Specify the method that returns the primary key used for the association. By default this is +id+.
- # [:include]
- # Specify second-order associations that should be eager loaded when this object is loaded.
# [:as]
# Specifies a polymorphic interface (See <tt>belongs_to</tt>).
- # [:select]
- # By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed if, for example,
- # you want to do a join but not include the joined columns. Do not forget to include the
- # primary and foreign keys, otherwise it will raise an error.
# [:through]
# Specifies a Join Model through which to perform the query. Options for <tt>:class_name</tt>,
# <tt>:primary_key</tt>, and <tt>:foreign_key</tt> are ignored, as the association uses the
@@ -1280,14 +1236,14 @@ module ActiveRecord
# [:source_type]
# Specifies type of the source association used by <tt>has_one :through</tt> queries where the source
# association is a polymorphic +belongs_to+.
- # [:readonly]
- # If true, the associated object is readonly through the association.
# [:validate]
# If +false+, don't validate the associated object when saving the parent object. +false+ by default.
# [:autosave]
# If true, always save the associated object or destroy it if marked for destruction,
# when saving the parent object. If false, never save or destroy the associated object.
# By default, only save the associated object if it's a new record.
+ #
+ # Note that <tt>accepts_nested_attributes_for</tt> sets <tt>:autosave</tt> to <tt>true</tt>.
# [:inverse_of]
# Specifies the name of the <tt>belongs_to</tt> association on the associated object
# that is the inverse of this <tt>has_one</tt> association. Does not work in combination
@@ -1298,14 +1254,14 @@ module ActiveRecord
# has_one :credit_card, :dependent => :destroy # destroys the associated credit card
# has_one :credit_card, :dependent => :nullify # updates the associated records foreign
# # key value to NULL rather than destroying it
- # has_one :last_comment, :class_name => "Comment", :order => "posted_on"
- # has_one :project_manager, :class_name => "Person", :conditions => "role = 'project_manager'"
- # has_one :attachment, :as => :attachable
- # has_one :boss, :readonly => :true
- # has_one :club, :through => :membership
- # has_one :primary_address, :through => :addressables, :conditions => ["addressable.primary = ?", true], :source => :addressable
- def has_one(name, options = {})
- Builder::HasOne.build(self, name, options)
+ # has_one :last_comment, -> { order 'posted_on' }, :class_name => "Comment"
+ # has_one :project_manager, -> { where role: 'project_manager' }, :class_name => "Person"
+ # has_one :attachment, as: :attachable
+ # has_one :boss, readonly: :true
+ # has_one :club, through: :membership
+ # has_one :primary_address, -> { where primary: true }, through: :addressables, source: :addressable
+ def has_one(name, scope = nil, options = {})
+ Builder::HasOne.build(self, name, scope, options)
end
# Specifies a one-to-one association with another class. This method should only be used
@@ -1350,13 +1306,6 @@ module ActiveRecord
# Specify the class name of the association. Use it only if that name can't be inferred
# from the association name. So <tt>belongs_to :author</tt> will by default be linked to the Author class, but
# if the real class name is Person, you'll have to specify it with this option.
- # [:conditions]
- # Specify the conditions that the associated object must meet in order to be included as a +WHERE+
- # SQL fragment, such as <tt>authorized = 1</tt>.
- # [:select]
- # By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed
- # if, for example, you want to do a join but not include the joined columns. Do not
- # forget to include the primary and foreign keys, otherwise it will raise an error.
# [:foreign_key]
# Specify the foreign key used for the association. By default this is guessed to be the name
# of the association with an "_id" suffix. So a class that defines a <tt>belongs_to :person</tt>
@@ -1382,19 +1331,17 @@ module ActiveRecord
# and +decrement_counter+. The counter cache is incremented when an object of this
# 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). You can also specify a custom counter
+ # 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
+ # 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>.)
# Note: Specifying a counter cache will add it to that model's list of readonly attributes
# using +attr_readonly+.
- # [:include]
- # Specify second-order associations that should be eager loaded when this object is loaded.
# [:polymorphic]
# Specify this association is a polymorphic association by passing +true+.
# Note: If you've enabled the counter cache, then you may want to add the counter cache attribute
# to the +attr_readonly+ list in the associated classes (e.g. <tt>class Post; attr_readonly :comments_count; end</tt>).
- # [:readonly]
- # If true, the associated object is readonly through the association.
# [:validate]
# If +false+, don't validate the associated objects when saving the parent object. +false+ by default.
# [:autosave]
@@ -1402,6 +1349,8 @@ module ActiveRecord
# saving the parent object.
# If false, never save or destroy the associated object.
# By default, only save the associated object if it's a new record.
+ #
+ # Note that <tt>accepts_nested_attributes_for</tt> sets <tt>:autosave</tt> to <tt>true</tt>.
# [:touch]
# If true, the associated object will be touched (the updated_at/on attributes set to now)
# when this record is either saved or destroyed. If you specify a symbol, that attribute
@@ -1413,24 +1362,24 @@ module ActiveRecord
# See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail.
#
# Option examples:
- # belongs_to :firm, :foreign_key => "client_of"
- # belongs_to :person, :primary_key => "name", :foreign_key => "person_name"
- # belongs_to :author, :class_name => "Person", :foreign_key => "author_id"
- # belongs_to :valid_coupon, :class_name => "Coupon", :foreign_key => "coupon_id",
- # :conditions => 'discounts > #{payments_count}'
- # belongs_to :attachable, :polymorphic => true
- # belongs_to :project, :readonly => true
- # belongs_to :post, :counter_cache => true
- # belongs_to :company, :touch => true
- # belongs_to :company, :touch => :employees_last_updated_at
- def belongs_to(name, options = {})
- Builder::BelongsTo.build(self, name, options)
+ # belongs_to :firm, foreign_key: "client_of"
+ # belongs_to :person, primary_key: "name", foreign_key: "person_name"
+ # belongs_to :author, class_name: "Person", foreign_key: "author_id"
+ # belongs_to :valid_coupon, ->(o) { where "discounts > #{o.payments_count}" },
+ # class_name: "Coupon", foreign_key: "coupon_id"
+ # belongs_to :attachable, polymorphic: true
+ # belongs_to :project, readonly: true
+ # belongs_to :post, counter_cache: true
+ # belongs_to :company, touch: true
+ # belongs_to :company, touch: :employees_last_updated_at
+ def belongs_to(name, scope = nil, options = {})
+ Builder::BelongsTo.build(self, name, scope, options)
end
# Specifies a many-to-many relationship with another class. This associates two classes via an
# intermediate join table. Unless the join table is explicitly specified as an option, it is
# guessed using the lexical order of the class names. So a join between Developer and Project
- # will give the default join table name of "developers_projects" because "D" outranks "P".
+ # will give the default join table name of "developers_projects" because "D" precedes "P" alphabetically.
# Note that this precedence is calculated using the <tt><</tt> operator for String. This
# means that if the strings are of different lengths, and the strings are equal when compared
# up to the shortest length, then the longer string is considered of higher
@@ -1512,8 +1461,8 @@ module ActiveRecord
# * <tt>Developer#projects.size</tt>
# * <tt>Developer#projects.find(id)</tt>
# * <tt>Developer#projects.exists?(...)</tt>
- # * <tt>Developer#projects.build</tt> (similar to <tt>Project.new("project_id" => id)</tt>)
- # * <tt>Developer#projects.create</tt> (similar to <tt>c = Project.new("project_id" => id); c.save; c</tt>)
+ # * <tt>Developer#projects.build</tt> (similar to <tt>Project.new("developer_id" => id)</tt>)
+ # * <tt>Developer#projects.create</tt> (similar to <tt>c = Project.new("developer_id" => id); c.save; c</tt>)
# The declaration may include an options hash to specialize the behavior of the association.
#
# === Options
@@ -1536,47 +1485,6 @@ module ActiveRecord
# By default this is guessed to be the name of the associated class in lower-case and "_id" suffixed.
# So if a Person class makes a +has_and_belongs_to_many+ association to Project,
# the association will use "project_id" as the default <tt>:association_foreign_key</tt>.
- # [:conditions]
- # Specify the conditions that the associated object must meet in order to be included as a +WHERE+
- # SQL fragment, such as <tt>authorized = 1</tt>. Record creations from the association are
- # scoped if a hash is used.
- # <tt>has_many :posts, :conditions => {:published => true}</tt> will create published posts with <tt>@blog.posts.create</tt>
- # or <tt>@blog.posts.build</tt>.
- # [:order]
- # Specify the order in which the associated objects are returned as an <tt>ORDER BY</tt> SQL fragment,
- # such as <tt>last_name, first_name DESC</tt>
- # [:uniq]
- # If true, duplicate associated objects will be ignored by accessors and query methods.
- # [:finder_sql]
- # Overwrite the default generated SQL statement used to fetch the association with a manual statement
- # [:counter_sql]
- # Specify a complete SQL statement to fetch the size of the association. If <tt>:finder_sql</tt> is
- # specified but not <tt>:counter_sql</tt>, <tt>:counter_sql</tt> will be generated by
- # replacing <tt>SELECT ... FROM</tt> with <tt>SELECT COUNT(*) FROM</tt>.
- # [:delete_sql]
- # Overwrite the default generated SQL statement used to remove links between the associated
- # classes with a manual statement.
- # [:insert_sql]
- # Overwrite the default generated SQL statement used to add links between the associated classes
- # with a manual statement.
- # [:extend]
- # Anonymous module for extending the proxy, see "Association extensions".
- # [:include]
- # Specify second-order associations that should be eager loaded when the collection is loaded.
- # [:group]
- # An attribute name by which the result should be grouped. Uses the <tt>GROUP BY</tt> SQL-clause.
- # [:having]
- # Combined with +:group+ this can be used to filter the records that a <tt>GROUP BY</tt> returns.
- # Uses the <tt>HAVING</tt> SQL-clause.
- # [:limit]
- # An integer determining the limit on the number of rows that should be returned.
- # [:offset]
- # An integer determining the offset from where the rows should be fetched. So at 5,
- # it would skip the first 4 rows.
- # [:select]
- # By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed if, for example,
- # you want to do a join but not include the joined columns. Do not forget to include the primary
- # and foreign keys, otherwise it will raise an error.
# [:readonly]
# If true, all the associated objects are readonly through the association.
# [:validate]
@@ -1587,16 +1495,16 @@ module ActiveRecord
# If false, never save or destroy the associated objects.
# By default, only save associated objects that are new records.
#
+ # Note that <tt>accepts_nested_attributes_for</tt> sets <tt>:autosave</tt> to <tt>true</tt>.
+ #
# Option examples:
# has_and_belongs_to_many :projects
- # has_and_belongs_to_many :projects, :include => [ :milestones, :manager ]
- # has_and_belongs_to_many :nations, :class_name => "Country"
- # has_and_belongs_to_many :categories, :join_table => "prods_cats"
- # has_and_belongs_to_many :categories, :readonly => true
- # has_and_belongs_to_many :active_projects, :join_table => 'developers_projects', :delete_sql =>
- # "DELETE FROM developers_projects WHERE active=1 AND developer_id = #{id} AND project_id = #{record.id}"
- def has_and_belongs_to_many(name, options = {}, &extension)
- Builder::HasAndBelongsToMany.build(self, name, options, &extension)
+ # has_and_belongs_to_many :projects, -> { includes :milestones, :manager }
+ # has_and_belongs_to_many :nations, class_name: "Country"
+ # has_and_belongs_to_many :categories, join_table: "prods_cats"
+ # has_and_belongs_to_many :categories, -> { readonly }
+ def has_and_belongs_to_many(name, scope = nil, options = {}, &extension)
+ Builder::HasAndBelongsToMany.build(self, name, scope, options, &extension)
end
end
end
diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb
index 7887d59aad..495f0cde59 100644
--- a/activerecord/lib/active_record/associations/association.rb
+++ b/activerecord/lib/active_record/associations/association.rb
@@ -1,5 +1,4 @@
require 'active_support/core_ext/array/wrap'
-require 'active_support/core_ext/object/inclusion'
module ActiveRecord
module Associations
@@ -25,9 +24,7 @@ module ActiveRecord
def initialize(owner, reflection)
reflection.check_validity!
- @target = nil
@owner, @reflection = owner, reflection
- @updated = false
reset
reset_scope
@@ -38,14 +35,14 @@ module ActiveRecord
# post.comments.aliased_table_name # => "comments"
#
def aliased_table_name
- reflection.klass.table_name
+ klass.table_name
end
# Resets the \loaded flag to +false+ and sets the \target to +nil+.
def reset
@loaded = false
- IdentityMap.remove(target) if IdentityMap.enabled? && target
@target = nil
+ @stale_state = nil
end
# Reloads the \target and returns +self+ on success.
@@ -83,10 +80,15 @@ module ActiveRecord
loaded!
end
- def scoped
+ def scope
target_scope.merge(association_scope)
end
+ def scoped
+ ActiveSupport::Deprecation.warn("#scoped is deprecated. use #scope instead.")
+ scope
+ end
+
# The scope for this association.
#
# Note that the association_scope is merged into the target_scope only when the
@@ -120,7 +122,7 @@ module ActiveRecord
# Can be overridden (i.e. in ThroughAssociation) to merge in other scopes (i.e. the
# through association's scope)
def target_scope
- klass.scoped
+ klass.all
end
# Loads the \target if needed and returns it.
@@ -134,17 +136,8 @@ module ActiveRecord
# ActiveRecord::RecordNotFound is rescued within the method, and it is
# not reraised. The proxy is \reset and +nil+ is the return value.
def load_target
- if find_target?
- begin
- if IdentityMap.enabled? && association_class && association_class.respond_to?(:base_class)
- @target = IdentityMap.get(association_class, owner[reflection.foreign_key])
- end
- rescue NameError
- nil
- ensure
- @target ||= find_target
- end
- end
+ @target = find_target if (@stale_state && stale_target?) || find_target?
+
loaded! unless loaded?
target
rescue ActiveRecord::RecordNotFound
@@ -159,6 +152,21 @@ module ActiveRecord
end
end
+ # We can't dump @reflection since it contains the scope proc
+ def marshal_dump
+ reflection = @reflection
+ @reflection = nil
+
+ ivars = instance_variables.map { |name| [name, instance_variable_get(name)] }
+ [reflection.name, ivars]
+ end
+
+ def marshal_load(data)
+ reflection_name, ivars = data
+ ivars.each { |name, val| instance_variable_set(name, val) }
+ @reflection = @owner.class.reflect_on_association(reflection_name)
+ end
+
private
def find_target?
@@ -168,7 +176,7 @@ module ActiveRecord
def creation_attributes
attributes = {}
- if reflection.macro.in?([:has_one, :has_many]) && !options[:through]
+ if (reflection.macro == :has_one || reflection.macro == :has_many) && !options[:through]
attributes[reflection.foreign_key] = owner[reflection.active_record_primary_key]
if reflection.options[:as]
@@ -225,13 +233,10 @@ module ActiveRecord
def stale_state
end
- def association_class
- @reflection.klass
- end
-
- def build_record(attributes, options)
- reflection.build_association(attributes, options) do |record|
- record.assign_attributes(create_scope.except(*record.changed), :without_protection => true)
+ def build_record(attributes)
+ reflection.build_association(attributes) do |record|
+ attributes = create_scope.except(*(record.changed - [reflection.foreign_key]))
+ record.assign_attributes(attributes)
end
end
end
diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb
index 982084c9b8..1303822868 100644
--- a/activerecord/lib/active_record/associations/association_scope.rb
+++ b/activerecord/lib/active_record/associations/association_scope.rb
@@ -6,7 +6,7 @@ module ActiveRecord
attr_reader :association, :alias_tracker
delegate :klass, :owner, :reflection, :interpolate, :to => :association
- delegate :chain, :conditions, :options, :source_options, :active_record, :to => :reflection
+ delegate :chain, :scope_chain, :options, :source_options, :active_record, :to => :reflection
def initialize(association)
@association = association
@@ -15,23 +15,28 @@ module ActiveRecord
def scope
scope = klass.unscoped
- scope = scope.extending(*Array(options[:extend]))
-
- # It's okay to just apply all these like this. The options will only be present if the
- # association supports that option; this is enforced by the association builder.
- scope = scope.apply_finder_options(options.slice(
- :readonly, :include, :references, :order, :limit, :joins, :group, :having, :offset, :select))
+ scope.merge! eval_scope(klass, reflection.scope) if reflection.scope
+ add_constraints(scope)
+ end
- if options[:through] && !options[:include]
- scope = scope.includes(source_options[:include])
- end
+ private
- scope = scope.uniq if options[:uniq]
+ def column_for(table_name, column_name)
+ columns = alias_tracker.connection.schema_cache.columns_hash[table_name]
+ columns[column_name]
+ end
- add_constraints(scope)
+ def bind_value(scope, column, value)
+ substitute = alias_tracker.connection.substitute_at(
+ column, scope.bind_values.length)
+ scope.bind_values += [[column, value]]
+ substitute
end
- private
+ def bind(scope, table_name, column_name, value)
+ column = column_for table_name, column_name
+ bind_value scope, column, value
+ end
def add_constraints(scope)
tables = construct_tables
@@ -64,21 +69,14 @@ module ActiveRecord
foreign_key = reflection.active_record_primary_key
end
- conditions = self.conditions[i]
-
if reflection == chain.last
- scope = scope.where(table[key].eq(owner[foreign_key]))
+ bind_val = bind scope, table.table_name, key.to_s, owner[foreign_key]
+ scope = scope.where(table[key].eq(bind_val))
if reflection.type
- scope = scope.where(table[reflection.type].eq(owner.class.base_class.name))
- end
-
- conditions.each do |condition|
- if options[:through] && condition.is_a?(Hash)
- condition = { table.name => condition }
- end
-
- scope = scope.where(interpolate(condition))
+ value = owner.class.base_class.name
+ bind_val = bind scope, table.table_name, reflection.type.to_s, value
+ scope = scope.where(table[reflection.type].eq(bind_val))
end
else
constraint = table[key].eq(foreign_table[foreign_key])
@@ -89,13 +87,15 @@ module ActiveRecord
end
scope = scope.joins(join(foreign_table, constraint))
+ end
- conditions.each do |condition|
- condition = interpolate(condition)
- condition = { (table.table_alias || table.name) => condition } unless i == 0
+ # Exclude the scope of the association itself, because that
+ # was already merged in the #scope method.
+ (scope_chain[i] - [self.reflection.scope]).each do |scope_chain_item|
+ item = eval_scope(reflection.klass, scope_chain_item)
- scope = scope.where(condition)
- end
+ scope.includes! item.includes_values
+ scope.where_values += item.where_values
end
end
@@ -117,6 +117,13 @@ module ActiveRecord
end
end
+ def eval_scope(klass, scope)
+ if scope.is_a?(Relation)
+ scope
+ else
+ klass.unscoped.instance_exec(owner, &scope)
+ end
+ end
end
end
end
diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb
index 97f531d064..75f72c1a46 100644
--- a/activerecord/lib/active_record/associations/belongs_to_association.rb
+++ b/activerecord/lib/active_record/associations/belongs_to_association.rb
@@ -2,6 +2,11 @@ module ActiveRecord
# = Active Record Belongs To Associations
module Associations
class BelongsToAssociation < SingularAssociation #:nodoc:
+
+ def handle_dependency
+ target.send(options[:dependent]) if load_target
+ end
+
def replace(record)
raise_on_type_mismatch(record) if record
@@ -14,6 +19,11 @@ module ActiveRecord
self.target = record
end
+ def reset
+ super
+ @updated = false
+ end
+
def updated?
@updated
end
@@ -72,7 +82,7 @@ module ActiveRecord
end
def stale_state
- owner[reflection.foreign_key].to_s
+ owner[reflection.foreign_key] && owner[reflection.foreign_key].to_s
end
end
end
diff --git a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb
index 2ee5dbbd70..88ce03a3cd 100644
--- a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb
+++ b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb
@@ -27,7 +27,8 @@ module ActiveRecord
end
def stale_state
- [super, owner[reflection.foreign_type].to_s]
+ foreign_key = super
+ foreign_key && [foreign_key.to_s, owner[reflection.foreign_type].to_s]
end
end
end
diff --git a/activerecord/lib/active_record/associations/builder/association.rb b/activerecord/lib/active_record/associations/builder/association.rb
index 2059d8acdf..1df876bf62 100644
--- a/activerecord/lib/active_record/associations/builder/association.rb
+++ b/activerecord/lib/active_record/associations/builder/association.rb
@@ -1,86 +1,106 @@
module ActiveRecord::Associations::Builder
class Association #:nodoc:
- class_attribute :valid_options
- self.valid_options = [:class_name, :foreign_key, :select, :conditions, :include, :extend, :readonly, :validate, :references]
+ class << self
+ attr_accessor :valid_options
+ end
- # Set by subclasses
- class_attribute :macro
+ self.valid_options = [:class_name, :foreign_key, :validate]
- attr_reader :model, :name, :options, :reflection
+ attr_reader :model, :name, :scope, :options, :reflection
- def self.build(model, name, options)
- new(model, name, options).build
+ def self.build(*args, &block)
+ new(*args, &block).build
end
- def initialize(model, name, options)
- @model, @name, @options = model, name, options
+ def initialize(model, name, scope, options)
+ @model = model
+ @name = name
+
+ if scope.is_a?(Hash)
+ @scope = nil
+ @options = scope
+ else
+ @scope = scope
+ @options = options
+ end
+
+ if @scope && @scope.arity == 0
+ prev_scope = @scope
+ @scope = proc { instance_exec(&prev_scope) }
+ end
end
def mixin
@model.generated_feature_methods
end
+ include Module.new { def build; end }
+
def build
validate_options
- reflection = model.create_reflection(self.class.macro, name, options, model)
define_accessors
- reflection
+ configure_dependency if options[:dependent]
+ @reflection = model.create_reflection(macro, name, scope, options, model)
+ super # provides an extension point
+ @reflection
end
- private
+ def macro
+ raise NotImplementedError
+ end
- def validate_options
- options.assert_valid_keys(self.class.valid_options)
- end
+ def valid_options
+ Association.valid_options
+ end
- def define_accessors
- define_readers
- define_writers
- end
+ def validate_options
+ options.assert_valid_keys(valid_options)
+ end
- def define_readers
- name = self.name
- mixin.redefine_method(name) do |*params|
- association(name).reader(*params)
+ def define_accessors
+ define_readers
+ define_writers
+ end
+
+ def define_readers
+ mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
+ def #{name}(*args)
+ association(:#{name}).reader(*args)
end
- end
+ CODE
+ end
- def define_writers
- name = self.name
- mixin.redefine_method("#{name}=") do |value|
- association(name).writer(value)
+ def define_writers
+ mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
+ def #{name}=(value)
+ association(:#{name}).writer(value)
end
- end
+ CODE
+ end
- def dependent_restrict_raises?
- ActiveRecord::Base.dependent_restrict_raises == true
+ def configure_dependency
+ unless valid_dependent_options.include? options[:dependent]
+ raise ArgumentError, "The :dependent option must be one of #{valid_dependent_options}, but is :#{options[:dependent]}"
end
- def dependent_restrict_deprecation_warning
- if dependent_restrict_raises?
- msg = "In the next release, `:dependent => :restrict` will not raise a `DeleteRestrictionError`. "\
- "Instead, it will add an error on the model. To fix this warning, make sure your code " \
- "isn't relying on a `DeleteRestrictionError` and then add " \
- "`config.active_record.dependent_restrict_raises = false` to your application config."
- ActiveSupport::Deprecation.warn msg
- end
+ if options[:dependent] == :restrict
+ ActiveSupport::Deprecation.warn(
+ "The :restrict option is deprecated. Please use :restrict_with_exception instead, which " \
+ "provides the same functionality."
+ )
end
- def define_restrict_dependency_method
- name = self.name
- mixin.redefine_method(dependency_method_name) do
- # has_many or has_one associations
- if send(name).respond_to?(:exists?) ? send(name).exists? : !send(name).nil?
- if dependent_restrict_raises?
- raise ActiveRecord::DeleteRestrictionError.new(name)
- else
- key = association(name).reflection.macro == :has_one ? "one" : "many"
- errors.add(:base, :"restrict_dependent_destroy.#{key}",
- :record => self.class.human_attribute_name(name).downcase)
- return false
- end
- end
+ mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
+ def #{macro}_dependent_for_#{name}
+ association(:#{name}).handle_dependency
end
- end
+ CODE
+
+ model.before_destroy "#{macro}_dependent_for_#{name}"
+ end
+
+ def valid_dependent_options
+ raise NotImplementedError
+ end
end
end
diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb
index 4183c222de..2f2600b7fb 100644
--- a/activerecord/lib/active_record/associations/builder/belongs_to.rb
+++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb
@@ -1,10 +1,12 @@
-require 'active_support/core_ext/object/inclusion'
-
module ActiveRecord::Associations::Builder
class BelongsTo < SingularAssociation #:nodoc:
- self.macro = :belongs_to
+ def macro
+ :belongs_to
+ end
- self.valid_options += [:foreign_type, :polymorphic, :touch]
+ def valid_options
+ super + [:foreign_type, :polymorphic, :touch]
+ end
def constructable?
!options[:polymorphic]
@@ -14,74 +16,51 @@ module ActiveRecord::Associations::Builder
reflection = super
add_counter_cache_callbacks(reflection) if options[:counter_cache]
add_touch_callbacks(reflection) if options[:touch]
- configure_dependency
reflection
end
- private
+ def add_counter_cache_callbacks(reflection)
+ cache_column = reflection.counter_cache_column
- def add_counter_cache_callbacks(reflection)
- cache_column = reflection.counter_cache_column
- name = self.name
-
- method_name = "belongs_to_counter_cache_after_create_for_#{name}"
- mixin.redefine_method(method_name) do
- record = send(name)
- record.class.increment_counter(cache_column, record.id) unless record.nil?
+ 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?
end
- model.after_create(method_name)
- method_name = "belongs_to_counter_cache_before_destroy_for_#{name}"
- mixin.redefine_method(method_name) do
+ def belongs_to_counter_cache_before_destroy_for_#{name}
unless marked_for_destruction?
- record = send(name)
- record.class.decrement_counter(cache_column, record.id) unless record.nil?
+ record = #{name}
+ record.class.decrement_counter(:#{cache_column}, record.id) unless record.nil?
end
end
- model.before_destroy(method_name)
+ CODE
- model.send(:module_eval,
- "#{reflection.class_name}.send(:attr_readonly,\"#{cache_column}\".intern) if defined?(#{reflection.class_name}) && #{reflection.class_name}.respond_to?(:attr_readonly)", __FILE__, __LINE__
- )
- end
+ model.after_create "belongs_to_counter_cache_after_create_for_#{name}"
+ model.before_destroy "belongs_to_counter_cache_before_destroy_for_#{name}"
- def add_touch_callbacks(reflection)
- name = self.name
- method_name = "belongs_to_touch_after_save_or_destroy_for_#{name}"
- touch = options[:touch]
+ klass = reflection.class_name.safe_constantize
+ klass.attr_readonly cache_column if klass && klass.respond_to?(:attr_readonly)
+ end
- mixin.redefine_method(method_name) do
- record = send(name)
+ def add_touch_callbacks(reflection)
+ mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
+ def belongs_to_touch_after_save_or_destroy_for_#{name}
+ record = #{name}
unless record.nil?
- if touch == true
- record.touch
- else
- record.touch(touch)
- end
+ record.touch #{options[:touch].inspect if options[:touch] != true}
end
end
+ CODE
- model.after_save(method_name)
- model.after_touch(method_name)
- model.after_destroy(method_name)
- end
-
- def configure_dependency
- if options[:dependent]
- unless options[:dependent].in?([:destroy, :delete])
- raise ArgumentError, "The :dependent option expects either :destroy or :delete (#{options[:dependent].inspect})"
- 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}"
+ end
- method_name = "belongs_to_dependent_#{options[:dependent]}_for_#{name}"
- model.send(:class_eval, <<-eoruby, __FILE__, __LINE__ + 1)
- def #{method_name}
- association = #{name}
- association.#{options[:dependent]} if association
- end
- eoruby
- model.after_destroy method_name
- end
- end
+ def valid_dependent_options
+ [:destroy, :delete]
+ 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 768f70b6c9..1b382f7285 100644
--- a/activerecord/lib/active_record/associations/builder/collection_association.rb
+++ b/activerecord/lib/active_record/associations/builder/collection_association.rb
@@ -2,23 +2,19 @@ module ActiveRecord::Associations::Builder
class CollectionAssociation < Association #:nodoc:
CALLBACKS = [:before_add, :after_add, :before_remove, :after_remove]
- self.valid_options += [
- :table_name, :order, :group, :having, :limit, :offset, :uniq, :finder_sql,
- :counter_sql, :before_add, :after_add, :before_remove, :after_remove
- ]
-
- attr_reader :block_extension
-
- def self.build(model, name, options, &extension)
- new(model, name, options, &extension).build
+ def valid_options
+ super + [:table_name, :finder_sql, :counter_sql, :before_add, :after_add, :before_remove, :after_remove]
end
- def initialize(model, name, options, &extension)
- super(model, name, options)
+ attr_reader :block_extension, :extension_module
+
+ def initialize(*args, &extension)
+ super(*args)
@block_extension = extension
end
def build
+ show_deprecation_warnings
wrap_block_extension
reflection = super
CALLBACKS.each { |callback_name| define_callback(callback_name) }
@@ -29,47 +25,61 @@ module ActiveRecord::Associations::Builder
true
end
- private
+ def show_deprecation_warnings
+ [:finder_sql, :counter_sql].each do |name|
+ if options.include? name
+ ActiveSupport::Deprecation.warn("The :#{name} association option is deprecated. Please find an alternative (such as using scopes).")
+ end
+ end
+ end
+
+ def wrap_block_extension
+ if block_extension
+ @extension_module = mod = Module.new(&block_extension)
+ silence_warnings do
+ model.parent.const_set(extension_module_name, mod)
+ end
- def wrap_block_extension
- options[:extend] = Array(options[:extend])
+ prev_scope = @scope
- if block_extension
- silence_warnings do
- model.parent.const_set(extension_module_name, Module.new(&block_extension))
- end
- options[:extend].push("#{model.parent}::#{extension_module_name}".constantize)
+ if prev_scope
+ @scope = proc { |owner| instance_exec(owner, &prev_scope).extending(mod) }
+ else
+ @scope = proc { extending(mod) }
end
end
+ end
- def extension_module_name
- @extension_module_name ||= "#{model.to_s.demodulize}#{name.to_s.camelize}AssociationExtension"
- end
+ def extension_module_name
+ @extension_module_name ||= "#{model.name.demodulize}#{name.to_s.camelize}AssociationExtension"
+ end
- def define_callback(callback_name)
- full_callback_name = "#{callback_name}_for_#{name}"
+ def define_callback(callback_name)
+ full_callback_name = "#{callback_name}_for_#{name}"
- # TODO : why do i need method_defined? I think its because of the inheritance chain
- model.class_attribute full_callback_name.to_sym unless model.method_defined?(full_callback_name)
- model.send("#{full_callback_name}=", Array(options[callback_name.to_sym]))
- end
+ # TODO : why do i need method_defined? I think its because of the inheritance chain
+ model.class_attribute full_callback_name.to_sym unless model.method_defined?(full_callback_name)
+ model.send("#{full_callback_name}=", Array(options[callback_name.to_sym]))
+ end
- def define_readers
- super
+ def define_readers
+ super
- name = self.name
- mixin.redefine_method("#{name.to_s.singularize}_ids") do
- association(name).ids_reader
+ mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
+ def #{name.to_s.singularize}_ids
+ association(:#{name}).ids_reader
end
- end
+ CODE
+ end
- def define_writers
- super
+ def define_writers
+ super
- name = self.name
- mixin.redefine_method("#{name.to_s.singularize}_ids=") do |ids|
- association(name).ids_writer(ids)
+ mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
+ def #{name.to_s.singularize}_ids=(ids)
+ association(:#{name}).ids_writer(ids)
end
- end
+ CODE
+ end
end
end
diff --git a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb
index 0b634ab944..bdac02b5bf 100644
--- a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb
+++ b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb
@@ -1,57 +1,39 @@
module ActiveRecord::Associations::Builder
class HasAndBelongsToMany < CollectionAssociation #:nodoc:
- self.macro = :has_and_belongs_to_many
+ def macro
+ :has_and_belongs_to_many
+ end
- self.valid_options += [:join_table, :association_foreign_key, :delete_sql, :insert_sql]
+ def valid_options
+ super + [:join_table, :association_foreign_key, :delete_sql, :insert_sql]
+ end
def build
reflection = super
- check_validity(reflection)
define_destroy_hook
reflection
end
- private
+ def show_deprecation_warnings
+ super
- def define_destroy_hook
- name = self.name
- model.send(:include, Module.new {
- class_eval <<-RUBY, __FILE__, __LINE__ + 1
- def destroy_associations
- association(#{name.to_sym.inspect}).delete_all_on_destroy
- super
- end
- RUBY
- })
- end
-
- # TODO: These checks should probably be moved into the Reflection, and we should not be
- # redefining the options[:join_table] value - instead we should define a
- # reflection.join_table method.
- def check_validity(reflection)
- if reflection.association_foreign_key == reflection.foreign_key
- raise ActiveRecord::HasAndBelongsToManyAssociationForeignKeyNeeded.new(reflection)
+ [:delete_sql, :insert_sql].each do |name|
+ if options.include? name
+ ActiveSupport::Deprecation.warn("The :#{name} association option is deprecated. Please find an alternative (such as using has_many :through).")
end
-
- reflection.options[:join_table] ||= join_table_name(
- model.send(:undecorated_table_name, model.to_s),
- model.send(:undecorated_table_name, reflection.class_name)
- )
end
+ end
- # Generates a join table name from two provided table names.
- # The names in the join table names end up in lexicographic order.
- #
- # join_table_name("members", "clubs") # => "clubs_members"
- # join_table_name("members", "special_clubs") # => "members_special_clubs"
- def join_table_name(first_table_name, second_table_name)
- if first_table_name < second_table_name
- join_table = "#{first_table_name}_#{second_table_name}"
- else
- join_table = "#{second_table_name}_#{first_table_name}"
- end
-
- model.table_name_prefix + join_table + model.table_name_suffix
- end
+ def define_destroy_hook
+ name = self.name
+ model.send(:include, Module.new {
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
+ def destroy_associations
+ association(:#{name}).delete_all
+ super
+ end
+ RUBY
+ })
+ end
end
end
diff --git a/activerecord/lib/active_record/associations/builder/has_many.rb b/activerecord/lib/active_record/associations/builder/has_many.rb
index 9ddfd433e4..ab8225460a 100644
--- a/activerecord/lib/active_record/associations/builder/has_many.rb
+++ b/activerecord/lib/active_record/associations/builder/has_many.rb
@@ -1,60 +1,15 @@
-require 'active_support/core_ext/object/inclusion'
-
module ActiveRecord::Associations::Builder
class HasMany < CollectionAssociation #:nodoc:
- self.macro = :has_many
-
- self.valid_options += [:primary_key, :dependent, :as, :through, :source, :source_type, :inverse_of]
-
- def build
- reflection = super
- configure_dependency
- reflection
+ def macro
+ :has_many
end
- private
-
- def configure_dependency
- if options[:dependent]
- unless options[:dependent].in?([:destroy, :delete_all, :nullify, :restrict])
- raise ArgumentError, "The :dependent option expects either :destroy, :delete_all, " \
- ":nullify or :restrict (#{options[:dependent].inspect})"
- end
-
- dependent_restrict_deprecation_warning if options[:dependent] == :restrict
- send("define_#{options[:dependent]}_dependency_method")
- model.before_destroy dependency_method_name
- end
- end
-
- def define_destroy_dependency_method
- name = self.name
- mixin.redefine_method(dependency_method_name) do
- send(name).each do |o|
- # No point in executing the counter update since we're going to destroy the parent anyway
- o.mark_for_destruction
- end
-
- send(name).delete_all
- end
- end
-
- def define_delete_all_dependency_method
- name = self.name
- mixin.redefine_method(dependency_method_name) do
- association(name).delete_all_on_destroy
- end
- end
-
- def define_nullify_dependency_method
- name = self.name
- mixin.redefine_method(dependency_method_name) do
- send(name).delete_all
- end
- end
+ def valid_options
+ super + [:primary_key, :dependent, :as, :through, :source, :source_type, :inverse_of]
+ end
- def dependency_method_name
- "has_many_dependent_for_#{name}"
- end
+ def valid_dependent_options
+ [:destroy, :delete_all, :nullify, :restrict, :restrict_with_error, :restrict_with_exception]
+ end
end
end
diff --git a/activerecord/lib/active_record/associations/builder/has_one.rb b/activerecord/lib/active_record/associations/builder/has_one.rb
index bc8a212bee..0da564f402 100644
--- a/activerecord/lib/active_record/associations/builder/has_one.rb
+++ b/activerecord/lib/active_record/associations/builder/has_one.rb
@@ -1,56 +1,25 @@
-require 'active_support/core_ext/object/inclusion'
-
module ActiveRecord::Associations::Builder
class HasOne < SingularAssociation #:nodoc:
- self.macro = :has_one
-
- self.valid_options += [:order, :as]
+ def macro
+ :has_one
+ end
- class_attribute :through_options
- self.through_options = [:through, :source, :source_type]
+ def valid_options
+ valid = super + [:order, :as]
+ valid += [:through, :source, :source_type] if options[:through]
+ valid
+ end
def constructable?
!options[:through]
end
- def build
- reflection = super
- configure_dependency unless options[:through]
- reflection
+ def configure_dependency
+ super unless options[:through]
end
- private
-
- def validate_options
- valid_options = self.class.valid_options
- valid_options += self.class.through_options if options[:through]
- options.assert_valid_keys(valid_options)
- end
-
- def configure_dependency
- if options[:dependent]
- unless options[:dependent].in?([:destroy, :delete, :nullify, :restrict])
- raise ArgumentError, "The :dependent option expects either :destroy, :delete, " \
- ":nullify or :restrict (#{options[:dependent].inspect})"
- end
-
- dependent_restrict_deprecation_warning if options[:dependent] == :restrict
- send("define_#{options[:dependent]}_dependency_method")
- model.before_destroy dependency_method_name
- end
- end
-
- def define_destroy_dependency_method
- name = self.name
- mixin.redefine_method(dependency_method_name) do
- association(name).delete
- end
- end
- alias :define_delete_dependency_method :define_destroy_dependency_method
- alias :define_nullify_dependency_method :define_destroy_dependency_method
-
- def dependency_method_name
- "has_one_dependent_#{options[:dependent]}_for_#{name}"
- end
+ def valid_dependent_options
+ [:destroy, :delete, :nullify, :restrict, :restrict_with_error, :restrict_with_exception]
+ end
end
end
diff --git a/activerecord/lib/active_record/associations/builder/singular_association.rb b/activerecord/lib/active_record/associations/builder/singular_association.rb
index 436b6c1524..6a5830e57f 100644
--- a/activerecord/lib/active_record/associations/builder/singular_association.rb
+++ b/activerecord/lib/active_record/associations/builder/singular_association.rb
@@ -1,6 +1,8 @@
module ActiveRecord::Associations::Builder
class SingularAssociation < Association #:nodoc:
- self.valid_options += [:remote, :dependent, :counter_cache, :primary_key, :inverse_of]
+ def valid_options
+ super + [:remote, :dependent, :counter_cache, :primary_key, :inverse_of]
+ end
def constructable?
true
@@ -11,22 +13,20 @@ module ActiveRecord::Associations::Builder
define_constructors if constructable?
end
- private
-
- def define_constructors
- name = self.name
-
- mixin.redefine_method("build_#{name}") do |*params, &block|
- association(name).build(*params, &block)
+ def define_constructors
+ mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
+ def build_#{name}(*args, &block)
+ association(:#{name}).build(*args, &block)
end
- mixin.redefine_method("create_#{name}") do |*params, &block|
- association(name).create(*params, &block)
+ def create_#{name}(*args, &block)
+ association(:#{name}).create(*args, &block)
end
- mixin.redefine_method("create_#{name}!") do |*params, &block|
- association(name).create!(*params, &block)
+ def create_#{name}!(*args, &block)
+ association(:#{name}).create!(*args, &block)
end
- end
+ CODE
+ end
end
end
diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb
index b2136605e1..fe3e5b00f7 100644
--- a/activerecord/lib/active_record/associations/collection_association.rb
+++ b/activerecord/lib/active_record/associations/collection_association.rb
@@ -6,6 +6,15 @@ module ActiveRecord
# ease the implementation of association proxies that represent
# collections. See the class hierarchy in AssociationProxy.
#
+ # CollectionAssociation:
+ # HasAndBelongsToManyAssociation => has_and_belongs_to_many
+ # HasManyAssociation => has_many
+ # HasManyThroughAssociation + ThroughAssociation => has_many :through
+ #
+ # CollectionAssociation class provides common methods to the collections
+ # defined by +has_and_belongs_to_many+, +has_many+ or +has_many+ with
+ # +:through association+ option.
+ #
# You need to be careful with assumptions regarding the target: The proxy
# does not fetch records from the database until it needs them, but new
# ones created with +build+ are added to the target. So, the target may be
@@ -16,12 +25,6 @@ module ActiveRecord
# If you need to work on all current children, new and existing records,
# +load_target+ and the +loaded+ flag are your friends.
class CollectionAssociation < Association #:nodoc:
- attr_reader :proxy
-
- def initialize(owner, reflection)
- super
- @proxy = CollectionProxy.new(self)
- end
# Implements the reader method, e.g. foo.items for Foo.has_many :items
def reader(force_reload = false)
@@ -31,7 +34,7 @@ module ActiveRecord
reload
end
- proxy
+ CollectionProxy.new(self)
end
# Implements the writer method, e.g. foo.items= for Foo.has_many :items
@@ -47,18 +50,7 @@ module ActiveRecord
end
else
column = "#{reflection.quoted_table_name}.#{reflection.association_primary_key}"
- relation = scoped
-
- including = (relation.eager_load_values + relation.includes_values).uniq
-
- if including.any?
- join_dependency = ActiveRecord::Associations::JoinDependency.new(reflection.klass, including, [])
- relation = join_dependency.join_associations.inject(relation) do |r, association|
- association.join_relation(r)
- end
- end
-
- relation.pluck(column)
+ scope.pluck(column)
end
end
@@ -71,7 +63,7 @@ module ActiveRecord
end
def reset
- @loaded = false
+ super
@target = []
end
@@ -79,7 +71,7 @@ module ActiveRecord
if block_given?
load_target.select.each { |e| yield e }
else
- scoped.select(select)
+ scope.select(select)
end
end
@@ -90,7 +82,7 @@ module ActiveRecord
if options[:finder_sql]
find_by_scan(*args)
else
- scoped.find(*args)
+ scope.find(*args)
end
end
end
@@ -103,26 +95,27 @@ module ActiveRecord
first_or_last(:last, *args)
end
- def build(attributes = {}, options = {}, &block)
+ def build(attributes = {}, &block)
if attributes.is_a?(Array)
- attributes.collect { |attr| build(attr, options, &block) }
+ attributes.collect { |attr| build(attr, &block) }
else
- add_to_target(build_record(attributes, options)) do |record|
+ add_to_target(build_record(attributes)) do |record|
yield(record) if block_given?
end
end
end
- def create(attributes = {}, options = {}, &block)
- create_record(attributes, options, &block)
+ def create(attributes = {}, &block)
+ create_record(attributes, &block)
end
- def create!(attributes = {}, options = {}, &block)
- create_record(attributes, options, true, &block)
+ def create!(attributes = {}, &block)
+ create_record(attributes, true, &block)
end
- # Add +records+ to this association. Returns +self+ so method calls may be chained.
- # Since << flattens its argument list and inserts each record, +push+ and +concat+ behave identically.
+ # Add +records+ to this association. Returns +self+ so method calls may
+ # be chained. Since << flattens its argument list and inserts each record,
+ # +push+ and +concat+ behave identically.
def concat(*records)
load_target if owner.new_record?
@@ -148,23 +141,16 @@ module ActiveRecord
end
end
- # Remove all records from this association
+ # Remove all records from this association.
#
# See delete for more info.
def delete_all
- delete(load_target).tap do
+ delete(:all).tap do
reset
loaded!
end
end
- # Called when the association is declared as :dependent => :delete_all. This is
- # an optimised version which avoids loading the records into memory. Not really
- # for public consumption.
- def delete_all_on_destroy
- scoped.delete_all
- end
-
# Destroy all the records from this association.
#
# See destroy for more info.
@@ -175,12 +161,12 @@ module ActiveRecord
end
end
- # Calculate sum using SQL, not Enumerable
+ # Calculate sum using SQL, not Enumerable.
def sum(*args)
if block_given?
- scoped.sum(*args) { |*block_args| yield(*block_args) }
+ scope.sum(*args) { |*block_args| yield(*block_args) }
else
- scoped.sum(*args)
+ scope.sum(*args)
end
end
@@ -197,13 +183,13 @@ module ActiveRecord
reflection.klass.count_by_sql(custom_counter_sql)
else
- if options[:uniq]
+ if association_scope.uniq_value
# This is needed because 'SELECT count(DISTINCT *)..' is not valid SQL.
column_name ||= reflection.klass.primary_key
- count_options.merge!(:distinct => true)
+ count_options[:distinct] = true
end
- value = scoped.count(column_name, count_options)
+ value = scope.count(column_name, count_options)
limit = options[:limit]
offset = options[:offset]
@@ -224,7 +210,18 @@ module ActiveRecord
# are actually removed from the database, that depends precisely on
# +delete_records+. They are in any case removed from the collection.
def delete(*records)
- delete_or_destroy(records, options[:dependent])
+ dependent = options[:dependent]
+
+ if records.first == :all
+ if loaded? || dependent == :destroy
+ delete_or_destroy(load_target, dependent)
+ else
+ delete_records(:all, dependent)
+ end
+ else
+ records = find(records) if records.any? { |record| record.kind_of?(Fixnum) || record.kind_of?(String) }
+ delete_or_destroy(records, dependent)
+ end
end
# Destroy +records+ and remove them from this association calling
@@ -248,11 +245,15 @@ module ActiveRecord
# This method is abstract in the sense that it relies on
# +count_records+, which is a method descendants have to provide.
def size
- if !find_target? || (loaded? && !options[:uniq])
- target.size
- elsif !loaded? && options[:group]
+ if !find_target? || loaded?
+ if association_scope.uniq_value
+ target.uniq.size
+ else
+ target.size
+ end
+ elsif !loaded? && !association_scope.group_values.empty?
load_target.size
- elsif !loaded? && !options[:uniq] && target.is_a?(Array)
+ elsif !loaded? && !association_scope.uniq_value && target.is_a?(Array)
unsaved_records = target.select { |r| r.new_record? }
unsaved_records.size + count_records
else
@@ -269,13 +270,24 @@ module ActiveRecord
load_target.size
end
- # Equivalent to <tt>collection.size.zero?</tt>. If the collection has
- # not been already loaded and you are going to fetch the records anyway
- # it is better to check <tt>collection.length.zero?</tt>.
+ # Returns true if the collection is empty.
+ #
+ # If the collection has been loaded or the <tt>:counter_sql</tt> option
+ # is provided, it is equivalent to <tt>collection.size.zero?</tt>. If the
+ # collection has not been loaded, it is equivalent to
+ # <tt>collection.exists?</tt>. If the collection has not already been
+ # loaded and you are going to fetch the records anyway it is better to
+ # check <tt>collection.length.zero?</tt>.
def empty?
- size.zero?
+ if loaded? || options[:counter_sql]
+ size.zero?
+ else
+ !scope.exists?
+ end
end
+ # Returns true if the collections is not empty.
+ # Equivalent to +!collection.empty?+.
def any?
if block_given?
load_target.any? { |*block_args| yield(*block_args) }
@@ -284,7 +296,8 @@ module ActiveRecord
end
end
- # Returns true if the collection has more than 1 record. Equivalent to collection.size > 1.
+ # Returns true if the collection has more than 1 record.
+ # Equivalent to +collection.size > 1+.
def many?
if block_given?
load_target.many? { |*block_args| yield(*block_args) }
@@ -293,15 +306,15 @@ module ActiveRecord
end
end
- def uniq(collection = load_target)
+ def uniq
seen = {}
- collection.find_all do |record|
+ load_target.find_all do |record|
seen[record.id] = true unless seen.key?(record.id)
end
end
- # Replace this collection with +other_array+
- # This will perform a diff and delete/add only records that have changed.
+ # Replace this collection with +other_array+. This will perform a diff
+ # and delete/add only records that have changed.
def replace(other_array)
other_array.each { |val| raise_on_type_mismatch(val) }
original_target = load_target.dup
@@ -319,7 +332,7 @@ module ActiveRecord
include_in_memory?(record)
else
load_target if options[:finder_sql]
- loaded? ? target.include?(record) : scoped.exists?(record)
+ loaded? ? target.include?(record) : scope.exists?(record)
end
else
false
@@ -339,7 +352,7 @@ module ActiveRecord
callback(:before_add, record)
yield(record) if block_given?
- if options[:uniq] && index = @target.index(record)
+ if association_scope.uniq_value && index = @target.index(record)
@target[index] = record
else
@target << record
@@ -360,7 +373,7 @@ module ActiveRecord
# replace the SELECT clause with COUNT(SELECTS), preserving any hints within /* ... */
interpolate(options[:finder_sql]).sub(/SELECT\b(\/\*.*?\*\/ )?(.*)\bFROM\b/im) do
count_with = $2.to_s
- count_with = '*' if count_with.blank? || count_with =~ /,/
+ count_with = '*' if count_with.blank? || count_with =~ /,/ || count_with =~ /\.\*/
"SELECT #{$1}COUNT(#{count_with}) FROM"
end
end
@@ -375,10 +388,9 @@ module ActiveRecord
if options[:finder_sql]
reflection.klass.find_by_sql(custom_finder_sql)
else
- scoped.all
+ scope.to_a
end
- records = options[:uniq] ? uniq(records) : records
records.each { |record| set_inverse_instance(record) }
records
end
@@ -413,16 +425,16 @@ module ActiveRecord
persisted + memory
end
- def create_record(attributes, options, raise = false, &block)
+ def create_record(attributes, raise = false, &block)
unless owner.persisted?
raise ActiveRecord::RecordNotSaved, "You cannot call create unless the parent is saved"
end
if attributes.is_a?(Array)
- attributes.collect { |attr| create_record(attr, options, raise, &block) }
+ attributes.collect { |attr| create_record(attr, raise, &block) }
else
transaction do
- add_to_target(build_record(attributes, options)) do |record|
+ add_to_target(build_record(attributes)) do |record|
yield(record) if block_given?
insert_record(record, true, raise)
end
@@ -436,7 +448,7 @@ module ActiveRecord
end
def create_scope
- scoped.scope_for_create.stringify_keys
+ scope.scope_for_create.stringify_keys
end
def delete_or_destroy(records, method)
@@ -474,6 +486,8 @@ module ActiveRecord
raise RecordNotSaved, "Failed to replace #{reflection.name} because one or more of the " \
"new records could not be saved."
end
+
+ target
end
def concat_records(records)
@@ -544,7 +558,7 @@ module ActiveRecord
# If using a custom finder_sql, #find scans the entire collection.
def find_by_scan(*args)
expects_array = args.first.kind_of?(Array)
- ids = args.flatten.compact.uniq.map { |arg| arg.to_i }
+ ids = args.flatten.compact.map{ |arg| arg.to_i }.uniq
if ids.size == 1
id = ids.first
@@ -559,8 +573,8 @@ module ActiveRecord
def first_or_last(type, *args)
args.shift if args.first.is_a?(Hash) && args.first.empty?
- collection = fetch_first_or_last_using_find?(args) ? scoped : load_target
- collection.send(type, *args)
+ collection = fetch_first_or_last_using_find?(args) ? scope : load_target
+ collection.send(type, *args).tap {|it| set_inverse_instance it }
end
end
end
diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb
index 5eda0387c4..c113957faa 100644
--- a/activerecord/lib/active_record/associations/collection_proxy.rb
+++ b/activerecord/lib/active_record/associations/collection_proxy.rb
@@ -18,14 +18,8 @@ module ActiveRecord
# <tt>@owner</tt>, the collection of its posts as <tt>@target</tt>, and
# the <tt>@reflection</tt> object represents a <tt>:has_many</tt> macro.
#
- # This class has most of the basic instance methods removed, and delegates
- # unknown methods to <tt>@target</tt> via <tt>method_missing</tt>. As a
- # corner case, it even removes the +class+ method and that's why you get
- #
- # blog.posts.class # => Array
- #
- # though the object behind <tt>blog.posts</tt> is not an Array, but an
- # ActiveRecord::Associations::HasManyAssociation.
+ # This class delegates unknown methods to <tt>@target</tt> via
+ # <tt>method_missing</tt>.
#
# The <tt>@target</tt> object is not \loaded until needed. For example,
#
@@ -33,26 +27,811 @@ module ActiveRecord
#
# is computed directly through SQL and does not trigger by itself the
# instantiation of the actual post records.
- class CollectionProxy # :nodoc:
- alias :proxy_extend :extend
+ class CollectionProxy < Relation
+ def initialize(association) #:nodoc:
+ @association = association
+ super association.klass, association.klass.arel_table
+ merge! association.scope
+ end
- instance_methods.each { |m| undef_method m unless m.to_s =~ /^(?:nil\?|send|object_id|to_a)$|^__|^respond_to|proxy_/ }
+ def target
+ @association.target
+ end
- delegate :group, :order, :limit, :joins, :where, :preload, :eager_load, :includes, :from,
- :lock, :readonly, :having, :pluck, :to => :scoped
+ def load_target
+ @association.load_target
+ end
- delegate :target, :load_target, :loaded?, :to => :@association
+ def loaded?
+ @association.loaded?
+ end
- delegate :select, :find, :first, :last,
- :build, :create, :create!,
- :concat, :replace, :delete_all, :destroy_all, :delete, :destroy, :uniq,
- :sum, :count, :size, :length, :empty?,
- :any?, :many?, :include?,
- :to => :@association
+ ##
+ # Works in two ways.
+ #
+ # *First:* Specify a subset of fields to be selected from the result set.
+ #
+ # class Person < ActiveRecord::Base
+ # has_many :pets
+ # end
+ #
+ # person.pets
+ # # => [
+ # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
+ # # #<Pet id: 2, name: "Spook", person_id: 1>,
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
+ # # ]
+ #
+ # person.pets.select(:name)
+ # # => [
+ # # #<Pet id: nil, name: "Fancy-Fancy">,
+ # # #<Pet id: nil, name: "Spook">,
+ # # #<Pet id: nil, name: "Choo-Choo">
+ # # ]
+ #
+ # person.pets.select([:id, :name])
+ # # => [
+ # # #<Pet id: 1, name: "Fancy-Fancy">,
+ # # #<Pet id: 2, name: "Spook">,
+ # # #<Pet id: 3, name: "Choo-Choo">
+ # # ]
+ #
+ # Be careful because this also means you’re initializing a model
+ # object with only the fields that you’ve selected. If you attempt
+ # to access a field that is not in the initialized record you’ll
+ # receive:
+ #
+ # person.pets.select(:name).first.person_id
+ # # => 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,
+ # converting them into an array and iterating through them using
+ # Array#select.
+ #
+ # person.pets.select { |pet| pet.name =~ /oo/ }
+ # # => [
+ # # #<Pet id: 2, name: "Spook", person_id: 1>,
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
+ # # ]
+ #
+ # person.pets.select(:name) { |pet| pet.name =~ /oo/ }
+ # # => [
+ # # #<Pet id: 2, name: "Spook">,
+ # # #<Pet id: 3, name: "Choo-Choo">
+ # # ]
+ def select(select = nil, &block)
+ @association.select(select, &block)
+ end
- def initialize(association)
- @association = association
- Array(association.options[:extend]).each { |ext| proxy_extend(ext) }
+ ##
+ # Finds an object in the collection responding to the +id+. Uses the same
+ # rules as +ActiveRecord::Base.find+. Returns +ActiveRecord::RecordNotFound++
+ # error if the object can not be found.
+ #
+ # class Person < ActiveRecord::Base
+ # has_many :pets
+ # end
+ #
+ # person.pets
+ # # => [
+ # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
+ # # #<Pet id: 2, name: "Spook", person_id: 1>,
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
+ # # ]
+ #
+ # person.pets.find(1) # => #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>
+ # person.pets.find(4) # => ActiveRecord::RecordNotFound: Couldn't find Pet with id=4
+ #
+ # person.pets.find(2) { |pet| pet.name.downcase! }
+ # # => #<Pet id: 2, name: "fancy-fancy", person_id: 1>
+ #
+ # person.pets.find(2, 3)
+ # # => [
+ # # #<Pet id: 2, name: "Spook", person_id: 1>,
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
+ # # ]
+ def find(*args, &block)
+ @association.find(*args, &block)
+ end
+
+ ##
+ # Returns the first record, or the first +n+ records, from the collection.
+ # If the collection is empty, the first form returns +nil+, and the second
+ # form returns an empty array.
+ #
+ # class Person < ActiveRecord::Base
+ # has_many :pets
+ # end
+ #
+ # person.pets
+ # # => [
+ # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
+ # # #<Pet id: 2, name: "Spook", person_id: 1>,
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
+ # # ]
+ #
+ # person.pets.first # => #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>
+ #
+ # person.pets.first(2)
+ # # => [
+ # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
+ # # #<Pet id: 2, name: "Spook", person_id: 1>
+ # # ]
+ #
+ # another_person_without.pets # => []
+ # another_person_without.pets.first # => nil
+ # another_person_without.pets.first(3) # => []
+ def first(*args)
+ @association.first(*args)
+ end
+
+ ##
+ # Returns the last record, or the last +n+ records, from the collection.
+ # If the collection is empty, the first form returns +nil+, and the second
+ # form returns an empty array.
+ #
+ # class Person < ActiveRecord::Base
+ # has_many :pets
+ # end
+ #
+ # person.pets
+ # # => [
+ # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
+ # # #<Pet id: 2, name: "Spook", person_id: 1>,
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
+ # # ]
+ #
+ # person.pets.last # => #<Pet id: 3, name: "Choo-Choo", person_id: 1>
+ #
+ # person.pets.last(2)
+ # # => [
+ # # #<Pet id: 2, name: "Spook", person_id: 1>,
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
+ # # ]
+ #
+ # another_person_without.pets # => []
+ # another_person_without.pets.last # => nil
+ # another_person_without.pets.last(3) # => []
+ def last(*args)
+ @association.last(*args)
+ end
+
+ ##
+ # Returns a new object of the collection type that has been instantiated
+ # with +attributes+ and linked to this object, but have not yet been saved.
+ # You can pass an array of attributes hashes, this will return an array
+ # with the new objects.
+ #
+ # class Person
+ # has_many :pets
+ # end
+ #
+ # person.pets.build
+ # # => #<Pet id: nil, name: nil, person_id: 1>
+ #
+ # person.pets.build(name: 'Fancy-Fancy')
+ # # => #<Pet id: nil, name: "Fancy-Fancy", person_id: 1>
+ #
+ # person.pets.build([{name: 'Spook'}, {name: 'Choo-Choo'}, {name: 'Brain'}])
+ # # => [
+ # # #<Pet id: nil, name: "Spook", person_id: 1>,
+ # # #<Pet id: nil, name: "Choo-Choo", person_id: 1>,
+ # # #<Pet id: nil, name: "Brain", person_id: 1>
+ # # ]
+ #
+ # person.pets.size # => 5 # size of the collection
+ # person.pets.count # => 0 # count from database
+ def build(attributes = {}, &block)
+ @association.build(attributes, &block)
+ end
+
+ ##
+ # 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
+ # passes the validations).
+ #
+ # class Person
+ # has_many :pets
+ # end
+ #
+ # person.pets.create(name: 'Fancy-Fancy')
+ # # => #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>
+ #
+ # person.pets.create([{name: 'Spook'}, {name: 'Choo-Choo'}])
+ # # => [
+ # # #<Pet id: 2, name: "Spook", person_id: 1>,
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
+ # # ]
+ #
+ # person.pets.size # => 3
+ # person.pets.count # => 3
+ #
+ # person.pets.find(1, 2, 3)
+ # # => [
+ # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
+ # # #<Pet id: 2, name: "Spook", person_id: 1>,
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
+ # # ]
+ def create(attributes = {}, &block)
+ @association.create(attributes, &block)
+ end
+
+ ##
+ # Like +create+, except that if the record is invalid, raises an exception.
+ #
+ # class Person
+ # has_many :pets
+ # end
+ #
+ # class Pet
+ # validates :name, presence: true
+ # end
+ #
+ # person.pets.create!(name: nil)
+ # # => ActiveRecord::RecordInvalid: Validation failed: Name can't be blank
+ def create!(attributes = {}, &block)
+ @association.create!(attributes, &block)
+ end
+
+ ##
+ # Add one or more records to the collection by setting their foreign keys
+ # to the association's primary key. Since << flattens its argument list and
+ # inserts each record, +push+ and +concat+ behave identically. Returns +self+
+ # so method calls may be chained.
+ #
+ # class Person < ActiveRecord::Base
+ # pets :has_many
+ # end
+ #
+ # person.pets.size # => 0
+ # person.pets.concat(Pet.new(name: 'Fancy-Fancy'))
+ # person.pets.concat(Pet.new(name: 'Spook'), Pet.new(name: 'Choo-Choo'))
+ # person.pets.size # => 3
+ #
+ # person.id # => 1
+ # person.pets
+ # # => [
+ # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
+ # # #<Pet id: 2, name: "Spook", person_id: 1>,
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
+ # # ]
+ #
+ # person.pets.concat([Pet.new(name: 'Brain'), Pet.new(name: 'Benny')])
+ # person.pets.size # => 5
+ def concat(*records)
+ @association.concat(*records)
+ end
+
+ ##
+ # Replace this collection with +other_array+. This will perform a diff
+ # and delete/add only records that have changed.
+ #
+ # class Person < ActiveRecord::Base
+ # has_many :pets
+ # end
+ #
+ # person.pets
+ # # => [#<Pet id: 1, name: "Gorby", group: "cats", person_id: 1>]
+ #
+ # other_pets = [Pet.new(name: 'Puff', group: 'celebrities']
+ #
+ # person.pets.replace(other_pets)
+ #
+ # person.pets
+ # # => [#<Pet id: 2, name: "Puff", group: "celebrities", person_id: 1>]
+ #
+ # If the supplied array has an incorrect association type, it raises
+ # an <tt>ActiveRecord::AssociationTypeMismatch</tt> error:
+ #
+ # person.pets.replace(["doo", "ggie", "gaga"])
+ # # => ActiveRecord::AssociationTypeMismatch: Pet expected, got String
+ def replace(other_array)
+ @association.replace(other_array)
+ end
+
+ ##
+ # Deletes all the records from the collection. For +has_many+ associations,
+ # the deletion is done according to the strategy specified by the <tt>:dependent</tt>
+ # option. Returns an array with the deleted records.
+ #
+ # If no <tt>:dependent</tt> option is given, then it will follow the
+ # default strategy. The default strategy is <tt>:nullify</tt>. This
+ # sets the foreign keys to <tt>NULL</tt>. For, +has_many+ <tt>:through</tt>,
+ # the default strategy is +delete_all+.
+ #
+ # class Person < ActiveRecord::Base
+ # has_many :pets # dependent: :nullify option by default
+ # end
+ #
+ # person.pets.size # => 3
+ # person.pets
+ # # => [
+ # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
+ # # #<Pet id: 2, name: "Spook", person_id: 1>,
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
+ # # ]
+ #
+ # person.pets.delete_all
+ # # => [
+ # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
+ # # #<Pet id: 2, name: "Spook", person_id: 1>,
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
+ # # ]
+ #
+ # person.pets.size # => 0
+ # person.pets # => []
+ #
+ # Pet.find(1, 2, 3)
+ # # => [
+ # # #<Pet id: 1, name: "Fancy-Fancy", person_id: nil>,
+ # # #<Pet id: 2, name: "Spook", person_id: nil>,
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: nil>
+ # # ]
+ #
+ # If it is set to <tt>:destroy</tt> all the objects from the collection
+ # are removed by calling their +destroy+ method. See +destroy+ for more
+ # information.
+ #
+ # class Person < ActiveRecord::Base
+ # has_many :pets, dependent: :destroy
+ # end
+ #
+ # person.pets.size # => 3
+ # person.pets
+ # # => [
+ # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
+ # # #<Pet id: 2, name: "Spook", person_id: 1>,
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
+ # # ]
+ #
+ # person.pets.delete_all
+ # # => [
+ # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
+ # # #<Pet id: 2, name: "Spook", person_id: 1>,
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
+ # # ]
+ #
+ # Pet.find(1, 2, 3)
+ # # => ActiveRecord::RecordNotFound
+ #
+ # If it is set to <tt>:delete_all</tt>, all the objects are deleted
+ # *without* calling their +destroy+ method.
+ #
+ # class Person < ActiveRecord::Base
+ # has_many :pets, dependent: :delete_all
+ # end
+ #
+ # person.pets.size # => 3
+ # person.pets
+ # # => [
+ # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
+ # # #<Pet id: 2, name: "Spook", person_id: 1>,
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
+ # # ]
+ #
+ # person.pets.delete_all
+ # # => [
+ # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
+ # # #<Pet id: 2, name: "Spook", person_id: 1>,
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
+ # # ]
+ #
+ # Pet.find(1, 2, 3)
+ # # => ActiveRecord::RecordNotFound
+ def delete_all
+ @association.delete_all
+ end
+
+ ##
+ # Deletes the records of the collection directly from the database.
+ # This will _always_ remove the records ignoring the +:dependent+
+ # option.
+ #
+ # class Person < ActiveRecord::Base
+ # has_many :pets
+ # end
+ #
+ # person.pets.size # => 3
+ # person.pets
+ # # => [
+ # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
+ # # #<Pet id: 2, name: "Spook", person_id: 1>,
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
+ # # ]
+ #
+ # person.pets.destroy_all
+ #
+ # person.pets.size # => 0
+ # person.pets # => []
+ #
+ # Pet.find(1) # => Couldn't find Pet with id=1
+ def destroy_all
+ @association.destroy_all
+ end
+
+ ##
+ # Deletes the +records+ supplied and removes them from the collection. For
+ # +has_many+ associations, the deletion is done according to the strategy
+ # specified by the <tt>:dependent</tt> option. Returns an array with the
+ # deleted records.
+ #
+ # If no <tt>:dependent</tt> option is given, then it will follow the default
+ # strategy. The default strategy is <tt>:nullify</tt>. This sets the foreign
+ # keys to <tt>NULL</tt>. For, +has_many+ <tt>:through</tt>, the default
+ # strategy is +delete_all+.
+ #
+ # class Person < ActiveRecord::Base
+ # has_many :pets # dependent: :nullify option by default
+ # end
+ #
+ # person.pets.size # => 3
+ # person.pets
+ # # => [
+ # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
+ # # #<Pet id: 2, name: "Spook", person_id: 1>,
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
+ # # ]
+ #
+ # person.pets.delete(Pet.find(1))
+ # # => [#<Pet id: 1, name: "Fancy-Fancy", person_id: 1>]
+ #
+ # person.pets.size # => 2
+ # person.pets
+ # # => [
+ # # #<Pet id: 2, name: "Spook", person_id: 1>,
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
+ # # ]
+ #
+ # Pet.find(1)
+ # # => #<Pet id: 1, name: "Fancy-Fancy", person_id: nil>
+ #
+ # If it is set to <tt>:destroy</tt> all the +records+ are removed by calling
+ # their +destroy+ method. See +destroy+ for more information.
+ #
+ # class Person < ActiveRecord::Base
+ # has_many :pets, dependent: :destroy
+ # end
+ #
+ # person.pets.size # => 3
+ # person.pets
+ # # => [
+ # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
+ # # #<Pet id: 2, name: "Spook", person_id: 1>,
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
+ # # ]
+ #
+ # person.pets.delete(Pet.find(1), Pet.find(3))
+ # # => [
+ # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
+ # # ]
+ #
+ # person.pets.size # => 1
+ # person.pets
+ # # => [#<Pet id: 2, name: "Spook", person_id: 1>]
+ #
+ # Pet.find(1, 3)
+ # # => ActiveRecord::RecordNotFound: Couldn't find all Pets with IDs (1, 3)
+ #
+ # If it is set to <tt>:delete_all</tt>, all the +records+ are deleted
+ # *without* calling their +destroy+ method.
+ #
+ # class Person < ActiveRecord::Base
+ # has_many :pets, dependent: :delete_all
+ # end
+ #
+ # person.pets.size # => 3
+ # person.pets
+ # # => [
+ # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
+ # # #<Pet id: 2, name: "Spook", person_id: 1>,
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
+ # # ]
+ #
+ # person.pets.delete(Pet.find(1))
+ # # => [#<Pet id: 1, name: "Fancy-Fancy", person_id: 1>]
+ #
+ # person.pets.size # => 2
+ # person.pets
+ # # => [
+ # # #<Pet id: 2, name: "Spook", person_id: 1>,
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
+ # # ]
+ #
+ # Pet.find(1)
+ # # => ActiveRecord::RecordNotFound: Couldn't find Pet with id=1
+ #
+ # You can pass +Fixnum+ or +String+ values, it finds the records
+ # responding to the +id+ and executes delete on them.
+ #
+ # class Person < ActiveRecord::Base
+ # has_many :pets
+ # end
+ #
+ # person.pets.size # => 3
+ # person.pets
+ # # => [
+ # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
+ # # #<Pet id: 2, name: "Spook", person_id: 1>,
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
+ # # ]
+ #
+ # person.pets.delete("1")
+ # # => [#<Pet id: 1, name: "Fancy-Fancy", person_id: 1>]
+ #
+ # person.pets.delete(2, 3)
+ # # => [
+ # # #<Pet id: 2, name: "Spook", person_id: 1>,
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
+ # # ]
+ def delete(*records)
+ @association.delete(*records)
+ end
+
+ ##
+ # Destroys the +records+ supplied and removes them from the collection.
+ # This method will _always_ remove record from the database ignoring
+ # the +:dependent+ option. Returns an array with the removed records.
+ #
+ # class Person < ActiveRecord::Base
+ # has_many :pets
+ # end
+ #
+ # person.pets.size # => 3
+ # person.pets
+ # # => [
+ # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
+ # # #<Pet id: 2, name: "Spook", person_id: 1>,
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
+ # # ]
+ #
+ # person.pets.destroy(Pet.find(1))
+ # # => [#<Pet id: 1, name: "Fancy-Fancy", person_id: 1>]
+ #
+ # person.pets.size # => 2
+ # person.pets
+ # # => [
+ # # #<Pet id: 2, name: "Spook", person_id: 1>,
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
+ # # ]
+ #
+ # person.pets.destroy(Pet.find(2), Pet.find(3))
+ # # => [
+ # # #<Pet id: 2, name: "Spook", person_id: 1>,
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
+ # # ]
+ #
+ # person.pets.size # => 0
+ # person.pets # => []
+ #
+ # Pet.find(1, 2, 3) # => ActiveRecord::RecordNotFound: Couldn't find all Pets with IDs (1, 2, 3)
+ #
+ # You can pass +Fixnum+ or +String+ values, it finds the records
+ # responding to the +id+ and then deletes them from the database.
+ #
+ # person.pets.size # => 3
+ # person.pets
+ # # => [
+ # # #<Pet id: 4, name: "Benny", person_id: 1>,
+ # # #<Pet id: 5, name: "Brain", person_id: 1>,
+ # # #<Pet id: 6, name: "Boss", person_id: 1>
+ # # ]
+ #
+ # person.pets.destroy("4")
+ # # => #<Pet id: 4, name: "Benny", person_id: 1>
+ #
+ # person.pets.size # => 2
+ # person.pets
+ # # => [
+ # # #<Pet id: 5, name: "Brain", person_id: 1>,
+ # # #<Pet id: 6, name: "Boss", person_id: 1>
+ # # ]
+ #
+ # person.pets.destroy(5, 6)
+ # # => [
+ # # #<Pet id: 5, name: "Brain", person_id: 1>,
+ # # #<Pet id: 6, name: "Boss", person_id: 1>
+ # # ]
+ #
+ # person.pets.size # => 0
+ # person.pets # => []
+ #
+ # Pet.find(4, 5, 6) # => ActiveRecord::RecordNotFound: Couldn't find all Pets with IDs (4, 5, 6)
+ def destroy(*records)
+ @association.destroy(*records)
+ end
+
+ ##
+ # Specifies whether the records should be unique or not.
+ #
+ # class Person < ActiveRecord::Base
+ # has_many :pets
+ # end
+ #
+ # person.pets.select(:name)
+ # # => [
+ # # #<Pet name: "Fancy-Fancy">,
+ # # #<Pet name: "Fancy-Fancy">
+ # # ]
+ #
+ # person.pets.select(:name).uniq
+ # # => [#<Pet name: "Fancy-Fancy">]
+ def uniq
+ @association.uniq
+ end
+
+ ##
+ # Count all records using SQL.
+ #
+ # class Person < ActiveRecord::Base
+ # has_many :pets
+ # end
+ #
+ # person.pets.count # => 3
+ # person.pets
+ # # => [
+ # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
+ # # #<Pet id: 2, name: "Spook", person_id: 1>,
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
+ # # ]
+ def count(column_name = nil, options = {})
+ @association.count(column_name, options)
+ end
+
+ ##
+ # Returns the size of the collection. If the collection hasn't been loaded,
+ # it executes a <tt>SELECT COUNT(*)</tt> query.
+ #
+ # class Person < ActiveRecord::Base
+ # has_many :pets
+ # end
+ #
+ # person.pets.size # => 3
+ # # executes something like SELECT COUNT(*) FROM "pets" WHERE "pets"."person_id" = 1
+ #
+ # person.pets # This will execute a SELECT * FROM query
+ # # => [
+ # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
+ # # #<Pet id: 2, name: "Spook", person_id: 1>,
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
+ # # ]
+ #
+ # person.pets.size # => 3
+ # # Because the collection is already loaded, this will behave like
+ # # collection.size and no SQL count query is executed.
+ def size
+ @association.size
+ end
+
+ ##
+ # Returns the size of the collection calling +size+ on the target.
+ # If the collection has been already loaded, +length+ and +size+ are
+ # equivalent.
+ #
+ # class Person < ActiveRecord::Base
+ # has_many :pets
+ # end
+ #
+ # person.pets.length # => 3
+ # # executes something like SELECT "pets".* FROM "pets" WHERE "pets"."person_id" = 1
+ #
+ # # Because the collection is loaded, you can
+ # # call the collection with no additional queries:
+ # person.pets
+ # # => [
+ # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
+ # # #<Pet id: 2, name: "Spook", person_id: 1>,
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
+ # # ]
+ def length
+ @association.length
+ end
+
+ ##
+ # Returns +true+ if the collection is empty.
+ #
+ # class Person < ActiveRecord::Base
+ # has_many :pets
+ # end
+ #
+ # person.pets.count # => 1
+ # person.pets.empty? # => false
+ #
+ # person.pets.delete_all
+ #
+ # person.pets.count # => 0
+ # person.pets.empty? # => true
+ def empty?
+ @association.empty?
+ end
+
+ ##
+ # Returns +true+ if the collection is not empty.
+ #
+ # class Person < ActiveRecord::Base
+ # has_many :pets
+ # end
+ #
+ # person.pets.count # => 0
+ # person.pets.any? # => false
+ #
+ # person.pets << Pet.new(name: 'Snoop')
+ # person.pets.count # => 0
+ # person.pets.any? # => true
+ #
+ # You can also pass a block to define criteria. The behaviour
+ # is the same, it returns true if the collection based on the
+ # criteria is not empty.
+ #
+ # person.pets
+ # # => [#<Pet name: "Snoop", group: "dogs">]
+ #
+ # person.pets.any? do |pet|
+ # pet.group == 'cats'
+ # end
+ # # => false
+ #
+ # person.pets.any? do |pet|
+ # pet.group == 'dogs'
+ # end
+ # # => true
+ def any?(&block)
+ @association.any?(&block)
+ end
+
+ ##
+ # Returns true if the collection has more than one record.
+ # Equivalent to <tt>collection.size > 1</tt>.
+ #
+ # class Person < ActiveRecord::Base
+ # has_many :pets
+ # end
+ #
+ # person.pets.count #=> 1
+ # person.pets.many? #=> false
+ #
+ # person.pets << Pet.new(name: 'Snoopy')
+ # person.pets.count #=> 2
+ # person.pets.many? #=> true
+ #
+ # You can also pass a block to define criteria. The
+ # behaviour is the same, it returns true if the collection
+ # based on the criteria has more than one record.
+ #
+ # person.pets
+ # # => [
+ # # #<Pet name: "Gorby", group: "cats">,
+ # # #<Pet name: "Puff", group: "cats">,
+ # # #<Pet name: "Snoop", group: "dogs">
+ # # ]
+ #
+ # person.pets.many? do |pet|
+ # pet.group == 'dogs'
+ # end
+ # # => false
+ #
+ # person.pets.many? do |pet|
+ # pet.group == 'cats'
+ # end
+ # # => true
+ def many?(&block)
+ @association.many?(&block)
+ end
+
+ ##
+ # Returns +true+ if the given object is present in the collection.
+ #
+ # class Person < ActiveRecord::Base
+ # has_many :pets
+ # end
+ #
+ # person.pets # => [#<Pet id: 20, name: "Snoop">]
+ #
+ # person.pets.include?(Pet.find(20)) # => true
+ # person.pets.include?(Pet.find(21)) # => false
+ def include?(record)
+ @association.include?(record)
end
alias_method :new, :build
@@ -61,67 +840,143 @@ module ActiveRecord
@association
end
- def scoped
+ # We don't want this object to be put on the scoping stack, because
+ # that could create an infinite loop where we call an @association
+ # method, which gets the current scope, which is this object, which
+ # delegates to @association, and so on.
+ def scoping
+ @association.scope.scoping { yield }
+ end
+
+ # Returns a <tt>Relation</tt> object for the records in this association
+ def scope
association = @association
- association.scoped.extending do
+
+ @association.scope.extending! do
define_method(:proxy_association) { association }
end
end
- def respond_to?(name, include_private = false)
- super ||
- (load_target && target.respond_to?(name, include_private)) ||
- proxy_association.klass.respond_to?(name, include_private)
- end
-
- def method_missing(method, *args, &block)
- match = DynamicFinderMatch.match(method)
- if match && match.instantiator?
- send(:find_or_instantiator_by_attributes, match, match.attribute_names, *args) do |r|
- proxy_association.send :set_owner_attributes, r
- proxy_association.send :add_to_target, r
- yield(r) if block_given?
- end
-
- elsif target.respond_to?(method) || (!proxy_association.klass.respond_to?(method) && Class.respond_to?(method))
- if load_target
- if target.respond_to?(method)
- target.send(method, *args, &block)
- else
- begin
- super
- rescue NoMethodError => e
- raise e, e.message.sub(/ for #<.*$/, " via proxy for #{target}")
- end
- end
- end
-
- else
- scoped.readonly(nil).send(method, *args, &block)
- end
- end
+ # :nodoc:
+ alias spawn scope
- # Forwards <tt>===</tt> explicitly to the \target because the instance method
- # removal above doesn't catch it. Loads the \target if needed.
- def ===(other)
- other === load_target
+ # Equivalent to <tt>Array#==</tt>. Returns +true+ if the two arrays
+ # contain the same number of elements and if each element is equal
+ # to the corresponding element in the other array, otherwise returns
+ # +false+.
+ #
+ # class Person < ActiveRecord::Base
+ # has_many :pets
+ # end
+ #
+ # person.pets
+ # # => [
+ # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
+ # # #<Pet id: 2, name: "Spook", person_id: 1>
+ # # ]
+ #
+ # other = person.pets.to_ary
+ #
+ # person.pets == other
+ # # => true
+ #
+ # other = [Pet.new(id: 1), Pet.new(id: 2)]
+ #
+ # person.pets == other
+ # # => false
+ def ==(other)
+ load_target == other
end
+ # Returns a new array of objects from the collection. If the collection
+ # hasn't been loaded, it fetches the records from the database.
+ #
+ # class Person < ActiveRecord::Base
+ # has_many :pets
+ # end
+ #
+ # person.pets
+ # # => [
+ # # #<Pet id: 4, name: "Benny", person_id: 1>,
+ # # #<Pet id: 5, name: "Brain", person_id: 1>,
+ # # #<Pet id: 6, name: "Boss", person_id: 1>
+ # # ]
+ #
+ # other_pets = person.pets.to_ary
+ # # => [
+ # # #<Pet id: 4, name: "Benny", person_id: 1>,
+ # # #<Pet id: 5, name: "Brain", person_id: 1>,
+ # # #<Pet id: 6, name: "Boss", person_id: 1>
+ # # ]
+ #
+ # other_pets.replace([Pet.new(name: 'BooGoo')])
+ #
+ # other_pets
+ # # => [#<Pet id: nil, name: "BooGoo", person_id: 1>]
+ #
+ # person.pets
+ # # This is not affected by replace
+ # # => [
+ # # #<Pet id: 4, name: "Benny", person_id: 1>,
+ # # #<Pet id: 5, name: "Brain", person_id: 1>,
+ # # #<Pet id: 6, name: "Boss", person_id: 1>
+ # # ]
def to_ary
load_target.dup
end
alias_method :to_a, :to_ary
+ # Adds one or more +records+ to the collection by setting their foreign keys
+ # to the association‘s primary key. Returns +self+, so several appends may be
+ # chained together.
+ #
+ # class Person < ActiveRecord::Base
+ # has_many :pets
+ # end
+ #
+ # person.pets.size # => 0
+ # person.pets << Pet.new(name: 'Fancy-Fancy')
+ # person.pets << [Pet.new(name: 'Spook'), Pet.new(name: 'Choo-Choo')]
+ # person.pets.size # => 3
+ #
+ # person.id # => 1
+ # person.pets
+ # # => [
+ # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
+ # # #<Pet id: 2, name: "Spook", person_id: 1>,
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
+ # # ]
def <<(*records)
proxy_association.concat(records) && self
end
alias_method :push, :<<
+ # Equivalent to +delete_all+. The difference is that returns +self+, instead
+ # of an array with the deleted objects, so methods can be chained. See
+ # +delete_all+ for more information.
def clear
delete_all
self
end
+ # Reloads the collection from the database. Returns +self+.
+ # Equivalent to <tt>collection(true)</tt>.
+ #
+ # class Person < ActiveRecord::Base
+ # has_many :pets
+ # end
+ #
+ # person.pets # fetches pets from the database
+ # # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>]
+ #
+ # person.pets # uses the pets cache
+ # # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>]
+ #
+ # person.pets.reload # fetches pets from the database
+ # # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>]
+ #
+ # person.pets(true)  # fetches pets from the database
+ # # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>]
def reload
proxy_association.reload
self
diff --git a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb
index a4cea99372..93618721bb 100644
--- a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb
+++ b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb
@@ -5,7 +5,7 @@ module ActiveRecord
attr_reader :join_table
def initialize(owner, reflection)
- @join_table = Arel::Table.new(reflection.options[:join_table])
+ @join_table = Arel::Table.new(reflection.join_table)
super
end
@@ -32,10 +32,6 @@ module ActiveRecord
record
end
- # ActiveRecord::Relation#delete_all needs to support joins before we can use a
- # SQL-only implementation.
- alias delete_all_on_destroy delete_all
-
private
def count_records
@@ -44,13 +40,20 @@ module ActiveRecord
def delete_records(records, method)
if sql = options[:delete_sql]
+ records = load_target if records == :all
records.each { |record| owner.connection.delete(interpolate(sql, record)) }
else
- relation = join_table
- stmt = relation.where(relation[reflection.foreign_key].eq(owner.id).
- and(relation[reflection.association_foreign_key].in(records.map { |x| x.id }.compact))
- ).compile_delete
- owner.connection.delete stmt
+ relation = join_table
+ condition = relation[reflection.foreign_key].eq(owner.id)
+
+ unless records == :all
+ condition = condition.and(
+ relation[reflection.association_foreign_key]
+ .in(records.map { |x| x.id }.compact)
+ )
+ end
+
+ owner.connection.delete(relation.where(condition).compile_delete)
end
end
diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb
index 059e6c77bc..74864d271f 100644
--- a/activerecord/lib/active_record/associations/has_many_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_association.rb
@@ -7,6 +7,28 @@ module ActiveRecord
# is provided by its child HasManyThroughAssociation.
class HasManyAssociation < CollectionAssociation #:nodoc:
+ def handle_dependency
+ case options[:dependent]
+ when :restrict, :restrict_with_exception
+ raise ActiveRecord::DeleteRestrictionError.new(reflection.name) unless empty?
+
+ when :restrict_with_error
+ unless empty?
+ record = klass.human_attribute_name(reflection.name).downcase
+ owner.errors.add(:base, :"restrict_dependent_destroy.many", record: record)
+ false
+ end
+
+ else
+ if options[:dependent] == :destroy
+ # No point in executing the counter update since we're going to destroy the parent anyway
+ load_target.each(&:mark_for_destruction)
+ end
+
+ delete_all
+ end
+ end
+
def insert_record(record, validate = true, raise = false)
set_owner_attributes(record)
@@ -38,7 +60,7 @@ module ActiveRecord
elsif options[:counter_sql] || options[:finder_sql]
reflection.klass.count_by_sql(custom_counter_sql)
else
- scoped.count
+ scope.count
end
# If there's nothing in the database and @target has no new records
@@ -46,7 +68,7 @@ module ActiveRecord
# documented side-effect of the method that may avoid an extra SELECT.
@target ||= [] and loaded! if count == 0
- [options[:limit], count].compact.min
+ [association_scope.limit_value, count].compact.min
end
def has_cached_counter?(reflection = reflection)
@@ -89,8 +111,12 @@ module ActiveRecord
records.each { |r| r.destroy }
update_counter(-records.length) unless inverse_updates_counter_cache?
else
- keys = records.map { |r| r[reflection.association_primary_key] }
- scope = scoped.where(reflection.association_primary_key => keys)
+ 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)
+ end
if method == :delete_all
update_counter(-scope.delete_all)
diff --git a/activerecord/lib/active_record/associations/has_many_through_association.rb b/activerecord/lib/active_record/associations/has_many_through_association.rb
index 9657cb081d..c7d8a84a7e 100644
--- a/activerecord/lib/active_record/associations/has_many_through_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_through_association.rb
@@ -1,4 +1,3 @@
-require 'active_support/core_ext/object/blank'
module ActiveRecord
# = Active Record Has Many Through Association
@@ -38,6 +37,20 @@ module ActiveRecord
super
end
+ def concat_records(records)
+ ensure_not_nested
+
+ records = super
+
+ if owner.new_record? && records
+ records.flatten.each do |record|
+ build_through_record(record)
+ end
+ end
+
+ records
+ end
+
def insert_record(record, validate = true, raise = false)
ensure_not_nested
@@ -54,10 +67,6 @@ module ActiveRecord
record
end
- # ActiveRecord::Relation#delete_all needs to support joins before we can use a
- # SQL-only implementation.
- alias delete_all_on_destroy delete_all
-
private
def through_association
@@ -73,7 +82,9 @@ module ActiveRecord
# association
def build_through_record(record)
@through_records[record.object_id] ||= begin
- through_record = through_association.build(construct_join_attributes(record))
+ ensure_mutable
+
+ through_record = through_association.build
through_record.send("#{source_reflection.name}=", record)
through_record
end
@@ -85,10 +96,10 @@ module ActiveRecord
@through_records.delete(record.object_id)
end
- def build_record(attributes, options = {})
+ def build_record(attributes)
ensure_not_nested
- record = super(attributes, options)
+ record = super(attributes)
inverse = source_reflection.inverse_of
if inverse
@@ -124,7 +135,12 @@ module ActiveRecord
def delete_records(records, method)
ensure_not_nested
- scope = through_association.scoped.where(construct_join_attributes(*records))
+ # This is unoptimised; it will load all the target records
+ # even when we just want to delete everything.
+ records = load_target if records == :all
+
+ scope = through_association.scope
+ scope.where! construct_join_attributes(*records)
case method
when :destroy
@@ -168,7 +184,7 @@ module ActiveRecord
def find_target
return [] unless target_reflection_has_associated_record?
- scoped.all
+ scope.to_a
end
# NOTE - not sure that we can actually cope with inverses here
diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb
index 2131edbc20..06bead41de 100644
--- a/activerecord/lib/active_record/associations/has_one_association.rb
+++ b/activerecord/lib/active_record/associations/has_one_association.rb
@@ -1,26 +1,45 @@
-require 'active_support/core_ext/object/inclusion'
module ActiveRecord
# = Active Record Belongs To Has One Association
module Associations
class HasOneAssociation < SingularAssociation #:nodoc:
+
+ def handle_dependency
+ case options[:dependent]
+ when :restrict, :restrict_with_exception
+ raise ActiveRecord::DeleteRestrictionError.new(reflection.name) if load_target
+
+ when :restrict_with_error
+ if load_target
+ record = klass.human_attribute_name(reflection.name).downcase
+ owner.errors.add(:base, :"restrict_dependent_destroy.one", record: record)
+ false
+ end
+
+ else
+ delete
+ end
+ end
+
def replace(record, save = true)
raise_on_type_mismatch(record) if record
load_target
- reflection.klass.transaction do
- if target && target != record
- remove_target!(options[:dependent]) unless target.destroyed?
- end
+ # If target and record are nil, or target is equal to record,
+ # we don't need to have transaction.
+ if (target || record) && target != record
+ reflection.klass.transaction do
+ remove_target!(options[:dependent]) if target && !target.destroyed?
- if record
- set_owner_attributes(record)
- set_inverse_instance(record)
+ if record
+ set_owner_attributes(record)
+ set_inverse_instance(record)
- if owner.persisted? && save && !record.save
- nullify_owner_attributes(record)
- set_owner_attributes(target) if target
- raise RecordNotSaved, "Failed to save the new associated #{reflection.name}."
+ if owner.persisted? && save && !record.save
+ nullify_owner_attributes(record)
+ set_owner_attributes(target) if target
+ raise RecordNotSaved, "Failed to save the new associated #{reflection.name}."
+ end
end
end
end
@@ -36,7 +55,7 @@ module ActiveRecord
when :destroy
target.destroy
when :nullify
- target.update_attribute(reflection.foreign_key, nil)
+ target.update_columns(reflection.foreign_key => nil)
end
end
end
@@ -52,16 +71,19 @@ module ActiveRecord
end
def remove_target!(method)
- if method.in?([:delete, :destroy])
- target.send(method)
- else
- nullify_owner_attributes(target)
+ case method
+ when :delete
+ target.delete
+ when :destroy
+ target.destroy
+ else
+ nullify_owner_attributes(target)
- if target.persisted? && owner.persisted? && !target.save
- set_owner_attributes(target)
- raise RecordNotSaved, "Failed to remove the existing associated #{reflection.name}. " +
- "The record failed to save when after its foreign key was set to nil."
- end
+ if target.persisted? && owner.persisted? && !target.save
+ set_owner_attributes(target)
+ raise RecordNotSaved, "Failed to remove the existing associated #{reflection.name}. " +
+ "The record failed to save after its foreign key was set to nil."
+ end
end
end
diff --git a/activerecord/lib/active_record/associations/join_dependency/join_association.rb b/activerecord/lib/active_record/associations/join_dependency/join_association.rb
index 0d7d28e458..0d3b4dbab1 100644
--- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb
+++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb
@@ -92,14 +92,21 @@ module ActiveRecord
constraint = build_constraint(reflection, table, key, foreign_table, foreign_key)
- conditions = self.conditions[i].dup
- conditions << { reflection.type => foreign_klass.base_class.name } if reflection.type
+ scope_chain_items = scope_chain[i]
- conditions.each do |condition|
- condition = active_record.send(:sanitize_sql, interpolate(condition), table.table_alias || table.name)
- condition = Arel.sql(condition) unless condition.is_a?(Arel::Node)
+ if reflection.type
+ scope_chain_items += [
+ ActiveRecord::Relation.new(reflection.klass, table)
+ .where(reflection.type => foreign_klass.base_class.name)
+ ]
+ end
+
+ scope_chain_items.each do |item|
+ unless item.is_a?(Relation)
+ item = ActiveRecord::Relation.new(reflection.klass, table).instance_exec(self, &item)
+ end
- constraint = constraint.and(condition)
+ constraint = constraint.and(item.arel.constraints) unless item.arel.constraints.empty?
end
relation.from(join(table, constraint))
@@ -137,18 +144,8 @@ module ActiveRecord
table.table_alias || table.name
end
- def conditions
- @conditions ||= reflection.conditions.reverse
- end
-
- private
-
- def interpolate(conditions)
- if conditions.respond_to?(:to_proc)
- instance_eval(&conditions)
- else
- conditions
- end
+ def scope_chain
+ @scope_chain ||= reflection.scope_chain.reverse
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 2b1d888a9a..711f7b3ce1 100644
--- a/activerecord/lib/active_record/associations/join_dependency/join_part.rb
+++ b/activerecord/lib/active_record/associations/join_dependency/join_part.rb
@@ -54,7 +54,7 @@ module ActiveRecord
unless @column_names_with_alias
@column_names_with_alias = []
- ([primary_key] + (column_names - [primary_key])).each_with_index do |column_name, i|
+ ([primary_key] + (column_names - [primary_key])).compact.each_with_index do |column_name, i|
@column_names_with_alias << [column_name, "#{aliased_prefix}_r#{i}"]
end
end
diff --git a/activerecord/lib/active_record/associations/join_helper.rb b/activerecord/lib/active_record/associations/join_helper.rb
index cea6ad6944..5a41b40c8f 100644
--- a/activerecord/lib/active_record/associations/join_helper.rb
+++ b/activerecord/lib/active_record/associations/join_helper.rb
@@ -19,7 +19,7 @@ module ActiveRecord
if reflection.source_macro == :has_and_belongs_to_many
tables << alias_tracker.aliased_table_for(
- (reflection.source_reflection || reflection).options[:join_table],
+ (reflection.source_reflection || reflection).join_table,
table_alias_for(reflection, true)
)
end
diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb
index fafed94ff2..ce5bf15f10 100644
--- a/activerecord/lib/active_record/associations/preloader.rb
+++ b/activerecord/lib/active_record/associations/preloader.rb
@@ -12,7 +12,7 @@ module ActiveRecord
# and all of its books via a single query:
#
# SELECT * FROM authors
- # LEFT OUTER JOIN books ON authors.id = books.id
+ # LEFT OUTER JOIN books ON authors.id = books.author_id
# WHERE authors.name = 'Ken Akamatsu'
#
# However, this could result in many rows that contain redundant data. After
@@ -42,7 +42,7 @@ module ActiveRecord
autoload :HasAndBelongsToMany, 'active_record/associations/preloader/has_and_belongs_to_many'
autoload :BelongsTo, 'active_record/associations/preloader/belongs_to'
- attr_reader :records, :associations, :options, :model
+ attr_reader :records, :associations, :preload_scope, :model
# Eager loads the named associations for the given Active Record record(s).
#
@@ -78,15 +78,10 @@ module ActiveRecord
# [ :books, :author ]
# { :author => :avatar }
# [ :books, { :author => :avatar } ]
- #
- # +options+ contains options that will be passed to ActiveRecord::Base#find
- # (which is called under the hood for preloading records). But it is passed
- # only one level deep in the +associations+ argument, i.e. it's not passed
- # to the child associations when +associations+ is a Hash.
- def initialize(records, associations, options = {})
- @records = Array.wrap(records).compact.uniq
- @associations = Array.wrap(associations)
- @options = options
+ def initialize(records, associations, preload_scope = nil)
+ @records = Array.wrap(records).compact.uniq
+ @associations = Array.wrap(associations)
+ @preload_scope = preload_scope || Relation.new(nil, nil)
end
def run
@@ -110,7 +105,7 @@ module ActiveRecord
def preload_hash(association)
association.each do |parent, child|
- Preloader.new(records, parent, options).run
+ Preloader.new(records, parent, preload_scope).run
Preloader.new(records.map { |record| record.send(parent) }.flatten, child).run
end
end
@@ -125,7 +120,7 @@ module ActiveRecord
def preload_one(association)
grouped_records(association).each do |reflection, klasses|
klasses.each do |klass, records|
- preloader_for(reflection).new(klass, records, reflection, options).run
+ preloader_for(reflection).new(klass, records, reflection, preload_scope).run
end
end
end
diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb
index 253998fb23..cbf5e734ea 100644
--- a/activerecord/lib/active_record/associations/preloader/association.rb
+++ b/activerecord/lib/active_record/associations/preloader/association.rb
@@ -2,16 +2,16 @@ module ActiveRecord
module Associations
class Preloader
class Association #:nodoc:
- attr_reader :owners, :reflection, :preload_options, :model, :klass
-
- def initialize(klass, owners, reflection, preload_options)
- @klass = klass
- @owners = owners
- @reflection = reflection
- @preload_options = preload_options || {}
- @model = owners.first && owners.first.class
- @scoped = nil
- @owners_by_key = nil
+ attr_reader :owners, :reflection, :preload_scope, :model, :klass
+
+ def initialize(klass, owners, reflection, preload_scope)
+ @klass = klass
+ @owners = owners
+ @reflection = reflection
+ @preload_scope = preload_scope
+ @model = owners.first && owners.first.class
+ @scope = nil
+ @owners_by_key = nil
end
def run
@@ -24,12 +24,12 @@ module ActiveRecord
raise NotImplementedError
end
- def scoped
- @scoped ||= build_scope
+ def scope
+ @scope ||= build_scope
end
def records_for(ids)
- scoped.where(association_key.in(ids))
+ scope.where(association_key.in(ids))
end
def table
@@ -77,7 +77,7 @@ module ActiveRecord
# Some databases impose a limit on the number of ids in a list (in Oracle it's 1000)
# Make several smaller queries if necessary or make one query if the adapter supports it
sliced = owner_keys.each_slice(model.connection.in_clause_length || owner_keys.size)
- records = sliced.map { |slice| records_for(slice) }.flatten
+ records = sliced.map { |slice| records_for(slice).to_a }.flatten
end
# Each record may have multiple owners, and vice-versa
@@ -92,33 +92,29 @@ module ActiveRecord
records_by_owner
end
+ def reflection_scope
+ @reflection_scope ||= reflection.scope ? klass.unscoped.instance_exec(nil, &reflection.scope) : klass.unscoped
+ end
+
def build_scope
- scope = klass.scoped
+ scope = klass.unscoped
+ scope.default_scoped = true
- scope = scope.where(interpolate(options[:conditions]))
- scope = scope.where(interpolate(preload_options[:conditions]))
+ values = reflection_scope.values
+ preload_values = preload_scope.values
- scope = scope.select(preload_options[:select] || options[:select] || table[Arel.star])
- scope = scope.includes(preload_options[:include] || options[:include])
+ scope.where_values = Array(values[:where]) + Array(preload_values[:where])
+ scope.references_values = Array(values[:references]) + Array(preload_values[:references])
+
+ scope.select! preload_values[:select] || values[:select] || table[Arel.star]
+ scope.includes! preload_values[:includes] || values[:includes]
if options[:as]
- scope = scope.where(
- klass.table_name => {
- reflection.type => model.base_class.sti_name
- }
- )
+ scope.where!(klass.table_name => { reflection.type => model.base_class.sti_name })
end
scope
end
-
- def interpolate(conditions)
- if conditions.respond_to?(:to_proc)
- klass.send(:instance_eval, &conditions)
- else
- conditions
- end
- end
end
end
end
diff --git a/activerecord/lib/active_record/associations/preloader/collection_association.rb b/activerecord/lib/active_record/associations/preloader/collection_association.rb
index c248aeaaf6..e6cd35e7a1 100644
--- a/activerecord/lib/active_record/associations/preloader/collection_association.rb
+++ b/activerecord/lib/active_record/associations/preloader/collection_association.rb
@@ -6,7 +6,7 @@ module ActiveRecord
private
def build_scope
- super.order(preload_options[:order] || options[:order])
+ super.order(preload_scope.values[:order] || reflection_scope.values[:order])
end
def preload
diff --git a/activerecord/lib/active_record/associations/preloader/has_and_belongs_to_many.rb b/activerecord/lib/active_record/associations/preloader/has_and_belongs_to_many.rb
index b77b667219..8e8925f0a9 100644
--- a/activerecord/lib/active_record/associations/preloader/has_and_belongs_to_many.rb
+++ b/activerecord/lib/active_record/associations/preloader/has_and_belongs_to_many.rb
@@ -6,7 +6,7 @@ module ActiveRecord
def initialize(klass, records, reflection, preload_options)
super
- @join_table = Arel::Table.new(options[:join_table]).alias('t0')
+ @join_table = Arel::Table.new(reflection.join_table).alias('t0')
end
# Unlike the other associations, we want to get a raw array of rows so that we can
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 c6e9ede356..9a662d3f53 100644
--- a/activerecord/lib/active_record/associations/preloader/has_many_through.rb
+++ b/activerecord/lib/active_record/associations/preloader/has_many_through.rb
@@ -6,7 +6,7 @@ module ActiveRecord
def associated_records_by_owner
super.each do |owner, records|
- records.uniq! if options[:uniq]
+ records.uniq! if reflection_scope.uniq_value
end
end
end
diff --git a/activerecord/lib/active_record/associations/preloader/has_one.rb b/activerecord/lib/active_record/associations/preloader/has_one.rb
index 848448bb48..24728e9f01 100644
--- a/activerecord/lib/active_record/associations/preloader/has_one.rb
+++ b/activerecord/lib/active_record/associations/preloader/has_one.rb
@@ -14,7 +14,7 @@ module ActiveRecord
private
def build_scope
- super.order(preload_options[:order] || options[:order])
+ super.order(preload_scope.values[:order] || reflection_scope.values[:order])
end
end
diff --git a/activerecord/lib/active_record/associations/preloader/through_association.rb b/activerecord/lib/active_record/associations/preloader/through_association.rb
index ad6374d09a..1c1ba11c44 100644
--- a/activerecord/lib/active_record/associations/preloader/through_association.rb
+++ b/activerecord/lib/active_record/associations/preloader/through_association.rb
@@ -14,10 +14,7 @@ module ActiveRecord
def associated_records_by_owner
through_records = through_records_by_owner
- ActiveRecord::Associations::Preloader.new(
- through_records.values.flatten,
- source_reflection.name, options
- ).run
+ Preloader.new(through_records.values.flatten, source_reflection.name, reflection_scope).run
through_records.each do |owner, records|
records.map! { |r| r.send(source_reflection.name) }.flatten!
@@ -28,10 +25,7 @@ module ActiveRecord
private
def through_records_by_owner
- ActiveRecord::Associations::Preloader.new(
- owners, through_reflection.name,
- through_options
- ).run
+ Preloader.new(owners, through_reflection.name, through_scope).run
Hash[owners.map do |owner|
through_records = Array.wrap(owner.send(through_reflection.name))
@@ -45,21 +39,22 @@ module ActiveRecord
end]
end
- def through_options
- through_options = {}
+ def through_scope
+ through_scope = through_reflection.klass.unscoped
if options[:source_type]
- through_options[:conditions] = { reflection.foreign_type => options[:source_type] }
+ through_scope.where! reflection.foreign_type => options[:source_type]
else
- if options[:conditions]
- through_options[:include] = options[:include] || options[:source]
- through_options[:conditions] = options[:conditions]
+ unless reflection_scope.where_values.empty?
+ through_scope.includes_values = reflection_scope.values[:includes] || options[:source]
+ through_scope.where_values = reflection_scope.values[:where]
end
- through_options[:order] = options[:order]
+ through_scope.order! reflection_scope.values[:order]
+ through_scope.references! reflection_scope.values[:references]
end
- through_options
+ through_scope
end
end
end
diff --git a/activerecord/lib/active_record/associations/singular_association.rb b/activerecord/lib/active_record/associations/singular_association.rb
index a1a921bcb4..32f4557c28 100644
--- a/activerecord/lib/active_record/associations/singular_association.rb
+++ b/activerecord/lib/active_record/associations/singular_association.rb
@@ -17,16 +17,16 @@ module ActiveRecord
replace(record)
end
- def create(attributes = {}, options = {}, &block)
- create_record(attributes, options, &block)
+ def create(attributes = {}, &block)
+ create_record(attributes, &block)
end
- def create!(attributes = {}, options = {}, &block)
- create_record(attributes, options, true, &block)
+ def create!(attributes = {}, &block)
+ create_record(attributes, true, &block)
end
- def build(attributes = {}, options = {})
- record = build_record(attributes, options)
+ def build(attributes = {})
+ record = build_record(attributes)
yield(record) if block_given?
set_new_record(record)
record
@@ -35,11 +35,11 @@ module ActiveRecord
private
def create_scope
- scoped.scope_for_create.stringify_keys.except(klass.primary_key)
+ scope.scope_for_create.stringify_keys.except(klass.primary_key)
end
def find_target
- scoped.first.tap { |record| set_inverse_instance(record) }
+ scope.first.tap { |record| set_inverse_instance(record) }
end
# Implemented by subclasses
@@ -51,8 +51,8 @@ module ActiveRecord
replace(record)
end
- def create_record(attributes, options, raise_error = false)
- record = build_record(attributes, options)
+ def create_record(attributes, raise_error = false)
+ record = build_record(attributes)
yield(record) if block_given?
saved = record.save
set_new_record(record)
diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb
index f95e5337c2..b9e014735b 100644
--- a/activerecord/lib/active_record/associations/through_association.rb
+++ b/activerecord/lib/active_record/associations/through_association.rb
@@ -15,7 +15,7 @@ module ActiveRecord
scope = super
chain[1..-1].each do |reflection|
scope = scope.merge(
- reflection.klass.scoped.with_default_scope.
+ reflection.klass.all.with_default_scope.
except(:select, :create_with, :includes, :preload, :joins, :eager_load)
)
end
@@ -37,9 +37,7 @@ module ActiveRecord
# situation it is more natural for the user to just create or modify their join records
# directly as required.
def construct_join_attributes(*records)
- if source_reflection.macro != :belongs_to
- raise HasManyThroughCantAssociateThroughHasOneOrManyReflection.new(owner, reflection)
- end
+ ensure_mutable
join_attributes = {
source_reflection.foreign_key =>
@@ -64,7 +62,7 @@ module ActiveRecord
# properly support stale-checking for nested associations.
def stale_state
if through_reflection.macro == :belongs_to
- owner[through_reflection.foreign_key].to_s
+ owner[through_reflection.foreign_key] && owner[through_reflection.foreign_key].to_s
end
end
@@ -73,6 +71,12 @@ module ActiveRecord
!owner[through_reflection.foreign_key].nil?
end
+ def ensure_mutable
+ if source_reflection.macro != :belongs_to
+ raise HasManyThroughCantAssociateThroughHasOneOrManyReflection.new(owner, reflection)
+ end
+ end
+
def ensure_not_nested
if reflection.nested?
raise HasManyThroughNestedAssociationsAreReadonly.new(owner, reflection)
diff --git a/activerecord/lib/active_record/attribute_assignment.rb b/activerecord/lib/active_record/attribute_assignment.rb
index bf9fe70b31..af13b75a9d 100644
--- a/activerecord/lib/active_record/attribute_assignment.rb
+++ b/activerecord/lib/active_record/attribute_assignment.rb
@@ -1,114 +1,57 @@
-require 'active_support/concern'
module ActiveRecord
module AttributeAssignment
extend ActiveSupport::Concern
- include ActiveModel::MassAssignmentSecurity
+ include ActiveModel::DeprecatedMassAssignmentSecurity
+ include ActiveModel::ForbiddenAttributesProtection
- module ClassMethods
- private
-
- # The primary key and inheritance column can never be set by mass-assignment for security reasons.
- def attributes_protected_by_default
- default = [ primary_key, inheritance_column ]
- default << 'id' unless primary_key.eql? 'id'
- default
- end
- end
-
- # Allows you to set all the attributes at once by passing in a hash with keys
- # matching the attribute names (which again matches the column names).
- #
- # If any attributes are protected by either +attr_protected+ or
- # +attr_accessible+ then only settable attributes will be assigned.
- #
- # class User < ActiveRecord::Base
- # attr_protected :is_admin
- # end
- #
- # user = User.new
- # user.attributes = { :username => 'Phusion', :is_admin => true }
- # user.username # => "Phusion"
- # user.is_admin? # => false
- def attributes=(new_attributes)
- return unless new_attributes.is_a?(Hash)
-
- assign_attributes(new_attributes)
- end
-
- # Allows you to set all the attributes for a particular mass-assignment
- # security role by passing in a hash of attributes with keys matching
- # the attribute names (which again matches the column names) and the role
- # name using the :as option.
+ # Allows you to set all the attributes by passing in a hash of attributes with
+ # keys matching the attribute names (which again matches the column names).
#
- # To bypass mass-assignment security you can use the :without_protection => true
- # option.
- #
- # class User < ActiveRecord::Base
- # attr_accessible :name
- # attr_accessible :name, :is_admin, :as => :admin
- # end
- #
- # user = User.new
- # user.assign_attributes({ :name => 'Josh', :is_admin => true })
- # user.name # => "Josh"
- # user.is_admin? # => false
- #
- # user = User.new
- # user.assign_attributes({ :name => 'Josh', :is_admin => true }, :as => :admin)
- # user.name # => "Josh"
- # user.is_admin? # => true
- #
- # user = User.new
- # user.assign_attributes({ :name => 'Josh', :is_admin => true }, :without_protection => true)
- # user.name # => "Josh"
- # user.is_admin? # => true
- def assign_attributes(new_attributes, options = {})
- return unless new_attributes
-
- attributes = new_attributes.stringify_keys
- multi_parameter_attributes = []
+ # If the passed hash responds to <tt>permitted?</tt> method and the return value
+ # of this method is +false+ an <tt>ActiveModel::ForbiddenAttributesError</tt>
+ # exception is raised.
+ def assign_attributes(new_attributes)
+ return if new_attributes.blank?
+
+ attributes = new_attributes.stringify_keys
+ multi_parameter_attributes = []
nested_parameter_attributes = []
- @mass_assignment_options = options
- unless options[:without_protection]
- attributes = sanitize_for_mass_assignment(attributes, mass_assignment_role)
- end
+ attributes = sanitize_for_mass_assignment(attributes)
attributes.each do |k, v|
if k.include?("(")
multi_parameter_attributes << [ k, v ]
- elsif respond_to?("#{k}=")
- if v.is_a?(Hash)
- nested_parameter_attributes << [ k, v ]
- else
- send("#{k}=", v)
- end
+ elsif v.is_a?(Hash)
+ nested_parameter_attributes << [ k, v ]
else
- raise(UnknownAttributeError, "unknown attribute: #{k}")
+ _assign_attribute(k, v)
end
end
- # assign any deferred nested attributes after the base attributes have been set
- nested_parameter_attributes.each do |k,v|
- send("#{k}=", v)
- end
-
- @mass_assignment_options = nil
- assign_multiparameter_attributes(multi_parameter_attributes)
+ assign_nested_parameter_attributes(nested_parameter_attributes) unless nested_parameter_attributes.empty?
+ assign_multiparameter_attributes(multi_parameter_attributes) unless multi_parameter_attributes.empty?
end
- protected
+ alias attributes= assign_attributes
- def mass_assignment_options
- @mass_assignment_options ||= {}
- end
+ private
- def mass_assignment_role
- mass_assignment_options[:as] || :default
+ def _assign_attribute(k, v)
+ public_send("#{k}=", v)
+ rescue NoMethodError
+ if respond_to?("#{k}=")
+ raise
+ else
+ raise UnknownAttributeError, "unknown attribute: #{k}"
+ end
end
- private
+ # Assign any deferred nested attributes after the base attributes have been set.
+ def assign_nested_parameter_attributes(pairs)
+ pairs.each { |k, v| _assign_attribute(k, v) }
+ end
# Instantiates objects for all attribute classes that needs more than one constructor parameter. This is done
# by calling new on the column type or aggregation type (through composed_of) object with these parameters.
@@ -116,91 +59,34 @@ module ActiveRecord
# written_on (a date type) with Date.new("2004", "6", "24"). You can also specify a typecast character in the
# parentheses to have the parameters typecasted before they're used in the constructor. Use i for Fixnum,
# f for Float, s for String, and a for Array. If all the values for a given attribute are empty, the
- # attribute will be set to nil.
+ # attribute will be set to +nil+.
def assign_multiparameter_attributes(pairs)
execute_callstack_for_multiparameter_attributes(
extract_callstack_for_multiparameter_attributes(pairs)
)
end
- def instantiate_time_object(name, values)
- if self.class.send(:create_time_zone_conversion_attribute?, name, column_for_attribute(name))
- Time.zone.local(*values)
- else
- Time.time_with_datetime_fallback(self.class.default_timezone, *values)
- end
- end
-
def execute_callstack_for_multiparameter_attributes(callstack)
errors = []
callstack.each do |name, values_with_empty_parameters|
begin
- send(name + "=", read_value_from_parameter(name, values_with_empty_parameters))
+ send("#{name}=", MultiparameterAttribute.new(self, name, values_with_empty_parameters).read_value)
rescue => ex
- errors << AttributeAssignmentError.new("error on assignment #{values_with_empty_parameters.values.inspect} to #{name}", ex, name)
+ errors << AttributeAssignmentError.new("error on assignment #{values_with_empty_parameters.values.inspect} to #{name} (#{ex.message})", ex, name)
end
end
unless errors.empty?
- raise MultiparameterAssignmentErrors.new(errors), "#{errors.size} error(s) on assignment of multiparameter attributes"
- end
- end
-
- def read_value_from_parameter(name, values_hash_from_param)
- klass = (self.class.reflect_on_aggregation(name.to_sym) || column_for_attribute(name)).klass
- if values_hash_from_param.values.all?{|v|v.nil?}
- nil
- elsif klass == Time
- read_time_parameter_value(name, values_hash_from_param)
- elsif klass == Date
- read_date_parameter_value(name, values_hash_from_param)
- else
- read_other_parameter_value(klass, name, values_hash_from_param)
- end
- end
-
- def read_time_parameter_value(name, values_hash_from_param)
- # If Date bits were not provided, error
- raise "Missing Parameter" if [1,2,3].any?{|position| !values_hash_from_param.has_key?(position)}
- max_position = extract_max_param_for_multiparameter_attributes(values_hash_from_param, 6)
- # If Date bits were provided but blank, then return nil
- return nil if (1..3).any? {|position| values_hash_from_param[position].blank?}
-
- set_values = (1..max_position).collect{|position| values_hash_from_param[position] }
- # If Time bits are not there, then default to 0
- (3..5).each {|i| set_values[i] = set_values[i].blank? ? 0 : set_values[i]}
- instantiate_time_object(name, set_values)
- end
-
- def read_date_parameter_value(name, values_hash_from_param)
- return nil if (1..3).any? {|position| values_hash_from_param[position].blank?}
- set_values = [values_hash_from_param[1], values_hash_from_param[2], values_hash_from_param[3]]
- begin
- Date.new(*set_values)
- rescue ArgumentError # if Date.new raises an exception on an invalid date
- instantiate_time_object(name, set_values).to_date # we instantiate Time object and convert it back to a date thus using Time's logic in handling invalid dates
- end
- end
-
- def read_other_parameter_value(klass, name, values_hash_from_param)
- max_position = extract_max_param_for_multiparameter_attributes(values_hash_from_param)
- values = (1..max_position).collect do |position|
- raise "Missing Parameter" if !values_hash_from_param.has_key?(position)
- values_hash_from_param[position]
+ error_descriptions = errors.map { |ex| ex.message }.join(",")
+ raise MultiparameterAssignmentErrors.new(errors), "#{errors.size} error(s) on assignment of multiparameter attributes [#{error_descriptions}]"
end
- klass.new(*values)
- end
-
- def extract_max_param_for_multiparameter_attributes(values_hash_from_param, upper_cap = 100)
- [values_hash_from_param.keys.max,upper_cap].min
end
def extract_callstack_for_multiparameter_attributes(pairs)
attributes = { }
- pairs.each do |pair|
- multiparameter_name, value = pair
+ pairs.each do |(multiparameter_name, value)|
attribute_name = multiparameter_name.split("(").first
- attributes[attribute_name] = {} unless attributes.include?(attribute_name)
+ attributes[attribute_name] ||= {}
parameter_value = value.empty? ? nil : type_cast_attribute_value(multiparameter_name, value)
attributes[attribute_name][find_parameter_position(multiparameter_name)] ||= parameter_value
@@ -217,5 +103,100 @@ module ActiveRecord
multiparameter_name.scan(/\(([0-9]*).*\)/).first.first.to_i
end
+ class MultiparameterAttribute #:nodoc:
+ attr_reader :object, :name, :values, :column
+
+ def initialize(object, name, values)
+ @object = object
+ @name = name
+ @values = values
+ end
+
+ def read_value
+ return if values.values.compact.empty?
+
+ @column = object.class.reflect_on_aggregation(name.to_sym) || object.column_for_attribute(name)
+ klass = column.klass
+
+ if klass == Time
+ read_time
+ elsif klass == Date
+ read_date
+ else
+ read_other(klass)
+ end
+ end
+
+ private
+
+ def instantiate_time_object(set_values)
+ if object.class.send(:create_time_zone_conversion_attribute?, name, column)
+ Time.zone.local(*set_values)
+ else
+ Time.time_with_datetime_fallback(object.class.default_timezone, *set_values)
+ end
+ end
+
+ def read_time
+ # If column is a :time (and not :date or :timestamp) there is no need to validate if
+ # there are year/month/day fields
+ if column.type == :time
+ # if the column is a time set the values to their defaults as January 1, 1970, but only if they're nil
+ { 1 => 1970, 2 => 1, 3 => 1 }.each do |key,value|
+ values[key] ||= value
+ end
+ else
+ # else column is a timestamp, so if Date bits were not provided, error
+ validate_missing_parameters!([1,2,3])
+
+ # If Date bits were provided but blank, then return nil
+ return if blank_date_parameter?
+ end
+
+ max_position = extract_max_param(6)
+ set_values = values.values_at(*(1..max_position))
+ # If Time bits are not there, then default to 0
+ (3..5).each { |i| set_values[i] = set_values[i].presence || 0 }
+ instantiate_time_object(set_values)
+ end
+
+ def read_date
+ return if blank_date_parameter?
+ set_values = values.values_at(1,2,3)
+ begin
+ Date.new(*set_values)
+ rescue ArgumentError # if Date.new raises an exception on an invalid date
+ instantiate_time_object(set_values).to_date # we instantiate Time object and convert it back to a date thus using Time's logic in handling invalid dates
+ end
+ end
+
+ def read_other(klass)
+ max_position = extract_max_param
+ positions = (1..max_position)
+ validate_missing_parameters!(positions)
+
+ set_values = values.values_at(*positions)
+ klass.new(*set_values)
+ end
+
+ # Checks whether some blank date parameter exists. Note that this is different
+ # than the validate_missing_parameters! method, since it just checks for blank
+ # positions instead of missing ones, and does not raise in case one blank position
+ # exists. The caller is responsible to handle the case of this returning true.
+ def blank_date_parameter?
+ (1..3).any? { |position| values[position].blank? }
+ end
+
+ # If some position is not provided, it errors out a missing parameter exception.
+ def validate_missing_parameters!(positions)
+ if missing_parameter = positions.detect { |position| !values.key?(position) }
+ raise ArgumentError.new("Missing Parameter - #{name}(#{missing_parameter})")
+ end
+ end
+
+ def extract_max_param(upper_cap = 100)
+ [values.keys.max, upper_cap].min
+ end
+ end
end
end
diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb
index 3e27e85f02..ced15bc330 100644
--- a/activerecord/lib/active_record/attribute_methods.rb
+++ b/activerecord/lib/active_record/attribute_methods.rb
@@ -1,5 +1,4 @@
require 'active_support/core_ext/enumerable'
-require 'active_support/deprecation'
module ActiveRecord
# = Active Record Attribute Methods
@@ -16,19 +15,6 @@ module ActiveRecord
include TimeZoneConversion
include Dirty
include Serialization
-
- # Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example,
- # "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)).
- # (Alias for the protected read_attribute method).
- def [](attr_name)
- read_attribute(attr_name)
- end
-
- # Updates the attribute identified by <tt>attr_name</tt> with the specified +value+.
- # (Alias for the protected write_attribute method).
- def []=(attr_name, value)
- write_attribute(attr_name, value)
- end
end
module ClassMethods
@@ -49,14 +35,6 @@ module ActiveRecord
@attribute_methods_generated ||= false
end
- # We will define the methods as instance methods, but will call them as singleton
- # methods. This allows us to use method_defined? to check if the method exists,
- # which is fast and won't give any false positives from the ancestors (because
- # there are no ancestors).
- def generated_external_attribute_methods
- @generated_external_attribute_methods ||= Module.new { extend self }
- end
-
def undefine_attribute_methods
super if attribute_methods_generated?
@attribute_methods_generated = false
@@ -157,7 +135,9 @@ module ActiveRecord
# Returns a hash of all the attributes with their names as keys and the values of the attributes as values.
def attributes
- Hash[@attributes.map { |name, _| [name, read_attribute(name)] }]
+ attribute_names.each_with_object({}) { |name, attrs|
+ attrs[name] = read_attribute(name)
+ }
end
# Returns an <tt>#inspect</tt>-like string for the value of the
@@ -189,7 +169,7 @@ module ActiveRecord
# nil nor empty? (the latter only applies to objects that respond to empty?, most notably Strings).
def attribute_present?(attribute)
value = read_attribute(attribute)
- !value.nil? || (value.respond_to?(:empty?) && !value.empty?)
+ !value.nil? && !(value.respond_to?(:empty?) && value.empty?)
end
# Returns the column object for the named attribute.
@@ -198,6 +178,19 @@ module ActiveRecord
self.class.columns_hash[name.to_s]
end
+ # Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example,
+ # "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)).
+ # (Alias for the protected read_attribute method).
+ def [](attr_name)
+ read_attribute(attr_name)
+ end
+
+ # Updates the attribute identified by <tt>attr_name</tt> with the specified +value+.
+ # (Alias for the protected write_attribute method).
+ def []=(attr_name, value)
+ write_attribute(attr_name, value)
+ end
+
protected
def clone_attributes(reader_method = :read_attribute, attributes = {})
@@ -214,37 +207,64 @@ module ActiveRecord
value
end
- # Returns a copy of the attributes hash where all the values have been safely quoted for use in
- # an Arel insert/update method.
- def arel_attributes_values(include_primary_key = true, include_readonly_attributes = true, attribute_names = @attributes.keys)
- attrs = {}
- klass = self.class
- arel_table = klass.arel_table
+ def arel_attributes_with_values_for_create(pk_attribute_allowed)
+ arel_attributes_with_values(attributes_for_create(pk_attribute_allowed))
+ end
- attribute_names.each do |name|
- if (column = column_for_attribute(name)) && (include_primary_key || !column.primary)
+ def arel_attributes_with_values_for_update(attribute_names)
+ arel_attributes_with_values(attributes_for_update(attribute_names))
+ end
- if include_readonly_attributes || !self.class.readonly_attributes.include?(name)
+ def attribute_method?(attr_name)
+ defined?(@attributes) && @attributes.include?(attr_name)
+ end
- value = if klass.serialized_attributes.include?(name)
- @attributes[name].serialized_value
- else
- # FIXME: we need @attributes to be used consistently.
- # If the values stored in @attributes were already type
- # casted, this code could be simplified
- read_attribute(name)
- end
+ private
- attrs[arel_table[name]] = value
- end
- end
- end
+ # Returns a Hash of the Arel::Attributes and attribute values that have been
+ # type casted for use in an Arel insert/update method.
+ def arel_attributes_with_values(attribute_names)
+ attrs = {}
+ arel_table = self.class.arel_table
+ attribute_names.each do |name|
+ attrs[arel_table[name]] = typecasted_attribute_value(name)
+ end
attrs
end
- def attribute_method?(attr_name)
- defined?(@attributes) && @attributes.include?(attr_name)
+ # Filters the primary keys and readonly attributes from the attribute names.
+ def attributes_for_update(attribute_names)
+ attribute_names.select do |name|
+ column_for_attribute(name) && !pk_attribute?(name) && !readonly_attribute?(name)
+ end
+ end
+
+ # Filters out the primary keys, from the attribute names, when the primary
+ # key is to be generated (e.g. the id attribute has no value).
+ def attributes_for_create(pk_attribute_allowed)
+ @attributes.keys.select do |name|
+ column_for_attribute(name) && (pk_attribute_allowed || !pk_attribute?(name))
+ end
+ end
+
+ def readonly_attribute?(name)
+ self.class.readonly_attributes.include?(name)
+ end
+
+ def pk_attribute?(name)
+ column_for_attribute(name).primary
+ end
+
+ def typecasted_attribute_value(name)
+ if self.class.serialized_attributes.include?(name)
+ @attributes[name].serialized_value
+ else
+ # FIXME: we need @attributes to be used consistently.
+ # If the values stored in @attributes were already typecasted, this code
+ # could be simplified
+ read_attribute(name)
+ end
end
end
end
diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb
index 433d508357..60e5b0e2bb 100644
--- a/activerecord/lib/active_record/attribute_methods/dirty.rb
+++ b/activerecord/lib/active_record/attribute_methods/dirty.rb
@@ -1,12 +1,16 @@
-require 'active_support/core_ext/class/attribute'
-require 'active_support/core_ext/object/blank'
+require 'active_support/core_ext/module/attribute_accessors'
module ActiveRecord
+ ActiveSupport.on_load(:active_record_config) do
+ mattr_accessor :partial_updates, instance_accessor: false
+ self.partial_updates = true
+ end
+
module AttributeMethods
module Dirty
extend ActiveSupport::Concern
+
include ActiveModel::Dirty
- include AttributeMethods::Write
included do
if self < ::ActiveRecord::Timestamp
@@ -14,7 +18,6 @@ module ActiveRecord
end
config_attribute :partial_updates
- self.partial_updates = true
end
# Attempts to +save+ the record and clears changed attributes if successful.
@@ -22,8 +25,6 @@ module ActiveRecord
if status = super
@previously_changed = changes
@changed_attributes.clear
- elsif IdentityMap.enabled?
- IdentityMap.remove(self)
end
status
end
@@ -34,9 +35,6 @@ module ActiveRecord
@previously_changed = changes
@changed_attributes.clear
end
- rescue
- IdentityMap.remove(self) if IdentityMap.enabled?
- raise
end
# <tt>reload</tt> the record and clears changed attributes.
@@ -58,8 +56,6 @@ module ActiveRecord
@changed_attributes.delete(attr) unless _field_changed?(attr, old, value)
else
old = clone_attribute_value(:read_attribute, attr)
- # Save Time objects as TimeWithZone if time_zone_aware_attributes == true
- old = old.in_time_zone if clone_with_time_zone_conversion_attribute?(attr, old)
@changed_attributes[attr] = old if _field_changed?(attr, old, value)
end
@@ -79,11 +75,8 @@ module ActiveRecord
def _field_changed?(attr, old, value)
if column = column_for_attribute(attr)
- if column.number? && column.null && (old.nil? || old == 0) && value.blank?
- # For nullable numeric columns, NULL gets stored in database for blank (i.e. '') values.
- # Hence we don't record it as a change if the value changes from nil to ''.
- # If an old value of 0 is set to '' we want this to get changed to nil as otherwise it'll
- # be typecast back to 0 (''.to_i => 0)
+ if column.number? && (changes_from_nil_to_empty_string?(column, old, value) ||
+ changes_from_zero_to_string?(old, value))
value = nil
else
value = column.type_cast(value)
@@ -93,8 +86,17 @@ module ActiveRecord
old != value
end
- def clone_with_time_zone_conversion_attribute?(attr, old)
- old.class.name == "Time" && time_zone_aware_attributes && !self.skip_time_zone_conversion_for_attributes.include?(attr.to_sym)
+ def changes_from_nil_to_empty_string?(column, old, value)
+ # For nullable numeric columns, NULL gets stored in database for blank (i.e. '') values.
+ # Hence we don't record it as a change if the value changes from nil to ''.
+ # If an old value of 0 is set to '' we want this to get changed to nil as otherwise it'll
+ # be typecast back to 0 (''.to_i => 0)
+ column.null && (old.nil? || old == 0) && value.blank?
+ end
+
+ def changes_from_zero_to_string?(old, value)
+ # For columns with old 0 and value non-empty string
+ old == 0 && value.is_a?(String) && value.present? && value != '0'
end
end
end
diff --git a/activerecord/lib/active_record/attribute_methods/primary_key.rb b/activerecord/lib/active_record/attribute_methods/primary_key.rb
index 8e0b3e1402..aa6704d5c9 100644
--- a/activerecord/lib/active_record/attribute_methods/primary_key.rb
+++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb
@@ -18,7 +18,7 @@ module ActiveRecord
# Sets the primary key value
def id=(value)
- write_attribute(self.class.primary_key, value)
+ write_attribute(self.class.primary_key, value) if self.class.primary_key
end
# Queries the primary key value
@@ -43,12 +43,6 @@ module ActiveRecord
if attr_name == primary_key && attr_name != 'id'
generated_attribute_methods.send(:alias_method, :id, primary_key)
- generated_external_attribute_methods.module_eval <<-CODE, __FILE__, __LINE__
- def id(v, attributes, attributes_cache, attr_name)
- attr_name = '#{primary_key}'
- send(attr_name, attributes[attr_name], attributes, attributes_cache, attr_name)
- end
- CODE
end
end
@@ -59,8 +53,7 @@ module ActiveRecord
end
# Defines the primary key field -- can be overridden in subclasses. Overwriting will negate any effect of the
- # primary_key_prefix_type setting, though. Since primary keys are usually protected from mass assignment,
- # remember to let your database generate them or include the key in +attr_accessible+.
+ # primary_key_prefix_type setting, though.
def primary_key
@primary_key = reset_primary_key unless defined? @primary_key
@primary_key
@@ -111,9 +104,8 @@ module ActiveRecord
# end
# Project.primary_key # => "foo_id"
def primary_key=(value)
- @original_primary_key = @primary_key if defined?(@primary_key)
- @primary_key = value && value.to_s
- @quoted_primary_key = nil
+ @primary_key = value && value.to_s
+ @quoted_primary_key = nil
end
end
end
diff --git a/activerecord/lib/active_record/attribute_methods/query.rb b/activerecord/lib/active_record/attribute_methods/query.rb
index 1e841dc8e0..0f9723febb 100644
--- a/activerecord/lib/active_record/attribute_methods/query.rb
+++ b/activerecord/lib/active_record/attribute_methods/query.rb
@@ -1,5 +1,3 @@
-require 'active_support/core_ext/object/blank'
-
module ActiveRecord
module AttributeMethods
module Query
@@ -10,7 +8,7 @@ module ActiveRecord
end
def query_attribute(attr_name)
- value = read_attribute(attr_name)
+ value = read_attribute(attr_name) { |n| missing_attribute(n, caller) }
case value
when true then true
diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb
index 846ac03d82..1a4cb25dd7 100644
--- a/activerecord/lib/active_record/attribute_methods/read.rb
+++ b/activerecord/lib/active_record/attribute_methods/read.rb
@@ -1,4 +1,8 @@
module ActiveRecord
+ ActiveSupport.on_load(:active_record_config) do
+ mattr_accessor :attribute_types_cached_by_default, instance_accessor: false
+ end
+
module AttributeMethods
module Read
extend ActiveSupport::Concern
@@ -6,8 +10,7 @@ module ActiveRecord
ATTRIBUTE_TYPES_CACHED_BY_DEFAULT = [:datetime, :timestamp, :time, :date]
included do
- config_attribute :attribute_types_cached_by_default, :global => true
- self.attribute_types_cached_by_default = ATTRIBUTE_TYPES_CACHED_BY_DEFAULT
+ config_attribute :attribute_types_cached_by_default
end
module ClassMethods
@@ -29,35 +32,8 @@ module ActiveRecord
cached_attributes.include?(attr_name)
end
- def undefine_attribute_methods
- generated_external_attribute_methods.module_eval do
- instance_methods.each { |m| undef_method(m) }
- end
-
- super
- end
-
- def type_cast_attribute(attr_name, attributes, cache = {}) #:nodoc:
- return unless attr_name
- attr_name = attr_name.to_s
-
- if generated_external_attribute_methods.method_defined?(attr_name)
- if attributes.has_key?(attr_name) || attr_name == 'id'
- generated_external_attribute_methods.send(attr_name, attributes[attr_name], attributes, cache, attr_name)
- end
- elsif !attribute_methods_generated?
- # If we haven't generated the caster methods yet, do that and
- # then try again
- define_attribute_methods
- type_cast_attribute(attr_name, attributes, cache)
- else
- # If we get here, the attribute has no associated DB column, so
- # just return it verbatim.
- attributes[attr_name]
- end
- end
-
protected
+
# We want to generate the methods via module_eval rather than define_method,
# because define_method is slower on dispatch and uses more memory (because it
# creates a closure).
@@ -67,19 +43,9 @@ module ActiveRecord
# we first define with the __temp__ identifier, and then use alias method to
# rename it to what we want.
def define_method_attribute(attr_name)
- cast_code = attribute_cast_code(attr_name)
-
generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
def __temp__
- #{internal_attribute_access_code(attr_name, cast_code)}
- end
- alias_method '#{attr_name}', :__temp__
- undef_method :__temp__
- STR
-
- generated_external_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
- def __temp__(v, attributes, attributes_cache, attr_name)
- #{external_attribute_access_code(attr_name, cast_code)}
+ read_attribute(:'#{attr_name}') { |n| missing_attribute(n, caller) }
end
alias_method '#{attr_name}', :__temp__
undef_method :__temp__
@@ -87,6 +53,7 @@ module ActiveRecord
end
private
+
def cacheable_column?(column)
if attribute_types_cached_by_default == ATTRIBUTE_TYPES_CACHED_BY_DEFAULT
! serialized_attributes.include? column.name
@@ -94,33 +61,27 @@ module ActiveRecord
attribute_types_cached_by_default.include?(column.type)
end
end
-
- def internal_attribute_access_code(attr_name, cast_code)
- "read_attribute('#{attr_name}') { |n| missing_attribute(n, caller) }"
- end
-
- def external_attribute_access_code(attr_name, cast_code)
- access_code = "v && #{cast_code}"
-
- if cache_attribute?(attr_name)
- access_code = "attributes_cache[attr_name] ||= (#{access_code})"
- end
-
- access_code
- end
-
- def attribute_cast_code(attr_name)
- columns_hash[attr_name].type_cast_code('v')
- end
end
+ ActiveRecord::Model.attribute_types_cached_by_default = ATTRIBUTE_TYPES_CACHED_BY_DEFAULT
+
# Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example,
# "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)).
def read_attribute(attr_name)
+ return unless attr_name
+ name_sym = attr_name.to_sym
+
# If it's cached, just return it
- @attributes_cache.fetch(attr_name) { |name|
+ # We use #[] first as a perf optimization for non-nil values. See https://gist.github.com/3552829.
+ @attributes_cache[name_sym] || @attributes_cache.fetch(name_sym) {
+ name = attr_name.to_s
+
column = @columns_hash.fetch(name) {
- return self.class.type_cast_attribute(name, @attributes, @attributes_cache)
+ return @attributes.fetch(name) {
+ if name_sym == :id && self.class.primary_key != name
+ read_attribute(self.class.primary_key)
+ end
+ }
}
value = @attributes.fetch(name) {
@@ -128,7 +89,7 @@ module ActiveRecord
}
if self.class.cache_attribute?(name)
- @attributes_cache[name] = column.type_cast(value)
+ @attributes_cache[name_sym] = column.type_cast(value)
else
column.type_cast value
end
diff --git a/activerecord/lib/active_record/attribute_methods/serialization.rb b/activerecord/lib/active_record/attribute_methods/serialization.rb
index 165785c8fb..bdda5bc009 100644
--- a/activerecord/lib/active_record/attribute_methods/serialization.rb
+++ b/activerecord/lib/active_record/attribute_methods/serialization.rb
@@ -6,10 +6,46 @@ module ActiveRecord
included do
# Returns a hash of all the attributes that have been specified for serialization as
# keys and their class restriction as values.
- config_attribute :serialized_attributes
+ class_attribute :serialized_attributes, instance_accessor: false
self.serialized_attributes = {}
end
+ module ClassMethods
+ # 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 serialization is done through YAML. If +class_name+ is specified, the serialized object must be of that
+ # class on retrieval or SerializationTypeMismatch will be raised.
+ #
+ # ==== Parameters
+ #
+ # * +attr_name+ - The field name that should be serialized.
+ # * +class_name+ - Optional, class name that the object type should be equal to.
+ #
+ # ==== Example
+ # # Serialize a preferences attribute
+ # class User < ActiveRecord::Base
+ # serialize :preferences
+ # end
+ def serialize(attr_name, class_name = Object)
+ include Behavior
+
+ coder = if [:load, :dump].all? { |x| class_name.respond_to?(x) }
+ class_name
+ else
+ Coders::YAMLColumn.new(class_name)
+ end
+
+ # merge new serialized attribute and create new hash to ensure that each class in inheritance hierarchy
+ # has its own hash of own serialized attributes
+ self.serialized_attributes = serialized_attributes.merge(attr_name.to_s => coder)
+ end
+ end
+
+ def serialized_attributes
+ ActiveSupport::Deprecation.warn("Instance level serialized_attributes method is deprecated, please use class level method.")
+ defined?(@serialized_attributes) ? @serialized_attributes : self.class.serialized_attributes
+ end
+
class Type # :nodoc:
def initialize(column)
@column = column
@@ -44,70 +80,50 @@ module ActiveRecord
end
end
- module ClassMethods
- # 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 serialization is done through YAML. If +class_name+ is specified, the serialized object must be of that
- # class on retrieval or SerializationTypeMismatch will be raised.
- #
- # ==== Parameters
- #
- # * +attr_name+ - The field name that should be serialized.
- # * +class_name+ - Optional, class name that the object type should be equal to.
- #
- # ==== Example
- # # Serialize a preferences attribute
- # class User < ActiveRecord::Base
- # serialize :preferences
- # end
- def serialize(attr_name, class_name = Object)
- coder = if [:load, :dump].all? { |x| class_name.respond_to?(x) }
- class_name
- else
- Coders::YAMLColumn.new(class_name)
- end
+ # This is only added to the model when serialize is called, which
+ # ensures we do not make things slower when serialization is not used.
+ module Behavior #:nodoc:
+ extend ActiveSupport::Concern
- # merge new serialized attribute and create new hash to ensure that each class in inheritance hierarchy
- # has its own hash of own serialized attributes
- self.serialized_attributes = serialized_attributes.merge(attr_name.to_s => coder)
- end
-
- def initialize_attributes(attributes) #:nodoc:
- super
+ module ClassMethods
+ def initialize_attributes(attributes, options = {})
+ serialized = (options.delete(:serialized) { true }) ? :serialized : :unserialized
+ super(attributes, options)
- serialized_attributes.each do |key, coder|
- if attributes.key?(key)
- attributes[key] = Attribute.new(coder, attributes[key], :serialized)
+ serialized_attributes.each do |key, coder|
+ if attributes.key?(key)
+ attributes[key] = Attribute.new(coder, attributes[key], serialized)
+ end
end
+
+ attributes
end
- attributes
- end
+ private
- private
+ def attribute_cast_code(attr_name)
+ if serialized_attributes.include?(attr_name)
+ "v.unserialized_value"
+ else
+ super
+ end
+ end
+ end
- def attribute_cast_code(attr_name)
- if serialized_attributes.include?(attr_name)
- "v.unserialized_value"
+ def type_cast_attribute_for_write(column, value)
+ if column && coder = self.class.serialized_attributes[column.name]
+ Attribute.new(coder, value, :unserialized)
else
super
end
end
- end
-
- def type_cast_attribute_for_write(column, value)
- if column && coder = self.class.serialized_attributes[column.name]
- Attribute.new(coder, value, :unserialized)
- else
- super
- end
- end
- def read_attribute_before_type_cast(attr_name)
- if serialized_attributes.include?(attr_name)
- super.unserialized_value
- else
- super
+ def read_attribute_before_type_cast(attr_name)
+ if self.class.serialized_attributes.include?(attr_name)
+ super.unserialized_value
+ else
+ super
+ end
end
end
end
diff --git a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
index 20372c5c18..9647d03be4 100644
--- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
+++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
@@ -1,7 +1,13 @@
-require 'active_support/core_ext/class/attribute'
-require 'active_support/core_ext/object/inclusion'
module ActiveRecord
+ ActiveSupport.on_load(:active_record_config) do
+ mattr_accessor :time_zone_aware_attributes, instance_accessor: false
+ self.time_zone_aware_attributes = false
+
+ mattr_accessor :skip_time_zone_conversion_for_attributes, instance_accessor: false
+ self.skip_time_zone_conversion_for_attributes = []
+ end
+
module AttributeMethods
module TimeZoneConversion
class Type # :nodoc:
@@ -22,11 +28,8 @@ module ActiveRecord
extend ActiveSupport::Concern
included do
- config_attribute :time_zone_aware_attributes, :global => true
- self.time_zone_aware_attributes = false
-
+ config_attribute :time_zone_aware_attributes, global: true
config_attribute :skip_time_zone_conversion_for_attributes
- self.skip_time_zone_conversion_for_attributes = []
end
module ClassMethods
@@ -56,9 +59,14 @@ module ActiveRecord
unless time.acts_like?(:time)
time = time.is_a?(String) ? Time.zone.parse(time) : time.to_time rescue time
end
- time = time.in_time_zone rescue nil if time
- write_attribute(:#{attr_name}, original_time)
- @attributes_cache["#{attr_name}"] = time
+ zoned_time = time && time.in_time_zone rescue nil
+ rounded_time = round_usec(zoned_time)
+ rounded_value = round_usec(read_attribute("#{attr_name}"))
+ if (rounded_value != rounded_time) || (!rounded_value && original_time)
+ write_attribute("#{attr_name}", original_time)
+ #{attr_name}_will_change!
+ @attributes_cache[:"#{attr_name}"] = zoned_time
+ end
end
EOV
generated_attribute_methods.module_eval(method_body, __FILE__, line)
@@ -74,6 +82,12 @@ module ActiveRecord
[:datetime, :timestamp].include?(column.type)
end
end
+
+ private
+ def round_usec(value)
+ return unless value
+ value.change(:usec => 0)
+ end
end
end
end
diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb
index 50435921b1..5a39cb0125 100644
--- a/activerecord/lib/active_record/attribute_methods/write.rb
+++ b/activerecord/lib/active_record/attribute_methods/write.rb
@@ -25,13 +25,13 @@ module ActiveRecord
def write_attribute(attr_name, value)
attr_name = attr_name.to_s
attr_name = self.class.primary_key if attr_name == 'id' && self.class.primary_key
- @attributes_cache.delete(attr_name)
+ @attributes_cache.delete(attr_name.to_sym)
column = column_for_attribute(attr_name)
# If we're dealing with a binary column, write the data to the cache
# so we don't attempt to typecast multiple times.
if column && column.binary?
- @attributes_cache[attr_name] = value
+ @attributes_cache[attr_name.to_sym] = value
end
if column || @attributes.has_key?(attr_name)
diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb
index 0468a73c7d..a30f888a7a 100644
--- a/activerecord/lib/active_record/autosave_association.rb
+++ b/activerecord/lib/active_record/autosave_association.rb
@@ -28,7 +28,7 @@ module ActiveRecord
# Association with autosave option defines several callbacks on your
# model (before_save, after_create, after_update). Please note that
# callbacks are executed in the order they were defined in
- # model. You should avoid modyfing the association content, before
+ # model. You should avoid modifying the association content, before
# autosave callbacks are executed. Placing your callbacks after
# associations is usually a good practice.
#
@@ -78,7 +78,7 @@ module ActiveRecord
# When <tt>:autosave</tt> is not declared new children are saved when their parent is saved:
#
# class Post
- # has_many :comments # :autosave option is no declared
+ # has_many :comments # :autosave option is not declared
# end
#
# post = Post.new(:title => 'ruby rocks')
@@ -93,7 +93,8 @@ module ActiveRecord
# post.comments.create(:body => 'hello world')
# post.save # => saves both post and comment
#
- # When <tt>:autosave</tt> is true all children is saved, no matter whether they are new records:
+ # When <tt>:autosave</tt> is true all children are saved, no matter whether they
+ # are new records or not:
#
# class Post
# has_many :comments, :autosave => true
@@ -126,23 +127,17 @@ module ActiveRecord
module AutosaveAssociation
extend ActiveSupport::Concern
- ASSOCIATION_TYPES = %w{ HasOne HasMany BelongsTo HasAndBelongsToMany }
-
module AssociationBuilderExtension #:nodoc:
- def self.included(base)
- base.valid_options << :autosave
- end
-
def build
- reflection = super
model.send(:add_autosave_association_callbacks, reflection)
- reflection
+ super
end
end
included do
- ASSOCIATION_TYPES.each do |type|
- Associations::Builder.const_get(type).send(:include, AssociationBuilderExtension)
+ Associations::Builder::Association.class_eval do
+ self.valid_options << :autosave
+ include AssociationBuilderExtension
end
end
@@ -190,23 +185,21 @@ module ActiveRecord
# Doesn't use after_save as that would save associations added in after_create/after_update twice
after_create save_method
after_update save_method
+ elsif reflection.macro == :has_one
+ define_method(save_method) { save_has_one_association(reflection) }
+ # Configures two callbacks instead of a single after_save so that
+ # the model may rely on their execution order relative to its
+ # own callbacks.
+ #
+ # For example, given that after_creates run before after_saves, if
+ # we configured instead an after_save there would be no way to fire
+ # a custom after_create callback after the child association gets
+ # created.
+ after_create save_method
+ after_update save_method
else
- if reflection.macro == :has_one
- define_method(save_method) { save_has_one_association(reflection) }
- # Configures two callbacks instead of a single after_save so that
- # the model may rely on their execution order relative to its
- # own callbacks.
- #
- # For example, given that after_creates run before after_saves, if
- # we configured instead an after_save there would be no way to fire
- # a custom after_create callback after the child association gets
- # created.
- after_create save_method
- after_update save_method
- else
- define_non_cyclic_method(save_method, reflection) { save_belongs_to_association(reflection) }
- before_save save_method
- end
+ define_non_cyclic_method(save_method, reflection) { save_belongs_to_association(reflection) }
+ before_save save_method
end
end
@@ -329,14 +322,14 @@ module ActiveRecord
autosave = reflection.options[:autosave]
if records = associated_records_to_validate_or_save(association, @new_record_before_save, autosave)
- begin
+ records_to_destroy = []
records.each do |record|
next if record.destroyed?
saved = true
if autosave && record.marked_for_destruction?
- association.proxy.destroy(record)
+ records_to_destroy << record
elsif autosave != false && (@new_record_before_save || record.new_record?)
if autosave
saved = association.insert_record(record, false)
@@ -349,11 +342,10 @@ module ActiveRecord
raise ActiveRecord::Rollback unless saved
end
- rescue
- records.each {|x| IdentityMap.remove(x) } if IdentityMap.enabled?
- raise
- 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 d4d0220fb7..a4705b24ca 100644
--- a/activerecord/lib/active_record/base.rb
+++ b/activerecord/lib/active_record/base.rb
@@ -4,7 +4,6 @@ require 'active_support/benchmarkable'
require 'active_support/dependencies'
require 'active_support/descendants_tracker'
require 'active_support/time'
-require 'active_support/core_ext/class/attribute'
require 'active_support/core_ext/class/attribute_accessors'
require 'active_support/core_ext/class/delegating_attributes'
require 'active_support/core_ext/array/extract_options'
@@ -13,11 +12,8 @@ require 'active_support/core_ext/hash/indifferent_access'
require 'active_support/core_ext/hash/slice'
require 'active_support/core_ext/string/behavior'
require 'active_support/core_ext/kernel/singleton_class'
-require 'active_support/core_ext/module/delegation'
require 'active_support/core_ext/module/introspection'
require 'active_support/core_ext/object/duplicable'
-require 'active_support/core_ext/object/blank'
-require 'active_support/deprecation'
require 'arel'
require 'active_record/errors'
require 'active_record/log_subscriber'
@@ -201,6 +197,9 @@ module ActiveRecord #:nodoc:
# # Now 'Bob' exist and is an 'admin'
# User.find_or_create_by_name('Bob', :age => 40) { |u| u.admin = true }
#
+ # Adding an exclamation point (!) on to the end of <tt>find_or_create_by_</tt> will
+ # raise an <tt>ActiveRecord::RecordInvalid</tt> error if the new record is invalid.
+ #
# Use the <tt>find_or_initialize_by_</tt> finder if you want to return a new record without
# saving it first. Protected attributes won't be set unless they are given in a block.
#
@@ -301,21 +300,22 @@ module ActiveRecord #:nodoc:
# (or a bad spelling of an existing one).
# * AssociationTypeMismatch - The object assigned to the association wasn't of the type
# specified in the association definition.
- # * SerializationTypeMismatch - The serialized object wasn't of the class specified as the second parameter.
- # * ConnectionNotEstablished+ - No connection has been established. Use <tt>establish_connection</tt>
+ # * AttributeAssignmentError - An error occurred while doing a mass assignment through the
+ # <tt>attributes=</tt> method.
+ # You can inspect the +attribute+ property of the exception object to determine which attribute
+ # triggered the error.
+ # * ConnectionNotEstablished - No connection has been established. Use <tt>establish_connection</tt>
# before querying.
- # * RecordNotFound - No record responded to the +find+ method. Either the row with the given ID doesn't exist
- # or the row didn't meet the additional restrictions. Some +find+ calls do not raise this exception to signal
- # nothing was found, please check its documentation for further details.
- # * StatementInvalid - The database server rejected the SQL statement. The precise error is added in the message.
# * MultiparameterAssignmentErrors - Collection of errors that occurred during a mass assignment using the
# <tt>attributes=</tt> method. The +errors+ property of this exception contains an array of
# AttributeAssignmentError
# objects that should be inspected to determine which attributes triggered the errors.
- # * AttributeAssignmentError - An error occurred while doing a mass assignment through the
- # <tt>attributes=</tt> method.
- # You can inspect the +attribute+ property of the exception object to determine which attribute
- # triggered the error.
+ # * RecordInvalid - raised by save! and create! when the record is invalid.
+ # * RecordNotFound - No record responded to the +find+ method. Either the row with the given ID doesn't exist
+ # or the row didn't meet the additional restrictions. Some +find+ calls do not raise this exception to signal
+ # nothing was found, please check its documentation for further details.
+ # * SerializationTypeMismatch - The serialized object wasn't of the class specified as the second parameter.
+ # * StatementInvalid - The database server rejected the SQL statement. The precise error is added in the message.
#
# *Note*: The attributes listed are class-level attributes (accessible from both the class and instance level).
# So it's possible to assign a logger to the class through <tt>Base.logger=</tt> which will then be used by all
@@ -325,4 +325,4 @@ module ActiveRecord #:nodoc:
end
end
-ActiveSupport.run_load_hooks(:active_record, ActiveRecord::Model::DeprecationProxy)
+ActiveSupport.run_load_hooks(:active_record, ActiveRecord::Model::DeprecationProxy.new)
diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb
index a050fabf35..111208d0b9 100644
--- a/activerecord/lib/active_record/callbacks.rb
+++ b/activerecord/lib/active_record/callbacks.rb
@@ -34,7 +34,7 @@ module ActiveRecord
# Examples:
# class CreditCard < ActiveRecord::Base
# # Strip everything but digits, so the user can specify "555 234 34" or
- # # "5552-3434" or both will mean "55523434"
+ # # "5552-3434" and both will mean "55523434"
# before_validation(:on => :create) do
# self.number = number.gsub(/[^0-9]/, "") if attribute_present?("number")
# end
@@ -231,30 +231,6 @@ module ActiveRecord
# Returns true or false depending on whether the proc is contained in the before_save callback chain on a Topic model.
#
module Callbacks
- # We can't define callbacks directly on ActiveRecord::Model because
- # it is a module. So we queue up the definitions and execute them
- # when ActiveRecord::Model is included.
- module Register #:nodoc:
- def self.extended(base)
- base.config_attribute :_callbacks_register
- base._callbacks_register = []
- end
-
- def self.setup(base)
- base._callbacks_register.each do |item|
- base.send(*item)
- end
- end
-
- def define_callbacks(*args)
- self._callbacks_register << [:define_callbacks, *args]
- end
-
- def define_model_callbacks(*args)
- self._callbacks_register << [:define_model_callbacks, *args]
- end
- end
-
extend ActiveSupport::Concern
CALLBACKS = [
diff --git a/activerecord/lib/active_record/coders/yaml_column.rb b/activerecord/lib/active_record/coders/yaml_column.rb
index 77af540c3e..f17e7158de 100644
--- a/activerecord/lib/active_record/coders/yaml_column.rb
+++ b/activerecord/lib/active_record/coders/yaml_column.rb
@@ -1,12 +1,10 @@
+require 'yaml'
+
module ActiveRecord
# :stopdoc:
module Coders
class YAMLColumn
- RESCUE_ERRORS = [ ArgumentError ]
-
- if defined?(Psych) && defined?(Psych::SyntaxError)
- RESCUE_ERRORS << Psych::SyntaxError
- end
+ RESCUE_ERRORS = [ ArgumentError, Psych::SyntaxError ]
attr_accessor :object_class
@@ -15,7 +13,13 @@ module ActiveRecord
end
def dump(obj)
- YAML.dump(obj) unless obj.nil?
+ return if obj.nil?
+
+ unless obj.is_a?(object_class)
+ raise SerializationTypeMismatch,
+ "Attribute was supposed to be a #{object_class}, but was a #{obj.class}. -- #{obj.inspect}"
+ end
+ YAML.dump obj
end
def load(yaml)
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 06b9bc5765..42bd16db80 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
@@ -5,17 +5,11 @@ require 'active_support/core_ext/module/deprecation'
module ActiveRecord
# Raised when a connection could not be obtained within the connection
- # acquisition timeout period.
+ # acquisition timeout period: because max connections in pool
+ # are in use.
class ConnectionTimeoutError < ConnectionNotEstablished
end
- # Raised when a connection pool is full and another connection is requested
- class PoolFullError < ConnectionNotEstablished
- def initialize size, timeout
- super("Connection pool of size #{size} and timeout #{timeout}s is full")
- end
- end
-
module ConnectionAdapters
# Connection pool base class for managing Active Record database
# connections.
@@ -57,13 +51,150 @@ module ActiveRecord
#
# == Options
#
- # There are two connection-pooling-related options that you can add to
+ # There are several connection-pooling-related options that you can add to
# your database connection configuration:
#
# * +pool+: number indicating size of connection pool (default 5)
- # * +wait_timeout+: number of seconds to block and wait for a connection
+ # * +checkout_timeout+: number of seconds to block and wait for a connection
# before giving up and raising a timeout error (default 5 seconds).
+ # * +reaping_frequency+: frequency in seconds to periodically run the
+ # Reaper, which attempts to find and close dead connections, which can
+ # occur if a programmer forgets to close a connection at the end of a
+ # thread or a thread dies unexpectedly. (Default nil, which means don't
+ # run the Reaper).
+ # * +dead_connection_timeout+: number of seconds from last checkout
+ # after which the Reaper will consider a connection reapable. (default
+ # 5 seconds).
class ConnectionPool
+ # Threadsafe, fair, FIFO queue. Meant to be used by ConnectionPool
+ # with which it shares a Monitor. But could be a generic Queue.
+ #
+ # The Queue in stdlib's 'thread' could replace this class except
+ # stdlib's doesn't support waiting with a timeout.
+ class Queue
+ def initialize(lock = Monitor.new)
+ @lock = lock
+ @cond = @lock.new_cond
+ @num_waiting = 0
+ @queue = []
+ end
+
+ # Test if any threads are currently waiting on the queue.
+ def any_waiting?
+ synchronize do
+ @num_waiting > 0
+ end
+ end
+
+ # Return the number of threads currently waiting on this
+ # queue.
+ def num_waiting
+ synchronize do
+ @num_waiting
+ end
+ end
+
+ # Add +element+ to the queue. Never blocks.
+ def add(element)
+ synchronize do
+ @queue.push element
+ @cond.signal
+ end
+ end
+
+ # If +element+ is in the queue, remove and return it, or nil.
+ def delete(element)
+ synchronize do
+ @queue.delete(element)
+ end
+ end
+
+ # Remove all elements from the queue.
+ def clear
+ synchronize do
+ @queue.clear
+ end
+ end
+
+ # Remove the head of the queue.
+ #
+ # If +timeout+ is not given, remove and return the head the
+ # queue if the number of available elements is strictly
+ # greater than the number of threads currently waiting (that
+ # is, don't jump ahead in line). Otherwise, return nil.
+ #
+ # If +timeout+ is given, block if it there is no element
+ # available, waiting up to +timeout+ seconds for an element to
+ # become available.
+ #
+ # Raises:
+ # - ConnectionTimeoutError if +timeout+ is given and no element
+ # becomes available after +timeout+ seconds,
+ def poll(timeout = nil)
+ synchronize do
+ if timeout
+ no_wait_poll || wait_poll(timeout)
+ else
+ no_wait_poll
+ end
+ end
+ end
+
+ private
+
+ def synchronize(&block)
+ @lock.synchronize(&block)
+ end
+
+ # Test if the queue currently contains any elements.
+ def any?
+ !@queue.empty?
+ end
+
+ # A thread can remove an element from the queue without
+ # waiting if an only if the number of currently available
+ # connections is strictly greater than the number of waiting
+ # threads.
+ def can_remove_no_wait?
+ @queue.size > @num_waiting
+ end
+
+ # Removes and returns the head of the queue if possible, or nil.
+ def remove
+ @queue.shift
+ end
+
+ # Remove and return the head the queue if the number of
+ # available elements is strictly greater than the number of
+ # threads currently waiting. Otherwise, return nil.
+ def no_wait_poll
+ remove if can_remove_no_wait?
+ end
+
+ # Waits on the queue up to +timeout+ seconds, then removes and
+ # returns the head of the queue.
+ def wait_poll(timeout)
+ @num_waiting += 1
+
+ t0 = Time.now
+ elapsed = 0
+ loop do
+ @cond.wait(timeout - elapsed)
+
+ return remove if any?
+
+ elapsed = Time.now - t0
+ if elapsed >= timeout
+ msg = 'could not obtain a database connection within %0.3f seconds (waited %0.3f seconds)' %
+ [timeout, elapsed]
+ raise ConnectionTimeoutError, msg
+ end
+ end
+ ensure
+ @num_waiting -= 1
+ end
+ end
+
# Every +frequency+ seconds, the reaper will call +reap+ on +pool+.
# A reaper instantiated with a nil frequency will never reap the
# connection pool.
@@ -91,7 +222,7 @@ module ActiveRecord
include MonitorMixin
- attr_accessor :automatic_reconnect, :timeout
+ attr_accessor :automatic_reconnect, :checkout_timeout, :dead_connection_timeout
attr_reader :spec, :connections, :size, :reaper
# Creates a new ConnectionPool object. +spec+ is a ConnectionSpecification
@@ -108,7 +239,8 @@ module ActiveRecord
# The cache of reserved connections mapped to threads
@reserved_connections = {}
- @timeout = spec.config[:wait_timeout] || 5
+ @checkout_timeout = spec.config[:checkout_timeout] || 5
+ @dead_connection_timeout = spec.config[:dead_connection_timeout]
@reaper = Reaper.new self, spec.config[:reaping_frequency]
@reaper.run
@@ -117,6 +249,16 @@ module ActiveRecord
@connections = []
@automatic_reconnect = true
+
+ @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
@@ -125,21 +267,28 @@ module ActiveRecord
# #connection can be called any number of times; the connection is
# held in a hash keyed by the thread id.
def connection
- @reserved_connections[current_connection_id] ||= checkout
+ synchronize do
+ @reserved_connections[current_connection_id] ||= checkout
+ end
end
- # Check to see if there is an active connection in this connection
- # pool.
+ # Is there an open connection that is being used for the current thread?
def active_connection?
- active_connections.any?
+ synchronize do
+ @reserved_connections.fetch(current_connection_id) {
+ return false
+ }.in_use?
+ end
end
# Signal that the thread is finished with the current connection.
# #release_connection releases the connection-thread association
# and returns the connection to the pool.
def release_connection(with_id = current_connection_id)
- conn = @reserved_connections.delete(with_id)
- checkin conn if conn
+ synchronize do
+ conn = @reserved_connections.delete(with_id)
+ checkin conn if conn
+ end
end
# If a connection already exists yield it to the block. If no connection
@@ -167,6 +316,7 @@ module ActiveRecord
conn.disconnect!
end
@connections = []
+ @available.clear
end
end
@@ -181,22 +331,17 @@ module ActiveRecord
@connections.delete_if do |conn|
conn.requires_reloading?
end
- end
- end
-
- # Verify active connections and remove and disconnect connections
- # associated with stale threads.
- def verify_active_connections! #:nodoc:
- synchronize do
- @connections.each do |connection|
- connection.verify!
+ @available.clear
+ @connections.each do |conn|
+ @available.add conn
end
end
end
def clear_stale_cached_connections! # :nodoc:
+ reap
end
- deprecate :clear_stale_cached_connections!
+ deprecate :clear_stale_cached_connections! => "Please use #reap instead"
# Check-out a database connection from the pool, indicating that you want
# to use it. You should call #checkin when you no longer need this.
@@ -206,30 +351,17 @@ module ActiveRecord
#
# If all connections are leased and the pool is at capacity (meaning the
# number of currently leased connections is greater than or equal to the
- # size limit set), an ActiveRecord::PoolFullError exception will be raised.
+ # size limit set), an ActiveRecord::ConnectionTimeoutError exception will be raised.
#
# Returns: an AbstractAdapter object.
#
# Raises:
- # - PoolFullError: no connection can be obtained from the pool.
+ # - ConnectionTimeoutError: no connection can be obtained from the pool.
def checkout
- # Checkout an available connection
synchronize do
- # Try to find a connection that hasn't been leased, and lease it
- conn = connections.find { |c| c.lease }
-
- # If all connections were leased, and we have room to expand,
- # create a new connection and lease it.
- if !conn && connections.size < size
- conn = checkout_new_connection
- conn.lease
- end
-
- if conn
- checkout_and_verify conn
- else
- raise PoolFullError.new(size, timeout)
- end
+ conn = acquire_connection
+ conn.lease
+ checkout_and_verify(conn)
end
end
@@ -243,6 +375,10 @@ module ActiveRecord
conn.run_callbacks :checkin do
conn.expire
end
+
+ release conn
+
+ @available.add conn
end
end
@@ -251,13 +387,13 @@ module ActiveRecord
def remove(conn)
synchronize do
@connections.delete conn
+ @available.delete conn
# FIXME: we might want to store the key on the connection so that removing
# from the reserved hash will be a little easier.
- thread_id = @reserved_connections.keys.find { |k|
- @reserved_connections[k] == conn
- }
- @reserved_connections.delete thread_id if thread_id
+ release conn
+
+ @available.add checkout_new_connection if @available.any_waiting?
end
end
@@ -266,7 +402,7 @@ module ActiveRecord
# or a thread dies unexpectedly.
def reap
synchronize do
- stale = Time.now - @timeout
+ stale = Time.now - @dead_connection_timeout
connections.dup.each do |conn|
remove conn if conn.in_use? && stale > conn.last_use && !conn.active?
end
@@ -275,12 +411,41 @@ module ActiveRecord
private
+ # Acquire a connection by one of 1) immediately removing one
+ # from the queue of available connections, 2) creating a new
+ # connection if the pool is not at capacity, 3) waiting on the
+ # queue for a connection to become available.
+ #
+ # Raises:
+ # - ConnectionTimeoutError if a connection could not be acquired
+ def acquire_connection
+ if conn = @available.poll
+ conn
+ elsif @connections.size < @size
+ checkout_new_connection
+ else
+ @available.poll(@checkout_timeout)
+ end
+ end
+
+ def release(conn)
+ thread_id = if @reserved_connections[current_connection_id] == conn
+ current_connection_id
+ else
+ @reserved_connections.keys.find { |k|
+ @reserved_connections[k] == conn
+ }
+ end
+
+ @reserved_connections.delete thread_id if thread_id
+ end
+
def new_connection
- ActiveRecord::Base.send(spec.adapter_method, spec.config)
+ ActiveRecord::Model.send(spec.adapter_method, spec.config)
end
def current_connection_id #:nodoc:
- ActiveRecord::Base.connection_id ||= Thread.current.object_id
+ ActiveRecord::Model.connection_id ||= Thread.current.object_id
end
def checkout_new_connection
@@ -298,10 +463,6 @@ module ActiveRecord
end
c
end
-
- def active_connections
- @connections.find_all { |c| c.in_use? }
- end
end
# ConnectionHandler is a collection of ConnectionPool objects. It is used
@@ -326,47 +487,42 @@ module ActiveRecord
#
# Normally there is only a single ConnectionHandler instance, accessible via
# ActiveRecord::Base.connection_handler. Active Record models use this to
- # determine that connection pool that they should use.
+ # determine the connection pool that they should use.
class ConnectionHandler
- def initialize(pools = Hash.new { |h,k| h[k] = {} })
- @connection_pools = pools
- @class_to_pool = Hash.new { |h,k| h[k] = {} }
+ def initialize
+ @owner_to_pool = Hash.new { |h,k| h[k] = {} }
+ @class_to_pool = Hash.new { |h,k| h[k] = {} }
end
def connection_pools
- @connection_pools[Process.pid]
+ owner_to_pool.values.compact
end
- def establish_connection(name, spec)
- set_pool_for_spec spec, ConnectionAdapters::ConnectionPool.new(spec)
- set_class_to_pool name, connection_pools[spec]
+ def establish_connection(owner, spec)
+ @class_to_pool.clear
+ owner_to_pool[owner] = ConnectionAdapters::ConnectionPool.new(spec)
end
# Returns true if there are any active connections among the connection
# pools that the ConnectionHandler is managing.
def active_connections?
- connection_pools.values.any? { |pool| pool.active_connection? }
+ connection_pools.any?(&:active_connection?)
end
# Returns any connections in use by the current thread back to the pool,
# and also returns connections to the pool cached by threads that are no
# longer alive.
def clear_active_connections!
- connection_pools.each_value {|pool| pool.release_connection }
+ connection_pools.each(&:release_connection)
end
# Clears the cache which maps classes.
def clear_reloadable_connections!
- connection_pools.each_value {|pool| pool.clear_reloadable_connections! }
+ connection_pools.each(&:clear_reloadable_connections!)
end
def clear_all_connections!
- connection_pools.each_value {|pool| pool.disconnect! }
- end
-
- # Verify active connections.
- def verify_active_connections! #:nodoc:
- connection_pools.each_value {|pool| pool.verify_active_connections! }
+ connection_pools.each(&:disconnect!)
end
# Locate the connection of the nearest super class. This can be an
@@ -389,54 +545,62 @@ module ActiveRecord
# connection and the defined connection (if they exist). The result
# can be used as an argument for establish_connection, for easily
# re-establishing the connection.
- def remove_connection(klass)
- pool = class_to_pool.delete(klass.name)
- return nil unless pool
-
- connection_pools.delete pool.spec
- pool.automatic_reconnect = false
- pool.disconnect!
- pool.spec.config
+ def remove_connection(owner)
+ if pool = owner_to_pool.delete(owner)
+ @class_to_pool.clear
+ pool.automatic_reconnect = false
+ pool.disconnect!
+ pool.spec.config
+ end
end
+ # Retrieving the connection pool happens a lot so we cache it in @class_to_pool.
+ # This makes retrieving the connection pool O(1) once the process is warm.
+ # When a connection is established or removed, we invalidate the cache.
+ #
+ # Ideally we would use #fetch here, as class_to_pool[klass] may sometimes be nil.
+ # However, benchmarking (https://gist.github.com/3552829) showed that #fetch is
+ # significantly slower than #[]. So in the nil case, no caching will take place,
+ # but that's ok since the nil case is not the common one that we wish to optimise
+ # for.
def retrieve_connection_pool(klass)
- pool = get_pool_for_class klass.name
- return pool if pool
- return nil if ActiveRecord::Model == klass
- retrieve_connection_pool klass.active_record_super
+ class_to_pool[klass] ||= begin
+ until pool = pool_for(klass)
+ klass = klass.superclass
+ break unless klass < ActiveRecord::Tag
+ end
+
+ class_to_pool[klass] = pool || pool_for(ActiveRecord::Model)
+ end
end
private
- def class_to_pool
- @class_to_pool[Process.pid]
- end
-
- def set_pool_for_spec(spec, pool)
- @connection_pools[Process.pid][spec] = pool
+ def owner_to_pool
+ @owner_to_pool[Process.pid]
end
- def set_class_to_pool(name, pool)
- @class_to_pool[Process.pid][name] = pool
- pool
+ def class_to_pool
+ @class_to_pool[Process.pid]
end
- def get_pool_for_class(klass)
- @class_to_pool[Process.pid].fetch(klass) {
- c_to_p = @class_to_pool.values.find { |class_to_pool|
- class_to_pool[klass]
- }
-
- if c_to_p
- pool = c_to_p[klass]
- pool = ConnectionAdapters::ConnectionPool.new pool.spec
- set_pool_for_spec pool.spec, pool
- set_class_to_pool klass, pool
+ def pool_for(owner)
+ owner_to_pool.fetch(owner) {
+ if ancestor_pool = pool_from_any_process_for(owner)
+ # A connection was established in an ancestor process that must have
+ # subsequently forked. We can't reuse the connection, but we can copy
+ # the specification and establish a new connection with it.
+ establish_connection owner, ancestor_pool.spec
else
- set_class_to_pool klass, nil
+ owner_to_pool[owner] = nil
end
}
end
+
+ def pool_from_any_process_for(owner)
+ owner_to_pool = @owner_to_pool.values.find { |v| v[owner] }
+ owner_to_pool && owner_to_pool[owner]
+ end
end
class ConnectionManagement
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
index db99c3fbef..32e3c7f5d8 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
@@ -1,9 +1,15 @@
module ActiveRecord
module ConnectionAdapters # :nodoc:
module DatabaseStatements
+ def initialize
+ super
+ reset_transaction
+ end
+
# Converts an arel AST to SQL
def to_sql(arel, binds = [])
if arel.respond_to?(:ast)
+ binds = binds.dup
visitor.accept(arel.ast) do
quote(*binds.shift.reverse)
end
@@ -20,14 +26,14 @@ module ActiveRecord
# Returns a record hash with the column names as keys and column values
# as values.
- def select_one(arel, name = nil)
- result = select_all(arel, name)
+ def select_one(arel, name = nil, binds = [])
+ result = select_all(arel, name, binds)
result.first if result
end
# Returns a single value from a record
- def select_value(arel, name = nil)
- if result = select_one(arel, name)
+ def select_value(arel, name = nil, binds = [])
+ if result = select_one(arel, name, binds)
result.values.first
end
end
@@ -57,21 +63,21 @@ module ActiveRecord
end
# Executes insert +sql+ statement in the context of this connection using
- # +binds+ as the bind substitutes. +name+ is the logged along with
+ # +binds+ as the bind substitutes. +name+ is logged along with
# the executed +sql+ statement.
- def exec_insert(sql, name, binds)
+ def exec_insert(sql, name, binds, pk = nil, sequence_name = nil)
exec_query(sql, name, binds)
end
# Executes delete +sql+ statement in the context of this connection using
- # +binds+ as the bind substitutes. +name+ is the logged along with
+ # +binds+ as the bind substitutes. +name+ is logged along with
# the executed +sql+ statement.
def exec_delete(sql, name, binds)
exec_query(sql, name, binds)
end
# Executes update +sql+ statement in the context of this connection using
- # +binds+ as the bind substitutes. +name+ is the logged along with
+ # +binds+ as the bind substitutes. +name+ is logged along with
# the executed +sql+ statement.
def exec_update(sql, name, binds)
exec_query(sql, name, binds)
@@ -87,7 +93,7 @@ module ActiveRecord
# passed in as +id_value+.
def insert(arel, name = nil, pk = nil, id_value = nil, sequence_name = nil, binds = [])
sql, binds = sql_for_insert(to_sql(arel, binds), pk, id_value, sequence_name, binds)
- value = exec_insert(sql, name, binds)
+ value = exec_insert(sql, name, binds, pk, sequence_name)
id_value || last_inserted_id(value)
end
@@ -101,20 +107,6 @@ module ActiveRecord
exec_delete(to_sql(arel, binds), name, binds)
end
- # Checks whether there is currently no transaction active. This is done
- # by querying the database driver, and does not use the transaction
- # house-keeping information recorded by #increment_open_transactions and
- # friends.
- #
- # Returns true if there is no transaction active, false if there is a
- # transaction active, and nil if this information is unknown.
- #
- # Not all adapters supports transaction state introspection. Currently,
- # only the PostgreSQL adapter supports this.
- def outside_transaction?
- nil
- end
-
# Returns +true+ when the connection adapter supports prepared statement
# caching, otherwise returns +false+
def supports_statement_cache?
@@ -166,82 +158,60 @@ module ActiveRecord
def transaction(options = {})
options.assert_valid_keys :requires_new, :joinable
- last_transaction_joinable = defined?(@transaction_joinable) ? @transaction_joinable : nil
- if options.has_key?(:joinable)
- @transaction_joinable = options[:joinable]
+ if !options[:requires_new] && current_transaction.joinable?
+ yield
else
- @transaction_joinable = true
+ within_new_transaction(options) { yield }
end
- requires_new = options[:requires_new] || !last_transaction_joinable
-
- transaction_open = false
- @_current_transaction_records ||= []
+ rescue ActiveRecord::Rollback
+ # rollbacks are silently swallowed
+ end
- begin
- if block_given?
- if requires_new || open_transactions == 0
- if open_transactions == 0
- begin_db_transaction
- elsif requires_new
- create_savepoint
- end
- increment_open_transactions
- transaction_open = true
- @_current_transaction_records.push([])
- end
- yield
- end
- rescue Exception => database_transaction_rollback
- if transaction_open && !outside_transaction?
- transaction_open = false
- decrement_open_transactions
- if open_transactions == 0
- rollback_db_transaction
- rollback_transaction_records(true)
- else
- rollback_to_savepoint
- rollback_transaction_records(false)
- end
- end
- raise unless database_transaction_rollback.is_a?(ActiveRecord::Rollback)
- end
+ def within_new_transaction(options = {}) #:nodoc:
+ begin_transaction(options)
+ yield
+ rescue Exception => error
+ rollback_transaction
+ raise
ensure
- @transaction_joinable = last_transaction_joinable
-
- if outside_transaction?
- @open_transactions = 0
- elsif transaction_open
- decrement_open_transactions
- begin
- if open_transactions == 0
- commit_db_transaction
- commit_transaction_records
- else
- release_savepoint
- save_point_records = @_current_transaction_records.pop
- unless save_point_records.blank?
- @_current_transaction_records.push([]) if @_current_transaction_records.empty?
- @_current_transaction_records.last.concat(save_point_records)
- end
- end
- rescue Exception => database_transaction_rollback
- if open_transactions == 0
- rollback_db_transaction
- rollback_transaction_records(true)
- else
- rollback_to_savepoint
- rollback_transaction_records(false)
- end
- raise
- end
+ begin
+ commit_transaction unless error
+ rescue Exception
+ rollback_transaction
+ raise
end
end
+ def current_transaction #:nodoc:
+ @transaction
+ end
+
+ def transaction_open?
+ @transaction.open?
+ end
+
+ def begin_transaction(options = {}) #:nodoc:
+ @transaction = @transaction.begin
+ @transaction.joinable = options.fetch(:joinable, true)
+ @transaction
+ end
+
+ def commit_transaction #:nodoc:
+ @transaction = @transaction.commit
+ end
+
+ def rollback_transaction #:nodoc:
+ @transaction = @transaction.rollback
+ end
+
+ def reset_transaction #:nodoc:
+ @transaction = ClosedTransaction.new(self)
+ end
+
# Register a record with the current transaction so that its after_commit and after_rollback callbacks
# can be called.
def add_transaction_record(record)
- last_batch = @_current_transaction_records.last
- last_batch << record if last_batch
+ @transaction.add_record(record)
end
# Begins the transaction (and turns off auto-committing).
@@ -312,13 +282,27 @@ module ActiveRecord
# on mysql (even when aliasing the tables), but mysql allows using JOIN directly in
# an UPDATE statement, so in the mysql adapters we redefine this to do that.
def join_to_update(update, select) #:nodoc:
- subselect = select.clone
- subselect.projections = [update.key]
+ key = update.key
+ subselect = subquery_for(key, select)
+
+ update.where key.in(subselect)
+ end
+
+ def join_to_delete(delete, select, key) #:nodoc:
+ subselect = subquery_for(key, select)
- update.where update.key.in(subselect)
+ delete.where key.in(subselect)
end
protected
+
+ # Return a subquery for the given key using the join information.
+ def subquery_for(key, select)
+ subselect = select.clone
+ subselect.projections = [key]
+ subselect
+ end
+
# Returns an array of record hashes with the column names as keys and
# column values as values.
def select(sql, name = nil, binds = [])
@@ -341,42 +325,6 @@ module ActiveRecord
update_sql(sql, name)
end
- # Send a rollback message to all records after they have been rolled back. If rollback
- # is false, only rollback records since the last save point.
- def rollback_transaction_records(rollback)
- if rollback
- records = @_current_transaction_records.flatten
- @_current_transaction_records.clear
- else
- records = @_current_transaction_records.pop
- end
-
- unless records.blank?
- records.uniq.each do |record|
- begin
- record.rolledback!(rollback)
- rescue Exception => e
- record.logger.error(e) if record.respond_to?(:logger) && record.logger
- end
- end
- end
- end
-
- # Send a commit message to all records after they have been committed.
- def commit_transaction_records
- records = @_current_transaction_records.flatten
- @_current_transaction_records.clear
- unless records.blank?
- records.uniq.each do |record|
- begin
- record.committed!
- rescue Exception => e
- record.logger.error(e) if record.respond_to?(:logger) && record.logger
- end
- end
- end
- end
-
def sql_for_insert(sql, pk, id_value, sequence_name, binds)
[sql, binds]
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb
index 17377bad96..be6fda95b4 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb
@@ -2,7 +2,7 @@ module ActiveRecord
module ConnectionAdapters # :nodoc:
module QueryCache
class << self
- def included(base)
+ def included(base) #:nodoc:
dirties_query_cache base, :insert, :update, :delete
end
@@ -56,7 +56,7 @@ module ActiveRecord
end
def select_all(arel, name = nil, binds = [])
- if @query_cache_enabled
+ if @query_cache_enabled && !locked?(arel)
sql = to_sql(arel, binds)
cache_sql(sql, binds) { super(sql, name, binds) }
else
@@ -65,6 +65,7 @@ module ActiveRecord
end
private
+
def cache_sql(sql, binds)
result =
if @query_cache[sql].key?(binds)
@@ -83,6 +84,10 @@ module ActiveRecord
result.collect { |row| row.dup }
end
end
+
+ def locked?(arel)
+ arel.respond_to?(:locked) && arel.locked
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
index 44ac37c498..60a9eee7c7 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
@@ -31,9 +31,10 @@ module ActiveRecord
# BigDecimals need to be put in a non-normalized form and quoted.
when nil then "NULL"
when BigDecimal then value.to_s('F')
- when Numeric then value.to_s
+ when Numeric, ActiveSupport::Duration then value.to_s
when Date, Time then "'#{quoted_date(value)}'"
when Symbol then "'#{quote_string(value.to_s)}'"
+ when Class then "'#{value.to_s}'"
else
"'#{quote_string(YAML.dump(value))}'"
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
index ad2e8634eb..dca355aa93 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
@@ -1,4 +1,3 @@
-require 'active_support/core_ext/object/blank'
require 'date'
require 'set'
require 'bigdecimal'
@@ -6,6 +5,9 @@ require 'bigdecimal/util'
module ActiveRecord
module ConnectionAdapters #:nodoc:
+ # Abstract representation of an index definition on a table. Instances of
+ # this type are typically created and returned by methods in database
+ # adapters. e.g. ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter#indexes
class IndexDefinition < Struct.new(:table, :name, :unique, :columns, :lengths, :orders, :where) #:nodoc:
end
@@ -20,7 +22,7 @@ module ActiveRecord
end
def sql_type
- base.type_to_sql(type.to_sym, limit, precision, scale) rescue type
+ base.type_to_sql(type.to_sym, limit, precision, scale)
end
def to_sql
@@ -62,11 +64,12 @@ module ActiveRecord
class TableDefinition
# An array of ColumnDefinition objects, representing the column changes
# that have been defined.
- attr_accessor :columns
+ attr_accessor :columns, :indexes
def initialize(base)
@columns = []
@columns_hash = {}
+ @indexes = {}
@base = base
end
@@ -209,19 +212,22 @@ module ActiveRecord
#
# TableDefinition#references will add an appropriately-named _id column, plus a corresponding _type
# column if the <tt>:polymorphic</tt> option is supplied. If <tt>:polymorphic</tt> is a hash of
- # options, these will be used when creating the <tt>_type</tt> column. So what can be written like this:
+ # options, these will be used when creating the <tt>_type</tt> column. The <tt>:index</tt> option
+ # will also create an index, similar to calling <tt>add_index</tt>. So what can be written like this:
#
# create_table :taggings do |t|
# t.integer :tag_id, :tagger_id, :taggable_id
# t.string :tagger_type
# t.string :taggable_type, :default => 'Photo'
# end
+ # add_index :taggings, :tag_id, :name => 'index_taggings_on_tag_id'
+ # add_index :taggings, [:tagger_id, :tagger_type]
#
# Can also be written as follows using references:
#
# create_table :taggings do |t|
- # t.references :tag
- # t.references :tagger, :polymorphic => true
+ # t.references :tag, :index => { :name => 'index_taggings_on_tag_id' }
+ # t.references :tagger, :polymorphic => true, :index => true
# t.references :taggable, :polymorphic => { :default => 'Photo' }
# end
def column(name, type, options = {})
@@ -253,10 +259,18 @@ module ActiveRecord
EOV
end
+ # Adds index options to the indexes hash, keyed by column name
+ # This is primarily used to track indexes that need to be created after the table
+ #
+ # index(:account_id, :name => 'index_projects_on_account_id')
+ def index(column_name, options = {})
+ indexes[column_name] = options
+ end
+
# Appends <tt>:datetime</tt> columns <tt>:created_at</tt> and
# <tt>:updated_at</tt> to the table.
def timestamps(*args)
- options = { :null => false }.merge(args.extract_options!)
+ options = args.extract_options!
column(:created_at, :datetime, options)
column(:updated_at, :datetime, options)
end
@@ -264,9 +278,11 @@ module ActiveRecord
def references(*args)
options = args.extract_options!
polymorphic = options.delete(:polymorphic)
+ index_options = options.delete(:index)
args.each do |col|
column("#{col}_id", :integer, options)
- column("#{col}_type", :string, polymorphic.is_a?(Hash) ? polymorphic : options) unless polymorphic.nil?
+ 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
end
end
alias :belongs_to :references
@@ -331,7 +347,7 @@ module ActiveRecord
# Adds a new column to the named table.
# See TableDefinition#column for details of the options you can use.
- # ===== Example
+ #
# ====== Creating a simple column
# t.column(:name, :string)
def column(column_name, type, options = {})
@@ -346,7 +362,6 @@ module ActiveRecord
# Adds a new index to the table. +column_name+ can be a single Symbol, or
# an Array of Symbols. See SchemaStatements#add_index
#
- # ===== Examples
# ====== Creating a simple index
# t.index(:name)
# ====== Creating a unique index
@@ -363,7 +378,7 @@ module ActiveRecord
end
# Adds timestamps (+created_at+ and +updated_at+) columns to the table. See SchemaStatements#add_timestamps
- # ===== Example
+ #
# t.timestamps
def timestamps
@base.add_timestamps(@table_name)
@@ -371,7 +386,7 @@ module ActiveRecord
# Changes the column's definition according to the new options.
# See TableDefinition#column for details of the options you can use.
- # ===== Examples
+ #
# t.change(:name, :string, :limit => 80)
# t.change(:description, :text)
def change(column_name, type, options = {})
@@ -379,7 +394,7 @@ module ActiveRecord
end
# Sets a new default value for a column. See SchemaStatements#change_column_default
- # ===== Examples
+ #
# t.change_default(:qualification, 'new')
# t.change_default(:authorized, 1)
def change_default(column_name, default)
@@ -387,16 +402,15 @@ module ActiveRecord
end
# Removes the column(s) from the table definition.
- # ===== Examples
+ #
# t.remove(:qualification)
# t.remove(:qualification, :experience)
def remove(*column_names)
- @base.remove_column(@table_name, column_names)
+ @base.remove_column(@table_name, *column_names)
end
# Removes the given index from the table.
#
- # ===== Examples
# ====== Remove the index_table_name_on_column in the table_name table
# t.remove_index :column
# ====== Remove the index named index_table_name_on_branch_id in the table_name table
@@ -410,14 +424,14 @@ module ActiveRecord
end
# Removes the timestamp columns (+created_at+ and +updated_at+) from the table.
- # ===== Example
+ #
# t.remove_timestamps
def remove_timestamps
@base.remove_timestamps(@table_name)
end
# Renames a column.
- # ===== Example
+ #
# t.rename(:description, :name)
def rename(column_name, new_column_name)
@base.rename_column(@table_name, column_name, new_column_name)
@@ -425,38 +439,34 @@ module ActiveRecord
# Adds a reference. Optionally adds a +type+ column, if <tt>:polymorphic</tt> option is provided.
# <tt>references</tt> and <tt>belongs_to</tt> are acceptable.
- # ===== Examples
- # t.references(:goat)
- # t.references(:goat, :polymorphic => true)
- # t.belongs_to(:goat)
+ #
+ # t.references(:user)
+ # t.belongs_to(:supplier, polymorphic: true)
+ #
def references(*args)
options = args.extract_options!
- polymorphic = options.delete(:polymorphic)
- args.each do |col|
- @base.add_column(@table_name, "#{col}_id", :integer, options)
- @base.add_column(@table_name, "#{col}_type", :string, polymorphic.is_a?(Hash) ? polymorphic : options) unless polymorphic.nil?
+ args.each do |ref_name|
+ @base.add_reference(@table_name, ref_name, options)
end
end
alias :belongs_to :references
# Removes a reference. Optionally removes a +type+ column.
# <tt>remove_references</tt> and <tt>remove_belongs_to</tt> are acceptable.
- # ===== Examples
- # t.remove_references(:goat)
- # t.remove_references(:goat, :polymorphic => true)
- # t.remove_belongs_to(:goat)
+ #
+ # t.remove_references(:user)
+ # t.remove_belongs_to(:supplier, polymorphic: true)
+ #
def remove_references(*args)
options = args.extract_options!
- polymorphic = options.delete(:polymorphic)
- args.each do |col|
- @base.remove_column(@table_name, "#{col}_id")
- @base.remove_column(@table_name, "#{col}_type") unless polymorphic.nil?
+ args.each do |ref_name|
+ @base.remove_reference(@table_name, ref_name, options)
end
end
- alias :remove_belongs_to :remove_references
+ alias :remove_belongs_to :remove_references
# Adds a column or columns of a specified type
- # ===== Examples
+ #
# t.string(:goat)
# t.string(:goat, :sheep)
%w( string text integer float decimal datetime timestamp time date binary boolean ).each do |column_type|
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb
new file mode 100644
index 0000000000..9d6111b51e
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb
@@ -0,0 +1,56 @@
+module ActiveRecord
+ module ConnectionAdapters # :nodoc:
+ # The goal of this module is to move Adapter specific column
+ # definitions to the Adapter instead of having it in the schema
+ # dumper itself. This code represents the normal case.
+ # We can then redefine how certain data types may be handled in the schema dumper on the
+ # Adapter level by over-writing this code inside the database spececific adapters
+ module ColumnDumper
+ def column_spec(column, types)
+ spec = prepare_column_options(column, types)
+ (spec.keys - [:name, :type]).each{ |k| spec[k].insert(0, "#{k.to_s}: ")}
+ spec
+ end
+
+ # This can be overridden on a Adapter level basis to support other
+ # extended datatypes (Example: Adding an array option in the
+ # PostgreSQLAdapter)
+ def prepare_column_options(column, types)
+ spec = {}
+ spec[:name] = column.name.inspect
+
+ # AR has an optimization which handles zero-scale decimals as integers. This
+ # code ensures that the dumper still dumps the column as a decimal.
+ spec[:type] = if column.type == :integer && /^(numeric|decimal)/ =~ column.sql_type
+ 'decimal'
+ else
+ column.type.to_s
+ end
+ spec[:limit] = column.limit.inspect if column.limit != types[column.type][:limit] && spec[:type] != 'decimal'
+ spec[:precision] = column.precision.inspect if column.precision
+ spec[:scale] = column.scale.inspect if column.scale
+ spec[:null] = 'false' unless column.null
+ spec[:default] = default_string(column.default) if column.has_default?
+ spec
+ end
+
+ # Lists the valid migration options
+ def migration_keys
+ [:name, :limit, :precision, :scale, :default, :null]
+ end
+
+ private
+
+ def default_string(value)
+ case value
+ when BigDecimal
+ value.to_s
+ when Date, DateTime, Time
+ "'#{value.to_s(:db)}'"
+ else
+ value.inspect
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
index 6bac05191d..86d6266af9 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
@@ -1,5 +1,3 @@
-require 'active_support/deprecation/reporting'
-require 'active_record/schema_migration'
require 'active_record/migration/join_table'
module ActiveRecord
@@ -16,12 +14,11 @@ module ActiveRecord
# Truncates a table alias according to the limits of the current adapter.
def table_alias_for(table_name)
- table_name[0...table_alias_length].gsub(/\./, '_')
+ table_name[0...table_alias_length].tr('.', '_')
end
# Checks to see if the table +table_name+ exists on the database.
#
- # === Example
# table_exists?(:developers)
def table_exists?(table_name)
tables.include?(table_name.to_s)
@@ -32,7 +29,6 @@ module ActiveRecord
# Checks to see if an index exists on a table for a given index definition.
#
- # === Examples
# # Check an index exists
# index_exists?(:suppliers, :company_id)
#
@@ -60,7 +56,6 @@ module ActiveRecord
# Checks to see if a column exists in a given table.
#
- # === Examples
# # Check a column exists
# column_exists?(:suppliers, :name)
#
@@ -68,13 +63,18 @@ module ActiveRecord
# column_exists?(:suppliers, :name, :string)
#
# # Check a column exists with a specific definition
- # column_exists?(:suppliers, :name, :string, :limit => 100)
+ # column_exists?(:suppliers, :name, :string, limit: 100)
+ # column_exists?(:suppliers, :name, :string, default: 'default')
+ # column_exists?(:suppliers, :name, :string, null: false)
+ # column_exists?(:suppliers, :tax, :decimal, precision: 8, scale: 2)
def column_exists?(table_name, column_name, type = nil, options = {})
columns(table_name).any?{ |c| c.name == column_name.to_s &&
- (!type || c.type == type) &&
- (!options[:limit] || c.limit == options[:limit]) &&
- (!options[:precision] || c.precision == options[:precision]) &&
- (!options[:scale] || c.scale == options[:scale]) }
+ (!type || c.type == type) &&
+ (!options.key?(:limit) || c.limit == options[:limit]) &&
+ (!options.key?(:precision) || c.precision == options[:precision]) &&
+ (!options.key?(:scale) || c.scale == options[:scale]) &&
+ (!options.key?(:default) || c.default == options[:default]) &&
+ (!options.key?(:null) || c.null == options[:null]) }
end
# Creates a new table with the name +table_name+. +table_name+ may either
@@ -126,7 +126,6 @@ module ActiveRecord
# Set to true to drop the table before creating it.
# Defaults to false.
#
- # ===== Examples
# ====== Add a backend specific option to the generated SQL (MySQL)
# create_table(:suppliers, :options => 'ENGINE=InnoDB DEFAULT CHARSET=utf8')
# generates:
@@ -171,10 +170,11 @@ module ActiveRecord
create_sql << td.to_sql
create_sql << ") #{options[:options]}"
execute create_sql
+ td.indexes.each_pair { |c,o| add_index table_name, c, o }
end
# Creates a new join table with the name created using the lexical order of the first two
- # arguments. These arguments can be be a String or a Symbol.
+ # arguments. These arguments can be a String or a Symbol.
#
# # Creates a table called 'assemblies_parts' with no id.
# create_join_table(:assemblies, :parts)
@@ -192,7 +192,6 @@ module ActiveRecord
# Set to true to drop the table before creating it.
# Defaults to false.
#
- # ===== Examples
# ====== Add a backend specific option to the generated SQL (MySQL)
# create_join_table(:assemblies, :parts, :options => 'ENGINE=InnoDB DEFAULT CHARSET=utf8')
# generates:
@@ -204,17 +203,19 @@ module ActiveRecord
join_table_name = find_join_table_name(table_1, table_2, options)
column_options = options.delete(:column_options) || {}
- column_options.reverse_merge!({:null => false})
+ column_options.reverse_merge!(null: false)
- create_table(join_table_name, options.merge!(:id => false)) do |td|
- td.integer :"#{table_1.to_s.singularize}_id", column_options
- td.integer :"#{table_2.to_s.singularize}_id", column_options
+ t1_column, t2_column = [table_1, table_2].map{ |t| t.to_s.singularize.foreign_key }
+
+ create_table(join_table_name, options.merge!(id: false)) do |td|
+ td.integer t1_column, column_options
+ td.integer t2_column, column_options
+ yield td if block_given?
end
end
# A block for changing columns in +table+.
#
- # === Example
# # change_table() yields a Table instance
# change_table(:suppliers) do |t|
# t.column :name, :string, :limit => 60
@@ -228,7 +229,6 @@ module ActiveRecord
#
# Defaults to false.
#
- # ===== Examples
# ====== Add a column
# change_table(:suppliers) do |t|
# t.column :name, :string, :limit => 60
@@ -287,7 +287,7 @@ module ActiveRecord
end
# Renames a table.
- # ===== Example
+ #
# rename_table('octopuses', 'octopi')
def rename_table(table_name, new_name)
raise NotImplementedError, "rename_table is not implemented"
@@ -307,7 +307,7 @@ module ActiveRecord
end
# Removes the column(s) from the table definition.
- # ===== Examples
+ #
# remove_column(:suppliers, :qualification)
# remove_columns(:suppliers, :qualification, :experience)
def remove_column(table_name, *column_names)
@@ -317,7 +317,7 @@ module ActiveRecord
# Changes the column's definition according to the new options.
# See TableDefinition#column for details of the options you can use.
- # ===== Examples
+ #
# change_column(:suppliers, :name, :string, :limit => 80)
# change_column(:accounts, :description, :text)
def change_column(table_name, column_name, type, options = {})
@@ -325,7 +325,7 @@ module ActiveRecord
end
# Sets a new default value for a column.
- # ===== Examples
+ #
# change_column_default(:suppliers, :qualification, 'new')
# change_column_default(:accounts, :authorized, 1)
# change_column_default(:users, :email, nil)
@@ -334,7 +334,7 @@ module ActiveRecord
end
# Renames a column.
- # ===== Example
+ #
# rename_column(:suppliers, :description, :name)
def rename_column(table_name, column_name, new_column_name)
raise NotImplementedError, "rename_column is not implemented"
@@ -346,8 +346,6 @@ module ActiveRecord
# The index will be named after the table and the column name(s), unless
# you pass <tt>:name</tt> as an option.
#
- # ===== Examples
- #
# ====== Creating a simple index
# add_index(:suppliers, :name)
# generates
@@ -375,7 +373,7 @@ module ActiveRecord
# Note: SQLite doesn't support index length
#
# ====== Creating an index with a sort order (desc or asc, asc is the default)
- # add_index(:accounts, [:branch_id, :party_id, :surname], :order => {:branch_id => :desc, :part_id => :asc})
+ # add_index(:accounts, [:branch_id, :party_id, :surname], :order => {:branch_id => :desc, :party_id => :asc})
# generates
# CREATE INDEX by_branch_desc_party ON accounts(branch_id DESC, party_id ASC, surname)
#
@@ -447,6 +445,42 @@ module ActiveRecord
indexes(table_name).detect { |i| i.name == index_name }
end
+ # Adds a reference. Optionally adds a +type+ column, if <tt>:polymorphic</tt> option is provided.
+ # <tt>add_reference</tt> and <tt>add_belongs_to</tt> are acceptable.
+ #
+ # ====== Create a user_id column
+ # add_reference(:products, :user)
+ #
+ # ====== Create a supplier_id and supplier_type columns
+ # add_belongs_to(:products, :supplier, polymorphic: true)
+ #
+ # ====== Create a supplier_id, supplier_type columns and appropriate index
+ # add_reference(:products, :supplier, polymorphic: true, index: true)
+ #
+ def add_reference(table_name, ref_name, options = {})
+ polymorphic = options.delete(:polymorphic)
+ 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
+ end
+ alias :add_belongs_to :add_reference
+
+ # Removes the reference(s). Also removes a +type+ column if one exists.
+ # <tt>remove_reference</tt>, <tt>remove_references</tt> and <tt>remove_belongs_to</tt> are acceptable.
+ #
+ # ====== Remove the reference
+ # remove_reference(:products, :user, index: true)
+ #
+ # ====== Remove polymorphic reference
+ # remove_reference(:products, :supplier, polymorphic: true)
+ #
+ def remove_reference(table_name, ref_name, options = {})
+ remove_column(table_name, "#{ref_name}_id")
+ remove_column(table_name, "#{ref_name}_type") if options[:polymorphic]
+ end
+ alias :remove_belongs_to :remove_reference
+
# Returns a string of <tt>CREATE TABLE</tt> SQL statement(s) for recreating the
# entire structure of the database.
def structure_dump
@@ -455,7 +489,7 @@ module ActiveRecord
def dump_schema_information #:nodoc:
sm_table = ActiveRecord::Migrator.schema_migrations_table_name
- ActiveRecord::SchemaMigration.order('version').all.map { |sm|
+ ActiveRecord::SchemaMigration.order('version').map { |sm|
"INSERT INTO #{sm_table} (version) VALUES ('#{sm.version}');"
}.join "\n\n"
end
@@ -536,15 +570,15 @@ module ActiveRecord
end
# Adds timestamps (created_at and updated_at) columns to the named table.
- # ===== Examples
+ #
# add_timestamps(:suppliers)
def add_timestamps(table_name)
- add_column table_name, :created_at, :datetime, :null => false
- add_column table_name, :updated_at, :datetime, :null => false
+ add_column table_name, :created_at, :datetime
+ add_column table_name, :updated_at, :datetime
end
# Removes the timestamp columns (created_at and updated_at) from the table definition.
- # ===== Examples
+ #
# remove_timestamps(:suppliers)
def remove_timestamps(table_name)
remove_column table_name, :updated_at
@@ -556,7 +590,7 @@ module ActiveRecord
if options.is_a?(Hash) && order = options[:order]
case order
when Hash
- column_names.each {|name| option_strings[name] += " #{order[name].to_s.upcase}" if order.has_key?(name)}
+ column_names.each {|name| option_strings[name] += " #{order[name].upcase}" if order.has_key?(name)}
when String
column_names.each {|name| option_strings[name] += " #{order.upcase}"}
end
@@ -617,8 +651,6 @@ module ActiveRecord
end
def columns_for_remove(table_name, *column_names)
- column_names = column_names.flatten
-
raise ArgumentError.new("You must specify at least one column name. Example: remove_column(:people, :first_name)") if column_names.blank?
column_names.map {|column_name| quote_column_name(column_name) }
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb
new file mode 100644
index 0000000000..2117eae5cb
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb
@@ -0,0 +1,156 @@
+module ActiveRecord
+ module ConnectionAdapters
+ class Transaction #:nodoc:
+ attr_reader :connection
+
+ def initialize(connection)
+ @connection = connection
+ end
+ end
+
+ class ClosedTransaction < Transaction #:nodoc:
+ def number
+ 0
+ end
+
+ def begin
+ RealTransaction.new(connection, self)
+ end
+
+ def closed?
+ true
+ end
+
+ def open?
+ false
+ end
+
+ def joinable?
+ false
+ end
+
+ # This is a noop when there are no open transactions
+ def add_record(record)
+ end
+ end
+
+ class OpenTransaction < Transaction #:nodoc:
+ attr_reader :parent, :records
+ attr_writer :joinable
+
+ def initialize(connection, parent)
+ super connection
+
+ @parent = parent
+ @records = []
+ @finishing = false
+ @joinable = true
+ end
+
+ # This state is necesarry so that we correctly handle stuff that might
+ # happen in a commit/rollback. But it's kinda distasteful. Maybe we can
+ # find a better way to structure it in the future.
+ def finishing?
+ @finishing
+ end
+
+ def joinable?
+ @joinable && !finishing?
+ end
+
+ def number
+ if finishing?
+ parent.number
+ else
+ parent.number + 1
+ end
+ end
+
+ def begin
+ if finishing?
+ parent.begin
+ else
+ SavepointTransaction.new(connection, self)
+ end
+ end
+
+ def rollback
+ @finishing = true
+ perform_rollback
+ parent
+ end
+
+ def commit
+ @finishing = true
+ perform_commit
+ parent
+ end
+
+ def add_record(record)
+ records << record
+ end
+
+ def rollback_records
+ records.uniq.each do |record|
+ begin
+ record.rolledback!(parent.closed?)
+ rescue => e
+ record.logger.error(e) if record.respond_to?(:logger) && record.logger
+ end
+ end
+ end
+
+ def commit_records
+ records.uniq.each do |record|
+ begin
+ record.committed!
+ rescue => e
+ record.logger.error(e) if record.respond_to?(:logger) && record.logger
+ end
+ end
+ end
+
+ def closed?
+ false
+ end
+
+ def open?
+ true
+ end
+ end
+
+ class RealTransaction < OpenTransaction #:nodoc:
+ def initialize(connection, parent)
+ super
+ connection.begin_db_transaction
+ end
+
+ def perform_rollback
+ connection.rollback_db_transaction
+ rollback_records
+ end
+
+ def perform_commit
+ connection.commit_db_transaction
+ commit_records
+ end
+ end
+
+ class SavepointTransaction < OpenTransaction #:nodoc:
+ def initialize(connection, parent)
+ super
+ connection.create_savepoint
+ end
+
+ def perform_rollback
+ connection.rollback_to_savepoint
+ rollback_records
+ end
+
+ def perform_commit
+ connection.release_savepoint
+ records.each { |r| parent.add_record(r) }
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
index c6af682d28..3a8fbcf93f 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
@@ -2,9 +2,10 @@ require 'date'
require 'bigdecimal'
require 'bigdecimal/util'
require 'active_support/core_ext/benchmark'
-require 'active_support/deprecation'
require 'active_record/connection_adapters/schema_cache'
+require 'active_record/connection_adapters/abstract/schema_dumper'
require 'monitor'
+require 'active_support/deprecation'
module ActiveRecord
module ConnectionAdapters # :nodoc:
@@ -34,6 +35,12 @@ module ActiveRecord
autoload :QueryCache
end
+ autoload_at 'active_record/connection_adapters/abstract/transaction' do
+ autoload :ClosedTransaction
+ autoload :RealTransaction
+ autoload :SavepointTransaction
+ end
+
# Active Record supports multiple database systems. AbstractAdapter and
# related classes form the abstraction layer which makes this possible.
# An AbstractAdapter represents a connection to a database, and provides an
@@ -53,23 +60,22 @@ module ActiveRecord
include QueryCache
include ActiveSupport::Callbacks
include MonitorMixin
+ include ColumnDumper
define_callbacks :checkout, :checkin
attr_accessor :visitor, :pool
- attr_reader :schema_cache, :last_use, :in_use
+ attr_reader :schema_cache, :last_use, :in_use, :logger
alias :in_use? :in_use
def initialize(connection, logger = nil, pool = nil) #:nodoc:
super()
- @active = nil
@connection = connection
@in_use = false
@instrumenter = ActiveSupport::Notifications.instrumenter
@last_use = false
@logger = logger
- @open_transactions = 0
@pool = pool
@query_cache = Hash.new { |h,sql| h[sql] = {} }
@query_cache_enabled = false
@@ -86,6 +92,11 @@ module ActiveRecord
end
end
+ def schema_cache=(cache)
+ cache.connection = self
+ @schema_cache = cache
+ end
+
def expire
@in_use = false
end
@@ -158,11 +169,6 @@ module ActiveRecord
# QUOTING ==================================================
- # Override to return the quoted table name. Defaults to column quoting.
- def quote_table_name(name)
- quote_column_name(name)
- end
-
# Returns a bind substitution value given a +column+ and list of current
# +binds+
def substitute_at(column, index)
@@ -182,19 +188,21 @@ module ActiveRecord
# checking whether the database is actually capable of responding, i.e. whether
# the connection isn't stale.
def active?
- @active != false
end
# Disconnects from the database if already connected, and establishes a
- # new connection with the database.
+ # new connection with the database. Implementors should call super if they
+ # override the default implementation.
def reconnect!
- @active = true
+ clear_cache!
+ reset_transaction
end
# Disconnects from the database if already connected. Otherwise, this
# method does nothing.
def disconnect!
- @active = false
+ clear_cache!
+ reset_transaction
end
# Reset the state of this connection, directing the DBMS to clear
@@ -237,18 +245,21 @@ module ActiveRecord
@connection
end
- attr_reader :open_transactions
+ def open_transactions
+ @transaction.number
+ end
def increment_open_transactions
- @open_transactions += 1
+ ActiveSupport::Deprecation.warn "#increment_open_transactions is deprecated and has no effect"
end
def decrement_open_transactions
- @open_transactions -= 1
+ ActiveSupport::Deprecation.warn "#decrement_open_transactions is deprecated and has no effect"
end
def transaction_joinable=(joinable)
- @transaction_joinable = joinable
+ ActiveSupport::Deprecation.warn "#transaction_joinable= is deprecated. Please pass the :joinable option to #begin_transaction instead."
+ @transaction.joinable = joinable
end
def create_savepoint
@@ -279,26 +290,25 @@ module ActiveRecord
protected
- def log(sql, name = "SQL", binds = [])
- @instrumenter.instrument(
- "sql.active_record",
- :sql => sql,
- :name => name,
- :connection_id => object_id,
- :binds => binds) { yield }
- rescue Exception => e
- message = "#{e.class.name}: #{e.message}: #{sql}"
- @logger.debug message if @logger
- exception = translate_exception(e, message)
- exception.set_backtrace e.backtrace
- raise exception
- end
-
- def translate_exception(e, message)
- # override in derived class
- ActiveRecord::StatementInvalid.new(message)
- end
-
+ def log(sql, name = "SQL", binds = [])
+ @instrumenter.instrument(
+ "sql.active_record",
+ :sql => sql,
+ :name => name,
+ :connection_id => object_id,
+ :binds => binds) { yield }
+ rescue => e
+ message = "#{e.class.name}: #{e.message}: #{sql}"
+ @logger.error message if @logger
+ exception = translate_exception(e, message)
+ exception.set_backtrace e.backtrace
+ raise exception
+ end
+
+ def translate_exception(exception, message)
+ # override in derived class
+ ActiveRecord::StatementInvalid.new(message)
+ end
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 eec7efadc2..1126fe7fce 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
@@ -1,4 +1,3 @@
-require 'active_support/core_ext/object/blank'
require 'arel/visitors/bind_visitor'
module ActiveRecord
@@ -72,6 +71,8 @@ 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
@@ -251,7 +252,7 @@ module ActiveRecord
end
# MysqlAdapter has to free a result after using it, so we use this method to write
- # stuff in a abstract way without concerning ourselves about whether it needs to be
+ # stuff in an abstract way without concerning ourselves about whether it needs to be
# explicitly freed or not.
def execute_and_free(sql, name = nil) #:nodoc:
yield execute(sql, name)
@@ -264,19 +265,19 @@ module ActiveRecord
def begin_db_transaction
execute "BEGIN"
- rescue Exception
+ rescue
# Transactions aren't supported
end
def commit_db_transaction #:nodoc:
execute "COMMIT"
- rescue Exception
+ rescue
# Transactions aren't supported
end
def rollback_db_transaction #:nodoc:
execute "ROLLBACK"
- rescue Exception
+ rescue
# Transactions aren't supported
end
@@ -294,19 +295,10 @@ module ActiveRecord
# In the simple case, MySQL allows us to place JOINs directly into the UPDATE
# query. However, this does not allow for LIMIT, OFFSET and ORDER. To support
- # these, we must use a subquery. However, MySQL is too stupid to create a
- # temporary table for this automatically, so we have to give it some prompting
- # in the form of a subsubquery. Ugh!
+ # these, we must use a subquery.
def join_to_update(update, select) #:nodoc:
if select.limit || select.offset || select.orders.any?
- subsubselect = select.clone
- subsubselect.projections = [update.key]
-
- subselect = Arel::SelectManager.new(select.engine)
- subselect.project Arel.sql(update.key.name)
- subselect.from subsubselect.as('__active_record_temp')
-
- update.where update.key.in(subselect)
+ super
else
update.table select.source
update.wheres = select.constraints
@@ -322,10 +314,10 @@ module ActiveRecord
sql = "SHOW TABLES"
end
- select_all(sql).map { |table|
+ select_all(sql, 'SCHEMA').map { |table|
table.delete('Table_type')
sql = "SHOW CREATE TABLE #{quote_table_name(table.to_a.first.last)}"
- exec_without_stmt(sql).first['Create Table'] + ";\n\n"
+ exec_query(sql, 'SCHEMA').first['Create Table'] + ";\n\n"
}.join
end
@@ -375,7 +367,7 @@ module ActiveRecord
def tables(name = nil, database = nil, like = nil) #:nodoc:
sql = "SHOW TABLES "
- sql << "IN #{database} " if database
+ sql << "IN #{quote_table_name(database)} " if database
sql << "LIKE #{quote(like)}" if like
execute_and_free(sql, 'SCHEMA') do |result|
@@ -484,15 +476,26 @@ module ActiveRecord
# Maps logical Rails types to MySQL-specific data types.
def type_to_sql(type, limit = nil, precision = nil, scale = nil)
- return super unless type.to_s == 'integer'
-
- case limit
- when 1; 'tinyint'
- when 2; 'smallint'
- when 3; 'mediumint'
- when nil, 4, 11; 'int(11)' # compatibility with MySQL default
- when 5..8; 'bigint'
- else raise(ActiveRecordError, "No integer type has byte size #{limit}")
+ case type.to_s
+ when 'integer'
+ case limit
+ when 1; 'tinyint'
+ when 2; 'smallint'
+ when 3; 'mediumint'
+ when nil, 4, 11; 'int(11)' # compatibility with MySQL default
+ when 5..8; 'bigint'
+ else raise(ActiveRecordError, "No integer type has byte size #{limit}")
+ end
+ when 'text'
+ case limit
+ when 0..0xff; 'tinytext'
+ when nil, 0x100..0xffff; 'text'
+ when 0x10000..0xffffff; 'mediumtext'
+ when 0x1000000..0xffffffff; 'longtext'
+ else raise(ActiveRecordError, "No text type has character length #{limit}")
+ end
+ else
+ super
end
end
@@ -506,7 +509,7 @@ module ActiveRecord
# SHOW VARIABLES LIKE 'name'
def show_variable(name)
- variables = select_all("SHOW VARIABLES LIKE '#{name}'")
+ variables = select_all("SHOW VARIABLES LIKE '#{name}'", 'SCHEMA')
variables.first['Value'] unless variables.empty?
end
@@ -514,8 +517,8 @@ module ActiveRecord
def pk_and_sequence_for(table)
execute_and_free("SHOW CREATE TABLE #{quote_table_name(table)}", 'SCHEMA') do |result|
create_table = each_hash(result).first[:"Create Table"]
- if create_table.to_s =~ /PRIMARY KEY\s+\((.+)\)/
- keys = $1.split(",").map { |key| key.gsub(/[`"]/, "") }
+ if create_table.to_s =~ /PRIMARY KEY\s+(?:USING\s+\w+\s+)?\((.+)\)/
+ keys = $1.split(",").map { |key| key.delete('`"') }
keys.length == 1 ? [keys.first, nil] : nil
else
nil
@@ -547,6 +550,17 @@ module ActiveRecord
protected
+ # MySQL is too stupid to create a temporary table for use subquery, so we have
+ # to give it some prompting in the form of a subsubquery. Ugh!
+ def subquery_for(key, select)
+ subsubselect = select.clone
+ subsubselect.projections = [key]
+
+ subselect = Arel::SelectManager.new(select.engine)
+ subselect.project Arel.sql(key.name)
+ subselect.from subsubselect.as('__active_record_temp')
+ end
+
def add_index_length(option_strings, column_names, options = {})
if options.is_a?(Hash) && length = options[:length]
case length
@@ -617,7 +631,7 @@ module ActiveRecord
raise ActiveRecordError, "No such column: #{table_name}.#{column_name}"
end
- current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'")["Type"]
+ current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'", 'SCHEMA')["Type"]
rename_column_sql = "CHANGE #{quote_column_name(column_name)} #{quote_column_name(new_column_name)} #{current_type}"
add_column_options!(rename_column_sql, options)
rename_column_sql
diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb
index 78e54c4c9b..816b5e17c1 100644
--- a/activerecord/lib/active_record/connection_adapters/column.rb
+++ b/activerecord/lib/active_record/connection_adapters/column.rb
@@ -94,7 +94,7 @@ module ActiveRecord
case type
when :string, :text then value
- when :integer then value.to_i rescue value ? 1 : 0
+ when :integer then value.to_i
when :float then value.to_f
when :decimal then klass.value_to_decimal(value)
when :datetime, :timestamp then klass.string_to_time(value)
@@ -107,11 +107,14 @@ module ActiveRecord
end
def type_cast_code(var_name)
+ ActiveSupport::Deprecation.warn("Column#type_cast_code is deprecated in favor of" \
+ "using Column#type_cast only, and it is going to be removed in future Rails versions.")
+
klass = self.class.name
case type
when :string, :text then var_name
- when :integer then "(#{var_name}.to_i rescue #{var_name} ? 1 : 0)"
+ when :integer then "(#{var_name}.to_i)"
when :float then "#{var_name}.to_f"
when :decimal then "#{klass}.value_to_decimal(#{var_name})"
when :datetime, :timestamp then "#{klass}.string_to_time(#{var_name})"
@@ -120,6 +123,8 @@ module ActiveRecord
when :binary then "#{klass}.binary_to_string(#{var_name})"
when :boolean then "#{klass}.value_to_boolean(#{var_name})"
when :hstore then "#{klass}.string_to_hstore(#{var_name})"
+ when :inet, :cidr then "#{klass}.string_to_cidr(#{var_name})"
+ when :json then "#{klass}.string_to_json(#{var_name})"
else var_name
end
end
@@ -154,7 +159,7 @@ module ActiveRecord
def value_to_date(value)
if value.is_a?(String)
- return nil if value.empty?
+ return nil if value.blank?
fast_string_to_date(value) || fallback_string_to_date(value)
elsif value.respond_to?(:to_date)
value.to_date
@@ -165,16 +170,22 @@ module ActiveRecord
def string_to_time(string)
return string unless string.is_a?(String)
- return nil if string.empty?
+ return nil if string.blank?
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.empty?
+ return nil if string.blank?
+
+ dummy_time_string = "2000-01-01 #{string}"
- string_to_time "2000-01-01 #{string}"
+ fast_string_to_time(dummy_time_string) || begin
+ time_hash = Date._parse(dummy_time_string)
+ return nil if time_hash[:hour].nil?
+ new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction))
+ end
end
# convert something to a boolean
@@ -204,7 +215,7 @@ module ActiveRecord
# '0.123456' -> 123456
# '1.123456' -> 123456
def microseconds(time)
- ((time[:sec_fraction].to_f % 1) * 1_000_000).to_i
+ time[:sec_fraction] ? (time[:sec_fraction] * 1_000_000).to_i : 0
end
def new_date(year, mon, mday)
@@ -229,7 +240,7 @@ module ActiveRecord
# Doesn't handle time zones.
def fast_string_to_time(string)
if string =~ Format::ISO_DATETIME
- microsec = ($7.to_f * 1_000_000).to_i
+ microsec = ($7.to_r * 1_000_000).to_i
new_time $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, microsec
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/connection_specification.rb
index 8491d42b86..b9a61f7d91 100644
--- a/activerecord/lib/active_record/connection_adapters/connection_specification.rb
+++ b/activerecord/lib/active_record/connection_adapters/connection_specification.rb
@@ -1,3 +1,5 @@
+require 'uri'
+
module ActiveRecord
module ConnectionAdapters
class ConnectionSpecification #:nodoc:
@@ -70,7 +72,7 @@ module ActiveRecord
:port => config.port,
:database => config.path.sub(%r{^/},""),
:host => config.host }
- spec.reject!{ |_,value| !value }
+ spec.reject!{ |_,value| value.blank? }
if config.query
options = Hash[config.query.split("&").map{ |pair| pair.split("=") }].symbolize_keys
spec.merge!(options)
diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
index 3f45f23de8..328d080687 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
@@ -74,6 +74,7 @@ module ActiveRecord
end
def reconnect!
+ super
disconnect!
connect
end
@@ -82,6 +83,7 @@ module ActiveRecord
# Disconnects from the database if already connected.
# Otherwise, this method does nothing.
def disconnect!
+ super
unless @connection.nil?
@connection.close
@connection = nil
@@ -226,7 +228,7 @@ module ActiveRecord
end
alias :create :insert_sql
- def exec_insert(sql, name, binds)
+ def exec_insert(sql, name, binds, pk = nil, sequence_name = nil)
execute to_sql(sql, binds), name
end
@@ -253,6 +255,14 @@ module ActiveRecord
# By default, MySQL 'where id is null' selects the last inserted id.
# Turn this off. http://dev.rubyonrails.org/ticket/6778
variable_assignments = ['SQL_AUTO_IS_NULL=0']
+
+ # Make MySQL reject illegal values rather than truncating or
+ # blanking them. See
+ # http://dev.mysql.com/doc/refman/5.5/en/server-sql-mode.html#sqlmode_strict_all_tables
+ if @config.fetch(:strict, true)
+ variable_assignments << "SQL_MODE='STRICT_ALL_TABLES'"
+ end
+
encoding = @config[:encoding]
# make sure we set the encoding
@@ -260,7 +270,7 @@ module ActiveRecord
# increase timeout so mysql server doesn't disconnect us
wait_timeout = @config[:wait_timeout]
- wait_timeout = 2592000 unless wait_timeout.is_a?(Fixnum)
+ wait_timeout = 2147483 unless wait_timeout.is_a?(Fixnum)
variable_assignments << "@@wait_timeout = #{wait_timeout}"
execute("SET #{variable_assignments.join(', ')}", :skip_logging)
diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
index 724dbff1f0..0b936bbf39 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
@@ -7,8 +7,6 @@ require 'mysql'
class Mysql
class Time
- ###
- # This monkey patch is for test_additional_columns_from_join_table
def to_date
Date.new(year, month, day)
end
@@ -53,6 +51,7 @@ module ActiveRecord
# * <tt>:database</tt> - The name of the database. No default, must be provided.
# * <tt>:encoding</tt> - (Optional) Sets the client encoding by executing "SET NAMES <encoding>" after connection.
# * <tt>:reconnect</tt> - Defaults to false (See MySQL documentation: http://dev.mysql.com/doc/refman/5.0/en/auto-reconnect.html).
+ # * <tt>:strict</tt> - Defaults to true. Enable STRICT_ALL_TABLES. (See MySQL documentation: http://dev.mysql.com/doc/refman/5.5/en/server-sql-mode.html)
# * <tt>:sslca</tt> - Necessary to use MySQL with an SSL connection.
# * <tt>:sslkey</tt> - Necessary to use MySQL with an SSL connection.
# * <tt>:sslcert</tt> - Necessary to use MySQL with an SSL connection.
@@ -190,14 +189,15 @@ module ActiveRecord
end
def reconnect!
+ super
disconnect!
- clear_cache!
connect
end
# Disconnects from the database if already connected. Otherwise, this
# method does nothing.
def disconnect!
+ super
@connection.close rescue nil
end
@@ -214,7 +214,7 @@ module ActiveRecord
def select_rows(sql, name = nil)
@connection.query_with_result = true
- rows = exec_without_stmt(sql, name).rows
+ rows = exec_query(sql, name).rows
@connection.more_results && @connection.next_result # invoking stored procedures with CLIENT_MULTI_RESULTS requires this to tidy up else connection will be dropped
rows
end
@@ -278,31 +278,164 @@ module ActiveRecord
end
def exec_query(sql, name = 'SQL', binds = [])
- log(sql, name, binds) do
- exec_stmt(sql, name, binds) do |cols, stmt|
- ActiveRecord::Result.new(cols, stmt.to_a) if cols
- end
+ # If the configuration sets prepared_statements:false, binds will
+ # always be empty, since the bind variables will have been already
+ # substituted and removed from binds by BindVisitor, so this will
+ # effectively disable prepared statement usage completely.
+ if binds.empty?
+ result_set, affected_rows = exec_without_stmt(sql, name)
+ else
+ result_set, affected_rows = exec_stmt(sql, name, binds)
end
+
+ yield affected_rows if block_given?
+
+ result_set
end
def last_inserted_id(result)
@connection.insert_id
end
+ module Fields
+ class Type
+ def type; end
+
+ def type_cast_for_write(value)
+ value
+ end
+ end
+
+ class Identity < Type
+ def type_cast(value); value; end
+ end
+
+ class Integer < Type
+ def type_cast(value)
+ return if value.nil?
+
+ value.to_i rescue value ? 1 : 0
+ end
+ end
+
+ class Date < Type
+ def type; :date; end
+
+ def type_cast(value)
+ return if value.nil?
+
+ # FIXME: probably we can improve this since we know it is mysql
+ # specific
+ ConnectionAdapters::Column.value_to_date value
+ end
+ end
+
+ class DateTime < Type
+ def type; :datetime; end
+
+ def type_cast(value)
+ return if value.nil?
+
+ # FIXME: probably we can improve this since we know it is mysql
+ # specific
+ ConnectionAdapters::Column.string_to_time value
+ end
+ end
+
+ class Time < Type
+ def type; :time; end
+
+ def type_cast(value)
+ return if value.nil?
+
+ # FIXME: probably we can improve this since we know it is mysql
+ # specific
+ ConnectionAdapters::Column.string_to_dummy_time value
+ end
+ end
+
+ class Float < Type
+ def type; :float; end
+
+ def type_cast(value)
+ return if value.nil?
+
+ value.to_f
+ end
+ end
+
+ class Decimal < Type
+ def type_cast(value)
+ return if value.nil?
+
+ ConnectionAdapters::Column.value_to_decimal value
+ end
+ end
+
+ class Boolean < Type
+ def type_cast(value)
+ return if value.nil?
+
+ ConnectionAdapters::Column.value_to_boolean value
+ end
+ end
+
+ TYPES = {}
+
+ # Register an MySQL +type_id+ with a typcasting object in
+ # +type+.
+ def self.register_type(type_id, type)
+ TYPES[type_id] = type
+ end
+
+ def self.alias_type(new, old)
+ TYPES[new] = TYPES[old]
+ 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
+ alias_type Mysql::Field::TYPE_NEWDECIMAL, Mysql::Field::TYPE_LONG
+
+ register_type Mysql::Field::TYPE_VAR_STRING, Fields::Identity.new
+ register_type Mysql::Field::TYPE_BLOB, Fields::Identity.new
+ register_type Mysql::Field::TYPE_DATE, Fields::Date.new
+ register_type Mysql::Field::TYPE_DATETIME, Fields::DateTime.new
+ register_type Mysql::Field::TYPE_TIME, Fields::Time.new
+ register_type Mysql::Field::TYPE_FLOAT, Fields::Float.new
+
+ Mysql::Field.constants.grep(/TYPE/).map { |class_name|
+ Mysql::Field.const_get class_name
+ }.reject { |const| TYPES.key? const }.each do |const|
+ register_type const, Fields::Identity.new
+ end
+ end
+
def exec_without_stmt(sql, name = 'SQL') # :nodoc:
# Some queries, like SHOW CREATE TABLE don't work through the prepared
# statement API. For those queries, we need to use this method. :'(
log(sql, name) do
result = @connection.query(sql)
- cols = []
- rows = []
+ affected_rows = @connection.affected_rows
if result
- cols = result.fetch_fields.map { |field| field.name }
- rows = result.to_a
+ types = {}
+ result.fetch_fields.each { |field|
+ if field.decimals > 0
+ types[field.name] = Fields::Decimal.new
+ else
+ types[field.name] = Fields::TYPES.fetch(field.type) {
+ Fields::Identity.new
+ }
+ end
+ }
+ result_set = ActiveRecord::Result.new(types.keys, result.to_a, types)
result.free
+ else
+ result_set = ActiveRecord::Result.new([], [])
end
- ActiveRecord::Result.new(cols, rows)
+
+ [result_set, affected_rows]
end
end
@@ -320,16 +453,18 @@ module ActiveRecord
alias :create :insert_sql
def exec_delete(sql, name, binds)
- log(sql, name, binds) do
- exec_stmt(sql, name, binds) do |cols, stmt|
- stmt.affected_rows
- end
+ affected_rows = 0
+
+ exec_query(sql, name, binds) do |n|
+ affected_rows = n
end
+
+ affected_rows
end
alias :exec_update :exec_delete
def begin_db_transaction #:nodoc:
- exec_without_stmt "BEGIN"
+ exec_query "BEGIN"
rescue Mysql::Error
# Transactions aren't supported
end
@@ -338,41 +473,44 @@ module ActiveRecord
def exec_stmt(sql, name, binds)
cache = {}
- if binds.empty?
- stmt = @connection.prepare(sql)
- else
- cache = @statements[sql] ||= {
- :stmt => @connection.prepare(sql)
- }
- stmt = cache[:stmt]
- end
+ log(sql, name, binds) do
+ if binds.empty?
+ stmt = @connection.prepare(sql)
+ else
+ cache = @statements[sql] ||= {
+ :stmt => @connection.prepare(sql)
+ }
+ stmt = cache[:stmt]
+ end
- begin
- stmt.execute(*binds.map { |col, val| type_cast(val, col) })
- rescue Mysql::Error => e
- # Older versions of MySQL leave the prepared statement in a bad
- # place when an error occurs. To support older mysql versions, we
- # need to close the statement and delete the statement from the
- # cache.
- stmt.close
- @statements.delete sql
- raise e
- end
+ begin
+ stmt.execute(*binds.map { |col, val| type_cast(val, col) })
+ rescue Mysql::Error => e
+ # Older versions of MySQL leave the prepared statement in a bad
+ # place when an error occurs. To support older mysql versions, we
+ # need to close the statement and delete the statement from the
+ # cache.
+ stmt.close
+ @statements.delete sql
+ raise e
+ end
- cols = nil
- if metadata = stmt.result_metadata
- cols = cache[:cols] ||= metadata.fetch_fields.map { |field|
- field.name
- }
- end
+ cols = nil
+ if metadata = stmt.result_metadata
+ cols = cache[:cols] ||= metadata.fetch_fields.map { |field|
+ field.name
+ }
+ end
- result = yield [cols, stmt]
+ result_set = ActiveRecord::Result.new(cols, stmt.to_a) if cols
+ affected_rows = stmt.affected_rows
- stmt.result_metadata.free if cols
- stmt.free_result
- stmt.close if binds.empty?
+ stmt.result_metadata.free if cols
+ stmt.free_result
+ stmt.close if binds.empty?
- result
+ [result_set, affected_rows]
+ end
end
def connect
@@ -404,6 +542,13 @@ module ActiveRecord
# By default, MySQL 'where id is null' selects the last inserted id.
# Turn this off. http://dev.rubyonrails.org/ticket/6778
execute("SET SQL_AUTO_IS_NULL=0", :skip_logging)
+
+ # Make MySQL reject illegal values rather than truncating or
+ # blanking them. See
+ # http://dev.mysql.com/doc/refman/5.5/en/server-sql-mode.html#sqlmode_strict_all_tables
+ if @config.fetch(:strict, true)
+ execute("SET SQL_MODE='STRICT_ALL_TABLES'", :skip_logging)
+ end
end
def select(sql, name = nil, binds = [])
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb b/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb
new file mode 100644
index 0000000000..b7d24f2bb3
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb
@@ -0,0 +1,97 @@
+module ActiveRecord
+ module ConnectionAdapters
+ class PostgreSQLColumn < Column
+ module ArrayParser
+ private
+ # Loads pg_array_parser if available. String parsing can be
+ # performed quicker by a native extension, which will not create
+ # a large amount of Ruby objects that will need to be garbage
+ # collected. pg_array_parser has a C and Java extension
+ begin
+ require 'pg_array_parser'
+ include PgArrayParser
+ rescue LoadError
+ def parse_pg_array(string)
+ parse_data(string, 0)
+ end
+ end
+
+ def parse_data(string, index)
+ local_index = index
+ array = []
+ while(local_index < string.length)
+ case string[local_index]
+ when '{'
+ local_index,array = parse_array_contents(array, string, local_index + 1)
+ when '}'
+ return array
+ end
+ local_index += 1
+ end
+
+ array
+ end
+
+ def parse_array_contents(array, string, index)
+ is_escaping = false
+ is_quoted = false
+ was_quoted = false
+ current_item = ''
+
+ local_index = index
+ while local_index
+ token = string[local_index]
+ if is_escaping
+ current_item << token
+ is_escaping = false
+ else
+ if is_quoted
+ case token
+ when '"'
+ is_quoted = false
+ was_quoted = true
+ when "\\"
+ is_escaping = true
+ else
+ current_item << token
+ end
+ else
+ case token
+ when "\\"
+ is_escaping = true
+ when ','
+ add_item_to_array(array, current_item, was_quoted)
+ current_item = ''
+ was_quoted = false
+ when '"'
+ is_quoted = true
+ when '{'
+ internal_items = []
+ local_index,internal_items = parse_array_contents(internal_items, string, local_index + 1)
+ array.push(internal_items)
+ when '}'
+ add_item_to_array(array, current_item, was_quoted)
+ return local_index,array
+ else
+ current_item << token
+ end
+ end
+ end
+
+ local_index += 1
+ end
+ return local_index,array
+ end
+
+ def add_item_to_array(array, current_item, quoted)
+ if current_item.length == 0
+ elsif !quoted && current_item == 'NULL'
+ array.push nil
+ else
+ array.push current_item
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb b/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb
new file mode 100644
index 0000000000..62d091357d
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb
@@ -0,0 +1,124 @@
+module ActiveRecord
+ module ConnectionAdapters
+ class PostgreSQLColumn < Column
+ module Cast
+ def string_to_time(string)
+ return string unless String === string
+
+ case string
+ when 'infinity'; 1.0 / 0.0
+ when '-infinity'; -1.0 / 0.0
+ else
+ super
+ end
+ end
+
+ def hstore_to_string(object)
+ if Hash === object
+ object.map { |k,v|
+ "#{escape_hstore(k)}=>#{escape_hstore(v)}"
+ }.join ','
+ else
+ object
+ end
+ end
+
+ def string_to_hstore(string)
+ if string.nil?
+ nil
+ elsif String === string
+ Hash[string.scan(HstorePair).map { |k,v|
+ v = v.upcase == 'NULL' ? nil : v.gsub(/^"(.*)"$/,'\1').gsub(/\\(.)/, '\1')
+ k = k.gsub(/^"(.*)"$/,'\1').gsub(/\\(.)/, '\1')
+ [k,v]
+ }]
+ else
+ string
+ end
+ end
+
+ def json_to_string(object)
+ if Hash === object
+ ActiveSupport::JSON.encode(object)
+ else
+ object
+ end
+ end
+
+ def array_to_string(value, column, adapter, should_be_quoted = false)
+ casted_values = value.map do |val|
+ if String === val
+ if val == "NULL"
+ "\"#{val}\""
+ else
+ quote_and_escape(adapter.type_cast(val, column, true))
+ end
+ else
+ adapter.type_cast(val, column, true)
+ end
+ end
+ "{#{casted_values.join(',')}}"
+ end
+
+ def string_to_json(string)
+ if String === string
+ ActiveSupport::JSON.decode(string)
+ else
+ string
+ end
+ end
+
+ def string_to_cidr(string)
+ if string.nil?
+ nil
+ elsif String === string
+ IPAddr.new(string)
+ else
+ string
+ end
+ end
+
+ def cidr_to_string(object)
+ if IPAddr === object
+ "#{object.to_s}/#{object.instance_variable_get(:@mask_addr).to_s(2).count('1')}"
+ else
+ object
+ end
+ end
+
+ def string_to_array(string, oid)
+ parse_pg_array(string).map{|val| oid.type_cast val}
+ end
+
+ private
+
+ HstorePair = begin
+ quoted_string = /"[^"\\]*(?:\\.[^"\\]*)*"/
+ unquoted_string = /(?:\\.|[^\s,])[^\s=,\\]*(?:\\.[^\s=,\\]*|=[^,>])*/
+ /(#{quoted_string}|#{unquoted_string})\s*=>\s*(#{quoted_string}|#{unquoted_string})/
+ end
+
+ def escape_hstore(value)
+ if value.nil?
+ 'NULL'
+ else
+ if value == ""
+ '""'
+ else
+ '"%s"' % value.to_s.gsub(/(["\\])/, '\\\\\1')
+ end
+ end
+ end
+
+ def quote_and_escape(value)
+ case value
+ when "NULL"
+ value
+ else
+ "\"#{value.gsub(/"/,"\\\"")}\""
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb
new file mode 100644
index 0000000000..c8437c18cc
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb
@@ -0,0 +1,240 @@
+require 'active_support/deprecation'
+
+module ActiveRecord
+ module ConnectionAdapters
+ class PostgreSQLAdapter < AbstractAdapter
+ module DatabaseStatements
+ def explain(arel, binds = [])
+ sql = "EXPLAIN #{to_sql(arel, binds)}"
+ ExplainPrettyPrinter.new.pp(exec_query(sql, 'EXPLAIN', binds))
+ end
+
+ class ExplainPrettyPrinter # :nodoc:
+ # Pretty prints the result of a EXPLAIN in a way that resembles the output of the
+ # PostgreSQL shell:
+ #
+ # QUERY PLAN
+ # ------------------------------------------------------------------------------
+ # Nested Loop Left Join (cost=0.00..37.24 rows=8 width=0)
+ # Join Filter: (posts.user_id = users.id)
+ # -> Index Scan using users_pkey on users (cost=0.00..8.27 rows=1 width=4)
+ # Index Cond: (id = 1)
+ # -> Seq Scan on posts (cost=0.00..28.88 rows=8 width=4)
+ # Filter: (posts.user_id = 1)
+ # (6 rows)
+ #
+ def pp(result)
+ header = result.columns.first
+ lines = result.rows.map(&:first)
+
+ # We add 2 because there's one char of padding at both sides, note
+ # the extra hyphens in the example above.
+ width = [header, *lines].map(&:length).max + 2
+
+ pp = []
+
+ pp << header.center(width).rstrip
+ pp << '-' * width
+
+ pp += lines.map {|line| " #{line}"}
+
+ nrows = result.rows.length
+ rows_label = nrows == 1 ? 'row' : 'rows'
+ pp << "(#{nrows} #{rows_label})"
+
+ pp.join("\n") + "\n"
+ end
+ end
+
+ # Executes a SELECT query and returns an array of rows. Each row is an
+ # array of field values.
+ def select_rows(sql, name = nil)
+ select_raw(sql, name).last
+ end
+
+ # Executes an INSERT query and returns the new record's ID
+ def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
+ unless pk
+ # Extract the table from the insert sql. Yuck.
+ table_ref = extract_table_ref_from_insert_sql(sql)
+ pk = primary_key(table_ref) if table_ref
+ end
+
+ if pk && use_insert_returning?
+ select_value("#{sql} RETURNING #{quote_column_name(pk)}")
+ elsif pk
+ super
+ last_insert_id_value(sequence_name || default_sequence_name(table_ref, pk))
+ else
+ super
+ end
+ end
+
+ def create
+ super.insert
+ end
+
+ # create a 2D array representing the result set
+ def result_as_array(res) #:nodoc:
+ # check if we have any binary column and if they need escaping
+ ftypes = Array.new(res.nfields) do |i|
+ [i, res.ftype(i)]
+ end
+
+ rows = res.values
+ return rows unless ftypes.any? { |_, x|
+ x == BYTEA_COLUMN_TYPE_OID || x == MONEY_COLUMN_TYPE_OID
+ }
+
+ typehash = ftypes.group_by { |_, type| type }
+ binaries = typehash[BYTEA_COLUMN_TYPE_OID] || []
+ monies = typehash[MONEY_COLUMN_TYPE_OID] || []
+
+ rows.each do |row|
+ # unescape string passed BYTEA field (OID == 17)
+ binaries.each do |index, _|
+ row[index] = unescape_bytea(row[index])
+ end
+
+ # If this is a money type column and there are any currency symbols,
+ # then strip them off. Indeed it would be prettier to do this in
+ # PostgreSQLColumn.string_to_decimal but would break form input
+ # fields that call value_before_type_cast.
+ monies.each do |index, _|
+ data = row[index]
+ # Because money output is formatted according to the locale, there are two
+ # cases to consider (note the decimal separators):
+ # (1) $12,345,678.12
+ # (2) $12.345.678,12
+ case data
+ when /^-?\D+[\d,]+\.\d{2}$/ # (1)
+ data.gsub!(/[^-\d.]/, '')
+ when /^-?\D+[\d.]+,\d{2}$/ # (2)
+ data.gsub!(/[^-\d,]/, '').sub!(/,/, '.')
+ end
+ end
+ end
+ end
+
+ # Queries the database and returns the results in an Array-like object
+ def query(sql, name = nil) #:nodoc:
+ log(sql, name) do
+ result_as_array @connection.async_exec(sql)
+ end
+ end
+
+ # Executes an SQL statement, returning a PGresult object on success
+ # or raising a PGError exception otherwise.
+ def execute(sql, name = nil)
+ log(sql, name) do
+ @connection.async_exec(sql)
+ end
+ end
+
+ def substitute_at(column, index)
+ Arel::Nodes::BindParam.new "$#{index + 1}"
+ end
+
+ def exec_query(sql, name = 'SQL', binds = [])
+ log(sql, name, binds) do
+ result = binds.empty? ? exec_no_cache(sql, binds) :
+ exec_cache(sql, binds)
+
+ types = {}
+ result.fields.each_with_index do |fname, i|
+ ftype = result.ftype i
+ fmod = result.fmod i
+ types[fname] = OID::TYPE_MAP.fetch(ftype, fmod) { |oid, mod|
+ warn "unknown OID: #{fname}(#{oid}) (#{sql})"
+ OID::Identity.new
+ }
+ end
+
+ ret = ActiveRecord::Result.new(result.fields, result.values, types)
+ result.clear
+ return ret
+ end
+ end
+
+ def exec_delete(sql, name = 'SQL', binds = [])
+ log(sql, name, binds) do
+ result = binds.empty? ? exec_no_cache(sql, binds) :
+ exec_cache(sql, binds)
+ affected = result.cmd_tuples
+ result.clear
+ affected
+ end
+ end
+ alias :exec_update :exec_delete
+
+ def sql_for_insert(sql, pk, id_value, sequence_name, binds)
+ unless pk
+ # Extract the table from the insert sql. Yuck.
+ table_ref = extract_table_ref_from_insert_sql(sql)
+ pk = primary_key(table_ref) if table_ref
+ end
+
+ if pk && use_insert_returning?
+ sql = "#{sql} RETURNING #{quote_column_name(pk)}"
+ end
+
+ [sql, binds]
+ end
+
+ def exec_insert(sql, name, binds, pk = nil, sequence_name = nil)
+ val = exec_query(sql, name, binds)
+ if !use_insert_returning? && pk
+ unless sequence_name
+ table_ref = extract_table_ref_from_insert_sql(sql)
+ sequence_name = default_sequence_name(table_ref, pk)
+ return val unless sequence_name
+ end
+ last_insert_id_result(sequence_name)
+ else
+ val
+ end
+ end
+
+ # Executes an UPDATE query and returns the number of affected tuples.
+ def update_sql(sql, name = nil)
+ super.cmd_tuples
+ end
+
+ # Begins a transaction.
+ def begin_db_transaction
+ execute "BEGIN"
+ end
+
+ # Commits a transaction.
+ def commit_db_transaction
+ execute "COMMIT"
+ end
+
+ # Aborts a transaction.
+ def rollback_db_transaction
+ execute "ROLLBACK"
+ end
+
+ def outside_transaction?
+ ActiveSupport::Deprecation.warn(
+ "#outside_transaction? is deprecated. This method was only really used " \
+ "internally, but you can use #transaction_open? instead."
+ )
+ @connection.transaction_status == PGconn::PQTRANS_IDLE
+ end
+
+ def create_savepoint
+ execute("SAVEPOINT #{current_savepoint_name}")
+ end
+
+ def rollback_to_savepoint
+ execute("ROLLBACK TO SAVEPOINT #{current_savepoint_name}")
+ end
+
+ def release_savepoint
+ execute("RELEASE SAVEPOINT #{current_savepoint_name}")
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb
index c82afc232c..52344f61c0 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb
@@ -63,6 +63,21 @@ module ActiveRecord
end
end
+ class Array < Type
+ attr_reader :subtype
+ def initialize(subtype)
+ @subtype = subtype
+ end
+
+ def type_cast(value)
+ if String === value
+ ConnectionAdapters::PostgreSQLColumn.string_to_array value, @subtype
+ else
+ value
+ end
+ end
+ end
+
class Integer < Type
def type_cast(value)
return if value.nil?
@@ -137,6 +152,22 @@ module ActiveRecord
end
end
+ class Cidr < Type
+ def type_cast(value)
+ return if value.nil?
+
+ ConnectionAdapters::PostgreSQLColumn.string_to_cidr value
+ end
+ end
+
+ class Json < Type
+ def type_cast(value)
+ return if value.nil?
+
+ ConnectionAdapters::PostgreSQLColumn.string_to_json value
+ end
+ end
+
class TypeMap
def initialize
@mapping = {}
@@ -212,11 +243,10 @@ module ActiveRecord
# FIXME: why are we keeping these types as strings?
alias_type 'tsvector', 'text'
alias_type 'interval', 'text'
- alias_type 'cidr', 'text'
- alias_type 'inet', 'text'
- alias_type 'macaddr', 'text'
alias_type 'bit', 'text'
alias_type 'varbit', 'text'
+ alias_type 'macaddr', 'text'
+ alias_type 'uuid', 'text'
# FIXME: I don't think this is correct. We should probably be returning a parsed date,
# but the tests pass with a string returned.
@@ -237,6 +267,10 @@ module ActiveRecord
register_type 'polygon', OID::Identity.new
register_type 'circle', OID::Identity.new
register_type 'hstore', OID::Hstore.new
+ register_type 'json', OID::Json.new
+
+ register_type 'cidr', OID::Cidr.new
+ alias_type 'inet', 'cidr'
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
new file mode 100644
index 0000000000..37d43d891d
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
@@ -0,0 +1,141 @@
+module ActiveRecord
+ module ConnectionAdapters
+ class PostgreSQLAdapter < AbstractAdapter
+ module Quoting
+ # Escapes binary strings for bytea input to the database.
+ def escape_bytea(value)
+ PGconn.escape_bytea(value) if value
+ end
+
+ # Unescapes bytea output from a database to the binary string it represents.
+ # NOTE: This is NOT an inverse of escape_bytea! This is only to be used
+ # on escaped binary output from database drive.
+ def unescape_bytea(value)
+ PGconn.unescape_bytea(value) if value
+ end
+
+ # Quotes PostgreSQL-specific data types for SQL input.
+ def quote(value, column = nil) #:nodoc:
+ return super unless column
+
+ case value
+ when Array
+ if column.array
+ "'#{PostgreSQLColumn.array_to_string(value, column, self)}'"
+ else
+ super
+ end
+ when Hash
+ case column.sql_type
+ when 'hstore' then super(PostgreSQLColumn.hstore_to_string(value), column)
+ when 'json' then super(PostgreSQLColumn.json_to_string(value), column)
+ else super
+ end
+ when IPAddr
+ case column.sql_type
+ when 'inet', 'cidr' then super(PostgreSQLColumn.cidr_to_string(value), column)
+ else super
+ end
+ when Float
+ if value.infinite? && column.type == :datetime
+ "'#{value.to_s.downcase}'"
+ elsif value.infinite? || value.nan?
+ "'#{value.to_s}'"
+ else
+ super
+ end
+ when Numeric
+ return super unless column.sql_type == 'money'
+ # Not truly string input, so doesn't require (or allow) escape string syntax.
+ "'#{value}'"
+ when String
+ case column.sql_type
+ when 'bytea' then "'#{escape_bytea(value)}'"
+ when 'xml' then "xml '#{quote_string(value)}'"
+ when /^bit/
+ case value
+ when /^[01]*$/ then "B'#{value}'" # Bit-string notation
+ when /^[0-9A-F]*$/i then "X'#{value}'" # Hexadecimal notation
+ end
+ else
+ super
+ end
+ else
+ super
+ end
+ end
+
+ def type_cast(value, column, array_member = false)
+ return super(value, column) unless column
+
+ case value
+ when NilClass
+ if column.array && array_member
+ 'NULL'
+ elsif column.array
+ value
+ else
+ super(value, column)
+ end
+ when Array
+ return super(value, column) unless column.array
+ PostgreSQLColumn.array_to_string(value, column, self)
+ when String
+ return super(value, column) unless 'bytea' == column.sql_type
+ { :value => value, :format => 1 }
+ when Hash
+ case column.sql_type
+ when 'hstore' then PostgreSQLColumn.hstore_to_string(value)
+ when 'json' then PostgreSQLColumn.json_to_string(value)
+ else super(value, column)
+ end
+ when IPAddr
+ return super(value, column) unless ['inet','cidr'].includes? column.sql_type
+ PostgreSQLColumn.cidr_to_string(value)
+ else
+ super(value, column)
+ end
+ end
+
+ # Quotes strings for use in SQL input.
+ def quote_string(s) #:nodoc:
+ @connection.escape(s)
+ end
+
+ # Checks the following cases:
+ #
+ # - table_name
+ # - "table.name"
+ # - schema_name.table_name
+ # - schema_name."table.name"
+ # - "schema.name".table_name
+ # - "schema.name"."table.name"
+ def quote_table_name(name)
+ schema, name_part = extract_pg_identifier_from_name(name.to_s)
+
+ unless name_part
+ quote_column_name(schema)
+ else
+ table_name, name_part = extract_pg_identifier_from_name(name_part)
+ "#{quote_column_name(schema)}.#{quote_column_name(table_name)}"
+ end
+ end
+
+ # Quotes column names for use in SQL queries.
+ def quote_column_name(name) #:nodoc:
+ PGconn.quote_ident(name.to_s)
+ end
+
+ # Quote date/time values for use in SQL input. Includes microseconds
+ # if the value is a Time responding to usec.
+ def quoted_date(value) #:nodoc:
+ if value.acts_like?(:time) && value.respond_to?(:usec)
+ "#{super}.#{sprintf("%06d", value.usec)}"
+ else
+ super
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb b/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb
new file mode 100644
index 0000000000..16da3ea732
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb
@@ -0,0 +1,22 @@
+module ActiveRecord
+ module ConnectionAdapters
+ class PostgreSQLAdapter < AbstractAdapter
+ module ReferentialIntegrity
+ def supports_disable_referential_integrity? #:nodoc:
+ true
+ end
+
+ def disable_referential_integrity #:nodoc:
+ if supports_disable_referential_integrity? then
+ execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} DISABLE TRIGGER ALL" }.join(";"))
+ end
+ yield
+ ensure
+ if supports_disable_referential_integrity? then
+ execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} ENABLE TRIGGER ALL" }.join(";"))
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
new file mode 100644
index 0000000000..8a073bf878
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
@@ -0,0 +1,446 @@
+module ActiveRecord
+ module ConnectionAdapters
+ class PostgreSQLAdapter < AbstractAdapter
+ module SchemaStatements
+ # Drops the database specified on the +name+ attribute
+ # and creates it again using the provided +options+.
+ def recreate_database(name, options = {}) #:nodoc:
+ drop_database(name)
+ create_database(name, options)
+ end
+
+ # Create a new PostgreSQL database. Options include <tt>:owner</tt>, <tt>:template</tt>,
+ # <tt>:encoding</tt>, <tt>:collation</tt>, <tt>:ctype</tt>,
+ # <tt>:tablespace</tt>, and <tt>:connection_limit</tt> (note that MySQL uses
+ # <tt>:charset</tt> while PostgreSQL uses <tt>:encoding</tt>).
+ #
+ # Example:
+ # create_database config[:database], config
+ # create_database 'foo_development', :encoding => 'unicode'
+ def create_database(name, options = {})
+ options = options.reverse_merge(:encoding => "utf8")
+
+ option_string = options.symbolize_keys.sum do |key, value|
+ case key
+ when :owner
+ " OWNER = \"#{value}\""
+ when :template
+ " TEMPLATE = \"#{value}\""
+ when :encoding
+ " ENCODING = '#{value}'"
+ when :collation
+ " LC_COLLATE = '#{value}'"
+ when :ctype
+ " LC_CTYPE = '#{value}'"
+ when :tablespace
+ " TABLESPACE = \"#{value}\""
+ when :connection_limit
+ " CONNECTION LIMIT = #{value}"
+ else
+ ""
+ end
+ end
+
+ execute "CREATE DATABASE #{quote_table_name(name)}#{option_string}"
+ end
+
+ # Drops a PostgreSQL database.
+ #
+ # Example:
+ # drop_database 'matt_development'
+ def drop_database(name) #:nodoc:
+ execute "DROP DATABASE IF EXISTS #{quote_table_name(name)}"
+ end
+
+ # Returns the list of all tables in the schema search path or a specified schema.
+ def tables(name = nil)
+ query(<<-SQL, 'SCHEMA').map { |row| row[0] }
+ SELECT tablename
+ FROM pg_tables
+ WHERE schemaname = ANY (current_schemas(false))
+ SQL
+ end
+
+ # Returns true if table exists.
+ # If the schema is not specified as part of +name+ then it will only find tables within
+ # the current schema search path (regardless of permissions to access tables in other schemas)
+ def table_exists?(name)
+ schema, table = Utils.extract_schema_and_table(name.to_s)
+ return false unless table
+
+ binds = [[nil, table]]
+ binds << [nil, schema] if schema
+
+ exec_query(<<-SQL, 'SCHEMA').rows.first[0].to_i > 0
+ SELECT COUNT(*)
+ FROM pg_class c
+ LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
+ WHERE c.relkind in ('v','r')
+ AND c.relname = '#{table.gsub(/(^"|"$)/,'')}'
+ AND n.nspname = #{schema ? "'#{schema}'" : 'ANY (current_schemas(false))'}
+ SQL
+ end
+
+ # Returns true if schema exists.
+ def schema_exists?(name)
+ exec_query(<<-SQL, 'SCHEMA').rows.first[0].to_i > 0
+ SELECT COUNT(*)
+ FROM pg_namespace
+ WHERE nspname = '#{name}'
+ SQL
+ end
+
+ # Returns an array of indexes for the given table.
+ def indexes(table_name, name = nil)
+ result = query(<<-SQL, 'SCHEMA')
+ SELECT distinct i.relname, d.indisunique, d.indkey, pg_get_indexdef(d.indexrelid), t.oid
+ FROM pg_class t
+ INNER JOIN pg_index d ON t.oid = d.indrelid
+ INNER JOIN pg_class i ON d.indexrelid = i.oid
+ WHERE i.relkind = 'i'
+ AND d.indisprimary = 'f'
+ AND t.relname = '#{table_name}'
+ AND i.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname = ANY (current_schemas(false)) )
+ ORDER BY i.relname
+ SQL
+
+ result.map do |row|
+ index_name = row[0]
+ unique = row[1] == 't'
+ indkey = row[2].split(" ")
+ inddef = row[3]
+ oid = row[4]
+
+ columns = Hash[query(<<-SQL, "SCHEMA")]
+ SELECT a.attnum, a.attname
+ FROM pg_attribute a
+ WHERE a.attrelid = #{oid}
+ AND a.attnum IN (#{indkey.join(",")})
+ SQL
+
+ 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]
+
+ column_names.empty? ? nil : IndexDefinition.new(table_name, index_name, unique, column_names, [], orders, where)
+ end.compact
+ end
+
+ # Returns the list of all column definitions for a table.
+ def columns(table_name)
+ # Limit, precision, and scale are all handled by the superclass.
+ column_definitions(table_name).map do |column_name, type, default, notnull, oid, fmod|
+ oid = OID::TYPE_MAP.fetch(oid.to_i, fmod.to_i) {
+ OID::Identity.new
+ }
+ PostgreSQLColumn.new(column_name, default, oid, type, notnull == 'f')
+ end
+ end
+
+ # Returns the current database name.
+ def current_database
+ query('select current_database()', 'SCHEMA')[0][0]
+ end
+
+ # Returns the current schema name.
+ def current_schema
+ query('SELECT current_schema', 'SCHEMA')[0][0]
+ end
+
+ # Returns the current database encoding format.
+ def encoding
+ query(<<-end_sql, 'SCHEMA')[0][0]
+ SELECT pg_encoding_to_char(pg_database.encoding) FROM pg_database
+ WHERE pg_database.datname LIKE '#{current_database}'
+ end_sql
+ end
+
+ # Returns the current database collation.
+ def collation
+ query(<<-end_sql, 'SCHEMA')[0][0]
+ SELECT pg_database.datcollate FROM pg_database WHERE pg_database.datname LIKE '#{current_database}'
+ end_sql
+ end
+
+ # Returns the current database ctype.
+ def ctype
+ query(<<-end_sql, 'SCHEMA')[0][0]
+ SELECT pg_database.datctype FROM pg_database WHERE pg_database.datname LIKE '#{current_database}'
+ end_sql
+ end
+
+ # Returns an array of schema names.
+ def schema_names
+ query(<<-SQL, 'SCHEMA').flatten
+ SELECT nspname
+ FROM pg_namespace
+ WHERE nspname !~ '^pg_.*'
+ AND nspname NOT IN ('information_schema')
+ ORDER by nspname;
+ SQL
+ end
+
+ # Creates a schema for the given schema name.
+ def create_schema schema_name
+ execute "CREATE SCHEMA #{schema_name}"
+ end
+
+ # Drops the schema for the given schema name.
+ def drop_schema schema_name
+ execute "DROP SCHEMA #{schema_name} CASCADE"
+ end
+
+ # Sets the schema search path to a string of comma-separated schema names.
+ # Names beginning with $ have to be quoted (e.g. $user => '$user').
+ # See: http://www.postgresql.org/docs/current/static/ddl-schemas.html
+ #
+ # This should be not be called manually but set in database.yml.
+ def schema_search_path=(schema_csv)
+ if schema_csv
+ execute("SET search_path TO #{schema_csv}", 'SCHEMA')
+ @schema_search_path = schema_csv
+ end
+ end
+
+ # Returns the active schema search path.
+ def schema_search_path
+ @schema_search_path ||= query('SHOW search_path', 'SCHEMA')[0][0]
+ end
+
+ # Returns the current client message level.
+ def client_min_messages
+ query('SHOW client_min_messages', 'SCHEMA')[0][0]
+ end
+
+ # Set the client message level.
+ def client_min_messages=(level)
+ execute("SET client_min_messages TO '#{level}'", 'SCHEMA')
+ end
+
+ # Returns the sequence name for a table's primary key or some other specified key.
+ def default_sequence_name(table_name, pk = nil) #:nodoc:
+ result = serial_sequence(table_name, pk || 'id')
+ return nil unless result
+ result.split('.').last
+ rescue ActiveRecord::StatementInvalid
+ "#{table_name}_#{pk || 'id'}_seq"
+ end
+
+ def serial_sequence(table, column)
+ result = exec_query(<<-eosql, 'SCHEMA')
+ SELECT pg_get_serial_sequence('#{table}', '#{column}')
+ eosql
+ result.rows.first.first
+ end
+
+ # Resets the sequence of a table's primary key to the maximum value.
+ def reset_pk_sequence!(table, pk = nil, sequence = nil) #:nodoc:
+ unless pk and sequence
+ default_pk, default_sequence = pk_and_sequence_for(table)
+
+ pk ||= default_pk
+ sequence ||= default_sequence
+ end
+
+ if @logger && pk && !sequence
+ @logger.warn "#{table} has primary key #{pk} with no default sequence"
+ end
+
+ if pk && sequence
+ quoted_sequence = quote_table_name(sequence)
+
+ select_value <<-end_sql, 'SCHEMA'
+ SELECT setval('#{quoted_sequence}', (SELECT COALESCE(MAX(#{quote_column_name pk})+(SELECT increment_by FROM #{quoted_sequence}), (SELECT min_value FROM #{quoted_sequence})) FROM #{quote_table_name(table)}), false)
+ end_sql
+ end
+ end
+
+ # Returns a table's primary key and belonging sequence.
+ def pk_and_sequence_for(table) #:nodoc:
+ # First try looking for a sequence with a dependency on the
+ # given table's primary key.
+ result = query(<<-end_sql, 'SCHEMA')[0]
+ SELECT attr.attname, seq.relname
+ FROM pg_class seq,
+ pg_attribute attr,
+ pg_depend dep,
+ pg_namespace name,
+ pg_constraint cons
+ WHERE seq.oid = dep.objid
+ AND seq.relkind = 'S'
+ AND attr.attrelid = dep.refobjid
+ AND attr.attnum = dep.refobjsubid
+ AND attr.attrelid = cons.conrelid
+ AND attr.attnum = cons.conkey[1]
+ AND cons.contype = 'p'
+ AND dep.refobjid = '#{quote_table_name(table)}'::regclass
+ end_sql
+
+ if result.nil? or result.empty?
+ # If that fails, try parsing the primary key's default value.
+ # Support the 7.x and 8.0 nextval('foo'::text) as well as
+ # the 8.1+ nextval('foo'::regclass).
+ result = query(<<-end_sql, 'SCHEMA')[0]
+ SELECT attr.attname,
+ CASE
+ WHEN split_part(def.adsrc, '''', 2) ~ '.' THEN
+ substr(split_part(def.adsrc, '''', 2),
+ strpos(split_part(def.adsrc, '''', 2), '.')+1)
+ ELSE split_part(def.adsrc, '''', 2)
+ END
+ FROM pg_class t
+ JOIN pg_attribute attr ON (t.oid = attrelid)
+ JOIN pg_attrdef def ON (adrelid = attrelid AND adnum = attnum)
+ JOIN pg_constraint cons ON (conrelid = adrelid AND adnum = conkey[1])
+ WHERE t.oid = '#{quote_table_name(table)}'::regclass
+ AND cons.contype = 'p'
+ AND def.adsrc ~* 'nextval'
+ end_sql
+ end
+
+ [result.first, result.last]
+ rescue
+ nil
+ end
+
+ # Returns just a table's primary key
+ def primary_key(table)
+ row = exec_query(<<-end_sql, 'SCHEMA').rows.first
+ SELECT DISTINCT(attr.attname)
+ FROM pg_attribute attr
+ INNER JOIN pg_depend dep ON attr.attrelid = dep.refobjid AND attr.attnum = dep.refobjsubid
+ INNER JOIN pg_constraint cons ON attr.attrelid = cons.conrelid AND attr.attnum = cons.conkey[1]
+ WHERE cons.contype = 'p'
+ AND dep.refobjid = '#{table}'::regclass
+ end_sql
+
+ row && row.first
+ end
+
+ # Renames a table.
+ # Also renames a table's primary key sequence if the sequence name matches the
+ # Active Record default.
+ #
+ # Example:
+ # rename_table('octopuses', 'octopi')
+ def rename_table(name, new_name)
+ clear_cache!
+ execute "ALTER TABLE #{quote_table_name(name)} RENAME TO #{quote_table_name(new_name)}"
+ pk, seq = pk_and_sequence_for(new_name)
+ if seq == "#{name}_#{pk}_seq"
+ new_seq = "#{new_name}_#{pk}_seq"
+ execute "ALTER TABLE #{quote_table_name(seq)} RENAME TO #{quote_table_name(new_seq)}"
+ end
+ end
+
+ # Adds a new column to the named table.
+ # See TableDefinition#column for details of the options you can use.
+ def add_column(table_name, column_name, type, options = {})
+ clear_cache!
+ add_column_sql = "ALTER TABLE #{quote_table_name(table_name)} ADD COLUMN #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
+ add_column_options!(add_column_sql, options)
+
+ execute add_column_sql
+ end
+
+ # Changes the column of a table.
+ def change_column(table_name, column_name, type, options = {})
+ clear_cache!
+ quoted_table_name = quote_table_name(table_name)
+
+ execute "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quote_column_name(column_name)} TYPE #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
+
+ change_column_default(table_name, column_name, options[:default]) if options_include_default?(options)
+ change_column_null(table_name, column_name, options[:null], options[:default]) if options.key?(:null)
+ end
+
+ # Changes the default value of a table column.
+ def change_column_default(table_name, column_name, default)
+ clear_cache!
+ execute "ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} SET DEFAULT #{quote(default)}"
+ end
+
+ def change_column_null(table_name, column_name, null, default = nil)
+ clear_cache!
+ unless null || default.nil?
+ execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL")
+ end
+ execute("ALTER TABLE #{quote_table_name(table_name)} ALTER #{quote_column_name(column_name)} #{null ? 'DROP' : 'SET'} NOT NULL")
+ end
+
+ # Renames a column in a table.
+ def rename_column(table_name, column_name, new_column_name)
+ clear_cache!
+ execute "ALTER TABLE #{quote_table_name(table_name)} RENAME COLUMN #{quote_column_name(column_name)} TO #{quote_column_name(new_column_name)}"
+ end
+
+ def remove_index!(table_name, index_name) #:nodoc:
+ execute "DROP INDEX #{quote_table_name(index_name)}"
+ end
+
+ def rename_index(table_name, old_name, new_name)
+ execute "ALTER INDEX #{quote_column_name(old_name)} RENAME TO #{quote_table_name(new_name)}"
+ end
+
+ def index_name_length
+ 63
+ end
+
+ # Maps logical Rails types to PostgreSQL-specific data types.
+ def type_to_sql(type, limit = nil, precision = nil, scale = nil)
+ case type.to_s
+ when 'binary'
+ # PostgreSQL doesn't support limits on binary (bytea) columns.
+ # The hard limit is 1Gb, because of a 32-bit size field, and TOAST.
+ case limit
+ when nil, 0..0x3fffffff; super(type)
+ else raise(ActiveRecordError, "No binary type has byte size #{limit}.")
+ end
+ when 'integer'
+ return 'integer' unless limit
+
+ case limit
+ when 1, 2; 'smallint'
+ when 3, 4; 'integer'
+ when 5..8; 'bigint'
+ else raise(ActiveRecordError, "No integer type has byte size #{limit}. Use a numeric with precision 0 instead.")
+ end
+ when 'datetime'
+ return super unless precision
+
+ case precision
+ when 0..6; "timestamp(#{precision})"
+ else raise(ActiveRecordError, "No timestamp type has precision of #{precision}. The allowed range of precision is from 0 to 6")
+ end
+ else
+ super
+ end
+ 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")
+ def distinct(columns, orders) #:nodoc:
+ return "DISTINCT #{columns}" if orders.empty?
+
+ # Construct a clean list of column names from the ORDER BY clause, removing
+ # any ASC/DESC modifiers
+ order_columns = orders.collect do |s|
+ s = s.to_sql unless s.is_a?(String)
+ s.gsub(/\s+(ASC|DESC)\s*(NULLS\s+(FIRST|LAST)\s*)?/i, '')
+ end
+ order_columns.delete_if { |c| c.blank? }
+ order_columns = order_columns.zip((0...order_columns.size).to_a).map { |s,i| "#{s} AS alias_#{i}" }
+
+ "DISTINCT #{columns}, #{order_columns * ', '}"
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
index c675b64a26..761052a788 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
@@ -1,13 +1,20 @@
require 'active_record/connection_adapters/abstract_adapter'
-require 'active_support/core_ext/object/blank'
require 'active_record/connection_adapters/statement_pool'
require 'active_record/connection_adapters/postgresql/oid'
+require 'active_record/connection_adapters/postgresql/cast'
+require 'active_record/connection_adapters/postgresql/array_parser'
+require 'active_record/connection_adapters/postgresql/quoting'
+require 'active_record/connection_adapters/postgresql/schema_statements'
+require 'active_record/connection_adapters/postgresql/database_statements'
+require 'active_record/connection_adapters/postgresql/referential_integrity'
require 'arel/visitors/bind_visitor'
# Make sure we're using pg high enough for PGResult#values
gem 'pg', '~> 0.11'
require 'pg'
+require 'ipaddr'
+
module ActiveRecord
module ConnectionHandling
# Establishes a connection to the database that's used by all Active Record objects
@@ -16,8 +23,8 @@ module ActiveRecord
# Forward any unused config params to PGconn.connect.
[:statement_limit, :encoding, :min_messages, :schema_search_path,
- :schema_order, :adapter, :pool, :wait_timeout, :template,
- :reaping_frequency].each do |key|
+ :schema_order, :adapter, :pool, :checkout_timeout, :template,
+ :reaping_frequency, :insert_returning].each do |key|
conn_params.delete key
end
conn_params.delete_if { |k,v| v.nil? }
@@ -35,63 +42,24 @@ module ActiveRecord
module ConnectionAdapters
# PostgreSQL-specific extensions to column definitions in a table.
class PostgreSQLColumn < Column #:nodoc:
+ attr_accessor :array
# Instantiates a new PostgreSQL column definition in a table.
def initialize(name, default, oid_type, sql_type = nil, null = true)
@oid_type = oid_type
- super(name, self.class.extract_value_from_default(default), sql_type, null)
+ if sql_type =~ /\[\]$/
+ @array = true
+ super(name, self.class.extract_value_from_default(default), sql_type[0..sql_type.length - 3], null)
+ else
+ @array = false
+ super(name, self.class.extract_value_from_default(default), sql_type, null)
+ end
end
# :stopdoc:
class << self
+ include ConnectionAdapters::PostgreSQLColumn::Cast
+ include ConnectionAdapters::PostgreSQLColumn::ArrayParser
attr_accessor :money_precision
- def string_to_time(string)
- return string unless String === string
-
- case string
- when 'infinity' then 1.0 / 0.0
- when '-infinity' then -1.0 / 0.0
- else
- super
- end
- end
-
- def hstore_to_string(object)
- if Hash === object
- object.map { |k,v|
- "#{escape_hstore(k)}=>#{escape_hstore(v)}"
- }.join ','
- else
- object
- end
- end
-
- def string_to_hstore(string)
- if string.nil?
- nil
- elsif String === string
- Hash[string.scan(HstorePair).map { |k,v|
- v = v.upcase == 'NULL' ? nil : v.gsub(/^"(.*)"$/,'\1').gsub(/\\(.)/, '\1')
- k = k.gsub(/^"(.*)"$/,'\1').gsub(/\\(.)/, '\1')
- [k,v]
- }]
- else
- string
- end
- end
-
- private
- HstorePair = begin
- quoted_string = /"[^"\\]*(?:\\.[^"\\]*)*"/
- unquoted_string = /(?:\\.|[^\s,])[^\s=,\\]*(?:\\.[^\s=,\\]*|=[^,>])*/
- /(#{quoted_string}|#{unquoted_string})\s*=>\s*(#{quoted_string}|#{unquoted_string})/
- end
-
- def escape_hstore(value)
- value.nil? ? 'NULL'
- : value =~ /[=\s,>]/ ? '"%s"' % value.gsub(/(["\\])/, '\\\\\1')
- : value == "" ? '""'
- : value.to_s.gsub(/(["\\])/, '\\\\\1')
- end
end
# :startdoc:
@@ -146,6 +114,9 @@ module ActiveRecord
# Hstore
when /\A'(.*)'::hstore\z/
$1
+ # JSON
+ when /\A'(.*)'::json\z/
+ $1
# Object identifier types
when /\A-?\d+\z/
$1
@@ -164,83 +135,94 @@ module ActiveRecord
end
private
- def extract_limit(sql_type)
- case sql_type
- when /^bigint/i; 8
- when /^smallint/i; 2
- else super
+
+ def extract_limit(sql_type)
+ case sql_type
+ when /^bigint/i; 8
+ when /^smallint/i; 2
+ when /^timestamp/i; nil
+ else super
+ end
end
- end
- # Extracts the scale from PostgreSQL-specific data types.
- def extract_scale(sql_type)
- # Money type has a fixed scale of 2.
- sql_type =~ /^money/ ? 2 : super
- end
+ # Extracts the scale from PostgreSQL-specific data types.
+ def extract_scale(sql_type)
+ # Money type has a fixed scale of 2.
+ sql_type =~ /^money/ ? 2 : super
+ end
- # Extracts the precision from PostgreSQL-specific data types.
- def extract_precision(sql_type)
- if sql_type == 'money'
- self.class.money_precision
- else
- super
+ # Extracts the precision from PostgreSQL-specific data types.
+ def extract_precision(sql_type)
+ if sql_type == 'money'
+ self.class.money_precision
+ elsif sql_type =~ /timestamp/i
+ $1.to_i if sql_type =~ /\((\d+)\)/
+ else
+ super
+ end
end
- end
- # Maps PostgreSQL-specific data types to logical Rails types.
- def simplified_type(field_type)
- case field_type
- # Numeric and monetary types
- when /^(?:real|double precision)$/
- :float
- # Monetary types
- when 'money'
- :decimal
- when 'hstore'
- :hstore
- # Character types
- when /^(?:character varying|bpchar)(?:\(\d+\))?$/
- :string
- # Binary data types
- when 'bytea'
- :binary
- # Date/time types
- when /^timestamp with(?:out)? time zone$/
- :datetime
- when 'interval'
- :string
- # Geometric types
- when /^(?:point|line|lseg|box|"?path"?|polygon|circle)$/
- :string
- # Network address types
- when /^(?:cidr|inet|macaddr)$/
- :string
- # Bit strings
- when /^bit(?: varying)?(?:\(\d+\))?$/
- :string
- # XML type
- when 'xml'
- :xml
- # tsvector type
- when 'tsvector'
- :tsvector
- # Arrays
- when /^\D+\[\]$/
- :string
- # Object identifier types
- when 'oid'
- :integer
- # UUID type
- when 'uuid'
- :string
- # Small and big integer types
- when /^(?:small|big)int$/
- :integer
- # Pass through all types that are not specific to PostgreSQL.
- else
- super
+ # Maps PostgreSQL-specific data types to logical Rails types.
+ def simplified_type(field_type)
+ case field_type
+ # Numeric and monetary types
+ when /^(?:real|double precision)$/
+ :float
+ # Monetary types
+ when 'money'
+ :decimal
+ when 'hstore'
+ :hstore
+ # Network address types
+ when 'inet'
+ :inet
+ when 'cidr'
+ :cidr
+ when 'macaddr'
+ :macaddr
+ # Character types
+ when /^(?:character varying|bpchar)(?:\(\d+\))?$/
+ :string
+ # Binary data types
+ when 'bytea'
+ :binary
+ # Date/time types
+ when /^timestamp with(?:out)? time zone$/
+ :datetime
+ when /^interval(?:|\(\d+\))$/
+ :string
+ # Geometric types
+ when /^(?:point|line|lseg|box|"?path"?|polygon|circle)$/
+ :string
+ # Bit strings
+ when /^bit(?: varying)?(?:\(\d+\))?$/
+ :string
+ # XML type
+ when 'xml'
+ :xml
+ # tsvector type
+ when 'tsvector'
+ :tsvector
+ # Arrays
+ when /^\D+\[\]$/
+ :string
+ # Object identifier types
+ when 'oid'
+ :integer
+ # UUID type
+ when 'uuid'
+ :uuid
+ # JSON type
+ when 'json'
+ :json
+ # Small and big integer types
+ when /^(?:small|big)int$/
+ :integer
+ # Pass through all types that are not specific to PostgreSQL.
+ else
+ super
+ end
end
- end
end
# The PostgreSQL adapter works with the native C (https://bitbucket.org/ged/ruby-pg) driver.
@@ -259,6 +241,8 @@ module ActiveRecord
# <encoding></tt> call on the connection.
# * <tt>:min_messages</tt> - An optional client min messages that is used in a
# <tt>SET client_min_messages TO <min_messages></tt> call on the connection.
+ # * <tt>:insert_returning</tt> - An optional boolean to control the use or <tt>RETURNING</tt> for <tt>INSERT<tt> statements
+ # defaults to true.
#
# Any further options are used as connection parameters to libpq. See
# http://www.postgresql.org/docs/9.1/static/libpq-connect.html for the
@@ -267,6 +251,10 @@ module ActiveRecord
# In addition, default connection parameters of libpq can be set per environment variables.
# See http://www.postgresql.org/docs/9.1/static/libpq-envars.html .
class PostgreSQLAdapter < AbstractAdapter
+ class ColumnDefinition < ActiveRecord::ConnectionAdapters::ColumnDefinition
+ attr_accessor :array
+ end
+
class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition
def xml(*args)
options = args.extract_options!
@@ -281,33 +269,93 @@ module ActiveRecord
def hstore(name, options = {})
column(name, 'hstore', options)
end
+
+ def inet(name, options = {})
+ column(name, 'inet', options)
+ end
+
+ def cidr(name, options = {})
+ column(name, 'cidr', options)
+ end
+
+ def macaddr(name, options = {})
+ column(name, 'macaddr', options)
+ end
+
+ def uuid(name, options = {})
+ column(name, 'uuid', options)
+ end
+
+ def json(name, options = {})
+ column(name, 'json', options)
+ end
+
+ def column(name, type = nil, options = {})
+ super
+ column = self[name]
+ column.array = options[:array]
+
+ self
+ end
+
+ private
+
+ def new_column_definition(base, name, type)
+ definition = ColumnDefinition.new base, name, type
+ @columns << definition
+ @columns_hash[name] = definition
+ definition
+ end
end
ADAPTER_NAME = 'PostgreSQL'
NATIVE_DATABASE_TYPES = {
- :primary_key => "serial primary key",
- :string => { :name => "character varying", :limit => 255 },
- :text => { :name => "text" },
- :integer => { :name => "integer" },
- :float => { :name => "float" },
- :decimal => { :name => "decimal" },
- :datetime => { :name => "timestamp" },
- :timestamp => { :name => "timestamp" },
- :time => { :name => "time" },
- :date => { :name => "date" },
- :binary => { :name => "bytea" },
- :boolean => { :name => "boolean" },
- :xml => { :name => "xml" },
- :tsvector => { :name => "tsvector" },
- :hstore => { :name => "hstore" }
+ primary_key: "serial primary key",
+ string: { name: "character varying", limit: 255 },
+ text: { name: "text" },
+ integer: { name: "integer" },
+ float: { name: "float" },
+ decimal: { name: "decimal" },
+ datetime: { name: "timestamp" },
+ timestamp: { name: "timestamp" },
+ time: { name: "time" },
+ date: { name: "date" },
+ binary: { name: "bytea" },
+ boolean: { name: "boolean" },
+ xml: { name: "xml" },
+ tsvector: { name: "tsvector" },
+ hstore: { name: "hstore" },
+ inet: { name: "inet" },
+ cidr: { name: "cidr" },
+ macaddr: { name: "macaddr" },
+ uuid: { name: "uuid" },
+ json: { name: "json" }
}
+ include Quoting
+ include ReferentialIntegrity
+ include SchemaStatements
+ include DatabaseStatements
+
# Returns 'PostgreSQL' as adapter name for identification purposes.
def adapter_name
ADAPTER_NAME
end
+ # Adds `:array` option to the default set provided by the
+ # AbstractAdapter
+ def prepare_column_options(column, types)
+ spec = super
+ spec[:array] = 'true' if column.respond_to?(:array) && column.array
+ spec
+ end
+
+ # Adds `:array` as a valid migration key
+ def migration_keys
+ super + [:array]
+ end
+
# Returns +true+, since this connection adapter supports prepared statement
# caching.
def supports_statement_cache?
@@ -359,19 +407,20 @@ module ActiveRecord
end
private
- def cache
- @cache[Process.pid]
- end
- def dealloc(key)
- @connection.query "DEALLOCATE #{key}" if connection_active?
- end
+ def cache
+ @cache[Process.pid]
+ end
- def connection_active?
- @connection.status == PGconn::CONNECTION_OK
- rescue PGError
- false
- end
+ def dealloc(key)
+ @connection.query "DEALLOCATE #{key}" if connection_active?
+ end
+
+ def connection_active?
+ @connection.status == PGconn::CONNECTION_OK
+ rescue PGError
+ false
+ end
end
class BindSubstitution < Arel::Visitors::PostgreSQL # :nodoc:
@@ -406,6 +455,7 @@ module ActiveRecord
initialize_type_map
@local_tz = execute('SHOW TIME ZONE', 'SCHEMA').first["TimeZone"]
+ @use_insert_returning = @config.key?(:insert_returning) ? @config[:insert_returning] : true
end
# Clears the prepared statements cache.
@@ -415,14 +465,15 @@ module ActiveRecord
# Is this connection alive and ready for queries?
def active?
- @connection.status == PGconn::CONNECTION_OK
+ @connection.query 'SELECT 1'
+ true
rescue PGError
false
end
# Close then reopen the connection.
def reconnect!
- clear_cache!
+ super
@connection.reset
configure_connection
end
@@ -435,7 +486,7 @@ module ActiveRecord
# Disconnects from the database if already connected. Otherwise, this
# method does nothing.
def disconnect!
- clear_cache!
+ super
@connection.close rescue nil
end
@@ -481,109 +532,14 @@ module ActiveRecord
# Returns the configured supported identifier length supported by PostgreSQL
def table_alias_length
- @table_alias_length ||= query('SHOW max_identifier_length')[0][0].to_i
- end
-
- # QUOTING ==================================================
-
- # Escapes binary strings for bytea input to the database.
- def escape_bytea(value)
- PGconn.escape_bytea(value) if value
- end
-
- # Unescapes bytea output from a database to the binary string it represents.
- # NOTE: This is NOT an inverse of escape_bytea! This is only to be used
- # on escaped binary output from database drive.
- def unescape_bytea(value)
- PGconn.unescape_bytea(value) if value
- end
-
- # Quotes PostgreSQL-specific data types for SQL input.
- def quote(value, column = nil) #:nodoc:
- return super unless column
-
- case value
- when Hash
- case column.sql_type
- when 'hstore' then super(PostgreSQLColumn.hstore_to_string(value), column)
- else super
- end
- when Float
- return super unless value.infinite? && column.type == :datetime
- "'#{value.to_s.downcase}'"
- when Numeric
- return super unless column.sql_type == 'money'
- # Not truly string input, so doesn't require (or allow) escape string syntax.
- "'#{value}'"
- when String
- case column.sql_type
- when 'bytea' then "'#{escape_bytea(value)}'"
- when 'xml' then "xml '#{quote_string(value)}'"
- when /^bit/
- case value
- when /^[01]*$/ then "B'#{value}'" # Bit-string notation
- when /^[0-9A-F]*$/i then "X'#{value}'" # Hexadecimal notation
- end
- else
- super
- end
- else
- super
- end
- end
-
- def type_cast(value, column)
- return super unless column
-
- case value
- when String
- return super unless 'bytea' == column.sql_type
- { :value => value, :format => 1 }
- when Hash
- return super unless 'hstore' == column.sql_type
- PostgreSQLColumn.hstore_to_string(value)
- else
- super
- end
- end
-
- # Quotes strings for use in SQL input.
- def quote_string(s) #:nodoc:
- @connection.escape(s)
- end
-
- # Checks the following cases:
- #
- # - table_name
- # - "table.name"
- # - schema_name.table_name
- # - schema_name."table.name"
- # - "schema.name".table_name
- # - "schema.name"."table.name"
- def quote_table_name(name)
- schema, name_part = extract_pg_identifier_from_name(name.to_s)
-
- unless name_part
- quote_column_name(schema)
- else
- table_name, name_part = extract_pg_identifier_from_name(name_part)
- "#{quote_column_name(schema)}.#{quote_column_name(table_name)}"
- end
- end
-
- # Quotes column names for use in SQL queries.
- def quote_column_name(name) #:nodoc:
- PGconn.quote_ident(name.to_s)
+ @table_alias_length ||= query('SHOW max_identifier_length', 'SCHEMA')[0][0].to_i
end
- # Quote date/time values for use in SQL input. Includes microseconds
- # if the value is a Time responding to usec.
- def quoted_date(value) #:nodoc:
- if value.acts_like?(:time) && value.respond_to?(:usec)
- "#{super}.#{sprintf("%06d", value.usec)}"
- else
- super
+ def add_column_options!(sql, options)
+ if options[:array] || options[:column].try(:array)
+ sql << '[]'
end
+ super
end
# Set the authorized user for this session
@@ -592,610 +548,6 @@ module ActiveRecord
exec_query "SET SESSION AUTHORIZATION #{user}"
end
- # REFERENTIAL INTEGRITY ====================================
-
- def supports_disable_referential_integrity? #:nodoc:
- true
- end
-
- def disable_referential_integrity #:nodoc:
- if supports_disable_referential_integrity? then
- execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} DISABLE TRIGGER ALL" }.join(";"))
- end
- yield
- ensure
- if supports_disable_referential_integrity? then
- execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} ENABLE TRIGGER ALL" }.join(";"))
- end
- end
-
- # DATABASE STATEMENTS ======================================
-
- def explain(arel, binds = [])
- sql = "EXPLAIN #{to_sql(arel, binds)}"
- ExplainPrettyPrinter.new.pp(exec_query(sql, 'EXPLAIN', binds))
- end
-
- class ExplainPrettyPrinter # :nodoc:
- # Pretty prints the result of a EXPLAIN in a way that resembles the output of the
- # PostgreSQL shell:
- #
- # QUERY PLAN
- # ------------------------------------------------------------------------------
- # Nested Loop Left Join (cost=0.00..37.24 rows=8 width=0)
- # Join Filter: (posts.user_id = users.id)
- # -> Index Scan using users_pkey on users (cost=0.00..8.27 rows=1 width=4)
- # Index Cond: (id = 1)
- # -> Seq Scan on posts (cost=0.00..28.88 rows=8 width=4)
- # Filter: (posts.user_id = 1)
- # (6 rows)
- #
- def pp(result)
- header = result.columns.first
- lines = result.rows.map(&:first)
-
- # We add 2 because there's one char of padding at both sides, note
- # the extra hyphens in the example above.
- width = [header, *lines].map(&:length).max + 2
-
- pp = []
-
- pp << header.center(width).rstrip
- pp << '-' * width
-
- pp += lines.map {|line| " #{line}"}
-
- nrows = result.rows.length
- rows_label = nrows == 1 ? 'row' : 'rows'
- pp << "(#{nrows} #{rows_label})"
-
- pp.join("\n") + "\n"
- end
- end
-
- # Executes a SELECT query and returns an array of rows. Each row is an
- # array of field values.
- def select_rows(sql, name = nil)
- select_raw(sql, name).last
- end
-
- # Executes an INSERT query and returns the new record's ID
- def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
- unless pk
- # Extract the table from the insert sql. Yuck.
- table_ref = extract_table_ref_from_insert_sql(sql)
- pk = primary_key(table_ref) if table_ref
- end
-
- if pk
- select_value("#{sql} RETURNING #{quote_column_name(pk)}")
- else
- super
- end
- end
- alias :create :insert
-
- # create a 2D array representing the result set
- def result_as_array(res) #:nodoc:
- # check if we have any binary column and if they need escaping
- ftypes = Array.new(res.nfields) do |i|
- [i, res.ftype(i)]
- end
-
- rows = res.values
- return rows unless ftypes.any? { |_, x|
- x == BYTEA_COLUMN_TYPE_OID || x == MONEY_COLUMN_TYPE_OID
- }
-
- typehash = ftypes.group_by { |_, type| type }
- binaries = typehash[BYTEA_COLUMN_TYPE_OID] || []
- monies = typehash[MONEY_COLUMN_TYPE_OID] || []
-
- rows.each do |row|
- # unescape string passed BYTEA field (OID == 17)
- binaries.each do |index, _|
- row[index] = unescape_bytea(row[index])
- end
-
- # If this is a money type column and there are any currency symbols,
- # then strip them off. Indeed it would be prettier to do this in
- # PostgreSQLColumn.string_to_decimal but would break form input
- # fields that call value_before_type_cast.
- monies.each do |index, _|
- data = row[index]
- # Because money output is formatted according to the locale, there are two
- # cases to consider (note the decimal separators):
- # (1) $12,345,678.12
- # (2) $12.345.678,12
- case data
- when /^-?\D+[\d,]+\.\d{2}$/ # (1)
- data.gsub!(/[^-\d.]/, '')
- when /^-?\D+[\d.]+,\d{2}$/ # (2)
- data.gsub!(/[^-\d,]/, '').sub!(/,/, '.')
- end
- end
- end
- end
-
-
- # Queries the database and returns the results in an Array-like object
- def query(sql, name = nil) #:nodoc:
- log(sql, name) do
- result_as_array @connection.async_exec(sql)
- end
- end
-
- # Executes an SQL statement, returning a PGresult object on success
- # or raising a PGError exception otherwise.
- def execute(sql, name = nil)
- log(sql, name) do
- @connection.async_exec(sql)
- end
- end
-
- def substitute_at(column, index)
- Arel::Nodes::BindParam.new "$#{index + 1}"
- end
-
- class Result < ActiveRecord::Result
- def initialize(columns, rows, column_types)
- super(columns, rows)
- @column_types = column_types
- end
- end
-
- def exec_query(sql, name = 'SQL', binds = [])
- log(sql, name, binds) do
- result = binds.empty? ? exec_no_cache(sql, binds) :
- exec_cache(sql, binds)
-
- types = {}
- result.fields.each_with_index do |fname, i|
- ftype = result.ftype i
- fmod = result.fmod i
- types[fname] = OID::TYPE_MAP.fetch(ftype, fmod) { |oid, mod|
- warn "unknown OID: #{fname}(#{oid}) (#{sql})"
- OID::Identity.new
- }
- end
-
- ret = Result.new(result.fields, result.values, types)
- result.clear
- return ret
- end
- end
-
- def exec_delete(sql, name = 'SQL', binds = [])
- log(sql, name, binds) do
- result = binds.empty? ? exec_no_cache(sql, binds) :
- exec_cache(sql, binds)
- affected = result.cmd_tuples
- result.clear
- affected
- end
- end
- alias :exec_update :exec_delete
-
- def sql_for_insert(sql, pk, id_value, sequence_name, binds)
- unless pk
- # Extract the table from the insert sql. Yuck.
- table_ref = extract_table_ref_from_insert_sql(sql)
- pk = primary_key(table_ref) if table_ref
- end
-
- sql = "#{sql} RETURNING #{quote_column_name(pk)}" if pk
-
- [sql, binds]
- end
-
- # Executes an UPDATE query and returns the number of affected tuples.
- def update_sql(sql, name = nil)
- super.cmd_tuples
- end
-
- # Begins a transaction.
- def begin_db_transaction
- execute "BEGIN"
- end
-
- # Commits a transaction.
- def commit_db_transaction
- execute "COMMIT"
- end
-
- # Aborts a transaction.
- def rollback_db_transaction
- execute "ROLLBACK"
- end
-
- def outside_transaction?
- @connection.transaction_status == PGconn::PQTRANS_IDLE
- end
-
- def create_savepoint
- execute("SAVEPOINT #{current_savepoint_name}")
- end
-
- def rollback_to_savepoint
- execute("ROLLBACK TO SAVEPOINT #{current_savepoint_name}")
- end
-
- def release_savepoint
- execute("RELEASE SAVEPOINT #{current_savepoint_name}")
- end
-
- # SCHEMA STATEMENTS ========================================
-
- # Drops the database specified on the +name+ attribute
- # and creates it again using the provided +options+.
- def recreate_database(name, options = {}) #:nodoc:
- drop_database(name)
- create_database(name, options)
- end
-
- # Create a new PostgreSQL database. Options include <tt>:owner</tt>, <tt>:template</tt>,
- # <tt>:encoding</tt>, <tt>:tablespace</tt>, and <tt>:connection_limit</tt> (note that MySQL uses
- # <tt>:charset</tt> while PostgreSQL uses <tt>:encoding</tt>).
- #
- # Example:
- # create_database config[:database], config
- # create_database 'foo_development', :encoding => 'unicode'
- def create_database(name, options = {})
- options = options.reverse_merge(:encoding => "utf8")
-
- option_string = options.symbolize_keys.sum do |key, value|
- case key
- when :owner
- " OWNER = \"#{value}\""
- when :template
- " TEMPLATE = \"#{value}\""
- when :encoding
- " ENCODING = '#{value}'"
- when :tablespace
- " TABLESPACE = \"#{value}\""
- when :connection_limit
- " CONNECTION LIMIT = #{value}"
- else
- ""
- end
- end
-
- execute "CREATE DATABASE #{quote_table_name(name)}#{option_string}"
- end
-
- # Drops a PostgreSQL database.
- #
- # Example:
- # drop_database 'matt_development'
- def drop_database(name) #:nodoc:
- execute "DROP DATABASE IF EXISTS #{quote_table_name(name)}"
- end
-
- # Returns the list of all tables in the schema search path or a specified schema.
- def tables(name = nil)
- query(<<-SQL, 'SCHEMA').map { |row| row[0] }
- SELECT tablename
- FROM pg_tables
- WHERE schemaname = ANY (current_schemas(false))
- SQL
- end
-
- # Returns true if table exists.
- # If the schema is not specified as part of +name+ then it will only find tables within
- # the current schema search path (regardless of permissions to access tables in other schemas)
- def table_exists?(name)
- schema, table = Utils.extract_schema_and_table(name.to_s)
- return false unless table
-
- binds = [[nil, table]]
- binds << [nil, schema] if schema
-
- exec_query(<<-SQL, 'SCHEMA', binds).rows.first[0].to_i > 0
- SELECT COUNT(*)
- FROM pg_class c
- LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
- WHERE c.relkind in ('v','r')
- AND c.relname = $1
- AND n.nspname = #{schema ? '$2' : 'ANY (current_schemas(false))'}
- SQL
- end
-
- # Returns true if schema exists.
- def schema_exists?(name)
- exec_query(<<-SQL, 'SCHEMA', [[nil, name]]).rows.first[0].to_i > 0
- SELECT COUNT(*)
- FROM pg_namespace
- WHERE nspname = $1
- SQL
- end
-
- # Returns an array of indexes for the given table.
- def indexes(table_name, name = nil)
- result = query(<<-SQL, name)
- SELECT distinct i.relname, d.indisunique, d.indkey, pg_get_indexdef(d.indexrelid), t.oid
- FROM pg_class t
- INNER JOIN pg_index d ON t.oid = d.indrelid
- INNER JOIN pg_class i ON d.indexrelid = i.oid
- WHERE i.relkind = 'i'
- AND d.indisprimary = 'f'
- AND t.relname = '#{table_name}'
- AND i.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname = ANY (current_schemas(false)) )
- ORDER BY i.relname
- SQL
-
-
- result.map do |row|
- index_name = row[0]
- unique = row[1] == 't'
- indkey = row[2].split(" ")
- inddef = row[3]
- oid = row[4]
-
- columns = Hash[query(<<-SQL, "Columns for index #{row[0]} on #{table_name}")]
- SELECT a.attnum, a.attname
- FROM pg_attribute a
- WHERE a.attrelid = #{oid}
- AND a.attnum IN (#{indkey.join(",")})
- SQL
-
- 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]
-
- column_names.empty? ? nil : IndexDefinition.new(table_name, index_name, unique, column_names, [], orders, where)
- end.compact
- end
-
- # Returns the list of all column definitions for a table.
- def columns(table_name)
- # Limit, precision, and scale are all handled by the superclass.
- column_definitions(table_name).map do |column_name, type, default, notnull, oid, fmod|
- oid = OID::TYPE_MAP.fetch(oid.to_i, fmod.to_i) {
- OID::Identity.new
- }
- PostgreSQLColumn.new(column_name, default, oid, type, notnull == 'f')
- end
- end
-
- # Returns the current database name.
- def current_database
- query('select current_database()')[0][0]
- end
-
- # Returns the current schema name.
- def current_schema
- query('SELECT current_schema', 'SCHEMA')[0][0]
- end
-
- # Returns the current database encoding format.
- def encoding
- query(<<-end_sql)[0][0]
- SELECT pg_encoding_to_char(pg_database.encoding) FROM pg_database
- WHERE pg_database.datname LIKE '#{current_database}'
- end_sql
- end
-
- # Sets the schema search path to a string of comma-separated schema names.
- # Names beginning with $ have to be quoted (e.g. $user => '$user').
- # See: http://www.postgresql.org/docs/current/static/ddl-schemas.html
- #
- # This should be not be called manually but set in database.yml.
- def schema_search_path=(schema_csv)
- if schema_csv
- execute "SET search_path TO #{schema_csv}"
- @schema_search_path = schema_csv
- end
- end
-
- # Returns the active schema search path.
- def schema_search_path
- @schema_search_path ||= query('SHOW search_path', 'SCHEMA')[0][0]
- end
-
- # Returns the current client message level.
- def client_min_messages
- query('SHOW client_min_messages', 'SCHEMA')[0][0]
- end
-
- # Set the client message level.
- def client_min_messages=(level)
- execute("SET client_min_messages TO '#{level}'", 'SCHEMA')
- end
-
- # Returns the sequence name for a table's primary key or some other specified key.
- def default_sequence_name(table_name, pk = nil) #:nodoc:
- serial_sequence(table_name, pk || 'id').split('.').last
- rescue ActiveRecord::StatementInvalid
- "#{table_name}_#{pk || 'id'}_seq"
- end
-
- def serial_sequence(table, column)
- result = exec_query(<<-eosql, 'SCHEMA', [[nil, table], [nil, column]])
- SELECT pg_get_serial_sequence($1, $2)
- eosql
- result.rows.first.first
- end
-
- # Resets the sequence of a table's primary key to the maximum value.
- def reset_pk_sequence!(table, pk = nil, sequence = nil) #:nodoc:
- unless pk and sequence
- default_pk, default_sequence = pk_and_sequence_for(table)
-
- pk ||= default_pk
- sequence ||= default_sequence
- end
-
- if @logger && pk && !sequence
- @logger.warn "#{table} has primary key #{pk} with no default sequence"
- end
-
- if pk && sequence
- quoted_sequence = quote_table_name(sequence)
-
- select_value <<-end_sql, 'Reset sequence'
- SELECT setval('#{quoted_sequence}', (SELECT COALESCE(MAX(#{quote_column_name pk})+(SELECT increment_by FROM #{quoted_sequence}), (SELECT min_value FROM #{quoted_sequence})) FROM #{quote_table_name(table)}), false)
- end_sql
- end
- end
-
- # Returns a table's primary key and belonging sequence.
- def pk_and_sequence_for(table) #:nodoc:
- # First try looking for a sequence with a dependency on the
- # given table's primary key.
- result = query(<<-end_sql, 'PK and serial sequence')[0]
- SELECT attr.attname, seq.relname
- FROM pg_class seq,
- pg_attribute attr,
- pg_depend dep,
- pg_namespace name,
- pg_constraint cons
- WHERE seq.oid = dep.objid
- AND seq.relkind = 'S'
- AND attr.attrelid = dep.refobjid
- AND attr.attnum = dep.refobjsubid
- AND attr.attrelid = cons.conrelid
- AND attr.attnum = cons.conkey[1]
- AND cons.contype = 'p'
- AND dep.refobjid = '#{quote_table_name(table)}'::regclass
- end_sql
-
- if result.nil? or result.empty?
- # If that fails, try parsing the primary key's default value.
- # Support the 7.x and 8.0 nextval('foo'::text) as well as
- # the 8.1+ nextval('foo'::regclass).
- result = query(<<-end_sql, 'PK and custom sequence')[0]
- SELECT attr.attname,
- CASE
- WHEN split_part(def.adsrc, '''', 2) ~ '.' THEN
- substr(split_part(def.adsrc, '''', 2),
- strpos(split_part(def.adsrc, '''', 2), '.')+1)
- ELSE split_part(def.adsrc, '''', 2)
- END
- FROM pg_class t
- JOIN pg_attribute attr ON (t.oid = attrelid)
- JOIN pg_attrdef def ON (adrelid = attrelid AND adnum = attnum)
- JOIN pg_constraint cons ON (conrelid = adrelid AND adnum = conkey[1])
- WHERE t.oid = '#{quote_table_name(table)}'::regclass
- AND cons.contype = 'p'
- AND def.adsrc ~* 'nextval'
- end_sql
- end
-
- [result.first, result.last]
- rescue
- nil
- end
-
- # Returns just a table's primary key
- def primary_key(table)
- row = exec_query(<<-end_sql, 'SCHEMA', [[nil, table]]).rows.first
- SELECT DISTINCT(attr.attname)
- FROM pg_attribute attr
- INNER JOIN pg_depend dep ON attr.attrelid = dep.refobjid AND attr.attnum = dep.refobjsubid
- INNER JOIN pg_constraint cons ON attr.attrelid = cons.conrelid AND attr.attnum = cons.conkey[1]
- WHERE cons.contype = 'p'
- AND dep.refobjid = $1::regclass
- end_sql
-
- row && row.first
- end
-
- # Renames a table.
- #
- # Example:
- # rename_table('octopuses', 'octopi')
- def rename_table(name, new_name)
- clear_cache!
- execute "ALTER TABLE #{quote_table_name(name)} RENAME TO #{quote_table_name(new_name)}"
- end
-
- # Adds a new column to the named table.
- # See TableDefinition#column for details of the options you can use.
- def add_column(table_name, column_name, type, options = {})
- clear_cache!
- add_column_sql = "ALTER TABLE #{quote_table_name(table_name)} ADD COLUMN #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
- add_column_options!(add_column_sql, options)
-
- execute add_column_sql
- end
-
- # Changes the column of a table.
- def change_column(table_name, column_name, type, options = {})
- clear_cache!
- quoted_table_name = quote_table_name(table_name)
-
- execute "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quote_column_name(column_name)} TYPE #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
-
- change_column_default(table_name, column_name, options[:default]) if options_include_default?(options)
- change_column_null(table_name, column_name, options[:null], options[:default]) if options.key?(:null)
- end
-
- # Changes the default value of a table column.
- def change_column_default(table_name, column_name, default)
- clear_cache!
- execute "ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} SET DEFAULT #{quote(default)}"
- end
-
- def change_column_null(table_name, column_name, null, default = nil)
- clear_cache!
- unless null || default.nil?
- execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL")
- end
- execute("ALTER TABLE #{quote_table_name(table_name)} ALTER #{quote_column_name(column_name)} #{null ? 'DROP' : 'SET'} NOT NULL")
- end
-
- # Renames a column in a table.
- def rename_column(table_name, column_name, new_column_name)
- clear_cache!
- execute "ALTER TABLE #{quote_table_name(table_name)} RENAME COLUMN #{quote_column_name(column_name)} TO #{quote_column_name(new_column_name)}"
- end
-
- def remove_index!(table_name, index_name) #:nodoc:
- execute "DROP INDEX #{quote_table_name(index_name)}"
- end
-
- def rename_index(table_name, old_name, new_name)
- execute "ALTER INDEX #{quote_column_name(old_name)} RENAME TO #{quote_table_name(new_name)}"
- end
-
- def index_name_length
- 63
- end
-
- # Maps logical Rails types to PostgreSQL-specific data types.
- def type_to_sql(type, limit = nil, precision = nil, scale = nil)
- return super unless type.to_s == 'integer'
- return 'integer' unless limit
-
- case limit
- when 1, 2; 'smallint'
- when 3, 4; 'integer'
- when 5..8; 'bigint'
- else raise(ActiveRecordError, "No integer type has byte size #{limit}. Use a numeric with precision 0 instead.")
- 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")
- def distinct(columns, orders) #:nodoc:
- return "DISTINCT #{columns}" if orders.empty?
-
- # Construct a clean list of column names from the ORDER BY clause, removing
- # any ASC/DESC modifiers
- order_columns = orders.collect { |s| s.gsub(/\s+(ASC|DESC)\s*/i, '') }
- order_columns.delete_if { |c| c.blank? }
- order_columns = order_columns.zip((0...order_columns.size).to_a).map { |s,i| "#{s} AS alias_#{i}" }
-
- "DISTINCT #{columns}, #{order_columns * ', '}"
- end
-
module Utils
extend self
@@ -1215,17 +567,26 @@ module ActiveRecord
end
end
+ def use_insert_returning?
+ @use_insert_returning
+ end
+
protected
+
# Returns the version of the connected PostgreSQL server.
def postgresql_version
@connection.server_version
end
+ # See http://www.postgresql.org/docs/9.1/static/errcodes-appendix.html
+ FOREIGN_KEY_VIOLATION = "23503"
+ UNIQUE_VIOLATION = "23505"
+
def translate_exception(exception, message)
- case exception.message
- when /duplicate key value violates unique constraint/
+ case exception.result.try(:error_field, PGresult::PG_DIAG_SQLSTATE)
+ when UNIQUE_VIOLATION
RecordNotUnique.new(message, exception)
- when /violates foreign key constraint/
+ when FOREIGN_KEY_VIOLATION
InvalidForeignKey.new(message, exception)
else
super
@@ -1233,21 +594,30 @@ module ActiveRecord
end
private
- def initialize_type_map
- result = execute('SELECT oid, typname, typelem, typdelim FROM pg_type', 'SCHEMA')
- leaves, nodes = result.partition { |row| row['typelem'] == '0' }
- # populate the leaf nodes
- leaves.find_all { |row| OID.registered_type? row['typname'] }.each do |row|
- OID::TYPE_MAP[row['oid'].to_i] = OID::NAMES[row['typname']]
- end
+ def initialize_type_map
+ result = execute('SELECT oid, typname, typelem, typdelim, typinput FROM pg_type', 'SCHEMA')
+ leaves, nodes = result.partition { |row| row['typelem'] == '0' }
+
+ # populate the leaf nodes
+ leaves.find_all { |row| OID.registered_type? row['typname'] }.each do |row|
+ OID::TYPE_MAP[row['oid'].to_i] = OID::NAMES[row['typname']]
+ end
- # populate composite types
- nodes.find_all { |row| OID::TYPE_MAP.key? row['typelem'].to_i }.each do |row|
- vector = OID::Vector.new row['typdelim'], OID::TYPE_MAP[row['typelem'].to_i]
- OID::TYPE_MAP[row['oid'].to_i] = vector
+ arrays, nodes = nodes.partition { |row| row['typinput'] == 'array_in' }
+
+ # populate composite types
+ nodes.find_all { |row| OID::TYPE_MAP.key? row['typelem'].to_i }.each do |row|
+ vector = OID::Vector.new row['typdelim'], OID::TYPE_MAP[row['typelem'].to_i]
+ OID::TYPE_MAP[row['oid'].to_i] = vector
+ end
+
+ # populate array types
+ arrays.find_all { |row| OID::TYPE_MAP.key? row['typelem'].to_i }.each do |row|
+ array = OID::Array.new OID::TYPE_MAP[row['typelem'].to_i]
+ OID::TYPE_MAP[row['oid'].to_i] = array
+ end
end
- end
FEATURE_NOT_SUPPORTED = "0A000" # :nodoc:
@@ -1327,7 +697,7 @@ module ActiveRecord
if @config[:encoding]
@connection.set_client_encoding(@config[:encoding])
end
- self.client_min_messages = @config[:min_messages] if @config[:min_messages]
+ self.client_min_messages = @config[:min_messages] || 'warning'
self.schema_search_path = @config[:schema_search_path] || @config[:schema_order]
# Use standard-conforming strings if available so we don't have to do the E'...' dance.
@@ -1344,8 +714,15 @@ module ActiveRecord
# Returns the current ID of a table's sequence.
def last_insert_id(sequence_name) #:nodoc:
- r = exec_query("SELECT currval($1)", 'SQL', [[nil, sequence_name]])
- Integer(r.rows.first.first)
+ Integer(last_insert_id_value(sequence_name))
+ end
+
+ def last_insert_id_value(sequence_name)
+ last_insert_id_result(sequence_name).rows.first.first
+ end
+
+ def last_insert_id_result(sequence_name) #:nodoc:
+ exec_query("SELECT currval('#{sequence_name}')", 'SQL')
end
# Executes a SELECT query and returns the results, performing any data type
@@ -1382,12 +759,12 @@ module ActiveRecord
# - ::regclass is a function that gives the id for a table name
def column_definitions(table_name) #:nodoc:
exec_query(<<-end_sql, 'SCHEMA').rows
- SELECT a.attname, format_type(a.atttypid, a.atttypmod), d.adsrc, a.attnotnull, a.atttypid, a.atttypmod
- FROM pg_attribute a LEFT JOIN pg_attrdef d
- ON a.attrelid = d.adrelid AND a.attnum = d.adnum
- WHERE a.attrelid = '#{quote_table_name(table_name)}'::regclass
- AND a.attnum > 0 AND NOT a.attisdropped
- ORDER BY a.attnum
+ SELECT a.attname, format_type(a.atttypid, a.atttypmod), d.adsrc, a.attnotnull, a.atttypid, a.atttypmod
+ FROM pg_attribute a LEFT JOIN pg_attrdef d
+ ON a.attrelid = d.adrelid AND a.attnum = d.adnum
+ WHERE a.attrelid = '#{quote_table_name(table_name)}'::regclass
+ AND a.attnum > 0 AND NOT a.attisdropped
+ ORDER BY a.attnum
end_sql
end
diff --git a/activerecord/lib/active_record/connection_adapters/schema_cache.rb b/activerecord/lib/active_record/connection_adapters/schema_cache.rb
index 962718da56..aad1f9a7ef 100644
--- a/activerecord/lib/active_record/connection_adapters/schema_cache.rb
+++ b/activerecord/lib/active_record/connection_adapters/schema_cache.rb
@@ -1,26 +1,17 @@
module ActiveRecord
module ConnectionAdapters
class SchemaCache
- attr_reader :columns, :columns_hash, :primary_keys, :tables
- attr_reader :connection
+ attr_reader :columns, :columns_hash, :primary_keys, :tables, :version
+ attr_accessor :connection
def initialize(conn)
@connection = conn
- @tables = {}
- @columns = Hash.new do |h, table_name|
- h[table_name] = conn.columns(table_name)
- end
-
- @columns_hash = Hash.new do |h, table_name|
- h[table_name] = Hash[columns[table_name].map { |col|
- [col.name, col]
- }]
- end
-
- @primary_keys = Hash.new do |h, table_name|
- h[table_name] = table_exists?(table_name) ? conn.primary_key(table_name) : nil
- end
+ @columns = {}
+ @columns_hash = {}
+ @primary_keys = {}
+ @tables = {}
+ prepare_default_proc
end
# A cached lookup for table existence.
@@ -30,12 +21,22 @@ module ActiveRecord
@tables[name] = connection.table_exists?(name)
end
+ # Add internal cache for table with +table_name+.
+ def add(table_name)
+ if table_exists?(table_name)
+ @primary_keys[table_name]
+ @columns[table_name]
+ @columns_hash[table_name]
+ end
+ end
+
# Clears out internal caches
def clear!
@columns.clear
@columns_hash.clear
@primary_keys.clear
@tables.clear
+ @version = nil
end
# Clear out internal caches for table with +table_name+.
@@ -45,6 +46,37 @@ module ActiveRecord
@primary_keys.delete table_name
@tables.delete table_name
end
+
+ def marshal_dump
+ # if we get current version during initialization, it happens stack over flow.
+ @version = ActiveRecord::Migrator.current_version
+ [@version] + [:@columns, :@columns_hash, :@primary_keys, :@tables].map do |val|
+ self.instance_variable_get(val).inject({}) { |h, v| h[v[0]] = v[1]; h }
+ end
+ end
+
+ def marshal_load(array)
+ @version, @columns, @columns_hash, @primary_keys, @tables = array
+ prepare_default_proc
+ end
+
+ private
+
+ def prepare_default_proc
+ @columns.default_proc = Proc.new do |h, table_name|
+ h[table_name] = connection.columns(table_name)
+ end
+
+ @columns_hash.default_proc = Proc.new do |h, table_name|
+ h[table_name] = Hash[columns[table_name].map { |col|
+ [col.name, col]
+ }]
+ end
+
+ @primary_keys.default_proc = Proc.new do |h, table_name|
+ h[table_name] = table_exists?(table_name) ? connection.primary_key(table_name) : nil
+ end
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
index ee5d10859c..4a48812807 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
@@ -1,6 +1,8 @@
-require 'active_record/connection_adapters/sqlite_adapter'
+require 'active_record/connection_adapters/abstract_adapter'
+require 'active_record/connection_adapters/statement_pool'
+require 'arel/visitors/bind_visitor'
-gem 'sqlite3', '~> 1.3.5'
+gem 'sqlite3', '~> 1.3.6'
require 'sqlite3'
module ActiveRecord
@@ -19,10 +21,6 @@ module ActiveRecord
config[:database] = File.expand_path(config[:database], Rails.root)
end
- unless 'sqlite3' == config[:adapter]
- raise ArgumentError, 'adapter name should be "sqlite3"'
- end
-
db = SQLite3::Database.new(
config[:database],
:results_as_hash => true
@@ -35,7 +33,189 @@ module ActiveRecord
end
module ConnectionAdapters #:nodoc:
- class SQLite3Adapter < SQLiteAdapter # :nodoc:
+ class SQLite3Column < Column #:nodoc:
+ class << self
+ def binary_to_string(value)
+ if value.encoding != Encoding::ASCII_8BIT
+ value = value.force_encoding(Encoding::ASCII_8BIT)
+ end
+ value
+ end
+ end
+ end
+
+ # The SQLite3 adapter works SQLite 3.6.16 or newer
+ # with the sqlite3-ruby drivers (available as gem from https://rubygems.org/gems/sqlite3).
+ #
+ # Options:
+ #
+ # * <tt>:database</tt> - Path to the database file.
+ class SQLite3Adapter < AbstractAdapter
+ class Version
+ include Comparable
+
+ def initialize(version_string)
+ @version = version_string.split('.').map { |v| v.to_i }
+ end
+
+ def <=>(version_string)
+ @version <=> version_string.split('.').map { |v| v.to_i }
+ end
+ end
+
+ class StatementPool < ConnectionAdapters::StatementPool
+ def initialize(connection, max)
+ super
+ @cache = Hash.new { |h,pid| h[pid] = {} }
+ end
+
+ def each(&block); cache.each(&block); end
+ def key?(key); cache.key?(key); end
+ def [](key); cache[key]; end
+ def length; cache.length; end
+
+ def []=(sql, key)
+ while @max <= cache.size
+ dealloc(cache.shift.last[:stmt])
+ end
+ cache[sql] = key
+ end
+
+ def clear
+ cache.values.each do |hash|
+ dealloc hash[:stmt]
+ end
+ cache.clear
+ end
+
+ private
+ def cache
+ @cache[$$]
+ end
+
+ def dealloc(stmt)
+ stmt.close unless stmt.closed?
+ end
+ end
+
+ class BindSubstitution < Arel::Visitors::SQLite # :nodoc:
+ include Arel::Visitors::BindVisitor
+ end
+
+ def initialize(connection, logger, config)
+ super(connection, logger)
+
+ @active = nil
+ @statements = StatementPool.new(@connection,
+ config.fetch(:statement_limit) { 1000 })
+ @config = config
+
+ if config.fetch(:prepared_statements) { true }
+ @visitor = Arel::Visitors::SQLite.new self
+ else
+ @visitor = BindSubstitution.new self
+ end
+ end
+
+ def adapter_name #:nodoc:
+ 'SQLite'
+ end
+
+ # Returns true
+ def supports_ddl_transactions?
+ true
+ end
+
+ # Returns true if SQLite version is '3.6.8' or greater, false otherwise.
+ def supports_savepoints?
+ sqlite_version >= '3.6.8'
+ end
+
+ # Returns true, since this connection adapter supports prepared statement
+ # caching.
+ def supports_statement_cache?
+ true
+ end
+
+ # Returns true, since this connection adapter supports migrations.
+ def supports_migrations? #:nodoc:
+ true
+ end
+
+ # Returns true.
+ def supports_primary_key? #:nodoc:
+ true
+ end
+
+ def requires_reloading?
+ true
+ end
+
+ # Returns true
+ def supports_add_column?
+ true
+ end
+
+ def active?
+ @active != false
+ end
+
+ # Disconnects from the database if already connected. Otherwise, this
+ # method does nothing.
+ def disconnect!
+ super
+ @active = false
+ @connection.close rescue nil
+ end
+
+ # Clears the prepared statements cache.
+ def clear_cache!
+ @statements.clear
+ end
+
+ # Returns true
+ def supports_count_distinct? #:nodoc:
+ true
+ end
+
+ # Returns true
+ def supports_autoincrement? #:nodoc:
+ true
+ end
+
+ def supports_index_sort_order?
+ true
+ end
+
+ def native_database_types #:nodoc:
+ {
+ :primary_key => default_primary_key_type,
+ :string => { :name => "varchar", :limit => 255 },
+ :text => { :name => "text" },
+ :integer => { :name => "integer" },
+ :float => { :name => "float" },
+ :decimal => { :name => "decimal" },
+ :datetime => { :name => "datetime" },
+ :timestamp => { :name => "datetime" },
+ :time => { :name => "time" },
+ :date => { :name => "date" },
+ :binary => { :name => "blob" },
+ :boolean => { :name => "boolean" }
+ }
+ end
+
+ # Returns the current database encoding format as a string, eg: 'UTF-8'
+ def encoding
+ @connection.encoding.to_s
+ end
+
+ # Returns true.
+ def supports_explain?
+ true
+ end
+
+ # QUOTING ==================================================
+
def quote(value, column = nil)
if value.kind_of?(String) && column && column.type == :binary && column.class.respond_to?(:string_to_binary)
s = column.class.string_to_binary(value).unpack("H*")[0]
@@ -45,11 +225,391 @@ module ActiveRecord
end
end
- # Returns the current database encoding format as a string, eg: 'UTF-8'
- def encoding
- @connection.encoding.to_s
+ def quote_string(s) #:nodoc:
+ @connection.class.quote(s)
+ end
+
+ def quote_column_name(name) #:nodoc:
+ %Q("#{name.to_s.gsub('"', '""')}")
+ end
+
+ # Quote date/time values for use in SQL input. Includes microseconds
+ # if the value is a Time responding to usec.
+ def quoted_date(value) #:nodoc:
+ if value.respond_to?(:usec)
+ "#{super}.#{sprintf("%06d", value.usec)}"
+ else
+ super
+ end
+ end
+
+ def type_cast(value, column) # :nodoc:
+ return value.to_f if BigDecimal === value
+ return super unless String === value
+ return super unless column && value
+
+ value = super
+ if column.type == :string && value.encoding == Encoding::ASCII_8BIT
+ logger.error "Binary data inserted for `string` type on column `#{column.name}`" if logger
+ value.encode! 'utf-8'
+ end
+ value
+ end
+
+ # DATABASE STATEMENTS ======================================
+
+ def explain(arel, binds = [])
+ sql = "EXPLAIN QUERY PLAN #{to_sql(arel, binds)}"
+ ExplainPrettyPrinter.new.pp(exec_query(sql, 'EXPLAIN', binds))
+ end
+
+ class ExplainPrettyPrinter
+ # Pretty prints the result of a EXPLAIN QUERY PLAN in a way that resembles
+ # the output of the SQLite shell:
+ #
+ # 0|0|0|SEARCH TABLE users USING INTEGER PRIMARY KEY (rowid=?) (~1 rows)
+ # 0|1|1|SCAN TABLE posts (~100000 rows)
+ #
+ def pp(result) # :nodoc:
+ result.rows.map do |row|
+ row.join('|')
+ end.join("\n") + "\n"
+ end
+ end
+
+ def exec_query(sql, name = nil, binds = [])
+ log(sql, name, binds) do
+
+ # Don't cache statements without bind values
+ if binds.empty?
+ stmt = @connection.prepare(sql)
+ cols = stmt.columns
+ records = stmt.to_a
+ stmt.close
+ stmt = records
+ else
+ cache = @statements[sql] ||= {
+ :stmt => @connection.prepare(sql)
+ }
+ stmt = cache[:stmt]
+ cols = cache[:cols] ||= stmt.columns
+ stmt.reset!
+ stmt.bind_params binds.map { |col, val|
+ type_cast(val, col)
+ }
+ end
+
+ ActiveRecord::Result.new(cols, stmt.to_a)
+ end
end
+ def exec_delete(sql, name = 'SQL', binds = [])
+ exec_query(sql, name, binds)
+ @connection.changes
+ end
+ alias :exec_update :exec_delete
+
+ def last_inserted_id(result)
+ @connection.last_insert_row_id
+ end
+
+ def execute(sql, name = nil) #:nodoc:
+ log(sql, name) { @connection.execute(sql) }
+ end
+
+ def update_sql(sql, name = nil) #:nodoc:
+ super
+ @connection.changes
+ end
+
+ def delete_sql(sql, name = nil) #:nodoc:
+ sql += " WHERE 1=1" unless sql =~ /WHERE/i
+ super sql, name
+ end
+
+ def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc:
+ super
+ id_value || @connection.last_insert_row_id
+ end
+ alias :create :insert_sql
+
+ def select_rows(sql, name = nil)
+ exec_query(sql, name).rows
+ end
+
+ def create_savepoint
+ execute("SAVEPOINT #{current_savepoint_name}")
+ end
+
+ def rollback_to_savepoint
+ execute("ROLLBACK TO SAVEPOINT #{current_savepoint_name}")
+ end
+
+ def release_savepoint
+ execute("RELEASE SAVEPOINT #{current_savepoint_name}")
+ end
+
+ def begin_db_transaction #:nodoc:
+ log('begin transaction',nil) { @connection.transaction }
+ end
+
+ def commit_db_transaction #:nodoc:
+ log('commit transaction',nil) { @connection.commit }
+ end
+
+ def rollback_db_transaction #:nodoc:
+ log('rollback transaction',nil) { @connection.rollback }
+ end
+
+ # SCHEMA STATEMENTS ========================================
+
+ def tables(name = nil, table_name = nil) #:nodoc:
+ sql = <<-SQL
+ SELECT name
+ FROM sqlite_master
+ WHERE type = 'table' AND NOT name = 'sqlite_sequence'
+ SQL
+ sql << " AND name = #{quote_table_name(table_name)}" if table_name
+
+ exec_query(sql, 'SCHEMA').map do |row|
+ row['name']
+ end
+ end
+
+ def table_exists?(table_name)
+ table_name && tables(nil, table_name).any?
+ end
+
+ # Returns an array of +SQLite3Column+ objects for the table specified by +table_name+.
+ def columns(table_name) #:nodoc:
+ table_structure(table_name).map do |field|
+ case field["dflt_value"]
+ when /^null$/i
+ field["dflt_value"] = nil
+ when /^'(.*)'$/m
+ field["dflt_value"] = $1.gsub("''", "'")
+ when /^"(.*)"$/m
+ field["dflt_value"] = $1.gsub('""', '"')
+ end
+
+ SQLite3Column.new(field['name'], field['dflt_value'], field['type'], field['notnull'].to_i == 0)
+ end
+ end
+
+ # Returns an array of indexes for the given table.
+ def indexes(table_name, name = nil) #:nodoc:
+ exec_query("PRAGMA index_list(#{quote_table_name(table_name)})", 'SCHEMA').map do |row|
+ IndexDefinition.new(
+ table_name,
+ row['name'],
+ row['unique'] != 0,
+ exec_query("PRAGMA index_info('#{row['name']}')", "SCHEMA").map { |col|
+ col['name']
+ })
+ end
+ end
+
+ def primary_key(table_name) #:nodoc:
+ column = table_structure(table_name).find { |field|
+ field['pk'] == 1
+ }
+ column && column['name']
+ end
+
+ def remove_index!(table_name, index_name) #:nodoc:
+ exec_query "DROP INDEX #{quote_column_name(index_name)}"
+ end
+
+ # Renames a table.
+ #
+ # Example:
+ # rename_table('octopuses', 'octopi')
+ def rename_table(name, new_name)
+ exec_query "ALTER TABLE #{quote_table_name(name)} RENAME TO #{quote_table_name(new_name)}"
+ end
+
+ # See: http://www.sqlite.org/lang_altertable.html
+ # SQLite has an additional restriction on the ALTER TABLE statement
+ def valid_alter_table_options( type, options)
+ type.to_sym != :primary_key
+ end
+
+ def add_column(table_name, column_name, type, options = {}) #:nodoc:
+ if supports_add_column? && valid_alter_table_options( type, options )
+ super(table_name, column_name, type, options)
+ else
+ alter_table(table_name) do |definition|
+ definition.column(column_name, type, options)
+ end
+ end
+ end
+
+ def remove_column(table_name, *column_names) #:nodoc:
+ raise ArgumentError.new("You must specify at least one column name. Example: remove_column(:people, :first_name)") if column_names.empty?
+ column_names.each do |column_name|
+ alter_table(table_name) do |definition|
+ definition.columns.delete(definition[column_name])
+ end
+ end
+ end
+ alias :remove_columns :remove_column
+
+ def change_column_default(table_name, column_name, default) #:nodoc:
+ alter_table(table_name) do |definition|
+ definition[column_name].default = default
+ end
+ end
+
+ def change_column_null(table_name, column_name, null, default = nil)
+ unless null || default.nil?
+ exec_query("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL")
+ end
+ alter_table(table_name) do |definition|
+ definition[column_name].null = null
+ end
+ end
+
+ def change_column(table_name, column_name, type, options = {}) #:nodoc:
+ alter_table(table_name) do |definition|
+ include_default = options_include_default?(options)
+ definition[column_name].instance_eval do
+ self.type = type
+ self.limit = options[:limit] if options.include?(:limit)
+ self.default = options[:default] if include_default
+ self.null = options[:null] if options.include?(:null)
+ self.precision = options[:precision] if options.include?(:precision)
+ self.scale = options[:scale] if options.include?(:scale)
+ end
+ end
+ end
+
+ def rename_column(table_name, column_name, new_column_name) #:nodoc:
+ unless columns(table_name).detect{|c| c.name == column_name.to_s }
+ raise ActiveRecord::ActiveRecordError, "Missing column #{table_name}.#{column_name}"
+ end
+ alter_table(table_name, :rename => {column_name.to_s => new_column_name.to_s})
+ end
+
+ def empty_insert_statement_value
+ "VALUES(NULL)"
+ end
+
+ protected
+ def select(sql, name = nil, binds = []) #:nodoc:
+ exec_query(sql, name, binds)
+ end
+
+ def table_structure(table_name)
+ structure = exec_query("PRAGMA table_info(#{quote_table_name(table_name)})", 'SCHEMA').to_hash
+ raise(ActiveRecord::StatementInvalid, "Could not find table '#{table_name}'") if structure.empty?
+ structure
+ end
+
+ def alter_table(table_name, options = {}) #:nodoc:
+ altered_table_name = "altered_#{table_name}"
+ caller = lambda {|definition| yield definition if block_given?}
+
+ transaction do
+ move_table(table_name, altered_table_name,
+ options.merge(:temporary => true))
+ move_table(altered_table_name, table_name, &caller)
+ end
+ end
+
+ def move_table(from, to, options = {}, &block) #:nodoc:
+ copy_table(from, to, options, &block)
+ drop_table(from)
+ end
+
+ def copy_table(from, to, options = {}) #:nodoc:
+ from_primary_key = primary_key(from)
+ options[:primary_key] = from_primary_key if from_primary_key != 'id'
+ unless options[:primary_key]
+ options[:id] = columns(from).detect{|c| c.name == 'id'}.present? && from_primary_key == 'id'
+ end
+ create_table(to, options) do |definition|
+ @definition = definition
+ columns(from).each do |column|
+ column_name = options[:rename] ?
+ (options[:rename][column.name] ||
+ options[:rename][column.name.to_sym] ||
+ column.name) : column.name
+
+ @definition.column(column_name, column.type,
+ :limit => column.limit, :default => column.default,
+ :precision => column.precision, :scale => column.scale,
+ :null => column.null)
+ end
+ @definition.primary_key(from_primary_key) if from_primary_key
+ yield @definition if block_given?
+ end
+
+ copy_table_indexes(from, to, options[:rename] || {})
+ copy_table_contents(from, to,
+ @definition.columns.map {|column| column.name},
+ options[:rename] || {})
+ end
+
+ def copy_table_indexes(from, to, rename = {}) #:nodoc:
+ indexes(from).each do |index|
+ name = index.name
+ if to == "altered_#{from}"
+ name = "temp_#{name}"
+ elsif from == "altered_#{to}"
+ name = name[5..-1]
+ end
+
+ to_column_names = columns(to).map { |c| c.name }
+ columns = index.columns.map {|c| rename[c] || c }.select do |column|
+ to_column_names.include?(column)
+ end
+
+ unless columns.empty?
+ # index name can't be the same
+ opts = { :name => name.gsub(/_(#{from})_/, "_#{to}_") }
+ opts[:unique] = true if index.unique
+ add_index(to, columns, opts)
+ end
+ end
+ end
+
+ def copy_table_contents(from, to, columns, rename = {}) #:nodoc:
+ column_mappings = Hash[columns.map {|name| [name, name]}]
+ rename.each { |a| column_mappings[a.last] = a.first }
+ from_columns = columns(from).collect {|col| col.name}
+ columns = columns.find_all{|col| from_columns.include?(column_mappings[col])}
+ quoted_columns = columns.map { |col| quote_column_name(col) } * ','
+
+ quoted_to = quote_table_name(to)
+ exec_query("SELECT * FROM #{quote_table_name(from)}").each do |row|
+ sql = "INSERT INTO #{quoted_to} (#{quoted_columns}) VALUES ("
+ sql << columns.map {|col| quote row[column_mappings[col]]} * ', '
+ sql << ')'
+ exec_query sql
+ end
+ end
+
+ def sqlite_version
+ @sqlite_version ||= SQLite3Adapter::Version.new(select_value('select sqlite_version(*)'))
+ end
+
+ def default_primary_key_type
+ if supports_autoincrement?
+ 'INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL'
+ else
+ 'INTEGER PRIMARY KEY NOT NULL'
+ end
+ end
+
+ def translate_exception(exception, message)
+ case exception.message
+ when /column(s)? .* (is|are) not unique/
+ RecordNotUnique.new(message, exception)
+ else
+ super
+ end
+ end
+
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb
deleted file mode 100644
index c85c4c6e51..0000000000
--- a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb
+++ /dev/null
@@ -1,563 +0,0 @@
-require 'active_record/connection_adapters/abstract_adapter'
-require 'active_record/connection_adapters/statement_pool'
-require 'arel/visitors/bind_visitor'
-
-module ActiveRecord
- module ConnectionAdapters #:nodoc:
- class SQLiteColumn < Column #:nodoc:
- class << self
- def binary_to_string(value)
- if value.encoding != Encoding::ASCII_8BIT
- value = value.force_encoding(Encoding::ASCII_8BIT)
- end
- value
- end
- end
- end
-
- # The SQLite adapter works with both the 2.x and 3.x series of SQLite with the sqlite-ruby
- # drivers (available both as gems and from http://rubyforge.org/projects/sqlite-ruby/).
- #
- # Options:
- #
- # * <tt>:database</tt> - Path to the database file.
- class SQLiteAdapter < AbstractAdapter
- class Version
- include Comparable
-
- def initialize(version_string)
- @version = version_string.split('.').map { |v| v.to_i }
- end
-
- def <=>(version_string)
- @version <=> version_string.split('.').map { |v| v.to_i }
- end
- end
-
- class StatementPool < ConnectionAdapters::StatementPool
- def initialize(connection, max)
- super
- @cache = Hash.new { |h,pid| h[pid] = {} }
- end
-
- def each(&block); cache.each(&block); end
- def key?(key); cache.key?(key); end
- def [](key); cache[key]; end
- def length; cache.length; end
-
- def []=(sql, key)
- while @max <= cache.size
- dealloc(cache.shift.last[:stmt])
- end
- cache[sql] = key
- end
-
- def clear
- cache.values.each do |hash|
- dealloc hash[:stmt]
- end
- cache.clear
- end
-
- private
- def cache
- @cache[$$]
- end
-
- def dealloc(stmt)
- stmt.close unless stmt.closed?
- end
- end
-
- class BindSubstitution < Arel::Visitors::SQLite # :nodoc:
- include Arel::Visitors::BindVisitor
- end
-
- def initialize(connection, logger, config)
- super(connection, logger)
- @statements = StatementPool.new(@connection,
- config.fetch(:statement_limit) { 1000 })
- @config = config
-
- if config.fetch(:prepared_statements) { true }
- @visitor = Arel::Visitors::SQLite.new self
- else
- @visitor = BindSubstitution.new self
- end
- end
-
- def adapter_name #:nodoc:
- 'SQLite'
- end
-
- # Returns true if SQLite version is '2.0.0' or greater, false otherwise.
- def supports_ddl_transactions?
- sqlite_version >= '2.0.0'
- end
-
- # Returns true if SQLite version is '3.6.8' or greater, false otherwise.
- def supports_savepoints?
- sqlite_version >= '3.6.8'
- end
-
- # Returns true, since this connection adapter supports prepared statement
- # caching.
- def supports_statement_cache?
- true
- end
-
- # Returns true, since this connection adapter supports migrations.
- def supports_migrations? #:nodoc:
- true
- end
-
- # Returns true.
- def supports_primary_key? #:nodoc:
- true
- end
-
- # Returns true.
- def supports_explain?
- true
- end
-
- def requires_reloading?
- true
- end
-
- # Returns true if SQLite version is '3.1.6' or greater, false otherwise.
- def supports_add_column?
- sqlite_version >= '3.1.6'
- end
-
- # Disconnects from the database if already connected. Otherwise, this
- # method does nothing.
- def disconnect!
- super
- clear_cache!
- @connection.close rescue nil
- end
-
- # Clears the prepared statements cache.
- def clear_cache!
- @statements.clear
- end
-
- # Returns true if SQLite version is '3.2.6' or greater, false otherwise.
- def supports_count_distinct? #:nodoc:
- sqlite_version >= '3.2.6'
- end
-
- # Returns true if SQLite version is '3.1.0' or greater, false otherwise.
- def supports_autoincrement? #:nodoc:
- sqlite_version >= '3.1.0'
- end
-
- def supports_index_sort_order?
- sqlite_version >= '3.3.0'
- end
-
- def native_database_types #:nodoc:
- {
- :primary_key => default_primary_key_type,
- :string => { :name => "varchar", :limit => 255 },
- :text => { :name => "text" },
- :integer => { :name => "integer" },
- :float => { :name => "float" },
- :decimal => { :name => "decimal" },
- :datetime => { :name => "datetime" },
- :timestamp => { :name => "datetime" },
- :time => { :name => "time" },
- :date => { :name => "date" },
- :binary => { :name => "blob" },
- :boolean => { :name => "boolean" }
- }
- end
-
-
- # QUOTING ==================================================
-
- def quote_string(s) #:nodoc:
- @connection.class.quote(s)
- end
-
- def quote_column_name(name) #:nodoc:
- %Q("#{name.to_s.gsub('"', '""')}")
- end
-
- # Quote date/time values for use in SQL input. Includes microseconds
- # if the value is a Time responding to usec.
- def quoted_date(value) #:nodoc:
- if value.respond_to?(:usec)
- "#{super}.#{sprintf("%06d", value.usec)}"
- else
- super
- end
- end
-
- def type_cast(value, column) # :nodoc:
- return value.to_f if BigDecimal === value
- return super unless String === value
- return super unless column && value
-
- value = super
- if column.type == :string && value.encoding == Encoding::ASCII_8BIT
- @logger.error "Binary data inserted for `string` type on column `#{column.name}`"
- value.encode! 'utf-8'
- end
- value
- end
-
- # DATABASE STATEMENTS ======================================
-
- def explain(arel, binds = [])
- sql = "EXPLAIN QUERY PLAN #{to_sql(arel, binds)}"
- ExplainPrettyPrinter.new.pp(exec_query(sql, 'EXPLAIN', binds))
- end
-
- class ExplainPrettyPrinter
- # Pretty prints the result of a EXPLAIN QUERY PLAN in a way that resembles
- # the output of the SQLite shell:
- #
- # 0|0|0|SEARCH TABLE users USING INTEGER PRIMARY KEY (rowid=?) (~1 rows)
- # 0|1|1|SCAN TABLE posts (~100000 rows)
- #
- def pp(result) # :nodoc:
- result.rows.map do |row|
- row.join('|')
- end.join("\n") + "\n"
- end
- end
-
- def exec_query(sql, name = nil, binds = [])
- log(sql, name, binds) do
-
- # Don't cache statements without bind values
- if binds.empty?
- stmt = @connection.prepare(sql)
- cols = stmt.columns
- records = stmt.to_a
- stmt.close
- stmt = records
- else
- cache = @statements[sql] ||= {
- :stmt => @connection.prepare(sql)
- }
- stmt = cache[:stmt]
- cols = cache[:cols] ||= stmt.columns
- stmt.reset!
- stmt.bind_params binds.map { |col, val|
- type_cast(val, col)
- }
- end
-
- ActiveRecord::Result.new(cols, stmt.to_a)
- end
- end
-
- def exec_delete(sql, name = 'SQL', binds = [])
- exec_query(sql, name, binds)
- @connection.changes
- end
- alias :exec_update :exec_delete
-
- def last_inserted_id(result)
- @connection.last_insert_row_id
- end
-
- def execute(sql, name = nil) #:nodoc:
- log(sql, name) { @connection.execute(sql) }
- end
-
- def update_sql(sql, name = nil) #:nodoc:
- super
- @connection.changes
- end
-
- def delete_sql(sql, name = nil) #:nodoc:
- sql += " WHERE 1=1" unless sql =~ /WHERE/i
- super sql, name
- end
-
- def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc:
- super
- id_value || @connection.last_insert_row_id
- end
- alias :create :insert_sql
-
- def select_rows(sql, name = nil)
- exec_query(sql, name).rows
- end
-
- def create_savepoint
- execute("SAVEPOINT #{current_savepoint_name}")
- end
-
- def rollback_to_savepoint
- execute("ROLLBACK TO SAVEPOINT #{current_savepoint_name}")
- end
-
- def release_savepoint
- execute("RELEASE SAVEPOINT #{current_savepoint_name}")
- end
-
- def begin_db_transaction #:nodoc:
- log('begin transaction',nil) { @connection.transaction }
- end
-
- def commit_db_transaction #:nodoc:
- log('commit transaction',nil) { @connection.commit }
- end
-
- def rollback_db_transaction #:nodoc:
- log('rollback transaction',nil) { @connection.rollback }
- end
-
- # SCHEMA STATEMENTS ========================================
-
- def tables(name = 'SCHEMA', table_name = nil) #:nodoc:
- sql = <<-SQL
- SELECT name
- FROM sqlite_master
- WHERE type = 'table' AND NOT name = 'sqlite_sequence'
- SQL
- sql << " AND name = #{quote_table_name(table_name)}" if table_name
-
- exec_query(sql, name).map do |row|
- row['name']
- end
- end
-
- def table_exists?(name)
- name && tables('SCHEMA', name).any?
- end
-
- # Returns an array of +SQLiteColumn+ objects for the table specified by +table_name+.
- def columns(table_name) #:nodoc:
- table_structure(table_name).map do |field|
- case field["dflt_value"]
- when /^null$/i
- field["dflt_value"] = nil
- when /^'(.*)'$/
- field["dflt_value"] = $1.gsub(/''/, "'")
- when /^"(.*)"$/
- field["dflt_value"] = $1.gsub(/""/, '"')
- end
-
- SQLiteColumn.new(field['name'], field['dflt_value'], field['type'], field['notnull'].to_i == 0)
- end
- end
-
- # Returns an array of indexes for the given table.
- def indexes(table_name, name = nil) #:nodoc:
- exec_query("PRAGMA index_list(#{quote_table_name(table_name)})", name).map do |row|
- IndexDefinition.new(
- table_name,
- row['name'],
- row['unique'] != 0,
- exec_query("PRAGMA index_info('#{row['name']}')").map { |col|
- col['name']
- })
- end
- end
-
- def primary_key(table_name) #:nodoc:
- column = table_structure(table_name).find { |field|
- field['pk'] == 1
- }
- column && column['name']
- end
-
- def remove_index!(table_name, index_name) #:nodoc:
- exec_query "DROP INDEX #{quote_column_name(index_name)}"
- end
-
- # Renames a table.
- #
- # Example:
- # rename_table('octopuses', 'octopi')
- def rename_table(name, new_name)
- exec_query "ALTER TABLE #{quote_table_name(name)} RENAME TO #{quote_table_name(new_name)}"
- end
-
- # See: http://www.sqlite.org/lang_altertable.html
- # SQLite has an additional restriction on the ALTER TABLE statement
- def valid_alter_table_options( type, options)
- type.to_sym != :primary_key
- end
-
- def add_column(table_name, column_name, type, options = {}) #:nodoc:
- if supports_add_column? && valid_alter_table_options( type, options )
- super(table_name, column_name, type, options)
- else
- alter_table(table_name) do |definition|
- definition.column(column_name, type, options)
- end
- end
- end
-
- def remove_column(table_name, *column_names) #:nodoc:
- raise ArgumentError.new("You must specify at least one column name. Example: remove_column(:people, :first_name)") if column_names.empty?
- column_names.flatten.each do |column_name|
- alter_table(table_name) do |definition|
- definition.columns.delete(definition[column_name])
- end
- end
- end
- alias :remove_columns :remove_column
-
- def change_column_default(table_name, column_name, default) #:nodoc:
- alter_table(table_name) do |definition|
- definition[column_name].default = default
- end
- end
-
- def change_column_null(table_name, column_name, null, default = nil)
- unless null || default.nil?
- exec_query("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL")
- end
- alter_table(table_name) do |definition|
- definition[column_name].null = null
- end
- end
-
- def change_column(table_name, column_name, type, options = {}) #:nodoc:
- alter_table(table_name) do |definition|
- include_default = options_include_default?(options)
- definition[column_name].instance_eval do
- self.type = type
- self.limit = options[:limit] if options.include?(:limit)
- self.default = options[:default] if include_default
- self.null = options[:null] if options.include?(:null)
- self.precision = options[:precision] if options.include?(:precision)
- self.scale = options[:scale] if options.include?(:scale)
- end
- end
- end
-
- def rename_column(table_name, column_name, new_column_name) #:nodoc:
- unless columns(table_name).detect{|c| c.name == column_name.to_s }
- raise ActiveRecord::ActiveRecordError, "Missing column #{table_name}.#{column_name}"
- end
- alter_table(table_name, :rename => {column_name.to_s => new_column_name.to_s})
- end
-
- def empty_insert_statement_value
- "VALUES(NULL)"
- end
-
- protected
- def select(sql, name = nil, binds = []) #:nodoc:
- exec_query(sql, name, binds)
- end
-
- def table_structure(table_name)
- structure = exec_query("PRAGMA table_info(#{quote_table_name(table_name)})", 'SCHEMA').to_hash
- raise(ActiveRecord::StatementInvalid, "Could not find table '#{table_name}'") if structure.empty?
- structure
- end
-
- def alter_table(table_name, options = {}) #:nodoc:
- altered_table_name = "altered_#{table_name}"
- caller = lambda {|definition| yield definition if block_given?}
-
- transaction do
- move_table(table_name, altered_table_name,
- options.merge(:temporary => true))
- move_table(altered_table_name, table_name, &caller)
- end
- end
-
- def move_table(from, to, options = {}, &block) #:nodoc:
- copy_table(from, to, options, &block)
- drop_table(from)
- end
-
- def copy_table(from, to, options = {}) #:nodoc:
- options = options.merge(:id => (!columns(from).detect{|c| c.name == 'id'}.nil? && 'id' == primary_key(from).to_s))
- create_table(to, options) do |definition|
- @definition = definition
- columns(from).each do |column|
- column_name = options[:rename] ?
- (options[:rename][column.name] ||
- options[:rename][column.name.to_sym] ||
- column.name) : column.name
-
- @definition.column(column_name, column.type,
- :limit => column.limit, :default => column.default,
- :precision => column.precision, :scale => column.scale,
- :null => column.null)
- end
- @definition.primary_key(primary_key(from)) if primary_key(from)
- yield @definition if block_given?
- end
-
- copy_table_indexes(from, to, options[:rename] || {})
- copy_table_contents(from, to,
- @definition.columns.map {|column| column.name},
- options[:rename] || {})
- end
-
- def copy_table_indexes(from, to, rename = {}) #:nodoc:
- indexes(from).each do |index|
- name = index.name
- if to == "altered_#{from}"
- name = "temp_#{name}"
- elsif from == "altered_#{to}"
- name = name[5..-1]
- end
-
- to_column_names = columns(to).map { |c| c.name }
- columns = index.columns.map {|c| rename[c] || c }.select do |column|
- to_column_names.include?(column)
- end
-
- unless columns.empty?
- # index name can't be the same
- opts = { :name => name.gsub(/_(#{from})_/, "_#{to}_") }
- opts[:unique] = true if index.unique
- add_index(to, columns, opts)
- end
- end
- end
-
- def copy_table_contents(from, to, columns, rename = {}) #:nodoc:
- column_mappings = Hash[columns.map {|name| [name, name]}]
- rename.each { |a| column_mappings[a.last] = a.first }
- from_columns = columns(from).collect {|col| col.name}
- columns = columns.find_all{|col| from_columns.include?(column_mappings[col])}
- quoted_columns = columns.map { |col| quote_column_name(col) } * ','
-
- quoted_to = quote_table_name(to)
- exec_query("SELECT * FROM #{quote_table_name(from)}").each do |row|
- sql = "INSERT INTO #{quoted_to} (#{quoted_columns}) VALUES ("
- sql << columns.map {|col| quote row[column_mappings[col]]} * ', '
- sql << ')'
- exec_query sql
- end
- end
-
- def sqlite_version
- @sqlite_version ||= SQLiteAdapter::Version.new(select_value('select sqlite_version(*)'))
- end
-
- def default_primary_key_type
- if supports_autoincrement?
- 'INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL'
- else
- 'INTEGER PRIMARY KEY NOT NULL'
- end
- end
-
- def translate_exception(exception, message)
- case exception.message
- when /column(s)? .* (is|are) not unique/
- RecordNotUnique.new(message, exception)
- else
- super
- end
- end
-
- end
- end
-end
diff --git a/activerecord/lib/active_record/connection_handling.rb b/activerecord/lib/active_record/connection_handling.rb
index d7746826a9..3531be05bf 100644
--- a/activerecord/lib/active_record/connection_handling.rb
+++ b/activerecord/lib/active_record/connection_handling.rb
@@ -1,4 +1,3 @@
-require 'active_support/core_ext/module/delegation'
module ActiveRecord
module ConnectionHandling
@@ -45,7 +44,7 @@ module ActiveRecord
end
remove_connection
- connection_handler.establish_connection name, spec
+ connection_handler.establish_connection self, spec
end
# Returns the connection currently associated with the class. This can
@@ -90,7 +89,11 @@ module ActiveRecord
connection_handler.remove_connection(klass)
end
+ def clear_cache! # :nodoc:
+ connection.schema_cache.clear!
+ end
+
delegate :clear_active_connections!, :clear_reloadable_connections!,
- :clear_all_connections!, :verify_active_connections!, :to => :connection_handler
+ :clear_all_connections!, :to => :connection_handler
end
end
diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb
index 3f90d25962..f97c363871 100644
--- a/activerecord/lib/active_record/core.rb
+++ b/activerecord/lib/active_record/core.rb
@@ -1,87 +1,90 @@
-require 'active_support/concern'
+require 'active_support/core_ext/hash/indifferent_access'
+require 'active_support/core_ext/object/duplicable'
require 'thread'
module ActiveRecord
+ ActiveSupport.on_load(:active_record_config) do
+ ##
+ # :singleton-method:
+ #
+ # Accepts a logger conforming to the interface of Log4r which is then
+ # passed on to any new database connections made and which can be
+ # retrieved on both a class and instance level by calling +logger+.
+ mattr_accessor :logger, instance_accessor: false
+
+ ##
+ # :singleton-method:
+ # Contains the database configuration - as is typically stored in config/database.yml -
+ # as a Hash.
+ #
+ # For example, the following database.yml...
+ #
+ # development:
+ # adapter: sqlite3
+ # database: db/development.sqlite3
+ #
+ # production:
+ # adapter: sqlite3
+ # database: db/production.sqlite3
+ #
+ # ...would result in ActiveRecord::Base.configurations to look like this:
+ #
+ # {
+ # 'development' => {
+ # 'adapter' => 'sqlite3',
+ # 'database' => 'db/development.sqlite3'
+ # },
+ # 'production' => {
+ # 'adapter' => 'sqlite3',
+ # 'database' => 'db/production.sqlite3'
+ # }
+ # }
+ mattr_accessor :configurations, instance_accessor: false
+ self.configurations = {}
+
+ ##
+ # :singleton-method:
+ # Determines whether to use Time.utc (using :utc) or Time.local (using :local) when pulling
+ # dates and times from the database. This is set to :utc by default.
+ mattr_accessor :default_timezone, instance_accessor: false
+ self.default_timezone = :utc
+
+ ##
+ # :singleton-method:
+ # Specifies the format to use when dumping the database schema with Rails'
+ # Rakefile. If :sql, the schema is dumped as (potentially database-
+ # specific) SQL statements. If :ruby, the schema is dumped as an
+ # ActiveRecord::Schema file which can be loaded into any database that
+ # supports migrations. Use :ruby if you want to have different database
+ # adapters for, e.g., your development and test environments.
+ mattr_accessor :schema_format, instance_accessor: false
+ self.schema_format = :ruby
+
+ ##
+ # :singleton-method:
+ # Specify whether or not to use timestamps for migration versions
+ mattr_accessor :timestamped_migrations, instance_accessor: false
+ self.timestamped_migrations = true
+
+ mattr_accessor :connection_handler, instance_accessor: false
+ self.connection_handler = ConnectionAdapters::ConnectionHandler.new
+
+ mattr_accessor :dependent_restrict_raises, instance_accessor: false
+ self.dependent_restrict_raises = true
+ end
+
module Core
extend ActiveSupport::Concern
included do
##
# :singleton-method:
- # Accepts a logger conforming to the interface of Log4r or the default Ruby 1.8+ Logger class,
- # which is then passed on to any new database connections made and which can be retrieved on both
- # a class and instance level by calling +logger+.
- config_attribute :logger, :global => true
-
- ##
- # :singleton-method:
- # Contains the database configuration - as is typically stored in config/database.yml -
- # as a Hash.
- #
- # For example, the following database.yml...
- #
- # development:
- # adapter: sqlite3
- # database: db/development.sqlite3
- #
- # production:
- # adapter: sqlite3
- # database: db/production.sqlite3
- #
- # ...would result in ActiveRecord::Base.configurations to look like this:
- #
- # {
- # 'development' => {
- # 'adapter' => 'sqlite3',
- # 'database' => 'db/development.sqlite3'
- # },
- # 'production' => {
- # 'adapter' => 'sqlite3',
- # 'database' => 'db/production.sqlite3'
- # }
- # }
- config_attribute :configurations, :global => true
- self.configurations = {}
-
- ##
- # :singleton-method:
- # Determines whether to use Time.utc (using :utc) or Time.local (using :local) when pulling
- # dates and times from the database. This is set to :utc by default.
- config_attribute :default_timezone, :global => true
- self.default_timezone = :utc
-
- ##
- # :singleton-method:
- # Specifies the format to use when dumping the database schema with Rails'
- # Rakefile. If :sql, the schema is dumped as (potentially database-
- # specific) SQL statements. If :ruby, the schema is dumped as an
- # ActiveRecord::Schema file which can be loaded into any database that
- # supports migrations. Use :ruby if you want to have different database
- # adapters for, e.g., your development and test environments.
- config_attribute :schema_format, :global => true
- self.schema_format = :ruby
-
- ##
- # :singleton-method:
- # Specify whether or not to use timestamps for migration versions
- config_attribute :timestamped_migrations, :global => true
- self.timestamped_migrations = true
-
- ##
- # :singleton-method:
# The connection handler
config_attribute :connection_handler
- self.connection_handler = ConnectionAdapters::ConnectionHandler.new
- ##
- # :singleton-method:
- # Specifies wether or not has_many or has_one association option
- # :dependent => :restrict raises an exception. If set to true, the
- # ActiveRecord::DeleteRestrictionError exception will be raised
- # along with a DEPRECATION WARNING. If set to false, an error would
- # be added to the model instead.
- config_attribute :dependent_restrict_raises, :global => true
- self.dependent_restrict_raises = true
+ %w(logger configurations default_timezone schema_format timestamped_migrations).each do |name|
+ config_attribute name, global: true
+ end
end
module ClassMethods
@@ -125,23 +128,29 @@ module ActiveRecord
object.is_a?(self)
end
+ # Returns an instance of <tt>Arel::Table</tt> loaded with the current table name.
+ #
+ # class Post < ActiveRecord::Base
+ # scope :published_and_commented, published.and(self.arel_table[:comments_count].gt(0))
+ # end
def arel_table
@arel_table ||= Arel::Table.new(table_name, arel_engine)
end
+ # Returns the Arel engine.
def arel_engine
- @arel_engine ||= connection_handler.connection_pools[name] ? self : active_record_super.arel_engine
+ @arel_engine ||= connection_handler.retrieve_connection_pool(self) ? self : active_record_super.arel_engine
end
private
def relation #:nodoc:
- @relation ||= Relation.new(self, arel_table)
+ relation = Relation.new(self, arel_table)
if finder_needs_type_condition?
- @relation.where(type_condition).create_with(inheritance_column.to_sym => sti_name)
+ relation.where(type_condition).create_with(inheritance_column.to_sym => sti_name)
else
- @relation
+ relation
end
end
end
@@ -151,32 +160,24 @@ module ActiveRecord
# In both instances, valid attribute keys are determined by the column names of the associated table --
# hence you can't have attributes that aren't part of the table columns.
#
- # +initialize+ respects mass-assignment security and accepts either +:as+ or +:without_protection+ options
- # in the +options+ parameter.
- #
- # ==== Examples
+ # ==== Example:
# # Instantiates a single new object
# User.new(:first_name => 'Jamie')
- #
- # # Instantiates a single new object using the :admin mass-assignment security role
- # User.new({ :first_name => 'Jamie', :is_admin => true }, :as => :admin)
- #
- # # Instantiates a single new object bypassing mass-assignment security
- # User.new({ :first_name => 'Jamie', :is_admin => true }, :without_protection => true)
- def initialize(attributes = nil, options = {})
- @attributes = self.class.initialize_attributes(self.class.column_defaults.dup)
+ def initialize(attributes = nil)
+ defaults = self.class.column_defaults.dup
+ defaults.each { |k, v| defaults[k] = v.dup if v.duplicable? }
+
+ @attributes = self.class.initialize_attributes(defaults)
@columns_hash = self.class.column_types.dup
init_internals
-
ensure_proper_type
-
populate_with_current_scope_attributes
- assign_attributes(attributes, options) if attributes
+ assign_attributes(attributes) if attributes
yield self if block_given?
- run_callbacks :initialize
+ run_callbacks :initialize unless _initialize_callbacks.empty?
end
# Initialize an empty model object from +coder+. +coder+ must contain
@@ -190,10 +191,9 @@ module ActiveRecord
# post.init_with('attributes' => { 'title' => 'hello world' })
# post.title # => 'hello world'
def init_with(coder)
- @attributes = self.class.initialize_attributes(coder['attributes'])
+ @attributes = self.class.initialize_attributes(coder['attributes'])
@columns_hash = self.class.column_types.merge(coder['column_types'] || {})
-
init_internals
@new_record = false
@@ -204,22 +204,41 @@ module ActiveRecord
self
end
+ ##
+ # :method: clone
+ # Identical to Ruby's clone method. This is a "shallow" copy. Be warned that your attributes are not copied.
+ # That means that modifying attributes of the clone will modify the original, since they will both point to the
+ # same attributes hash. If you need a copy of your attributes hash, please use the #dup method.
+ #
+ # user = User.first
+ # new_user = user.clone
+ # user.name # => "Bob"
+ # new_user.name = "Joe"
+ # user.name # => "Joe"
+ #
+ # user.object_id == new_user.object_id # => false
+ # user.name.object_id == new_user.name.object_id # => true
+ #
+ # user.name.object_id == user.dup.name.object_id # => false
+
+ ##
+ # :method: dup
# Duped objects have no id assigned and are treated as new records. Note
# that this is a "shallow" copy as it copies the object's attributes
# only, not its associations. The extent of a "deep" copy is application
# specific and is therefore left to the application to implement according
# to its need.
# The dup method does not preserve the timestamps (created|updated)_(at|on).
- def initialize_dup(other)
- cloned_attributes = other.clone_attributes(:read_attribute_before_type_cast)
- self.class.initialize_attributes(cloned_attributes)
- cloned_attributes.delete(self.class.primary_key)
+ ##
+ def initialize_dup(other) # :nodoc:
+ cloned_attributes = other.clone_attributes(:read_attribute_before_type_cast)
+ self.class.initialize_attributes(cloned_attributes, :serialized => false)
@attributes = cloned_attributes
@attributes[self.class.primary_key] = nil
- run_callbacks(:initialize) if _initialize_callbacks.any?
+ run_callbacks(:initialize) unless _initialize_callbacks.empty?
@changed_attributes = {}
self.class.column_defaults.each do |attr, orig_value|
@@ -278,7 +297,8 @@ module ActiveRecord
# Freeze the attributes hash such that associations are still accessible, even on destroyed records.
def freeze
- @attributes.freeze; self
+ @attributes.freeze
+ self
end
# Returns +true+ if the attributes hash has been frozen.
@@ -290,8 +310,6 @@ module ActiveRecord
def <=>(other_object)
if other_object.is_a?(self.class)
self.to_key <=> other_object.to_key
- else
- nil
end
end
@@ -327,6 +345,11 @@ module ActiveRecord
"#<#{self.class} #{inspection}>"
end
+ # Returns a hash of the given methods with their names as keys and returned values as values.
+ def slice(*methods)
+ Hash[methods.map { |method| [method, public_send(method)] }].with_indifferent_access
+ end
+
private
# Under Ruby 1.9, Array#flatten will call #to_ary (recursively) on each of the elements
@@ -336,26 +359,26 @@ module ActiveRecord
#
# So we can avoid the method_missing hit by explicitly defining #to_ary as nil here.
#
- # See also http://tenderlovemaking.com/2011/06/28/til-its-ok-to-return-nil-from-to_ary/
+ # See also http://tenderlovemaking.com/2011/06/28/til-its-ok-to-return-nil-from-to_ary.html
def to_ary # :nodoc:
nil
end
def init_internals
pk = self.class.primary_key
-
@attributes[pk] = nil unless @attributes.key?(pk)
- @relation = nil
- @aggregation_cache = {}
- @association_cache = {}
- @attributes_cache = {}
- @previously_changed = {}
- @changed_attributes = {}
- @readonly = false
- @destroyed = false
- @marked_for_destruction = false
- @new_record = true
+ @aggregation_cache = {}
+ @association_cache = {}
+ @attributes_cache = {}
+ @previously_changed = {}
+ @changed_attributes = {}
+ @readonly = false
+ @destroyed = false
+ @marked_for_destruction = false
+ @new_record = true
+ @txn = nil
+ @_start_transaction_state = {}
end
end
end
diff --git a/activerecord/lib/active_record/counter_cache.rb b/activerecord/lib/active_record/counter_cache.rb
index 224f5276eb..c877079b25 100644
--- a/activerecord/lib/active_record/counter_cache.rb
+++ b/activerecord/lib/active_record/counter_cache.rb
@@ -1,113 +1,115 @@
module ActiveRecord
# = Active Record Counter Cache
module CounterCache
- # Resets one or more counter caches to their correct value using an SQL
- # count query. This is useful when adding new counter caches, or if the
- # counter has been corrupted or modified directly by SQL.
- #
- # ==== Parameters
- #
- # * +id+ - The id of the object you wish to reset a counter on.
- # * +counters+ - One or more counter names to reset
- #
- # ==== Examples
- #
- # # For Post with id #1 records reset the comments_count
- # Post.reset_counters(1, :comments)
- def reset_counters(id, *counters)
- object = find(id)
- counters.each do |association|
- has_many_association = reflect_on_association(association.to_sym)
+ extend ActiveSupport::Concern
- foreign_key = has_many_association.foreign_key.to_s
- child_class = has_many_association.klass
- belongs_to = child_class.reflect_on_all_associations(:belongs_to)
- reflection = belongs_to.find { |e| e.foreign_key.to_s == foreign_key }
- counter_name = reflection.counter_cache_column
+ module ClassMethods
+ # Resets one or more counter caches to their correct value using an SQL
+ # count query. This is useful when adding new counter caches, or if the
+ # counter has been corrupted or modified directly by SQL.
+ #
+ # ==== Parameters
+ #
+ # * +id+ - The id of the object you wish to reset a counter on.
+ # * +counters+ - One or more counter names to reset
+ #
+ # ==== Examples
+ #
+ # # For Post with id #1 records reset the comments_count
+ # Post.reset_counters(1, :comments)
+ def reset_counters(id, *counters)
+ object = find(id)
+ counters.each do |association|
+ has_many_association = reflect_on_association(association.to_sym)
- stmt = unscoped.where(arel_table[primary_key].eq(object.id)).arel.compile_update({
- arel_table[counter_name] => object.send(association).count
- })
- connection.update stmt
- end
- return true
- end
+ foreign_key = has_many_association.foreign_key.to_s
+ child_class = has_many_association.klass
+ belongs_to = child_class.reflect_on_all_associations(:belongs_to)
+ reflection = belongs_to.find { |e| e.foreign_key.to_s == foreign_key && e.options[:counter_cache].present? }
+ counter_name = reflection.counter_cache_column
- # A generic "counter updater" implementation, intended primarily to be
- # used by increment_counter and decrement_counter, but which may also
- # be useful on its own. It simply does a direct SQL update for the record
- # with the given ID, altering the given hash of counters by the amount
- # given by the corresponding value:
- #
- # ==== Parameters
- #
- # * +id+ - The id of the object you wish to update a counter on or an Array of ids.
- # * +counters+ - An Array of Hashes containing the names of the fields
- # to update as keys and the amount to update the field by as values.
- #
- # ==== Examples
- #
- # # For the Post with id of 5, decrement the comment_count by 1, and
- # # increment the action_count by 1
- # Post.update_counters 5, :comment_count => -1, :action_count => 1
- # # Executes the following SQL:
- # # UPDATE posts
- # # SET comment_count = COALESCE(comment_count, 0) - 1,
- # # action_count = COALESCE(action_count, 0) + 1
- # # WHERE id = 5
- #
- # # For the Posts with id of 10 and 15, increment the comment_count by 1
- # Post.update_counters [10, 15], :comment_count => 1
- # # Executes the following SQL:
- # # UPDATE posts
- # # SET comment_count = COALESCE(comment_count, 0) + 1
- # # WHERE id IN (10, 15)
- def update_counters(id, counters)
- updates = counters.map do |counter_name, value|
- operator = value < 0 ? '-' : '+'
- quoted_column = connection.quote_column_name(counter_name)
- "#{quoted_column} = COALESCE(#{quoted_column}, 0) #{operator} #{value.abs}"
+ stmt = unscoped.where(arel_table[primary_key].eq(object.id)).arel.compile_update({
+ arel_table[counter_name] => object.send(association).count
+ })
+ connection.update stmt
+ end
+ return true
end
- IdentityMap.remove_by_id(symbolized_base_class, id) if IdentityMap.enabled?
+ # A generic "counter updater" implementation, intended primarily to be
+ # used by increment_counter and decrement_counter, but which may also
+ # be useful on its own. It simply does a direct SQL update for the record
+ # with the given ID, altering the given hash of counters by the amount
+ # given by the corresponding value:
+ #
+ # ==== Parameters
+ #
+ # * +id+ - The id of the object you wish to update a counter on or an Array of ids.
+ # * +counters+ - An Array of Hashes containing the names of the fields
+ # to update as keys and the amount to update the field by as values.
+ #
+ # ==== Examples
+ #
+ # # For the Post with id of 5, decrement the comment_count by 1, and
+ # # increment the action_count by 1
+ # Post.update_counters 5, :comment_count => -1, :action_count => 1
+ # # Executes the following SQL:
+ # # UPDATE posts
+ # # SET comment_count = COALESCE(comment_count, 0) - 1,
+ # # action_count = COALESCE(action_count, 0) + 1
+ # # WHERE id = 5
+ #
+ # # For the Posts with id of 10 and 15, increment the comment_count by 1
+ # Post.update_counters [10, 15], :comment_count => 1
+ # # Executes the following SQL:
+ # # UPDATE posts
+ # # SET comment_count = COALESCE(comment_count, 0) + 1
+ # # WHERE id IN (10, 15)
+ def update_counters(id, counters)
+ updates = counters.map do |counter_name, value|
+ operator = value < 0 ? '-' : '+'
+ quoted_column = connection.quote_column_name(counter_name)
+ "#{quoted_column} = COALESCE(#{quoted_column}, 0) #{operator} #{value.abs}"
+ end
- update_all(updates.join(', '), primary_key => id )
- end
+ where(primary_key => id).update_all updates.join(', ')
+ end
- # Increment a number field by one, usually representing a count.
- #
- # This is used for caching aggregate values, so that they don't need to be computed every time.
- # For example, a DiscussionBoard may cache post_count and comment_count otherwise every time the board is
- # shown it would have to run an SQL query to find how many posts and comments there are.
- #
- # ==== Parameters
- #
- # * +counter_name+ - The name of the field that should be incremented.
- # * +id+ - The id of the object that should be incremented.
- #
- # ==== Examples
- #
- # # Increment the post_count column for the record with an id of 5
- # DiscussionBoard.increment_counter(:post_count, 5)
- def increment_counter(counter_name, id)
- update_counters(id, counter_name => 1)
- end
+ # Increment a number field by one, usually representing a count.
+ #
+ # This is used for caching aggregate values, so that they don't need to be computed every time.
+ # For example, a DiscussionBoard may cache post_count and comment_count otherwise every time the board is
+ # shown it would have to run an SQL query to find how many posts and comments there are.
+ #
+ # ==== Parameters
+ #
+ # * +counter_name+ - The name of the field that should be incremented.
+ # * +id+ - The id of the object that should be incremented.
+ #
+ # ==== Examples
+ #
+ # # Increment the post_count column for the record with an id of 5
+ # DiscussionBoard.increment_counter(:post_count, 5)
+ def increment_counter(counter_name, id)
+ update_counters(id, counter_name => 1)
+ end
- # Decrement a number field by one, usually representing a count.
- #
- # This works the same as increment_counter but reduces the column value by 1 instead of increasing it.
- #
- # ==== Parameters
- #
- # * +counter_name+ - The name of the field that should be decremented.
- # * +id+ - The id of the object that should be decremented.
- #
- # ==== Examples
- #
- # # Decrement the post_count column for the record with an id of 5
- # DiscussionBoard.decrement_counter(:post_count, 5)
- def decrement_counter(counter_name, id)
- update_counters(id, counter_name => -1)
+ # Decrement a number field by one, usually representing a count.
+ #
+ # This works the same as increment_counter but reduces the column value by 1 instead of increasing it.
+ #
+ # ==== Parameters
+ #
+ # * +counter_name+ - The name of the field that should be decremented.
+ # * +id+ - The id of the object that should be decremented.
+ #
+ # ==== Examples
+ #
+ # # Decrement the post_count column for the record with an id of 5
+ # DiscussionBoard.decrement_counter(:post_count, 5)
+ def decrement_counter(counter_name, id)
+ update_counters(id, counter_name => -1)
+ end
end
end
end
diff --git a/activerecord/lib/active_record/dynamic_finder_match.rb b/activerecord/lib/active_record/dynamic_finder_match.rb
deleted file mode 100644
index 38dbbef5fc..0000000000
--- a/activerecord/lib/active_record/dynamic_finder_match.rb
+++ /dev/null
@@ -1,84 +0,0 @@
-module ActiveRecord
-
- # = Active Record Dynamic Finder Match
- #
- # Refer to ActiveRecord::Base documentation for Dynamic attribute-based finders for detailed info
- #
- class DynamicFinderMatch
- def self.match(method)
- method = method.to_s
- klass = [FindBy, FindByBang, FindOrInitializeCreateBy].find do |_klass|
- _klass.matches?(method)
- end
- klass.new(method) if klass
- end
-
- def self.matches?(method)
- method =~ self::METHOD_PATTERN
- end
-
- def initialize(method)
- @finder = :first
- @instantiator = nil
- match_data = method.match(self.class::METHOD_PATTERN)
- @attribute_names = match_data[-1].split("_and_")
- initialize_from_match_data(match_data)
- end
-
- attr_reader :finder, :attribute_names, :instantiator
-
- def finder?
- @finder && !@instantiator
- end
-
- def creator?
- @finder == :first && @instantiator == :create
- end
-
- def instantiator?
- @instantiator
- end
-
- def bang?
- false
- end
-
- def valid_arguments?(arguments)
- arguments.size >= @attribute_names.size
- end
-
- private
-
- def initialize_from_match_data(match_data)
- end
- end
-
- class FindBy < DynamicFinderMatch
- METHOD_PATTERN = /^find_(all_|last_)?by_([_a-zA-Z]\w*)$/
-
- def initialize_from_match_data(match_data)
- @finder = :last if match_data[1] == 'last_'
- @finder = :all if match_data[1] == 'all_'
- end
- end
-
- class FindByBang < DynamicFinderMatch
- METHOD_PATTERN = /^find_by_([_a-zA-Z]\w*)\!$/
-
- def bang?
- true
- end
- end
-
- class FindOrInitializeCreateBy < DynamicFinderMatch
- METHOD_PATTERN = /^find_or_(initialize|create)_by_([_a-zA-Z]\w*)$/
-
- def initialize_from_match_data(match_data)
- @instantiator = match_data[1] == 'initialize' ? :new : :create
- end
-
- def valid_arguments?(arguments)
- arguments.size == 1 && arguments.first.is_a?(Hash) || super
- end
- end
-end
diff --git a/activerecord/lib/active_record/dynamic_matchers.rb b/activerecord/lib/active_record/dynamic_matchers.rb
index 60ce3dd4f1..3bac31c6aa 100644
--- a/activerecord/lib/active_record/dynamic_matchers.rb
+++ b/activerecord/lib/active_record/dynamic_matchers.rb
@@ -1,79 +1,131 @@
module ActiveRecord
- module DynamicMatchers
- def respond_to?(method_id, include_private = false)
- if match = DynamicFinderMatch.match(method_id)
- return true if all_attributes_exists?(match.attribute_names)
- elsif match = DynamicScopeMatch.match(method_id)
- return true if all_attributes_exists?(match.attribute_names)
- end
+ module DynamicMatchers #:nodoc:
+ # This code in this file seems to have a lot of indirection, but the indirection
+ # is there to provide extension points for the activerecord-deprecated_finders
+ # gem. When we stop supporting activerecord-deprecated_finders (from Rails 5),
+ # then we can remove the indirection.
- super
+ def respond_to?(name, include_private = false)
+ match = Method.match(self, name)
+ match && match.valid? || super
end
private
- # Enables dynamic finders like <tt>User.find_by_user_name(user_name)</tt> and
- # <tt>User.scoped_by_user_name(user_name). Refer to Dynamic attribute-based finders
- # section at the top of this file for more detailed information.
- #
- # It's even possible to use all the additional parameters to +find+. For example, the
- # full interface for +find_all_by_amount+ is actually <tt>find_all_by_amount(amount, options)</tt>.
- #
- # Each dynamic finder using <tt>scoped_by_*</tt> is also defined in the class after it
- # is first invoked, so that future attempts to use it do not run through method_missing.
- def method_missing(method_id, *arguments, &block)
- if match = (DynamicFinderMatch.match(method_id) || DynamicScopeMatch.match(method_id))
- attribute_names = match.attribute_names
- super unless all_attributes_exists?(attribute_names)
- unless match.valid_arguments?(arguments)
- method_trace = "#{__FILE__}:#{__LINE__}:in `#{method_id}'"
- backtrace = [method_trace] + caller
- raise ArgumentError, "wrong number of arguments (#{arguments.size} for #{attribute_names.size})", backtrace
- end
- if match.respond_to?(:scope?) && match.scope?
- self.class_eval <<-METHOD, __FILE__, __LINE__ + 1
- def self.#{method_id}(*args) # def self.scoped_by_user_name_and_password(*args)
- attributes = Hash[[:#{attribute_names.join(',:')}].zip(args)] # attributes = Hash[[:user_name, :password].zip(args)]
- #
- scoped(:conditions => attributes) # scoped(:conditions => attributes)
- end # end
- METHOD
- send(method_id, *arguments)
- elsif match.finder?
- options = arguments.extract_options!
- relation = options.any? ? scoped(options) : scoped
- relation.send :find_by_attributes, match, attribute_names, *arguments, &block
- elsif match.instantiator?
- scoped.send :find_or_instantiator_by_attributes, match, attribute_names, *arguments, &block
- end
+ def method_missing(name, *arguments, &block)
+ match = Method.match(self, name)
+
+ if match && match.valid?
+ match.define
+ send(name, *arguments, &block)
else
super
end
end
- # Similar in purpose to +expand_hash_conditions_for_aggregates+.
- def expand_attribute_names_for_aggregates(attribute_names)
- attribute_names.map { |attribute_name|
- unless (aggregation = reflect_on_aggregation(attribute_name.to_sym)).nil?
- aggregate_mapping(aggregation).map do |field_attr, _|
- field_attr.to_sym
- end
- else
- attribute_name.to_sym
+ class Method
+ @matchers = []
+
+ class << self
+ attr_reader :matchers
+
+ def match(model, name)
+ klass = matchers.find { |k| name =~ k.pattern }
+ klass.new(model, name) if klass
+ end
+
+ def pattern
+ /^#{prefix}_([_a-zA-Z]\w*)#{suffix}$/
+ end
+
+ def prefix
+ raise NotImplementedError
end
- }.flatten
+
+ def suffix
+ ''
+ end
+ end
+
+ attr_reader :model, :name, :attribute_names
+
+ def initialize(model, name)
+ @model = model
+ @name = name.to_s
+ @attribute_names = @name.match(self.class.pattern)[1].split('_and_')
+ @attribute_names.map! { |n| @model.attribute_aliases[n] || n }
+ end
+
+ def valid?
+ attribute_names.all? { |name| model.columns_hash[name] || model.reflect_on_aggregation(name.to_sym) }
+ end
+
+ def define
+ model.class_eval <<-CODE, __FILE__, __LINE__ + 1
+ def self.#{name}(#{signature})
+ #{body}
+ end
+ CODE
+ end
+
+ def body
+ raise NotImplementedError
+ end
end
- def all_attributes_exists?(attribute_names)
- (expand_attribute_names_for_aggregates(attribute_names) -
- column_methods_hash.keys).empty?
+ module Finder
+ # Extended in activerecord-deprecated_finders
+ def body
+ result
+ end
+
+ # Extended in activerecord-deprecated_finders
+ def result
+ "#{finder}(#{attributes_hash})"
+ end
+
+ # Extended in activerecord-deprecated_finders
+ def signature
+ attribute_names.join(', ')
+ end
+
+ def attributes_hash
+ "{" + attribute_names.map { |name| ":#{name} => #{name}" }.join(',') + "}"
+ end
+
+ def finder
+ raise NotImplementedError
+ end
end
- def aggregate_mapping(reflection)
- mapping = reflection.options[:mapping] || [reflection.name, reflection.name]
- mapping.first.is_a?(Array) ? mapping : [mapping]
+ class FindBy < Method
+ Method.matchers << self
+ include Finder
+
+ def self.prefix
+ "find_by"
+ end
+
+ def finder
+ "find_by"
+ end
end
+ class FindByBang < Method
+ Method.matchers << self
+ include Finder
+
+ def self.prefix
+ "find_by"
+ end
+
+ def self.suffix
+ "!"
+ end
+ def finder
+ "find_by!"
+ end
+ end
end
end
diff --git a/activerecord/lib/active_record/dynamic_scope_match.rb b/activerecord/lib/active_record/dynamic_scope_match.rb
deleted file mode 100644
index a502155aac..0000000000
--- a/activerecord/lib/active_record/dynamic_scope_match.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-module ActiveRecord
-
- # = Active Record Dynamic Scope Match
- #
- # Provides dynamic attribute-based scopes such as <tt>scoped_by_price(4.99)</tt>
- # if, for example, the <tt>Product</tt> has an attribute with that name. You can
- # chain more <tt>scoped_by_* </tt> methods after the other. It acts like a named
- # scope except that it's dynamic.
- class DynamicScopeMatch
- def self.match(method)
- return unless method.to_s =~ /^scoped_by_([_a-zA-Z]\w*)$/
- new(true, $1 && $1.split('_and_'))
- end
-
- def initialize(scope, attribute_names)
- @scope = scope
- @attribute_names = attribute_names
- end
-
- attr_reader :scope, :attribute_names
- alias :scope? :scope
-
- def valid_arguments?(arguments)
- arguments.size >= @attribute_names.size
- end
- end
-end
diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb
index fc80f3081e..5f157fde6d 100644
--- a/activerecord/lib/active_record/errors.rb
+++ b/activerecord/lib/active_record/errors.rb
@@ -53,6 +53,10 @@ module ActiveRecord
class RecordNotSaved < ActiveRecordError
end
+ # Raised by ActiveRecord::Base.destroy! when a call to destroy would return false.
+ 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).
class StatementInvalid < ActiveRecordError
@@ -102,13 +106,11 @@ module ActiveRecord
attr_reader :record, :attempted_action
def initialize(record, attempted_action)
+ super("Attempted to #{attempted_action} a stale object: #{record.class.name}")
@record = record
@attempted_action = attempted_action
end
- def message
- "Attempted to #{attempted_action} a stale object: #{record.class.name}"
- end
end
# Raised when association is being configured improperly or
@@ -164,9 +166,9 @@ module ActiveRecord
class AttributeAssignmentError < ActiveRecordError
attr_reader :exception, :attribute
def initialize(message, exception, attribute)
+ super(message)
@exception = exception
@attribute = attribute
- @message = message
end
end
@@ -185,11 +187,12 @@ module ActiveRecord
attr_reader :model
def initialize(model)
+ super("Unknown primary key for table #{model.table_name} in model #{model}.")
@model = model
end
- def message
- "Unknown primary key for table #{model.table_name} in model #{model}."
- end
+ end
+
+ class ImmutableRelation < ActiveRecordError
end
end
diff --git a/activerecord/lib/active_record/explain.rb b/activerecord/lib/active_record/explain.rb
index 01cacf6153..9e0390bed1 100644
--- a/activerecord/lib/active_record/explain.rb
+++ b/activerecord/lib/active_record/explain.rb
@@ -1,12 +1,12 @@
-require 'active_support/core_ext/class/attribute'
+require 'active_support/lazy_load_hooks'
module ActiveRecord
+ ActiveSupport.on_load(:active_record_config) do
+ mattr_accessor :auto_explain_threshold_in_seconds, instance_accessor: false
+ end
+
module Explain
- def self.extended(base)
- # If a query takes longer than these many seconds we log its query plan
- # automatically. nil disables this feature.
- base.config_attribute :auto_explain_threshold_in_seconds, :global => true
- end
+ delegate :auto_explain_threshold_in_seconds, :auto_explain_threshold_in_seconds=, to: 'ActiveRecord::Model'
# If auto explain is enabled, this method triggers EXPLAIN logging for the
# queries triggered by the block if it takes more than the threshold as a
@@ -52,7 +52,7 @@ module ActiveRecord
# 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:
- queries && queries.map do |sql, bind|
+ str = queries && queries.map do |sql, bind|
[].tap do |msg|
msg << "EXPLAIN for: #{sql}"
unless bind.empty?
@@ -62,6 +62,12 @@ module ActiveRecord
msg << connection.explain(sql, bind)
end.join("\n")
end.join("\n")
+
+ # Overriding inspect to be more human readable, specially in the console.
+ def str.inspect
+ self
+ end
+ str
end
# Silences automatic EXPLAIN logging for the duration of the block.
@@ -70,7 +76,7 @@ module ActiveRecord
# the threshold is set to 0.
#
# As the name of the method suggests this only applies to automatic
- # EXPLAINs, manual calls to +ActiveRecord::Relation#explain+ run.
+ # EXPLAINs, manual calls to <tt>ActiveRecord::Relation#explain</tt> run.
def silence_auto_explain
current = Thread.current
original, current[:available_queries_for_explain] = current[:available_queries_for_explain], false
diff --git a/activerecord/lib/active_record/explain_subscriber.rb b/activerecord/lib/active_record/explain_subscriber.rb
index 1f8c4fc203..0f927496fb 100644
--- a/activerecord/lib/active_record/explain_subscriber.rb
+++ b/activerecord/lib/active_record/explain_subscriber.rb
@@ -2,9 +2,12 @@ require 'active_support/notifications'
module ActiveRecord
class ExplainSubscriber # :nodoc:
- def call(*args)
+ def start(name, id, payload)
+ # unused
+ end
+
+ def finish(name, id, payload)
if queries = Thread.current[:available_queries_for_explain]
- payload = args.last
queries << payload.values_at(:sql, :binds) unless ignore_payload?(payload)
end
end
@@ -15,8 +18,9 @@ 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
def ignore_payload?(payload)
- payload[:exception] || IGNORED_PAYLOADS.include?(payload[:name])
+ payload[:exception] || IGNORED_PAYLOADS.include?(payload[:name]) || payload[:sql] !~ EXPLAINED_SQLS
end
ActiveSupport::Notifications.subscribe("sql.active_record", new)
diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb
index b82d5b5621..60fc653735 100644
--- a/activerecord/lib/active_record/fixtures.rb
+++ b/activerecord/lib/active_record/fixtures.rb
@@ -2,18 +2,13 @@ require 'erb'
require 'yaml'
require 'zlib'
require 'active_support/dependencies'
-require 'active_support/core_ext/object/blank'
require 'active_record/fixtures/file'
+require 'active_record/errors'
-if defined? ActiveRecord
+module ActiveRecord
class FixtureClassNotFound < ActiveRecord::ActiveRecordError #:nodoc:
end
-else
- class FixtureClassNotFound < StandardError #:nodoc:
- end
-end
-module ActiveRecord
# \Fixtures are a way of organizing data that you want to test against; in short, sample data.
#
# They are stored in YAML files, one file per model, which are placed in the directory
@@ -87,7 +82,7 @@ module ActiveRecord
# end
#
# test "find_alt_method_2" do
- # assert_equal "Ruby on Rails", @rubyonrails.news
+ # assert_equal "Ruby on Rails", @rubyonrails.name
# end
#
# In order to use these methods to access fixtured data within your testcases, you must specify one of the
@@ -376,19 +371,23 @@ module ActiveRecord
#
# Any fixture labeled "DEFAULTS" is safely ignored.
class Fixtures
+ #--
+ # NOTE: an instance of Fixtures can be called fixture_set, it is normally stored in a single YAML file and possibly in a folder with the same name.
+ #++
MAX_ID = 2 ** 30 - 1
@@all_cached_fixtures = Hash.new { |h,k| h[k] = {} }
- def self.default_fixture_model_name(fixture_name) # :nodoc:
+ def self.default_fixture_model_name(fixture_set_name) # :nodoc:
ActiveRecord::Base.pluralize_table_names ?
- fixture_name.singularize.camelize :
- fixture_name.camelize
+ fixture_set_name.singularize.camelize :
+ fixture_set_name.camelize
end
- def self.default_fixture_table_name(fixture_name) # :nodoc:
- "#{ActiveRecord::Base.table_name_prefix}"\
- "#{fixture_name.tr('/', '_')}#{ActiveRecord::Base.table_name_suffix}".to_sym
+ def self.default_fixture_table_name(fixture_set_name) # :nodoc:
+ "#{ ActiveRecord::Base.table_name_prefix }"\
+ "#{ fixture_set_name.tr('/', '_') }"\
+ "#{ ActiveRecord::Base.table_name_suffix }".to_sym
end
def self.reset_cache
@@ -415,11 +414,11 @@ module ActiveRecord
cache_for_connection(connection).update(fixtures_map)
end
- def self.instantiate_fixtures(object, fixture_name, fixtures, load_instances = true)
+ def self.instantiate_fixtures(object, fixture_set, load_instances = true)
if load_instances
- fixtures.each do |name, fixture|
+ fixture_set.each do |fixture_name, fixture|
begin
- object.instance_variable_set "@#{name}", fixture.find
+ object.instance_variable_set "@#{fixture_name}", fixture.find
rescue FixtureClassNotFound
nil
end
@@ -428,61 +427,59 @@ module ActiveRecord
end
def self.instantiate_all_loaded_fixtures(object, load_instances = true)
- all_loaded_fixtures.each do |table_name, fixtures|
- ActiveRecord::Fixtures.instantiate_fixtures(object, table_name, fixtures, load_instances)
+ all_loaded_fixtures.each_value do |fixture_set|
+ instantiate_fixtures(object, fixture_set, load_instances)
end
end
cattr_accessor :all_loaded_fixtures
self.all_loaded_fixtures = {}
- def self.create_fixtures(fixtures_directory, table_names, class_names = {})
- table_names = Array(table_names).map(&:to_s)
+ def self.create_fixtures(fixtures_directory, fixture_set_names, class_names = {})
+ fixture_set_names = Array(fixture_set_names).map(&:to_s)
class_names = class_names.stringify_keys
# FIXME: Apparently JK uses this.
connection = block_given? ? yield : ActiveRecord::Base.connection
- files_to_read = table_names.reject { |table_name|
- fixture_is_cached?(connection, table_name)
+ files_to_read = fixture_set_names.reject { |fs_name|
+ fixture_is_cached?(connection, fs_name)
}
unless files_to_read.empty?
connection.disable_referential_integrity do
fixtures_map = {}
- fixture_files = files_to_read.map do |path|
- fixture_name = path
-
- fixtures_map[fixture_name] = new( # ActiveRecord::Fixtures.new
+ fixture_sets = files_to_read.map do |fs_name|
+ fixtures_map[fs_name] = new( # ActiveRecord::Fixtures.new
connection,
- fixture_name,
- class_names[fixture_name.to_s] || default_fixture_model_name(fixture_name),
- ::File.join(fixtures_directory, path))
+ fs_name,
+ class_names[fs_name] || default_fixture_model_name(fs_name),
+ ::File.join(fixtures_directory, fs_name))
end
all_loaded_fixtures.update(fixtures_map)
connection.transaction(:requires_new => true) do
- fixture_files.each do |ff|
- conn = ff.model_class.respond_to?(:connection) ? ff.model_class.connection : connection
- table_rows = ff.table_rows
+ fixture_sets.each do |fs|
+ conn = fs.model_class.respond_to?(:connection) ? fs.model_class.connection : connection
+ table_rows = fs.table_rows
table_rows.keys.each do |table|
conn.delete "DELETE FROM #{conn.quote_table_name(table)}", 'Fixture Delete'
end
- table_rows.each do |table_name,rows|
+ table_rows.each do |fixture_set_name, rows|
rows.each do |row|
- conn.insert_fixture(row, table_name)
+ conn.insert_fixture(row, fixture_set_name)
end
end
end
# Cap primary key sequences to max(pk).
if connection.respond_to?(:reset_pk_sequence!)
- fixture_files.each do |ff|
- connection.reset_pk_sequence!(ff.table_name)
+ fixture_sets.each do |fs|
+ connection.reset_pk_sequence!(fs.table_name)
end
end
end
@@ -490,7 +487,7 @@ module ActiveRecord
cache_fixtures(connection, fixtures_map)
end
end
- cached_fixtures(connection, table_names)
+ cached_fixtures(connection, fixture_set_names)
end
# Returns a consistent, platform-independent identifier for +label+.
@@ -501,26 +498,23 @@ module ActiveRecord
attr_reader :table_name, :name, :fixtures, :model_class
- def initialize(connection, fixture_name, class_name, fixture_path)
- @connection = connection
- @fixture_path = fixture_path
- @name = fixture_name
- @class_name = class_name
-
- @fixtures = {}
+ def initialize(connection, name, class_name, path)
+ @fixtures = {} # Ordered hash
+ @name = name
+ @path = path
- # Should be an AR::Base type class
- if class_name.is_a?(Class)
+ if class_name.is_a?(Class) # TODO: Should be an AR::Base type class, or any?
@model_class = class_name
else
@model_class = class_name.constantize rescue nil
end
- @connection = model_class.connection if model_class && model_class.respond_to?(:connection)
+ @connection = ( model_class.respond_to?(:connection) ?
+ model_class.connection : connection )
@table_name = ( model_class.respond_to?(:table_name) ?
model_class.table_name :
- self.class.default_fixture_table_name(fixture_name) )
+ self.class.default_fixture_table_name(name) )
read_fixture_files
end
@@ -559,8 +553,8 @@ module ActiveRecord
if model_class && model_class < ActiveRecord::Model
# fill in timestamp columns if they aren't specified and the model is set to record_timestamps
if model_class.record_timestamps
- timestamp_column_names.each do |name|
- row[name] = now unless row.key?(name)
+ timestamp_column_names.each do |c_name|
+ row[c_name] = now unless row.key?(c_name)
end
end
@@ -599,7 +593,7 @@ module ActiveRecord
when :has_and_belongs_to_many
if (targets = row.delete(association.name.to_s))
targets = targets.is_a?(Array) ? targets : targets.split(/\s*,\s*/)
- table_name = association.options[:join_table]
+ table_name = association.join_table
rows[table_name].concat targets.map { |target|
{ association.foreign_key => row[primary_key_name],
association.association_foreign_key => ActiveRecord::Fixtures.identify(target) }
@@ -638,26 +632,23 @@ module ActiveRecord
end
def read_fixture_files
- yaml_files = Dir["#{@fixture_path}/**/*.yml"].select { |f|
+ yaml_files = Dir["#{@path}/**/*.yml"].select { |f|
::File.file?(f)
} + [yaml_file_path]
yaml_files.each do |file|
Fixtures::File.open(file) do |fh|
- fh.each do |name, row|
- fixtures[name] = ActiveRecord::Fixture.new(row, model_class)
+ fh.each do |fixture_name, row|
+ fixtures[fixture_name] = ActiveRecord::Fixture.new(row, model_class)
end
end
end
end
def yaml_file_path
- "#{@fixture_path}.yml"
+ "#{@path}.yml"
end
- def yaml_fixtures_key(path)
- ::File.basename(@fixture_path).split(".").first
- end
end
class Fixture #:nodoc:
@@ -712,7 +703,7 @@ module ActiveRecord
class_attribute :fixture_table_names
class_attribute :fixture_class_names
class_attribute :use_transactional_fixtures
- class_attribute :use_instantiated_fixtures # true, false, or :no_instances
+ class_attribute :use_instantiated_fixtures # true, false, or :no_instances
class_attribute :pre_loaded_fixtures
self.fixture_table_names = []
@@ -720,9 +711,8 @@ module ActiveRecord
self.use_instantiated_fixtures = false
self.pre_loaded_fixtures = false
- self.fixture_class_names = Hash.new do |h, fixture_name|
- fixture_name = fixture_name.to_s
- h[fixture_name] = ActiveRecord::Fixtures.default_fixture_model_name(fixture_name)
+ self.fixture_class_names = Hash.new do |h, fixture_set_name|
+ h[fixture_set_name] = ActiveRecord::Fixtures.default_fixture_model_name(fixture_set_name)
end
end
@@ -746,18 +736,18 @@ module ActiveRecord
self.fixture_class_names = self.fixture_class_names.merge(class_names.stringify_keys)
end
- def fixtures(*fixture_names)
- if fixture_names.first == :all
- fixture_names = Dir["#{fixture_path}/**/*.yml"].map { |f|
+ def fixtures(*fixture_set_names)
+ if fixture_set_names.first == :all
+ fixture_set_names = Dir["#{fixture_path}/**/*.yml"].map { |f|
File.basename f, '.yml'
}
else
- fixture_names = fixture_names.flatten.map { |n| n.to_s }
+ fixture_set_names = fixture_set_names.flatten.map { |n| n.to_s }
end
- self.fixture_table_names |= fixture_names
- require_fixture_classes(fixture_names)
- setup_fixture_accessors(fixture_names)
+ self.fixture_table_names |= fixture_set_names
+ require_fixture_classes(fixture_set_names)
+ setup_fixture_accessors(fixture_set_names)
end
def try_to_load_dependency(file_name)
@@ -772,35 +762,39 @@ module ActiveRecord
end
end
- def require_fixture_classes(fixture_names = nil)
- (fixture_names || fixture_table_names).each do |fixture_name|
- file_name = fixture_name.to_s
+ def require_fixture_classes(fixture_set_names = nil)
+ if fixture_set_names
+ fixture_set_names = fixture_set_names.map { |n| n.to_s }
+ else
+ fixture_set_names = fixture_table_names
+ end
+
+ fixture_set_names.each do |file_name|
file_name = file_name.singularize if ActiveRecord::Base.pluralize_table_names
try_to_load_dependency(file_name)
end
end
- def setup_fixture_accessors(fixture_names = nil)
- fixture_names = Array(fixture_names || fixture_table_names)
+ def setup_fixture_accessors(fixture_set_names = nil)
+ fixture_set_names = Array(fixture_set_names || fixture_table_names)
methods = Module.new do
- fixture_names.each do |fixture_name|
- fixture_name = fixture_name.to_s
- accessor_name = fixture_name.tr('/', '_').to_sym
+ fixture_set_names.each do |fs_name|
+ fs_name = fs_name.to_s
+ accessor_name = fs_name.tr('/', '_').to_sym
- define_method(accessor_name) do |*fixtures|
- force_reload = fixtures.pop if fixtures.last == true || fixtures.last == :reload
+ define_method(accessor_name) do |*fixture_names|
+ force_reload = fixture_names.pop if fixture_names.last == true || fixture_names.last == :reload
- @fixture_cache[fixture_name] ||= {}
+ @fixture_cache[fs_name] ||= {}
- instances = fixtures.map do |fixture|
- @fixture_cache[fixture_name].delete(fixture) if force_reload
+ instances = fixture_names.map do |f_name|
+ f_name = f_name.to_s
+ @fixture_cache[fs_name].delete(f_name) if force_reload
- if @loaded_fixtures[fixture_name][fixture.to_s]
- ActiveRecord::IdentityMap.without do
- @fixture_cache[fixture_name][fixture] ||= @loaded_fixtures[fixture_name][fixture.to_s].find
- end
+ if @loaded_fixtures[fs_name][f_name]
+ @fixture_cache[fs_name][f_name] ||= @loaded_fixtures[fs_name][f_name].find
else
- raise StandardError, "No entry named '#{fixture}' found for fixture collection '#{fixture_name}'"
+ raise StandardError, "No fixture named '#{f_name}' found for fixture set '#{fs_name}'"
end
end
@@ -829,7 +823,7 @@ module ActiveRecord
end
def setup_fixtures
- return unless !ActiveRecord::Base.configurations.blank?
+ return if ActiveRecord::Base.configurations.blank?
if pre_loaded_fixtures && !use_transactional_fixtures
raise RuntimeError, 'pre_loaded_fixtures requires use_transactional_fixtures'
@@ -849,9 +843,7 @@ module ActiveRecord
end
@fixture_connections = enlist_fixture_connections
@fixture_connections.each do |connection|
- connection.increment_open_transactions
- connection.transaction_joinable = false
- connection.begin_db_transaction
+ connection.begin_transaction joinable: false
end
# Load fixtures for every test.
else
@@ -874,10 +866,7 @@ module ActiveRecord
# Rollback changes if a transaction is active.
if run_in_transaction?
@fixture_connections.each do |connection|
- if connection.open_transactions != 0
- connection.rollback_db_transaction
- connection.decrement_open_transactions
- end
+ connection.rollback_transaction if connection.transaction_open?
end
@fixture_connections.clear
end
@@ -885,7 +874,7 @@ module ActiveRecord
end
def enlist_fixture_connections
- ActiveRecord::Base.connection_handler.connection_pools.values.map(&:connection)
+ ActiveRecord::Base.connection_handler.connection_pools.map(&:connection)
end
private
@@ -907,8 +896,8 @@ module ActiveRecord
ActiveRecord::Fixtures.instantiate_all_loaded_fixtures(self, load_instances?)
else
raise RuntimeError, 'Load fixtures before instantiating them.' if @loaded_fixtures.nil?
- @loaded_fixtures.each do |fixture_name, fixtures|
- ActiveRecord::Fixtures.instantiate_fixtures(self, fixture_name, fixtures, load_instances?)
+ @loaded_fixtures.each_value do |fixture_set|
+ ActiveRecord::Fixtures.instantiate_fixtures(self, fixture_set, load_instances?)
end
end
end
diff --git a/activerecord/lib/active_record/fixtures/file.rb b/activerecord/lib/active_record/fixtures/file.rb
index 6547791144..a9cabf5a7b 100644
--- a/activerecord/lib/active_record/fixtures/file.rb
+++ b/activerecord/lib/active_record/fixtures/file.rb
@@ -24,37 +24,33 @@ module ActiveRecord
rows.each(&block)
end
- RESCUE_ERRORS = [ ArgumentError ] # :nodoc:
+ RESCUE_ERRORS = [ ArgumentError, Psych::SyntaxError ] # :nodoc:
private
- if defined?(Psych) && defined?(Psych::SyntaxError)
- RESCUE_ERRORS << Psych::SyntaxError
- end
-
- def rows
- return @rows if @rows
+ def rows
+ return @rows if @rows
+
+ begin
+ data = YAML.load(render(IO.read(@file)))
+ rescue *RESCUE_ERRORS => error
+ raise Fixture::FormatError, "a YAML error occurred parsing #{@file}. Please note that YAML must be consistently indented using spaces. Tabs are not allowed. Please have a look at http://www.yaml.org/faq.html\nThe exact error was:\n #{error.class}: #{error}", error.backtrace
+ end
+ @rows = data ? validate(data).to_a : []
+ end
- begin
- data = YAML.load(render(IO.read(@file)))
- rescue *RESCUE_ERRORS => error
- raise Fixture::FormatError, "a YAML error occurred parsing #{@file}. Please note that YAML must be consistently indented using spaces. Tabs are not allowed. Please have a look at http://www.yaml.org/faq.html\nThe exact error was:\n #{error.class}: #{error}", error.backtrace
+ def render(content)
+ ERB.new(content).result
end
- @rows = data ? validate(data).to_a : []
- end
- def render(content)
- ERB.new(content).result
- end
+ # Validate our unmarshalled data.
+ def validate(data)
+ unless Hash === data || YAML::Omap === data
+ raise Fixture::FormatError, 'fixture is not a hash'
+ end
- # Validate our unmarshalled data.
- def validate(data)
- unless Hash === data || YAML::Omap === data
- raise Fixture::FormatError, 'fixture is not a hash'
+ raise Fixture::FormatError unless data.all? { |name, row| Hash === row }
+ data
end
-
- raise Fixture::FormatError unless data.all? { |name, row| Hash === row }
- data
- end
end
end
end
diff --git a/activerecord/lib/active_record/identity_map.rb b/activerecord/lib/active_record/identity_map.rb
deleted file mode 100644
index d9777bb2f6..0000000000
--- a/activerecord/lib/active_record/identity_map.rb
+++ /dev/null
@@ -1,144 +0,0 @@
-module ActiveRecord
- # = Active Record Identity Map
- #
- # Ensures that each object gets loaded only once by keeping every loaded
- # object in a map. Looks up objects using the map when referring to them.
- #
- # More information on Identity Map pattern:
- # http://www.martinfowler.com/eaaCatalog/identityMap.html
- #
- # == Configuration
- #
- # In order to enable IdentityMap, set <tt>config.active_record.identity_map = true</tt>
- # in your <tt>config/application.rb</tt> file.
- #
- # IdentityMap is disabled by default and still in development (i.e. use it with care).
- #
- # == Associations
- #
- # Active Record Identity Map does not track associations yet. For example:
- #
- # comment = @post.comments.first
- # comment.post = nil
- # @post.comments.include?(comment) #=> true
- #
- # Ideally, the example above would return false, removing the comment object from the
- # post association when the association is nullified. This may cause side effects, as
- # in the situation below, if Identity Map is enabled:
- #
- # Post.has_many :comments, :dependent => :destroy
- #
- # comment = @post.comments.first
- # comment.post = nil
- # comment.save
- # Post.destroy(@post.id)
- #
- # Without using Identity Map, the code above will destroy the @post object leaving
- # the comment object intact. However, once we enable Identity Map, the post loaded
- # by Post.destroy is exactly the same object as the object @post. As the object @post
- # still has the comment object in @post.comments, once Identity Map is enabled, the
- # comment object will be accidently removed.
- #
- # This inconsistency is meant to be fixed in future Rails releases.
- #
- module IdentityMap
-
- class << self
- def enabled=(flag)
- Thread.current[:identity_map_enabled] = flag
- end
-
- def enabled
- Thread.current[:identity_map_enabled]
- end
- alias enabled? enabled
-
- def repository
- Thread.current[:identity_map] ||= Hash.new { |h,k| h[k] = {} }
- end
-
- def use
- old, self.enabled = enabled, true
-
- yield if block_given?
- ensure
- self.enabled = old
- clear
- end
-
- def without
- old, self.enabled = enabled, false
-
- yield if block_given?
- ensure
- self.enabled = old
- end
-
- def get(klass, primary_key)
- record = repository[klass.symbolized_sti_name][primary_key]
-
- if record.is_a?(klass)
- ActiveSupport::Notifications.instrument("identity.active_record",
- :line => "From Identity Map (id: #{primary_key})",
- :name => "#{klass} Loaded",
- :connection_id => object_id)
-
- record
- else
- nil
- end
- end
-
- def add(record)
- repository[record.class.symbolized_sti_name][record.id] = record
- end
-
- def remove(record)
- repository[record.class.symbolized_sti_name].delete(record.id)
- end
-
- def remove_by_id(symbolized_sti_name, id)
- repository[symbolized_sti_name].delete(id)
- end
-
- def clear
- repository.clear
- end
- end
-
- # Reinitialize an Identity Map model object from +coder+.
- # +coder+ must contain the attributes necessary for initializing an empty
- # model object.
- def reinit_with(coder)
- @attributes_cache = {}
- dirty = @changed_attributes.keys
- attributes = self.class.initialize_attributes(coder['attributes'].except(*dirty))
- @attributes.update(attributes)
- @changed_attributes.update(coder['attributes'].slice(*dirty))
- @changed_attributes.delete_if{|k,v| v.eql? @attributes[k]}
-
- run_callbacks :find
-
- self
- end
-
- class Middleware
- def initialize(app)
- @app = app
- end
-
- def call(env)
- enabled = IdentityMap.enabled
- IdentityMap.enabled = true
-
- response = @app.call(env)
- response[2] = Rack::BodyProxy.new(response[2]) do
- IdentityMap.enabled = enabled
- IdentityMap.clear
- end
-
- response
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb
index 2c766411a0..35273b0d81 100644
--- a/activerecord/lib/active_record/inheritance.rb
+++ b/activerecord/lib/active_record/inheritance.rb
@@ -1,13 +1,16 @@
-require 'active_support/concern'
module ActiveRecord
+ ActiveSupport.on_load(:active_record_config) do
+ # Determine whether to store the full constant name including namespace when using STI
+ mattr_accessor :store_full_sti_class, instance_accessor: false
+ self.store_full_sti_class = true
+ end
+
module Inheritance
extend ActiveSupport::Concern
included do
- # Determine whether to store the full constant name including namespace when using STI
config_attribute :store_full_sti_class
- self.store_full_sti_class = true
end
module ClassMethods
@@ -37,17 +40,43 @@ module ActiveRecord
@symbolized_sti_name ||= sti_name.present? ? sti_name.to_sym : symbolized_base_class
end
- # Returns the base AR subclass that this class descends from. If A
- # extends AR::Base, A.base_class will return A. If B descends from A
+ # Returns the class descending directly from ActiveRecord::Base (or
+ # that includes ActiveRecord::Model), or an abstract class, if any, in
+ # the inheritance hierarchy.
+ #
+ # If A extends AR::Base, A.base_class will return A. If B descends from A
# through some arbitrarily deep hierarchy, B.base_class will return A.
#
# If B < A and C < B and if A is an abstract_class then both B.base_class
# and C.base_class would return B as the answer since A is an abstract_class.
def base_class
- class_of_active_record_descendant(self)
+ unless self < ActiveRecord::Tag
+ raise ActiveRecordError, "#{name} doesn't belong in a hierarchy descending from ActiveRecord"
+ end
+
+ sup = active_record_super
+ if sup == Base || sup == Model || sup.abstract_class?
+ self
+ else
+ sup.base_class
+ end
end
# Set this to true if this is an abstract class (see <tt>abstract_class?</tt>).
+ # If you are using inheritance with ActiveRecord and don't want child classes
+ # to utilize the implied STI table name of the parent class, this will need to be true.
+ # For example, given the following:
+ #
+ # class SuperClass < ActiveRecord::Base
+ # self.abstract_class = true
+ # end
+ # class Child < SuperClass
+ # self.table_name = 'the_table_i_really_want'
+ # end
+ #
+ #
+ # <tt>self.abstract_class = true</tt> is required to make <tt>Child<.find,.create, or any Arel method></tt> use <tt>the_table_i_really_want</tt> instead of a table called <tt>super_classes</tt>
+ #
attr_accessor :abstract_class
# Returns whether this class is an abstract class or not.
@@ -63,26 +92,9 @@ module ActiveRecord
# single-table inheritance model that makes it possible to create
# objects of different types from the same table.
def instantiate(record, column_types = {})
- sti_class = find_sti_class(record[inheritance_column])
- record_id = sti_class.primary_key && record[sti_class.primary_key]
-
- if ActiveRecord::IdentityMap.enabled? && record_id
- if (column = sti_class.columns_hash[sti_class.primary_key]) && column.number?
- record_id = record_id.to_i
- end
- if instance = IdentityMap.get(sti_class, record_id)
- instance.reinit_with('attributes' => record)
- else
- instance = sti_class.allocate.init_with('attributes' => record)
- IdentityMap.add(instance)
- end
- else
- column_types = sti_class.decorate_columns(column_types)
- instance = sti_class.allocate.init_with('attributes' => record,
- 'column_types' => column_types)
- end
-
- instance
+ sti_class = find_sti_class(record[inheritance_column])
+ column_types = sti_class.decorate_columns(column_types)
+ sti_class.allocate.init_with('attributes' => record, 'column_types' => column_types)
end
# For internal use.
@@ -95,21 +107,6 @@ module ActiveRecord
protected
- # Returns the class descending directly from ActiveRecord::Base or an
- # abstract class, if any, in the inheritance hierarchy.
- def class_of_active_record_descendant(klass)
- unless klass < Model
- raise ActiveRecordError, "#{name} doesn't belong in a hierarchy descending from ActiveRecord"
- end
-
- sup = klass.active_record_super
- if [Base, Model].include?(klass) || [Base, Model].include?(sup) || sup.abstract_class?
- klass
- else
- class_of_active_record_descendant(sup)
- end
- end
-
# Returns the class type of the record using the current module as a prefix. So descendants of
# MyApp::Business::Account would appear as MyApp::Business::AccountSubclass.
def compute_type(type_name)
diff --git a/activerecord/lib/active_record/integration.rb b/activerecord/lib/active_record/integration.rb
index 2c42f4cca5..23c272ef12 100644
--- a/activerecord/lib/active_record/integration.rb
+++ b/activerecord/lib/active_record/integration.rb
@@ -39,7 +39,7 @@ module ActiveRecord
when new_record?
"#{self.class.model_name.cache_key}/new"
when timestamp = self[:updated_at]
- timestamp = timestamp.utc.to_s(:number)
+ timestamp = timestamp.utc.to_s(:nsec)
"#{self.class.model_name.cache_key}/#{id}-#{timestamp}"
else
"#{self.class.model_name.cache_key}/#{id}"
diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb
index 4d73cdd37a..e96ed00f9c 100644
--- a/activerecord/lib/active_record/locking/optimistic.rb
+++ b/activerecord/lib/active_record/locking/optimistic.rb
@@ -1,4 +1,9 @@
module ActiveRecord
+ ActiveSupport.on_load(:active_record_config) do
+ mattr_accessor :lock_optimistically, instance_accessor: false
+ self.lock_optimistically = true
+ end
+
module Locking
# == What is Optimistic Locking
#
@@ -51,8 +56,7 @@ module ActiveRecord
extend ActiveSupport::Concern
included do
- config_attribute :lock_optimistically, :global => true
- self.lock_optimistically = true
+ config_attribute :lock_optimistically
end
def locking_enabled? #:nodoc:
@@ -82,9 +86,9 @@ module ActiveRecord
stmt = relation.where(
relation.table[self.class.primary_key].eq(id).and(
- relation.table[lock_col].eq(quote_value(previous_lock_value))
+ relation.table[lock_col].eq(self.class.quote_value(previous_lock_value))
)
- ).arel.compile_update(arel_attributes_values(false, false, attribute_names))
+ ).arel.compile_update(arel_attributes_with_values_for_update(attribute_names))
affected_rows = connection.update stmt
@@ -101,24 +105,29 @@ module ActiveRecord
end
end
- def destroy #:nodoc:
- return super unless locking_enabled?
+ def destroy_row
+ affected_rows = super
- if persisted?
- table = self.class.arel_table
- lock_col = self.class.locking_column
- predicate = table[self.class.primary_key].eq(id).
- and(table[lock_col].eq(send(lock_col).to_i))
+ if locking_enabled? && affected_rows != 1
+ raise ActiveRecord::StaleObjectError.new(self, "destroy")
+ end
- affected_rows = self.class.unscoped.where(predicate).delete_all
+ affected_rows
+ end
- unless affected_rows == 1
- raise ActiveRecord::StaleObjectError.new(self, "destroy")
- end
+ def relation_for_destroy
+ relation = super
+
+ if locking_enabled?
+ column_name = self.class.locking_column
+ column = self.class.columns_hash[column_name]
+ substitute = connection.substitute_at(column, relation.bind_values.length)
+
+ relation = relation.where(self.class.arel_table[column_name].eq(substitute))
+ relation.bind_values << [column, self[column_name].to_i]
end
- @destroyed = true
- freeze
+ relation
end
module ClassMethods
@@ -133,8 +142,7 @@ module ActiveRecord
# Set the column to use for optimistic locking. Defaults to +lock_version+.
def locking_column=(value)
- @original_locking_column = @locking_column if defined?(@locking_column)
- @locking_column = value.to_s
+ @locking_column = value.to_s
end
# The version column used for optimistic locking. Defaults to +lock_version+.
@@ -160,16 +168,16 @@ module ActiveRecord
super
end
- # If the locking column has no default value set,
- # start the lock version at zero. Note we can't use
- # <tt>locking_enabled?</tt> at this point as
- # <tt>@attributes</tt> may not have been initialized yet.
- def initialize_attributes(attributes) #:nodoc:
- if attributes.key?(locking_column) && lock_optimistically
- attributes[locking_column] ||= 0
- end
+ def column_defaults
+ @column_defaults ||= begin
+ defaults = super
- attributes
+ if defaults.key?(locking_column) && lock_optimistically
+ defaults[locking_column] ||= 0
+ end
+
+ defaults
+ end
end
end
end
diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb
index 2a9139749d..c1d57855a9 100644
--- a/activerecord/lib/active_record/migration.rb
+++ b/activerecord/lib/active_record/migration.rb
@@ -1,7 +1,4 @@
-require "active_support/core_ext/module/delegation"
require "active_support/core_ext/class/attribute_accessors"
-require 'active_support/deprecation'
-require 'active_record/schema_migration'
require 'set'
module ActiveRecord
@@ -33,6 +30,12 @@ module ActiveRecord
end
end
+ class PendingMigrationError < ActiveRecordError#:nodoc:
+ def initialize
+ super("Migrations are pending run 'rake db:migrate RAILS_ENV=#{ENV['RAILS_ENV']}' to resolve the issue")
+ end
+ end
+
# = Active Record Migrations
#
# Migrations can manage the evolution of a schema used by several physical
@@ -47,7 +50,7 @@ module ActiveRecord
#
# class AddSsl < ActiveRecord::Migration
# def up
- # add_column :accounts, :ssl_enabled, :boolean, :default => 1
+ # add_column :accounts, :ssl_enabled, :boolean, :default => true
# end
#
# def down
@@ -327,10 +330,28 @@ module ActiveRecord
class Migration
autoload :CommandRecorder, 'active_record/migration/command_recorder'
+
+ # This class is used to verify that all migrations have been run before
+ # loading a web page if config.active_record.migration_error is set to :page_load
+ class CheckPending
+ def initialize(app)
+ @app = app
+ end
+
+ def call(env)
+ ActiveRecord::Migration.check_pending!
+ @app.call(env)
+ end
+ end
+
class << self
attr_accessor :delegate # :nodoc:
end
+ def self.check_pending!
+ raise ActiveRecord::PendingMigrationError if ActiveRecord::Migrator.needs_migration?
+ end
+
def self.method_missing(name, *args, &block) # :nodoc:
(delegate || superclass.delegate).send(name, *args, &block)
end
@@ -606,6 +627,14 @@ module ActiveRecord
end
end
+ def needs_migration?
+ current_version < last_version
+ end
+
+ def last_version
+ migrations(migrations_paths).last.try(:version)||0
+ end
+
def proper_table_name(name)
# Use the Active Record objects own table_name, or pre/suffix from ActiveRecord::Base if name is a symbol/string
name.table_name rescue "#{ActiveRecord::Base.table_name_prefix}#{name}#{ActiveRecord::Base.table_name_suffix}"
diff --git a/activerecord/lib/active_record/migration/command_recorder.rb b/activerecord/lib/active_record/migration/command_recorder.rb
index 96b62fdd61..95f4360578 100644
--- a/activerecord/lib/active_record/migration/command_recorder.rb
+++ b/activerecord/lib/active_record/migration/command_recorder.rb
@@ -51,13 +51,15 @@ module ActiveRecord
super || delegate.respond_to?(*args)
end
- [:create_table, :create_join_table, :change_table, :rename_table, :add_column, :remove_column, :rename_index, :rename_column, :add_index, :remove_index, :add_timestamps, :remove_timestamps, :change_column, :change_column_default].each do |method|
+ [:create_table, :create_join_table, :change_table, :rename_table, :add_column, :remove_column, :rename_index, :rename_column, :add_index, :remove_index, :add_timestamps, :remove_timestamps, :change_column, :change_column_default, :add_reference, :remove_reference].each do |method|
class_eval <<-EOV, __FILE__, __LINE__ + 1
def #{method}(*args) # def create_table(*args)
record(:"#{method}", args) # record(:create_table, args)
end # end
EOV
end
+ alias :add_belongs_to :add_reference
+ alias :remove_belongs_to :remove_reference
private
@@ -102,6 +104,16 @@ module ActiveRecord
[:remove_timestamps, args]
end
+ def invert_add_reference(args)
+ [:remove_reference, args]
+ end
+ alias :invert_add_belongs_to :invert_add_reference
+
+ def invert_remove_reference(args)
+ [:add_reference, args]
+ end
+ alias :invert_remove_belongs_to :invert_remove_reference
+
# Forwards any missing method call to the \target.
def method_missing(method, *args, &block)
@delegate.send(method, *args, &block)
diff --git a/activerecord/lib/active_record/migration/join_table.rb b/activerecord/lib/active_record/migration/join_table.rb
index 01a580781b..e880ae97bb 100644
--- a/activerecord/lib/active_record/migration/join_table.rb
+++ b/activerecord/lib/active_record/migration/join_table.rb
@@ -4,13 +4,11 @@ module ActiveRecord
private
def find_join_table_name(table_1, table_2, options = {})
- options.delete(:table_name) { join_table_name(table_1, table_2) }
+ options.delete(:table_name) || join_table_name(table_1, table_2)
end
def join_table_name(table_1, table_2)
- tables_names = [table_1, table_2].map(&:to_s).sort
-
- tables_names.join("_").to_sym
+ [table_1, table_2].sort.join("_").to_sym
end
end
end
diff --git a/activerecord/lib/active_record/model.rb b/activerecord/lib/active_record/model.rb
index 86de5ab2fa..16d9d404e3 100644
--- a/activerecord/lib/active_record/model.rb
+++ b/activerecord/lib/active_record/model.rb
@@ -1,49 +1,68 @@
-require 'active_support/deprecation'
+require 'active_support/core_ext/module/attribute_accessors'
module ActiveRecord
- # <tt>ActiveRecord::Model</tt> can be included into a class to add Active Record persistence.
- # This is an alternative to inheriting from <tt>ActiveRecord::Base</tt>. Example:
+ module Configuration # :nodoc:
+ # This just abstracts out how we define configuration options in AR. Essentially we
+ # have mattr_accessors on the ActiveRecord:Model constant that define global defaults.
+ # Classes that then use AR get class_attributes defined, which means that when they
+ # are assigned the default will be overridden for that class and subclasses. (Except
+ # when options[:global] == true, in which case there is one global value always.)
+ def config_attribute(name, options = {})
+ if options[:global]
+ class_eval <<-CODE, __FILE__, __LINE__ + 1
+ def self.#{name}; ActiveRecord::Model.#{name}; end
+ def #{name}; ActiveRecord::Model.#{name}; end
+ def self.#{name}=(val); ActiveRecord::Model.#{name} = val; end
+ CODE
+ else
+ options[:instance_writer] ||= false
+ class_attribute name, options
+
+ singleton_class.class_eval <<-CODE, __FILE__, __LINE__ + 1
+ remove_method :#{name}
+ def #{name}; ActiveRecord::Model.#{name}; end
+ CODE
+ end
+ end
+ end
+
+ # This allows us to detect an ActiveRecord::Model while it's in the process of
+ # being included.
+ module Tag; end
+
+ # <tt>ActiveRecord::Model</tt> can be included into a class to add Active Record
+ # persistence. This is an alternative to inheriting from <tt>ActiveRecord::Base</tt>.
#
# class Post
# include ActiveRecord::Model
# end
- #
module Model
- module ClassMethods #:nodoc:
- include ActiveSupport::Callbacks::ClassMethods
- include ActiveModel::Naming
- include QueryCache::ClassMethods
- include ActiveSupport::Benchmarkable
- include ActiveSupport::DescendantsTracker
-
- include Querying
- include Translation
- include DynamicMatchers
- include CounterCache
- include Explain
- include ConnectionHandling
- end
-
- def self.included(base)
- return if base.singleton_class < ClassMethods
+ extend ActiveSupport::Concern
+ extend ConnectionHandling
+ extend ActiveModel::Observing::ClassMethods
+ def self.append_features(base)
base.class_eval do
- extend ClassMethods
- Callbacks::Register.setup(self)
- initialize_generated_modules unless self == Base
+ include Tag
+ extend Configuration
end
+
+ super
end
- extend ActiveModel::Configuration
- extend ActiveModel::Callbacks
- extend ActiveModel::MassAssignmentSecurity::ClassMethods
- extend ActiveModel::AttributeMethods::ClassMethods
- extend Callbacks::Register
- extend Explain
- extend ConnectionHandling
+ included do
+ extend ActiveModel::Naming
+ extend ActiveSupport::Benchmarkable
+ extend ActiveSupport::DescendantsTracker
+
+ extend QueryCache::ClassMethods
+ extend Querying
+ extend Translation
+ extend DynamicMatchers
+ extend Explain
+ extend ConnectionHandling
- def self.extend(*modules)
- ClassMethods.send(:include, *modules)
+ initialize_generated_modules unless self == Base
end
include Persistence
@@ -52,18 +71,26 @@ module ActiveRecord
include Inheritance
include Scoping
include Sanitization
- include Integration
include AttributeAssignment
include ActiveModel::Conversion
+ include Integration
include Validations
- include Locking::Optimistic, Locking::Pessimistic
+ include CounterCache
+ include Locking::Optimistic
+ include Locking::Pessimistic
include AttributeMethods
- include Callbacks, ActiveModel::Observing, Timestamp
+ include Callbacks
+ include ActiveModel::Observing
+ include Timestamp
include Associations
- include IdentityMap
include ActiveModel::SecurePassword
- include AutosaveAssociation, NestedAttributes
- include Aggregations, Transactions, Reflection, Serialization, Store
+ include AutosaveAssociation
+ include NestedAttributes
+ include Aggregations
+ include Transactions
+ include Reflection
+ include Serialization
+ include Store
include Core
class << self
@@ -75,35 +102,69 @@ module ActiveRecord
false
end
+ # Defines the name of the table column which will store the class name on single-table
+ # inheritance situations.
+ #
+ # The default inheritance column name is +type+, which means it's a
+ # reserved word inside Active Record. To be able to use single-table
+ # inheritance with another column name, or to use the column +type+ in
+ # your own model for something else, you can override this method to
+ # return a different name:
+ #
+ # def self.inheritance_column
+ # 'zoink'
+ # end
def inheritance_column
'type'
end
end
- module DeprecationProxy #:nodoc:
- class << self
- instance_methods.each { |m| undef_method m unless m =~ /^__|^object_id$|^instance_eval$/ }
-
- def method_missing(name, *args, &block)
- if Model.respond_to?(name)
- Model.send(name, *args, &block)
- else
- ActiveSupport::Deprecation.warn(
- "The object passed to the active_record load hook was previously ActiveRecord::Base " \
- "(a Class). Now it is ActiveRecord::Model (a Module). You have called `#{name}' which " \
- "is only defined on ActiveRecord::Base. Please change your code so that it works with " \
- "a module rather than a class. (Model is included in Base, so anything added to Model " \
- "will be available on Base as well.)"
- )
- Base.send(name, *args, &block)
- end
+ class DeprecationProxy < BasicObject #:nodoc:
+ def initialize(model = Model, base = Base)
+ @model = model
+ @base = base
+ end
+
+ def method_missing(name, *args, &block)
+ if @model.respond_to?(name, true)
+ @model.send(name, *args, &block)
+ else
+ ::ActiveSupport::Deprecation.warn(
+ "The object passed to the active_record load hook was previously ActiveRecord::Base " \
+ "(a Class). Now it is ActiveRecord::Model (a Module). You have called `#{name}' which " \
+ "is only defined on ActiveRecord::Base. Please change your code so that it works with " \
+ "a module rather than a class. (Model is included in Base, so anything added to Model " \
+ "will be available on Base as well.)"
+ )
+ @base.send(name, *args, &block)
end
+ end
+
+ alias send method_missing
- alias send method_missing
+ def extend(*mods)
+ ::ActiveSupport::Deprecation.warn(
+ "The object passed to the active_record load hook was previously ActiveRecord::Base " \
+ "(a Class). Now it is ActiveRecord::Model (a Module). You have called `extend' which " \
+ "would add singleton methods to Model. This is presumably not what you want, since the " \
+ "methods would not be inherited down to Base. Rather than using extend, please use " \
+ "ActiveSupport::Concern + include, which will ensure that your class methods are " \
+ "inherited."
+ )
+ @base.extend(*mods)
end
end
end
+ # This hook is where config accessors on Model get defined.
+ #
+ # We don't want to just open the Model module and add stuff to it in other files, because
+ # that would cause Model to load, which causes all sorts of loading order issues.
+ #
+ # We need this hook rather than just using the :active_record one, because users of the
+ # :active_record hook may need to use config options.
+ ActiveSupport.run_load_hooks(:active_record_config, Model)
+
# Load Base at this point, because the active_record load hook is run in that file.
Base
end
diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb
index b8764217d3..99de16cd33 100644
--- a/activerecord/lib/active_record/model_schema.rb
+++ b/activerecord/lib/active_record/model_schema.rb
@@ -1,7 +1,18 @@
-require 'active_support/concern'
-require 'active_support/core_ext/class/attribute_accessors'
module ActiveRecord
+ ActiveSupport.on_load(:active_record_config) do
+ mattr_accessor :primary_key_prefix_type, instance_accessor: false
+
+ mattr_accessor :table_name_prefix, instance_accessor: false
+ self.table_name_prefix = ""
+
+ mattr_accessor :table_name_suffix, instance_accessor: false
+ self.table_name_suffix = ""
+
+ mattr_accessor :pluralize_table_names, instance_accessor: false
+ self.pluralize_table_names = true
+ end
+
module ModelSchema
extend ActiveSupport::Concern
@@ -13,7 +24,7 @@ module ActiveRecord
# the Product class will look for "productid" instead of "id" as the primary column. If the
# latter is specified, the Product class will look for "product_id" instead of "id". Remember
# that this is a global setting for all Active Records.
- config_attribute :primary_key_prefix_type, :global => true
+ config_attribute :primary_key_prefix_type, global: true
##
# :singleton-method:
@@ -26,14 +37,12 @@ module ActiveRecord
# a namespace by defining a singleton method in the parent module called table_name_prefix which
# returns your chosen prefix.
config_attribute :table_name_prefix
- self.table_name_prefix = ""
##
# :singleton-method:
# Works like +table_name_prefix+, but appends instead of prepends (set to "_basecamp" gives "projects_basecamp",
# "people_basecamp"). By default, the suffix is the empty string.
config_attribute :table_name_suffix
- self.table_name_suffix = ""
##
# :singleton-method:
@@ -41,7 +50,6 @@ module ActiveRecord
# If true, the default table name for a Product class will be +products+. If false, it would just be +product+.
# See table_name for the full rules on table/class naming. This is true, by default.
config_attribute :pluralize_table_names
- self.pluralize_table_names = true
end
module ClassMethods
@@ -114,11 +122,18 @@ module ActiveRecord
# You can also just define your own <tt>self.table_name</tt> method; see
# the documentation for ActiveRecord::Base#table_name.
def table_name=(value)
- @original_table_name = @table_name if defined?(@table_name)
- @table_name = value && value.to_s
- @quoted_table_name = nil
- @arel_table = nil
- @relation = Relation.new(self, arel_table)
+ value = value && value.to_s
+
+ if defined?(@table_name)
+ return if value == @table_name
+ reset_column_information if connected?
+ end
+
+ @table_name = value
+ @quoted_table_name = nil
+ @arel_table = nil
+ @sequence_name = nil unless defined?(@explicit_sequence_name) && @explicit_sequence_name
+ @relation = Relation.new(self, arel_table)
end
# Returns a quoted version of the table name, used to construct SQL statements.
@@ -128,16 +143,12 @@ module ActiveRecord
# Computes the table name, (re)sets it internally, and returns it.
def reset_table_name #:nodoc:
- if abstract_class?
- self.table_name = if active_record_super == Base || active_record_super.abstract_class?
- nil
- else
- active_record_super.table_name
- end
+ self.table_name = if abstract_class?
+ active_record_super == Base ? nil : active_record_super.table_name
elsif active_record_super.abstract_class?
- self.table_name = active_record_super.table_name || compute_table_name
+ active_record_super.table_name || compute_table_name
else
- self.table_name = compute_table_name
+ compute_table_name
end
end
@@ -152,8 +163,8 @@ module ActiveRecord
# Sets the value of inheritance_column
def inheritance_column=(value)
- @original_inheritance_column = inheritance_column
- @inheritance_column = value.to_s
+ @inheritance_column = value.to_s
+ @explicit_inheritance_column = true
end
def sequence_name
@@ -165,7 +176,8 @@ module ActiveRecord
end
def reset_sequence_name #:nodoc:
- self.sequence_name = connection.default_sequence_name(table_name, primary_key)
+ @explicit_sequence_name = false
+ @sequence_name = connection.default_sequence_name(table_name, primary_key)
end
# Sets the name of the sequence to use when generating ids to the given
@@ -183,8 +195,8 @@ module ActiveRecord
# self.sequence_name = "projectseq" # default would have been "project_seq"
# end
def sequence_name=(value)
- @original_sequence_name = @sequence_name if defined?(@sequence_name)
@sequence_name = value.to_s
+ @explicit_sequence_name = true
end
# Indicates whether the table associated with this class exists
@@ -213,7 +225,7 @@ module ActiveRecord
def decorate_columns(columns_hash) # :nodoc:
return if columns_hash.empty?
- serialized_attributes.keys.each do |key|
+ serialized_attributes.each_key do |key|
columns_hash[key] = AttributeMethods::Serialization::Type.new(columns_hash[key])
end
@@ -247,13 +259,12 @@ module ActiveRecord
# and true as the value. This makes it possible to do O(1) lookups in respond_to? to check if a given method for attribute
# is available.
def column_methods_hash #:nodoc:
- @dynamic_methods_hash ||= column_names.inject(Hash.new(false)) do |methods, attr|
+ @dynamic_methods_hash ||= column_names.each_with_object(Hash.new(false)) do |attr, methods|
attr_name = attr.to_s
methods[attr.to_sym] = attr_name
methods["#{attr}=".to_sym] = attr_name
methods["#{attr}?".to_sym] = attr_name
methods["#{attr}_before_type_cast".to_sym] = attr_name
- methods
end
end
@@ -296,12 +307,15 @@ module ActiveRecord
@column_types = nil
@content_columns = nil
@dynamic_methods_hash = nil
- @inheritance_column = nil
+ @inheritance_column = nil unless defined?(@explicit_inheritance_column) && @explicit_inheritance_column
@relation = nil
end
- def clear_cache! # :nodoc:
- connection.schema_cache.clear!
+ # This is a hook for use by modules that need to do extra stuff to
+ # attributes when they are initialized. (e.g. attribute
+ # serialization)
+ def initialize_attributes(attributes, options = {}) #:nodoc:
+ attributes
end
private
@@ -309,8 +323,7 @@ module ActiveRecord
# Guesses the table name, but does not decorate it with prefix and suffix information.
def undecorated_table_name(class_name = base_class.name)
table_name = class_name.to_s.demodulize.underscore
- table_name = table_name.pluralize if pluralize_table_names
- table_name
+ pluralize_table_names ? table_name.pluralize : table_name
end
# Computes and returns a table name according to default conventions.
diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb
index 9e21039c4f..2e7fb3bbb3 100644
--- a/activerecord/lib/active_record/nested_attributes.rb
+++ b/activerecord/lib/active_record/nested_attributes.rb
@@ -1,10 +1,13 @@
require 'active_support/core_ext/hash/except'
require 'active_support/core_ext/object/try'
-require 'active_support/core_ext/object/blank'
require 'active_support/core_ext/hash/indifferent_access'
-require 'active_support/core_ext/class/attribute'
module ActiveRecord
+ ActiveSupport.on_load(:active_record_config) do
+ mattr_accessor :nested_attributes_options, instance_accessor: false
+ self.nested_attributes_options = {}
+ end
+
module NestedAttributes #:nodoc:
class TooManyRecords < ActiveRecordError
end
@@ -13,16 +16,15 @@ module ActiveRecord
included do
config_attribute :nested_attributes_options
- self.nested_attributes_options = {}
end
# = Active Record Nested Attributes
#
# Nested attributes allow you to save attributes on associated records
- # through the parent. By default nested attribute updating is turned off,
- # you can enable it using the accepts_nested_attributes_for class method.
- # When you enable nested attributes an attribute writer is defined on
- # the model.
+ # through the parent. By default nested attribute updating is turned off
+ # and you can enable it using the accepts_nested_attributes_for class
+ # method. When you enable nested attributes an attribute writer is
+ # defined on the model.
#
# The attribute writer is named after the association, which means that
# in the following example, two new methods are added to your model:
@@ -192,18 +194,6 @@ module ActiveRecord
# the parent model is saved. This happens inside the transaction initiated
# by the parents save method. See ActiveRecord::AutosaveAssociation.
#
- # === Using with attr_accessible
- #
- # The use of <tt>attr_accessible</tt> can interfere with nested attributes
- # if you're not careful. For example, if the <tt>Member</tt> model above
- # was using <tt>attr_accessible</tt> like this:
- #
- # attr_accessible :name
- #
- # You would need to modify it to look like this:
- #
- # attr_accessible :name, :posts_attributes
- #
# === Validating the presence of a parent model
#
# If you want to validate that a child record is associated with a parent
@@ -222,9 +212,7 @@ module ActiveRecord
module ClassMethods
REJECT_ALL_BLANK_PROC = proc { |attributes| attributes.all? { |key, value| key == '_destroy' || value.blank? } }
- # Defines an attributes writer for the specified association(s). If you
- # are using <tt>attr_protected</tt> or <tt>attr_accessible</tt>, then you
- # will need to add the attribute writer to the allowed list.
+ # Defines an attributes writer for the specified association(s).
#
# Supported options:
# [:allow_destroy]
@@ -243,15 +231,24 @@ module ActiveRecord
# any value for _destroy.
# [:limit]
# Allows you to specify the maximum number of the associated records that
- # can be processed with the nested attributes. If the size of the
+ # can be processed with the nested attributes. Limit also can be specified as a
+ # Proc or a Symbol pointing to a method that should return number. If the size of the
# nested attributes array exceeds the specified limit, NestedAttributes::TooManyRecords
# exception is raised. If omitted, any number associations can be processed.
# Note that the :limit option is only applicable to one-to-many associations.
# [:update_only]
- # Allows you to specify that an existing record may only be updated.
- # A new record may only be created when there is no existing record.
- # This option only works for one-to-one associations and is ignored for
- # collection associations. This option is off by default.
+ # For a one-to-one association, this option allows you to specify how
+ # nested attributes are to be used when an associated record already
+ # exists. In general, an existing record may either be updated with the
+ # new set of attribute values or be replaced by a wholly new record
+ # containing those values. By default the :update_only option is +false+
+ # and the nested attributes are used to update the existing record only
+ # if they include the record's <tt>:id</tt> value. Otherwise a new
+ # record will be instantiated and used to replace the existing one.
+ # However if the :update_only option is +true+, the nested attributes
+ # are used to update the record's attributes always, regardless of
+ # whether the <tt>:id</tt> is present. The option is ignored for collection
+ # associations.
#
# Examples:
# # creates avatar_attributes=
@@ -280,12 +277,12 @@ module ActiveRecord
# def pirate_attributes=(attributes)
# assign_nested_attributes_for_one_to_one_association(:pirate, attributes, mass_assignment_options)
# end
- class_eval <<-eoruby, __FILE__, __LINE__ + 1
+ generated_feature_methods.module_eval <<-eoruby, __FILE__, __LINE__ + 1
if method_defined?(:#{association_name}_attributes=)
remove_method(:#{association_name}_attributes=)
end
def #{association_name}_attributes=(attributes)
- assign_nested_attributes_for_#{type}_association(:#{association_name}, attributes, mass_assignment_options)
+ assign_nested_attributes_for_#{type}_association(:#{association_name}, attributes)
end
eoruby
else
@@ -312,31 +309,34 @@ module ActiveRecord
# Assigns the given attributes to the association.
#
- # If update_only is false and the given attributes include an <tt>:id</tt>
- # that matches the existing record's id, then the existing record will be
- # modified. If update_only is true, a new record is only created when no
- # object exists. Otherwise a new record will be built.
+ # If an associated record does not yet exist, one will be instantiated. If
+ # an associated record already exists, the method's behavior depends on
+ # the value of the update_only option. If update_only is +false+ and the
+ # given attributes include an <tt>:id</tt> that matches the existing record's
+ # id, then the existing record will be modified. If no <tt>:id</tt> is provided
+ # it will be replaced with a new record. If update_only is +true+ the existing
+ # record will be modified regardless of whether an <tt>:id</tt> is provided.
#
# If the given attributes include a matching <tt>:id</tt> attribute, or
# update_only is true, and a <tt>:_destroy</tt> key set to a truthy value,
# then the existing record will be marked for destruction.
- def assign_nested_attributes_for_one_to_one_association(association_name, attributes, assignment_opts = {})
+ def assign_nested_attributes_for_one_to_one_association(association_name, attributes)
options = self.nested_attributes_options[association_name]
attributes = attributes.with_indifferent_access
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], assignment_opts) unless call_reject_if(association_name, attributes)
+ assign_to_or_mark_for_destruction(record, attributes, options[:allow_destroy]) unless call_reject_if(association_name, attributes)
- elsif attributes['id'].present? && !assignment_opts[:without_protection]
+ elsif attributes['id'].present?
raise_nested_attributes_record_not_found(association_name, attributes['id'])
elsif !reject_new_record?(association_name, attributes)
method = "build_#{association_name}"
if respond_to?(method)
- send(method, attributes.except(*unassignable_keys(assignment_opts)), assignment_opts)
+ send(method, attributes.except(*UNASSIGNABLE_KEYS))
else
- raise ArgumentError, "Cannot build association #{association_name}. Are you trying to build a polymorphic one-to-one association?"
+ raise ArgumentError, "Cannot build association `#{association_name}'. Are you trying to build a polymorphic one-to-one association?"
end
end
end
@@ -358,7 +358,7 @@ module ActiveRecord
# })
#
# Will update the name of the Person with ID 1, build a new associated
- # person with the name `John', and mark the associated Person with ID 2
+ # person with the name 'John', and mark the associated Person with ID 2
# for destruction.
#
# Also accepts an Array of attribute hashes:
@@ -368,15 +368,26 @@ module ActiveRecord
# { :name => 'John' },
# { :id => '2', :_destroy => true }
# ])
- def assign_nested_attributes_for_collection_association(association_name, attributes_collection, assignment_opts = {})
+ def assign_nested_attributes_for_collection_association(association_name, attributes_collection)
options = self.nested_attributes_options[association_name]
unless attributes_collection.is_a?(Hash) || attributes_collection.is_a?(Array)
raise ArgumentError, "Hash or Array expected, got #{attributes_collection.class.name} (#{attributes_collection.inspect})"
end
- if options[:limit] && attributes_collection.size > options[:limit]
- raise TooManyRecords, "Maximum #{options[:limit]} records are allowed. Got #{attributes_collection.size} records instead."
+ if limit = options[:limit]
+ limit = case limit
+ when Symbol
+ send(limit)
+ when Proc
+ limit.call
+ else
+ limit
+ end
+
+ if limit && attributes_collection.size > limit
+ raise TooManyRecords, "Maximum #{limit} records are allowed. Got #{attributes_collection.size} records instead."
+ end
end
if attributes_collection.is_a? Hash
@@ -394,7 +405,7 @@ module ActiveRecord
association.target
else
attribute_ids = attributes_collection.map {|a| a['id'] || a[:id] }.compact
- attribute_ids.empty? ? [] : association.scoped.where(association.klass.primary_key => attribute_ids)
+ attribute_ids.empty? ? [] : association.scope.where(association.klass.primary_key => attribute_ids)
end
attributes_collection.each do |attributes|
@@ -402,7 +413,7 @@ module ActiveRecord
if attributes['id'].blank?
unless reject_new_record?(association_name, attributes)
- association.build(attributes.except(*unassignable_keys(assignment_opts)), assignment_opts)
+ association.build(attributes.except(*UNASSIGNABLE_KEYS))
end
elsif existing_record = existing_records.detect { |record| record.id.to_s == attributes['id'].to_s }
unless association.loaded? || call_reject_if(association_name, attributes)
@@ -415,14 +426,11 @@ module ActiveRecord
else
association.add_to_target(existing_record)
end
-
end
if !call_reject_if(association_name, attributes)
- assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy], assignment_opts)
+ assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy])
end
- elsif assignment_opts[:without_protection]
- association.build(attributes.except(*unassignable_keys(assignment_opts)), assignment_opts)
else
raise_nested_attributes_record_not_found(association_name, attributes['id'])
end
@@ -431,8 +439,8 @@ module ActiveRecord
# Updates a record with the +attributes+ or marks it for destruction if
# +allow_destroy+ is +true+ and has_destroy_flag? returns +true+.
- def assign_to_or_mark_for_destruction(record, attributes, allow_destroy, assignment_opts)
- record.assign_attributes(attributes.except(*unassignable_keys(assignment_opts)), assignment_opts)
+ def assign_to_or_mark_for_destruction(record, attributes, allow_destroy)
+ record.assign_attributes(attributes.except(*UNASSIGNABLE_KEYS))
record.mark_for_destruction if has_destroy_flag?(attributes) && allow_destroy
end
@@ -461,9 +469,5 @@ module ActiveRecord
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
-
- def unassignable_keys(assignment_opts)
- assignment_opts[:without_protection] ? UNASSIGNABLE_KEYS - %w[id] : UNASSIGNABLE_KEYS
- end
end
end
diff --git a/activerecord/lib/active_record/null_relation.rb b/activerecord/lib/active_record/null_relation.rb
index 60c37ac2b7..4c1c91e3df 100644
--- a/activerecord/lib/active_record/null_relation.rb
+++ b/activerecord/lib/active_record/null_relation.rb
@@ -1,10 +1,61 @@
# -*- coding: utf-8 -*-
module ActiveRecord
- # = Active Record Null Relation
- class NullRelation < Relation
+ module NullRelation # :nodoc:
def exec_queries
@records = []
end
+
+ def pluck(_column_name)
+ []
+ end
+
+ def delete_all(_conditions = nil)
+ 0
+ end
+
+ def update_all(_updates, _conditions = nil, _options = {})
+ 0
+ end
+
+ def delete(_id_or_array)
+ 0
+ end
+
+ def size
+ 0
+ end
+
+ def empty?
+ true
+ end
+
+ def any?
+ false
+ end
+
+ def many?
+ false
+ end
+
+ def to_sql
+ @to_sql ||= ""
+ end
+
+ def where_values_hash
+ {}
+ end
+
+ def count
+ 0
+ end
+
+ def calculate(_operation, _column_name, _options = {})
+ nil
+ end
+
+ def exists?(_id = false)
+ false
+ end
end
-end \ No newline at end of file
+end
diff --git a/activerecord/lib/active_record/observer.rb b/activerecord/lib/active_record/observer.rb
index fdf17c003c..6b2f6f98a5 100644
--- a/activerecord/lib/active_record/observer.rb
+++ b/activerecord/lib/active_record/observer.rb
@@ -1,4 +1,3 @@
-require 'active_support/core_ext/class/attribute'
module ActiveRecord
# = Active Record Observer
@@ -74,6 +73,12 @@ module ActiveRecord
#
# Observers will not be invoked unless you define these in your application configuration.
#
+ # If you are using Active Record outside Rails, activate the observers explicitly in a configuration or
+ # environment file:
+ #
+ # ActiveRecord::Base.add_observer CommentObserver.instance
+ # ActiveRecord::Base.add_observer SignupObserver.instance
+ #
# == Loading
#
# Observers register themselves in the model class they observe, since it is the class that
diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb
index 9bc046c775..2eaad1d469 100644
--- a/activerecord/lib/active_record/persistence.rb
+++ b/activerecord/lib/active_record/persistence.rb
@@ -1,5 +1,3 @@
-require 'active_support/concern'
-
module ActiveRecord
# = Active Record Persistence
module Persistence
@@ -19,12 +17,6 @@ module ActiveRecord
# # Create a single new object
# User.create(:first_name => 'Jamie')
#
- # # Create a single new object using the :admin mass-assignment security role
- # User.create({ :first_name => 'Jamie', :is_admin => true }, :as => :admin)
- #
- # # Create a single new object bypassing mass-assignment security
- # User.create({ :first_name => 'Jamie', :is_admin => true }, :without_protection => true)
- #
# # Create an Array of new objects
# User.create([{ :first_name => 'Jamie' }, { :first_name => 'Jeremy' }])
#
@@ -37,11 +29,11 @@ module ActiveRecord
# User.create([{ :first_name => 'Jamie' }, { :first_name => 'Jeremy' }]) do |u|
# u.is_admin = false
# end
- def create(attributes = nil, options = {}, &block)
+ def create(attributes = nil, &block)
if attributes.is_a?(Array)
- attributes.collect { |attr| create(attr, options, &block) }
+ attributes.collect { |attr| create(attr, &block) }
else
- object = new(attributes, options, &block)
+ object = new(attributes, &block)
object.save
object
end
@@ -115,36 +107,37 @@ module ActiveRecord
# callbacks, Observer methods, or any <tt>:dependent</tt> association
# options, use <tt>#destroy</tt>.
def delete
- if persisted?
- self.class.delete(id)
- IdentityMap.remove(self) if IdentityMap.enabled?
- end
+ self.class.delete(id) if persisted?
@destroyed = true
freeze
end
# Deletes the record in the database and freezes this instance to reflect
# that no changes should be made (since they can't be persisted).
+ #
+ # There's a series of callbacks associated with <tt>destroy</tt>. If
+ # the <tt>before_destroy</tt> callback return +false+ the action is cancelled
+ # and <tt>destroy</tt> returns +false+. See
+ # ActiveRecord::Callbacks for further details.
def destroy
+ raise ReadOnlyRecord if readonly?
destroy_associations
-
- if persisted?
- IdentityMap.remove(self) if IdentityMap.enabled?
- pk = self.class.primary_key
- column = self.class.columns_hash[pk]
- substitute = connection.substitute_at(column, 0)
-
- relation = self.class.unscoped.where(
- self.class.arel_table[pk].eq(substitute))
-
- relation.bind_values = [[column, id]]
- relation.delete_all
- end
-
+ destroy_row if persisted?
@destroyed = true
freeze
end
+ # Deletes the record in the database and freezes this instance to reflect
+ # that no changes should be made (since they can't be persisted).
+ #
+ # There's a series of callbacks associated with <tt>destroy!</tt>. If
+ # the <tt>before_destroy</tt> callback return +false+ the action is cancelled
+ # and <tt>destroy!</tt> raises ActiveRecord::RecordNotDestroyed. See
+ # ActiveRecord::Callbacks for further details.
+ def destroy!
+ destroy || raise(ActiveRecord::RecordNotDestroyed)
+ end
+
# Returns an instance of the specified +klass+ with the attributes of the
# current record. This is mostly useful in relation to single-table
# inheritance structures where you want a subclass to appear as the
@@ -162,7 +155,7 @@ module ActiveRecord
became.instance_variable_set("@new_record", new_record?)
became.instance_variable_set("@destroyed", destroyed?)
became.instance_variable_set("@errors", errors)
- became.type = klass.name unless self.class.descends_from_active_record?
+ became.public_send("#{klass.inheritance_column}=", klass.name) unless self.class.descends_from_active_record?
became
end
@@ -176,55 +169,68 @@ module ActiveRecord
#
def update_attribute(name, value)
name = name.to_s
- raise ActiveRecordError, "#{name} is marked as readonly" if self.class.readonly_attributes.include?(name)
+ verify_readonly_attribute(name)
send("#{name}=", value)
save(:validate => false)
end
- # Updates a single attribute of an object, without calling save.
- #
- # * Validation is skipped.
- # * Callbacks are skipped.
- # * updated_at/updated_on column is not updated if that column is available.
- #
- # Raises an +ActiveRecordError+ when called on new objects, or when the +name+
- # attribute is marked as readonly.
- def update_column(name, value)
- name = name.to_s
- raise ActiveRecordError, "#{name} is marked as readonly" if self.class.readonly_attributes.include?(name)
- raise ActiveRecordError, "can not update on a new record object" unless persisted?
- raw_write_attribute(name, value)
- self.class.update_all({ name => value }, self.class.primary_key => id) == 1
- end
-
# Updates the attributes of the model from the passed-in hash and saves the
# record, all wrapped in a transaction. If the object is invalid, the saving
# will fail and false will be returned.
- #
- # When updating model attributes, mass-assignment security protection is respected.
- # If no +:as+ option is supplied then the +:default+ role will be used.
- # If you want to bypass the protection given by +attr_protected+ and
- # +attr_accessible+ then you can do so using the +:without_protection+ option.
- def update_attributes(attributes, options = {})
+ def update_attributes(attributes)
# The following transaction covers any possible database side-effects of the
# attributes assignment. For example, setting the IDs of a child collection.
with_transaction_returning_status do
- assign_attributes(attributes, options)
+ assign_attributes(attributes)
save
end
end
# Updates its receiver just like +update_attributes+ but calls <tt>save!</tt> instead
# of +save+, so an exception is raised if the record is invalid.
- def update_attributes!(attributes, options = {})
+ def update_attributes!(attributes)
# The following transaction covers any possible database side-effects of the
# attributes assignment. For example, setting the IDs of a child collection.
with_transaction_returning_status do
- assign_attributes(attributes, options)
+ assign_attributes(attributes)
save!
end
end
+ # Updates a single attribute of an object, without calling save.
+ #
+ # * Validation is skipped.
+ # * Callbacks are skipped.
+ # * updated_at/updated_on column is not updated if that column is available.
+ #
+ # Raises an +ActiveRecordError+ when called on new objects, or when the +name+
+ # attribute is marked as readonly.
+ def update_column(name, value)
+ update_columns(name => value)
+ end
+
+ # Updates the attributes from the passed-in hash, without calling save.
+ #
+ # * Validation is skipped.
+ # * Callbacks are skipped.
+ # * updated_at/updated_on column is not updated if that column is available.
+ #
+ # Raises an +ActiveRecordError+ when called on new objects, or when at least
+ # one of the attributes is marked as readonly.
+ def update_columns(attributes)
+ raise ActiveRecordError, "can not update on a new record object" unless persisted?
+
+ attributes.each_key do |key|
+ verify_readonly_attribute(key.to_s)
+ end
+
+ attributes.each do |k,v|
+ raw_write_attribute(k,v)
+ end
+
+ self.class.where(self.class.primary_key => id).update_all(attributes) == 1
+ end
+
# Initializes +attribute+ to zero if +nil+ and adds the value passed as +by+ (default is 1).
# The increment is performed directly on the underlying attribute, no setter is invoked.
# Only makes sense for number-based attributes. Returns +self+.
@@ -284,11 +290,15 @@ module ActiveRecord
clear_aggregation_cache
clear_association_cache
- IdentityMap.without do
- fresh_object = self.class.unscoped { self.class.find(id, options) }
- @attributes.update(fresh_object.instance_variable_get('@attributes'))
- @columns_hash = fresh_object.instance_variable_get('@columns_hash')
- end
+ fresh_object =
+ if options && options[:lock]
+ self.class.unscoped { self.class.lock.find(id) }
+ else
+ self.class.unscoped { self.class.find(id) }
+ end
+
+ @attributes.update(fresh_object.instance_variable_get('@attributes'))
+ @columns_hash = fresh_object.instance_variable_get('@columns_hash')
@attributes_cache = {}
self
@@ -323,14 +333,15 @@ module ActiveRecord
changes = {}
attributes.each do |column|
- changes[column.to_s] = write_attribute(column.to_s, current_time)
+ column = column.to_s
+ changes[column] = write_attribute(column, current_time)
end
changes[self.class.locking_column] = increment_lock if locking_enabled?
@changed_attributes.except!(*changes.keys)
primary_key = self.class.primary_key
- self.class.unscoped.update_all(changes, { primary_key => self[primary_key] }) == 1
+ self.class.unscoped.where(primary_key => self[primary_key]).update_all(changes) == 1
end
end
@@ -340,6 +351,22 @@ module ActiveRecord
def destroy_associations
end
+ def destroy_row
+ relation_for_destroy.delete_all
+ end
+
+ def relation_for_destroy
+ pk = self.class.primary_key
+ column = self.class.columns_hash[pk]
+ substitute = connection.substitute_at(column, 0)
+
+ relation = self.class.unscoped.where(
+ self.class.arel_table[pk].eq(substitute))
+
+ relation.bind_values = [[column, id]]
+ relation
+ end
+
def create_or_update
raise ReadOnlyRecord if readonly?
result = new_record? ? create : update
@@ -349,7 +376,7 @@ module ActiveRecord
# Updates the associated record with values matching those of the instance attributes.
# Returns the number of affected rows.
def update(attribute_names = @attributes.keys)
- attributes_with_values = arel_attributes_values(false, false, attribute_names)
+ attributes_with_values = arel_attributes_with_values_for_update(attribute_names)
return 0 if attributes_with_values.empty?
klass = self.class
stmt = klass.unscoped.where(klass.arel_table[klass.primary_key].eq(id)).arel.compile_update(attributes_with_values)
@@ -359,15 +386,17 @@ module ActiveRecord
# Creates a record with values matching those of the instance attributes
# and returns its id.
def create
- attributes_values = arel_attributes_values(!id.nil?)
+ attributes_values = arel_attributes_with_values_for_create(!id.nil?)
new_id = self.class.unscoped.insert attributes_values
-
self.id ||= new_id if self.class.primary_key
- IdentityMap.add(self) if IdentityMap.enabled?
@new_record = false
id
end
+
+ def verify_readonly_attribute(name)
+ raise ActiveRecordError, "#{name} is marked as readonly" if self.class.readonly_attributes.include?(name)
+ end
end
end
diff --git a/activerecord/lib/active_record/query_cache.rb b/activerecord/lib/active_record/query_cache.rb
index 9701898415..2bd8ecda20 100644
--- a/activerecord/lib/active_record/query_cache.rb
+++ b/activerecord/lib/active_record/query_cache.rb
@@ -1,4 +1,3 @@
-require 'active_support/core_ext/object/blank'
module ActiveRecord
# = Active Record Query Cache
@@ -34,16 +33,22 @@ module ActiveRecord
response = @app.call(env)
response[2] = Rack::BodyProxy.new(response[2]) do
- ActiveRecord::Base.connection_id = connection_id
- ActiveRecord::Base.connection.clear_query_cache
- ActiveRecord::Base.connection.disable_query_cache! unless enabled
+ restore_query_cache_settings(connection_id, enabled)
end
response
rescue Exception => e
+ restore_query_cache_settings(connection_id, enabled)
+ raise e
+ end
+
+ private
+
+ def restore_query_cache_settings(connection_id, enabled)
+ ActiveRecord::Base.connection_id = connection_id
ActiveRecord::Base.connection.clear_query_cache
ActiveRecord::Base.connection.disable_query_cache! unless enabled
- raise e
end
+
end
end
diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb
index 0e6fecbc4b..13e09eda53 100644
--- a/activerecord/lib/active_record/querying.rb
+++ b/activerecord/lib/active_record/querying.rb
@@ -1,16 +1,15 @@
-require 'active_support/core_ext/module/delegation'
-require 'active_support/deprecation'
module ActiveRecord
module Querying
- delegate :find, :first, :first!, :last, :last!, :all, :exists?, :any?, :many?, :to => :scoped
- delegate :first_or_create, :first_or_create!, :first_or_initialize, :to => :scoped
- delegate :destroy, :destroy_all, :delete, :delete_all, :update, :update_all, :to => :scoped
- delegate :find_each, :find_in_batches, :to => :scoped
+ delegate :find, :take, :take!, :first, :first!, :last, :last!, :exists?, :any?, :many?, :to => :all
+ delegate :first_or_create, :first_or_create!, :first_or_initialize, :to => :all
+ delegate :find_by, :find_by!, :to => :all
+ delegate :destroy, :destroy_all, :delete, :delete_all, :update, :update_all, :to => :all
+ delegate :find_each, :find_in_batches, :to => :all
delegate :select, :group, :order, :except, :reorder, :limit, :offset, :joins,
:where, :preload, :eager_load, :includes, :from, :lock, :readonly,
- :having, :create_with, :uniq, :references, :none, :to => :scoped
- delegate :count, :average, :minimum, :maximum, :sum, :calculate, :pluck, :to => :scoped
+ :having, :create_with, :uniq, :references, :none, :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
@@ -61,8 +60,10 @@ module ActiveRecord
#
# Product.count_by_sql "SELECT COUNT(*) FROM sales s, customers c WHERE s.customer_id = c.id"
def count_by_sql(sql)
- sql = sanitize_conditions(sql)
- connection.select_value(sql, "#{name} Count").to_i
+ logging_query_plan do
+ sql = sanitize_conditions(sql)
+ connection.select_value(sql, "#{name} Count").to_i
+ end
end
end
end
diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb
index 058dd58efb..a9f80ccd5f 100644
--- a/activerecord/lib/active_record/railtie.rb
+++ b/activerecord/lib/active_record/railtie.rb
@@ -29,7 +29,13 @@ module ActiveRecord
'ActiveRecord::RecordNotSaved' => :unprocessable_entity
)
+
+ config.active_record.use_schema_cache_dump = true
+
+ config.eager_load_namespaces << ActiveRecord
+
rake_tasks do
+ require "active_record/base"
load "active_record/railties/databases.rake"
end
@@ -38,10 +44,15 @@ module ActiveRecord
# first time. Also, make it output to STDERR.
console do |app|
require "active_record/railties/console_sandbox" if app.sandbox?
+ require "active_record/base"
console = ActiveSupport::Logger.new(STDERR)
Rails.logger.extend ActiveSupport::Logger.broadcast console
end
+ runner do |app|
+ require "active_record/base"
+ end
+
initializer "active_record.initialize_timezone" do
ActiveSupport.on_load(:active_record) do
self.time_zone_aware_attributes = true
@@ -53,16 +64,34 @@ module ActiveRecord
ActiveSupport.on_load(:active_record) { self.logger ||= ::Rails.logger }
end
- initializer "active_record.identity_map" do |app|
- config.app_middleware.insert_after "::ActionDispatch::Callbacks",
- "ActiveRecord::IdentityMap::Middleware" if config.active_record.delete(:identity_map)
+ initializer "active_record.migration_error" do |app|
+ if config.active_record.delete(:migration_error) == :page_load
+ config.app_middleware.insert_after "::ActionDispatch::Callbacks",
+ "ActiveRecord::Migration::CheckPending"
+ end
+ end
+
+ initializer "active_record.check_schema_cache_dump" do
+ if config.active_record.delete(:use_schema_cache_dump)
+ config.after_initialize do |app|
+ ActiveSupport.on_load(:active_record) do
+ filename = File.join(app.config.paths["db"].first, "schema_cache.dump")
+
+ if File.file?(filename)
+ cache = Marshal.load File.binread filename
+ if cache.version == ActiveRecord::Migrator.current_version
+ ActiveRecord::Model.connection.schema_cache = cache
+ else
+ warn "schema_cache.dump is expired. Current version is #{ActiveRecord::Migrator.current_version}, but cache version is #{cache.version}."
+ end
+ end
+ end
+ end
+ end
end
initializer "active_record.set_configs" do |app|
ActiveSupport.on_load(:active_record) do
- if app.config.active_record.delete(:whitelist_attributes)
- attr_accessible(nil)
- end
app.config.active_record.each do |k,v|
send "#{k}=", v
end
@@ -73,7 +102,9 @@ module ActiveRecord
# and then establishes the connection.
initializer "active_record.initialize_database" do |app|
ActiveSupport.on_load(:active_record) do
- self.configurations = app.config.database_configuration
+ unless ENV['DATABASE_URL']
+ self.configurations = app.config.database_configuration
+ end
establish_connection
end
end
@@ -87,18 +118,12 @@ module ActiveRecord
end
initializer "active_record.set_reloader_hooks" do |app|
- hook = lambda do
- ActiveRecord::Base.clear_reloadable_connections!
- ActiveRecord::Base.clear_cache!
- end
+ hook = app.config.reload_classes_only_on_change ? :to_prepare : :to_cleanup
- if app.config.reload_classes_only_on_change
- ActiveSupport.on_load(:active_record) do
- ActionDispatch::Reloader.to_prepare(&hook)
- end
- else
- ActiveSupport.on_load(:active_record) do
- ActionDispatch::Reloader.to_cleanup(&hook)
+ ActiveSupport.on_load(:active_record) do
+ ActionDispatch::Reloader.send(hook) do
+ ActiveRecord::Model.clear_reloadable_connections!
+ ActiveRecord::Model.clear_cache!
end
end
end
@@ -107,14 +132,15 @@ module ActiveRecord
config.watchable_files.concat ["#{app.root}/db/schema.rb", "#{app.root}/db/structure.sql"]
end
- config.after_initialize do
+ config.after_initialize do |app|
ActiveSupport.on_load(:active_record) do
- ActiveRecord::Base.instantiate_observers
+ ActiveRecord::Model.instantiate_observers
ActionDispatch::Reloader.to_prepare do
- ActiveRecord::Base.instantiate_observers
+ ActiveRecord::Model.instantiate_observers
end
end
+
end
end
end
diff --git a/activerecord/lib/active_record/railties/console_sandbox.rb b/activerecord/lib/active_record/railties/console_sandbox.rb
index 65a3d68619..90b462fad6 100644
--- a/activerecord/lib/active_record/railties/console_sandbox.rb
+++ b/activerecord/lib/active_record/railties/console_sandbox.rb
@@ -1,6 +1,4 @@
-ActiveRecord::Base.connection.increment_open_transactions
ActiveRecord::Base.connection.begin_db_transaction
at_exit do
ActiveRecord::Base.connection.rollback_db_transaction
- ActiveRecord::Base.connection.decrement_open_transactions
end
diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake
index 14bb0a724e..d134978128 100644
--- a/activerecord/lib/active_record/railties/databases.rake
+++ b/activerecord/lib/active_record/railties/databases.rake
@@ -1,8 +1,7 @@
-require 'active_support/core_ext/object/inclusion'
require 'active_record'
db_namespace = namespace :db do
- task :load_config => :rails_env do
+ task :load_config do
ActiveRecord::Base.configurations = Rails.application.config.database_configuration
ActiveRecord::Migrator.migrations_paths = Rails.application.paths['db/migrate'].to_a
@@ -14,139 +13,36 @@ db_namespace = namespace :db do
end
namespace :create do
- # desc 'Create all the local databases defined in config/database.yml'
task :all => :load_config do
- ActiveRecord::Base.configurations.each_value do |config|
- # Skip entries that don't have a database key, such as the first entry here:
- #
- # defaults: &defaults
- # adapter: mysql
- # username: root
- # password:
- # host: localhost
- #
- # development:
- # database: blog_development
- # *defaults
- next unless config['database']
- # Only connect to local databases
- local_database?(config) { create_database(config) }
- end
+ ActiveRecord::Tasks::DatabaseTasks.create_all
end
end
- desc 'Create the database from config/database.yml for the current Rails.env (use db:create:all to create all dbs in the config)'
- task :create => :load_config do
- configs_for_environment.each { |config| create_database(config) }
- ActiveRecord::Base.establish_connection(configs_for_environment.first)
- end
-
- def mysql_creation_options(config)
- @charset = ENV['CHARSET'] || 'utf8'
- @collation = ENV['COLLATION'] || 'utf8_unicode_ci'
- {:charset => (config['charset'] || @charset), :collation => (config['collation'] || @collation)}
- end
-
- def create_database(config)
- begin
- if config['adapter'] =~ /sqlite/
- if File.exist?(config['database'])
- $stderr.puts "#{config['database']} already exists"
- else
- begin
- # Create the SQLite database
- ActiveRecord::Base.establish_connection(config)
- ActiveRecord::Base.connection
- rescue Exception => e
- $stderr.puts e, *(e.backtrace)
- $stderr.puts "Couldn't create database for #{config.inspect}"
- end
- end
- return # Skip the else clause of begin/rescue
- else
- ActiveRecord::Base.establish_connection(config)
- ActiveRecord::Base.connection
- end
- rescue
- case config['adapter']
- when /mysql/
- if config['adapter'] =~ /jdbc/
- #FIXME After Jdbcmysql gives this class
- require 'active_record/railties/jdbcmysql_error'
- error_class = ArJdbcMySQL::Error
- else
- error_class = config['adapter'] =~ /mysql2/ ? Mysql2::Error : Mysql::Error
- end
- access_denied_error = 1045
- begin
- ActiveRecord::Base.establish_connection(config.merge('database' => nil))
- ActiveRecord::Base.connection.create_database(config['database'], mysql_creation_options(config))
- ActiveRecord::Base.establish_connection(config)
- rescue error_class => sqlerr
- if sqlerr.errno == access_denied_error
- print "#{sqlerr.error}. \nPlease provide the root password for your mysql installation\n>"
- root_password = $stdin.gets.strip
- grant_statement = "GRANT ALL PRIVILEGES ON #{config['database']}.* " \
- "TO '#{config['username']}'@'localhost' " \
- "IDENTIFIED BY '#{config['password']}' WITH GRANT OPTION;"
- ActiveRecord::Base.establish_connection(config.merge(
- 'database' => nil, 'username' => 'root', 'password' => root_password))
- ActiveRecord::Base.connection.create_database(config['database'], mysql_creation_options(config))
- ActiveRecord::Base.connection.execute grant_statement
- ActiveRecord::Base.establish_connection(config)
- else
- $stderr.puts sqlerr.error
- $stderr.puts "Couldn't create database for #{config.inspect}, charset: #{config['charset'] || @charset}, collation: #{config['collation'] || @collation}"
- $stderr.puts "(if you set the charset manually, make sure you have a matching collation)" if config['charset']
- end
- end
- when /postgresql/
- @encoding = config['encoding'] || ENV['CHARSET'] || 'utf8'
- begin
- ActiveRecord::Base.establish_connection(config.merge('database' => 'postgres', 'schema_search_path' => 'public'))
- ActiveRecord::Base.connection.create_database(config['database'], config.merge('encoding' => @encoding))
- ActiveRecord::Base.establish_connection(config)
- rescue Exception => e
- $stderr.puts e, *(e.backtrace)
- $stderr.puts "Couldn't create database for #{config.inspect}"
- end
- end
+ desc 'Create the database from DATABASE_URL or config/database.yml for the current Rails.env (use db:create:all to create all dbs in the config)'
+ task :create => [:load_config] do
+ if ENV['DATABASE_URL']
+ ActiveRecord::Tasks::DatabaseTasks.create_database_url
else
- $stderr.puts "#{config['database']} already exists"
+ ActiveRecord::Tasks::DatabaseTasks.create_current
end
end
namespace :drop do
- # desc 'Drops all the local databases defined in config/database.yml'
task :all => :load_config do
- ActiveRecord::Base.configurations.each_value do |config|
- # Skip entries that don't have a database key
- next unless config['database']
- begin
- # Only connect to local databases
- local_database?(config) { drop_database(config) }
- rescue Exception => e
- $stderr.puts "Couldn't drop #{config['database']} : #{e.inspect}"
- end
- end
+ ActiveRecord::Tasks::DatabaseTasks.drop_all
end
end
- desc 'Drops the database for the current Rails.env (use db:drop:all to drop all databases)'
- task :drop => :load_config do
- configs_for_environment.each { |config| drop_database_and_rescue(config) }
- end
-
- def local_database?(config, &block)
- if config['host'].in?(['127.0.0.1', 'localhost']) || config['host'].blank?
- yield
+ desc 'Drops the database using DATABASE_URL or the current Rails.env (use db:drop:all to drop all databases)'
+ task :drop => [:load_config] do
+ if ENV['DATABASE_URL']
+ ActiveRecord::Tasks::DatabaseTasks.drop_database_url
else
- $stderr.puts "This task only modifies local databases. #{config['database']} is on a remote host."
+ ActiveRecord::Tasks::DatabaseTasks.drop_current
end
end
-
- desc "Migrate the database (options: VERSION=x, VERBOSE=false)."
+ desc "Migrate the database (options: VERSION=x, VERBOSE=false, SCOPE=blog)."
task :migrate => [:environment, :load_config] do
ActiveRecord::Migration.verbose = ENV["VERBOSE"] ? ENV["VERBOSE"] == "true" : true
ActiveRecord::Migrator.migrate(ActiveRecord::Migrator.migrations_paths, ENV["VERSION"] ? ENV["VERSION"].to_i : nil) do |migration|
@@ -200,8 +96,6 @@ db_namespace = namespace :db do
desc 'Display status of migrations'
task :status => [:environment, :load_config] do
- config = ActiveRecord::Base.configurations[Rails.env || 'development']
- ActiveRecord::Base.establish_connection(config)
unless ActiveRecord::Base.connection.table_exists?(ActiveRecord::Migrator.schema_migrations_table_name)
puts 'Schema migrations table does not exist yet.'
next # means "return" for rake task
@@ -222,7 +116,7 @@ db_namespace = namespace :db do
['up', version, '********** NO FILE **********']
end
# output
- puts "\ndatabase: #{config['database']}\n\n"
+ puts "\ndatabase: #{ActiveRecord::Base.connection_config[:database]}\n\n"
puts "#{'Status'.center(8)} #{'Migration ID'.ljust(14)} Migration Name"
puts "-" * 50
(db_list + file_list).sort_by {|migration| migration[1]}.each do |migration|
@@ -247,48 +141,32 @@ db_namespace = namespace :db do
end
# desc 'Drops and recreates the database from db/schema.rb for the current environment and loads the seeds.'
- task :reset => :environment do
+ task :reset => [:environment, :load_config] do
db_namespace["drop"].invoke
db_namespace["setup"].invoke
end
# desc "Retrieves the charset for the current environment's database"
- task :charset => :environment do
- config = ActiveRecord::Base.configurations[Rails.env || 'development']
- case config['adapter']
- when /mysql/
- ActiveRecord::Base.establish_connection(config)
- puts ActiveRecord::Base.connection.charset
- when /postgresql/
- ActiveRecord::Base.establish_connection(config)
- puts ActiveRecord::Base.connection.encoding
- when /sqlite/
- ActiveRecord::Base.establish_connection(config)
- puts ActiveRecord::Base.connection.encoding
- else
- $stderr.puts 'sorry, your database adapter is not supported yet, feel free to submit a patch'
- end
+ task :charset => [:environment, :load_config] do
+ puts ActiveRecord::Tasks::DatabaseTasks.charset_current
end
# desc "Retrieves the collation for the current environment's database"
- task :collation => :environment do
- config = ActiveRecord::Base.configurations[Rails.env || 'development']
- case config['adapter']
- when /mysql/
- ActiveRecord::Base.establish_connection(config)
- puts ActiveRecord::Base.connection.collation
- else
- $stderr.puts 'sorry, your database adapter is not supported yet, feel free to submit a patch'
+ task :collation => [:environment, :load_config] 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'
end
end
desc 'Retrieves the current schema version number'
- task :version => :environment do
+ task :version => [:environment, :load_config] do
puts "Current version: #{ActiveRecord::Migrator.current_version}"
end
# desc "Raises an error if there are pending migrations"
- task :abort_if_pending_migrations => :environment do
+ task :abort_if_pending_migrations => [:environment, :load_config] do
pending_migrations = ActiveRecord::Migrator.new(:up, ActiveRecord::Migrator.migrations_paths).pending_migrations
if pending_migrations.any?
@@ -311,20 +189,19 @@ db_namespace = namespace :db do
namespace :fixtures do
desc "Load fixtures into the current environment's database. Load specific fixtures using FIXTURES=x,y. Load from subdirectory in test/fixtures using FIXTURES_DIR=z. Specify an alternative path (eg. spec/fixtures) using FIXTURES_PATH=spec/fixtures."
- task :load => :environment do
+ task :load => [:environment, :load_config] do
require 'active_record/fixtures'
- ActiveRecord::Base.establish_connection(Rails.env)
base_dir = File.join [Rails.root, ENV['FIXTURES_PATH'] || %w{test fixtures}].flatten
fixtures_dir = File.join [base_dir, ENV['FIXTURES_DIR']].compact
- (ENV['FIXTURES'] ? ENV['FIXTURES'].split(/,/) : Dir["#{fixtures_dir}/**/*.{yml,csv}"].map {|f| f[(fixtures_dir.size + 1)..-5] }).each do |fixture_file|
+ (ENV['FIXTURES'] ? ENV['FIXTURES'].split(/,/) : Dir["#{fixtures_dir}/**/*.yml"].map {|f| f[(fixtures_dir.size + 1)..-5] }).each do |fixture_file|
ActiveRecord::Fixtures.create_fixtures(fixtures_dir, fixture_file)
end
end
# desc "Search for a fixture given a LABEL or ID. Specify an alternative path (eg. spec/fixtures) using FIXTURES_PATH=spec/fixtures."
- task :identify => :environment do
+ task :identify => [:environment, :load_config] do
require 'active_record/fixtures'
label, id = ENV['LABEL'], ENV['ID']
@@ -353,14 +230,13 @@ db_namespace = namespace :db do
require 'active_record/schema_dumper'
filename = ENV['SCHEMA'] || "#{Rails.root}/db/schema.rb"
File.open(filename, "w:utf-8") do |file|
- ActiveRecord::Base.establish_connection(Rails.env)
ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, file)
end
db_namespace['schema:dump'].reenable
end
desc 'Load a schema.rb file into the database'
- task :load => :environment do
+ task :load => [:environment, :load_config] do
file = ENV['SCHEMA'] || "#{Rails.root}/db/schema.rb"
if File.exists?(file)
load(file)
@@ -369,82 +245,90 @@ db_namespace = namespace :db do
end
end
- task :load_if_ruby => 'db:create' do
+ task :load_if_ruby => ['db:create', :environment] do
db_namespace["schema:load"].invoke if ActiveRecord::Base.schema_format == :ruby
end
+
+ namespace :cache 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")
+
+ con.schema_cache.clear!
+ con.tables.each { |table| con.schema_cache.add(table) }
+ open(filename, 'wb') { |f| f.write(Marshal.dump(con.schema_cache)) }
+ end
+
+ desc 'Clear a db/schema_cache.dump file.'
+ task :clear => [:environment, :load_config] do
+ filename = File.join(Rails.application.config.paths["db"].first, "schema_cache.dump")
+ FileUtils.rm(filename) if File.exists?(filename)
+ end
+ end
+
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 do
- abcs = ActiveRecord::Base.configurations
+ task :dump => [:environment, :load_config] do
filename = ENV['DB_STRUCTURE'] || File.join(Rails.root, "db", "structure.sql")
- case abcs[Rails.env]['adapter']
- when /mysql/, 'oci', 'oracle'
- ActiveRecord::Base.establish_connection(abcs[Rails.env])
+ current_config = ActiveRecord::Tasks::DatabaseTasks.current_config
+ case current_config['adapter']
+ when /mysql/, /postgresql/, /sqlite/
+ ActiveRecord::Tasks::DatabaseTasks.structure_dump(current_config, filename)
+ when 'oci', 'oracle'
+ ActiveRecord::Base.establish_connection(current_config)
File.open(filename, "w:utf-8") { |f| f << ActiveRecord::Base.connection.structure_dump }
- when /postgresql/
- set_psql_env(abcs[Rails.env])
- search_path = abcs[Rails.env]['schema_search_path']
- unless search_path.blank?
- search_path = search_path.split(",").map{|search_path_part| "--schema=#{search_path_part.strip}" }.join(" ")
- end
- `pg_dump -i -s -x -O -f #{filename} #{search_path} #{abcs[Rails.env]['database']}`
- raise 'Error dumping database' if $?.exitstatus == 1
- when /sqlite/
- dbfile = abcs[Rails.env]['database']
- `sqlite3 #{dbfile} .schema > #{filename}`
when 'sqlserver'
- `smoscript -s #{abcs[Rails.env]['host']} -d #{abcs[Rails.env]['database']} -u #{abcs[Rails.env]['username']} -p #{abcs[Rails.env]['password']} -f #{filename} -A -U`
+ `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(abcs[Rails.env])
- db_string = firebird_db_string(abcs[Rails.env])
+ set_firebird_env(current_config)
+ db_string = firebird_db_string(current_config)
sh "isql -a #{db_string} > #{filename}"
else
- raise "Task not supported by '#{abcs[Rails.env]["adapter"]}'"
+ raise "Task not supported by '#{current_config["adapter"]}'"
end
if ActiveRecord::Base.connection.supports_migrations?
File.open(filename, "a") { |f| f << ActiveRecord::Base.connection.dump_schema_information }
end
+ db_namespace['structure:dump'].reenable
end
# desc "Recreate the databases from the structure.sql file"
task :load => [:environment, :load_config] do
- env = ENV['RAILS_ENV'] || 'test'
-
- abcs = ActiveRecord::Base.configurations
+ current_config = ActiveRecord::Tasks::DatabaseTasks.current_config(:env => (ENV['RAILS_ENV'] || 'test'))
filename = ENV['DB_STRUCTURE'] || File.join(Rails.root, "db", "structure.sql")
- case abcs[env]['adapter']
- when /mysql/
- ActiveRecord::Base.establish_connection(abcs[env])
- ActiveRecord::Base.connection.execute('SET foreign_key_checks = 0')
- IO.read(filename).split("\n\n").each do |table|
- ActiveRecord::Base.connection.execute(table)
- end
- when /postgresql/
- set_psql_env(abcs[env])
- `psql -f "#{filename}" #{abcs[env]['database']}`
- when /sqlite/
- dbfile = abcs[env]['database']
- `sqlite3 #{dbfile} < "#{filename}"`
+ case current_config['adapter']
+ when /mysql/, /postgresql/, /sqlite/
+ ActiveRecord::Tasks::DatabaseTasks.structure_load(current_config, filename)
when 'sqlserver'
- `sqlcmd -S #{abcs[env]['host']} -d #{abcs[env]['database']} -U #{abcs[env]['username']} -P #{abcs[env]['password']} -i #{filename}`
+ `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(abcs[env])
+ 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(abcs[env])
- db_string = firebird_db_string(abcs[env])
+ set_firebird_env(current_config)
+ db_string = firebird_db_string(current_config)
sh "isql -i #{filename} #{db_string}"
else
- raise "Task not supported by '#{abcs[env]['adapter']}'"
+ raise "Task not supported by '#{current_config['adapter']}'"
end
end
- task :load_if_sql => 'db:create' do
+ task :load_if_sql => ['db:create', :environment] do
db_namespace["structure:load"].invoke if ActiveRecord::Base.schema_format == :sql
end
end
@@ -458,46 +342,48 @@ db_namespace = namespace :db do
db_namespace["test:load_schema"].invoke
when :sql
db_namespace["test:load_structure"].invoke
- end
+ end
+ end
+
+ # desc "Recreate the test database from an existent schema.rb file"
+ task :load_schema => 'db:test:purge' do
+ ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations['test'])
+ ActiveRecord::Schema.verbose = false
+ db_namespace["schema:load"].invoke
end
# desc "Recreate the test database from an existent structure.sql file"
task :load_structure => 'db:test:purge' do
begin
- old_env, ENV['RAILS_ENV'] = ENV['RAILS_ENV'], 'test'
+ ActiveRecord::Tasks::DatabaseTasks.current_config(:config => ActiveRecord::Base.configurations['test'])
db_namespace["structure:load"].invoke
ensure
- ENV['RAILS_ENV'] = old_env
+ ActiveRecord::Tasks::DatabaseTasks.current_config(:config => nil)
end
end
- # desc "Recreate the test database from an existent schema.rb file"
- task :load_schema => 'db:test:purge' do
- ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations['test'])
- ActiveRecord::Schema.verbose = false
- db_namespace["schema:load"].invoke
+ # desc "Recreate the test database from a fresh schema"
+ task :clone do
+ case ActiveRecord::Base.schema_format
+ when :ruby
+ db_namespace["test:clone_schema"].invoke
+ when :sql
+ db_namespace["test:clone_structure"].invoke
+ end
end
# desc "Recreate the test database from a fresh schema.rb file"
- task :clone => %w(db:schema:dump db:test:load_schema)
+ task :clone_schema => ["db:schema:dump", "db:test:load_schema"]
# desc "Recreate the test database from a fresh structure.sql file"
task :clone_structure => [ "db:structure:dump", "db:test:load_structure" ]
# desc "Empty the test database"
- task :purge => :environment do
+ task :purge => [:environment, :load_config] do
abcs = ActiveRecord::Base.configurations
case abcs['test']['adapter']
- when /mysql/
- ActiveRecord::Base.establish_connection(:test)
- ActiveRecord::Base.connection.recreate_database(abcs['test']['database'], mysql_creation_options(abcs['test']))
- when /postgresql/
- ActiveRecord::Base.clear_active_connections!
- drop_database(abcs['test'])
- create_database(abcs['test'])
- when /sqlite/
- dbfile = abcs['test']['database']
- File.delete(dbfile) if File.exist?(dbfile)
+ when /mysql/, /postgresql/, /sqlite/
+ ActiveRecord::Tasks::DatabaseTasks.purge abcs['test']
when 'sqlserver'
test = abcs.deep_dup['test']
test_database = test['database']
@@ -520,25 +406,10 @@ db_namespace = namespace :db do
# desc 'Check for pending migrations and load the test schema'
task :prepare => 'db:abort_if_pending_migrations' do
unless ActiveRecord::Base.configurations.blank?
- db_namespace[{ :sql => 'test:clone_structure', :ruby => 'test:load' }[ActiveRecord::Base.schema_format]].invoke
+ db_namespace['test:load'].invoke
end
end
end
-
- namespace :sessions do
- # desc "Creates a sessions migration for use with ActiveRecord::SessionStore"
- task :create => :environment do
- raise 'Task unavailable to this database (no migration support)' unless ActiveRecord::Base.connection.supports_migrations?
- Rails.application.load_generators
- require 'rails/generators/rails/session_migration/session_migration_generator'
- Rails::Generators::SessionMigrationGenerator.start [ ENV['MIGRATION'] || 'add_sessions_table' ]
- end
-
- # desc "Clear the sessions table"
- task :clear => :environment do
- ActiveRecord::Base.connection.execute "DELETE FROM #{session_table_name}"
- end
- end
end
namespace :railties do
@@ -546,8 +417,8 @@ namespace :railties do
# desc "Copies missing migrations from Railties (e.g. engines). You can specify Railties to use with FROM=railtie1,railtie2"
task :migrations => :'db:load_config' do
to_load = ENV['FROM'].blank? ? :all : ENV['FROM'].split(",").map {|n| n.strip }
- railties = ActiveSupport::OrderedHash.new
- Rails.application.railties.all do |railtie|
+ railties = {}
+ Rails.application.railties.each do |railtie|
next unless to_load == :all || to_load.include?(railtie.railtie_name)
if railtie.respond_to?(:paths) && (path = railtie.paths['db/migrate'].first)
@@ -563,7 +434,7 @@ namespace :railties do
puts "Copied migration #{migration.basename} from #{name}"
end
- ActiveRecord::Migration.copy( ActiveRecord::Migrator.migrations_paths.first, railties,
+ ActiveRecord::Migration.copy(ActiveRecord::Migrator.migrations_paths.first, railties,
:on_skip => on_skip, :on_copy => on_copy)
end
end
@@ -571,53 +442,3 @@ end
task 'test:prepare' => 'db:test:prepare'
-def drop_database(config)
- case config['adapter']
- when /mysql/
- ActiveRecord::Base.establish_connection(config)
- ActiveRecord::Base.connection.drop_database config['database']
- when /sqlite/
- require 'pathname'
- path = Pathname.new(config['database'])
- file = path.absolute? ? path.to_s : File.join(Rails.root, path)
-
- FileUtils.rm(file)
- when /postgresql/
- ActiveRecord::Base.establish_connection(config.merge('database' => 'postgres', 'schema_search_path' => 'public'))
- ActiveRecord::Base.connection.drop_database config['database']
- end
-end
-
-def drop_database_and_rescue(config)
- begin
- drop_database(config)
- rescue Exception => e
- $stderr.puts "Couldn't drop #{config['database']} : #{e.inspect}"
- end
-end
-
-def configs_for_environment
- environments = [Rails.env]
- environments << 'test' if Rails.env.development?
- ActiveRecord::Base.configurations.values_at(*environments).compact.reject { |config| config['database'].blank? }
-end
-
-def session_table_name
- ActiveRecord::SessionStore::Session.table_name
-end
-
-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 set_psql_env(config)
- ENV['PGHOST'] = config['host'] if config['host']
- ENV['PGPORT'] = config['port'].to_s if config['port']
- ENV['PGPASSWORD'] = config['password'].to_s if config['password']
- ENV['PGUSER'] = config['username'].to_s if config['username']
-end
diff --git a/activerecord/lib/active_record/railties/jdbcmysql_error.rb b/activerecord/lib/active_record/railties/jdbcmysql_error.rb
index 0b75983484..6a38211bff 100644
--- a/activerecord/lib/active_record/railties/jdbcmysql_error.rb
+++ b/activerecord/lib/active_record/railties/jdbcmysql_error.rb
@@ -1,6 +1,6 @@
#FIXME Remove if ArJdbcMysql will give.
module ArJdbcMySQL #:nodoc:
- class Error < StandardError
+ class Error < StandardError #:nodoc:
attr_accessor :error_number, :sql_state
def initialize msg
diff --git a/activerecord/lib/active_record/readonly_attributes.rb b/activerecord/lib/active_record/readonly_attributes.rb
index 836b15e2ce..b3c20c4aff 100644
--- a/activerecord/lib/active_record/readonly_attributes.rb
+++ b/activerecord/lib/active_record/readonly_attributes.rb
@@ -1,12 +1,10 @@
-require 'active_support/concern'
-require 'active_support/core_ext/class/attribute'
module ActiveRecord
module ReadonlyAttributes
extend ActiveSupport::Concern
included do
- config_attribute :_attr_readonly
+ class_attribute :_attr_readonly, instance_accessor: false
self._attr_readonly = []
end
@@ -22,5 +20,10 @@ module ActiveRecord
self._attr_readonly
end
end
+
+ def _attr_readonly
+ ActiveSupport::Deprecation.warn("Instance level _attr_readonly method is deprecated, please use class level method.")
+ 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 8f8a3ec3bb..f322b96f79 100644
--- a/activerecord/lib/active_record/reflection.rb
+++ b/activerecord/lib/active_record/reflection.rb
@@ -1,5 +1,3 @@
-require 'active_support/core_ext/class/attribute'
-require 'active_support/core_ext/object/inclusion'
module ActiveRecord
# = Active Record Reflection
@@ -7,8 +5,7 @@ module ActiveRecord
extend ActiveSupport::Concern
included do
- extend ActiveModel::Configuration
- config_attribute :reflections
+ class_attribute :reflections
self.reflections = {}
end
@@ -21,13 +18,13 @@ module ActiveRecord
# MacroReflection class has info for AggregateReflection and AssociationReflection
# classes.
module ClassMethods
- def create_reflection(macro, name, options, active_record)
+ def create_reflection(macro, name, scope, options, active_record)
case macro
- when :has_many, :belongs_to, :has_one, :has_and_belongs_to_many
- klass = options[:through] ? ThroughReflection : AssociationReflection
- reflection = klass.new(macro, name, options, active_record)
- when :composed_of
- reflection = AggregateReflection.new(macro, name, options, active_record)
+ when :has_many, :belongs_to, :has_one, :has_and_belongs_to_many
+ klass = options[:through] ? ThroughReflection : AssociationReflection
+ reflection = klass.new(macro, name, scope, options, active_record)
+ when :composed_of
+ reflection = AggregateReflection.new(macro, name, scope, options, active_record)
end
self.reflections = self.reflections.merge(name => reflection)
@@ -44,7 +41,8 @@ module ActiveRecord
# Account.reflect_on_aggregation(:balance) # => the balance AggregateReflection
#
def reflect_on_aggregation(aggregation)
- reflections[aggregation].is_a?(AggregateReflection) ? reflections[aggregation] : nil
+ reflection = reflections[aggregation]
+ reflection if reflection.is_a?(AggregateReflection)
end
# Returns an array of AssociationReflection objects for all the
@@ -68,7 +66,8 @@ module ActiveRecord
# Invoice.reflect_on_association(:line_items).macro # returns :has_many
#
def reflect_on_association(association)
- reflections[association].is_a?(AssociationReflection) ? reflections[association] : nil
+ reflection = reflections[association]
+ reflection if reflection.is_a?(AssociationReflection)
end
# Returns an array of AssociationReflection objects for all associations which have <tt>:autosave</tt> enabled.
@@ -77,7 +76,6 @@ module ActiveRecord
end
end
-
# Abstract base class for AggregateReflection and AssociationReflection. Objects of
# AggregateReflection and AssociationReflection are returned by the Reflection::ClassMethods.
class MacroReflection
@@ -93,6 +91,8 @@ module ActiveRecord
# <tt>has_many :clients</tt> returns <tt>:has_many</tt>
attr_reader :macro
+ attr_reader :scope
+
# Returns the hash of options used for the macro.
#
# <tt>composed_of :balance, :class_name => 'Money'</tt> returns <tt>{ :class_name => "Money" }</tt>
@@ -103,9 +103,10 @@ module ActiveRecord
attr_reader :plural_name # :nodoc:
- def initialize(macro, name, options, active_record)
+ def initialize(macro, name, scope, options, active_record)
@macro = macro
@name = name
+ @scope = scope
@options = options
@active_record = active_record
@plural_name = active_record.pluralize_table_names ?
@@ -138,10 +139,6 @@ module ActiveRecord
active_record == other_aggregation.active_record
end
- def sanitized_conditions #:nodoc:
- @sanitized_conditions ||= klass.send(:sanitize_sql, options[:conditions]) if options[:conditions]
- end
-
private
def derive_class_name
name.to_s.camelize
@@ -152,6 +149,10 @@ module ActiveRecord
# Holds all the meta-data about an aggregation as it was specified in the
# Active Record class.
class AggregateReflection < MacroReflection #:nodoc:
+ def mapping
+ mapping = options[:mapping] || [name, name]
+ mapping.first.is_a?(Array) ? mapping : [mapping]
+ end
end
# Holds all the meta-data about an association as it was specified in the
@@ -173,15 +174,15 @@ module ActiveRecord
@klass ||= active_record.send(:compute_type, class_name)
end
- def initialize(macro, name, options, active_record)
+ def initialize(*args)
super
@collection = [:has_many, :has_and_belongs_to_many].include?(macro)
end
# Returns a new, unsaved instance of the associated class. +options+ will
# be passed to the class's constructor.
- def build_association(*options, &block)
- klass.new(*options, &block)
+ def build_association(attributes, &block)
+ klass.new(attributes, &block)
end
def table_name
@@ -192,6 +193,10 @@ module ActiveRecord
@quoted_table_name ||= klass.quoted_table_name
end
+ def join_table
+ @join_table ||= options[:join_table] || derive_join_table
+ end
+
def foreign_key
@foreign_key ||= options[:foreign_key] || derive_foreign_key
end
@@ -239,6 +244,10 @@ module ActiveRecord
def check_validity!
check_validity_of_inverse!
+
+ if has_and_belongs_to_many? && association_foreign_key == foreign_key
+ raise HasAndBelongsToManyAssociationForeignKeyNeeded.new(self)
+ end
end
def check_validity_of_inverse!
@@ -267,11 +276,10 @@ module ActiveRecord
false
end
- # An array of arrays of conditions. Each item in the outside array corresponds to a reflection
- # in the #chain. The inside arrays are simply conditions (and each condition may itself be
- # a hash, array, arel predicate, etc...)
- def conditions
- [[options[:conditions]].compact]
+ # An array of arrays of scopes. Each item in the outside array corresponds to a reflection
+ # in the #chain.
+ def scope_chain
+ scope ? [[scope]] : [[]]
end
alias :source_macro :macro
@@ -321,6 +329,10 @@ module ActiveRecord
macro == :belongs_to
end
+ def has_and_belongs_to_many?
+ macro == :has_and_belongs_to_many
+ end
+
def association_class
case macro
when :belongs_to
@@ -346,6 +358,10 @@ module ActiveRecord
end
end
+ def polymorphic?
+ options.key? :polymorphic
+ end
+
private
def derive_class_name
class_name = name.to_s.camelize
@@ -363,6 +379,10 @@ module ActiveRecord
end
end
+ def derive_join_table
+ [active_record.table_name, klass.table_name].sort.join("\0").gsub(/^(.*_)(.+)\0\1(.+)/, '\1\2_\3').gsub("\0", "_")
+ end
+
def primary_key(klass)
klass.primary_key || raise(UnknownPrimaryKey.new(klass))
end
@@ -431,29 +451,25 @@ module ActiveRecord
# has_many :tags
# end
#
- # There may be conditions on Person.comment_tags, Article.comment_tags and/or Comment.tags,
+ # There may be scopes on Person.comment_tags, Article.comment_tags and/or Comment.tags,
# but only Comment.tags will be represented in the #chain. So this method creates an array
- # of conditions corresponding to the chain. Each item in the #conditions array corresponds
- # to an item in the #chain, and is itself an array of conditions from an arbitrary number
- # of relevant reflections, plus any :source_type or polymorphic :as constraints.
- def conditions
- @conditions ||= begin
- conditions = source_reflection.conditions.map { |c| c.dup }
+ # of scopes corresponding to the chain.
+ def scope_chain
+ @scope_chain ||= begin
+ scope_chain = source_reflection.scope_chain.map(&:dup)
- # Add to it the conditions from this reflection if necessary.
- conditions.first << options[:conditions] if options[:conditions]
+ # Add to it the scope from this reflection (if any)
+ scope_chain.first << scope if scope
- through_conditions = through_reflection.conditions
+ through_scope_chain = through_reflection.scope_chain
if options[:source_type]
- through_conditions.first << { foreign_type => options[:source_type] }
+ through_scope_chain.first <<
+ through_reflection.klass.where(foreign_type => options[:source_type])
end
# Recursively fill out the rest of the array from the through reflection
- conditions += through_conditions
-
- # And return
- conditions
+ scope_chain + through_scope_chain
end
end
diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb
index 3d1ca4f99b..ed80422336 100644
--- a/activerecord/lib/active_record/relation.rb
+++ b/activerecord/lib/active_record/relation.rb
@@ -1,33 +1,34 @@
# -*- coding: utf-8 -*-
-require 'active_support/core_ext/object/blank'
-require 'active_support/deprecation'
module ActiveRecord
# = Active Record Relation
class Relation
JoinOperation = Struct.new(:relation, :join_class, :on)
- ASSOCIATION_METHODS = [:includes, :eager_load, :preload]
- MULTI_VALUE_METHODS = [:select, :group, :order, :joins, :where, :having, :bind, :references]
- SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :from, :reordering, :reverse_order, :uniq]
+
+ MULTI_VALUE_METHODS = [:includes, :eager_load, :preload, :select, :group,
+ :order, :joins, :where, :having, :bind, :references,
+ :extending]
+
+ SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :from, :reordering,
+ :reverse_order, :uniq, :create_with]
+
+ VALUE_METHODS = MULTI_VALUE_METHODS + SINGLE_VALUE_METHODS
include FinderMethods, Calculations, SpawnMethods, QueryMethods, Batches, Explain, Delegation
attr_reader :table, :klass, :loaded
- attr_accessor :extensions, :default_scoped
+ attr_accessor :default_scoped
+ alias :model :klass
alias :loaded? :loaded
alias :default_scoped? :default_scoped
- def initialize(klass, table)
- @klass, @table = klass, table
-
+ def initialize(klass, table, values = {})
+ @klass = klass
+ @table = table
+ @values = values
@implicit_readonly = nil
@loaded = false
@default_scoped = false
-
- SINGLE_VALUE_METHODS.each {|v| instance_variable_set(:"@#{v}_value", nil)}
- (ASSOCIATION_METHODS + MULTI_VALUE_METHODS).each {|v| instance_variable_set(:"@#{v}_values", [])}
- @extensions = []
- @create_with_value = {}
end
def insert(values)
@@ -73,28 +74,62 @@ module ActiveRecord
binds)
end
+ # Initializes new record from relation while maintaining the current
+ # scope.
+ #
+ # Expects arguments in the same format as +Base.new+.
+ #
+ # users = User.where(name: 'DHH')
+ # user = users.new # => #<User id: nil, name: "DHH", created_at: nil, updated_at: nil>
+ #
+ # You can also pass a block to new with the new record as argument:
+ #
+ # user = users.new { |user| user.name = 'Oscar' }
+ # user.name # => Oscar
def new(*args, &block)
scoping { @klass.new(*args, &block) }
end
def initialize_copy(other)
- @bind_values = @bind_values.dup
+ @values = @values.dup
+ @values[:bind] = @values[:bind].dup if @values[:bind]
reset
end
alias build new
+ # Tries to create a new record with the same scoped attributes
+ # defined in the relation. Returns the initialized object if validation fails.
+ #
+ # Expects arguments in the same format as +Base.create+.
+ #
+ # ==== Examples
+ # users = User.where(name: 'Oscar')
+ # users.create # #<User id: 3, name: "oscar", ...>
+ #
+ # users.create(name: 'fxn')
+ # users.create # #<User id: 4, name: "fxn", ...>
+ #
+ # users.create { |user| user.name = 'tenderlove' }
+ # # #<User id: 5, name: "tenderlove", ...>
+ #
+ # users.create(name: nil) # validation on name
+ # # #<User id: nil, name: nil, ...>
def create(*args, &block)
scoping { @klass.create(*args, &block) }
end
+ # Similar to #create, but calls +create!+ on the base class. Raises
+ # an exception if a validation error occurs.
+ #
+ # Expects arguments in the same format as <tt>Base.create!</tt>.
def create!(*args, &block)
scoping { @klass.create!(*args, &block) }
end
# Tries to load the first record; if it fails, then <tt>create</tt> is called with the same arguments as this method.
#
- # Expects arguments in the same format as <tt>Base.create</tt>.
+ # Expects arguments in the same format as +Base.create+.
#
# ==== Examples
# # Find the first user named Penélope or create a new one.
@@ -116,22 +151,22 @@ module ActiveRecord
# user.last_name = "O'Hara"
# end
# # => <User id: 2, first_name: 'Scarlett', last_name: 'Johansson'>
- def first_or_create(attributes = nil, options = {}, &block)
- first || create(attributes, options, &block)
+ def first_or_create(attributes = nil, &block)
+ first || create(attributes, &block)
end
# Like <tt>first_or_create</tt> but calls <tt>create!</tt> so an exception is raised if the created record is invalid.
#
# Expects arguments in the same format as <tt>Base.create!</tt>.
- def first_or_create!(attributes = nil, options = {}, &block)
- first || create!(attributes, options, &block)
+ def first_or_create!(attributes = nil, &block)
+ first || create!(attributes, &block)
end
# Like <tt>first_or_create</tt> but calls <tt>new</tt> instead of <tt>create</tt>.
#
# Expects arguments in the same format as <tt>Base.new</tt>.
- def first_or_initialize(attributes = nil, options = {}, &block)
- first || new(attributes, options, &block)
+ def first_or_initialize(attributes = nil, &block)
+ first || new(attributes, &block)
end
# Runs EXPLAIN on the query or queries triggered by this relation and
@@ -142,58 +177,17 @@ module ActiveRecord
# are needed by the next ones when eager loading is going on.
#
# Please see further details in the
- # {Active Record Query Interface guide}[http://edgeguides.rubyonrails.org/active_record_querying.html#running-explain].
+ # {Active Record Query Interface guide}[http://guides.rubyonrails.org/active_record_querying.html#running-explain].
def explain
_, queries = collecting_queries_for_explain { exec_queries }
exec_explain(queries)
end
+ # Converts relation objects to Array.
def to_a
- # We monitor here the entire execution rather than individual SELECTs
- # because from the point of view of the user fetching the records of a
- # relation is a single unit of work. You want to know if this call takes
- # too long, not if the individual queries take too long.
- #
- # It could be the case that none of the queries involved surpass the
- # threshold, and at the same time the sum of them all does. The user
- # should get a query plan logged in that case.
- logging_query_plan do
- exec_queries
- end
- end
-
- def exec_queries
- return @records if loaded?
-
- default_scoped = with_default_scope
-
- if default_scoped.equal?(self)
- @records = if @readonly_value.nil? && !@klass.locking_enabled?
- eager_loading? ? find_with_associations : @klass.find_by_sql(arel, @bind_values)
- else
- IdentityMap.without do
- eager_loading? ? find_with_associations : @klass.find_by_sql(arel, @bind_values)
- end
- end
-
- preload = @preload_values
- preload += @includes_values unless eager_loading?
- preload.each do |associations|
- ActiveRecord::Associations::Preloader.new(@records, associations).run
- end
-
- # @readonly_value is true only if set explicitly. @implicit_readonly is true if there
- # are JOINS and no explicit SELECT.
- readonly = @readonly_value.nil? ? @implicit_readonly : @readonly_value
- @records.each { |record| record.readonly! } if readonly
- else
- @records = default_scoped.to_a
- end
-
- @loaded = true
+ load
@records
end
- private :exec_queries
def as_json(options = nil) #:nodoc:
to_a.as_json(options)
@@ -212,6 +206,7 @@ module ActiveRecord
c.respond_to?(:zero?) ? c.zero? : c.empty?
end
+ # Returns true if there are any records.
def any?
if block_given?
to_a.any? { |*block_args| yield(*block_args) }
@@ -220,18 +215,17 @@ module ActiveRecord
end
end
+ # Returns true if there is more than one record.
def many?
if block_given?
to_a.many? { |*block_args| yield(*block_args) }
else
- @limit_value ? to_a.many? : size > 1
+ limit_value ? to_a.many? : size > 1
end
end
# Scope all queries to the current scope.
#
- # ==== Example
- #
# Comment.where(:post_id => 1).scoping do
# Comment.first # SELECT * FROM comments WHERE post_id = 1
# end
@@ -239,7 +233,10 @@ module ActiveRecord
# Please check unscoped if you want to remove all previous scopes (including
# the default_scope) during the execution of a block.
def scoping
- @klass.with_scope(self, :overwrite) { yield }
+ previous, klass.current_scope = klass.current_scope, self
+ yield
+ ensure
+ klass.current_scope = previous
end
# Updates all records with details given if they match a set of conditions supplied, limits and order can
@@ -250,50 +247,35 @@ module ActiveRecord
# ==== Parameters
#
# * +updates+ - A string, array, or hash representing the SET part of an SQL statement.
- # * +conditions+ - A string, array, or hash representing the WHERE part of an SQL statement.
- # See conditions in the intro.
- # * +options+ - Additional options are <tt>:limit</tt> and <tt>:order</tt>, see the examples for usage.
#
# ==== Examples
#
# # Update all customers with the given attributes
- # Customer.update_all :wants_email => true
+ # Customer.update_all wants_email: true
#
# # Update all books with 'Rails' in their title
- # Book.update_all "author = 'David'", "title LIKE '%Rails%'"
- #
- # # Update all avatars migrated more than a week ago
- # Avatar.update_all ['migrated_at = ?', Time.now.utc], ['migrated_at > ?', 1.week.ago]
+ # Book.where('title LIKE ?', '%Rails%').update_all(author: 'David')
#
# # Update all books that match conditions, but limit it to 5 ordered by date
- # Book.update_all "author = 'David'", "title LIKE '%Rails%'", :order => 'created_at', :limit => 5
- #
- # # Conditions from the current relation also works
- # Book.where('title LIKE ?', '%Rails%').update_all(:author => 'David')
- #
- # # The same idea applies to limit and order
# Book.where('title LIKE ?', '%Rails%').order(:created_at).limit(5).update_all(:author => 'David')
- def update_all(updates, conditions = nil, options = {})
- IdentityMap.repository[symbolized_base_class].clear if IdentityMap.enabled?
- if conditions || options.present?
- where(conditions).apply_finder_options(options.slice(:limit, :order)).update_all(updates)
- else
- stmt = Arel::UpdateManager.new(arel.engine)
+ def update_all(updates)
+ raise ArgumentError, "Empty list of attributes to change" if updates.blank?
- stmt.set Arel.sql(@klass.send(:sanitize_sql_for_assignment, updates))
- stmt.table(table)
- stmt.key = table[primary_key]
+ stmt = Arel::UpdateManager.new(arel.engine)
- if joins_values.any?
- @klass.connection.join_to_update(stmt, arel)
- else
- stmt.take(arel.limit)
- stmt.order(*arel.orders)
- stmt.wheres = arel.constraints
- end
+ stmt.set Arel.sql(@klass.send(:sanitize_sql_for_assignment, updates))
+ stmt.table(table)
+ stmt.key = table[primary_key]
- @klass.connection.update stmt, 'SQL', bind_values
+ if joins_values.any?
+ @klass.connection.join_to_update(stmt, arel)
+ else
+ stmt.take(arel.limit)
+ stmt.order(*arel.orders)
+ stmt.wheres = arel.constraints
end
+
+ @klass.connection.update stmt, 'SQL', bind_values
end
# Updates an object (or multiple objects) and saves it to the database, if validations pass.
@@ -307,14 +289,14 @@ module ActiveRecord
# ==== Examples
#
# # Updates one record
- # Person.update(15, :user_name => 'Samuel', :group => 'expert')
+ # Person.update(15, user_name: 'Samuel', group: 'expert')
#
# # Updates multiple records
# people = { 1 => { "first_name" => "David" }, 2 => { "first_name" => "Jeremy" } }
# Person.update(people.keys, people.values)
def update(id, attributes)
if id.is_a?(Array)
- id.each.with_index.map {|one_id, idx| update(one_id, attributes[idx])}
+ id.map.with_index { |one_id, idx| update(one_id, attributes[idx]) }
else
object = find(id)
object.update_attributes(attributes)
@@ -347,7 +329,7 @@ module ActiveRecord
# ==== Examples
#
# Person.destroy_all("last_login < '2004-04-04'")
- # Person.destroy_all(:status => "inactive")
+ # Person.destroy_all(status: "inactive")
# Person.where(:age => 0..18).destroy_all
def destroy_all(conditions = nil)
if conditions
@@ -384,17 +366,12 @@ module ActiveRecord
end
end
- # Deletes the records matching +conditions+ without instantiating the records first, and hence not
- # calling the +destroy+ method nor invoking callbacks. This is a single SQL DELETE statement that
- # goes straight to the database, much more efficient than +destroy_all+. Be careful with relations
- # though, in particular <tt>:dependent</tt> rules defined on associations are not honored. Returns
- # the number of rows affected.
- #
- # ==== Parameters
- #
- # * +conditions+ - Conditions are specified the same way as with +find+ method.
- #
- # ==== Example
+ # Deletes the records matching +conditions+ without instantiating the records
+ # first, and hence not calling the +destroy+ method nor invoking callbacks. This
+ # is a single SQL DELETE statement that goes straight to the database, much more
+ # efficient than +destroy_all+. Be careful with relations though, in particular
+ # <tt>:dependent</tt> rules defined on associations are not honored. Returns the
+ # number of rows affected.
#
# Post.delete_all("person_id = 5 AND (category = 'Something' OR category = 'Else')")
# Post.delete_all(["person_id = ? AND (category = ? OR category = ?)", 5, 'Something', 'Else'])
@@ -403,13 +380,27 @@ module ActiveRecord
# Both calls delete the affected posts all at once with a single DELETE statement.
# If you need to destroy dependent associations or call your <tt>before_*</tt> or
# +after_destroy+ callbacks, use the +destroy_all+ method instead.
+ #
+ # If a limit scope is supplied, +delete_all+ raises an ActiveRecord error:
+ #
+ # Post.limit(100).delete_all
+ # # => ActiveRecord::ActiveRecordError: delete_all doesn't support limit scope
def delete_all(conditions = nil)
- IdentityMap.repository[symbolized_base_class] = {} if IdentityMap.enabled?
+ raise ActiveRecordError.new("delete_all doesn't support limit scope") if self.limit_value
+
if conditions
where(conditions).delete_all
else
- statement = arel.compile_delete
- affected = @klass.connection.delete(statement, 'SQL', bind_values)
+ stmt = Arel::DeleteManager.new(arel.engine)
+ stmt.from(table)
+
+ if joins_values.any?
+ @klass.connection.join_to_delete(stmt, arel, table[primary_key])
+ else
+ stmt.wheres = arel.constraints
+ end
+
+ affected = @klass.connection.delete(stmt, 'SQL', bind_values)
reset
affected
@@ -437,14 +428,35 @@ module ActiveRecord
# # Delete multiple rows
# Todo.delete([2,3,4])
def delete(id_or_array)
- IdentityMap.remove_by_id(self.symbolized_base_class, id_or_array) if IdentityMap.enabled?
where(primary_key => id_or_array).delete_all
end
+ # Causes the records to be loaded from the database if they have not
+ # been loaded already. You can use this if for some reason you need
+ # to explicitly load some records before actually using them. The
+ # return value is the relation itself, not the records.
+ #
+ # Post.where(published: true).load # => #<ActiveRecord::Relation>
+ def load
+ unless loaded?
+ # We monitor here the entire execution rather than individual SELECTs
+ # because from the point of view of the user fetching the records of a
+ # relation is a single unit of work. You want to know if this call takes
+ # too long, not if the individual queries take too long.
+ #
+ # It could be the case that none of the queries involved surpass the
+ # threshold, and at the same time the sum of them all does. The user
+ # should get a query plan logged in that case.
+ logging_query_plan { exec_queries }
+ end
+
+ self
+ end
+
+ # Forces reloading of relation.
def reload
reset
- to_a # force reload
- self
+ load
end
def reset
@@ -454,26 +466,40 @@ module ActiveRecord
self
end
+ # Returns sql statement for the relation.
+ #
+ # Users.where(name: 'Oscar').to_sql
+ # # => SELECT "users".* FROM "users" WHERE "users"."name" = 'Oscar'
def to_sql
- @to_sql ||= klass.connection.to_sql(arel, @bind_values.dup)
+ @to_sql ||= klass.connection.to_sql(arel, bind_values.dup)
end
+ # Returns a hash of where conditions
+ #
+ # Users.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|
node.left.relation.name == table_name
}
- Hash[equalities.map { |where| [where.left.name, where.right] }]
+ binds = Hash[bind_values.find_all(&:first).map { |column, v| [column.name, v] }]
+
+ Hash[equalities.map { |where|
+ name = where.left.name
+ [name, binds.fetch(name.to_s) { where.right }]
+ }]
end
def scope_for_create
@scope_for_create ||= where_values_hash.merge(create_with_value)
end
+ # Returns true if relation needs eager loading.
def eager_loading?
@should_eager_load ||=
- @eager_load_values.any? ||
- @includes_values.any? && (joined_includes_values.any? || references_eager_loaded_tables?)
+ eager_load_values.any? ||
+ includes_values.any? && (joined_includes_values.any? || references_eager_loaded_tables?)
end
# Joins that are also marked for preloading. In which case we should just eager load them.
@@ -481,9 +507,10 @@ module ActiveRecord
# represent the same association, but that aren't matched by this. Also, we could have
# nested hashes which partially match, e.g. { :a => :b } & { :a => [:b, :c] }
def joined_includes_values
- @includes_values & @joins_values
+ includes_values & joins_values
end
+ # Compares two relations for equality.
def ==(other)
case other
when Relation
@@ -493,10 +520,6 @@ module ActiveRecord
end
end
- def inspect
- to_a.inspect
- end
-
def pretty_print(q)
q.pp(self.to_a)
end
@@ -511,8 +534,48 @@ module ActiveRecord
end
end
+ # Returns true if relation is blank.
+ def blank?
+ to_a.blank?
+ end
+
+ def values
+ @values.dup
+ end
+
+ def inspect
+ entries = to_a.take([limit_value, 11].compact.min).map!(&:inspect)
+ entries[10] = '...' if entries.size == 11
+
+ "#<#{self.class.name} [#{entries.join(', ')}]>"
+ end
+
private
+ def exec_queries
+ default_scoped = with_default_scope
+
+ if default_scoped.equal?(self)
+ @records = eager_loading? ? find_with_associations : @klass.find_by_sql(arel, bind_values)
+
+ preload = preload_values
+ preload += includes_values unless eager_loading?
+ preload.each do |associations|
+ ActiveRecord::Associations::Preloader.new(@records, associations).run
+ end
+
+ # @readonly_value is true only if set explicitly. @implicit_readonly is true if there
+ # are JOINS and no explicit SELECT.
+ readonly = readonly_value.nil? ? @implicit_readonly : readonly_value
+ @records.each { |record| record.readonly! } if readonly
+ else
+ @records = default_scoped.to_a
+ end
+
+ @loaded = true
+ @records
+ end
+
def references_eager_loaded_tables?
joined_tables = arel.join_sources.map do |join|
if join.is_a?(Arel::Nodes::StringJoin)
diff --git a/activerecord/lib/active_record/relation/batches.rb b/activerecord/lib/active_record/relation/batches.rb
index 2fd89882ff..d32048cce1 100644
--- a/activerecord/lib/active_record/relation/batches.rb
+++ b/activerecord/lib/active_record/relation/batches.rb
@@ -1,21 +1,26 @@
-require 'active_support/core_ext/object/blank'
module ActiveRecord
module Batches
- # Yields each record that was found by the find +options+. The find is
- # performed by find_in_batches with a batch size of 1000 (or as
- # specified by the <tt>:batch_size</tt> option).
+ # Looping through a collection of records from the database
+ # (using the +all+ method, for example) is very inefficient
+ # since it will try to instantiate all the objects at once.
#
- # Example:
+ # In that case, batch processing methods allow you to work
+ # with the records in batches, thereby greatly reducing memory consumption.
+ #
+ # The #find_each method uses #find_in_batches with a batch size of 1000 (or as
+ # specified by the +:batch_size+ option).
+ #
+ # Person.all.find_each do |person|
+ # person.do_awesome_stuff
+ # end
#
# Person.where("age > 21").find_each do |person|
# person.party_all_night!
# end
#
- # Note: This method is only intended to use for batch processing of
- # large amounts of records that wouldn't fit in memory all at once. If
- # you just need to loop over less than 1000 records, it's probably
- # better just to use the regular find methods.
+ # You can also pass the +:start+ option to specify
+ # an offset to control the starting point.
def find_each(options = {})
find_in_batches(options) do |records|
records.each { |record| yield record }
@@ -23,51 +28,50 @@ module ActiveRecord
end
# Yields each batch of records that was found by the find +options+ as
- # an array. The size of each batch is set by the <tt>:batch_size</tt>
+ # an array. The size of each batch is set by the +:batch_size+
# option; the default is 1000.
#
# You can control the starting point for the batch processing by
- # supplying the <tt>:start</tt> option. This is especially useful if you
+ # supplying the +:start+ option. This is especially useful if you
# want multiple workers dealing with the same processing queue. You can
# make worker 1 handle all the records between id 0 and 10,000 and
- # worker 2 handle from 10,000 and beyond (by setting the <tt>:start</tt>
- # option on that worker).
+ # worker 2 handle from 10,000 and beyond (by setting the +:start+
+ # option on that worker). You can also use non-integer-based primary keys
+ # if start point is set.
#
# It's not possible to set the order. That is automatically set to
- # ascending on the primary key ("id ASC") to make the batch ordering
- # work. This also mean that this method only works with integer-based
- # primary keys. You can't set the limit either, that's used to control
+ # ascending on the primary key (e.g. "id ASC") to make the batch ordering
+ # work. You can't set the limit either, that's used to control
# the batch sizes.
#
- # Example:
- #
# Person.where("age > 21").find_in_batches do |group|
# sleep(50) # Make sure it doesn't get too crowded in there!
# group.each { |person| person.party_all_night! }
# end
+ #
+ # # Let's process the next 2000 records
+ # Person.all.find_in_batches(start: 2000, batch_size: 2000) do |group|
+ # group.each { |person| person.party_all_night! }
+ # end
def find_in_batches(options = {})
+ options.assert_valid_keys(:start, :batch_size)
+
relation = self
unless arel.orders.blank? && arel.taken.blank?
ActiveRecord::Base.logger.warn("Scoped order and limit are ignored, it's forced to be batch order and batch size")
end
- if (finder_options = options.except(:start, :batch_size)).present?
- raise "You can't specify an order, it's forced to be #{batch_order}" if options[:order].present?
- raise "You can't specify a limit, it's forced to be the batch_size" if options[:limit].present?
-
- relation = apply_finder_options(finder_options)
- end
-
- start = options.delete(:start).to_i
+ start = options.delete(:start)
+ start ||= 0
batch_size = options.delete(:batch_size) || 1000
relation = relation.reorder(batch_order).limit(batch_size)
- records = relation.where(table[primary_key].gteq(start)).all
+ records = relation.where(table[primary_key].gteq(start)).to_a
while records.any?
records_size = records.size
- primary_key_offset = records.last.id
+ primary_key_offset = records.last.send(primary_key)
yield records
diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb
index 63365e501b..7c43d844d0 100644
--- a/activerecord/lib/active_record/relation/calculations.rb
+++ b/activerecord/lib/active_record/relation/calculations.rb
@@ -1,61 +1,30 @@
-require 'active_support/core_ext/object/blank'
require 'active_support/core_ext/object/try'
module ActiveRecord
module Calculations
- # Count operates using three different approaches.
- #
- # * Count all: By not passing any parameters to count, it will return a count of all the rows for the model.
- # * Count using column: By passing a column name to count, it will return a count of all the
- # rows for the model with supplied column present.
- # * Count using options will find the row count matched by the options used.
- #
- # The third approach, count using options, accepts an option hash as the only parameter. The options are:
- #
- # * <tt>:conditions</tt>: An SQL fragment like "administrator = 1" or [ "user_name = ?", username ].
- # See conditions in the intro to ActiveRecord::Base.
- # * <tt>:joins</tt>: Either an SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id"
- # (rarely needed) or named associations in the same form used for the <tt>:include</tt> option, which will
- # perform an INNER JOIN on the associated table(s). If the value is a string, then the records
- # will be returned read-only since they will have attributes that do not correspond to the table's columns.
- # Pass <tt>:readonly => false</tt> to override.
- # * <tt>:include</tt>: Named associations that should be loaded alongside using LEFT OUTER JOINs.
- # The symbols named refer to already defined associations. When using named associations, count
- # returns the number of DISTINCT items for the model you're counting.
- # See eager loading under Associations.
- # * <tt>:order</tt>: An SQL fragment like "created_at DESC, name" (really only used with GROUP BY calculations).
- # * <tt>:group</tt>: An attribute name by which the result should be grouped. Uses the GROUP BY SQL-clause.
- # * <tt>:select</tt>: By default, this is * as in SELECT * FROM, but can be changed if you, for example,
- # want to do a join but not include the joined columns.
- # * <tt>:distinct</tt>: Set this to true to make this a distinct calculation, such as
- # SELECT COUNT(DISTINCT posts.id) ...
- # * <tt>:from</tt> - By default, this is the table name of the class, but can be changed to an
- # alternate table name (or even the name of a database view).
- #
- # Examples for counting all:
- # Person.count # returns the total count of all people
- #
- # Examples for counting by column:
- # Person.count(:age) # returns the total count of all people whose age is present in database
- #
- # Examples for count with options:
- # Person.count(:conditions => "age > 26")
- #
- # # because of the named association, it finds the DISTINCT count using LEFT OUTER JOIN.
- # Person.count(:conditions => "age > 26 AND job.salary > 60000", :include => :job)
- #
- # # finds the number of rows matching the conditions and joins.
- # Person.count(:conditions => "age > 26 AND job.salary > 60000",
- # :joins => "LEFT JOIN jobs on jobs.person_id = person.id")
- #
- # Person.count('id', :conditions => "age > 26") # Performs a COUNT(id)
- # Person.count(:all, :conditions => "age > 26") # Performs a COUNT(*) (:all is an alias for '*')
- #
- # Note: <tt>Person.count(:all)</tt> will not work because it will use <tt>:all</tt> as the condition.
- # Use Person.count instead.
+ # Count the records.
+ #
+ # Person.count
+ # # => the total count of all people
+ #
+ # Person.count(:age)
+ # # => returns the total count of all people whose age is present in database
+ #
+ # Person.count(:all)
+ # # => performs a COUNT(*) (:all is an alias for '*')
+ #
+ # Person.count(:age, distinct: true)
+ # # => counts the number of different age values
+ #
+ # Person.where("age > 26").count { |person| person.gender == 'female' }
+ # # => queries people where "age > 26" then count the loaded results filtering by gender
def count(column_name = nil, options = {})
- column_name, options = nil, column_name if column_name.is_a?(Hash)
- calculate(:count, column_name, options)
+ if block_given?
+ self.to_a.count { |item| yield item }
+ else
+ column_name, options = nil, column_name if column_name.is_a?(Hash)
+ calculate(:count, column_name, options)
+ end
end
# Calculates the average value on a given column. Returns +nil+ if there's
@@ -89,30 +58,35 @@ module ActiveRecord
# +calculate+ for examples with options.
#
# Person.sum('age') # => 4562
+ # # => returns the total sum of all people's age
+ #
+ # Person.where('age > 100').sum { |person| person.age - 100 }
+ # # queries people where "age > 100" then perform a sum calculation with the block returns
def sum(*args)
if block_given?
- self.to_a.sum(*args) {|*block_args| yield(*block_args)}
+ self.to_a.sum(*args) { |item| yield item }
else
calculate(:sum, *args)
end
end
# This calculates aggregate values in the given column. Methods for count, sum, average,
- # minimum, and maximum have been added as shortcuts. Options such as <tt>:conditions</tt>,
- # <tt>:order</tt>, <tt>:group</tt>, <tt>:having</tt>, and <tt>:joins</tt> can be passed to customize the query.
+ # minimum, and maximum have been added as shortcuts.
#
# There are two basic forms of output:
+ #
# * Single aggregate value: The single value is type cast to Fixnum for COUNT, Float
# for AVG, and the given column's type for everything else.
- # * Grouped values: This returns an ordered hash of the values and groups them by the
- # <tt>:group</tt> option. It takes either a column name, or the name of a belongs_to association.
#
- # values = Person.maximum(:age, :group => 'last_name')
+ # * Grouped values: This returns an ordered hash of the values and groups them. It
+ # takes either a column name, or the name of a belongs_to association.
+ #
+ # values = Person.group('last_name').maximum(:age)
# puts values["Drake"]
# => 43
#
# drake = Family.find_by_last_name('Drake')
- # values = Person.maximum(:age, :group => :family) # Person belongs_to :family
+ # values = Person.group(:family).maximum(:age) # Person belongs_to :family
# puts values[drake]
# => 43
#
@@ -120,85 +94,113 @@ module ActiveRecord
# ...
# end
#
- # Options:
- # * <tt>:conditions</tt> - An SQL fragment like "administrator = 1" or [ "user_name = ?", username ].
- # See conditions in the intro to ActiveRecord::Base.
- # * <tt>:include</tt>: Eager loading, see Associations for details. Since calculations don't load anything,
- # the purpose of this is to access fields on joined tables in your conditions, order, or group clauses.
- # * <tt>:joins</tt> - An SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id".
- # (Rarely needed).
- # The records will be returned read-only since they will have attributes that do not correspond to the
- # table's columns.
- # * <tt>:order</tt> - An SQL fragment like "created_at DESC, name" (really only used with GROUP BY calculations).
- # * <tt>:group</tt> - An attribute name by which the result should be grouped. Uses the GROUP BY SQL-clause.
- # * <tt>:select</tt> - By default, this is * as in SELECT * FROM, but can be changed if you for example
- # want to do a join, but not include the joined columns.
- # * <tt>:distinct</tt> - Set this to true to make this a distinct calculation, such as
- # SELECT COUNT(DISTINCT posts.id) ...
- #
# Examples:
# Person.calculate(:count, :all) # The same as Person.count
# Person.average(:age) # SELECT AVG(age) FROM people...
- # Person.minimum(:age, :conditions => ['last_name != ?', 'Drake']) # Selects the minimum age for
- # # everyone with a last name other than 'Drake'
#
# # Selects the minimum age for any family without any minors
- # Person.minimum(:age, :having => 'min(age) > 17', :group => :last_name)
+ # Person.group(:last_name).having("min(age) > 17").minimum(:age)
#
# Person.sum("2 * age")
def calculate(operation, column_name, options = {})
- if options.except(:distinct).present?
- apply_finder_options(options.except(:distinct)).calculate(operation, column_name, :distinct => options[:distinct])
- else
- relation = with_default_scope
+ relation = with_default_scope
- if relation.equal?(self)
- if eager_loading? || (includes_values.present? && references_eager_loaded_tables?)
- construct_relation_for_association_calculations.calculate(operation, column_name, options)
- else
- perform_calculation(operation, column_name, options)
- end
+ if relation.equal?(self)
+ if has_include?(column_name)
+ construct_relation_for_association_calculations.calculate(operation, column_name, options)
else
- relation.calculate(operation, column_name, options)
+ perform_calculation(operation, column_name, options)
end
+ else
+ relation.calculate(operation, column_name, options)
end
rescue ThrowResult
0
end
- # This method is designed to perform select by a single column as direct SQL query
- # Returns <tt>Array</tt> with values of the specified column name
- # The values has same data type as column.
+ # Use <tt>pluck</tt> as a shortcut to select a single attribute without
+ # loading a bunch of records just to grab one attribute you want.
+ #
+ # Person.pluck(:name)
+ #
+ # instead of
+ #
+ # Person.all.map(&:name)
+ #
+ # Pluck returns an <tt>Array</tt> of attribute values type-casted to match
+ # the plucked column name, if it can be deduced. Plucking an SQL fragment
+ # returns String values by default.
#
# Examples:
#
- # Person.pluck(:id) # SELECT people.id FROM people
- # Person.uniq.pluck(:role) # SELECT DISTINCT role FROM people
- # Person.where(:confirmed => true).limit(5).pluck(:id)
+ # Person.pluck(:id)
+ # # SELECT people.id FROM people
+ # # => [1, 2, 3]
#
- def pluck(column_name)
- key = column_name.to_s.split('.', 2).last
-
- if column_name.is_a?(Symbol) && column_names.include?(column_name.to_s)
- column_name = "#{table_name}.#{column_name}"
+ # Person.pluck(:id, :name)
+ # # SELECT people.id, people.name FROM people
+ # # => [[1, 'David'], [2, 'Jeremy'], [3, 'Jose']]
+ #
+ # Person.uniq.pluck(:role)
+ # # SELECT DISTINCT role FROM people
+ # # => ['admin', 'member', 'guest']
+ #
+ # Person.where(:age => 21).limit(5).pluck(:id)
+ # # SELECT people.id FROM people WHERE people.age = 21 LIMIT 5
+ # # => [2, 3]
+ #
+ # Person.pluck('DATEDIFF(updated_at, created_at)')
+ # # SELECT DATEDIFF(updated_at, created_at) FROM people
+ # # => ['0', '27761', '173']
+ #
+ 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
+ end
end
- result = klass.connection.select_all(select(column_name).arel)
- types = result.column_types.merge klass.column_types
- column = types[key]
+ if has_include?(column_names.first)
+ construct_relation_for_association_calculations.pluck(*column_names)
+ else
+ result = klass.connection.select_all(select(column_names).arel, nil, bind_values)
+ columns = result.columns.map do |key|
+ klass.column_types.fetch(key) {
+ result.column_types.fetch(key) {
+ Class.new { def type_cast(v); v; end }.new
+ }
+ }
+ end
- result.map do |attributes|
- value = klass.initialize_attributes(attributes)[key]
- if column
- column.type_cast value
- else
- value
+ result = result.map do |attributes|
+ values = klass.initialize_attributes(attributes).values
+
+ columns.zip(values).map do |column, value|
+ column.type_cast(value)
+ end
end
+ columns.one? ? result.map!(&:first) : result
end
end
+ # Pluck all the ID's for the relation using the table's primary key
+ #
+ # Examples:
+ #
+ # Person.ids # SELECT people.id FROM people
+ # Person.joins(:companies).ids # SELECT people.id FROM people INNER JOIN companies ON companies.person_id = people.id
+ def ids
+ pluck primary_key
+ end
+
private
+ def has_include?(column_name)
+ eager_loading? || (includes_values.present? && (column_name || references_eager_loaded_tables?))
+ end
+
def perform_calculation(operation, column_name, options = {})
operation = operation.to_s.downcase
@@ -216,7 +218,7 @@ module ActiveRecord
distinct = nil if column_name =~ /\s*DISTINCT\s+/i
end
- if @group_values.any?
+ if group_values.any?
execute_grouped_calculation(operation, column_name, distinct)
else
execute_simple_calculation(operation, column_name, distinct)
@@ -254,14 +256,21 @@ module ActiveRecord
query_builder = relation.arel
end
- type_cast_calculated_value(@klass.connection.select_value(query_builder), column_for(column_name), operation)
+ result = @klass.connection.select_value(query_builder, nil, relation.bind_values)
+ type_cast_calculated_value(result, column_for(column_name), operation)
end
def execute_grouped_calculation(operation, column_name, distinct) #:nodoc:
- group_attr = @group_values
- association = @klass.reflect_on_association(group_attr.first.to_sym)
- associated = group_attr.size == 1 && association && association.macro == :belongs_to # only count belongs_to associations
- group_fields = Array(associated ? association.foreign_key : group_attr)
+ group_attrs = group_values
+
+ if group_attrs.first.respond_to?(:to_sym)
+ association = @klass.reflect_on_association(group_attrs.first.to_sym)
+ associated = group_attrs.size == 1 && association && association.macro == :belongs_to # only count belongs_to associations
+ group_fields = Array(associated ? association.foreign_key : group_attrs)
+ else
+ group_fields = group_attrs
+ end
+
group_aliases = group_fields.map { |field| column_alias_for(field) }
group_columns = group_aliases.zip(group_fields).map { |aliaz,field|
[aliaz, column_for(field)]
@@ -281,16 +290,20 @@ module ActiveRecord
operation,
distinct).as(aggregate_alias)
]
- select_values += @select_values unless @having_values.empty?
+ select_values += select_values unless having_values.empty?
select_values.concat group_fields.zip(group_aliases).map { |field,aliaz|
- "#{field} AS #{aliaz}"
+ if field.respond_to?(:as)
+ field.as(aliaz)
+ else
+ "#{field} AS #{aliaz}"
+ end
}
- relation = except(:group).group(group.join(','))
+ relation = except(:group).group(group)
relation.select_values = select_values
- calculated_data = @klass.connection.select_all(relation)
+ calculated_data = @klass.connection.select_all(relation, nil, bind_values)
if association
key_ids = calculated_data.collect { |row| row[group_aliases.first] }
@@ -298,11 +311,11 @@ module ActiveRecord
key_records = Hash[key_records.map { |r| [r.id, r] }]
end
- ActiveSupport::OrderedHash[calculated_data.map do |row|
- key = group_columns.map { |aliaz, column|
+ Hash[calculated_data.map do |row|
+ key = group_columns.map { |aliaz, column|
type_cast_calculated_value(row[aliaz], column)
}
- key = key.first if key.size == 1
+ key = key.first if key.size == 1
key = key_records[key] if associated
[key, type_cast_calculated_value(row[aggregate_alias], column_for(column_name), operation)]
end]
@@ -317,6 +330,7 @@ module ActiveRecord
# column_alias_for("count(*)") # => "count_all"
# column_alias_for("count", "id") # => "count_id"
def column_alias_for(*keys)
+ keys.map! {|k| k.respond_to?(:to_sql) ? k.to_sql : k}
table_name = keys.join(' ')
table_name.downcase!
table_name.gsub!(/\*/, 'all')
@@ -328,7 +342,7 @@ module ActiveRecord
end
def column_for(field)
- field_name = field.to_s.split('.').last
+ field_name = field.respond_to?(:name) ? field.name.to_s : field.to_s.split('.').last
@klass.columns.detect { |c| c.name.to_s == field_name }
end
@@ -346,8 +360,8 @@ module ActiveRecord
end
def select_for_count
- if @select_values.present?
- select = @select_values.join(", ")
+ if select_values.present?
+ select = select_values.join(", ")
select if select !~ /[,*]/
end
end
diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb
index f5fdf437bf..ab8b36c8ab 100644
--- a/activerecord/lib/active_record/relation/delegation.rb
+++ b/activerecord/lib/active_record/relation/delegation.rb
@@ -1,7 +1,6 @@
-require 'active_support/core_ext/module/delegation'
module ActiveRecord
- module Delegation
+ module Delegation # :nodoc:
# Set up common delegations for performance (avoids method_missing)
delegate :to_xml, :to_yaml, :length, :collect, :map, :each, :all?, :include?, :to_ary, :to => :to_a
delegate :table_name, :quoted_table_name, :primary_key, :quoted_primary_key,
@@ -32,12 +31,12 @@ module ActiveRecord
protected
def method_missing(method, *args, &block)
- if Array.method_defined?(method)
- ::ActiveRecord::Delegation.delegate method, :to => :to_a
- to_a.send(method, *args, &block)
- elsif @klass.respond_to?(method)
+ if @klass.respond_to?(method)
::ActiveRecord::Delegation.delegate_to_scoped_klass(method)
scoping { @klass.send(method, *args, &block) }
+ elsif Array.method_defined?(method)
+ ::ActiveRecord::Delegation.delegate method, :to => :to_a
+ to_a.send(method, *args, &block)
elsif arel.respond_to?(method)
::ActiveRecord::Delegation.delegate method, :to => :arel
arel.send(method, *args, &block)
@@ -46,4 +45,4 @@ module ActiveRecord
end
end
end
-end \ No newline at end of file
+end
diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb
index 26a5165abf..84aaa39fed 100644
--- a/activerecord/lib/active_record/relation/finder_methods.rb
+++ b/activerecord/lib/active_record/relation/finder_methods.rb
@@ -1,85 +1,23 @@
-require 'active_support/core_ext/object/blank'
require 'active_support/core_ext/hash/indifferent_access'
module ActiveRecord
module FinderMethods
- # Find operates with four different retrieval approaches:
+ # Find by id - This can either be a specific id (1), a list of ids (1, 5, 6), or an array of ids ([5, 6, 10]).
+ # If no record can be found for all of the listed ids, then RecordNotFound will be raised. If the primary key
+ # is an integer, find by id coerces its arguments using +to_i+.
#
- # * Find by id - This can either be a specific id (1), a list of ids (1, 5, 6), or an array of ids ([5, 6, 10]).
- # If no record can be found for all of the listed ids, then RecordNotFound will be raised.
- # * Find first - This will return the first record matched by the options used. These options can either be specific
- # conditions or merely an order. If no record can be matched, +nil+ is returned. Use
- # <tt>Model.find(:first, *args)</tt> or its shortcut <tt>Model.first(*args)</tt>.
- # * Find last - This will return the last record matched by the options used. These options can either be specific
- # conditions or merely an order. If no record can be matched, +nil+ is returned. Use
- # <tt>Model.find(:last, *args)</tt> or its shortcut <tt>Model.last(*args)</tt>.
- # * Find all - This will return all the records matched by the options used.
- # If no records are found, an empty array is returned. Use
- # <tt>Model.find(:all, *args)</tt> or its shortcut <tt>Model.all(*args)</tt>.
- #
- # All approaches accept an options hash as their last parameter.
- #
- # ==== Options
- #
- # * <tt>:conditions</tt> - An SQL fragment like "administrator = 1", <tt>["user_name = ?", username]</tt>,
- # or <tt>["user_name = :user_name", { :user_name => user_name }]</tt>. See conditions in the intro.
- # * <tt>:order</tt> - An SQL fragment like "created_at DESC, name".
- # * <tt>:group</tt> - An attribute name by which the result should be grouped. Uses the <tt>GROUP BY</tt> SQL-clause.
- # * <tt>:having</tt> - Combined with +:group+ this can be used to filter the records that a
- # <tt>GROUP BY</tt> returns. Uses the <tt>HAVING</tt> SQL-clause.
- # * <tt>:limit</tt> - An integer determining the limit on the number of rows that should be returned.
- # * <tt>:offset</tt> - An integer determining the offset from where the rows should be fetched. So at 5,
- # it would skip rows 0 through 4.
- # * <tt>:joins</tt> - Either an SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id" (rarely needed),
- # named associations in the same form used for the <tt>:include</tt> option, which will perform an
- # <tt>INNER JOIN</tt> on the associated table(s),
- # or an array containing a mixture of both strings and named associations.
- # If the value is a string, then the records will be returned read-only since they will
- # have attributes that do not correspond to the table's columns.
- # Pass <tt>:readonly => false</tt> to override.
- # * <tt>:include</tt> - Names associations that should be loaded alongside. The symbols named refer
- # to already defined associations. See eager loading under Associations.
- # * <tt>:select</tt> - By default, this is "*" as in "SELECT * FROM", but can be changed if you,
- # for example, want to do a join but not include the joined columns. Takes a string with the SELECT SQL fragment (e.g. "id, name").
- # * <tt>:from</tt> - By default, this is the table name of the class, but can be changed
- # to an alternate table name (or even the name of a database view).
- # * <tt>:readonly</tt> - Mark the returned records read-only so they cannot be saved or updated.
- # * <tt>:lock</tt> - An SQL fragment like "FOR UPDATE" or "LOCK IN SHARE MODE".
- # <tt>:lock => true</tt> gives connection's default exclusive lock, usually "FOR UPDATE".
- #
- # ==== Examples
- #
- # # find by id
# Person.find(1) # returns the object for ID = 1
+ # Person.find("1") # returns the object for ID = 1
# Person.find(1, 2, 6) # returns an array for objects with IDs in (1, 2, 6)
# Person.find([7, 17]) # returns an array for objects with IDs in (7, 17)
# Person.find([1]) # returns an array for the object with ID = 1
# Person.where("administrator = 1").order("created_on DESC").find(1)
#
# Note that returned records may not be in the same order as the ids you
- # provide since database rows are unordered. Give an explicit <tt>:order</tt>
+ # provide since database rows are unordered. Give an explicit <tt>order</tt>
# to ensure the results are sorted.
#
- # ==== Examples
- #
- # # find first
- # Person.first # returns the first object fetched by SELECT * FROM people
- # Person.where(["user_name = ?", user_name]).first
- # Person.where(["user_name = :u", { :u => user_name }]).first
- # Person.order("created_on DESC").offset(5).first
- #
- # # find last
- # Person.last # returns the last object fetched by SELECT * FROM people
- # Person.where(["user_name = ?", user_name]).last
- # Person.order("created_on DESC").offset(5).last
- #
- # # find all
- # Person.all # returns an array of objects for all the rows fetched by SELECT * FROM people
- # Person.where(["category IN (?)", categories]).limit(50).all
- # Person.where({ :friends => ["Bob", "Steve", "Fred"] }).all
- # Person.offset(10).limit(10).all
- # Person.includes([:account, :friends]).all
- # Person.group("category").all
+ # ==== Find with lock
#
# Example for find with a lock: Imagine two concurrent transactions:
# each will read <tt>person.visits == 2</tt>, add 1 to it, and save, resulting
@@ -93,30 +31,62 @@ module ActiveRecord
# person.save!
# end
def find(*args)
- return to_a.find { |*block_args| yield(*block_args) } if block_given?
-
- options = args.extract_options!
-
- if options.present?
- apply_finder_options(options).find(*args)
+ if block_given?
+ to_a.find { |*block_args| yield(*block_args) }
else
- case args.first
- when :first, :last, :all
- send(args.first)
- else
- find_with_ids(*args)
- end
+ find_with_ids(*args)
end
end
- # A convenience wrapper for <tt>find(:first, *args)</tt>. You can pass in all the
- # same arguments to this method as you can to <tt>find(:first)</tt>.
- def first(*args)
- if args.any?
- if args.first.kind_of?(Integer) || (loaded? && !args.first.kind_of?(Hash))
- limit(*args).to_a
+ # Finds the first record matching the specified conditions. There
+ # is no implied ording so if order matters, you should specify it
+ # yourself.
+ #
+ # If no record is found, returns <tt>nil</tt>.
+ #
+ # Post.find_by name: 'Spartacus', rating: 4
+ # Post.find_by "published_at < ?", 2.weeks.ago
+ def find_by(*args)
+ where(*args).take
+ end
+
+ # Like <tt>find_by</tt>, except that if no record is found, raises
+ # an <tt>ActiveRecord::RecordNotFound</tt> error.
+ def find_by!(*args)
+ where(*args).take!
+ end
+
+ # Gives a record (or N records if a parameter is supplied) without any implied
+ # order. The order will depend on the database implementation.
+ # If an order is supplied it will be respected.
+ #
+ # Person.take # returns an object fetched by SELECT * FROM people
+ # Person.take(5) # returns 5 objects fetched by SELECT * FROM people LIMIT 5
+ # Person.where(["name LIKE '%?'", name]).take
+ def take(limit = nil)
+ limit ? limit(limit).to_a : find_take
+ end
+
+ # Same as +take+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record
+ # is found. Note that <tt>take!</tt> accepts no arguments.
+ def take!
+ take or raise RecordNotFound
+ end
+
+ # Find the first record (or first N records if a parameter is supplied).
+ # If no order is defined it will order by primary key.
+ #
+ # Person.first # returns the first object fetched by SELECT * FROM people
+ # Person.where(["user_name = ?", user_name]).first
+ # Person.where(["user_name = :u", { :u => user_name }]).first
+ # Person.order("created_on DESC").offset(5).first
+ # Person.first(3) # returns the first three objects fetched by SELECT * FROM people LIMIT 3
+ def first(limit = nil)
+ if limit
+ if order_values.empty? && primary_key
+ order(arel_table[primary_key].asc).limit(limit).to_a
else
- apply_finder_options(args.first).first
+ limit(limit).to_a
end
else
find_first
@@ -129,18 +99,27 @@ module ActiveRecord
first or raise RecordNotFound
end
- # A convenience wrapper for <tt>find(:last, *args)</tt>. You can pass in all the
- # same arguments to this method as you can to <tt>find(:last)</tt>.
- def last(*args)
- if args.any?
- if args.first.kind_of?(Integer) || (loaded? && !args.first.kind_of?(Hash))
- if order_values.empty?
- order("#{primary_key} DESC").limit(*args).reverse
- else
- to_a.last(*args)
- end
+ # Find the last record (or last N records if a parameter is supplied).
+ # If no order is defined it will order by primary key.
+ #
+ # Person.last # returns the last object fetched by SELECT * FROM people
+ # Person.where(["user_name = ?", user_name]).last
+ # Person.order("created_on DESC").offset(5).last
+ # Person.last(3) # returns the last three objects fetched by SELECT * FROM people.
+ #
+ # Take note that in that last case, the results are sorted in ascending order:
+ #
+ # [#<Person id:2>, #<Person id:3>, #<Person id:4>]
+ #
+ # and not:
+ #
+ # [#<Person id:4>, #<Person id:3>, #<Person id:2>]
+ def last(limit = nil)
+ if limit
+ if order_values.empty? && primary_key
+ order(arel_table[primary_key].desc).limit(limit).reverse
else
- apply_finder_options(args.first).last
+ to_a.last(limit)
end
else
find_last
@@ -153,14 +132,8 @@ module ActiveRecord
last or raise RecordNotFound
end
- # A convenience wrapper for <tt>find(:all, *args)</tt>. You can pass in all the
- # same arguments to this method as you can to <tt>find(:all)</tt>.
- def all(*args)
- args.any? ? apply_finder_options(args.first).to_a : to_a
- end
-
- # Returns true if a record exists in the table that matches the +id+ or
- # conditions given, or false otherwise. The argument can take five forms:
+ # Returns +true+ if a record exists in the table that matches the +id+ or
+ # conditions given, or +false+ otherwise. The argument can take six forms:
#
# * Integer - Finds the record with this primary key.
# * String - Finds the record with a primary key corresponding to this
@@ -168,8 +141,9 @@ module ActiveRecord
# * Array - Finds the record that matches these +find+-style conditions
# (such as <tt>['color = ?', 'red']</tt>).
# * Hash - Finds the record that matches these +find+-style conditions
- # (such as <tt>{:color => 'red'}</tt>).
- # * No args - Returns false if the table is empty, true otherwise.
+ # (such as <tt>{color: 'red'}</tt>).
+ # * +false+ - Returns always +false+.
+ # * No args - Returns +false+ if the table is empty, +true+ otherwise.
#
# For more information about specifying conditions as a Hash or Array,
# see the Conditions section in the introduction to ActiveRecord::Base.
@@ -178,29 +152,30 @@ module ActiveRecord
# 'Jamie'</tt>), since it would be sanitized and then queried against
# the primary key column, like <tt>id = 'name = \'Jamie\''</tt>.
#
- # ==== Examples
# Person.exists?(5)
# Person.exists?('5')
- # Person.exists?(:name => "David")
# Person.exists?(['name LIKE ?', "%#{query}%"])
+ # Person.exists?(name: 'David')
+ # Person.exists?(false)
# Person.exists?
- def exists?(id = false)
- return false if id.nil?
-
- id = id.id if ActiveRecord::Model === id
+ def exists?(conditions = :none)
+ conditions = conditions.id if ActiveRecord::Model === conditions
+ return false if !conditions
join_dependency = construct_join_dependency_for_association_find
relation = construct_relation_for_association_find(join_dependency)
- relation = relation.except(:select, :order).select("1").limit(1)
+ relation = relation.except(:select, :order).select("1 AS one").limit(1)
- case id
+ case conditions
when Array, Hash
- relation = relation.where(id)
+ relation = relation.where(conditions)
else
- relation = relation.where(table[primary_key].eq(id)) if id
+ relation = relation.where(table[primary_key].eq(conditions)) if conditions != :none
end
- connection.select_value(relation, "#{name} Exists") ? true : false
+ connection.select_value(relation, "#{name} Exists", relation.bind_values)
+ rescue ThrowResult
+ false
end
protected
@@ -215,12 +190,12 @@ module ActiveRecord
end
def construct_join_dependency_for_association_find
- including = (@eager_load_values + @includes_values).uniq
+ including = (eager_load_values + includes_values).uniq
ActiveRecord::Associations::JoinDependency.new(@klass, including, [])
end
def construct_relation_for_association_calculations
- including = (@eager_load_values + @includes_values).uniq
+ 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)
@@ -258,44 +233,6 @@ module ActiveRecord
ids_array.empty? ? raise(ThrowResult) : table[primary_key].in(ids_array)
end
- def find_by_attributes(match, attributes, *args)
- conditions = Hash[attributes.map {|a| [a, args[attributes.index(a)]]}]
- result = where(conditions).send(match.finder)
-
- if match.bang? && result.blank?
- raise RecordNotFound, "Couldn't find #{@klass.name} with #{conditions.to_a.collect {|p| p.join(' = ')}.join(', ')}"
- else
- yield(result) if block_given?
- result
- end
- end
-
- def find_or_instantiator_by_attributes(match, attributes, *args)
- options = args.size > 1 && args.last(2).all?{ |a| a.is_a?(Hash) } ? args.extract_options! : {}
- protected_attributes_for_create, unprotected_attributes_for_create = {}, {}
- args.each_with_index do |arg, i|
- if arg.is_a?(Hash)
- protected_attributes_for_create = args[i].with_indifferent_access
- else
- unprotected_attributes_for_create[attributes[i]] = args[i]
- end
- end
-
- conditions = (protected_attributes_for_create.merge(unprotected_attributes_for_create)).slice(*attributes).symbolize_keys
-
- record = where(conditions).first
-
- unless record
- record = @klass.new(protected_attributes_for_create, options) do |r|
- r.assign_attributes(unprotected_attributes_for_create, :without_protection => true)
- end
- yield(record) if block_given?
- record.save if match.instantiator == :create
- end
-
- record
- end
-
def find_with_ids(*ids)
return to_a.find { |*block_args| yield(*block_args) } if block_given?
@@ -318,21 +255,11 @@ module ActiveRecord
def find_one(id)
id = id.id if ActiveRecord::Base === id
- if IdentityMap.enabled? && where_values.blank? &&
- limit_value.blank? && order_values.blank? &&
- includes_values.blank? && preload_values.blank? &&
- readonly_value.nil? && joins_values.blank? &&
- !@klass.locking_enabled? &&
- record = IdentityMap.get(@klass, id)
- return record
- end
-
column = columns_hash[primary_key]
-
- substitute = connection.substitute_at(column, @bind_values.length)
+ substitute = connection.substitute_at(column, bind_values.length)
relation = where(table[primary_key].eq(substitute))
- relation.bind_values = [[column, id]]
- record = relation.first
+ relation.bind_values += [[column, id]]
+ record = relation.take
unless record
conditions = arel.where_sql
@@ -344,18 +271,18 @@ module ActiveRecord
end
def find_some(ids)
- result = where(table[primary_key].in(ids)).all
+ result = where(table[primary_key].in(ids)).to_a
expected_size =
- if @limit_value && ids.size > @limit_value
- @limit_value
+ if limit_value && ids.size > limit_value
+ limit_value
else
ids.size
end
# 11 ids with limit 3, offset 9 should give 2 results.
- if @offset_value && (ids.size - @offset_value < expected_size)
- expected_size = ids.size - @offset_value
+ if offset_value && (ids.size - offset_value < expected_size)
+ expected_size = ids.size - offset_value
end
if result.size == expected_size
@@ -370,11 +297,24 @@ module ActiveRecord
end
end
+ def find_take
+ if loaded?
+ @records.first
+ else
+ @take ||= limit(1).to_a.first
+ end
+ end
+
def find_first
if loaded?
@records.first
else
- @first ||= limit(1).to_a[0]
+ @first ||=
+ if with_default_scope.order_values.empty? && primary_key
+ order(arel_table[primary_key].asc).limit(1).to_a.first
+ else
+ limit(1).to_a.first
+ end
end
end
@@ -386,7 +326,7 @@ module ActiveRecord
if offset_value || limit_value
to_a.last
else
- reverse_order.limit(1).to_a[0]
+ reverse_order.limit(1).to_a.first
end
end
end
diff --git a/activerecord/lib/active_record/relation/merger.rb b/activerecord/lib/active_record/relation/merger.rb
new file mode 100644
index 0000000000..e5b50673da
--- /dev/null
+++ b/activerecord/lib/active_record/relation/merger.rb
@@ -0,0 +1,119 @@
+require 'active_support/core_ext/hash/keys'
+
+module ActiveRecord
+ class Relation
+ class HashMerger # :nodoc:
+ attr_reader :relation, :hash
+
+ def initialize(relation, hash)
+ hash.assert_valid_keys(*Relation::VALUE_METHODS)
+
+ @relation = relation
+ @hash = hash
+ end
+
+ def merge
+ Merger.new(relation, other).merge
+ end
+
+ # Applying values to a relation has some side effects. E.g.
+ # interpolation might take place for where values. So we should
+ # build a relation to merge in rather than directly merging
+ # the values.
+ def other
+ other = Relation.new(relation.klass, relation.table)
+ hash.each { |k, v| other.send("#{k}!", v) }
+ other
+ end
+ end
+
+ class Merger # :nodoc:
+ attr_reader :relation, :values
+
+ def initialize(relation, other)
+ if other.default_scoped? && other.klass != relation.klass
+ other = other.with_default_scope
+ end
+
+ @relation = relation
+ @values = other.values
+ end
+
+ def normal_values
+ Relation::SINGLE_VALUE_METHODS +
+ Relation::MULTI_VALUE_METHODS -
+ [:where, :order, :bind, :reverse_order, :lock, :create_with, :reordering, :from]
+ end
+
+ def merge
+ normal_values.each do |name|
+ value = values[name]
+ relation.send("#{name}!", value) unless value.blank?
+ end
+
+ merge_multi_values
+ merge_single_values
+
+ relation
+ end
+
+ private
+
+ def merge_multi_values
+ relation.where_values = merged_wheres
+ relation.bind_values = merged_binds
+
+ if values[:reordering]
+ # override any order specified in the original relation
+ relation.reorder! values[:order]
+ elsif values[:order]
+ # merge in order_values from r
+ relation.order! values[:order]
+ end
+
+ relation.extend(*values[:extending]) unless values[:extending].blank?
+ end
+
+ def merge_single_values
+ relation.from_value = values[:from] unless relation.from_value
+ relation.lock_value = values[:lock] unless relation.lock_value
+ relation.reverse_order_value = values[:reverse_order]
+
+ unless values[:create_with].blank?
+ relation.create_with_value = (relation.create_with_value || {}).merge(values[:create_with])
+ end
+ end
+
+ def merged_binds
+ if values[:bind]
+ (relation.bind_values + values[:bind]).uniq(&:first)
+ else
+ relation.bind_values
+ end
+ end
+
+ def merged_wheres
+ if values[:where]
+ merged_wheres = relation.where_values + values[:where]
+
+ unless relation.where_values.empty?
+ # Remove equalities with duplicated left-hand. Last one wins.
+ seen = {}
+ merged_wheres = merged_wheres.reverse.reject { |w|
+ nuke = false
+ if w.respond_to?(:operator) && w.operator == :==
+ nuke = seen[w.left]
+ seen[w.left] = true
+ end
+ nuke
+ }.reverse
+ end
+
+ merged_wheres
+ else
+ relation.where_values
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb
index 1d04e763f6..71030cb5d7 100644
--- a/activerecord/lib/active_record/relation/predicate_builder.rb
+++ b/activerecord/lib/active_record/relation/predicate_builder.rb
@@ -1,66 +1,97 @@
module ActiveRecord
class PredicateBuilder # :nodoc:
- def self.build_from_hash(engine, attributes, default_table)
- predicates = attributes.map do |column, value|
+ def self.build_from_hash(klass, attributes, default_table)
+ queries = []
+
+ attributes.each do |column, value|
table = default_table
if value.is_a?(Hash)
- table = Arel::Table.new(column, engine)
- build_from_hash(engine, value, table)
+ table = Arel::Table.new(column, default_table.engine)
+ association = klass.reflect_on_association(column.to_sym)
+
+ if value.empty?
+ queries.concat ['1 = 2']
+ else
+ value.each do |k, v|
+ queries.concat expand(association && association.klass, table, k, v)
+ end
+ end
else
column = column.to_s
if column.include?('.')
table_name, column = column.split('.', 2)
- table = Arel::Table.new(table_name, engine)
+ table = Arel::Table.new(table_name, default_table.engine)
end
- build(table[column.to_sym], value)
+ queries.concat expand(klass, table, column, value)
+ end
+ end
+
+ queries
+ end
+
+ def self.expand(klass, table, column, value)
+ queries = []
+
+ # Find the foreign key when using queries such as:
+ # Post.where(:author => author)
+ #
+ # For polymorphic relationships, find the foreign key and type:
+ # PriceEstimate.where(:estimate_of => treasure)
+ if klass && value.class < ActiveRecord::Tag && reflection = klass.reflect_on_association(column.to_sym)
+ if reflection.polymorphic?
+ queries << build(table[reflection.foreign_type], value.class.base_class)
end
+
+ column = reflection.foreign_key
end
- predicates.flatten
+
+ queries << build(table[column.to_sym], value)
+ queries
end
def self.references(attributes)
- references = attributes.map do |key, value|
+ attributes.map do |key, value|
if value.is_a?(Hash)
key
else
key = key.to_s
key.split('.').first.to_sym if key.include?('.')
end
- end
- references.compact
+ end.compact
end
private
def self.build(attribute, value)
case value
- when ActiveRecord::Relation
- value = value.select(value.klass.arel_table[value.klass.primary_key]) if value.select_values.empty?
- attribute.in(value.arel.ast)
when Array, ActiveRecord::Associations::CollectionProxy
values = value.to_a.map {|x| x.is_a?(ActiveRecord::Model) ? x.id : x}
- ranges, values = values.partition {|v| v.is_a?(Range) || v.is_a?(Arel::Relation)}
+ ranges, values = values.partition {|v| v.is_a?(Range)}
- array_predicates = ranges.map {|range| attribute.in(range)}
-
- if values.include?(nil)
+ values_predicate = if values.include?(nil)
values = values.compact
+
case values.length
when 0
- array_predicates << attribute.eq(nil)
+ attribute.eq(nil)
when 1
- array_predicates << attribute.eq(values.first).or(attribute.eq(nil))
+ attribute.eq(values.first).or(attribute.eq(nil))
else
- array_predicates << attribute.in(values).or(attribute.eq(nil))
+ attribute.in(values).or(attribute.eq(nil))
end
else
- array_predicates << attribute.in(values)
+ attribute.in(values)
end
- array_predicates.inject {|composite, predicate| composite.or(predicate)}
- when Range, Arel::Relation
+ array_predicates = ranges.map { |range| attribute.in(range) }
+ array_predicates << values_predicate
+ array_predicates.inject { |composite, predicate| composite.or(predicate) }
+ when ActiveRecord::Relation
+ value = value.select(value.klass.arel_table[value.klass.primary_key]) if value.select_values.empty?
+ attribute.in(value.arel.ast)
+ when Range
attribute.in(value)
when ActiveRecord::Model
attribute.eq(value.id)
diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb
index 87dd513880..3c59bd8a68 100644
--- a/activerecord/lib/active_record/relation/query_methods.rb
+++ b/activerecord/lib/active_record/relation/query_methods.rb
@@ -1,66 +1,134 @@
require 'active_support/core_ext/array/wrap'
-require 'active_support/core_ext/object/blank'
module ActiveRecord
module QueryMethods
extend ActiveSupport::Concern
- attr_accessor :includes_values, :eager_load_values, :preload_values,
- :select_values, :group_values, :order_values, :joins_values,
- :where_values, :having_values, :bind_values,
- :limit_value, :offset_value, :lock_value, :readonly_value, :create_with_value,
- :from_value, :reordering_value, :reverse_order_value,
- :uniq_value, :references_values
+ Relation::MULTI_VALUE_METHODS.each do |name|
+ class_eval <<-CODE, __FILE__, __LINE__ + 1
+ def #{name}_values # def select_values
+ @values[:#{name}] || [] # @values[:select] || []
+ end # end
+ #
+ def #{name}_values=(values) # def select_values=(values)
+ raise ImmutableRelation if @loaded # raise ImmutableRelation if @loaded
+ @values[:#{name}] = values # @values[:select] = values
+ end # end
+ CODE
+ end
+
+ (Relation::SINGLE_VALUE_METHODS - [:create_with]).each do |name|
+ class_eval <<-CODE, __FILE__, __LINE__ + 1
+ def #{name}_value # def readonly_value
+ @values[:#{name}] # @values[:readonly]
+ end # end
+ CODE
+ end
+
+ Relation::SINGLE_VALUE_METHODS.each do |name|
+ class_eval <<-CODE, __FILE__, __LINE__ + 1
+ def #{name}_value=(value) # def readonly_value=(value)
+ raise ImmutableRelation if @loaded # raise ImmutableRelation if @loaded
+ @values[:#{name}] = value # @values[:readonly] = value
+ end # end
+ CODE
+ end
+ def create_with_value # :nodoc:
+ @values[:create_with] || {}
+ end
+
+ alias extensions extending_values
+
+ # Specify relationships to be included in the result set. For
+ # example:
+ #
+ # users = User.includes(:address)
+ # users.each do |user|
+ # user.address.city
+ # end
+ #
+ # allows you to access the +address+ attribute of the +User+ model without
+ # firing an additional query. This will often result in a
+ # performance improvement over a simple +join+.
+ #
+ # === conditions
+ #
+ # If you want to add conditions to your included models you'll have
+ # to explicitly reference them. For example:
+ #
+ # User.includes(:posts).where('posts.name = ?', 'example')
+ #
+ # Will throw an error, but this will work:
+ #
+ # User.includes(:posts).where('posts.name = ?', 'example').references(:posts)
def includes(*args)
- args.reject! {|a| a.blank? }
+ args.empty? ? self : spawn.includes!(*args)
+ end
- return self if args.empty?
+ # Like #includes, but modifies the relation in place.
+ def includes!(*args)
+ args.reject! {|a| a.blank? }
- relation = clone
- relation.includes_values = (relation.includes_values + args).flatten.uniq
- relation
+ self.includes_values = (includes_values + args).flatten.uniq
+ self
end
+ # Forces eager loading by performing a LEFT OUTER JOIN on +args+:
+ #
+ # User.eager_load(:posts)
+ # => SELECT "users"."id" AS t0_r0, "users"."name" AS t0_r1, ...
+ # FROM "users" LEFT OUTER JOIN "posts" ON "posts"."user_id" =
+ # "users"."id"
def eager_load(*args)
- return self if args.blank?
+ args.blank? ? self : spawn.eager_load!(*args)
+ end
- relation = clone
- relation.eager_load_values += args
- relation
+ # Like #eager_load, but modifies relation in place.
+ def eager_load!(*args)
+ self.eager_load_values += args
+ self
end
+ # Allows preloading of +args+, in the same way that +includes+ does:
+ #
+ # User.preload(:posts)
+ # => SELECT "posts".* FROM "posts" WHERE "posts"."user_id" IN (1, 2, 3)
def preload(*args)
- return self if args.blank?
+ args.blank? ? self : spawn.preload!(*args)
+ end
- relation = clone
- relation.preload_values += args
- relation
+ # Like #preload, but modifies relation in place.
+ def preload!(*args)
+ self.preload_values += args
+ self
end
# Used to indicate that an association is referenced by an SQL string, and should
# therefore be JOINed in any query rather than loaded separately.
#
- # For example:
- #
# User.includes(:posts).where("posts.name = 'foo'")
# # => Doesn't JOIN the posts table, resulting in an error.
#
# User.includes(:posts).where("posts.name = 'foo'").references(:posts)
# # => Query now knows the string references posts, so adds a JOIN
def references(*args)
- return self if args.blank?
+ args.blank? ? self : spawn.references!(*args)
+ end
- relation = clone
- relation.references_values = (references_values + args.flatten.map(&:to_s)).uniq
- relation
+ # Like #references, but modifies relation in place.
+ def references!(*args)
+ args.flatten!
+
+ self.references_values = (references_values + args.map!(&:to_s)).uniq
+ self
end
# Works in two unique ways.
#
# First: takes a block so it can be used just like Array#select.
#
- # Model.scoped.select { |m| m.field == value }
+ # Model.all.select { |m| m.field == value }
#
# This will build an array of objects from the database for the scope,
# converting them into an array and iterating through them using Array#select.
@@ -68,8 +136,8 @@ module ActiveRecord
# Second: Modifies the SELECT statement for the query so that only certain
# fields are retrieved:
#
- # >> Model.select(:field)
- # => [#<Model field:value>]
+ # Model.select(:field)
+ # # => [#<Model field:value>]
#
# Although in the above example it looks as though this method returns an
# array, it actually returns a relation object and can have other query
@@ -77,44 +145,77 @@ module ActiveRecord
#
# The argument to the method can also be an array of fields.
#
- # >> Model.select([:field, :other_field, :and_one_more])
- # => [#<Model field: "value", other_field: "value", and_one_more: "value">]
+ # Model.select(:field, :other_field, :and_one_more)
+ # # => [#<Model field: "value", other_field: "value", and_one_more: "value">]
#
# Accessing attributes of an object that do not have fields retrieved by a select
# will throw <tt>ActiveModel::MissingAttributeError</tt>:
#
- # >> Model.select(:field).first.other_field
- # => ActiveModel::MissingAttributeError: missing attribute: other_field
- def select(value = Proc.new)
+ # Model.select(:field).first.other_field
+ # # => ActiveModel::MissingAttributeError: missing attribute: other_field
+ def select(*fields)
if block_given?
- to_a.select {|*block_args| value.call(*block_args) }
+ to_a.select { |*block_args| yield(*block_args) }
else
- relation = clone
- relation.select_values += Array.wrap(value)
- relation
+ raise ArgumentError, 'Call this with at least one field' if fields.empty?
+ spawn.select!(*fields)
end
end
+ # Like #select, but modifies relation in place.
+ def select!(*fields)
+ self.select_values += fields.flatten
+ self
+ end
+
+ # Allows to specify a group attribute:
+ #
+ # User.group(:name)
+ # => SELECT "users".* FROM "users" GROUP BY name
+ #
+ # Returns an array with distinct records based on the +group+ attribute:
+ #
+ # User.select([:id, :name])
+ # => [#<User id: 1, name: "Oscar">, #<User id: 2, name: "Oscar">, #<User id: 3, name: "Foo">
+ #
+ # User.group(:name)
+ # => [#<User id: 3, name: "Foo", ...>, #<User id: 2, name: "Oscar", ...>]
def group(*args)
- return self if args.blank?
+ args.blank? ? self : spawn.group!(*args)
+ end
+
+ # Like #group, but modifies relation in place.
+ def group!(*args)
+ args.flatten!
- relation = clone
- relation.group_values += args.flatten
- relation
+ self.group_values += args
+ self
end
+ # Allows to specify an order attribute:
+ #
+ # User.order('name')
+ # => SELECT "users".* FROM "users" ORDER BY name
+ #
+ # User.order('name DESC')
+ # => SELECT "users".* FROM "users" ORDER BY name DESC
+ #
+ # User.order('name DESC, email')
+ # => SELECT "users".* FROM "users" ORDER BY name DESC, email
def order(*args)
- return self if args.blank?
+ args.blank? ? self : spawn.order!(*args)
+ end
+
+ # Like #order, but modifies relation in place.
+ def order!(*args)
+ args.flatten!
- args = args.flatten
references = args.reject { |arg| Arel::Node === arg }
- .map { |arg| arg =~ /^([a-zA-Z]\w*)\.(\w+)/ && $1 }
- .compact
+ references.map! { |arg| arg =~ /^([a-zA-Z]\w*)\.(\w+)/ && $1 }.compact!
+ references!(references) if references.any?
- relation = clone
- relation = relation.references(references) if references.any?
- relation.order_values += args
- relation
+ self.order_values = args + self.order_values
+ self
end
# Replaces any existing order defined on the relation with the specified order.
@@ -125,83 +226,238 @@ module ActiveRecord
#
# User.order('email DESC').reorder('id ASC').order('name ASC')
#
- # generates a query with 'ORDER BY id ASC, name ASC'.
- #
+ # generates a query with 'ORDER BY name ASC, id ASC'.
def reorder(*args)
- return self if args.blank?
+ args.blank? ? self : spawn.reorder!(*args)
+ end
+
+ # Like #reorder, but modifies relation in place.
+ def reorder!(*args)
+ args.flatten!
- relation = clone
- relation.reordering_value = true
- relation.order_values = args.flatten
- relation
+ self.reordering_value = true
+ self.order_values = args
+ self
end
+ # Performs a joins on +args+:
+ #
+ # User.joins(:posts)
+ # => SELECT "users".* FROM "users" INNER JOIN "posts" ON "posts"."user_id" = "users"."id"
def joins(*args)
- return self if args.compact.blank?
-
- relation = clone
+ args.compact.blank? ? self : spawn.joins!(*args)
+ end
+ # Like #joins, but modifies relation in place.
+ def joins!(*args)
args.flatten!
- relation.joins_values += args
- relation
+ self.joins_values += args
+ self
end
def bind(value)
- relation = clone
- relation.bind_values += [value]
- relation
+ spawn.bind!(value)
+ end
+
+ def bind!(value)
+ self.bind_values += [value]
+ self
end
+ # Returns a new relation, which is the result of filtering the current relation
+ # according to the conditions in the arguments.
+ #
+ # #where accepts conditions in one of several formats. In the examples below, the resulting
+ # SQL is given as an illustration; the actual query generated may be different depending
+ # on the database adapter.
+ #
+ # === string
+ #
+ # A single string, without additional arguments, is passed to the query
+ # constructor as a SQL fragment, and used in the where clause of the query.
+ #
+ # Client.where("orders_count = '2'")
+ # # SELECT * from clients where orders_count = '2';
+ #
+ # Note that building your own string from user input may expose your application
+ # to injection attacks if not done properly. As an alternative, it is recommended
+ # to use one of the following methods.
+ #
+ # === array
+ #
+ # If an array is passed, then the first element of the array is treated as a template, and
+ # the remaining elements are inserted into the template to generate the condition.
+ # Active Record takes care of building the query to avoid injection attacks, and will
+ # convert from the ruby type to the database type where needed. Elements are inserted
+ # into the string in the order in which they appear.
+ #
+ # User.where(["name = ? and email = ?", "Joe", "joe@example.com"])
+ # # SELECT * FROM users WHERE name = 'Joe' AND email = 'joe@example.com';
+ #
+ # Alternatively, you can use named placeholders in the template, and pass a hash as the
+ # second element of the array. The names in the template are replaced with the corresponding
+ # values from the hash.
+ #
+ # User.where(["name = :name and email = :email", { name: "Joe", email: "joe@example.com" }])
+ # # SELECT * FROM users WHERE name = 'Joe' AND email = 'joe@example.com';
+ #
+ # This can make for more readable code in complex queries.
+ #
+ # Lastly, you can use sprintf-style % escapes in the template. This works slightly differently
+ # than the previous methods; you are responsible for ensuring that the values in the template
+ # are properly quoted. The values are passed to the connector for quoting, but the caller
+ # is responsible for ensuring they are enclosed in quotes in the resulting SQL. After quoting,
+ # the values are inserted using the same escapes as the Ruby core method <tt>Kernel::sprintf</tt>.
+ #
+ # User.where(["name = '%s' and email = '%s'", "Joe", "joe@example.com"])
+ # # SELECT * FROM users WHERE name = 'Joe' AND email = 'joe@example.com';
+ #
+ # If #where is called with multiple arguments, these are treated as if they were passed as
+ # the elements of a single array.
+ #
+ # User.where("name = :name and email = :email", { name: "Joe", email: "joe@example.com" })
+ # # SELECT * FROM users WHERE name = 'Joe' AND email = 'joe@example.com';
+ #
+ # When using strings to specify conditions, you can use any operator available from
+ # the database. While this provides the most flexibility, you can also unintentionally introduce
+ # dependencies on the underlying database. If your code is intended for general consumption,
+ # test with multiple database backends.
+ #
+ # === hash
+ #
+ # #where will also accept a hash condition, in which the keys are fields and the values
+ # are values to be searched for.
+ #
+ # Fields can be symbols or strings. Values can be single values, arrays, or ranges.
+ #
+ # User.where({ name: "Joe", email: "joe@example.com" })
+ # # SELECT * FROM users WHERE name = 'Joe' AND email = 'joe@example.com'
+ #
+ # User.where({ name: ["Alice", "Bob"]})
+ # # SELECT * FROM users WHERE name IN ('Alice', 'Bob')
+ #
+ # User.where({ created_at: (Time.now.midnight - 1.day)..Time.now.midnight })
+ # # SELECT * FROM users WHERE (created_at BETWEEN '2012-06-09 07:00:00.000000' AND '2012-06-10 07:00:00.000000')
+ #
+ # In the case of a belongs_to relationship, an association key can be used
+ # to specify the model if an ActiveRecord object is used as the value.
+ #
+ # author = Author.find(1)
+ #
+ # # The following queries will be equivalent:
+ # Post.where(:author => author)
+ # Post.where(:author_id => author)
+ #
+ # This also works with polymorphic belongs_to relationships:
+ #
+ # treasure = Treasure.create(:name => 'gold coins')
+ # treasure.price_estimates << PriceEstimate.create(:price => 125)
+ #
+ # # The following queries will be equivalent:
+ # PriceEstimate.where(:estimate_of => treasure)
+ # PriceEstimate.where(:estimate_of_type => 'Treasure', :estimate_of_id => treasure)
+ #
+ # === Joins
+ #
+ # If the relation is the result of a join, you may create a condition which uses any of the
+ # tables in the join. For string and array conditions, use the table name in the condition.
+ #
+ # User.joins(:posts).where("posts.created_at < ?", Time.now)
+ #
+ # For hash conditions, you can either use the table name in the key, or use a sub-hash.
+ #
+ # User.joins(:posts).where({ "posts.published" => true })
+ # User.joins(:posts).where({ :posts => { :published => true } })
+ #
+ # === empty condition
+ #
+ # If the condition returns true for blank?, then where is a no-op and returns the current relation.
def where(opts, *rest)
- return self if opts.blank?
+ opts.blank? ? self : spawn.where!(opts, *rest)
+ end
- relation = clone
- relation = relation.references(PredicateBuilder.references(opts)) if Hash === opts
- relation.where_values += build_where(opts, rest)
- relation
+ # #where! is identical to #where, except that instead of returning a new relation, it adds
+ # the condition to the existing relation.
+ def where!(opts, *rest)
+ references!(PredicateBuilder.references(opts)) if Hash === opts
+
+ self.where_values += build_where(opts, rest)
+ self
end
+ # Allows to specify a HAVING clause. Note that you can't use HAVING
+ # without also specifying a GROUP clause.
+ #
+ # Order.having('SUM(price) > 30').group('user_id')
def having(opts, *rest)
- return self if opts.blank?
+ opts.blank? ? self : spawn.having!(opts, *rest)
+ end
+
+ # Like #having, but modifies relation in place.
+ def having!(opts, *rest)
+ references!(PredicateBuilder.references(opts)) if Hash === opts
- relation = clone
- relation = relation.references(PredicateBuilder.references(opts)) if Hash === opts
- relation.having_values += build_where(opts, rest)
- relation
+ self.having_values += build_where(opts, rest)
+ self
end
+ # Specifies a limit for the number of records to retrieve.
+ #
+ # User.limit(10) # generated SQL has 'LIMIT 10'
+ #
+ # User.limit(10).limit(20) # generated SQL has 'LIMIT 20'
def limit(value)
- relation = clone
- relation.limit_value = value
- relation
+ spawn.limit!(value)
end
+ # Like #limit, but modifies relation in place.
+ def limit!(value)
+ self.limit_value = value
+ self
+ end
+
+ # Specifies the number of rows to skip before returning rows.
+ #
+ # User.offset(10) # generated SQL has "OFFSET 10"
+ #
+ # Should be used with order.
+ #
+ # User.offset(10).order("name ASC")
def offset(value)
- relation = clone
- relation.offset_value = value
- relation
+ spawn.offset!(value)
+ end
+
+ # Like #offset, but modifies relation in place.
+ def offset!(value)
+ self.offset_value = value
+ self
end
+ # Specifies locking settings (default to +true+). For more information
+ # on locking, please see +ActiveRecord::Locking+.
def lock(locks = true)
- relation = clone
+ spawn.lock!(locks)
+ end
+ # Like #lock, but modifies relation in place.
+ def lock!(locks = true)
case locks
when String, TrueClass, NilClass
- relation.lock_value = locks || true
+ self.lock_value = locks || true
else
- relation.lock_value = false
+ self.lock_value = false
end
- relation
+ self
end
# Returns a chainable relation with zero records, specifically an
- # instance of the NullRelation class.
+ # instance of the <tt>ActiveRecord::NullRelation</tt> class.
#
- # The returned NullRelation inherits from Relation and implements the
- # Null Object pattern so it is an object with defined null behavior:
- # it always returns an empty array of records and does not query the database.
+ # The returned <tt>ActiveRecord::NullRelation</tt> inherits from Relation and implements the
+ # Null Object pattern. It is an object with defined null behavior and always returns an empty
+ # array of records without quering the database.
#
# Any subsequent condition chained to the returned relation will continue
# generating an empty relation and will not fire any query to the database.
@@ -226,25 +482,73 @@ module ActiveRecord
# end
#
def none
- NullRelation.new(@klass, @table)
+ extending(NullRelation)
end
+ # Sets readonly attributes for the returned relation. If value is
+ # true (default), attempting to update a record will result in an error.
+ #
+ # users = User.readonly
+ # users.first.save
+ # => ActiveRecord::ReadOnlyRecord: ActiveRecord::ReadOnlyRecord
def readonly(value = true)
- relation = clone
- relation.readonly_value = value
- relation
+ spawn.readonly!(value)
+ end
+
+ # Like #readonly, but modifies relation in place.
+ def readonly!(value = true)
+ self.readonly_value = value
+ self
end
+ # Sets attributes to be used when creating new records from a
+ # relation object.
+ #
+ # users = User.where(name: 'Oscar')
+ # users.new.name # => 'Oscar'
+ #
+ # users = users.create_with(name: 'DHH')
+ # users.new.name # => 'DHH'
+ #
+ # You can pass +nil+ to +create_with+ to reset attributes:
+ #
+ # users = users.create_with(nil)
+ # users.new.name # => 'Oscar'
def create_with(value)
- relation = clone
- relation.create_with_value = value ? create_with_value.merge(value) : {}
- relation
+ spawn.create_with!(value)
+ end
+
+ # Like #create_with but modifies the relation in place. Raises
+ # +ImmutableRelation+ if the relation has already been loaded.
+ #
+ # users = User.all.create_with!(name: 'Oscar')
+ # users.new.name # => 'Oscar'
+ def create_with!(value)
+ self.create_with_value = value ? create_with_value.merge(value) : {}
+ self
+ end
+
+ # Specifies table from which the records will be fetched. For example:
+ #
+ # Topic.select('title').from('posts')
+ # #=> SELECT title FROM posts
+ #
+ # Can accept other relation objects. For example:
+ #
+ # Topic.select('title').from(Topic.approved)
+ # # => SELECT title FROM (SELECT * FROM topics WHERE approved = 't') subquery
+ #
+ # Topic.select('a.title').from(Topic.approved, :a)
+ # # => SELECT a.title FROM (SELECT * FROM topics WHERE approved = 't') a
+ #
+ def from(value, subquery_name = nil)
+ spawn.from!(value, subquery_name)
end
- def from(value)
- relation = clone
- relation.from_value = value
- relation
+ # Like #from, but modifies relation in place.
+ def from!(value, subquery_name = nil)
+ self.from_value = [value, subquery_name]
+ self
end
# Specifies whether the records should be unique or not. For example:
@@ -258,9 +562,13 @@ module ActiveRecord
# User.select(:name).uniq.uniq(false)
# # => You can also remove the uniqueness
def uniq(value = true)
- relation = clone
- relation.uniq_value = value
- relation
+ spawn.uniq!(value)
+ end
+
+ # Like #uniq, but modifies relation in place.
+ def uniq!(value = true)
+ self.uniq_value = value
+ self
end
# Used to extend a scope with additional methods, either through
@@ -276,16 +584,16 @@ module ActiveRecord
# end
# end
#
- # scope = Model.scoped.extending(Pagination)
+ # scope = Model.all.extending(Pagination)
# scope.page(params[:page])
#
# You can also pass a list of modules:
#
- # scope = Model.scoped.extending(Pagination, SomethingElse)
+ # scope = Model.all.extending(Pagination, SomethingElse)
#
# === Using a block
#
- # scope = Model.scoped.extending do
+ # scope = Model.all.extending do
# def page(number)
# # pagination code goes here
# end
@@ -294,54 +602,71 @@ module ActiveRecord
#
# You can also use a block and a module list:
#
- # scope = Model.scoped.extending(Pagination) do
+ # scope = Model.all.extending(Pagination) do
# def per_page(number)
# # pagination code goes here
# end
# end
- def extending(*modules)
- modules << Module.new(&Proc.new) if block_given?
+ def extending(*modules, &block)
+ if modules.any? || block
+ spawn.extending!(*modules, &block)
+ else
+ self
+ end
+ end
- return self if modules.empty?
+ # Like #extending, but modifies relation in place.
+ def extending!(*modules, &block)
+ modules << Module.new(&block) if block_given?
- relation = clone
- relation.send(:apply_modules, modules.flatten)
- relation
+ self.extending_values += modules.flatten
+ extend(*extending_values) if extending_values.any?
+
+ self
end
+ # Reverse the existing order clause on the relation.
+ #
+ # User.order('name ASC').reverse_order # generated SQL has 'ORDER BY name DESC'
def reverse_order
- relation = clone
- relation.reverse_order_value = !relation.reverse_order_value
- relation
+ spawn.reverse_order!
+ end
+
+ # Like #reverse_order, but modifies relation in place.
+ def reverse_order!
+ self.reverse_order_value = !reverse_order_value
+ self
end
+ # Returns the Arel object associated with the relation.
def arel
@arel ||= with_default_scope.build_arel
end
+ # Like #arel, but ignores the default scope of the model.
def build_arel
- arel = table.from table
+ arel = Arel::SelectManager.new(table.engine, table)
- build_joins(arel, @joins_values) unless @joins_values.empty?
+ build_joins(arel, joins_values) unless joins_values.empty?
- collapse_wheres(arel, (@where_values - ['']).uniq)
+ collapse_wheres(arel, (where_values - ['']).uniq)
- arel.having(*@having_values.uniq.reject{|h| h.blank?}) unless @having_values.empty?
+ arel.having(*having_values.uniq.reject{|h| h.blank?}) unless having_values.empty?
- arel.take(connection.sanitize_limit(@limit_value)) if @limit_value
- arel.skip(@offset_value) if @offset_value
+ arel.take(connection.sanitize_limit(limit_value)) if limit_value
+ arel.skip(offset_value.to_i) if offset_value
- arel.group(*@group_values.uniq.reject{|g| g.blank?}) unless @group_values.empty?
+ arel.group(*group_values.uniq.reject{|g| g.blank?}) unless group_values.empty?
- order = @order_values
- order = reverse_sql_order(order) if @reverse_order_value
+ order = order_values
+ order = reverse_sql_order(order) if reverse_order_value
arel.order(*order.uniq.reject{|o| o.blank?}) unless order.empty?
- build_select(arel, @select_values.uniq)
+ build_select(arel, select_values.uniq)
- arel.distinct(@uniq_value)
- arel.from(@from_value) if @from_value
- arel.lock(@lock_value) if @lock_value
+ arel.distinct(uniq_value)
+ arel.from(build_from) if from_value
+ arel.lock(lock_value) if lock_value
arel
end
@@ -383,12 +708,23 @@ module ActiveRecord
[@klass.send(:sanitize_sql, other.empty? ? opts : ([opts] + other))]
when Hash
attributes = @klass.send(:expand_hash_conditions_for_aggregates, opts)
- PredicateBuilder.build_from_hash(table.engine, attributes, table)
+ PredicateBuilder.build_from_hash(klass, attributes, table)
else
[opts]
end
end
+ def build_from
+ opts, name = from_value
+ case opts
+ when Relation
+ name ||= 'subquery'
+ opts.arel.as(name.to_s)
+ else
+ opts
+ end
+ end
+
def build_joins(manager, joins)
buckets = joins.group_by do |join|
case join
@@ -443,13 +779,6 @@ module ActiveRecord
end
end
- def apply_modules(modules)
- unless modules.empty?
- @extensions += modules
- modules.each {|extension| extend(extension) }
- end
- end
-
def reverse_sql_order(order_query)
order_query = ["#{quoted_table_name}.#{quoted_primary_key} ASC"] if order_query.empty?
diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb
index 7131aa29b6..5394c1b28b 100644
--- a/activerecord/lib/active_record/relation/spawn_methods.rb
+++ b/activerecord/lib/active_record/relation/spawn_methods.rb
@@ -1,75 +1,53 @@
-require 'active_support/core_ext/object/blank'
+require 'active_support/core_ext/hash/except'
+require 'active_support/core_ext/hash/slice'
+require 'active_record/relation/merger'
module ActiveRecord
module SpawnMethods
- def merge(r)
- return self unless r
- return to_a & r if r.is_a?(Array)
- merged_relation = clone
-
- r = r.with_default_scope if r.default_scoped? && r.klass != klass
-
- Relation::ASSOCIATION_METHODS.each do |method|
- value = r.send(:"#{method}_values")
-
- unless value.empty?
- if method == :includes
- merged_relation = merged_relation.includes(value)
- else
- merged_relation.send(:"#{method}_values=", value)
- end
- end
- end
-
- (Relation::MULTI_VALUE_METHODS - [:joins, :where, :order]).each do |method|
- value = r.send(:"#{method}_values")
- merged_relation.send(:"#{method}_values=", merged_relation.send(:"#{method}_values") + value) if value.present?
- end
-
- merged_relation.joins_values += r.joins_values
-
- merged_wheres = @where_values + r.where_values
-
- unless @where_values.empty?
- # Remove duplicates, last one wins.
- seen = Hash.new { |h,table| h[table] = {} }
- merged_wheres = merged_wheres.reverse.reject { |w|
- nuke = false
- if w.respond_to?(:operator) && w.operator == :==
- name = w.left.name
- table = w.left.relation.name
- nuke = seen[table][name]
- seen[table][name] = true
- end
- nuke
- }.reverse
- end
-
- merged_relation.where_values = merged_wheres
+ # This is overridden by Associations::CollectionProxy
+ def spawn #:nodoc:
+ clone
+ end
- (Relation::SINGLE_VALUE_METHODS - [:lock, :create_with, :reordering]).each do |method|
- value = r.send(:"#{method}_value")
- merged_relation.send(:"#{method}_value=", value) unless value.nil?
+ # Merges in the conditions from <tt>other</tt>, if <tt>other</tt> is an <tt>ActiveRecord::Relation</tt>.
+ # Returns an array representing the intersection of the resulting records with <tt>other</tt>, if <tt>other</tt> is an array.
+ #
+ # ==== Examples
+ #
+ # Post.where(:published => true).joins(:comments).merge( Comment.where(:spam => false) )
+ # # Performs a single join query with both where conditions.
+ #
+ # recent_posts = Post.order('created_at DESC').first(5)
+ # Post.where(:published => true).merge(recent_posts)
+ # # Returns the intersection of all published posts with the 5 most recently created posts.
+ # # (This is just an example. You'd probably want to do this with a single query!)
+ #
+ # Procs will be evaluated by merge:
+ #
+ # Post.where(published: true).merge(-> { joins(:comments) })
+ # # => Post.where(published: true).joins(:comments)
+ #
+ # This is mainly intended for sharing common conditions between multiple associations.
+ #
+ def merge(other)
+ if other.is_a?(Array)
+ to_a & other
+ elsif other
+ spawn.merge!(other)
+ else
+ self
end
+ end
- merged_relation.lock_value = r.lock_value unless merged_relation.lock_value
-
- merged_relation = merged_relation.create_with(r.create_with_value) unless r.create_with_value.empty?
-
- if (r.reordering_value)
- # override any order specified in the original relation
- merged_relation.reordering_value = true
- merged_relation.order_values = r.order_values
+ # Like #merge, but applies changes in place.
+ def merge!(other)
+ if !other.is_a?(Relation) && other.respond_to?(:to_proc)
+ instance_exec(&other)
else
- # merge in order_values from r
- merged_relation.order_values += r.order_values
+ klass = other.is_a?(Hash) ? Relation::HashMerger : Relation::Merger
+ klass.new(self, other).merge
end
-
- # Apply scope extension modules
- merged_relation.send :apply_modules, r.extensions
-
- merged_relation
end
# Removes from the query the condition(s) specified in +skips+.
@@ -80,20 +58,9 @@ module ActiveRecord
# Post.where('id > 10').order('id asc').except(:where) # discards the where condition but keeps the order
#
def except(*skips)
- result = self.class.new(@klass, table)
+ result = Relation.new(klass, table, values.except(*skips))
result.default_scoped = default_scoped
-
- ((Relation::ASSOCIATION_METHODS + Relation::MULTI_VALUE_METHODS) - skips).each do |method|
- result.send(:"#{method}_values=", send(:"#{method}_values"))
- end
-
- (Relation::SINGLE_VALUE_METHODS - skips).each do |method|
- result.send(:"#{method}_value=", send(:"#{method}_value"))
- end
-
- # Apply scope extension modules
- result.send(:apply_modules, extensions)
-
+ result.extend(*extending_values) if extending_values.any?
result
end
@@ -105,44 +72,11 @@ module ActiveRecord
# Post.order('id asc').only(:where, :order) # uses the specified order
#
def only(*onlies)
- result = self.class.new(@klass, table)
+ result = Relation.new(klass, table, values.slice(*onlies))
result.default_scoped = default_scoped
-
- ((Relation::ASSOCIATION_METHODS + Relation::MULTI_VALUE_METHODS) & onlies).each do |method|
- result.send(:"#{method}_values=", send(:"#{method}_values"))
- end
-
- (Relation::SINGLE_VALUE_METHODS & onlies).each do |method|
- result.send(:"#{method}_value=", send(:"#{method}_value"))
- end
-
- # Apply scope extension modules
- result.send(:apply_modules, extensions)
-
+ result.extend(*extending_values) if extending_values.any?
result
end
- VALID_FIND_OPTIONS = [ :conditions, :include, :joins, :limit, :offset, :extend, :references,
- :order, :select, :readonly, :group, :having, :from, :lock ]
-
- def apply_finder_options(options)
- relation = clone
- return relation unless options
-
- options.assert_valid_keys(VALID_FIND_OPTIONS)
- finders = options.dup
- finders.delete_if { |key, value| value.nil? && key != :limit }
-
- ((VALID_FIND_OPTIONS - [:conditions, :include, :extend]) & finders.keys).each do |finder|
- relation = relation.send(finder, finders[finder])
- end
-
- relation = relation.where(finders[:conditions]) if options.has_key?(:conditions)
- relation = relation.includes(finders[:include]) if options.has_key?(:include)
- relation = relation.extending(finders[:extend]) if options.has_key?(:extend)
-
- relation
- end
-
end
end
diff --git a/activerecord/lib/active_record/result.rb b/activerecord/lib/active_record/result.rb
index fb4b89b87b..425b9b41d8 100644
--- a/activerecord/lib/active_record/result.rb
+++ b/activerecord/lib/active_record/result.rb
@@ -10,11 +10,11 @@ module ActiveRecord
attr_reader :columns, :rows, :column_types
- def initialize(columns, rows)
+ def initialize(columns, rows, column_types = {})
@columns = columns
@rows = rows
@hash_rows = nil
- @column_types = {}
+ @column_types = column_types
end
def each
@@ -28,6 +28,7 @@ module ActiveRecord
alias :map! :map
alias :collect! :map
+ # Returns true if there are no records.
def empty?
rows.empty?
end
@@ -52,9 +53,15 @@ module ActiveRecord
private
def hash_rows
- @hash_rows ||= @rows.map { |row|
- Hash[@columns.zip(row)]
- }
+ @hash_rows ||=
+ begin
+ # We freeze the strings to prevent them getting duped when
+ # used as keys in ActiveRecord::Model's @attributes hash
+ columns = @columns.map { |c| c.dup.freeze }
+ @rows.map { |row|
+ Hash[columns.zip(row)]
+ }
+ end
end
end
end
diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb
index 2d7d83d160..42b4cff4b8 100644
--- a/activerecord/lib/active_record/sanitization.rb
+++ b/activerecord/lib/active_record/sanitization.rb
@@ -1,5 +1,3 @@
-require 'active_support/concern'
-
module ActiveRecord
module Sanitization
extend ActiveSupport::Concern
@@ -37,9 +35,9 @@ module ActiveRecord
# { :name => nil, :group_id => 4 } returns "name = NULL , group_id='4'"
def sanitize_sql_for_assignment(assignments)
case assignments
- when Array; sanitize_sql_array(assignments)
- when Hash; sanitize_sql_hash_for_assignment(assignments)
- else assignments
+ when Array; sanitize_sql_array(assignments)
+ when Hash; sanitize_sql_hash_for_assignment(assignments)
+ else assignments
end
end
@@ -57,8 +55,8 @@ module ActiveRecord
def expand_hash_conditions_for_aggregates(attrs)
expanded_attrs = {}
attrs.each do |attr, value|
- unless (aggregation = reflect_on_aggregation(attr.to_sym)).nil?
- mapping = aggregate_mapping(aggregation)
+ if aggregation = reflect_on_aggregation(attr.to_sym)
+ mapping = aggregation.mapping
mapping.each do |field_attr, aggregate_attr|
if mapping.size == 1 && !value.respond_to?(aggregate_attr)
expanded_attrs[field_attr] = value
@@ -90,8 +88,8 @@ module ActiveRecord
def sanitize_sql_hash_for_conditions(attrs, default_table_name = self.table_name)
attrs = expand_hash_conditions_for_aggregates(attrs)
- table = Arel::Table.new(table_name).alias(default_table_name)
- PredicateBuilder.build_from_hash(arel_engine, attrs, table).map { |b|
+ table = Arel::Table.new(table_name, arel_engine).alias(default_table_name)
+ PredicateBuilder.build_from_hash(self.class, attrs, table).map { |b|
connection.visitor.accept b
}.join(' AND ')
end
@@ -180,15 +178,8 @@ module ActiveRecord
end
# TODO: Deprecate this
- def quoted_id #:nodoc:
- quote_value(id, column_for_attribute(self.class.primary_key))
- end
-
- private
-
- # Quote strings appropriately for SQL statements.
- def quote_value(value, column = nil)
- self.class.connection.quote(value, column)
+ def quoted_id
+ self.class.quote_value(id, column_for_attribute(self.class.primary_key))
end
end
end
diff --git a/activerecord/lib/active_record/schema.rb b/activerecord/lib/active_record/schema.rb
index d815ab05ac..eaa4aa7086 100644
--- a/activerecord/lib/active_record/schema.rb
+++ b/activerecord/lib/active_record/schema.rb
@@ -1,4 +1,3 @@
-require 'active_support/core_ext/object/blank'
module ActiveRecord
# = Active Record Schema
@@ -12,16 +11,16 @@ module ActiveRecord
#
# ActiveRecord::Schema.define do
# create_table :authors do |t|
- # t.string :name, :null => false
+ # t.string :name, null: false
# end
#
# add_index :authors, :name, :unique
#
# create_table :posts do |t|
- # t.integer :author_id, :null => false
+ # t.integer :author_id, null: false
# t.string :subject
# t.text :body
- # t.boolean :private, :default => false
+ # t.boolean :private, default: false
# end
#
# add_index :posts, :author_id
@@ -34,6 +33,15 @@ module ActiveRecord
ActiveRecord::Migrator.migrations_paths
end
+ def define(info, &block)
+ instance_eval(&block)
+
+ unless info[:version].blank?
+ initialize_schema_migrations_table
+ assume_migrated_upto_version(info[:version], migrations_paths)
+ end
+ end
+
# Eval the given block. All methods available to the current connection
# adapter are available within the block, so you can easily use the
# database definition DSL to build up your schema (+create_table+,
@@ -42,17 +50,11 @@ module ActiveRecord
# The +info+ hash is optional, and if given is used to define metadata
# about the current schema (currently, only the schema's version):
#
- # ActiveRecord::Schema.define(:version => 20380119000001) do
+ # ActiveRecord::Schema.define(version: 20380119000001) do
# ...
# end
def self.define(info={}, &block)
- schema = new
- schema.instance_eval(&block)
-
- unless info[:version].blank?
- initialize_schema_migrations_table
- assume_migrated_upto_version(info[:version], schema.migrations_paths)
- end
+ new.define(info, &block)
end
end
end
diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb
index dcbd165e58..36bde44e7c 100644
--- a/activerecord/lib/active_record/schema_dumper.rb
+++ b/activerecord/lib/active_record/schema_dumper.rb
@@ -38,9 +38,9 @@ module ActiveRecord
end
def header(stream)
- define_params = @version ? ":version => #{@version}" : ""
+ define_params = @version ? "version: #{@version}" : ""
- if stream.respond_to?(:external_encoding)
+ if stream.respond_to?(:external_encoding) && stream.external_encoding
stream.puts "# encoding: #{stream.external_encoding.name}"
end
@@ -55,7 +55,7 @@ module ActiveRecord
# from scratch. The latter is a flawed and unsustainable approach (the more migrations
# you'll amass, the slower it'll run and the greater likelihood for issues).
#
-# It's strongly recommended to check this file into your version control system.
+# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(#{define_params}) do
@@ -70,8 +70,8 @@ HEADER
@connection.tables.sort.each do |tbl|
next if ['schema_migrations', ignore_tables].flatten.any? do |ignored|
case ignored
- when String; tbl == ignored
- when Regexp; tbl =~ ignored
+ when String; remove_prefix_and_suffix(tbl) == ignored
+ when Regexp; remove_prefix_and_suffix(tbl) =~ ignored
else
raise StandardError, 'ActiveRecord::SchemaDumper.ignore_tables accepts an array of String and / or Regexp values.'
end
@@ -92,42 +92,26 @@ HEADER
pk = @connection.primary_key(table)
end
- tbl.print " create_table #{table.inspect}"
+ tbl.print " create_table #{remove_prefix_and_suffix(table).inspect}"
if columns.detect { |c| c.name == pk }
if pk != 'id'
- tbl.print %Q(, :primary_key => "#{pk}")
+ tbl.print %Q(, primary_key: "#{pk}")
end
else
- tbl.print ", :id => false"
+ tbl.print ", id: false"
end
- tbl.print ", :force => true"
+ tbl.print ", force: true"
tbl.puts " do |t|"
# then dump all non-primary key columns
column_specs = columns.map do |column|
raise StandardError, "Unknown type '#{column.sql_type}' for column '#{column.name}'" if @types[column.type].nil?
next if column.name == pk
- spec = {}
- spec[:name] = column.name.inspect
-
- # AR has an optimization which handles zero-scale decimals as integers. This
- # code ensures that the dumper still dumps the column as a decimal.
- spec[:type] = if column.type == :integer && /^(numeric|decimal)/ =~ column.sql_type
- 'decimal'
- else
- column.type.to_s
- end
- spec[:limit] = column.limit.inspect if column.limit != @types[column.type][:limit] && spec[:type] != 'decimal'
- spec[:precision] = column.precision.inspect if column.precision
- spec[:scale] = column.scale.inspect if column.scale
- spec[:null] = 'false' unless column.null
- spec[:default] = default_string(column.default) if column.has_default?
- (spec.keys - [:name, :type]).each{ |k| spec[k].insert(0, "#{k.inspect} => ")}
- spec
+ @connection.column_spec(column, @types)
end.compact
# find all migration keys used in this table
- keys = [:name, :limit, :precision, :scale, :default, :null]
+ keys = @connection.migration_keys
# figure out the lengths for each column based on above keys
lengths = keys.map { |key|
@@ -170,34 +154,23 @@ HEADER
stream
end
- def default_string(value)
- case value
- when BigDecimal
- value.to_s
- when Date, DateTime, Time
- "'" + value.to_s(:db) + "'"
- else
- value.inspect
- end
- end
-
def indexes(table, stream)
if (indexes = @connection.indexes(table)).any?
add_index_statements = indexes.map do |index|
statement_parts = [
- ('add_index ' + index.table.inspect),
+ ('add_index ' + remove_prefix_and_suffix(index.table).inspect),
index.columns.inspect,
- (':name => ' + index.name.inspect),
+ ('name: ' + index.name.inspect),
]
- statement_parts << ':unique => true' if index.unique
+ statement_parts << 'unique: true' if index.unique
index_lengths = (index.lengths || []).compact
- statement_parts << (':length => ' + Hash[index.columns.zip(index.lengths)].inspect) unless index_lengths.empty?
+ statement_parts << ('length: ' + Hash[index.columns.zip(index.lengths)].inspect) unless index_lengths.empty?
index_orders = (index.orders || {})
- statement_parts << (':order => ' + index.orders.inspect) unless index_orders.empty?
+ statement_parts << ('order: ' + index.orders.inspect) unless index_orders.empty?
- statement_parts << (':where => ' + index.where.inspect) if index.where
+ statement_parts << ('where: ' + index.where.inspect) if index.where
' ' + statement_parts.join(', ')
end
@@ -206,5 +179,9 @@ HEADER
stream.puts
end
end
+
+ def remove_prefix_and_suffix(table)
+ table.gsub(/^(#{ActiveRecord::Base.table_name_prefix})(.+)(#{ActiveRecord::Base.table_name_suffix})$/, "\\2")
+ end
end
end
diff --git a/activerecord/lib/active_record/schema_migration.rb b/activerecord/lib/active_record/schema_migration.rb
index 236ec563d2..9830abe7d8 100644
--- a/activerecord/lib/active_record/schema_migration.rb
+++ b/activerecord/lib/active_record/schema_migration.rb
@@ -4,10 +4,13 @@ require 'active_record/base'
module ActiveRecord
class SchemaMigration < ActiveRecord::Base
- attr_accessible :version
def self.table_name
- Base.table_name_prefix + 'schema_migrations' + Base.table_name_suffix
+ "#{Base.table_name_prefix}schema_migrations#{Base.table_name_suffix}"
+ end
+
+ def self.index_name
+ "#{Base.table_name_prefix}unique_schema_migrations#{Base.table_name_suffix}"
end
def self.create_table
@@ -15,14 +18,13 @@ module ActiveRecord
connection.create_table(table_name, :id => false) do |t|
t.column :version, :string, :null => false
end
- connection.add_index table_name, :version, :unique => true,
- :name => "#{Base.table_name_prefix}unique_schema_migrations#{Base.table_name_suffix}"
+ 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 => "#{Base.table_name_prefix}unique_schema_migrations#{Base.table_name_suffix}"
+ connection.remove_index table_name, :name => index_name
connection.drop_table(table_name)
end
end
diff --git a/activerecord/lib/active_record/scoping.rb b/activerecord/lib/active_record/scoping.rb
index a8f5e96190..0c3fd1bd29 100644
--- a/activerecord/lib/active_record/scoping.rb
+++ b/activerecord/lib/active_record/scoping.rb
@@ -1,4 +1,3 @@
-require 'active_support/concern'
module ActiveRecord
module Scoping
@@ -10,118 +9,6 @@ module ActiveRecord
end
module ClassMethods
- # with_scope lets you apply options to inner block incrementally. It takes a hash and the keys must be
- # <tt>:find</tt> or <tt>:create</tt>. <tt>:find</tt> parameter is <tt>Relation</tt> while
- # <tt>:create</tt> parameters are an attributes hash.
- #
- # class Article < ActiveRecord::Base
- # def self.create_with_scope
- # with_scope(:find => where(:blog_id => 1), :create => { :blog_id => 1 }) do
- # find(1) # => SELECT * from articles WHERE blog_id = 1 AND id = 1
- # a = create(1)
- # a.blog_id # => 1
- # end
- # end
- # end
- #
- # In nested scopings, all previous parameters are overwritten by the innermost rule, with the exception of
- # <tt>where</tt>, <tt>includes</tt>, and <tt>joins</tt> operations in <tt>Relation</tt>, which are merged.
- #
- # <tt>joins</tt> operations are uniqued so multiple scopes can join in the same table without table aliasing
- # problems. If you need to join multiple tables, but still want one of the tables to be uniqued, use the
- # array of strings format for your joins.
- #
- # class Article < ActiveRecord::Base
- # def self.find_with_scope
- # with_scope(:find => where(:blog_id => 1).limit(1), :create => { :blog_id => 1 }) do
- # with_scope(:find => limit(10)) do
- # all # => SELECT * from articles WHERE blog_id = 1 LIMIT 10
- # end
- # with_scope(:find => where(:author_id => 3)) do
- # all # => SELECT * from articles WHERE blog_id = 1 AND author_id = 3 LIMIT 1
- # end
- # end
- # end
- # end
- #
- # You can ignore any previous scopings by using the <tt>with_exclusive_scope</tt> method.
- #
- # class Article < ActiveRecord::Base
- # def self.find_with_exclusive_scope
- # with_scope(:find => where(:blog_id => 1).limit(1)) do
- # with_exclusive_scope(:find => limit(10)) do
- # all # => SELECT * from articles LIMIT 10
- # end
- # end
- # end
- # end
- #
- # *Note*: the +:find+ scope also has effect on update and deletion methods, like +update_all+ and +delete_all+.
- def with_scope(scope = {}, action = :merge, &block)
- # If another Active Record class has been passed in, get its current scope
- scope = scope.current_scope if !scope.is_a?(Relation) && scope.respond_to?(:current_scope)
-
- previous_scope = self.current_scope
-
- if scope.is_a?(Hash)
- # Dup first and second level of hash (method and params).
- scope = scope.dup
- scope.each do |method, params|
- scope[method] = params.dup unless params == true
- end
-
- scope.assert_valid_keys([ :find, :create ])
- relation = construct_finder_arel(scope[:find] || {})
- relation.default_scoped = true unless action == :overwrite
-
- if previous_scope && previous_scope.create_with_value && scope[:create]
- scope_for_create = if action == :merge
- previous_scope.create_with_value.merge(scope[:create])
- else
- scope[:create]
- end
-
- relation = relation.create_with(scope_for_create)
- else
- scope_for_create = scope[:create]
- scope_for_create ||= previous_scope.create_with_value if previous_scope
- relation = relation.create_with(scope_for_create) if scope_for_create
- end
-
- scope = relation
- end
-
- scope = previous_scope.merge(scope) if previous_scope && action == :merge
-
- self.current_scope = scope
- begin
- yield
- ensure
- self.current_scope = previous_scope
- end
- end
-
- protected
-
- # Works like with_scope, but discards any nested properties.
- def with_exclusive_scope(method_scoping = {}, &block)
- if method_scoping.values.any? { |e| e.is_a?(ActiveRecord::Relation) }
- raise ArgumentError, <<-MSG
- New finder API can not be used with_exclusive_scope. You can either call unscoped to get an anonymous scope not bound to the default_scope:
-
- User.unscoped.where(:active => true)
-
- Or call unscoped with a block:
-
- User.unscoped do
- User.where(:active => true).all
- end
-
- MSG
- end
- with_scope(method_scoping, :overwrite, &block)
- end
-
def current_scope #:nodoc:
Thread.current["#{self}_current_scope"]
end
@@ -129,15 +16,6 @@ module ActiveRecord
def current_scope=(scope) #:nodoc:
Thread.current["#{self}_current_scope"] = scope
end
-
- private
-
- def construct_finder_arel(options = {}, scope = nil)
- relation = options.is_a?(Hash) ? unscoped.apply_finder_options(options) : options
- relation = scope.merge(relation) if scope
- relation
- end
-
end
def populate_with_current_scope_attributes
diff --git a/activerecord/lib/active_record/scoping/default.rb b/activerecord/lib/active_record/scoping/default.rb
index 5f05d146f2..a2a85d4b96 100644
--- a/activerecord/lib/active_record/scoping/default.rb
+++ b/activerecord/lib/active_record/scoping/default.rb
@@ -1,4 +1,3 @@
-require 'active_support/concern'
module ActiveRecord
module Scoping
@@ -7,7 +6,7 @@ module ActiveRecord
included do
# Stores the default scope for the class
- config_attribute :default_scopes
+ class_attribute :default_scopes, instance_writer: false
self.default_scopes = []
end
@@ -30,14 +29,14 @@ module ActiveRecord
# Post.limit(10) # Fires "SELECT * FROM posts LIMIT 10"
# }
#
- # It is recommended to use the block form of unscoped because chaining
- # unscoped with <tt>scope</tt> does not work. Assuming that
+ # It is recommended that you use the block form of unscoped because
+ # chaining unscoped with <tt>scope</tt> does not work. Assuming that
# <tt>published</tt> is a <tt>scope</tt>, the following two statements
- # are equal: the default_scope is applied on both.
+ # are equal: the <tt>default_scope</tt> is applied on both.
#
# Post.unscoped.published
# Post.published
- def unscoped #:nodoc:
+ def unscoped
block_given? ? relation.scoping { yield } : relation
end
@@ -51,7 +50,7 @@ module ActiveRecord
# the model.
#
# class Article < ActiveRecord::Base
- # default_scope where(:published => true)
+ # default_scope { where(:published => true) }
# end
#
# Article.all # => SELECT * FROM articles WHERE published = true
@@ -62,12 +61,6 @@ module ActiveRecord
# Article.new.published # => true
# Article.create.published # => true
#
- # You can also use <tt>default_scope</tt> with a block, in order to have it lazily evaluated:
- #
- # class Article < ActiveRecord::Base
- # default_scope { where(:published_at => Time.now - 1.week) }
- # end
- #
# (You can also pass any object which responds to <tt>call</tt> to the <tt>default_scope</tt>
# macro, and it will be called when building the default scope.)
#
@@ -75,8 +68,8 @@ module ActiveRecord
# be merged together:
#
# class Article < ActiveRecord::Base
- # default_scope where(:published => true)
- # default_scope where(:rating => 'G')
+ # default_scope { where(:published => true) }
+ # default_scope { where(:rating => 'G') }
# end
#
# Article.all # => SELECT * FROM articles WHERE published = true AND rating = 'G'
@@ -92,21 +85,30 @@ module ActiveRecord
# # Should return a scope, you can call 'super' here etc.
# end
# end
- def default_scope(scope = {})
+ def default_scope(scope = nil)
scope = Proc.new if block_given?
+
+ if scope.is_a?(Relation) || !scope.respond_to?(:call)
+ ActiveSupport::Deprecation.warn(
+ "Calling #default_scope without a block is deprecated. For example instead " \
+ "of `default_scope where(color: 'red')`, please use " \
+ "`default_scope { where(color: 'red') }`. (Alternatively you can just redefine " \
+ "self.default_scope.)"
+ )
+ end
+
self.default_scopes = default_scopes + [scope]
end
def build_default_scope #:nodoc:
- if method(:default_scope).owner != ActiveRecord::Scoping::Default::ClassMethods
+ if !Base.is_a?(method(:default_scope).owner)
+ # The user has defined their own default scope method, so call that
evaluate_default_scope { default_scope }
elsif default_scopes.any?
evaluate_default_scope do
default_scopes.inject(relation) do |default_scope, scope|
- if scope.is_a?(Hash)
- default_scope.apply_finder_options(scope)
- elsif !scope.is_a?(Relation) && scope.respond_to?(:call)
- default_scope.merge(scope.call)
+ if !scope.is_a?(Relation) && scope.respond_to?(:call)
+ default_scope.merge(unscoped { scope.call })
else
default_scope.merge(scope)
end
diff --git a/activerecord/lib/active_record/scoping/named.rb b/activerecord/lib/active_record/scoping/named.rb
index 0edc3f1dcc..75f31229b5 100644
--- a/activerecord/lib/active_record/scoping/named.rb
+++ b/activerecord/lib/active_record/scoping/named.rb
@@ -1,8 +1,6 @@
require 'active_support/core_ext/array'
require 'active_support/core_ext/hash/except'
require 'active_support/core_ext/kernel/singleton_class'
-require 'active_support/core_ext/object/blank'
-require 'active_support/core_ext/class/attribute'
module ActiveRecord
# = Active Record Named \Scopes
@@ -11,33 +9,25 @@ module ActiveRecord
extend ActiveSupport::Concern
module ClassMethods
- # Returns an anonymous \scope.
+ # Returns an <tt>ActiveRecord::Relation</tt> scope object.
#
- # posts = Post.scoped
+ # posts = Post.all
# posts.size # Fires "select count(*) from posts" and returns the count
# posts.each {|p| puts p.name } # Fires "select * from posts" and loads post objects
#
- # fruits = Fruit.scoped
+ # fruits = Fruit.all
# fruits = fruits.where(:color => 'red') if options[:red_only]
# fruits = fruits.limit(10) if limited?
#
- # Anonymous \scopes tend to be useful when procedurally generating complex
- # queries, where passing intermediate values (\scopes) around as first-class
- # objects is convenient.
- #
# You can define a \scope that applies to all finders using
# ActiveRecord::Base.default_scope.
- def scoped(options = nil)
- if options
- scoped.apply_finder_options(options)
+ def all
+ if current_scope
+ current_scope.clone
else
- if current_scope
- current_scope.clone
- else
- scope = relation.clone
- scope.default_scoped = true
- scope
- end
+ scope = relation
+ scope.default_scoped = true
+ scope
end
end
@@ -48,7 +38,7 @@ module ActiveRecord
if current_scope
current_scope.scope_for_create
else
- scope = relation.clone
+ scope = relation
scope.default_scoped = true
scope.scope_for_create
end
@@ -171,29 +161,27 @@ module ActiveRecord
# Article.published.featured.latest_article
# Article.featured.titles
- def scope(name, scope_options = {})
- name = name.to_sym
- valid_scope_name?(name)
- extension = Module.new(&Proc.new) if block_given?
-
- scope_proc = lambda do |*args|
- options = scope_options.respond_to?(:call) ? unscoped { scope_options.call(*args) } : scope_options
- options = scoped.apply_finder_options(options) if options.is_a?(Hash)
-
- relation = scoped.merge(options)
+ def scope(name, body, &block)
+ extension = Module.new(&block) if block
- extension ? relation.extending(extension) : relation
+ # Check body.is_a?(Relation) to prevent the relation actually being
+ # loaded by respond_to?
+ if body.is_a?(Relation) || !body.respond_to?(:call)
+ ActiveSupport::Deprecation.warn(
+ "Using #scope without passing a callable object is deprecated. For " \
+ "example `scope :red, where(color: 'red')` should be changed to " \
+ "`scope :red, -> { where(color: 'red') }`. There are numerous gotchas " \
+ "in the former usage and it makes the implementation more complicated " \
+ "and buggy. (If you prefer, you can just define a class method named " \
+ "`self.red`.)"
+ )
end
- singleton_class.send(:redefine_method, name, &scope_proc)
- end
-
- protected
+ singleton_class.send(:define_method, name) do |*args|
+ options = body.respond_to?(:call) ? unscoped { body.call(*args) } : body
+ relation = all.merge(options)
- def valid_scope_name?(name)
- if respond_to?(name, true)
- logger.warn "Creating scope :#{name}. " \
- "Overwriting existing method #{self.name}.#{name}."
+ extension ? relation.extending(extension) : relation
end
end
end
diff --git a/activerecord/lib/active_record/serialization.rb b/activerecord/lib/active_record/serialization.rb
index 41e3b92499..e8dd312a47 100644
--- a/activerecord/lib/active_record/serialization.rb
+++ b/activerecord/lib/active_record/serialization.rb
@@ -1,9 +1,21 @@
module ActiveRecord #:nodoc:
+ ActiveSupport.on_load(:active_record_config) do
+ mattr_accessor :include_root_in_json, instance_accessor: false
+ self.include_root_in_json = true
+ end
+
# = Active Record Serialization
module Serialization
extend ActiveSupport::Concern
include ActiveModel::Serializers::JSON
+ included do
+ singleton_class.class_eval do
+ remove_method :include_root_in_json
+ delegate :include_root_in_json, to: 'ActiveRecord::Model'
+ end
+ end
+
def serializable_hash(options = nil)
options = options.try(:clone) || {}
diff --git a/activerecord/lib/active_record/serializers/xml_serializer.rb b/activerecord/lib/active_record/serializers/xml_serializer.rb
index 2e60521638..834d01a1e8 100644
--- a/activerecord/lib/active_record/serializers/xml_serializer.rb
+++ b/activerecord/lib/active_record/serializers/xml_serializer.rb
@@ -18,8 +18,8 @@ module ActiveRecord #:nodoc:
# <id type="integer">1</id>
# <approved type="boolean">false</approved>
# <replies-count type="integer">0</replies-count>
- # <bonus-time type="datetime">2000-01-01T08:28:00+12:00</bonus-time>
- # <written-on type="datetime">2003-07-16T09:28:00+1200</written-on>
+ # <bonus-time type="dateTime">2000-01-01T08:28:00+12:00</bonus-time>
+ # <written-on type="dateTime">2003-07-16T09:28:00+1200</written-on>
# <content>Have a nice day</content>
# <author-email-address>david@loudthinking.com</author-email-address>
# <parent-id></parent-id>
@@ -177,11 +177,6 @@ module ActiveRecord #:nodoc:
end
class XmlSerializer < ActiveModel::Serializers::Xml::Serializer #:nodoc:
- def initialize(*args)
- super
- options[:except] = Array(options[:except]) | Array(@serializable.class.inheritance_column)
- end
-
class Attribute < ActiveModel::Serializers::Xml::Serializer::Attribute #:nodoc:
def compute_type
klass = @serializable.class
diff --git a/activerecord/lib/active_record/session_store.rb b/activerecord/lib/active_record/session_store.rb
deleted file mode 100644
index ce43ae8066..0000000000
--- a/activerecord/lib/active_record/session_store.rb
+++ /dev/null
@@ -1,358 +0,0 @@
-module ActiveRecord
- # = Active Record Session Store
- #
- # A session store backed by an Active Record class. A default class is
- # provided, but any object duck-typing to an Active Record Session class
- # with text +session_id+ and +data+ attributes is sufficient.
- #
- # The default assumes a +sessions+ tables with columns:
- # +id+ (numeric primary key),
- # +session_id+ (text, or longtext if your session data exceeds 65K), and
- # +data+ (text or longtext; careful if your session data exceeds 65KB).
- #
- # The +session_id+ column should always be indexed for speedy lookups.
- # Session data is marshaled to the +data+ column in Base64 format.
- # If the data you write is larger than the column's size limit,
- # ActionController::SessionOverflowError will be raised.
- #
- # You may configure the table name, primary key, and data column.
- # For example, at the end of <tt>config/application.rb</tt>:
- #
- # ActiveRecord::SessionStore::Session.table_name = 'legacy_session_table'
- # ActiveRecord::SessionStore::Session.primary_key = 'session_id'
- # ActiveRecord::SessionStore::Session.data_column_name = 'legacy_session_data'
- #
- # Note that setting the primary key to the +session_id+ frees you from
- # having a separate +id+ column if you don't want it. However, you must
- # set <tt>session.model.id = session.session_id</tt> by hand! A before filter
- # on ApplicationController is a good place.
- #
- # Since the default class is a simple Active Record, you get timestamps
- # for free if you add +created_at+ and +updated_at+ datetime columns to
- # the +sessions+ table, making periodic session expiration a snap.
- #
- # You may provide your own session class implementation, whether a
- # feature-packed Active Record or a bare-metal high-performance SQL
- # store, by setting
- #
- # ActiveRecord::SessionStore.session_class = MySessionClass
- #
- # You must implement these methods:
- #
- # self.find_by_session_id(session_id)
- # initialize(hash_of_session_id_and_data, options_hash = {})
- # attr_reader :session_id
- # attr_accessor :data
- # save
- # destroy
- #
- # The example SqlBypass class is a generic SQL session store. You may
- # use it as a basis for high-performance database-specific stores.
- class SessionStore < ActionDispatch::Session::AbstractStore
- module ClassMethods # :nodoc:
- def marshal(data)
- ::Base64.encode64(Marshal.dump(data)) if data
- end
-
- def unmarshal(data)
- Marshal.load(::Base64.decode64(data)) if data
- end
-
- def drop_table!
- connection.schema_cache.clear_table_cache!(table_name)
- connection.drop_table table_name
- end
-
- def create_table!
- connection.schema_cache.clear_table_cache!(table_name)
- connection.create_table(table_name) do |t|
- t.string session_id_column, :limit => 255
- t.text data_column_name
- end
- connection.add_index table_name, session_id_column, :unique => true
- end
- end
-
- # The default Active Record class.
- class Session < ActiveRecord::Base
- extend ClassMethods
-
- ##
- # :singleton-method:
- # Customizable data column name. Defaults to 'data'.
- cattr_accessor :data_column_name
- self.data_column_name = 'data'
-
- attr_accessible :session_id, :data, :marshaled_data
-
- before_save :marshal_data!
- before_save :raise_on_session_data_overflow!
-
- class << self
- def data_column_size_limit
- @data_column_size_limit ||= columns_hash[data_column_name].limit
- end
-
- # Hook to set up sessid compatibility.
- def find_by_session_id(session_id)
- setup_sessid_compatibility!
- find_by_session_id(session_id)
- end
-
- private
- def session_id_column
- 'session_id'
- end
-
- # Compatibility with tables using sessid instead of session_id.
- def setup_sessid_compatibility!
- # Reset column info since it may be stale.
- reset_column_information
- if columns_hash['sessid']
- def self.find_by_session_id(*args)
- find_by_sessid(*args)
- end
-
- define_method(:session_id) { sessid }
- define_method(:session_id=) { |session_id| self.sessid = session_id }
- else
- class << self; remove_possible_method :find_by_session_id; end
-
- def self.find_by_session_id(session_id)
- find :first, :conditions => {:session_id=>session_id}
- end
- end
- end
- end
-
- def initialize(attributes = nil, options = {})
- @data = nil
- super
- end
-
- # Lazy-unmarshal session state.
- def data
- @data ||= self.class.unmarshal(read_attribute(@@data_column_name)) || {}
- end
-
- attr_writer :data
-
- # Has the session been loaded yet?
- def loaded?
- @data
- end
-
- private
- def marshal_data!
- return false unless loaded?
- write_attribute(@@data_column_name, self.class.marshal(data))
- end
-
- # Ensures that the data about to be stored in the database is not
- # larger than the data storage column. Raises
- # ActionController::SessionOverflowError.
- def raise_on_session_data_overflow!
- return false unless loaded?
- limit = self.class.data_column_size_limit
- if limit and read_attribute(@@data_column_name).size > limit
- raise ActionController::SessionOverflowError
- end
- end
- end
-
- # A barebones session store which duck-types with the default session
- # store but bypasses Active Record and issues SQL directly. This is
- # an example session model class meant as a basis for your own classes.
- #
- # The database connection, table name, and session id and data columns
- # are configurable class attributes. Marshaling and unmarshaling
- # are implemented as class methods that you may override. By default,
- # marshaling data is
- #
- # ::Base64.encode64(Marshal.dump(data))
- #
- # and unmarshaling data is
- #
- # Marshal.load(::Base64.decode64(data))
- #
- # This marshaling behavior is intended to store the widest range of
- # binary session data in a +text+ column. For higher performance,
- # store in a +blob+ column instead and forgo the Base64 encoding.
- class SqlBypass
- extend ClassMethods
-
- ##
- # :singleton-method:
- # The table name defaults to 'sessions'.
- cattr_accessor :table_name
- @@table_name = 'sessions'
-
- ##
- # :singleton-method:
- # The session id field defaults to 'session_id'.
- cattr_accessor :session_id_column
- @@session_id_column = 'session_id'
-
- ##
- # :singleton-method:
- # The data field defaults to 'data'.
- cattr_accessor :data_column
- @@data_column = 'data'
-
- class << self
- alias :data_column_name :data_column
-
- # Use the ActiveRecord::Base.connection by default.
- attr_writer :connection
-
- # Use the ActiveRecord::Base.connection_pool by default.
- attr_writer :connection_pool
-
- def connection
- @connection ||= ActiveRecord::Base.connection
- end
-
- def connection_pool
- @connection_pool ||= ActiveRecord::Base.connection_pool
- end
-
- # Look up a session by id and unmarshal its data if found.
- def find_by_session_id(session_id)
- if record = connection.select_one("SELECT * FROM #{@@table_name} WHERE #{@@session_id_column}=#{connection.quote(session_id)}")
- new(:session_id => session_id, :marshaled_data => record['data'])
- end
- end
- end
-
- delegate :connection, :connection=, :connection_pool, :connection_pool=, :to => self
-
- attr_reader :session_id, :new_record
- alias :new_record? :new_record
-
- attr_writer :data
-
- # Look for normal and marshaled data, self.find_by_session_id's way of
- # telling us to postpone unmarshaling until the data is requested.
- # We need to handle a normal data attribute in case of a new record.
- def initialize(attributes)
- @session_id = attributes[:session_id]
- @data = attributes[:data]
- @marshaled_data = attributes[:marshaled_data]
- @new_record = @marshaled_data.nil?
- end
-
- # Lazy-unmarshal session state.
- def data
- unless @data
- if @marshaled_data
- @data, @marshaled_data = self.class.unmarshal(@marshaled_data) || {}, nil
- else
- @data = {}
- end
- end
- @data
- end
-
- def loaded?
- @data
- end
-
- def save
- return false unless loaded?
- marshaled_data = self.class.marshal(data)
- connect = connection
-
- if @new_record
- @new_record = false
- connect.update <<-end_sql, 'Create session'
- INSERT INTO #{table_name} (
- #{connect.quote_column_name(session_id_column)},
- #{connect.quote_column_name(data_column)} )
- VALUES (
- #{connect.quote(session_id)},
- #{connect.quote(marshaled_data)} )
- end_sql
- else
- connect.update <<-end_sql, 'Update session'
- UPDATE #{table_name}
- SET #{connect.quote_column_name(data_column)}=#{connect.quote(marshaled_data)}
- WHERE #{connect.quote_column_name(session_id_column)}=#{connect.quote(session_id)}
- end_sql
- end
- end
-
- def destroy
- return if @new_record
-
- connect = connection
- connect.delete <<-end_sql, 'Destroy session'
- DELETE FROM #{table_name}
- WHERE #{connect.quote_column_name(session_id_column)}=#{connect.quote(session_id)}
- end_sql
- end
- end
-
- # The class used for session storage. Defaults to
- # ActiveRecord::SessionStore::Session
- cattr_accessor :session_class
- self.session_class = Session
-
- SESSION_RECORD_KEY = 'rack.session.record'
- ENV_SESSION_OPTIONS_KEY = Rack::Session::Abstract::ENV_SESSION_OPTIONS_KEY
-
- private
- def get_session(env, sid)
- Base.silence do
- unless sid and session = @@session_class.find_by_session_id(sid)
- # If the sid was nil or if there is no pre-existing session under the sid,
- # force the generation of a new sid and associate a new session associated with the new sid
- sid = generate_sid
- session = @@session_class.new(:session_id => sid, :data => {})
- end
- env[SESSION_RECORD_KEY] = session
- [sid, session.data]
- end
- end
-
- def set_session(env, sid, session_data, options)
- Base.silence do
- record = get_session_model(env, sid)
- record.data = session_data
- return false unless record.save
-
- session_data = record.data
- if session_data && session_data.respond_to?(:each_value)
- session_data.each_value do |obj|
- obj.clear_association_cache if obj.respond_to?(:clear_association_cache)
- end
- end
- end
-
- sid
- end
-
- def destroy_session(env, session_id, options)
- if sid = current_session_id(env)
- Base.silence do
- get_session_model(env, sid).destroy
- env[SESSION_RECORD_KEY] = nil
- end
- end
-
- generate_sid unless options[:drop]
- end
-
- def get_session_model(env, sid)
- if env[ENV_SESSION_OPTIONS_KEY][:id].nil?
- env[SESSION_RECORD_KEY] = find_session(sid)
- else
- env[SESSION_RECORD_KEY] ||= find_session(sid)
- end
- end
-
- def find_session(id)
- @@session_class.find_by_session_id(id) ||
- @@session_class.new(:session_id => id, :data => {})
- end
- end
-end
diff --git a/activerecord/lib/active_record/store.rb b/activerecord/lib/active_record/store.rb
index 1c7b839e5e..df7f58c81f 100644
--- a/activerecord/lib/active_record/store.rb
+++ b/activerecord/lib/active_record/store.rb
@@ -1,6 +1,8 @@
+require 'active_support/core_ext/hash/indifferent_access'
+
module ActiveRecord
# Store gives you a thin wrapper around serialize for the purpose of storing hashes in a single column.
- # It's like a simple key/value store backed into your record when you don't care about being able to
+ # It's like a simple key/value store baked into your record when you don't care about being able to
# query that store outside the context of a single record.
#
# You can then declare accessors to this store that are then accessible just like any other attribute
@@ -10,42 +12,135 @@ module ActiveRecord
# Make sure that you declare the database column used for the serialized store as a text, so there's
# plenty of room.
#
+ # You can set custom coder to encode/decode your serialized attributes to/from different formats.
+ # JSON, YAML, Marshal are supported out of the box. Generally it can be any wrapper that provides +load+ and +dump+.
+ #
# Examples:
#
# class User < ActiveRecord::Base
- # store :settings, accessors: [ :color, :homepage ]
+ # store :settings, accessors: [ :color, :homepage ], coder: JSON
# end
#
# u = User.new(color: 'black', homepage: '37signals.com')
# u.color # Accessor stored attribute
# u.settings[:country] = 'Denmark' # Any attribute, even if not specified with an accessor
#
+ # # There is no difference between strings and symbols for accessing custom attributes
+ # u.settings[:country] # => 'Denmark'
+ # u.settings['country'] # => 'Denmark'
+ #
# # Add additional accessors to an existing store through store_accessor
# class SuperUser < User
# store_accessor :settings, :privileges, :servants
# end
+ #
+ # The stored attribute names can be retrieved using +stored_attributes+.
+ #
+ # User.stored_attributes[:settings] # [:color, :homepage]
+ #
+ # == Overwriting default accessors
+ #
+ # All stored values are automatically available through accessors on the Active Record
+ # object, but sometimes you want to specialize this behavior. This can be done by overwriting
+ # the default accessors (using the same name as the attribute) and calling
+ # <tt>read_store_attribute(store_attribute_name, attr_name)</tt> and
+ # <tt>write_store_attribute(store_attribute_name, attr_name, value)</tt> to actually
+ # change things.
+ #
+ # class Song < ActiveRecord::Base
+ # # Uses a stored integer to hold the volume adjustment of the song
+ # store :settings, accessors: [:volume_adjustment]
+ #
+ # def volume_adjustment=(decibels)
+ # write_store_attribute(:settings, :volume_adjustment, decibels.to_i)
+ # end
+ #
+ # def volume_adjustment
+ # read_store_attribute(:settings, :volume_adjustment).to_i
+ # end
+ # end
module Store
extend ActiveSupport::Concern
+ included do
+ class_attribute :stored_attributes, instance_accessor: false
+ self.stored_attributes = {}
+ end
+
module ClassMethods
def store(store_attribute, options = {})
- serialize store_attribute, Hash
+ serialize store_attribute, IndifferentCoder.new(options[:coder])
store_accessor(store_attribute, options[:accessors]) if options.has_key? :accessors
end
def store_accessor(store_attribute, *keys)
- keys.flatten.each do |key|
+ keys = keys.flatten
+ keys.each do |key|
define_method("#{key}=") do |value|
- send("#{store_attribute}=", {}) unless send(store_attribute).is_a?(Hash)
- send(store_attribute)[key] = value
- send("#{store_attribute}_will_change!")
+ write_store_attribute(store_attribute, key, value)
end
define_method(key) do
- send("#{store_attribute}=", {}) unless send(store_attribute).is_a?(Hash)
- send(store_attribute)[key]
+ read_store_attribute(store_attribute, key)
end
end
+
+ self.stored_attributes[store_attribute] ||= []
+ self.stored_attributes[store_attribute] |= keys
+ end
+ end
+
+ protected
+ def read_store_attribute(store_attribute, key)
+ attribute = initialize_store_attribute(store_attribute)
+ attribute[key]
+ end
+
+ def write_store_attribute(store_attribute, key, value)
+ attribute = initialize_store_attribute(store_attribute)
+ if value != attribute[key]
+ send :"#{store_attribute}_will_change!"
+ attribute[key] = value
+ end
+ end
+
+ private
+ def initialize_store_attribute(store_attribute)
+ attribute = send(store_attribute)
+ unless attribute.is_a?(HashWithIndifferentAccess)
+ attribute = IndifferentCoder.as_indifferent_hash(attribute)
+ send :"#{store_attribute}=", attribute
+ end
+ attribute
+ end
+
+ class IndifferentCoder # :nodoc:
+ def initialize(coder_or_class_name)
+ @coder =
+ if coder_or_class_name.respond_to?(:load) && coder_or_class_name.respond_to?(:dump)
+ coder_or_class_name
+ else
+ ActiveRecord::Coders::YAMLColumn.new(coder_or_class_name || Object)
+ end
+ end
+
+ def dump(obj)
+ @coder.dump self.class.as_indifferent_hash(obj)
+ end
+
+ def load(yaml)
+ self.class.as_indifferent_hash @coder.load(yaml)
+ end
+
+ def self.as_indifferent_hash(obj)
+ case obj
+ when HashWithIndifferentAccess
+ obj
+ when Hash
+ obj.with_indifferent_access
+ else
+ HashWithIndifferentAccess.new
+ end
end
end
end
diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb
new file mode 100644
index 0000000000..fda51b3d76
--- /dev/null
+++ b/activerecord/lib/active_record/tasks/database_tasks.rb
@@ -0,0 +1,150 @@
+module ActiveRecord
+ module Tasks # :nodoc:
+ module DatabaseTasks # :nodoc:
+ extend self
+
+ attr_writer :current_config
+
+ LOCAL_HOSTS = ['127.0.0.1', 'localhost']
+
+ def register_task(pattern, task)
+ @tasks ||= {}
+ @tasks[pattern] = task
+ end
+
+ register_task(/mysql/, ActiveRecord::Tasks::MySQLDatabaseTasks)
+ register_task(/postgresql/, ActiveRecord::Tasks::PostgreSQLDatabaseTasks)
+ register_task(/sqlite/, ActiveRecord::Tasks::SQLiteDatabaseTasks)
+
+ def current_config(options = {})
+ options.reverse_merge! :env => Rails.env
+ if options.has_key?(:config)
+ @current_config = options[:config]
+ else
+ @current_config ||= if ENV['DATABASE_URL']
+ database_url_config
+ else
+ ActiveRecord::Base.configurations[options[:env]]
+ end
+ end
+ end
+
+ def create(*arguments)
+ configuration = arguments.first
+ class_for_adapter(configuration['adapter']).new(*arguments).create
+ rescue Exception => error
+ $stderr.puts error, *(error.backtrace)
+ $stderr.puts "Couldn't create database for #{configuration.inspect}"
+ end
+
+ def create_all
+ each_local_configuration { |configuration| create configuration }
+ end
+
+ def create_current(environment = Rails.env)
+ each_current_configuration(environment) { |configuration|
+ create configuration
+ }
+ ActiveRecord::Base.establish_connection environment
+ end
+
+ def create_database_url
+ create database_url_config
+ end
+
+ def drop(*arguments)
+ configuration = arguments.first
+ class_for_adapter(configuration['adapter']).new(*arguments).drop
+ rescue Exception => error
+ $stderr.puts error, *(error.backtrace)
+ $stderr.puts "Couldn't drop #{configuration['database']}"
+ end
+
+ def drop_all
+ each_local_configuration { |configuration| drop configuration }
+ end
+
+ def drop_current(environment = Rails.env)
+ each_current_configuration(environment) { |configuration|
+ drop configuration
+ }
+ end
+
+ def drop_database_url
+ drop database_url_config
+ end
+
+ def charset_current(environment = Rails.env)
+ charset ActiveRecord::Base.configurations[environment]
+ end
+
+ def charset(*arguments)
+ configuration = arguments.first
+ class_for_adapter(configuration['adapter']).new(*arguments).charset
+ end
+
+ def collation_current(environment = Rails.env)
+ collation ActiveRecord::Base.configurations[environment]
+ end
+
+ def collation(*arguments)
+ configuration = arguments.first
+ class_for_adapter(configuration['adapter']).new(*arguments).collation
+ end
+
+ def purge(configuration)
+ class_for_adapter(configuration['adapter']).new(configuration).purge
+ end
+
+ def structure_dump(*arguments)
+ configuration = arguments.first
+ filename = arguments.delete_at 1
+ class_for_adapter(configuration['adapter']).new(*arguments).structure_dump(filename)
+ end
+
+ def structure_load(*arguments)
+ configuration = arguments.first
+ filename = arguments.delete_at 1
+ class_for_adapter(configuration['adapter']).new(*arguments).structure_load(filename)
+ end
+
+ private
+
+ def database_url_config
+ @database_url_config ||=
+ ConnectionAdapters::ConnectionSpecification::Resolver.new(ENV["DATABASE_URL"], {}).spec.config.stringify_keys
+ end
+
+ def class_for_adapter(adapter)
+ key = @tasks.keys.detect { |pattern| adapter[pattern] }
+ @tasks[key]
+ end
+
+ def each_current_configuration(environment)
+ environments = [environment]
+ environments << 'test' if environment.development?
+
+ configurations = ActiveRecord::Base.configurations.values_at(*environments)
+ configurations.compact.each do |configuration|
+ yield configuration unless configuration['database'].blank?
+ end
+ end
+
+ def each_local_configuration
+ ActiveRecord::Base.configurations.each_value do |configuration|
+ next unless configuration['database']
+
+ if local_database?(configuration)
+ yield configuration
+ else
+ $stderr.puts "This task only modifies local databases. #{configuration['database']} is on a remote host."
+ end
+ end
+ end
+
+ def local_database?(configuration)
+ configuration['host'].blank? || LOCAL_HOSTS.include?(configuration['host'])
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb
new file mode 100644
index 0000000000..2340f949b7
--- /dev/null
+++ b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb
@@ -0,0 +1,128 @@
+module ActiveRecord
+ module Tasks # :nodoc:
+ class MySQLDatabaseTasks # :nodoc:
+
+ DEFAULT_CHARSET = ENV['CHARSET'] || 'utf8'
+ DEFAULT_COLLATION = ENV['COLLATION'] || 'utf8_unicode_ci'
+ ACCESS_DENIED_ERROR = 1045
+
+ delegate :connection, :establish_connection, to: ActiveRecord::Base
+
+ def initialize(configuration)
+ @configuration = configuration
+ end
+
+ def create
+ establish_connection configuration_without_database
+ connection.create_database configuration['database'], creation_options
+ establish_connection configuration
+ rescue error_class => error
+ raise error unless error.errno == ACCESS_DENIED_ERROR
+
+ $stdout.print error.error
+ establish_connection root_configuration_without_database
+ connection.create_database configuration['database'], creation_options
+ connection.execute grant_statement.gsub(/\s+/, ' ').strip
+ establish_connection configuration
+ rescue error_class => error
+ $stderr.puts error.error
+ $stderr.puts "Couldn't create database for #{configuration.inspect}, #{creation_options.inspect}"
+ $stderr.puts "(If you set the charset manually, make sure you have a matching collation)" if configuration['encoding']
+ end
+
+ def drop
+ establish_connection configuration
+ connection.drop_database configuration['database']
+ end
+
+ def purge
+ establish_connection :test
+ connection.recreate_database configuration['database'], creation_options
+ end
+
+ def charset
+ connection.charset
+ end
+
+ def collation
+ connection.collation
+ end
+
+ def structure_dump(filename)
+ args = prepare_command_options('mysqldump')
+ args.concat(["--result-file", "#{filename}"])
+ args.concat(["--no-data"])
+ args.concat(["#{configuration['database']}"])
+ Kernel.system(*args)
+ end
+
+ def structure_load(filename)
+ args = prepare_command_options('mysql')
+ args.concat(['--execute', %{SET FOREIGN_KEY_CHECKS = 0; SOURCE #{filename}; SET FOREIGN_KEY_CHECKS = 1}])
+ args.concat(["--database", "#{configuration['database']}"])
+ Kernel.system(*args)
+ end
+
+ private
+
+ def configuration
+ @configuration
+ end
+
+ def configuration_without_database
+ configuration.merge('database' => nil)
+ end
+
+ def creation_options
+ {
+ charset: (configuration['encoding'] || DEFAULT_CHARSET),
+ collation: (configuration['collation'] || DEFAULT_COLLATION)
+ }
+ end
+
+ def error_class
+ case configuration['adapter']
+ when /jdbc/
+ require 'active_record/railties/jdbcmysql_error'
+ ArJdbcMySQL::Error
+ when /mysql2/
+ Mysql2::Error
+ else
+ Mysql::Error
+ end
+ end
+
+ def grant_statement
+ <<-SQL
+GRANT ALL PRIVILEGES ON #{configuration['database']}.*
+ TO '#{configuration['username']}'@'localhost'
+IDENTIFIED BY '#{configuration['password']}' WITH GRANT OPTION;
+ SQL
+ end
+
+ def root_configuration_without_database
+ configuration_without_database.merge(
+ 'username' => 'root',
+ 'password' => root_password
+ )
+ end
+
+ def root_password
+ $stdout.print "Please provide the root password for your mysql installation\n>"
+ $stdin.gets.strip
+ end
+
+ def prepare_command_options(command)
+ args = [command]
+ args.concat(['--user', configuration['username']]) if configuration['username']
+ args << "--password=#{configuration['password']}" if configuration['password']
+ args.concat(['--default-character-set', configuration['encoding']]) if configuration['encoding']
+ configuration.slice('host', 'port', 'socket').each do |k, v|
+ args.concat([ "--#{k}", v ]) if v
+ end
+ args
+ 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
new file mode 100644
index 0000000000..ea5cb888fb
--- /dev/null
+++ b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb
@@ -0,0 +1,85 @@
+require 'shellwords'
+
+module ActiveRecord
+ module Tasks # :nodoc:
+ class PostgreSQLDatabaseTasks # :nodoc:
+
+ DEFAULT_ENCODING = ENV['CHARSET'] || 'utf8'
+
+ delegate :connection, :establish_connection, :clear_active_connections!,
+ to: ActiveRecord::Base
+
+ def initialize(configuration)
+ @configuration = configuration
+ end
+
+ def create(master_established = false)
+ establish_master_connection unless master_established
+ connection.create_database configuration['database'],
+ configuration.merge('encoding' => encoding)
+ establish_connection configuration
+ end
+
+ def drop
+ establish_master_connection
+ connection.drop_database configuration['database']
+ end
+
+ def charset
+ connection.encoding
+ end
+
+ def collation
+ connection.collation
+ end
+
+ def purge
+ clear_active_connections!
+ drop
+ create true
+ end
+
+ def structure_dump(filename)
+ set_psql_env
+ search_path = configuration['schema_search_path']
+ unless search_path.blank?
+ search_path = search_path.split(",").map{|search_path_part| "--schema=#{Shellwords.escape(search_path_part.strip)}" }.join(" ")
+ end
+
+ command = "pg_dump -i -s -x -O -f #{Shellwords.escape(filename)} #{search_path} #{Shellwords.escape(configuration['database'])}"
+ raise 'Error dumping database' unless Kernel.system(command)
+
+ File.open(filename, "a") { |f| f << "SET search_path TO #{ActiveRecord::Base.connection.schema_search_path};\n\n" }
+ end
+
+ def structure_load(filename)
+ set_psql_env
+ Kernel.system("psql -f #{filename} #{configuration['database']}")
+ end
+
+ private
+
+ def configuration
+ @configuration
+ end
+
+ def encoding
+ configuration['encoding'] || DEFAULT_ENCODING
+ end
+
+ def establish_master_connection
+ establish_connection configuration.merge(
+ 'database' => 'postgres',
+ 'schema_search_path' => 'public'
+ )
+ end
+
+ def set_psql_env
+ ENV['PGHOST'] = configuration['host'] if configuration['host']
+ ENV['PGPORT'] = configuration['port'].to_s if configuration['port']
+ ENV['PGPASSWORD'] = configuration['password'].to_s if configuration['password']
+ ENV['PGUSER'] = configuration['username'].to_s if configuration['username']
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb b/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb
new file mode 100644
index 0000000000..da01058a82
--- /dev/null
+++ b/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb
@@ -0,0 +1,55 @@
+module ActiveRecord
+ module Tasks # :nodoc:
+ class SQLiteDatabaseTasks # :nodoc:
+
+ delegate :connection, :establish_connection, to: ActiveRecord::Base
+
+ def initialize(configuration, root = Rails.root)
+ @configuration, @root = configuration, root
+ end
+
+ def create
+ if File.exist?(configuration['database'])
+ $stderr.puts "#{configuration['database']} already exists"
+ return
+ end
+
+ establish_connection configuration
+ connection
+ end
+
+ def drop
+ require 'pathname'
+ path = Pathname.new configuration['database']
+ file = path.absolute? ? path.to_s : File.join(root, path)
+
+ FileUtils.rm(file) if File.exist?(file)
+ end
+ alias :purge :drop
+
+ def charset
+ connection.encoding
+ end
+
+ def structure_dump(filename)
+ dbfile = configuration['database']
+ `sqlite3 #{dbfile} .schema > #{filename}`
+ end
+
+ def structure_load(filename)
+ dbfile = configuration['database']
+ `sqlite3 #{dbfile} < "#{filename}"`
+ end
+
+ private
+
+ def configuration
+ @configuration
+ end
+
+ def root
+ @root
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/test_case.rb b/activerecord/lib/active_record/test_case.rb
index 4d881f0f7d..c035ad43a2 100644
--- a/activerecord/lib/active_record/test_case.rb
+++ b/activerecord/lib/active_record/test_case.rb
@@ -1,4 +1,3 @@
-require 'active_support/deprecation'
require 'active_support/test_case'
ActiveSupport::Deprecation.warn('ActiveRecord::TestCase is deprecated, please use ActiveSupport::TestCase')
@@ -7,18 +6,8 @@ module ActiveRecord
#
# Defines some test assertions to test against SQL queries.
class TestCase < ActiveSupport::TestCase #:nodoc:
- setup :cleanup_identity_map
-
- def setup
- cleanup_identity_map
- end
-
def teardown
- SQLCounter.log.clear
- end
-
- def cleanup_identity_map
- ActiveRecord::IdentityMap.clear
+ SQLCounter.clear_log
end
def assert_date_from_db(expected, actual, message = nil)
@@ -32,47 +21,57 @@ module ActiveRecord
end
def assert_sql(*patterns_to_match)
- SQLCounter.log = []
+ SQLCounter.clear_log
yield
- SQLCounter.log
+ SQLCounter.log_all
ensure
failed_patterns = []
patterns_to_match.each do |pattern|
- failed_patterns << pattern unless SQLCounter.log.any?{ |sql| pattern === sql }
+ failed_patterns << pattern unless SQLCounter.log_all.any?{ |sql| pattern === sql }
end
assert failed_patterns.empty?, "Query pattern(s) #{failed_patterns.map{ |p| p.inspect }.join(', ')} not found.#{SQLCounter.log.size == 0 ? '' : "\nQueries:\n#{SQLCounter.log.join("\n")}"}"
end
- def assert_queries(num = 1)
- SQLCounter.log = []
+ def assert_queries(num = 1, options = {})
+ ignore_none = options.fetch(:ignore_none) { num == :any }
+ SQLCounter.clear_log
yield
ensure
- assert_equal num, SQLCounter.log.size, "#{SQLCounter.log.size} instead of #{num} queries were executed.#{SQLCounter.log.size == 0 ? '' : "\nQueries:\n#{SQLCounter.log.join("\n")}"}"
+ the_log = ignore_none ? SQLCounter.log_all : SQLCounter.log
+ if num == :any
+ assert_operator the_log.size, :>=, 1, "1 or more queries expected, but none were executed."
+ else
+ mesg = "#{the_log.size} instead of #{num} queries were executed.#{the_log.size == 0 ? '' : "\nQueries:\n#{the_log.join("\n")}"}"
+ assert_equal num, the_log.size, mesg
+ end
end
def assert_no_queries(&block)
- prev_ignored_sql = SQLCounter.ignored_sql
- SQLCounter.ignored_sql = []
- assert_queries(0, &block)
- ensure
- SQLCounter.ignored_sql = prev_ignored_sql
+ assert_queries(0, :ignore_none => true, &block)
end
end
class SQLCounter
class << self
- attr_accessor :ignored_sql, :log
+ attr_accessor :ignored_sql, :log, :log_all
+ def clear_log; self.log = []; self.log_all = []; end
end
- self.log = []
+ self.clear_log
self.ignored_sql = [/^PRAGMA (?!(table_info))/, /^SELECT currval/, /^SELECT CAST/, /^SELECT @@IDENTITY/, /^SELECT @@ROWCOUNT/, /^SAVEPOINT/, /^ROLLBACK TO SAVEPOINT/, /^RELEASE SAVEPOINT/, /^SHOW max_identifier_length/, /^BEGIN/, /^COMMIT/]
# FIXME: this needs to be refactored so specific database can add their own
- # ignored SQL. This ignored SQL is for Oracle.
- ignored_sql.concat [/^select .*nextval/i, /^SAVEPOINT/, /^ROLLBACK TO/, /^\s*select .* from all_triggers/im]
-
+ # ignored SQL, or better yet, use a different notification for the queries
+ # instead examining the SQL content.
+ oracle_ignored = [/^select .*nextval/i, /^SAVEPOINT/, /^ROLLBACK TO/, /^\s*select .* from all_triggers/im]
+ mysql_ignored = [/^SHOW TABLES/i, /^SHOW FULL FIELDS/]
+ postgresql_ignored = [/^\s*select\b.*\bfrom\b.*pg_namespace\b/im, /^\s*select\b.*\battname\b.*\bfrom\b.*\bpg_attribute\b/im]
+
+ [oracle_ignored, mysql_ignored, postgresql_ignored].each do |db_ignored_sql|
+ ignored_sql.concat db_ignored_sql
+ end
attr_reader :ignore
@@ -85,8 +84,10 @@ module ActiveRecord
# FIXME: this seems bad. we should probably have a better way to indicate
# the query was cached
- return if 'CACHE' == values[:name] || ignore =~ sql
- self.class.log << sql
+ return if 'CACHE' == values[:name]
+
+ self.class.log_all << sql
+ self.class.log << sql unless ignore =~ sql
end
end
diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb
index c717fdea47..c32e0d6bf8 100644
--- a/activerecord/lib/active_record/timestamp.rb
+++ b/activerecord/lib/active_record/timestamp.rb
@@ -1,6 +1,10 @@
-require 'active_support/core_ext/class/attribute'
module ActiveRecord
+ ActiveSupport.on_load(:active_record_config) do
+ mattr_accessor :record_timestamps, instance_accessor: false
+ self.record_timestamps = true
+ end
+
# = Active Record Timestamp
#
# Active Record automatically timestamps create and update operations if the
@@ -33,8 +37,7 @@ module ActiveRecord
extend ActiveSupport::Concern
included do
- config_attribute :record_timestamps, :instance_writer => true
- self.record_timestamps = true
+ config_attribute :record_timestamps, instance_writer: true
end
def initialize_dup(other)
diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb
index b492377d18..934393b4e7 100644
--- a/activerecord/lib/active_record/transactions.rb
+++ b/activerecord/lib/active_record/transactions.rb
@@ -165,7 +165,7 @@ module ActiveRecord
# writing, the only database that we're aware of that supports true nested
# transactions, is MS-SQL. Because of this, Active Record emulates nested
# transactions by using savepoints on MySQL and PostgreSQL. See
- # http://dev.mysql.com/doc/refman/5.0/en/savepoint.html
+ # http://dev.mysql.com/doc/refman/5.6/en/savepoint.html
# for more information about savepoints.
#
# === Callbacks
@@ -208,6 +208,21 @@ module ActiveRecord
connection.transaction(options, &block)
end
+ # This callback is called after a record has been created, updated, or destroyed.
+ #
+ # You can specify that the callback should only be fired by a certain action with
+ # the +:on+ option:
+ #
+ # after_commit :do_foo, :on => :create
+ # after_commit :do_bar, :on => :update
+ # after_commit :do_baz, :on => :destroy
+ #
+ # Also, to have the callback fired on create and update, but not on destroy:
+ #
+ # after_commit :do_zoo, :if => :persisted?
+ #
+ # Note that transactional fixtures do not play well with this feature. Please
+ # use the +test_after_commit+ gem to have these hooks fired in tests.
def after_commit(*args, &block)
options = args.last
if options.is_a?(Hash) && options[:on]
@@ -217,6 +232,9 @@ module ActiveRecord
set_callback(:commit, :after, *args, &block)
end
+ # This callback is called after a create, update, or destroy are rolled back.
+ #
+ # Please check the documentation of +after_commit+ for options.
def after_rollback(*args, &block)
options = args.last
if options.is_a?(Hash) && options[:on]
@@ -251,7 +269,6 @@ module ActiveRecord
remember_transaction_record_state
yield
rescue Exception
- IdentityMap.remove(self) if IdentityMap.enabled?
restore_transaction_record_state
raise
ensure
@@ -270,7 +287,6 @@ module ActiveRecord
def rolledback!(force_restore_state = false) #:nodoc:
run_callbacks :rollback
ensure
- IdentityMap.remove(self) if IdentityMap.enabled?
restore_transaction_record_state(force_restore_state)
end
@@ -292,7 +308,13 @@ module ActiveRecord
status = nil
self.class.transaction do
add_to_transaction
- status = yield
+ begin
+ status = yield
+ rescue ActiveRecord::Rollback
+ @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1
+ status = nil
+ end
+
raise ActiveRecord::Rollback unless status
end
status
@@ -302,32 +324,26 @@ 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 ||= {}
@_start_transaction_state[:id] = id if has_attribute?(self.class.primary_key)
- 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[:new_record] = @new_record
+ @_start_transaction_state[:destroyed] = @destroyed
@_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) + 1
end
# Clear the new record state and id of a record.
def clear_transaction_record_state #:nodoc:
- if defined?(@_start_transaction_state)
- @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1
- remove_instance_variable(:@_start_transaction_state) if @_start_transaction_state[:level] < 1
- end
+ @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1
+ @_start_transaction_state.clear if @_start_transaction_state[:level] < 1
end
# Restore the new record state and id of a record that was previously saved by a call to save_record_state.
def restore_transaction_record_state(force = false) #:nodoc:
- if defined?(@_start_transaction_state)
+ unless @_start_transaction_state.empty?
@_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1
- if @_start_transaction_state[:level] < 1
- restore_state = remove_instance_variable(:@_start_transaction_state)
- @attributes = @attributes.dup if @attributes.frozen?
+ if @_start_transaction_state[:level] < 1 || force
+ restore_state = @_start_transaction_state
+ was_frozen = @attributes.frozen?
+ @attributes = @attributes.dup if was_frozen
@new_record = restore_state[:new_record]
@destroyed = restore_state[:destroyed]
if restore_state.has_key?(:id)
@@ -336,13 +352,15 @@ module ActiveRecord
@attributes.delete(self.class.primary_key)
@attributes_cache.delete(self.class.primary_key)
end
+ @attributes.freeze if was_frozen
+ @_start_transaction_state.clear
end
end
end
# Determine if a record was created or destroyed in a transaction. State should be one of :new_record or :destroyed.
def transaction_record_state(state) #:nodoc:
- @_start_transaction_state[state] if defined?(@_start_transaction_state)
+ @_start_transaction_state[state]
end
# Determine if a transaction included an action for :create, :update, or :destroy. Used in filtering callbacks.
diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb
index 4b075183c3..ed561bfb3c 100644
--- a/activerecord/lib/active_record/validations.rb
+++ b/activerecord/lib/active_record/validations.rb
@@ -14,7 +14,7 @@ module ActiveRecord
def initialize(record)
@record = record
errors = @record.errors.full_messages.join(", ")
- super(I18n.t("activerecord.errors.messages.record_invalid", :errors => errors))
+ super(I18n.t(:"#{@record.class.i18n_scope}.errors.messages.record_invalid", :errors => errors, :default => :"errors.messages.record_invalid"))
end
end
@@ -32,11 +32,11 @@ module ActiveRecord
module ClassMethods
# Creates an object just like Base.create but calls <tt>save!</tt> instead of +save+
# so an exception is raised if the record is invalid.
- def create!(attributes = nil, options = {}, &block)
+ def create!(attributes = nil, &block)
if attributes.is_a?(Array)
- attributes.collect { |attr| create!(attr, options, &block) }
+ attributes.collect { |attr| create!(attr, &block) }
else
- object = new(attributes, options)
+ object = new(attributes)
yield(object) if block_given?
object.save!
object
@@ -81,3 +81,4 @@ end
require "active_record/validations/associated"
require "active_record/validations/uniqueness"
+require "active_record/validations/presence"
diff --git a/activerecord/lib/active_record/validations/associated.rb b/activerecord/lib/active_record/validations/associated.rb
index afce149da9..1fa6629980 100644
--- a/activerecord/lib/active_record/validations/associated.rb
+++ b/activerecord/lib/active_record/validations/associated.rb
@@ -1,6 +1,6 @@
module ActiveRecord
module Validations
- class AssociatedValidator < ActiveModel::EachValidator
+ 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?
record.errors.add(attribute, :invalid, options.merge(:value => value))
@@ -9,7 +9,8 @@ module ActiveRecord
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
+ # themselves. Works with any kind of association.
#
# class Book < ActiveRecord::Base
# has_many :pages
@@ -18,23 +19,28 @@ module ActiveRecord
# validates_associated :pages, :library
# end
#
- # WARNING: This validation must not be used on both ends of an association. Doing so will lead to a circular dependency and cause infinite recursion.
+ # WARNING: This validation must not be used on both ends of an association.
+ # Doing so will lead to a circular dependency and cause infinite recursion.
#
- # NOTE: This validation will not fail if the association hasn't been assigned. If you want to
- # ensure that the association is both present and guaranteed to be valid, you also need to
- # use +validates_presence_of+.
+ # NOTE: This validation will not fail if the association hasn't been
+ # assigned. If you want to ensure that the association is both present and
+ # guaranteed to be valid, you also need to use +validates_presence_of+.
#
# Configuration options:
- # * <tt>:message</tt> - A custom error message (default is: "is invalid")
+ #
+ # * <tt>:message</tt> - A custom error message (default is: "is invalid").
# * <tt>:on</tt> - Specifies when this validation is active. Runs in all
# validation contexts by default (+nil+), other options are <tt>:create</tt>
# and <tt>:update</tt>.
- # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should
- # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The
- # method, proc or string should return or evaluate to a true or false value.
- # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should
- # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
- # method, proc or string should return or evaluate to a true or false value.
+ # * <tt>:if</tt> - Specifies a method, proc or string to call to determine
+ # if the validation should occur (e.g. <tt>if: :allow_validation</tt>,
+ # or <tt>if: Proc.new { |user| user.signup_step > 2 }</tt>). The method,
+ # proc or string should return or evaluate to a +true+ or +false+ value.
+ # * <tt>:unless</tt> - Specifies a method, proc or string to call to
+ # determine if the validation should not occur (e.g. <tt>unless: :skip_validation</tt>,
+ # or <tt>unless: => Proc.new { |user| user.signup_step <= 2 }</tt>). The
+ # method, proc or string should return or evaluate to a +true+ or +false+
+ # value.
def validates_associated(*attr_names)
validates_with AssociatedValidator, _merge_attributes(attr_names)
end
diff --git a/activerecord/lib/active_record/validations/presence.rb b/activerecord/lib/active_record/validations/presence.rb
new file mode 100644
index 0000000000..056527b512
--- /dev/null
+++ b/activerecord/lib/active_record/validations/presence.rb
@@ -0,0 +1,64 @@
+module ActiveRecord
+ module Validations
+ class PresenceValidator < ActiveModel::Validations::PresenceValidator
+ def validate(record)
+ super
+ attributes.each do |attribute|
+ next unless record.class.reflect_on_association(attribute)
+ value = record.send(attribute)
+ if Array(value).all? { |r| r.marked_for_destruction? }
+ record.errors.add(attribute, :blank, options)
+ end
+ end
+ end
+ end
+
+ module ClassMethods
+ # Validates that the specified attributes are not blank (as defined by
+ # Object#blank?), and, if the attribute is an association, that the
+ # associated object is not marked for destruction. Happens by default
+ # on save.
+ #
+ # class Person < ActiveRecord::Base
+ # has_one :face
+ # validates_presence_of :face
+ # end
+ #
+ # The face attribute must be in the object and it cannot be blank or marked
+ # for destruction.
+ #
+ # If you want to validate the presence of a boolean field (where the real values
+ # are true and false), you will want to use
+ # <tt>validates_inclusion_of :field_name, :in => [true, false]</tt>.
+ #
+ # This is due to the way Object#blank? handles boolean values:
+ # <tt>false.blank? # => true</tt>.
+ #
+ # This validator defers to the ActiveModel validation for presence, adding the
+ # check to see that an associated object is not marked for destruction. This
+ # prevents the parent object from validating successfully and saving, which then
+ # deletes the associated object, thus putting the parent object into an invalid
+ # state.
+ #
+ # Configuration options:
+ # * <tt>:message</tt> - A custom error message (default is: "can't be blank").
+ # * <tt>:on</tt> - Specifies when this validation is active. Runs in all
+ # validation contexts by default (+nil+), other options are <tt>:create</tt>
+ # and <tt>:update</tt>.
+ # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if
+ # the validation should occur (e.g. <tt>:if => :allow_validation</tt>, or
+ # <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The method, proc
+ # or string should return or evaluate to a true or false value.
+ # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine
+ # if the validation should not occur (e.g. <tt>:unless => :skip_validation</tt>,
+ # or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The method,
+ # proc or string should return or evaluate to a true or false value.
+ # * <tt>:strict</tt> - Specifies whether validation should be strict.
+ # See <tt>ActiveModel::Validation#validates!</tt> for more information.
+ #
+ def validates_presence_of(*attr_names)
+ validates_with PresenceValidator, _merge_attributes(attr_names)
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb
index 9556878f63..c117872ac8 100644
--- a/activerecord/lib/active_record/validations/uniqueness.rb
+++ b/activerecord/lib/active_record/validations/uniqueness.rb
@@ -2,7 +2,7 @@ require 'active_support/core_ext/array/prepend_and_append'
module ActiveRecord
module Validations
- class UniquenessValidator < ActiveModel::EachValidator
+ class UniquenessValidator < ActiveModel::EachValidator #:nodoc:
def initialize(options)
super(options.reverse_merge(:case_sensitive => true))
end
@@ -26,7 +26,7 @@ module ActiveRecord
relation = relation.and(table[finder_class.primary_key.to_sym].not_eq(record.send(:id))) if record.persisted?
Array(options[:scope]).each do |scope_item|
- scope_value = record.send(scope_item)
+ scope_value = record.read_attribute(scope_item)
reflection = record.class.reflect_on_association(scope_item)
if reflection
scope_value = record.send(reflection.foreign_key)
@@ -35,8 +35,14 @@ module ActiveRecord
relation = relation.and(table[scope_item].eq(scope_value))
end
- if finder_class.unscoped.where(relation).exists?
- record.errors.add(attribute, :taken, options.except(:case_sensitive, :scope).merge(:value => value))
+ relation = finder_class.unscoped.where(relation)
+
+ if options[:conditions]
+ relation = relation.merge(options[:conditions])
+ end
+
+ if relation.exists?
+ record.errors.add(attribute, :taken, options.except(:case_sensitive, :scope, :conditions).merge(:value => value))
end
end
@@ -81,44 +87,67 @@ module ActiveRecord
end
module ClassMethods
- # Validates whether the value of the specified attributes are unique across the system.
- # Useful for making sure that only one user
+ # Validates whether the value of the specified attributes are unique
+ # across the system. Useful for making sure that only one user
# can be named "davidhh".
#
# class Person < ActiveRecord::Base
# validates_uniqueness_of :user_name
# end
#
- # It can also validate whether the value of the specified attributes are unique based on a scope parameter:
+ # It can also validate whether the value of the specified attributes are
+ # unique based on a <tt>:scope</tt> parameter:
#
# class Person < ActiveRecord::Base
- # validates_uniqueness_of :user_name, :scope => :account_id
+ # validates_uniqueness_of :user_name, scope: :account_id
# end
#
- # Or even multiple scope parameters. For example, making sure that a teacher can only be on the schedule once
- # per semester for a particular class.
+ # Or even multiple scope parameters. For example, making sure that a
+ # teacher can only be on the schedule once per semester for a particular
+ # class.
#
# class TeacherSchedule < ActiveRecord::Base
- # validates_uniqueness_of :teacher_id, :scope => [:semester_id, :class_id]
+ # validates_uniqueness_of :teacher_id, scope: [:semester_id, :class_id]
+ # end
+ #
+ # It is also possible to limit the uniqueness constraint to a set of
+ # records matching certain conditions. In this example archived articles
+ # are not being taken into consideration when validating uniqueness
+ # of the title attribute:
+ #
+ # class Article < ActiveRecord::Base
+ # validates_uniqueness_of :title, conditions: where('status != ?', 'archived')
# end
#
- # When the record is created, a check is performed to make sure that no record exists in the database
- # with the given value for the specified attribute (that maps to a column). When the record is updated,
+ # When the record is created, a check is performed to make sure that no
+ # record exists in the database with the given value for the specified
+ # attribute (that maps to a column). When the record is updated,
# the same check is made but disregarding the record itself.
#
# Configuration options:
- # * <tt>:message</tt> - Specifies a custom error message (default is: "has already been taken").
- # * <tt>:scope</tt> - One or more columns by which to limit the scope of the uniqueness constraint.
- # * <tt>:case_sensitive</tt> - Looks for an exact match. Ignored by non-text columns (+true+ by default).
- # * <tt>:allow_nil</tt> - If set to true, skips this validation if the attribute is +nil+ (default is +false+).
- # * <tt>:allow_blank</tt> - If set to true, skips this validation if the attribute is blank (default is +false+).
- # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should
- # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>).
- # The method, proc or string should return or evaluate to a true or false value.
- # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should
- # not occur (e.g. <tt>:unless => :skip_validation</tt>, or
- # <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The method, proc or string should
- # return or evaluate to a true or false value.
+ #
+ # * <tt>:message</tt> - Specifies a custom error message (default is:
+ # "has already been taken").
+ # * <tt>:scope</tt> - One or more columns by which to limit the scope of
+ # the uniqueness constraint.
+ # * <tt>:conditions</tt> - Specify the conditions to be included as a
+ # <tt>WHERE</tt> SQL fragment to limit the uniqueness constraint lookup
+ # (e.g. <tt>conditions: where('status = ?', 'active')</tt>).
+ # * <tt>:case_sensitive</tt> - Looks for an exact match. Ignored by
+ # non-text columns (+true+ by default).
+ # * <tt>:allow_nil</tt> - If set to +true+, skips this validation if the
+ # attribute is +nil+ (default is +false+).
+ # * <tt>:allow_blank</tt> - If set to +true+, skips this validation if the
+ # attribute is blank (default is +false+).
+ # * <tt>:if</tt> - Specifies a method, proc or string to call to determine
+ # if the validation should occur (e.g. <tt>if: :allow_validation</tt>,
+ # or <tt>if: Proc.new { |user| user.signup_step > 2 }</tt>). The method,
+ # proc or string should return or evaluate to a +true+ or +false+ value.
+ # * <tt>:unless</tt> - Specifies a method, proc or string to call to
+ # determine if the validation should ot occur (e.g. <tt>unless: :skip_validation</tt>,
+ # or <tt>unless: Proc.new { |user| user.signup_step <= 2 }</tt>). The
+ # method, proc or string should return or evaluate to a +true+ or +false+
+ # value.
#
# === Concurrency and integrity
#
@@ -174,16 +203,16 @@ module ActiveRecord
#
# The bundled ActiveRecord::ConnectionAdapters distinguish unique index
# constraint errors from other types of database errors by throwing an
- # ActiveRecord::RecordNotUnique exception.
- # For other adapters you will have to parse the (database-specific) exception
- # message to detect such a case.
+ # ActiveRecord::RecordNotUnique exception. For other adapters you will
+ # have to parse the (database-specific) exception message to detect such
+ # a case.
+ #
# The following bundled adapters throw the ActiveRecord::RecordNotUnique exception:
+ #
# * ActiveRecord::ConnectionAdapters::MysqlAdapter
# * ActiveRecord::ConnectionAdapters::Mysql2Adapter
- # * ActiveRecord::ConnectionAdapters::SQLiteAdapter
# * ActiveRecord::ConnectionAdapters::SQLite3Adapter
# * ActiveRecord::ConnectionAdapters::PostgreSQLAdapter
- #
def validates_uniqueness_of(*attr_names)
validates_with UniquenessValidator, _merge_attributes(attr_names)
end