aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib
diff options
context:
space:
mode:
Diffstat (limited to 'activerecord/lib')
-rw-r--r--activerecord/lib/active_record.rb4
-rw-r--r--activerecord/lib/active_record/aggregations.rb12
-rw-r--r--activerecord/lib/active_record/associations.rb24
-rw-r--r--activerecord/lib/active_record/associations/association.rb26
-rw-r--r--activerecord/lib/active_record/associations/association_scope.rb15
-rw-r--r--activerecord/lib/active_record/associations/belongs_to_association.rb7
-rw-r--r--activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb3
-rw-r--r--activerecord/lib/active_record/associations/collection_association.rb4
-rw-r--r--activerecord/lib/active_record/associations/collection_proxy.rb29
-rw-r--r--activerecord/lib/active_record/associations/has_many_through_association.rb4
-rw-r--r--activerecord/lib/active_record/associations/preloader/association.rb5
-rw-r--r--activerecord/lib/active_record/associations/through_association.rb12
-rw-r--r--activerecord/lib/active_record/attribute_methods.rb83
-rw-r--r--activerecord/lib/active_record/attribute_methods/dirty.rb5
-rw-r--r--activerecord/lib/active_record/attribute_methods/primary_key.rb11
-rw-r--r--activerecord/lib/active_record/attribute_methods/read.rb68
-rw-r--r--activerecord/lib/active_record/autosave_association.rb43
-rw-r--r--activerecord/lib/active_record/base.rb24
-rw-r--r--activerecord/lib/active_record/coders/yaml_column.rb8
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb92
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb30
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb29
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb9
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_adapter.rb51
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb30
-rw-r--r--activerecord/lib/active_record/connection_adapters/column.rb12
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb10
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql_adapter.rb8
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb87
-rw-r--r--activerecord/lib/active_record/connection_adapters/schema_cache.rb64
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb568
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb563
-rw-r--r--activerecord/lib/active_record/core.rb21
-rw-r--r--activerecord/lib/active_record/counter_cache.rb4
-rw-r--r--activerecord/lib/active_record/dynamic_finder_match.rb84
-rw-r--r--activerecord/lib/active_record/dynamic_matchers.rb167
-rw-r--r--activerecord/lib/active_record/dynamic_scope_match.rb27
-rw-r--r--activerecord/lib/active_record/explain.rb2
-rw-r--r--activerecord/lib/active_record/fixtures.rb14
-rw-r--r--activerecord/lib/active_record/identity_map.rb144
-rw-r--r--activerecord/lib/active_record/inheritance.rb37
-rw-r--r--activerecord/lib/active_record/locking/optimistic.rb36
-rw-r--r--activerecord/lib/active_record/model.rb1
-rw-r--r--activerecord/lib/active_record/model_schema.rb28
-rw-r--r--activerecord/lib/active_record/nested_attributes.rb2
-rw-r--r--activerecord/lib/active_record/null_relation.rb53
-rw-r--r--activerecord/lib/active_record/persistence.rb71
-rw-r--r--activerecord/lib/active_record/querying.rb5
-rw-r--r--activerecord/lib/active_record/railtie.rb26
-rw-r--r--activerecord/lib/active_record/railties/databases.rake24
-rw-r--r--activerecord/lib/active_record/railties/jdbcmysql_error.rb2
-rw-r--r--activerecord/lib/active_record/reflection.rb22
-rw-r--r--activerecord/lib/active_record/relation.rb124
-rw-r--r--activerecord/lib/active_record/relation/batches.rb9
-rw-r--r--activerecord/lib/active_record/relation/calculations.rb132
-rw-r--r--activerecord/lib/active_record/relation/finder_methods.rb266
-rw-r--r--activerecord/lib/active_record/relation/merger.rb121
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder.rb31
-rw-r--r--activerecord/lib/active_record/relation/query_methods.rb286
-rw-r--r--activerecord/lib/active_record/relation/spawn_methods.rb150
-rw-r--r--activerecord/lib/active_record/result.rb1
-rw-r--r--activerecord/lib/active_record/sanitization.rb10
-rw-r--r--activerecord/lib/active_record/schema_dumper.rb4
-rw-r--r--activerecord/lib/active_record/scoping.rb121
-rw-r--r--activerecord/lib/active_record/scoping/default.rb36
-rw-r--r--activerecord/lib/active_record/scoping/named.rb54
-rw-r--r--activerecord/lib/active_record/session_store.rb2
-rw-r--r--activerecord/lib/active_record/test_case.rb10
-rw-r--r--activerecord/lib/active_record/transactions.rb10
-rw-r--r--activerecord/lib/active_record/validations.rb2
-rw-r--r--activerecord/lib/active_record/validations/uniqueness.rb21
-rw-r--r--activerecord/lib/rails/generators/active_record/migration/templates/migration.rb9
-rw-r--r--activerecord/lib/rails/generators/active_record/model/model_generator.rb7
-rw-r--r--activerecord/lib/rails/generators/active_record/model/templates/model.rb5
74 files changed, 2034 insertions, 2087 deletions
diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb
index 73c8a06ab7..ed26b4899f 100644
--- a/activerecord/lib/active_record.rb
+++ b/activerecord/lib/active_record.rb
@@ -25,6 +25,7 @@ require 'active_support'
require 'active_support/i18n'
require 'active_model'
require 'arel'
+require 'active_record_deprecated_finders'
require 'active_record/version'
@@ -62,10 +63,7 @@ module ActiveRecord
autoload :CounterCache
autoload :ConnectionHandling
autoload :DynamicMatchers
- autoload :DynamicFinderMatch
- autoload :DynamicScopeMatch
autoload :Explain
- autoload :IdentityMap
autoload :Inheritance
autoload :Integration
autoload :Migration
diff --git a/activerecord/lib/active_record/aggregations.rb b/activerecord/lib/active_record/aggregations.rb
index c39284539c..a4db627535 100644
--- a/activerecord/lib/active_record/aggregations.rb
+++ b/activerecord/lib/active_record/aggregations.rb
@@ -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.
#
diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb
index 958821add6..c30e8e08b8 100644
--- a/activerecord/lib/active_record/associations.rb
+++ b/activerecord/lib/active_record/associations.rb
@@ -1129,7 +1129,7 @@ module ActiveRecord
# 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
+ # you want to do a join but not include the joined columns, for example. 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>).
@@ -1264,8 +1264,8 @@ module ActiveRecord
# [: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
+ # By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed if
+ # you want to do a join but not include the joined columns, for example. 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>,
@@ -1355,7 +1355,7 @@ module ActiveRecord
# 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
+ # if you want to do a join but not include the joined columns, for example. 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
@@ -1382,7 +1382,9 @@ 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
@@ -1430,7 +1432,7 @@ module ActiveRecord
# 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 +1514,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
@@ -1574,8 +1576,8 @@ module ActiveRecord
# 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
+ # By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed if
+ # you want to do a join but exclude the joined columns, for example. 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.
@@ -1594,7 +1596,7 @@ module ActiveRecord
# 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}"
+ # proc { |record| "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)
end
diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb
index 7887d59aad..e75003f261 100644
--- a/activerecord/lib/active_record/associations/association.rb
+++ b/activerecord/lib/active_record/associations/association.rb
@@ -25,9 +25,7 @@ module ActiveRecord
def initialize(owner, reflection)
reflection.check_validity!
- @target = nil
@owner, @reflection = owner, reflection
- @updated = false
reset
reset_scope
@@ -38,14 +36,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.
@@ -134,17 +132,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
@@ -225,13 +214,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)
+ attributes = create_scope.except(*(record.changed - [reflection.foreign_key]))
+ record.assign_attributes(attributes, :without_protection => true)
end
end
end
diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb
index 2972b7e13e..5a44d3a156 100644
--- a/activerecord/lib/active_record/associations/association_scope.rb
+++ b/activerecord/lib/active_record/associations/association_scope.rb
@@ -15,19 +15,20 @@ module ActiveRecord
def scope
scope = klass.unscoped
- scope = scope.extending(*Array(options[:extend]))
+
+ 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!(options.slice(
+ :readonly, :references, :order, :limit, :joins, :group, :having, :offset, :select, :uniq))
- if options[:through] && !options[:include]
- scope = scope.includes(source_options[:include])
+ if options[:include]
+ scope.includes! options[:include]
+ elsif options[:through]
+ scope.includes! source_options[:include]
end
- scope = scope.uniq if options[:uniq]
-
add_constraints(scope)
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..ddfc6f6c05 100644
--- a/activerecord/lib/active_record/associations/belongs_to_association.rb
+++ b/activerecord/lib/active_record/associations/belongs_to_association.rb
@@ -14,6 +14,11 @@ module ActiveRecord
self.target = record
end
+ def reset
+ super
+ @updated = false
+ end
+
def updated?
@updated
end
@@ -72,7 +77,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/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb
index b2136605e1..14aa557b6c 100644
--- a/activerecord/lib/active_record/associations/collection_association.rb
+++ b/activerecord/lib/active_record/associations/collection_association.rb
@@ -71,7 +71,7 @@ module ActiveRecord
end
def reset
- @loaded = false
+ super
@target = []
end
@@ -544,7 +544,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
diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb
index 5eda0387c4..261a829281 100644
--- a/activerecord/lib/active_record/associations/collection_proxy.rb
+++ b/activerecord/lib/active_record/associations/collection_proxy.rb
@@ -61,11 +61,15 @@ module ActiveRecord
@association
end
- def scoped
+ def scoped(options = nil)
association = @association
- association.scoped.extending do
+ scope = association.scoped
+
+ scope.extending! do
define_method(:proxy_association) { association }
end
+ scope.merge!(options) if options
+ scope
end
def respond_to?(name, include_private = false)
@@ -75,9 +79,9 @@ module ActiveRecord
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|
+ match = DynamicMatchers::Method.match(self, method)
+ if match && match.is_a?(DynamicMatchers::Instantiator)
+ scoped.send(method, *args) do |r|
proxy_association.send :set_owner_attributes, r
proxy_association.send :add_to_target, r
yield(r) if block_given?
@@ -97,7 +101,7 @@ module ActiveRecord
end
else
- scoped.readonly(nil).send(method, *args, &block)
+ scoped.readonly(nil).public_send(method, *args, &block)
end
end
@@ -126,6 +130,19 @@ module ActiveRecord
proxy_association.reload
self
end
+
+ # Define array public methods because we know it should be invoked over
+ # the target, so we can have a performance improvement using those methods
+ # in association collections
+ Array.public_instance_methods.each do |m|
+ unless method_defined?(m)
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
+ def #{m}(*args, &block)
+ target.public_send(:#{m}, *args, &block) if load_target
+ end
+ RUBY
+ end
+ end
end
end
end
diff --git a/activerecord/lib/active_record/associations/has_many_through_association.rb b/activerecord/lib/active_record/associations/has_many_through_association.rb
index 9657cb081d..53d49fef2e 100644
--- a/activerecord/lib/active_record/associations/has_many_through_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_through_association.rb
@@ -73,7 +73,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
diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb
index 253998fb23..b4c3908b10 100644
--- a/activerecord/lib/active_record/associations/preloader/association.rb
+++ b/activerecord/lib/active_record/associations/preloader/association.rb
@@ -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
@@ -93,7 +93,8 @@ module ActiveRecord
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]))
diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb
index f95e5337c2..be890e5767 100644
--- a/activerecord/lib/active_record/associations/through_association.rb
+++ b/activerecord/lib/active_record/associations/through_association.rb
@@ -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_methods.rb b/activerecord/lib/active_record/attribute_methods.rb
index 3e27e85f02..39ea885246 100644
--- a/activerecord/lib/active_record/attribute_methods.rb
+++ b/activerecord/lib/active_record/attribute_methods.rb
@@ -49,14 +49,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
@@ -189,7 +181,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.
@@ -214,37 +206,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
+
+ def attribute_method?(attr_name)
+ defined?(@attributes) && @attributes.include?(attr_name)
+ end
- if include_readonly_attributes || !self.class.readonly_attributes.include?(name)
+ private
- 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
+ # 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
- attrs[arel_table[name]] = value
- end
- end
+ 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 3a737e5b35..11c63591e3 100644
--- a/activerecord/lib/active_record/attribute_methods/dirty.rb
+++ b/activerecord/lib/active_record/attribute_methods/dirty.rb
@@ -22,8 +22,6 @@ module ActiveRecord
if status = super
@previously_changed = changes
@changed_attributes.clear
- elsif IdentityMap.enabled?
- IdentityMap.remove(self)
end
status
end
@@ -34,9 +32,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.
diff --git a/activerecord/lib/active_record/attribute_methods/primary_key.rb b/activerecord/lib/active_record/attribute_methods/primary_key.rb
index 8e0b3e1402..7b7811a706 100644
--- a/activerecord/lib/active_record/attribute_methods/primary_key.rb
+++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb
@@ -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
@@ -111,9 +105,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/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb
index 846ac03d82..dcc3d79de9 100644
--- a/activerecord/lib/active_record/attribute_methods/read.rb
+++ b/activerecord/lib/active_record/attribute_methods/read.rb
@@ -29,35 +29,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 +40,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 +50,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 +58,19 @@ 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
# 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)
# If it's cached, just return it
- @attributes_cache.fetch(attr_name) { |name|
+ @attributes_cache.fetch(attr_name.to_s) { |name|
column = @columns_hash.fetch(name) {
- return self.class.type_cast_attribute(name, @attributes, @attributes_cache)
+ return @attributes.fetch(name) {
+ if name == 'id' && self.class.primary_key != name
+ read_attribute(self.class.primary_key)
+ end
+ }
}
value = @attributes.fetch(name) {
diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb
index 4bafadc666..3005bef092 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.
#
@@ -191,23 +191,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
@@ -330,14 +328,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)
@@ -350,11 +348,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.proxy.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..189985b671 100644
--- a/activerecord/lib/active_record/base.rb
+++ b/activerecord/lib/active_record/base.rb
@@ -201,6 +201,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 +304,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
diff --git a/activerecord/lib/active_record/coders/yaml_column.rb b/activerecord/lib/active_record/coders/yaml_column.rb
index 77af540c3e..66a0c83c41 100644
--- a/activerecord/lib/active_record/coders/yaml_column.rb
+++ b/activerecord/lib/active_record/coders/yaml_column.rb
@@ -15,7 +15,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 37a9d216df..46c7fc71ac 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
@@ -2,6 +2,7 @@ require 'thread'
require 'monitor'
require 'set'
require 'active_support/core_ext/module/deprecation'
+require 'timeout'
module ActiveRecord
# Raised when a connection could not be obtained within the connection
@@ -11,9 +12,6 @@ module ActiveRecord
# 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
@@ -94,6 +92,21 @@ module ActiveRecord
attr_accessor :automatic_reconnect, :timeout
attr_reader :spec, :connections, :size, :reaper
+ class Latch # :nodoc:
+ def initialize
+ @mutex = Mutex.new
+ @cond = ConditionVariable.new
+ end
+
+ def release
+ @mutex.synchronize { @cond.broadcast }
+ end
+
+ def await
+ @mutex.synchronize { @cond.wait @mutex }
+ end
+ end
+
# Creates a new ConnectionPool object. +spec+ is a ConnectionSpecification
# object which describes database connection information (e.g. adapter,
# host name, username, password, etc), as well as the maximum size for
@@ -115,6 +128,7 @@ module ActiveRecord
# default max pool size to 5
@size = (spec.config[:pool] && spec.config[:pool].to_i) || 5
+ @latch = Latch.new
@connections = []
@automatic_reconnect = true
end
@@ -128,18 +142,21 @@ module ActiveRecord
@reserved_connections[current_connection_id] ||= checkout
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?
+ @reserved_connections.fetch(current_connection_id) {
+ return false
+ }.in_use?
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
@@ -185,8 +202,9 @@ module ActiveRecord
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.
@@ -203,23 +221,23 @@ module ActiveRecord
# Raises:
# - PoolFullError: 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
+ loop do
+ # 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)
+ return checkout_and_verify(conn) if conn
end
+
+ Timeout.timeout(@timeout, PoolFullError) { @latch.await }
end
end
@@ -233,7 +251,10 @@ module ActiveRecord
conn.run_callbacks :checkin do
conn.expire
end
+
+ release conn
end
+ @latch.release
end
# Remove a connection from the connection pool. The connection will
@@ -244,11 +265,9 @@ module ActiveRecord
# 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
end
+ @latch.release
end
# Removes dead connections from the pool. A dead connection can occur
@@ -261,10 +280,23 @@ module ActiveRecord
remove conn if conn.in_use? && stale > conn.last_use && !conn.active?
end
end
+ @latch.release
end
private
+ 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)
end
@@ -288,10 +320,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
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 174450eb00..7b2961a04a 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
@@ -57,21 +57,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 +87,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
@@ -312,13 +312,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 update.key.in(subselect)
+ update.where key.in(subselect)
+ end
+
+ def join_to_delete(delete, select, key) #:nodoc:
+ subselect = subquery_for(key, select)
+
+ 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 = [])
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..f0b6ae2b7d 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
@@ -6,6 +6,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
@@ -62,11 +65,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 +213,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 = {})
@@ -252,6 +259,14 @@ module ActiveRecord
end # end
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
+ # === Examples
+ # 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.
@@ -264,9 +279,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?
+ 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
@@ -391,7 +408,7 @@ module ActiveRecord
# 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.
@@ -432,9 +449,11 @@ module ActiveRecord
def references(*args)
options = args.extract_options!
polymorphic = options.delete(:polymorphic)
+ index_options = options.delete(:index)
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?
+ @base.add_index(@table_name, 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
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 2c210e5ba2..e7a4f061fd 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
@@ -16,7 +16,7 @@ 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.
@@ -171,10 +171,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)
@@ -375,7 +376,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)
#
@@ -617,8 +618,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_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
index c6af682d28..c6faae77cc 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
@@ -57,7 +57,7 @@ module ActiveRecord
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:
@@ -86,6 +86,11 @@ module ActiveRecord
end
end
+ def schema_cache=(cache)
+ cache.connection = self
+ @schema_cache = cache
+ end
+
def expire
@in_use = false
end
@@ -158,11 +163,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)
@@ -279,26 +279,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 Exception => 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 e33903622b..a848838a4e 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
@@ -294,19 +294,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
@@ -375,7 +366,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|
@@ -525,8 +516,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
@@ -558,6 +549,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
diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb
index 78e54c4c9b..1933ce2b46 100644
--- a/activerecord/lib/active_record/connection_adapters/column.rb
+++ b/activerecord/lib/active_record/connection_adapters/column.rb
@@ -1,4 +1,5 @@
require 'set'
+require 'active_support/deprecation'
module ActiveRecord
# :stopdoc:
@@ -94,7 +95,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,6 +108,9 @@ 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
@@ -154,7 +158,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,14 +169,14 @@ 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?
string_to_time "2000-01-01 #{string}"
end
diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
index 3f45f23de8..350ccce03d 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
@@ -226,7 +226,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 +253,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
diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
index 724dbff1f0..0b6734b010 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
@@ -53,6 +53,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.
@@ -404,6 +405,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_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
index d2126a3e19..4d5459939b 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
@@ -17,7 +17,7 @@ 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|
+ :reaping_frequency, :insert_returning].each do |key|
conn_params.delete key
end
conn_params.delete_if { |k,v| v.nil? }
@@ -88,9 +88,8 @@ module ActiveRecord
def escape_hstore(value)
value.nil? ? 'NULL'
- : value =~ /[=\s,>]/ ? '"%s"' % value.gsub(/(["\\])/, '\\\\\1')
: value == "" ? '""'
- : value.to_s.gsub(/(["\\])/, '\\\\\1')
+ : '"%s"' % value.to_s.gsub(/(["\\])/, '\\\\\1')
end
end
# :startdoc:
@@ -259,6 +258,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
@@ -406,6 +407,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.
@@ -509,8 +511,13 @@ module ActiveRecord
else super
end
when Float
- return super unless value.infinite? && column.type == :datetime
- "'#{value.to_s.downcase}'"
+ 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.
@@ -667,8 +674,11 @@ module ActiveRecord
pk = primary_key(table_ref) if table_ref
end
- if pk
+ 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
@@ -783,11 +793,27 @@ module ActiveRecord
pk = primary_key(table_ref) if table_ref
end
- sql = "#{sql} RETURNING #{quote_column_name(pk)}" if pk
+ 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
@@ -978,6 +1004,27 @@ module ActiveRecord
end_sql
end
+ # Returns an array of schema names.
+ def schema_names
+ query(<<-SQL).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
@@ -985,7 +1032,7 @@ module ActiveRecord
# 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}"
+ execute("SET search_path TO #{schema_csv}", 'SCHEMA')
@schema_search_path = schema_csv
end
end
@@ -1007,7 +1054,9 @@ module ActiveRecord
# 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
+ result = serial_sequence(table_name, pk || 'id')
+ return nil unless result
+ result.split('.').last
rescue ActiveRecord::StatementInvalid
"#{table_name}_#{pk || 'id'}_seq"
end
@@ -1189,7 +1238,10 @@ module ActiveRecord
# 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*(NULLS\s+(FIRST|LAST)\s*)?/i, '') }
+ 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}" }
@@ -1215,6 +1267,10 @@ module ActiveRecord
end
end
+ def use_insert_returning?
+ @use_insert_returning
+ end
+
protected
# Returns the version of the connected PostgreSQL server.
def postgresql_version
@@ -1344,8 +1400,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($1)", 'SQL', [[nil, sequence_name]])
end
# Executes a SELECT query and returns the results, performing any data type
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..44e407a561 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
@@ -35,7 +37,184 @@ 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)
+ @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
+
+ # 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
+ 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,10 +224,387 @@ 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 = '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 +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 /^'(.*)'$/
+ field["dflt_value"] = $1.gsub("''", "'")
+ when /^"(.*)"$/
+ 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)})", 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.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 ||= 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
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/core.rb b/activerecord/lib/active_record/core.rb
index 3f90d25962..a869ed8c04 100644
--- a/activerecord/lib/active_record/core.rb
+++ b/activerecord/lib/active_record/core.rb
@@ -1,4 +1,6 @@
require 'active_support/concern'
+require 'active_support/core_ext/hash/indifferent_access'
+require 'active_support/core_ext/object/deep_dup'
require 'thread'
module ActiveRecord
@@ -130,18 +132,18 @@ module ActiveRecord
end
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
@@ -164,7 +166,7 @@ module ActiveRecord
# # 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)
+ @attributes = self.class.initialize_attributes(self.class.column_defaults.deep_dup)
@columns_hash = self.class.column_types.dup
init_internals
@@ -176,7 +178,7 @@ module ActiveRecord
assign_attributes(attributes, options) if attributes
yield self if block_given?
- run_callbacks :initialize
+ run_callbacks :initialize if _initialize_callbacks.any?
end
# Initialize an empty model object from +coder+. +coder+ must contain
@@ -193,7 +195,6 @@ module ActiveRecord
@attributes = self.class.initialize_attributes(coder['attributes'])
@columns_hash = self.class.column_types.merge(coder['column_types'] || {})
-
init_internals
@new_record = false
@@ -327,6 +328,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
@@ -346,7 +352,6 @@ module ActiveRecord
@attributes[pk] = nil unless @attributes.key?(pk)
- @relation = nil
@aggregation_cache = {}
@association_cache = {}
@attributes_cache = {}
diff --git a/activerecord/lib/active_record/counter_cache.rb b/activerecord/lib/active_record/counter_cache.rb
index 224f5276eb..b163ef3c12 100644
--- a/activerecord/lib/active_record/counter_cache.rb
+++ b/activerecord/lib/active_record/counter_cache.rb
@@ -69,9 +69,7 @@ module ActiveRecord
"#{quoted_column} = COALESCE(#{quoted_column}, 0) #{operator} #{value.abs}"
end
- IdentityMap.remove_by_id(symbolized_base_class, id) if IdentityMap.enabled?
-
- update_all(updates.join(', '), primary_key => id )
+ where(primary_key => id).update_all updates.join(', ')
end
# Increment a number field by one, usually representing a count.
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..e278e62ce7 100644
--- a/activerecord/lib/active_record/dynamic_matchers.rb
+++ b/activerecord/lib/active_record/dynamic_matchers.rb
@@ -1,79 +1,130 @@
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 active_record_deprecated_finders
+ # gem. When we stop supporting active_record_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_')
+ 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 active_record_deprecated_finders
+ def body
+ result
+ end
+
+ # Extended in active_record_deprecated_finders
+ def result
+ "#{finder}(#{attributes_hash})"
+ end
+
+ # Extended in active_record_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/explain.rb b/activerecord/lib/active_record/explain.rb
index 01cacf6153..313fdb3487 100644
--- a/activerecord/lib/active_record/explain.rb
+++ b/activerecord/lib/active_record/explain.rb
@@ -70,7 +70,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/fixtures.rb b/activerecord/lib/active_record/fixtures.rb
index b82d5b5621..a01e2f74ff 100644
--- a/activerecord/lib/active_record/fixtures.rb
+++ b/activerecord/lib/active_record/fixtures.rb
@@ -4,16 +4,12 @@ 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
@@ -796,9 +792,7 @@ module ActiveRecord
@fixture_cache[fixture_name].delete(fixture) 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
+ @fixture_cache[fixture_name][fixture] ||= @loaded_fixtures[fixture_name][fixture.to_s].find
else
raise StandardError, "No entry named '#{fixture}' found for fixture collection '#{fixture_name}'"
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'
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..46d253b0a7 100644
--- a/activerecord/lib/active_record/inheritance.rb
+++ b/activerecord/lib/active_record/inheritance.rb
@@ -48,6 +48,20 @@ module ActiveRecord
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 +77,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.
diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb
index 4d73cdd37a..a3412582fa 100644
--- a/activerecord/lib/active_record/locking/optimistic.rb
+++ b/activerecord/lib/active_record/locking/optimistic.rb
@@ -84,7 +84,7 @@ module ActiveRecord
relation.table[self.class.primary_key].eq(id).and(
relation.table[lock_col].eq(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 +101,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 +138,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+.
diff --git a/activerecord/lib/active_record/model.rb b/activerecord/lib/active_record/model.rb
index 86de5ab2fa..105d1e0e2b 100644
--- a/activerecord/lib/active_record/model.rb
+++ b/activerecord/lib/active_record/model.rb
@@ -60,7 +60,6 @@ module ActiveRecord
include AttributeMethods
include Callbacks, ActiveModel::Observing, Timestamp
include Associations
- include IdentityMap
include ActiveModel::SecurePassword
include AutosaveAssociation, NestedAttributes
include Aggregations, Transactions, Reflection, Serialization, Store
diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb
index b8764217d3..7f38dda11e 100644
--- a/activerecord/lib/active_record/model_schema.rb
+++ b/activerecord/lib/active_record/model_schema.rb
@@ -114,11 +114,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.
@@ -152,8 +159,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 +172,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 +191,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
@@ -296,7 +304,7 @@ 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
diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb
index 6bf0becad8..32a1dae6bc 100644
--- a/activerecord/lib/active_record/nested_attributes.rb
+++ b/activerecord/lib/active_record/nested_attributes.rb
@@ -288,7 +288,7 @@ 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
diff --git a/activerecord/lib/active_record/null_relation.rb b/activerecord/lib/active_record/null_relation.rb
index 60c37ac2b7..c2d3eeb8ce 100644
--- a/activerecord/lib/active_record/null_relation.rb
+++ b/activerecord/lib/active_record/null_relation.rb
@@ -6,5 +6,58 @@ module ActiveRecord
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
diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb
index 9bc046c775..4a987c2343 100644
--- a/activerecord/lib/active_record/persistence.rb
+++ b/activerecord/lib/active_record/persistence.rb
@@ -115,10 +115,7 @@ 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
@@ -127,20 +124,7 @@ module ActiveRecord
# that no changes should be made (since they can't be persisted).
def destroy
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
@@ -176,7 +160,7 @@ 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
@@ -191,10 +175,10 @@ module ActiveRecord
# 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)
+ verify_readonly_attribute(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
+ self.class.where(self.class.primary_key => id).update_all(name => value) == 1
end
# Updates the attributes of the model from the passed-in hash and saves the
@@ -284,11 +268,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 +311,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 +329,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 +354,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 +364,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/querying.rb b/activerecord/lib/active_record/querying.rb
index 0e6fecbc4b..4d8283bcff 100644
--- a/activerecord/lib/active_record/querying.rb
+++ b/activerecord/lib/active_record/querying.rb
@@ -3,14 +3,15 @@ require 'active_support/deprecation'
module ActiveRecord
module Querying
- delegate :find, :first, :first!, :last, :last!, :all, :exists?, :any?, :many?, :to => :scoped
+ delegate :find, :take, :take!, :first, :first!, :last, :last!, :all, :exists?, :any?, :many?, :to => :scoped
delegate :first_or_create, :first_or_create!, :first_or_initialize, :to => :scoped
+ delegate :find_by, :find_by!, :to => :scoped
delegate :destroy, :destroy_all, :delete, :delete_all, :update, :update_all, :to => :scoped
delegate :find_each, :find_in_batches, :to => :scoped
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
+ delegate :count, :average, :minimum, :maximum, :sum, :calculate, :pluck, :ids, :to => :scoped
# 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
diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb
index 058dd58efb..eb2769f1ef 100644
--- a/activerecord/lib/active_record/railtie.rb
+++ b/activerecord/lib/active_record/railtie.rb
@@ -53,11 +53,6 @@ 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)
- end
-
initializer "active_record.set_configs" do |app|
ActiveSupport.on_load(:active_record) do
if app.config.active_record.delete(:whitelist_attributes)
@@ -73,7 +68,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
@@ -107,7 +104,7 @@ 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
@@ -115,6 +112,21 @@ module ActiveRecord
ActiveRecord::Base.instantiate_observers
end
end
+
+ ActiveSupport.on_load(:active_record) do
+ if app.config.use_schema_cache_dump
+ 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::Base.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
end
diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake
index 14bb0a724e..f26e18b1e0 100644
--- a/activerecord/lib/active_record/railties/databases.rake
+++ b/activerecord/lib/active_record/railties/databases.rake
@@ -372,6 +372,25 @@ db_namespace = namespace :db do
task :load_if_ruby => 'db:create' 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 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 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
@@ -407,6 +426,7 @@ db_namespace = namespace :db do
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"
@@ -458,7 +478,7 @@ 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 structure.sql file"
@@ -546,7 +566,7 @@ 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
+ railties = {}
Rails.application.railties.all do |railtie|
next unless to_load == :all || to_load.include?(railtie.railtie_name)
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/reflection.rb b/activerecord/lib/active_record/reflection.rb
index 8f8a3ec3bb..c380b5c029 100644
--- a/activerecord/lib/active_record/reflection.rb
+++ b/activerecord/lib/active_record/reflection.rb
@@ -23,11 +23,11 @@ module ActiveRecord
module ClassMethods
def create_reflection(macro, name, 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, options, active_record)
+ when :composed_of
+ reflection = AggregateReflection.new(macro, name, options, active_record)
end
self.reflections = self.reflections.merge(name => reflection)
@@ -44,7 +44,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 +69,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 +79,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
@@ -152,6 +153,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
@@ -452,7 +457,6 @@ module ActiveRecord
# Recursively fill out the rest of the array from the through reflection
conditions += through_conditions
- # And return
conditions
end
end
diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb
index 7531e1fe6f..779e052e3c 100644
--- a/activerecord/lib/active_record/relation.rb
+++ b/activerecord/lib/active_record/relation.rb
@@ -6,28 +6,30 @@ 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 :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)
@@ -78,7 +80,8 @@ module ActiveRecord
end
def initialize_copy(other)
- @bind_values = @bind_values.dup
+ @values = @values.dup
+ @values[:bind] = @values[:bind].dup if @values[:bind]
reset
end
@@ -168,23 +171,17 @@ module ActiveRecord
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
+ @records = eager_loading? ? find_with_associations : @klass.find_by_sql(arel, bind_values)
- preload = @preload_values
- preload += @includes_values unless eager_loading?
+ 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
+ readonly = readonly_value.nil? ? @implicit_readonly : readonly_value
@records.each { |record| record.readonly! } if readonly
else
@records = default_scoped.to_a
@@ -224,7 +221,7 @@ module ActiveRecord
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
@@ -239,7 +236,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
@@ -260,40 +260,26 @@ module ActiveRecord
# 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]
- #
- # # 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
+ # # Update all books that match conditions, but limit it to 5 ordered by date
# 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)
+ stmt = Arel::UpdateManager.new(arel.engine)
- stmt.set Arel.sql(@klass.send(:sanitize_sql_for_assignment, updates))
- stmt.table(table)
- stmt.key = table[primary_key]
+ stmt.set Arel.sql(@klass.send(:sanitize_sql_for_assignment, updates))
+ stmt.table(table)
+ stmt.key = table[primary_key]
- 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
+ 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.
@@ -404,12 +390,21 @@ module ActiveRecord
# If you need to destroy dependent associations or call your <tt>before_*</tt> or
# +after_destroy+ callbacks, use the +destroy_all+ method instead.
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,7 +432,6 @@ 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
@@ -455,7 +449,7 @@ module ActiveRecord
end
def to_sql
- @to_sql ||= klass.connection.to_sql(arel, @bind_values.dup)
+ @to_sql ||= klass.connection.to_sql(arel, bind_values.dup)
end
def where_values_hash
@@ -477,8 +471,8 @@ module ActiveRecord
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.
@@ -486,7 +480,7 @@ 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
def ==(other)
@@ -516,6 +510,14 @@ module ActiveRecord
end
end
+ def blank?
+ to_a.blank?
+ end
+
+ def values
+ @values.dup
+ end
+
private
def references_eager_loaded_tables?
diff --git a/activerecord/lib/active_record/relation/batches.rb b/activerecord/lib/active_record/relation/batches.rb
index 2fd89882ff..15f838a5ab 100644
--- a/activerecord/lib/active_record/relation/batches.rb
+++ b/activerecord/lib/active_record/relation/batches.rb
@@ -46,19 +46,14 @@ module ActiveRecord
# 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
batch_size = options.delete(:batch_size) || 1000
diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb
index 510d01085d..3ce9995031 100644
--- a/activerecord/lib/active_record/relation/calculations.rb
+++ b/activerecord/lib/active_record/relation/calculations.rb
@@ -3,56 +3,19 @@ require 'active_support/core_ext/object/try'
module ActiveRecord
module Calculations
- # Count operates using three different approaches.
+ # Count the records.
#
- # * 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.
+ # Person.count
+ # # => the total count of all people
#
- # The third approach, count using options, accepts an option hash as the only parameter. The options are:
+ # Person.count(:age)
+ # # => returns the total count of all people whose age is present in database
#
- # * <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).
+ # Person.count(:all)
+ # # => performs a COUNT(*) (:all is an alias for '*')
#
- # 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.
+ # Person.count(:age, distinct: true)
+ # # => counts the number of different age values
def count(column_name = nil, options = {})
column_name, options = nil, column_name if column_name.is_a?(Hash)
calculate(:count, column_name, options)
@@ -98,21 +61,22 @@ module ActiveRecord
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,47 +84,25 @@ 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
-
- 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
+ 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
- relation.calculate(operation, column_name, options)
+ perform_calculation(operation, column_name, options)
end
+ else
+ relation.calculate(operation, column_name, options)
end
rescue ThrowResult
0
@@ -174,7 +116,7 @@ module ActiveRecord
#
# 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.where(:age => 21).limit(5).pluck(:id) # SELECT people.id FROM people WHERE people.age = 21 LIMIT 5
#
def pluck(column_name)
key = column_name.to_s.split('.', 2).last
@@ -197,6 +139,16 @@ module ActiveRecord
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 perform_calculation(operation, column_name, options = {})
@@ -216,7 +168,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)
@@ -259,7 +211,7 @@ module ActiveRecord
end
def execute_grouped_calculation(operation, column_name, distinct) #:nodoc:
- group_attr = @group_values
+ 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)
@@ -282,7 +234,7 @@ 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}"
@@ -299,7 +251,7 @@ module ActiveRecord
key_records = Hash[key_records.map { |r| [r.id, r] }]
end
- ActiveSupport::OrderedHash[calculated_data.map do |row|
+ Hash[calculated_data.map do |row|
key = group_columns.map { |aliaz, column|
type_cast_calculated_value(row[aliaz], column)
}
@@ -347,8 +299,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/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb
index 4cd703e0a5..cc716bbfd1 100644
--- a/activerecord/lib/active_record/relation/finder_methods.rb
+++ b/activerecord/lib/active_record/relation/finder_methods.rb
@@ -3,83 +3,24 @@ 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.
- # * 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".
+ # 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+.
#
# ==== 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 +34,66 @@ 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.
+ #
+ # Examples:
+ #
+ # 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.
+ #
+ # Examples:
+ #
+ # 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
+ 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 +106,20 @@ 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.
+ #
+ # Examples:
+ #
+ # 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
+ 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,10 +132,16 @@ 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
+ # Examples:
+ #
+ # 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
+ def all
+ to_a
end
# Returns true if a record exists in the table that matches the +id+ or
@@ -200,7 +185,7 @@ module ActiveRecord
relation = relation.where(table[primary_key].eq(id)) if id
end
- connection.select_value(relation, "#{name} Exists", relation.bind_values) ? true : false
+ connection.select_value(relation, "#{name} Exists", relation.bind_values)
end
protected
@@ -215,12 +200,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 +243,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 +265,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
+ record = relation.take
unless record
conditions = arel.where_sql
@@ -347,15 +284,15 @@ module ActiveRecord
result = where(table[primary_key].in(ids)).all
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 +307,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 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 +336,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..3f880ce5e9
--- /dev/null
+++ b/activerecord/lib/active_record/relation/merger.rb
@@ -0,0 +1,121 @@
+require 'active_support/core_ext/object/blank'
+require 'active_support/core_ext/hash/keys'
+
+module ActiveRecord
+ class Relation
+ class HashMerger
+ 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
+ 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]
+ 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.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 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_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..b40bf2b3cf 100644
--- a/activerecord/lib/active_record/relation/predicate_builder.rb
+++ b/activerecord/lib/active_record/relation/predicate_builder.rb
@@ -1,7 +1,7 @@
module ActiveRecord
class PredicateBuilder # :nodoc:
def self.build_from_hash(engine, attributes, default_table)
- predicates = attributes.map do |column, value|
+ attributes.map do |column, value|
table = default_table
if value.is_a?(Hash)
@@ -17,20 +17,18 @@ module ActiveRecord
build(table[column.to_sym], value)
end
- end
- predicates.flatten
+ end.flatten
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
@@ -41,26 +39,27 @@ module ActiveRecord
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 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..855477eaed 100644
--- a/activerecord/lib/active_record/relation/query_methods.rb
+++ b/activerecord/lib/active_record/relation/query_methods.rb
@@ -5,37 +5,67 @@ 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)
+ @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
+ #
+ def #{name}_value=(value) # def readonly_value=(value)
+ @values[:#{name}] = value # @values[:readonly] = value
+ end # end
+ CODE
+ end
+
+ def create_with_value
+ @values[:create_with] || {}
+ end
+
+ def create_with_value=(value)
+ @values[:create_with] = value
+ end
+
+ alias extensions extending_values
def includes(*args)
- args.reject! {|a| a.blank? }
+ args.empty? ? self : clone.includes!(*args)
+ end
- return self if args.empty?
+ 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
def eager_load(*args)
- return self if args.blank?
+ args.blank? ? self : clone.eager_load!(*args)
+ end
- relation = clone
- relation.eager_load_values += args
- relation
+ def eager_load!(*args)
+ self.eager_load_values += args
+ self
end
def preload(*args)
- return self if args.blank?
+ args.blank? ? self : clone.preload!(*args)
+ end
- relation = clone
- relation.preload_values += args
- relation
+ def preload!(*args)
+ self.preload_values += args
+ self
end
# Used to indicate that an association is referenced by an SQL string, and should
@@ -49,11 +79,12 @@ module ActiveRecord
# User.includes(:posts).where("posts.name = 'foo'").references(:posts)
# # => Query now knows the string references posts, so adds a JOIN
def references(*args)
- return self if args.blank?
+ args.blank? ? self : clone.references!(*args)
+ end
- relation = clone
- relation.references_values = (references_values + args.flatten.map(&:to_s)).uniq
- relation
+ def references!(*args)
+ self.references_values = (references_values + args.flatten.map(&:to_s)).uniq
+ self
end
# Works in two unique ways.
@@ -87,34 +118,40 @@ module ActiveRecord
# => ActiveModel::MissingAttributeError: missing attribute: other_field
def select(value = Proc.new)
if block_given?
- to_a.select {|*block_args| value.call(*block_args) }
+ to_a.select { |*block_args| value.call(*block_args) }
else
- relation = clone
- relation.select_values += Array.wrap(value)
- relation
+ clone.select!(value)
end
end
+ def select!(value)
+ self.select_values += Array.wrap(value)
+ self
+ end
+
def group(*args)
- return self if args.blank?
+ args.blank? ? self : clone.group!(*args)
+ end
- relation = clone
- relation.group_values += args.flatten
- relation
+ def group!(*args)
+ self.group_values += args.flatten
+ self
end
def order(*args)
- return self if args.blank?
+ args.blank? ? self : clone.order!(*args)
+ end
+ def order!(*args)
args = args.flatten
+
references = args.reject { |arg| Arel::Node === arg }
.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
end
# Replaces any existing order defined on the relation with the specified order.
@@ -128,72 +165,88 @@ module ActiveRecord
# generates a query with 'ORDER BY id ASC, name ASC'.
#
def reorder(*args)
- return self if args.blank?
+ args.blank? ? self : clone.reorder!(*args)
+ end
- relation = clone
- relation.reordering_value = true
- relation.order_values = args.flatten
- relation
+ def reorder!(*args)
+ self.reordering_value = true
+ self.order_values = args.flatten
+ self
end
def joins(*args)
- return self if args.compact.blank?
-
- relation = clone
+ args.compact.blank? ? self : clone.joins!(*args)
+ end
+ 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
+ clone.bind!(value)
+ end
+
+ def bind!(value)
+ self.bind_values += [value]
+ self
end
def where(opts, *rest)
- return self if opts.blank?
+ opts.blank? ? self : clone.where!(opts, *rest)
+ end
- relation = clone
- relation = relation.references(PredicateBuilder.references(opts)) if Hash === opts
- relation.where_values += build_where(opts, rest)
- relation
+ def where!(opts, *rest)
+ references!(PredicateBuilder.references(opts)) if Hash === opts
+
+ self.where_values += build_where(opts, rest)
+ self
end
def having(opts, *rest)
- return self if opts.blank?
+ opts.blank? ? self : clone.having!(opts, *rest)
+ end
- relation = clone
- relation = relation.references(PredicateBuilder.references(opts)) if Hash === opts
- relation.having_values += build_where(opts, rest)
- relation
+ def having!(opts, *rest)
+ references!(PredicateBuilder.references(opts)) if Hash === opts
+
+ self.having_values += build_where(opts, rest)
+ self
end
def limit(value)
- relation = clone
- relation.limit_value = value
- relation
+ clone.limit!(value)
+ end
+
+ def limit!(value)
+ self.limit_value = value
+ self
end
def offset(value)
- relation = clone
- relation.offset_value = value
- relation
+ clone.offset!(value)
+ end
+
+ def offset!(value)
+ self.offset_value = value
+ self
end
def lock(locks = true)
- relation = clone
+ clone.lock!(locks)
+ end
+ 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
@@ -230,21 +283,30 @@ module ActiveRecord
end
def readonly(value = true)
- relation = clone
- relation.readonly_value = value
- relation
+ clone.readonly!(value)
+ end
+
+ def readonly!(value = true)
+ self.readonly_value = value
+ self
end
def create_with(value)
- relation = clone
- relation.create_with_value = value ? create_with_value.merge(value) : {}
- relation
+ clone.create_with!(value)
+ end
+
+ def create_with!(value)
+ self.create_with_value = value ? create_with_value.merge(value) : {}
+ self
end
def from(value)
- relation = clone
- relation.from_value = value
- relation
+ clone.from!(value)
+ end
+
+ def from!(value)
+ self.from_value = value
+ self
end
# Specifies whether the records should be unique or not. For example:
@@ -258,9 +320,12 @@ 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
+ clone.uniq!(value)
+ end
+
+ def uniq!(value = true)
+ self.uniq_value = value
+ self
end
# Used to extend a scope with additional methods, either through
@@ -299,20 +364,30 @@ module ActiveRecord
# # 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
+ clone.extending!(*modules, &block)
+ else
+ self
+ end
+ end
- return self if modules.empty?
+ 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
def reverse_order
- relation = clone
- relation.reverse_order_value = !relation.reverse_order_value
- relation
+ clone.reverse_order!
+ end
+
+ def reverse_order!
+ self.reverse_order_value = !reverse_order_value
+ self
end
def arel
@@ -322,26 +397,26 @@ module ActiveRecord
def build_arel
arel = table.from 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(from_value) if from_value
+ arel.lock(lock_value) if lock_value
arel
end
@@ -443,13 +518,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 03ba8c8628..f6d178db7a 100644
--- a/activerecord/lib/active_record/relation/spawn_methods.rb
+++ b/activerecord/lib/active_record/relation/spawn_methods.rb
@@ -1,81 +1,37 @@
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, :binds]).each do |method|
- value = r.send(:"#{method}_values")
- next if value.empty?
-
- value += merged_relation.send(:"#{method}_values")
- merged_relation.send :"#{method}_values=", value
- end
-
- merged_relation.joins_values += r.joins_values
-
- merged_wheres = @where_values + r.where_values
-
- merged_binds = (@bind_values + r.bind_values).uniq(&:first)
-
- 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
- merged_relation.bind_values = merged_binds
-
- (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?
- 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
+
+ # 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!)
+ #
+ def merge(other)
+ if other.is_a?(Array)
+ to_a & other
+ elsif other
+ clone.merge!(other)
else
- # merge in order_values from r
- merged_relation.order_values += r.order_values
+ self
end
+ end
- # Apply scope extension modules
- merged_relation.send :apply_modules, r.extensions
-
- merged_relation
+ def merge!(other)
+ klass = other.is_a?(Hash) ? Relation::HashMerger : Relation::Merger
+ klass.new(self, other).merge
end
# Removes from the query the condition(s) specified in +skips+.
@@ -86,20 +42,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 = self.class.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
@@ -111,44 +56,11 @@ module ActiveRecord
# Post.order('id asc').only(:where, :order) # uses the specified order
#
def only(*onlies)
- result = self.class.new(@klass, table)
+ result = self.class.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..fd276ccf5d 100644
--- a/activerecord/lib/active_record/result.rb
+++ b/activerecord/lib/active_record/result.rb
@@ -28,6 +28,7 @@ module ActiveRecord
alias :map! :map
alias :collect! :map
+ # Returns true if there are no records.
def empty?
rows.empty?
end
diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb
index 2d7d83d160..5530be3219 100644
--- a/activerecord/lib/active_record/sanitization.rb
+++ b/activerecord/lib/active_record/sanitization.rb
@@ -37,9 +37,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 +57,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
diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb
index dcbd165e58..7cbe2db408 100644
--- a/activerecord/lib/active_record/schema_dumper.rb
+++ b/activerecord/lib/active_record/schema_dumper.rb
@@ -40,7 +40,7 @@ module ActiveRecord
def header(stream)
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
diff --git a/activerecord/lib/active_record/scoping.rb b/activerecord/lib/active_record/scoping.rb
index a8f5e96190..66a486ae0a 100644
--- a/activerecord/lib/active_record/scoping.rb
+++ b/activerecord/lib/active_record/scoping.rb
@@ -10,118 +10,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 +17,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..db833fc7f1 100644
--- a/activerecord/lib/active_record/scoping/default.rb
+++ b/activerecord/lib/active_record/scoping/default.rb
@@ -1,4 +1,5 @@
require 'active_support/concern'
+require 'active_support/deprecation'
module ActiveRecord
module Scoping
@@ -30,7 +31,7 @@ module ActiveRecord
# Post.limit(10) # Fires "SELECT * FROM posts LIMIT 10"
# }
#
- # It is recommended to use the block form of unscoped because chaining
+ # 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.
@@ -51,7 +52,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 +63,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 +70,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 +87,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..2af476c1ba 100644
--- a/activerecord/lib/active_record/scoping/named.rb
+++ b/activerecord/lib/active_record/scoping/named.rb
@@ -3,6 +3,7 @@ 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'
+require 'active_support/deprecation'
module ActiveRecord
# = Active Record Named \Scopes
@@ -28,17 +29,16 @@ module ActiveRecord
# 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)
+ if current_scope
+ 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
+
+ scope.merge!(options) if options
+ scope
end
##
@@ -48,7 +48,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,30 +171,28 @@ 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?
+ def scope(name, body, &block)
+ extension = Module.new(&block) if block
- 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)
+ # Check body.is_a?(Relation) to prevent the relation actually being
+ # loaded by respond_to?
+ if body.is_a?(Relation) || !body.respond_to?(:call)
+ ActiveSupport::Deprecation.warn(
+ "Using #scope without passing a callable object is deprecated. For " \
+ "example `scope :red, where(color: 'red')` should be changed to " \
+ "`scope :red, -> { where(color: 'red') }`. There are numerous gotchas " \
+ "in the former usage and it makes the implementation more complicated " \
+ "and buggy. (If you prefer, you can just define a class method named " \
+ "`self.red`.)"
+ )
+ end
+ singleton_class.send(:define_method, name) do |*args|
+ options = body.respond_to?(:call) ? unscoped { body.call(*args) } : body
relation = scoped.merge(options)
extension ? relation.extending(extension) : relation
end
-
- singleton_class.send(:redefine_method, name, &scope_proc)
- end
-
- protected
-
- def valid_scope_name?(name)
- if respond_to?(name, true)
- logger.warn "Creating scope :#{name}. " \
- "Overwriting existing method #{self.name}.#{name}."
- end
end
end
end
diff --git a/activerecord/lib/active_record/session_store.rb b/activerecord/lib/active_record/session_store.rb
index ce43ae8066..ed47a26749 100644
--- a/activerecord/lib/active_record/session_store.rb
+++ b/activerecord/lib/active_record/session_store.rb
@@ -119,7 +119,7 @@ module ActiveRecord
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}
+ where(session_id: session_id).first
end
end
end
diff --git a/activerecord/lib/active_record/test_case.rb b/activerecord/lib/active_record/test_case.rb
index 4d881f0f7d..fcaa4b74a6 100644
--- a/activerecord/lib/active_record/test_case.rb
+++ b/activerecord/lib/active_record/test_case.rb
@@ -7,20 +7,10 @@ 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
- end
-
def assert_date_from_db(expected, actual, message = nil)
# SybaseAdapter doesn't have a separate column type just for dates,
# so the time is in the string and incorrectly formatted
diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb
index b492377d18..64e5640791 100644
--- a/activerecord/lib/active_record/transactions.rb
+++ b/activerecord/lib/active_record/transactions.rb
@@ -251,7 +251,6 @@ module ActiveRecord
remember_transaction_record_state
yield
rescue Exception
- IdentityMap.remove(self) if IdentityMap.enabled?
restore_transaction_record_state
raise
ensure
@@ -270,7 +269,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
@@ -304,12 +302,8 @@ module ActiveRecord
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
diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb
index 4b075183c3..d06020b3ce 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
diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb
index 9556878f63..9e4b588ac2 100644
--- a/activerecord/lib/active_record/validations/uniqueness.rb
+++ b/activerecord/lib/active_record/validations/uniqueness.rb
@@ -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
@@ -102,6 +108,14 @@ module ActiveRecord
# 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,
# the same check is made but disregarding the record itself.
@@ -109,6 +123,8 @@ module ActiveRecord
# 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>: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+).
@@ -180,7 +196,6 @@ module ActiveRecord
# The following bundled adapters throw the ActiveRecord::RecordNotUnique exception:
# * ActiveRecord::ConnectionAdapters::MysqlAdapter
# * ActiveRecord::ConnectionAdapters::Mysql2Adapter
- # * ActiveRecord::ConnectionAdapters::SQLiteAdapter
# * ActiveRecord::ConnectionAdapters::SQLite3Adapter
# * ActiveRecord::ConnectionAdapters::PostgreSQLAdapter
#
diff --git a/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb b/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb
index d084a00ed7..b9b5ec7956 100644
--- a/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb
+++ b/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb
@@ -5,7 +5,7 @@ class <%= migration_class_name %> < ActiveRecord::Migration
add_column :<%= table_name %>, :<%= attribute.name %>, :<%= attribute.type %><%= attribute.inject_options %>
<%- if attribute.has_index? -%>
add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %>
- <%- end %>
+ <%- end -%>
<%- end -%>
end
<%- else -%>
@@ -13,9 +13,9 @@ class <%= migration_class_name %> < ActiveRecord::Migration
<% attributes.each do |attribute| -%>
<%- if migration_action -%>
<%= migration_action %>_column :<%= table_name %>, :<%= attribute.name %><% if migration_action == 'add' %>, :<%= attribute.type %><%= attribute.inject_options %><% end %>
- <% if attribute.has_index? && migration_action == 'add' %>
+ <%- if attribute.has_index? && migration_action == 'add' -%>
add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %>
- <% end -%>
+ <%- end -%>
<%- end -%>
<%- end -%>
end
@@ -24,6 +24,9 @@ class <%= migration_class_name %> < ActiveRecord::Migration
<% attributes.reverse.each do |attribute| -%>
<%- if migration_action -%>
<%= migration_action == 'add' ? 'remove' : 'add' %>_column :<%= table_name %>, :<%= attribute.name %><% if migration_action == 'remove' %>, :<%= attribute.type %><%= attribute.inject_options %><% end %>
+ <%- if attribute.has_index? && migration_action == 'remove' -%>
+ add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %>
+ <%- end -%>
<%- end -%>
<%- end -%>
end
diff --git a/activerecord/lib/rails/generators/active_record/model/model_generator.rb b/activerecord/lib/rails/generators/active_record/model/model_generator.rb
index 99a022461e..8e6ef20285 100644
--- a/activerecord/lib/rails/generators/active_record/model/model_generator.rb
+++ b/activerecord/lib/rails/generators/active_record/model/model_generator.rb
@@ -14,6 +14,7 @@ module ActiveRecord
def create_migration_file
return unless options[:migration] && options[:parent].nil?
+ attributes.each { |a| a.attr_options.delete(:index) if a.reference? && !a.has_index? } if options[:indexes] == false
migration_template "migration.rb", "db/migrate/create_#{table_name}.rb"
end
@@ -27,7 +28,11 @@ module ActiveRecord
end
def attributes_with_index
- attributes.select { |a| a.has_index? || (a.reference? && options[:indexes]) }
+ attributes.select { |a| !a.reference? && a.has_index? }
+ end
+
+ def accessible_attributes
+ attributes.reject(&:reference?)
end
hook_for :test_framework
diff --git a/activerecord/lib/rails/generators/active_record/model/templates/model.rb b/activerecord/lib/rails/generators/active_record/model/templates/model.rb
index 5c47f8b241..d56f9f57a4 100644
--- a/activerecord/lib/rails/generators/active_record/model/templates/model.rb
+++ b/activerecord/lib/rails/generators/active_record/model/templates/model.rb
@@ -3,5 +3,10 @@ class <%= class_name %> < <%= parent_class_name.classify %>
<% attributes.select {|attr| attr.reference? }.each do |attribute| -%>
belongs_to :<%= attribute.name %>
<% end -%>
+<% if !accessible_attributes.empty? -%>
+ attr_accessible <%= accessible_attributes.map {|a| ":#{a.name}" }.sort.join(', ') %>
+<% else -%>
+ # attr_accessible :title, :body
+<% end -%>
end
<% end -%>