Rails 2.1.1

Among other things, a security fix.
This commit is contained in:
Jacques Distler 2008-09-07 00:54:05 -05:00
parent d2c4c8737c
commit d4f97345db
354 changed files with 21027 additions and 3072 deletions

View file

@ -24,16 +24,14 @@
$:.unshift(File.dirname(__FILE__)) unless
$:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
unless defined? ActiveSupport
active_support_path = File.dirname(__FILE__) + "/../../activesupport/lib"
if File.exist?(active_support_path)
$:.unshift active_support_path
require 'active_support'
else
require 'rubygems'
gem 'activesupport'
require 'active_support'
end
active_support_path = File.dirname(__FILE__) + "/../../activesupport/lib"
if File.exist?(active_support_path)
$:.unshift active_support_path
require 'active_support'
else
require 'rubygems'
gem 'activesupport'
require 'active_support'
end
require 'active_record/base'

View file

@ -51,9 +51,7 @@ module ActiveRecord
def add_preloaded_record_to_collection(parent_records, reflection_name, associated_record)
parent_records.each do |parent_record|
association_proxy = parent_record.send(reflection_name)
association_proxy.loaded
association_proxy.target = associated_record
parent_record.send("set_#{reflection_name}_target", associated_record)
end
end
@ -103,17 +101,17 @@ module ActiveRecord
associated_records = reflection.klass.find(:all, :conditions => [conditions, ids],
:include => options[:include],
:joins => "INNER JOIN #{connection.quote_table_name options[:join_table]} as t0 ON #{reflection.klass.quoted_table_name}.#{reflection.klass.primary_key} = t0.#{reflection.association_foreign_key}",
:select => "#{options[:select] || table_name+'.*'}, t0.#{reflection.primary_key_name} as _parent_record_id",
:select => "#{options[:select] || table_name+'.*'}, t0.#{reflection.primary_key_name} as the_parent_record_id",
:order => options[:order])
set_association_collection_records(id_to_record_map, reflection.name, associated_records, '_parent_record_id')
set_association_collection_records(id_to_record_map, reflection.name, associated_records, 'the_parent_record_id')
end
def preload_has_one_association(records, reflection, preload_options={})
id_to_record_map, ids = construct_id_map(records)
options = reflection.options
records.each {|record| record.send("set_#{reflection.name}_target", nil)}
if options[:through]
records.each {|record| record.send(reflection.name) && record.send(reflection.name).loaded}
through_records = preload_through_records(records, reflection, options[:through])
through_reflection = reflections[options[:through]]
through_primary_key = through_reflection.primary_key_name
@ -126,8 +124,6 @@ module ActiveRecord
end
end
else
records.each {|record| record.send("set_#{reflection.name}_target", nil)}
set_association_single_records(id_to_record_map, reflection.name, find_associated_records(ids, reflection, preload_options), reflection.primary_key_name)
end
end
@ -188,7 +184,6 @@ module ActiveRecord
through_records
end
# FIXME: quoting
def preload_belongs_to_association(records, reflection, preload_options={})
options = reflection.options
primary_key_name = reflection.primary_key_name
@ -227,9 +222,19 @@ module ActiveRecord
table_name = klass.quoted_table_name
primary_key = klass.primary_key
conditions = "#{table_name}.#{primary_key} IN (?)"
conditions = "#{table_name}.#{connection.quote_column_name(primary_key)} IN (?)"
conditions << append_conditions(options, preload_options)
associated_records = klass.find(:all, :conditions => [conditions, id_map.keys.uniq],
column_type = klass.columns.detect{|c| c.name == primary_key}.type
ids = id_map.keys.uniq.map do |id|
if column_type == :integer
id.to_i
elsif column_type == :float
id.to_f
else
id
end
end
associated_records = klass.find(:all, :conditions => [conditions, ids],
:include => options[:include],
:select => options[:select],
:joins => options[:joins],
@ -243,7 +248,7 @@ module ActiveRecord
table_name = reflection.klass.quoted_table_name
if interface = reflection.options[:as]
conditions = "#{reflection.klass.quoted_table_name}.#{connection.quote_column_name "#{interface}_id"} IN (?) and #{reflection.klass.quoted_table_name}.#{connection.quote_column_name "#{interface}_type"} = '#{self.base_class.name.demodulize}'"
conditions = "#{reflection.klass.quoted_table_name}.#{connection.quote_column_name "#{interface}_id"} IN (?) and #{reflection.klass.quoted_table_name}.#{connection.quote_column_name "#{interface}_type"} = '#{self.base_class.sti_name}'"
else
foreign_key = reflection.primary_key_name
conditions = "#{reflection.klass.quoted_table_name}.#{foreign_key} IN (?)"

View file

@ -690,6 +690,7 @@ module ActiveRecord
# association is a polymorphic +belongs_to+.
# * <tt>:uniq</tt> - If true, duplicates will be omitted from the collection. Useful in conjunction with <tt>:through</tt>.
# * <tt>:readonly</tt> - If true, all the associated objects are readonly through the association.
# * <tt>:validate</tt> - If false, don't validate the associated objects when saving the parent object. true by default.
#
# Option examples:
# has_many :comments, :order => "posted_on"
@ -710,6 +711,7 @@ module ActiveRecord
configure_dependency_for_has_many(reflection)
add_multiple_associated_validation_callbacks(reflection.name) unless options[:validate] == false
add_multiple_associated_save_callbacks(reflection.name)
add_association_callbacks(reflection.name, reflection.options)
@ -769,6 +771,7 @@ module ActiveRecord
# * <tt>:source_type</tt> - Specifies type of the source association used by <tt>has_one :through</tt> queries where the source
# association is a polymorphic +belongs_to+.
# * <tt>:readonly</tt> - If true, the associated object is readonly through the association.
# * <tt>:validate</tt> - If false, don't validate the associated object when saving the parent object. +false+ by default.
#
# Option examples:
# has_one :credit_card, :dependent => :destroy # destroys the associated credit card
@ -799,7 +802,7 @@ module ActiveRecord
end
after_save method_name
add_single_associated_save_callbacks(reflection.name)
add_single_associated_validation_callbacks(reflection.name) if options[:validate] == true
association_accessor_methods(reflection, HasOneAssociation)
association_constructor_method(:build, reflection, HasOneAssociation)
association_constructor_method(:create, reflection, HasOneAssociation)
@ -857,6 +860,7 @@ module ActiveRecord
# Note: If you've enabled the counter cache, then you may want to add the counter cache attribute
# to the +attr_readonly+ list in the associated classes (e.g. <tt>class Post; attr_readonly :comments_count; end</tt>).
# * <tt>:readonly</tt> - If true, the associated object is readonly through the association.
# * <tt>:validate</tt> - If false, don't validate the associated objects when saving the parent object. +false+ by default.
#
# Option examples:
# belongs_to :firm, :foreign_key => "client_of"
@ -937,6 +941,8 @@ module ActiveRecord
)
end
add_single_associated_validation_callbacks(reflection.name) if options[:validate] == true
configure_dependency_for_belongs_to(reflection)
end
@ -1025,6 +1031,7 @@ module ActiveRecord
# * <tt>:select</tt> - By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed if, for example, you want to do a join
# but not include the joined columns. Do not forget to include the primary and foreign keys, otherwise it will raise an error.
# * <tt>:readonly</tt> - If true, all the associated objects are readonly through the association.
# * <tt>:validate</tt> - If false, don't validate the associated objects when saving the parent object. +true+ by default.
#
# Option examples:
# has_and_belongs_to_many :projects
@ -1037,6 +1044,7 @@ module ActiveRecord
def has_and_belongs_to_many(association_id, options = {}, &extension)
reflection = create_has_and_belongs_to_many_reflection(association_id, options, &extension)
add_multiple_associated_validation_callbacks(reflection.name) unless options[:validate] == false
add_multiple_associated_save_callbacks(reflection.name)
collection_accessor_methods(reflection, HasAndBelongsToManyAssociation)
@ -1103,10 +1111,9 @@ module ActiveRecord
association.create_through_record(new_value)
self.send(reflection.name, new_value)
else
association.replace(new_value)
association.replace(new_value)
instance_variable_set(ivar, new_value.nil? ? nil : association)
end
instance_variable_set(ivar, new_value.nil? ? nil : association)
end
define_method("set_#{reflection.name}_target") do |target|
@ -1157,7 +1164,7 @@ module ActiveRecord
end
end
def add_single_associated_save_callbacks(association_name)
def add_single_associated_validation_callbacks(association_name)
method_name = "validate_associated_records_for_#{association_name}".to_sym
define_method(method_name) do
association = instance_variable_get("@#{association_name}")
@ -1169,7 +1176,7 @@ module ActiveRecord
validate method_name
end
def add_multiple_associated_save_callbacks(association_name)
def add_multiple_associated_validation_callbacks(association_name)
method_name = "validate_associated_records_for_#{association_name}".to_sym
ivar = "@#{association_name}"
@ -1190,6 +1197,10 @@ module ActiveRecord
end
validate method_name
end
def add_multiple_associated_save_callbacks(association_name)
ivar = "@#{association_name}"
method_name = "before_save_associated_records_for_#{association_name}".to_sym
define_method(method_name) do
@ -1211,7 +1222,6 @@ module ActiveRecord
else
[]
end
records_to_save.each { |record| association.send(:insert_record, record) } unless records_to_save.blank?
# reconstruct the SQL queries now that we know the owner's id
@ -1343,7 +1353,8 @@ module ActiveRecord
:uniq,
:finder_sql, :counter_sql,
:before_add, :after_add, :before_remove, :after_remove,
:extend, :readonly
:extend, :readonly,
:validate
)
options[:extend] = create_extension_modules(association_id, extension, options[:extend])
@ -1353,7 +1364,7 @@ module ActiveRecord
def create_has_one_reflection(association_id, options)
options.assert_valid_keys(
:class_name, :foreign_key, :remote, :select, :conditions, :order, :include, :dependent, :counter_cache, :extend, :as, :readonly
:class_name, :foreign_key, :remote, :select, :conditions, :order, :include, :dependent, :counter_cache, :extend, :as, :readonly, :validate
)
create_reflection(:has_one, association_id, options, self)
@ -1361,7 +1372,7 @@ module ActiveRecord
def create_has_one_through_reflection(association_id, options)
options.assert_valid_keys(
:class_name, :foreign_key, :remote, :select, :conditions, :order, :include, :dependent, :counter_cache, :extend, :as, :through, :source, :source_type
:class_name, :foreign_key, :remote, :select, :conditions, :order, :include, :dependent, :counter_cache, :extend, :as, :through, :source, :source_type, :validate
)
create_reflection(:has_one, association_id, options, self)
end
@ -1369,7 +1380,7 @@ module ActiveRecord
def create_belongs_to_reflection(association_id, options)
options.assert_valid_keys(
:class_name, :foreign_key, :foreign_type, :remote, :select, :conditions, :include, :dependent,
:counter_cache, :extend, :polymorphic, :readonly
:counter_cache, :extend, :polymorphic, :readonly, :validate
)
reflection = create_reflection(:belongs_to, association_id, options, self)
@ -1388,7 +1399,8 @@ module ActiveRecord
:uniq,
:finder_sql, :delete_sql, :insert_sql,
:before_add, :after_add, :before_remove, :after_remove,
:extend, :readonly
:extend, :readonly,
:validate
)
options[:extend] = create_extension_modules(association_id, extension, options[:extend])
@ -1465,10 +1477,15 @@ module ActiveRecord
join_dependency.joins_for_table_name(table)
}.flatten.compact.uniq
order = options[:order]
if scoped_order = (scope && scope[:order])
order = order ? "#{order}, #{scoped_order}" : scoped_order
end
is_distinct = !options[:joins].blank? || include_eager_conditions?(options, tables_from_conditions) || include_eager_order?(options, tables_from_order)
sql = "SELECT "
if is_distinct
sql << connection.distinct("#{connection.quote_table_name table_name}.#{primary_key}", options[:order])
sql << connection.distinct("#{connection.quote_table_name table_name}.#{primary_key}", order)
else
sql << primary_key
end
@ -1482,8 +1499,8 @@ module ActiveRecord
add_conditions!(sql, options[:conditions], scope)
add_group!(sql, options[:group], scope)
if options[:order] && is_distinct
connection.add_order_by_for_association_limiting!(sql, options)
if order && is_distinct
connection.add_order_by_for_association_limiting!(sql, :order => order)
else
add_order!(sql, options[:order], scope)
end
@ -1502,19 +1519,19 @@ module ActiveRecord
else all << cond
end
end
conditions.join(' ').scan(/([\.\w]+).?\./).flatten
conditions.join(' ').scan(/([\.a-zA-Z_]+).?\./).flatten
end
def order_tables(options)
order = options[:order]
order = [options[:order], scope(:find, :order) ].join(", ")
return [] unless order && order.is_a?(String)
order.scan(/([\.\w]+).?\./).flatten
order.scan(/([\.a-zA-Z_]+).?\./).flatten
end
def selects_tables(options)
select = options[:select]
return [] unless select && select.is_a?(String)
select.scan(/"?([\.\w]+)"?.?\./).flatten
select.scan(/"?([\.a-zA-Z_]+)"?.?\./).flatten
end
# Checks if the conditions reference a table other than the current model table
@ -1638,7 +1655,9 @@ module ActiveRecord
end
def join_for_table_name(table_name)
@joins.select{|j|j.aliased_table_name == table_name.gsub(/^\"(.*)\"$/){$1} }.first rescue nil
join = (@joins.select{|j|j.aliased_table_name == table_name.gsub(/^\"(.*)\"$/){$1} }.first) rescue nil
return join unless join.nil?
@joins.select{|j|j.is_a?(JoinAssociation) && j.aliased_join_table_name == table_name.gsub(/^\"(.*)\"$/){$1} }.first rescue nil
end
def joins_for_table_name(table_name)
@ -1714,6 +1733,7 @@ module ActiveRecord
collection.target.push(association)
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)
when :belongs_to
@ -1795,7 +1815,7 @@ module ActiveRecord
@aliased_join_table_name = aliased_table_name_for(reflection.options[:join_table], "_join")
end
if reflection.macro == :has_many && reflection.options[:through]
if [:has_many, :has_one].include?(reflection.macro) && reflection.options[:through]
@aliased_join_table_name = aliased_table_name_for(reflection.through_reflection.klass.table_name, "_join")
end
end
@ -1819,7 +1839,7 @@ module ActiveRecord
]
when :has_many, :has_one
case
when reflection.macro == :has_many && reflection.options[:through]
when reflection.options[:through]
through_conditions = through_reflection.options[:conditions] ? "AND #{interpolate_sql(sanitize_sql(through_reflection.options[:conditions]))}" : ''
jt_foreign_key = jt_as_extra = jt_source_extra = jt_sti_extra = nil
@ -1855,7 +1875,7 @@ module ActiveRecord
jt_sti_extra = " AND %s.%s = %s" % [
connection.quote_table_name(aliased_join_table_name),
connection.quote_column_name(through_reflection.active_record.inheritance_column),
through_reflection.klass.quote_value(through_reflection.klass.name.demodulize)]
through_reflection.klass.quote_value(through_reflection.klass.sti_name)]
end
when :belongs_to
first_key = primary_key
@ -1920,10 +1940,8 @@ module ActiveRecord
else
""
end || ''
join << %(AND %s.%s = %s ) % [
connection.quote_table_name(aliased_table_name),
connection.quote_column_name(klass.inheritance_column),
klass.quote_value(klass.name.demodulize)] unless klass.descends_from_active_record?
join << %(AND %s) % [
klass.send(:type_condition, aliased_table_name)] unless klass.descends_from_active_record?
[through_reflection, reflection].each do |ref|
join << "AND #{interpolate_sql(sanitize_sql(ref.options[:conditions]))} " if ref && ref.options[:conditions]

View file

@ -78,11 +78,14 @@ module ActiveRecord
@loaded = false
end
def build(attributes = {})
def build(attributes = {}, &block)
if attributes.is_a?(Array)
attributes.collect { |attr| build(attr) }
attributes.collect { |attr| build(attr, &block) }
else
build_record(attributes) { |record| set_belongs_to_association_for(record) }
build_record(attributes) do |record|
block.call(record) if block_given?
set_belongs_to_association_for(record)
end
end
end
@ -187,7 +190,7 @@ module ActiveRecord
if @owner.new_record? || (loaded? && !@reflection.options[:uniq])
@target.size
elsif !loaded? && !@reflection.options[:uniq] && @target.is_a?(Array)
unsaved_records = Array(@target.detect { |r| r.new_record? })
unsaved_records = @target.select { |r| r.new_record? }
unsaved_records.size + count_records
else
count_records
@ -335,7 +338,7 @@ module ActiveRecord
callback(:before_add, record)
yield(record) if block_given?
@target ||= [] unless loaded?
@target << record
@target << record unless @reflection.options[:uniq] && @target.include?(record)
callback(:after_add, record)
record
end

View file

@ -69,8 +69,8 @@ module ActiveRecord
@target
end
def respond_to?(symbol, include_priv = false)
proxy_respond_to?(symbol, include_priv) || (load_target && @target.respond_to?(symbol, include_priv))
def respond_to?(*args)
proxy_respond_to?(*args) || (load_target && @target.respond_to?(*args))
end
# Explicitly proxy === because the instance method removal above
@ -131,10 +131,6 @@ module ActiveRecord
records.map { |record| record.quoted_id }.join(',')
end
def interpolate_sql_options!(options, *keys)
keys.each { |key| options[key] &&= interpolate_sql(options[key]) }
end
def interpolate_sql(sql, record = nil)
@owner.send(:interpolate_sql, sql, record)
end

View file

@ -70,10 +70,8 @@ module ActiveRecord
end
def construct_sql
interpolate_sql_options!(@reflection.options, :finder_sql)
if @reflection.options[:finder_sql]
@finder_sql = @reflection.options[:finder_sql]
@finder_sql = interpolate_sql(@reflection.options[:finder_sql])
else
@finder_sql = "#{@owner.connection.quote_table_name @reflection.options[:join_table]}.#{@reflection.primary_key_name} = #{@owner.quoted_id} "
@finder_sql << " AND (#{conditions})" if conditions
@ -87,6 +85,7 @@ module ActiveRecord
:joins => @join_sql,
:readonly => false,
:order => @reflection.options[:order],
:include => @reflection.options[:include],
:limit => @reflection.options[:limit] } }
end

View file

@ -14,7 +14,16 @@ module ActiveRecord
@finder_sql + " AND (#{sanitize_sql(options[:conditions])})"
options[:include] ||= @reflection.options[:include]
@reflection.klass.count(column_name, options)
value = @reflection.klass.count(column_name, options)
limit = @reflection.options[:limit]
offset = @reflection.options[:offset]
if limit || offset
[ [value - offset.to_i, 0].max, limit.to_i ].min
else
value
end
end
end
@ -27,8 +36,11 @@ module ActiveRecord
else
@reflection.klass.count(:conditions => @counter_sql, :include => @reflection.options[:include])
end
@target = [] and loaded if count == 0
# If there's nothing in the database and @target has no new records
# we are certain the current target is an empty array. This is a
# documented side-effect of the method that may avoid an extra SELECT.
@target ||= [] and loaded if count == 0
if @reflection.options[:limit]
count = [ @reflection.options[:limit], count ].min
@ -100,7 +112,7 @@ module ActiveRecord
create_scoping = {}
set_belongs_to_association_for(create_scoping)
{
:find => { :conditions => @finder_sql, :readonly => false, :order => @reflection.options[:order], :limit => @reflection.options[:limit] },
:find => { :conditions => @finder_sql, :readonly => false, :order => @reflection.options[:order], :limit => @reflection.options[:limit], :include => @reflection.options[:include]},
:create => create_scoping
}
end

View file

@ -237,7 +237,7 @@ module ActiveRecord
end
def build_sti_condition
"#{@reflection.through_reflection.quoted_table_name}.#{@reflection.through_reflection.klass.inheritance_column} = #{@reflection.klass.quote_value(@reflection.through_reflection.klass.sti_name)}"
@reflection.through_reflection.klass.send(:type_condition)
end
alias_method :sql_conditions, :conditions

View file

@ -21,8 +21,8 @@ module ActiveRecord
def replace(obj, dont_save = false)
load_target
unless @target.nil?
if dependent? && !dont_save && @target != obj
unless @target.nil? || @target == obj
if dependent? && !dont_save
@target.destroy unless @target.new_record?
@owner.clear_association_cache
else

View file

@ -22,6 +22,10 @@ module ActiveRecord
def find_target
super.first
end
def reset_target!
@target = nil
end
end
end

View file

@ -372,7 +372,7 @@ module ActiveRecord #:nodoc:
def self.reset_subclasses #:nodoc:
nonreloadables = []
subclasses.each do |klass|
unless Dependencies.autoloaded? klass
unless ActiveSupport::Dependencies.autoloaded? klass
nonreloadables << klass
next
end
@ -439,6 +439,10 @@ module ActiveRecord #:nodoc:
cattr_accessor :schema_format , :instance_writer => false
@@schema_format = :ruby
# Specify whether or not to use timestamps for migration numbers
cattr_accessor :timestamped_migrations , :instance_writer => false
@@timestamped_migrations = true
# Determine whether to store the full constant name including namespace when using STI
superclass_delegating_accessor :store_full_sti_class
self.store_full_sti_class = false
@ -828,7 +832,7 @@ module ActiveRecord #:nodoc:
def update_counters(id, counters)
updates = counters.inject([]) { |list, (counter_name, increment)|
sign = increment < 0 ? "-" : "+"
list << "#{connection.quote_column_name(counter_name)} = #{connection.quote_column_name(counter_name)} #{sign} #{increment.abs}"
list << "#{connection.quote_column_name(counter_name)} = COALESCE(#{connection.quote_column_name(counter_name)}, 0) #{sign} #{increment.abs}"
}.join(", ")
update_all(updates, "#{connection.quote_column_name(primary_key)} = #{quote_value(id)}")
end
@ -1465,7 +1469,7 @@ module ActiveRecord #:nodoc:
def construct_finder_sql(options)
scope = scope(:find)
sql = "SELECT #{options[:select] || (scope && scope[:select]) || (options[:joins] && quoted_table_name + '.*') || '*'} "
sql = "SELECT #{options[:select] || (scope && scope[:select]) || ((options[:joins] || (scope && scope[:joins])) && quoted_table_name + '.*') || '*'} "
sql << "FROM #{(scope && scope[:from]) || options[:from] || quoted_table_name} "
add_joins!(sql, options, scope)
@ -1577,10 +1581,11 @@ module ActiveRecord #:nodoc:
sql << "WHERE #{merged_conditions} " unless merged_conditions.blank?
end
def type_condition
def type_condition(table_alias=nil)
quoted_table_alias = self.connection.quote_table_name(table_alias || table_name)
quoted_inheritance_column = connection.quote_column_name(inheritance_column)
type_condition = subclasses.inject("#{quoted_table_name}.#{quoted_inheritance_column} = '#{sti_name}' ") do |condition, subclass|
condition << "OR #{quoted_table_name}.#{quoted_inheritance_column} = '#{subclass.sti_name}' "
type_condition = subclasses.inject("#{quoted_table_alias}.#{quoted_inheritance_column} = '#{sti_name}' ") do |condition, subclass|
condition << "OR #{quoted_table_alias}.#{quoted_inheritance_column} = '#{subclass.sti_name}' "
end
" (#{type_condition}) "
@ -1717,7 +1722,7 @@ module ActiveRecord #:nodoc:
def attribute_condition(argument)
case argument
when nil then "IS ?"
when Array, ActiveRecord::Associations::AssociationCollection then "IN (?)"
when Array, ActiveRecord::Associations::AssociationCollection, ActiveRecord::NamedScope::Scope then "IN (?)"
when Range then "BETWEEN ? AND ?"
else "= ?"
end
@ -2053,9 +2058,10 @@ module ActiveRecord #:nodoc:
end
def replace_named_bind_variables(statement, bind_vars) #:nodoc:
statement.gsub(/:([a-zA-Z]\w*)/) do
match = $1.to_sym
if bind_vars.include?(match)
statement.gsub(/(:?):([a-zA-Z]\w*)/) do
if $1 == ':' # skip postgresql casts
$& # return the whole match
elsif bind_vars.include?(match = $2.to_sym)
quote_bound_value(bind_vars[match])
else
raise PreparedStatementInvalid, "missing value for :#{match} in #{statement}"
@ -2064,13 +2070,18 @@ module ActiveRecord #:nodoc:
end
def expand_range_bind_variables(bind_vars) #:nodoc:
bind_vars.sum do |var|
expanded = []
bind_vars.each do |var|
if var.is_a?(Range)
[var.first, var.last]
expanded << var.first
expanded << var.last
else
[var]
expanded << var
end
end
expanded
end
def quote_bound_value(value) #:nodoc:
@ -2572,8 +2583,15 @@ module ActiveRecord #:nodoc:
quoted = {}
connection = self.class.connection
attribute_names.each do |name|
if column = column_for_attribute(name)
quoted[name] = connection.quote(read_attribute(name), column) unless !include_primary_key && column.primary
if (column = column_for_attribute(name)) && (include_primary_key || !column.primary)
value = read_attribute(name)
# We need explicit to_yaml because quote() does not properly convert Time/Date fields to YAML.
if value && self.class.serialized_attributes.has_key?(name) && (value.acts_like?(:date) || value.acts_like?(:time))
value = value.to_yaml
end
quoted[name] = connection.quote(value, column)
end
end
include_readonly_attributes ? quoted : remove_readonly_attributes(quoted)

View file

@ -1,6 +1,6 @@
module ActiveRecord
module Calculations #:nodoc:
CALCULATIONS_OPTIONS = [:conditions, :joins, :order, :select, :group, :having, :distinct, :limit, :offset, :include]
CALCULATIONS_OPTIONS = [:conditions, :joins, :order, :select, :group, :having, :distinct, :limit, :offset, :include, :from]
def self.included(base)
base.extend(ClassMethods)
end
@ -27,6 +27,8 @@ module ActiveRecord
# * <tt>:select</tt>: By default, this is * as in SELECT * FROM, but can be changed if you, for example, want to do a join but not
# include the joined columns.
# * <tt>:distinct</tt>: Set this to true to make this a distinct calculation, such as SELECT COUNT(DISTINCT posts.id) ...
# * <tt>:from</tt> - By default, this is the table name of the class, but can be changed to an alternate table name (or even the name
# of a database view).
#
# Examples for counting all:
# Person.count # returns the total count of all people
@ -71,7 +73,7 @@ module ActiveRecord
#
# Person.sum('age')
def sum(column_name, options = {})
calculate(:sum, column_name, options) || 0
calculate(:sum, column_name, options)
end
# This calculates aggregate values in the given column. Methods for count, sum, average, minimum, and maximum have been added as shortcuts.
@ -178,13 +180,23 @@ module ActiveRecord
sql = "SELECT COUNT(*) AS #{aggregate_alias}" if use_workaround
sql << ", #{options[:group_field]} AS #{options[:group_alias]}" if options[:group]
sql << " FROM (SELECT #{distinct}#{column_name}" if use_workaround
sql << " FROM #{connection.quote_table_name(table_name)} "
if options[:from]
sql << " FROM #{options[:from]} "
else
sql << " FROM (SELECT #{distinct}#{column_name}" if use_workaround
sql << " FROM #{connection.quote_table_name(table_name)} "
end
joins = ""
add_joins!(joins, options, scope)
if merged_includes.any?
join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(self, merged_includes, options[:joins])
join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(self, merged_includes, joins)
sql << join_dependency.join_associations.collect{|join| join.association_join }.join
end
add_joins!(sql, options, scope)
sql << joins unless joins.blank?
add_conditions!(sql, options[:conditions], scope)
add_limited_ids_condition!(sql, options, join_dependency) if join_dependency && !using_limitable_reflections?(join_dependency.reflections) && ((scope && scope[:limit]) || options[:limit])
@ -205,7 +217,7 @@ module ActiveRecord
sql << " ORDER BY #{options[:order]} " if options[:order]
add_limit!(sql, options, scope)
sql << ')' if use_workaround
sql << ') AS #{aggregate_alias}_subquery' if use_workaround
sql
end
@ -266,6 +278,7 @@ module ActiveRecord
operation = operation.to_s.downcase
case operation
when 'count' then value.to_i
when 'sum' then value =~ /\./ ? value.to_f : value.to_i
when 'avg' then value && value.to_f
else column ? column.type_cast(value) : value
end

View file

@ -257,7 +257,10 @@ module ActiveRecord
def to_sql
column_sql = "#{base.quote_column_name(name)} #{sql_type}"
add_column_options!(column_sql, :null => null, :default => default) unless type.to_sym == :primary_key
column_options = {}
column_options[:null] = null unless null.nil?
column_options[:default] = default unless default.nil?
add_column_options!(column_sql, column_options) unless type.to_sym == :primary_key
column_sql
end
alias to_s :to_sql
@ -304,8 +307,7 @@ module ActiveRecord
#
# Available options are (none of these exists by default):
# * <tt>:limit</tt> -
# Requests a maximum column length (<tt>:string</tt>, <tt>:text</tt>,
# <tt>:binary</tt> or <tt>:integer</tt> columns only)
# Requests a maximum column length. This is number of characters for <tt>:string</tt> and <tt>:text</tt> columns and number of bytes for :binary and :integer columns.
# * <tt>:default</tt> -
# The column's default value. Use nil for NULL.
# * <tt>:null</tt> -
@ -442,9 +444,10 @@ module ActiveRecord
# Appends <tt>:datetime</tt> columns <tt>:created_at</tt> and
# <tt>:updated_at</tt> to the table.
def timestamps
column(:created_at, :datetime)
column(:updated_at, :datetime)
def timestamps(*args)
options = args.extract_options!
column(:created_at, :datetime, options)
column(:updated_at, :datetime, options)
end
def references(*args)

View file

@ -331,15 +331,26 @@ module ActiveRecord
end
def assume_migrated_upto_version(version)
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|
filename.split('/').last.split('_').first.to_i
end
execute "INSERT INTO #{sm_table} (version) VALUES ('#{version}')" unless migrated.include?(version.to_i)
(versions - migrated).select { |v| v < version.to_i }.each do |v|
execute "INSERT INTO #{sm_table} (version) VALUES ('#{v}')"
unless migrated.include?(version)
execute "INSERT INTO #{sm_table} (version) VALUES ('#{version}')"
end
inserted = Set.new
(versions - migrated).each do |v|
if inserted.include?(v)
raise "Duplicate migration #{v}. Please renumber your migrations to resolve the conflict."
elsif v < version
execute "INSERT INTO #{sm_table} (version) VALUES ('#{v}')"
inserted << v
end
end
end
@ -372,13 +383,9 @@ module ActiveRecord
def add_column_options!(sql, options) #:nodoc:
sql << " DEFAULT #{quote(options[:default], options[:column])}" if options_include_default?(options)
# must explcitly check for :null to allow change_column to work on migrations
if options.has_key? :null
if options[:null] == false
sql << " NOT NULL"
else
sql << " NULL"
end
# must explicitly check for :null to allow change_column to work on migrations
if options[:null] == false
sql << " NOT NULL"
end
end

View file

@ -50,10 +50,7 @@ module ActiveRecord
rescue LoadError => cannot_require_mysql
# Use the bundled Ruby/MySQL driver if no driver is already in place
begin
ActiveRecord::Base.logger.info(
"WARNING: You're using the Ruby-based MySQL library that ships with Rails. This library is not suited for production. " +
"Please install the C-based MySQL library instead (gem install mysql)."
) if ActiveRecord::Base.logger
ActiveSupport::Deprecation.warn "You're using the Ruby-based MySQL library that ships with Rails. This library will be REMOVED FROM RAILS 2.2. Please switch to the offical mysql gem: `gem install mysql`", caller
require 'active_record/vendor/mysql'
rescue LoadError
@ -113,7 +110,8 @@ module ActiveRecord
end
def extract_limit(sql_type)
if sql_type =~ /blob|text/i
case sql_type
when /blob|text/i
case sql_type
when /tiny/i
255
@ -124,6 +122,11 @@ module ActiveRecord
else
super # we could return 65535 here, but we leave it undecorated by default
end
when /^bigint/i; 8
when /^int/i; 4
when /^mediumint/i; 3
when /^smallint/i; 2
when /^tinyint/i; 1
else
super
end
@ -193,10 +196,10 @@ module ActiveRecord
def native_database_types #:nodoc:
{
:primary_key => "int(11) DEFAULT NULL auto_increment PRIMARY KEY",
:primary_key => "int(11) DEFAULT NULL auto_increment PRIMARY KEY".freeze,
:string => { :name => "varchar", :limit => 255 },
:text => { :name => "text" },
:integer => { :name => "int"},
:integer => { :name => "int", :limit => 4 },
:float => { :name => "float" },
:decimal => { :name => "decimal" },
:datetime => { :name => "datetime" },
@ -336,10 +339,11 @@ module ActiveRecord
def add_limit_offset!(sql, options) #:nodoc:
if limit = options[:limit]
limit = sanitize_limit(limit)
unless offset = options[:offset]
sql << " LIMIT #{limit}"
else
sql << " LIMIT #{offset}, #{limit}"
sql << " LIMIT #{offset.to_i}, #{limit}"
end
end
end
@ -439,18 +443,29 @@ module ActiveRecord
end
def change_column_default(table_name, column_name, default) #:nodoc:
current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'")["Type"]
column = column_for(table_name, column_name)
change_column table_name, column_name, column.sql_type, :default => default
end
execute("ALTER TABLE #{quote_table_name(table_name)} CHANGE #{quote_column_name(column_name)} #{quote_column_name(column_name)} #{current_type} DEFAULT #{quote(default)}")
def change_column_null(table_name, column_name, null, default = nil)
column = column_for(table_name, column_name)
unless null || default.nil?
execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL")
end
change_column table_name, column_name, column.sql_type, :null => null
end
def change_column(table_name, column_name, type, options = {}) #:nodoc:
column = column_for(table_name, column_name)
unless options_include_default?(options)
if column = columns(table_name).find { |c| c.name == column_name.to_s }
options[:default] = column.default
else
raise "No such column: #{table_name}.#{column_name}"
end
options[:default] = column.default
end
unless options.has_key?(:null)
options[:null] = column.null
end
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])}"
@ -459,8 +474,17 @@ module ActiveRecord
end
def rename_column(table_name, column_name, new_column_name) #:nodoc:
options = {}
if column = columns(table_name).find { |c| c.name == column_name.to_s }
options[:default] = column.default
options[:null] = column.null
else
raise ActiveRecordError, "No such column: #{table_name}.#{column_name}"
end
current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'")["Type"]
execute "ALTER TABLE #{quote_table_name(table_name)} CHANGE #{quote_column_name(column_name)} #{quote_column_name(new_column_name)} #{current_type}"
rename_column_sql = "ALTER TABLE #{quote_table_name(table_name)} CHANGE #{quote_column_name(column_name)} #{quote_column_name(new_column_name)} #{current_type}"
add_column_options!(rename_column_sql, options)
execute(rename_column_sql)
end
# Maps logical Rails types to MySQL-specific data types.
@ -468,14 +492,12 @@ module ActiveRecord
return super unless type.to_s == 'integer'
case limit
when 0..3
"smallint(#{limit})"
when 4..8
"int(#{limit})"
when 9..20
"bigint(#{limit})"
else
'int(11)'
when 1; 'tinyint'
when 2; 'smallint'
when 3; 'mediumint'
when nil, 4, 11; 'int(11)' # compatibility with MySQL default
when 5..8; 'bigint'
else raise(ActiveRecordError, "No integer type has byte size #{limit}")
end
end
@ -525,6 +547,13 @@ module ActiveRecord
def version
@version ||= @connection.server_info.scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map { |v| v.to_i }
end
def column_for(table_name, column_name)
unless column = columns(table_name).find { |c| c.name == column_name.to_s }
raise "No such column: #{table_name}.#{column_name}"
end
column
end
end
end
end

