CREATE TABLE IF NOT EXISTS users ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), email TEXT NOT NULL UNIQUE, password_hash TEXT NOT NULL, name TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE UNIQUE INDEX IF NOT EXISTS users_email_lower_idx ON users (LOWER(email)); CREATE TABLE IF NOT EXISTS galleries ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, slug VARCHAR(160) NOT NULL UNIQUE, title TEXT NOT NULL, client_name TEXT NOT NULL, description TEXT NOT NULL DEFAULT '', status TEXT NOT NULL DEFAULT 'draft', password_hash TEXT, downloads_enabled BOOLEAN NOT NULL DEFAULT TRUE, favorites_enabled BOOLEAN NOT NULL DEFAULT TRUE, download_all_enabled BOOLEAN NOT NULL DEFAULT TRUE, watermark_enabled BOOLEAN NOT NULL DEFAULT FALSE, expires_at TIMESTAMPTZ, cover_media_id UUID, theme_config JSONB NOT NULL DEFAULT '{}'::jsonb, branding_config JSONB NOT NULL DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), published_at TIMESTAMPTZ, CONSTRAINT galleries_status_check CHECK (status IN ('draft', 'published', 'archived')) ); CREATE INDEX IF NOT EXISTS galleries_user_id_created_at_idx ON galleries (user_id, created_at DESC); CREATE TABLE IF NOT EXISTS media ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), gallery_id UUID NOT NULL REFERENCES galleries(id) ON DELETE CASCADE, original_filename TEXT NOT NULL, mime_type TEXT NOT NULL, file_size BIGINT NOT NULL DEFAULT 0, storage_key TEXT NOT NULL, external_url TEXT, thumbnail_key TEXT, preview_key TEXT, processing_status TEXT NOT NULL DEFAULT 'UPLOADING', processing_error TEXT, width INTEGER, height INTEGER, duration_seconds DOUBLE PRECISION, sort_order INTEGER NOT NULL DEFAULT 0, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT media_processing_status_check CHECK (processing_status IN ('UPLOADING', 'PROCESSING', 'READY', 'FAILED')) ); CREATE INDEX IF NOT EXISTS media_gallery_sort_order_idx ON media (gallery_id, sort_order, id); CREATE TABLE IF NOT EXISTS favorites ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), gallery_id UUID NOT NULL REFERENCES galleries(id) ON DELETE CASCADE, media_id UUID NOT NULL REFERENCES media(id) ON DELETE CASCADE, visitor_id TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE (gallery_id, media_id, visitor_id) ); CREATE TABLE IF NOT EXISTS downloads ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), gallery_id UUID NOT NULL REFERENCES galleries(id) ON DELETE CASCADE, media_id UUID REFERENCES media(id) ON DELETE SET NULL, visitor_id TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS downloads_gallery_created_at_idx ON downloads (gallery_id, created_at DESC); CREATE TABLE IF NOT EXISTS download_jobs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), gallery_id UUID NOT NULL REFERENCES galleries(id) ON DELETE CASCADE, visitor_id TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'QUEUED', storage_key TEXT, error TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), completed_at TIMESTAMPTZ, CONSTRAINT download_jobs_status_check CHECK (status IN ('QUEUED', 'PROCESSING', 'READY', 'FAILED')) ); CREATE INDEX IF NOT EXISTS download_jobs_gallery_visitor_idx ON download_jobs (gallery_id, visitor_id, created_at DESC);