diff options
Diffstat (limited to 'activerecord/lib/arel')
-rw-r--r-- | activerecord/lib/arel/nodes/delete_statement.rb | 6 | ||||
-rw-r--r-- | activerecord/lib/arel/nodes/update_statement.rb | 6 | ||||
-rw-r--r-- | activerecord/lib/arel/tree_manager.rb | 5 | ||||
-rw-r--r-- | activerecord/lib/arel/visitors/mysql.rb | 52 | ||||
-rw-r--r-- | activerecord/lib/arel/visitors/to_sql.rb | 68 |
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) |