feat(v2.2): 建立分区关系表和AppData迁移预演

This commit is contained in:
Script Generator
2026-07-03 10:23:18 +08:00
parent 0d9e6fee29
commit 5a90923031
16 changed files with 2440 additions and 8 deletions

View File

@@ -0,0 +1,414 @@
-- V2.2 domain schema foundation.
-- High-growth business tables are partitioned from the start so primary,
-- unique, and foreign key shapes do not need to be redesigned after data grows.
-- Preserve the early prototype requirement table before replacing it with the
-- V2.2 partitioned requirements pool table.
ALTER TABLE "requirements" RENAME TO "requirements_legacy";
ALTER TABLE "requirements_legacy" RENAME CONSTRAINT "requirements_pkey" TO "requirements_legacy_pkey";
ALTER TABLE "requirements_legacy" RENAME CONSTRAINT "requirements_product_id_fkey" TO "requirements_legacy_product_id_fkey";
ALTER TABLE "requirements_legacy" RENAME CONSTRAINT "requirements_creator_id_fkey" TO "requirements_legacy_creator_id_fkey";
ALTER TABLE "versions" ADD COLUMN "project_id" TEXT;
ALTER TABLE "versions" ADD CONSTRAINT "versions_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "projects"("id") ON DELETE SET NULL ON UPDATE CASCADE;
CREATE INDEX "versions_product_id_project_id_idx" ON "versions"("product_id", "project_id");
CREATE TABLE "task_categories" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"code" TEXT,
"group" TEXT NOT NULL,
"is_system" BOOLEAN NOT NULL DEFAULT false,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "task_categories_pkey" PRIMARY KEY ("id"),
CONSTRAINT "task_categories_group_name_key" UNIQUE ("group", "name")
);
CREATE UNIQUE INDEX "task_categories_group_code_key" ON "task_categories"("group", "code") WHERE "code" IS NOT NULL;
CREATE TABLE "requirements" (
"id" TEXT NOT NULL,
"product_id" TEXT NOT NULL,
"project_id" TEXT,
"version_id" TEXT,
"code" TEXT NOT NULL,
"title" TEXT NOT NULL,
"description" TEXT NOT NULL DEFAULT '',
"status" TEXT NOT NULL DEFAULT 'draft',
"priority" INTEGER NOT NULL DEFAULT 0,
"type" TEXT,
"source_type" TEXT,
"source_target" TEXT,
"platform" TEXT,
"creator_id" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "requirements_pkey" PRIMARY KEY ("id", "product_id"),
CONSTRAINT "requirements_product_id_code_key" UNIQUE ("product_id", "code"),
CONSTRAINT "requirements_product_id_fkey" FOREIGN KEY ("product_id") REFERENCES "products"("id") ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT "requirements_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "projects"("id") ON DELETE SET NULL ON UPDATE CASCADE,
CONSTRAINT "requirements_version_id_fkey" FOREIGN KEY ("version_id") REFERENCES "versions"("id") ON DELETE SET NULL ON UPDATE CASCADE,
CONSTRAINT "requirements_creator_id_fkey" FOREIGN KEY ("creator_id") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE
) PARTITION BY HASH ("product_id");
CREATE TABLE "requirements_p00" PARTITION OF "requirements" FOR VALUES WITH (MODULUS 16, REMAINDER 0);
CREATE TABLE "requirements_p01" PARTITION OF "requirements" FOR VALUES WITH (MODULUS 16, REMAINDER 1);
CREATE TABLE "requirements_p02" PARTITION OF "requirements" FOR VALUES WITH (MODULUS 16, REMAINDER 2);
CREATE TABLE "requirements_p03" PARTITION OF "requirements" FOR VALUES WITH (MODULUS 16, REMAINDER 3);
CREATE TABLE "requirements_p04" PARTITION OF "requirements" FOR VALUES WITH (MODULUS 16, REMAINDER 4);
CREATE TABLE "requirements_p05" PARTITION OF "requirements" FOR VALUES WITH (MODULUS 16, REMAINDER 5);
CREATE TABLE "requirements_p06" PARTITION OF "requirements" FOR VALUES WITH (MODULUS 16, REMAINDER 6);
CREATE TABLE "requirements_p07" PARTITION OF "requirements" FOR VALUES WITH (MODULUS 16, REMAINDER 7);
CREATE TABLE "requirements_p08" PARTITION OF "requirements" FOR VALUES WITH (MODULUS 16, REMAINDER 8);
CREATE TABLE "requirements_p09" PARTITION OF "requirements" FOR VALUES WITH (MODULUS 16, REMAINDER 9);
CREATE TABLE "requirements_p10" PARTITION OF "requirements" FOR VALUES WITH (MODULUS 16, REMAINDER 10);
CREATE TABLE "requirements_p11" PARTITION OF "requirements" FOR VALUES WITH (MODULUS 16, REMAINDER 11);
CREATE TABLE "requirements_p12" PARTITION OF "requirements" FOR VALUES WITH (MODULUS 16, REMAINDER 12);
CREATE TABLE "requirements_p13" PARTITION OF "requirements" FOR VALUES WITH (MODULUS 16, REMAINDER 13);
CREATE TABLE "requirements_p14" PARTITION OF "requirements" FOR VALUES WITH (MODULUS 16, REMAINDER 14);
CREATE TABLE "requirements_p15" PARTITION OF "requirements" FOR VALUES WITH (MODULUS 16, REMAINDER 15);
CREATE INDEX "requirements_product_project_status_created_at_idx" ON "requirements"("product_id", "project_id", "status", "created_at" DESC);
CREATE INDEX "requirements_version_status_created_at_idx" ON "requirements"("version_id", "status", "created_at" DESC);
CREATE INDEX "requirements_creator_created_at_idx" ON "requirements"("creator_id", "created_at" DESC);
CREATE TABLE "version_plans" (
"id" TEXT NOT NULL,
"version_id" TEXT NOT NULL,
"product_id" TEXT NOT NULL,
"project_id" TEXT,
"type" TEXT NOT NULL,
"title" TEXT NOT NULL,
"status" TEXT NOT NULL DEFAULT 'pending',
"owner_id" TEXT,
"expected_start_at" TIMESTAMP(3),
"expected_end_at" TIMESTAMP(3),
"actual_start_at" TIMESTAMP(3),
"completed_at" TIMESTAMP(3),
"result_url" TEXT,
"requirement_coverage" JSONB NOT NULL DEFAULT '[]',
"logs" JSONB NOT NULL DEFAULT '[]',
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "version_plans_pkey" PRIMARY KEY ("id"),
CONSTRAINT "version_plans_version_id_fkey" FOREIGN KEY ("version_id") REFERENCES "versions"("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "version_plans_product_id_fkey" FOREIGN KEY ("product_id") REFERENCES "products"("id") ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT "version_plans_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "projects"("id") ON DELETE SET NULL ON UPDATE CASCADE,
CONSTRAINT "version_plans_owner_id_fkey" FOREIGN KEY ("owner_id") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE
);
CREATE INDEX "version_plans_version_type_status_idx" ON "version_plans"("version_id", "type", "status");
CREATE INDEX "version_plans_owner_status_end_idx" ON "version_plans"("owner_id", "status", "expected_end_at") WHERE "status" <> 'completed';
CREATE TABLE "dev_tasks" (
"id" TEXT NOT NULL,
"version_id" TEXT NOT NULL,
"product_id" TEXT NOT NULL,
"project_id" TEXT NOT NULL,
"requirement_id" TEXT,
"requirement_product_id" TEXT,
"category_id" TEXT,
"code" TEXT NOT NULL,
"title" TEXT NOT NULL,
"description" TEXT NOT NULL DEFAULT '',
"status" TEXT NOT NULL DEFAULT 'todo',
"priority" INTEGER NOT NULL DEFAULT 0,
"assignee_id" TEXT,
"creator_id" TEXT,
"is_blocked" BOOLEAN NOT NULL DEFAULT false,
"block_reason" TEXT,
"expected_start_at" TIMESTAMP(3),
"expected_end_at" TIMESTAMP(3),
"start_date" TIMESTAMP(3),
"completed_at" TIMESTAMP(3),
"estimate_hours" DOUBLE PRECISION,
"ai_estimate_hours" DOUBLE PRECISION,
"references" JSONB NOT NULL DEFAULT '[]',
"ai_draft" BOOLEAN NOT NULL DEFAULT false,
"ai_draft_at" TIMESTAMP(3),
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "dev_tasks_pkey" PRIMARY KEY ("id", "version_id"),
CONSTRAINT "dev_tasks_version_id_code_key" UNIQUE ("version_id", "code"),
CONSTRAINT "dev_tasks_version_id_fkey" FOREIGN KEY ("version_id") REFERENCES "versions"("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "dev_tasks_product_id_fkey" FOREIGN KEY ("product_id") REFERENCES "products"("id") ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT "dev_tasks_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "projects"("id") ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT "dev_tasks_requirement_fkey" FOREIGN KEY ("requirement_id", "requirement_product_id") REFERENCES "requirements"("id", "product_id") ON DELETE SET NULL ON UPDATE CASCADE,
CONSTRAINT "dev_tasks_category_id_fkey" FOREIGN KEY ("category_id") REFERENCES "task_categories"("id") ON DELETE SET NULL ON UPDATE CASCADE,
CONSTRAINT "dev_tasks_assignee_id_fkey" FOREIGN KEY ("assignee_id") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE,
CONSTRAINT "dev_tasks_creator_id_fkey" FOREIGN KEY ("creator_id") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE
) PARTITION BY HASH ("version_id");
CREATE TABLE "dev_tasks_p00" PARTITION OF "dev_tasks" FOR VALUES WITH (MODULUS 16, REMAINDER 0);
CREATE TABLE "dev_tasks_p01" PARTITION OF "dev_tasks" FOR VALUES WITH (MODULUS 16, REMAINDER 1);
CREATE TABLE "dev_tasks_p02" PARTITION OF "dev_tasks" FOR VALUES WITH (MODULUS 16, REMAINDER 2);
CREATE TABLE "dev_tasks_p03" PARTITION OF "dev_tasks" FOR VALUES WITH (MODULUS 16, REMAINDER 3);
CREATE TABLE "dev_tasks_p04" PARTITION OF "dev_tasks" FOR VALUES WITH (MODULUS 16, REMAINDER 4);
CREATE TABLE "dev_tasks_p05" PARTITION OF "dev_tasks" FOR VALUES WITH (MODULUS 16, REMAINDER 5);
CREATE TABLE "dev_tasks_p06" PARTITION OF "dev_tasks" FOR VALUES WITH (MODULUS 16, REMAINDER 6);
CREATE TABLE "dev_tasks_p07" PARTITION OF "dev_tasks" FOR VALUES WITH (MODULUS 16, REMAINDER 7);
CREATE TABLE "dev_tasks_p08" PARTITION OF "dev_tasks" FOR VALUES WITH (MODULUS 16, REMAINDER 8);
CREATE TABLE "dev_tasks_p09" PARTITION OF "dev_tasks" FOR VALUES WITH (MODULUS 16, REMAINDER 9);
CREATE TABLE "dev_tasks_p10" PARTITION OF "dev_tasks" FOR VALUES WITH (MODULUS 16, REMAINDER 10);
CREATE TABLE "dev_tasks_p11" PARTITION OF "dev_tasks" FOR VALUES WITH (MODULUS 16, REMAINDER 11);
CREATE TABLE "dev_tasks_p12" PARTITION OF "dev_tasks" FOR VALUES WITH (MODULUS 16, REMAINDER 12);
CREATE TABLE "dev_tasks_p13" PARTITION OF "dev_tasks" FOR VALUES WITH (MODULUS 16, REMAINDER 13);
CREATE TABLE "dev_tasks_p14" PARTITION OF "dev_tasks" FOR VALUES WITH (MODULUS 16, REMAINDER 14);
CREATE TABLE "dev_tasks_p15" PARTITION OF "dev_tasks" FOR VALUES WITH (MODULUS 16, REMAINDER 15);
CREATE INDEX "dev_tasks_version_status_updated_at_idx" ON "dev_tasks"("version_id", "status", "updated_at" DESC);
CREATE INDEX "dev_tasks_requirement_id_idx" ON "dev_tasks"("requirement_id");
CREATE INDEX "dev_tasks_assignee_unfinished_idx" ON "dev_tasks"("assignee_id", "status", "updated_at" DESC) WHERE "status" <> 'submitted';
CREATE TABLE "test_cases" (
"id" TEXT NOT NULL,
"version_id" TEXT NOT NULL,
"product_id" TEXT NOT NULL,
"project_id" TEXT NOT NULL,
"requirement_id" TEXT,
"requirement_product_id" TEXT,
"category_id" TEXT,
"code" TEXT NOT NULL,
"title" TEXT NOT NULL,
"description" TEXT NOT NULL DEFAULT '',
"status" TEXT NOT NULL DEFAULT 'pending',
"round_no" INTEGER NOT NULL DEFAULT 1,
"priority" INTEGER NOT NULL DEFAULT 0,
"assignee_id" TEXT,
"creator_id" TEXT,
"planned_test_at" TIMESTAMP(3),
"planned_end_at" TIMESTAMP(3),
"started_at" TIMESTAMP(3),
"completed_at" TIMESTAMP(3),
"estimate_hours" DOUBLE PRECISION,
"ai_estimate_hours" DOUBLE PRECISION,
"references" JSONB NOT NULL DEFAULT '[]',
"ai_draft" BOOLEAN NOT NULL DEFAULT false,
"ai_draft_at" TIMESTAMP(3),
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "test_cases_pkey" PRIMARY KEY ("id", "version_id"),
CONSTRAINT "test_cases_version_id_code_key" UNIQUE ("version_id", "code"),
CONSTRAINT "test_cases_version_id_fkey" FOREIGN KEY ("version_id") REFERENCES "versions"("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "test_cases_product_id_fkey" FOREIGN KEY ("product_id") REFERENCES "products"("id") ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT "test_cases_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "projects"("id") ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT "test_cases_requirement_fkey" FOREIGN KEY ("requirement_id", "requirement_product_id") REFERENCES "requirements"("id", "product_id") ON DELETE SET NULL ON UPDATE CASCADE,
CONSTRAINT "test_cases_category_id_fkey" FOREIGN KEY ("category_id") REFERENCES "task_categories"("id") ON DELETE SET NULL ON UPDATE CASCADE,
CONSTRAINT "test_cases_assignee_id_fkey" FOREIGN KEY ("assignee_id") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE,
CONSTRAINT "test_cases_creator_id_fkey" FOREIGN KEY ("creator_id") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE
) PARTITION BY HASH ("version_id");
CREATE TABLE "test_cases_p00" PARTITION OF "test_cases" FOR VALUES WITH (MODULUS 16, REMAINDER 0);
CREATE TABLE "test_cases_p01" PARTITION OF "test_cases" FOR VALUES WITH (MODULUS 16, REMAINDER 1);
CREATE TABLE "test_cases_p02" PARTITION OF "test_cases" FOR VALUES WITH (MODULUS 16, REMAINDER 2);
CREATE TABLE "test_cases_p03" PARTITION OF "test_cases" FOR VALUES WITH (MODULUS 16, REMAINDER 3);
CREATE TABLE "test_cases_p04" PARTITION OF "test_cases" FOR VALUES WITH (MODULUS 16, REMAINDER 4);
CREATE TABLE "test_cases_p05" PARTITION OF "test_cases" FOR VALUES WITH (MODULUS 16, REMAINDER 5);
CREATE TABLE "test_cases_p06" PARTITION OF "test_cases" FOR VALUES WITH (MODULUS 16, REMAINDER 6);
CREATE TABLE "test_cases_p07" PARTITION OF "test_cases" FOR VALUES WITH (MODULUS 16, REMAINDER 7);
CREATE TABLE "test_cases_p08" PARTITION OF "test_cases" FOR VALUES WITH (MODULUS 16, REMAINDER 8);
CREATE TABLE "test_cases_p09" PARTITION OF "test_cases" FOR VALUES WITH (MODULUS 16, REMAINDER 9);
CREATE TABLE "test_cases_p10" PARTITION OF "test_cases" FOR VALUES WITH (MODULUS 16, REMAINDER 10);
CREATE TABLE "test_cases_p11" PARTITION OF "test_cases" FOR VALUES WITH (MODULUS 16, REMAINDER 11);
CREATE TABLE "test_cases_p12" PARTITION OF "test_cases" FOR VALUES WITH (MODULUS 16, REMAINDER 12);
CREATE TABLE "test_cases_p13" PARTITION OF "test_cases" FOR VALUES WITH (MODULUS 16, REMAINDER 13);
CREATE TABLE "test_cases_p14" PARTITION OF "test_cases" FOR VALUES WITH (MODULUS 16, REMAINDER 14);
CREATE TABLE "test_cases_p15" PARTITION OF "test_cases" FOR VALUES WITH (MODULUS 16, REMAINDER 15);
CREATE INDEX "test_cases_version_round_status_updated_at_idx" ON "test_cases"("version_id", "round_no", "status", "updated_at" DESC);
CREATE INDEX "test_cases_requirement_id_idx" ON "test_cases"("requirement_id");
CREATE INDEX "test_cases_assignee_unfinished_idx" ON "test_cases"("assignee_id", "status", "updated_at" DESC) WHERE "status" NOT IN ('passed', 'failed', 'blocked');
CREATE TABLE "bugs" (
"id" TEXT NOT NULL,
"version_id" TEXT NOT NULL,
"product_id" TEXT NOT NULL,
"project_id" TEXT NOT NULL,
"test_case_id" TEXT,
"test_case_version_id" TEXT,
"code" TEXT NOT NULL,
"title" TEXT NOT NULL,
"description" TEXT NOT NULL DEFAULT '',
"status" TEXT NOT NULL DEFAULT 'open',
"severity" TEXT NOT NULL DEFAULT 'normal',
"priority" INTEGER NOT NULL DEFAULT 0,
"assignee_id" TEXT,
"reporter_id" TEXT,
"planned_fix_at" TIMESTAMP(3),
"resolved_at" TIMESTAMP(3),
"closed_at" TIMESTAMP(3),
"resolution" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "bugs_pkey" PRIMARY KEY ("id", "version_id"),
CONSTRAINT "bugs_version_id_code_key" UNIQUE ("version_id", "code"),
CONSTRAINT "bugs_version_id_fkey" FOREIGN KEY ("version_id") REFERENCES "versions"("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "bugs_product_id_fkey" FOREIGN KEY ("product_id") REFERENCES "products"("id") ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT "bugs_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "projects"("id") ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT "bugs_test_case_fkey" FOREIGN KEY ("test_case_id", "test_case_version_id") REFERENCES "test_cases"("id", "version_id") ON DELETE SET NULL ON UPDATE CASCADE,
CONSTRAINT "bugs_assignee_id_fkey" FOREIGN KEY ("assignee_id") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE,
CONSTRAINT "bugs_reporter_id_fkey" FOREIGN KEY ("reporter_id") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE
) PARTITION BY HASH ("version_id");
CREATE TABLE "bugs_p00" PARTITION OF "bugs" FOR VALUES WITH (MODULUS 16, REMAINDER 0);
CREATE TABLE "bugs_p01" PARTITION OF "bugs" FOR VALUES WITH (MODULUS 16, REMAINDER 1);
CREATE TABLE "bugs_p02" PARTITION OF "bugs" FOR VALUES WITH (MODULUS 16, REMAINDER 2);
CREATE TABLE "bugs_p03" PARTITION OF "bugs" FOR VALUES WITH (MODULUS 16, REMAINDER 3);
CREATE TABLE "bugs_p04" PARTITION OF "bugs" FOR VALUES WITH (MODULUS 16, REMAINDER 4);
CREATE TABLE "bugs_p05" PARTITION OF "bugs" FOR VALUES WITH (MODULUS 16, REMAINDER 5);
CREATE TABLE "bugs_p06" PARTITION OF "bugs" FOR VALUES WITH (MODULUS 16, REMAINDER 6);
CREATE TABLE "bugs_p07" PARTITION OF "bugs" FOR VALUES WITH (MODULUS 16, REMAINDER 7);
CREATE TABLE "bugs_p08" PARTITION OF "bugs" FOR VALUES WITH (MODULUS 16, REMAINDER 8);
CREATE TABLE "bugs_p09" PARTITION OF "bugs" FOR VALUES WITH (MODULUS 16, REMAINDER 9);
CREATE TABLE "bugs_p10" PARTITION OF "bugs" FOR VALUES WITH (MODULUS 16, REMAINDER 10);
CREATE TABLE "bugs_p11" PARTITION OF "bugs" FOR VALUES WITH (MODULUS 16, REMAINDER 11);
CREATE TABLE "bugs_p12" PARTITION OF "bugs" FOR VALUES WITH (MODULUS 16, REMAINDER 12);
CREATE TABLE "bugs_p13" PARTITION OF "bugs" FOR VALUES WITH (MODULUS 16, REMAINDER 13);
CREATE TABLE "bugs_p14" PARTITION OF "bugs" FOR VALUES WITH (MODULUS 16, REMAINDER 14);
CREATE TABLE "bugs_p15" PARTITION OF "bugs" FOR VALUES WITH (MODULUS 16, REMAINDER 15);
CREATE INDEX "bugs_version_status_severity_updated_at_idx" ON "bugs"("version_id", "status", "severity", "updated_at" DESC);
CREATE INDEX "bugs_test_case_id_idx" ON "bugs"("test_case_id");
CREATE INDEX "bugs_assignee_open_idx" ON "bugs"("assignee_id", "status", "updated_at" DESC) WHERE "status" IN ('open', 'fixing', 'fixed', 'verifying');
CREATE TABLE "work_activities" (
"id" TEXT NOT NULL,
"version_id" TEXT,
"product_id" TEXT,
"project_id" TEXT,
"actor_id" TEXT,
"actor_name" TEXT NOT NULL DEFAULT '',
"source_type" TEXT NOT NULL,
"source_id" TEXT NOT NULL,
"source_version_id" TEXT,
"action" TEXT NOT NULL,
"title" TEXT NOT NULL,
"metadata" JSONB NOT NULL DEFAULT '{}',
"occurred_at" TIMESTAMP(3) NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "work_activities_pkey" PRIMARY KEY ("id", "created_at")
) PARTITION BY RANGE ("created_at");
CREATE TABLE "work_activities_default" PARTITION OF "work_activities" DEFAULT;
CREATE INDEX "work_activities_version_created_at_idx" ON "work_activities"("version_id", "created_at" DESC);
CREATE INDEX "work_activities_actor_created_at_idx" ON "work_activities"("actor_id", "created_at" DESC);
CREATE TABLE "task_worklogs" (
"id" TEXT NOT NULL,
"version_id" TEXT,
"product_id" TEXT,
"project_id" TEXT,
"user_id" TEXT,
"source_type" TEXT NOT NULL,
"source_id" TEXT NOT NULL,
"source_version_id" TEXT,
"work_date" DATE NOT NULL,
"hours" DOUBLE PRECISION NOT NULL DEFAULT 0,
"content" TEXT NOT NULL DEFAULT '',
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "task_worklogs_pkey" PRIMARY KEY ("id", "created_at")
) PARTITION BY RANGE ("created_at");
CREATE TABLE "task_worklogs_default" PARTITION OF "task_worklogs" DEFAULT;
CREATE INDEX "task_worklogs_user_date_idx" ON "task_worklogs"("user_id", "work_date" DESC);
CREATE INDEX "task_worklogs_version_date_idx" ON "task_worklogs"("version_id", "work_date" DESC);
CREATE TABLE "overtime_records" (
"id" TEXT NOT NULL,
"product_id" TEXT,
"project_id" TEXT,
"version_id" TEXT,
"user_id" TEXT,
"reason" TEXT NOT NULL DEFAULT '',
"start_at" TIMESTAMP(3) NOT NULL,
"end_at" TIMESTAMP(3) NOT NULL,
"hours" DOUBLE PRECISION NOT NULL DEFAULT 0,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "overtime_records_pkey" PRIMARY KEY ("id", "created_at")
) PARTITION BY RANGE ("created_at");
CREATE TABLE "overtime_records_default" PARTITION OF "overtime_records" DEFAULT;
CREATE INDEX "overtime_records_user_created_at_idx" ON "overtime_records"("user_id", "created_at" DESC);
CREATE INDEX "overtime_records_project_created_at_idx" ON "overtime_records"("project_id", "created_at" DESC);
CREATE TABLE "xiaobao_risk_snapshots" (
"id" TEXT NOT NULL,
"version_id" TEXT NOT NULL,
"snapshot_date" DATE NOT NULL,
"risk_level" TEXT NOT NULL,
"risk_score" INTEGER NOT NULL DEFAULT 0,
"risk_signature" TEXT NOT NULL,
"snapshot" JSONB NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "xiaobao_risk_snapshots_pkey" PRIMARY KEY ("id", "created_at"),
CONSTRAINT "xiaobao_risk_snapshots_version_id_fkey" FOREIGN KEY ("version_id") REFERENCES "versions"("id") ON DELETE CASCADE ON UPDATE CASCADE
) PARTITION BY RANGE ("created_at");
CREATE TABLE "xiaobao_risk_snapshots_default" PARTITION OF "xiaobao_risk_snapshots" DEFAULT;
CREATE INDEX "xiaobao_risk_snapshots_version_created_at_idx" ON "xiaobao_risk_snapshots"("version_id", "created_at" DESC);
CREATE INDEX "xiaobao_risk_snapshots_date_idx" ON "xiaobao_risk_snapshots"("snapshot_date" DESC);
CREATE TABLE "xiaobao_risk_insights" (
"id" TEXT NOT NULL,
"version_id" TEXT NOT NULL,
"risk_signature" TEXT NOT NULL,
"status" TEXT NOT NULL DEFAULT 'generated',
"insight" JSONB NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "xiaobao_risk_insights_pkey" PRIMARY KEY ("id", "created_at"),
CONSTRAINT "xiaobao_risk_insights_version_id_fkey" FOREIGN KEY ("version_id") REFERENCES "versions"("id") ON DELETE CASCADE ON UPDATE CASCADE
) PARTITION BY RANGE ("created_at");
CREATE TABLE "xiaobao_risk_insights_default" PARTITION OF "xiaobao_risk_insights" DEFAULT;
CREATE INDEX "xiaobao_risk_insights_version_signature_idx" ON "xiaobao_risk_insights"("version_id", "risk_signature", "created_at" DESC);
CREATE TABLE "ai_logs" (
"id" TEXT NOT NULL,
"provider" TEXT NOT NULL,
"model" TEXT NOT NULL,
"operation" TEXT NOT NULL,
"status" TEXT NOT NULL,
"prompt_tokens" INTEGER NOT NULL DEFAULT 0,
"completion_tokens" INTEGER NOT NULL DEFAULT 0,
"duration_ms" INTEGER NOT NULL DEFAULT 0,
"error_message" TEXT,
"metadata" JSONB NOT NULL DEFAULT '{}',
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "ai_logs_pkey" PRIMARY KEY ("id", "created_at")
) PARTITION BY RANGE ("created_at");
CREATE TABLE "ai_logs_default" PARTITION OF "ai_logs" DEFAULT;
CREATE INDEX "ai_logs_operation_created_at_idx" ON "ai_logs"("operation", "created_at" DESC);
CREATE INDEX "ai_logs_status_created_at_idx" ON "ai_logs"("status", "created_at" DESC);
CREATE TABLE "xiaobao_risk_summaries" (
"version_id" TEXT NOT NULL,
"risk_level" TEXT NOT NULL,
"risk_score" INTEGER NOT NULL DEFAULT 0,
"confidence" INTEGER NOT NULL DEFAULT 0,
"forecast_release_date" TIMESTAMP(3),
"risk_signature" TEXT NOT NULL,
"summary" JSONB NOT NULL,
"dirty" BOOLEAN NOT NULL DEFAULT false,
"recomputed_at" TIMESTAMP(3),
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "xiaobao_risk_summaries_pkey" PRIMARY KEY ("version_id"),
CONSTRAINT "xiaobao_risk_summaries_version_id_fkey" FOREIGN KEY ("version_id") REFERENCES "versions"("id") ON DELETE CASCADE ON UPDATE CASCADE
);

View File

@@ -49,6 +49,7 @@ model Project {
product Product @relation(fields: [productId], references: [id])
sprints Sprint[]
tasks Task[]
versions Version[]
members ProjectMember[]
@@map("projects")
@@ -72,6 +73,7 @@ model Sprint {
model Version {
id String @id @default(cuid())
productId String @map("product_id")
projectId String? @map("project_id")
name String
description String @default("")
releaseDate DateTime? @map("release_date")
@@ -79,25 +81,35 @@ model Version {
updatedAt DateTime @updatedAt @map("updated_at")
product Product @relation(fields: [productId], references: [id])
project Project? @relation(fields: [projectId], references: [id])
tasks Task[]
@@map("versions")
}
model Requirement {
id String @id @default(cuid())
id String @default(cuid())
productId String @map("product_id")
projectId String? @map("project_id")
versionId String? @map("version_id")
code String
title String
description String @default("")
status String @default("draft")
priority Int @default(0)
creatorId String @map("creator_id")
type String?
sourceType String? @map("source_type")
sourceTarget String? @map("source_target")
platform String?
creatorId String? @map("creator_id")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
product Product @relation(fields: [productId], references: [id])
creator User @relation(fields: [creatorId], references: [id])
creator User? @relation(fields: [creatorId], references: [id])
@@id([id, productId])
@@unique([productId, code])
@@map("requirements")
}
@@ -168,6 +180,250 @@ model ProjectMember {
@@map("project_members")
}
model TaskCategory {
id String @id @default(cuid())
name String
code String?
group String
isSystem Boolean @default(false) @map("is_system")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@unique([group, name])
@@map("task_categories")
}
model VersionPlan {
id String @id @default(cuid())
versionId String @map("version_id")
productId String @map("product_id")
projectId String? @map("project_id")
type String
title String
status String @default("pending")
ownerId String? @map("owner_id")
expectedStartAt DateTime? @map("expected_start_at")
expectedEndAt DateTime? @map("expected_end_at")
actualStartAt DateTime? @map("actual_start_at")
completedAt DateTime? @map("completed_at")
resultUrl String? @map("result_url")
requirementCoverage Json @default("[]") @map("requirement_coverage")
logs Json @default("[]")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@map("version_plans")
}
model DevTask {
id String @default(cuid())
versionId String @map("version_id")
productId String @map("product_id")
projectId String @map("project_id")
requirementId String? @map("requirement_id")
requirementProductId String? @map("requirement_product_id")
categoryId String? @map("category_id")
code String
title String
description String @default("")
status String @default("todo")
priority Int @default(0)
assigneeId String? @map("assignee_id")
creatorId String? @map("creator_id")
isBlocked Boolean @default(false) @map("is_blocked")
blockReason String? @map("block_reason")
expectedStartAt DateTime? @map("expected_start_at")
expectedEndAt DateTime? @map("expected_end_at")
startDate DateTime? @map("start_date")
completedAt DateTime? @map("completed_at")
estimateHours Float? @map("estimate_hours")
aiEstimateHours Float? @map("ai_estimate_hours")
references Json @default("[]")
aiDraft Boolean @default(false) @map("ai_draft")
aiDraftAt DateTime? @map("ai_draft_at")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@id([id, versionId])
@@unique([versionId, code])
@@map("dev_tasks")
}
model TestCase {
id String @default(cuid())
versionId String @map("version_id")
productId String @map("product_id")
projectId String @map("project_id")
requirementId String? @map("requirement_id")
requirementProductId String? @map("requirement_product_id")
categoryId String? @map("category_id")
code String
title String
description String @default("")
status String @default("pending")
roundNo Int @default(1) @map("round_no")
priority Int @default(0)
assigneeId String? @map("assignee_id")
creatorId String? @map("creator_id")
plannedTestAt DateTime? @map("planned_test_at")
plannedEndAt DateTime? @map("planned_end_at")
startedAt DateTime? @map("started_at")
completedAt DateTime? @map("completed_at")
estimateHours Float? @map("estimate_hours")
aiEstimateHours Float? @map("ai_estimate_hours")
references Json @default("[]")
aiDraft Boolean @default(false) @map("ai_draft")
aiDraftAt DateTime? @map("ai_draft_at")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@id([id, versionId])
@@unique([versionId, code])
@@map("test_cases")
}
model Bug {
id String @default(cuid())
versionId String @map("version_id")
productId String @map("product_id")
projectId String @map("project_id")
testCaseId String? @map("test_case_id")
testCaseVersionId String? @map("test_case_version_id")
code String
title String
description String @default("")
status String @default("open")
severity String @default("normal")
priority Int @default(0)
assigneeId String? @map("assignee_id")
reporterId String? @map("reporter_id")
plannedFixAt DateTime? @map("planned_fix_at")
resolvedAt DateTime? @map("resolved_at")
closedAt DateTime? @map("closed_at")
resolution String?
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@id([id, versionId])
@@unique([versionId, code])
@@map("bugs")
}
model WorkActivity {
id String @default(cuid())
versionId String? @map("version_id")
productId String? @map("product_id")
projectId String? @map("project_id")
actorId String? @map("actor_id")
actorName String @default("") @map("actor_name")
sourceType String @map("source_type")
sourceId String @map("source_id")
sourceVersionId String? @map("source_version_id")
action String
title String
metadata Json @default("{}")
occurredAt DateTime @map("occurred_at")
createdAt DateTime @default(now()) @map("created_at")
@@id([id, createdAt])
@@map("work_activities")
}
model TaskWorklog {
id String @default(cuid())
versionId String? @map("version_id")
productId String? @map("product_id")
projectId String? @map("project_id")
userId String? @map("user_id")
sourceType String @map("source_type")
sourceId String @map("source_id")
sourceVersionId String? @map("source_version_id")
workDate DateTime @db.Date @map("work_date")
hours Float @default(0)
content String @default("")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@id([id, createdAt])
@@map("task_worklogs")
}
model OvertimeRecord {
id String @default(cuid())
productId String? @map("product_id")
projectId String? @map("project_id")
versionId String? @map("version_id")
userId String? @map("user_id")
reason String @default("")
startAt DateTime @map("start_at")
endAt DateTime @map("end_at")
hours Float @default(0)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@id([id, createdAt])
@@map("overtime_records")
}
model XiaobaoRiskSnapshot {
id String @default(cuid())
versionId String @map("version_id")
snapshotDate DateTime @db.Date @map("snapshot_date")
riskLevel String @map("risk_level")
riskScore Int @default(0) @map("risk_score")
riskSignature String @map("risk_signature")
snapshot Json
createdAt DateTime @default(now()) @map("created_at")
@@id([id, createdAt])
@@map("xiaobao_risk_snapshots")
}
model XiaobaoRiskInsight {
id String @default(cuid())
versionId String @map("version_id")
riskSignature String @map("risk_signature")
status String @default("generated")
insight Json
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@id([id, createdAt])
@@map("xiaobao_risk_insights")
}
model AiLog {
id String @default(cuid())
provider String
model String
operation String
status String
promptTokens Int @default(0) @map("prompt_tokens")
completionTokens Int @default(0) @map("completion_tokens")
durationMs Int @default(0) @map("duration_ms")
errorMessage String? @map("error_message")
metadata Json @default("{}")
createdAt DateTime @default(now()) @map("created_at")
@@id([id, createdAt])
@@map("ai_logs")
}
model XiaobaoRiskSummary {
versionId String @id @map("version_id")
riskLevel String @map("risk_level")
riskScore Int @default(0) @map("risk_score")
confidence Int @default(0)
forecastReleaseDate DateTime? @map("forecast_release_date")
riskSignature String @map("risk_signature")
summary Json
dirty Boolean @default(false)
recomputedAt DateTime? @map("recomputed_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@map("xiaobao_risk_summaries")
}
model AppData {
key String @id
value Json

View File

@@ -0,0 +1,104 @@
import { existsSync, readFileSync } from 'node:fs';
import { join } from 'node:path';
const migrationPath = join(
process.cwd(),
'prisma',
'migrations',
'20260703000000_v22_partitioned_domain_schema',
'migration.sql',
);
function readMigrationSql() {
if (!existsSync(migrationPath)) {
throw new Error(`Missing V2.2 migration: ${migrationPath}`);
}
return readFileSync(migrationPath, 'utf8');
}
function expectPartitionedTable(
sql: string,
table: string,
method: 'HASH' | 'RANGE',
key: string,
primaryKey: string[],
) {
const quotedPrimaryKey = primaryKey.map((column) => `"${column}"`).join(', ');
expect(sql).toMatch(
new RegExp(
`CREATE TABLE "${table}"[\\s\\S]*CONSTRAINT "${table}_pkey" PRIMARY KEY \\(${quotedPrimaryKey}\\)[\\s\\S]*PARTITION BY ${method} \\("${key}"\\);`,
),
);
}
function expectHashPartitions(sql: string, table: string, count: number) {
const matches = sql.match(
new RegExp(`PARTITION OF "${table}" FOR VALUES WITH \\(MODULUS ${count}, REMAINDER`, 'g'),
);
expect(matches).toHaveLength(count);
}
describe('V2.2 partitioned domain schema migration', () => {
it('keeps every hash partitioned business table primary key anchored by the partition key', () => {
const sql = readMigrationSql();
expectPartitionedTable(sql, 'requirements', 'HASH', 'product_id', ['id', 'product_id']);
expectPartitionedTable(sql, 'dev_tasks', 'HASH', 'version_id', ['id', 'version_id']);
expectPartitionedTable(sql, 'test_cases', 'HASH', 'version_id', ['id', 'version_id']);
expectPartitionedTable(sql, 'bugs', 'HASH', 'version_id', ['id', 'version_id']);
expectHashPartitions(sql, 'requirements', 16);
expectHashPartitions(sql, 'dev_tasks', 16);
expectHashPartitions(sql, 'test_cases', 16);
expectHashPartitions(sql, 'bugs', 16);
});
it('includes partition keys in business unique constraints', () => {
const sql = readMigrationSql();
expect(sql).toMatch(/CONSTRAINT "requirements_product_id_code_key" UNIQUE \("product_id", "code"\)/);
expect(sql).toMatch(/CONSTRAINT "dev_tasks_version_id_code_key" UNIQUE \("version_id", "code"\)/);
expect(sql).toMatch(/CONSTRAINT "test_cases_version_id_code_key" UNIQUE \("version_id", "code"\)/);
expect(sql).toMatch(/CONSTRAINT "bugs_version_id_code_key" UNIQUE \("version_id", "code"\)/);
});
it('uses composite foreign keys when a referenced table is partitioned', () => {
const sql = readMigrationSql();
expect(sql).toMatch(
/FOREIGN KEY \("requirement_id", "requirement_product_id"\) REFERENCES "requirements"\("id", "product_id"\)/,
);
expect(sql).toMatch(
/FOREIGN KEY \("test_case_id", "test_case_version_id"\) REFERENCES "test_cases"\("id", "version_id"\)/,
);
});
it('range partitions append-only evidence and snapshot tables by created_at', () => {
const sql = readMigrationSql();
expectPartitionedTable(sql, 'work_activities', 'RANGE', 'created_at', ['id', 'created_at']);
expectPartitionedTable(sql, 'task_worklogs', 'RANGE', 'created_at', ['id', 'created_at']);
expectPartitionedTable(sql, 'overtime_records', 'RANGE', 'created_at', ['id', 'created_at']);
expectPartitionedTable(sql, 'xiaobao_risk_snapshots', 'RANGE', 'created_at', ['id', 'created_at']);
expectPartitionedTable(sql, 'ai_logs', 'RANGE', 'created_at', ['id', 'created_at']);
for (const table of [
'work_activities',
'task_worklogs',
'overtime_records',
'xiaobao_risk_snapshots',
'ai_logs',
]) {
expect(sql).toMatch(new RegExp(`CREATE TABLE "${table}_default" PARTITION OF "${table}" DEFAULT;`));
}
});
it('keeps Xiaobao current summaries as one row per version instead of partitioned history', () => {
const sql = readMigrationSql();
expect(sql).toMatch(
/CREATE TABLE "xiaobao_risk_summaries"[\s\S]*CONSTRAINT "xiaobao_risk_summaries_pkey" PRIMARY KEY \("version_id"\)/,
);
expect(sql).not.toMatch(/CREATE TABLE "xiaobao_risk_summaries"[\s\S]*PARTITION BY/);
});
});

View File

@@ -0,0 +1,50 @@
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
const schemaPath = join(process.cwd(), 'prisma', 'schema.prisma');
function readSchema() {
return readFileSync(schemaPath, 'utf8');
}
function modelBlock(schema: string, modelName: string) {
const match = schema.match(new RegExp(`model ${modelName} \\{[\\s\\S]*?\\n\\}`));
if (!match) throw new Error(`Missing Prisma model: ${modelName}`);
return match[0];
}
describe('V2.2 Prisma partitioned model shape', () => {
it('models partitioned business tables with composite ids that include the partition key', () => {
const schema = readSchema();
expect(modelBlock(schema, 'Requirement')).toMatch(/@@id\(\[id, productId\]\)/);
expect(modelBlock(schema, 'DevTask')).toMatch(/@@id\(\[id, versionId\]\)/);
expect(modelBlock(schema, 'TestCase')).toMatch(/@@id\(\[id, versionId\]\)/);
expect(modelBlock(schema, 'Bug')).toMatch(/@@id\(\[id, versionId\]\)/);
});
it('keeps partition-key-scoped unique business codes in Prisma models', () => {
const schema = readSchema();
expect(modelBlock(schema, 'Requirement')).toMatch(/@@unique\(\[productId, code\]\)/);
expect(modelBlock(schema, 'DevTask')).toMatch(/@@unique\(\[versionId, code\]\)/);
expect(modelBlock(schema, 'TestCase')).toMatch(/@@unique\(\[versionId, code\]\)/);
expect(modelBlock(schema, 'Bug')).toMatch(/@@unique\(\[versionId, code\]\)/);
});
it('models append-only partitioned tables with createdAt in the composite id', () => {
const schema = readSchema();
expect(modelBlock(schema, 'WorkActivity')).toMatch(/@@id\(\[id, createdAt\]\)/);
expect(modelBlock(schema, 'TaskWorklog')).toMatch(/@@id\(\[id, createdAt\]\)/);
expect(modelBlock(schema, 'OvertimeRecord')).toMatch(/@@id\(\[id, createdAt\]\)/);
expect(modelBlock(schema, 'XiaobaoRiskSnapshot')).toMatch(/@@id\(\[id, createdAt\]\)/);
expect(modelBlock(schema, 'AiLog')).toMatch(/@@id\(\[id, createdAt\]\)/);
});
it('keeps Xiaobao current summaries keyed by version id only', () => {
const schema = readSchema();
expect(modelBlock(schema, 'XiaobaoRiskSummary')).toMatch(/versionId\s+String\s+@id/);
});
});