Skip to content

Commit c27556a

Browse files
nganclaude
andcommitted
Verify foreign keys after mount when configured
Mirror Rails' fixture-loading FK check: when ActiveRecord.verify_foreign_keys_for_fixtures is true (default since Rails 8.0 load_defaults), call connection.check_all_foreign_keys_valid! after the batch executes. PG and SQLite raise on violations; MySQL is a no-op. Wrap any StatementInvalid in a FixtureKit::Error with a hint that the cache may be stale relative to the schema or fixture definitions — most likely trigger is a developer regenerating fixtures against a different schema than the one currently loaded. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent eecc0a3 commit c27556a

2 files changed

Lines changed: 80 additions & 0 deletions

File tree

lib/fixture_kit/coders/active_record_coder.rb

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ def mount(data)
4141
# This should be revisited when Rails 8.2 makes it public.
4242
connection.__send__(:execute_batch, statements, "FixtureKit Insert")
4343
end
44+
45+
verify_foreign_keys!(connection)
4446
end
4547
end
4648
end
@@ -93,6 +95,19 @@ def build_insert_sql(table_name, columns, rows, connection)
9395
"INSERT INTO #{quoted_table} (#{quoted_columns.join(", ")}) VALUES #{rows.join(", ")}"
9496
end
9597

98+
def verify_foreign_keys!(connection)
99+
return unless ActiveRecord.verify_foreign_keys_for_fixtures
100+
101+
begin
102+
connection.check_all_foreign_keys_valid!
103+
rescue ActiveRecord::StatementInvalid => e
104+
raise FixtureKit::Error,
105+
"Foreign key violations found in cached fixture data. The cache may be " \
106+
"stale relative to your current schema or fixture definitions. " \
107+
"Original error:\n\n#{e.message}"
108+
end
109+
end
110+
96111
def models_by_pool(data)
97112
seen = Set.new
98113

spec/unit/coders/active_record_coder_spec.rb

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,71 @@ def exercise_user_write_operations(suffix)
146146

147147
coder.mount(records)
148148
end
149+
150+
context "when ActiveRecord.verify_foreign_keys_for_fixtures is true" do
151+
around do |example|
152+
previous = ActiveRecord.verify_foreign_keys_for_fixtures
153+
ActiveRecord.verify_foreign_keys_for_fixtures = true
154+
example.run
155+
ensure
156+
ActiveRecord.verify_foreign_keys_for_fixtures = previous
157+
end
158+
159+
it "calls check_all_foreign_keys_valid! after the batch executes" do
160+
records = { User => "INSERT INTO users (id, name) VALUES (1, 'Alice')" }
161+
fake_connection = stub_fake_connection
162+
stub_pool(User, fake_connection)
163+
164+
coder.mount(records)
165+
166+
expect(fake_connection).to have_received(:check_all_foreign_keys_valid!).once
167+
end
168+
169+
it "wraps an FK violation in a FixtureKit::Error with a hint about stale cache" do
170+
records = { User => "INSERT INTO users (id, name) VALUES (1, 'Alice')" }
171+
fake_connection = stub_fake_connection
172+
allow(fake_connection).to receive(:check_all_foreign_keys_valid!)
173+
.and_raise(ActiveRecord::StatementInvalid, "Foreign key violations found: orders")
174+
stub_pool(User, fake_connection)
175+
176+
expect { coder.mount(records) }.to raise_error(FixtureKit::Error, /cached fixture data.*stale.*Foreign key violations found: orders/m)
177+
end
178+
end
179+
180+
context "when ActiveRecord.verify_foreign_keys_for_fixtures is false" do
181+
around do |example|
182+
previous = ActiveRecord.verify_foreign_keys_for_fixtures
183+
ActiveRecord.verify_foreign_keys_for_fixtures = false
184+
example.run
185+
ensure
186+
ActiveRecord.verify_foreign_keys_for_fixtures = previous
187+
end
188+
189+
it "does not call check_all_foreign_keys_valid!" do
190+
records = { User => "INSERT INTO users (id, name) VALUES (1, 'Alice')" }
191+
fake_connection = stub_fake_connection
192+
stub_pool(User, fake_connection)
193+
194+
coder.mount(records)
195+
196+
expect(fake_connection).not_to have_received(:check_all_foreign_keys_valid!)
197+
end
198+
end
199+
200+
def stub_fake_connection
201+
fake_connection = double("connection")
202+
allow(fake_connection).to receive(:disable_referential_integrity).and_yield
203+
allow(fake_connection).to receive(:execute_batch)
204+
allow(fake_connection).to receive(:quote_table_name) { |name| %("#{name}") }
205+
allow(fake_connection).to receive(:check_all_foreign_keys_valid!)
206+
fake_connection
207+
end
208+
209+
def stub_pool(model, connection)
210+
pool = double("pool-for-#{model.name}")
211+
allow(pool).to receive(:with_connection).and_yield(connection)
212+
allow(model).to receive(:connection_pool).and_return(pool)
213+
end
149214
end
150215

151216
describe "sql.active_record payload name format assumptions" do

0 commit comments

Comments
 (0)