diff options
author | Pratik Naik <pratiknaik@gmail.com> | 2009-11-17 22:47:23 +0000 |
---|---|---|
committer | Pratik Naik <pratiknaik@gmail.com> | 2009-11-17 22:47:23 +0000 |
commit | 5446d5cb05b50a9a3f317ded774be438e0eff909 (patch) | |
tree | 6b0b87efe3e95783763208215a3159fb63217a6d /activerecord/lib | |
parent | 9754debb9a72f9385950e5282f3642b995ab76d8 (diff) | |
parent | f8877d4b2a2a6f68770b376f0b1391a6295f62f2 (diff) | |
download | rails-5446d5cb05b50a9a3f317ded774be438e0eff909.tar.gz rails-5446d5cb05b50a9a3f317ded774be438e0eff909.tar.bz2 rails-5446d5cb05b50a9a3f317ded774be438e0eff909.zip |
Merge remote branch 'mainstream/master'
Conflicts:
activesupport/lib/active_support/core_ext/hash/conversions.rb
Diffstat (limited to 'activerecord/lib')
44 files changed, 1141 insertions, 1035 deletions
diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index 2f1e7573d8..8195e78826 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -23,14 +23,13 @@ activesupport_path = "#{File.dirname(__FILE__)}/../../activesupport/lib" $:.unshift(activesupport_path) if File.directory?(activesupport_path) -require 'active_support' -begin - require 'active_model' -rescue LoadError - $:.unshift "#{File.dirname(__FILE__)}/../../activemodel/lib" - require 'active_model' -end +activemodel_path = "#{File.dirname(__FILE__)}/../../activemodel/lib" +$:.unshift(activemodel_path) if File.directory?(activemodel_path) + +require 'active_support' +require 'active_model' +require 'arel' module ActiveRecord # TODO: Review explicit loads to see if they will automatically be handled by the initializer. @@ -47,7 +46,9 @@ module ActiveRecord autoload :AssociationPreload, 'active_record/association_preload' autoload :Associations, 'active_record/associations' autoload :AttributeMethods, 'active_record/attribute_methods' + autoload :Attributes, 'active_record/attributes' autoload :AutosaveAssociation, 'active_record/autosave_association' + autoload :Relation, 'active_record/relation' autoload :Base, 'active_record/base' autoload :Batches, 'active_record/batches' autoload :Calculations, 'active_record/calculations' @@ -69,7 +70,7 @@ module ActiveRecord autoload :TestCase, 'active_record/test_case' autoload :Timestamp, 'active_record/timestamp' autoload :Transactions, 'active_record/transactions' - autoload :Validator, 'active_record/validator' + autoload :Types, 'active_record/types' autoload :Validations, 'active_record/validations' module AttributeMethods @@ -82,6 +83,20 @@ module ActiveRecord autoload :Write, 'active_record/attribute_methods/write' end + module Attributes + autoload :Aliasing, 'active_record/attributes/aliasing' + autoload :Store, 'active_record/attributes/store' + autoload :Typecasting, 'active_record/attributes/typecasting' + end + + module Type + autoload :Number, 'active_record/types/number' + autoload :Object, 'active_record/types/object' + autoload :Serialize, 'active_record/types/serialize' + autoload :TimeWithZone, 'active_record/types/time_with_zone' + autoload :Unknown, 'active_record/types/unknown' + end + module Locking autoload :Optimistic, 'active_record/locking/optimistic' autoload :Pessimistic, 'active_record/locking/pessimistic' @@ -92,4 +107,5 @@ module ActiveRecord end end +Arel::Table.engine = Arel::Sql::Engine.new(ActiveRecord::Base) I18n.load_path << File.dirname(__FILE__) + '/active_record/locale/en.yml' diff --git a/activerecord/lib/active_record/association_preload.rb b/activerecord/lib/active_record/association_preload.rb index e41fda7a4b..9f7b2a60b2 100644 --- a/activerecord/lib/active_record/association_preload.rb +++ b/activerecord/lib/active_record/association_preload.rb @@ -1,3 +1,6 @@ +require 'active_support/core_ext/array/wrap' +require 'active_support/core_ext/enumerable' + module ActiveRecord # See ActiveRecord::AssociationPreload::ClassMethods for documentation. module AssociationPreload #:nodoc: @@ -82,7 +85,7 @@ module ActiveRecord # only one level deep in the +associations+ argument, i.e. it's not passed # to the child associations when +associations+ is a Hash. def preload_associations(records, associations, preload_options={}) - records = [records].flatten.compact.uniq + records = Array.wrap(records).compact.uniq return if records.empty? case associations when Array then associations.each {|association| preload_associations(records, association, preload_options)} @@ -92,7 +95,7 @@ module ActiveRecord raise "parent must be an association name" unless parent.is_a?(String) || parent.is_a?(Symbol) preload_associations(records, parent, preload_options) reflection = reflections[parent] - parents = records.map {|record| record.send(reflection.name)}.flatten.compact + parents = records.sum { |record| Array.wrap(record.send(reflection.name)) } unless parents.empty? parents.first.class.preload_associations(parents, child) end @@ -123,7 +126,8 @@ module ActiveRecord parent_records.each do |parent_record| association_proxy = parent_record.send(reflection_name) association_proxy.loaded - association_proxy.target.push(*[associated_record].flatten) + association_proxy.target.push *Array.wrap(associated_record) + association_proxy.__send__(:set_inverse_instance, associated_record, parent_record) end end @@ -254,6 +258,7 @@ module ActiveRecord through_reflection = reflections[through_association] through_primary_key = through_reflection.primary_key_name + through_records = [] if reflection.options[:source_type] interface = reflection.source_reflection.options[:foreign_type] preload_options = {:conditions => ["#{connection.quote_column_name interface} = ?", reflection.options[:source_type]]} @@ -262,23 +267,22 @@ module ActiveRecord records.first.class.preload_associations(records, through_association, preload_options) # Dont cache the association - we would only be caching a subset - through_records = [] records.each do |record| proxy = record.send(through_association) if proxy.respond_to?(:target) - through_records << proxy.target + through_records.concat Array.wrap(proxy.target) proxy.reset else # this is a has_one :through reflection through_records << proxy if proxy end end - through_records.flatten! else records.first.class.preload_associations(records, through_association) - through_records = records.map {|record| record.send(through_association)}.flatten + records.each do |record| + through_records.concat Array.wrap(record.send(through_association)) + end end - through_records.compact! through_records end diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 5cce1c8736..0539aa4db2 100755 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -438,7 +438,7 @@ module ActiveRecord # @group.users.collect { |u| u.avatar }.flatten # select all avatars for all users in the group # @group.avatars # selects all avatars by going through the User join model. # - # An important caveat with going through +has_one+ or +has_many+ associations on the join model is that these associations are + # An important caveat with going through +has_one+ or +has_many+ associations on the join model is that these associations are # *read-only*. For example, the following would not work following the previous example: # # @group.avatars << Avatar.new # this would work if User belonged_to Avatar rather than the other way around. @@ -544,14 +544,14 @@ module ActiveRecord # # Since only one table is loaded at a time, conditions or orders cannot reference tables other than the main one. If this is the case # Active Record falls back to the previously used LEFT OUTER JOIN based strategy. For example - # + # # Post.find(:all, :include => [ :author, :comments ], :conditions => ['comments.approved = ?', true]) # # This will result in a single SQL query with joins along the lines of: <tt>LEFT OUTER JOIN comments ON comments.post_id = posts.id</tt> and # <tt>LEFT OUTER JOIN authors ON authors.id = posts.author_id</tt>. Note that using conditions like this can have unintended consequences. # In the above example posts with no approved comments are not returned at all, because the conditions apply to the SQL statement as a whole # and not just to the association. You must disambiguate column references for this fallback to happen, for example - # <tt>:order => "author.name DESC"</tt> will work but <tt>:order => "name DESC"</tt> will not. + # <tt>:order => "author.name DESC"</tt> will work but <tt>:order => "name DESC"</tt> will not. # # If you do want eager load only some members of an association it is usually more natural to <tt>:include</tt> an association # which has conditions defined on it: @@ -588,7 +588,7 @@ module ActiveRecord # This will execute one query to load the addresses and load the addressables with one query per addressable type. # For example if all the addressables are either of class Person or Company then a total of 3 queries will be executed. The list of # addressable types to load is determined on the back of the addresses loaded. This is not supported if Active Record has to fallback - # to the previous implementation of eager loading and will raise ActiveRecord::EagerLoadPolymorphicError. The reason is that the parent + # to the previous implementation of eager loading and will raise ActiveRecord::EagerLoadPolymorphicError. The reason is that the parent # model's type is a column value so its corresponding table name cannot be put in the +FROM+/+JOIN+ clauses of that query. # # == Table Aliasing @@ -697,7 +697,7 @@ module ActiveRecord # d.level = 10 # d.level == t.dungeon.level # => false # - # The +Dungeon+ instances +d+ and <tt>t.dungeon</tt> in the above example refer to the same object data from the database, but are + # The +Dungeon+ instances +d+ and <tt>t.dungeon</tt> in the above example refer to the same object data from the database, but are # actually different in-memory copies of that data. Specifying the <tt>:inverse_of</tt> option on associations lets you tell # +ActiveRecord+ about inverse relationships and it will optimise object loading. For example, if we changed our model definitions to: # @@ -864,8 +864,8 @@ module ActiveRecord # [:autosave] # If true, always save any loaded members and destroy members marked for destruction, when saving the parent object. Off by default. # [:inverse_of] - # Specifies the name of the <tt>belongs_to</tt> association on the associated object that is the inverse of this <tt>has_many</tt> - # association. Does not work in combination with <tt>:through</tt> or <tt>:as</tt> options. + # Specifies the name of the <tt>belongs_to</tt> association on the associated object that is the inverse of this <tt>has_many</tt> + # association. Does not work in combination with <tt>:through</tt> or <tt>:as</tt> options. # See ActiveRecord::Associations::ClassMethods's overview on Bi-directional assocations for more detail. # # Option examples: @@ -963,7 +963,7 @@ module ActiveRecord # but not include the joined columns. Do not forget to include the primary and foreign keys, otherwise it will raise an error. # [:through] # Specifies a Join Model through which to perform the query. Options for <tt>:class_name</tt> and <tt>:foreign_key</tt> - # are ignored, as the association uses the source reflection. You can only use a <tt>:through</tt> query through a + # are ignored, as the association uses the source reflection. You can only use a <tt>:through</tt> query through a # <tt>has_one</tt> or <tt>belongs_to</tt> association on the join model. # [:source] # Specifies the source association name used by <tt>has_one :through</tt> queries. Only use it if the name cannot be @@ -979,8 +979,8 @@ module ActiveRecord # [:autosave] # If true, always save the associated object or destroy it if marked for destruction, when saving the parent object. Off by default. # [:inverse_of] - # Specifies the name of the <tt>belongs_to</tt> association on the associated object that is the inverse of this <tt>has_one</tt> - # association. Does not work in combination with <tt>:through</tt> or <tt>:as</tt> options. + # Specifies the name of the <tt>belongs_to</tt> association on the associated object that is the inverse of this <tt>has_one</tt> + # association. Does not work in combination with <tt>:through</tt> or <tt>:as</tt> options. # See ActiveRecord::Associations::ClassMethods's overview on Bi-directional assocations for more detail. # # Option examples: @@ -1084,8 +1084,8 @@ module ActiveRecord # If true, the associated object will be touched (the updated_at/on attributes set to now) when this record is either saved or # destroyed. If you specify a symbol, that attribute will be updated with the current time instead of the updated_at/on attribute. # [:inverse_of] - # Specifies the name of the <tt>has_one</tt> or <tt>has_many</tt> association on the associated object that is the inverse of this <tt>belongs_to</tt> - # association. Does not work in combination with the <tt>:polymorphic</tt> options. + # Specifies the name of the <tt>has_one</tt> or <tt>has_many</tt> association on the associated object that is the inverse of this <tt>belongs_to</tt> + # association. Does not work in combination with the <tt>:polymorphic</tt> options. # See ActiveRecord::Associations::ClassMethods's overview on Bi-directional assocations for more detail. # # Option examples: @@ -1227,8 +1227,8 @@ module ActiveRecord # the association will use "project_id" as the default <tt>:association_foreign_key</tt>. # [:conditions] # Specify the conditions that the associated object must meet in order to be included as a +WHERE+ - # SQL fragment, such as <tt>authorized = 1</tt>. Record creations from the association are scoped if a hash is used. - # <tt>has_many :posts, :conditions => {:published => true}</tt> will create published posts with <tt>@blog.posts.create</tt> + # SQL fragment, such as <tt>authorized = 1</tt>. Record creations from the association are scoped if a hash is used. + # <tt>has_many :posts, :conditions => {:published => true}</tt> will create published posts with <tt>@blog.posts.create</tt> # or <tt>@blog.posts.build</tt>. # [:order] # Specify the order in which the associated objects are returned as an <tt>ORDER BY</tt> SQL fragment, @@ -1441,12 +1441,12 @@ module ActiveRecord "#{reflection.class_name}.send(:attr_readonly,\"#{cache_column}\".intern) if defined?(#{reflection.class_name}) && #{reflection.class_name}.respond_to?(:attr_readonly)" ) end - + def add_touch_callbacks(reflection, touch_attribute) method_name = "belongs_to_touch_after_save_or_destroy_for_#{reflection.name}".to_sym define_method(method_name) do association = send(reflection.name) - + if touch_attribute == true association.touch unless association.nil? else @@ -1457,9 +1457,9 @@ module ActiveRecord after_destroy(method_name) end - def find_with_associations(options = {}) + def find_with_associations(options = {}, join_dependency = nil) catch :invalid_query do - join_dependency = JoinDependency.new(self, merge_includes(scope(:find, :include), options[:include]), options[:joins]) + join_dependency ||= JoinDependency.new(self, merge_includes(scope(:find, :include), options[:include]), options[:joins]) rows = select_all_rows(options, join_dependency) return join_dependency.instantiate(rows) end @@ -1479,7 +1479,7 @@ module ActiveRecord if reflection.options.include?(:dependent) # Add polymorphic type if the :as option is present dependent_conditions = [] - dependent_conditions << "#{reflection.primary_key_name} = \#{record.quoted_id}" + dependent_conditions << "#{reflection.primary_key_name} = \#{record.#{reflection.name}.send(:owner_quoted_id)}" dependent_conditions << "#{reflection.options[:as]}_type = '#{base_class.name}'" if reflection.options[:as] dependent_conditions << sanitize_sql(reflection.options[:conditions], reflection.quoted_table_name) if reflection.options[:conditions] dependent_conditions << extra_conditions if extra_conditions @@ -1687,19 +1687,6 @@ module ActiveRecord reflection end - def reflect_on_included_associations(associations) - [ associations ].flatten.collect { |association| reflect_on_association(association.to_s.intern) } - end - - def guard_against_unlimitable_reflections(reflections, options) - if (options[:offset] || options[:limit]) && !using_limitable_reflections?(reflections) - raise( - ConfigurationError, - "You can not use offset and limit together with has_many or has_and_belongs_to_many associations" - ) - end - end - def select_all_rows(options, join_dependency) connection.select_all( construct_finder_sql_with_included_associations(options, join_dependency), @@ -1707,82 +1694,67 @@ module ActiveRecord ) end - def construct_finder_sql_with_included_associations(options, join_dependency) + def construct_finder_arel_with_included_associations(options, join_dependency) scope = scope(:find) - sql = "SELECT #{column_aliases(join_dependency)} FROM #{(scope && scope[:from]) || options[:from] || quoted_table_name} " - sql << join_dependency.join_associations.collect{|join| join.association_join }.join - add_joins!(sql, options[:joins], scope) - add_conditions!(sql, options[:conditions], scope) - add_limited_ids_condition!(sql, options, join_dependency) if !using_limitable_reflections?(join_dependency.reflections) && ((scope && scope[:limit]) || options[:limit]) + relation = arel_table((scope && scope[:from]) || options[:from]) - add_group!(sql, options[:group], options[:having], scope) - add_order!(sql, options[:order], scope) - add_limit!(sql, options, scope) if using_limitable_reflections?(join_dependency.reflections) - add_lock!(sql, options, scope) + for association in join_dependency.join_associations + relation = association.join_relation(relation) + end - return sanitize_sql(sql) + relation = relation.joins(construct_join(options[:joins], scope)). + select(column_aliases(join_dependency)). + group(construct_group(options[:group], options[:having], scope)). + order(construct_order(options[:order], scope)). + conditions(construct_conditions(options[:conditions], scope)) + + relation = relation.conditions(construct_arel_limited_ids_condition(options, join_dependency)) if !using_limitable_reflections?(join_dependency.reflections) && ((scope && scope[:limit]) || options[:limit]) + relation = relation.limit(construct_limit(options[:limit], scope)) if using_limitable_reflections?(join_dependency.reflections) + + relation end - def add_limited_ids_condition!(sql, options, join_dependency) - unless (id_list = select_limited_ids_list(options, join_dependency)).empty? - sql << "#{condition_word(sql)} #{connection.quote_table_name table_name}.#{primary_key} IN (#{id_list}) " - else + def construct_finder_sql_with_included_associations(options, join_dependency) + construct_finder_arel_with_included_associations(options, join_dependency).to_sql + end + + def construct_arel_limited_ids_condition(options, join_dependency) + if (ids_array = select_limited_ids_array(options, join_dependency)).empty? throw :invalid_query + else + Arel::Predicates::In.new( + Arel::SqlLiteral.new("#{connection.quote_table_name table_name}.#{primary_key}"), + ids_array + ) end end - def select_limited_ids_list(options, join_dependency) - pk = columns_hash[primary_key] - + def select_limited_ids_array(options, join_dependency) connection.select_all( construct_finder_sql_for_association_limiting(options, join_dependency), "#{name} Load IDs For Limited Eager Loading" - ).collect { |row| connection.quote(row[primary_key], pk) }.join(", ") + ).collect { |row| row[primary_key] } end def construct_finder_sql_for_association_limiting(options, join_dependency) - scope = scope(:find) - - # Only join tables referenced in order or conditions since this is particularly slow on the pre-query. - tables_from_conditions = conditions_tables(options) - tables_from_order = order_tables(options) - all_tables = tables_from_conditions + tables_from_order - distinct_join_associations = all_tables.uniq.map{|table| - 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 + scope = scope(:find) - 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}", order) - else - sql << primary_key - end - sql << " FROM #{connection.quote_table_name table_name} " + relation = arel_table(options[:from]) - if is_distinct - sql << distinct_join_associations.collect { |assoc| assoc.association_join }.join - add_joins!(sql, options[:joins], scope) + for association in join_dependency.join_associations + relation = association.join_relation(relation) end - add_conditions!(sql, options[:conditions], scope) - add_group!(sql, options[:group], options[:having], scope) - - if order && is_distinct - connection.add_order_by_for_association_limiting!(sql, :order => order) - else - add_order!(sql, options[:order], scope) - end + relation = relation.joins(construct_join(options[:joins], scope)). + conditions(construct_conditions(options[:conditions], scope)). + group(construct_group(options[:group], options[:having], scope)). + order(construct_order(options[:order], scope)). + limit(construct_limit(options[:limit], scope)). + offset(construct_limit(options[:offset], scope)). + select(connection.distinct("#{connection.quote_table_name table_name}.#{primary_key}", construct_order(options[:order], scope(:find)).join(","))) - add_limit!(sql, options, scope) - - return sanitize_sql(sql) + relation.to_sql end def tables_in_string(string) @@ -1836,7 +1808,7 @@ module ActiveRecord if array_of_strings?(merged_joins) tables_in_string(merged_joins.join(' ')) else - join_dependency = ActiveRecord::Associations::ClassMethods::InnerJoinDependency.new(self, merged_joins, nil) + join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(self, merged_joins, nil) join_dependency.join_associations.collect {|join_association| [join_association.aliased_join_table_name, join_association.aliased_table_name]}.flatten.compact end else @@ -1886,10 +1858,6 @@ module ActiveRecord end end - def condition_word(sql) - sql =~ /where/i ? " AND " : "WHERE " - end - def create_extension_modules(association_id, block_extension, extensions) if block_extension extension_module_name = "#{self.to_s.demodulize}#{association_id.to_s.camelize}AssociationExtension" @@ -1953,37 +1921,22 @@ module ActiveRecord reflection = base.reflections[name] is_collection = [:has_many, :has_and_belongs_to_many].include?(reflection.macro) - parent_records = records.map do |record| - descendant = record.send(reflection.name) - next unless descendant - descendant.target.uniq! if is_collection - descendant - end.flatten.compact + parent_records = [] + records.each do |record| + if descendant = record.send(reflection.name) + if is_collection + parent_records.concat descendant.target.uniq + else + parent_records << descendant + end + end + end remove_duplicate_results!(reflection.klass, parent_records, associations[name]) unless parent_records.empty? end end end - def join_for_table_name(table_name) - 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) - join = join_for_table_name(table_name) - result = nil - if join && join.is_a?(JoinAssociation) - result = [join] - if join.parent && join.parent.is_a?(JoinAssociation) - result = joins_for_table_name(join.parent.aliased_table_name) + - result - end - end - result - end - protected def build(associations, parent = nil) parent ||= @joins.last @@ -2007,7 +1960,6 @@ module ActiveRecord end end - # overridden in InnerJoinDependency subclass def build_join_association(reflection, parent) JoinAssociation.new(reflection, self, parent) end @@ -2132,6 +2084,7 @@ module ActiveRecord @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 if reflection.macro == :has_and_belongs_to_many @aliased_join_table_name = aliased_table_name_for(reflection.options[:join_table], "_join") @@ -2143,42 +2096,39 @@ module ActiveRecord end def association_join + return @join if @join connection = reflection.active_record.connection - join = case reflection.macro + @join = case reflection.macro when :has_and_belongs_to_many - " #{join_type} %s ON %s.%s = %s.%s " % [ - table_alias_for(options[:join_table], aliased_join_table_name), + ["%s.%s = %s.%s " % [ connection.quote_table_name(aliased_join_table_name), options[:foreign_key] || reflection.active_record.to_s.foreign_key, connection.quote_table_name(parent.aliased_table_name), - reflection.active_record.primary_key] + - " #{join_type} %s ON %s.%s = %s.%s " % [ - table_name_and_alias, + reflection.active_record.primary_key], + "%s.%s = %s.%s " % [ connection.quote_table_name(aliased_table_name), klass.primary_key, connection.quote_table_name(aliased_join_table_name), options[:association_foreign_key] || klass.to_s.foreign_key ] + ] when :has_many, :has_one - case - 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 - first_key = second_key = as_extra = nil - - if through_reflection.options[:as] # has_many :through against a polymorphic join - jt_foreign_key = through_reflection.options[:as].to_s + '_id' - jt_as_extra = " AND %s.%s = %s" % [ - connection.quote_table_name(aliased_join_table_name), - connection.quote_column_name(through_reflection.options[:as].to_s + '_type'), - klass.quote_value(parent.active_record.base_class.name) - ] - else - jt_foreign_key = through_reflection.primary_key_name - end + if reflection.options[:through] + jt_foreign_key = jt_as_extra = jt_source_extra = jt_sti_extra = nil + first_key = second_key = as_extra = nil - case source_reflection.macro + if through_reflection.options[:as] # has_many :through against a polymorphic join + jt_foreign_key = through_reflection.options[:as].to_s + '_id' + jt_as_extra = " AND %s.%s = %s" % [ + connection.quote_table_name(aliased_join_table_name), + connection.quote_column_name(through_reflection.options[:as].to_s + '_type'), + klass.quote_value(parent.active_record.base_class.name) + ] + else + jt_foreign_key = through_reflection.primary_key_name + end + + case source_reflection.macro when :has_many if source_reflection.options[:as] first_key = "#{source_reflection.options[:as]}_id" @@ -2211,65 +2161,77 @@ module ActiveRecord else second_key = source_reflection.primary_key_name end - end - - " #{join_type} %s ON (%s.%s = %s.%s%s%s%s) " % [ - table_alias_for(through_reflection.klass.table_name, aliased_join_table_name), - connection.quote_table_name(parent.aliased_table_name), - connection.quote_column_name(parent.primary_key), - connection.quote_table_name(aliased_join_table_name), - connection.quote_column_name(jt_foreign_key), - jt_as_extra, jt_source_extra, jt_sti_extra - ] + - " #{join_type} %s ON (%s.%s = %s.%s%s) " % [ - table_name_and_alias, - connection.quote_table_name(aliased_table_name), - connection.quote_column_name(first_key), - connection.quote_table_name(aliased_join_table_name), - connection.quote_column_name(second_key), - as_extra - ] + end + + ["(%s.%s = %s.%s%s%s%s) " % [ + connection.quote_table_name(parent.aliased_table_name), + connection.quote_column_name(parent.primary_key), + connection.quote_table_name(aliased_join_table_name), + connection.quote_column_name(jt_foreign_key), + jt_as_extra, jt_source_extra, jt_sti_extra], + "(%s.%s = %s.%s%s) " % [ + connection.quote_table_name(aliased_table_name), + connection.quote_column_name(first_key), + connection.quote_table_name(aliased_join_table_name), + connection.quote_column_name(second_key), + as_extra] + ] - when reflection.options[:as] && [:has_many, :has_one].include?(reflection.macro) - " #{join_type} %s ON %s.%s = %s.%s AND %s.%s = %s" % [ - table_name_and_alias, - connection.quote_table_name(aliased_table_name), - "#{reflection.options[:as]}_id", - connection.quote_table_name(parent.aliased_table_name), - parent.primary_key, - connection.quote_table_name(aliased_table_name), - "#{reflection.options[:as]}_type", - klass.quote_value(parent.active_record.base_class.name) - ] - else - foreign_key = options[:foreign_key] || reflection.active_record.name.foreign_key - " #{join_type} %s ON %s.%s = %s.%s " % [ - table_name_and_alias, - aliased_table_name, - foreign_key, - parent.aliased_table_name, - reflection.options[:primary_key] || parent.primary_key - ] + elsif reflection.options[:as] + "%s.%s = %s.%s AND %s.%s = %s" % [ + connection.quote_table_name(aliased_table_name), + "#{reflection.options[:as]}_id", + connection.quote_table_name(parent.aliased_table_name), + parent.primary_key, + connection.quote_table_name(aliased_table_name), + "#{reflection.options[:as]}_type", + klass.quote_value(parent.active_record.base_class.name) + ] + else + foreign_key = options[:foreign_key] || reflection.active_record.name.foreign_key + "%s.%s = %s.%s " % [ + aliased_table_name, + foreign_key, + parent.aliased_table_name, + reflection.options[:primary_key] || parent.primary_key + ] end when :belongs_to - " #{join_type} %s ON %s.%s = %s.%s " % [ - table_name_and_alias, - connection.quote_table_name(aliased_table_name), - reflection.klass.primary_key, - connection.quote_table_name(parent.aliased_table_name), - options[:foreign_key] || reflection.primary_key_name - ] - else - "" - end || '' - join << %(AND %s) % [ + "%s.%s = %s.%s " % [ + connection.quote_table_name(aliased_table_name), + reflection.klass.primary_key, + connection.quote_table_name(parent.aliased_table_name), + options[:foreign_key] || reflection.primary_key_name + ] + end + @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], aliased_table_name))} " if ref && ref.options[:conditions] + @join << "AND #{interpolate_sql(sanitize_sql(ref.options[:conditions], aliased_table_name))} " if ref && ref.options[:conditions] end - join + @join + end + + def relation + if reflection.macro == :has_and_belongs_to_many + [Arel::Table.new(table_alias_for(options[:join_table], aliased_join_table_name)), Arel::Table.new(table_name_and_alias)] + elsif reflection.options[:through] + [Arel::Table.new(table_alias_for(through_reflection.klass.table_name, aliased_join_table_name)), Arel::Table.new(table_name_and_alias)] + else + Arel::Table.new(table_name_and_alias) + end + end + + def join_relation(joining_relation, join = nil) + if (relations = relation).is_a?(Array) + joining_relation. + joins(relations.first, Arel::OuterJoin).on(association_join.first). + joins(relations.last, Arel::OuterJoin).on(association_join.last) + else + joining_relation.joins(relations, Arel::OuterJoin).on(association_join) + end end protected @@ -2297,7 +2259,7 @@ module ActiveRecord end def table_alias_for(table_name, table_alias) - "#{reflection.active_record.connection.quote_table_name(table_name)} #{table_alias if table_name != table_alias}".strip + "#{table_name} #{table_alias if table_name != table_alias}".strip end def table_name_and_alias @@ -2307,28 +2269,8 @@ module ActiveRecord def interpolate_sql(sql) instance_eval("%@#{sql.gsub('@', '\@')}@") end - - private - def join_type - "LEFT OUTER JOIN" - end - end - end - - class InnerJoinDependency < JoinDependency # :nodoc: - protected - def build_join_association(reflection, parent) - InnerJoinAssociation.new(reflection, self, parent) - end - - class InnerJoinAssociation < JoinAssociation - private - def join_type - "INNER JOIN" - end end end - end end end diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index 1b7bf42b91..25e329c0c1 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -476,7 +476,7 @@ module ActiveRecord def callback(method, record) callbacks_for(method).each do |callback| - ActiveSupport::Callbacks::Callback.new(method, callback, record).call(@owner, record) + ActiveSupport::DeprecatedCallbacks::Callback.new(method, callback, record).call(@owner, record) end end diff --git a/activerecord/lib/active_record/associations/association_proxy.rb b/activerecord/lib/active_record/associations/association_proxy.rb index e36b04ea95..7d8f4670fa 100644 --- a/activerecord/lib/active_record/associations/association_proxy.rb +++ b/activerecord/lib/active_record/associations/association_proxy.rb @@ -156,15 +156,6 @@ module ActiveRecord @reflection.options[:dependent] end - # Returns a string with the IDs of +records+ joined with a comma, quoted - # if needed. The result is ready to be inserted into a SQL IN clause. - # - # quoted_record_ids(records) # => "23,56,58,67" - # - def quoted_record_ids(records) - records.map { |record| record.quoted_id }.join(',') - end - def interpolate_sql(sql, record = nil) @owner.send(:interpolate_sql, sql, record) end @@ -265,10 +256,16 @@ module ActiveRecord end end - # Array#flatten has problems with recursive arrays. Going one level - # deeper solves the majority of the problems. - def flatten_deeper(array) - array.collect { |element| (element.respond_to?(:flatten) && !element.is_a?(Hash)) ? element.flatten : element }.flatten + if RUBY_VERSION < '1.9.2' + # Array#flatten has problems with recursive arrays before Ruby 1.9.2. + # Going one level deeper solves the majority of the problems. + def flatten_deeper(array) + array.collect { |element| (element.respond_to?(:flatten) && !element.is_a?(Hash)) ? element.flatten : element }.flatten + end + else + def flatten_deeper(array) + array.flatten + end end # Returns the ID of the owner, quoted if needed. diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb index 628033c87a..d2f2267e5c 100644 --- a/activerecord/lib/active_record/associations/belongs_to_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_association.rb @@ -36,11 +36,11 @@ module ActiveRecord loaded record end - + def updated? @updated end - + private def find_target find_method = if @reflection.options[:primary_key] diff --git a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb index 417e2fdc0f..c646fe488b 100644 --- a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb +++ b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb @@ -34,7 +34,7 @@ module ActiveRecord options[:readonly] = finding_with_ambiguous_select?(options[:select] || @reflection.options[:select]) options[:select] ||= (@reflection.options[:select] || '*') end - + def count_records load_target.size end @@ -56,26 +56,23 @@ module ActiveRecord if @reflection.options[:insert_sql] @owner.connection.insert(interpolate_sql(@reflection.options[:insert_sql], record)) else + relation = arel_table(@reflection.options[:join_table]) attributes = columns.inject({}) do |attrs, column| case column.name.to_s when @reflection.primary_key_name.to_s - attrs[column.name] = owner_quoted_id + attrs[relation[column.name]] = owner_quoted_id when @reflection.association_foreign_key.to_s - attrs[column.name] = record.quoted_id + attrs[relation[column.name]] = record.quoted_id else if record.has_attribute?(column.name) value = @owner.send(:quote_value, record[column.name], column) - attrs[column.name] = value unless value.nil? + attrs[relation[column.name]] = value unless value.nil? end end attrs end - sql = - "INSERT INTO #{@owner.connection.quote_table_name @reflection.options[:join_table]} (#{@owner.send(:quoted_column_names, attributes).join(', ')}) " + - "VALUES (#{attributes.values.join(', ')})" - - @owner.connection.insert(sql) + relation.insert(attributes) end return true @@ -85,9 +82,10 @@ module ActiveRecord if sql = @reflection.options[:delete_sql] records.each { |record| @owner.connection.delete(interpolate_sql(sql, record)) } else - ids = quoted_record_ids(records) - sql = "DELETE FROM #{@owner.connection.quote_table_name @reflection.options[:join_table]} WHERE #{@reflection.primary_key_name} = #{owner_quoted_id} AND #{@reflection.association_foreign_key} IN (#{ids})" - @owner.connection.delete(sql) + relation = arel_table(@reflection.options[:join_table]) + relation.conditions(relation[@reflection.primary_key_name].eq(@owner.id). + and(Arel::Predicates::In.new(relation[@reflection.association_foreign_key], records.map(&:id))) + ).delete end end diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index 73d3c23cd3..cd31b0e211 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -40,11 +40,11 @@ module ActiveRecord # 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 end - + return count end @@ -69,11 +69,11 @@ module ActiveRecord when :delete_all @reflection.klass.delete(records.map { |record| record.id }) else - ids = quoted_record_ids(records) - @reflection.klass.update_all( - "#{@reflection.primary_key_name} = NULL", - "#{@reflection.primary_key_name} = #{owner_quoted_id} AND #{@reflection.klass.primary_key} IN (#{ids})" - ) + relation = arel_table(@reflection.table_name) + relation.conditions(relation[@reflection.primary_key_name].eq(@owner.id). + and(Arel::Predicates::In.new(relation[@reflection.klass.primary_key], records.map(&:id))) + ).update(relation[@reflection.primary_key_name] => nil) + @owner.class.update_counters(@owner.id, cached_counter_attribute_name => -records.size) if has_cached_counter? end end @@ -88,11 +88,11 @@ module ActiveRecord @finder_sql = interpolate_sql(@reflection.options[:finder_sql]) when @reflection.options[:as] - @finder_sql = + @finder_sql = "#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_id = #{owner_quoted_id} AND " + "#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_type = #{@owner.class.quote_value(@owner.class.base_class.name.to_s)}" @finder_sql << " AND (#{conditions})" if conditions - + else @finder_sql = "#{@reflection.quoted_table_name}.#{@reflection.primary_key_name} = #{owner_quoted_id}" @finder_sql << " AND (#{conditions})" if conditions diff --git a/activerecord/lib/active_record/associations/has_many_through_association.rb b/activerecord/lib/active_record/associations/has_many_through_association.rb index 829f0ac0c5..214ce5959a 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -56,7 +56,7 @@ module ActiveRecord options[:joins] = construct_joins(options[:joins]) options[:include] = @reflection.source_reflection.options[:include] if options[:include].nil? && @reflection.source_reflection.options[:include] end - + def insert_record(record, force = true, validate = true) if record.new_record? if force diff --git a/activerecord/lib/active_record/associations/through_association_scope.rb b/activerecord/lib/active_record/associations/through_association_scope.rb index 16b6123439..1924156e2a 100644 --- a/activerecord/lib/active_record/associations/through_association_scope.rb +++ b/activerecord/lib/active_record/associations/through_association_scope.rb @@ -42,7 +42,7 @@ module ActiveRecord end def construct_from - @reflection.quoted_table_name + @reflection.table_name end def construct_select(custom_select = nil) diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index ab7ad34b9e..3a9a67e3a2 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -31,11 +31,10 @@ module ActiveRecord self.class.define_attribute_methods method_name = method_id.to_s guard_private_attribute_method!(method_name, args) - if self.class.generated_attribute_methods.instance_methods.include?(method_name) - return self.send(method_id, *args, &block) - end + send(method_id, *args, &block) + else + super end - super end def respond_to?(*args) diff --git a/activerecord/lib/active_record/attribute_methods/before_type_cast.rb b/activerecord/lib/active_record/attribute_methods/before_type_cast.rb index a4e144f233..74921241f7 100644 --- a/activerecord/lib/active_record/attribute_methods/before_type_cast.rb +++ b/activerecord/lib/active_record/attribute_methods/before_type_cast.rb @@ -8,25 +8,18 @@ module ActiveRecord end def read_attribute_before_type_cast(attr_name) - @attributes[attr_name] + _attributes.without_typecast[attr_name] end # Returns a hash of attributes before typecasting and deserialization. def attributes_before_type_cast - self.attribute_names.inject({}) do |attrs, name| - attrs[name] = read_attribute_before_type_cast(name) - attrs - end + _attributes.without_typecast end private # Handle *_before_type_cast for method_missing. def attribute_before_type_cast(attribute_name) - if attribute_name == 'id' - read_attribute_before_type_cast(self.class.primary_key) - else - read_attribute_before_type_cast(attribute_name) - end + read_attribute_before_type_cast(attribute_name) end end end diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb index 4df0f1af69..4a3ab9ea82 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -1,5 +1,3 @@ -require 'active_support/core_ext/object/tap' - module ActiveRecord module AttributeMethods module Dirty diff --git a/activerecord/lib/active_record/attribute_methods/query.rb b/activerecord/lib/active_record/attribute_methods/query.rb index a949d80120..0154ee35f8 100644 --- a/activerecord/lib/active_record/attribute_methods/query.rb +++ b/activerecord/lib/active_record/attribute_methods/query.rb @@ -8,23 +8,7 @@ module ActiveRecord end def query_attribute(attr_name) - unless value = read_attribute(attr_name) - false - else - column = self.class.columns_hash[attr_name] - if column.nil? - if Numeric === value || value !~ /[^0-9]/ - !value.to_i.zero? - else - return false if ActiveRecord::ConnectionAdapters::Column::FALSE_VALUES.include?(value) - !value.blank? - end - elsif column.number? - !value.zero? - else - !value.blank? - end - end + _attributes.has?(attr_name) end private @@ -35,3 +19,5 @@ module ActiveRecord end end end + + diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index 3da3d9d8cc..97caec7744 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -37,11 +37,7 @@ module ActiveRecord protected def define_method_attribute(attr_name) - if self.serialized_attributes[attr_name] - define_read_method_for_serialized_attribute(attr_name) - else - define_read_method(attr_name.to_sym, attr_name, columns_hash[attr_name]) - end + define_read_method(attr_name.to_sym, attr_name, columns_hash[attr_name]) if attr_name == primary_key && attr_name != "id" define_read_method(:id, attr_name, columns_hash[attr_name]) @@ -49,18 +45,12 @@ module ActiveRecord end private - # Define read method for serialized attribute. - def define_read_method_for_serialized_attribute(attr_name) - generated_attribute_methods.module_eval("def #{attr_name}; unserialize_attribute('#{attr_name}'); end", __FILE__, __LINE__) - end # Define an attribute reader method. Cope with nil column. def define_read_method(symbol, attr_name, column) - cast_code = column.type_cast_code('v') if column - access_code = cast_code ? "(v=@attributes['#{attr_name}']) && #{cast_code}" : "@attributes['#{attr_name}']" - + access_code = "_attributes['#{attr_name}']" unless attr_name.to_s == self.primary_key.to_s - access_code = access_code.insert(0, "missing_attribute('#{attr_name}', caller) unless @attributes.has_key?('#{attr_name}'); ") + access_code = access_code.insert(0, "missing_attribute('#{attr_name}', caller) unless _attributes.key?('#{attr_name}'); ") end if cache_attribute?(attr_name) @@ -73,38 +63,7 @@ module ActiveRecord # Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example, # "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)). 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 column = column_for_attribute(attr_name) - if unserializable_attribute?(attr_name, column) - unserialize_attribute(attr_name) - else - column.type_cast(value) - end - else - value - end - else - nil - end - end - - # Returns true if the attribute is of a text column and marked for serialization. - def unserializable_attribute?(attr_name, column) - column.text? && self.class.serialized_attributes[attr_name] - end - - # Returns the unserialized object of the attribute. - def unserialize_attribute(attr_name) - unserialized_object = object_from_yaml(@attributes[attr_name]) - - if unserialized_object.is_a?(self.class.serialized_attributes[attr_name]) || unserialized_object.nil? - @attributes.frozen? ? unserialized_object : @attributes[attr_name] = unserialized_object - else - raise SerializationTypeMismatch, - "#{attr_name} was supposed to be a #{self.class.serialized_attributes[attr_name]}, but was a #{unserialized_object.class.to_s}" - end + _attributes[attr_name] end private diff --git a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb index a8e3e28a7a..4ac0c7f608 100644 --- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb +++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb @@ -12,48 +12,20 @@ module ActiveRecord end module ClassMethods + + def cache_attribute?(attr_name) + time_zone_aware?(attr_name) || super + end + protected - # Defined for all +datetime+ and +timestamp+ attributes when +time_zone_aware_attributes+ are enabled. - # This enhanced read method automatically converts the UTC time stored in the database to the time zone stored in Time.zone. - def define_method_attribute(attr_name) - if create_time_zone_conversion_attribute?(attr_name, columns_hash[attr_name]) - method_body = <<-EOV - def #{attr_name}(reload = false) - cached = @attributes_cache['#{attr_name}'] - return cached if cached && !reload - time = read_attribute('#{attr_name}') - @attributes_cache['#{attr_name}'] = time.acts_like?(:time) ? time.in_time_zone : time - end - EOV - generated_attribute_methods.module_eval(method_body, __FILE__, __LINE__) - else - super - end - end - # Defined for all +datetime+ and +timestamp+ attributes when +time_zone_aware_attributes+ are enabled. - # This enhanced write method will automatically convert the time passed to it to the zone stored in Time.zone. - def define_method_attribute=(attr_name) - if create_time_zone_conversion_attribute?(attr_name, columns_hash[attr_name]) - method_body = <<-EOV - def #{attr_name}=(time) - unless time.acts_like?(:time) - time = time.is_a?(String) ? Time.zone.parse(time) : time.to_time rescue time - end - time = time.in_time_zone rescue nil if time - write_attribute(:#{attr_name}, time) - end - EOV - generated_attribute_methods.module_eval(method_body, __FILE__, __LINE__) - else - super - end + def time_zone_aware?(attr_name) + column = columns_hash[attr_name] + time_zone_aware_attributes && + !skip_time_zone_conversion_for_attributes.include?(attr_name.to_sym) && + [:datetime, :timestamp].include?(column.type) end - private - def create_time_zone_conversion_attribute?(name, column) - time_zone_aware_attributes && !skip_time_zone_conversion_for_attributes.include?(name.to_sym) && [:datetime, :timestamp].include?(column.type) - end end end end diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb index e31acac050..37eadbe0a9 100644 --- a/activerecord/lib/active_record/attribute_methods/write.rb +++ b/activerecord/lib/active_record/attribute_methods/write.rb @@ -17,14 +17,9 @@ module ActiveRecord # Updates the attribute identified by <tt>attr_name</tt> with the specified +value+. Empty strings for fixnum and float # columns are turned into +nil+. def write_attribute(attr_name, value) - attr_name = attr_name.to_s - attr_name = self.class.primary_key if attr_name == 'id' + attr_name = _attributes.unalias(attr_name) @attributes_cache.delete(attr_name) - if (column = column_for_attribute(attr_name)) && column.number? - @attributes[attr_name] = convert_number_column_value(value) - else - @attributes[attr_name] = value - end + _attributes[attr_name] = value end private diff --git a/activerecord/lib/active_record/attributes.rb b/activerecord/lib/active_record/attributes.rb new file mode 100644 index 0000000000..e4d9e89821 --- /dev/null +++ b/activerecord/lib/active_record/attributes.rb @@ -0,0 +1,37 @@ +module ActiveRecord + module Attributes + + # Returns true if the given attribute is in the attributes hash + def has_attribute?(attr_name) + _attributes.key?(attr_name) + end + + # Returns an array of names for the attributes available on this object sorted alphabetically. + def attribute_names + _attributes.keys.sort! + end + + # Returns a hash of all the attributes with their names as keys and the values of the attributes as values. + def attributes + attributes = _attributes.dup + attributes.typecast! unless _attributes.frozen? + attributes.to_h + end + + protected + + # Not to be confused with the public #attributes method, which returns a typecasted Hash. + def _attributes + @attributes + end + + def initialize_attribute_store(merge_attributes = nil) + @attributes = ActiveRecord::Attributes::Store.new + @attributes.merge!(merge_attributes) if merge_attributes + @attributes.types.merge!(self.class.attribute_types) + @attributes.aliases.merge!('id' => self.class.primary_key) unless 'id' == self.class.primary_key + @attributes + end + + end +end diff --git a/activerecord/lib/active_record/attributes/aliasing.rb b/activerecord/lib/active_record/attributes/aliasing.rb new file mode 100644 index 0000000000..db77739d1f --- /dev/null +++ b/activerecord/lib/active_record/attributes/aliasing.rb @@ -0,0 +1,42 @@ +module ActiveRecord + module Attributes + module Aliasing + # Allows access to keys using aliased names. + # + # Example: + # class Attributes < Hash + # include Aliasing + # end + # + # attributes = Attributes.new + # attributes.aliases['id'] = 'fancy_primary_key' + # attributes['fancy_primary_key'] = 2020 + # + # attributes['id'] + # => 2020 + # + # Additionally, symbols are always aliases of strings: + # attributes[:fancy_primary_key] + # => 2020 + # + def [](key) + super(unalias(key)) + end + + def []=(key, value) + super(unalias(key), value) + end + + def aliases + @aliases ||= {} + end + + def unalias(key) + key = key.to_s + aliases[key] || key + end + + end + end +end + diff --git a/activerecord/lib/active_record/attributes/store.rb b/activerecord/lib/active_record/attributes/store.rb new file mode 100644 index 0000000000..61109f4acc --- /dev/null +++ b/activerecord/lib/active_record/attributes/store.rb @@ -0,0 +1,15 @@ +module ActiveRecord + module Attributes + class Store < Hash + include ActiveRecord::Attributes::Typecasting + include ActiveRecord::Attributes::Aliasing + + # Attributes not mapped to a column are handled using Type::Unknown, + # which enables boolean typecasting for unmapped keys. + def types + @types ||= Hash.new(Type::Unknown.new) + end + + end + end +end diff --git a/activerecord/lib/active_record/attributes/typecasting.rb b/activerecord/lib/active_record/attributes/typecasting.rb new file mode 100644 index 0000000000..56c32f9895 --- /dev/null +++ b/activerecord/lib/active_record/attributes/typecasting.rb @@ -0,0 +1,117 @@ +module ActiveRecord + module Attributes + module Typecasting + # Typecasts values during access based on their key mapping to a Type. + # + # Example: + # class Attributes < Hash + # include Typecasting + # end + # + # attributes = Attributes.new + # attributes.types['comments_count'] = Type::Integer + # attributes['comments_count'] = '5' + # + # attributes['comments_count'] + # => 5 + # + # To support keys not mapped to a typecaster, add a default to types. + # attributes.types.default = Type::Unknown + # attributes['age'] = '25' + # attributes['age'] + # => '25' + # + # A valid type supports #cast, #precast, #boolean, and #appendable? methods. + # + def [](key) + value = super(key) + typecast_read(key, value) + end + + def []=(key, value) + super(key, typecast_write(key, value)) + end + + def to_h + hash = {} + hash.merge!(self) + hash + end + + def dup # :nodoc: + copy = super + copy.types = types.dup + copy + end + + # Provides a duplicate with typecasting disabled. + # + # Example: + # attributes = Attributes.new + # attributes.types['comments_count'] = Type::Integer + # attributes['comments_count'] = '5' + # + # attributes.without_typecast['comments_count'] + # => '5' + # + def without_typecast + dup.without_typecast! + end + + def without_typecast! + types.clear + self + end + + def typecast! + keys.each { |key| self[key] = self[key] } + self + end + + # Check if key has a value that typecasts to true. + # + # attributes = Attributes.new + # attributes.types['comments_count'] = Type::Integer + # + # attributes['comments_count'] = 0 + # attributes.has?('comments_count') + # => false + # + # attributes['comments_count'] = 1 + # attributes.has?('comments_count') + # => true + # + def has?(key) + value = self[key] + boolean_typecast(key, value) + end + + def types + @types ||= {} + end + + protected + + def types=(other_types) + @types = other_types + end + + def boolean_typecast(key, value) + value ? types[key].boolean(value) : false + end + + def typecast_read(key, value) + type = types[key] + value = type.cast(value) + self[key] = value if type.appendable? && !frozen? + + value + end + + def typecast_write(key, value) + types[key].precast(value) + end + + end + end +end diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 150e3fc263..056f29f029 100755 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -1,5 +1,7 @@ +require 'benchmark' require 'yaml' require 'set' +require 'active_support/benchmarkable' require 'active_support/dependencies' require 'active_support/time' require 'active_support/core_ext/class/attribute_accessors' @@ -10,7 +12,6 @@ require 'active_support/core_ext/hash/deep_merge' require 'active_support/core_ext/hash/indifferent_access' require 'active_support/core_ext/hash/slice' require 'active_support/core_ext/string/behavior' -require 'active_support/core_ext/symbol' require 'active_support/core_ext/object/metaclass' module ActiveRecord #:nodoc: @@ -421,7 +422,7 @@ module ActiveRecord #:nodoc: # So it's possible to assign a logger to the class through <tt>Base.logger=</tt> which will then be used by all # instances in the current object space. class Base - ## + ## # :singleton-method: # Accepts a logger conforming to the interface of Log4r or the default Ruby 1.8+ Logger class, which is then passed # on to any new database connections made and which can be retrieved on both a class and instance level by calling +logger+. @@ -455,11 +456,11 @@ module ActiveRecord #:nodoc: # as a Hash. # # For example, the following database.yml... - # + # # development: # adapter: sqlite3 # database: db/development.sqlite3 - # + # # production: # adapter: sqlite3 # database: db/production.sqlite3 @@ -661,10 +662,26 @@ module ActiveRecord #:nodoc: find(:last, *args) end - # This is an alias for find(:all). You can pass in all the same arguments to this method as you can - # to find(:all) + # Returns an ActiveRecord::Relation object. You can pass in all the same arguments to this method as you can + # to find(:all). def all(*args) - find(:all, *args) + options = args.extract_options! + + if options.empty? && !scoped?(:find) + relation = arel_table + else + relation = construct_finder_arel(options) + include_associations = merge_includes(scope(:find, :include), options[:include]) + + if include_associations.any? + if references_eager_loaded_tables?(options) + relation.eager_load(include_associations) + else + relation.preload(include_associations) + end + end + end + relation end # Executes a custom SQL query against your database and returns all the results. The results will @@ -860,26 +877,23 @@ module ActiveRecord #:nodoc: # # Update all books that match our conditions, but limit it to 5 ordered by date # Book.update_all "author = 'David'", "title LIKE '%Rails%'", :order => 'created_at', :limit => 5 def update_all(updates, conditions = nil, options = {}) - sql = "UPDATE #{quoted_table_name} SET #{sanitize_sql_for_assignment(updates)} " - scope = scope(:find) - select_sql = "" - add_conditions!(select_sql, conditions, scope) + relation = arel_table - if options.has_key?(:limit) || (scope && scope[:limit]) + if conditions = construct_conditions(conditions, scope) + relation = relation.conditions(Arel::SqlLiteral.new(conditions)) + end + + relation = if options.has_key?(:limit) || (scope && scope[:limit]) # Only take order from scope if limit is also provided by scope, this # is useful for updating a has_many association with a limit. - add_order!(select_sql, options[:order], scope) - - add_limit!(select_sql, options, scope) - sql.concat(connection.limited_update_conditions(select_sql, quoted_table_name, connection.quote_column_name(primary_key))) + relation.order(construct_order(options[:order], scope)).limit(construct_limit(options[:limit], scope)) else - add_order!(select_sql, options[:order], nil) - sql.concat(select_sql) + relation.order(options[:order]) end - connection.update(sql, "#{name} Update") + relation.update(sanitize_sql_for_assignment(updates)) end # Destroys the records matching +conditions+ by instantiating each @@ -930,9 +944,11 @@ module ActiveRecord #:nodoc: # Both calls delete the affected posts all at once with a single DELETE statement. If you need to destroy dependent # associations or call your <tt>before_*</tt> or +after_destroy+ callbacks, use the +destroy_all+ method instead. def delete_all(conditions = nil) - sql = "DELETE FROM #{quoted_table_name} " - add_conditions!(sql, conditions, scope(:find)) - connection.delete(sql, "#{name} Delete all") + if conditions + arel_table.conditions(Arel::SqlLiteral.new(construct_conditions(conditions, scope(:find)))).delete + else + arel_table.delete + end end # Returns the result of an SQL statement that should only include a COUNT(*) in the SELECT part. @@ -1060,7 +1076,7 @@ module ActiveRecord #:nodoc: # If the access logic of your application is richer you can use <tt>Hash#except</tt> # or <tt>Hash#slice</tt> to sanitize the hash of parameters before they are # passed to Active Record. - # + # # For example, it could be the case that the list of protected attributes # for a given model depends on the role of the user: # @@ -1108,7 +1124,7 @@ module ActiveRecord #:nodoc: # If the access logic of your application is richer you can use <tt>Hash#except</tt> # or <tt>Hash#slice</tt> to sanitize the hash of parameters before they are # passed to Active Record. - # + # # For example, it could be the case that the list of accessible attributes # for a given model depends on the role of the user: # @@ -1135,7 +1151,7 @@ module ActiveRecord #:nodoc: # Returns an array of all the attributes that have been specified as readonly. def readonly_attributes - read_inheritable_attribute(:attr_readonly) + read_inheritable_attribute(:attr_readonly) || [] end # If you have an attribute that needs to be saved to the database as an object, and retrieved as the same object, @@ -1362,17 +1378,18 @@ module ActiveRecord #:nodoc: # end def reset_column_information undefine_attribute_methods - @column_names = @columns = @columns_hash = @content_columns = @dynamic_methods_hash = @inheritance_column = nil + @arel_table = @column_names = @columns = @columns_hash = @content_columns = @dynamic_methods_hash = @inheritance_column = nil end def reset_column_information_and_inheritable_attributes_for_all_subclasses#:nodoc: subclasses.each { |klass| klass.reset_inheritable_attributes; klass.reset_column_information } end - def self_and_descendants_from_active_record#nodoc: + # Set the lookup ancestors for ActiveModel. + def lookup_ancestors #:nodoc: klass = self classes = [klass] - while klass != klass.base_class + while klass != klass.base_class classes << klass = klass.superclass end classes @@ -1383,32 +1400,9 @@ module ActiveRecord #:nodoc: [self] end - # Transforms attribute key names into a more humane format, such as "First name" instead of "first_name". Example: - # Person.human_attribute_name("first_name") # => "First name" - # This used to be deprecated in favor of humanize, but is now preferred, because it automatically uses the I18n - # module now. - # Specify +options+ with additional translating options. - def human_attribute_name(attribute_key_name, options = {}) - defaults = self_and_descendants_from_active_record.map do |klass| - :"#{klass.name.underscore}.#{attribute_key_name}" - end - defaults << options[:default] if options[:default] - defaults.flatten! - defaults << attribute_key_name.to_s.humanize - options[:count] ||= 1 - I18n.translate(defaults.shift, options.merge(:default => defaults, :scope => [:activerecord, :attributes])) - end - - # Transform the modelname into a more humane format, using I18n. - # By default, it will underscore then humanize the class name (BlogPost.human_name #=> "Blog post"). - # Default scope of the translation is activerecord.models - # Specify +options+ with additional translating options. - def human_name(options = {}) - defaults = self_and_descendants_from_active_record.map do |klass| - :"#{klass.name.underscore}" - end - defaults << self.name.underscore.humanize - I18n.translate(defaults.shift, {:scope => [:activerecord, :models], :count => 1, :default => defaults}.merge(options)) + # Set the i18n scope to overwrite ActiveModel. + def i18n_scope #:nodoc: + :activerecord end # True if this isn't a concrete subclass needing a STI type condition. @@ -1448,38 +1442,6 @@ module ActiveRecord #:nodoc: connection.quote(object) end - # Log and benchmark multiple statements in a single block. Example: - # - # Project.benchmark("Creating project") do - # project = Project.create("name" => "stuff") - # project.create_manager("name" => "David") - # project.milestones << Milestone.find(:all) - # end - # - # The benchmark is only recorded if the current level of the logger is less than or equal to the <tt>log_level</tt>, - # which makes it easy to include benchmarking statements in production software that will remain inexpensive because - # the benchmark will only be conducted if the log level is low enough. - # - # The logging of the multiple statements is turned off unless <tt>use_silence</tt> is set to false. - def benchmark(title, log_level = Logger::DEBUG, use_silence = true) - if logger && logger.level <= log_level - result = nil - ms = Benchmark.ms { result = use_silence ? silence { yield } : yield } - logger.add(log_level, '%s (%.1fms)' % [title, ms]) - result - else - yield - end - end - - # Silences the logger for the duration of the block. - def silence - old_logger_level, logger.level = logger.level, Logger::ERROR if logger - yield - ensure - logger.level = old_logger_level if logger - end - # Overwrite the default class equality method to provide support for association proxies. def ===(object) object.is_a?(self) @@ -1529,6 +1491,15 @@ module ActiveRecord #:nodoc: "(#{segments.join(') AND (')})" unless segments.empty? end + + def arel_table(table = nil) + table = table_name if table.blank? + if @arel_table.nil? || @arel_table.name != table + @arel_table = Relation.new(self, Arel::Table.new(table)) + end + @arel_table + end + private def find_initial(options) options.update(:limit => 1) @@ -1650,7 +1621,7 @@ module ActiveRecord #:nodoc: def instantiate(record) object = find_sti_class(record[inheritance_column]).allocate - object.instance_variable_set(:'@attributes', record) + object.send(:initialize_attribute_store, record) object.instance_variable_set(:'@attributes_cache', {}) object.send(:_run_find_callbacks) @@ -1693,22 +1664,83 @@ module ActiveRecord #:nodoc: end end - def construct_finder_sql(options) - scope = scope(:find) - sql = "SELECT #{options[:select] || (scope && scope[:select]) || default_select(options[:joins] || (scope && scope[:joins]))} " - sql << "FROM #{options[:from] || (scope && scope[:from]) || quoted_table_name} " + def construct_finder_arel(options = {}, scope = scope(:find)) + # TODO add lock to Arel + relation = arel_table(options[:from]). + joins(construct_join(options[:joins], scope)). + conditions(construct_conditions(options[:conditions], scope)). + select(options[:select] || (scope && scope[:select]) || default_select(options[:joins] || (scope && scope[:joins]))). + group(construct_group(options[:group], options[:having], scope)). + order(construct_order(options[:order], scope)). + limit(construct_limit(options[:limit], scope)). + offset(construct_offset(options[:offset], scope)) + + relation = relation.readonly if options[:readonly] - add_joins!(sql, options[:joins], scope) - add_conditions!(sql, options[:conditions], scope) + relation + end + + def construct_finder_sql(options, scope = scope(:find)) + construct_finder_arel(options, scope).to_sql + end - add_group!(sql, options[:group], options[:having], scope) - add_order!(sql, options[:order], scope) - add_limit!(sql, options, scope) - add_lock!(sql, options, scope) + def construct_join(joins, scope) + merged_joins = scope && scope[:joins] && joins ? merge_joins(scope[:joins], joins) : (joins || scope && scope[:joins]) + case merged_joins + when Symbol, Hash, Array + if array_of_strings?(merged_joins) + merged_joins.join(' ') + " " + else + build_association_joins(merged_joins) + end + when String + " #{merged_joins} " + else + "" + end + end + def construct_group(group, having, scope) + sql = '' + if group + sql << group.to_s + sql << " HAVING #{sanitize_sql_for_conditions(having)}" if having + elsif scope && (scoped_group = scope[:group]) + sql << scoped_group.to_s + sql << " HAVING #{sanitize_sql_for_conditions(scope[:having])}" if scope[:having] + end sql end + def construct_order(order, scope) + orders = [] + scoped_order = scope[:order] if scope + if order + orders << order + orders << scoped_order if scoped_order && scoped_order != order + elsif scoped_order + orders << scoped_order + end + orders + end + + def construct_limit(limit, scope) + limit ||= scope[:limit] if scope + limit + end + + def construct_offset(offset, scope) + offset ||= scope[:offset] if scope + offset + end + + def construct_conditions(conditions, scope) + conditions = [conditions] + conditions << scope[:conditions] if scope + conditions << type_condition if finder_needs_type_condition? + merge_conditions(*conditions) + end + # Merges includes so that the result is a valid +include+ def merge_includes(first, second) (safe_to_array(first) + safe_to_array(second)).uniq @@ -1718,10 +1750,7 @@ module ActiveRecord #:nodoc: if joins.any?{|j| j.is_a?(String) || array_of_strings?(j) } joins = joins.collect do |join| join = [join] if join.is_a?(String) - unless array_of_strings?(join) - join_dependency = ActiveRecord::Associations::ClassMethods::InnerJoinDependency.new(self, join, nil) - join = join_dependency.join_associations.collect { |assoc| assoc.association_join } - end + join = build_association_joins(join) unless array_of_strings?(join) join end joins.flatten.map{|j| j.strip}.uniq @@ -1730,6 +1759,19 @@ module ActiveRecord #:nodoc: end end + def build_association_joins(joins) + join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(self, joins, nil) + relation = arel_table.relation + join_dependency.join_associations.map { |association| + if (association_relation = association.relation).is_a?(Array) + [Arel::InnerJoin.new(relation, association_relation.first, association.association_join.first).joins(relation), + Arel::InnerJoin.new(relation, association_relation.last, association.association_join.last).joins(relation)].join() + else + Arel::InnerJoin.new(relation, association_relation, association.association_join).joins(relation) + end + }.join(" ") + end + # Object#to_a is deprecated, though it does have the desired behavior def safe_to_array(o) case o @@ -1746,44 +1788,6 @@ module ActiveRecord #:nodoc: o.is_a?(Array) && o.all?{|obj| obj.is_a?(String)} end - def add_order!(sql, order, scope = :auto) - scope = scope(:find) if :auto == scope - scoped_order = scope[:order] if scope - if order - sql << " ORDER BY #{order}" - if scoped_order && scoped_order != order - sql << ", #{scoped_order}" - end - else - sql << " ORDER BY #{scoped_order}" if scoped_order - end - end - - def add_group!(sql, group, having, scope = :auto) - if group - sql << " GROUP BY #{group}" - sql << " HAVING #{sanitize_sql_for_conditions(having)}" if having - else - scope = scope(:find) if :auto == scope - if scope && (scoped_group = scope[:group]) - sql << " GROUP BY #{scoped_group}" - sql << " HAVING #{sanitize_sql_for_conditions(scope[:having])}" if scope[:having] - end - end - end - - # The optional scope argument is for the current <tt>:find</tt> scope. - def add_limit!(sql, options, scope = :auto) - scope = scope(:find) if :auto == scope - - if scope - options[:limit] ||= scope[:limit] - options[:offset] ||= scope[:offset] - end - - connection.add_limit_offset!(sql, options) - end - # The optional scope argument is for the current <tt>:find</tt> scope. # The <tt>:lock</tt> option has precedence over a scoped <tt>:lock</tt>. def add_lock!(sql, options, scope = :auto) @@ -1792,38 +1796,10 @@ module ActiveRecord #:nodoc: connection.add_lock!(sql, options) end - # The optional scope argument is for the current <tt>:find</tt> scope. - def add_joins!(sql, joins, scope = :auto) - scope = scope(:find) if :auto == scope - merged_joins = scope && scope[:joins] && joins ? merge_joins(scope[:joins], joins) : (joins || scope && scope[:joins]) - case merged_joins - when Symbol, Hash, Array - if array_of_strings?(merged_joins) - sql << merged_joins.join(' ') + " " - else - join_dependency = ActiveRecord::Associations::ClassMethods::InnerJoinDependency.new(self, merged_joins, nil) - sql << " #{join_dependency.join_associations.collect { |assoc| assoc.association_join }.join} " - end - when String - sql << " #{merged_joins} " - end - end - - # Adds a sanitized version of +conditions+ to the +sql+ string. Note that the passed-in +sql+ string is changed. - # The optional scope argument is for the current <tt>:find</tt> scope. - def add_conditions!(sql, conditions, scope = :auto) - scope = scope(:find) if :auto == scope - conditions = [conditions] - conditions << scope[:conditions] if scope - conditions << type_condition if finder_needs_type_condition? - merged_conditions = merge_conditions(*conditions) - sql << "WHERE #{merged_conditions} " unless merged_conditions.blank? - end - def type_condition(table_alias=nil) quoted_table_alias = self.connection.quote_table_name(table_alias || table_name) quoted_inheritance_column = connection.quote_column_name(inheritance_column) - type_condition = subclasses.inject("#{quoted_table_alias}.#{quoted_inheritance_column} = '#{sti_name}' ") do |condition, subclass| + 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 @@ -1964,7 +1940,7 @@ module ActiveRecord #:nodoc: attributes = construct_attributes_from_arguments( # attributes = construct_attributes_from_arguments( [:#{attribute_names.join(',:')}], args # [:user_name, :password], args ) # ) - # + # scoped(:conditions => attributes) # scoped(:conditions => attributes) end # end }, __FILE__, __LINE__ @@ -2416,7 +2392,7 @@ module ActiveRecord #:nodoc: # In both instances, valid attribute keys are determined by the column names of the associated table -- # hence you can't have attributes that aren't part of the table columns. def initialize(attributes = nil) - @attributes = attributes_from_column_definition + initialize_attribute_store(attributes_from_column_definition) @attributes_cache = {} @new_record = true ensure_proper_type @@ -2442,7 +2418,7 @@ module ActiveRecord #:nodoc: callback(:after_initialize) if respond_to_without_attributes?(:after_initialize) cloned_attributes = other.clone_attributes(:read_attribute_before_type_cast) cloned_attributes.delete(self.class.primary_key) - @attributes = cloned_attributes + initialize_attribute_store(cloned_attributes) clear_aggregation_cache @attributes_cache = {} @new_record = true @@ -2469,7 +2445,7 @@ module ActiveRecord #:nodoc: # name # end # end - # + # # user = User.find_by_name('Phusion') # user_path(user) # => "/users/Phusion" def to_param @@ -2520,12 +2496,12 @@ module ActiveRecord #:nodoc: # If +perform_validation+ is true validations run. If any of them fail # the action is cancelled and +save+ returns +false+. If the flag is # false validations are bypassed altogether. See - # ActiveRecord::Validations for more information. + # ActiveRecord::Validations for more information. # # There's a series of callbacks associated with +save+. If any of the # <tt>before_*</tt> callbacks return +false+ the action is cancelled and # +save+ returns +false+. See ActiveRecord::Callbacks for further - # details. + # details. def save create_or_update end @@ -2537,12 +2513,12 @@ module ActiveRecord #:nodoc: # # With <tt>save!</tt> validations always run. If any of them fail # ActiveRecord::RecordInvalid gets raised. See ActiveRecord::Validations - # for more information. + # for more information. # # There's a series of callbacks associated with <tt>save!</tt>. If any of # the <tt>before_*</tt> callbacks return +false+ the action is cancelled # and <tt>save!</tt> raises ActiveRecord::RecordNotSaved. See - # ActiveRecord::Callbacks for further details. + # ActiveRecord::Callbacks for further details. def save! create_or_update || raise(RecordNotSaved) end @@ -2567,11 +2543,7 @@ module ActiveRecord #:nodoc: # be made (since they can't be persisted). def destroy unless new_record? - connection.delete( - "DELETE FROM #{self.class.quoted_table_name} " + - "WHERE #{connection.quote_column_name(self.class.primary_key)} = #{quoted_id}", - "#{self.class.name} Destroy" - ) + self.class.arel_table.conditions(self.class.arel_table[self.class.primary_key].eq(id)).delete end @destroyed = true @@ -2672,7 +2644,7 @@ module ActiveRecord #:nodoc: def reload(options = nil) clear_aggregation_cache clear_association_cache - @attributes.update(self.class.find(self.id, options).instance_variable_get('@attributes')) + _attributes.update(self.class.find(self.id, options).instance_variable_get('@attributes')) @attributes_cache = {} self end @@ -2702,12 +2674,12 @@ module ActiveRecord #:nodoc: # class User < ActiveRecord::Base # attr_protected :is_admin # end - # + # # user = User.new # user.attributes = { :username => 'Phusion', :is_admin => true } # user.username # => "Phusion" # user.is_admin? # => false - # + # # user.send(:attributes=, { :username => 'Phusion', :is_admin => true }, false) # user.is_admin? # => true def attributes=(new_attributes, guard_protected_attributes = true) @@ -2769,16 +2741,6 @@ module ActiveRecord #:nodoc: !value.blank? end - # Returns true if the given attribute is in the attributes hash - def has_attribute?(attr_name) - @attributes.has_key?(attr_name.to_s) - end - - # Returns an array of names for the attributes available on this object sorted alphabetically. - def attribute_names - @attributes.keys.sort - end - # Returns the column object for the named attribute. def column_for_attribute(name) self.class.columns_hash[name.to_s] @@ -2866,14 +2828,9 @@ module ActiveRecord #:nodoc: # Updates the associated record with values matching those of the instance attributes. # Returns the number of affected rows. def update(attribute_names = @attributes.keys) - quoted_attributes = attributes_with_quotes(false, false, attribute_names) - return 0 if quoted_attributes.empty? - connection.update( - "UPDATE #{self.class.quoted_table_name} " + - "SET #{quoted_comma_pair_list(connection, quoted_attributes)} " + - "WHERE #{connection.quote_column_name(self.class.primary_key)} = #{quote_value(id)}", - "#{self.class.name} Update" - ) + attributes_with_values = arel_attributes_values(false, false, attribute_names) + return 0 if attributes_with_values.empty? + self.class.arel_table.conditions(self.class.arel_table[self.class.primary_key].eq(id)).update(attributes_with_values) end # Creates a record with values matching those of the instance attributes @@ -2883,18 +2840,15 @@ module ActiveRecord #:nodoc: self.id = connection.next_sequence_value(self.class.sequence_name) end - quoted_attributes = attributes_with_quotes + attributes_values = arel_attributes_values - statement = if quoted_attributes.empty? - connection.empty_insert_statement(self.class.table_name) + new_id = if attributes_values.empty? + self.class.arel_table.insert connection.empty_insert_statement_value else - "INSERT INTO #{self.class.quoted_table_name} " + - "(#{quoted_column_names.join(', ')}) " + - "VALUES(#{quoted_attributes.values.join(', ')})" + self.class.arel_table.insert attributes_values end - self.id = connection.insert(statement, "#{self.class.name} Create", - self.class.primary_key, self.id, self.class.sequence_name) + self.id ||= new_id @new_record = false id @@ -2910,18 +2864,6 @@ module ActiveRecord #:nodoc: end end - def convert_number_column_value(value) - if value == false - 0 - elsif value == true - 1 - elsif value.is_a?(String) && value.blank? - nil - else - value - end - end - def remove_attributes_protected_from_mass_assignment(attributes) safe_attributes = if self.class.accessible_attributes.nil? && self.class.protected_attributes.nil? @@ -2985,6 +2927,26 @@ module ActiveRecord #:nodoc: include_readonly_attributes ? quoted : remove_readonly_attributes(quoted) end + # Returns a copy of the attributes hash where all the values have been safely quoted for use in + # an Arel insert/update method. + def arel_attributes_values(include_primary_key = true, include_readonly_attributes = true, attribute_names = @attributes.keys) + attrs = {} + attribute_names.each do |name| + if (column = column_for_attribute(name)) && (include_primary_key || !column.primary) + + if include_readonly_attributes || (!include_readonly_attributes && !self.class.readonly_attributes.include?(name)) + value = read_attribute(name) + + if value && ((self.class.serialized_attributes.has_key?(name) && (value.acts_like?(:date) || value.acts_like?(:time))) || value.is_a?(Hash) || value.is_a?(Array)) + value = value.to_yaml + end + attrs[self.class.arel_table[name]] = value + end + end + end + attrs + end + # Quote strings appropriately for SQL statements. def quote_value(value, column = nil) self.class.connection.quote(value, column) @@ -3020,7 +2982,7 @@ module ActiveRecord #:nodoc: end def instantiate_time_object(name, values) - if self.class.send(:create_time_zone_conversion_attribute?, name, column_for_attribute(name)) + if self.class.send(:time_zone_aware?, name) Time.zone.local(*values) else Time.time_with_datetime_fallback(@@default_timezone, *values) @@ -3092,13 +3054,6 @@ module ActiveRecord #:nodoc: hash.inject([]) { |list, pair| list << "#{pair.first} = #{pair.last}" }.join(", ") end - def quoted_column_names(attributes = attributes_with_quotes) - connection = self.class.connection - attributes.keys.collect do |column_name| - connection.quote_column_name(column_name) - end - end - def self.quoted_table_name self.connection.quote_table_name(self.table_name) end @@ -3114,15 +3069,13 @@ module ActiveRecord #:nodoc: comma_pair_list(quote_columns(quoter, hash)) end - def object_from_yaml(string) - return string unless string.is_a?(String) && string =~ /^---/ - YAML::load(string) rescue string - end end Base.class_eval do extend ActiveModel::Naming extend QueryCache::ClassMethods + extend ActiveSupport::Benchmarkable + include Validations include Locking::Optimistic, Locking::Pessimistic include AttributeMethods @@ -3130,6 +3083,7 @@ module ActiveRecord #:nodoc: include AttributeMethods::PrimaryKey include AttributeMethods::TimeZoneConversion include AttributeMethods::Dirty + include Attributes, Types include Callbacks, ActiveModel::Observing, Timestamp include Associations, AssociationPreload, NamedScope include ActiveModel::Conversion @@ -3139,6 +3093,7 @@ module ActiveRecord #:nodoc: include AutosaveAssociation, NestedAttributes include Aggregations, Transactions, Reflection, Batches, Calculations, Serialization + end end diff --git a/activerecord/lib/active_record/calculations.rb b/activerecord/lib/active_record/calculations.rb index 646fed1a0b..40242333e5 100644 --- a/activerecord/lib/active_record/calculations.rb +++ b/activerecord/lib/active_record/calculations.rb @@ -53,7 +53,7 @@ module ActiveRecord # # Person.average('age') # => 35.8 def average(column_name, options = {}) - calculate(:avg, column_name, options) + calculate(:average, column_name, options) end # Calculates the minimum value on a given column. The value is returned @@ -62,7 +62,7 @@ module ActiveRecord # # Person.minimum('age') # => 7 def minimum(column_name, options = {}) - calculate(:min, column_name, options) + calculate(:minimum, column_name, options) end # Calculates the maximum value on a given column. The value is returned @@ -71,7 +71,7 @@ module ActiveRecord # # Person.maximum('age') # => 93 def maximum(column_name, options = {}) - calculate(:max, column_name, options) + calculate(:maximum, column_name, options) end # Calculates the sum of values on a given column. The value is returned @@ -123,20 +123,97 @@ module ActiveRecord # Person.sum("2 * age") def calculate(operation, column_name, options = {}) validate_calculation_options(operation, options) - column_name = options[:select] if options[:select] - column_name = '*' if column_name == :all - column = column_for column_name + operation = operation.to_s.downcase + + scope = scope(:find) + + merged_includes = merge_includes(scope ? scope[:include] : [], options[:include]) + + if operation == "count" + if merged_includes.any? + distinct = true + column_name = options[:select] || primary_key + end + + distinct = nil if column_name.to_s =~ /\s*DISTINCT\s+/i + distinct ||= options[:distinct] + else + distinct = nil + end + catch :invalid_query do + relation = if merged_includes.any? + join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(self, merged_includes, construct_join(options[:joins], scope)) + construct_finder_arel_with_included_associations(options, join_dependency) + else + relation = arel_table(options[:from]). + joins(construct_join(options[:joins], scope)). + conditions(construct_conditions(options[:conditions], scope)). + order(options[:order]). + limit(options[:limit]). + offset(options[:offset]) + end if options[:group] - return execute_grouped_calculation(operation, column_name, column, options) + return execute_grouped_calculation(operation, column_name, options, relation) else - return execute_simple_calculation(operation, column_name, column, options) + return execute_simple_calculation(operation, column_name, options.merge(:distinct => distinct), relation) end end 0 end - protected + def execute_simple_calculation(operation, column_name, options, relation) #:nodoc: + column = if column_names.include?(column_name.to_s) + Arel::Attribute.new(arel_table(options[:from] || table_name), + options[:select] || column_name) + else + Arel::SqlLiteral.new(options[:select] || + (column_name == :all ? "*" : column_name.to_s)) + end + + relation = relation.select(operation == 'count' ? column.count(options[:distinct]) : column.send(operation)) + + type_cast_calculated_value(connection.select_value(relation.to_sql), column_for(column_name), operation) + end + + def execute_grouped_calculation(operation, column_name, options, relation) #:nodoc: + group_attr = options[:group].to_s + association = reflect_on_association(group_attr.to_sym) + associated = association && association.macro == :belongs_to # only count belongs_to associations + group_field = associated ? association.primary_key_name : group_attr + group_alias = column_alias_for(group_field) + group_column = column_for group_field + + options[:group] = connection.adapter_name == 'FrontBase' ? group_alias : group_field + + aggregate_alias = column_alias_for(operation, column_name) + + options[:select] = (operation == 'count' && column_name == :all) ? + "COUNT(*) AS count_all" : + Arel::Attribute.new(arel_table, column_name).send(operation).as(aggregate_alias).to_sql + + options[:select] << ", #{group_field} AS #{group_alias}" + + relation = relation.select(options[:select]).group(construct_group(options[:group], options[:having], nil)) + + calculated_data = connection.select_all(relation.to_sql) + + if association + key_ids = calculated_data.collect { |row| row[group_alias] } + key_records = association.klass.base_class.find(key_ids) + key_records = key_records.inject({}) { |hsh, r| hsh.merge(r.id => r) } + end + + calculated_data.inject(ActiveSupport::OrderedHash.new) do |all, row| + key = type_cast_calculated_value(row[group_alias], group_column) + key = key_records[key] if associated + value = row[aggregate_alias] + all[key] = type_cast_calculated_value(value, column_for(column_name), operation) + all + end + end + + protected def construct_count_options_from_args(*args) options = {} column_name = :all @@ -166,111 +243,6 @@ module ActiveRecord [column_name || :all, options] end - def construct_calculation_sql(operation, column_name, options) #:nodoc: - operation = operation.to_s.downcase - options = options.symbolize_keys - - scope = scope(:find) - merged_includes = merge_includes(scope ? scope[:include] : [], options[:include]) - aggregate_alias = column_alias_for(operation, column_name) - column_name = "#{connection.quote_table_name(table_name)}.#{column_name}" if column_names.include?(column_name.to_s) - - if operation == 'count' - if merged_includes.any? - options[:distinct] = true - column_name = options[:select] || [connection.quote_table_name(table_name), primary_key] * '.' - end - - if options[:distinct] - use_workaround = !connection.supports_count_distinct? - end - end - - if options[:distinct] && column_name.to_s !~ /\s*DISTINCT\s+/i - distinct = 'DISTINCT ' - end - sql = "SELECT #{operation}(#{distinct}#{column_name}) AS #{aggregate_alias}" - - # A (slower) workaround if we're using a backend, like sqlite, that doesn't support COUNT DISTINCT. - sql = "SELECT COUNT(*) AS #{aggregate_alias}" if use_workaround - - sql << ", #{options[:group_field]} AS #{options[:group_alias]}" if options[:group] - if options[:from] - sql << " FROM #{options[:from]} " - elsif scope && scope[:from] && !use_workaround - sql << " FROM #{scope[: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[:joins], scope) - - if merged_includes.any? - join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(self, merged_includes, joins) - sql << join_dependency.join_associations.collect{|join| join.association_join }.join - end - - 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]) - - if options[:group] - group_key = connection.adapter_name == 'FrontBase' ? :group_alias : :group_field - sql << " GROUP BY #{options[group_key]} " - end - - if options[:group] && options[:having] - having = sanitize_sql_for_conditions(options[:having]) - - # FrontBase requires identifiers in the HAVING clause and chokes on function calls - if connection.adapter_name == 'FrontBase' - having.downcase! - having.gsub!(/#{operation}\s*\(\s*#{column_name}\s*\)/, aggregate_alias) - end - - sql << " HAVING #{having} " - end - - sql << " ORDER BY #{options[:order]} " if options[:order] - add_limit!(sql, options, scope) - sql << ") #{aggregate_alias}_subquery" if use_workaround - sql - end - - def execute_simple_calculation(operation, column_name, column, options) #:nodoc: - value = connection.select_value(construct_calculation_sql(operation, column_name, options)) - type_cast_calculated_value(value, column, operation) - end - - def execute_grouped_calculation(operation, column_name, column, options) #:nodoc: - group_attr = options[:group].to_s - association = reflect_on_association(group_attr.to_sym) - associated = association && association.macro == :belongs_to # only count belongs_to associations - group_field = associated ? association.primary_key_name : group_attr - group_alias = column_alias_for(group_field) - group_column = column_for group_field - sql = construct_calculation_sql(operation, column_name, options.merge(:group_field => group_field, :group_alias => group_alias)) - calculated_data = connection.select_all(sql) - aggregate_alias = column_alias_for(operation, column_name) - - if association - key_ids = calculated_data.collect { |row| row[group_alias] } - key_records = association.klass.base_class.find(key_ids) - key_records = key_records.inject({}) { |hsh, r| hsh.merge(r.id => r) } - end - - calculated_data.inject(ActiveSupport::OrderedHash.new) do |all, row| - key = type_cast_calculated_value(row[group_alias], group_column) - key = key_records[key] if associated - value = row[aggregate_alias] - all[key] = type_cast_calculated_value(value, column, operation) - all - end - end - private def validate_calculation_options(operation, options = {}) options.assert_valid_keys(CALCULATIONS_OPTIONS) @@ -301,11 +273,10 @@ module ActiveRecord end def type_cast_calculated_value(value, column, operation = nil) - operation = operation.to_s.downcase case operation when 'count' then value.to_i when 'sum' then type_cast_using_column(value || '0', column) - when 'avg' then value && (value.is_a?(Fixnum) ? value.to_f : value).to_d + when 'average' then value && (value.is_a?(Fixnum) ? value.to_f : value).to_d else type_cast_using_column(value, column) end end diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb index 40a25811c4..b25893a1c3 100644 --- a/activerecord/lib/active_record/callbacks.rb +++ b/activerecord/lib/active_record/callbacks.rb @@ -20,7 +20,7 @@ module ActiveRecord # * (6) <tt>after_save</tt> # # That's a total of eight callbacks, which gives you immense power to react and prepare for each state in the - # Active Record lifecycle. The sequence for calling <tt>Base#save</tt> for an existing record is similar, except that each + # Active Record lifecycle. The sequence for calling <tt>Base#save</tt> for an existing record is similar, except that each # <tt>_on_create</tt> callback is replaced by the corresponding <tt>_on_update</tt> callback. # # Examples: @@ -210,7 +210,7 @@ module ActiveRecord # instead of quietly returning +false+. module Callbacks extend ActiveSupport::Concern - include ActiveSupport::NewCallbacks + include ActiveSupport::Callbacks CALLBACKS = [ :after_initialize, :after_find, :before_validation, :after_validation, diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb index 12253eac3f..377f2a44c5 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -211,9 +211,10 @@ module ActiveRecord # calling +checkout+ on this pool. def checkin(conn) @connection_mutex.synchronize do - conn.run_callbacks :checkin - @checked_out.delete conn - @queue.signal + conn.run_callbacks :checkin do + @checked_out.delete conn + @queue.signal + end end end @@ -255,9 +256,10 @@ module ActiveRecord end def checkout_and_verify(c) - c.verify! - c.run_callbacks :checkout - @checked_out << c + c.run_callbacks :checkout do + c.verify! + @checked_out << c + end c end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb index 08601da00a..be89873632 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -53,7 +53,7 @@ module ActiveRecord def delete(sql, name = nil) delete_sql(sql, name) end - + # Checks whether there is currently no transaction active. This is done # by querying the database driver, and does not use the transaction # house-keeping information recorded by #increment_open_transactions and @@ -170,7 +170,7 @@ module ActiveRecord end end end - + # Begins the transaction (and turns off auto-committing). def begin_db_transaction() end @@ -181,33 +181,6 @@ module ActiveRecord # done if the transaction block raises an exception or returns false. def rollback_db_transaction() end - # Alias for <tt>add_limit_offset!</tt>. - def add_limit!(sql, options) - add_limit_offset!(sql, options) if options - end - - # Appends +LIMIT+ and +OFFSET+ options to an SQL statement, or some SQL - # fragment that has the same semantics as LIMIT and OFFSET. - # - # +options+ must be a Hash which contains a +:limit+ option (required) - # and an +:offset+ option (optional). - # - # This method *modifies* the +sql+ parameter. - # - # ===== Examples - # add_limit_offset!('SELECT * FROM suppliers', {:limit => 10, :offset => 50}) - # generates - # SELECT * FROM suppliers LIMIT 10 OFFSET 50 - def add_limit_offset!(sql, options) - if limit = options[:limit] - sql << " LIMIT #{sanitize_limit(limit)}" - if offset = options[:offset] - sql << " OFFSET #{offset.to_i}" - end - end - sql - end - # Appends a locking clause to an SQL statement. # This method *modifies* the +sql+ parameter. # # SELECT * FROM suppliers FOR UPDATE @@ -235,8 +208,8 @@ module ActiveRecord execute "INSERT INTO #{quote_table_name(table_name)} (#{fixture.key_list}) VALUES (#{fixture.value_list})", 'Fixture Insert' end - def empty_insert_statement(table_name) - "INSERT INTO #{quote_table_name(table_name)} VALUES(DEFAULT)" + def empty_insert_statement_value + "VALUES(DEFAULT)" end def case_sensitive_equality_operator diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb index 20787ec510..e731bc84f0 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -411,12 +411,6 @@ module ActiveRecord "DISTINCT #{columns}" end - # ORDER BY clause for the passed order option. - # PostgreSQL overrides this due to its stricter standards compliance. - def add_order_by_for_association_limiting!(sql, options) - sql << " ORDER BY #{options[:order]}" - end - # Adds timestamps (created_at and updated_at) columns to the named table. # ===== Examples # add_timestamps(:suppliers) diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index 78c7a4b697..8fae26b790 100755 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -1,4 +1,3 @@ -require 'benchmark' require 'date' require 'bigdecimal' require 'bigdecimal/util' @@ -12,8 +11,6 @@ require 'active_record/connection_adapters/abstract/connection_pool' require 'active_record/connection_adapters/abstract/connection_specification' require 'active_record/connection_adapters/abstract/query_cache' -require 'active_support/core_ext/benchmark' - module ActiveRecord module ConnectionAdapters # :nodoc: # ActiveRecord supports multiple database systems. AbstractAdapter and @@ -33,6 +30,7 @@ module ActiveRecord include Quoting, DatabaseStatements, SchemaStatements include QueryCache include ActiveSupport::Callbacks + define_callbacks :checkout, :checkin @@row_even = true @@ -75,7 +73,7 @@ module ActiveRecord def supports_ddl_transactions? false end - + # Does this adapter support savepoints? PostgreSQL and MySQL do, SQLite # does not. def supports_savepoints? @@ -193,6 +191,7 @@ module ActiveRecord end def log_info(sql, name, ms) + @runtime += ms if @logger && @logger.debug? name = '%s (%.1fms)' % [name || 'SQL', ms] @logger.debug(format_log_entry(name, sql.squeeze(' '))) @@ -200,13 +199,8 @@ module ActiveRecord end protected - def log(sql, name) - event = ActiveSupport::Orchestra.instrument(:sql, :sql => sql, :name => name) do - yield if block_given? - end - @runtime += event.duration - log_info(sql, name, event.duration) - event.result + def log(sql, name, &block) + ActiveSupport::Notifications.instrument(:sql, :sql => sql, :name => name, &block) rescue Exception => e # Log message and raise exception. # Set last_verification to 0, so that connection gets verified diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index 1072eb7ac1..ad36ff22e3 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -211,7 +211,7 @@ module ActiveRecord def supports_migrations? #:nodoc: true end - + def supports_primary_key? #:nodoc: true end @@ -334,6 +334,7 @@ module ActiveRecord super sql, name id_value || @connection.insert_id end + alias :create :insert_sql def update_sql(sql, name = nil) #:nodoc: super @@ -370,18 +371,6 @@ module ActiveRecord execute("RELEASE SAVEPOINT #{current_savepoint_name}") end - 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.to_i}, #{limit}" - end - end - end - - # SCHEMA STATEMENTS ======================================== def structure_dump #:nodoc: diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 84cf1ad9fd..1d52c5ec14 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -510,6 +510,7 @@ module ActiveRecord end end end + alias :create :insert # create a 2D array representing the result set def result_as_array(res) #:nodoc: @@ -928,20 +929,6 @@ module ActiveRecord sql << order_columns * ', ' end - # Returns an ORDER BY clause for the passed order option. - # - # PostgreSQL does not allow arbitrary ordering when using DISTINCT ON, so we work around this - # by wrapping the +sql+ string as a sub-select and ordering in that query. - def add_order_by_for_association_limiting!(sql, options) #:nodoc: - return sql if options[:order].blank? - - order = options[:order].split(',').collect { |s| s.strip }.reject(&:blank?) - order.map! { |s| 'DESC' if s =~ /\bdesc$/i } - order = order.zip((0...order.size).to_a).map { |s,i| "id_list.alias_#{i} #{s}" }.join(', ') - - sql.replace "SELECT * FROM (#{sql}) AS id_list ORDER BY #{order}" - end - protected # Returns the version of the connected PostgreSQL version. def postgresql_version diff --git a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb index 5ed7094169..c9c2892ba4 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb @@ -11,11 +11,11 @@ module ActiveRecord raise ArgumentError, "No database file specified. Missing argument: database" end - # Allow database path relative to RAILS_ROOT, but only if + # Allow database path relative to Rails.root, but only if # the database path is not the special path that tells # Sqlite to build a database only in memory. - if Object.const_defined?(:RAILS_ROOT) && ':memory:' != config[:database] - config[:database] = File.expand_path(config[:database], RAILS_ROOT) + if Object.const_defined?(:Rails) && ':memory:' != config[:database] + config[:database] = File.expand_path(config[:database], Rails.root) end end end @@ -91,7 +91,7 @@ module ActiveRecord def supports_add_column? sqlite_version >= '3.1.6' end - + def disconnect! super @connection.close rescue nil @@ -163,6 +163,7 @@ module ActiveRecord def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc: super || @connection.last_insert_row_id end + alias :create :insert_sql def select_rows(sql, name = nil) execute(sql, name).map do |row| @@ -289,8 +290,8 @@ module ActiveRecord alter_table(table_name, :rename => {column_name.to_s => new_column_name.to_s}) end - def empty_insert_statement(table_name) - "INSERT INTO #{table_name} VALUES(NULL)" + def empty_insert_statement_value + "VALUES(NULL)" end protected @@ -337,7 +338,7 @@ module ActiveRecord (options[:rename][column.name] || options[:rename][column.name.to_sym] || column.name) : column.name - + @definition.column(column_name, column.type, :limit => column.limit, :default => column.default, :null => column.null) diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb index cec5ca3324..c8cd79a2b0 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -89,12 +89,14 @@ module ActiveRecord attribute_names.uniq! begin - affected_rows = connection.update(<<-end_sql, "#{self.class.name} Update with optimistic locking") - UPDATE #{self.class.quoted_table_name} - SET #{quoted_comma_pair_list(connection, attributes_with_quotes(false, false, attribute_names))} - WHERE #{self.class.primary_key} = #{quote_value(id)} - AND #{self.class.quoted_locking_column} = #{quote_value(previous_value)} - end_sql + arel_table = self.class.arel_table(self.class.table_name) + + affected_rows = arel_table.where( + arel_table[self.class.primary_key].eq(quoted_id).and( + arel_table[self.class.locking_column].eq(quote_value(previous_value)) + ) + ).update(arel_attributes_values(false, false, attribute_names)) + unless affected_rows == 1 raise ActiveRecord::StaleObjectError, "Attempted to update a stale object" @@ -116,12 +118,13 @@ module ActiveRecord lock_col = self.class.locking_column previous_value = send(lock_col).to_i - affected_rows = connection.delete( - "DELETE FROM #{self.class.quoted_table_name} " + - "WHERE #{connection.quote_column_name(self.class.primary_key)} = #{quoted_id} " + - "AND #{self.class.quoted_locking_column} = #{quote_value(previous_value)}", - "#{self.class.name} Destroy" - ) + arel_table = self.class.arel_table(self.class.table_name) + + affected_rows = arel_table.where( + arel_table[self.class.primary_key].eq(quoted_id).and( + arel_table[self.class.locking_column].eq(quote_value(previous_value)) + ) + ).delete unless affected_rows == 1 raise ActiveRecord::StaleObjectError, "Attempted to delete a stale object" diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index c2bc26f33e..c218c5bfd1 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -106,7 +106,7 @@ module ActiveRecord # # The Rails package has several tools to help create and apply migrations. # - # To generate a new migration, you can use + # To generate a new migration, you can use # script/generate migration MyNewMigration # # where MyNewMigration is the name of your migration. The generator will @@ -124,16 +124,16 @@ module ActiveRecord # def self.up # add_column :tablenames, :fieldname, :string # end - # + # # def self.down # remove_column :tablenames, :fieldname # end # end - # + # # To run migrations against the currently configured database, use # <tt>rake db:migrate</tt>. This will update the database by running all of the # pending migrations, creating the <tt>schema_migrations</tt> table - # (see "About the schema_migrations table" section below) if missing. It will also + # (see "About the schema_migrations table" section below) if missing. It will also # invoke the db:schema:dump task, which will update your db/schema.rb file # to match the structure of your database. # @@ -243,7 +243,7 @@ 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: @@ -256,7 +256,7 @@ module ActiveRecord # off by setting: # # config.active_record.timestamped_migrations = false - # + # # In environment.rb. # class Migration @@ -340,10 +340,6 @@ module ActiveRecord self.verbose = save end - def connection - ActiveRecord::Base.connection - end - def method_missing(method, *arguments, &block) arg_list = arguments.map(&:inspect) * ', ' @@ -351,7 +347,7 @@ module ActiveRecord unless arguments.empty? || method == :execute arguments[0] = Migrator.proper_table_name(arguments.first) end - connection.send(method, *arguments, &block) + Base.connection.send(method, *arguments, &block) end end end @@ -403,7 +399,7 @@ module ActiveRecord def down(migrations_path, target_version = nil) self.new(:down, migrations_path, target_version).migrate end - + def run(direction, migrations_path, target_version) self.new(direction, migrations_path, target_version).run end @@ -413,7 +409,8 @@ module ActiveRecord end def get_all_versions - Base.connection.select_values("SELECT version FROM #{schema_migrations_table_name}").map(&:to_i).sort + table = Arel::Table.new(schema_migrations_table_name) + Base.connection.select_values(table.project(table['version']).to_sql).map(&:to_i).sort end def current_version @@ -447,17 +444,17 @@ module ActiveRecord def initialize(direction, migrations_path, target_version = nil) raise StandardError.new("This database does not yet support migrations") unless Base.connection.supports_migrations? Base.connection.initialize_schema_migrations_table - @direction, @migrations_path, @target_version = direction, migrations_path, target_version + @direction, @migrations_path, @target_version = direction, migrations_path, target_version end def current_version migrated.last || 0 end - + def current_migration migrations.detect { |m| m.version == current_version } end - + def run target = migrations.detect { |m| m.version == @target_version } raise UnknownMigrationVersionError.new(@target_version) if target.nil? @@ -474,14 +471,14 @@ module ActiveRecord if target.nil? && !@target_version.nil? && @target_version > 0 raise UnknownMigrationVersionError.new(@target_version) end - + start = up? ? 0 : (migrations.index(current) || 0) finish = migrations.index(target) || migrations.size - 1 runnable = migrations[start..finish] - + # skip the last migration if we're headed down, but not ALL the way down runnable.pop if down? && !target.nil? - + runnable.each do |migration| Base.logger.info "Migrating to #{migration.name} (#{migration.version})" @@ -509,28 +506,28 @@ module ActiveRecord def migrations @migrations ||= begin files = Dir["#{@migrations_path}/[0-9]*_*.rb"] - + migrations = files.inject([]) do |klasses, file| version, name = file.scan(/([0-9]+)_([_a-z0-9]*).rb/).first - + raise IllegalMigrationNameError.new(file) unless version version = version.to_i - + if klasses.detect { |m| m.version == version } - raise DuplicateMigrationVersionError.new(version) + raise DuplicateMigrationVersionError.new(version) end if klasses.detect { |m| m.name == name.camelize } - raise DuplicateMigrationNameError.new(name.camelize) + raise DuplicateMigrationNameError.new(name.camelize) end - + migration = MigrationProxy.new migration.name = name.camelize migration.version = version migration.filename = file klasses << migration end - + migrations = migrations.sort_by(&:version) down? ? migrations.reverse : migrations end @@ -547,15 +544,15 @@ module ActiveRecord private def record_version_state_after_migrating(version) - sm_table = self.class.schema_migrations_table_name + table = Arel::Table.new(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}'") + @migrated_versions.delete(version) + table.where(table["version"].eq(version.to_s)).delete else - @migrated_versions.push(version.to_i).sort! - Base.connection.insert("INSERT INTO #{sm_table} (version) VALUES ('#{version}')") + @migrated_versions.push(version).sort! + table.insert table["version"] => version.to_s end end diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index f65ee9465e..f61a4a6c73 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -250,6 +250,8 @@ module ActiveRecord assign_nested_attributes_for_#{type}_association(:#{association_name}, attributes) end }, __FILE__, __LINE__ + + add_autosave_association_callbacks(reflection) else raise ArgumentError, "No association found for name `#{association_name}'. Has it been defined yet?" end diff --git a/activerecord/lib/active_record/notifications.rb b/activerecord/lib/active_record/notifications.rb new file mode 100644 index 0000000000..562a5b91f4 --- /dev/null +++ b/activerecord/lib/active_record/notifications.rb @@ -0,0 +1,5 @@ +require 'active_support/notifications' + +ActiveSupport::Notifications.subscribe("sql") do |name, before, after, result, instrumenter_id, payload| + ActiveRecord::Base.connection.log_info(payload[:sql], name, after - before) +end diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb new file mode 100644 index 0000000000..5f0eec754f --- /dev/null +++ b/activerecord/lib/active_record/relation.rb @@ -0,0 +1,127 @@ +module ActiveRecord + class Relation + delegate :to_sql, :to => :relation + delegate :length, :collect, :find, :map, :each, :to => :to_a + attr_reader :relation, :klass + + def initialize(klass, relation) + @klass, @relation = klass, relation + @readonly = false + @associations_to_preload = [] + @eager_load_associations = [] + end + + def preload(association) + @associations_to_preload += association + self + end + + def eager_load(association) + @eager_load_associations += association + self + end + + def readonly + @readonly = true + self + end + + def to_a + records = if @eager_load_associations.any? + catch :invalid_query do + return @klass.send(:find_with_associations, { + :select => @relation.send(:select_clauses).join(', '), + :joins => @relation.joins(relation), + :group => @relation.send(:group_clauses).join(', '), + :order => @relation.send(:order_clauses).join(', '), + :conditions => @relation.send(:where_clauses).join("\n\tAND "), + :limit => @relation.taken, + :offset => @relation.skipped + }, + ActiveRecord::Associations::ClassMethods::JoinDependency.new(@klass, @eager_load_associations, nil)) + end + [] + else + @klass.find_by_sql(@relation.to_sql) + end + + @klass.send(:preload_associations, records, @associations_to_preload) unless @associations_to_preload.empty? + records.each { |record| record.readonly! } if @readonly + + records + end + + def first + @relation = @relation.take(1) + to_a.first + end + + def select(selects) + selects.blank? ? self : Relation.new(@klass, @relation.project(selects)) + end + + def group(groups) + groups.blank? ? self : Relation.new(@klass, @relation.group(groups)) + end + + def order(orders) + orders.blank? ? self : Relation.new(@klass, @relation.order(orders)) + end + + def limit(limits) + limits.blank? ? self : Relation.new(@klass, @relation.take(limits)) + end + + def offset(offsets) + offsets.blank? ? self : Relation.new(@klass, @relation.skip(offsets)) + end + + def on(join) + join.blank? ? self : Relation.new(@klass, @relation.on(join)) + end + + def joins(join, join_type = nil) + if join.blank? + self + else + join = case join + when String + @relation.join(join) + when Hash, Array, Symbol + if @klass.send(:array_of_strings?, join) + @relation.join(join.join(' ')) + else + @relation.join(@klass.send(:build_association_joins, join)) + end + else + @relation.join(join, join_type) + end + Relation.new(@klass, join) + end + end + + def conditions(conditions) + if conditions.blank? + self + else + conditions = @klass.send(:merge_conditions, conditions) if [String, Hash, Array].include?(conditions.class) + Relation.new(@klass, @relation.where(conditions)) + end + end + + def respond_to?(method) + @relation.respond_to?(method) || Array.method_defined?(method) || super + end + + private + def method_missing(method, *args, &block) + if @relation.respond_to?(method) + @relation.send(method, *args, &block) + elsif Array.method_defined?(method) + to_a.send(method, *args, &block) + else + super + end + end + end +end diff --git a/activerecord/lib/active_record/types.rb b/activerecord/lib/active_record/types.rb new file mode 100644 index 0000000000..74f569352b --- /dev/null +++ b/activerecord/lib/active_record/types.rb @@ -0,0 +1,38 @@ +module ActiveRecord + module Types + extend ActiveSupport::Concern + + module ClassMethods + + def attribute_types + attribute_types = {} + columns.each do |column| + options = {} + options[:time_zone_aware] = time_zone_aware?(column.name) + options[:serialize] = serialized_attributes[column.name] + + attribute_types[column.name] = to_type(column, options) + end + attribute_types + end + + private + + def to_type(column, options = {}) + type_class = if options[:time_zone_aware] + Type::TimeWithZone + elsif options[:serialize] + Type::Serialize + elsif [ :integer, :float, :decimal ].include?(column.type) + Type::Number + else + Type::Object + end + + type_class.new(column, options) + end + + end + + end +end diff --git a/activerecord/lib/active_record/types/number.rb b/activerecord/lib/active_record/types/number.rb new file mode 100644 index 0000000000..cfbe877575 --- /dev/null +++ b/activerecord/lib/active_record/types/number.rb @@ -0,0 +1,30 @@ +module ActiveRecord + module Type + class Number < Object + + def boolean(value) + value = cast(value) + !(value.nil? || value.zero?) + end + + def precast(value) + convert_number_column_value(value) + end + + private + + def convert_number_column_value(value) + if value == false + 0 + elsif value == true + 1 + elsif value.is_a?(String) && value.blank? + nil + else + value + end + end + + end + end +end
\ No newline at end of file diff --git a/activerecord/lib/active_record/types/object.rb b/activerecord/lib/active_record/types/object.rb new file mode 100644 index 0000000000..ec3f861abd --- /dev/null +++ b/activerecord/lib/active_record/types/object.rb @@ -0,0 +1,37 @@ +module ActiveRecord + module Type + module Casting + + def cast(value) + typecaster.type_cast(value) + end + + def precast(value) + value + end + + def boolean(value) + cast(value).present? + end + + # Attributes::Typecasting stores appendable? types (e.g. serialized Arrays) when typecasting reads. + def appendable? + false + end + + end + + class Object + include Casting + + attr_reader :name, :options + attr_reader :typecaster + + def initialize(typecaster = nil, options = {}) + @typecaster, @options = typecaster, options + end + + end + + end +end
\ No newline at end of file diff --git a/activerecord/lib/active_record/types/serialize.rb b/activerecord/lib/active_record/types/serialize.rb new file mode 100644 index 0000000000..7b6af1981f --- /dev/null +++ b/activerecord/lib/active_record/types/serialize.rb @@ -0,0 +1,33 @@ +module ActiveRecord + module Type + class Serialize < Object + + def cast(value) + unserialize(value) + end + + def appendable? + true + end + + protected + + def unserialize(value) + unserialized_object = object_from_yaml(value) + + if unserialized_object.is_a?(@options[:serialize]) || unserialized_object.nil? + unserialized_object + else + raise SerializationTypeMismatch, + "#{name} was supposed to be a #{@options[:serialize]}, but was a #{unserialized_object.class.to_s}" + end + end + + def object_from_yaml(string) + return string unless string.is_a?(String) && string =~ /^---/ + YAML::load(string) rescue string + end + + end + end +end
\ No newline at end of file diff --git a/activerecord/lib/active_record/types/time_with_zone.rb b/activerecord/lib/active_record/types/time_with_zone.rb new file mode 100644 index 0000000000..3a8b9292f9 --- /dev/null +++ b/activerecord/lib/active_record/types/time_with_zone.rb @@ -0,0 +1,20 @@ +module ActiveRecord + module Type + class TimeWithZone < Object + + def cast(time) + time = super(time) + time.acts_like?(:time) ? time.in_time_zone : time + end + + def precast(time) + unless time.acts_like?(:time) + time = time.is_a?(String) ? ::Time.zone.parse(time) : time.to_time rescue time + end + time = time.in_time_zone rescue nil if time + super(time) + end + + end + end +end diff --git a/activerecord/lib/active_record/types/unknown.rb b/activerecord/lib/active_record/types/unknown.rb new file mode 100644 index 0000000000..f832c7b304 --- /dev/null +++ b/activerecord/lib/active_record/types/unknown.rb @@ -0,0 +1,37 @@ +module ActiveRecord + module Type + # Useful for handling attributes not mapped to types. Performs some boolean typecasting, + # but otherwise leaves the value untouched. + class Unknown + + def cast(value) + value + end + + def precast(value) + value + end + + # Attempts typecasting to handle numeric, false and blank values. + def boolean(value) + empty = (numeric?(value) && value.to_i.zero?) || false?(value) || value.blank? + !empty + end + + def appendable? + false + end + + protected + + def false?(value) + ActiveRecord::ConnectionAdapters::Column::FALSE_VALUES.include?(value) + end + + def numeric?(value) + Numeric === value || value !~ /[^0-9]/ + end + + end + end +end
\ No newline at end of file diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb index ab79b520a2..e8a2a72735 100644 --- a/activerecord/lib/active_record/validations.rb +++ b/activerecord/lib/active_record/validations.rb @@ -1,5 +1,3 @@ -require 'active_support/core_ext/integer/even_odd' - module ActiveRecord # Raised by <tt>save!</tt> and <tt>create!</tt> when the record is invalid. Use the # +record+ method to retrieve the record which did not validate. @@ -17,94 +15,10 @@ module ActiveRecord end end - class Errors < ActiveModel::Errors - class << self - def default_error_messages - message = "Errors.default_error_messages has been deprecated. Please use I18n.translate('activerecord.errors.messages')." - ActiveSupport::Deprecation.warn(message) - - I18n.translate 'activerecord.errors.messages' - end - end - - # Returns all the full error messages in an array. - # - # class Company < ActiveRecord::Base - # validates_presence_of :name, :address, :email - # validates_length_of :name, :in => 5..30 - # end - # - # company = Company.create(:address => '123 First St.') - # company.errors.full_messages # => - # ["Name is too short (minimum is 5 characters)", "Name can't be blank", "Address can't be blank"] - def full_messages(options = {}) - full_messages = [] - - each do |attribute, messages| - messages = Array.wrap(messages) - next if messages.empty? - - if attribute == :base - messages.each {|m| full_messages << m } - else - attr_name = @base.class.human_attribute_name(attribute.to_s) - prefix = attr_name + I18n.t('activerecord.errors.format.separator', :default => ' ') - messages.each do |m| - full_messages << "#{prefix}#{m}" - end - end - end - - full_messages - end - - # Translates an error message in it's default scope (<tt>activerecord.errrors.messages</tt>). - # Error messages are first looked up in <tt>models.MODEL.attributes.ATTRIBUTE.MESSAGE</tt>, if it's not there, - # it's looked up in <tt>models.MODEL.MESSAGE</tt> and if that is not there it returns the translation of the - # default message (e.g. <tt>activerecord.errors.messages.MESSAGE</tt>). The translated model name, - # translated attribute name and the value are available for interpolation. - # - # When using inheritance in your models, it will check all the inherited models too, but only if the model itself - # hasn't been found. Say you have <tt>class Admin < User; end</tt> and you wanted the translation for the <tt>:blank</tt> - # error +message+ for the <tt>title</tt> +attribute+, it looks for these translations: - # - # <ol> - # <li><tt>activerecord.errors.models.admin.attributes.title.blank</tt></li> - # <li><tt>activerecord.errors.models.admin.blank</tt></li> - # <li><tt>activerecord.errors.models.user.attributes.title.blank</tt></li> - # <li><tt>activerecord.errors.models.user.blank</tt></li> - # <li><tt>activerecord.errors.messages.blank</tt></li> - # <li>any default you provided through the +options+ hash (in the activerecord.errors scope)</li> - # </ol> - def generate_message(attribute, message = :invalid, options = {}) - message, options[:default] = options[:default], message if options[:default].is_a?(Symbol) - - defaults = @base.class.self_and_descendants_from_active_record.map do |klass| - [ :"models.#{klass.name.underscore}.attributes.#{attribute}.#{message}", - :"models.#{klass.name.underscore}.#{message}" ] - end - - defaults << options.delete(:default) - defaults = defaults.compact.flatten << :"messages.#{message}" - - key = defaults.shift - value = @base.respond_to?(attribute) ? @base.send(attribute) : nil - - options = { :default => defaults, - :model => @base.class.human_name, - :attribute => @base.class.human_attribute_name(attribute.to_s), - :value => value, - :scope => [:activerecord, :errors] - }.merge(options) - - I18n.translate(key, options) - end - end - module Validations extend ActiveSupport::Concern - include ActiveSupport::Callbacks + include ActiveSupport::DeprecatedCallbacks include ActiveModel::Validations included do @@ -165,11 +79,6 @@ module ActiveRecord errors.empty? end - - # Returns the Errors object that holds all information about attribute error messages. - def errors - @errors ||= Errors.new(self) - end end end end diff --git a/activerecord/lib/active_record/validator.rb b/activerecord/lib/active_record/validator.rb deleted file mode 100644 index 83a33f4dcd..0000000000 --- a/activerecord/lib/active_record/validator.rb +++ /dev/null @@ -1,68 +0,0 @@ -module ActiveRecord #:nodoc: - - # A simple base class that can be used along with ActiveRecord::Base.validates_with - # - # class Person < ActiveRecord::Base - # validates_with MyValidator - # end - # - # class MyValidator < ActiveRecord::Validator - # def validate - # if some_complex_logic - # record.errors[:base] = "This record is invalid" - # end - # end - # - # private - # def some_complex_logic - # # ... - # end - # end - # - # Any class that inherits from ActiveRecord::Validator will have access to <tt>record</tt>, - # which is an instance of the record being validated, and must implement a method called <tt>validate</tt>. - # - # class Person < ActiveRecord::Base - # validates_with MyValidator - # end - # - # class MyValidator < ActiveRecord::Validator - # def validate - # record # => The person instance being validated - # options # => Any non-standard options passed to validates_with - # end - # end - # - # To cause a validation error, you must add to the <tt>record<tt>'s errors directly - # from within the validators message - # - # class MyValidator < ActiveRecord::Validator - # def validate - # record.errors[:base] << "This is some custom error message" - # record.errors[:first_name] << "This is some complex validation" - # # etc... - # end - # end - # - # To add behavior to the initialize method, use the following signature: - # - # class MyValidator < ActiveRecord::Validator - # def initialize(record, options) - # super - # @my_custom_field = options[:field_name] || :first_name - # end - # end - # - class Validator - attr_reader :record, :options - - def initialize(record, options) - @record = record - @options = options - end - - def validate - raise "You must override this method" - end - end -end |