aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitmodules3
-rw-r--r--activerecord/lib/active_record.rb8
-rwxr-xr-xactiverecord/lib/active_record/associations.rb350
-rw-r--r--activerecord/lib/active_record/associations/association_proxy.rb9
-rw-r--r--activerecord/lib/active_record/associations/belongs_to_association.rb4
-rw-r--r--activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb22
-rw-r--r--activerecord/lib/active_record/associations/has_many_association.rb18
-rw-r--r--activerecord/lib/active_record/associations/has_many_through_association.rb2
-rw-r--r--activerecord/lib/active_record/associations/through_association_scope.rb2
-rwxr-xr-xactiverecord/lib/active_record/base.rb301
-rw-r--r--activerecord/lib/active_record/calculations.rb203
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb35
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb6
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql_adapter.rb15
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb15
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb9
-rw-r--r--activerecord/lib/active_record/locking/optimistic.rb27
-rw-r--r--activerecord/lib/active_record/migration.rb59
-rw-r--r--activerecord/lib/active_record/relation.rb103
-rw-r--r--activerecord/test/cases/adapter_test.rb12
-rw-r--r--activerecord/test/cases/associations/eager_load_nested_include_test.rb2
-rw-r--r--activerecord/test/cases/associations/eager_test.rb10
-rw-r--r--activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb8
-rw-r--r--activerecord/test/cases/associations/inner_join_association_test.rb2
-rw-r--r--activerecord/test/cases/associations_test.rb19
-rwxr-xr-xactiverecord/test/cases/base_test.rb2
-rw-r--r--activerecord/test/cases/calculations_test.rb2
-rw-r--r--activerecord/test/cases/finder_test.rb4
-rw-r--r--activerecord/test/cases/inheritance_test.rb4
-rw-r--r--activerecord/test/cases/method_scoping_test.rb6
-rw-r--r--activerecord/test/cases/named_scope_test.rb8
-rw-r--r--activerecord/test/cases/relations_test.rb89
m---------arel6
33 files changed, 700 insertions, 665 deletions
diff --git a/.gitmodules b/.gitmodules
index e69de29bb2..30755285a1 100644
--- a/.gitmodules
+++ 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 2f1e7573d8..d9310a9927 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'
@@ -92,4 +97,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 497115e4ff..1e1f1a4c57 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:
@@ -1085,8 +1085,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:
@@ -1228,8 +1228,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,
@@ -1442,12 +1442,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
@@ -1688,19 +1688,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),
@@ -1708,82 +1695,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
+
+ 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)
- return sanitize_sql(sql)
+ 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::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
-
- 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
+ scope = scope(:find)
- add_conditions!(sql, options[:conditions], scope)
- add_group!(sql, options[:group], options[:having], scope)
+ relation = arel_table(options[:from])
- if order && is_distinct
- connection.add_order_by_for_association_limiting!(sql, :order => order)
- else
- add_order!(sql, options[:order], scope)
+ for association in join_dependency.join_associations
+ relation = association.join_relation(relation)
end
- add_limit!(sql, options, scope)
+ 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(",")))
- return sanitize_sql(sql)
+ relation.to_sql
end
def tables_in_string(string)
@@ -1837,7 +1809,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
@@ -1887,10 +1859,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"
@@ -1966,25 +1934,6 @@ module ActiveRecord
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
@@ -2008,7 +1957,6 @@ module ActiveRecord
end
end
- # overridden in InnerJoinDependency subclass
def build_join_association(reflection, parent)
JoinAssociation.new(reflection, self, parent)
end
@@ -2133,6 +2081,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")
@@ -2144,42 +2093,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
-
- case source_reflection.macro
+ if reflection.options[:through]
+ 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
+
+ case source_reflection.macro
when :has_many
if source_reflection.options[:as]
first_key = "#{source_reflection.options[:as]}_id"
@@ -2212,65 +2158,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
@@ -2298,7 +2256,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
@@ -2308,28 +2266,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_proxy.rb b/activerecord/lib/active_record/associations/association_proxy.rb
index e36b04ea95..75218c01d2 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
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..4672b0723e 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::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..29ba84ee37 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::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/base.rb b/activerecord/lib/active_record/base.rb
index 502fe0442e..a1b6606e3e 100755
--- a/activerecord/lib/active_record/base.rb
+++ b/activerecord/lib/active_record/base.rb
@@ -421,7 +421,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 +455,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
@@ -664,7 +664,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
@@ -860,26 +864,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 conditions = construct_conditions(conditions, scope)
+ relation = relation.conditions(Arel::SqlLiteral.new(conditions))
+ end
- if options.has_key?(:limit) || (scope && scope[:limit])
+ 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 +931,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 +1063,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 +1111,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 +1138,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,7 +1365,7 @@ 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:
@@ -1372,7 +1375,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
@@ -1529,6 +1532,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)
@@ -1693,22 +1705,84 @@ 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))
- add_joins!(sql, options[:joins], scope)
- add_conditions!(sql, options[:conditions], scope)
+ relation = relation.readonly if options[:readonly]
- add_group!(sql, options[:group], options[:having], scope)
- add_order!(sql, options[:order], scope)
- add_limit!(sql, options, scope)
- add_lock!(sql, options, scope)
+ relation
+
+ end
+ def construct_finder_sql(options, scope = scope(:find))
+ construct_finder_arel(options, scope).to_sql
+ end
+
+ 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 +1792,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 +1801,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 +1830,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 +1838,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 +1982,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__
@@ -2446,7 +2464,7 @@ module ActiveRecord #:nodoc:
# name
# end
# end
- #
+ #
# user = User.find_by_name('Phusion')
# user_path(user) # => "/users/Phusion"
def to_param
@@ -2497,12 +2515,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
@@ -2514,12 +2532,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
@@ -2544,11 +2562,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(self.class.table_name).conditions(self.class.arel_table[self.class.primary_key].eq(id)).delete
end
@destroyed = true
@@ -2692,12 +2706,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)
@@ -2841,14 +2855,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(self.class.table_name).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
@@ -2858,18 +2867,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
@@ -2960,6 +2966,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)
@@ -3067,13 +3093,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
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/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/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..5a49fc2d2f 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb
@@ -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 adb3a3f75e..09a558a636 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
@@ -402,7 +398,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
@@ -412,7 +408,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
@@ -446,17 +443,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?
@@ -473,14 +470,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})"
@@ -508,28 +505,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
@@ -546,15 +543,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..4b53857d36
--- /dev/null
+++ b/activerecord/lib/active_record/relation.rb
@@ -0,0 +1,103 @@
+module ActiveRecord
+ class Relation
+ delegate :to_sql, :to => :relation
+ attr_reader :relation, :klass
+
+ def initialize(klass, relation)
+ @klass, @relation = klass, relation
+ @readonly = false
+ end
+
+ def readonly
+ @readonly = true
+ self
+ end
+
+ def to_a
+ records = @klass.find_by_sql(@relation.to_sql)
+
+ records.each { |record| record.readonly! } if @readonly
+
+ records
+ end
+
+ def each(&block)
+ to_a.each(&block)
+ 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)
+ if @relation.respond_to?(method) || Array.instance_methods.include?(method.to_s)
+ true
+ else
+ super
+ end
+ 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 9463b7b14a..c59be264a4 100644
--- a/activerecord/test/cases/adapter_test.rb
+++ b/activerecord/test/cases/adapter_test.rb
@@ -122,18 +122,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_no_match /schema/, @connection.add_limit_offset!("", :limit=>sql_inject)
- assert_no_match /schema/, @connection.add_limit_offset!("", :limit=>sql_inject, :offset=>7)
- end
-
- def test_add_limit_offset_should_sanitize_sql_injection_for_limit_with_comas
- sql_inject = "1, 7 procedure help()"
- assert_no_match /procedure/, @connection.add_limit_offset!("", :limit=>sql_inject)
- assert_no_match /procedure/, @connection.add_limit_offset!("", :limit=>sql_inject, :offset=>7)
- 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/eager_load_nested_include_test.rb b/activerecord/test/cases/associations/eager_load_nested_include_test.rb
index f313a75233..e8db6d5dab 100644
--- a/activerecord/test/cases/associations/eager_load_nested_include_test.rb
+++ b/activerecord/test/cases/associations/eager_load_nested_include_test.rb
@@ -72,10 +72,8 @@ class EagerLoadPolyAssocsTest < ActiveRecord::TestCase
ShapeExpression, NonPolyOne, NonPolyTwo].each do |c|
c.delete_all
end
-
end
-
def generate_test_object_graphs
1.upto(NUM_SIMPLE_OBJS) do
[Circle, Square, Triangle, NonPolyOne, NonPolyTwo].map(&:create!)
diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb
index 811ebfbe3f..d5a4d9007b 100644
--- a/activerecord/test/cases/associations/eager_test.rb
+++ b/activerecord/test/cases/associations/eager_test.rb
@@ -301,13 +301,13 @@ class EagerAssociationTest < ActiveRecord::TestCase
subscriber =Subscriber.find(subscribers(:second).id, :include => :subscriptions)
assert_equal subscriptions, subscriber.subscriptions.sort_by(&:id)
end
-
+
def test_eager_load_has_many_through_with_string_keys
books = books(:awdr, :rfr)
subscriber = Subscriber.find(subscribers(:second).id, :include => :books)
assert_equal books, subscriber.books.sort_by(&:id)
end
-
+
def test_eager_load_belongs_to_with_string_keys
subscriber = subscribers(:second)
subscription = Subscription.find(subscriptions(:webster_awdr).id, :include => :subscriber)
@@ -434,7 +434,7 @@ class EagerAssociationTest < ActiveRecord::TestCase
author_posts_without_comments = author.posts.select { |post| post.comments.blank? }
assert_equal author_posts_without_comments.size, author.posts.count(:all, :include => :comments, :conditions => 'comments.id is null')
end
-
+
def test_eager_count_performed_on_a_has_many_through_association_with_multi_table_conditional
person = people(:michael)
person_posts_without_comments = person.posts.select { |post| post.comments.blank? }
@@ -823,7 +823,7 @@ class EagerAssociationTest < ActiveRecord::TestCase
assert_equal expected, firm.clients_using_primary_key
end
end
-
+
def test_preload_has_one_using_primary_key
expected = Firm.find(:first).account_using_primary_key
firm = Firm.find :first, :include => :account_using_primary_key
@@ -839,5 +839,5 @@ class EagerAssociationTest < ActiveRecord::TestCase
assert_equal expected, firm.account_using_primary_key
end
end
-
+
end
diff --git a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb
index 11a159686e..1bce45865f 100644
--- a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb
+++ b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb
@@ -732,7 +732,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
assert_equal [projects(:active_record), projects(:action_controller)].map(&:id).sort, developer.project_ids.sort
end
- def test_select_limited_ids_list
+ def test_select_limited_ids_array
# Set timestamps
Developer.transaction do
Developer.find(:all, :order => 'id').each_with_index do |record, i|
@@ -742,9 +742,9 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
join_base = ActiveRecord::Associations::ClassMethods::JoinDependency::JoinBase.new(Project)
join_dep = ActiveRecord::Associations::ClassMethods::JoinDependency.new(join_base, :developers, nil)
- projects = Project.send(:select_limited_ids_list, {:order => 'developers.created_at'}, join_dep)
+ projects = Project.send(:select_limited_ids_array, {:order => 'developers.created_at'}, join_dep)
assert !projects.include?("'"), projects
- assert_equal %w(1 2), projects.scan(/\d/).sort
+ assert_equal ["1", "2"], projects.sort
end
def test_scoped_find_on_through_association_doesnt_return_read_only_records
@@ -770,7 +770,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
assert_equal developer, project.developers.find(:first)
assert_equal project, developer.projects.find(:first)
end
-
+
def test_self_referential_habtm_without_foreign_key_set_should_raise_exception
assert_raise(ActiveRecord::HasAndBelongsToManyAssociationForeignKeyNeeded) {
Member.class_eval 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 8421a8fb07..df15c1a797 100755
--- a/activerecord/test/cases/base_test.rb
+++ b/activerecord/test/cases/base_test.rb
@@ -1893,7 +1893,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 c2e02763f6..004f4d0ea6 100644
--- a/activerecord/test/cases/calculations_test.rb
+++ b/activerecord/test/cases/calculations_test.rb
@@ -27,7 +27,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 7b6bf597a8..3de07797d4 100644
--- a/activerecord/test/cases/finder_test.rb
+++ b/activerecord/test/cases/finder_test.rb
@@ -1073,10 +1073,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 5cd11e9799..73e51fbd91 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 35f7bc5443..6dec474f7d 100644
--- a/activerecord/test/cases/method_scoping_test.rb
+++ b/activerecord/test/cases/method_scoping_test.rb
@@ -380,7 +380,7 @@ class NestedScopingTest < ActiveRecord::TestCase
Developer.with_scope(:find => { :conditions => "salary < 100000" }) do
Developer.with_scope(:find => { :offset => 1, :order => 'id asc' }) do
# Oracle adapter does not generated space after asc therefore trailing space removed from regex
- 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
@@ -593,12 +593,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 bb2aba9d92..13427daf53 100644
--- a/activerecord/test/cases/named_scope_test.rb
+++ b/activerecord/test/cases/named_scope_test.rb
@@ -345,14 +345,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..655eb3314d
--- /dev/null
+++ b/activerecord/test/cases/relations_test.rb
@@ -0,0 +1,89 @@
+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 RelationTest < ActiveRecord::TestCase
+ fixtures :authors, :topics, :entrants, :developers, :companies, :developers_projects, :accounts, :categories, :categorizations, :posts
+
+ def test_finding_with_conditions
+ assert_equal Author.find(:all, :conditions => "name = 'David'"), Author.all.conditions("name = 'David'").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").limit(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").limit(2).offset(1)
+
+ assert_equal(2, entrants.size)
+ assert_equal(entrants(:second).name, entrants.first.name)
+
+ entrants = Entrant.all.order("id ASC").limit(2).offset(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").select("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.joins(:account).conditions({: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.joins('LEFT JOIN developers_projects ON developers.id = developers_projects.developer_id').conditions('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.conditions({ :approved => false }).to_a
+ end
+
+ def test_joins_with_string_array
+ person_with_reader_and_post = Post.all.joins([
+ "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
+
+ def test_relation_responds_to_delegated_methods
+ relation = Topic.all
+
+ ["map", "uniq", "sort", "insert", "delete", "update"].each do |method|
+ assert relation.respond_to?(method)
+ end
+ end
+
+ def test_find_with_readonly_option
+ Developer.all.each { |d| assert !d.readonly? }
+ Developer.all.readonly.each { |d| assert d.readonly? }
+ Developer.all(:readonly => true).each { |d| assert d.readonly? }
+ end
+end
+
diff --git a/arel b/arel
new file mode 160000
+Subproject 8852db7087a8f4f98e5fd26fa33bac14a540097