View file

@ -23,8 +23,8 @@ module ActiveRecord
config = config.symbolize_keys
host = config[:host]
port = config[:port] || 5432
username = config[:username].to_s
password = config[:password].to_s
username = config[:username].to_s if config[:username]
password = config[:password].to_s if config[:password]
if config.has_key?(:database)
database = config[:database]
@ -47,6 +47,14 @@ module ActiveRecord
end
private
def extract_limit(sql_type)
case sql_type
when /^bigint/i; 8
when /^smallint/i; 2
else super
end
end
# Extracts the scale from PostgreSQL-specific data types.
def extract_scale(sql_type)
# Money type has a fixed scale of 2.
@ -174,8 +182,8 @@ module ActiveRecord
def self.extract_value_from_default(default)
case default
# Numeric types
when /\A-?\d+(\.\d*)?\z/
default
when /\A\(?(-?\d+(\.\d*)?\)?)\z/
$1
# Character types
when /\A'(.*)'::(?:character varying|bpchar|text)\z/m
$1
@ -319,6 +327,10 @@ module ActiveRecord
has_support
end
def supports_insert_with_returning?
postgresql_version >= 80200
end
# Returns the configured supported identifier length supported by PostgreSQL,
# or report the default of 63 on PostgreSQL 7.x.
def table_alias_length
@ -360,7 +372,7 @@ module ActiveRecord
# There are some incorrectly compiled postgres drivers out there
# that don't define PGconn.escape.
self.class.instance_eval do
undef_method(:quote_string)
remove_method(:quote_string)
end
end
quote_string(s)
@ -411,8 +423,34 @@ module ActiveRecord
# Executes an INSERT query and returns the new record's ID
def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
# Extract the table from the insert sql. Yuck.
table = sql.split(" ", 4)[2].gsub('"', '')
super || pk && last_insert_id(table, sequence_name || default_sequence_name(table, pk))
# Try an insert with 'returning id' if available (PG >= 8.2)
if supports_insert_with_returning?
pk, sequence_name = *pk_and_sequence_for(table) unless pk
if pk
id = select_value("#{sql} RETURNING #{quote_column_name(pk)}")
clear_query_cache
return id
end
end
# Otherwise, insert then grab last_insert_id.
if insert_id = super
insert_id
else
# If neither pk nor sequence name is given, look them up.
unless pk || sequence_name
pk, sequence_name = *pk_and_sequence_for(table)
end
# If a pk is given, fallback to default sequence name.
# Don't fetch last insert id for a table without a pk.
if pk && sequence_name ||= default_sequence_name(table, pk)
last_insert_id(table, sequence_name)
end
end
end
# create a 2D array representing the result set
@ -492,13 +530,13 @@ module ActiveRecord
option_string = options.symbolize_keys.sum do |key, value|
case key
when :owner
" OWNER = '#{value}'"
" OWNER = \"#{value}\""
when :template
" TEMPLATE = #{value}"
" TEMPLATE = \"#{value}\""
when :encoding
" ENCODING = '#{value}'"
when :tablespace
" TABLESPACE = #{value}"
" TABLESPACE = \"#{value}\""
when :connection_limit
" CONNECTION LIMIT = #{value}"
else
@ -506,7 +544,7 @@ module ActiveRecord
end
end
execute "CREATE DATABASE #{name}#{option_string}"
execute "CREATE DATABASE #{quote_table_name(name)}#{option_string}"
end
# Drops a PostgreSQL database
@ -514,7 +552,15 @@ module ActiveRecord
# Example:
# drop_database 'matt_development'
def drop_database(name) #:nodoc:
execute "DROP DATABASE IF EXISTS #{name}"
if postgresql_version >= 80200
execute "DROP DATABASE IF EXISTS #{quote_table_name(name)}"
else
begin
execute "DROP DATABASE #{quote_table_name(name)}"
rescue ActiveRecord::StatementInvalid
@logger.warn "#{name} database doesn't exist." if @logger
end
end
end
@ -676,7 +722,7 @@ module ActiveRecord
# Renames a table.
def rename_table(name, new_name)
execute "ALTER TABLE #{name} RENAME TO #{new_name}"
execute "ALTER TABLE #{quote_table_name(name)} RENAME TO #{quote_table_name(new_name)}"
end
# Adds a new column to the named table.
@ -698,7 +744,8 @@ module ActiveRecord
begin
execute "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quote_column_name(column_name)} TYPE #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
rescue ActiveRecord::StatementInvalid
rescue ActiveRecord::StatementInvalid => e
raise e if postgresql_version > 80000
# This is PostgreSQL 7.x, so we have to use a more arcane way of doing it.
begin
begin_db_transaction
@ -743,15 +790,14 @@ module ActiveRecord
def type_to_sql(type, limit = nil, precision = nil, scale = nil)
return super unless type.to_s == 'integer'
if limit.nil? || limit == 4
'integer'
elsif limit < 4
'smallint'
else
'bigint'
case limit
when 1..2; 'smallint'
when 3..4, nil; 'integer'
when 5..8; 'bigint'
else raise(ActiveRecordError, "No integer type has byte size #{limit}. Use a numeric with precision 0 instead.")
end
end
# Returns a SELECT DISTINCT clause for a given set of columns and a given ORDER BY clause.
#
# PostgreSQL requires the ORDER BY columns in the select list for distinct queries, and

