diff options
26 files changed, 487 insertions, 483 deletions
diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000000..30755285a1 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "arel"] + path = arel + url = git://github.com/miloops/arel.git diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index 5e9ce0600a..ab271291c5 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -25,11 +25,15 @@ activesupport_path = "#{File.dirname(__FILE__)}/../../activesupport/lib" $:.unshift(activesupport_path) if File.directory?(activesupport_path) require 'active_support' +arel_path = "#{File.dirname(__FILE__)}/../../arel/lib" +$:.unshift(arel_path) if File.directory?(arel_path) +require 'arel' + begin require 'active_model' rescue LoadError $:.unshift "#{File.dirname(__FILE__)}/../../activemodel/lib" - require 'active_model' + require 'active_model' end module ActiveRecord @@ -48,6 +52,7 @@ module ActiveRecord autoload :Associations, 'active_record/associations' autoload :AttributeMethods, 'active_record/attribute_methods' 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' @@ -90,4 +95,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/associations.rb b/activerecord/lib/active_record/associations.rb index 66aa9332c8..60da632b3b 100755 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -516,14 +516,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: @@ -560,7 +560,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 @@ -669,7 +669,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: # @@ -836,8 +836,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: @@ -933,7 +933,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 @@ -949,8 +949,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: @@ -1055,8 +1055,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: @@ -1198,8 +1198,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, @@ -1412,12 +1412,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 @@ -1629,7 +1629,7 @@ module ActiveRecord options[:extend] = create_extension_modules(association_id, extension, options[:extend]) reflection = create_reflection(:has_and_belongs_to_many, association_id, options, self) - + if reflection.association_foreign_key == reflection.primary_key_name raise HasAndBelongsToManyAssociationForeignKeyNeeded.new(reflection) end @@ -1661,24 +1661,29 @@ module ActiveRecord def construct_finder_sql_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]) + joins = join_dependency.join_associations.collect{|join| join.association_join }.join + joins << construct_join(options[:joins], scope) + + conditions = construct_conditions(options[:conditions], scope) || '' + conditions << construct_limited_ids_condition(conditions, options, join_dependency) if !using_limitable_reflections?(join_dependency.reflections) && ((scope && scope[:limit]) || options[:limit]) + + relation = arel_table((scope && scope[:from]) || options[:from]). + join(joins). + where(conditions). + project(column_aliases(join_dependency)). + group(construct_group(options[:group], options[:having], scope)). + order(construct_order(options[:order], scope) + ) - 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) + relation = relation.take(construct_limit(options[:limit], scope)) if using_limitable_reflections?(join_dependency.reflections) - return sanitize_sql(sql) + return sanitize_sql(relation.to_sql) end - def add_limited_ids_condition!(sql, options, join_dependency) + def construct_limited_ids_condition(where, 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}) " + "#{where.blank? ? '' : ' AND '} #{connection.quote_table_name table_name}.#{primary_key} IN (#{id_list}) " else throw :invalid_query end @@ -1694,47 +1699,18 @@ module ActiveRecord 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| + options[:joins] = 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 - - 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} " - - if is_distinct - sql << distinct_join_associations.collect { |assoc| assoc.association_join }.join - add_joins!(sql, options[:joins], scope) - 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 - - add_limit!(sql, options, scope) + }.flatten.compact.uniq.collect { |assoc| assoc.association_join }.join - return sanitize_sql(sql) + construct_finder_sql(options.merge( + :select => connection.distinct("#{connection.quote_table_name table_name}.#{primary_key}", construct_order(options[:order], scope(:find)).join(",")) + ) + ) end def tables_in_string(string) @@ -1838,10 +1814,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" 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 e21ef90391..2a1d645859 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -36,7 +36,7 @@ module ActiveRecord return @target.size if loaded? return count end - + protected def target_reflection_has_associated_record? if @reflection.through_reflection.macro == :belongs_to && @owner[@reflection.through_reflection.primary_key_name].blank? @@ -52,7 +52,7 @@ module ActiveRecord options[:joins] = construct_joins(options[:joins]) options[:include] = @reflection.source_reflection.options[:include] if options[:include].nil? 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 8e7ce33814..c172e7b8f9 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/base.rb b/activerecord/lib/active_record/base.rb index ce93ea8eee..2b50333682 100755 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -426,7 +426,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+. @@ -460,11 +460,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 @@ -669,7 +669,11 @@ module ActiveRecord #:nodoc: # This is an alias for find(:all). You can pass in all the same arguments to this method as you can # to find(:all) def all(*args) - find(:all, *args) + if args.empty? && !scoped?(:find) + arel_table + else + construct_finder_arel(*args) + end end # Executes a custom SQL query against your database and returns all the results. The results will @@ -865,26 +869,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.relation + + if conditions = construct_conditions(conditions, scope) + relation = relation.where(Arel::SqlLiteral.new(conditions)) + end 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 = relation.order(construct_order(options[:order], scope)).take(construct_limit(options[:limit], scope)) else - add_order!(select_sql, options[:order], nil) - sql.concat(select_sql) + relation = relation.order(construct_order(options[:order], nil)) end - connection.update(sql, "#{name} Update") + relation.update(sanitize_sql_for_assignment(updates)) end # Destroys the records matching +conditions+ by instantiating each @@ -935,9 +936,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.where(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. @@ -1140,7 +1143,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, @@ -1377,7 +1380,7 @@ module ActiveRecord #:nodoc: def self_and_descendants_from_active_record#nodoc: klass = self classes = [klass] - while klass != klass.base_class + while klass != klass.base_class classes << klass = klass.superclass end classes @@ -1534,6 +1537,12 @@ module ActiveRecord #:nodoc: "(#{segments.join(') AND (')})" unless segments.empty? end + + def arel_table(table = nil) + table = table_name if table.blank? + Relation.new(self, table) + end + private def find_initial(options) options.update(:limit => 1) @@ -1713,22 +1722,81 @@ 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 + arel_table(options[:from]). + join(construct_join(options[:joins], scope)). + where(construct_conditions(options[:conditions], scope)). + project(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)). + take(construct_limit(options[:limit], scope)). + skip(construct_offset(options[:offset], scope) + ) + end - add_joins!(sql, options[:joins], scope) - add_conditions!(sql, options[:conditions], scope) + def construct_finder_sql(options, scope = scope(:find)) + construct_finder_arel(options, scope).relation.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 + join_dependency = ActiveRecord::Associations::ClassMethods::InnerJoinDependency.new(self, merged_joins, nil) + " #{join_dependency.join_associations.collect { |assoc| assoc.association_join }.join} " + 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 @@ -1766,44 +1834,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) @@ -1812,38 +1842,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 @@ -1984,7 +1986,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__ @@ -2462,7 +2464,7 @@ module ActiveRecord #:nodoc: # name # end # end - # + # # user = User.find_by_name('Phusion') # user_path(user) # => "/users/Phusion" def to_param @@ -2508,12 +2510,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 @@ -2525,12 +2527,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 @@ -2554,11 +2556,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" - ) + arel_table(true).where(arel_table[self.class.primary_key].eq(id)).delete end freeze @@ -2701,12 +2699,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) @@ -2850,14 +2848,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? + arel_table(true).where(arel_table[self.class.primary_key].eq(id)).update(attributes_with_values) end # Creates a record with values matching those of the instance attributes @@ -2867,18 +2860,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? + 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(', ')})" + 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 @@ -2947,6 +2937,11 @@ module ActiveRecord #:nodoc: default end + def arel_table(reload = nil) + @arel_table = Relation.new(self.class, self.class.table_name) if reload || @arel_table.nil? + @arel_table + end + # Returns a copy of the attributes hash where all the values have been safely quoted for use in # an SQL statement. def attributes_with_quotes(include_primary_key = true, include_readonly_attributes = true, attribute_names = @attributes.keys) @@ -2967,6 +2962,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) + value = read_attribute(name) + + if include_readonly_attributes || (!include_readonly_attributes && !self.class.readonly_attributes.include?(name)) + # We need explicit to_yaml because quote() does not properly convert Time/Date fields to YAML. + if value && self.class.serialized_attributes.has_key?(name) && (value.acts_like?(:date) || value.acts_like?(:time)) + value = value.to_yaml + end + attrs[arel_table[name]] = (value.is_a?(Hash) || value.is_a?(Array)) ? value.to_yaml : value + end + end + end + attrs + end + # Quote strings appropriately for SQL statements. def quote_value(value, column = nil) self.class.connection.quote(value, column) diff --git a/activerecord/lib/active_record/calculations.rb b/activerecord/lib/active_record/calculations.rb index 727f4c1dc6..5c7247b9c0 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,95 @@ 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]) + joins = construct_join(options[:joins], scope) + + if merged_includes.any? + join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(self, merged_includes, joins) + joins << join_dependency.join_associations.collect{|join| join.association_join }.join + end + + 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 + conditions = construct_conditions(options[:conditions], scope) + conditions << construct_limited_ids_condition(conditions, options, join_dependency) if join_dependency && !using_limitable_reflections?(join_dependency.reflections) && ((scope && scope[:limit]) || options[:limit]) + if options[:group] - return execute_grouped_calculation(operation, column_name, column, options) + return execute_grouped_calculation(operation, column_name, options.merge(:conditions => conditions, :joins => joins, :distinct => distinct)) else - return execute_simple_calculation(operation, column_name, column, options) + return execute_simple_calculation(operation, column_name, options.merge(:conditions => conditions, :joins => joins, :distinct => distinct)) end end 0 end - protected + def execute_simple_calculation(operation, column_name, options) #: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 + + value = construct_finder_sql(options.merge( + :select => operation == 'count' ? column.count(options[:distinct]) : column.send(operation) + ), nil) + + type_cast_calculated_value(connection.select_value(value), column_for(column_name), operation) + end + + def execute_grouped_calculation(operation, column_name, 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 + + 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}" + + calculated_data = connection.select_all(construct_finder_sql(options, nil)) + + 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,108 +241,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]} " - 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 = {}) @@ -299,11 +272,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/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 2473c772e3..b2c5c78bf7 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/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index 83cb9cff15..9173c8bca3 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -212,7 +212,7 @@ module ActiveRecord def supports_migrations? #:nodoc: true end - + def supports_savepoints? #:nodoc: true end @@ -331,6 +331,7 @@ module ActiveRecord super sql, name id_value || @connection.insert_id end + alias :create :insert_sql def update_sql(sql, name = nil) #:nodoc: super @@ -367,18 +368,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 e77ae93c21..fc8ca357e5 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -68,7 +68,7 @@ module ActiveRecord # depending on the server specifics super end - + # Maps PostgreSQL-specific data types to logical Rails types. def simplified_type(field_type) case field_type @@ -103,7 +103,7 @@ module ActiveRecord :string # Arrays when /^\D+\[\]$/ - :string + :string # Object identifier types when /^oid$/ :integer @@ -112,7 +112,7 @@ module ActiveRecord super end end - + # Extracts the value from a PostgreSQL column default definition. def self.extract_value_from_default(default) case default @@ -273,7 +273,7 @@ module ActiveRecord def supports_ddl_transactions? true end - + def supports_savepoints? true end @@ -498,6 +498,7 @@ module ActiveRecord end end end + alias :create :insert # create a 2D array representing the result set def result_as_array(res) #:nodoc: @@ -564,7 +565,7 @@ module ActiveRecord def rollback_db_transaction execute "ROLLBACK" end - + if defined?(PGconn::PQTRANS_IDLE) # The ruby-pg driver supports inspecting the transaction status, # while the ruby-postgres driver does not. @@ -909,20 +910,6 @@ module ActiveRecord sql = "DISTINCT ON (#{columns}) #{columns}, " 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. @@ -1044,7 +1031,7 @@ module ActiveRecord if res.ftype(cell_index) == MONEY_COLUMN_TYPE_OID # Because money output is formatted according to the locale, there are two # cases to consider (note the decimal separators): - # (1) $12,345,678.12 + # (1) $12,345,678.12 # (2) $12.345.678,12 case column = row[cell_index] when /^-?\D+[\d,]+\.\d{2}$/ # (1) diff --git a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb index c0f5046bff..e939701d63 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb @@ -108,7 +108,7 @@ module ActiveRecord def supports_add_column? sqlite_version >= '3.1.6' end - + def disconnect! super @connection.close rescue nil @@ -180,6 +180,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| @@ -306,8 +307,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 @@ -354,7 +355,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..4e833ec871 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -89,12 +89,12 @@ 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 + affected_rows = arel_table(true).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 +116,11 @@ 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" - ) + affected_rows = arel_table(true).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 3963baa6b8..7631f3ec35 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -105,7 +105,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 @@ -123,16 +123,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. # @@ -242,7 +242,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: @@ -255,7 +255,7 @@ module ActiveRecord # off by setting: # # config.active_record.timestamped_migrations = false - # + # # In environment.rb. # class Migration @@ -339,10 +339,6 @@ module ActiveRecord self.verbose = save end - def connection - ActiveRecord::Base.connection - end - def method_missing(method, *arguments, &block) arg_list = arguments.map(&:inspect) * ', ' @@ -350,7 +346,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 @@ -390,9 +386,9 @@ module ActiveRecord def rollback(migrations_path, steps=1) migrator = self.new(:down, migrations_path) start_index = migrator.migrations.index(migrator.current_migration) - + return unless start_index - + finish = migrator.migrations[start_index + steps] down(migrations_path, finish ? finish.version : 0) end @@ -404,7 +400,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 @@ -414,7 +410,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 @@ -435,17 +432,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? @@ -462,14 +459,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})" @@ -497,28 +494,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 @@ -535,15 +532,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/relation.rb b/activerecord/lib/active_record/relation.rb new file mode 100644 index 0000000000..1c3a1dc53b --- /dev/null +++ b/activerecord/lib/active_record/relation.rb @@ -0,0 +1,52 @@ +module ActiveRecord + class Relation + delegate :delete, :to_sql, :to => :relation + CLAUSES_METHODS = ["project", "group", "order", "take", "skip"].freeze + attr_reader :relation, :klass + + def initialize(klass, table = nil) + @klass = klass + @relation = Arel::Table.new(table || @klass.table_name) + end + + def to_a + @klass.find_by_sql(@relation.to_sql) + end + + def first + @relation = @relation.take(1) + to_a.first + end + + for clause in CLAUSES_METHODS + class_eval %{ + def #{clause}(_#{clause}) + @relation = @relation.#{clause}(_#{clause}) if _#{clause} + self + end + } + end + + def join(joins) + @relation = @relation.join(@klass.send(:construct_join, joins, nil)) if !joins.blank? + self + end + + def where(conditions) + if !conditions.blank? + conditions = @klass.send(:merge_conditions, conditions) if [String, Hash, Array].include?(conditions.class) + @relation = @relation.where(conditions) + end + self + end + + private + def method_missing(method, *args, &block) + if @relation.respond_to?(method) + @relation.send(method, *args, &block) + elsif Array.instance_methods.include?(method.to_s) + to_a.send(method, *args, &block) + end + end + end +end diff --git a/activerecord/test/cases/adapter_test.rb b/activerecord/test/cases/adapter_test.rb index 80530194ff..12808194f9 100644 --- a/activerecord/test/cases/adapter_test.rb +++ b/activerecord/test/cases/adapter_test.rb @@ -110,27 +110,6 @@ class AdapterTest < ActiveRecord::TestCase end end - def test_add_limit_offset_should_sanitize_sql_injection_for_limit_without_comas - sql_inject = "1 select * from schema" - assert_equal " LIMIT 1", @connection.add_limit_offset!("", :limit=>sql_inject) - if current_adapter?(:MysqlAdapter) - assert_equal " LIMIT 7, 1", @connection.add_limit_offset!("", :limit=>sql_inject, :offset=>7) - else - assert_equal " LIMIT 1 OFFSET 7", @connection.add_limit_offset!("", :limit=>sql_inject, :offset=>7) - end - end - - def test_add_limit_offset_should_sanitize_sql_injection_for_limit_with_comas - sql_inject = "1, 7 procedure help()" - if current_adapter?(:MysqlAdapter) - assert_equal " LIMIT 1,7", @connection.add_limit_offset!("", :limit=>sql_inject) - assert_equal " LIMIT 7, 1", @connection.add_limit_offset!("", :limit=> '1 ; DROP TABLE USERS', :offset=>7) - else - assert_equal " LIMIT 1,7", @connection.add_limit_offset!("", :limit=>sql_inject) - assert_equal " LIMIT 1,7 OFFSET 7", @connection.add_limit_offset!("", :limit=>sql_inject, :offset=>7) - end - end - def test_uniqueness_violations_are_translated_to_specific_exception @connection.execute "INSERT INTO subscribers(nick) VALUES('me')" assert_raises(ActiveRecord::RecordNotUnique) do diff --git a/activerecord/test/cases/associations/inner_join_association_test.rb b/activerecord/test/cases/associations/inner_join_association_test.rb index 7141531740..5f08c40005 100644 --- a/activerecord/test/cases/associations/inner_join_association_test.rb +++ b/activerecord/test/cases/associations/inner_join_association_test.rb @@ -26,7 +26,7 @@ class InnerJoinAssociationTest < ActiveRecord::TestCase def test_construct_finder_sql_applies_association_conditions sql = Author.send(:construct_finder_sql, :joins => :categories_like_general, :conditions => "TERMINATING_MARKER") - assert_match /INNER JOIN .?categories.? ON.*AND.*.?General.?.*TERMINATING_MARKER/, sql + assert_match /INNER JOIN .?categories.? ON.*AND.*.?General.?(.|\n)*TERMINATING_MARKER/, sql end def test_construct_finder_sql_applies_aliases_tables_on_association_conditions diff --git a/activerecord/test/cases/associations_test.rb b/activerecord/test/cases/associations_test.rb index 056a29438a..e429c1d157 100644 --- a/activerecord/test/cases/associations_test.rb +++ b/activerecord/test/cases/associations_test.rb @@ -65,25 +65,6 @@ class AssociationsTest < ActiveRecord::TestCase assert_equal 1, firm.clients(true).size, "New firm should have reloaded clients count" end - def test_storing_in_pstore - require "tmpdir" - store_filename = File.join(Dir.tmpdir, "ar-pstore-association-test") - File.delete(store_filename) if File.exist?(store_filename) - require "pstore" - apple = Firm.create("name" => "Apple") - natural = Client.new("name" => "Natural Company") - apple.clients << natural - - db = PStore.new(store_filename) - db.transaction do - db["apple"] = apple - end - - db = PStore.new(store_filename) - db.transaction do - assert_equal "Natural Company", db["apple"].clients.first.name - end - end end class AssociationProxyTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 7169d93841..6bfe05b4fd 100755 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -1779,7 +1779,7 @@ class BasicsTest < ActiveRecord::TestCase end def test_all_with_conditions - assert_equal Developer.find(:all, :order => 'id desc'), Developer.all(:order => 'id desc') + assert_equal Developer.find(:all, :order => 'id desc'), Developer.all.order('id desc').to_a end def test_find_ordered_last diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb index 75f52dfa4a..8edbdbd20f 100644 --- a/activerecord/test/cases/calculations_test.rb +++ b/activerecord/test/cases/calculations_test.rb @@ -25,7 +25,7 @@ class CalculationsTest < ActiveRecord::TestCase def test_should_return_nil_as_average assert_nil NumericData.average(:bank_balance) end - + def test_type_cast_calculated_value_should_convert_db_averages_of_fixnum_class_to_decimal assert_equal 0, NumericData.send(:type_cast_calculated_value, 0, nil, 'avg') assert_equal 53.0, NumericData.send(:type_cast_calculated_value, 53, nil, 'avg') diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index d8f5695a0f..f8c8d3648a 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -1072,10 +1072,10 @@ class FinderTest < ActiveRecord::TestCase end def test_finder_with_scoped_from - all_topics = Topic.all + all_topics = Topic.find(:all) Topic.with_scope(:find => { :from => 'fake_topics' }) do - assert_equal all_topics, Topic.all(:from => 'topics') + assert_equal all_topics, Topic.all(:from => 'topics').to_a end end diff --git a/activerecord/test/cases/inheritance_test.rb b/activerecord/test/cases/inheritance_test.rb index 167d3abad9..8580d67e5f 100644 --- a/activerecord/test/cases/inheritance_test.rb +++ b/activerecord/test/cases/inheritance_test.rb @@ -30,7 +30,7 @@ class InheritanceTest < ActiveRecord::TestCase ensure ActiveRecord::Base.store_full_sti_class = old end - + def test_should_store_full_class_name_with_store_full_sti_class_option_enabled old = ActiveRecord::Base.store_full_sti_class ActiveRecord::Base.store_full_sti_class = true @@ -39,7 +39,7 @@ class InheritanceTest < ActiveRecord::TestCase ensure ActiveRecord::Base.store_full_sti_class = old end - + def test_different_namespace_subclass_should_load_correctly_with_store_full_sti_class_option old = ActiveRecord::Base.store_full_sti_class ActiveRecord::Base.store_full_sti_class = true diff --git a/activerecord/test/cases/method_scoping_test.rb b/activerecord/test/cases/method_scoping_test.rb index d8246f49b8..d4e63ce2fd 100644 --- a/activerecord/test/cases/method_scoping_test.rb +++ b/activerecord/test/cases/method_scoping_test.rb @@ -379,7 +379,7 @@ class NestedScopingTest < ActiveRecord::TestCase poor_jamis = developers(:poor_jamis) Developer.with_scope(:find => { :conditions => "salary < 100000" }) do Developer.with_scope(:find => { :offset => 1, :order => 'id asc' }) do - assert_sql /ORDER BY id asc / do + assert_sql /ORDER BY id asc/ do assert_equal(poor_jamis, Developer.find(:first, :order => 'id asc')) end end @@ -592,12 +592,12 @@ class DefaultScopingTest < ActiveRecord::TestCase end def test_default_scope_with_conditions_string - assert_equal Developer.find_all_by_name('David').map(&:id).sort, DeveloperCalledDavid.all.map(&:id).sort + assert_equal Developer.find_all_by_name('David').map(&:id).sort, DeveloperCalledDavid.all.to_a.map(&:id).sort assert_equal nil, DeveloperCalledDavid.create!.name end def test_default_scope_with_conditions_hash - assert_equal Developer.find_all_by_name('Jamis').map(&:id).sort, DeveloperCalledJamis.all.map(&:id).sort + assert_equal Developer.find_all_by_name('Jamis').map(&:id).sort, DeveloperCalledJamis.all.to_a.map(&:id).sort assert_equal 'Jamis', DeveloperCalledJamis.create!.name end diff --git a/activerecord/test/cases/named_scope_test.rb b/activerecord/test/cases/named_scope_test.rb index f4fdc9a39d..3cb6b01e80 100644 --- a/activerecord/test/cases/named_scope_test.rb +++ b/activerecord/test/cases/named_scope_test.rb @@ -344,14 +344,14 @@ class NamedScopeTest < ActiveRecord::TestCase def test_chaining_should_use_latest_conditions_when_searching # Normal hash conditions - assert_equal Topic.all(:conditions => {:approved => true}), Topic.rejected.approved.all - assert_equal Topic.all(:conditions => {:approved => false}), Topic.approved.rejected.all + assert_equal Topic.all(:conditions => {:approved => true}).to_a, Topic.rejected.approved.all.to_a + assert_equal Topic.all(:conditions => {:approved => false}).to_a, Topic.approved.rejected.all.to_a # Nested hash conditions with same keys - assert_equal [posts(:sti_comments)], Post.with_special_comments.with_very_special_comments.all + assert_equal [posts(:sti_comments)], Post.with_special_comments.with_very_special_comments.all.to_a # Nested hash conditions with different keys - assert_equal [posts(:sti_comments)], Post.with_special_comments.with_post(4).all.uniq + assert_equal [posts(:sti_comments)], Post.with_special_comments.with_post(4).all.to_a.uniq end def test_named_scopes_batch_finders diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb new file mode 100644 index 0000000000..c095d9455e --- /dev/null +++ b/activerecord/test/cases/relations_test.rb @@ -0,0 +1,78 @@ +require "cases/helper" +require 'models/post' +require 'models/topic' +require 'models/reply' +require 'models/author' +require 'models/entrant' +require 'models/developer' +require 'models/company' + +class RealtionTest < ActiveRecord::TestCase + fixtures :authors, :topics, :entrants, :developers, :companies, :developers_projects, :accounts, :categories, :categorizations, :posts + + def test_finding_with_conditions + authors = Author.all.where("name = 'David'") + + assert_equal Author.find(:all, :conditions => "name = 'David'"), authors.to_a + end + + def test_finding_with_order + topics = Topic.all.order('id') + assert_equal 4, topics.size + assert_equal topics(:first).title, topics.first.title + end + + def test_finding_with_order_and_take + entrants = Entrant.all.order("id ASC").take(2).to_a + + assert_equal(2, entrants.size) + assert_equal(entrants(:first).name, entrants.first.name) + end + + def test_finding_with_order_limit_and_offset + entrants = Entrant.all.order("id ASC").take(2).skip(1) + + assert_equal(2, entrants.size) + assert_equal(entrants(:second).name, entrants.first.name) + + entrants = Entrant.all.order("id ASC").take(2).skip(2) + assert_equal(1, entrants.size) + assert_equal(entrants(:third).name, entrants.first.name) + end + + def test_finding_with_group + developers = Developer.all.group("salary").project("salary").to_a + assert_equal 4, developers.size + assert_equal 4, developers.map(&:salary).uniq.size + end + + def test_finding_with_hash_conditions_on_joined_table + firms = DependentFirm.all.join(:account).where({:name => 'RailsCore', :accounts => { :credit_limit => 55..60 }}).to_a + assert_equal 1, firms.size + assert_equal companies(:rails_core), firms.first + end + + def test_find_all_with_join + developers_on_project_one = Developer.all.join('LEFT JOIN developers_projects ON developers.id = developers_projects.developer_id').where('project_id=1').to_a + + assert_equal 3, developers_on_project_one.length + developer_names = developers_on_project_one.map { |d| d.name } + assert developer_names.include?('David') + assert developer_names.include?('Jamis') + end + + def test_find_on_hash_conditions + assert_equal Topic.find(:all, :conditions => {:approved => false}), Topic.all.where({ :approved => false }).to_a + end + + def test_joins_with_string_array + person_with_reader_and_post = Post.all.join([ + "INNER JOIN categorizations ON categorizations.post_id = posts.id", + "INNER JOIN categories ON categories.id = categorizations.category_id AND categories.type = 'SpecialCategory'" + ] + ).to_a + assert_equal 1, person_with_reader_and_post.size + end + +end + diff --git a/arel b/arel new file mode 160000 +Subproject 808b9e90a38c6c19e109da8eb5f2a264fd780d9 |