-- DropForeignKey -- Category.userId is becoming optional -- a category now belongs to -- exactly one of a User (Home) or a Project, never both. ALTER TABLE "Category" DROP CONSTRAINT "Category_userId_fkey"; -- AlterTable ALTER TABLE "Category" ALTER COLUMN "userId" DROP NOT NULL; ALTER TABLE "Category" ADD COLUMN "projectId" TEXT; -- CreateTable CREATE TABLE "Project" ( "id" TEXT NOT NULL, "title" TEXT NOT NULL, "ownerId" TEXT NOT NULL, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "updatedAt" TIMESTAMP(3) NOT NULL, CONSTRAINT "Project_pkey" PRIMARY KEY ("id") ); -- CreateIndex CREATE INDEX "Project_ownerId_createdAt_idx" ON "Project"("ownerId", "createdAt"); -- CreateIndex CREATE INDEX "Category_projectId_order_idx" ON "Category"("projectId", "order"); -- AddForeignKey ALTER TABLE "Category" ADD CONSTRAINT "Category_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "Category" ADD CONSTRAINT "Category_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "Project" ADD CONSTRAINT "Project_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; -- Every existing Category row already has userId set and projectId null, -- so this holds immediately with no backfill -- enforce the "exactly one -- of userId/projectId" invariant at the database level too, not just in -- application code (lib/access.ts). ALTER TABLE "Category" ADD CONSTRAINT "Category_owner_xor_project" CHECK ( ("userId" IS NOT NULL AND "projectId" IS NULL) OR ("userId" IS NULL AND "projectId" IS NOT NULL) );