View file

@ -238,6 +238,15 @@ module ActiveRecord
end
end
def change_column_null(table_name, column_name, null, default = nil)
unless null || default.nil?
execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL")
end
alter_table(table_name) do |definition|
definition[column_name].null = null
end
end
def change_column(table_name, column_name, type, options = {}) #:nodoc:
alter_table(table_name) do |definition|
include_default = options_include_default?(options)
@ -251,6 +260,9 @@ module ActiveRecord
end
def rename_column(table_name, column_name, new_column_name) #:nodoc:
unless columns(table_name).detect{|c| c.name == column_name.to_s }
raise ActiveRecord::ActiveRecordError, "Missing column #{table_name}.#{column_name}"
end
alter_table(table_name, :rename => {column_name.to_s => new_column_name.to_s})
end

View file

@ -123,7 +123,10 @@ module ActiveRecord
attr = attr.to_s
# The attribute already has an unsaved change.
unless changed_attributes.include?(attr)
if changed_attributes.include?(attr)
old = changed_attributes[attr]
changed_attributes.delete(attr) unless field_changed?(attr, old, value)
else
old = clone_attribute_value(:read_attribute, attr)
changed_attributes[attr] = old if field_changed?(attr, old, value)
end
@ -134,7 +137,9 @@ module ActiveRecord
def update_with_dirty
if partial_updates?
update_without_dirty(changed)
# Serialized attributes should always be written in case they've been
# changed in place.
update_without_dirty(changed | self.class.serialized_attributes.keys)
else
update_without_dirty
end
@ -142,9 +147,11 @@ module ActiveRecord
def field_changed?(attr, old, value)
if column = column_for_attribute(attr)
if column.type == :integer && column.null && old.nil?
if column.type == :integer && column.null && (old.nil? || old == 0)
# For nullable integer columns, NULL gets stored in database for blank (i.e. '') values.
# Hence we don't record it as a change if the value changes from nil to ''.
# If an old value of 0 is set to '' we want this to get changed to nil as otherwise it'll
# be typecast back to 0 (''.to_i => 0)
value = nil if value.blank?
else
value = column.type_cast(value)

