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:
parent
4b0ab8e4b2
commit
611b621d75
3 changed files with 84 additions and 2 deletions
1
docs/erd.svg
Normal file
1
docs/erd.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 552 KiB |
|
|
@ -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");
|
||||
|
|
@ -144,6 +144,7 @@ model Product {
|
|||
pbis Pbi[]
|
||||
sprints Sprint[]
|
||||
stories Story[]
|
||||
tasks Task[]
|
||||
todos Todo[]
|
||||
members ProductMember[]
|
||||
active_for_users User[] @relation("UserActiveProduct")
|
||||
|
|
@ -160,7 +161,7 @@ model Pbi {
|
|||
id String @id @default(cuid())
|
||||
product Product @relation(fields: [product_id], references: [id], onDelete: Cascade)
|
||||
product_id String
|
||||
code String? @db.VarChar(30)
|
||||
code String @db.VarChar(30)
|
||||
title String
|
||||
description String?
|
||||
priority Int
|
||||
|
|
@ -188,7 +189,7 @@ model Story {
|
|||
sprint_id String?
|
||||
assignee User? @relation("StoryAssignee", fields: [assignee_id], references: [id], onDelete: SetNull)
|
||||
assignee_id String?
|
||||
code String? @db.VarChar(30)
|
||||
code String @db.VarChar(30)
|
||||
title String
|
||||
description String?
|
||||
acceptance_criteria String?
|
||||
|
|
@ -246,8 +247,11 @@ model Task {
|
|||
id String @id @default(cuid())
|
||||
story Story @relation(fields: [story_id], references: [id], onDelete: Cascade)
|
||||
story_id String
|
||||
product Product @relation(fields: [product_id], references: [id], onDelete: Cascade)
|
||||
product_id String
|
||||
sprint Sprint? @relation(fields: [sprint_id], references: [id])
|
||||
sprint_id String?
|
||||
code String @db.VarChar(30)
|
||||
title String
|
||||
description String?
|
||||
implementation_plan String?
|
||||
|
|
@ -266,8 +270,10 @@ model Task {
|
|||
claude_questions ClaudeQuestion[]
|
||||
claude_jobs ClaudeJob[]
|
||||
|
||||
@@unique([product_id, code])
|
||||
@@index([story_id, priority, sort_order])
|
||||
@@index([sprint_id, status])
|
||||
@@index([product_id])
|
||||
@@map("tasks")
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue