-- ========================================== -- Chat Switchboard — 013 Files -- ========================================== -- Unified files table replacing attachments. -- Handles both upgrade (attachments exists) and fresh install. -- ========================================== CREATE TABLE IF NOT EXISTS files ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), channel_id UUID REFERENCES channels(id) ON DELETE CASCADE, message_id UUID REFERENCES messages(id) ON DELETE SET NULL, user_id UUID REFERENCES users(id) ON DELETE SET NULL, project_id UUID REFERENCES projects(id) ON DELETE SET NULL, origin VARCHAR(20) NOT NULL DEFAULT 'user_upload' CHECK (origin IN ('user_upload', 'tool_output', 'system')), filename VARCHAR(255) NOT NULL, content_type VARCHAR(127) NOT NULL DEFAULT 'application/octet-stream', size_bytes BIGINT NOT NULL DEFAULT 0, storage_key TEXT NOT NULL, display_hint VARCHAR(20) NOT NULL DEFAULT 'download' CHECK (display_hint IN ('inline', 'download', 'thumbnail')), extracted_text TEXT, metadata JSONB DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); -- Ensure attachments table exists so the INSERT is always valid. -- Upgrade: no-op (table already has data). Fresh install: empty table. CREATE TABLE IF NOT EXISTS attachments ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), channel_id UUID, user_id UUID, message_id UUID, project_id UUID, filename VARCHAR(255), content_type VARCHAR(127), size_bytes BIGINT, storage_key TEXT, extracted_text TEXT, metadata JSONB, created_at TIMESTAMPTZ ); INSERT INTO files ( id, channel_id, message_id, user_id, project_id, origin, filename, content_type, size_bytes, storage_key, display_hint, extracted_text, metadata, created_at, updated_at ) SELECT id, channel_id, message_id, user_id, project_id, 'user_upload', filename, content_type, size_bytes, storage_key, CASE WHEN content_type LIKE 'image/%' THEN 'inline' WHEN content_type LIKE 'video/%' THEN 'inline' WHEN content_type LIKE 'audio/%' THEN 'inline' WHEN content_type = 'application/pdf' THEN 'thumbnail' WHEN content_type LIKE 'text/%' THEN 'inline' ELSE 'download' END, extracted_text, metadata, created_at, created_at FROM attachments ON CONFLICT (id) DO NOTHING; DROP TABLE IF EXISTS attachments; CREATE INDEX IF NOT EXISTS idx_files_channel ON files(channel_id); CREATE INDEX IF NOT EXISTS idx_files_message ON files(message_id); CREATE INDEX IF NOT EXISTS idx_files_user ON files(user_id); CREATE INDEX IF NOT EXISTS idx_files_project ON files(project_id) WHERE project_id IS NOT NULL; CREATE INDEX IF NOT EXISTS idx_files_origin ON files(origin); CREATE INDEX IF NOT EXISTS idx_files_orphan ON files(created_at) WHERE message_id IS NULL AND origin = 'user_upload';