Skip to content

Instantly share code, notes, and snippets.

@ippeiukai
Last active August 29, 2015 14:24
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ippeiukai/179c2e9442eae28ff44b to your computer and use it in GitHub Desktop.
Save ippeiukai/179c2e9442eae28ff44b to your computer and use it in GitHub Desktop.
Bulk update some rows with different values. (for Sequel)
module SequelBulkUpdate
class << self
# NOTE assumes the relation is simple
#
# @param relation [Sequel::Dataset] scope of update
# @param values [Array<Hash{Symbol=>_},#to_h>] values of each row required for updating
# @param where_columns [Array<Symbol>] columns used to identify the target row
# @param set_columns [Array<Symbol>] columns to update
def bulk_update(dataset, values, where_columns, set_columns)
_bulk_update(:update, dataset, values, where_columns, set_columns)
end
def bulk_update_sql(dataset, values, where_columns, set_columns)
_bulk_update(:update_sql, dataset, values, where_columns, set_columns)
end
private
def _bulk_update(update_method, dataset, values, where_columns, set_columns)
return if values.empty?
db = dataset.db
temporary_table_columns = [*where_columns, *set_columns]
temporary_table_dataset = values.each_with_index.inject(db.dataset) do |tmp_dataset, (record, i)|
values = record.to_h.values_at(*temporary_table_columns)
if i == 0
values_with_alias = temporary_table_columns.zip(values).map do |col_name, value|
Sequel.as(value, col_name)
end
tmp_dataset.select(*values_with_alias)
else
tmp_dataset.union(db.dataset.select(*values), all: true, from_self: false)
end
end
if dataset.supports_modifying_joins?
temporary_table_name = dataset.unused_table_alias(:update_data)
join_cond = where_columns.map do |col|
{Sequel.qualify(temporary_table_name, col) => Sequel.qualify(dataset.first_source_table, col)}
end.inject(&:merge)
update_cols = set_columns.map do |col|
{col => Sequel.qualify(temporary_table_name, col)}
end.inject(&:merge)
dataset.
join(Sequel.as(temporary_table_dataset, temporary_table_name), join_cond).
public_send(update_method, update_cols)
else
# SQLite etc.
correlate_cond = where_columns.map do |col|
{col => Sequel.qualify(dataset.first_source_table, col)}
end.inject(&:merge)
update_cols = set_columns.map do |col|
{col => db.dataset.select(col).from(temporary_table_dataset).where(correlate_cond)}
end.inject(&:merge)
dataset.
where(db.dataset.from(temporary_table_dataset).where(correlate_cond).exists).
public_send(update_method, update_cols)
end
end
end
extend ActiveSupport::Concern
# @see ActiveRecordBulkUpdate.bulk_update
def bulk_update(*args, &block)
SequelBulkUpdate.bulk_update(self, *args, &block)
end
# @see ActiveRecordBulkUpdate.bulk_update_sql
def bulk_update_sql(*args, &block)
SequelBulkUpdate.bulk_update_sql(self, *args, &block)
end
end
@ippeiukai
Copy link
Author

Demo:

DB = Sequel.sqlite(':memory:')
DB.create_table!(:t) { primary_key :id; Integer :c }
DB[:t].multi_insert([{id: 1, c: 100}, {id: 2, c: 200}, {id: 3, c: 300}])
DB[:t].all  # => [{:id=>1, :c=>100}, {:id=>2, :c=>200}, {:id=>3, :c=>300}]

DB[:t].extend(SequelBulkUpdate).bulk_update([{id: 3, c: 30}, {id: 1, c: 1000}, {id: 4, c: 4}], [:id], [:c])
DB[:t].all  # => [{:id=>1, :c=>1000}, {:id=>2, :c=>200}, {:id=>3, :c=>30}]

Special thanks to following posts:
http://stackoverflow.com/a/19033152/4212945
http://stackoverflow.com/a/21074659/4212945

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