diff --git a/packages/db/migrations/0194_careless_pete_wisdom.sql b/packages/db/migrations/0194_careless_pete_wisdom.sql index e1ab6769c3..6c57f39f03 100644 --- a/packages/db/migrations/0194_careless_pete_wisdom.sql +++ b/packages/db/migrations/0194_careless_pete_wisdom.sql @@ -5,6 +5,9 @@ -- permission_group_member table also gains a denormalized workspace_id column -- so the database can enforce the "one group per user per workspace" -- invariant with a composite unique index. +-- +-- Every statement below is written to be idempotent so the migration can be +-- safely re-run after a partial failure. -- 0. Backfill workspace -> organization links for grandfathered workspaces whose -- billed account user is the sole owner of exactly one organization. This is a @@ -29,13 +32,42 @@ WHERE w."organization_id" IS NULL AND w."billed_account_user_id" = owner_orgs."user_id";--> statement-breakpoint -- 1. Add workspace_id columns as nullable so existing rows can coexist during the data migration. -ALTER TABLE "permission_group" ADD COLUMN "workspace_id" text;--> statement-breakpoint -ALTER TABLE "permission_group" ADD CONSTRAINT "permission_group_workspace_id_workspace_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspace"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint -ALTER TABLE "permission_group_member" ADD COLUMN "workspace_id" text;--> statement-breakpoint -ALTER TABLE "permission_group_member" ADD CONSTRAINT "permission_group_member_workspace_id_workspace_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspace"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "permission_group" ADD COLUMN IF NOT EXISTS "workspace_id" text;--> statement-breakpoint +DO $$ BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.table_constraints + WHERE constraint_name = 'permission_group_workspace_id_workspace_id_fk' + AND table_name = 'permission_group' + ) THEN + ALTER TABLE "permission_group" + ADD CONSTRAINT "permission_group_workspace_id_workspace_id_fk" + FOREIGN KEY ("workspace_id") REFERENCES "public"."workspace"("id") + ON DELETE cascade ON UPDATE no action; + END IF; +END $$;--> statement-breakpoint + +ALTER TABLE "permission_group_member" ADD COLUMN IF NOT EXISTS "workspace_id" text;--> statement-breakpoint +DO $$ BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.table_constraints + WHERE constraint_name = 'permission_group_member_workspace_id_workspace_id_fk' + AND table_name = 'permission_group_member' + ) THEN + ALTER TABLE "permission_group_member" + ADD CONSTRAINT "permission_group_member_workspace_id_workspace_id_fk" + FOREIGN KEY ("workspace_id") REFERENCES "public"."workspace"("id") + ON DELETE cascade ON UPDATE no action; + END IF; +END $$;--> statement-breakpoint + +-- 1b. Relax NOT NULL on permission_group.organization_id before the data migration. +-- Step 3 inserts clone rows with organization_id = NULL to mark them as the new +-- workspace-scoped shape. This DROP NOT NULL is a no-op if already nullable. +ALTER TABLE "permission_group" ALTER COLUMN "organization_id" DROP NOT NULL;--> statement-breakpoint -- 2. Materialize a plan of (source permission group, target workspace, new clone id) -- so we can insert the clone rows AND the member rows with stable references. +-- Temp tables are always fresh per transaction, so this is naturally idempotent. CREATE TEMP TABLE "__permission_group_clone_plan" ( "source_id" text NOT NULL, "cloned_id" text NOT NULL, @@ -49,6 +81,8 @@ JOIN "workspace" w ON w."organization_id" = pg."organization_id" WHERE pg."organization_id" IS NOT NULL;--> statement-breakpoint -- 3. Create the workspace-scoped clone rows using the planned ids. +-- Naturally idempotent: after a successful prior run there are no org-scoped +-- rows left, so the clone plan is empty and this INSERT is a no-op. INSERT INTO "permission_group" ( "id", "workspace_id", @@ -101,6 +135,7 @@ WHERE EXISTS ( );--> statement-breakpoint -- 5. Delete legacy org-scoped rows now that clones exist. +-- Idempotent: no rows match on a re-run. DELETE FROM "permission_group_member" WHERE "permission_group_id" IN ( SELECT "id" FROM "permission_group" WHERE "organization_id" IS NOT NULL @@ -109,20 +144,20 @@ WHERE "permission_group_id" IN ( DELETE FROM "permission_group" WHERE "organization_id" IS NOT NULL;--> statement-breakpoint -- 6. Enforce NOT NULL on both workspace_id columns now that every surviving row has one. +-- SET NOT NULL is a no-op if already NOT NULL. ALTER TABLE "permission_group" ALTER COLUMN "workspace_id" SET NOT NULL;--> statement-breakpoint ALTER TABLE "permission_group_member" ALTER COLUMN "workspace_id" SET NOT NULL;--> statement-breakpoint -- 7. Drop legacy structures and swap indexes. -ALTER TABLE "permission_group" DROP CONSTRAINT "permission_group_organization_id_organization_id_fk";--> statement-breakpoint -DROP INDEX "permission_group_org_name_unique";--> statement-breakpoint -DROP INDEX "permission_group_org_auto_add_unique";--> statement-breakpoint -DROP INDEX "permission_group_member_user_id_unique";--> statement-breakpoint -ALTER TABLE "permission_group" DROP COLUMN "organization_id";--> statement-breakpoint -CREATE UNIQUE INDEX "permission_group_workspace_name_unique" ON "permission_group" USING btree ("workspace_id","name");--> statement-breakpoint -CREATE UNIQUE INDEX "permission_group_workspace_auto_add_unique" ON "permission_group" USING btree ("workspace_id") WHERE auto_add_new_members = true;--> statement-breakpoint -CREATE UNIQUE INDEX "permission_group_member_group_user_unique" ON "permission_group_member" USING btree ("permission_group_id","user_id");--> statement-breakpoint -CREATE UNIQUE INDEX "permission_group_member_workspace_user_unique" ON "permission_group_member" USING btree ("workspace_id","user_id");--> statement-breakpoint +ALTER TABLE "permission_group" DROP CONSTRAINT IF EXISTS "permission_group_organization_id_organization_id_fk";--> statement-breakpoint +DROP INDEX IF EXISTS "permission_group_org_name_unique";--> statement-breakpoint +DROP INDEX IF EXISTS "permission_group_org_auto_add_unique";--> statement-breakpoint +DROP INDEX IF EXISTS "permission_group_member_user_id_unique";--> statement-breakpoint +ALTER TABLE "permission_group" DROP COLUMN IF EXISTS "organization_id";--> statement-breakpoint +CREATE UNIQUE INDEX IF NOT EXISTS "permission_group_workspace_name_unique" ON "permission_group" USING btree ("workspace_id","name");--> statement-breakpoint +CREATE UNIQUE INDEX IF NOT EXISTS "permission_group_workspace_auto_add_unique" ON "permission_group" USING btree ("workspace_id") WHERE auto_add_new_members = true;--> statement-breakpoint +CREATE UNIQUE INDEX IF NOT EXISTS "permission_group_member_group_user_unique" ON "permission_group_member" USING btree ("permission_group_id","user_id");--> statement-breakpoint +CREATE UNIQUE INDEX IF NOT EXISTS "permission_group_member_workspace_user_unique" ON "permission_group_member" USING btree ("workspace_id","user_id");--> statement-breakpoint -- 8. Sweep any residual dead config keys from pre-existing workspace-scoped rows (if any). UPDATE "permission_group" SET "config" = ("config" - 'hideEnvironmentTab' - 'hideTemplates') WHERE "config" ? 'hideEnvironmentTab' OR "config" ? 'hideTemplates'; -