Skip to content

Instantly share code, notes, and snippets.

@ippeiukai
Last active August 29, 2015 14:18
Show Gist options
  • Save ippeiukai/23eee3d5ad8c0ab4515f to your computer and use it in GitHub Desktop.
Save ippeiukai/23eee3d5ad8c0ab4515f to your computer and use it in GitHub Desktop.
bulk update of records in rails with UPDATE SET FROM (VALUES ...) AS WHERE
# NOTE assumes the relation is very very simple
def bulk_update(relation, values, where_columns, set_columns)
return if values.empty?
connection = relation.connection
base_class = relation.base_class
if relation.arel.where_sql.present?
relation_ids = relation.ids
end
quote_column_name_fn = connection.method(:quote_column_name)
quote_column_value_fn = ->(value, col_name) { base_class.quote_value(value, base_class.column_for_attribute(col_name)) }
destination_table_name_quoted = relation.quoted_table_name
temporary_table_name_quoted = quote_column_name_fn.('tmp_data')
temporary_table_columns = [*where_columns, *set_columns]
temporary_table_def = "#{temporary_table_name_quoted}(#{temporary_table_columns.map(&quote_column_name_fn).join(', ')})"
temporary_table_values_list = values.map do |record|
record.to_h.values_at(*temporary_table_columns).map.with_index do |val, i|
# cast is to make sure temporary table is correctly typed (e.g. timestamp without explicit type coercion is text)
"CAST(#{quote_column_value_fn.(val, temporary_table_columns[i])} AS #{base_class.column_for_attribute(temporary_table_columns[i]).sql_type})"
end
end
set_column_names_quoted = set_columns.map(&quote_column_name_fn)
set_fields = set_column_names_quoted.map { |col| "#{col} = #{temporary_table_name_quoted}.#{col}" }.join(', ')
where_column_names_quoted = where_columns.map(&quote_column_name_fn)
where_cond = where_column_names_quoted.map { |col| "#{destination_table_name_quoted}.#{col} = #{temporary_table_name_quoted}.#{col}" }.join(' AND ')
if relation_ids
relation_cond = "#{destination_table_name_quoted}.#{quote_column_name_fn.(relation.primary_key)} IN (#{relation_ids.map {|val| quote_column_value_fn.(val, relation.primary_key) }.join(', ')})"
end
connection.update(<<-SQL)
UPDATE #{destination_table_name_quoted}
SET #{set_fields}
FROM
(VALUES
#{temporary_table_values_list.map { |vals| "(#{vals.join(', ')})" }.join(', ')}
) AS #{temporary_table_def}
WHERE
#{[where_cond, relation_cond].compact.join(' AND ')}
SQL
end
@ippeiukai
Copy link
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment