121 lines
7.2 KiB
MySQL
121 lines
7.2 KiB
MySQL
|
|
-- ============================================================================
|
||
|
|
-- 0003 — Promote workspaces to a top-level table.
|
||
|
|
-- ============================================================================
|
||
|
|
-- Block A of the EchoDo commercial plan. We:
|
||
|
|
-- 1. Create the new `workspaces` table.
|
||
|
|
-- 2. Copy every existing `objects` row of type='workspace' INTO `workspaces`,
|
||
|
|
-- preserving the same UUID so existing FKs (which all point at
|
||
|
|
-- objects.id today) remain valid mid-migration.
|
||
|
|
-- 3. Mint a `slug` for each workspace from its title (or falling back to a
|
||
|
|
-- short UUID prefix if the slug would collide / be empty).
|
||
|
|
-- 4. Drop the old objects→objects FK on every anchor table and re-add a new
|
||
|
|
-- FK pointing at workspaces.id.
|
||
|
|
-- 5. Delete the now-redundant type='workspace' rows from `objects` and add
|
||
|
|
-- NOT NULL on objects.workspace_id (it was nullable for self-references).
|
||
|
|
-- ============================================================================
|
||
|
|
|
||
|
|
-- 1. New workspaces table
|
||
|
|
CREATE TABLE "workspaces" (
|
||
|
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||
|
|
"slug" varchar(60) NOT NULL,
|
||
|
|
"name" varchar(200) NOT NULL,
|
||
|
|
"owner_user_id" uuid NOT NULL,
|
||
|
|
"plan_tier" varchar(20) DEFAULT 'free' NOT NULL,
|
||
|
|
"archived_at" timestamp with time zone,
|
||
|
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||
|
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||
|
|
);
|
||
|
|
--> statement-breakpoint
|
||
|
|
|
||
|
|
-- 2. Copy existing workspaces (objects WHERE type='workspace') into the new
|
||
|
|
-- table. Owner is best-effort: prefer created_by, otherwise the first
|
||
|
|
-- workspace_member that exists, otherwise the first user in the system
|
||
|
|
-- (single-tenant homelab fallback).
|
||
|
|
INSERT INTO "workspaces" ("id", "slug", "name", "owner_user_id", "plan_tier", "archived_at", "created_at", "updated_at")
|
||
|
|
SELECT
|
||
|
|
o.id,
|
||
|
|
-- slug: lowercased, alphanum+dash, fallback to first 8 chars of UUID.
|
||
|
|
COALESCE(
|
||
|
|
NULLIF(
|
||
|
|
regexp_replace(lower(trim(o.title)), '[^a-z0-9]+', '-', 'g'),
|
||
|
|
''
|
||
|
|
),
|
||
|
|
substring(o.id::text from 1 for 8)
|
||
|
|
),
|
||
|
|
COALESCE(NULLIF(trim(o.title), ''), 'Workspace'),
|
||
|
|
COALESCE(
|
||
|
|
o.created_by,
|
||
|
|
(SELECT wm.user_id FROM "workspace_members" wm WHERE wm.workspace_id = o.id ORDER BY wm.created_at ASC LIMIT 1),
|
||
|
|
(SELECT u.id FROM "users" u ORDER BY u.created_at ASC LIMIT 1)
|
||
|
|
),
|
||
|
|
'free',
|
||
|
|
o.archived_at,
|
||
|
|
o.created_at,
|
||
|
|
o.updated_at
|
||
|
|
FROM "objects" o
|
||
|
|
WHERE o.type = 'workspace';
|
||
|
|
--> statement-breakpoint
|
||
|
|
|
||
|
|
-- 2b. Disambiguate any duplicate slugs (e.g. two workspaces both titled "Tasks")
|
||
|
|
-- by suffixing with the short UUID. Only touches collisions.
|
||
|
|
UPDATE "workspaces" w
|
||
|
|
SET "slug" = w.slug || '-' || substring(w.id::text from 1 for 6)
|
||
|
|
WHERE w.id IN (
|
||
|
|
SELECT id FROM (
|
||
|
|
SELECT id, row_number() OVER (PARTITION BY slug ORDER BY created_at) AS rn
|
||
|
|
FROM "workspaces"
|
||
|
|
) ranked
|
||
|
|
WHERE ranked.rn > 1
|
||
|
|
);
|
||
|
|
--> statement-breakpoint
|
||
|
|
|
||
|
|
-- 2c. Safety net: if there is at least one user but NO workspace exists yet
|
||
|
|
-- (fresh DB on a brand-new Coolify deploy), seed a default one so the
|
||
|
|
-- NOT NULL FK swap below cannot fail at runtime.
|
||
|
|
INSERT INTO "workspaces" ("slug", "name", "owner_user_id")
|
||
|
|
SELECT 'default', 'Default Workspace', u.id
|
||
|
|
FROM "users" u
|
||
|
|
WHERE NOT EXISTS (SELECT 1 FROM "workspaces")
|
||
|
|
ORDER BY u.created_at ASC
|
||
|
|
LIMIT 1;
|
||
|
|
--> statement-breakpoint
|
||
|
|
|
||
|
|
-- 3. Drop old objects.id-based FKs on every anchor table.
|
||
|
|
ALTER TABLE "objects" DROP CONSTRAINT "objects_workspace_id_objects_id_fk";--> statement-breakpoint
|
||
|
|
ALTER TABLE "workspace_members" DROP CONSTRAINT "workspace_members_workspace_id_objects_id_fk";--> statement-breakpoint
|
||
|
|
ALTER TABLE "object_type_defs" DROP CONSTRAINT "object_type_defs_workspace_id_objects_id_fk";--> statement-breakpoint
|
||
|
|
ALTER TABLE "property_definitions" DROP CONSTRAINT "property_definitions_workspace_id_objects_id_fk";--> statement-breakpoint
|
||
|
|
ALTER TABLE "templates" DROP CONSTRAINT "templates_workspace_id_objects_id_fk";--> statement-breakpoint
|
||
|
|
ALTER TABLE "forms" DROP CONSTRAINT "forms_workspace_id_objects_id_fk";--> statement-breakpoint
|
||
|
|
ALTER TABLE "markdown_backlog_items" DROP CONSTRAINT "markdown_backlog_items_workspace_id_objects_id_fk";--> statement-breakpoint
|
||
|
|
ALTER TABLE "cursor_sync_mappings" DROP CONSTRAINT "cursor_sync_mappings_workspace_id_objects_id_fk";--> statement-breakpoint
|
||
|
|
|
||
|
|
-- 4. Backfill any orphan `workspace_id` values (rows whose old workspace
|
||
|
|
-- object was deleted before this migration ran). Reassign to the first
|
||
|
|
-- available workspace so the NOT NULL constraint below holds.
|
||
|
|
UPDATE "objects"
|
||
|
|
SET "workspace_id" = (SELECT id FROM "workspaces" ORDER BY created_at ASC LIMIT 1)
|
||
|
|
WHERE "workspace_id" IS NULL
|
||
|
|
AND EXISTS (SELECT 1 FROM "workspaces");
|
||
|
|
--> statement-breakpoint
|
||
|
|
|
||
|
|
-- 5. Now objects.workspace_id is fully populated → flip to NOT NULL.
|
||
|
|
ALTER TABLE "objects" ALTER COLUMN "workspace_id" SET NOT NULL;--> statement-breakpoint
|
||
|
|
|
||
|
|
-- 6. Remove the obsolete type='workspace' rows from `objects` (they live in
|
||
|
|
-- `workspaces` now). Use a guard so this is a no-op on a fresh DB.
|
||
|
|
DELETE FROM "objects" WHERE "type" = 'workspace';--> statement-breakpoint
|
||
|
|
|
||
|
|
-- 7. New FK constraints + indexes.
|
||
|
|
ALTER TABLE "workspaces" ADD CONSTRAINT "workspaces_owner_user_id_users_id_fk" FOREIGN KEY ("owner_user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||
|
|
CREATE UNIQUE INDEX "workspaces_slug_unique" ON "workspaces" USING btree ("slug");--> statement-breakpoint
|
||
|
|
CREATE INDEX "workspaces_owner_user_id_idx" ON "workspaces" USING btree ("owner_user_id");--> statement-breakpoint
|
||
|
|
ALTER TABLE "objects" ADD CONSTRAINT "objects_workspace_id_workspaces_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspaces"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||
|
|
ALTER TABLE "workspace_members" ADD CONSTRAINT "workspace_members_workspace_id_workspaces_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspaces"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||
|
|
ALTER TABLE "object_type_defs" ADD CONSTRAINT "object_type_defs_workspace_id_workspaces_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspaces"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||
|
|
ALTER TABLE "property_definitions" ADD CONSTRAINT "property_definitions_workspace_id_workspaces_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspaces"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||
|
|
ALTER TABLE "templates" ADD CONSTRAINT "templates_workspace_id_workspaces_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspaces"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||
|
|
ALTER TABLE "forms" ADD CONSTRAINT "forms_workspace_id_workspaces_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspaces"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||
|
|
ALTER TABLE "markdown_backlog_items" ADD CONSTRAINT "markdown_backlog_items_workspace_id_workspaces_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspaces"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||
|
|
ALTER TABLE "cursor_sync_mappings" ADD CONSTRAINT "cursor_sync_mappings_workspace_id_workspaces_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspaces"("id") ON DELETE cascade ON UPDATE no action;
|