View file

@ -68,6 +68,7 @@ module ActiveRecord
def update_with_lock(attribute_names = @attributes.keys) #:nodoc:
return update_without_lock(attribute_names) unless locking_enabled?
return 0 if attribute_names.empty?
lock_col = self.class.locking_column
previous_value = send(lock_col).to_i

View file

@ -238,6 +238,22 @@ module ActiveRecord
# lower than the current schema version: when migrating up, those
# never-applied "interleaved" migrations will be automatically applied, and
# when migrating down, never-applied "interleaved" migrations will be skipped.
#
# == Timestamped Migrations
#
# By default, Rails generates migrations that look like:
#
# 20080717013526_your_migration_name.rb
#
# The prefix is a generation timestamp (in UTC).
#
# If you'd prefer to use numeric prefixes, you can turn timestamped migrations
# off by setting:
#
# config.active_record.timestamped_migrations = false
#
# In environment.rb.
#
class Migration
@@verbose = true
cattr_accessor :verbose
@ -369,11 +385,17 @@ module ActiveRecord
Base.table_name_prefix + 'schema_migrations' + Base.table_name_suffix
end
def get_all_versions
Base.connection.select_values("SELECT version FROM #{schema_migrations_table_name}").map(&:to_i).sort
end
def current_version
version = Base.connection.select_values(
"SELECT version FROM #{schema_migrations_table_name}"
).map(&:to_i).max rescue nil
version || 0
sm_table = schema_migrations_table_name
if Base.connection.table_exists?(sm_table)
get_all_versions.max || 0
else
0
end
end
def proper_table_name(name)
@ -389,7 +411,7 @@ module ActiveRecord
end
def current_version
self.class.current_version
migrated.last || 0
end
def current_migration
@ -399,7 +421,10 @@ module ActiveRecord
def run
target = migrations.detect { |m| m.version == @target_version }
raise UnknownMigrationVersionError.new(@target_version) if target.nil?
target.migrate(@direction)
unless (up? && migrated.include?(target.version.to_i)) || (down? && !migrated.include?(target.version.to_i))
target.migrate(@direction)
record_version_state_after_migrating(target.version)
end
end
def migrate
@ -470,17 +495,19 @@ module ActiveRecord
end
def migrated
sm_table = self.class.schema_migrations_table_name
Base.connection.select_values("SELECT version FROM #{sm_table}").map(&:to_i).sort
@migrated_versions ||= self.class.get_all_versions
end
private
def record_version_state_after_migrating(version)
sm_table = self.class.schema_migrations_table_name
@migrated_versions ||= []
if down?
@migrated_versions.delete(version.to_i)
Base.connection.update("DELETE FROM #{sm_table} WHERE version = '#{version}'")
else
@migrated_versions.push(version.to_i).sort!
Base.connection.insert("INSERT INTO #{sm_table} (version) VALUES ('#{version}')")
end
end

