-- ============================================================ -- Migration: 002_triggers.sql -- Automated: updated_at, deleted_at, event recording, audit, security, stats -- -- Usage: -- Application sets session variable before writes: -- SET LOCAL app.current_user_id = '00000000-0000-0000-0000-000000000001'; -- Triggers read it for actor_id in events / audit / security logs. -- ============================================================ -- ============================================================ -- 1. set_updated_at — auto-refresh updated_at on any UPDATE -- Applied to ALL tables with an updated_at column. -- ============================================================ CREATE OR REPLACE FUNCTION set_updated_at() RETURNS TRIGGER AS $$ BEGIN NEW.updated_at = NOW(); RETURN NEW; END; $$ LANGUAGE plpgsql; -- Tables with updated_at (generated from models/) DROP TRIGGER IF EXISTS trg_agent_updated_at ON agent; CREATE TRIGGER trg_agent_updated_at BEFORE UPDATE ON agent FOR EACH ROW EXECUTE FUNCTION set_updated_at(); DROP TRIGGER IF EXISTS trg_agent_event_subscription_updated_at ON agent_event_subscription; CREATE TRIGGER trg_agent_event_subscription_updated_at BEFORE UPDATE ON agent_event_subscription FOR EACH ROW EXECUTE FUNCTION set_updated_at(); DROP TRIGGER IF EXISTS trg_agent_execution_updated_at ON agent_execution; CREATE TRIGGER trg_agent_execution_updated_at BEFORE UPDATE ON agent_execution FOR EACH ROW EXECUTE FUNCTION set_updated_at(); DROP TRIGGER IF EXISTS trg_agent_feedback_updated_at ON agent_feedback; CREATE TRIGGER trg_agent_feedback_updated_at BEFORE UPDATE ON agent_feedback FOR EACH ROW EXECUTE FUNCTION set_updated_at(); DROP TRIGGER IF EXISTS trg_agent_schedule_updated_at ON agent_schedule; CREATE TRIGGER trg_agent_schedule_updated_at BEFORE UPDATE ON agent_schedule FOR EACH ROW EXECUTE FUNCTION set_updated_at(); DROP TRIGGER IF EXISTS trg_agent_version_updated_at ON agent_version; CREATE TRIGGER trg_agent_version_updated_at BEFORE UPDATE ON agent_version FOR EACH ROW EXECUTE FUNCTION set_updated_at(); DROP TRIGGER IF EXISTS trg_agent_workspace_binding_updated_at ON agent_workspace_binding; CREATE TRIGGER trg_agent_workspace_binding_updated_at BEFORE UPDATE ON agent_workspace_binding FOR EACH ROW EXECUTE FUNCTION set_updated_at(); DROP TRIGGER IF EXISTS trg_ai_model_updated_at ON ai_model; CREATE TRIGGER trg_ai_model_updated_at BEFORE UPDATE ON ai_model FOR EACH ROW EXECUTE FUNCTION set_updated_at(); DROP TRIGGER IF EXISTS trg_ai_model_capability_updated_at ON ai_model_capability; CREATE TRIGGER trg_ai_model_capability_updated_at BEFORE UPDATE ON ai_model_capability FOR EACH ROW EXECUTE FUNCTION set_updated_at(); DROP TRIGGER IF EXISTS trg_ai_model_version_updated_at ON ai_model_version; CREATE TRIGGER trg_ai_model_version_updated_at BEFORE UPDATE ON ai_model_version FOR EACH ROW EXECUTE FUNCTION set_updated_at(); DROP TRIGGER IF EXISTS trg_channel_updated_at ON channel; CREATE TRIGGER trg_channel_updated_at BEFORE UPDATE ON channel FOR EACH ROW EXECUTE FUNCTION set_updated_at(); DROP TRIGGER IF EXISTS trg_channel_member_updated_at ON channel_member; CREATE TRIGGER trg_channel_member_updated_at BEFORE UPDATE ON channel_member FOR EACH ROW EXECUTE FUNCTION set_updated_at(); DROP TRIGGER IF EXISTS trg_channel_member_role_updated_at ON channel_member_role; CREATE TRIGGER trg_channel_member_role_updated_at BEFORE UPDATE ON channel_member_role FOR EACH ROW EXECUTE FUNCTION set_updated_at(); DROP TRIGGER IF EXISTS trg_channel_repo_link_updated_at ON channel_repo_link; CREATE TRIGGER trg_channel_repo_link_updated_at BEFORE UPDATE ON channel_repo_link FOR EACH ROW EXECUTE FUNCTION set_updated_at(); DROP TRIGGER IF EXISTS trg_channel_slash_command_updated_at ON channel_slash_command; CREATE TRIGGER trg_channel_slash_command_updated_at BEFORE UPDATE ON channel_slash_command FOR EACH ROW EXECUTE FUNCTION set_updated_at(); DROP TRIGGER IF EXISTS trg_channel_stats_updated_at ON channel_stats; CREATE TRIGGER trg_channel_stats_updated_at BEFORE UPDATE ON channel_stats FOR EACH ROW EXECUTE FUNCTION set_updated_at(); DROP TRIGGER IF EXISTS trg_channel_webhook_updated_at ON channel_webhook; CREATE TRIGGER trg_channel_webhook_updated_at BEFORE UPDATE ON channel_webhook FOR EACH ROW EXECUTE FUNCTION set_updated_at(); DROP TRIGGER IF EXISTS trg_conversation_updated_at ON conversation; CREATE TRIGGER trg_conversation_updated_at BEFORE UPDATE ON conversation FOR EACH ROW EXECUTE FUNCTION set_updated_at(); DROP TRIGGER IF EXISTS trg_conversation_bookmark_updated_at ON conversation_bookmark; CREATE TRIGGER trg_conversation_bookmark_updated_at BEFORE UPDATE ON conversation_bookmark FOR EACH ROW EXECUTE FUNCTION set_updated_at(); DROP TRIGGER IF EXISTS trg_conversation_message_updated_at ON conversation_message; CREATE TRIGGER trg_conversation_message_updated_at BEFORE UPDATE ON conversation_message FOR EACH ROW EXECUTE FUNCTION set_updated_at(); DROP TRIGGER IF EXISTS trg_conversation_participant_updated_at ON conversation_participant; CREATE TRIGGER trg_conversation_participant_updated_at BEFORE UPDATE ON conversation_participant FOR EACH ROW EXECUTE FUNCTION set_updated_at(); DROP TRIGGER IF EXISTS trg_conversation_summary_updated_at ON conversation_summary; CREATE TRIGGER trg_conversation_summary_updated_at BEFORE UPDATE ON conversation_summary FOR EACH ROW EXECUTE FUNCTION set_updated_at(); DROP TRIGGER IF EXISTS trg_conversation_tool_call_updated_at ON conversation_tool_call; CREATE TRIGGER trg_conversation_tool_call_updated_at BEFORE UPDATE ON conversation_tool_call FOR EACH ROW EXECUTE FUNCTION set_updated_at(); DROP TRIGGER IF EXISTS trg_issue_updated_at ON issue; CREATE TRIGGER trg_issue_updated_at BEFORE UPDATE ON issue FOR EACH ROW EXECUTE FUNCTION set_updated_at(); DROP TRIGGER IF EXISTS trg_issue_comment_updated_at ON issue_comment; CREATE TRIGGER trg_issue_comment_updated_at BEFORE UPDATE ON issue_comment FOR EACH ROW EXECUTE FUNCTION set_updated_at(); DROP TRIGGER IF EXISTS trg_issue_label_updated_at ON issue_label; CREATE TRIGGER trg_issue_label_updated_at BEFORE UPDATE ON issue_label FOR EACH ROW EXECUTE FUNCTION set_updated_at(); DROP TRIGGER IF EXISTS trg_issue_milestone_updated_at ON issue_milestone; CREATE TRIGGER trg_issue_milestone_updated_at BEFORE UPDATE ON issue_milestone FOR EACH ROW EXECUTE FUNCTION set_updated_at(); DROP TRIGGER IF EXISTS trg_issue_reminder_updated_at ON issue_reminder; CREATE TRIGGER trg_issue_reminder_updated_at BEFORE UPDATE ON issue_reminder FOR EACH ROW EXECUTE FUNCTION set_updated_at(); DROP TRIGGER IF EXISTS trg_issue_stats_updated_at ON issue_stats; CREATE TRIGGER trg_issue_stats_updated_at BEFORE UPDATE ON issue_stats FOR EACH ROW EXECUTE FUNCTION set_updated_at(); DROP TRIGGER IF EXISTS trg_issue_subscriber_updated_at ON issue_subscriber; CREATE TRIGGER trg_issue_subscriber_updated_at BEFORE UPDATE ON issue_subscriber FOR EACH ROW EXECUTE FUNCTION set_updated_at(); DROP TRIGGER IF EXISTS trg_issue_template_updated_at ON issue_template; CREATE TRIGGER trg_issue_template_updated_at BEFORE UPDATE ON issue_template FOR EACH ROW EXECUTE FUNCTION set_updated_at(); DROP TRIGGER IF EXISTS trg_message_updated_at ON message; CREATE TRIGGER trg_message_updated_at BEFORE UPDATE ON message FOR EACH ROW EXECUTE FUNCTION set_updated_at(); DROP TRIGGER IF EXISTS trg_message_bookmark_updated_at ON message_bookmark; CREATE TRIGGER trg_message_bookmark_updated_at BEFORE UPDATE ON message_bookmark FOR EACH ROW EXECUTE FUNCTION set_updated_at(); DROP TRIGGER IF EXISTS trg_message_thread_updated_at ON message_thread; CREATE TRIGGER trg_message_thread_updated_at BEFORE UPDATE ON message_thread FOR EACH ROW EXECUTE FUNCTION set_updated_at(); DROP TRIGGER IF EXISTS trg_notification_updated_at ON notification; CREATE TRIGGER trg_notification_updated_at BEFORE UPDATE ON notification FOR EACH ROW EXECUTE FUNCTION set_updated_at(); DROP TRIGGER IF EXISTS trg_notification_block_updated_at ON notification_block; CREATE TRIGGER trg_notification_block_updated_at BEFORE UPDATE ON notification_block FOR EACH ROW EXECUTE FUNCTION set_updated_at(); DROP TRIGGER IF EXISTS trg_notification_delivery_updated_at ON notification_delivery; CREATE TRIGGER trg_notification_delivery_updated_at BEFORE UPDATE ON notification_delivery FOR EACH ROW EXECUTE FUNCTION set_updated_at(); DROP TRIGGER IF EXISTS trg_notification_subscription_updated_at ON notification_subscription; CREATE TRIGGER trg_notification_subscription_updated_at BEFORE UPDATE ON notification_subscription FOR EACH ROW EXECUTE FUNCTION set_updated_at(); DROP TRIGGER IF EXISTS trg_notification_template_updated_at ON notification_template; CREATE TRIGGER trg_notification_template_updated_at BEFORE UPDATE ON notification_template FOR EACH ROW EXECUTE FUNCTION set_updated_at(); DROP TRIGGER IF EXISTS trg_pr_check_run_updated_at ON pr_check_run; CREATE TRIGGER trg_pr_check_run_updated_at BEFORE UPDATE ON pr_check_run FOR EACH ROW EXECUTE FUNCTION set_updated_at(); DROP TRIGGER IF EXISTS trg_pr_file_updated_at ON pr_file; CREATE TRIGGER trg_pr_file_updated_at BEFORE UPDATE ON pr_file FOR EACH ROW EXECUTE FUNCTION set_updated_at(); DROP TRIGGER IF EXISTS trg_pr_label_updated_at ON pr_label; CREATE TRIGGER trg_pr_label_updated_at BEFORE UPDATE ON pr_label FOR EACH ROW EXECUTE FUNCTION set_updated_at(); DROP TRIGGER IF EXISTS trg_pr_merge_strategy_updated_at ON pr_merge_strategy; CREATE TRIGGER trg_pr_merge_strategy_updated_at BEFORE UPDATE ON pr_merge_strategy FOR EACH ROW EXECUTE FUNCTION set_updated_at(); DROP TRIGGER IF EXISTS trg_pr_status_updated_at ON pr_status; CREATE TRIGGER trg_pr_status_updated_at BEFORE UPDATE ON pr_status FOR EACH ROW EXECUTE FUNCTION set_updated_at(); DROP TRIGGER IF EXISTS trg_pr_subscription_updated_at ON pr_subscription; CREATE TRIGGER trg_pr_subscription_updated_at BEFORE UPDATE ON pr_subscription FOR EACH ROW EXECUTE FUNCTION set_updated_at(); DROP TRIGGER IF EXISTS trg_pull_request_updated_at ON pull_request; CREATE TRIGGER trg_pull_request_updated_at BEFORE UPDATE ON pull_request FOR EACH ROW EXECUTE FUNCTION set_updated_at(); DROP TRIGGER IF EXISTS trg_repo_updated_at ON repo; CREATE TRIGGER trg_repo_updated_at BEFORE UPDATE ON repo FOR EACH ROW EXECUTE FUNCTION set_updated_at(); DROP TRIGGER IF EXISTS trg_repo_branch_updated_at ON repo_branch; CREATE TRIGGER trg_repo_branch_updated_at BEFORE UPDATE ON repo_branch FOR EACH ROW EXECUTE FUNCTION set_updated_at(); DROP TRIGGER IF EXISTS trg_repo_commit_comment_updated_at ON repo_commit_comment; CREATE TRIGGER trg_repo_commit_comment_updated_at BEFORE UPDATE ON repo_commit_comment FOR EACH ROW EXECUTE FUNCTION set_updated_at(); DROP TRIGGER IF EXISTS trg_repo_commit_status_updated_at ON repo_commit_status; CREATE TRIGGER trg_repo_commit_status_updated_at BEFORE UPDATE ON repo_commit_status FOR EACH ROW EXECUTE FUNCTION set_updated_at(); DROP TRIGGER IF EXISTS trg_repo_deploy_key_updated_at ON repo_deploy_key; CREATE TRIGGER trg_repo_deploy_key_updated_at BEFORE UPDATE ON repo_deploy_key FOR EACH ROW EXECUTE FUNCTION set_updated_at(); DROP TRIGGER IF EXISTS trg_repo_member_updated_at ON repo_member; CREATE TRIGGER trg_repo_member_updated_at BEFORE UPDATE ON repo_member FOR EACH ROW EXECUTE FUNCTION set_updated_at(); DROP TRIGGER IF EXISTS trg_repo_push_lock_updated_at ON repo_push_lock; CREATE TRIGGER trg_repo_push_lock_updated_at BEFORE UPDATE ON repo_push_lock FOR EACH ROW EXECUTE FUNCTION set_updated_at(); DROP TRIGGER IF EXISTS trg_repo_release_updated_at ON repo_release; CREATE TRIGGER trg_repo_release_updated_at BEFORE UPDATE ON repo_release FOR EACH ROW EXECUTE FUNCTION set_updated_at(); DROP TRIGGER IF EXISTS trg_repo_stats_updated_at ON repo_stats; CREATE TRIGGER trg_repo_stats_updated_at BEFORE UPDATE ON repo_stats FOR EACH ROW EXECUTE FUNCTION set_updated_at(); DROP TRIGGER IF EXISTS trg_repo_watch_updated_at ON repo_watch; CREATE TRIGGER trg_repo_watch_updated_at BEFORE UPDATE ON repo_watch FOR EACH ROW EXECUTE FUNCTION set_updated_at(); DROP TRIGGER IF EXISTS trg_repo_webhook_updated_at ON repo_webhook; CREATE TRIGGER trg_repo_webhook_updated_at BEFORE UPDATE ON repo_webhook FOR EACH ROW EXECUTE FUNCTION set_updated_at(); DROP TRIGGER IF EXISTS trg_user_updated_at ON "user"; CREATE TRIGGER trg_user_updated_at BEFORE UPDATE ON "user" FOR EACH ROW EXECUTE FUNCTION set_updated_at(); DROP TRIGGER IF EXISTS trg_user_appearance_updated_at ON user_appearance; CREATE TRIGGER trg_user_appearance_updated_at BEFORE UPDATE ON user_appearance FOR EACH ROW EXECUTE FUNCTION set_updated_at(); DROP TRIGGER IF EXISTS trg_user_device_updated_at ON user_device; CREATE TRIGGER trg_user_device_updated_at BEFORE UPDATE ON user_device FOR EACH ROW EXECUTE FUNCTION set_updated_at(); DROP TRIGGER IF EXISTS trg_user_gpg_key_updated_at ON user_gpg_key; CREATE TRIGGER trg_user_gpg_key_updated_at BEFORE UPDATE ON user_gpg_key FOR EACH ROW EXECUTE FUNCTION set_updated_at(); DROP TRIGGER IF EXISTS trg_user_mail_updated_at ON user_mail; CREATE TRIGGER trg_user_mail_updated_at BEFORE UPDATE ON user_mail FOR EACH ROW EXECUTE FUNCTION set_updated_at(); DROP TRIGGER IF EXISTS trg_user_notify_setting_updated_at ON user_notify_setting; CREATE TRIGGER trg_user_notify_setting_updated_at BEFORE UPDATE ON user_notify_setting FOR EACH ROW EXECUTE FUNCTION set_updated_at(); DROP TRIGGER IF EXISTS trg_user_password_updated_at ON user_password; CREATE TRIGGER trg_user_password_updated_at BEFORE UPDATE ON user_password FOR EACH ROW EXECUTE FUNCTION set_updated_at(); DROP TRIGGER IF EXISTS trg_user_personal_access_token_updated_at ON user_personal_access_token; CREATE TRIGGER trg_user_personal_access_token_updated_at BEFORE UPDATE ON user_personal_access_token FOR EACH ROW EXECUTE FUNCTION set_updated_at(); DROP TRIGGER IF EXISTS trg_user_profile_updated_at ON user_profile; CREATE TRIGGER trg_user_profile_updated_at BEFORE UPDATE ON user_profile FOR EACH ROW EXECUTE FUNCTION set_updated_at(); DROP TRIGGER IF EXISTS trg_user_ssh_key_updated_at ON user_ssh_key; CREATE TRIGGER trg_user_ssh_key_updated_at BEFORE UPDATE ON user_ssh_key FOR EACH ROW EXECUTE FUNCTION set_updated_at(); DROP TRIGGER IF EXISTS trg_workspace_updated_at ON workspace; CREATE TRIGGER trg_workspace_updated_at BEFORE UPDATE ON workspace FOR EACH ROW EXECUTE FUNCTION set_updated_at(); DROP TRIGGER IF EXISTS trg_workspace_billing_updated_at ON workspace_billing; CREATE TRIGGER trg_workspace_billing_updated_at BEFORE UPDATE ON workspace_billing FOR EACH ROW EXECUTE FUNCTION set_updated_at(); DROP TRIGGER IF EXISTS trg_workspace_custom_branding_updated_at ON workspace_custom_branding; CREATE TRIGGER trg_workspace_custom_branding_updated_at BEFORE UPDATE ON workspace_custom_branding FOR EACH ROW EXECUTE FUNCTION set_updated_at(); DROP TRIGGER IF EXISTS trg_workspace_domain_updated_at ON workspace_domain; CREATE TRIGGER trg_workspace_domain_updated_at BEFORE UPDATE ON workspace_domain FOR EACH ROW EXECUTE FUNCTION set_updated_at(); DROP TRIGGER IF EXISTS trg_workspace_integration_updated_at ON workspace_integration; CREATE TRIGGER trg_workspace_integration_updated_at BEFORE UPDATE ON workspace_integration FOR EACH ROW EXECUTE FUNCTION set_updated_at(); DROP TRIGGER IF EXISTS trg_workspace_member_updated_at ON workspace_member; CREATE TRIGGER trg_workspace_member_updated_at BEFORE UPDATE ON workspace_member FOR EACH ROW EXECUTE FUNCTION set_updated_at(); DROP TRIGGER IF EXISTS trg_workspace_pending_approval_updated_at ON workspace_pending_approval; CREATE TRIGGER trg_workspace_pending_approval_updated_at BEFORE UPDATE ON workspace_pending_approval FOR EACH ROW EXECUTE FUNCTION set_updated_at(); DROP TRIGGER IF EXISTS trg_workspace_settings_updated_at ON workspace_settings; CREATE TRIGGER trg_workspace_settings_updated_at BEFORE UPDATE ON workspace_settings FOR EACH ROW EXECUTE FUNCTION set_updated_at(); DROP TRIGGER IF EXISTS trg_workspace_stats_updated_at ON workspace_stats; CREATE TRIGGER trg_workspace_stats_updated_at BEFORE UPDATE ON workspace_stats FOR EACH ROW EXECUTE FUNCTION set_updated_at(); DROP TRIGGER IF EXISTS trg_workspace_webhook_updated_at ON workspace_webhook; CREATE TRIGGER trg_workspace_webhook_updated_at BEFORE UPDATE ON workspace_webhook FOR EACH ROW EXECUTE FUNCTION set_updated_at(); -- ============================================================ -- 2. set_deleted_at — auto-timestamp soft delete -- Fires when deleted_at transitions NULL → NOT NULL. -- Works for ANY table with a deleted_at column (no status dependency). -- ============================================================ CREATE OR REPLACE FUNCTION set_deleted_at() RETURNS TRIGGER AS $$ BEGIN IF NEW.deleted_at IS NOT NULL AND OLD.deleted_at IS NULL THEN NEW.deleted_at = NOW(); END IF; RETURN NEW; END; $$ LANGUAGE plpgsql; -- Only tables that contain BOTH status AND deleted_at columns -- Verified from models: user, repo, workspace, issue, pull_request, channel, -- conversation, conversation_message, agent, message, issue_comment, -- repo_commit_comment, repo_release, notification, ai_model, conversation_attachment DROP TRIGGER IF EXISTS trg_user_deleted_at ON "user"; CREATE TRIGGER trg_user_deleted_at BEFORE UPDATE ON "user" FOR EACH ROW EXECUTE FUNCTION set_deleted_at(); DROP TRIGGER IF EXISTS trg_repo_deleted_at ON repo; CREATE TRIGGER trg_repo_deleted_at BEFORE UPDATE ON repo FOR EACH ROW EXECUTE FUNCTION set_deleted_at(); DROP TRIGGER IF EXISTS trg_workspace_deleted_at ON workspace; CREATE TRIGGER trg_workspace_deleted_at BEFORE UPDATE ON workspace FOR EACH ROW EXECUTE FUNCTION set_deleted_at(); DROP TRIGGER IF EXISTS trg_issue_deleted_at ON issue; CREATE TRIGGER trg_issue_deleted_at BEFORE UPDATE ON issue FOR EACH ROW EXECUTE FUNCTION set_deleted_at(); DROP TRIGGER IF EXISTS trg_pull_request_deleted_at ON pull_request; CREATE TRIGGER trg_pull_request_deleted_at BEFORE UPDATE ON pull_request FOR EACH ROW EXECUTE FUNCTION set_deleted_at(); DROP TRIGGER IF EXISTS trg_channel_deleted_at ON channel; CREATE TRIGGER trg_channel_deleted_at BEFORE UPDATE ON channel FOR EACH ROW EXECUTE FUNCTION set_deleted_at(); DROP TRIGGER IF EXISTS trg_conversation_deleted_at ON conversation; CREATE TRIGGER trg_conversation_deleted_at BEFORE UPDATE ON conversation FOR EACH ROW EXECUTE FUNCTION set_deleted_at(); DROP TRIGGER IF EXISTS trg_conversation_message_deleted_at ON conversation_message; CREATE TRIGGER trg_conversation_message_deleted_at BEFORE UPDATE ON conversation_message FOR EACH ROW EXECUTE FUNCTION set_deleted_at(); DROP TRIGGER IF EXISTS trg_agent_deleted_at ON agent; CREATE TRIGGER trg_agent_deleted_at BEFORE UPDATE ON agent FOR EACH ROW EXECUTE FUNCTION set_deleted_at(); DROP TRIGGER IF EXISTS trg_message_deleted_at ON message; CREATE TRIGGER trg_message_deleted_at BEFORE UPDATE ON message FOR EACH ROW EXECUTE FUNCTION set_deleted_at(); DROP TRIGGER IF EXISTS trg_issue_comment_deleted_at ON issue_comment; CREATE TRIGGER trg_issue_comment_deleted_at BEFORE UPDATE ON issue_comment FOR EACH ROW EXECUTE FUNCTION set_deleted_at(); DROP TRIGGER IF EXISTS trg_repo_commit_comment_deleted_at ON repo_commit_comment; CREATE TRIGGER trg_repo_commit_comment_deleted_at BEFORE UPDATE ON repo_commit_comment FOR EACH ROW EXECUTE FUNCTION set_deleted_at(); DROP TRIGGER IF EXISTS trg_repo_release_deleted_at ON repo_release; CREATE TRIGGER trg_repo_release_deleted_at BEFORE UPDATE ON repo_release FOR EACH ROW EXECUTE FUNCTION set_deleted_at(); DROP TRIGGER IF EXISTS trg_notification_deleted_at ON notification; CREATE TRIGGER trg_notification_deleted_at BEFORE UPDATE ON notification FOR EACH ROW EXECUTE FUNCTION set_deleted_at(); DROP TRIGGER IF EXISTS trg_ai_model_deleted_at ON ai_model; CREATE TRIGGER trg_ai_model_deleted_at BEFORE UPDATE ON ai_model FOR EACH ROW EXECUTE FUNCTION set_deleted_at(); DROP TRIGGER IF EXISTS trg_conversation_attachment_deleted_at ON conversation_attachment; CREATE TRIGGER trg_conversation_attachment_deleted_at BEFORE UPDATE ON conversation_attachment FOR EACH ROW EXECUTE FUNCTION set_deleted_at(); -- ============================================================ -- 3. Helper: resolve current user from session variable -- Application must SET LOCAL app.current_user_id = '...' before writes. -- ============================================================ CREATE OR REPLACE FUNCTION app_current_user_id() RETURNS UUID AS $$ BEGIN RETURN current_setting('app.current_user_id', true)::UUID; EXCEPTION WHEN OTHERS THEN RETURN NULL; END; $$ LANGUAGE plpgsql STABLE; -- ============================================================ -- 4. Event recording — issue / pull_request / channel -- Tracks: created, renamed, state_changed, priority_changed, -- draft_toggled, archived, restored. -- ============================================================ -- 4a. issue events CREATE OR REPLACE FUNCTION record_issue_event() RETURNS TRIGGER AS $$ DECLARE actor UUID := app_current_user_id(); BEGIN IF TG_OP = 'INSERT' THEN INSERT INTO issue_event (issue_id, actor_id, event_type, created_at) VALUES (NEW.id, COALESCE(actor, NEW.author_id), 'created', NOW()); RETURN NEW; END IF; IF NEW.title IS DISTINCT FROM OLD.title THEN INSERT INTO issue_event (issue_id, actor_id, event_type, old_value, new_value, created_at) VALUES (NEW.id, actor, 'renamed', OLD.title, NEW.title, NOW()); END IF; IF NEW.state IS DISTINCT FROM OLD.state THEN INSERT INTO issue_event (issue_id, actor_id, event_type, old_value, new_value, created_at) VALUES (NEW.id, actor, 'state_changed', OLD.state, NEW.state, NOW()); END IF; IF NEW.priority IS DISTINCT FROM OLD.priority THEN INSERT INTO issue_event (issue_id, actor_id, event_type, old_value, new_value, created_at) VALUES (NEW.id, actor, 'priority_changed', OLD.priority, NEW.priority, NOW()); END IF; IF NEW.body IS DISTINCT FROM OLD.body THEN INSERT INTO issue_event (issue_id, actor_id, event_type, created_at) VALUES (NEW.id, actor, 'body_updated', NOW()); END IF; RETURN NEW; END; $$ LANGUAGE plpgsql; DROP TRIGGER IF EXISTS trg_issue_event ON issue; CREATE TRIGGER trg_issue_event AFTER INSERT OR UPDATE ON issue FOR EACH ROW EXECUTE FUNCTION record_issue_event(); -- 4b. pull_request events CREATE OR REPLACE FUNCTION record_pr_event() RETURNS TRIGGER AS $$ DECLARE actor UUID := app_current_user_id(); BEGIN IF TG_OP = 'INSERT' THEN INSERT INTO pr_event (pull_request_id, actor_id, event_type, created_at) VALUES (NEW.id, COALESCE(actor, NEW.author_id), 'created', NOW()); RETURN NEW; END IF; IF NEW.title IS DISTINCT FROM OLD.title THEN INSERT INTO pr_event (pull_request_id, actor_id, event_type, old_value, new_value, created_at) VALUES (NEW.id, actor, 'renamed', OLD.title, NEW.title, NOW()); END IF; IF NEW.state IS DISTINCT FROM OLD.state THEN INSERT INTO pr_event (pull_request_id, actor_id, event_type, old_value, new_value, created_at) VALUES (NEW.id, actor, 'state_changed', OLD.state, NEW.state, NOW()); END IF; IF NEW.draft IS DISTINCT FROM OLD.draft THEN INSERT INTO pr_event (pull_request_id, actor_id, event_type, old_value, new_value, created_at) VALUES (NEW.id, actor, 'draft_toggled', OLD.draft::text, NEW.draft::text, NOW()); END IF; RETURN NEW; END; $$ LANGUAGE plpgsql; DROP TRIGGER IF EXISTS trg_pr_event ON pull_request; CREATE TRIGGER trg_pr_event AFTER INSERT OR UPDATE ON pull_request FOR EACH ROW EXECUTE FUNCTION record_pr_event(); -- 4c. channel events CREATE OR REPLACE FUNCTION record_channel_event() RETURNS TRIGGER AS $$ DECLARE actor UUID := app_current_user_id(); BEGIN IF TG_OP = 'INSERT' THEN INSERT INTO channel_event (channel_id, actor_id, event_type, created_at) VALUES (NEW.id, COALESCE(actor, NEW.created_by), 'created', NOW()); RETURN NEW; END IF; IF NEW.name IS DISTINCT FROM OLD.name THEN INSERT INTO channel_event (channel_id, actor_id, event_type, old_value, new_value, created_at) VALUES (NEW.id, actor, 'renamed', OLD.name, NEW.name, NOW()); END IF; IF NEW.archived IS DISTINCT FROM OLD.archived THEN INSERT INTO channel_event (channel_id, actor_id, event_type, old_value, new_value, created_at) VALUES (NEW.id, actor, 'archive_toggled', OLD.archived::text, NEW.archived::text, NOW()); END IF; RETURN NEW; END; $$ LANGUAGE plpgsql; DROP TRIGGER IF EXISTS trg_channel_event ON channel; CREATE TRIGGER trg_channel_event AFTER INSERT OR UPDATE ON channel FOR EACH ROW EXECUTE FUNCTION record_channel_event(); -- ============================================================ -- 5. Workspace audit log -- Only applied to tables that directly own a workspace_id column. -- actor_id resolved from app.current_user_id session variable. -- ============================================================ -- Tables with workspace_id (verified from models): -- repo, channel, channel_invitation, channel_slash_command, -- agent, agent_event_subscription, agent_execution, agent_schedule, -- agent_workspace_binding, conversation, -- notification, notification_block, notification_subscription, -- workspace_billing, workspace_custom_branding, workspace_domain, -- workspace_integration, workspace_invitation, workspace_member, -- workspace_pending_approval, workspace_settings, workspace_webhook CREATE OR REPLACE FUNCTION record_workspace_audit() RETURNS TRIGGER AS $$ DECLARE ws_id UUID; actor UUID := app_current_user_id(); action_text TEXT; BEGIN ws_id := COALESCE(NEW.workspace_id, OLD.workspace_id); IF ws_id IS NULL THEN RETURN COALESCE(NEW, OLD); END IF; action_text := CASE TG_OP WHEN 'INSERT' THEN 'created' WHEN 'UPDATE' THEN 'updated' WHEN 'DELETE' THEN 'deleted' END; INSERT INTO workspace_audit_log (workspace_id, actor_id, action, target_type, target_id, created_at) VALUES (ws_id, actor, action_text, TG_TABLE_NAME, COALESCE(NEW.id, OLD.id), NOW()); RETURN COALESCE(NEW, OLD); END; $$ LANGUAGE plpgsql; -- Apply to tables with workspace_id column DROP TRIGGER IF EXISTS trg_repo_audit ON repo; CREATE TRIGGER trg_repo_audit AFTER INSERT OR UPDATE OR DELETE ON repo FOR EACH ROW EXECUTE FUNCTION record_workspace_audit(); DROP TRIGGER IF EXISTS trg_channel_audit ON channel; CREATE TRIGGER trg_channel_audit AFTER INSERT OR UPDATE OR DELETE ON channel FOR EACH ROW EXECUTE FUNCTION record_workspace_audit(); DROP TRIGGER IF EXISTS trg_channel_slash_command_audit ON channel_slash_command; CREATE TRIGGER trg_channel_slash_command_audit AFTER INSERT OR UPDATE OR DELETE ON channel_slash_command FOR EACH ROW EXECUTE FUNCTION record_workspace_audit(); DROP TRIGGER IF EXISTS trg_agent_audit ON agent; CREATE TRIGGER trg_agent_audit AFTER INSERT OR UPDATE OR DELETE ON agent FOR EACH ROW EXECUTE FUNCTION record_workspace_audit(); DROP TRIGGER IF EXISTS trg_agent_schedule_audit ON agent_schedule; CREATE TRIGGER trg_agent_schedule_audit AFTER INSERT OR UPDATE OR DELETE ON agent_schedule FOR EACH ROW EXECUTE FUNCTION record_workspace_audit(); DROP TRIGGER IF EXISTS trg_agent_workspace_binding_audit ON agent_workspace_binding; CREATE TRIGGER trg_agent_workspace_binding_audit AFTER INSERT OR UPDATE OR DELETE ON agent_workspace_binding FOR EACH ROW EXECUTE FUNCTION record_workspace_audit(); DROP TRIGGER IF EXISTS trg_conversation_audit ON conversation; CREATE TRIGGER trg_conversation_audit AFTER INSERT OR UPDATE OR DELETE ON conversation FOR EACH ROW EXECUTE FUNCTION record_workspace_audit(); DROP TRIGGER IF EXISTS trg_channel_invitation_audit ON channel_invitation; CREATE TRIGGER trg_channel_invitation_audit AFTER INSERT OR UPDATE OR DELETE ON channel_invitation FOR EACH ROW EXECUTE FUNCTION record_workspace_audit(); DROP TRIGGER IF EXISTS trg_agent_event_subscription_audit ON agent_event_subscription; CREATE TRIGGER trg_agent_event_subscription_audit AFTER INSERT OR UPDATE OR DELETE ON agent_event_subscription FOR EACH ROW EXECUTE FUNCTION record_workspace_audit(); DROP TRIGGER IF EXISTS trg_agent_execution_audit ON agent_execution; CREATE TRIGGER trg_agent_execution_audit AFTER INSERT OR UPDATE OR DELETE ON agent_execution FOR EACH ROW EXECUTE FUNCTION record_workspace_audit(); DROP TRIGGER IF EXISTS trg_notification_audit ON notification; CREATE TRIGGER trg_notification_audit AFTER INSERT OR UPDATE OR DELETE ON notification FOR EACH ROW EXECUTE FUNCTION record_workspace_audit(); DROP TRIGGER IF EXISTS trg_notification_block_audit ON notification_block; CREATE TRIGGER trg_notification_block_audit AFTER INSERT OR UPDATE OR DELETE ON notification_block FOR EACH ROW EXECUTE FUNCTION record_workspace_audit(); DROP TRIGGER IF EXISTS trg_notification_subscription_audit ON notification_subscription; CREATE TRIGGER trg_notification_subscription_audit AFTER INSERT OR UPDATE OR DELETE ON notification_subscription FOR EACH ROW EXECUTE FUNCTION record_workspace_audit(); DROP TRIGGER IF EXISTS trg_workspace_billing_audit ON workspace_billing; CREATE TRIGGER trg_workspace_billing_audit AFTER INSERT OR UPDATE OR DELETE ON workspace_billing FOR EACH ROW EXECUTE FUNCTION record_workspace_audit(); DROP TRIGGER IF EXISTS trg_workspace_custom_branding_audit ON workspace_custom_branding; CREATE TRIGGER trg_workspace_custom_branding_audit AFTER INSERT OR UPDATE OR DELETE ON workspace_custom_branding FOR EACH ROW EXECUTE FUNCTION record_workspace_audit(); DROP TRIGGER IF EXISTS trg_workspace_domain_audit ON workspace_domain; CREATE TRIGGER trg_workspace_domain_audit AFTER INSERT OR UPDATE OR DELETE ON workspace_domain FOR EACH ROW EXECUTE FUNCTION record_workspace_audit(); DROP TRIGGER IF EXISTS trg_workspace_integration_audit ON workspace_integration; CREATE TRIGGER trg_workspace_integration_audit AFTER INSERT OR UPDATE OR DELETE ON workspace_integration FOR EACH ROW EXECUTE FUNCTION record_workspace_audit(); DROP TRIGGER IF EXISTS trg_workspace_invitation_audit ON workspace_invitation; CREATE TRIGGER trg_workspace_invitation_audit AFTER INSERT OR UPDATE OR DELETE ON workspace_invitation FOR EACH ROW EXECUTE FUNCTION record_workspace_audit(); DROP TRIGGER IF EXISTS trg_workspace_member_audit ON workspace_member; CREATE TRIGGER trg_workspace_member_audit AFTER INSERT OR UPDATE OR DELETE ON workspace_member FOR EACH ROW EXECUTE FUNCTION record_workspace_audit(); DROP TRIGGER IF EXISTS trg_workspace_pending_approval_audit ON workspace_pending_approval; CREATE TRIGGER trg_workspace_pending_approval_audit AFTER INSERT OR UPDATE OR DELETE ON workspace_pending_approval FOR EACH ROW EXECUTE FUNCTION record_workspace_audit(); DROP TRIGGER IF EXISTS trg_workspace_settings_audit ON workspace_settings; CREATE TRIGGER trg_workspace_settings_audit AFTER INSERT OR UPDATE OR DELETE ON workspace_settings FOR EACH ROW EXECUTE FUNCTION record_workspace_audit(); DROP TRIGGER IF EXISTS trg_workspace_webhook_audit ON workspace_webhook; CREATE TRIGGER trg_workspace_webhook_audit AFTER INSERT OR UPDATE OR DELETE ON workspace_webhook FOR EACH ROW EXECUTE FUNCTION record_workspace_audit(); -- ============================================================ -- 6. User security log -- Password changes, logins, session revocations. -- ============================================================ CREATE OR REPLACE FUNCTION record_user_security_event() RETURNS TRIGGER AS $$ DECLARE actor UUID := app_current_user_id(); BEGIN IF TG_OP = 'UPDATE' THEN IF NEW.password_hash IS DISTINCT FROM OLD.password_hash THEN INSERT INTO user_security_log (user_id, event_type, description, created_at) VALUES (NEW.user_id, 'password_changed', 'Password updated', NOW()); END IF; END IF; RETURN NEW; END; $$ LANGUAGE plpgsql; DROP TRIGGER IF EXISTS trg_user_password_security ON user_password; CREATE TRIGGER trg_user_password_security AFTER UPDATE ON user_password FOR EACH ROW EXECUTE FUNCTION record_user_security_event(); CREATE OR REPLACE FUNCTION record_user_session_event() RETURNS TRIGGER AS $$ DECLARE actor UUID := app_current_user_id(); BEGIN IF TG_OP = 'INSERT' THEN INSERT INTO user_security_log (user_id, event_type, description, ip_address, user_agent, created_at) VALUES (NEW.user_id, 'login', 'User logged in', NEW.ip_address, NEW.user_agent, NOW()); ELSIF TG_OP = 'UPDATE' AND NEW.revoked_at IS NOT NULL AND OLD.revoked_at IS NULL THEN INSERT INTO user_security_log (user_id, event_type, description, created_at) VALUES (NEW.user_id, 'session_revoked', 'Session revoked', NOW()); END IF; RETURN NEW; END; $$ LANGUAGE plpgsql; DROP TRIGGER IF EXISTS trg_user_session_event ON user_session; CREATE TRIGGER trg_user_session_event AFTER INSERT OR UPDATE ON user_session FOR EACH ROW EXECUTE FUNCTION record_user_session_event(); -- ============================================================ -- 7. Stats auto-maintenance -- issue_stats.comments_count — incremented on comment insert/delete. -- channel_stats.messages_count — incremented on message insert. -- Stats rows MUST be pre-created (no auto-INSERT with zero counts). -- ============================================================ -- 7a. Issue comment stats (UPDATE only, no INSERT) CREATE OR REPLACE FUNCTION update_issue_stats() RETURNS TRIGGER AS $$ BEGIN IF TG_OP = 'INSERT' THEN UPDATE issue_stats SET comments_count = comments_count + 1, updated_at = NOW() WHERE issue_id = NEW.issue_id; IF NOT FOUND THEN RAISE WARNING 'issue_stats row missing for issue_id %. Insert skipped; seed stats row first.', NEW.issue_id; END IF; ELSIF TG_OP = 'DELETE' THEN UPDATE issue_stats SET comments_count = GREATEST(comments_count - 1, 0), updated_at = NOW() WHERE issue_id = OLD.issue_id; END IF; RETURN COALESCE(NEW, OLD); END; $$ LANGUAGE plpgsql; DROP TRIGGER IF EXISTS trg_issue_comment_stats ON issue_comment; CREATE TRIGGER trg_issue_comment_stats AFTER INSERT OR DELETE ON issue_comment FOR EACH ROW EXECUTE FUNCTION update_issue_stats(); -- 7b. Channel message stats (UPDATE only, no INSERT) CREATE OR REPLACE FUNCTION update_channel_stats() RETURNS TRIGGER AS $$ BEGIN IF TG_OP = 'INSERT' THEN UPDATE channel_stats SET messages_count = messages_count + 1, last_activity_at = NOW(), updated_at = NOW() WHERE channel_id = NEW.channel_id; IF NOT FOUND THEN RAISE WARNING 'channel_stats row missing for channel_id %. Insert skipped; seed stats row first.', NEW.channel_id; END IF; END IF; RETURN NEW; END; $$ LANGUAGE plpgsql; DROP TRIGGER IF EXISTS trg_channel_message_stats ON message; CREATE TRIGGER trg_channel_message_stats AFTER INSERT ON message FOR EACH ROW EXECUTE FUNCTION update_channel_stats();