Skip to content

Commit 9434e52

Browse files
committed
fix(db): make the folder migration replay-safe below its embedded COMMIT
The file ends in CONCURRENTLY index builds under a COMMIT, so a failure there replays the whole file from the top — but the statements above were not idempotent, which would have wedged the deploy on retry. Every one is now idempotent, matching 0250_workspace_forking.sql: CREATE TYPE and every ADD CONSTRAINT in a duplicate_object handler, IF NOT EXISTS on tables, columns and indexes, IF EXISTS on the constraint drops, OR REPLACE on the function, and a drop-then-create for the trigger. Both backfills already carried ON CONFLICT (id) DO NOTHING.
1 parent 0e059d9 commit 9434e52

1 file changed

Lines changed: 52 additions & 24 deletions

File tree

packages/db/migrations/0272_generic_folders_and_pinning.sql

Lines changed: 52 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
1-
CREATE TYPE "public"."folder_resource_type" AS ENUM('workflow', 'file', 'knowledge_base', 'table');--> statement-breakpoint
2-
CREATE TABLE "folder" (
1+
-- Replay-safety: this file ends in CONCURRENTLY index ops below an embedded COMMIT,
2+
-- so a failure there replays the whole file from the top — every statement here is
3+
-- idempotent (matches the pattern in 0250_workspace_forking.sql).
4+
DO $$ BEGIN
5+
CREATE TYPE "public"."folder_resource_type" AS ENUM('workflow', 'file', 'knowledge_base', 'table');
6+
EXCEPTION WHEN duplicate_object THEN null;
7+
END $$;--> statement-breakpoint
8+
CREATE TABLE IF NOT EXISTS "folder" (
39
"id" text PRIMARY KEY NOT NULL,
410
"resource_type" "folder_resource_type" NOT NULL,
511
"name" text NOT NULL,
@@ -13,7 +19,7 @@ CREATE TABLE "folder" (
1319
"deleted_at" timestamp
1420
);
1521
--> statement-breakpoint
16-
CREATE TABLE "pinned_item" (
22+
CREATE TABLE IF NOT EXISTS "pinned_item" (
1723
"id" text PRIMARY KEY NOT NULL,
1824
"user_id" text NOT NULL,
1925
"workspace_id" text NOT NULL,
@@ -30,27 +36,39 @@ CREATE TABLE "pinned_item" (
3036
-- workflow.folderId in schema.ts. The invariant is enforced in the application layer
3137
-- on both the old and new code paths meanwhile.
3238
-- migration-safe: strictly loosens the column, no cross-deploy write can be rejected by removing this constraint; see contract-pending marker on workflow.folderId in schema.ts
33-
ALTER TABLE "workflow" DROP CONSTRAINT "workflow_folder_id_workflow_folder_id_fk";
39+
ALTER TABLE "workflow" DROP CONSTRAINT IF EXISTS "workflow_folder_id_workflow_folder_id_fk";
3440
--> statement-breakpoint
3541
-- Same reasoning as the workflow.folder_id drop above.
3642
-- migration-safe: strictly loosens the column, no cross-deploy write can be rejected by removing this constraint; see contract-pending marker on workspaceFiles.folderId in schema.ts
37-
ALTER TABLE "workspace_files" DROP CONSTRAINT "workspace_files_folder_id_workspace_file_folders_id_fk";
43+
ALTER TABLE "workspace_files" DROP CONSTRAINT IF EXISTS "workspace_files_folder_id_workspace_file_folders_id_fk";
3844
--> statement-breakpoint
39-
ALTER TABLE "knowledge_base" ADD COLUMN "folder_id" text;--> statement-breakpoint
40-
ALTER TABLE "user_table_definitions" ADD COLUMN "folder_id" text;--> statement-breakpoint
41-
ALTER TABLE "folder" ADD CONSTRAINT "folder_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
42-
ALTER TABLE "folder" ADD CONSTRAINT "folder_workspace_id_workspace_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspace"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
43-
ALTER TABLE "pinned_item" ADD CONSTRAINT "pinned_item_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
44-
ALTER TABLE "pinned_item" ADD CONSTRAINT "pinned_item_workspace_id_workspace_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspace"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
45-
CREATE INDEX "folder_user_idx" ON "folder" USING btree ("user_id");--> statement-breakpoint
46-
CREATE INDEX "folder_workspace_resource_parent_idx" ON "folder" USING btree ("workspace_id","resource_type","parent_id");--> statement-breakpoint
47-
CREATE INDEX "folder_parent_sort_idx" ON "folder" USING btree ("parent_id","sort_order");--> statement-breakpoint
48-
CREATE INDEX "folder_deleted_at_idx" ON "folder" USING btree ("deleted_at");--> statement-breakpoint
49-
CREATE INDEX "folder_workspace_deleted_partial_idx" ON "folder" USING btree ("workspace_id","deleted_at") WHERE "folder"."deleted_at" IS NOT NULL;--> statement-breakpoint
50-
CREATE UNIQUE INDEX "folder_workspace_resource_parent_name_active_unique" ON "folder" USING btree ("workspace_id","resource_type",coalesce("parent_id", ''),"name") WHERE "folder"."deleted_at" IS NULL;--> statement-breakpoint
51-
CREATE INDEX "pinned_item_user_workspace_idx" ON "pinned_item" USING btree ("user_id","workspace_id");--> statement-breakpoint
52-
CREATE INDEX "pinned_item_resource_idx" ON "pinned_item" USING btree ("resource_type","resource_id");--> statement-breakpoint
53-
CREATE UNIQUE INDEX "pinned_item_user_resource_unique" ON "pinned_item" USING btree ("user_id","resource_type","resource_id");--> statement-breakpoint
45+
ALTER TABLE "knowledge_base" ADD COLUMN IF NOT EXISTS "folder_id" text;--> statement-breakpoint
46+
ALTER TABLE "user_table_definitions" ADD COLUMN IF NOT EXISTS "folder_id" text;--> statement-breakpoint
47+
DO $$ BEGIN
48+
ALTER TABLE "folder" ADD CONSTRAINT "folder_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;
49+
EXCEPTION WHEN duplicate_object THEN null;
50+
END $$;--> statement-breakpoint
51+
DO $$ BEGIN
52+
ALTER TABLE "folder" ADD CONSTRAINT "folder_workspace_id_workspace_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspace"("id") ON DELETE cascade ON UPDATE no action;
53+
EXCEPTION WHEN duplicate_object THEN null;
54+
END $$;--> statement-breakpoint
55+
DO $$ BEGIN
56+
ALTER TABLE "pinned_item" ADD CONSTRAINT "pinned_item_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;
57+
EXCEPTION WHEN duplicate_object THEN null;
58+
END $$;--> statement-breakpoint
59+
DO $$ BEGIN
60+
ALTER TABLE "pinned_item" ADD CONSTRAINT "pinned_item_workspace_id_workspace_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspace"("id") ON DELETE cascade ON UPDATE no action;
61+
EXCEPTION WHEN duplicate_object THEN null;
62+
END $$;--> statement-breakpoint
63+
CREATE INDEX IF NOT EXISTS "folder_user_idx" ON "folder" USING btree ("user_id");--> statement-breakpoint
64+
CREATE INDEX IF NOT EXISTS "folder_workspace_resource_parent_idx" ON "folder" USING btree ("workspace_id","resource_type","parent_id");--> statement-breakpoint
65+
CREATE INDEX IF NOT EXISTS "folder_parent_sort_idx" ON "folder" USING btree ("parent_id","sort_order");--> statement-breakpoint
66+
CREATE INDEX IF NOT EXISTS "folder_deleted_at_idx" ON "folder" USING btree ("deleted_at");--> statement-breakpoint
67+
CREATE INDEX IF NOT EXISTS "folder_workspace_deleted_partial_idx" ON "folder" USING btree ("workspace_id","deleted_at") WHERE "folder"."deleted_at" IS NOT NULL;--> statement-breakpoint
68+
CREATE UNIQUE INDEX IF NOT EXISTS "folder_workspace_resource_parent_name_active_unique" ON "folder" USING btree ("workspace_id","resource_type",coalesce("parent_id", ''),"name") WHERE "folder"."deleted_at" IS NULL;--> statement-breakpoint
69+
CREATE INDEX IF NOT EXISTS "pinned_item_user_workspace_idx" ON "pinned_item" USING btree ("user_id","workspace_id");--> statement-breakpoint
70+
CREATE INDEX IF NOT EXISTS "pinned_item_resource_idx" ON "pinned_item" USING btree ("resource_type","resource_id");--> statement-breakpoint
71+
CREATE UNIQUE INDEX IF NOT EXISTS "pinned_item_user_resource_unique" ON "pinned_item" USING btree ("user_id","resource_type","resource_id");--> statement-breakpoint
5472
-- A folder may only be parented by a folder of the same resourceType in the same
5573
-- workspace. Neither invariant is expressible as a plain FK, so it is enforced here as
5674
-- defense-in-depth behind the application-layer check. BEFORE INSERT OR UPDATE covers the
@@ -61,7 +79,7 @@ CREATE UNIQUE INDEX "pinned_item_user_resource_unique" ON "pinned_item" USING bt
6179
-- whose parent has not been inserted yet finds no parent row, and passes rather than
6280
-- raising. The real referential check is the FK, which Postgres evaluates as an
6381
-- AFTER-ROW trigger at end of statement, once every row is present.
64-
CREATE FUNCTION "folder_parent_resource_type_match"() RETURNS trigger AS $$
82+
CREATE OR REPLACE FUNCTION "folder_parent_resource_type_match"() RETURNS trigger AS $$
6583
DECLARE
6684
parent_resource_type "folder_resource_type";
6785
parent_workspace_id text;
@@ -82,6 +100,7 @@ BEGIN
82100
END;
83101
$$ LANGUAGE plpgsql;
84102
--> statement-breakpoint
103+
DROP TRIGGER IF EXISTS "folder_parent_resource_type_match" ON "folder";--> statement-breakpoint
85104
CREATE TRIGGER "folder_parent_resource_type_match"
86105
BEFORE INSERT OR UPDATE ON "folder"
87106
FOR EACH ROW EXECUTE FUNCTION "folder_parent_resource_type_match"();
@@ -187,16 +206,25 @@ ON CONFLICT (id) DO NOTHING;
187206
-- single INSERT...SELECT carrying children and parents in arbitrary order would in fact
188207
-- satisfy it — but creating the constraint here removes any dependence on that subtlety,
189208
-- costs nothing on a table this size, and makes the backfill obviously correct on review.
190-
ALTER TABLE "folder" ADD CONSTRAINT "folder_parent_id_folder_id_fk" FOREIGN KEY ("parent_id") REFERENCES "public"."folder"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
209+
DO $$ BEGIN
210+
ALTER TABLE "folder" ADD CONSTRAINT "folder_parent_id_folder_id_fk" FOREIGN KEY ("parent_id") REFERENCES "public"."folder"("id") ON DELETE set null ON UPDATE no action;
211+
EXCEPTION WHEN duplicate_object THEN null;
212+
END $$;--> statement-breakpoint
191213
-- knowledge_base.folder_id / user_table_definitions.folder_id were added earlier in this
192214
-- migration and are all-NULL, and no deployed code reads or writes them yet — so adding
193215
-- their FK here (unlike the workflow/workspace_files drops above) carries no cross-deploy
194216
-- write-compatibility risk. NOT VALID + an immediate VALIDATE avoids the full-table lock a
195217
-- plain ADD CONSTRAINT takes; validating an all-NULL column is instant either way, but this
196218
-- matches the established pattern (see migrations/0243_kb_workspace_cascade.sql).
197-
ALTER TABLE "knowledge_base" ADD CONSTRAINT "knowledge_base_folder_id_folder_id_fk" FOREIGN KEY ("folder_id") REFERENCES "public"."folder"("id") ON DELETE set null ON UPDATE no action NOT VALID;--> statement-breakpoint
219+
DO $$ BEGIN
220+
ALTER TABLE "knowledge_base" ADD CONSTRAINT "knowledge_base_folder_id_folder_id_fk" FOREIGN KEY ("folder_id") REFERENCES "public"."folder"("id") ON DELETE set null ON UPDATE no action NOT VALID;
221+
EXCEPTION WHEN duplicate_object THEN null;
222+
END $$;--> statement-breakpoint
198223
ALTER TABLE "knowledge_base" VALIDATE CONSTRAINT "knowledge_base_folder_id_folder_id_fk";--> statement-breakpoint
199-
ALTER TABLE "user_table_definitions" ADD CONSTRAINT "user_table_definitions_folder_id_folder_id_fk" FOREIGN KEY ("folder_id") REFERENCES "public"."folder"("id") ON DELETE set null ON UPDATE no action NOT VALID;--> statement-breakpoint
224+
DO $$ BEGIN
225+
ALTER TABLE "user_table_definitions" ADD CONSTRAINT "user_table_definitions_folder_id_folder_id_fk" FOREIGN KEY ("folder_id") REFERENCES "public"."folder"("id") ON DELETE set null ON UPDATE no action NOT VALID;
226+
EXCEPTION WHEN duplicate_object THEN null;
227+
END $$;--> statement-breakpoint
200228
ALTER TABLE "user_table_definitions" VALIDATE CONSTRAINT "user_table_definitions_folder_id_folder_id_fk";--> statement-breakpoint
201229
-- knowledge_base and user_table_definitions are existing, populated tables: build their new
202230
-- indexes CONCURRENTLY so the build never takes ACCESS EXCLUSIVE on a live relation (runner

0 commit comments

Comments
 (0)