feat(v2.2): 建立分区关系表和AppData迁移预演
This commit is contained in:
@@ -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
|
||||
);
|
||||
@@ -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
|
||||
|
||||
104
apps/server/prisma/v22-partitioned-schema.spec.ts
Normal file
104
apps/server/prisma/v22-partitioned-schema.spec.ts
Normal 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/);
|
||||
});
|
||||
});
|
||||
50
apps/server/prisma/v22-prisma-models.spec.ts
Normal file
50
apps/server/prisma/v22-prisma-models.spec.ts
Normal 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/);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user