Files
appks/migrate/002_triggers.sql
2026-06-07 11:30:56 +08:00

668 lines
34 KiB
PL/PgSQL

-- ============================================================
-- 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();