-- Multi-Event Invitation App Database Schema -- PostgreSQL Migration Script -- Created: 2026-02-23 -- ============================================ -- STEP 1: Enable UUID Extension -- ============================================ CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; -- ============================================ -- STEP 2: Create Users Table -- ============================================ -- Track users who can manage events 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 idx_users_email ON users(email); -- Migrate existing owners from guests table to users (optional, run after creating guests table) -- INSERT INTO users (email) SELECT DISTINCT owner FROM guests WHERE owner IS NOT NULL AND owner != 'self-service' ON CONFLICT DO NOTHING; -- ============================================ -- STEP 3: Create Events Table -- ============================================ -- Store multiple events 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 idx_events_created_at ON events(created_at); -- ============================================ -- STEP 4: Create Event Members Table (Authorization) -- ============================================ -- Track which users are members of which events and their roles 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 idx_event_members_event_id ON event_members(event_id); CREATE INDEX idx_event_members_user_id ON event_members(user_id); CREATE INDEX idx_event_members_event_user ON event_members(event_id, user_id); -- ============================================ -- STEP 5: Create Guests Table (Refactored) -- ============================================ -- Store guest information scoped by event 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), -- Guest Information first_name TEXT NOT NULL, last_name TEXT NOT NULL, email TEXT, phone_number TEXT, -- RSVP & Preferences 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, -- Event Details table_number TEXT, side TEXT, -- e.g. "groom side" / "bride side" / "Dvir side" / "Vered side" -- Source Information owner_email TEXT, -- Email of person who added this guest source TEXT DEFAULT 'manual' CHECK (source IN ('google', 'manual', 'self-service')), -- Notes & Metadata notes TEXT, created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); -- Indexes for performance CREATE INDEX idx_guests_event_id ON guests_v2(event_id); CREATE INDEX idx_guests_added_by_user_id ON guests_v2(added_by_user_id); CREATE INDEX idx_guests_event_user ON guests_v2(event_id, added_by_user_id); CREATE INDEX idx_guests_phone_number ON guests_v2(phone_number); CREATE INDEX idx_guests_event_phone ON guests_v2(event_id, phone_number); CREATE INDEX idx_guests_event_status ON guests_v2(event_id, rsvp_status); CREATE INDEX idx_guests_owner_email ON guests_v2(event_id, owner_email); CREATE INDEX idx_guests_source ON guests_v2(event_id, source); -- Trigger for auto-updating updated_at on guests_v2 CREATE OR REPLACE FUNCTION update_guests_v2_updated_at() RETURNS TRIGGER AS $$ BEGIN NEW.updated_at = CURRENT_TIMESTAMP; RETURN NEW; END; $$ LANGUAGE 'plpgsql'; CREATE TRIGGER update_guests_v2_timestamp BEFORE UPDATE ON guests_v2 FOR EACH ROW EXECUTE FUNCTION update_guests_v2_updated_at(); -- ============================================ -- STEP 6: Migration from Old Schema (Optional) -- ============================================ -- This section is optional and only needed if migrating existing data -- Create migration function to handle existing guests table -- Run this if you have existing data in the old guests table /* DO $$ DECLARE default_event_id UUID; default_user_id UUID; BEGIN -- Create a default event for migration INSERT INTO events (name, date, location) VALUES ('Migrated Wedding', NOW(), 'Unknown') RETURNING id INTO default_event_id; -- Create default user for unmapped owners INSERT INTO users (email) VALUES ('admin@example.com') ON CONFLICT DO NOTHING; SELECT id INTO default_user_id FROM users WHERE email = 'admin@example.com'; -- Migrate data from old guests table to new one INSERT INTO guests_v2 (event_id, added_by_user_id, first_name, last_name, phone, side, status, notes) SELECT default_event_id, COALESCE( (SELECT id FROM users WHERE email = guests.owner), default_user_id ), guests.first_name, guests.last_name, COALESCE(guests.phone_number, ''), NULL, CASE WHEN guests.rsvp_status = 'accepted' THEN 'confirmed' WHEN guests.rsvp_status = 'declined' THEN 'declined' ELSE 'invited' END, COALESCE( 'Meal: ' || guests.meal_preference || '; Plus-one: ' || guests.plus_one_name, notes ) FROM guests; -- Create event_members record for default user INSERT INTO event_members (event_id, user_id, role, display_name) VALUES (default_event_id, default_user_id, 'admin', 'Admin') ON CONFLICT DO NOTHING; END $$; */ -- ============================================ -- STEP 7: ALTER Guests Table (if already exists - add missing columns) -- ============================================ -- Safe migration that handles existing data -- Migration logic using a PL/pgSQL block DO $$ BEGIN -- Check if column exists before creating/renaming -- If rsvp_status doesn't exist, handle the rename or creation IF NOT EXISTS ( SELECT FROM information_schema.columns WHERE table_name = 'guests_v2' AND column_name = 'rsvp_status' ) THEN -- If old status column exists, rename it IF EXISTS ( SELECT FROM information_schema.columns WHERE table_name = 'guests_v2' AND column_name = 'status' ) THEN ALTER TABLE guests_v2 RENAME COLUMN status TO rsvp_status; ELSE -- Add rsvp_status column with default ALTER TABLE guests_v2 ADD COLUMN rsvp_status TEXT DEFAULT 'invited'; END IF; END IF; -- Handle phone_number migration IF NOT EXISTS ( SELECT FROM information_schema.columns WHERE table_name = 'guests_v2' AND column_name = 'phone_number' ) THEN IF EXISTS ( SELECT FROM information_schema.columns WHERE table_name = 'guests_v2' AND column_name = 'phone' ) THEN ALTER TABLE guests_v2 RENAME COLUMN phone TO phone_number; ELSE ALTER TABLE guests_v2 ADD COLUMN phone_number TEXT; END IF; END IF; -- Add other missing columns IF NOT EXISTS ( SELECT FROM information_schema.columns WHERE table_name = 'guests_v2' AND column_name = 'email' ) THEN ALTER TABLE guests_v2 ADD COLUMN email TEXT; END IF; IF NOT EXISTS ( SELECT FROM information_schema.columns WHERE table_name = 'guests_v2' AND column_name = 'meal_preference' ) THEN ALTER TABLE guests_v2 ADD COLUMN meal_preference TEXT; END IF; IF NOT EXISTS ( SELECT FROM information_schema.columns WHERE table_name = 'guests_v2' AND column_name = 'has_plus_one' ) THEN ALTER TABLE guests_v2 ADD COLUMN has_plus_one BOOLEAN DEFAULT FALSE; END IF; IF NOT EXISTS ( SELECT FROM information_schema.columns WHERE table_name = 'guests_v2' AND column_name = 'plus_one_name' ) THEN ALTER TABLE guests_v2 ADD COLUMN plus_one_name TEXT; END IF; IF NOT EXISTS ( SELECT FROM information_schema.columns WHERE table_name = 'guests_v2' AND column_name = 'table_number' ) THEN ALTER TABLE guests_v2 ADD COLUMN table_number TEXT; END IF; IF NOT EXISTS ( SELECT FROM information_schema.columns WHERE table_name = 'guests_v2' AND column_name = 'owner_email' ) THEN ALTER TABLE guests_v2 ADD COLUMN owner_email TEXT; END IF; IF NOT EXISTS ( SELECT FROM information_schema.columns WHERE table_name = 'guests_v2' AND column_name = 'source' ) THEN ALTER TABLE guests_v2 ADD COLUMN source TEXT DEFAULT 'manual'; END IF; RAISE NOTICE 'Migration completed successfully'; END $$; -- Update CHECK constraint for rsvp_status if needed DO $$ BEGIN -- Drop old constraint if it exists ALTER TABLE guests_v2 DROP CONSTRAINT IF EXISTS guests_v2_status_check; -- Add new constraint ALTER TABLE guests_v2 ADD CONSTRAINT guests_v2_rsvp_status_check CHECK (rsvp_status IN ('invited', 'confirmed', 'declined')); EXCEPTION WHEN OTHERS THEN -- Constraint might already exist, that's okay NULL; END $$; -- Add source constraint DO $$ BEGIN ALTER TABLE guests_v2 ADD CONSTRAINT guests_v2_source_check CHECK (source IN ('google', 'manual', 'self-service')); EXCEPTION WHEN OTHERS THEN NULL; END $$; -- Add missing indexes if they don't exist CREATE INDEX IF NOT EXISTS idx_guests_owner_email ON guests_v2(event_id, owner_email); CREATE INDEX IF NOT EXISTS idx_guests_source ON guests_v2(event_id, source); CREATE INDEX IF NOT EXISTS idx_guests_phone_number ON guests_v2(phone_number); CREATE INDEX IF NOT EXISTS idx_guests_event_phone_new ON guests_v2(event_id, phone_number); -- ============================================ -- STEP 15: Add WhatsApp Template Fields (Migration) -- ============================================ -- Add WhatsApp-related columns to events table for wedding invitation template DO $$ BEGIN ALTER TABLE events ADD COLUMN partner1_name TEXT; EXCEPTION WHEN OTHERS THEN NULL; END $$; DO $$ BEGIN ALTER TABLE events ADD COLUMN partner2_name TEXT; EXCEPTION WHEN OTHERS THEN NULL; END $$; DO $$ BEGIN ALTER TABLE events ADD COLUMN venue TEXT; EXCEPTION WHEN OTHERS THEN NULL; END $$; DO $$ BEGIN ALTER TABLE events ADD COLUMN event_time TEXT; EXCEPTION WHEN OTHERS THEN NULL; END $$; DO $$ BEGIN ALTER TABLE events ADD COLUMN guest_link TEXT; EXCEPTION WHEN OTHERS THEN NULL; END $$; -- Create index for query efficiency CREATE INDEX IF NOT EXISTS idx_events_guest_link ON events(guest_link);