Created
April 15, 2015 21:44
-
-
Save gregnavis/e93c8cdc2a254e356622 to your computer and use it in GitHub Desktop.
An InsertQuery object that can be used to insert models in bulk.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class InsertQuery | |
def initialize(model_class, column_names, returning = nil) | |
@model_class = model_class | |
@column_names = column_names | |
@returning = returning | |
@rows = [] | |
@executed = false | |
end | |
def insert(model_attributes) | |
ensure_not_executed | |
rows << prepare_model_attributes(model_attributes, Time.zone.now) | |
end | |
def prepare_model_attributes(model_attributes, now) | |
column_names.map do |column_name| | |
prepare_model_attribute(column_name, model_attributes, now) | |
end | |
end | |
def prepare_model_attribute(column_name, model_attributes, now) | |
column_name = column_name.to_sym | |
if %i(created_at updated_at).include?(column_name) | |
if model_attributes.include?(column_name) | |
model_attributes[column_name] | |
else | |
now | |
end | |
else | |
model_attributes.fetch(column_name, :default) | |
end | |
end | |
def execute | |
ensure_not_executed | |
yield(self) if block_given? | |
result = connection.execute(query).values unless rows.empty? | |
self.executed = true | |
result | |
end | |
def self.execute(*constructor_args, &block) | |
new(*constructor_args).execute(&block) | |
end | |
private | |
attr_reader :model_class, :column_names, :rows, :returning | |
attr_accessor :executed | |
delegate :connection, to: :model_class | |
def ensure_not_executed | |
fail("the query has been already executed") if executed | |
end | |
def query | |
<<-SQL | |
INSERT INTO | |
#{model_class.table_name} | |
(#{column_names.join(', ')}) | |
VALUES #{ | |
rows.map do |row| | |
"(#{row.map { |datum| quote(datum) }.join(', ')})" | |
end.join(', ') | |
} | |
#{returning_clause} | |
SQL | |
end | |
def quote(datum) | |
if datum == :default | |
'default' | |
else | |
connection.quote(datum) | |
end | |
end | |
def returning_clause | |
%(RETURNING #{returning}) if returning | |
end | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require 'test_helper' | |
class InsertQueryTest < ActiveSupport::TestCase | |
test 'does nothing when no data provided' do | |
@query.execute | |
assert_equal(0, Dog.count) | |
end | |
test 'can execute only once' do | |
@query.execute | |
assert_raise(RuntimeError) { @query.execute } | |
end | |
test 'inserts multiple rows and returns the specified columns' do | |
@query.insert(name: 'Woof', age: 3) | |
@query.insert(name: 'Waf', age: 1) | |
result = @query.execute | |
assert_equal(2, Dog.count) | |
assert_equal([['Woof'], ['Waf']], result) | |
end | |
test 'automatically executes the block supplied to #execute' do | |
result = InsertQuery.new(Dog, [:name, :age], :name).execute do |query| | |
query.insert(name: 'Woof', age: 1) | |
end | |
assert_equal(1, Dog.count) | |
assert_equal([['Woof']], result) | |
end | |
test 'omitting an attribute uses its default value' do | |
@query.insert(name: 'Woof') | |
@query.execute | |
assert_equal([1], Dog.pluck(:age)) | |
end | |
test "set created_at and updated_at to now if they are absent" do | |
# Calls to #beginning_of_minute are required in order to reset the | |
# microseconds part. Otherwise the assertion below fails yet the result | |
# of inspecting the arrays is the same (despite the fact that they just | |
# compared unequal!). | |
time = 1.hour.ago.beginning_of_minute | |
now = Time.zone.now.beginning_of_minute | |
travel_to(now) | |
@query.insert(name: 'Woof', created_at: time) | |
@query.insert(name: 'Waf', updated_at: time) | |
@query.insert(name: 'Wef', created_at: nil, updated_at: nil) | |
@query.execute | |
assert_equal([[time, now], [now, time], [nil, nil]].sort, Dog.pluck(:created_at, :updated_at).sort) | |
end | |
setup do | |
Temping.create :dogs do | |
with_columns do |t| | |
t.string :name | |
t.integer :age, default: 1 | |
t.timestamp :created_at | |
t.timestamp :updated_at | |
end | |
end | |
@query = InsertQuery.new(Dog, [:name, :age, :created_at, :updated_at], :name) | |
end | |
teardown do | |
travel_back | |
# This is required to make Temping work. Without it the table is created | |
# before the first test and the model class is defined. However, after the | |
# test finishes the transaction is rolled back. The table is gone but the | |
# model is still defined. This makes Temping think the corresponding table | |
# is defined too. If we undefine the model then Temping correctly recreates | |
# the model and the table. | |
Object.send(:remove_const, :Dog) | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment