aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib
diff options
context:
space:
mode:
Diffstat (limited to 'activerecord/lib')
-rw-r--r--activerecord/lib/active_record.rb2
-rwxr-xr-xactiverecord/lib/active_record/associations.rb26
-rw-r--r--activerecord/lib/active_record/associations/association_collection.rb36
-rw-r--r--activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb44
-rw-r--r--activerecord/lib/active_record/associations/has_one_association.rb4
-rw-r--r--activerecord/lib/active_record/autosave_association.rb40
-rwxr-xr-xactiverecord/lib/active_record/base.rb226
-rw-r--r--activerecord/lib/active_record/calculations.rb200
-rw-r--r--activerecord/lib/active_record/callbacks.rb59
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb12
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb6
-rw-r--r--activerecord/lib/active_record/controller_runtime.rb27
-rw-r--r--activerecord/lib/active_record/nested_attributes.rb48
-rw-r--r--activerecord/lib/active_record/rails.rb11
-rw-r--r--activerecord/lib/active_record/rails/databases.rake469
-rw-r--r--activerecord/lib/active_record/reflection.rb18
-rw-r--r--activerecord/lib/active_record/relation.rb195
-rw-r--r--activerecord/lib/active_record/relational_calculations.rb177
-rw-r--r--activerecord/lib/active_record/validations/associated.rb16
-rw-r--r--activerecord/lib/active_record/validations/uniqueness.rb134
20 files changed, 1161 insertions, 589 deletions
diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb
index 196b87c0ac..7031c67539 100644
--- a/activerecord/lib/active_record.rb
+++ b/activerecord/lib/active_record.rb
@@ -48,10 +48,12 @@ module ActiveRecord
autoload :Attributes
autoload :AutosaveAssociation
autoload :Relation
+ autoload :RelationalCalculations
autoload :Base
autoload :Batches
autoload :Calculations
autoload :Callbacks
+ autoload :ControllerRuntime
autoload :DynamicFinderMatch
autoload :DynamicScopeMatch
autoload :Migration
diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb
index 2735bc5141..f0bad6c3ba 100755
--- a/activerecord/lib/active_record/associations.rb
+++ b/activerecord/lib/active_record/associations.rb
@@ -3,8 +3,8 @@ require 'active_support/core_ext/enumerable'
module ActiveRecord
class InverseOfAssociationNotFoundError < ActiveRecordError #:nodoc:
- def initialize(reflection)
- super("Could not find the inverse association for #{reflection.name} (#{reflection.options[:inverse_of].inspect} in #{reflection.class_name})")
+ def initialize(reflection, associated_class = nil)
+ super("Could not find the inverse association for #{reflection.name} (#{reflection.options[:inverse_of].inspect} in #{associated_class.nil? ? reflection.class_name : associated_class.name})")
end
end
@@ -1466,11 +1466,10 @@ module ActiveRecord
end
def find_with_associations(options = {}, join_dependency = nil)
- catch :invalid_query do
- join_dependency ||= JoinDependency.new(self, merge_includes(scope(:find, :include), options[:include]), options[:joins])
- rows = select_all_rows(options, join_dependency)
- return join_dependency.instantiate(rows)
- end
+ join_dependency ||= JoinDependency.new(self, merge_includes(scope(:find, :include), options[:include]), options[:joins])
+ rows = select_all_rows(options, join_dependency)
+ join_dependency.instantiate(rows)
+ rescue ThrowResult
[]
end
@@ -1707,7 +1706,7 @@ module ActiveRecord
def construct_finder_arel_with_included_associations(options, join_dependency)
scope = scope(:find)
- relation = arel_table((scope && scope[:from]) || options[:from])
+ relation = arel_table
for association in join_dependency.join_associations
relation = association.join_relation(relation)
@@ -1715,9 +1714,11 @@ module ActiveRecord
relation = relation.joins(construct_join(options[:joins], scope)).
select(column_aliases(join_dependency)).
- group(construct_group(options[:group], options[:having], scope)).
+ group(options[:group] || (scope && scope[:group])).
+ having(options[:having] || (scope && scope[:having])).
order(construct_order(options[:order], scope)).
- where(construct_conditions(options[:conditions], scope))
+ where(construct_conditions(options[:conditions], scope)).
+ from((scope && scope[:from]) || options[:from])
relation = relation.where(construct_arel_limited_ids_condition(options, join_dependency)) if !using_limitable_reflections?(join_dependency.reflections) && ((scope && scope[:limit]) || options[:limit])
relation = relation.limit(construct_limit(options[:limit], scope)) if using_limitable_reflections?(join_dependency.reflections)
@@ -1731,7 +1732,7 @@ module ActiveRecord
def construct_arel_limited_ids_condition(options, join_dependency)
if (ids_array = select_limited_ids_array(options, join_dependency)).empty?
- throw :invalid_query
+ raise ThrowResult
else
Arel::Predicates::In.new(
Arel::SqlLiteral.new("#{connection.quote_table_name table_name}.#{primary_key}"),
@@ -1758,7 +1759,8 @@ module ActiveRecord
relation = relation.joins(construct_join(options[:joins], scope)).
where(construct_conditions(options[:conditions], scope)).
- group(construct_group(options[:group], options[:having], scope)).
+ group(options[:group] || (scope && scope[:group])).
+ having(options[:having] || (scope && scope[:having])).
order(construct_order(options[:order], scope)).
limit(construct_limit(options[:limit], scope)).
offset(construct_limit(options[:offset], scope)).
diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb
index b85d76e8d3..b2b3a9789c 100644
--- a/activerecord/lib/active_record/associations/association_collection.rb
+++ b/activerecord/lib/active_record/associations/association_collection.rb
@@ -21,7 +21,7 @@ module ActiveRecord
construct_sql
end
- delegate :group, :order, :limit, :joins, :where, :preload, :eager_load, :to => :scoped
+ delegate :group, :order, :limit, :joins, :where, :preload, :eager_load, :from, :lock, :readonly, :having, :to => :scoped
def select(select = nil, &block)
if block_given?
@@ -52,27 +52,21 @@ module ActiveRecord
load_target.select { |r| ids.include?(r.id) }
end
else
- conditions = "#{@finder_sql}"
- if sanitized_conditions = sanitize_sql(options[:conditions])
- conditions << " AND (#{sanitized_conditions})"
- end
-
- options[:conditions] = conditions
+ merge_options_from_reflection!(options)
+ construct_find_options!(options)
+
+ find_scope = construct_scope[:find].slice(:conditions, :order)
+
+ with_scope(:find => find_scope) do
+ relation = @reflection.klass.send(:construct_finder_arel_with_includes, options)
- if options[:order] && @reflection.options[:order]
- options[:order] = "#{options[:order]}, #{@reflection.options[:order]}"
- elsif @reflection.options[:order]
- options[:order] = @reflection.options[:order]
+ case args.first
+ when :first, :last, :all
+ relation.send(args.first)
+ else
+ relation.find(*args)
+ end
end
-
- # Build options specific to association
- construct_find_options!(options)
-
- merge_options_from_reflection!(options)
-
- # Pass through args exactly as we received them.
- args << options
- @reflection.klass.find(*args)
end
end
@@ -183,7 +177,7 @@ module ActiveRecord
if @reflection.options[:counter_sql]
@reflection.klass.count_by_sql(@counter_sql)
else
- column_name, options = @reflection.klass.send(:construct_count_options_from_args, *args)
+ column_name, options = @reflection.klass.scoped.send(:construct_count_options_from_args, *args)
if @reflection.options[:uniq]
# This is needed because 'SELECT count(DISTINCT *)..' is not valid SQL.
column_name = "#{@reflection.quoted_table_name}.#{@reflection.klass.primary_key}" if column_name == :all
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 67e18d692d..f6edd6383c 100644
--- a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb
+++ b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb
@@ -13,6 +13,7 @@ module ActiveRecord
@updated = true
end
+ set_inverse_instance(record, @owner)
loaded
record
end
@@ -22,21 +23,44 @@ module ActiveRecord
end
private
- def find_target
- return nil if association_class.nil?
- if @reflection.options[:conditions]
- association_class.find(
- @owner[@reflection.primary_key_name],
- :select => @reflection.options[:select],
- :conditions => conditions,
- :include => @reflection.options[:include]
- )
+ # NOTE - for now, we're only supporting inverse setting from belongs_to back onto
+ # has_one associations.
+ def we_can_set_the_inverse_on_this?(record)
+ if @reflection.has_inverse?
+ inverse_association = @reflection.polymorphic_inverse_of(record.class)
+ inverse_association && inverse_association.macro == :has_one
else
- association_class.find(@owner[@reflection.primary_key_name], :select => @reflection.options[:select], :include => @reflection.options[:include])
+ false
+ end
+ end
+
+ def set_inverse_instance(record, instance)
+ return if record.nil? || !we_can_set_the_inverse_on_this?(record)
+ inverse_relationship = @reflection.polymorphic_inverse_of(record.class)
+ unless inverse_relationship.nil?
+ record.send(:"set_#{inverse_relationship.name}_target", instance)
end
end
+ def find_target
+ return nil if association_class.nil?
+
+ target =
+ if @reflection.options[:conditions]
+ association_class.find(
+ @owner[@reflection.primary_key_name],
+ :select => @reflection.options[:select],
+ :conditions => conditions,
+ :include => @reflection.options[:include]
+ )
+ else
+ association_class.find(@owner[@reflection.primary_key_name], :select => @reflection.options[:select], :include => @reflection.options[:include])
+ end
+ set_inverse_instance(target, @owner)
+ target
+ end
+
def foreign_key_present
!@owner[@reflection.primary_key_name].nil?
end
diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb
index b85a40b2e5..ea769fd48b 100644
--- a/activerecord/lib/active_record/associations/has_one_association.rb
+++ b/activerecord/lib/active_record/associations/has_one_association.rb
@@ -57,6 +57,7 @@ module ActiveRecord
@target = (AssociationProxy === obj ? obj.target : obj)
end
+ set_inverse_instance(obj, @owner)
@loaded = true
unless @owner.new_record? or obj.nil? or dont_save
@@ -120,10 +121,9 @@ module ActiveRecord
else
record[@reflection.primary_key_name] = @owner.id unless @owner.new_record?
self.target = record
+ set_inverse_instance(record, @owner)
end
- set_inverse_instance(record, @owner)
-
record
end
diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb
index c0d8904bc8..44c668b619 100644
--- a/activerecord/lib/active_record/autosave_association.rb
+++ b/activerecord/lib/active_record/autosave_association.rb
@@ -155,6 +155,13 @@ module ActiveRecord
# Adds a validate and save callback for the association as specified by
# the +reflection+.
+ #
+ # For performance reasons, we don't check whether to validate at runtime,
+ # but instead only define the method and callback when needed. However,
+ # this can change, for instance, when using nested attributes. Since we
+ # don't want the callbacks to get defined multiple times, there are
+ # guards that check if the save or validation methods have already been
+ # defined before actually defining them.
def add_autosave_association_callbacks(reflection)
save_method = "autosave_associated_records_for_#{reflection.name}"
validation_method = "validate_associated_records_for_#{reflection.name}"
@@ -162,28 +169,33 @@ module ActiveRecord
case reflection.macro
when :has_many, :has_and_belongs_to_many
- before_save :before_save_collection_association
+ unless method_defined?(save_method)
+ before_save :before_save_collection_association
- define_method(save_method) { save_collection_association(reflection) }
- # Doesn't use after_save as that would save associations added in after_create/after_update twice
- after_create save_method
- after_update save_method
+ define_method(save_method) { save_collection_association(reflection) }
+ # Doesn't use after_save as that would save associations added in after_create/after_update twice
+ after_create save_method
+ after_update save_method
+ end
- if force_validation || (reflection.macro == :has_many && reflection.options[:validate] != false)
+ if !method_defined?(validation_method) &&
+ (force_validation || (reflection.macro == :has_many && reflection.options[:validate] != false))
define_method(validation_method) { validate_collection_association(reflection) }
validate validation_method
end
else
- case reflection.macro
- when :has_one
- define_method(save_method) { save_has_one_association(reflection) }
- after_save save_method
- when :belongs_to
- define_method(save_method) { save_belongs_to_association(reflection) }
- before_save save_method
+ unless method_defined?(save_method)
+ case reflection.macro
+ when :has_one
+ define_method(save_method) { save_has_one_association(reflection) }
+ after_save save_method
+ when :belongs_to
+ define_method(save_method) { save_belongs_to_association(reflection) }
+ before_save save_method
+ end
end
- if force_validation
+ if !method_defined?(validation_method) && force_validation
define_method(validation_method) { validate_single_association(reflection) }
validate validation_method
end
diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb
index 2008dea5e9..07c5545171 100755
--- a/activerecord/lib/active_record/base.rb
+++ b/activerecord/lib/active_record/base.rb
@@ -69,6 +69,10 @@ module ActiveRecord #:nodoc:
class StatementInvalid < ActiveRecordError
end
+ # Raised when SQL statement is invalid and the application gets a blank result.
+ class ThrowResult < ActiveRecordError
+ end
+
# Parent class for all specific exceptions which wrap database driver exceptions
# provides access to the original exception also.
class WrappedDatabaseException < StatementInvalid
@@ -640,18 +644,19 @@ module ActiveRecord #:nodoc:
# end
def find(*args)
options = args.extract_options!
- validate_find_options(options)
set_readonly_option!(options)
+ relation = construct_finder_arel_with_includes(options)
+
case args.first
- when :first then find_initial(options)
- when :last then find_last(options)
- when :all then find_every(options)
- else find_from_ids(args, options)
+ when :first, :last, :all
+ relation.send(args.first)
+ else
+ relation.find(*args)
end
end
- delegate :select, :group, :order, :limit, :joins, :where, :preload, :eager_load, :to => :scoped
+ delegate :select, :group, :order, :limit, :joins, :where, :preload, :eager_load, :from, :lock, :readonly, :having, :to => :scoped
# 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>.
@@ -665,16 +670,10 @@ module ActiveRecord #:nodoc:
find(:last, *args)
end
- # Returns an ActiveRecord::Relation object. You can pass in all the same arguments to this method as you can
- # to find(:all).
+ # 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)
- options = args.extract_options!
-
- if options.empty? && !scoped?(:find)
- arel_table.to_a
- else
- construct_finder_arel_with_includes(options).to_a
- end
+ find(:all, *args)
end
# Executes a custom SQL query against your database and returns all the results. The results will
@@ -728,10 +727,13 @@ module ActiveRecord #:nodoc:
# Person.exists?(:name => "David")
# Person.exists?(['name LIKE ?', "%#{query}%"])
# Person.exists?
- def exists?(id_or_conditions = {})
- find_initial(
- :select => "#{quoted_table_name}.#{primary_key}",
- :conditions => expand_id_conditions(id_or_conditions)) ? true : false
+ def exists?(id_or_conditions = nil)
+ case id_or_conditions
+ when Array, Hash
+ where(id_or_conditions).exists?
+ else
+ scoped.exists?(id_or_conditions)
+ end
end
# Creates an object (or multiple objects) and saves it to the database, if validations pass.
@@ -916,7 +918,7 @@ module ActiveRecord #:nodoc:
# Person.destroy_all("last_login < '2004-04-04'")
# Person.destroy_all(:status => "inactive")
def destroy_all(conditions = nil)
- find(:all, :conditions => conditions).each { |object| object.destroy }
+ where(conditions).destroy_all
end
# Deletes the records matching +conditions+ without instantiating the records first, and hence not
@@ -937,11 +939,7 @@ module ActiveRecord #:nodoc:
# Both calls delete the affected posts all at once with a single DELETE statement. If you need to destroy dependent
# associations or call your <tt>before_*</tt> or +after_destroy+ callbacks, use the +destroy_all+ method instead.
def delete_all(conditions = nil)
- if conditions
- arel_table.where(Arel::SqlLiteral.new(construct_conditions(conditions, scope(:find)))).delete
- else
- arel_table.delete
- end
+ arel_table.where(construct_conditions(conditions, scope(:find))).delete_all
end
# Returns the result of an SQL statement that should only include a COUNT(*) in the SELECT part.
@@ -1512,120 +1510,6 @@ module ActiveRecord #:nodoc:
end
private
- def find_initial(options)
- options.update(:limit => 1)
- find_every(options).first
- end
-
- def find_last(options)
- order = options[:order]
-
- if order
- order = reverse_sql_order(order)
- elsif !scoped?(:find, :order)
- order = "#{table_name}.#{primary_key} DESC"
- end
-
- if scoped?(:find, :order)
- scope = scope(:find)
- original_scoped_order = scope[:order]
- scope[:order] = reverse_sql_order(original_scoped_order)
- end
-
- begin
- find_initial(options.merge({ :order => order }))
- ensure
- scope[:order] = original_scoped_order if original_scoped_order
- end
- end
-
- def reverse_sql_order(order_query)
- order_query.to_s.split(/,/).each { |s|
- if s.match(/\s(asc|ASC)$/)
- s.gsub!(/\s(asc|ASC)$/, ' DESC')
- elsif s.match(/\s(desc|DESC)$/)
- s.gsub!(/\s(desc|DESC)$/, ' ASC')
- else
- s.concat(' DESC')
- end
- }.join(',')
- end
-
- def find_every(options)
- include_associations = merge_includes(scope(:find, :include), options[:include])
-
- if include_associations.any? && references_eager_loaded_tables?(options)
- records = find_with_associations(options)
- else
- records = find_by_sql(construct_finder_sql(options))
- if include_associations.any?
- preload_associations(records, include_associations)
- end
- end
-
- records.each { |record| record.readonly! } if options[:readonly]
-
- records
- end
-
- def find_from_ids(ids, options)
- expects_array = ids.first.kind_of?(Array)
- return ids.first if expects_array && ids.first.empty?
-
- ids = ids.flatten.compact.uniq
-
- case ids.size
- when 0
- raise RecordNotFound, "Couldn't find #{name} without an ID"
- when 1
- result = find_one(ids.first, options)
- expects_array ? [ result ] : result
- else
- find_some(ids, options)
- end
- end
-
- def find_one(id, options)
- conditions = " AND (#{sanitize_sql(options[:conditions])})" if options[:conditions]
- options.update :conditions => "#{quoted_table_name}.#{connection.quote_column_name(primary_key)} = #{quote_value(id,columns_hash[primary_key])}#{conditions}"
-
- # Use find_every(options).first since the primary key condition
- # already ensures we have a single record. Using find_initial adds
- # a superfluous :limit => 1.
- if result = find_every(options).first
- result
- else
- raise RecordNotFound, "Couldn't find #{name} with ID=#{id}#{conditions}"
- end
- end
-
- def find_some(ids, options)
- conditions = " AND (#{sanitize_sql(options[:conditions])})" if options[:conditions]
- ids_list = ids.map { |id| quote_value(id,columns_hash[primary_key]) }.join(',')
- options.update :conditions => "#{quoted_table_name}.#{connection.quote_column_name(primary_key)} IN (#{ids_list})#{conditions}"
-
- result = find_every(options)
-
- # Determine expected size from limit and offset, not just ids.size.
- expected_size =
- if options[:limit] && ids.size > options[:limit]
- options[:limit]
- else
- ids.size
- end
-
- # 11 ids with limit 3, offset 9 should give 2 results.
- if options[:offset] && (ids.size - options[:offset] < expected_size)
- expected_size = ids.size - options[:offset]
- end
-
- if result.size == expected_size
- result
- else
- raise RecordNotFound, "Couldn't find all #{name.pluralize} with IDs (#{ids_list})#{conditions} (found #{result.size} results, but was looking for #{expected_size})"
- end
- end
-
# Finder methods must instantiate through this method to work with the
# single-table inheritance model that makes it possible to create
# objects of different types from the same table.
@@ -1676,17 +1560,21 @@ module ActiveRecord #:nodoc:
end
def construct_finder_arel(options = {}, scope = scope(:find))
- # TODO add lock to Arel
validate_find_options(options)
- relation = arel_table(options[:from]).
+ relation = arel_table.
joins(construct_join(options[:joins], scope)).
where(construct_conditions(options[:conditions], scope)).
select(options[:select] || (scope && scope[:select]) || default_select(options[:joins] || (scope && scope[:joins]))).
- group(construct_group(options[:group], options[:having], scope)).
+ group(options[:group] || (scope && scope[:group])).
+ having(options[:having] || (scope && scope[:having])).
order(construct_order(options[:order], scope)).
limit(construct_limit(options[:limit], scope)).
- offset(construct_offset(options[:offset], scope))
+ offset(construct_offset(options[:offset], scope)).
+ from(options[:from])
+
+ lock = (scope && scope[:lock]) || options[:lock]
+ relation = relation.lock if lock.present?
relation = relation.readonly if options[:readonly]
@@ -1708,10 +1596,6 @@ module ActiveRecord #:nodoc:
relation
end
- def construct_finder_sql(options, scope = scope(:find))
- construct_finder_arel(options, scope).to_sql
- end
-
def construct_join(joins, scope)
merged_joins = scope && scope[:joins] && joins ? merge_joins(scope[:joins], joins) : (joins || scope && scope[:joins])
case merged_joins
@@ -1728,20 +1612,9 @@ module ActiveRecord #:nodoc:
end
end
- def construct_group(group, having, scope)
- sql = ''
- if group
- sql << group.to_s
- sql << " HAVING #{sanitize_sql_for_conditions(having)}" if having
- elsif scope && (scoped_group = scope[:group])
- sql << scoped_group.to_s
- sql << " HAVING #{sanitize_sql_for_conditions(scope[:having])}" if scope[:having]
- end
- sql
- end
-
def construct_order(order, scope)
orders = []
+
scoped_order = scope[:order] if scope
if order
orders << order
@@ -1749,7 +1622,8 @@ module ActiveRecord #:nodoc:
elsif scoped_order
orders << scoped_order
end
- orders
+
+ orders.reject {|o| o.blank?}
end
def construct_limit(limit, scope)
@@ -1771,7 +1645,7 @@ module ActiveRecord #:nodoc:
# Merges includes so that the result is a valid +include+
def merge_includes(first, second)
- (safe_to_array(first) + safe_to_array(second)).uniq
+ (Array.wrap(first) + Array.wrap(second)).uniq
end
def merge_joins(*joins)
@@ -1783,7 +1657,7 @@ module ActiveRecord #:nodoc:
end
joins.flatten.map{|j| j.strip}.uniq
else
- joins.collect{|j| safe_to_array(j)}.flatten.uniq
+ joins.collect{|j| Array.wrap(j)}.flatten.uniq
end
end
@@ -1800,30 +1674,10 @@ module ActiveRecord #:nodoc:
}.join(" ")
end
- # Object#to_a is deprecated, though it does have the desired behavior
- def safe_to_array(o)
- case o
- when NilClass
- []
- when Array
- o
- else
- [o]
- end
- end
-
def array_of_strings?(o)
o.is_a?(Array) && o.all?{|obj| obj.is_a?(String)}
end
- # The optional scope argument is for the current <tt>:find</tt> scope.
- # The <tt>:lock</tt> option has precedence over a scoped <tt>:lock</tt>.
- def add_lock!(sql, options, scope = :auto)
- scope = scope(:find) if :auto == scope
- options = options.reverse_merge(:lock => scope[:lock]) if scope
- connection.add_lock!(sql, options)
- end
-
def type_condition(table_alias=nil)
quoted_table_alias = self.connection.quote_table_name(table_alias || table_name)
quoted_inheritance_column = connection.quote_column_name(inheritance_column)
@@ -1925,14 +1779,6 @@ module ActiveRecord #:nodoc:
end
end
- # Interpret Array and Hash as conditions and anything else as an id.
- def expand_id_conditions(id_or_conditions)
- case id_or_conditions
- when Array, Hash then id_or_conditions
- else sanitize_sql(primary_key => id_or_conditions)
- end
- end
-
protected
# Scope parameters to method calls within the block. Takes a hash of method_name => parameters hash.
# method_name may be <tt>:find</tt> or <tt>:create</tt>. <tt>:find</tt> parameters may include the <tt>:conditions</tt>, <tt>:joins</tt>,
diff --git a/activerecord/lib/active_record/calculations.rb b/activerecord/lib/active_record/calculations.rb
index fcba23dc0d..d51d9f2159 100644
--- a/activerecord/lib/active_record/calculations.rb
+++ b/activerecord/lib/active_record/calculations.rb
@@ -44,7 +44,26 @@ module ActiveRecord
#
# Note: <tt>Person.count(:all)</tt> will not work because it will use <tt>:all</tt> as the condition. Use Person.count instead.
def count(*args)
- calculate(:count, *construct_count_options_from_args(*args))
+ case args.size
+ when 0
+ construct_calculation_arel.count
+ when 1
+ if args[0].is_a?(Hash)
+ options = args[0]
+ distinct = options.has_key?(:distinct) ? options.delete(:distinct) : false
+ construct_calculation_arel(options).count(options[:select], :distinct => distinct)
+ else
+ construct_calculation_arel.count(args[0])
+ end
+ when 2
+ column_name, options = args
+ distinct = options.has_key?(:distinct) ? options.delete(:distinct) : false
+ construct_calculation_arel(options).count(column_name, :distinct => distinct)
+ else
+ raise ArgumentError, "Unexpected parameters passed to count(): #{args.inspect}"
+ end
+ rescue ThrowResult
+ 0
end
# Calculates the average value on a given column. The value is returned as
@@ -122,168 +141,63 @@ module ActiveRecord
# Person.minimum(:age, :having => 'min(age) > 17', :group => :last_name) # Selects the minimum age for any family without any minors
# Person.sum("2 * age")
def calculate(operation, column_name, options = {})
- validate_calculation_options(operation, options)
- operation = operation.to_s.downcase
-
- scope = scope(:find)
+ construct_calculation_arel(options).calculate(operation, column_name, options.slice(:distinct))
+ rescue ThrowResult
+ 0
+ end
- merged_includes = merge_includes(scope ? scope[:include] : [], options[:include])
+ private
+ def validate_calculation_options(options = {})
+ options.assert_valid_keys(CALCULATIONS_OPTIONS)
+ end
- if operation == "count"
- if merged_includes.any?
- distinct = true
- column_name = options[:select] || primary_key
- end
+ def construct_calculation_arel(options = {})
+ validate_calculation_options(options)
+ options = options.except(:distinct)
- distinct = nil if column_name.to_s =~ /\s*DISTINCT\s+/i
- distinct ||= options[:distinct]
- else
- distinct = nil
- end
+ scope = scope(:find)
+ includes = merge_includes(scope ? scope[:include] : [], options[:include])
- catch :invalid_query do
- relation = if merged_includes.any?
- join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(self, merged_includes, construct_join(options[:joins], scope))
- construct_finder_arel_with_included_associations(options, join_dependency)
+ if includes.any?
+ join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(self, includes, construct_join(options[:joins], scope))
+ construct_calculation_arel_with_included_associations(options, join_dependency)
else
- relation = arel_table(options[:from]).
+ arel_table.
joins(construct_join(options[:joins], scope)).
+ from((scope && scope[:from]) || options[:from]).
where(construct_conditions(options[:conditions], scope)).
order(options[:order]).
limit(options[:limit]).
- offset(options[:offset])
- end
- if options[:group]
- return execute_grouped_calculation(operation, column_name, options, relation)
- else
- return execute_simple_calculation(operation, column_name, options.merge(:distinct => distinct), relation)
+ offset(options[:offset]).
+ group(options[:group]).
+ having(options[:having]).
+ select(options[:select] || (scope && scope[:select]) || default_select(options[:joins] || (scope && scope[:joins])))
end
end
- 0
- end
-
- def execute_simple_calculation(operation, column_name, options, relation) #:nodoc:
- column = if column_names.include?(column_name.to_s)
- Arel::Attribute.new(arel_table(options[:from] || table_name),
- options[:select] || column_name)
- else
- Arel::SqlLiteral.new(options[:select] ||
- (column_name == :all ? "*" : column_name.to_s))
- end
-
- relation = relation.select(operation == 'count' ? column.count(options[:distinct]) : column.send(operation))
-
- type_cast_calculated_value(connection.select_value(relation.to_sql), column_for(column_name), operation)
- end
- def execute_grouped_calculation(operation, column_name, options, relation) #:nodoc:
- group_attr = options[:group].to_s
- association = reflect_on_association(group_attr.to_sym)
- associated = association && association.macro == :belongs_to # only count belongs_to associations
- group_field = associated ? association.primary_key_name : group_attr
- group_alias = column_alias_for(group_field)
- group_column = column_for group_field
+ def construct_calculation_arel_with_included_associations(options, join_dependency)
+ scope = scope(:find)
- options[:group] = connection.adapter_name == 'FrontBase' ? group_alias : group_field
+ relation = arel_table
- aggregate_alias = column_alias_for(operation, column_name)
-
- options[:select] = (operation == 'count' && column_name == :all) ?
- "COUNT(*) AS count_all" :
- Arel::Attribute.new(arel_table, column_name).send(operation).as(aggregate_alias).to_sql
-
- options[:select] << ", #{group_field} AS #{group_alias}"
-
- relation = relation.select(options[:select]).group(construct_group(options[:group], options[:having], nil))
-
- calculated_data = connection.select_all(relation.to_sql)
-
- if association
- key_ids = calculated_data.collect { |row| row[group_alias] }
- key_records = association.klass.base_class.find(key_ids)
- key_records = key_records.inject({}) { |hsh, r| hsh.merge(r.id => r) }
- end
-
- calculated_data.inject(ActiveSupport::OrderedHash.new) do |all, row|
- key = type_cast_calculated_value(row[group_alias], group_column)
- key = key_records[key] if associated
- value = row[aggregate_alias]
- all[key] = type_cast_calculated_value(value, column_for(column_name), operation)
- all
- end
- end
-
- protected
- def construct_count_options_from_args(*args)
- options = {}
- column_name = :all
-
- # We need to handle
- # count()
- # count(:column_name=:all)
- # count(options={})
- # count(column_name=:all, options={})
- # selects specified by scopes
- case args.size
- when 0
- column_name = scope(:find)[:select] if scope(:find)
- when 1
- if args[0].is_a?(Hash)
- column_name = scope(:find)[:select] if scope(:find)
- options = args[0]
- else
- column_name = args[0]
- end
- when 2
- column_name, options = args
- else
- raise ArgumentError, "Unexpected parameters passed to count(): #{args.inspect}"
+ for association in join_dependency.join_associations
+ relation = association.join_relation(relation)
end
- [column_name || :all, options]
- end
-
- private
- def validate_calculation_options(operation, options = {})
- options.assert_valid_keys(CALCULATIONS_OPTIONS)
- end
+ relation = relation.joins(construct_join(options[:joins], scope)).
+ select(column_aliases(join_dependency)).
+ group(options[:group]).
+ having(options[:having]).
+ order(options[:order]).
+ where(construct_conditions(options[:conditions], scope)).
+ from((scope && scope[:from]) || options[:from])
- # Converts the given keys to the value that the database adapter returns as
- # a usable column name:
- #
- # column_alias_for("users.id") # => "users_id"
- # column_alias_for("sum(id)") # => "sum_id"
- # column_alias_for("count(distinct users.id)") # => "count_distinct_users_id"
- # column_alias_for("count(*)") # => "count_all"
- # column_alias_for("count", "id") # => "count_id"
- def column_alias_for(*keys)
- table_name = keys.join(' ')
- table_name.downcase!
- table_name.gsub!(/\*/, 'all')
- table_name.gsub!(/\W+/, ' ')
- table_name.strip!
- table_name.gsub!(/ +/, '_')
+ relation = relation.where(construct_arel_limited_ids_condition(options, join_dependency)) if !using_limitable_reflections?(join_dependency.reflections) && ((scope && scope[:limit]) || options[:limit])
+ relation = relation.limit(construct_limit(options[:limit], scope)) if using_limitable_reflections?(join_dependency.reflections)
- connection.table_alias_for(table_name)
+ relation
end
- def column_for(field)
- field_name = field.to_s.split('.').last
- columns.detect { |c| c.name.to_s == field_name }
- end
-
- def type_cast_calculated_value(value, column, operation = nil)
- case operation
- when 'count' then value.to_i
- when 'sum' then type_cast_using_column(value || '0', column)
- when 'average' then value && (value.is_a?(Fixnum) ? value.to_f : value).to_d
- else type_cast_using_column(value, column)
- end
- end
-
- def type_cast_using_column(value, column)
- column ? column.type_cast(value) : value
- end
end
end
end
diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb
index b25893a1c3..e1d772bd95 100644
--- a/activerecord/lib/active_record/callbacks.rb
+++ b/activerecord/lib/active_record/callbacks.rb
@@ -1,5 +1,3 @@
-require 'observer'
-
module ActiveRecord
# Callbacks are hooks into the lifecycle of an Active Record object that allow you to trigger logic
# before or after an alteration of the object state. This can be used to make sure that associated and
@@ -210,7 +208,6 @@ module ActiveRecord
# instead of quietly returning +false+.
module Callbacks
extend ActiveSupport::Concern
- include ActiveSupport::Callbacks
CALLBACKS = [
:after_initialize, :after_find, :before_validation, :after_validation,
@@ -224,60 +221,14 @@ module ActiveRecord
alias_method_chain method, :callbacks
end
- define_callbacks :initialize, :find, :save, :create, :update, :destroy,
- :validation, :terminator => "result == false", :scope => [:kind, :name]
+ extend ActiveModel::Callbacks
+
+ define_model_callbacks :initialize, :find, :only => :after
+ define_model_callbacks :save, :create, :update, :destroy
+ define_model_callbacks :validation, :only => [:before, :after]
end
module ClassMethods
- def after_initialize(*args, &block)
- options = args.extract_options!
- options[:prepend] = true
- set_callback(:initialize, :after, *(args << options), &block)
- end
-
- def after_find(*args, &block)
- options = args.extract_options!
- options[:prepend] = true
- set_callback(:find, :after, *(args << options), &block)
- end
-
- [:save, :create, :update, :destroy].each do |callback|
- module_eval <<-CALLBACKS, __FILE__, __LINE__
- def before_#{callback}(*args, &block)
- set_callback(:#{callback}, :before, *args, &block)
- end
-
- def around_#{callback}(*args, &block)
- set_callback(:#{callback}, :around, *args, &block)
- end
-
- def after_#{callback}(*args, &block)
- options = args.extract_options!
- options[:prepend] = true
- options[:if] = Array(options[:if]) << "!halted && value != false"
- set_callback(:#{callback}, :after, *(args << options), &block)
- end
- CALLBACKS
- end
-
- def before_validation(*args, &block)
- options = args.extract_options!
- if options[:on]
- options[:if] = Array(options[:if])
- options[:if] << "@_on_validate == :#{options[:on]}"
- end
- set_callback(:validation, :before, *(args << options), &block)
- end
-
- def after_validation(*args, &block)
- options = args.extract_options!
- options[:if] = Array(options[:if])
- options[:if] << "!halted"
- options[:if] << "@_on_validate == :#{options[:on]}" if options[:on]
- options[:prepend] = true
- set_callback(:validation, :after, *(args << options), &block)
- end
-
def method_added(meth)
super
if CALLBACKS.include?(meth.to_sym)
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 be89873632..027d736484 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
@@ -181,18 +181,6 @@ module ActiveRecord
# done if the transaction block raises an exception or returns false.
def rollback_db_transaction() end
- # Appends a locking clause to an SQL statement.
- # This method *modifies* the +sql+ parameter.
- # # SELECT * FROM suppliers FOR UPDATE
- # add_lock! 'SELECT * FROM suppliers', :lock => true
- # add_lock! 'SELECT * FROM suppliers', :lock => ' FOR UPDATE'
- def add_lock!(sql, options)
- case lock = options[:lock]
- when true; sql << ' FOR UPDATE'
- when String; sql << " #{lock}"
- end
- end
-
def default_sequence_name(table, column)
nil
end
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb
index c9c2892ba4..78b897add6 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb
@@ -183,12 +183,6 @@ module ActiveRecord
catch_schema_changes { @connection.rollback }
end
- # SELECT ... FOR UPDATE is redundant since the table is locked.
- def add_lock!(sql, options) #:nodoc:
- sql
- end
-
-
# SCHEMA STATEMENTS ========================================
def tables(name = nil) #:nodoc:
diff --git a/activerecord/lib/active_record/controller_runtime.rb b/activerecord/lib/active_record/controller_runtime.rb
new file mode 100644
index 0000000000..1281901ae8
--- /dev/null
+++ b/activerecord/lib/active_record/controller_runtime.rb
@@ -0,0 +1,27 @@
+module ActiveRecord
+ module ControllerRuntime
+ extend ActiveSupport::Concern
+
+ attr_internal :db_runtime
+
+ def cleanup_view_runtime
+ if Object.const_defined?("ActiveRecord") && ActiveRecord::Base.connected?
+ db_rt_before_render = ActiveRecord::Base.connection.reset_runtime
+ runtime = super
+ db_rt_after_render = ActiveRecord::Base.connection.reset_runtime
+ self.db_runtime = db_rt_before_render + db_rt_after_render
+ runtime - db_rt_after_render
+ else
+ super
+ end
+ end
+
+ module ClassMethods
+ def process_log_action(controller)
+ super
+ db_runtime = controller.send :db_runtime
+ logger.info(" ActiveRecord runtime: %.1fms" % db_runtime.to_f) if db_runtime
+ end
+ end
+ end
+end \ No newline at end of file
diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb
index ca3110a374..ff3a51d5c0 100644
--- a/activerecord/lib/active_record/nested_attributes.rb
+++ b/activerecord/lib/active_record/nested_attributes.rb
@@ -212,6 +212,11 @@ module ActiveRecord
# nested attributes array exceeds the specified limit, NestedAttributes::TooManyRecords
# exception is raised. If omitted, any number associations can be processed.
# Note that the :limit option is only applicable to one-to-many associations.
+ # [:update_only]
+ # Allows you to specify that an existing record may only be updated.
+ # A new record may only be created when there is no existing record.
+ # This option only works for one-to-one associations and is ignored for
+ # collection associations. This option is off by default.
#
# Examples:
# # creates avatar_attributes=
@@ -221,9 +226,9 @@ module ActiveRecord
# # creates avatar_attributes= and posts_attributes=
# accepts_nested_attributes_for :avatar, :posts, :allow_destroy => true
def accepts_nested_attributes_for(*attr_names)
- options = { :allow_destroy => false }
+ options = { :allow_destroy => false, :update_only => false }
options.update(attr_names.extract_options!)
- options.assert_valid_keys(:allow_destroy, :reject_if, :limit)
+ options.assert_valid_keys(:allow_destroy, :reject_if, :limit, :update_only)
attr_names.each do |association_name|
if reflection = reflect_on_association(association_name)
@@ -235,7 +240,7 @@ module ActiveRecord
end
reflection.options[:autosave] = true
-
+ add_autosave_association_callbacks(reflection)
self.nested_attributes_options[association_name.to_sym] = options
if options[:reject_if] == :all_blank
@@ -243,15 +248,13 @@ module ActiveRecord
end
# def pirate_attributes=(attributes)
- # assign_nested_attributes_for_one_to_one_association(:pirate, attributes, false)
+ # assign_nested_attributes_for_one_to_one_association(:pirate, attributes)
# end
class_eval %{
def #{association_name}_attributes=(attributes)
assign_nested_attributes_for_#{type}_association(:#{association_name}, attributes)
end
}, __FILE__, __LINE__
-
- add_autosave_association_callbacks(reflection)
else
raise ArgumentError, "No association found for name `#{association_name}'. Has it been defined yet?"
end
@@ -286,28 +289,29 @@ module ActiveRecord
# Assigns the given attributes to the association.
#
- # If the given attributes include an <tt>:id</tt> that matches the existing
- # record’s id, then the existing record will be modified. Otherwise a new
- # record will be built.
+ # If update_only is false and the given attributes include an <tt>:id</tt>
+ # that matches the existing record’s id, then the existing record will be
+ # modified. If update_only is true, a new record is only created when no
+ # object exists. Otherwise a new record will be built.
#
- # If the given attributes include a matching <tt>:id</tt> attribute _and_ a
- # <tt>:_destroy</tt> key set to a truthy value, then the existing record
- # will be marked for destruction.
+ # If the given attributes include a matching <tt>:id</tt> attribute, or
+ # update_only is true, and a <tt>:_destroy</tt> key set to a truthy value,
+ # then the existing record will be marked for destruction.
def assign_nested_attributes_for_one_to_one_association(association_name, attributes)
options = self.nested_attributes_options[association_name]
attributes = attributes.with_indifferent_access
+ check_existing_record = (options[:update_only] || !attributes['id'].blank?)
- if attributes['id'].blank?
- unless reject_new_record?(association_name, attributes)
- method = "build_#{association_name}"
- if respond_to?(method)
- send(method, attributes.except(*UNASSIGNABLE_KEYS))
- else
- raise ArgumentError, "Cannot build association #{association_name}. Are you trying to build a polymorphic one-to-one association?"
- end
+ if check_existing_record && (record = send(association_name)) &&
+ (options[:update_only] || record.id.to_s == attributes['id'].to_s)
+ assign_to_or_mark_for_destruction(record, attributes, options[:allow_destroy])
+ elsif !reject_new_record?(association_name, attributes)
+ method = "build_#{association_name}"
+ if respond_to?(method)
+ send(method, attributes.except(*UNASSIGNABLE_KEYS))
+ else
+ raise ArgumentError, "Cannot build association #{association_name}. Are you trying to build a polymorphic one-to-one association?"
end
- elsif (existing_record = send(association_name)) && existing_record.id.to_s == attributes['id'].to_s
- assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy])
end
end
diff --git a/activerecord/lib/active_record/rails.rb b/activerecord/lib/active_record/rails.rb
index ddbc555113..e7de699974 100644
--- a/activerecord/lib/active_record/rails.rb
+++ b/activerecord/lib/active_record/rails.rb
@@ -7,6 +7,13 @@ require "action_controller/rails"
module ActiveRecord
class Plugin < Rails::Plugin
plugin_name :active_record
+ include_modules_in "ActiveRecord::Base"
+
+ config.action_controller.include "ActiveRecord::ControllerRuntime"
+
+ rake_tasks do
+ load "active_record/rails/databases.rake"
+ end
initializer "active_record.set_configs" do |app|
app.config.active_record.each do |k,v|
@@ -50,8 +57,8 @@ module ActiveRecord
initializer "active_record.notifications" do
require 'active_support/notifications'
- ActiveSupport::Notifications.subscribe("sql") do |name, before, after, result, instrumenter_id, payload|
- ActiveRecord::Base.connection.log_info(payload[:sql], name, after - before)
+ ActiveSupport::Notifications.subscribe("sql") do |name, before, after, instrumenter_id, payload|
+ ActiveRecord::Base.connection.log_info(payload[:sql], payload[:name], (after - before) * 1000)
end
end
diff --git a/activerecord/lib/active_record/rails/databases.rake b/activerecord/lib/active_record/rails/databases.rake
new file mode 100644
index 0000000000..a35a6c156b
--- /dev/null
+++ b/activerecord/lib/active_record/rails/databases.rake
@@ -0,0 +1,469 @@
+namespace :db do
+ task :load_config => :rails_env do
+ require 'active_record'
+ ActiveRecord::Base.configurations = Rails::Configuration.new.database_configuration
+ end
+
+ namespace :create do
+ desc 'Create all the local databases defined in config/database.yml'
+ task :all => :load_config do
+ ActiveRecord::Base.configurations.each_value do |config|
+ # Skip entries that don't have a database key, such as the first entry here:
+ #
+ # defaults: &defaults
+ # adapter: mysql
+ # username: root
+ # password:
+ # host: localhost
+ #
+ # development:
+ # database: blog_development
+ # <<: *defaults
+ next unless config['database']
+ # Only connect to local databases
+ local_database?(config) { create_database(config) }
+ end
+ end
+ end
+
+ desc 'Create the database defined in config/database.yml for the current RAILS_ENV'
+ task :create => :load_config do
+ create_database(ActiveRecord::Base.configurations[RAILS_ENV])
+ end
+
+ def create_database(config)
+ begin
+ if config['adapter'] =~ /sqlite/
+ if File.exist?(config['database'])
+ $stderr.puts "#{config['database']} already exists"
+ else
+ begin
+ # Create the SQLite database
+ ActiveRecord::Base.establish_connection(config)
+ ActiveRecord::Base.connection
+ rescue
+ $stderr.puts $!, *($!.backtrace)
+ $stderr.puts "Couldn't create database for #{config.inspect}"
+ end
+ end
+ return # Skip the else clause of begin/rescue
+ else
+ ActiveRecord::Base.establish_connection(config)
+ ActiveRecord::Base.connection
+ end
+ rescue
+ case config['adapter']
+ when 'mysql'
+ @charset = ENV['CHARSET'] || 'utf8'
+ @collation = ENV['COLLATION'] || 'utf8_unicode_ci'
+ creation_options = {:charset => (config['charset'] || @charset), :collation => (config['collation'] || @collation)}
+ begin
+ ActiveRecord::Base.establish_connection(config.merge('database' => nil))
+ ActiveRecord::Base.connection.create_database(config['database'], creation_options)
+ ActiveRecord::Base.establish_connection(config)
+ rescue Mysql::Error => sqlerr
+ if sqlerr.errno == Mysql::Error::ER_ACCESS_DENIED_ERROR
+ print "#{sqlerr.error}. \nPlease provide the root password for your mysql installation\n>"
+ root_password = $stdin.gets.strip
+ grant_statement = "GRANT ALL PRIVILEGES ON #{config['database']}.* " \
+ "TO '#{config['username']}'@'localhost' " \
+ "IDENTIFIED BY '#{config['password']}' WITH GRANT OPTION;"
+ ActiveRecord::Base.establish_connection(config.merge(
+ 'database' => nil, 'username' => 'root', 'password' => root_password))
+ ActiveRecord::Base.connection.create_database(config['database'], creation_options)
+ ActiveRecord::Base.connection.execute grant_statement
+ ActiveRecord::Base.establish_connection(config)
+ else
+ $stderr.puts sqlerr.error
+ $stderr.puts "Couldn't create database for #{config.inspect}, charset: #{config['charset'] || @charset}, collation: #{config['collation'] || @collation}"
+ $stderr.puts "(if you set the charset manually, make sure you have a matching collation)" if config['charset']
+ end
+ end
+ when 'postgresql'
+ @encoding = config[:encoding] || ENV['CHARSET'] || 'utf8'
+ begin
+ ActiveRecord::Base.establish_connection(config.merge('database' => 'postgres', 'schema_search_path' => 'public'))
+ ActiveRecord::Base.connection.create_database(config['database'], config.merge('encoding' => @encoding))
+ ActiveRecord::Base.establish_connection(config)
+ rescue
+ $stderr.puts $!, *($!.backtrace)
+ $stderr.puts "Couldn't create database for #{config.inspect}"
+ end
+ end
+ else
+ $stderr.puts "#{config['database']} already exists"
+ end
+ end
+
+ namespace :drop do
+ desc 'Drops all the local databases defined in config/database.yml'
+ task :all => :load_config do
+ ActiveRecord::Base.configurations.each_value do |config|
+ # Skip entries that don't have a database key
+ next unless config['database']
+ begin
+ # Only connect to local databases
+ local_database?(config) { drop_database(config) }
+ rescue Exception => e
+ puts "Couldn't drop #{config['database']} : #{e.inspect}"
+ end
+ end
+ end
+ end
+
+ desc 'Drops the database for the current RAILS_ENV'
+ task :drop => :load_config do
+ config = ActiveRecord::Base.configurations[RAILS_ENV || 'development']
+ begin
+ drop_database(config)
+ rescue Exception => e
+ puts "Couldn't drop #{config['database']} : #{e.inspect}"
+ end
+ end
+
+ def local_database?(config, &block)
+ if %w( 127.0.0.1 localhost ).include?(config['host']) || config['host'].blank?
+ yield
+ else
+ puts "This task only modifies local databases. #{config['database']} is on a remote host."
+ end
+ end
+
+
+ desc "Migrate the database through scripts in db/migrate and update db/schema.rb by invoking db:schema:dump. Target specific version with VERSION=x. Turn off output with VERBOSE=false."
+ task :migrate => :environment do
+ ActiveRecord::Migration.verbose = ENV["VERBOSE"] ? ENV["VERBOSE"] == "true" : true
+ ActiveRecord::Migrator.migrate("db/migrate/", ENV["VERSION"] ? ENV["VERSION"].to_i : nil)
+ Rake::Task["db:schema:dump"].invoke if ActiveRecord::Base.schema_format == :ruby
+ end
+
+ namespace :migrate do
+ desc 'Rollbacks the database one migration and re migrate up. If you want to rollback more than one step, define STEP=x. Target specific version with VERSION=x.'
+ task :redo => :environment do
+ if ENV["VERSION"]
+ Rake::Task["db:migrate:down"].invoke
+ Rake::Task["db:migrate:up"].invoke
+ else
+ Rake::Task["db:rollback"].invoke
+ Rake::Task["db:migrate"].invoke
+ end
+ end
+
+ desc 'Resets your database using your migrations for the current environment'
+ task :reset => ["db:drop", "db:create", "db:migrate"]
+
+ desc 'Runs the "up" for a given migration VERSION.'
+ task :up => :environment do
+ version = ENV["VERSION"] ? ENV["VERSION"].to_i : nil
+ raise "VERSION is required" unless version
+ ActiveRecord::Migrator.run(:up, "db/migrate/", version)
+ Rake::Task["db:schema:dump"].invoke if ActiveRecord::Base.schema_format == :ruby
+ end
+
+ desc 'Runs the "down" for a given migration VERSION.'
+ task :down => :environment do
+ version = ENV["VERSION"] ? ENV["VERSION"].to_i : nil
+ raise "VERSION is required" unless version
+ ActiveRecord::Migrator.run(:down, "db/migrate/", version)
+ Rake::Task["db:schema:dump"].invoke if ActiveRecord::Base.schema_format == :ruby
+ end
+ end
+
+ desc 'Rolls the schema back to the previous version. Specify the number of steps with STEP=n'
+ task :rollback => :environment do
+ step = ENV['STEP'] ? ENV['STEP'].to_i : 1
+ ActiveRecord::Migrator.rollback('db/migrate/', step)
+ Rake::Task["db:schema:dump"].invoke if ActiveRecord::Base.schema_format == :ruby
+ end
+
+ desc 'Pushes the schema to the next version. Specify the number of steps with STEP=n'
+ task :forward => :environment do
+ step = ENV['STEP'] ? ENV['STEP'].to_i : 1
+ ActiveRecord::Migrator.forward('db/migrate/', step)
+ Rake::Task["db:schema:dump"].invoke if ActiveRecord::Base.schema_format == :ruby
+ end
+
+ desc 'Drops and recreates the database from db/schema.rb for the current environment and loads the seeds.'
+ task :reset => [ 'db:drop', 'db:setup' ]
+
+ desc "Retrieves the charset for the current environment's database"
+ task :charset => :environment do
+ config = ActiveRecord::Base.configurations[RAILS_ENV || 'development']
+ case config['adapter']
+ when 'mysql'
+ ActiveRecord::Base.establish_connection(config)
+ puts ActiveRecord::Base.connection.charset
+ when 'postgresql'
+ ActiveRecord::Base.establish_connection(config)
+ puts ActiveRecord::Base.connection.encoding
+ else
+ puts 'sorry, your database adapter is not supported yet, feel free to submit a patch'
+ end
+ end
+
+ desc "Retrieves the collation for the current environment's database"
+ task :collation => :environment do
+ config = ActiveRecord::Base.configurations[RAILS_ENV || 'development']
+ case config['adapter']
+ when 'mysql'
+ ActiveRecord::Base.establish_connection(config)
+ puts ActiveRecord::Base.connection.collation
+ else
+ puts 'sorry, your database adapter is not supported yet, feel free to submit a patch'
+ end
+ end
+
+ desc "Retrieves the current schema version number"
+ task :version => :environment do
+ puts "Current version: #{ActiveRecord::Migrator.current_version}"
+ end
+
+ desc "Raises an error if there are pending migrations"
+ task :abort_if_pending_migrations => :environment do
+ if defined? ActiveRecord
+ pending_migrations = ActiveRecord::Migrator.new(:up, 'db/migrate').pending_migrations
+
+ if pending_migrations.any?
+ puts "You have #{pending_migrations.size} pending migrations:"
+ pending_migrations.each do |pending_migration|
+ puts ' %4d %s' % [pending_migration.version, pending_migration.name]
+ end
+ abort %{Run "rake db:migrate" to update your database then try again.}
+ end
+ end
+ end
+
+ desc 'Create the database, load the schema, and initialize with the seed data'
+ task :setup => [ 'db:create', 'db:schema:load', 'db:seed' ]
+
+ desc 'Load the seed data from db/seeds.rb'
+ task :seed => :environment do
+ seed_file = File.join(Rails.root, 'db', 'seeds.rb')
+ load(seed_file) if File.exist?(seed_file)
+ end
+
+ namespace :fixtures do
+ desc "Load fixtures into the current environment's database. Load specific fixtures using FIXTURES=x,y. Load from subdirectory in test/fixtures using FIXTURES_DIR=z. Specify an alternative path (eg. spec/fixtures) using FIXTURES_PATH=spec/fixtures."
+ task :load => :environment do
+ require 'active_record/fixtures'
+ ActiveRecord::Base.establish_connection(Rails.env)
+ base_dir = ENV['FIXTURES_PATH'] ? File.join(Rails.root, ENV['FIXTURES_PATH']) : File.join(Rails.root, 'test', 'fixtures')
+ fixtures_dir = ENV['FIXTURES_DIR'] ? File.join(base_dir, ENV['FIXTURES_DIR']) : base_dir
+
+ (ENV['FIXTURES'] ? ENV['FIXTURES'].split(/,/).map {|f| File.join(fixtures_dir, f) } : Dir.glob(File.join(fixtures_dir, '*.{yml,csv}'))).each do |fixture_file|
+ Fixtures.create_fixtures(File.dirname(fixture_file), File.basename(fixture_file, '.*'))
+ end
+ end
+
+ desc "Search for a fixture given a LABEL or ID. Specify an alternative path (eg. spec/fixtures) using FIXTURES_PATH=spec/fixtures."
+ task :identify => :environment do
+ require "active_record/fixtures"
+
+ label, id = ENV["LABEL"], ENV["ID"]
+ raise "LABEL or ID required" if label.blank? && id.blank?
+
+ puts %Q(The fixture ID for "#{label}" is #{Fixtures.identify(label)}.) if label
+
+ base_dir = ENV['FIXTURES_PATH'] ? File.join(Rails.root, ENV['FIXTURES_PATH']) : File.join(Rails.root, 'test', 'fixtures')
+ Dir["#{base_dir}/**/*.yml"].each do |file|
+ if data = YAML::load(ERB.new(IO.read(file)).result)
+ data.keys.each do |key|
+ key_id = Fixtures.identify(key)
+
+ if key == label || key_id == id.to_i
+ puts "#{file}: #{key} (#{key_id})"
+ end
+ end
+ end
+ end
+ end
+ end
+
+ namespace :schema do
+ desc "Create a db/schema.rb file that can be portably used against any DB supported by AR"
+ task :dump => :environment do
+ require 'active_record/schema_dumper'
+ File.open(ENV['SCHEMA'] || "#{Rails.root}/db/schema.rb", "w") do |file|
+ ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, file)
+ end
+ Rake::Task["db:schema:dump"].reenable
+ end
+
+ desc "Load a schema.rb file into the database"
+ task :load => :environment do
+ file = ENV['SCHEMA'] || "#{Rails.root}/db/schema.rb"
+ if File.exists?(file)
+ load(file)
+ else
+ abort %{#{file} doesn't exist yet. Run "rake db:migrate" to create it then try again. If you do not intend to use a database, you should instead alter #{Rails.root}/config/application.rb to prevent active_record from loading: config.frameworks -= [ :active_record ]}
+ end
+ end
+ end
+
+ namespace :structure do
+ desc "Dump the database structure to a SQL file"
+ task :dump => :environment do
+ abcs = ActiveRecord::Base.configurations
+ case abcs[RAILS_ENV]["adapter"]
+ when "mysql", "oci", "oracle"
+ ActiveRecord::Base.establish_connection(abcs[RAILS_ENV])
+ File.open("#{Rails.root}/db/#{RAILS_ENV}_structure.sql", "w+") { |f| f << ActiveRecord::Base.connection.structure_dump }
+ when "postgresql"
+ ENV['PGHOST'] = abcs[RAILS_ENV]["host"] if abcs[RAILS_ENV]["host"]
+ ENV['PGPORT'] = abcs[RAILS_ENV]["port"].to_s if abcs[RAILS_ENV]["port"]
+ ENV['PGPASSWORD'] = abcs[RAILS_ENV]["password"].to_s if abcs[RAILS_ENV]["password"]
+ search_path = abcs[RAILS_ENV]["schema_search_path"]
+ unless search_path.blank?
+ search_path = search_path.split(",").map{|search_path| "--schema=#{search_path.strip}" }.join(" ")
+ end
+ `pg_dump -i -U "#{abcs[RAILS_ENV]["username"]}" -s -x -O -f db/#{RAILS_ENV}_structure.sql #{search_path} #{abcs[RAILS_ENV]["database"]}`
+ raise "Error dumping database" if $?.exitstatus == 1
+ when "sqlite", "sqlite3"
+ dbfile = abcs[RAILS_ENV]["database"] || abcs[RAILS_ENV]["dbfile"]
+ `#{abcs[RAILS_ENV]["adapter"]} #{dbfile} .schema > db/#{RAILS_ENV}_structure.sql`
+ when "sqlserver"
+ `scptxfr /s #{abcs[RAILS_ENV]["host"]} /d #{abcs[RAILS_ENV]["database"]} /I /f db\\#{RAILS_ENV}_structure.sql /q /A /r`
+ `scptxfr /s #{abcs[RAILS_ENV]["host"]} /d #{abcs[RAILS_ENV]["database"]} /I /F db\ /q /A /r`
+ when "firebird"
+ set_firebird_env(abcs[RAILS_ENV])
+ db_string = firebird_db_string(abcs[RAILS_ENV])
+ sh "isql -a #{db_string} > #{Rails.root}/db/#{RAILS_ENV}_structure.sql"
+ else
+ raise "Task not supported by '#{abcs["test"]["adapter"]}'"
+ end
+
+ if ActiveRecord::Base.connection.supports_migrations?
+ File.open("#{Rails.root}/db/#{RAILS_ENV}_structure.sql", "a") { |f| f << ActiveRecord::Base.connection.dump_schema_information }
+ end
+ end
+ end
+
+ namespace :test do
+ desc "Recreate the test database from the current schema.rb"
+ task :load => 'db:test:purge' do
+ ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations['test'])
+ ActiveRecord::Schema.verbose = false
+ Rake::Task["db:schema:load"].invoke
+ end
+
+ desc "Recreate the test database from the current environment's database schema"
+ task :clone => %w(db:schema:dump db:test:load)
+
+ desc "Recreate the test databases from the development structure"
+ task :clone_structure => [ "db:structure:dump", "db:test:purge" ] do
+ abcs = ActiveRecord::Base.configurations
+ case abcs["test"]["adapter"]
+ when "mysql"
+ ActiveRecord::Base.establish_connection(:test)
+ ActiveRecord::Base.connection.execute('SET foreign_key_checks = 0')
+ IO.readlines("#{Rails.root}/db/#{RAILS_ENV}_structure.sql").join.split("\n\n").each do |table|
+ ActiveRecord::Base.connection.execute(table)
+ end
+ when "postgresql"
+ ENV['PGHOST'] = abcs["test"]["host"] if abcs["test"]["host"]
+ ENV['PGPORT'] = abcs["test"]["port"].to_s if abcs["test"]["port"]
+ ENV['PGPASSWORD'] = abcs["test"]["password"].to_s if abcs["test"]["password"]
+ `psql -U "#{abcs["test"]["username"]}" -f #{Rails.root}/db/#{RAILS_ENV}_structure.sql #{abcs["test"]["database"]}`
+ when "sqlite", "sqlite3"
+ dbfile = abcs["test"]["database"] || abcs["test"]["dbfile"]
+ `#{abcs["test"]["adapter"]} #{dbfile} < #{Rails.root}/db/#{RAILS_ENV}_structure.sql`
+ when "sqlserver"
+ `osql -E -S #{abcs["test"]["host"]} -d #{abcs["test"]["database"]} -i db\\#{RAILS_ENV}_structure.sql`
+ when "oci", "oracle"
+ ActiveRecord::Base.establish_connection(:test)
+ IO.readlines("#{Rails.root}/db/#{RAILS_ENV}_structure.sql").join.split(";\n\n").each do |ddl|
+ ActiveRecord::Base.connection.execute(ddl)
+ end
+ when "firebird"
+ set_firebird_env(abcs["test"])
+ db_string = firebird_db_string(abcs["test"])
+ sh "isql -i #{Rails.root}/db/#{RAILS_ENV}_structure.sql #{db_string}"
+ else
+ raise "Task not supported by '#{abcs["test"]["adapter"]}'"
+ end
+ end
+
+ desc "Empty the test database"
+ task :purge => :environment do
+ abcs = ActiveRecord::Base.configurations
+ case abcs["test"]["adapter"]
+ when "mysql"
+ ActiveRecord::Base.establish_connection(:test)
+ ActiveRecord::Base.connection.recreate_database(abcs["test"]["database"], abcs["test"])
+ when "postgresql"
+ ActiveRecord::Base.clear_active_connections!
+ drop_database(abcs['test'])
+ create_database(abcs['test'])
+ when "sqlite","sqlite3"
+ dbfile = abcs["test"]["database"] || abcs["test"]["dbfile"]
+ File.delete(dbfile) if File.exist?(dbfile)
+ when "sqlserver"
+ dropfkscript = "#{abcs["test"]["host"]}.#{abcs["test"]["database"]}.DP1".gsub(/\\/,'-')
+ `osql -E -S #{abcs["test"]["host"]} -d #{abcs["test"]["database"]} -i db\\#{dropfkscript}`
+ `osql -E -S #{abcs["test"]["host"]} -d #{abcs["test"]["database"]} -i db\\#{RAILS_ENV}_structure.sql`
+ when "oci", "oracle"
+ ActiveRecord::Base.establish_connection(:test)
+ ActiveRecord::Base.connection.structure_drop.split(";\n\n").each do |ddl|
+ ActiveRecord::Base.connection.execute(ddl)
+ end
+ when "firebird"
+ ActiveRecord::Base.establish_connection(:test)
+ ActiveRecord::Base.connection.recreate_database!
+ else
+ raise "Task not supported by '#{abcs["test"]["adapter"]}'"
+ end
+ end
+
+ desc 'Check for pending migrations and load the test schema'
+ task :prepare => 'db:abort_if_pending_migrations' do
+ if defined?(ActiveRecord) && !ActiveRecord::Base.configurations.blank?
+ Rake::Task[{ :sql => "db:test:clone_structure", :ruby => "db:test:load" }[ActiveRecord::Base.schema_format]].invoke
+ end
+ end
+ end
+
+ namespace :sessions do
+ desc "Creates a sessions migration for use with ActiveRecord::SessionStore"
+ task :create => :environment do
+ raise "Task unavailable to this database (no migration support)" unless ActiveRecord::Base.connection.supports_migrations?
+ require 'rails/generators'
+ require 'rails/generators/rails/session_migration/session_migration_generator'
+ Rails::Generators::SessionMigrationGenerator.start [ ENV["MIGRATION"] || "add_sessions_table" ]
+ end
+
+ desc "Clear the sessions table"
+ task :clear => :environment do
+ ActiveRecord::Base.connection.execute "DELETE FROM #{session_table_name}"
+ end
+ end
+end
+
+def drop_database(config)
+ case config['adapter']
+ when 'mysql'
+ ActiveRecord::Base.establish_connection(config)
+ ActiveRecord::Base.connection.drop_database config['database']
+ when /^sqlite/
+ require 'pathname'
+ path = Pathname.new(config['database'])
+ file = path.absolute? ? path.to_s : File.join(Rails.root, path)
+
+ FileUtils.rm(file)
+ when 'postgresql'
+ ActiveRecord::Base.establish_connection(config.merge('database' => 'postgres', 'schema_search_path' => 'public'))
+ ActiveRecord::Base.connection.drop_database config['database']
+ end
+end
+
+def session_table_name
+ ActiveRecord::SessionStore::Session.table_name
+end
+
+def set_firebird_env(config)
+ ENV["ISC_USER"] = config["username"].to_s if config["username"]
+ ENV["ISC_PASSWORD"] = config["password"].to_s if config["password"]
+end
+
+def firebird_db_string(config)
+ FireRuby::Database.db_string_for(config.symbolize_keys)
+end
diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb
index db5d2b25ed..b751c9ad68 100644
--- a/activerecord/lib/active_record/reflection.rb
+++ b/activerecord/lib/active_record/reflection.rb
@@ -214,8 +214,10 @@ module ActiveRecord
end
def check_validity_of_inverse!
- if has_inverse? && inverse_of.nil?
- raise InverseOfAssociationNotFoundError.new(self)
+ unless options[:polymorphic]
+ if has_inverse? && inverse_of.nil?
+ raise InverseOfAssociationNotFoundError.new(self)
+ end
end
end
@@ -237,8 +239,16 @@ module ActiveRecord
def inverse_of
if has_inverse?
@inverse_of ||= klass.reflect_on_association(options[:inverse_of])
- else
- nil
+ end
+ end
+
+ def polymorphic_inverse_of(associated_class)
+ if has_inverse?
+ if inverse_relationship = associated_class.reflect_on_association(options[:inverse_of])
+ inverse_relationship
+ else
+ raise InverseOfAssociationNotFoundError.new(self, associated_class)
+ end
end
end
diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb
index c927270eee..64261db809 100644
--- a/activerecord/lib/active_record/relation.rb
+++ b/activerecord/lib/active_record/relation.rb
@@ -1,55 +1,120 @@
module ActiveRecord
class Relation
+ include RelationalCalculations
+
delegate :to_sql, :to => :relation
- delegate :length, :collect, :find, :map, :each, :to => :to_a
- attr_reader :relation, :klass
+ delegate :length, :collect, :map, :each, :all?, :to => :to_a
+
+ attr_reader :relation, :klass, :preload_associations, :eager_load_associations
+ attr_writer :readonly, :preload_associations, :eager_load_associations
- def initialize(klass, relation, readonly = false, preload = [], eager_load = [])
+ def initialize(klass, relation)
@klass, @relation = klass, relation
- @readonly = readonly
- @associations_to_preload = preload
- @eager_load_associations = eager_load
- @loaded = false
+ @preload_associations = []
+ @eager_load_associations = []
+ @loaded, @readonly = false
+ end
+
+ def merge(r)
+ raise ArgumentError, "Cannot merge a #{r.klass.name} relation with #{@klass.name} relation" if r.klass != @klass
+
+ joins(r.relation.joins(r.relation)).
+ group(r.send(:group_clauses).join(', ')).
+ order(r.send(:order_clauses).join(', ')).
+ where(r.send(:where_clause)).
+ limit(r.taken).
+ offset(r.skipped).
+ select(r.send(:select_clauses).join(', ')).
+ eager_load(r.eager_load_associations).
+ preload(r.preload_associations).
+ from(r.send(:sources).present? ? r.send(:from_clauses) : nil)
end
+ alias :& :merge
+
def preload(*associations)
- create_new_relation(@relation, @readonly, @associations_to_preload + Array.wrap(associations))
+ spawn.tap {|r| r.preload_associations += Array.wrap(associations) }
end
def eager_load(*associations)
- create_new_relation(@relation, @readonly, @associations_to_preload, @eager_load_associations + Array.wrap(associations))
+ spawn.tap {|r| r.eager_load_associations += Array.wrap(associations) }
end
- def readonly
- create_new_relation(@relation, true)
+ def readonly(status = true)
+ spawn.tap {|r| r.readonly = status }
end
def select(selects)
- create_new_relation(@relation.project(selects))
+ if selects.present?
+ relation = spawn(@relation.project(selects))
+ relation.readonly = @relation.joins(relation).present? ? false : @readonly
+ relation
+ else
+ spawn
+ end
+ end
+
+ def from(from)
+ from.present? ? spawn(@relation.from(from)) : spawn
+ end
+
+ def having(*args)
+ return spawn if args.blank?
+
+ if [String, Hash, Array].include?(args.first.class)
+ havings = @klass.send(:merge_conditions, args.size > 1 ? Array.wrap(args) : args.first)
+ else
+ havings = args.first
+ end
+
+ spawn(@relation.having(havings))
end
def group(groups)
- create_new_relation(@relation.group(groups))
+ groups.present? ? spawn(@relation.group(groups)) : spawn
end
def order(orders)
- create_new_relation(@relation.order(orders))
+ orders.present? ? spawn(@relation.order(orders)) : spawn
+ end
+
+ def lock(locks = true)
+ case locks
+ when String
+ spawn(@relation.lock(locks))
+ when TrueClass, NilClass
+ spawn(@relation.lock)
+ else
+ spawn
+ end
+ end
+
+ def reverse_order
+ relation = spawn
+ relation.instance_variable_set(:@orders, nil)
+
+ order_clause = @relation.send(:order_clauses).join(', ')
+ if order_clause.present?
+ relation.order(reverse_sql_order(order_clause))
+ else
+ relation.order("#{@klass.table_name}.#{@klass.primary_key} DESC")
+ end
end
def limit(limits)
- create_new_relation(@relation.take(limits))
+ limits.present? ? spawn(@relation.take(limits)) : spawn
end
def offset(offsets)
- create_new_relation(@relation.skip(offsets))
+ offsets.present? ? spawn(@relation.skip(offsets)) : spawn
end
def on(join)
- create_new_relation(@relation.on(join))
+ spawn(@relation.on(join))
end
def joins(join, join_type = nil)
- return self if join.blank?
+ return spawn if join.blank?
join_relation = case join
when String
@@ -64,45 +129,58 @@ module ActiveRecord
@relation.join(join, join_type)
end
- create_new_relation(join_relation)
+ spawn(join_relation).tap { |r| r.readonly = true }
end
def where(*args)
+ return spawn if args.blank?
+
if [String, Hash, Array].include?(args.first.class)
conditions = @klass.send(:merge_conditions, args.size > 1 ? Array.wrap(args) : args.first)
+ conditions = Arel::SqlLiteral.new(conditions) if conditions
else
conditions = args.first
end
- create_new_relation(@relation.where(conditions))
+ spawn(@relation.where(conditions))
end
- def respond_to?(method)
- @relation.respond_to?(method) || Array.method_defined?(method) || super
+ def respond_to?(method, include_private = false)
+ return true if @relation.respond_to?(method, include_private) || Array.method_defined?(method)
+
+ if match = DynamicFinderMatch.match(method)
+ return true if @klass.send(:all_attributes_exists?, match.attribute_names)
+ elsif match = DynamicScopeMatch.match(method)
+ return true if @klass.send(:all_attributes_exists?, match.attribute_names)
+ else
+ super
+ end
end
def to_a
return @records if loaded?
@records = if @eager_load_associations.any?
- catch :invalid_query do
- return @klass.send(:find_with_associations, {
+ begin
+ @klass.send(:find_with_associations, {
:select => @relation.send(:select_clauses).join(', '),
:joins => @relation.joins(relation),
:group => @relation.send(:group_clauses).join(', '),
:order => @relation.send(:order_clauses).join(', '),
:conditions => where_clause,
:limit => @relation.taken,
- :offset => @relation.skipped
+ :offset => @relation.skipped,
+ :from => (@relation.send(:from_clauses) if @relation.send(:sources).present?)
},
ActiveRecord::Associations::ClassMethods::JoinDependency.new(@klass, @eager_load_associations, nil))
+ rescue ThrowResult
+ []
end
- []
else
@klass.find_by_sql(@relation.to_sql)
end
- @associations_to_preload.each {|associations| @klass.send(:preload_associations, @records, associations) }
+ @preload_associations.each {|associations| @klass.send(:preload_associations, @records, associations) }
@records.each { |record| record.readonly! } if @readonly
@loaded = true
@@ -130,6 +208,12 @@ module ActiveRecord
end
end
+ def exists?(id = nil)
+ relation = select("#{@klass.quoted_table_name}.#{@klass.primary_key}").limit(1)
+ relation = relation.where(@klass.primary_key => id) if id
+ relation.first ? true : false
+ end
+
def first
if loaded?
@records.first
@@ -138,16 +222,54 @@ module ActiveRecord
end
end
+ def last
+ if loaded?
+ @records.last
+ else
+ @last ||= reverse_order.limit(1).to_a[0]
+ end
+ end
+
+ def size
+ loaded? ? @records.length : count
+ end
+
+ def empty?
+ loaded? ? @records.empty? : count.zero?
+ end
+
+ def destroy_all
+ to_a.each {|object| object.destroy}
+ reset
+ end
+
+ def delete_all
+ @relation.delete.tap { reset }
+ end
+
def loaded?
@loaded
end
def reload
@loaded = false
- @records = @first = nil
+ reset
+ end
+
+ def reset
+ @first = @last = nil
+ @records = []
self
end
+ def spawn(relation = @relation)
+ relation = self.class.new(@klass, relation)
+ relation.readonly = @readonly
+ relation.preload_associations = @preload_associations
+ relation.eager_load_associations = @eager_load_associations
+ relation
+ end
+
protected
def method_missing(method, *args, &block)
@@ -241,12 +363,21 @@ module ActiveRecord
end
end
- def create_new_relation(relation, readonly = @readonly, preload = @associations_to_preload, eager_load = @eager_load_associations)
- Relation.new(@klass, relation, readonly, preload, eager_load)
- end
-
def where_clause(join_string = "\n\tAND ")
@relation.send(:where_clauses).join(join_string)
end
+
+ def reverse_sql_order(order_query)
+ order_query.to_s.split(/,/).each { |s|
+ if s.match(/\s(asc|ASC)$/)
+ s.gsub!(/\s(asc|ASC)$/, ' DESC')
+ elsif s.match(/\s(desc|DESC)$/)
+ s.gsub!(/\s(desc|DESC)$/, ' ASC')
+ else
+ s.concat(' DESC')
+ end
+ }.join(',')
+ end
+
end
end
diff --git a/activerecord/lib/active_record/relational_calculations.rb b/activerecord/lib/active_record/relational_calculations.rb
new file mode 100644
index 0000000000..f42ffe0460
--- /dev/null
+++ b/activerecord/lib/active_record/relational_calculations.rb
@@ -0,0 +1,177 @@
+module ActiveRecord
+ module RelationalCalculations
+
+ def count(*args)
+ calculate(:count, *construct_count_options_from_args(*args))
+ end
+
+ def average(column_name)
+ calculate(:average, column_name)
+ end
+
+ def minimum(column_name)
+ calculate(:minimum, column_name)
+ end
+
+ def maximum(column_name)
+ calculate(:maximum, column_name)
+ end
+
+ def sum(column_name)
+ calculate(:sum, column_name)
+ end
+
+ def calculate(operation, column_name, options = {})
+ operation = operation.to_s.downcase
+
+ if operation == "count"
+ joins = @relation.joins(relation)
+ if joins.present? && joins =~ /LEFT OUTER/i
+ distinct = true
+ column_name = @klass.primary_key if column_name == :all
+ end
+
+ distinct = nil if column_name.to_s =~ /\s*DISTINCT\s+/i
+ distinct ||= options[:distinct]
+ else
+ distinct = nil
+ end
+
+ distinct = options[:distinct] || distinct
+ column_name = :all if column_name.blank? && operation == "count"
+
+ if @relation.send(:groupings).any?
+ return execute_grouped_calculation(operation, column_name)
+ else
+ return execute_simple_calculation(operation, column_name, distinct)
+ end
+ rescue ThrowResult
+ 0
+ end
+
+ private
+
+ def execute_simple_calculation(operation, column_name, distinct) #:nodoc:
+ column = if @klass.column_names.include?(column_name.to_s)
+ Arel::Attribute.new(@klass.arel_table, column_name)
+ else
+ Arel::SqlLiteral.new(column_name == :all ? "*" : column_name.to_s)
+ end
+
+ relation = select(operation == 'count' ? column.count(distinct) : column.send(operation))
+ type_cast_calculated_value(@klass.connection.select_value(relation.to_sql), column_for(column_name), operation)
+ end
+
+ def execute_grouped_calculation(operation, column_name) #:nodoc:
+ group_attr = @relation.send(:groupings).first.value
+ association = @klass.reflect_on_association(group_attr.to_sym)
+ associated = association && association.macro == :belongs_to # only count belongs_to associations
+ group_field = associated ? association.primary_key_name : group_attr
+ group_alias = column_alias_for(group_field)
+ group_column = column_for(group_field)
+
+ group = @klass.connection.adapter_name == 'FrontBase' ? group_alias : group_field
+
+ aggregate_alias = column_alias_for(operation, column_name)
+
+ select_statement = if operation == 'count' && column_name == :all
+ "COUNT(*) AS count_all"
+ else
+ Arel::Attribute.new(@klass.arel_table, column_name).send(operation).as(aggregate_alias).to_sql
+ end
+
+ select_statement << ", #{group_field} AS #{group_alias}"
+
+ relation = select(select_statement).group(group)
+
+ calculated_data = @klass.connection.select_all(relation.to_sql)
+
+ if association
+ key_ids = calculated_data.collect { |row| row[group_alias] }
+ key_records = association.klass.base_class.find(key_ids)
+ key_records = key_records.inject({}) { |hsh, r| hsh.merge(r.id => r) }
+ end
+
+ calculated_data.inject(ActiveSupport::OrderedHash.new) do |all, row|
+ key = type_cast_calculated_value(row[group_alias], group_column)
+ key = key_records[key] if associated
+ value = row[aggregate_alias]
+ all[key] = type_cast_calculated_value(value, column_for(column_name), operation)
+ all
+ end
+ end
+
+ def construct_count_options_from_args(*args)
+ options = {}
+ column_name = :all
+
+ # Handles count(), count(:column), count(:distinct => true), count(:column, :distinct => true)
+ # TODO : relation.projections only works when .select() was last in the chain. Fix it!
+ case args.size
+ when 0
+ select = get_projection_name_from_chained_relations
+ column_name = select if select !~ /(,|\*)/
+ when 1
+ if args[0].is_a?(Hash)
+ select = get_projection_name_from_chained_relations
+ column_name = select if select !~ /(,|\*)/
+ options = args[0]
+ else
+ column_name = args[0]
+ end
+ when 2
+ column_name, options = args
+ else
+ raise ArgumentError, "Unexpected parameters passed to count(): #{args.inspect}"
+ end
+
+ [column_name || :all, options]
+ end
+
+ # Converts the given keys to the value that the database adapter returns as
+ # a usable column name:
+ #
+ # column_alias_for("users.id") # => "users_id"
+ # column_alias_for("sum(id)") # => "sum_id"
+ # column_alias_for("count(distinct users.id)") # => "count_distinct_users_id"
+ # column_alias_for("count(*)") # => "count_all"
+ # column_alias_for("count", "id") # => "count_id"
+ def column_alias_for(*keys)
+ table_name = keys.join(' ')
+ table_name.downcase!
+ table_name.gsub!(/\*/, 'all')
+ table_name.gsub!(/\W+/, ' ')
+ table_name.strip!
+ table_name.gsub!(/ +/, '_')
+
+ @klass.connection.table_alias_for(table_name)
+ end
+
+ def column_for(field)
+ field_name = field.to_s.split('.').last
+ @klass.columns.detect { |c| c.name.to_s == field_name }
+ end
+
+ def type_cast_calculated_value(value, column, operation = nil)
+ case operation
+ when 'count' then value.to_i
+ when 'sum' then type_cast_using_column(value || '0', column)
+ when 'average' then value && (value.is_a?(Fixnum) ? value.to_f : value).to_d
+ else type_cast_using_column(value, column)
+ end
+ end
+
+ def type_cast_using_column(value, column)
+ column ? column.type_cast(value) : value
+ end
+
+ def get_projection_name_from_chained_relations(relation = @relation)
+ if relation.respond_to?(:projections) && relation.projections.present?
+ relation.send(:select_clauses).join(', ')
+ elsif relation.respond_to?(:relation)
+ get_projection_name_from_chained_relations(relation.relation)
+ end
+ end
+
+ end
+end
diff --git a/activerecord/lib/active_record/validations/associated.rb b/activerecord/lib/active_record/validations/associated.rb
index 92f47d770f..66b78682ad 100644
--- a/activerecord/lib/active_record/validations/associated.rb
+++ b/activerecord/lib/active_record/validations/associated.rb
@@ -1,5 +1,12 @@
module ActiveRecord
module Validations
+ class AssociatedValidator < ActiveModel::EachValidator
+ def validate_each(record, attribute, value)
+ return if (value.is_a?(Array) ? value : [value]).collect{ |r| r.nil? || r.valid? }.all?
+ record.errors.add(attribute, :invalid, :default => options[:message], :value => value)
+ end
+ end
+
module ClassMethods
# Validates whether the associated object or objects are all valid themselves. Works with any kind of association.
#
@@ -33,13 +40,8 @@ module ActiveRecord
# not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
# method, proc or string should return or evaluate to a true or false value.
def validates_associated(*attr_names)
- configuration = attr_names.extract_options!
-
- validates_each(attr_names, configuration) do |record, attr_name, value|
- unless (value.is_a?(Array) ? value : [value]).collect { |r| r.nil? || r.valid? }.all?
- record.errors.add(attr_name, :invalid, :default => configuration[:message], :value => value)
- end
- end
+ options = attr_names.extract_options!
+ validates_with AssociatedValidator, options.merge(:attributes => attr_names)
end
end
end
diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb
index 711086dc2c..ffbe1b5c40 100644
--- a/activerecord/lib/active_record/validations/uniqueness.rb
+++ b/activerecord/lib/active_record/validations/uniqueness.rb
@@ -1,5 +1,77 @@
module ActiveRecord
module Validations
+ class UniquenessValidator < ActiveModel::EachValidator
+ def initialize(options)
+ @klass = options.delete(:klass)
+ super(options.reverse_merge(:case_sensitive => true))
+ end
+
+ def validate_each(record, attribute, value)
+ finder_class = find_finder_class_for(record)
+ table_name = record.class.quoted_table_name
+ sql, params = mount_sql_and_params(finder_class, table_name, attribute, value)
+
+ Array(options[:scope]).each do |scope_item|
+ scope_value = record.send(scope_item)
+ sql << " AND " << record.class.send(:attribute_condition, "#{table_name}.#{scope_item}", scope_value)
+ params << scope_value
+ end
+
+ unless record.new_record?
+ sql << " AND #{record.class.quoted_table_name}.#{record.class.primary_key} <> ?"
+ params << record.send(:id)
+ end
+
+ finder_class.send(:with_exclusive_scope) do
+ if finder_class.exists?([sql, *params])
+ record.errors.add(attribute, :taken, :default => options[:message], :value => value)
+ end
+ end
+ end
+
+ protected
+
+ # The check for an existing value should be run from a class that
+ # isn't abstract. This means working down from the current class
+ # (self), to the first non-abstract class. Since classes don't know
+ # their subclasses, we have to build the hierarchy between self and
+ # the record's class.
+ def find_finder_class_for(record) #:nodoc:
+ class_hierarchy = [record.class]
+
+ while class_hierarchy.first != @klass
+ class_hierarchy.insert(0, class_hierarchy.first.superclass)
+ end
+
+ class_hierarchy.detect { |klass| !klass.abstract_class? }
+ end
+
+ def mount_sql_and_params(klass, table_name, attribute, value) #:nodoc:
+ column = klass.columns_hash[attribute.to_s]
+
+ operator = if value.nil?
+ "IS ?"
+ elsif column.text?
+ value = column.limit ? value.to_s.mb_chars[0, column.limit] : value.to_s
+ "#{klass.connection.case_sensitive_equality_operator} ?"
+ else
+ "= ?"
+ end
+
+ sql_attribute = "#{table_name}.#{klass.connection.quote_column_name(attribute)}"
+
+ if value.nil? || (options[:case_sensitive] || !column.text?)
+ sql = "#{sql_attribute} #{operator}"
+ params = [value]
+ else
+ sql = "LOWER(#{sql_attribute}) #{operator}"
+ params = [value.mb_chars.downcase]
+ end
+
+ [sql, params]
+ end
+ end
+
module ClassMethods
# Validates whether the value of the specified attributes are unique across the system. Useful for making sure that only one user
# can be named "davidhh".
@@ -69,6 +141,7 @@ module ActiveRecord
#
# This could even happen if you use transactions with the 'serializable'
# isolation level. There are several ways to get around this problem:
+ #
# - By locking the database table before validating, and unlocking it after
# saving. However, table locking is very expensive, and thus not
# recommended.
@@ -94,65 +167,10 @@ module ActiveRecord
# index constraint errors from other types of database errors, so you
# will have to parse the (database-specific) exception message to detect
# such a case.
+ #
def validates_uniqueness_of(*attr_names)
- configuration = { :case_sensitive => true }
- configuration.update(attr_names.extract_options!)
-
- validates_each(attr_names,configuration) do |record, attr_name, value|
- # The check for an existing value should be run from a class that
- # isn't abstract. This means working down from the current class
- # (self), to the first non-abstract class. Since classes don't know
- # their subclasses, we have to build the hierarchy between self and
- # the record's class.
- class_hierarchy = [record.class]
- while class_hierarchy.first != self
- class_hierarchy.insert(0, class_hierarchy.first.superclass)
- end
-
- # Now we can work our way down the tree to the first non-abstract
- # class (which has a database table to query from).
- finder_class = class_hierarchy.detect { |klass| !klass.abstract_class? }
-
- column = finder_class.columns_hash[attr_name.to_s]
-
- if value.nil?
- comparison_operator = "IS ?"
- elsif column.text?
- comparison_operator = "#{connection.case_sensitive_equality_operator} ?"
- value = column.limit ? value.to_s.mb_chars[0, column.limit] : value.to_s
- else
- comparison_operator = "= ?"
- end
-
- sql_attribute = "#{record.class.quoted_table_name}.#{connection.quote_column_name(attr_name)}"
-
- if value.nil? || (configuration[:case_sensitive] || !column.text?)
- condition_sql = "#{sql_attribute} #{comparison_operator}"
- condition_params = [value]
- else
- condition_sql = "LOWER(#{sql_attribute}) #{comparison_operator}"
- condition_params = [value.mb_chars.downcase]
- end
-
- if scope = configuration[:scope]
- Array(scope).map do |scope_item|
- scope_value = record.send(scope_item)
- condition_sql << " AND " << attribute_condition("#{record.class.quoted_table_name}.#{scope_item}", scope_value)
- condition_params << scope_value
- end
- end
-
- unless record.new_record?
- condition_sql << " AND #{record.class.quoted_table_name}.#{record.class.primary_key} <> ?"
- condition_params << record.send(:id)
- end
-
- finder_class.with_exclusive_scope do
- if finder_class.exists?([condition_sql, *condition_params])
- record.errors.add(attr_name, :taken, :default => configuration[:message], :value => value)
- end
- end
- end
+ options = attr_names.extract_options!
+ validates_with UniquenessValidator, options.merge(:attributes => attr_names, :klass => self)
end
end
end