From 6b7a55804176133526edf60261ea84c5a4292536 Mon Sep 17 00:00:00 2001 From: sishuo Date: Wed, 19 Aug 2026 16:19:39 +0800 Subject: [PATCH] fix(postgres-migration): make the seed re-runnable A seed that died partway could not be restarted. The second run re-inserted products, hit sku's unique constraint and aborted, so recovering meant manual cleanup before the file would run at all -- and the file gives no hint of that. Guard each INSERT with a count against its own target. Each table is loaded by one statement, so a table is either empty of seed rows or complete and there is no partially-seeded state for the guard to misjudge. The orders and order_items guards compare against the target rather than testing for emptiness, because app/writer.py inserts into both and would otherwise make them look finished while the seed rows are still missing. Verified in four states: fresh load, re-run when fully seeded, re-run after losing only order_items, and re-run with writer rows present. Re-running against an already-seeded RDS instance now reports four INSERT 0 0 and takes 20 seconds. Co-Authored-By: Claude Opus 5 (1M context) --- workshops/postgres_migration/sql/02_seed.sql | 23 ++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/workshops/postgres_migration/sql/02_seed.sql b/workshops/postgres_migration/sql/02_seed.sql index ab10be1..5046de2 100644 --- a/workshops/postgres_migration/sql/02_seed.sql +++ b/workshops/postgres_migration/sql/02_seed.sql @@ -13,6 +13,17 @@ -- -- The defaults are sized for the RDS instance created by ../terraform. They are far too -- large to run against a laptop container -- pass smaller values if you are testing. +-- +-- Re-runnable. Each INSERT is guarded by a count against its own target, so a seed that died +-- partway -- or was killed -- can simply be run again and only the unfinished tables reload. +-- Without the guards a second run aborts on products.sku's unique constraint and leaves the +-- reload impossible without manual cleanup. Each table is loaded by ONE statement, so a table is +-- either empty of seed rows or complete; there is no partially-seeded state for the guard to +-- misjudge. The orders and order_items guards compare against the target rather than testing for +-- emptiness, because app/writer.py inserts into both and would otherwise make them look done. +-- +-- To reseed at a different size, truncate first: +-- TRUNCATE order_items, orders, products, customers RESTART IDENTITY; \if :{?customers} \else \set customers 500000 @@ -36,20 +47,23 @@ INSERT INTO customers (email, region, created_at) SELECT 'c' || g || '@example.test', (ARRAY['us-east','us-west','eu-west','apac'])[1 + (g % 4)], now() - (random() * interval '365 days') -FROM generate_series(1, :customers) g; +FROM generate_series(1, :customers) g +WHERE (SELECT count(*) FROM customers) < :customers; INSERT INTO products (sku, category, unit_price) SELECT 'SKU-' || lpad(g::text, 7, '0'), (ARRAY['audio','video','home','outdoor','kitchen'])[1 + (g % 5)], round((random() * 400 + 5)::numeric, 2) -FROM generate_series(1, :products) g; +FROM generate_series(1, :products) g +WHERE (SELECT count(*) FROM products) < :products; INSERT INTO orders (customer_id, status, placed_at, updated_at) SELECT 1 + (random() * (:customers - 1))::bigint, (ARRAY['placed','paid','shipped','refunded'])[1 + (g % 4)], now() - (random() * interval '90 days'), now() -FROM generate_series(1, :orders) g; +FROM generate_series(1, :orders) g +WHERE (SELECT count(*) FROM orders) < :orders; -- One row per (order, item slot). order_items.placed_at is copied from the order so the -- benchmark queries in ../bench can filter either table on the same time window. @@ -59,6 +73,7 @@ SELECT o.order_id, 1 + (random() * 4)::int, round((random() * 500 + 1)::numeric, 2), o.placed_at -FROM orders o, generate_series(1, :items_per_order); +FROM orders o, generate_series(1, :items_per_order) +WHERE (SELECT count(*) FROM order_items) < :orders * :items_per_order; ANALYZE;