import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres'; /** * Migration: native_localization * * Transforms the existing schema (manual `locale` select column on each row) * into Payload's native localization join-table structure. * * Each statement is a separate db.execute call to avoid Drizzle multi-statement issues. */ export async function up({ db }: MigrateUpArgs): Promise { // ── 1. Global locale enum ──────────────────────────────────────────────────── await db.execute(sql` DO $$ BEGIN CREATE TYPE "public"."enum__locales" AS ENUM('de', 'en'); EXCEPTION WHEN duplicate_object THEN null; END $$ `); await db.execute(sql` DO $$ BEGIN CREATE TYPE "public"."enum__posts_v_published_locale" AS ENUM('de', 'en'); EXCEPTION WHEN duplicate_object THEN null; END $$ `); await db.execute(sql` DO $$ BEGIN CREATE TYPE "public"."enum__products_v_published_locale" AS ENUM('de', 'en'); EXCEPTION WHEN duplicate_object THEN null; END $$ `); await db.execute(sql` DO $$ BEGIN CREATE TYPE "public"."enum__pages_v_published_locale" AS ENUM('de', 'en'); EXCEPTION WHEN duplicate_object THEN null; END $$ `); await db.execute(sql` DO $$ BEGIN CREATE TYPE "public"."enum_pages_layout" AS ENUM('default', 'fullBleed'); EXCEPTION WHEN duplicate_object THEN null; END $$ `); await db.execute(sql` DO $$ BEGIN CREATE TYPE "public"."enum__pages_v_version_layout" AS ENUM('default', 'fullBleed'); EXCEPTION WHEN duplicate_object THEN null; END $$ `); await db.execute(sql` DO $$ BEGIN CREATE TYPE "public"."enum__pages_v_version_status" AS ENUM('draft', 'published'); EXCEPTION WHEN duplicate_object THEN null; END $$ `); await db.execute(sql` DO $$ BEGIN CREATE TYPE "public"."enum__posts_v_version_status" AS ENUM('draft', 'published'); EXCEPTION WHEN duplicate_object THEN null; END $$ `); await db.execute(sql` DO $$ BEGIN CREATE TYPE "public"."enum__products_v_version_status" AS ENUM('draft', 'published'); EXCEPTION WHEN duplicate_object THEN null; END $$ `); // ── 2. Alter pages table ───────────────────────────────────────────────────── await db.execute(sql`ALTER TABLE "pages" ADD COLUMN IF NOT EXISTS "layout" "enum_pages_layout" DEFAULT 'default'`); await db.execute(sql`ALTER TABLE "pages" ADD COLUMN IF NOT EXISTS "_status" "enum_pages_status" DEFAULT 'draft'`); // ── 3. Create pages_locales join table ─────────────────────────────────────── await db.execute(sql` CREATE TABLE IF NOT EXISTS "pages_locales" ( "title" varchar, "slug" varchar, "excerpt" varchar, "content" jsonb, "id" serial PRIMARY KEY NOT NULL, "_locale" "enum__locales" NOT NULL, "_parent_id" integer NOT NULL ) `); await db.execute(sql` DO $$ BEGIN ALTER TABLE "pages_locales" ADD CONSTRAINT "pages_locales_locale_parent_id_unique" UNIQUE("_locale", "_parent_id"); EXCEPTION WHEN duplicate_object THEN null; END $$ `); await db.execute(sql` DO $$ BEGIN ALTER TABLE "pages_locales" ADD CONSTRAINT "pages_locales_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "pages"("id") ON DELETE cascade; EXCEPTION WHEN duplicate_object THEN null; END $$ `); // ── 4. Backfill pages_locales from old pages rows ──────────────────────────── await db.execute(sql` INSERT INTO "pages_locales" ("title", "slug", "excerpt", "content", "_locale", "_parent_id") SELECT p.title, p.slug, p.excerpt, p.content, CASE WHEN p.locale::text = 'de' THEN 'de'::"enum__locales" ELSE 'en'::"enum__locales" END, p.id FROM "pages" p WHERE p.locale IS NOT NULL ON CONFLICT ("_locale", "_parent_id") DO UPDATE SET "title" = EXCLUDED."title", "slug" = EXCLUDED."slug", "excerpt" = EXCLUDED."excerpt", "content" = EXCLUDED."content" `); // ── 5. Drop old columns from pages ─────────────────────────────────────────── await db.execute(sql`ALTER TABLE "pages" DROP COLUMN IF EXISTS "title"`); await db.execute(sql`ALTER TABLE "pages" DROP COLUMN IF EXISTS "slug"`); await db.execute(sql`ALTER TABLE "pages" DROP COLUMN IF EXISTS "excerpt"`); await db.execute(sql`ALTER TABLE "pages" DROP COLUMN IF EXISTS "content"`); await db.execute(sql`ALTER TABLE "pages" DROP COLUMN IF EXISTS "locale"`); // ── 6. Create posts_locales join table ─────────────────────────────────────── await db.execute(sql` CREATE TABLE IF NOT EXISTS "posts_locales" ( "title" varchar, "slug" varchar, "excerpt" varchar, "category" varchar, "content" jsonb, "id" serial PRIMARY KEY NOT NULL, "_locale" "enum__locales" NOT NULL, "_parent_id" integer NOT NULL ) `); await db.execute(sql` DO $$ BEGIN ALTER TABLE "posts_locales" ADD CONSTRAINT "posts_locales_locale_parent_id_unique" UNIQUE("_locale", "_parent_id"); EXCEPTION WHEN duplicate_object THEN null; END $$ `); await db.execute(sql` DO $$ BEGIN ALTER TABLE "posts_locales" ADD CONSTRAINT "posts_locales_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "posts"("id") ON DELETE cascade; EXCEPTION WHEN duplicate_object THEN null; END $$ `); // ── 7. Backfill posts_locales ──────────────────────────────────────────────── await db.execute(sql` INSERT INTO "posts_locales" ("title", "slug", "excerpt", "category", "content", "_locale", "_parent_id") SELECT p.title, p.slug, p.excerpt, p.category, p.content, CASE WHEN p.locale::text = 'de' THEN 'de'::"enum__locales" ELSE 'en'::"enum__locales" END, p.id FROM "posts" p WHERE p.locale IS NOT NULL ON CONFLICT ("_locale", "_parent_id") DO UPDATE SET "title" = EXCLUDED."title", "slug" = EXCLUDED."slug", "excerpt" = EXCLUDED."excerpt", "category" = EXCLUDED."category", "content" = EXCLUDED."content" `); // ── 8. Drop old columns from posts ─────────────────────────────────────────── await db.execute(sql`ALTER TABLE "posts" DROP COLUMN IF EXISTS "title"`); await db.execute(sql`ALTER TABLE "posts" DROP COLUMN IF EXISTS "slug"`); await db.execute(sql`ALTER TABLE "posts" DROP COLUMN IF EXISTS "excerpt"`); await db.execute(sql`ALTER TABLE "posts" DROP COLUMN IF EXISTS "category"`); await db.execute(sql`ALTER TABLE "posts" DROP COLUMN IF EXISTS "content"`); await db.execute(sql`ALTER TABLE "posts" DROP COLUMN IF EXISTS "locale"`); // ── 9. Create products_locales join table ──────────────────────────────────── await db.execute(sql` CREATE TABLE IF NOT EXISTS "products_locales" ( "title" varchar, "description" varchar, "application" jsonb, "content" jsonb, "id" serial PRIMARY KEY NOT NULL, "_locale" "enum__locales" NOT NULL, "_parent_id" integer NOT NULL ) `); await db.execute(sql` DO $$ BEGIN ALTER TABLE "products_locales" ADD CONSTRAINT "products_locales_locale_parent_id_unique" UNIQUE("_locale", "_parent_id"); EXCEPTION WHEN duplicate_object THEN null; END $$ `); await db.execute(sql` DO $$ BEGIN ALTER TABLE "products_locales" ADD CONSTRAINT "products_locales_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "products"("id") ON DELETE cascade; EXCEPTION WHEN duplicate_object THEN null; END $$ `); // ── 10. Backfill products_locales ──────────────────────────────────────────── // Products were separate DE/EN rows — each becomes a locale entry on its own id await db.execute(sql` INSERT INTO "products_locales" ("title", "description", "application", "content", "_locale", "_parent_id") SELECT p.title, p.description, p.application, p.content, CASE WHEN p.locale::text = 'de' THEN 'de'::"enum__locales" ELSE 'en'::"enum__locales" END, p.id FROM "products" p WHERE p.locale IS NOT NULL ON CONFLICT ("_locale", "_parent_id") DO NOTHING `); // ── 11. Drop old columns from products ─────────────────────────────────────── await db.execute(sql`ALTER TABLE "products" DROP COLUMN IF EXISTS "title"`); await db.execute(sql`ALTER TABLE "products" DROP COLUMN IF EXISTS "description"`); await db.execute(sql`ALTER TABLE "products" DROP COLUMN IF EXISTS "application"`); await db.execute(sql`ALTER TABLE "products" DROP COLUMN IF EXISTS "content"`); await db.execute(sql`ALTER TABLE "products" DROP COLUMN IF EXISTS "locale"`); // ── 12. Version tables (_posts_v, _products_v, _pages_v) locale columns ────── await db.execute(sql`ALTER TABLE "_posts_v" ADD COLUMN IF NOT EXISTS "published_locale" "enum__posts_v_published_locale"`); await db.execute(sql`ALTER TABLE "_products_v" ADD COLUMN IF NOT EXISTS "published_locale" "enum__products_v_published_locale"`); await db.execute(sql`ALTER TABLE "_pages_v" ADD COLUMN IF NOT EXISTS "published_locale" "enum__pages_v_published_locale"`); // ── 13. Create _posts_v_locales ────────────────────────────────────────────── await db.execute(sql` CREATE TABLE IF NOT EXISTS "_posts_v_locales" ( "version_title" varchar, "version_slug" varchar, "version_excerpt" varchar, "version_category" varchar, "version_content" jsonb, "id" serial PRIMARY KEY NOT NULL, "_locale" "enum__locales" NOT NULL, "_parent_id" integer NOT NULL ) `); await db.execute(sql` DO $$ BEGIN ALTER TABLE "_posts_v_locales" ADD CONSTRAINT "_posts_v_locales_locale_parent_id_unique" UNIQUE("_locale", "_parent_id"); EXCEPTION WHEN duplicate_object THEN null; END $$ `); await db.execute(sql` DO $$ BEGIN ALTER TABLE "_posts_v_locales" ADD CONSTRAINT "_posts_v_locales_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "_posts_v"("id") ON DELETE cascade; EXCEPTION WHEN duplicate_object THEN null; END $$ `); // ── 14. Create _products_v_locales ─────────────────────────────────────────── await db.execute(sql` CREATE TABLE IF NOT EXISTS "_products_v_locales" ( "version_title" varchar, "version_description" varchar, "version_application" jsonb, "version_content" jsonb, "id" serial PRIMARY KEY NOT NULL, "_locale" "enum__locales" NOT NULL, "_parent_id" integer NOT NULL ) `); await db.execute(sql` DO $$ BEGIN ALTER TABLE "_products_v_locales" ADD CONSTRAINT "_products_v_locales_locale_parent_id_unique" UNIQUE("_locale", "_parent_id"); EXCEPTION WHEN duplicate_object THEN null; END $$ `); await db.execute(sql` DO $$ BEGIN ALTER TABLE "_products_v_locales" ADD CONSTRAINT "_products_v_locales_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "_products_v"("id") ON DELETE cascade; EXCEPTION WHEN duplicate_object THEN null; END $$ `); // ── 15. Create _pages_v_locales ────────────────────────────────────────────── await db.execute(sql` CREATE TABLE IF NOT EXISTS "_pages_v_locales" ( "version_title" varchar, "version_slug" varchar, "version_excerpt" varchar, "version_content" jsonb, "id" serial PRIMARY KEY NOT NULL, "_locale" "enum__locales" NOT NULL, "_parent_id" integer NOT NULL ) `); await db.execute(sql` DO $$ BEGIN ALTER TABLE "_pages_v_locales" ADD CONSTRAINT "_pages_v_locales_locale_parent_id_unique" UNIQUE("_locale", "_parent_id"); EXCEPTION WHEN duplicate_object THEN null; END $$ `); await db.execute(sql` DO $$ BEGIN ALTER TABLE "_pages_v_locales" ADD CONSTRAINT "_pages_v_locales_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "_pages_v"("id") ON DELETE cascade; EXCEPTION WHEN duplicate_object THEN null; END $$ `); // ── 16. Drop the now-redundant old locale enum ─────────────────────────────── await db.execute(sql`DROP TYPE IF EXISTS "public"."enum_pages_locale"`); await db.execute(sql`DROP TYPE IF EXISTS "public"."enum_posts_locale"`); await db.execute(sql`DROP TYPE IF EXISTS "public"."enum_products_locale"`); } export async function down({ db }: MigrateDownArgs): Promise { await db.execute(sql`DROP TABLE IF EXISTS "pages_locales" CASCADE`); await db.execute(sql`DROP TABLE IF EXISTS "_pages_v_locales" CASCADE`); await db.execute(sql`DROP TABLE IF EXISTS "posts_locales" CASCADE`); await db.execute(sql`DROP TABLE IF EXISTS "_posts_v_locales" CASCADE`); await db.execute(sql`DROP TABLE IF EXISTS "products_locales" CASCADE`); await db.execute(sql`DROP TABLE IF EXISTS "_products_v_locales" CASCADE`); }