389 lines
14 KiB
PL/PgSQL
389 lines
14 KiB
PL/PgSQL
-- =============================================================================
|
|
-- INVY — Production Migration Script
|
|
-- =============================================================================
|
|
-- SAFE: Additive-only. Nothing is dropped. All blocks are idempotent.
|
|
-- Run once to bring a production DB (old schema) in sync with the new schema.
|
|
--
|
|
-- Order of execution:
|
|
-- 1. Enable extensions
|
|
-- 2. Create new tables (IF NOT EXISTS)
|
|
-- 3. Patch existing tables (ADD COLUMN IF NOT EXISTS / ALTER/ADD CONSTRAINT)
|
|
-- 4. Migrate old `guests` rows → `guests_v2` (only when guests_v2 is empty)
|
|
-- 5. Add indexes and triggers (IF NOT EXISTS)
|
|
-- =============================================================================
|
|
|
|
-- =============================================================================
|
|
-- STEP 1 — Enable UUID extension
|
|
-- =============================================================================
|
|
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
|
|
|
|
|
-- =============================================================================
|
|
-- STEP 2a — Create `users` table
|
|
-- =============================================================================
|
|
CREATE TABLE IF NOT EXISTS users (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
email TEXT NOT NULL UNIQUE,
|
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
|
|
|
|
|
|
-- =============================================================================
|
|
-- STEP 2b — Create `events` table
|
|
-- =============================================================================
|
|
CREATE TABLE IF NOT EXISTS events (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
name TEXT NOT NULL,
|
|
date TIMESTAMP WITH TIME ZONE,
|
|
location TEXT,
|
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_events_created_at ON events(created_at);
|
|
|
|
|
|
-- =============================================================================
|
|
-- STEP 2c — Create `event_members` table
|
|
-- =============================================================================
|
|
CREATE TABLE IF NOT EXISTS event_members (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
event_id UUID NOT NULL REFERENCES events(id) ON DELETE CASCADE,
|
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
role TEXT NOT NULL DEFAULT 'admin'
|
|
CHECK (role IN ('admin', 'editor', 'viewer')),
|
|
display_name TEXT,
|
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
|
UNIQUE(event_id, user_id)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_event_members_event_id ON event_members(event_id);
|
|
CREATE INDEX IF NOT EXISTS idx_event_members_user_id ON event_members(user_id);
|
|
CREATE INDEX IF NOT EXISTS idx_event_members_event_user ON event_members(event_id, user_id);
|
|
|
|
|
|
-- =============================================================================
|
|
-- STEP 2d — Create `guests_v2` table
|
|
-- =============================================================================
|
|
CREATE TABLE IF NOT EXISTS guests_v2 (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
event_id UUID NOT NULL REFERENCES events(id) ON DELETE CASCADE,
|
|
added_by_user_id UUID NOT NULL REFERENCES users(id),
|
|
|
|
-- identity
|
|
first_name TEXT NOT NULL,
|
|
last_name TEXT NOT NULL DEFAULT '',
|
|
email TEXT,
|
|
phone TEXT, -- legacy alias
|
|
phone_number TEXT,
|
|
|
|
-- RSVP
|
|
rsvp_status TEXT NOT NULL DEFAULT 'invited'
|
|
CHECK (rsvp_status IN ('invited', 'confirmed', 'declined')),
|
|
meal_preference TEXT,
|
|
|
|
-- plus-one
|
|
has_plus_one BOOLEAN DEFAULT FALSE,
|
|
plus_one_name TEXT,
|
|
|
|
-- seating
|
|
table_number TEXT,
|
|
side TEXT, -- e.g. "groom", "bride"
|
|
|
|
-- provenance
|
|
owner_email TEXT,
|
|
source TEXT NOT NULL DEFAULT 'manual'
|
|
CHECK (source IN ('google', 'manual', 'self-service')),
|
|
|
|
notes TEXT,
|
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_guests_v2_event_id ON guests_v2(event_id);
|
|
CREATE INDEX IF NOT EXISTS idx_guests_v2_added_by ON guests_v2(added_by_user_id);
|
|
CREATE INDEX IF NOT EXISTS idx_guests_v2_event_user ON guests_v2(event_id, added_by_user_id);
|
|
CREATE INDEX IF NOT EXISTS idx_guests_v2_phone_number ON guests_v2(phone_number);
|
|
CREATE INDEX IF NOT EXISTS idx_guests_v2_event_phone ON guests_v2(event_id, phone_number);
|
|
CREATE INDEX IF NOT EXISTS idx_guests_v2_event_status ON guests_v2(event_id, rsvp_status);
|
|
CREATE INDEX IF NOT EXISTS idx_guests_v2_owner_email ON guests_v2(event_id, owner_email);
|
|
CREATE INDEX IF NOT EXISTS idx_guests_v2_source ON guests_v2(event_id, source);
|
|
|
|
|
|
-- =============================================================================
|
|
-- STEP 2e — Create `rsvp_tokens` table
|
|
-- =============================================================================
|
|
CREATE TABLE IF NOT EXISTS rsvp_tokens (
|
|
token TEXT PRIMARY KEY,
|
|
event_id UUID NOT NULL REFERENCES events(id) ON DELETE CASCADE,
|
|
guest_id UUID REFERENCES guests_v2(id) ON DELETE SET NULL,
|
|
phone TEXT,
|
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
|
expires_at TIMESTAMP WITH TIME ZONE,
|
|
used_at TIMESTAMP WITH TIME ZONE
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_rsvp_tokens_event_id ON rsvp_tokens(event_id);
|
|
CREATE INDEX IF NOT EXISTS idx_rsvp_tokens_guest_id ON rsvp_tokens(guest_id);
|
|
|
|
|
|
-- =============================================================================
|
|
-- STEP 3a — Patch `events` table: add WhatsApp / RSVP columns (IF NOT EXISTS)
|
|
-- =============================================================================
|
|
DO $$
|
|
BEGIN
|
|
ALTER TABLE events ADD COLUMN partner1_name TEXT; EXCEPTION WHEN duplicate_column THEN NULL;
|
|
END $$;
|
|
DO $$
|
|
BEGIN
|
|
ALTER TABLE events ADD COLUMN partner2_name TEXT; EXCEPTION WHEN duplicate_column THEN NULL;
|
|
END $$;
|
|
DO $$
|
|
BEGIN
|
|
ALTER TABLE events ADD COLUMN venue TEXT; EXCEPTION WHEN duplicate_column THEN NULL;
|
|
END $$;
|
|
DO $$
|
|
BEGIN
|
|
ALTER TABLE events ADD COLUMN event_time TEXT; EXCEPTION WHEN duplicate_column THEN NULL;
|
|
END $$;
|
|
DO $$
|
|
BEGIN
|
|
ALTER TABLE events ADD COLUMN guest_link TEXT; EXCEPTION WHEN duplicate_column THEN NULL;
|
|
END $$;
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_events_guest_link ON events(guest_link);
|
|
|
|
|
|
-- =============================================================================
|
|
-- STEP 3b — Patch `guests_v2`: add any missing columns (forward-compat)
|
|
-- =============================================================================
|
|
DO $$
|
|
BEGIN
|
|
ALTER TABLE guests_v2 ADD COLUMN phone TEXT;
|
|
EXCEPTION WHEN duplicate_column THEN NULL;
|
|
END $$;
|
|
DO $$
|
|
BEGIN
|
|
ALTER TABLE guests_v2 ADD COLUMN phone_number TEXT;
|
|
EXCEPTION WHEN duplicate_column THEN NULL;
|
|
END $$;
|
|
DO $$
|
|
BEGIN
|
|
ALTER TABLE guests_v2 ADD COLUMN last_name TEXT NOT NULL DEFAULT '';
|
|
EXCEPTION WHEN duplicate_column THEN NULL;
|
|
END $$;
|
|
DO $$
|
|
BEGIN
|
|
ALTER TABLE guests_v2 ADD COLUMN notes TEXT;
|
|
EXCEPTION WHEN duplicate_column THEN NULL;
|
|
END $$;
|
|
DO $$
|
|
BEGIN
|
|
ALTER TABLE guests_v2 ADD COLUMN side TEXT;
|
|
EXCEPTION WHEN duplicate_column THEN NULL;
|
|
END $$;
|
|
DO $$
|
|
BEGIN
|
|
ALTER TABLE guests_v2 ADD COLUMN source TEXT NOT NULL DEFAULT 'manual';
|
|
EXCEPTION WHEN duplicate_column THEN NULL;
|
|
END $$;
|
|
DO $$
|
|
BEGIN
|
|
ALTER TABLE guests_v2 ADD COLUMN owner_email TEXT;
|
|
EXCEPTION WHEN duplicate_column THEN NULL;
|
|
END $$;
|
|
|
|
|
|
-- Fix rsvp_status constraint: old versions used 'status' column name or enum
|
|
DO $$
|
|
BEGIN
|
|
-- rename `status` → `rsvp_status` if that old column exists
|
|
IF EXISTS (
|
|
SELECT 1 FROM information_schema.columns
|
|
WHERE table_name = 'guests_v2' AND column_name = 'status'
|
|
) AND NOT EXISTS (
|
|
SELECT 1 FROM information_schema.columns
|
|
WHERE table_name = 'guests_v2' AND column_name = 'rsvp_status'
|
|
) THEN
|
|
ALTER TABLE guests_v2 RENAME COLUMN status TO rsvp_status;
|
|
END IF;
|
|
END $$;
|
|
|
|
-- Ensure CHECK constraint is present (safe drop+add)
|
|
DO $$
|
|
BEGIN
|
|
ALTER TABLE guests_v2 DROP CONSTRAINT IF EXISTS guests_v2_rsvp_status_check;
|
|
ALTER TABLE guests_v2 ADD CONSTRAINT guests_v2_rsvp_status_check
|
|
CHECK (rsvp_status IN ('invited', 'confirmed', 'declined'));
|
|
EXCEPTION WHEN OTHERS THEN NULL;
|
|
END $$;
|
|
|
|
DO $$
|
|
BEGIN
|
|
ALTER TABLE guests_v2 DROP CONSTRAINT IF EXISTS guests_v2_source_check;
|
|
ALTER TABLE guests_v2 ADD CONSTRAINT guests_v2_source_check
|
|
CHECK (source IN ('google', 'manual', 'self-service'));
|
|
EXCEPTION WHEN OTHERS THEN NULL;
|
|
END $$;
|
|
|
|
|
|
-- =============================================================================
|
|
-- STEP 3c — updated_at triggers
|
|
-- =============================================================================
|
|
CREATE OR REPLACE FUNCTION _update_updated_at()
|
|
RETURNS TRIGGER LANGUAGE plpgsql AS $$
|
|
BEGIN
|
|
NEW.updated_at = CURRENT_TIMESTAMP;
|
|
RETURN NEW;
|
|
END;
|
|
$$;
|
|
|
|
DO $$
|
|
BEGIN
|
|
CREATE TRIGGER trg_guests_v2_updated_at
|
|
BEFORE UPDATE ON guests_v2
|
|
FOR EACH ROW EXECUTE FUNCTION _update_updated_at();
|
|
EXCEPTION WHEN duplicate_object THEN NULL;
|
|
END $$;
|
|
|
|
DO $$
|
|
BEGIN
|
|
CREATE TRIGGER trg_events_updated_at
|
|
BEFORE UPDATE ON events
|
|
FOR EACH ROW EXECUTE FUNCTION _update_updated_at();
|
|
EXCEPTION WHEN duplicate_object THEN NULL;
|
|
END $$;
|
|
|
|
|
|
-- =============================================================================
|
|
-- STEP 4 — Migrate old `guests` rows → `guests_v2`
|
|
--
|
|
-- Conditions:
|
|
-- • The old `guests` table must exist.
|
|
-- • guests_v2 must be EMPTY (idempotent guard — never runs twice).
|
|
--
|
|
-- Strategy:
|
|
-- • For each distinct `owner` in the old table create a row in `users`.
|
|
-- • Create one migration event ("Migrated Wedding") owned by the first user.
|
|
-- • Insert event_members for every owner → that event (role = admin).
|
|
-- • Insert guests mapping:
|
|
-- rsvp_status: 'pending' → 'invited', 'accepted' → 'confirmed', else as-is
|
|
-- phone_number field → phone_number + phone columns
|
|
-- owner → owner_email
|
|
-- source = 'google' (they came from Google import originally)
|
|
-- =============================================================================
|
|
DO $$
|
|
DECLARE
|
|
old_table_exists BOOLEAN;
|
|
new_table_empty BOOLEAN;
|
|
migration_event_id UUID;
|
|
default_user_id UUID;
|
|
owner_row RECORD;
|
|
owner_user_id UUID;
|
|
BEGIN
|
|
-- Check preconditions
|
|
SELECT EXISTS (
|
|
SELECT 1 FROM information_schema.tables
|
|
WHERE table_name = 'guests' AND table_schema = 'public'
|
|
) INTO old_table_exists;
|
|
|
|
SELECT (COUNT(*) = 0) FROM guests_v2 INTO new_table_empty;
|
|
|
|
IF NOT old_table_exists OR NOT new_table_empty THEN
|
|
RAISE NOTICE 'Migration skipped: old_table_exists=%, new_table_empty=%',
|
|
old_table_exists, new_table_empty;
|
|
RETURN;
|
|
END IF;
|
|
|
|
RAISE NOTICE 'Starting data migration from guests → guests_v2 …';
|
|
|
|
-- ── Create one user per distinct owner ──────────────────────────────────
|
|
FOR owner_row IN
|
|
SELECT DISTINCT COALESCE(NULLIF(TRIM(owner), ''), 'imported@invy.app') AS email
|
|
FROM guests
|
|
LOOP
|
|
INSERT INTO users (email)
|
|
VALUES (owner_row.email)
|
|
ON CONFLICT (email) DO NOTHING;
|
|
END LOOP;
|
|
|
|
-- ── Pick (or create) the migration event ────────────────────────────────
|
|
SELECT id INTO migration_event_id FROM events LIMIT 1;
|
|
|
|
IF migration_event_id IS NULL THEN
|
|
INSERT INTO events (name, date, location)
|
|
VALUES ('Migrated Wedding', CURRENT_TIMESTAMP, 'Imported from previous system')
|
|
RETURNING id INTO migration_event_id;
|
|
END IF;
|
|
|
|
-- ── Get a fallback user (the first one alphabetically) ──────────────────
|
|
SELECT id INTO default_user_id FROM users ORDER BY email LIMIT 1;
|
|
|
|
-- ── Create event_members entries for each owner ──────────────────────────
|
|
FOR owner_row IN
|
|
SELECT DISTINCT COALESCE(NULLIF(TRIM(owner), ''), 'imported@invy.app') AS email
|
|
FROM guests
|
|
LOOP
|
|
SELECT id INTO owner_user_id FROM users WHERE email = owner_row.email;
|
|
|
|
INSERT INTO event_members (event_id, user_id, role)
|
|
VALUES (migration_event_id, owner_user_id, 'admin')
|
|
ON CONFLICT (event_id, user_id) DO NOTHING;
|
|
END LOOP;
|
|
|
|
-- ── Copy guests ──────────────────────────────────────────────────────────
|
|
INSERT INTO guests_v2 (
|
|
event_id,
|
|
added_by_user_id,
|
|
first_name,
|
|
last_name,
|
|
email,
|
|
phone_number,
|
|
phone,
|
|
rsvp_status,
|
|
meal_preference,
|
|
has_plus_one,
|
|
plus_one_name,
|
|
table_number,
|
|
owner_email,
|
|
source,
|
|
notes,
|
|
created_at
|
|
)
|
|
SELECT
|
|
migration_event_id,
|
|
COALESCE(
|
|
(SELECT id FROM users WHERE email = NULLIF(TRIM(g.owner), '')),
|
|
default_user_id
|
|
),
|
|
g.first_name,
|
|
COALESCE(g.last_name, ''),
|
|
g.email,
|
|
g.phone_number,
|
|
g.phone_number,
|
|
CASE g.rsvp_status
|
|
WHEN 'accepted' THEN 'confirmed'
|
|
WHEN 'pending' THEN 'invited'
|
|
WHEN 'declined' THEN 'declined'
|
|
ELSE 'invited'
|
|
END,
|
|
g.meal_preference,
|
|
COALESCE(g.has_plus_one, FALSE),
|
|
g.plus_one_name,
|
|
g.table_number::TEXT,
|
|
NULLIF(TRIM(COALESCE(g.owner, '')), ''),
|
|
'google',
|
|
g.notes,
|
|
COALESCE(g.created_at, CURRENT_TIMESTAMP)
|
|
FROM guests g;
|
|
|
|
RAISE NOTICE 'Migration complete. Rows inserted: %', (SELECT COUNT(*) FROM guests_v2);
|
|
END $$;
|
|
|
|
|
|
-- =============================================================================
|
|
-- DONE
|
|
-- =============================================================================
|
|
SELECT
|
|
(SELECT COUNT(*) FROM users) AS users_total,
|
|
(SELECT COUNT(*) FROM events) AS events_total,
|
|
(SELECT COUNT(*) FROM guests_v2) AS guests_v2_total;
|