Deployment plan: dato_tour → tour rename (PR #1418)
expressive-napping-hummingbird.md
For the full content of this gist, refer to https://gist.github.com/jokull/f0ca2cb344f0b015b1c58cc931e5f82b
expressive-napping-hummingbird.md
For the full content of this gist, refer to https://gist.github.com/jokull/f0ca2cb344f0b015b1c58cc931e5f82b
Deployment plan: dato_tour → tour rename (PR #1418)
dato_tour → tour Rename (PR #1418)PR #1418 renames dato_tour → tour tables, switches 97 files, and establishes the foundation for admin-first tour creation. Existing tour rows keep DatoCMS IDs as Tour.id; new Tour.datoId column (nullable) links to DatoCMS content. New tours get cuid2 IDs.
Migration strategy: Expand-Contract (zero-downtime reads)
| Target | Method | Trigger |
|---|---|---|
| Next.js | Vercel GitHub integration | Auto on merge to main |
| Workers | CI auto-deploy | Auto on merge to main |
Both deploy in parallel after merge. Both reference the new tour/tour_id tables and columns.
tour — copy of dato_tour + dato_id column, all data backfilledtour_inventory — copy of dato_tour_inventory with tour_id, all data backfilledtour_id columns to child tables (alongside dato_tour_id)bokun_line_item, dato_tour_locale, tour_plan_override, linktivity_line_item, trip_template_day_itemtour_id = dato_tour_idDROP NOT NULL on all old dato_tour_id columnsdato_tour_locale and tour_plan_override (replaced with unique constraints) — PKs implicitly enforce NOT NULLtour/tour_inventory tablestour_id columns in child tablestour_id → tour.idgit checkout feat/tour-rename-expand
# Run migration against local Docker Postgres
cd packages/db && pnpm push
# Verify both old and new tables coexist
psql $LOCAL_DB_URL <<'SQL'
SELECT count(*) as dato_tours FROM dato_tour;
SELECT count(*) as tours FROM tour;
SELECT count(*) as old_inventory FROM dato_tour_inventory;
SELECT count(*) as new_inventory FROM tour_inventory;
SELECT tour_id, dato_tour_id FROM bokun_line_item LIMIT 3;
SQL
cd packages/db
dotenv -e .env.production drizzle-kit push --verbose --strict
No error window — old code still reads/writes dato_tour/dato_tour_id which are untouched.
-- Both old and new tables exist with matching counts
SELECT
(SELECT count(*) FROM dato_tour) as old_tours,
(SELECT count(*) FROM tour) as new_tours,
(SELECT count(*) FROM dato_tour_inventory) as old_inventory,
(SELECT count(*) FROM tour_inventory) as new_inventory;
-- Backfill complete
SELECT count(*) FROM tour WHERE dato_id IS NULL; -- expect 0
-- Child table dual columns
SELECT tour_id, dato_tour_id FROM bokun_line_item LIMIT 3;
-- expect both columns populated with same value
-- New constraints exist
SELECT conname FROM pg_constraint
WHERE conrelid = 'tour'::regclass AND contype = 'u';
-- Expected: tour_slug_unique, tour_dato_id_unique
Merge on GitHub → triggers both Vercel and Workers deploy (~2-5 min).
Reads: zero downtime. Old code reads old tables, new code reads new tables — both have the data.
Writes: brief gap during rollout (~seconds). Old instances write to dato_tour/dato_tour_id, new instances write to tour/tour_id. No sync between them. Inventory sync and webhook handlers re-converge on next run.
| # | Test | Action | Expected |
|---|---|---|---|
| 1 | Tour page | /en/tours/[slug]/[existing-id] | Renders with CMS content |
| 2 | Admin tours | /admin/tours | Table loads, all tours |
| 3 | Tour search | Trip builder experiences tab | Results with type: "tour" |
| 4 | Cart add | Add tour from detail page | Cart updates |
| 5 | Cart remove | Remove tour from cart | Cart updates |
| 6 | Inventory | /en/tours listing | Available tours show |
| 7 | Webhook sync | Edit tour in DatoCMS | modified_at updates in DB |
| 8 | Webhook skip | Create tour in DatoCMS | Logged as skipped |
| 9 | ISR revalidation | Edit tour in DatoCMS | Page revalidates |
| 10 | Sitemap | /sitemaps/tours.xml | Correct IDs in URLs |
| 11 | Orders | /orders with tour bookings | Tour line items render |
| 12 | Receipt | /orders/[id]/receipt | Tour names display |
| 13 | Checkout | Full checkout with tour | Payment succeeds |
dato_tour or tour_id SQL errorsreindexTours cron completestour_inventorySince old tables/columns are preserved, rollback is just reverting the code — no SQL needed.
Rollback sequence:
dato_tour/dato_tour_id which still exist untouchedIf you also want to clean up the expand artifacts:
-- Drop new tables
DROP TABLE IF EXISTS "tour_inventory";
DROP TABLE IF EXISTS "tour" CASCADE;
-- Drop new tour_id columns
ALTER TABLE "bokun_line_item" DROP COLUMN IF EXISTS "tour_id";
ALTER TABLE "dato_tour_locale" DROP COLUMN IF EXISTS "tour_id";
ALTER TABLE "tour_plan_override" DROP COLUMN IF EXISTS "tour_id";
ALTER TABLE "linktivity_line_item" DROP COLUMN IF EXISTS "tour_id";
ALTER TABLE "trip_template_day_item" DROP COLUMN IF EXISTS "tour_id";
-- Restore old constraints
ALTER TABLE "bokun_line_item" ALTER COLUMN "dato_tour_id" SET NOT NULL;
ALTER TABLE "linktivity_line_item" ALTER COLUMN "dato_tour_id" SET NOT NULL;
ALTER TABLE "dato_tour_locale" ALTER COLUMN "dato_tour_id" SET NOT NULL;
ALTER TABLE "tour_plan_override" ALTER COLUMN "dato_tour_id" SET NOT NULL;
ALTER TABLE "dato_tour_inventory" ALTER COLUMN "dato_tour_id" SET NOT NULL;
-- Restore old PKs
ALTER TABLE "dato_tour_locale" ADD CONSTRAINT "dato_tour_locale_pkey"
PRIMARY KEY("dato_tour_id","locale");
ALTER TABLE "tour_plan_override" ADD CONSTRAINT "tour_plan_override_dato_tour_id_plan_id_pk"
PRIMARY KEY("dato_tour_id","plan_id");
-- Then: drizzle-kit push from main to reconcile snapshot
After this PR is deployed and stable:
-- Drop old tables
DROP TABLE "dato_tour_inventory";
DROP TABLE "dato_tour";
-- Backfill any stragglers
UPDATE "bokun_line_item" SET "tour_id" = "dato_tour_id" WHERE "tour_id" IS NULL;
UPDATE "dato_tour_locale" SET "tour_id" = "dato_tour_id" WHERE "tour_id" IS NULL;
UPDATE "tour_plan_override" SET "tour_id" = "dato_tour_id" WHERE "tour_id" IS NULL;
UPDATE "linktivity_line_item" SET "tour_id" = "dato_tour_id" WHERE "tour_id" IS NULL;
UPDATE "trip_template_day_item" SET "tour_id" = "dato_tour_id" WHERE "tour_id" IS NULL;
-- Drop old columns
ALTER TABLE "bokun_line_item" DROP COLUMN "dato_tour_id";
ALTER TABLE "dato_tour_locale" DROP COLUMN "dato_tour_id";
ALTER TABLE "tour_plan_override" DROP COLUMN "dato_tour_id";
ALTER TABLE "linktivity_line_item" DROP COLUMN "dato_tour_id";
ALTER TABLE "trip_template_day_item" DROP COLUMN "dato_tour_id";
-- Add NOT NULL on tour_id
ALTER TABLE "bokun_line_item" ALTER COLUMN "tour_id" SET NOT NULL;
ALTER TABLE "dato_tour_locale" ALTER COLUMN "tour_id" SET NOT NULL;
ALTER TABLE "tour_plan_override" ALTER COLUMN "tour_id" SET NOT NULL;
ALTER TABLE "linktivity_line_item" ALTER COLUMN "tour_id" SET NOT NULL;
-- Promote unique constraints to PKs
ALTER TABLE "dato_tour_locale" ADD CONSTRAINT "dato_tour_locale_pkey"
PRIMARY KEY("tour_id","locale");
ALTER TABLE "tour_plan_override" ADD CONSTRAINT "tour_plan_override_tour_id_plan_id_pk"
PRIMARY KEY("tour_id","plan_id");
insertTourFromCms → cuid2 for new tourscreateTour admin proceduredato_tour_locale → tour_locale