# frozen_string_literal: true require 'active_record' module UpdateAllScope class UpdateAllScope AREL_SUPPORT_JOIN_TABLE = Gem::Version.new(Arel::VERSION) >= Gem::Version.new('10') def initialize(model: nil, relation: nil) @queries = [] @relation = relation || model.class.where(id: model.id) end def where(*args) @relation = @relation.where(*args) return self end def update(query, *binding_values) args = binding_values.size > 0 ? [[query, *binding_values]] : [query] @queries << klass.send(:sanitize_sql_for_assignment, *args) return self end def do_query! return 0 if @queries.empty? return @relation.update_all(updates_as_string) end def updates_as_string @queries.join(',') end def klass @relation.klass end # See: https://github.com/rails/rails/blob/fc5dd0b85189811062c85520fd70de8389b55aeb/activerecord/lib/active_record/relation.rb#L315 def to_arel if @relation.eager_loading? scope = UpdateAllScope.new(model: model, relation: @relation.apply_join_dependency) return scope.update(updates_as_string).to_update_manager end stmt = new_arel_update_manager stmt.set Arel.sql(klass.send(:sanitize_sql_for_assignment, updates_as_string)) stmt.table(stmt_table) stmt.key = arel_attribute(@relation.primary_key) if should_use_join_to_update? join_to_update(klass.connection, stmt, stmt.key) else stmt.take(@relation.arel.limit) stmt.order(*@relation.arel.orders) stmt.wheres = @relation.arel.constraints end return stmt end def to_sql connection = klass.connection sql, binds = to_sql_and_binds(connection, to_arel) type_casted_binds(connection, binds).each_with_index{|var, idx| sql = sql.gsub("$#{idx + 1}", connection.quote(var)) } return sql end private def should_use_join_to_update? return false if AREL_SUPPORT_JOIN_TABLE return true if has_join_values? return true if @relation.offset_value return false end def stmt_table return @relation.table if not AREL_SUPPORT_JOIN_TABLE return @relation.arel.join_sources.empty? ? @relation.table : @relation.arel.source end def new_arel_update_manager return Arel::UpdateManager.new(ActiveRecord::Base) if Gem::Version.new(Arel::VERSION) < Gem::Version.new('7') return Arel::UpdateManager.new end def has_join_values? return @relation.send(:has_join_values?) if @relation.respond_to?(:has_join_values?, true) return true if @relation.joins_values.any? return true if @relation.respond_to?(:left_outer_joins_values) and @relation.left_outer_joins_values.any? return false end def arel_attribute(name) return @relation.arel_attribute(name) if @relation.respond_to?(:arel_attribute) name = klass.attribute_alias(name) if klass.respond_to?(:attribute_alias?) && klass.attribute_alias?(name) # attribute_alias? is not defined in Rails 3. return @relation.arel_table[name] end def to_sql_and_binds(connection, arel_or_sql_string) return connection.send(:to_sql_and_binds, arel_or_sql_string, []) if connection.respond_to?(:to_sql_and_binds, true) return [arel_or_sql_string.dup.freeze, []] if !arel_or_sql_string.respond_to?(:ast) sql = accept(connection, arel_or_sql_string.ast) return [sql.freeze, bind_values] if sql.is_a?(String) return [sql.compile(bind_values, connection), bind_values] end def accept(connection, ast) return connection.visitor.accept(ast) if not connection.respond_to?(:collector) # For Rails 3 return connection.visitor.accept(ast, connection.collector) end def type_casted_binds(connection, binds) return connection.type_casted_binds(binds) if connection.respond_to?(:type_casted_binds) return binds.map{|column, value| connection.type_cast(value, column) } if binds.first.is_a?(Array) return binds.map{|attr| connection.type_cast(attr.value_for_database) } end def join_to_update(connection, stmt, key) return connection.join_to_update(stmt, @relation.arel) if connection.method(:join_to_update).arity == 2 return connection.join_to_update(stmt, @relation.arel, key) end def bind_values return @relation.bound_attributes if @relation.respond_to?(:bound_attributes) # For Rails 5.1, 5.2 return @relation.bind_values # For Rails 4.2 end end end