aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/arel
diff options
context:
space:
mode:
Diffstat (limited to 'activerecord/lib/arel')
-rw-r--r--activerecord/lib/arel/nodes/delete_statement.rb6
-rw-r--r--activerecord/lib/arel/nodes/update_statement.rb6
-rw-r--r--activerecord/lib/arel/tree_manager.rb5
-rw-r--r--activerecord/lib/arel/visitors/mysql.rb52
-rw-r--r--activerecord/lib/arel/visitors/to_sql.rb68
5 files changed, 94 insertions, 43 deletions
diff --git a/activerecord/lib/arel/nodes/delete_statement.rb b/activerecord/lib/arel/nodes/delete_statement.rb
index 5be42a084a..a419975335 100644
--- a/activerecord/lib/arel/nodes/delete_statement.rb
+++ b/activerecord/lib/arel/nodes/delete_statement.rb
@@ -3,7 +3,7 @@
module Arel # :nodoc: all
module Nodes
class DeleteStatement < Arel::Nodes::Node
- attr_accessor :left, :right, :orders, :limit, :key
+ attr_accessor :left, :right, :orders, :limit, :offset, :key
alias :relation :left
alias :relation= :left=
@@ -16,6 +16,7 @@ module Arel # :nodoc: all
@right = wheres
@orders = []
@limit = nil
+ @offset = nil
@key = nil
end
@@ -26,7 +27,7 @@ module Arel # :nodoc: all
end
def hash
- [self.class, @left, @right, @orders, @limit, @key].hash
+ [self.class, @left, @right, @orders, @limit, @offset, @key].hash
end
def eql?(other)
@@ -35,6 +36,7 @@ module Arel # :nodoc: all
self.right == other.right &&
self.orders == other.orders &&
self.limit == other.limit &&
+ self.offset == other.offset &&
self.key == other.key
end
alias :== :eql?
diff --git a/activerecord/lib/arel/nodes/update_statement.rb b/activerecord/lib/arel/nodes/update_statement.rb
index 017a553c4c..cfaa19e392 100644
--- a/activerecord/lib/arel/nodes/update_statement.rb
+++ b/activerecord/lib/arel/nodes/update_statement.rb
@@ -3,7 +3,7 @@
module Arel # :nodoc: all
module Nodes
class UpdateStatement < Arel::Nodes::Node
- attr_accessor :relation, :wheres, :values, :orders, :limit, :key
+ attr_accessor :relation, :wheres, :values, :orders, :limit, :offset, :key
def initialize
@relation = nil
@@ -11,6 +11,7 @@ module Arel # :nodoc: all
@values = []
@orders = []
@limit = nil
+ @offset = nil
@key = nil
end
@@ -21,7 +22,7 @@ module Arel # :nodoc: all
end
def hash
- [@relation, @wheres, @values, @orders, @limit, @key].hash
+ [@relation, @wheres, @values, @orders, @limit, @offset, @key].hash
end
def eql?(other)
@@ -31,6 +32,7 @@ module Arel # :nodoc: all
self.values == other.values &&
self.orders == other.orders &&
self.limit == other.limit &&
+ self.offset == other.offset &&
self.key == other.key
end
alias :== :eql?
diff --git a/activerecord/lib/arel/tree_manager.rb b/activerecord/lib/arel/tree_manager.rb
index 149c69ce7a..0476399618 100644
--- a/activerecord/lib/arel/tree_manager.rb
+++ b/activerecord/lib/arel/tree_manager.rb
@@ -10,6 +10,11 @@ module Arel # :nodoc: all
self
end
+ def offset(offset)
+ @ast.offset = Nodes::Offset.new(Nodes.build_quoted(offset)) if offset
+ self
+ end
+
def order(*expr)
@ast.orders = expr
self
diff --git a/activerecord/lib/arel/visitors/mysql.rb b/activerecord/lib/arel/visitors/mysql.rb
index 0f7d5aa803..9d9294fecc 100644
--- a/activerecord/lib/arel/visitors/mysql.rb
+++ b/activerecord/lib/arel/visitors/mysql.rb
@@ -56,18 +56,6 @@ module Arel # :nodoc: all
super
end
- def visit_Arel_Nodes_UpdateStatement(o, collector)
- collector << "UPDATE "
- collector = visit o.relation, collector
-
- unless o.values.empty?
- collector << " SET "
- collector = inject_join o.values, collector, ", "
- end
-
- collect_where_for(o, collector)
- end
-
def visit_Arel_Nodes_Concat(o, collector)
collector << " CONCAT("
visit o.left, collector
@@ -77,18 +65,42 @@ module Arel # :nodoc: all
collector
end
- def collect_where_for(o, collector)
- unless o.wheres.empty?
- collector << " WHERE "
- collector = inject_join o.wheres, collector, " AND "
+ # In the simple case, MySQL allows us to place JOINs directly into the UPDATE
+ # query. However, this does not allow for LIMIT, OFFSET and ORDER. To support
+ # these, we must use a subquery.
+ def prepare_update_statement(o)
+ if o.offset || has_join_sources?(o) && has_limit_or_offset_or_orders?(o)
+ super
+ else
+ o
end
+ end
- unless o.orders.empty?
- collector << " ORDER BY "
- collector = inject_join o.orders, collector, ", "
+ def prepare_delete_statement(o)
+ if o.offset || has_join_sources?(o)
+ super
+ else
+ o
+ end
+ end
+
+ # MySQL is too stupid to create a temporary table for use subquery, so we have
+ # to give it some prompting in the form of a subsubquery.
+ def build_subselect(key, o)
+ subselect = super
+
+ # Materialize subquery by adding distinct
+ # to work with MySQL 5.7.6 which sets optimizer_switch='derived_merge=on'
+ unless has_limit_or_offset_or_orders?(subselect)
+ core = subselect.cores.last
+ core.set_quantifier = Arel::Nodes::Distinct.new
end
- maybe_visit o.limit, collector
+ Nodes::SelectStatement.new.tap do |stmt|
+ core = stmt.cores.last
+ core.froms = Nodes::Grouping.new(subselect).as("__active_record_temp")
+ core.projections = [Arel.sql(quote_column_name(key.name))]
+ end
end
end
end
diff --git a/activerecord/lib/arel/visitors/to_sql.rb b/activerecord/lib/arel/visitors/to_sql.rb
index 575bfd6e36..7c0f6c2e97 100644
--- a/activerecord/lib/arel/visitors/to_sql.rb
+++ b/activerecord/lib/arel/visitors/to_sql.rb
@@ -74,25 +74,17 @@ module Arel # :nodoc: all
private
def visit_Arel_Nodes_DeleteStatement(o, collector)
+ o = prepare_delete_statement(o)
+
collector << "DELETE FROM "
collector = visit o.relation, collector
collect_where_for(o, collector)
end
- # FIXME: we should probably have a 2-pass visitor for this
- def build_subselect(key, o)
- stmt = Nodes::SelectStatement.new
- core = stmt.cores.first
- core.froms = o.relation
- core.wheres = o.wheres
- core.projections = [key]
- stmt.limit = o.limit
- stmt.orders = o.orders
- stmt
- end
-
def visit_Arel_Nodes_UpdateStatement(o, collector)
+ o = prepare_update_statement(o)
+
collector << "UPDATE "
collector = visit o.relation, collector
unless o.values.empty?
@@ -799,19 +791,57 @@ module Arel # :nodoc: all
}
end
- def collect_where_for(o, collector)
- if o.orders.empty? && o.limit.nil?
- wheres = o.wheres
+ def has_join_sources?(o)
+ o.relation.is_a?(Nodes::JoinSource) && !o.relation.right.empty?
+ end
+
+ def has_limit_or_offset_or_orders?(o)
+ o.limit || o.offset || !o.orders.empty?
+ end
+
+ # The default strategy for an UPDATE with joins is to use a subquery. This doesn't work
+ # on MySQL (even when aliasing the tables), but MySQL allows using JOIN directly in
+ # an UPDATE statement, so in the MySQL visitor we redefine this to do that.
+ def prepare_update_statement(o)
+ if o.key && (has_limit_or_offset_or_orders?(o) || has_join_sources?(o))
+ stmt = o.clone
+ stmt.limit = nil
+ stmt.offset = nil
+ stmt.orders = []
+ stmt.wheres = [Nodes::In.new(o.key, [build_subselect(o.key, o)])]
+ stmt.relation = o.relation.left if has_join_sources?(o)
+ stmt
else
- wheres = [Nodes::In.new(o.key, [build_subselect(o.key, o)])]
+ o
end
+ end
+ alias :prepare_delete_statement :prepare_update_statement
- unless wheres.empty?
+ # FIXME: we should probably have a 2-pass visitor for this
+ def build_subselect(key, o)
+ stmt = Nodes::SelectStatement.new
+ core = stmt.cores.first
+ core.froms = o.relation
+ core.wheres = o.wheres
+ core.projections = [key]
+ stmt.limit = o.limit
+ stmt.offset = o.offset
+ stmt.orders = o.orders
+ stmt
+ end
+
+ def collect_where_for(o, collector)
+ unless o.wheres.empty?
collector << " WHERE "
- collector = inject_join wheres, collector, " AND "
+ collector = inject_join o.wheres, collector, " AND "
end
- collector
+ unless o.orders.empty?
+ collector << " ORDER BY "
+ collector = inject_join o.orders, collector, ", "
+ end
+
+ maybe_visit o.limit, collector
end
def infix_value(o, collector, value)