1# frozen_string_literal: true 2 3require 'spec_helper' 4 5RSpec.describe BulkInsertableAssociations do 6 class BulkFoo < ApplicationRecord 7 include BulkInsertSafe 8 9 self.table_name = '_test_bulk_foos' 10 11 validates :name, presence: true 12 end 13 14 class BulkBar < ApplicationRecord 15 include BulkInsertSafe 16 17 self.table_name = '_test_bulk_bars' 18 end 19 20 SimpleBar = Class.new(ApplicationRecord) do 21 self.table_name = '_test_simple_bars' 22 end 23 24 class BulkParent < ApplicationRecord 25 include BulkInsertableAssociations 26 27 self.table_name = '_test_bulk_parents' 28 29 has_many :bulk_foos, class_name: 'BulkFoo' 30 has_many :bulk_hunks, class_name: 'BulkFoo' 31 has_many :bulk_bars, class_name: 'BulkBar' 32 has_many :simple_bars, class_name: 'SimpleBar' # not `BulkInsertSafe` 33 has_one :bulk_foo # not supported 34 end 35 36 before(:all) do 37 ActiveRecord::Schema.define do 38 create_table :_test_bulk_parents, force: true do |t| 39 t.string :name, null: true 40 end 41 42 create_table :_test_bulk_foos, force: true do |t| 43 t.string :name, null: true 44 t.belongs_to :bulk_parent, null: false 45 end 46 47 create_table :_test_bulk_bars, force: true do |t| 48 t.string :name, null: true 49 t.belongs_to :bulk_parent, null: false 50 end 51 52 create_table :_test_simple_bars, force: true do |t| 53 t.string :name, null: true 54 t.belongs_to :bulk_parent, null: false 55 end 56 end 57 end 58 59 after(:all) do 60 ActiveRecord::Schema.define do 61 drop_table :_test_bulk_foos, force: true 62 drop_table :_test_bulk_bars, force: true 63 drop_table :_test_simple_bars, force: true 64 drop_table :_test_bulk_parents, force: true 65 end 66 end 67 68 context 'saving bulk insertable associations' do 69 let(:parent) { BulkParent.new(name: 'parent') } 70 71 context 'when items already have IDs' do 72 it 'stores nothing and raises an error' do 73 build_items(parent: parent) { |n, item| item.id = n } 74 75 expect { save_with_bulk_inserts(parent) }.to raise_error(BulkInsertSafe::PrimaryKeySetError) 76 expect(BulkFoo.count).to eq(0) 77 end 78 end 79 80 context 'when items have no IDs set' do 81 it 'stores them all and updates items with IDs' do 82 items = build_items(parent: parent) 83 84 expect(BulkFoo).to receive(:bulk_insert!).once.and_call_original 85 expect { save_with_bulk_inserts(parent) }.to change { BulkFoo.count }.from(0).to(items.size) 86 expect(parent.bulk_foos.pluck(:id)).to all(be_a Integer) 87 end 88 end 89 90 context 'when items are empty' do 91 it 'does nothing' do 92 expect(parent.bulk_foos).to be_empty 93 94 expect { save_with_bulk_inserts(parent) }.not_to change { BulkFoo.count } 95 end 96 end 97 98 context 'when relation name does not match class name' do 99 it 'stores them all' do 100 items = build_items(parent: parent, relation: :bulk_hunks) 101 102 expect(BulkFoo).to receive(:bulk_insert!).once.and_call_original 103 104 expect { save_with_bulk_inserts(parent) }.to( 105 change { BulkFoo.count }.from(0).to(items.size) 106 ) 107 end 108 end 109 110 context 'with multiple threads' do 111 it 'isolates bulk insert behavior between threads' do 112 total_item_count = 10 113 parent1 = BulkParent.new(name: 'parent1') 114 parent2 = BulkParent.new(name: 'parent2') 115 build_items(parent: parent1, count: total_item_count / 2) 116 build_items(parent: parent2, count: total_item_count / 2) 117 118 expect(BulkFoo).to receive(:bulk_insert!).once.and_call_original 119 [ 120 Thread.new do 121 save_with_bulk_inserts(parent1) 122 end, 123 Thread.new do 124 parent2.save! 125 end 126 ].map(&:join) 127 128 expect(BulkFoo.count).to eq(total_item_count) 129 end 130 end 131 132 context 'with multiple associations' do 133 it 'isolates writes between associations' do 134 items1 = build_items(parent: parent, relation: :bulk_foos) 135 items2 = build_items(parent: parent, relation: :bulk_bars) 136 137 expect(BulkFoo).to receive(:bulk_insert!).once.and_call_original 138 expect(BulkBar).to receive(:bulk_insert!).once.and_call_original 139 140 expect { save_with_bulk_inserts(parent) }.to( 141 change { BulkFoo.count }.from(0).to(items1.size) 142 .and( 143 change { BulkBar.count }.from(0).to(items2.size) 144 )) 145 end 146 end 147 148 context 'passing bulk insert arguments' do 149 it 'disables validations on target association' do 150 items = build_items(parent: parent) 151 152 expect(BulkFoo).to receive(:bulk_insert!).with(items, validate: false).and_return true 153 154 save_with_bulk_inserts(parent) 155 end 156 end 157 158 it 'can disable bulk-inserts within a bulk-insert block' do 159 parent1 = BulkParent.new(name: 'parent1') 160 parent2 = BulkParent.new(name: 'parent2') 161 _items1 = build_items(parent: parent1) 162 items2 = build_items(parent: parent2) 163 164 expect(BulkFoo).to receive(:bulk_insert!).once.with(items2, validate: false) 165 166 BulkInsertableAssociations.with_bulk_insert(enabled: true) do 167 BulkInsertableAssociations.with_bulk_insert(enabled: false) do 168 parent1.save! 169 end 170 171 parent2.save! 172 end 173 end 174 175 context 'when association is not bulk-insert safe' do 176 it 'saves it normally' do 177 parent.simple_bars.build 178 179 expect(SimpleBar).not_to receive(:bulk_insert!) 180 expect { save_with_bulk_inserts(parent) }.to change { SimpleBar.count }.from(0).to(1) 181 end 182 end 183 184 context 'when association is not has_many' do 185 it 'saves it normally' do 186 parent.bulk_foo = BulkFoo.new(name: 'item') 187 188 expect(BulkFoo).not_to receive(:bulk_insert!) 189 expect { save_with_bulk_inserts(parent) }.to change { BulkFoo.count }.from(0).to(1) 190 end 191 end 192 193 context 'when an item is not valid' do 194 describe '.save' do 195 it 'invalidates the parent and returns false' do 196 build_invalid_items(parent: parent) 197 198 expect(BulkInsertableAssociations.with_bulk_insert { parent.save }).to be false # rubocop:disable Rails/SaveBang 199 expect(parent.errors[:bulk_foos].size).to eq(1) 200 201 expect(BulkFoo.count).to eq(0) 202 expect(BulkParent.count).to eq(0) 203 end 204 end 205 206 describe '.save!' do 207 it 'invalidates the parent and raises error' do 208 build_invalid_items(parent: parent) 209 210 expect { save_with_bulk_inserts(parent) }.to raise_error(ActiveRecord::RecordInvalid) 211 expect(parent.errors[:bulk_foos].size).to eq(1) 212 213 expect(BulkFoo.count).to eq(0) 214 expect(BulkParent.count).to eq(0) 215 end 216 end 217 end 218 end 219 220 private 221 222 def save_with_bulk_inserts(entity) 223 BulkInsertableAssociations.with_bulk_insert { entity.save! } 224 end 225 226 def build_items(parent:, relation: :bulk_foos, count: 10) 227 count.times do |n| 228 item = parent.send(relation).build(name: "item_#{n}", bulk_parent_id: parent.id) 229 yield(n, item) if block_given? 230 end 231 parent.send(relation) 232 end 233 234 def build_invalid_items(parent:) 235 build_items(parent: parent).tap do |items| 236 invalid_item = items.first 237 invalid_item.name = nil 238 expect(invalid_item).not_to be_valid 239 end 240 end 241end 242