diff options
Diffstat (limited to 'activerecord')
38 files changed, 711 insertions, 347 deletions
diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG index 8f283e1117..75657cb6ee 100644 --- a/activerecord/CHANGELOG +++ b/activerecord/CHANGELOG @@ -15,6 +15,10 @@ [Aaron Patterson] +*Rails 3.0.1 (October 15, 2010)* + +* Introduce a fix for CVE-2010-3993 + *Rails 3.0.0 (August 29, 2010)* * Changed update_attribute to not run callbacks and update the record directly in the database [Neeraj Singh] diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 565ebf8197..affa2fbcaf 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -1833,10 +1833,10 @@ module ActiveRecord end class JoinDependency # :nodoc: - attr_reader :joins, :reflections, :table_aliases + attr_reader :join_parts, :reflections, :table_aliases def initialize(base, associations, joins) - @joins = [JoinBase.new(base, joins)] + @join_parts = [JoinBase.new(base, joins)] @associations = associations @reflections = [] @base_records_hash = {} @@ -1849,17 +1849,17 @@ module ActiveRecord def graft(*associations) associations.each do |association| join_associations.detect {|a| association == a} || - build(association.reflection.name, association.find_parent_in(self) || join_base, association.join_class) + build(association.reflection.name, association.find_parent_in(self) || join_base, association.join_type) end self end def join_associations - @joins.last(@joins.length - 1) + join_parts.last(join_parts.length - 1) end def join_base - @joins[0] + join_parts.first end def count_aliases_from_table_joins(name) @@ -1917,22 +1917,24 @@ module ActiveRecord protected - def build(associations, parent = nil, join_class = Arel::InnerJoin) - parent ||= @joins.last + def build(associations, parent = nil, join_type = Arel::InnerJoin) + parent ||= join_parts.last case associations when Symbol, String reflection = parent.reflections[associations.to_s.intern] or raise ConfigurationError, "Association named '#{ associations }' was not found; perhaps you misspelled it?" @reflections << reflection - @joins << build_join_association(reflection, parent).with_join_class(join_class) + join_association = build_join_association(reflection, parent) + join_association.join_type = join_type + @join_parts << join_association when Array associations.each do |association| - build(association, parent, join_class) + build(association, parent, join_type) end when Hash associations.keys.sort{|a,b|a.to_s<=>b.to_s}.each do |name| - build(name, parent, join_class) - build(associations[name], nil, join_class) + build(name, parent, join_type) + build(associations[name], nil, join_type) end else raise ConfigurationError, associations.inspect @@ -1949,91 +1951,111 @@ module ActiveRecord JoinAssociation.new(reflection, self, parent) end - def construct(parent, associations, joins, row) + def construct(parent, associations, join_parts, row) case associations when Symbol, String - join = joins.detect{|j| j.reflection.name.to_s == associations.to_s && j.parent_table_name == parent.class.table_name } - raise(ConfigurationError, "No such association") if join.nil? + join_part = join_parts.detect { |j| + j.reflection.name.to_s == associations.to_s && + j.parent_table_name == parent.class.table_name } + raise(ConfigurationError, "No such association") if join_part.nil? - joins.delete(join) - construct_association(parent, join, row) + join_parts.delete(join_part) + construct_association(parent, join_part, row) when Array associations.each do |association| - construct(parent, association, joins, row) + construct(parent, association, join_parts, row) end when Hash associations.sort_by { |k,_| k.to_s }.each do |name, assoc| - join = joins.detect{|j| j.reflection.name.to_s == name.to_s && j.parent_table_name == parent.class.table_name } - raise(ConfigurationError, "No such association") if join.nil? - - association = construct_association(parent, join, row) - joins.delete(join) - construct(association, assoc, joins, row) if association + join_part = join_parts.detect{ |j| + j.reflection.name.to_s == name.to_s && + j.parent_table_name == parent.class.table_name } + raise(ConfigurationError, "No such association") if join_part.nil? + + association = construct_association(parent, join_part, row) + join_parts.delete(join_part) + construct(association, assoc, join_parts, row) if association end else raise ConfigurationError, associations.inspect end end - def construct_association(record, join, row) - return if record.id.to_s != join.parent.record_id(row).to_s + def construct_association(record, join_part, row) + return if record.id.to_s != join_part.parent.record_id(row).to_s - macro = join.reflection.macro + macro = join_part.reflection.macro if macro == :has_one - return if record.instance_variable_defined?("@#{join.reflection.name}") - association = join.instantiate(row) unless row[join.aliased_primary_key].nil? - set_target_and_inverse(join, association, record) + return if record.instance_variable_defined?("@#{join_part.reflection.name}") + association = join_part.instantiate(row) unless row[join_part.aliased_primary_key].nil? + set_target_and_inverse(join_part, association, record) else - return if row[join.aliased_primary_key].nil? - association = join.instantiate(row) + return if row[join_part.aliased_primary_key].nil? + association = join_part.instantiate(row) case macro when :has_many, :has_and_belongs_to_many - collection = record.send(join.reflection.name) + collection = record.send(join_part.reflection.name) collection.loaded collection.target.push(association) collection.__send__(:set_inverse_instance, association, record) when :belongs_to - set_target_and_inverse(join, association, record) + set_target_and_inverse(join_part, association, record) else - raise ConfigurationError, "unknown macro: #{join.reflection.macro}" + raise ConfigurationError, "unknown macro: #{join_part.reflection.macro}" end end association end - def set_target_and_inverse(join, association, record) - association_proxy = record.send("set_#{join.reflection.name}_target", association) + def set_target_and_inverse(join_part, association, record) + association_proxy = record.send("set_#{join_part.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, :arel_engine, :to => :active_record - - def initialize(active_record, joins = nil) + + # A JoinPart represents a part of a JoinDependency. It is an abstract class, inherited + # by JoinBase and JoinAssociation. A JoinBase represents the Active Record which + # everything else is being joined onto. A JoinAssociation represents an association which + # is joining to the base. A JoinAssociation may result in more than one actual join + # operations (for example a has_and_belongs_to_many JoinAssociation would result in + # two; one for the join table and one for the target table). + class JoinPart # :nodoc: + # The Active Record class which this join part is associated 'about'; for a JoinBase + # this is the actual base model, for a JoinAssociation this is the target model of the + # association. + attr_reader :active_record + + delegate :table_name, :column_names, :primary_key, :reflections, :sanitize_sql, :arel_engine, :to => :active_record + + def initialize(active_record) @active_record = active_record @cached_record = {} - @table_joins = joins end - + def ==(other) - other.class == self.class && - other.active_record == active_record && - other.table_joins == table_joins + raise NotImplementedError end - + + # An Arel::Table for the active_record + def table + raise NotImplementedError + end + + # The prefix to be used when aliasing columns in the active_record's table def aliased_prefix - "t0" + raise NotImplementedError end - + + # The alias for the active_record's table + def aliased_table_name + raise NotImplementedError + end + + # The alias for the primary key of the active_record's table def aliased_primary_key "#{aliased_prefix}_r0" end - - def aliased_table_name - active_record.table_name - end - + + # An array of [column_name, alias] pairs for the table def column_names_with_alias unless defined?(@column_names_with_alias) @column_names_with_alias = [] @@ -2059,33 +2081,74 @@ module ActiveRecord end end - class JoinAssociation < JoinBase # :nodoc: - attr_reader :reflection, :parent, :aliased_table_name, :aliased_prefix, :aliased_join_table_name, :parent_table_name, :join_class - delegate :options, :klass, :through_reflection, :source_reflection, :to => :reflection + class JoinBase < JoinPart # :nodoc: + # Extra joins provided when the JoinDependency was created + attr_reader :table_joins + + def initialize(active_record, joins = nil) + super(active_record) + @table_joins = joins + end + + def ==(other) + other.class == self.class && + other.active_record == active_record && + other.table_joins == table_joins + end + + def aliased_prefix + "t0" + end + + def table + Arel::Table.new(table_name, :engine => arel_engine, :columns => active_record.columns) + end + + def aliased_table_name + active_record.table_name + end + end + + class JoinAssociation < JoinPart # :nodoc: + # The reflection of the association represented + attr_reader :reflection + + # The JoinDependency object which this JoinAssociation exists within. This is mainly + # relevant for generating aliases which do not conflict with other joins which are + # part of the query. + attr_reader :join_dependency + + # A JoinBase instance representing the active record we are joining onto. + # (So in Author.has_many :posts, the Author would be that base record.) + attr_reader :parent + + # What type of join will be generated, either Arel::InnerJoin (default) or Arel::OuterJoin + attr_accessor :join_type + + # These implement abstract methods from the superclass + attr_reader :aliased_prefix, :aliased_table_name + + delegate :options, :through_reflection, :source_reflection, :to => :reflection + delegate :table, :table_name, :to => :parent, :prefix => true def initialize(reflection, join_dependency, parent = nil) reflection.check_validity! + if reflection.options[:polymorphic] raise EagerLoadPolymorphicError.new(reflection) end super(reflection.klass) - @join_dependency = join_dependency - @parent = parent - @reflection = reflection - @aliased_prefix = "t#{ join_dependency.joins.size }" - @parent_table_name = parent.active_record.table_name - @aliased_table_name = aliased_table_name_for(table_name) - @join = nil - @join_class = Arel::InnerJoin - - if reflection.macro == :has_and_belongs_to_many - @aliased_join_table_name = aliased_table_name_for(reflection.options[:join_table], "_join") - end - - 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 + + @reflection = reflection + @join_dependency = join_dependency + @parent = parent + @join_type = Arel::InnerJoin + + # This must be done eagerly upon initialisation because the alias which is produced + # depends on the state of the join dependency, but we want it to work the same way + # every time. + allocate_aliases end def ==(other) @@ -2095,63 +2158,29 @@ module ActiveRecord end def find_parent_in(other_join_dependency) - other_join_dependency.joins.detect do |join| - self.parent == join + other_join_dependency.join_parts.detect do |join_part| + self.parent == join_part end end - - def with_join_class(join_class) - @join_class = join_class - self - end - - def association_join - return @join if @join - - aliased_table = Arel::Table.new(table_name, :as => @aliased_table_name, - :engine => arel_engine, - :columns => klass.columns) - - parent_table = Arel::Table.new(parent.table_name, :as => parent.aliased_table_name, - :engine => arel_engine, - :columns => parent.active_record.columns) - - @join = send("build_#{reflection.macro}", aliased_table, parent_table) - - unless klass.descends_from_active_record? - sti_column = aliased_table[klass.inheritance_column] - sti_condition = sti_column.eq(klass.sti_name) - klass.descendants.each {|subclass| sti_condition = sti_condition.or(sti_column.eq(subclass.sti_name)) } - - @join << sti_condition - end - - [through_reflection, reflection].each do |ref| - if ref && ref.options[:conditions] - @join << interpolate_sql(sanitize_sql(ref.options[:conditions], aliased_table_name)) - end - end - - @join + + def join_to(relation) + send("join_#{reflection.macro}_to", relation) end - def relation - aliased = Arel::Table.new(table_name, :as => @aliased_table_name, - :engine => arel_engine, - :columns => klass.columns) - - if reflection.macro == :has_and_belongs_to_many - [Arel::Table.new(options[:join_table], :as => aliased_join_table_name, :engine => arel_engine), aliased] - elsif reflection.options[:through] - [Arel::Table.new(through_reflection.klass.table_name, :as => aliased_join_table_name, :engine => arel_engine), aliased] - else - aliased - end + def join_relation(joining_relation) + self.join_type = Arel::OuterJoin + joining_relation.joins(self) end - - def join_relation(joining_relation, join = nil) - joining_relation.joins(self.with_join_class(Arel::OuterJoin)) + + def table + @table ||= Arel::Table.new( + table_name, :as => aliased_table_name, + :engine => arel_engine, :columns => active_record.columns + ) end + + # More semantic name given we are talking about associations + alias_method :target_table, :table protected @@ -2180,87 +2209,174 @@ module ActiveRecord ActiveRecord::Base.pluralize_table_names ? table_name.to_s.pluralize : table_name end - def table_alias_for(table_name, table_alias) - "#{table_name} #{table_alias if table_name != table_alias}".strip - end - - def table_name_and_alias - table_alias_for table_name, @aliased_table_name - end - def interpolate_sql(sql) instance_eval("%@#{sql.gsub('@', '\@')}@", __FILE__, __LINE__) end private + + def allocate_aliases + @aliased_prefix = "t#{ join_dependency.join_parts.size }" + @aliased_table_name = aliased_table_name_for(table_name) + + if reflection.macro == :has_and_belongs_to_many + @aliased_join_table_name = aliased_table_name_for(reflection.options[:join_table], "_join") + elsif [: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 + + def process_conditions(conditions, table_name) + Arel.sql(interpolate_sql(sanitize_sql(conditions, table_name))) + end + + def join_target_table(relation, *conditions) + relation = relation.join(target_table, join_type) + + # If the target table is an STI model then we must be sure to only include records of + # its type and its sub-types. + unless active_record.descends_from_active_record? + sti_column = target_table[active_record.inheritance_column] + + sti_condition = sti_column.eq(active_record.sti_name) + active_record.descendants.each do |subclass| + sti_condition = sti_condition.or(sti_column.eq(subclass.sti_name)) + end + + conditions << sti_condition + end + + # If the reflection has conditions, add them + if options[:conditions] + conditions << process_conditions(options[:conditions], aliased_table_name) + end + + relation = relation.on(*conditions) + end - def build_has_and_belongs_to_many(aliased_table, parent_table) - join_table = Arel::Table.new(options[:join_table], :as => aliased_join_table_name, :engine => arel_engine) - fk = options[:foreign_key] || reflection.active_record.to_s.foreign_key - klass_fk = options[:association_foreign_key] || klass.to_s.foreign_key - - [ - join_table[fk].eq(parent_table[reflection.active_record.primary_key]), - aliased_table[klass.primary_key].eq(join_table[klass_fk]) - ] + def join_has_and_belongs_to_many_to(relation) + join_table = Arel::Table.new( + options[:join_table], :engine => arel_engine, + :as => @aliased_join_table_name + ) + + fk = options[:foreign_key] || reflection.active_record.to_s.foreign_key + klass_fk = options[:association_foreign_key] || reflection.klass.to_s.foreign_key + + relation = relation.join(join_table, join_type) + relation = relation.on( + join_table[fk]. + eq(parent_table[reflection.active_record.primary_key]) + ) + + join_target_table( + relation, + target_table[reflection.klass.primary_key]. + eq(join_table[klass_fk]) + ) end - def build_has_many(aliased_table, parent_table) + def join_has_many_to(relation) if reflection.options[:through] - join_table = Arel::Table.new(through_reflection.klass.table_name, - :as => aliased_join_table_name, - :engine => arel_engine) - jt_foreign_key = jt_as_extra = jt_source_extra = jt_sti_extra = nil - first_key = second_key = nil - - if through_reflection.options[:as] # has_many :through against a polymorphic join - as_key = through_reflection.options[:as].to_s - jt_foreign_key = as_key + '_id' - jt_as_extra = join_table[as_key + '_type'].eq(parent.active_record.base_class.name) - else - jt_foreign_key = through_reflection.primary_key_name - end - - case source_reflection.macro - when :has_many - second_key = options[:foreign_key] || primary_key + join_has_many_through_to(relation) + elsif reflection.options[:as] + join_has_many_polymorphic_to(relation) + else + foreign_key = options[:foreign_key] || reflection.active_record.name.foreign_key + primary_key = options[:primary_key] || parent.primary_key + + join_target_table( + relation, + target_table[foreign_key]. + eq(parent_table[primary_key]) + ) + end + end + alias :join_has_one_to :join_has_many_to + + def join_has_many_through_to(relation) + join_table = Arel::Table.new( + through_reflection.klass.table_name, :engine => arel_engine, + :as => @aliased_join_table_name + ) + + jt_conditions = [] + jt_foreign_key = first_key = second_key = nil + + if through_reflection.options[:as] # has_many :through against a polymorphic join + as_key = through_reflection.options[:as].to_s + jt_foreign_key = as_key + '_id' + + jt_conditions << + join_table[as_key + '_type']. + eq(parent.active_record.base_class.name) + else + jt_foreign_key = through_reflection.primary_key_name + end - if source_reflection.options[:as] - first_key = "#{source_reflection.options[:as]}_id" - else - first_key = through_reflection.klass.base_class.to_s.foreign_key - end + case source_reflection.macro + when :has_many + second_key = options[:foreign_key] || primary_key - unless through_reflection.klass.descends_from_active_record? - jt_sti_extra = join_table[through_reflection.active_record.inheritance_column].eq(through_reflection.klass.sti_name) - end - when :belongs_to - first_key = primary_key - if reflection.options[:source_type] - second_key = source_reflection.association_foreign_key - jt_source_extra = join_table[reflection.source_reflection.options[:foreign_type]].eq(reflection.options[:source_type]) - else - second_key = source_reflection.primary_key_name - end + if source_reflection.options[:as] + first_key = "#{source_reflection.options[:as]}_id" + else + first_key = through_reflection.klass.base_class.to_s.foreign_key end - [ - [parent_table[parent.primary_key].eq(join_table[jt_foreign_key]), jt_as_extra, jt_source_extra, jt_sti_extra].compact, - aliased_table[first_key].eq(join_table[second_key]) - ] - elsif reflection.options[:as] - id_rel = aliased_table["#{reflection.options[:as]}_id"].eq(parent_table[parent.primary_key]) - type_rel = aliased_table["#{reflection.options[:as]}_type"].eq(parent.active_record.base_class.name) - [id_rel, type_rel] - else - foreign_key = options[:foreign_key] || reflection.active_record.name.foreign_key - [aliased_table[foreign_key].eq(parent_table[reflection.options[:primary_key] || parent.primary_key])] + unless through_reflection.klass.descends_from_active_record? + jt_conditions << + join_table[through_reflection.active_record.inheritance_column]. + eq(through_reflection.klass.sti_name) + end + when :belongs_to + first_key = primary_key + + if reflection.options[:source_type] + second_key = source_reflection.association_foreign_key + + jt_conditions << + join_table[reflection.source_reflection.options[:foreign_type]]. + eq(reflection.options[:source_type]) + else + second_key = source_reflection.primary_key_name + end + end + + jt_conditions << + parent_table[parent.primary_key]. + eq(join_table[jt_foreign_key]) + + if through_reflection.options[:conditions] + jt_conditions << process_conditions(through_reflection.options[:conditions], aliased_table_name) end + + relation = relation.join(join_table, join_type).on(*jt_conditions) + + join_target_table( + relation, + target_table[first_key].eq(join_table[second_key]) + ) + end + + def join_has_many_polymorphic_to(relation) + join_target_table( + relation, + target_table["#{reflection.options[:as]}_id"]. + eq(parent_table[parent.primary_key]), + target_table["#{reflection.options[:as]}_type"]. + eq(parent.active_record.base_class.name) + ) end - alias :build_has_one :build_has_many - def build_belongs_to(aliased_table, parent_table) - [aliased_table[options[:primary_key] || reflection.klass.primary_key].eq(parent_table[options[:foreign_key] || reflection.primary_key_name])] + def join_belongs_to_to(relation) + foreign_key = options[:foreign_key] || reflection.primary_key_name + primary_key = options[:primary_key] || reflection.klass.primary_key + + join_target_table( + relation, + target_table[primary_key].eq(parent_table[foreign_key]) + ) end end end diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index 7297af9f79..67f70c434e 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -18,7 +18,7 @@ module ActiveRecord # method is defined by Active Record though. def instance_method_already_implemented?(method_name) method_name = method_name.to_s - @_defined_class_methods ||= ancestors.first(ancestors.index(ActiveRecord::Base)).sum([]) { |m| m.public_instance_methods(false) | m.private_instance_methods(false) | m.protected_instance_methods(false) }.map {|m| m.to_s }.to_set + @_defined_class_methods ||= ancestors.first(ancestors.index(ActiveRecord::Base)).sum([]) { |m| m.instance_methods(false) | m.private_instance_methods(false) }.map {|m| m.to_s }.to_set @@_defined_activerecord_methods ||= defined_activerecord_methods raise DangerousAttributeError, "#{method_name} is defined by ActiveRecord" if @@_defined_activerecord_methods.include?(method_name) @_defined_class_methods.include?(method_name) diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index 01699746d8..ad5a3e7562 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -85,7 +85,7 @@ module ActiveRecord def _read_attribute(attr_name) attr_name = attr_name.to_s attr_name = self.class.primary_key if attr_name == 'id' - if !(value = @attributes[attr_name]).nil? + if value = @attributes[attr_name] if column = column_for_attribute(attr_name) if unserializable_attribute?(attr_name, column) unserialize_attribute(attr_name) diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index ff6be4ff19..78b3507dd9 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -423,7 +423,7 @@ module ActiveRecord #:nodoc: class << self # Class methods delegate :find, :first, :last, :all, :destroy, :destroy_all, :exists?, :delete, :delete_all, :update, :update_all, :to => :scoped delegate :find_each, :find_in_batches, :to => :scoped - delegate :select, :group, :order, :reorder, :limit, :offset, :joins, :where, :preload, :eager_load, :includes, :from, :lock, :readonly, :having, :create_with, :to => :scoped + delegate :select, :group, :order, :except, :limit, :offset, :joins, :where, :preload, :eager_load, :includes, :from, :lock, :readonly, :having, :create_with, :to => :scoped delegate :count, :average, :minimum, :maximum, :sum, :calculate, :to => :scoped # Executes a custom SQL query against your database and returns all the results. The results will diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb index ec7035e540..3716937689 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb @@ -72,7 +72,7 @@ module ActiveRecord end adapter_method = "#{spec[:adapter]}_connection" - if !respond_to?(adapter_method) + unless respond_to?(adapter_method) raise AdapterNotFound, "database configuration specifies nonexistent #{spec[:adapter]} adapter" end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb index e2b3773a99..a7a12faac2 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb @@ -10,28 +10,31 @@ module ActiveRecord return value.quoted_id if value.respond_to?(:quoted_id) case value - when String, ActiveSupport::Multibyte::Chars - value = value.to_s - if column && column.type == :binary && column.class.respond_to?(:string_to_binary) - "'#{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 - "'#{quote_string(value)}'" # ' (for ruby-mode) - end - when NilClass then "NULL" - when TrueClass then (column && column.type == :integer ? '1' : quoted_true) - when FalseClass then (column && column.type == :integer ? '0' : quoted_false) - when Float, Fixnum, Bignum then value.to_s - # BigDecimals need to be output in a non-normalized form and quoted. - when BigDecimal then value.to_s('F') + when String, ActiveSupport::Multibyte::Chars + value = value.to_s + return "'#{quote_string(value)}'" unless column + + case column.type + when :binary then "'#{quote_string(column.string_to_binary(value))}'" + when :integer then value.to_i.to_s + when :float then value.to_f.to_s + else + "'#{quote_string(value)}'" + end + + when true, false + if column && column.type == :integer + value ? '1' : '0' else - if value.acts_like?(:date) || value.acts_like?(:time) - "'#{quoted_date(value)}'" - else - "'#{quote_string(value.to_s)}'" - end + value ? quoted_true : quoted_false + end + # BigDecimals need to be put in a non-normalized form and quoted. + when nil then "NULL" + when BigDecimal then value.to_s('F') + when Numeric then value.to_s + when Date, Time then "'#{quoted_date(value)}'" + else + "'#{quote_string(value.to_s)}'" end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb index 6480aeb171..60ccf9edf3 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -114,6 +114,11 @@ module ActiveRecord type_cast(default) end + # Used to convert from Strings to BLOBs + def string_to_binary(value) + self.class.string_to_binary(value) + end + class << self # Used to convert from Strings to BLOBs def string_to_binary(value) @@ -268,6 +273,10 @@ module ActiveRecord # for generating a number of table creation or table changing SQL statements. class ColumnDefinition < Struct.new(:base, :name, :type, :limit, :precision, :scale, :default, :null) #:nodoc: + def string_to_binary(value) + value + end + def sql_type base.type_to_sql(type.to_sym, limit, precision, scale) rescue type end diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index e7f7b37b27..a4b336dfaf 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -132,7 +132,7 @@ module ActiveRecord cattr_accessor :emulate_booleans self.emulate_booleans = true - ADAPTER_NAME = 'MySQL'.freeze + ADAPTER_NAME = 'MySQL' LOST_CONNECTION_ERROR_MESSAGES = [ "Server shutdown in progress", @@ -140,10 +140,10 @@ module ActiveRecord "Lost connection to MySQL server during query", "MySQL server has gone away" ] - QUOTED_TRUE, QUOTED_FALSE = '1'.freeze, '0'.freeze + QUOTED_TRUE, QUOTED_FALSE = '1', '0' NATIVE_DATABASE_TYPES = { - :primary_key => "int(11) DEFAULT NULL auto_increment PRIMARY KEY".freeze, + :primary_key => "int(11) DEFAULT NULL auto_increment PRIMARY KEY", :string => { :name => "varchar", :limit => 255 }, :text => { :name => "text" }, :integer => { :name => "int", :limit => 4 }, diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 5f14284615..5949985e4d 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -180,8 +180,6 @@ module ActiveRecord # <encoding></tt> call on the connection. # * <tt>:min_messages</tt> - An optional client min messages that is used in a # <tt>SET client_min_messages TO <min_messages></tt> call on the connection. - # * <tt>:allow_concurrency</tt> - If true, use async query methods so Ruby threads don't deadlock; - # otherwise, use blocking query methods. class PostgreSQLAdapter < AbstractAdapter class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition def xml(*args) @@ -467,8 +465,8 @@ module ActiveRecord # (2) $12.345.678,12 case data when /^-?\D+[\d,]+\.\d{2}$/ # (1) - data.gsub!(/[^-\d\.]/, '') - when /^-?\D+[\d\.]+,\d{2}$/ # (2) + data.gsub!(/[^-\d.]/, '') + when /^-?\D+[\d.]+,\d{2}$/ # (2) data.gsub!(/[^-\d,]/, '').sub!(/,/, '.') end end @@ -851,6 +849,10 @@ module ActiveRecord execute "DROP INDEX #{quote_table_name(index_name)}" end + def rename_index(table_name, old_name, new_name) + execute "ALTER INDEX #{quote_column_name(old_name)} RENAME TO #{quote_table_name(new_name)}" + end + def index_name_length 63 end @@ -930,7 +932,7 @@ module ActiveRecord PGconn.translate_results = false if PGconn.respond_to?(:translate_results=) # Ignore async_exec and async_query when using postgres-pr. - @async = @config[:allow_concurrency] && @connection.respond_to?(:async_exec) + @async = @connection.respond_to?(:async_exec) # 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 diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index bdd940f3ee..aca91c907d 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -324,9 +324,7 @@ module ActiveRecord assign_to_or_mark_for_destruction(record, attributes, options[:allow_destroy]) unless call_reject_if(association_name, attributes) elsif attributes['id'] - existing_record = self.class.reflect_on_association(association_name).klass.find(attributes['id']) - assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy]) - self.send(association_name.to_s+'=', existing_record) + raise_nested_attributes_record_not_found(association_name, attributes['id']) elsif !reject_new_record?(association_name, attributes) method = "build_#{association_name}" @@ -402,15 +400,12 @@ module ActiveRecord association.build(attributes.except(*UNASSIGNABLE_KEYS)) end - elsif existing_records.count == 0 #Existing record but not yet associated - existing_record = self.class.reflect_on_association(association_name).klass.find(attributes['id']) - association.send(:add_record_to_target_with_callbacks, existing_record) if !association.loaded? && !call_reject_if(association_name, attributes) - assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy]) - 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) if !association.loaded? && !call_reject_if(association_name, attributes) 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 @@ -430,7 +425,7 @@ module ActiveRecord ConnectionAdapters::Column.value_to_boolean(hash['_destroy']) end - # Determines if a new record should be built by checking for + # Determines if a new record should be build by checking for # has_destroy_flag? or if a <tt>:reject_if</tt> proc exists for this # association and evaluates to +true+. def reject_new_record?(association_name, attributes) @@ -446,5 +441,9 @@ module ActiveRecord 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 diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index 5ad440e58d..1fbc8a1d32 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -494,7 +494,7 @@ namespace :railties do end on_skip = Proc.new do |name, migration| - $stderr.puts "WARNING: Migration #{migration.basename} from #{name} has been skipped. Migration with the same name already exists." + puts "NOTE: Migration #{migration.basename} from #{name} has been skipped. Migration with the same name already exists." end on_copy = Proc.new do |name, migration, old_path| diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 04ba5b291e..f129b54f9a 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -379,7 +379,7 @@ module ActiveRecord return [] if string.blank? # always convert table names to downcase as in Oracle quoted table names are in uppercase # ignore raw_sql_ that is used by Oracle adapter as alias for limit/offset subqueries - string.scan(/([a-zA-Z_][\.\w]+).?\./).flatten.map{ |s| s.downcase }.uniq - ['raw_sql_'] + string.scan(/([a-zA-Z_][.\w]+).?\./).flatten.map{ |s| s.downcase }.uniq - ['raw_sql_'] end end diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index d79ef78b4d..6bf698fe97 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -177,7 +177,7 @@ module ActiveRecord distinct = options[:distinct] || distinct if @group_values.any? - execute_grouped_calculation(operation, column_name) + execute_grouped_calculation(operation, column_name, distinct) else execute_simple_calculation(operation, column_name, distinct) end @@ -191,19 +191,23 @@ module ActiveRecord end end + def operation_over_aggregate_column(column, operation, distinct) + operation == 'count' ? column.count(distinct) : column.send(operation) + end + def execute_simple_calculation(operation, column_name, distinct) #:nodoc: column = aggregate_column(column_name) # Postgresql doesn't like ORDER BY when there are no GROUP BY relation = except(:order) - select_value = operation == 'count' ? column.count(distinct) : column.send(operation) + select_value = operation_over_aggregate_column(column, operation, distinct) relation.select_values = [select_value] type_cast_calculated_value(@klass.connection.select_value(relation.to_sql), column_for(column_name), operation) end - def execute_grouped_calculation(operation, column_name) #:nodoc: + def execute_grouped_calculation(operation, column_name, distinct) #:nodoc: group_attr = @group_values.first association = @klass.reflect_on_association(group_attr.to_sym) associated = association && association.macro == :belongs_to # only count belongs_to associations @@ -221,7 +225,7 @@ module ActiveRecord relation = except(:group).group(group) relation.select_values = [ - aggregate_column(column_name).send(operation).as(aggregate_alias), + operation_over_aggregate_column(aggregate_column(column_name), operation, distinct).as(aggregate_alias), "#{group_field} AS #{group_alias}" ] @@ -273,7 +277,7 @@ module ActiveRecord else type_cast_using_column(value, column) end else - value + type_cast_using_column(value, column) end end diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index ede1c8821e..b763e22ec6 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -291,8 +291,8 @@ module ActiveRecord record = where(primary_key.eq(id)).first unless record - conditions = arel.wheres.map { |x| x.value }.join(', ') - conditions = " [WHERE #{conditions}]" if conditions.present? + conditions = arel.where_sql + conditions = " [#{conditions}]" if conditions raise RecordNotFound, "Couldn't find #{@klass.name} with ID=#{id}#{conditions}" end @@ -343,8 +343,11 @@ module ActiveRecord end def column_aliases(join_dependency) - join_dependency.joins.collect{|join| join.column_names_with_alias.collect{|column_name, aliased_name| - "#{connection.quote_table_name join.aliased_table_name}.#{connection.quote_column_name column_name} AS #{aliased_name}"}}.flatten.join(", ") + join_dependency.join_parts.collect { |join_part| + join_part.column_names_with_alias.collect{ |column_name, aliased_name| + "#{connection.quote_table_name join_part.aliased_table_name}.#{connection.quote_column_name column_name} AS #{aliased_name}" + } + }.flatten.join(", ") end def using_limitable_reflections?(reflections) diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 001207514d..59ce76ea42 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -6,7 +6,7 @@ module ActiveRecord extend ActiveSupport::Concern attr_accessor :includes_values, :eager_load_values, :preload_values, - :select_values, :group_values, :order_values, :reorder_flag, :joins_values, :where_values, :having_values, + :select_values, :group_values, :order_values, :joins_values, :where_values, :having_values, :limit_value, :offset_value, :lock_value, :readonly_value, :create_with_value, :from_value def includes(*args) @@ -53,15 +53,6 @@ module ActiveRecord relation end - def reorder(*args) - relation = clone - unless args.blank? - relation.order_values = args - relation.reorder_flag = true - end - relation - end - def joins(*args) relation = clone @@ -230,19 +221,8 @@ module ActiveRecord @implicit_readonly = true unless association_joins.empty? && stashed_association_joins.empty? - to_join = [] - join_dependency.join_associations.each do |association| - if (association_relation = association.relation).is_a?(Array) - to_join << [association_relation.first, association.join_class, association.association_join.first] - to_join << [association_relation.last, association.join_class, association.association_join.last] - else - to_join << [association_relation, association.join_class, association.association_join] - end - end - - to_join.uniq.each do |left, join_class, right| - relation = relation.join(left, join_class).on(*right) + relation = association.join_to(relation) end relation.join(custom_joins) diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb index 9ecdb99bee..dcddc3dac4 100644 --- a/activerecord/lib/active_record/relation/spawn_methods.rb +++ b/activerecord/lib/active_record/relation/spawn_methods.rb @@ -19,20 +19,11 @@ module ActiveRecord end end - (Relation::MULTI_VALUE_METHODS - [:joins, :where, :order]).each do |method| + (Relation::MULTI_VALUE_METHODS - [:joins, :where]).each do |method| value = r.send(:"#{method}_values") merged_relation.send(:"#{method}_values=", merged_relation.send(:"#{method}_values") + value) if value.present? end - order_value = r.order_values - if order_value.present? - if r.reorder_flag - merged_relation.order_values = order_value - else - merged_relation.order_values = merged_relation.order_values + order_value - end - end - merged_relation = merged_relation.joins(r.joins_values) merged_wheres = @where_values diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb index f5331bb8a9..e30b481fe1 100644 --- a/activerecord/lib/active_record/schema_dumper.rb +++ b/activerecord/lib/active_record/schema_dumper.rb @@ -114,9 +114,9 @@ HEADER column.type.to_s end spec[:limit] = column.limit.inspect if column.limit != @types[column.type][:limit] && spec[:type] != 'decimal' - spec[:precision] = column.precision.inspect if !column.precision.nil? - spec[:scale] = column.scale.inspect if !column.scale.nil? - spec[:null] = 'false' if !column.null + spec[:precision] = column.precision.inspect if column.precision + spec[:scale] = column.scale.inspect if column.scale + spec[:null] = 'false' unless column.null spec[:default] = default_string(column.default) if column.has_default? (spec.keys - [:name, :type]).each{ |k| spec[k].insert(0, "#{k.inspect} => ")} spec diff --git a/activerecord/lib/active_record/test_case.rb b/activerecord/lib/active_record/test_case.rb index 31b5a8651b..014a900c71 100644 --- a/activerecord/lib/active_record/test_case.rb +++ b/activerecord/lib/active_record/test_case.rb @@ -36,21 +36,6 @@ module ActiveRecord assert_queries(0, &block) end - def self.use_concurrent_connections - setup :connection_allow_concurrency_setup - teardown :connection_allow_concurrency_teardown - end - - def connection_allow_concurrency_setup - @connection = ActiveRecord::Base.remove_connection - ActiveRecord::Base.establish_connection(@connection.merge({:allow_concurrency => true})) - end - - def connection_allow_concurrency_teardown - ActiveRecord::Base.clear_all_connections! - ActiveRecord::Base.establish_connection(@connection) - end - def with_kcode(kcode) if RUBY_VERSION < '1.9' orig_kcode, $KCODE = $KCODE, kcode diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index a7709b9489..ab737f0f88 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -224,8 +224,8 @@ module ActiveRecord end # See ActiveRecord::Transactions::ClassMethods for detailed documentation. - def transaction(&block) - self.class.transaction(&block) + def transaction(options = {}, &block) + self.class.transaction(options, &block) end def destroy #:nodoc: diff --git a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb index b93e49613d..c7c32da9f3 100644 --- a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb +++ b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb @@ -16,7 +16,7 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase assert_equal 2, authors.size assert_equal 5, authors[0].posts.size assert_equal 1, authors[1].posts.size - assert_equal 9, authors[0].posts.collect{|post| post.comments.size }.inject(0){|sum,i| sum+i} + assert_equal 10, authors[0].posts.collect{|post| post.comments.size }.inject(0){|sum,i| sum+i} end def test_eager_association_loading_with_cascaded_two_levels_and_one_level @@ -24,7 +24,7 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase assert_equal 2, authors.size assert_equal 5, authors[0].posts.size assert_equal 1, authors[1].posts.size - assert_equal 9, authors[0].posts.collect{|post| post.comments.size }.inject(0){|sum,i| sum+i} + assert_equal 10, authors[0].posts.collect{|post| post.comments.size }.inject(0){|sum,i| sum+i} assert_equal 1, authors[0].categorizations.size assert_equal 2, authors[1].categorizations.size end @@ -35,7 +35,7 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase end authors = Author.joins(:posts).eager_load(:comments).where(:posts => {:taggings_count => 1}).all assert_equal 1, assert_no_queries { authors.size } - assert_equal 9, assert_no_queries { authors[0].comments.size } + assert_equal 10, assert_no_queries { authors[0].comments.size } end def test_eager_association_loading_grafts_stashed_associations_to_correct_parent @@ -57,7 +57,7 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase assert_equal 2, authors.size assert_equal 5, authors[0].posts.size assert_equal 1, authors[1].posts.size - assert_equal 9, authors[0].posts.collect{|post| post.comments.size }.inject(0){|sum,i| sum+i} + assert_equal 10, authors[0].posts.collect{|post| post.comments.size }.inject(0){|sum,i| sum+i} end def test_eager_association_loading_with_cascaded_two_levels_and_self_table_reference diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index 40859d425f..2d8e02e398 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -174,7 +174,7 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_eager_association_loading_with_belongs_to comments = Comment.find(:all, :include => :post) - assert_equal 10, comments.length + assert_equal 11, comments.length titles = comments.map { |c| c.post.title } assert titles.include?(posts(:welcome).title) assert titles.include?(posts(:sti_post_and_comments).title) @@ -532,7 +532,7 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_eager_has_many_with_association_inheritance post = Post.find(4, :include => [ :special_comments ]) post.special_comments.each do |special_comment| - assert_equal "SpecialComment", special_comment.class.to_s + assert special_comment.is_a?(SpecialComment) end end @@ -726,8 +726,8 @@ class EagerAssociationTest < ActiveRecord::TestCase posts = assert_queries(2) do Post.find(:all, :joins => :comments, :include => :author, :order => 'comments.id DESC') end - assert_equal posts(:eager_other), posts[0] - assert_equal authors(:mary), assert_no_queries { posts[0].author} + assert_equal posts(:eager_other), posts[1] + assert_equal authors(:mary), assert_no_queries { posts[1].author} end def test_eager_loading_with_conditions_on_joined_table_preloads diff --git a/activerecord/test/cases/associations/inner_join_association_test.rb b/activerecord/test/cases/associations/inner_join_association_test.rb index 4ba867dc7c..780eabc443 100644 --- a/activerecord/test/cases/associations/inner_join_association_test.rb +++ b/activerecord/test/cases/associations/inner_join_association_test.rb @@ -4,9 +4,12 @@ require 'models/comment' require 'models/author' require 'models/category' require 'models/categorization' +require 'models/tagging' +require 'models/tag' class InnerJoinAssociationTest < ActiveRecord::TestCase - fixtures :authors, :posts, :comments, :categories, :categories_posts, :categorizations + fixtures :authors, :posts, :comments, :categories, :categories_posts, :categorizations, + :taggings, :tags def test_construct_finder_sql_applies_aliases_tables_on_association_conditions result = Author.joins(:thinking_posts, :welcome_posts).to_a @@ -62,4 +65,23 @@ class InnerJoinAssociationTest < ActiveRecord::TestCase authors_with_welcoming_post_titles = Author.calculate(:count, 'authors.id', :joins => :posts, :distinct => true, :conditions => "posts.title like 'Welcome%'") assert_equal real_count, authors_with_welcoming_post_titles, "inner join and conditions should have only returned authors posting titles starting with 'Welcome'" end + + def test_find_with_sti_join + scope = Post.joins(:special_comments).where(:id => posts(:sti_comments).id) + + # The join should match SpecialComment and its subclasses only + assert scope.where("comments.type" => "Comment").empty? + assert !scope.where("comments.type" => "SpecialComment").empty? + assert !scope.where("comments.type" => "SubSpecialComment").empty? + end + + def test_find_with_conditions_on_reflection + assert !posts(:welcome).comments.empty? + assert Post.joins(:nonexistant_comments).where(:id => posts(:welcome).id).empty? # [sic!] + end + + def test_find_with_conditions_on_through_reflection + assert !posts(:welcome).tags.empty? + assert Post.joins(:misc_tags).where(:id => posts(:welcome).id).empty? + end end diff --git a/activerecord/test/cases/associations/join_model_test.rb b/activerecord/test/cases/associations/join_model_test.rb index f131dc01f6..7a22ce4dad 100644 --- a/activerecord/test/cases/associations/join_model_test.rb +++ b/activerecord/test/cases/associations/join_model_test.rb @@ -406,7 +406,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase author = Author.find :first, :conditions => ['name = ?', 'David'], :include => :comments, :order => 'comments.id' SpecialComment.new; VerySpecialComment.new assert_no_queries do - assert_equal [1,2,3,5,6,7,8,9,10], author.comments.collect(&:id) + assert_equal [1,2,3,5,6,7,8,9,10,12], author.comments.collect(&:id) end end @@ -508,7 +508,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase def test_has_many_through_collection_size_doesnt_load_target_if_not_loaded author = authors(:david) - assert_equal 9, author.comments.size + assert_equal 10, author.comments.size assert !author.comments.loaded? end diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 16fd9a7465..8acee9ac71 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -48,6 +48,11 @@ class Boolean < ActiveRecord::Base; end class BasicsTest < ActiveRecord::TestCase fixtures :topics, :companies, :developers, :projects, :computers, :accounts, :minimalistics, 'warehouse-things', :authors, :categorizations, :categories, :posts + def test_select_symbol + topic_ids = Topic.select(:id).map(&:id).sort + assert_equal Topic.find(:all).map(&:id).sort, topic_ids + end + def test_table_exists assert !NonExistentTable.table_exists? assert Topic.table_exists? diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb index 7ec40906d4..61fbf01a50 100644 --- a/activerecord/test/cases/calculations_test.rb +++ b/activerecord/test/cases/calculations_test.rb @@ -346,4 +346,13 @@ class CalculationsTest < ActiveRecord::TestCase def test_from_option_with_table_different_than_class assert_equal Account.count(:all), Company.count(:all, :from => 'accounts') end + + def test_distinct_is_honored_when_used_with_count_operation_after_group + # Count the number of authors for approved topics + approved_topics_count = Topic.group(:approved).count(:author_name)[true] + assert_equal approved_topics_count, 3 + # Count the number of distinct authors for approved Topics + distinct_authors_for_approved_count = Topic.group(:approved).count(:author_name, :distinct => true)[true] + assert_equal distinct_authors_for_approved_count, 2 + end end diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index 26b5096255..c058196078 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -259,7 +259,7 @@ class FinderTest < ActiveRecord::TestCase end def test_find_on_association_proxy_conditions - assert_equal [1, 2, 3, 5, 6, 7, 8, 9, 10], Comment.find_all_by_post_id(authors(:david).posts).map(&:id).sort + assert_equal [1, 2, 3, 5, 6, 7, 8, 9, 10, 12], Comment.find_all_by_post_id(authors(:david).posts).map(&:id).sort end def test_find_on_hash_conditions_with_range diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb index e7126964cd..4ddcdc010b 100644 --- a/activerecord/test/cases/locking_test.rb +++ b/activerecord/test/cases/locking_test.rb @@ -257,6 +257,7 @@ unless current_adapter?(:SybaseAdapter, :OpenBaseAdapter) fixtures :people, :readers def setup + Person.connection_pool.clear_reloadable_connections! # Avoid introspection queries during tests. Person.columns; Reader.columns end @@ -306,8 +307,6 @@ unless current_adapter?(:SybaseAdapter, :OpenBaseAdapter) end if current_adapter?(:PostgreSQLAdapter, :OracleAdapter) - use_concurrent_connections - def test_no_locks_no_wait first, second = duel { Person.find 1 } assert first.end > second.end diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index ef949300b0..e6eef805cf 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -422,7 +422,7 @@ if ActiveRecord::Base.connection.supports_migrations? # Sybase, and SQLite3 will not allow you to add a NOT NULL # column to a table without a default value. - unless current_adapter?(:SybaseAdapter, :SQLiteAdapter) + unless current_adapter?(:SybaseAdapter, :SQLite3Adapter) def test_add_column_not_null_without_default Person.connection.create_table :testings do |t| t.column :foo, :string @@ -821,7 +821,7 @@ if ActiveRecord::Base.connection.supports_migrations? end end - if current_adapter?(:SQLiteAdapter) + if current_adapter?(:SQLite3Adapter) def test_rename_table_for_sqlite_should_work_with_reserved_words begin assert_nothing_raised do @@ -1131,7 +1131,7 @@ if ActiveRecord::Base.connection.supports_migrations? # so this happens there too assert_kind_of BigDecimal, b.value_of_e assert_equal BigDecimal("2.7182818284590452353602875"), b.value_of_e - elsif current_adapter?(:SQLiteAdapter) + elsif current_adapter?(:SQLite3Adapter) # - SQLite3 stores a float, in violation of SQL assert_kind_of BigDecimal, b.value_of_e assert_in_delta BigDecimal("2.71828182845905"), b.value_of_e, 0.00000000000001 @@ -1588,7 +1588,7 @@ if ActiveRecord::Base.connection.supports_migrations? end end - if current_adapter?(:PostgreSQLAdapter) || current_adapter?(:SQLiteAdapter) || current_adapter?(:MysqlAdapter) || current_adapter?(:Mysql2Adapter) + if current_adapter?(:PostgreSQLAdapter) || current_adapter?(:SQLite3Adapter) || current_adapter?(:MysqlAdapter) || current_adapter?(:Mysql2Adapter) def test_xml_creates_xml_column type = current_adapter?(:PostgreSQLAdapter) ? 'xml' : :text diff --git a/activerecord/test/cases/nested_attributes_test.rb b/activerecord/test/cases/nested_attributes_test.rb index 8382ca048b..60f89da95e 100644 --- a/activerecord/test/cases/nested_attributes_test.rb +++ b/activerecord/test/cases/nested_attributes_test.rb @@ -203,6 +203,12 @@ class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase assert_equal 'Davy Jones Gold Dagger', @pirate.ship.name end + def test_should_raise_RecordNotFound_if_an_id_is_given_but_doesnt_return_a_record + assert_raise_with_message ActiveRecord::RecordNotFound, "Couldn't find Ship with ID=1234567890 for Pirate with ID=#{@pirate.id}" do + @pirate.ship_attributes = { :id => 1234567890 } + end + end + def test_should_take_a_hash_with_string_keys_and_update_the_associated_model @pirate.reload.ship_attributes = { 'id' => @ship.id, 'name' => 'Davy Jones Gold Dagger' } @@ -382,13 +388,10 @@ class TestNestedAttributesOnABelongsToAssociation < ActiveRecord::TestCase assert_equal 'Arr', @ship.pirate.catchphrase end - def test_should_associate_with_record_if_parent_record_is_not_saved - @ship.destroy - @pirate = Pirate.create(:catchphrase => 'Arr') - @ship = Ship.new(:name => 'Nights Dirty Lightning', :pirate_attributes => { :id => @pirate.id, :catchphrase => @pirate.catchphrase}) - - assert_equal @ship.name, 'Nights Dirty Lightning' - assert_equal @pirate, @ship.pirate + def test_should_raise_RecordNotFound_if_an_id_is_given_but_doesnt_return_a_record + assert_raise_with_message ActiveRecord::RecordNotFound, "Couldn't find Pirate with ID=1234567890 for Ship with ID=#{@ship.id}" do + @ship.pirate_attributes = { :id => 1234567890 } + end end def test_should_take_a_hash_with_string_keys_and_update_the_associated_model @@ -518,11 +521,6 @@ module NestedAttributesOnACollectionAssociationTests assert_equal ['Grace OMalley', 'Privateers Greed'], [@child_1.reload.name, @child_2.reload.name] end - def test_should_assign_existing_children_if_parent_is_new - @pirate = Pirate.new({:catchphrase => "Don' botharr talkin' like one, savvy?"}.merge(@alternate_params)) - assert_equal ['Grace OMalley', 'Privateers Greed'], [@pirate.send(@association_name)[0].name, @pirate.send(@association_name)[1].name] - end - def test_should_also_work_with_a_HashWithIndifferentAccess @pirate.send(association_setter, HashWithIndifferentAccess.new('foo' => HashWithIndifferentAccess.new(:id => @child_1.id, :name => 'Grace OMalley'))) @pirate.save @@ -586,8 +584,8 @@ module NestedAttributesOnACollectionAssociationTests assert_equal ['Grace OMalley', 'Privateers Greed'], [@child_1.name, @child_2.name] end - def test_should_not_raise_RecordNotFound_if_an_id_is_given_but_doesnt_return_a_record - assert_nothing_raised ActiveRecord::RecordNotFound do + def test_should_raise_RecordNotFound_if_an_id_is_given_but_doesnt_return_a_record + assert_raise_with_message ActiveRecord::RecordNotFound, "Couldn't find #{@child_1.class.name} with ID=1234567890 for Pirate with ID=#{@pirate.id}" do @pirate.attributes = { association_getter => [{ :id => 1234567890 }] } end end diff --git a/activerecord/test/cases/quoting_test.rb b/activerecord/test/cases/quoting_test.rb new file mode 100644 index 0000000000..2ef5b5a800 --- /dev/null +++ b/activerecord/test/cases/quoting_test.rb @@ -0,0 +1,220 @@ +require "cases/helper" + +module ActiveRecord + module ConnectionAdapters + class QuotingTest < ActiveRecord::TestCase + class FakeColumn < ActiveRecord::ConnectionAdapters::Column + attr_accessor :type + + def initialize type + @type = type + end + end + + def setup + @quoter = Class.new { include Quoting }.new + end + + def test_quoted_true + assert_equal "'t'", @quoter.quoted_true + end + + def test_quoted_false + assert_equal "'f'", @quoter.quoted_false + end + + def test_quote_column_name + assert_equal "foo", @quoter.quote_column_name('foo') + end + + def test_quote_table_name + assert_equal "foo", @quoter.quote_table_name('foo') + end + + def test_quote_table_name_calls_quote_column_name + @quoter.extend(Module.new { + def quote_column_name(string) + 'lol' + end + }) + assert_equal 'lol', @quoter.quote_table_name('foo') + end + + def test_quote_string + assert_equal "''", @quoter.quote_string("'") + assert_equal "\\\\", @quoter.quote_string("\\") + assert_equal "hi''i", @quoter.quote_string("hi'i") + assert_equal "hi\\\\i", @quoter.quote_string("hi\\i") + end + + def test_quoted_date + t = Date.today + assert_equal t.to_s(:db), @quoter.quoted_date(t) + end + + def test_quoted_time_utc + before = ActiveRecord::Base.default_timezone + ActiveRecord::Base.default_timezone = :utc + t = Time.now + assert_equal t.getutc.to_s(:db), @quoter.quoted_date(t) + ensure + ActiveRecord::Base.default_timezone = before + end + + def test_quoted_time_local + before = ActiveRecord::Base.default_timezone + ActiveRecord::Base.default_timezone = :local + t = Time.now + assert_equal t.getlocal.to_s(:db), @quoter.quoted_date(t) + ensure + ActiveRecord::Base.default_timezone = before + end + + def test_quoted_time_crazy + before = ActiveRecord::Base.default_timezone + ActiveRecord::Base.default_timezone = :asdfasdf + t = Time.now + assert_equal t.getlocal.to_s(:db), @quoter.quoted_date(t) + ensure + ActiveRecord::Base.default_timezone = before + end + + def test_quoted_datetime_utc + before = ActiveRecord::Base.default_timezone + ActiveRecord::Base.default_timezone = :utc + t = DateTime.now + assert_equal t.getutc.to_s(:db), @quoter.quoted_date(t) + ensure + ActiveRecord::Base.default_timezone = before + end + + ### + # DateTime doesn't define getlocal, so make sure it does nothing + def test_quoted_datetime_local + before = ActiveRecord::Base.default_timezone + ActiveRecord::Base.default_timezone = :local + t = DateTime.now + assert_equal t.to_s(:db), @quoter.quoted_date(t) + ensure + ActiveRecord::Base.default_timezone = before + end + + def test_quote_with_quoted_id + assert_equal 1, @quoter.quote(Struct.new(:quoted_id).new(1), nil) + assert_equal 1, @quoter.quote(Struct.new(:quoted_id).new(1), 'foo') + end + + def test_quote_nil + assert_equal 'NULL', @quoter.quote(nil, nil) + assert_equal 'NULL', @quoter.quote(nil, 'foo') + end + + def test_quote_true + assert_equal @quoter.quoted_true, @quoter.quote(true, nil) + assert_equal '1', @quoter.quote(true, Struct.new(:type).new(:integer)) + end + + def test_quote_false + assert_equal @quoter.quoted_false, @quoter.quote(false, nil) + assert_equal '0', @quoter.quote(false, Struct.new(:type).new(:integer)) + end + + def test_quote_float + float = 1.2 + assert_equal float.to_s, @quoter.quote(float, nil) + assert_equal float.to_s, @quoter.quote(float, Object.new) + end + + def test_quote_fixnum + fixnum = 1 + assert_equal fixnum.to_s, @quoter.quote(fixnum, nil) + assert_equal fixnum.to_s, @quoter.quote(fixnum, Object.new) + end + + def test_quote_bignum + bignum = 1 << 100 + assert_equal bignum.to_s, @quoter.quote(bignum, nil) + assert_equal bignum.to_s, @quoter.quote(bignum, Object.new) + end + + def test_quote_bigdecimal + bigdec = BigDecimal.new((1 << 100).to_s) + assert_equal bigdec.to_s('F'), @quoter.quote(bigdec, nil) + assert_equal bigdec.to_s('F'), @quoter.quote(bigdec, Object.new) + end + + def test_dates_and_times + @quoter.extend(Module.new { def quoted_date(value) 'lol' end }) + assert_equal "'lol'", @quoter.quote(Date.today, nil) + assert_equal "'lol'", @quoter.quote(Date.today, Object.new) + assert_equal "'lol'", @quoter.quote(Time.now, nil) + assert_equal "'lol'", @quoter.quote(Time.now, Object.new) + assert_equal "'lol'", @quoter.quote(DateTime.now, nil) + assert_equal "'lol'", @quoter.quote(DateTime.now, Object.new) + end + + def test_crazy_object + crazy = Class.new { def to_s; 'lol' end }.new + assert_equal "'lol'", @quoter.quote(crazy, nil) + assert_equal "'lol'", @quoter.quote(crazy, Object.new) + end + + def test_crazy_object_calls_quote_string + crazy = Class.new { def to_s; 'lo\l' end }.new + assert_equal "'lo\\\\l'", @quoter.quote(crazy, nil) + assert_equal "'lo\\\\l'", @quoter.quote(crazy, Object.new) + end + + def test_quote_string_no_column + assert_equal "'lo\\\\l'", @quoter.quote('lo\l', nil) + end + + def test_quote_as_mb_chars_no_column + string = ActiveSupport::Multibyte::Chars.new('lo\l') + assert_equal "'lo\\\\l'", @quoter.quote(string, nil) + end + + def test_quote_string_int_column + assert_equal "1", @quoter.quote('1', FakeColumn.new(:integer)) + assert_equal "1", @quoter.quote('1.2', FakeColumn.new(:integer)) + end + + def test_quote_string_float_column + assert_equal "1.0", @quoter.quote('1', FakeColumn.new(:float)) + assert_equal "1.2", @quoter.quote('1.2', FakeColumn.new(:float)) + end + + def test_quote_as_mb_chars_binary_column + string = ActiveSupport::Multibyte::Chars.new('lo\l') + assert_equal "'lo\\\\l'", @quoter.quote(string, FakeColumn.new(:binary)) + end + + def test_quote_binary_without_string_to_binary + assert_equal "'lo\\\\l'", @quoter.quote('lo\l', FakeColumn.new(:binary)) + end + + def test_quote_binary_with_string_to_binary + col = Class.new(FakeColumn) { + def string_to_binary(value) + 'foo' + end + }.new(:binary) + assert_equal "'foo'", @quoter.quote('lo\l', col) + end + + def test_quote_as_mb_chars_binary_column_with_string_to_binary + col = Class.new(FakeColumn) { + def string_to_binary(value) + 'foo' + end + }.new(:binary) + string = ActiveSupport::Multibyte::Chars.new('lo\l') + assert_equal "'foo'", @quoter.quote(string, col) + end + + def test_string_with_crazy_column + assert_equal "'lo\\\\l'", @quoter.quote('lo\l', FakeColumn.new(:foo)) + end + end + end +end diff --git a/activerecord/test/cases/relation_scoping_test.rb b/activerecord/test/cases/relation_scoping_test.rb index 090e6c8bc9..64365c1d75 100644 --- a/activerecord/test/cases/relation_scoping_test.rb +++ b/activerecord/test/cases/relation_scoping_test.rb @@ -384,18 +384,12 @@ class DefaultScopingTest < ActiveRecord::TestCase assert_equal expected, received end - def test_reorder_overrides_default_scope_order + def test_except_and_order_overrides_default_scope_order expected = Developer.order('name DESC').collect { |dev| dev.name } - received = DeveloperOrderedBySalary.reorder('name DESC').collect { |dev| dev.name } + received = DeveloperOrderedBySalary.except(:order).order('name DESC').collect { |dev| dev.name } assert_equal expected, received end - def test_reordered_scope_overrides_default_scope_order - not_expected = DeveloperOrderedBySalary.first # Jamis -> name DESC - received = DeveloperOrderedBySalary.reordered_by_name.first # David -> name - assert not_expected.id != received.id - end - def test_nested_exclusive_scope expected = Developer.find(:all, :limit => 100).collect { |dev| dev.salary } received = DeveloperOrderedBySalary.send(:with_exclusive_scope, :find => { :limit => 100 }) do diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index d642aeed8b..222819cbe5 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -135,12 +135,6 @@ class RelationTest < ActiveRecord::TestCase assert_equal topics(:fourth).title, topics.first.title end - def test_finding_with_reorder - topics = Topic.order('author_name').order('title').reorder('id') - assert_equal 4, topics.to_a.size - assert_equal topics(:first).title, topics.first.title - end - def test_finding_with_order_and_take entrants = Entrant.order("id ASC").limit(2).to_a diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index 66446b6b7e..9b2c7c00df 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -100,7 +100,7 @@ class SchemaDumperTest < ActiveRecord::TestCase assert_match %r{c_int_4.*}, output assert_no_match %r{c_int_4.*:limit}, output - elsif current_adapter?(:SQLiteAdapter) + elsif current_adapter?(:SQLite3Adapter) assert_match %r{c_int_1.*:limit => 1}, output assert_match %r{c_int_2.*:limit => 2}, output assert_match %r{c_int_3.*:limit => 3}, output @@ -109,7 +109,7 @@ class SchemaDumperTest < ActiveRecord::TestCase assert_match %r{c_int_without_limit.*}, output assert_no_match %r{c_int_without_limit.*:limit}, output - if current_adapter?(:SQLiteAdapter) + if current_adapter?(:SQLite3Adapter) assert_match %r{c_int_5.*:limit => 5}, output assert_match %r{c_int_6.*:limit => 6}, output assert_match %r{c_int_7.*:limit => 7}, output diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb index 9255190613..0fbcef4091 100644 --- a/activerecord/test/cases/transactions_test.rb +++ b/activerecord/test/cases/transactions_test.rb @@ -263,6 +263,27 @@ class TransactionTest < ActiveRecord::TestCase assert !@second.reload.approved? end if Topic.connection.supports_savepoints? + def test_force_savepoint_on_instance + @first.transaction do + @first.approved = true + @second.approved = false + @first.save! + @second.save! + + begin + @second.transaction :requires_new => true do + @first.happy = false + @first.save! + raise + end + rescue + end + end + + assert @first.reload.approved? + assert !@second.reload.approved? + end if Topic.connection.supports_savepoints? + def test_no_savepoint_in_nested_transaction_without_force Topic.transaction do @first.approved = true @@ -399,7 +420,7 @@ class TransactionTest < ActiveRecord::TestCase end def test_sqlite_add_column_in_transaction - return true unless current_adapter?(:SQLite3Adapter, :SQLiteAdapter) + return true unless current_adapter?(:SQLite3Adapter) # Test first if column creation/deletion works correctly when no # transaction is in place. @@ -529,8 +550,6 @@ end if Topic.connection.supports_savepoints? if current_adapter?(:PostgreSQLAdapter) class ConcurrentTransactionTest < TransactionTest - use_concurrent_connections - # This will cause transactions to overlap and fail unless they are performed on # separate database connections. def test_transaction_per_thread diff --git a/activerecord/test/fixtures/comments.yml b/activerecord/test/fixtures/comments.yml index 97d77f8b9a..ddbb823c49 100644 --- a/activerecord/test/fixtures/comments.yml +++ b/activerecord/test/fixtures/comments.yml @@ -57,3 +57,9 @@ eager_other_comment1: post_id: 7 body: go crazy type: SpecialComment + +sub_special_comment: + id: 12 + post_id: 4 + body: Sub special comment + type: SubSpecialComment diff --git a/activerecord/test/models/comment.rb b/activerecord/test/models/comment.rb index 9f6e2d3b71..88061b2145 100644 --- a/activerecord/test/models/comment.rb +++ b/activerecord/test/models/comment.rb @@ -23,6 +23,9 @@ class SpecialComment < Comment end end +class SubSpecialComment < SpecialComment +end + class VerySpecialComment < Comment def self.what_are_you 'a very special comment...' diff --git a/activerecord/test/models/developer.rb b/activerecord/test/models/developer.rb index 947583af76..32d060cf09 100644 --- a/activerecord/test/models/developer.rb +++ b/activerecord/test/models/developer.rb @@ -88,7 +88,6 @@ class DeveloperOrderedBySalary < ActiveRecord::Base self.table_name = 'developers' default_scope :order => 'salary DESC' scope :by_name, order('name DESC') - scope :reordered_by_name, reorder('name') def self.all_ordered_by_name with_scope(:find => { :order => 'name DESC' }) do |