View file

@ -82,6 +82,7 @@ module ActiveRecord
# expected_options = { :conditions => { :colored => 'red' } }
# assert_equal expected_options, Shirt.colored('red').proxy_options
def named_scope(name, options = {}, &block)
name = name.to_sym
scopes[name] = lambda do |parent_scope, *args|
Scope.new(parent_scope, case options
when Hash
@ -102,7 +103,7 @@ module ActiveRecord
attr_reader :proxy_scope, :proxy_options
[].methods.each do |m|
unless m =~ /(^__|^nil\?|^send|^object_id$|class|extend|find|count|sum|average|maximum|minimum|paginate|first|last|empty?)/
unless m =~ /(^__|^nil\?|^send|^object_id$|class|extend|^find$|count|sum|average|maximum|minimum|paginate|first|last|empty\?|respond_to\?)/
delegate m, :to => :proxy_found
end
end
@ -139,6 +140,10 @@ module ActiveRecord
@found ? @found.empty? : count.zero?
end
def respond_to?(method, include_private = false)
super || @proxy_scope.respond_to?(method, include_private)
end
protected
def proxy_found
@found || load_found

View file

@ -20,7 +20,7 @@ module ActiveRecord
# ActiveRecord::Base.observers = Cacher, GarbageCollector
#
# Note: Setting this does not instantiate the observers yet. +instantiate_observers+ is
# called during startup, and before each development request.
# called during startup, and before each development request.
def observers=(*observers)
@observers = observers.flatten
end
@ -130,11 +130,11 @@ module ActiveRecord
# Observers register themselves in the model class they observe, since it is the class that
# notifies them of events when they occur. As a side-effect, when an observer is loaded its
# corresponding model class is loaded.
#
#
# Up to (and including) Rails 2.0.2 observers were instantiated between plugins and
# application initializers. Now observers are loaded after application initializers,
# application initializers. Now observers are loaded after application initializers,
# so observed models can make use of extensions.
#
#
# If by any chance you are using observed models in the initialization you can still
# load their observers by calling <tt>ModelObserver.instance</tt> before. Observers are
# singletons and that call instantiates and registers them.
@ -189,7 +189,9 @@ module ActiveRecord
def add_observer!(klass)
klass.add_observer(self)
klass.class_eval 'def after_find() end' unless klass.respond_to?(:after_find)
if respond_to?(:after_find) && !klass.method_defined?(:after_find)
klass.class_eval 'def after_find() end'
end
end
end
end

View file

@ -22,11 +22,22 @@ module ActiveRecord
end
end
def assert_queries(num = 1)
$query_count = 0
def assert_sql(*patterns_to_match)
$queries_executed = []
yield
ensure
assert_equal num, $query_count, "#{$query_count} instead of #{num} queries were executed."
failed_patterns = []
patterns_to_match.each do |pattern|
failed_patterns << pattern unless $queries_executed.any?{ |sql| pattern === sql }
end
assert failed_patterns.empty?, "Query pattern(s) #{failed_patterns.map(&:inspect).join(', ')} not found."
end
def assert_queries(num = 1)
$queries_executed = []
yield
ensure
assert_equal num, $queries_executed.size, "#{$queries_executed.size} instead of #{num} queries were executed."
end
def assert_no_queries(&block)

View file

@ -480,8 +480,9 @@ module ActiveRecord
# validates_length_of :fax, :in => 7..32, :allow_nil => true
# validates_length_of :phone, :in => 7..32, :allow_blank => true
# validates_length_of :user_name, :within => 6..20, :too_long => "pick a shorter name", :too_short => "pick a longer name"
# validates_length_of :fav_bra_size, :minimum=>1, :too_short=>"please enter at least %d character"
# validates_length_of :smurf_leader, :is=>4, :message=>"papa is spelled with %d characters... don't play me."
# validates_length_of :fav_bra_size, :minimum => 1, :too_short => "please enter at least %d character"
# validates_length_of :smurf_leader, :is => 4, :message => "papa is spelled with %d characters... don't play me."
# validates_length_of :essay, :minimum => 100, :too_short => "Your essay must be at least %d words."), :tokenizer => lambda {|str| str.scan(/\w+/) }
# end
#
# Configuration options:
@ -492,7 +493,6 @@ module ActiveRecord
# * <tt>:in</tt> - A synonym(or alias) for <tt>:within</tt>.
# * <tt>:allow_nil</tt> - Attribute may be +nil+; skip validation.
# * <tt>:allow_blank</tt> - Attribute may be blank; skip validation.
#
# * <tt>:too_long</tt> - The error message if the attribute goes over the maximum (default is: "is too long (maximum is %d characters)").
# * <tt>:too_short</tt> - The error message if the attribute goes under the minimum (default is: "is too short (min is %d characters)").
# * <tt>:wrong_length</tt> - The error message if using the <tt>:is</tt> method and the attribute is the wrong size (default is: "is the wrong length (should be %d characters)").
@ -504,12 +504,16 @@ module ActiveRecord
# * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should
# not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
# method, proc or string should return or evaluate to a true or false value.
# * <tt>:tokenizer</tt> - Specifies how to split up the attribute string. (e.g. <tt>:tokenizer => lambda {|str| str.scan(/\w+/)}</tt> to
# count words as in above example.)
# Defaults to <tt>lambda{ |value| value.split(//) }</tt> which counts individual characters.
def validates_length_of(*attrs)
# Merge given options with defaults.
options = {
:too_long => ActiveRecord::Errors.default_error_messages[:too_long],
:too_short => ActiveRecord::Errors.default_error_messages[:too_short],
:wrong_length => ActiveRecord::Errors.default_error_messages[:wrong_length]
:wrong_length => ActiveRecord::Errors.default_error_messages[:wrong_length],
:tokenizer => lambda {|value| value.split(//)}
}.merge(DEFAULT_VALIDATION_OPTIONS)
options.update(attrs.extract_options!.symbolize_keys)
@ -536,7 +540,7 @@ module ActiveRecord
too_long = options[:too_long] % option_value.end
validates_each(attrs, options) do |record, attr, value|
value = value.split(//) if value.kind_of?(String)
value = options[:tokenizer].call(value) if value.kind_of?(String)
if value.nil? or value.size < option_value.begin
record.errors.add(attr, too_short)
elsif value.size > option_value.end
@ -553,7 +557,7 @@ module ActiveRecord
message = (options[:message] || options[message_options[option]]) % option_value
validates_each(attrs, options) do |record, attr, value|
value = value.split(//) if value.kind_of?(String)
value = options[:tokenizer].call(value) if value.kind_of?(String)
record.errors.add(attr, message) unless !value.nil? and value.size.method(validity_checks[option])[option_value]
end
end
@ -614,14 +618,20 @@ module ActiveRecord
# class (which has a database table to query from).
finder_class = class_hierarchy.detect { |klass| !klass.abstract_class? }
if value.nil? || (configuration[:case_sensitive] || !finder_class.columns_hash[attr_name.to_s].text?)
is_text_column = finder_class.columns_hash[attr_name.to_s].text?
if !value.nil? && is_text_column
value = value.to_s
end
if value.nil? || (configuration[:case_sensitive] || !is_text_column)
condition_sql = "#{record.class.quoted_table_name}.#{attr_name} #{attribute_condition(value)}"
condition_params = [value]
else
# sqlite has case sensitive SELECT query, while MySQL/Postgresql don't.
# Hence, this is needed only for sqlite.
condition_sql = "LOWER(#{record.class.quoted_table_name}.#{attr_name}) #{attribute_condition(value)}"
condition_params = [value.downcase]
condition_params = [value.chars.downcase]
end
if scope = configuration[:scope]
@ -851,7 +861,7 @@ module ActiveRecord
raw_value = raw_value.to_i
else
begin
raw_value = Kernel.Float(raw_value.to_s)
raw_value = Kernel.Float(raw_value)
rescue ArgumentError, TypeError
record.errors.add(attr_name, configuration[:message] || ActiveRecord::Errors.default_error_messages[:not_a_number])
next

View file

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