-- uuid 外键镜像 (PowerSync Sequential ID Mapping, 见 docs/V0.2/离线编辑模块/0.2.1实施方案.md §3) -- 目的: 同步流以 uuid 作为客户端主键 (离线新建行同步回填后本地 id 稳定不变), -- 外键列同步输出 uuid 镜像 (别名回 projectId/columnId/parentId/taskId), 客户端零改动。 -- 镜像列由触发器维护: REST/Prisma 写 int 外键 -> 触发器回填 uuid; upload 端在应用层解析 uuid -> int。 -- 1) 加列 (先可空) ALTER TABLE "BoardColumn" ADD COLUMN "projectUuid" VARCHAR(36); ALTER TABLE "Task" ADD COLUMN "projectUuid" VARCHAR(36), ADD COLUMN "columnUuid" VARCHAR(36), ADD COLUMN "parentUuid" VARCHAR(36); ALTER TABLE "ProjectMember" ADD COLUMN "projectUuid" VARCHAR(36); ALTER TABLE "TaskMember" ADD COLUMN "taskUuid" VARCHAR(36); ALTER TABLE "TaskTag" ADD COLUMN "taskUuid" VARCHAR(36); -- 2) 存量回填 UPDATE "BoardColumn" c SET "projectUuid" = p."uuid" FROM "Project" p WHERE c."projectId" = p."id"; UPDATE "Task" t SET "projectUuid" = p."uuid" FROM "Project" p WHERE t."projectId" = p."id"; UPDATE "Task" t SET "columnUuid" = c."uuid" FROM "BoardColumn" c WHERE t."columnId" = c."id"; UPDATE "Task" t SET "parentUuid" = pt."uuid" FROM "Task" pt WHERE t."parentId" = pt."id"; UPDATE "ProjectMember" pm SET "projectUuid" = p."uuid" FROM "Project" p WHERE pm."projectId" = p."id"; UPDATE "TaskMember" tm SET "taskUuid" = t."uuid" FROM "Task" t WHERE tm."taskId" = t."id"; UPDATE "TaskTag" tt SET "taskUuid" = t."uuid" FROM "Task" t WHERE tt."taskId" = t."id"; -- 3) 非空约束 ALTER TABLE "BoardColumn" ALTER COLUMN "projectUuid" SET NOT NULL; ALTER TABLE "Task" ALTER COLUMN "projectUuid" SET NOT NULL; ALTER TABLE "Task" ALTER COLUMN "columnUuid" SET NOT NULL; ALTER TABLE "ProjectMember" ALTER COLUMN "projectUuid" SET NOT NULL; ALTER TABLE "TaskMember" ALTER COLUMN "taskUuid" SET NOT NULL; ALTER TABLE "TaskTag" ALTER COLUMN "taskUuid" SET NOT NULL; -- 4) 外键 (级联与 int 外键一致) ALTER TABLE "BoardColumn" ADD CONSTRAINT "BoardColumn_projectUuid_fkey" FOREIGN KEY ("projectUuid") REFERENCES "Project"("uuid") ON DELETE CASCADE ON UPDATE CASCADE; ALTER TABLE "Task" ADD CONSTRAINT "Task_projectUuid_fkey" FOREIGN KEY ("projectUuid") REFERENCES "Project"("uuid") ON DELETE CASCADE ON UPDATE CASCADE; ALTER TABLE "Task" ADD CONSTRAINT "Task_columnUuid_fkey" FOREIGN KEY ("columnUuid") REFERENCES "BoardColumn"("uuid") ON DELETE CASCADE ON UPDATE CASCADE; ALTER TABLE "Task" ADD CONSTRAINT "Task_parentUuid_fkey" FOREIGN KEY ("parentUuid") REFERENCES "Task"("uuid") ON DELETE CASCADE ON UPDATE CASCADE; ALTER TABLE "ProjectMember" ADD CONSTRAINT "ProjectMember_projectUuid_fkey" FOREIGN KEY ("projectUuid") REFERENCES "Project"("uuid") ON DELETE CASCADE ON UPDATE CASCADE; ALTER TABLE "TaskMember" ADD CONSTRAINT "TaskMember_taskUuid_fkey" FOREIGN KEY ("taskUuid") REFERENCES "Task"("uuid") ON DELETE CASCADE ON UPDATE CASCADE; ALTER TABLE "TaskTag" ADD CONSTRAINT "TaskTag_taskUuid_fkey" FOREIGN KEY ("taskUuid") REFERENCES "Task"("uuid") ON DELETE CASCADE ON UPDATE CASCADE; -- 5) 触发器: 写入时按 int 外键回填 uuid 镜像 CREATE OR REPLACE FUNCTION "func_projectmember_uuid_map"() RETURNS TRIGGER AS $$ BEGIN SELECT "uuid" INTO NEW."projectUuid" FROM "Project" WHERE "id" = NEW."projectId"; IF NOT FOUND THEN RAISE EXCEPTION 'Project % 不存在', NEW."projectId"; END IF; RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER "trg_projectmember_uuid_map" BEFORE INSERT OR UPDATE ON "ProjectMember" FOR EACH ROW EXECUTE FUNCTION "func_projectmember_uuid_map"(); CREATE OR REPLACE FUNCTION "func_boardcolumn_uuid_map"() RETURNS TRIGGER AS $$ BEGIN SELECT "uuid" INTO NEW."projectUuid" FROM "Project" WHERE "id" = NEW."projectId"; IF NOT FOUND THEN RAISE EXCEPTION 'Project % 不存在', NEW."projectId"; END IF; RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER "trg_boardcolumn_uuid_map" BEFORE INSERT OR UPDATE ON "BoardColumn" FOR EACH ROW EXECUTE FUNCTION "func_boardcolumn_uuid_map"(); CREATE OR REPLACE FUNCTION "func_task_uuid_map"() RETURNS TRIGGER AS $$ BEGIN SELECT "uuid" INTO NEW."projectUuid" FROM "Project" WHERE "id" = NEW."projectId"; IF NOT FOUND THEN RAISE EXCEPTION 'Project % 不存在', NEW."projectId"; END IF; SELECT "uuid" INTO NEW."columnUuid" FROM "BoardColumn" WHERE "id" = NEW."columnId"; IF NOT FOUND THEN RAISE EXCEPTION 'BoardColumn % 不存在', NEW."columnId"; END IF; IF NEW."parentId" IS NULL THEN NEW."parentUuid" := NULL; ELSE SELECT "uuid" INTO NEW."parentUuid" FROM "Task" WHERE "id" = NEW."parentId"; IF NOT FOUND THEN RAISE EXCEPTION 'Task % 不存在', NEW."parentId"; END IF; END IF; RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER "trg_task_uuid_map" BEFORE INSERT OR UPDATE ON "Task" FOR EACH ROW EXECUTE FUNCTION "func_task_uuid_map"(); CREATE OR REPLACE FUNCTION "func_taskmember_uuid_map"() RETURNS TRIGGER AS $$ BEGIN SELECT "uuid" INTO NEW."taskUuid" FROM "Task" WHERE "id" = NEW."taskId"; IF NOT FOUND THEN RAISE EXCEPTION 'Task % 不存在', NEW."taskId"; END IF; RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER "trg_taskmember_uuid_map" BEFORE INSERT OR UPDATE ON "TaskMember" FOR EACH ROW EXECUTE FUNCTION "func_taskmember_uuid_map"(); CREATE OR REPLACE FUNCTION "func_tasktag_uuid_map"() RETURNS TRIGGER AS $$ BEGIN SELECT "uuid" INTO NEW."taskUuid" FROM "Task" WHERE "id" = NEW."taskId"; IF NOT FOUND THEN RAISE EXCEPTION 'Task % 不存在', NEW."taskId"; END IF; RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER "trg_tasktag_uuid_map" BEFORE INSERT OR UPDATE ON "TaskTag" FOR EACH ROW EXECUTE FUNCTION "func_tasktag_uuid_map"();