debian-mirror-gitlab/spec/models/concerns/bulk_insert_safe_spec.rb

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

273 lines
9 KiB
Ruby
Raw Permalink Normal View History

2020-03-13 15:44:24 +05:30
# frozen_string_literal: true
require 'spec_helper'
2023-04-23 21:23:45 +05:30
RSpec.describe BulkInsertSafe, feature_category: :database do
2020-04-08 14:13:33 +05:30
before(:all) do
ActiveRecord::Schema.define do
2021-12-11 22:18:48 +05:30
create_table :_test_bulk_insert_parent_items, force: true do |t|
2021-06-08 01:23:25 +05:30
t.string :name, null: false
end
2021-12-11 22:18:48 +05:30
create_table :_test_bulk_insert_items, force: true do |t|
2020-04-08 14:13:33 +05:30
t.string :name, null: true
t.integer :enum_value, null: false
t.text :encrypted_secret_value, null: false
t.string :encrypted_secret_value_iv, null: false
t.binary :sha_value, null: false, limit: 20
2020-04-22 19:07:51 +05:30
t.jsonb :jsonb_value, null: false
2021-12-11 22:18:48 +05:30
t.belongs_to :bulk_insert_parent_item, foreign_key: { to_table: :_test_bulk_insert_parent_items }, null: true
2021-11-18 22:05:49 +05:30
t.timestamps null: true
2020-04-08 14:13:33 +05:30
t.index :name, unique: true
end
2021-09-04 01:27:46 +05:30
2021-12-11 22:18:48 +05:30
create_table :_test_bulk_insert_items_with_composite_pk, id: false, force: true do |t|
2021-09-04 01:27:46 +05:30
t.integer :id, null: true
t.string :name, null: true
end
2021-12-11 22:18:48 +05:30
execute("ALTER TABLE _test_bulk_insert_items_with_composite_pk ADD PRIMARY KEY (id,name);")
2020-04-08 14:13:33 +05:30
end
end
after(:all) do
ActiveRecord::Schema.define do
2021-12-11 22:18:48 +05:30
drop_table :_test_bulk_insert_items, force: true
drop_table :_test_bulk_insert_parent_items, force: true
drop_table :_test_bulk_insert_items_with_composite_pk, force: true
2020-04-08 14:13:33 +05:30
end
2020-07-28 23:09:34 +05:30
end
2021-06-08 01:23:25 +05:30
BulkInsertParentItem = Class.new(ActiveRecord::Base) do
2021-12-11 22:18:48 +05:30
self.table_name = :_test_bulk_insert_parent_items
2021-06-08 01:23:25 +05:30
self.inheritance_column = :_type_disabled
def self.name
table_name.singularize.camelcase
end
end
let_it_be(:bulk_insert_parent_item) do
BulkInsertParentItem.create!(name: 'parent')
end
2020-07-28 23:09:34 +05:30
let_it_be(:bulk_insert_item_class) do
Class.new(ActiveRecord::Base) do
2021-12-11 22:18:48 +05:30
self.table_name = '_test_bulk_insert_items'
2020-06-23 00:09:42 +05:30
2020-07-28 23:09:34 +05:30
include BulkInsertSafe
include ShaAttribute
validates :name, :enum_value, :secret_value, :sha_value, :jsonb_value, presence: true
2021-06-08 01:23:25 +05:30
belongs_to :bulk_insert_parent_item
2020-07-28 23:09:34 +05:30
sha_attribute :sha_value
enum enum_value: { case_1: 1 }
attr_encrypted :secret_value,
mode: :per_attribute_iv,
algorithm: 'aes-256-gcm',
key: Settings.attr_encrypted_db_key_base_32,
insecure_mode: false
2023-01-13 00:05:48 +05:30
attribute :enum_value, default: 'case_1'
attribute :sha_value, default: '2fd4e1c67a2d28fced849ee1bb76e7391b93eb12'
attribute :jsonb_value, default: -> { { "key" => "value" } }
2020-07-28 23:09:34 +05:30
def self.name
'BulkInsertItem'
end
2021-06-08 01:23:25 +05:30
def self.valid_list(count, bulk_insert_parent_item: nil)
Array.new(count) { |n| new(name: "item-#{n}", secret_value: 'my-secret', bulk_insert_parent_item: bulk_insert_parent_item) }
2020-07-28 23:09:34 +05:30
end
def self.invalid_list(count)
2021-03-11 19:13:27 +05:30
Array.new(count) { new(secret_value: 'my-secret') }
2020-07-28 23:09:34 +05:30
end
end
2020-04-08 14:13:33 +05:30
end
2020-07-28 23:09:34 +05:30
describe 'BulkInsertItem' do
it_behaves_like 'a BulkInsertSafe model' do
let(:target_class) { bulk_insert_item_class.dup }
let(:valid_items_for_bulk_insertion) { target_class.valid_list(10) }
let(:invalid_items_for_bulk_insertion) { target_class.invalid_list(10) }
2020-04-08 14:13:33 +05:30
end
context 'when inheriting class methods' do
2020-07-28 23:09:34 +05:30
let(:inherited_unsafe_methods_module) do
Module.new do
extend ActiveSupport::Concern
included do
after_save -> { "unsafe" }
end
end
end
let(:inherited_safe_methods_module) do
Module.new do
extend ActiveSupport::Concern
included do
after_initialize -> { "safe" }
end
end
end
2020-04-08 14:13:33 +05:30
it 'raises an error when method is not bulk-insert safe' do
2020-07-28 23:09:34 +05:30
expect { bulk_insert_item_class.include(inherited_unsafe_methods_module) }
.to raise_error(bulk_insert_item_class::MethodNotAllowedError)
2020-04-08 14:13:33 +05:30
end
2020-03-13 15:44:24 +05:30
2020-04-08 14:13:33 +05:30
it 'does not raise an error when method is bulk-insert safe' do
2020-07-28 23:09:34 +05:30
expect { bulk_insert_item_class.include(inherited_safe_methods_module) }.not_to raise_error
2020-04-08 14:13:33 +05:30
end
2020-03-13 15:44:24 +05:30
end
2020-04-08 14:13:33 +05:30
context 'primary keys' do
it 'raises error if primary keys are set prior to insertion' do
2021-03-11 19:13:27 +05:30
item = bulk_insert_item_class.new(name: 'valid', id: 10, secret_value: 'my-secret')
2020-04-08 14:13:33 +05:30
2020-07-28 23:09:34 +05:30
expect { bulk_insert_item_class.bulk_insert!([item]) }
.to raise_error(bulk_insert_item_class::PrimaryKeySetError)
2020-04-08 14:13:33 +05:30
end
end
describe '.bulk_insert!' do
it 'inserts items in the given number of batches' do
2020-07-28 23:09:34 +05:30
items = bulk_insert_item_class.valid_list(10)
2020-04-08 14:13:33 +05:30
expect(ActiveRecord::InsertAll).to receive(:new).twice.and_call_original
2020-07-28 23:09:34 +05:30
bulk_insert_item_class.bulk_insert!(items, batch_size: 5)
2020-04-08 14:13:33 +05:30
end
2021-06-08 01:23:25 +05:30
it 'inserts items with belongs_to association' do
items = bulk_insert_item_class.valid_list(10, bulk_insert_parent_item: bulk_insert_parent_item)
bulk_insert_item_class.bulk_insert!(items, batch_size: 5)
expect(bulk_insert_item_class.last(items.size).map(&:bulk_insert_parent_item)).to eq([bulk_insert_parent_item] * 10)
end
2020-04-08 14:13:33 +05:30
it 'items can be properly fetched from database' do
2020-07-28 23:09:34 +05:30
items = bulk_insert_item_class.valid_list(10)
2020-04-08 14:13:33 +05:30
2020-07-28 23:09:34 +05:30
bulk_insert_item_class.bulk_insert!(items)
2020-04-08 14:13:33 +05:30
2020-07-28 23:09:34 +05:30
attribute_names = bulk_insert_item_class.attribute_names - %w[id created_at updated_at]
expect(bulk_insert_item_class.last(items.size).pluck(*attribute_names)).to eq(
2020-04-08 14:13:33 +05:30
items.pluck(*attribute_names))
end
it 'rolls back the transaction when any item is invalid' do
# second batch is bad
2021-06-08 01:23:25 +05:30
all_items = bulk_insert_item_class.valid_list(10) + bulk_insert_item_class.invalid_list(10)
2020-04-08 14:13:33 +05:30
expect do
2022-08-27 11:52:29 +05:30
bulk_insert_item_class.bulk_insert!(all_items, batch_size: 2)
rescue StandardError
nil
2020-07-28 23:09:34 +05:30
end.not_to change { bulk_insert_item_class.count }
2020-04-08 14:13:33 +05:30
end
2020-04-22 19:07:51 +05:30
it 'does nothing and returns an empty array when items are empty' do
2020-07-28 23:09:34 +05:30
expect(bulk_insert_item_class.bulk_insert!([])).to eq([])
expect(bulk_insert_item_class.count).to eq(0)
2020-04-08 14:13:33 +05:30
end
2020-04-22 19:07:51 +05:30
context 'with returns option set' do
2021-11-18 22:05:49 +05:30
let(:items) { bulk_insert_item_class.valid_list(1) }
2021-12-11 22:18:48 +05:30
subject(:legacy_bulk_insert) { bulk_insert_item_class.bulk_insert!(items, returns: returns) }
2021-11-18 22:05:49 +05:30
2020-04-22 19:07:51 +05:30
context 'when is set to :ids' do
2021-11-18 22:05:49 +05:30
let(:returns) { :ids }
2020-04-22 19:07:51 +05:30
2021-11-18 22:05:49 +05:30
it { is_expected.to contain_exactly(a_kind_of(Integer)) }
2020-04-22 19:07:51 +05:30
end
context 'when is set to nil' do
2021-11-18 22:05:49 +05:30
let(:returns) { nil }
2020-04-22 19:07:51 +05:30
2021-11-18 22:05:49 +05:30
it { is_expected.to eq([]) }
2020-04-22 19:07:51 +05:30
end
2021-11-18 22:05:49 +05:30
context 'when is set to a list of attributes' do
let(:returns) { [:id, :sha_value] }
2020-04-22 19:07:51 +05:30
2021-11-18 22:05:49 +05:30
it { is_expected.to contain_exactly([a_kind_of(Integer), '2fd4e1c67a2d28fced849ee1bb76e7391b93eb12']) }
2020-04-22 19:07:51 +05:30
end
end
2020-04-08 14:13:33 +05:30
end
context 'when duplicate items are to be inserted' do
2020-07-28 23:09:34 +05:30
let!(:existing_object) { bulk_insert_item_class.create!(name: 'duplicate', secret_value: 'old value') }
let(:new_object) { bulk_insert_item_class.new(name: 'duplicate', secret_value: 'new value') }
2020-04-08 14:13:33 +05:30
describe '.bulk_insert!' do
context 'when skip_duplicates is set to false' do
it 'raises an exception' do
2020-07-28 23:09:34 +05:30
expect { bulk_insert_item_class.bulk_insert!([new_object], skip_duplicates: false) }
2020-04-08 14:13:33 +05:30
.to raise_error(ActiveRecord::RecordNotUnique)
end
end
context 'when skip_duplicates is set to true' do
it 'does not update existing object' do
2020-07-28 23:09:34 +05:30
bulk_insert_item_class.bulk_insert!([new_object], skip_duplicates: true)
2020-04-08 14:13:33 +05:30
expect(existing_object.reload.secret_value).to eq('old value')
end
end
end
describe '.bulk_upsert!' do
2021-11-18 22:05:49 +05:30
subject(:bulk_upsert) { bulk_insert_item_class.bulk_upsert!([new_object], unique_by: %w[name]) }
2020-04-08 14:13:33 +05:30
it 'updates existing object' do
2021-11-18 22:05:49 +05:30
expect { bulk_upsert }.to change { existing_object.reload.secret_value }.to('new value')
end
2020-04-08 14:13:33 +05:30
2021-11-18 22:05:49 +05:30
context 'when the `created_at` attribute is provided' do
before do
new_object.created_at = 10.days.from_now
end
it 'does not change the existing `created_at` value' do
expect { bulk_upsert }.not_to change { existing_object.reload.created_at }
end
2020-04-08 14:13:33 +05:30
end
end
2020-03-13 15:44:24 +05:30
end
2021-09-04 01:27:46 +05:30
context 'when a model with composite primary key is inserted' do
let_it_be(:bulk_insert_items_with_composite_pk_class) do
Class.new(ActiveRecord::Base) do
2021-12-11 22:18:48 +05:30
self.table_name = '_test_bulk_insert_items_with_composite_pk'
2021-09-04 01:27:46 +05:30
include BulkInsertSafe
end
end
let(:new_object) { bulk_insert_items_with_composite_pk_class.new(id: 1, name: 'composite') }
it 'successfully inserts an item' do
expect(ActiveRecord::InsertAll).to receive(:new)
.with(
2021-11-18 22:05:49 +05:30
bulk_insert_items_with_composite_pk_class.insert_all_proxy_class, [new_object.as_json], on_duplicate: :raise, returning: false, unique_by: %w[id name]
2021-09-04 01:27:46 +05:30
).and_call_original
expect { bulk_insert_items_with_composite_pk_class.bulk_insert!([new_object]) }.to(
change(bulk_insert_items_with_composite_pk_class, :count).from(0).to(1)
)
end
end
2020-03-13 15:44:24 +05:30
end
end