feat(codes): code NOT NULL voor PBI/Story + Task.code + product_id denorm

- Pbi.code en Story.code worden NOT NULL (tot dusver optional)
- Task krijgt code String + product_id String denorm + @@unique([product_id, code])
- Product krijgt back-relation tasks Task[]
- Migratie backfillt bestaande NULL-rijen via PL/pgSQL:
  PBI-N (per product), ST-N (3-digit padded met GREATEST om
  truncatie van LPAD bij 4-digit nummers te voorkomen),
  T-N voor alle tasks
- Codes zijn stabiele identifiers (Jira-stijl flat-per-product),
  zodat re-parenting de code niet muteert

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Janpeter Visser 2026-05-04 08:36:19 +02:00
parent 4b0ab8e4b2
commit 611b621d75
3 changed files with 84 additions and 2 deletions

View file

@ -0,0 +1,75 @@
-- Codes verplicht maken voor PBI/Story en code-kolom + product_id denorm
-- toevoegen aan Task. Bestaande NULL-rijen worden gevuld via PL/pgSQL backfill.
-- 1) Tasks: product_id denorm (eerst nullable, backfill, dan NOT NULL + FK)
ALTER TABLE "tasks" ADD COLUMN "product_id" TEXT;
UPDATE "tasks" t SET "product_id" = s."product_id" FROM "stories" s WHERE s."id" = t."story_id";
ALTER TABLE "tasks" ALTER COLUMN "product_id" SET NOT NULL;
ALTER TABLE "tasks" ADD CONSTRAINT "tasks_product_id_fkey" FOREIGN KEY ("product_id") REFERENCES "products"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- 2) Tasks: code (eerst nullable t.b.v. backfill)
ALTER TABLE "tasks" ADD COLUMN "code" VARCHAR(30);
-- 3) Backfill PBI codes (alleen NULL-rijen, per product, op created_at)
DO $$
DECLARE rec RECORD;
DECLARE n INT;
BEGIN
FOR rec IN SELECT DISTINCT product_id FROM "pbis" WHERE code IS NULL LOOP
SELECT COALESCE(MAX(CAST(SUBSTRING(code FROM 'PBI-(\d+)$') AS INTEGER)), 0)
INTO n
FROM "pbis"
WHERE product_id = rec.product_id AND code ~ '^PBI-\d+$';
UPDATE "pbis" SET code = 'PBI-' || (n + sub.row_num)
FROM (
SELECT id, ROW_NUMBER() OVER (ORDER BY created_at, id) AS row_num
FROM "pbis"
WHERE product_id = rec.product_id AND code IS NULL
) sub
WHERE "pbis".id = sub.id;
END LOOP;
END $$;
-- 4) Backfill Story codes (TO_CHAR met FM-format: padding tot minimaal 3 chars zonder truncatie)
DO $$
DECLARE rec RECORD;
DECLARE n INT;
BEGIN
FOR rec IN SELECT DISTINCT product_id FROM "stories" WHERE code IS NULL LOOP
SELECT COALESCE(MAX(CAST(SUBSTRING(code FROM 'ST-(\d+)$') AS INTEGER)), 0)
INTO n
FROM "stories"
WHERE product_id = rec.product_id AND code ~ '^ST-\d+$';
UPDATE "stories" SET code = 'ST-' || LPAD((n + sub.row_num)::TEXT, GREATEST(3, LENGTH((n + sub.row_num)::TEXT)), '0')
FROM (
SELECT id, ROW_NUMBER() OVER (ORDER BY created_at, id) AS row_num
FROM "stories"
WHERE product_id = rec.product_id AND code IS NULL
) sub
WHERE "stories".id = sub.id;
END LOOP;
END $$;
-- 5) Backfill Task codes (alle rijen — kolom net toegevoegd)
DO $$
DECLARE rec RECORD;
BEGIN
FOR rec IN SELECT DISTINCT product_id FROM "tasks" WHERE code IS NULL LOOP
UPDATE "tasks" SET code = 'T-' || sub.row_num
FROM (
SELECT id, ROW_NUMBER() OVER (ORDER BY created_at, id) AS row_num
FROM "tasks"
WHERE product_id = rec.product_id AND code IS NULL
) sub
WHERE "tasks".id = sub.id;
END LOOP;
END $$;
-- 6) NOT NULL constraints
ALTER TABLE "pbis" ALTER COLUMN "code" SET NOT NULL;
ALTER TABLE "stories" ALTER COLUMN "code" SET NOT NULL;
ALTER TABLE "tasks" ALTER COLUMN "code" SET NOT NULL;
-- 7) Unique + lookup index op Task
CREATE UNIQUE INDEX "tasks_product_id_code_key" ON "tasks"("product_id", "code");
CREATE INDEX "tasks_product_id_idx" ON "tasks"("product_id");