Update to Rails 2.3.8
This commit is contained in:
parent
6677b46cb4
commit
f0635301aa
429 changed files with 17683 additions and 4047 deletions
|
@ -1,5 +1,5 @@
|
|||
#--
|
||||
# Copyright (c) 2004-2009 David Heinemeier Hansson
|
||||
# Copyright (c) 2004-2010 David Heinemeier Hansson
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining
|
||||
# a copy of this software and associated documentation files (the
|
||||
|
@ -80,5 +80,4 @@ module ActiveRecord
|
|||
end
|
||||
end
|
||||
|
||||
require 'active_record/i18n_interpolation_deprecation'
|
||||
I18n.load_path << File.dirname(__FILE__) + '/active_record/locale/en.yml'
|
||||
|
|
|
@ -126,6 +126,7 @@ module ActiveRecord
|
|||
association_proxy = parent_record.send(reflection_name)
|
||||
association_proxy.loaded
|
||||
association_proxy.target.push(*[associated_record].flatten)
|
||||
association_proxy.__send__(:set_inverse_instance, associated_record, parent_record)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -152,9 +153,15 @@ module ActiveRecord
|
|||
seen_keys[associated_record[key].to_s] = true
|
||||
mapped_records = id_to_record_map[associated_record[key].to_s]
|
||||
mapped_records.each do |mapped_record|
|
||||
mapped_record.send("set_#{reflection_name}_target", associated_record)
|
||||
association_proxy = mapped_record.send("set_#{reflection_name}_target", associated_record)
|
||||
association_proxy.__send__(:set_inverse_instance, associated_record, mapped_record)
|
||||
end
|
||||
end
|
||||
|
||||
id_to_record_map.each do |id, records|
|
||||
next if seen_keys.include?(id.to_s)
|
||||
records.each {|record| record.send("set_#{reflection_name}_target", nil) }
|
||||
end
|
||||
end
|
||||
|
||||
# Given a collection of ActiveRecord objects, constructs a Hash which maps
|
||||
|
@ -321,7 +328,7 @@ module ActiveRecord
|
|||
klass = klass_name.constantize
|
||||
|
||||
table_name = klass.quoted_table_name
|
||||
primary_key = klass.primary_key
|
||||
primary_key = reflection.options[:primary_key] || klass.primary_key
|
||||
column_type = klass.columns.detect{|c| c.name == primary_key}.type
|
||||
ids = id_map.keys.map do |id|
|
||||
if column_type == :integer
|
||||
|
|
|
@ -1,4 +1,10 @@
|
|||
module ActiveRecord
|
||||
class InverseOfAssociationNotFoundError < ActiveRecordError #:nodoc:
|
||||
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
|
||||
|
||||
class HasManyThroughAssociationNotFoundError < ActiveRecordError #:nodoc:
|
||||
def initialize(owner_class_name, reflection)
|
||||
super("Could not find the association #{reflection.options[:through].inspect} in model #{owner_class_name}")
|
||||
|
@ -1247,7 +1253,7 @@ module ActiveRecord
|
|||
|
||||
if association.nil? || force_reload
|
||||
association = association_proxy_class.new(self, reflection)
|
||||
retval = association.reload
|
||||
retval = force_reload ? reflection.klass.uncached { association.reload } : association.reload
|
||||
if retval.nil? and association_proxy_class == BelongsToAssociation
|
||||
association_instance_set(reflection.name, nil)
|
||||
return nil
|
||||
|
@ -1301,7 +1307,7 @@ module ActiveRecord
|
|||
association_instance_set(reflection.name, association)
|
||||
end
|
||||
|
||||
association.reload if force_reload
|
||||
reflection.klass.uncached { association.reload } if force_reload
|
||||
|
||||
association
|
||||
end
|
||||
|
@ -1409,40 +1415,39 @@ module ActiveRecord
|
|||
# finder conditions.
|
||||
def configure_dependency_for_has_many(reflection, extra_conditions = nil)
|
||||
if reflection.options.include?(:dependent)
|
||||
# Add polymorphic type if the :as option is present
|
||||
dependent_conditions = []
|
||||
dependent_conditions << "#{reflection.primary_key_name} = \#{record.#{reflection.name}.send(:owner_quoted_id)}"
|
||||
dependent_conditions << "#{reflection.options[:as]}_type = '#{base_class.name}'" if reflection.options[:as]
|
||||
dependent_conditions << sanitize_sql(reflection.options[:conditions], reflection.quoted_table_name) if reflection.options[:conditions]
|
||||
dependent_conditions << extra_conditions if extra_conditions
|
||||
dependent_conditions = dependent_conditions.collect {|where| "(#{where})" }.join(" AND ")
|
||||
dependent_conditions = dependent_conditions.gsub('@', '\@')
|
||||
case reflection.options[:dependent]
|
||||
when :destroy
|
||||
method_name = "has_many_dependent_destroy_for_#{reflection.name}".to_sym
|
||||
define_method(method_name) do
|
||||
send(reflection.name).each { |o| o.destroy }
|
||||
send(reflection.name).each do |o|
|
||||
# No point in executing the counter update since we're going to destroy the parent anyway
|
||||
counter_method = ('belongs_to_counter_cache_before_destroy_for_' + self.class.name.downcase).to_sym
|
||||
if(o.respond_to? counter_method) then
|
||||
class << o
|
||||
self
|
||||
end.send(:define_method, counter_method, Proc.new {})
|
||||
end
|
||||
o.destroy
|
||||
end
|
||||
end
|
||||
before_destroy method_name
|
||||
when :delete_all
|
||||
module_eval %Q{
|
||||
before_destroy do |record| # before_destroy do |record|
|
||||
delete_all_has_many_dependencies(record, # delete_all_has_many_dependencies(record,
|
||||
"#{reflection.name}", # "posts",
|
||||
#{reflection.class_name}, # Post,
|
||||
%@#{dependent_conditions}@) # %@...@) # this is a string literal like %(...)
|
||||
end # end
|
||||
}
|
||||
before_destroy do |record|
|
||||
record.class.send(:delete_all_has_many_dependencies,
|
||||
record,
|
||||
reflection.name,
|
||||
reflection.klass,
|
||||
reflection.dependent_conditions(record, record.class, extra_conditions))
|
||||
end
|
||||
when :nullify
|
||||
module_eval %Q{
|
||||
before_destroy do |record| # before_destroy do |record|
|
||||
nullify_has_many_dependencies(record, # nullify_has_many_dependencies(record,
|
||||
"#{reflection.name}", # "posts",
|
||||
#{reflection.class_name}, # Post,
|
||||
"#{reflection.primary_key_name}", # "user_id",
|
||||
%@#{dependent_conditions}@) # %@...@) # this is a string literal like %(...)
|
||||
end # end
|
||||
}
|
||||
before_destroy do |record|
|
||||
record.class.send(:nullify_has_many_dependencies,
|
||||
record,
|
||||
reflection.name,
|
||||
reflection.klass,
|
||||
reflection.primary_key_name,
|
||||
reflection.dependent_conditions(record, record.class, extra_conditions))
|
||||
end
|
||||
else
|
||||
raise ArgumentError, "The :dependent option expects either :destroy, :delete_all, or :nullify (#{reflection.options[:dependent].inspect})"
|
||||
end
|
||||
|
@ -1526,7 +1531,7 @@ module ActiveRecord
|
|||
:finder_sql, :counter_sql,
|
||||
:before_add, :after_add, :before_remove, :after_remove,
|
||||
:extend, :readonly,
|
||||
:validate
|
||||
:validate, :inverse_of
|
||||
]
|
||||
|
||||
def create_has_many_reflection(association_id, options, &extension)
|
||||
|
@ -1540,7 +1545,7 @@ module ActiveRecord
|
|||
@@valid_keys_for_has_one_association = [
|
||||
:class_name, :foreign_key, :remote, :select, :conditions, :order,
|
||||
:include, :dependent, :counter_cache, :extend, :as, :readonly,
|
||||
:validate, :primary_key
|
||||
:validate, :primary_key, :inverse_of
|
||||
]
|
||||
|
||||
def create_has_one_reflection(association_id, options)
|
||||
|
@ -1559,7 +1564,7 @@ module ActiveRecord
|
|||
@@valid_keys_for_belongs_to_association = [
|
||||
:class_name, :primary_key, :foreign_key, :foreign_type, :remote, :select, :conditions,
|
||||
:include, :dependent, :counter_cache, :extend, :polymorphic, :readonly,
|
||||
:validate, :touch
|
||||
:validate, :touch, :inverse_of
|
||||
]
|
||||
|
||||
def create_belongs_to_reflection(association_id, options)
|
||||
|
@ -1777,7 +1782,7 @@ module ActiveRecord
|
|||
end
|
||||
|
||||
def using_limitable_reflections?(reflections)
|
||||
reflections.reject { |r| [ :belongs_to, :has_one ].include?(r.macro) }.length.zero?
|
||||
reflections.collect(&:collection?).length.zero?
|
||||
end
|
||||
|
||||
def column_aliases(join_dependency)
|
||||
|
@ -1854,7 +1859,7 @@ module ActiveRecord
|
|||
case associations
|
||||
when Symbol, String
|
||||
reflection = base.reflections[associations]
|
||||
if reflection && [:has_many, :has_and_belongs_to_many].include?(reflection.macro)
|
||||
if reflection && reflection.collection?
|
||||
records.each { |record| record.send(reflection.name).target.uniq! }
|
||||
end
|
||||
when Array
|
||||
|
@ -1864,12 +1869,11 @@ module ActiveRecord
|
|||
when Hash
|
||||
associations.keys.each do |name|
|
||||
reflection = base.reflections[name]
|
||||
is_collection = [:has_many, :has_and_belongs_to_many].include?(reflection.macro)
|
||||
|
||||
parent_records = records.map do |record|
|
||||
descendant = record.send(reflection.name)
|
||||
next unless descendant
|
||||
descendant.target.uniq! if is_collection
|
||||
descendant.target.uniq! if reflection.collection?
|
||||
descendant
|
||||
end.flatten.compact
|
||||
|
||||
|
@ -1960,21 +1964,27 @@ module ActiveRecord
|
|||
return nil if record.id.to_s != join.parent.record_id(row).to_s or row[join.aliased_primary_key].nil?
|
||||
association = join.instantiate(row)
|
||||
collection.target.push(association)
|
||||
collection.__send__(:set_inverse_instance, association, record)
|
||||
when :has_one
|
||||
return if record.id.to_s != join.parent.record_id(row).to_s
|
||||
return if record.instance_variable_defined?("@#{join.reflection.name}")
|
||||
association = join.instantiate(row) unless row[join.aliased_primary_key].nil?
|
||||
record.send("set_#{join.reflection.name}_target", association)
|
||||
set_target_and_inverse(join, association, record)
|
||||
when :belongs_to
|
||||
return if record.id.to_s != join.parent.record_id(row).to_s or row[join.aliased_primary_key].nil?
|
||||
association = join.instantiate(row)
|
||||
record.send("set_#{join.reflection.name}_target", association)
|
||||
set_target_and_inverse(join, association, record)
|
||||
else
|
||||
raise ConfigurationError, "unknown macro: #{join.reflection.macro}"
|
||||
end
|
||||
return association
|
||||
end
|
||||
|
||||
def set_target_and_inverse(join, association, record)
|
||||
association_proxy = record.send("set_#{join.reflection.name}_target", association)
|
||||
association_proxy.__send__(:set_inverse_instance, association, record)
|
||||
end
|
||||
|
||||
class JoinBase # :nodoc:
|
||||
attr_reader :active_record, :table_joins
|
||||
delegate :table_name, :column_names, :primary_key, :reflections, :sanitize_sql, :to => :active_record
|
||||
|
@ -2162,7 +2172,7 @@ module ActiveRecord
|
|||
" #{join_type} %s ON %s.%s = %s.%s " % [
|
||||
table_name_and_alias,
|
||||
connection.quote_table_name(aliased_table_name),
|
||||
reflection.klass.primary_key,
|
||||
reflection.options[:primary_key] || reflection.klass.primary_key,
|
||||
connection.quote_table_name(parent.aliased_table_name),
|
||||
options[:foreign_key] || reflection.primary_key_name
|
||||
]
|
||||
|
|
|
@ -400,11 +400,24 @@ module ActiveRecord
|
|||
find(:all)
|
||||
end
|
||||
|
||||
@reflection.options[:uniq] ? uniq(records) : records
|
||||
records = @reflection.options[:uniq] ? uniq(records) : records
|
||||
records.each do |record|
|
||||
set_inverse_instance(record, @owner)
|
||||
end
|
||||
records
|
||||
end
|
||||
|
||||
def add_record_to_target_with_callbacks(record)
|
||||
callback(:before_add, record)
|
||||
yield(record) if block_given?
|
||||
@target ||= [] unless loaded?
|
||||
@target << record unless @reflection.options[:uniq] && @target.include?(record)
|
||||
callback(:after_add, record)
|
||||
set_inverse_instance(record, @owner)
|
||||
record
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def create_record(attrs)
|
||||
attrs.update(@reflection.options[:conditions]) if @reflection.options[:conditions].is_a?(Hash)
|
||||
ensure_owner_is_not_new
|
||||
|
@ -428,15 +441,6 @@ module ActiveRecord
|
|||
end
|
||||
end
|
||||
|
||||
def add_record_to_target_with_callbacks(record)
|
||||
callback(:before_add, record)
|
||||
yield(record) if block_given?
|
||||
@target ||= [] unless loaded?
|
||||
@target << record unless @reflection.options[:uniq] && @target.include?(record)
|
||||
callback(:after_add, record)
|
||||
record
|
||||
end
|
||||
|
||||
def remove_records(*records)
|
||||
records = flatten_deeper(records)
|
||||
records.each { |record| raise_on_type_mismatch(record) }
|
||||
|
|
|
@ -53,6 +53,7 @@ module ActiveRecord
|
|||
|
||||
def initialize(owner, reflection)
|
||||
@owner, @reflection = owner, reflection
|
||||
reflection.check_validity!
|
||||
Array(reflection.options[:extend]).each { |ext| proxy_extend(ext) }
|
||||
reset
|
||||
end
|
||||
|
@ -208,14 +209,10 @@ module ActiveRecord
|
|||
|
||||
private
|
||||
# Forwards any missing method call to the \target.
|
||||
def method_missing(method, *args)
|
||||
def method_missing(method, *args, &block)
|
||||
if load_target
|
||||
if @target.respond_to?(method)
|
||||
if block_given?
|
||||
@target.send(method, *args) { |*block_args| yield(*block_args) }
|
||||
else
|
||||
@target.send(method, *args)
|
||||
end
|
||||
@target.send(method, *args, &block)
|
||||
else
|
||||
super
|
||||
end
|
||||
|
@ -273,6 +270,19 @@ module ActiveRecord
|
|||
def owner_quoted_id
|
||||
@owner.quoted_id
|
||||
end
|
||||
|
||||
def set_inverse_instance(record, instance)
|
||||
return if record.nil? || !we_can_set_the_inverse_on_this?(record)
|
||||
inverse_relationship = @reflection.inverse_of
|
||||
unless inverse_relationship.nil?
|
||||
record.send(:"set_#{inverse_relationship.name}_target", instance)
|
||||
end
|
||||
end
|
||||
|
||||
# Override in subclasses
|
||||
def we_can_set_the_inverse_on_this?(record)
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -31,6 +31,8 @@ module ActiveRecord
|
|||
@updated = true
|
||||
end
|
||||
|
||||
set_inverse_instance(record, @owner)
|
||||
|
||||
loaded
|
||||
record
|
||||
end
|
||||
|
@ -46,13 +48,15 @@ module ActiveRecord
|
|||
else
|
||||
"find"
|
||||
end
|
||||
@reflection.klass.send(find_method,
|
||||
the_target = @reflection.klass.send(find_method,
|
||||
@owner[@reflection.primary_key_name],
|
||||
:select => @reflection.options[:select],
|
||||
:conditions => conditions,
|
||||
:include => @reflection.options[:include],
|
||||
:readonly => @reflection.options[:readonly]
|
||||
) if @owner[@reflection.primary_key_name]
|
||||
set_inverse_instance(the_target, @owner)
|
||||
the_target
|
||||
end
|
||||
|
||||
def foreign_key_present
|
||||
|
@ -71,6 +75,12 @@ module ActiveRecord
|
|||
@owner[@reflection.primary_key_name]
|
||||
end
|
||||
end
|
||||
|
||||
# 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)
|
||||
@reflection.has_inverse? && @reflection.inverse_of.macro == :has_one
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -13,6 +13,7 @@ module ActiveRecord
|
|||
@updated = true
|
||||
end
|
||||
|
||||
set_inverse_instance(record, @owner)
|
||||
loaded
|
||||
record
|
||||
end
|
||||
|
@ -22,19 +23,42 @@ module ActiveRecord
|
|||
end
|
||||
|
||||
private
|
||||
|
||||
# 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
|
||||
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?
|
||||
|
||||
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
|
||||
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
|
||||
|
|
|
@ -117,6 +117,11 @@ module ActiveRecord
|
|||
:create => create_scoping
|
||||
}
|
||||
end
|
||||
|
||||
def we_can_set_the_inverse_on_this?(record)
|
||||
inverse = @reflection.inverse_of
|
||||
return !inverse.nil?
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,11 +1,6 @@
|
|||
module ActiveRecord
|
||||
module Associations
|
||||
class HasManyThroughAssociation < HasManyAssociation #:nodoc:
|
||||
def initialize(owner, reflection)
|
||||
reflection.check_validity!
|
||||
super
|
||||
end
|
||||
|
||||
alias_method :new, :build
|
||||
|
||||
def create!(attrs = nil)
|
||||
|
@ -261,6 +256,11 @@ module ActiveRecord
|
|||
def cached_counter_attribute_name
|
||||
"#{@reflection.name}_count"
|
||||
end
|
||||
|
||||
# NOTE - not sure that we can actually cope with inverses here
|
||||
def we_can_set_the_inverse_on_this?(record)
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -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
|
||||
|
@ -77,13 +78,15 @@ module ActiveRecord
|
|||
|
||||
private
|
||||
def find_target
|
||||
@reflection.klass.find(:first,
|
||||
the_target = @reflection.klass.find(:first,
|
||||
:conditions => @finder_sql,
|
||||
:select => @reflection.options[:select],
|
||||
:order => @reflection.options[:order],
|
||||
:include => @reflection.options[:include],
|
||||
:readonly => @reflection.options[:readonly]
|
||||
)
|
||||
set_inverse_instance(the_target, @owner)
|
||||
the_target
|
||||
end
|
||||
|
||||
def construct_sql
|
||||
|
@ -118,6 +121,7 @@ module ActiveRecord
|
|||
else
|
||||
record[@reflection.primary_key_name] = @owner.id unless @owner.new_record?
|
||||
self.target = record
|
||||
set_inverse_instance(record, @owner)
|
||||
end
|
||||
|
||||
record
|
||||
|
@ -128,6 +132,11 @@ module ActiveRecord
|
|||
attrs.update(@reflection.options[:conditions]) if @reflection.options[:conditions].is_a?(Hash)
|
||||
attrs
|
||||
end
|
||||
|
||||
def we_can_set_the_inverse_on_this?(record)
|
||||
inverse = @reflection.inverse_of
|
||||
return !inverse.nil?
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -208,7 +208,7 @@ module ActiveRecord
|
|||
end
|
||||
|
||||
begin
|
||||
class_eval(method_definition, __FILE__, __LINE__)
|
||||
class_eval(method_definition, __FILE__)
|
||||
rescue SyntaxError => err
|
||||
generated_methods.delete(attr_name)
|
||||
if logger
|
||||
|
@ -230,6 +230,10 @@ module ActiveRecord
|
|||
# It's also possible to instantiate related objects, so a Client class belonging to the clients
|
||||
# table with a +master_id+ foreign key can instantiate master through Client#master.
|
||||
def method_missing(method_id, *args, &block)
|
||||
if method_id == :to_ary || method_id == :to_str
|
||||
raise NoMethodError, "undefined method `#{method_id}' for #{inspect}:#{self.class}"
|
||||
end
|
||||
|
||||
method_name = method_id.to_s
|
||||
|
||||
if self.class.private_method_defined?(method_name)
|
||||
|
|
|
@ -156,38 +156,42 @@ 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, which is
|
||||
# called _after_ the association has been defined. Since we don't want
|
||||
# the callbacks to get defined multiple times, there are guards that
|
||||
# check if the save or validation methods have already been defined
|
||||
# before actually defining them.
|
||||
def add_autosave_association_callbacks(reflection)
|
||||
save_method = "autosave_associated_records_for_#{reflection.name}"
|
||||
validation_method = "validate_associated_records_for_#{reflection.name}"
|
||||
force_validation = (reflection.options[:validate] == true || reflection.options[:autosave] == true)
|
||||
save_method = :"autosave_associated_records_for_#{reflection.name}"
|
||||
validation_method = :"validate_associated_records_for_#{reflection.name}"
|
||||
collection = reflection.collection?
|
||||
|
||||
case reflection.macro
|
||||
when :has_many, :has_and_belongs_to_many
|
||||
before_save :before_save_collection_association
|
||||
unless method_defined?(save_method)
|
||||
if collection
|
||||
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
|
||||
|
||||
if 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
|
||||
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
|
||||
else
|
||||
if reflection.macro == :has_one
|
||||
define_method(save_method) { save_has_one_association(reflection) }
|
||||
after_save save_method
|
||||
else
|
||||
define_method(save_method) { save_belongs_to_association(reflection) }
|
||||
before_save save_method
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if force_validation
|
||||
define_method(validation_method) { validate_single_association(reflection) }
|
||||
validate validation_method
|
||||
end
|
||||
if reflection.validate? && !method_defined?(validation_method)
|
||||
method = (collection ? :validate_collection_association : :validate_single_association)
|
||||
define_method(validation_method) { send(method, reflection) }
|
||||
validate validation_method
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -213,6 +217,12 @@ module ActiveRecord
|
|||
@marked_for_destruction
|
||||
end
|
||||
|
||||
# Returns whether or not this record has been changed in any way (including whether
|
||||
# any of its nested autosave associations are likewise changed)
|
||||
def changed_for_autosave?
|
||||
new_record? || changed? || marked_for_destruction? || nested_records_changed_for_autosave?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Returns the record for an association collection that should be validated
|
||||
|
@ -221,13 +231,28 @@ module ActiveRecord
|
|||
def associated_records_to_validate_or_save(association, new_record, autosave)
|
||||
if new_record
|
||||
association
|
||||
elsif association.loaded?
|
||||
autosave ? association : association.select { |record| record.new_record? }
|
||||
elsif autosave
|
||||
association.target.select { |record| record.changed_for_autosave? }
|
||||
else
|
||||
autosave ? association.target : association.target.select { |record| record.new_record? }
|
||||
association.target.select { |record| record.new_record? }
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# go through nested autosave associations that are loaded in memory (without loading
|
||||
# any new ones), and return true if is changed for autosave
|
||||
def nested_records_changed_for_autosave?
|
||||
self.class.reflect_on_all_autosave_associations.each do |reflection|
|
||||
if association = association_instance_get(reflection.name)
|
||||
if [:belongs_to, :has_one].include?(reflection.macro)
|
||||
return true if association.target && association.target.changed_for_autosave?
|
||||
else
|
||||
association.target.each {|record| return true if record.changed_for_autosave? }
|
||||
end
|
||||
end
|
||||
end
|
||||
false
|
||||
end
|
||||
|
||||
# Validate the association if <tt>:validate</tt> or <tt>:autosave</tt> is
|
||||
# turned on for the association specified by +reflection+.
|
||||
def validate_single_association(reflection)
|
||||
|
@ -293,13 +318,15 @@ module ActiveRecord
|
|||
association.destroy(record)
|
||||
elsif autosave != false && (@new_record_before_save || record.new_record?)
|
||||
if autosave
|
||||
association.send(:insert_record, record, false, false)
|
||||
saved = association.send(:insert_record, record, false, false)
|
||||
else
|
||||
association.send(:insert_record, record)
|
||||
end
|
||||
elsif autosave
|
||||
record.save(false)
|
||||
saved = record.save(false)
|
||||
end
|
||||
|
||||
raise ActiveRecord::Rollback if saved == false
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -326,7 +353,9 @@ module ActiveRecord
|
|||
key = reflection.options[:primary_key] ? send(reflection.options[:primary_key]) : id
|
||||
if autosave != false && (new_record? || association.new_record? || association[reflection.primary_key_name] != key || autosave)
|
||||
association[reflection.primary_key_name] = key
|
||||
association.save(!autosave)
|
||||
saved = association.save(!autosave)
|
||||
raise ActiveRecord::Rollback if !saved && autosave
|
||||
saved
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -347,7 +376,7 @@ module ActiveRecord
|
|||
if autosave && association.marked_for_destruction?
|
||||
association.destroy
|
||||
elsif autosave != false
|
||||
association.save(!autosave) if association.new_record? || autosave
|
||||
saved = association.save(!autosave) if association.new_record? || autosave
|
||||
|
||||
if association.updated?
|
||||
association_id = association.send(reflection.options[:primary_key] || :id)
|
||||
|
@ -357,6 +386,8 @@ module ActiveRecord
|
|||
self[reflection.options[:foreign_type]] = association.class.base_class.name.to_s
|
||||
end
|
||||
end
|
||||
|
||||
saved if autosave
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
113
vendor/rails/activerecord/lib/active_record/base.rb
vendored
113
vendor/rails/activerecord/lib/active_record/base.rb
vendored
|
@ -461,6 +461,9 @@ module ActiveRecord #:nodoc:
|
|||
# Accessor for the name of the prefix string to prepend to every table name. So if set to "basecamp_", all
|
||||
# table names will be named like "basecamp_projects", "basecamp_people", etc. This is a convenient way of creating a namespace
|
||||
# for tables in a shared database. By default, the prefix is the empty string.
|
||||
#
|
||||
# If you are organising your models within modules you can add a prefix to the models within a namespace by defining
|
||||
# a singleton method in the parent module called table_name_prefix which returns your chosen prefix.
|
||||
cattr_accessor :table_name_prefix, :instance_writer => false
|
||||
@@table_name_prefix = ""
|
||||
|
||||
|
@ -916,6 +919,29 @@ module ActiveRecord #:nodoc:
|
|||
connection.select_value(sql, "#{name} Count").to_i
|
||||
end
|
||||
|
||||
# Resets one or more counter caches to their correct value using an SQL
|
||||
# count query. This is useful when adding new counter caches, or if the
|
||||
# counter has been corrupted or modified directly by SQL.
|
||||
#
|
||||
# ==== Parameters
|
||||
#
|
||||
# * +id+ - The id of the object you wish to reset a counter on.
|
||||
# * +counters+ - One or more counter names to reset
|
||||
#
|
||||
# ==== Examples
|
||||
#
|
||||
# # For Post with id #1 records reset the comments_count
|
||||
# Post.reset_counters(1, :comments)
|
||||
def reset_counters(id, *counters)
|
||||
object = find(id)
|
||||
counters.each do |association|
|
||||
child_class = reflect_on_association(association).klass
|
||||
counter_name = child_class.reflect_on_association(self.name.downcase.to_sym).counter_cache_column
|
||||
|
||||
connection.update("UPDATE #{quoted_table_name} SET #{connection.quote_column_name(counter_name)} = #{object.send(association).count} WHERE #{connection.quote_column_name(primary_key)} = #{quote_value(object.id)}", "#{name} UPDATE")
|
||||
end
|
||||
end
|
||||
|
||||
# A generic "counter updater" implementation, intended primarily to be
|
||||
# used by increment_counter and decrement_counter, but which may also
|
||||
# be useful on its own. It simply does a direct SQL update for the record
|
||||
|
@ -1148,7 +1174,7 @@ module ActiveRecord #:nodoc:
|
|||
contained = contained.singularize if parent.pluralize_table_names
|
||||
contained << '_'
|
||||
end
|
||||
name = "#{table_name_prefix}#{contained}#{undecorated_table_name(base.name)}#{table_name_suffix}"
|
||||
name = "#{full_table_name_prefix}#{contained}#{undecorated_table_name(base.name)}#{table_name_suffix}"
|
||||
end
|
||||
|
||||
set_table_name(name)
|
||||
|
@ -1178,6 +1204,10 @@ module ActiveRecord #:nodoc:
|
|||
key
|
||||
end
|
||||
|
||||
def full_table_name_prefix #:nodoc:
|
||||
(parents.detect{ |p| p.respond_to?(:table_name_prefix) } || self).table_name_prefix
|
||||
end
|
||||
|
||||
# Defines the column name for use with single table inheritance
|
||||
# -- can be set in subclasses like so: self.inheritance_column = "type_id"
|
||||
def inheritance_column
|
||||
|
@ -1861,7 +1891,7 @@ module ActiveRecord #:nodoc:
|
|||
# find(:first, options.merge(finder_options))
|
||||
# end
|
||||
# end
|
||||
self.class_eval %{
|
||||
self.class_eval <<-EOS, __FILE__, __LINE__ + 1
|
||||
def self.#{method_id}(*args)
|
||||
options = args.extract_options!
|
||||
attributes = construct_attributes_from_arguments(
|
||||
|
@ -1881,7 +1911,7 @@ module ActiveRecord #:nodoc:
|
|||
end
|
||||
#{'result || raise(RecordNotFound, "Couldn\'t find #{name} with #{attributes.to_a.collect {|pair| "#{pair.first} = #{pair.second}"}.join(\', \')}")' if bang}
|
||||
end
|
||||
}, __FILE__, __LINE__
|
||||
EOS
|
||||
send(method_id, *arguments)
|
||||
elsif match.instantiator?
|
||||
instantiator = match.instantiator
|
||||
|
@ -1910,25 +1940,30 @@ module ActiveRecord #:nodoc:
|
|||
# record
|
||||
# end
|
||||
# end
|
||||
self.class_eval %{
|
||||
self.class_eval <<-EOS, __FILE__, __LINE__ + 1
|
||||
def self.#{method_id}(*args)
|
||||
guard_protected_attributes = false
|
||||
|
||||
if args[0].is_a?(Hash)
|
||||
guard_protected_attributes = true
|
||||
attributes = args[0].with_indifferent_access
|
||||
find_attributes = attributes.slice(*[:#{attribute_names.join(',:')}])
|
||||
else
|
||||
find_attributes = attributes = construct_attributes_from_arguments([:#{attribute_names.join(',:')}], args)
|
||||
attributes = [:#{attribute_names.join(',:')}]
|
||||
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
|
||||
|
||||
find_attributes = (protected_attributes_for_create.merge(unprotected_attributes_for_create)).slice(*attributes)
|
||||
|
||||
options = { :conditions => find_attributes }
|
||||
set_readonly_option!(options)
|
||||
|
||||
record = find(:first, options)
|
||||
|
||||
if record.nil?
|
||||
record = self.new { |r| r.send(:attributes=, attributes, guard_protected_attributes) }
|
||||
record = self.new do |r|
|
||||
r.send(:attributes=, protected_attributes_for_create, true) unless protected_attributes_for_create.empty?
|
||||
r.send(:attributes=, unprotected_attributes_for_create, false) unless unprotected_attributes_for_create.empty?
|
||||
end
|
||||
#{'yield(record) if block_given?'}
|
||||
#{'record.save' if instantiator == :create}
|
||||
record
|
||||
|
@ -1936,14 +1971,14 @@ module ActiveRecord #:nodoc:
|
|||
record
|
||||
end
|
||||
end
|
||||
}, __FILE__, __LINE__
|
||||
EOS
|
||||
send(method_id, *arguments, &block)
|
||||
end
|
||||
elsif match = DynamicScopeMatch.match(method_id)
|
||||
attribute_names = match.attribute_names
|
||||
super unless all_attributes_exists?(attribute_names)
|
||||
if match.scope?
|
||||
self.class_eval %{
|
||||
self.class_eval <<-EOS, __FILE__, __LINE__ + 1
|
||||
def self.#{method_id}(*args) # def self.scoped_by_user_name_and_password(*args)
|
||||
options = args.extract_options! # options = args.extract_options!
|
||||
attributes = construct_attributes_from_arguments( # attributes = construct_attributes_from_arguments(
|
||||
|
@ -1952,7 +1987,7 @@ module ActiveRecord #:nodoc:
|
|||
#
|
||||
scoped(:conditions => attributes) # scoped(:conditions => attributes)
|
||||
end # end
|
||||
}, __FILE__, __LINE__
|
||||
EOS
|
||||
send(method_id, *arguments)
|
||||
end
|
||||
else
|
||||
|
@ -2194,9 +2229,9 @@ module ActiveRecord #:nodoc:
|
|||
modularized_name = type_name_with_module(type_name)
|
||||
silence_warnings do
|
||||
begin
|
||||
class_eval(modularized_name, __FILE__, __LINE__)
|
||||
class_eval(modularized_name, __FILE__)
|
||||
rescue NameError
|
||||
class_eval(type_name, __FILE__, __LINE__)
|
||||
class_eval(type_name, __FILE__)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -2436,7 +2471,7 @@ module ActiveRecord #:nodoc:
|
|||
@new_record = true
|
||||
ensure_proper_type
|
||||
self.attributes = attributes unless attributes.nil?
|
||||
self.class.send(:scope, :create).each { |att,value| self.send("#{att}=", value) } if self.class.send(:scoped?, :create)
|
||||
assign_attributes(self.class.send(:scope, :create)) if self.class.send(:scoped?, :create)
|
||||
result = yield self if block_given?
|
||||
callback(:after_initialize) if respond_to_without_attributes?(:after_initialize)
|
||||
result
|
||||
|
@ -2693,7 +2728,7 @@ module ActiveRecord #:nodoc:
|
|||
def reload(options = nil)
|
||||
clear_aggregation_cache
|
||||
clear_association_cache
|
||||
@attributes.update(self.class.find(self.id, options).instance_variable_get('@attributes'))
|
||||
@attributes.update(self.class.send(:with_exclusive_scope) { self.class.find(self.id, options) }.instance_variable_get('@attributes'))
|
||||
@attributes_cache = {}
|
||||
self
|
||||
end
|
||||
|
@ -2736,26 +2771,15 @@ module ActiveRecord #:nodoc:
|
|||
attributes = new_attributes.dup
|
||||
attributes.stringify_keys!
|
||||
|
||||
multi_parameter_attributes = []
|
||||
attributes = remove_attributes_protected_from_mass_assignment(attributes) if guard_protected_attributes
|
||||
|
||||
attributes.each do |k, v|
|
||||
if k.include?("(")
|
||||
multi_parameter_attributes << [ k, v ]
|
||||
else
|
||||
respond_to?(:"#{k}=") ? send(:"#{k}=", v) : raise(UnknownAttributeError, "unknown attribute: #{k}")
|
||||
end
|
||||
end
|
||||
|
||||
assign_multiparameter_attributes(multi_parameter_attributes)
|
||||
assign_attributes(attributes) if attributes and attributes.any?
|
||||
end
|
||||
|
||||
# Returns a hash of all the attributes with their names as keys and the values of the attributes as values.
|
||||
def attributes
|
||||
self.attribute_names.inject({}) do |attrs, name|
|
||||
attrs[name] = read_attribute(name)
|
||||
attrs
|
||||
end
|
||||
attrs = {}
|
||||
attribute_names.each { |name| attrs[name] = read_attribute(name) }
|
||||
attrs
|
||||
end
|
||||
|
||||
# Returns a hash of attributes before typecasting and deserialization.
|
||||
|
@ -2869,6 +2893,23 @@ module ActiveRecord #:nodoc:
|
|||
end
|
||||
|
||||
private
|
||||
# Assigns attributes, dealing nicely with both multi and single paramater attributes
|
||||
# Assumes attributes is a hash
|
||||
|
||||
def assign_attributes(attributes={})
|
||||
multiparameter_attributes = []
|
||||
|
||||
attributes.each do |k, v|
|
||||
if k.to_s.include?("(")
|
||||
multiparameter_attributes << [ k, v ]
|
||||
else
|
||||
respond_to?(:"#{k}=") ? send(:"#{k}=", v) : raise(UnknownAttributeError, "unknown attribute: #{k}")
|
||||
end
|
||||
end
|
||||
|
||||
assign_multiparameter_attributes(multiparameter_attributes) unless multiparameter_attributes.empty?
|
||||
end
|
||||
|
||||
def create_or_update
|
||||
raise ReadOnlyRecord if readonly?
|
||||
result = new_record? ? create : update
|
||||
|
@ -3099,7 +3140,7 @@ module ActiveRecord #:nodoc:
|
|||
|
||||
# Returns a comma-separated pair list, like "key1 = val1, key2 = val2".
|
||||
def comma_pair_list(hash)
|
||||
hash.inject([]) { |list, pair| list << "#{pair.first} = #{pair.last}" }.join(", ")
|
||||
hash.map { |k,v| "#{k} = #{v}" }.join(", ")
|
||||
end
|
||||
|
||||
def quoted_column_names(attributes = attributes_with_quotes)
|
||||
|
|
|
@ -59,19 +59,23 @@ module ActiveRecord
|
|||
start = options.delete(:start).to_i
|
||||
batch_size = options.delete(:batch_size) || 1000
|
||||
|
||||
with_scope(:find => options.merge(:order => batch_order, :limit => batch_size)) do
|
||||
records = find(:all, :conditions => [ "#{table_name}.#{primary_key} >= ?", start ])
|
||||
proxy = scoped(options.merge(:order => batch_order, :limit => batch_size))
|
||||
records = proxy.find(:all, :conditions => [ "#{table_name}.#{primary_key} >= ?", start ])
|
||||
|
||||
while records.any?
|
||||
yield records
|
||||
while records.any?
|
||||
yield records
|
||||
|
||||
break if records.size < batch_size
|
||||
records = find(:all, :conditions => [ "#{table_name}.#{primary_key} > ?", records.last.id ])
|
||||
end
|
||||
break if records.size < batch_size
|
||||
|
||||
last_value = records.last.id
|
||||
|
||||
raise "You must include the primary key if you define a select" unless last_value.present?
|
||||
|
||||
records = proxy.find(:all, :conditions => [ "#{table_name}.#{primary_key} > ?", last_value ])
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
|
||||
private
|
||||
def batch_order
|
||||
"#{table_name}.#{primary_key} ASC"
|
||||
|
|
|
@ -294,12 +294,15 @@ module ActiveRecord
|
|||
end
|
||||
|
||||
def type_cast_calculated_value(value, column, operation = nil)
|
||||
operation = operation.to_s.downcase
|
||||
case operation
|
||||
if value.is_a?(String) || value.nil?
|
||||
case operation.to_s.downcase
|
||||
when 'count' then value.to_i
|
||||
when 'sum' then type_cast_using_column(value || '0', column)
|
||||
when 'avg' then value && (value.is_a?(Fixnum) ? value.to_f : value).to_d
|
||||
when 'avg' then value.try(:to_d)
|
||||
else type_cast_using_column(value, column)
|
||||
end
|
||||
else
|
||||
value
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -10,8 +10,8 @@ module ActiveRecord
|
|||
##
|
||||
# :singleton-method:
|
||||
# The connection handler
|
||||
cattr_accessor :connection_handler, :instance_writer => false
|
||||
@@connection_handler = ConnectionAdapters::ConnectionHandler.new
|
||||
superclass_delegating_accessor :connection_handler
|
||||
self.connection_handler = ConnectionAdapters::ConnectionHandler.new
|
||||
|
||||
# Returns the connection currently associated with the class. This can
|
||||
# also be used to "borrow" the connection to do database work that isn't
|
||||
|
@ -54,7 +54,7 @@ module ActiveRecord
|
|||
raise AdapterNotSpecified unless defined? RAILS_ENV
|
||||
establish_connection(RAILS_ENV)
|
||||
when ConnectionSpecification
|
||||
@@connection_handler.establish_connection(name, spec)
|
||||
self.connection_handler.establish_connection(name, spec)
|
||||
when Symbol, String
|
||||
if configuration = configurations[spec.to_s]
|
||||
establish_connection(configuration)
|
||||
|
|
57
vendor/rails/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb
vendored
Normal file
57
vendor/rails/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb
vendored
Normal file
|
@ -0,0 +1,57 @@
|
|||
module ActiveRecord
|
||||
module ConnectionAdapters # :nodoc:
|
||||
module DatabaseLimits
|
||||
|
||||
# the maximum length of a table alias
|
||||
def table_alias_length
|
||||
255
|
||||
end
|
||||
|
||||
# the maximum length of a column name
|
||||
def column_name_length
|
||||
64
|
||||
end
|
||||
|
||||
# the maximum length of a table name
|
||||
def table_name_length
|
||||
64
|
||||
end
|
||||
|
||||
# the maximum length of an index name
|
||||
def index_name_length
|
||||
64
|
||||
end
|
||||
|
||||
# the maximum number of columns per table
|
||||
def columns_per_table
|
||||
1024
|
||||
end
|
||||
|
||||
# the maximum number of indexes per table
|
||||
def indexes_per_table
|
||||
16
|
||||
end
|
||||
|
||||
# the maximum number of columns in a multicolumn index
|
||||
def columns_per_multicolumn_index
|
||||
16
|
||||
end
|
||||
|
||||
# the maximum number of elements in an IN (x,y,z) clause
|
||||
def in_clause_length
|
||||
65535
|
||||
end
|
||||
|
||||
# the maximum length of a SQL query
|
||||
def sql_query_length
|
||||
1048575
|
||||
end
|
||||
|
||||
# maximum number of joins in a single query
|
||||
def joins_per_query
|
||||
256
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
|
@ -13,7 +13,7 @@ module ActiveRecord
|
|||
|
||||
def dirties_query_cache(base, *method_names)
|
||||
method_names.each do |method_name|
|
||||
base.class_eval <<-end_code, __FILE__, __LINE__
|
||||
base.class_eval <<-end_code, __FILE__, __LINE__ + 1
|
||||
def #{method_name}_with_query_dirty(*args) # def update_with_query_dirty(*args)
|
||||
clear_query_cache if @query_cache_enabled # clear_query_cache if @query_cache_enabled
|
||||
#{method_name}_without_query_dirty(*args) # update_without_query_dirty(*args)
|
||||
|
|
|
@ -11,12 +11,12 @@ module ActiveRecord
|
|||
when String, ActiveSupport::Multibyte::Chars
|
||||
value = value.to_s
|
||||
if column && column.type == :binary && column.class.respond_to?(:string_to_binary)
|
||||
"#{quoted_string_prefix}'#{quote_string(column.class.string_to_binary(value))}'" # ' (for ruby-mode)
|
||||
"'#{quote_string(column.class.string_to_binary(value))}'" # ' (for ruby-mode)
|
||||
elsif column && [:integer, :float].include?(column.type)
|
||||
value = column.type == :integer ? value.to_i : value.to_f
|
||||
value.to_s
|
||||
else
|
||||
"#{quoted_string_prefix}'#{quote_string(value)}'" # ' (for ruby-mode)
|
||||
"'#{quote_string(value)}'" # ' (for ruby-mode)
|
||||
end
|
||||
when NilClass then "NULL"
|
||||
when TrueClass then (column && column.type == :integer ? '1' : quoted_true)
|
||||
|
@ -28,7 +28,7 @@ module ActiveRecord
|
|||
if value.acts_like?(:date) || value.acts_like?(:time)
|
||||
"'#{quoted_date(value)}'"
|
||||
else
|
||||
"#{quoted_string_prefix}'#{quote_string(value.to_yaml)}'"
|
||||
"'#{quote_string(value.to_yaml)}'"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -60,10 +60,6 @@ module ActiveRecord
|
|||
def quoted_date(value)
|
||||
value.to_s(:db)
|
||||
end
|
||||
|
||||
def quoted_string_prefix
|
||||
''
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -256,7 +256,7 @@ module ActiveRecord
|
|||
end
|
||||
end
|
||||
|
||||
class IndexDefinition < Struct.new(:table, :name, :unique, :columns) #:nodoc:
|
||||
class IndexDefinition < Struct.new(:table, :name, :unique, :columns, :lengths) #:nodoc:
|
||||
end
|
||||
|
||||
# Abstract representation of a column definition. Instances of this type
|
||||
|
|
|
@ -8,11 +8,6 @@ module ActiveRecord
|
|||
{}
|
||||
end
|
||||
|
||||
# This is the maximum length a table alias can be
|
||||
def table_alias_length
|
||||
255
|
||||
end
|
||||
|
||||
# Truncates a table alias according to the limits of the current adapter.
|
||||
def table_alias_for(table_name)
|
||||
table_name[0..table_alias_length-1].gsub(/\./, '_')
|
||||
|
@ -101,7 +96,7 @@ module ActiveRecord
|
|||
table_definition = TableDefinition.new(self)
|
||||
table_definition.primary_key(options[:primary_key] || Base.get_primary_key(table_name.to_s.singularize)) unless options[:id] == false
|
||||
|
||||
yield table_definition
|
||||
yield table_definition if block_given?
|
||||
|
||||
if options[:force] && table_exists?(table_name)
|
||||
drop_table(table_name, options)
|
||||
|
@ -246,18 +241,32 @@ module ActiveRecord
|
|||
# name.
|
||||
#
|
||||
# ===== Examples
|
||||
#
|
||||
# ====== Creating a simple index
|
||||
# add_index(:suppliers, :name)
|
||||
# generates
|
||||
# CREATE INDEX suppliers_name_index ON suppliers(name)
|
||||
#
|
||||
# ====== Creating a unique index
|
||||
# add_index(:accounts, [:branch_id, :party_id], :unique => true)
|
||||
# generates
|
||||
# CREATE UNIQUE INDEX accounts_branch_id_party_id_index ON accounts(branch_id, party_id)
|
||||
#
|
||||
# ====== Creating a named index
|
||||
# add_index(:accounts, [:branch_id, :party_id], :unique => true, :name => 'by_branch_party')
|
||||
# generates
|
||||
# CREATE UNIQUE INDEX by_branch_party ON accounts(branch_id, party_id)
|
||||
#
|
||||
# ====== Creating an index with specific key length
|
||||
# add_index(:accounts, :name, :name => 'by_name', :length => 10)
|
||||
# generates
|
||||
# CREATE INDEX by_name ON accounts(name(10))
|
||||
#
|
||||
# add_index(:accounts, [:name, :surname], :name => 'by_name_surname', :length => {:name => 10, :surname => 15})
|
||||
# generates
|
||||
# CREATE INDEX by_name_surname ON accounts(name(10), surname(15))
|
||||
#
|
||||
# Note: SQLite doesn't support index length
|
||||
def add_index(table_name, column_name, options = {})
|
||||
column_names = Array(column_name)
|
||||
index_name = index_name(table_name, :column => column_names)
|
||||
|
@ -268,7 +277,17 @@ module ActiveRecord
|
|||
else
|
||||
index_type = options
|
||||
end
|
||||
quoted_column_names = column_names.map { |e| quote_column_name(e) }.join(", ")
|
||||
|
||||
if index_name.length > index_name_length
|
||||
@logger.warn("Index name '#{index_name}' on table '#{table_name}' is too long; the limit is #{index_name_length} characters. Skipping.")
|
||||
return
|
||||
end
|
||||
if index_exists?(table_name, index_name, false)
|
||||
@logger.warn("Index name '#{index_name}' on table '#{table_name}' already exists. Skipping.")
|
||||
return
|
||||
end
|
||||
quoted_column_names = quoted_columns_for_index(column_names, options).join(", ")
|
||||
|
||||
execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} (#{quoted_column_names})"
|
||||
end
|
||||
|
||||
|
@ -283,7 +302,28 @@ module ActiveRecord
|
|||
# Remove the index named by_branch_party in the accounts table.
|
||||
# remove_index :accounts, :name => :by_branch_party
|
||||
def remove_index(table_name, options = {})
|
||||
execute "DROP INDEX #{quote_column_name(index_name(table_name, options))} ON #{table_name}"
|
||||
index_name = index_name(table_name, options)
|
||||
unless index_exists?(table_name, index_name, true)
|
||||
@logger.warn("Index name '#{index_name}' on table '#{table_name}' does not exist. Skipping.")
|
||||
return
|
||||
end
|
||||
remove_index!(table_name, index_name)
|
||||
end
|
||||
|
||||
def remove_index!(table_name, index_name) #:nodoc:
|
||||
execute "DROP INDEX #{quote_column_name(index_name)} ON #{table_name}"
|
||||
end
|
||||
|
||||
# Rename an index.
|
||||
#
|
||||
# Rename the index_people_on_last_name index to index_users_on_last_name
|
||||
# rename_index :people, 'index_people_on_last_name', 'index_users_on_last_name'
|
||||
def rename_index(table_name, old_name, new_name)
|
||||
# this is a naive implementation; some DBs may support this more efficiently (Postgres, for instance)
|
||||
old_index_def = indexes(table_name).detect { |i| i.name == old_name }
|
||||
return unless old_index_def
|
||||
remove_index(table_name, :name => old_name)
|
||||
add_index(table_name, old_index_def.columns, :name => new_name, :unique => old_index_def.unique)
|
||||
end
|
||||
|
||||
def index_name(table_name, options) #:nodoc:
|
||||
|
@ -300,6 +340,15 @@ module ActiveRecord
|
|||
end
|
||||
end
|
||||
|
||||
# Verify the existence of an index.
|
||||
#
|
||||
# The default argument is returned if the underlying implementation does not define the indexes method,
|
||||
# as there's no way to determine the correct answer in that case.
|
||||
def index_exists?(table_name, index_name, default)
|
||||
return default unless respond_to?(:indexes)
|
||||
indexes(table_name).detect { |i| i.name == index_name }
|
||||
end
|
||||
|
||||
# Returns a string of <tt>CREATE TABLE</tt> SQL statement(s) for recreating the
|
||||
# entire structure of the database.
|
||||
def structure_dump
|
||||
|
@ -336,12 +385,12 @@ module ActiveRecord
|
|||
end
|
||||
end
|
||||
|
||||
def assume_migrated_upto_version(version)
|
||||
def assume_migrated_upto_version(version, migrations_path = ActiveRecord::Migrator.migrations_path)
|
||||
version = version.to_i
|
||||
sm_table = quote_table_name(ActiveRecord::Migrator.schema_migrations_table_name)
|
||||
|
||||
migrated = select_values("SELECT version FROM #{sm_table}").map(&:to_i)
|
||||
versions = Dir['db/migrate/[0-9]*_*.rb'].map do |filename|
|
||||
versions = Dir["#{migrations_path}/[0-9]*_*.rb"].map do |filename|
|
||||
filename.split('/').last.split('_').first.to_i
|
||||
end
|
||||
|
||||
|
@ -426,6 +475,11 @@ module ActiveRecord
|
|||
end
|
||||
|
||||
protected
|
||||
# Overridden by the mysql adapter for supporting index lengths
|
||||
def quoted_columns_for_index(column_names, options = {})
|
||||
column_names.map {|name| quote_column_name(name) }
|
||||
end
|
||||
|
||||
def options_include_default?(options)
|
||||
options.include?(:default) && !(options[:null] == false && options[:default].nil?)
|
||||
end
|
||||
|
|
|
@ -11,6 +11,7 @@ require 'active_record/connection_adapters/abstract/quoting'
|
|||
require 'active_record/connection_adapters/abstract/connection_pool'
|
||||
require 'active_record/connection_adapters/abstract/connection_specification'
|
||||
require 'active_record/connection_adapters/abstract/query_cache'
|
||||
require 'active_record/connection_adapters/abstract/database_limits'
|
||||
|
||||
module ActiveRecord
|
||||
module ConnectionAdapters # :nodoc:
|
||||
|
@ -29,6 +30,7 @@ module ActiveRecord
|
|||
# notably, the instance methods provided by SchemaStatement are very useful.
|
||||
class AbstractAdapter
|
||||
include Quoting, DatabaseStatements, SchemaStatements
|
||||
include DatabaseLimits
|
||||
include QueryCache
|
||||
include ActiveSupport::Callbacks
|
||||
define_callbacks :checkout, :checkin
|
||||
|
|
|
@ -454,10 +454,11 @@ module ActiveRecord
|
|||
if current_index != row[2]
|
||||
next if row[2] == "PRIMARY" # skip the primary key
|
||||
current_index = row[2]
|
||||
indexes << IndexDefinition.new(row[0], row[2], row[1] == "0", [])
|
||||
indexes << IndexDefinition.new(row[0], row[2], row[1] == "0", [], [])
|
||||
end
|
||||
|
||||
indexes.last.columns << row[4]
|
||||
indexes.last.lengths << row[7]
|
||||
end
|
||||
result.free
|
||||
indexes
|
||||
|
@ -480,6 +481,13 @@ module ActiveRecord
|
|||
execute "RENAME TABLE #{quote_table_name(table_name)} TO #{quote_table_name(new_name)}"
|
||||
end
|
||||
|
||||
def add_column(table_name, column_name, type, options = {})
|
||||
add_column_sql = "ALTER TABLE #{quote_table_name(table_name)} ADD #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
|
||||
add_column_options!(add_column_sql, options)
|
||||
add_column_position!(add_column_sql, options)
|
||||
execute(add_column_sql)
|
||||
end
|
||||
|
||||
def change_column_default(table_name, column_name, default) #:nodoc:
|
||||
column = column_for(table_name, column_name)
|
||||
change_column table_name, column_name, column.sql_type, :default => default
|
||||
|
@ -508,6 +516,7 @@ module ActiveRecord
|
|||
|
||||
change_column_sql = "ALTER TABLE #{quote_table_name(table_name)} CHANGE #{quote_column_name(column_name)} #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
|
||||
add_column_options!(change_column_sql, options)
|
||||
add_column_position!(change_column_sql, options)
|
||||
execute(change_column_sql)
|
||||
end
|
||||
|
||||
|
@ -539,6 +548,13 @@ module ActiveRecord
|
|||
end
|
||||
end
|
||||
|
||||
def add_column_position!(sql, options)
|
||||
if options[:first]
|
||||
sql << " FIRST"
|
||||
elsif options[:after]
|
||||
sql << " AFTER #{quote_column_name(options[:after])}"
|
||||
end
|
||||
end
|
||||
|
||||
# SHOW VARIABLES LIKE 'name'
|
||||
def show_variable(name)
|
||||
|
@ -571,6 +587,20 @@ module ActiveRecord
|
|||
where_sql
|
||||
end
|
||||
|
||||
protected
|
||||
def quoted_columns_for_index(column_names, options = {})
|
||||
length = options[:length] if options.is_a?(Hash)
|
||||
|
||||
quoted_column_names = case length
|
||||
when Hash
|
||||
column_names.map {|name| length[name] ? "#{quote_column_name(name)}(#{length[name]})" : quote_column_name(name) }
|
||||
when Fixnum
|
||||
column_names.map {|name| "#{quote_column_name(name)}(#{length})"}
|
||||
else
|
||||
column_names.map {|name| quote_column_name(name) }
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def connect
|
||||
encoding = @config[:encoding]
|
||||
|
|
|
@ -261,20 +261,12 @@ module ActiveRecord
|
|||
true
|
||||
end
|
||||
|
||||
# Does PostgreSQL support standard conforming strings?
|
||||
def supports_standard_conforming_strings?
|
||||
# Temporarily set the client message level above error to prevent unintentional
|
||||
# error messages in the logs when working on a PostgreSQL database server that
|
||||
# does not support standard conforming strings.
|
||||
client_min_messages_old = client_min_messages
|
||||
self.client_min_messages = 'panic'
|
||||
|
||||
# postgres-pr does not raise an exception when client_min_messages is set higher
|
||||
# than error and "SHOW standard_conforming_strings" fails, but returns an empty
|
||||
# PGresult instead.
|
||||
has_support = query('SHOW standard_conforming_strings')[0][0] rescue false
|
||||
self.client_min_messages = client_min_messages_old
|
||||
has_support
|
||||
# Enable standard-conforming strings if available.
|
||||
def set_standard_conforming_strings
|
||||
old, self.client_min_messages = client_min_messages, 'panic'
|
||||
execute('SET standard_conforming_strings = on') rescue nil
|
||||
ensure
|
||||
self.client_min_messages = old
|
||||
end
|
||||
|
||||
def supports_insert_with_returning?
|
||||
|
@ -298,7 +290,7 @@ module ActiveRecord
|
|||
# QUOTING ==================================================
|
||||
|
||||
# Escapes binary strings for bytea input to the database.
|
||||
def escape_bytea(value)
|
||||
def escape_bytea(original_value)
|
||||
if @connection.respond_to?(:escape_bytea)
|
||||
self.class.instance_eval do
|
||||
define_method(:escape_bytea) do |value|
|
||||
|
@ -322,62 +314,40 @@ module ActiveRecord
|
|||
end
|
||||
end
|
||||
end
|
||||
escape_bytea(value)
|
||||
escape_bytea(original_value)
|
||||
end
|
||||
|
||||
# Unescapes bytea output from a database to the binary string it represents.
|
||||
# NOTE: This is NOT an inverse of escape_bytea! This is only to be used
|
||||
# on escaped binary output from database drive.
|
||||
def unescape_bytea(value)
|
||||
def unescape_bytea(original_value)
|
||||
# In each case, check if the value actually is escaped PostgreSQL bytea output
|
||||
# or an unescaped Active Record attribute that was just written.
|
||||
if PGconn.respond_to?(:unescape_bytea)
|
||||
if @connection.respond_to?(:unescape_bytea)
|
||||
self.class.instance_eval do
|
||||
define_method(:unescape_bytea) do |value|
|
||||
if value =~ /\\\d{3}/
|
||||
PGconn.unescape_bytea(value)
|
||||
else
|
||||
value
|
||||
end
|
||||
@connection.unescape_bytea(value) if value
|
||||
end
|
||||
end
|
||||
elsif PGconn.respond_to?(:unescape_bytea)
|
||||
self.class.instance_eval do
|
||||
define_method(:unescape_bytea) do |value|
|
||||
PGconn.unescape_bytea(value) if value
|
||||
end
|
||||
end
|
||||
else
|
||||
self.class.instance_eval do
|
||||
define_method(:unescape_bytea) do |value|
|
||||
if value =~ /\\\d{3}/
|
||||
result = ''
|
||||
i, max = 0, value.size
|
||||
while i < max
|
||||
char = value[i]
|
||||
if char == ?\\
|
||||
if value[i+1] == ?\\
|
||||
char = ?\\
|
||||
i += 1
|
||||
else
|
||||
char = value[i+1..i+3].oct
|
||||
i += 3
|
||||
end
|
||||
end
|
||||
result << char
|
||||
i += 1
|
||||
end
|
||||
result
|
||||
else
|
||||
value
|
||||
end
|
||||
end
|
||||
end
|
||||
raise 'Your PostgreSQL connection does not support unescape_bytea. Try upgrading to pg 0.9.0 or later.'
|
||||
end
|
||||
unescape_bytea(value)
|
||||
unescape_bytea(original_value)
|
||||
end
|
||||
|
||||
# Quotes PostgreSQL-specific data types for SQL input.
|
||||
def quote(value, column = nil) #:nodoc:
|
||||
if value.kind_of?(String) && column && column.type == :binary
|
||||
"#{quoted_string_prefix}'#{escape_bytea(value)}'"
|
||||
elsif value.kind_of?(String) && column && column.sql_type =~ /^xml$/
|
||||
"xml E'#{quote_string(value)}'"
|
||||
elsif value.kind_of?(Numeric) && column && column.sql_type =~ /^money$/
|
||||
"'#{escape_bytea(value)}'"
|
||||
elsif value.kind_of?(String) && column && column.sql_type == 'xml'
|
||||
"xml '#{quote_string(value)}'"
|
||||
elsif value.kind_of?(Numeric) && column && column.sql_type == 'money'
|
||||
# Not truly string input, so doesn't require (or allow) escape string syntax.
|
||||
"'#{value.to_s}'"
|
||||
elsif value.kind_of?(String) && column && column.sql_type =~ /^bit/
|
||||
|
@ -393,7 +363,7 @@ module ActiveRecord
|
|||
end
|
||||
|
||||
# Quotes strings for use in SQL input in the postgres driver for better performance.
|
||||
def quote_string(s) #:nodoc:
|
||||
def quote_string(original_value) #:nodoc:
|
||||
if @connection.respond_to?(:escape)
|
||||
self.class.instance_eval do
|
||||
define_method(:quote_string) do |s|
|
||||
|
@ -413,7 +383,7 @@ module ActiveRecord
|
|||
remove_method(:quote_string)
|
||||
end
|
||||
end
|
||||
quote_string(s)
|
||||
quote_string(original_value)
|
||||
end
|
||||
|
||||
# Checks the following cases:
|
||||
|
@ -889,9 +859,12 @@ module ActiveRecord
|
|||
execute "ALTER TABLE #{quote_table_name(table_name)} RENAME COLUMN #{quote_column_name(column_name)} TO #{quote_column_name(new_column_name)}"
|
||||
end
|
||||
|
||||
# Drops an index from a table.
|
||||
def remove_index(table_name, options = {})
|
||||
execute "DROP INDEX #{quote_table_name(index_name(table_name, options))}"
|
||||
def remove_index!(table_name, index_name) #:nodoc:
|
||||
execute "DROP INDEX #{quote_table_name(index_name)}"
|
||||
end
|
||||
|
||||
def index_name_length
|
||||
63
|
||||
end
|
||||
|
||||
# Maps logical Rails types to PostgreSQL-specific data types.
|
||||
|
@ -971,17 +944,6 @@ module ActiveRecord
|
|||
# Ignore async_exec and async_query when using postgres-pr.
|
||||
@async = @config[:allow_concurrency] && @connection.respond_to?(:async_exec)
|
||||
|
||||
# Use escape string syntax if available. We cannot do this lazily when encountering
|
||||
# the first string, because that could then break any transactions in progress.
|
||||
# See: http://www.postgresql.org/docs/current/static/runtime-config-compatible.html
|
||||
# If PostgreSQL doesn't know the standard_conforming_strings parameter then it doesn't
|
||||
# support escape string syntax. Don't override the inherited quoted_string_prefix.
|
||||
if supports_standard_conforming_strings?
|
||||
self.class.instance_eval do
|
||||
define_method(:quoted_string_prefix) { 'E' }
|
||||
end
|
||||
end
|
||||
|
||||
# Money type has a fixed precision of 10 in PostgreSQL 8.2 and below, and as of
|
||||
# PostgreSQL 8.3 it has a fixed precision of 19. PostgreSQLColumn.extract_precision
|
||||
# should know about this but can't detect it there, so deal with it here.
|
||||
|
@ -1011,6 +973,9 @@ module ActiveRecord
|
|||
end
|
||||
self.client_min_messages = @config[:min_messages] if @config[:min_messages]
|
||||
self.schema_search_path = @config[:schema_search_path] || @config[:schema_order]
|
||||
|
||||
# Use standard-conforming strings if available so we don't have to do the E'...' dance.
|
||||
set_standard_conforming_strings
|
||||
end
|
||||
|
||||
# Returns the current ID of a table's sequence.
|
||||
|
|
|
@ -220,20 +220,20 @@ module ActiveRecord
|
|||
SQL
|
||||
|
||||
execute(sql, name).map do |row|
|
||||
row[0]
|
||||
row['name']
|
||||
end
|
||||
end
|
||||
|
||||
def columns(table_name, name = nil) #:nodoc:
|
||||
table_structure(table_name).map do |field|
|
||||
SQLiteColumn.new(field['name'], field['dflt_value'], field['type'], field['notnull'] == "0")
|
||||
SQLiteColumn.new(field['name'], field['dflt_value'], field['type'], field['notnull'].to_i == 0)
|
||||
end
|
||||
end
|
||||
|
||||
def indexes(table_name, name = nil) #:nodoc:
|
||||
execute("PRAGMA index_list(#{quote_table_name(table_name)})", name).map do |row|
|
||||
index = IndexDefinition.new(table_name, row['name'])
|
||||
index.unique = row['unique'] != '0'
|
||||
index.unique = row['unique'].to_i != 0
|
||||
index.columns = execute("PRAGMA index_info('#{index.name}')").map { |col| col['name'] }
|
||||
index
|
||||
end
|
||||
|
@ -244,8 +244,8 @@ module ActiveRecord
|
|||
column ? column['name'] : nil
|
||||
end
|
||||
|
||||
def remove_index(table_name, options={}) #:nodoc:
|
||||
execute "DROP INDEX #{quote_column_name(index_name(table_name, options))}"
|
||||
def remove_index!(table_name, index_name) #:nodoc:
|
||||
execute "DROP INDEX #{quote_column_name(index_name)}"
|
||||
end
|
||||
|
||||
def rename_table(name, new_name)
|
||||
|
|
|
@ -167,13 +167,13 @@ module ActiveRecord
|
|||
|
||||
module ClassMethods
|
||||
def self.extended(base)
|
||||
base.metaclass.alias_method_chain(:alias_attribute, :dirty)
|
||||
base.singleton_class.alias_method_chain(:alias_attribute, :dirty)
|
||||
end
|
||||
|
||||
def alias_attribute_with_dirty(new_name, old_name)
|
||||
alias_attribute_without_dirty(new_name, old_name)
|
||||
DIRTY_SUFFIXES.each do |suffix|
|
||||
module_eval <<-STR, __FILE__, __LINE__+1
|
||||
module_eval <<-STR, __FILE__, __LINE__ + 1
|
||||
def #{new_name}#{suffix}; self.#{old_name}#{suffix}; end # def subject_changed?; self.title_changed?; end
|
||||
STR
|
||||
end
|
||||
|
|
|
@ -891,6 +891,7 @@ module ActiveRecord
|
|||
|
||||
instances.size == 1 ? instances.first : instances
|
||||
end
|
||||
private table_name
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -1,26 +0,0 @@
|
|||
# Deprecates the use of the former message interpolation syntax in activerecord
|
||||
# as in "must have %d characters". The new syntax uses explicit variable names
|
||||
# as in "{{value}} must have {{count}} characters".
|
||||
|
||||
require 'i18n/backend/simple'
|
||||
module I18n
|
||||
module Backend
|
||||
class Simple
|
||||
DEPRECATED_INTERPOLATORS = { '%d' => '{{count}}', '%s' => '{{value}}' }
|
||||
|
||||
protected
|
||||
def interpolate_with_deprecated_syntax(locale, string, values = {})
|
||||
return string unless string.is_a?(String) && !values.empty?
|
||||
|
||||
string = string.gsub(/%d|%s/) do |s|
|
||||
instead = DEPRECATED_INTERPOLATORS[s]
|
||||
ActiveSupport::Deprecation.warn "using #{s} in messages is deprecated; use #{instead} instead."
|
||||
instead
|
||||
end
|
||||
|
||||
interpolate_without_deprecated_syntax(locale, string, values)
|
||||
end
|
||||
alias_method_chain :interpolate, :deprecated_syntax
|
||||
end
|
||||
end
|
||||
end
|
|
@ -23,6 +23,16 @@ module ActiveRecord
|
|||
# p2.first_name = "should fail"
|
||||
# p2.save # Raises a ActiveRecord::StaleObjectError
|
||||
#
|
||||
# Optimistic locking will also check for stale data when objects are destroyed. Example:
|
||||
#
|
||||
# p1 = Person.find(1)
|
||||
# p2 = Person.find(1)
|
||||
#
|
||||
# p1.first_name = "Michael"
|
||||
# p1.save
|
||||
#
|
||||
# p2.destroy # Raises a ActiveRecord::StaleObjectError
|
||||
#
|
||||
# You're then responsible for dealing with the conflict by rescuing the exception and either rolling back, merging,
|
||||
# or otherwise apply the business logic needed to resolve the conflict.
|
||||
#
|
||||
|
@ -39,6 +49,7 @@ module ActiveRecord
|
|||
base.lock_optimistically = true
|
||||
|
||||
base.alias_method_chain :update, :lock
|
||||
base.alias_method_chain :destroy, :lock
|
||||
base.alias_method_chain :attributes_from_column_definition, :lock
|
||||
|
||||
class << base
|
||||
|
@ -86,7 +97,7 @@ module ActiveRecord
|
|||
end_sql
|
||||
|
||||
unless affected_rows == 1
|
||||
raise ActiveRecord::StaleObjectError, "Attempted to update a stale object"
|
||||
raise ActiveRecord::StaleObjectError, "Attempted to update a stale object: #{self.class.name}"
|
||||
end
|
||||
|
||||
affected_rows
|
||||
|
@ -98,6 +109,28 @@ module ActiveRecord
|
|||
end
|
||||
end
|
||||
|
||||
def destroy_with_lock #:nodoc:
|
||||
return destroy_without_lock unless locking_enabled?
|
||||
|
||||
unless new_record?
|
||||
lock_col = self.class.locking_column
|
||||
previous_value = send(lock_col).to_i
|
||||
|
||||
affected_rows = connection.delete(
|
||||
"DELETE FROM #{self.class.quoted_table_name} " +
|
||||
"WHERE #{connection.quote_column_name(self.class.primary_key)} = #{quoted_id} " +
|
||||
"AND #{self.class.quoted_locking_column} = #{quote_value(previous_value)}",
|
||||
"#{self.class.name} Destroy"
|
||||
)
|
||||
|
||||
unless affected_rows == 1
|
||||
raise ActiveRecord::StaleObjectError, "Attempted to delete a stale object: #{self.class.name}"
|
||||
end
|
||||
end
|
||||
|
||||
freeze
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
DEFAULT_LOCKING_COLUMN = 'lock_version'
|
||||
|
||||
|
|
|
@ -381,6 +381,7 @@ module ActiveRecord
|
|||
def migrate(migrations_path, target_version = nil)
|
||||
case
|
||||
when target_version.nil? then up(migrations_path, target_version)
|
||||
when current_version == 0 && target_version == 0 then # noop
|
||||
when current_version > target_version then down(migrations_path, target_version)
|
||||
else up(migrations_path, target_version)
|
||||
end
|
||||
|
@ -408,6 +409,10 @@ module ActiveRecord
|
|||
self.new(direction, migrations_path, target_version).run
|
||||
end
|
||||
|
||||
def migrations_path
|
||||
'db/migrate'
|
||||
end
|
||||
|
||||
def schema_migrations_table_name
|
||||
Base.table_name_prefix + 'schema_migrations' + Base.table_name_suffix
|
||||
end
|
||||
|
|
|
@ -184,6 +184,8 @@ module ActiveRecord
|
|||
# the parent model is saved. This happens inside the transaction initiated
|
||||
# by the parents save method. See ActiveRecord::AutosaveAssociation.
|
||||
module ClassMethods
|
||||
REJECT_ALL_BLANK_PROC = proc { |attributes| attributes.all? { |_, value| value.blank? } }
|
||||
|
||||
# Defines an attributes writer for the specified association(s). If you
|
||||
# are using <tt>attr_protected</tt> or <tt>attr_accessible</tt>, then you
|
||||
# will need to add the attribute writer to the allowed list.
|
||||
|
@ -208,39 +210,40 @@ 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=
|
||||
# accepts_nested_attributes_for :avatar, :reject_if => proc { |attributes| attributes['name'].blank? }
|
||||
# # creates avatar_attributes=
|
||||
# accepts_nested_attributes_for :avatar, :reject_if => :all_blank
|
||||
# # 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)
|
||||
options[:reject_if] = REJECT_ALL_BLANK_PROC if options[:reject_if] == :all_blank
|
||||
|
||||
attr_names.each do |association_name|
|
||||
if reflection = reflect_on_association(association_name)
|
||||
type = case reflection.macro
|
||||
when :has_one, :belongs_to
|
||||
:one_to_one
|
||||
when :has_many, :has_and_belongs_to_many
|
||||
:collection
|
||||
end
|
||||
|
||||
reflection.options[:autosave] = true
|
||||
self.nested_attributes_options[association_name.to_sym] = options
|
||||
add_autosave_association_callbacks(reflection)
|
||||
nested_attributes_options[association_name.to_sym] = options
|
||||
type = (reflection.collection? ? :collection : :one_to_one)
|
||||
|
||||
# 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 %{
|
||||
class_eval <<-EOS, __FILE__, __LINE__ + 1
|
||||
def #{association_name}_attributes=(attributes)
|
||||
assign_nested_attributes_for_#{type}_association(:#{association_name}, attributes)
|
||||
end
|
||||
}, __FILE__, __LINE__
|
||||
|
||||
add_autosave_association_callbacks(reflection)
|
||||
EOS
|
||||
else
|
||||
raise ArgumentError, "No association found for name `#{association_name}'. Has it been defined yet?"
|
||||
end
|
||||
|
@ -257,46 +260,41 @@ module ActiveRecord
|
|||
marked_for_destruction?
|
||||
end
|
||||
|
||||
# Deal with deprecated _delete.
|
||||
#
|
||||
def _delete #:nodoc:
|
||||
ActiveSupport::Deprecation.warn "_delete is deprecated in nested attributes. Use _destroy instead."
|
||||
_destroy
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Attribute hash keys that should not be assigned as normal attributes.
|
||||
# These hash keys are nested attributes implementation details.
|
||||
#
|
||||
# TODO Remove _delete from UNASSIGNABLE_KEYS when deprecation warning are
|
||||
# removed.
|
||||
UNASSIGNABLE_KEYS = %w( id _destroy _delete )
|
||||
UNASSIGNABLE_KEYS = %w( id _destroy )
|
||||
|
||||
# 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]
|
||||
options = 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 attributes['id']
|
||||
raise_nested_attributes_record_not_found(association_name, attributes['id'])
|
||||
|
||||
elsif !reject_new_record?(association_name, attributes)
|
||||
method = "build_#{association_name}"
|
||||
if respond_to?(method)
|
||||
send(method, attributes.except(*UNASSIGNABLE_KEYS))
|
||||
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
|
||||
|
||||
|
@ -328,7 +326,7 @@ module ActiveRecord
|
|||
# { :id => '2', :_destroy => true }
|
||||
# ])
|
||||
def assign_nested_attributes_for_collection_association(association_name, attributes_collection)
|
||||
options = self.nested_attributes_options[association_name]
|
||||
options = nested_attributes_options[association_name]
|
||||
|
||||
unless attributes_collection.is_a?(Hash) || attributes_collection.is_a?(Array)
|
||||
raise ArgumentError, "Hash or Array expected, got #{attributes_collection.class.name} (#{attributes_collection.inspect})"
|
||||
|
@ -342,15 +340,27 @@ module ActiveRecord
|
|||
attributes_collection = attributes_collection.sort_by { |index, _| index.to_i }.map { |_, attributes| attributes }
|
||||
end
|
||||
|
||||
association = send(association_name)
|
||||
|
||||
existing_records = if association.loaded?
|
||||
association.to_a
|
||||
else
|
||||
attribute_ids = attributes_collection.map {|a| a['id'] || a[:id] }.compact
|
||||
attribute_ids.present? ? association.all(:conditions => {association.primary_key => attribute_ids}) : []
|
||||
end
|
||||
|
||||
attributes_collection.each do |attributes|
|
||||
attributes = attributes.with_indifferent_access
|
||||
|
||||
if attributes['id'].blank?
|
||||
unless reject_new_record?(association_name, attributes)
|
||||
send(association_name).build(attributes.except(*UNASSIGNABLE_KEYS))
|
||||
association.build(attributes.except(*UNASSIGNABLE_KEYS))
|
||||
end
|
||||
elsif existing_record = send(association_name).detect { |record| record.id.to_s == attributes['id'].to_s }
|
||||
elsif existing_record = existing_records.detect { |record| record.id.to_s == attributes['id'].to_s }
|
||||
association.send(:add_record_to_target_with_callbacks, existing_record) unless association.loaded?
|
||||
assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy])
|
||||
else
|
||||
raise_nested_attributes_record_not_found(association_name, attributes['id'])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -367,8 +377,7 @@ module ActiveRecord
|
|||
|
||||
# Determines if a hash contains a truthy _destroy key.
|
||||
def has_destroy_flag?(hash)
|
||||
ConnectionAdapters::Column.value_to_boolean(hash['_destroy']) ||
|
||||
ConnectionAdapters::Column.value_to_boolean(hash['_delete']) # TODO Remove after deprecation.
|
||||
ConnectionAdapters::Column.value_to_boolean(hash['_destroy'])
|
||||
end
|
||||
|
||||
# Determines if a new record should be build by checking for
|
||||
|
@ -379,14 +388,17 @@ module ActiveRecord
|
|||
end
|
||||
|
||||
def call_reject_if(association_name, attributes)
|
||||
callback = self.nested_attributes_options[association_name][:reject_if]
|
||||
|
||||
case callback
|
||||
case callback = nested_attributes_options[association_name][:reject_if]
|
||||
when Symbol
|
||||
method(callback).arity == 0 ? send(callback) : send(callback, attributes)
|
||||
when Proc
|
||||
callback.try(:call, attributes)
|
||||
callback.call(attributes)
|
||||
end
|
||||
end
|
||||
|
||||
def raise_nested_attributes_record_not_found(association_name, record_id)
|
||||
reflection = self.class.reflect_on_association(association_name)
|
||||
raise RecordNotFound, "Couldn't find #{reflection.klass.name} with ID=#{record_id} for #{self.class.name} with ID=#{id}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -212,6 +212,15 @@ module ActiveRecord
|
|||
end
|
||||
|
||||
def check_validity!
|
||||
check_validity_of_inverse!
|
||||
end
|
||||
|
||||
def check_validity_of_inverse!
|
||||
unless options[:polymorphic]
|
||||
if has_inverse? && inverse_of.nil?
|
||||
raise InverseOfAssociationNotFoundError.new(self)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def through_reflection
|
||||
|
@ -225,10 +234,64 @@ module ActiveRecord
|
|||
nil
|
||||
end
|
||||
|
||||
def has_inverse?
|
||||
!@options[:inverse_of].nil?
|
||||
end
|
||||
|
||||
def inverse_of
|
||||
if has_inverse?
|
||||
@inverse_of ||= klass.reflect_on_association(options[:inverse_of])
|
||||
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
|
||||
|
||||
# Returns whether or not this association reflection is for a collection
|
||||
# association. Returns +true+ if the +macro+ is one of +has_many+ or
|
||||
# +has_and_belongs_to_many+, +false+ otherwise.
|
||||
def collection?
|
||||
if @collection.nil?
|
||||
@collection = [:has_many, :has_and_belongs_to_many].include?(macro)
|
||||
end
|
||||
@collection
|
||||
end
|
||||
|
||||
# Returns whether or not the association should be validated as part of
|
||||
# the parent's validation.
|
||||
#
|
||||
# Unless you explicitely disable validation with
|
||||
# <tt>:validate => false</tt>, it will take place when:
|
||||
#
|
||||
# * you explicitely enable validation; <tt>:validate => true</tt>
|
||||
# * you use autosave; <tt>:autosave => true</tt>
|
||||
# * the association is a +has_many+ association
|
||||
def validate?
|
||||
!options[:validate].nil? ? options[:validate] : (options[:autosave] == true || macro == :has_many)
|
||||
end
|
||||
|
||||
def dependent_conditions(record, base_class, extra_conditions)
|
||||
dependent_conditions = []
|
||||
dependent_conditions << "#{primary_key_name} = #{record.send(name).send(:owner_quoted_id)}"
|
||||
dependent_conditions << "#{options[:as]}_type = '#{base_class.name}'" if options[:as]
|
||||
dependent_conditions << klass.send(:sanitize_sql, options[:conditions]) if options[:conditions]
|
||||
dependent_conditions = dependent_conditions.collect {|where| "(#{where})" }.join(" AND ")
|
||||
dependent_conditions << extra_conditions if extra_conditions
|
||||
dependent_conditions = dependent_conditions.gsub('@', '\@')
|
||||
dependent_conditions
|
||||
end
|
||||
|
||||
private
|
||||
def derive_class_name
|
||||
class_name = name.to_s.camelize
|
||||
class_name = class_name.singularize if [ :has_many, :has_and_belongs_to_many ].include?(macro)
|
||||
class_name = class_name.singularize if collection?
|
||||
class_name
|
||||
end
|
||||
|
||||
|
@ -300,6 +363,8 @@ module ActiveRecord
|
|||
unless [:belongs_to, :has_many, :has_one].include?(source_reflection.macro) && source_reflection.options[:through].nil?
|
||||
raise HasManyThroughSourceAssociationMacroError.new(self)
|
||||
end
|
||||
|
||||
check_validity_of_inverse!
|
||||
end
|
||||
|
||||
def through_reflection_primary_key
|
||||
|
|
|
@ -28,6 +28,10 @@ module ActiveRecord
|
|||
class Schema < Migration
|
||||
private_class_method :new
|
||||
|
||||
def self.migrations_path
|
||||
ActiveRecord::Migrator.migrations_path
|
||||
end
|
||||
|
||||
# Eval the given block. All methods available to the current connection
|
||||
# adapter are available within the block, so you can easily use the
|
||||
# database definition DSL to build up your schema (+create_table+,
|
||||
|
@ -44,7 +48,7 @@ module ActiveRecord
|
|||
|
||||
unless info[:version].blank?
|
||||
initialize_schema_migrations_table
|
||||
assume_migrated_upto_version info[:version]
|
||||
assume_migrated_upto_version(info[:version], migrations_path)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -171,6 +171,9 @@ HEADER
|
|||
statment_parts << (':name => ' + index.name.inspect)
|
||||
statment_parts << ':unique => true' if index.unique
|
||||
|
||||
index_lengths = index.lengths.compact if index.lengths.is_a?(Array)
|
||||
statment_parts << (':length => ' + Hash[*index.columns.zip(index.lengths).flatten].inspect) if index_lengths.present?
|
||||
|
||||
' ' + statment_parts.join(', ')
|
||||
end
|
||||
|
||||
|
|
|
@ -79,7 +79,7 @@ module ActiveRecord #:nodoc:
|
|||
|
||||
def as_json(options = nil) #:nodoc:
|
||||
hash = Serializer.new(self, options).serializable_record
|
||||
hash = { self.class.model_name.element => hash } if include_root_in_json
|
||||
hash = { options[:root] || self.class.model_name.element => hash } if include_root_in_json
|
||||
hash
|
||||
end
|
||||
|
||||
|
|
|
@ -158,7 +158,7 @@ module ActiveRecord
|
|||
# If +message+ is a Symbol, it will be translated, using the appropriate scope (see translate_error).
|
||||
#
|
||||
def add(attribute, message = nil, options = {})
|
||||
options[:message] = options.delete(:default) if options.has_key?(:default)
|
||||
options[:message] = options.delete(:default) if options[:default].is_a?(Symbol)
|
||||
error, message = message, nil if message.is_a?(Error)
|
||||
|
||||
@errors[attribute.to_s] ||= []
|
||||
|
@ -239,6 +239,18 @@ module ActiveRecord
|
|||
@errors.each_key { |attr| @errors[attr].each { |error| yield attr, error.message } }
|
||||
end
|
||||
|
||||
# Yields each attribute and associated error per error added.
|
||||
#
|
||||
# class Company < ActiveRecord::Base
|
||||
# validates_presence_of :name, :address, :email
|
||||
# validates_length_of :name, :in => 5..30
|
||||
# end
|
||||
#
|
||||
# company = Company.create(:address => '123 First St.')
|
||||
# company.errors.each_error{|attr,err| puts "#{attr} - #{err.type}" }
|
||||
# # => name - :too_short
|
||||
# # name - :blank
|
||||
# # address - :blank
|
||||
def each_error
|
||||
@errors.each_key { |attr| @errors[attr].each { |error| yield attr, error } }
|
||||
end
|
||||
|
|
|
@ -2,7 +2,7 @@ module ActiveRecord
|
|||
module VERSION #:nodoc:
|
||||
MAJOR = 2
|
||||
MINOR = 3
|
||||
TINY = 5
|
||||
TINY = 8
|
||||
|
||||
STRING = [MAJOR, MINOR, TINY].join('.')
|
||||
end
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue