Skip to content

Instantly share code, notes, and snippets.

@jokull
Last active March 2, 2026 12:57
Show Gist options
  • Select an option

  • Save jokull/f0ca2cb344f0b015b1c58cc931e5f82b to your computer and use it in GitHub Desktop.

Select an option

Save jokull/f0ca2cb344f0b015b1c58cc931e5f82b to your computer and use it in GitHub Desktop.
Deployment plan: dato_tour → tour rename (PR #1418)

Deployment plan: dato_tour → tour rename (PR #1418)

Deployment Plan: dato_tourtour Rename (PR #1418)

Context

PR #1418 renames dato_tourtour 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)

  • Expand (this PR): New tables/columns created alongside old ones, data backfilled, old constraints relaxed
  • Deploy: New code uses new tables+columns. Old code still works against old tables
  • Contract (follow-up PR): Drop legacy tables, drop old columns, add NOT NULL

Deploy Targets

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.


What the Expand Migration Does

Creates new tables alongside old

  • tour — copy of dato_tour + dato_id column, all data backfilled
  • tour_inventory — copy of dato_tour_inventory with tour_id, all data backfilled

Adds tour_id columns to child tables (alongside dato_tour_id)

  • bokun_line_item, dato_tour_locale, tour_plan_override, linktivity_line_item, trip_template_day_item
  • All backfilled: tour_id = dato_tour_id

Relaxes old constraints

  • DROP NOT NULL on all old dato_tour_id columns
  • Drops composite PKs on dato_tour_locale and tour_plan_override (replaced with unique constraints) — PKs implicitly enforce NOT NULL
  • Old indexes, FKs, and tables remain untouched

Adds new constraints

  • All indexes from the Drizzle schema on new tour/tour_inventory tables
  • Indexes on new tour_id columns in child tables
  • FKs from tour_idtour.id
  • Unique constraints on new columns

Deployment Sequence

Step 0: Local Validation (Docker Postgres)

git 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

Step 1: Run Production Migration

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.

Step 2: Verify Migration (Supabase, before merge)

-- 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

Step 3: Merge PR

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.

Step 4: Manual Tests

# 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

Step 5: Monitor (24h)

  • Sentry: filter for dato_tour or tour_id SQL errors
  • Workers logs: reindexTours cron completes
  • Inventory writes to tour_inventory
  • DatoCMS edits trigger webhook sync (not creation)

Rollback Plan

Since old tables/columns are preserved, rollback is just reverting the code — no SQL needed.

Rollback sequence:

  1. Revert PR on GitHub (both Vercel + Workers auto-redeploy old code)
  2. Old code uses dato_tour/dato_tour_id which still exist untouched
  3. No database rollback needed

If 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

Contract Phase (Follow-up PR)

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");

Issue #1397 — Updated Follow-up

Done by this PR

  • Table/column renames + dato_id backfill (expand-contract)
  • Full code cutover (97 files)
  • Webhook: admin-only creation
  • insertTourFromCms → cuid2 for new tours
  • createTour admin procedure
  • Clean up datoId ?? id fallback

Follow-up PRs

  • Contract migration (drop old tables/columns)
  • Audit remaining legacy references
  • Optional: rename dato_tour_localetour_locale
  • Publishing eligibility gates
  • Admin UI for full tour lifecycle
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment