Update to Rails 2.3.8

This commit is contained in:
Jacques Distler 2010-05-25 12:45:45 -05:00
parent 6677b46cb4
commit f0635301aa
429 changed files with 17683 additions and 4047 deletions

View file

@ -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'

View file

@ -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

View file

@ -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
]

View file

@ -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) }

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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)

View file

@ -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"

View file

@ -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

View file

@ -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)

View 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

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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]

View file

@ -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.

View file

@ -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)

View file

@ -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

View file

@ -891,6 +891,7 @@ module ActiveRecord
instances.size == 1 ? instances.first : instances
end
private table_name
end
end

View file

@ -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

View file

@ -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'

View file

@ -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

View file

@ -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
# records 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 records 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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -2,7 +2,7 @@ module ActiveRecord
module VERSION #:nodoc:
MAJOR = 2
MINOR = 3
TINY = 5
TINY = 8
STRING = [MAJOR, MINOR, TINY].join('.')
end