From 611b621d75b8d9b0953b702d649ae6523a2f0367 Mon Sep 17 00:00:00 2001 From: Madhura68 Date: Mon, 4 May 2026 08:36:19 +0200 Subject: [PATCH] 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) --- docs/erd.svg | 1 + .../migration.sql | 75 +++++++++++++++++++ prisma/schema.prisma | 10 ++- 3 files changed, 84 insertions(+), 2 deletions(-) create mode 100644 docs/erd.svg create mode 100644 prisma/migrations/20260504055000_codes_required_and_task_code/migration.sql diff --git a/docs/erd.svg b/docs/erd.svg new file mode 100644 index 0000000..12b3637 --- /dev/null +++ b/docs/erd.svg @@ -0,0 +1 @@ +

active_product

user

enum:role

user

user

product

enum:status

pbi

product

sprint

assignee

enum:status

story

enum:type

enum:status

product

enum:status

story

product

sprint

enum:status

enum:verify_required

user

product

task

enum:status

claimed_by_token

enum:verify_result

user

token

product

user

user

product

user

story

task

product

asker

answerer

Role

PRODUCT_OWNER

PRODUCT_OWNER

SCRUM_MASTER

SCRUM_MASTER

DEVELOPER

DEVELOPER

StoryStatus

OPEN

OPEN

IN_SPRINT

IN_SPRINT

DONE

DONE

PbiStatus

READY

READY

BLOCKED

BLOCKED

DONE

DONE

ClaudeJobStatus

QUEUED

QUEUED

CLAIMED

CLAIMED

RUNNING

RUNNING

DONE

DONE

FAILED

FAILED

CANCELLED

CANCELLED

VerifyResult

ALIGNED

ALIGNED

PARTIAL

PARTIAL

EMPTY

EMPTY

DIVERGENT

DIVERGENT

VerifyRequired

ALIGNED

ALIGNED

ALIGNED_OR_PARTIAL

ALIGNED_OR_PARTIAL

ANY

ANY

TaskStatus

TO_DO

TO_DO

IN_PROGRESS

IN_PROGRESS

REVIEW

REVIEW

DONE

DONE

LogType

IMPLEMENTATION_PLAN

IMPLEMENTATION_PLAN

TEST_RESULT

TEST_RESULT

COMMIT

COMMIT

TestStatus

PASSED

PASSED

FAILED

FAILED

SprintStatus

ACTIVE

ACTIVE

COMPLETED

COMPLETED

users

String

id

🗝️

String

username

String

email

String

password_hash

Boolean

is_demo

String

bio

String

bio_detail

Bytes

avatar_data

DateTime

created_at

DateTime

updated_at

user_roles

String

id

🗝️

Role

role

api_tokens

String

id

🗝️

String

token_hash

String

label

DateTime

created_at

DateTime

revoked_at

products

String

id

🗝️

String

name

String

code

String

description

String

repo_url

String

definition_of_done

Boolean

auto_pr

Boolean

archived

DateTime

created_at

DateTime

updated_at

pbis

String

id

🗝️

String

code

String

title

String

description

Int

priority

Float

sort_order

PbiStatus

status

String

pr_url

DateTime

pr_merged_at

DateTime

created_at

DateTime

updated_at

stories

String

id

🗝️

String

code

String

title

String

description

String

acceptance_criteria

Int

priority

Float

sort_order

StoryStatus

status

DateTime

created_at

DateTime

updated_at

story_logs

String

id

🗝️

LogType

type

String

content

TestStatus

status

String

commit_hash

String

commit_message

Json

metadata

DateTime

created_at

sprints

String

id

🗝️

String

sprint_goal

SprintStatus

status

DateTime

start_date

DateTime

end_date

DateTime

created_at

DateTime

completed_at

tasks

String

id

🗝️

String

code

String

title

String

description

String

implementation_plan

Int

priority

Float

sort_order

TaskStatus

status

Boolean

verify_only

VerifyRequired

verify_required

String

repo_url

DateTime

created_at

DateTime

updated_at

claude_jobs

String

id

🗝️

ClaudeJobStatus

status

DateTime

claimed_at

DateTime

started_at

DateTime

finished_at

DateTime

pushed_at

VerifyResult

verify_result

String

plan_snapshot

String

branch

String

pr_url

String

summary

String

error

Int

retry_count

DateTime

created_at

DateTime

updated_at

claude_workers

String

id

🗝️

String

product_id

DateTime

started_at

DateTime

last_seen_at

product_members

String

id

🗝️

DateTime

created_at

todos

String

id

🗝️

String

title

String

description

Boolean

done

Boolean

archived

DateTime

created_at

DateTime

updated_at

login_pairings

String

id

🗝️

String

secret_hash

String

desktop_token_hash

String

status

String

desktop_ua

String

desktop_ip

DateTime

created_at

DateTime

expires_at

DateTime

approved_at

DateTime

consumed_at

claude_questions

String

id

🗝️

String

question

Json

options

String

status

String

answer

DateTime

answered_at

DateTime

created_at

DateTime

expires_at

\ No newline at end of file diff --git a/prisma/migrations/20260504055000_codes_required_and_task_code/migration.sql b/prisma/migrations/20260504055000_codes_required_and_task_code/migration.sql new file mode 100644 index 0000000..3ed5f99 --- /dev/null +++ b/prisma/migrations/20260504055000_codes_required_and_task_code/migration.sql @@ -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"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index acfcd92..130e322 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -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") }