CREATE EXTENSION IF NOT EXISTS pgcrypto; CREATE TABLE IF NOT EXISTS users ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), email TEXT NOT NULL, password_hash TEXT NOT NULL, email_verified_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ ); CREATE UNIQUE INDEX IF NOT EXISTS users_email_normalized_key ON users (LOWER(BTRIM(email))); CREATE TABLE IF NOT EXISTS sessions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, token_hash TEXT NOT NULL UNIQUE, csrf_token_hash TEXT NOT NULL, expires_at TIMESTAMPTZ NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), last_seen_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), revoked_at TIMESTAMPTZ ); CREATE INDEX IF NOT EXISTS sessions_user_id_idx ON sessions(user_id); CREATE INDEX IF NOT EXISTS sessions_expires_at_idx ON sessions(expires_at); CREATE TABLE IF NOT EXISTS email_verification_tokens ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, token_hash TEXT NOT NULL UNIQUE, expires_at TIMESTAMPTZ NOT NULL, used_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS email_verification_tokens_user_id_idx ON email_verification_tokens(user_id); CREATE TABLE IF NOT EXISTS password_reset_tokens ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, token_hash TEXT NOT NULL UNIQUE, expires_at TIMESTAMPTZ NOT NULL, used_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS password_reset_tokens_user_id_idx ON password_reset_tokens(user_id); CREATE TABLE IF NOT EXISTS app_settings ( key TEXT PRIMARY KEY, value TEXT NOT NULL, updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); DO $$ DECLARE id_type TEXT; pk_name TEXT; BEGIN SELECT data_type INTO id_type FROM information_schema.columns WHERE table_name = 'races' AND column_name = 'id'; IF id_type IS NOT NULL AND id_type <> 'uuid' THEN SELECT conname INTO pk_name FROM pg_constraint WHERE conrelid = 'races'::regclass AND contype = 'p' LIMIT 1; IF pk_name IS NOT NULL THEN EXECUTE format('ALTER TABLE races DROP CONSTRAINT %I', pk_name); END IF; IF NOT EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_name = 'races' AND column_name = 'slug' ) THEN ALTER TABLE races RENAME COLUMN id TO slug; ELSE ALTER TABLE races DROP COLUMN id; END IF; END IF; END $$; ALTER TABLE races ADD COLUMN IF NOT EXISTS id UUID DEFAULT gen_random_uuid(); UPDATE races SET id = gen_random_uuid() WHERE id IS NULL; ALTER TABLE races ALTER COLUMN id SET NOT NULL; DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_constraint WHERE conrelid = 'races'::regclass AND contype = 'p' ) THEN ALTER TABLE races ADD CONSTRAINT races_pkey PRIMARY KEY (id); END IF; END $$; ALTER TABLE races ADD COLUMN IF NOT EXISTS slug TEXT; UPDATE races SET slug = id::text WHERE slug IS NULL OR BTRIM(slug) = ''; ALTER TABLE races ALTER COLUMN slug SET NOT NULL; ALTER TABLE races ADD COLUMN IF NOT EXISTS owner_user_id UUID REFERENCES users(id) ON DELETE CASCADE; ALTER TABLE races ADD COLUMN IF NOT EXISTS source TEXT NOT NULL DEFAULT 'user'; CREATE UNIQUE INDEX IF NOT EXISTS races_owner_slug_key ON races(owner_user_id, slug) WHERE owner_user_id IS NOT NULL; CREATE INDEX IF NOT EXISTS races_owner_user_id_idx ON races(owner_user_id);