Portal: move Schedule to per-License-Key with daily/weekly and Block.day

Adds Schedule.licenseKeyId (unique, nullable) and Schedule.type, makes the
legacy userId optional/non-unique, adds Block.day and flips Block
dynamicAudio default to true. The migration backfills each existing
per-user schedule onto that account's oldest License Key as a daily
schedule, preserves unmappable schedules (licenseKeyId NULL) with a
NOTICE, and is reversible. Tested up/down against a scratch schema.
This commit is contained in:
NeilClack
2026-09-29 10:11:21 -05:00
parent 25b5eeb144
commit 138bf03ea9
9 changed files with 630 additions and 130 deletions
@@ -0,0 +1,46 @@
-- Phase 3: move Schedule from per-user to per-License-Key, add weekly support.
-- AlterTable: Schedule gains licenseKeyId + type, userId becomes optional.
ALTER TABLE "Schedule" ADD COLUMN "licenseKeyId" TEXT,
ADD COLUMN "type" TEXT;
-- AlterTable: Block gains day; new Blocks default dynamicAudio true.
ALTER TABLE "Block" ADD COLUMN "day" TEXT;
ALTER TABLE "Block" ALTER COLUMN "dynamicAudio" SET DEFAULT true;
-- DropIndex: userId is no longer the schedule identity.
DROP INDEX "Schedule_userId_key";
-- AlterTable: legacy owner becomes optional and non-unique.
ALTER TABLE "Schedule" ALTER COLUMN "userId" DROP NOT NULL;
-- CreateIndex
CREATE UNIQUE INDEX "Schedule_licenseKeyId_key" ON "Schedule"("licenseKeyId");
CREATE INDEX "Schedule_userId_idx" ON "Schedule"("userId");
-- AddForeignKey
ALTER TABLE "Schedule" ADD CONSTRAINT "Schedule_licenseKeyId_fkey" FOREIGN KEY ("licenseKeyId") REFERENCES "LicenseKey"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- Backfill: map each existing per-user Schedule to the account's oldest
-- License Key. Existing content was daily-only.
UPDATE "Schedule" s
SET "licenseKeyId" = lk."id",
"type" = COALESCE(s."type", 'daily')
FROM (
SELECT DISTINCT ON ("userId") "id", "userId"
FROM "LicenseKey"
WHERE "userId" IS NOT NULL
ORDER BY "userId", "createdAt" ASC, "id" ASC
) lk
WHERE s."userId" = lk."userId"
AND s."licenseKeyId" IS NULL;
-- Report (do not silently drop) schedules that cannot be mapped to a key.
DO $$
DECLARE unmapped integer;
BEGIN
SELECT count(*) INTO unmapped FROM "Schedule" WHERE "licenseKeyId" IS NULL;
IF unmapped > 0 THEN
RAISE NOTICE 'Phase 3 migration: % Schedule row(s) could not be mapped to a License Key (no owning user/license); retained with licenseKeyId NULL.', unmapped;
END IF;
END $$;