feat: init
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,667 @@
|
||||
-- ============================================================
|
||||
-- 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();
|
||||
@@ -0,0 +1,18 @@
|
||||
-- ============================================================
|
||||
-- Migration: 003_user_2fa.sql
|
||||
-- Table: user_2fa — TOTP two-factor authentication
|
||||
-- ============================================================
|
||||
|
||||
BEGIN;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS user_2fa (
|
||||
user_id UUID PRIMARY KEY REFERENCES "user"(id) ON DELETE CASCADE,
|
||||
secret TEXT NULL,
|
||||
backup_codes TEXT NOT NULL,
|
||||
enabled BOOLEAN NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL,
|
||||
updated_at TIMESTAMPTZ NOT NULL
|
||||
|
||||
);
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,8 @@
|
||||
-- Auth identity uniqueness and lookup hardening.
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_user_username_active_ci
|
||||
ON "user" (lower(username))
|
||||
WHERE deleted_at IS NULL;
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_user_mail_verified_email_ci
|
||||
ON user_mail (lower(email))
|
||||
WHERE is_verified = true;
|
||||
@@ -0,0 +1,124 @@
|
||||
-- 005: Issue workspace-level upgrade + slug removal
|
||||
-- 1. Drop slug from workspace
|
||||
-- 2. Lift issue from repo-level to workspace-level (repo_id -> workspace_id)
|
||||
-- 3. Add issue_repo_relation for multi-repo association
|
||||
-- 4. Add milestone_id to issue
|
||||
-- 5. Add missing unique constraints
|
||||
|
||||
ALTER TABLE workspace DROP COLUMN IF EXISTS slug;
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_workspace_name ON workspace (lower(name)) WHERE deleted_at IS NULL;
|
||||
|
||||
DROP INDEX IF EXISTS idx_issue_repo_id;
|
||||
DROP INDEX IF EXISTS idx_issue_repo_created;
|
||||
|
||||
ALTER TABLE issue DROP CONSTRAINT IF EXISTS issue_repo_id_fkey;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'issue'
|
||||
AND column_name = 'repo_id'
|
||||
)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'issue'
|
||||
AND column_name = 'workspace_id'
|
||||
) THEN
|
||||
ALTER TABLE issue RENAME COLUMN repo_id TO workspace_id;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_constraint
|
||||
WHERE conname = 'issue_workspace_id_fkey'
|
||||
AND conrelid = 'issue'::regclass
|
||||
) THEN
|
||||
ALTER TABLE issue
|
||||
ADD CONSTRAINT issue_workspace_id_fkey
|
||||
FOREIGN KEY (workspace_id) REFERENCES workspace(id) ON DELETE CASCADE;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_issue_workspace_id ON issue (workspace_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_issue_ws_created ON issue (workspace_id, created_at DESC);
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_constraint
|
||||
WHERE conname = 'uq_issue_workspace_number'
|
||||
AND conrelid = 'issue'::regclass
|
||||
) THEN
|
||||
ALTER TABLE issue
|
||||
ADD CONSTRAINT uq_issue_workspace_number UNIQUE (workspace_id, number);
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS issue_repo_relation (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
issue_id UUID NOT NULL REFERENCES issue(id) ON DELETE CASCADE,
|
||||
repo_id UUID NOT NULL REFERENCES repo(id) ON DELETE CASCADE,
|
||||
relation_type TEXT NOT NULL,
|
||||
created_by UUID NULL REFERENCES "user"(id) ON DELETE CASCADE,
|
||||
created_at TIMESTAMPTZ NOT NULL,
|
||||
CONSTRAINT uq_issue_repo_relation UNIQUE (issue_id, repo_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_issue_repo_relation_issue_id
|
||||
ON issue_repo_relation (issue_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_issue_repo_relation_repo_id
|
||||
ON issue_repo_relation (repo_id);
|
||||
|
||||
ALTER TABLE issue
|
||||
ADD COLUMN IF NOT EXISTS milestone_id UUID NULL REFERENCES issue_milestone(id) ON DELETE SET NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_issue_milestone_id ON issue (milestone_id);
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_constraint
|
||||
WHERE conname = 'uq_issue_label_relation'
|
||||
AND conrelid = 'issue_label_relation'::regclass
|
||||
) THEN
|
||||
ALTER TABLE issue_label_relation
|
||||
ADD CONSTRAINT uq_issue_label_relation UNIQUE (issue_id, label_id);
|
||||
END IF;
|
||||
END $$;
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_repo_workspace_name
|
||||
ON repo (workspace_id, lower(name)) WHERE deleted_at IS NULL;
|
||||
|
||||
-- ─── 7. pull_request: unique number per repo ───────────────────────────
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint
|
||||
WHERE conname = 'uq_pull_request_repo_number' AND conrelid = 'pull_request'::regclass
|
||||
) THEN
|
||||
ALTER TABLE pull_request
|
||||
ADD CONSTRAINT uq_pull_request_repo_number UNIQUE (repo_id, number);
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- ─── 8. pr_label_relation: unique constraint ────────────────────────────
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint
|
||||
WHERE conname = 'uq_pr_label_relation' AND conrelid = 'pr_label_relation'::regclass
|
||||
) THEN
|
||||
ALTER TABLE pr_label_relation
|
||||
ADD CONSTRAINT uq_pr_label_relation UNIQUE (pull_request_id, label_id);
|
||||
END IF;
|
||||
END $$;
|
||||
@@ -0,0 +1,76 @@
|
||||
-- 006: PR Reviews, Branch Protection, Fork enhancements
|
||||
|
||||
CREATE TABLE IF NOT EXISTS pr_review (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
pull_request_id UUID NOT NULL REFERENCES pull_request(id) ON DELETE CASCADE,
|
||||
author_id UUID NOT NULL REFERENCES "user"(id) ON DELETE CASCADE,
|
||||
state TEXT NOT NULL,
|
||||
body TEXT NULL,
|
||||
commit_sha TEXT NULL,
|
||||
submitted_at TIMESTAMPTZ NULL,
|
||||
dismissed_at TIMESTAMPTZ NULL,
|
||||
dismissed_by UUID NULL REFERENCES "user"(id) ON DELETE CASCADE,
|
||||
dismiss_reason TEXT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL,
|
||||
updated_at TIMESTAMPTZ NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_pr_review_pull_request_id ON pr_review (pull_request_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_pr_review_author_id ON pr_review (author_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS pr_review_comment (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
review_id UUID NOT NULL REFERENCES pr_review(id) ON DELETE CASCADE,
|
||||
pull_request_id UUID NOT NULL REFERENCES pull_request(id) ON DELETE CASCADE,
|
||||
author_id UUID NOT NULL REFERENCES "user"(id) ON DELETE CASCADE,
|
||||
body TEXT NOT NULL,
|
||||
path TEXT NOT NULL,
|
||||
line INTEGER NULL,
|
||||
original_line INTEGER NULL,
|
||||
start_line INTEGER NULL,
|
||||
original_start_line INTEGER NULL,
|
||||
diff_hunk TEXT NULL,
|
||||
in_reply_to_id UUID NULL REFERENCES pr_review_comment(id) ON DELETE CASCADE,
|
||||
edited_at TIMESTAMPTZ NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL,
|
||||
updated_at TIMESTAMPTZ NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_pr_review_comment_review_id ON pr_review_comment (review_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_pr_review_comment_pull_request_id ON pr_review_comment (pull_request_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_pr_review_comment_author_id ON pr_review_comment (author_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_pr_review_comment_path ON pr_review_comment (path);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS branch_protection_rule (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
repo_id UUID NOT NULL REFERENCES repo(id) ON DELETE CASCADE,
|
||||
pattern TEXT NOT NULL,
|
||||
require_approvals INTEGER NOT NULL DEFAULT 0,
|
||||
require_status_checks BOOLEAN NOT NULL DEFAULT false,
|
||||
required_status_checks TEXT[] NOT NULL DEFAULT '{}',
|
||||
require_linear_history BOOLEAN NOT NULL DEFAULT false,
|
||||
allow_force_pushes BOOLEAN NOT NULL DEFAULT false,
|
||||
allow_deletions BOOLEAN NOT NULL DEFAULT false,
|
||||
require_signed_commits BOOLEAN NOT NULL DEFAULT false,
|
||||
require_code_owner_review BOOLEAN NOT NULL DEFAULT false,
|
||||
dismiss_stale_reviews BOOLEAN NOT NULL DEFAULT false,
|
||||
restrict_pushes BOOLEAN NOT NULL DEFAULT false,
|
||||
push_allowances UUID[] NOT NULL DEFAULT '{}',
|
||||
restrict_review_dismissal BOOLEAN NOT NULL DEFAULT false,
|
||||
dismissal_allowances UUID[] NOT NULL DEFAULT '{}',
|
||||
require_conversation_resolution BOOLEAN NOT NULL DEFAULT false,
|
||||
created_by UUID NULL REFERENCES "user"(id) ON DELETE CASCADE,
|
||||
created_at TIMESTAMPTZ NOT NULL,
|
||||
updated_at TIMESTAMPTZ NOT NULL,
|
||||
CONSTRAINT uq_branch_protection_rule UNIQUE (repo_id, pattern)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_branch_protection_rule_repo_id ON branch_protection_rule (repo_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS repo_fork (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
parent_repo_id UUID NOT NULL REFERENCES repo(id) ON DELETE CASCADE,
|
||||
fork_repo_id UUID NOT NULL REFERENCES repo(id) ON DELETE CASCADE,
|
||||
forked_by UUID NOT NULL REFERENCES "user"(id) ON DELETE CASCADE,
|
||||
created_at TIMESTAMPTZ NOT NULL,
|
||||
CONSTRAINT uq_repo_fork UNIQUE (parent_repo_id, fork_repo_id)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_repo_fork_parent ON repo_fork (parent_repo_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_repo_fork_fork ON repo_fork (fork_repo_id);
|
||||
@@ -0,0 +1,16 @@
|
||||
-- 007: Issue reactions
|
||||
|
||||
-- ─── Issue Reactions ──────────────────────────────────────────────────────
|
||||
CREATE TABLE IF NOT EXISTS issue_reaction (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
issue_id UUID NOT NULL REFERENCES issue(id) ON DELETE CASCADE,
|
||||
user_id UUID NOT NULL REFERENCES "user"(id) ON DELETE CASCADE,
|
||||
content TEXT NOT NULL,
|
||||
target_type TEXT NOT NULL,
|
||||
target_id UUID NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL,
|
||||
CONSTRAINT uq_issue_reaction UNIQUE (issue_id, user_id, content, target_type)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_issue_reaction_issue_id ON issue_reaction (issue_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_issue_reaction_user_id ON issue_reaction (user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_issue_reaction_target ON issue_reaction (target_type, target_id);
|
||||
@@ -0,0 +1,34 @@
|
||||
-- 008: Wiki System
|
||||
|
||||
CREATE TABLE IF NOT EXISTS wiki_page (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
repo_id UUID NOT NULL REFERENCES repo(id) ON DELETE CASCADE,
|
||||
slug TEXT NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
author_id UUID NOT NULL REFERENCES "user"(id) ON DELETE CASCADE,
|
||||
last_editor_id UUID REFERENCES "user"(id) ON DELETE SET NULL,
|
||||
version INTEGER NOT NULL DEFAULT 1,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ NULL,
|
||||
CONSTRAINT uq_wiki_page_repo_slug UNIQUE (repo_id, slug)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS wiki_page_revision (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
page_id UUID NOT NULL REFERENCES wiki_page(id) ON DELETE CASCADE,
|
||||
version INTEGER NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
editor_id UUID NOT NULL REFERENCES "user"(id) ON DELETE CASCADE,
|
||||
commit_message TEXT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT uq_wiki_revision_page_version UNIQUE (page_id, version)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_wiki_page_repo_id ON wiki_page(repo_id);
|
||||
CREATE INDEX idx_wiki_page_slug ON wiki_page(slug);
|
||||
CREATE INDEX idx_wiki_page_deleted_at ON wiki_page(deleted_at) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX idx_wiki_revision_page_id ON wiki_page_revision(page_id);
|
||||
CREATE INDEX idx_wiki_revision_version ON wiki_page_revision(version);
|
||||
@@ -0,0 +1,320 @@
|
||||
-- 009: IM Features — Discord/Slack-class messaging support
|
||||
--
|
||||
-- New tables:
|
||||
-- user_presence, user_activity,
|
||||
-- channel_category, channel_permission_overwrite, im_integration,
|
||||
-- message_attachment, message_embed, message_draft, message_pin,
|
||||
-- message_edit_history, saved_message, thread_read_state,
|
||||
-- custom_emoji
|
||||
|
||||
-- ============================================================
|
||||
-- 1. User Presence
|
||||
-- ============================================================
|
||||
|
||||
-- models/users/user_presence.rs → user_presence
|
||||
CREATE TABLE IF NOT EXISTS user_presence (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES "user"(id) ON DELETE CASCADE,
|
||||
status TEXT NOT NULL,
|
||||
custom_status_text TEXT NULL,
|
||||
custom_status_emoji TEXT NULL,
|
||||
device_type TEXT NULL,
|
||||
ip_address TEXT NULL,
|
||||
last_active_at TIMESTAMPTZ NOT NULL,
|
||||
last_seen_at TIMESTAMPTZ NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL,
|
||||
updated_at TIMESTAMPTZ NOT NULL,
|
||||
CONSTRAINT uq_user_presence_user_id UNIQUE (user_id)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_user_presence_status ON user_presence (status);
|
||||
|
||||
-- ============================================================
|
||||
-- 2. User Activity (Rich Presence)
|
||||
-- ============================================================
|
||||
|
||||
-- models/users/user_activity.rs → user_activity
|
||||
CREATE TABLE IF NOT EXISTS user_activity (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES "user"(id) ON DELETE CASCADE,
|
||||
activity_type TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
details TEXT NULL,
|
||||
state TEXT NULL,
|
||||
application_id TEXT NULL,
|
||||
assets JSONB NULL,
|
||||
party_id TEXT NULL,
|
||||
party_current_size INTEGER NULL,
|
||||
party_max_size INTEGER NULL,
|
||||
large_image_url TEXT NULL,
|
||||
small_image_url TEXT NULL,
|
||||
start_at TIMESTAMPTZ NULL,
|
||||
end_at TIMESTAMPTZ NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL,
|
||||
updated_at TIMESTAMPTZ NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_user_activity_user_id ON user_activity (user_id);
|
||||
|
||||
-- ============================================================
|
||||
-- 3. Channel Categories
|
||||
-- ============================================================
|
||||
|
||||
-- models/channels/channel_categories.rs → channel_category
|
||||
CREATE TABLE IF NOT EXISTS channel_category (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
workspace_id UUID NOT NULL REFERENCES workspace(id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL,
|
||||
position INTEGER NOT NULL,
|
||||
collapsed BOOLEAN NOT NULL,
|
||||
created_by UUID NOT NULL REFERENCES "user"(id) ON DELETE CASCADE,
|
||||
created_at TIMESTAMPTZ NOT NULL,
|
||||
updated_at TIMESTAMPTZ NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_channel_category_workspace_id ON channel_category (workspace_id);
|
||||
|
||||
-- ============================================================
|
||||
-- 4. ALTER channel — add category_id (after channel_category exists)
|
||||
-- ============================================================
|
||||
|
||||
ALTER TABLE channel ADD COLUMN IF NOT EXISTS category_id UUID NULL REFERENCES channel_category(id) ON DELETE SET NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_channel_category_id ON channel (category_id);
|
||||
|
||||
-- ============================================================
|
||||
-- 5. Channel Permission Overwrites
|
||||
-- ============================================================
|
||||
|
||||
-- models/channels/channel_permission_overwrites.rs → channel_permission_overwrite
|
||||
CREATE TABLE IF NOT EXISTS channel_permission_overwrite (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
channel_id UUID NOT NULL REFERENCES channel(id) ON DELETE CASCADE,
|
||||
target_type TEXT NOT NULL,
|
||||
target_id UUID NOT NULL,
|
||||
allow TEXT[] NOT NULL,
|
||||
deny TEXT[] NOT NULL,
|
||||
created_by UUID NOT NULL REFERENCES "user"(id) ON DELETE CASCADE,
|
||||
created_at TIMESTAMPTZ NOT NULL,
|
||||
updated_at TIMESTAMPTZ NOT NULL,
|
||||
CONSTRAINT uq_channel_perm_overwrite UNIQUE (channel_id, target_type, target_id)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_channel_perm_overwrite_channel_id ON channel_permission_overwrite (channel_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_channel_perm_overwrite_target ON channel_permission_overwrite (target_type, target_id);
|
||||
|
||||
-- ============================================================
|
||||
-- 6. IM Integrations (External Bridge)
|
||||
-- ============================================================
|
||||
|
||||
-- models/channels/im_integrations.rs → im_integration
|
||||
CREATE TABLE IF NOT EXISTS im_integration (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
workspace_id UUID NOT NULL REFERENCES workspace(id) ON DELETE CASCADE,
|
||||
provider TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
external_workspace_id TEXT NULL,
|
||||
internal_channel_id UUID NULL REFERENCES channel(id) ON DELETE SET NULL,
|
||||
external_channel_id TEXT NULL,
|
||||
bot_token_ciphertext TEXT NULL,
|
||||
webhook_url TEXT NULL,
|
||||
sync_direction TEXT NOT NULL,
|
||||
user_mapping JSONB NULL,
|
||||
enabled BOOLEAN NOT NULL,
|
||||
installed_by UUID NOT NULL REFERENCES "user"(id) ON DELETE CASCADE,
|
||||
last_sync_at TIMESTAMPTZ NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL,
|
||||
updated_at TIMESTAMPTZ NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_im_integration_workspace_id ON im_integration (workspace_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_im_integration_internal_channel_id ON im_integration (internal_channel_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_im_integration_provider ON im_integration (provider);
|
||||
|
||||
-- ============================================================
|
||||
-- 7. Message Attachments
|
||||
-- ============================================================
|
||||
|
||||
-- models/channels/message_attachments.rs → message_attachment
|
||||
CREATE TABLE IF NOT EXISTS message_attachment (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
message_id UUID NOT NULL REFERENCES message(id) ON DELETE CASCADE,
|
||||
channel_id UUID NOT NULL REFERENCES channel(id) ON DELETE CASCADE,
|
||||
filename TEXT NOT NULL,
|
||||
url TEXT NOT NULL,
|
||||
proxy_url TEXT NULL,
|
||||
size_bytes BIGINT NOT NULL,
|
||||
mime_type TEXT NOT NULL,
|
||||
width INTEGER NULL,
|
||||
height INTEGER NULL,
|
||||
duration_ms BIGINT NULL,
|
||||
thumbnail_url TEXT NULL,
|
||||
blurhash TEXT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_message_attachment_message_id ON message_attachment (message_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_message_attachment_channel_id ON message_attachment (channel_id);
|
||||
|
||||
-- ============================================================
|
||||
-- 8. Message Embeds (Rich Text / Link Previews)
|
||||
-- ============================================================
|
||||
|
||||
-- models/channels/message_embeds.rs → message_embed
|
||||
CREATE TABLE IF NOT EXISTS message_embed (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
message_id UUID NOT NULL REFERENCES message(id) ON DELETE CASCADE,
|
||||
embed_type TEXT NOT NULL,
|
||||
title TEXT NULL,
|
||||
description TEXT NULL,
|
||||
url TEXT NULL,
|
||||
author_name TEXT NULL,
|
||||
author_url TEXT NULL,
|
||||
author_icon_url TEXT NULL,
|
||||
thumbnail_url TEXT NULL,
|
||||
thumbnail_width INTEGER NULL,
|
||||
thumbnail_height INTEGER NULL,
|
||||
image_url TEXT NULL,
|
||||
image_width INTEGER NULL,
|
||||
image_height INTEGER NULL,
|
||||
video_url TEXT NULL,
|
||||
video_width INTEGER NULL,
|
||||
video_height INTEGER NULL,
|
||||
color INTEGER NULL,
|
||||
fields JSONB NULL,
|
||||
footer_text TEXT NULL,
|
||||
footer_icon_url TEXT NULL,
|
||||
provider_name TEXT NULL,
|
||||
provider_url TEXT NULL,
|
||||
"timestamp" TIMESTAMPTZ NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_message_embed_message_id ON message_embed (message_id);
|
||||
|
||||
-- ============================================================
|
||||
-- 9. Message Drafts
|
||||
-- ============================================================
|
||||
|
||||
-- models/channels/message_drafts.rs → message_draft
|
||||
CREATE TABLE IF NOT EXISTS message_draft (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES "user"(id) ON DELETE CASCADE,
|
||||
channel_id UUID NOT NULL REFERENCES channel(id) ON DELETE CASCADE,
|
||||
thread_id UUID NULL,
|
||||
reply_to_message_id UUID NULL,
|
||||
content TEXT NOT NULL,
|
||||
attachments JSONB NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL,
|
||||
updated_at TIMESTAMPTZ NOT NULL,
|
||||
CONSTRAINT uq_message_draft_user_channel UNIQUE (user_id, channel_id)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_message_draft_user_id ON message_draft (user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_message_draft_channel_id ON message_draft (channel_id);
|
||||
|
||||
-- ============================================================
|
||||
-- 10. Custom Emojis
|
||||
-- ============================================================
|
||||
|
||||
-- models/channels/custom_emojis.rs → custom_emoji
|
||||
CREATE TABLE IF NOT EXISTS custom_emoji (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
workspace_id UUID NOT NULL REFERENCES workspace(id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL,
|
||||
url TEXT NOT NULL,
|
||||
animated BOOLEAN NOT NULL,
|
||||
managed BOOLEAN NOT NULL,
|
||||
created_by UUID NOT NULL REFERENCES "user"(id) ON DELETE CASCADE,
|
||||
created_at TIMESTAMPTZ NOT NULL,
|
||||
updated_at TIMESTAMPTZ NOT NULL,
|
||||
CONSTRAINT uq_custom_emoji_workspace_name UNIQUE (workspace_id, name)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_custom_emoji_workspace_id ON custom_emoji (workspace_id);
|
||||
|
||||
-- ============================================================
|
||||
-- 11. Message Pins
|
||||
-- ============================================================
|
||||
|
||||
-- models/channels/message_pins.rs → message_pin
|
||||
CREATE TABLE IF NOT EXISTS message_pin (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
message_id UUID NOT NULL REFERENCES message(id) ON DELETE CASCADE,
|
||||
channel_id UUID NOT NULL REFERENCES channel(id) ON DELETE CASCADE,
|
||||
pinned_by UUID NOT NULL REFERENCES "user"(id) ON DELETE CASCADE,
|
||||
pinned_at TIMESTAMPTZ NOT NULL,
|
||||
CONSTRAINT uq_message_pin_message_id UNIQUE (message_id)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_message_pin_channel_id ON message_pin (channel_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_message_pin_message_id ON message_pin (message_id);
|
||||
|
||||
-- ============================================================
|
||||
-- 12. Message Edit History
|
||||
-- ============================================================
|
||||
|
||||
-- models/channels/message_edit_history.rs → message_edit_history
|
||||
CREATE TABLE IF NOT EXISTS message_edit_history (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
message_id UUID NOT NULL REFERENCES message(id) ON DELETE CASCADE,
|
||||
channel_id UUID NOT NULL REFERENCES channel(id) ON DELETE CASCADE,
|
||||
previous_body TEXT NOT NULL,
|
||||
edited_by UUID NOT NULL REFERENCES "user"(id) ON DELETE CASCADE,
|
||||
edited_at TIMESTAMPTZ NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_message_edit_history_message_id ON message_edit_history (message_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_message_edit_history_channel_id ON message_edit_history (channel_id);
|
||||
|
||||
-- ============================================================
|
||||
-- 13. Saved Messages
|
||||
-- ============================================================
|
||||
|
||||
-- models/channels/saved_messages.rs → saved_message
|
||||
CREATE TABLE IF NOT EXISTS saved_message (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES "user"(id) ON DELETE CASCADE,
|
||||
message_id UUID NOT NULL REFERENCES message(id) ON DELETE CASCADE,
|
||||
channel_id UUID NOT NULL REFERENCES channel(id) ON DELETE CASCADE,
|
||||
note TEXT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL,
|
||||
CONSTRAINT uq_saved_message_user_message UNIQUE (user_id, message_id)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_saved_message_user_id ON saved_message (user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_saved_message_message_id ON saved_message (message_id);
|
||||
|
||||
-- ============================================================
|
||||
-- 14. Thread Read States
|
||||
-- ============================================================
|
||||
|
||||
-- models/channels/thread_read_states.rs → thread_read_state
|
||||
CREATE TABLE IF NOT EXISTS thread_read_state (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES "user"(id) ON DELETE CASCADE,
|
||||
thread_id UUID NOT NULL,
|
||||
channel_id UUID NOT NULL REFERENCES channel(id) ON DELETE CASCADE,
|
||||
last_read_message_id UUID NULL,
|
||||
last_read_at TIMESTAMPTZ NULL,
|
||||
updated_at TIMESTAMPTZ NOT NULL,
|
||||
CONSTRAINT uq_thread_read_state_user_thread UNIQUE (user_id, thread_id)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_thread_read_state_user_id ON thread_read_state (user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_thread_read_state_thread_id ON thread_read_state (thread_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_thread_read_state_channel_id ON thread_read_state (channel_id);
|
||||
|
||||
-- ============================================================
|
||||
-- 15. Triggers — auto-refresh updated_at
|
||||
-- ============================================================
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_user_presence_updated_at ON user_presence;
|
||||
CREATE TRIGGER trg_user_presence_updated_at BEFORE UPDATE ON user_presence FOR EACH ROW EXECUTE FUNCTION set_updated_at();
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_user_activity_updated_at ON user_activity;
|
||||
CREATE TRIGGER trg_user_activity_updated_at BEFORE UPDATE ON user_activity FOR EACH ROW EXECUTE FUNCTION set_updated_at();
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_channel_category_updated_at ON channel_category;
|
||||
CREATE TRIGGER trg_channel_category_updated_at BEFORE UPDATE ON channel_category FOR EACH ROW EXECUTE FUNCTION set_updated_at();
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_channel_permission_overwrite_updated_at ON channel_permission_overwrite;
|
||||
CREATE TRIGGER trg_channel_permission_overwrite_updated_at BEFORE UPDATE ON channel_permission_overwrite FOR EACH ROW EXECUTE FUNCTION set_updated_at();
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_im_integration_updated_at ON im_integration;
|
||||
CREATE TRIGGER trg_im_integration_updated_at BEFORE UPDATE ON im_integration FOR EACH ROW EXECUTE FUNCTION set_updated_at();
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_message_draft_updated_at ON message_draft;
|
||||
CREATE TRIGGER trg_message_draft_updated_at BEFORE UPDATE ON message_draft FOR EACH ROW EXECUTE FUNCTION set_updated_at();
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_custom_emoji_updated_at ON custom_emoji;
|
||||
CREATE TRIGGER trg_custom_emoji_updated_at BEFORE UPDATE ON custom_emoji FOR EACH ROW EXECUTE FUNCTION set_updated_at();
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_thread_read_state_updated_at ON thread_read_state;
|
||||
CREATE TRIGGER trg_thread_read_state_updated_at BEFORE UPDATE ON thread_read_state FOR EACH ROW EXECUTE FUNCTION set_updated_at();
|
||||
@@ -0,0 +1,187 @@
|
||||
-- 010: Channel Kinds — text, voice, stage, forum, announcement
|
||||
--
|
||||
-- ALTER:
|
||||
-- channel — add channel_kind + voice/forum/stage fields
|
||||
-- message_thread — add forum post fields (title, tags, pinned, locked)
|
||||
--
|
||||
-- New tables:
|
||||
-- forum_tag, voice_participant, stage,
|
||||
-- message_poll, message_poll_option, message_poll_vote
|
||||
|
||||
-- ============================================================
|
||||
-- 1. ALTER channel — add channel_kind + voice/forum/stage fields
|
||||
-- ============================================================
|
||||
|
||||
ALTER TABLE channel ADD COLUMN IF NOT EXISTS channel_kind TEXT NOT NULL DEFAULT 'text';
|
||||
ALTER TABLE channel ADD COLUMN IF NOT EXISTS position INTEGER NULL;
|
||||
ALTER TABLE channel ADD COLUMN IF NOT EXISTS nsfw BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
|
||||
-- Voice / Stage specific
|
||||
ALTER TABLE channel ADD COLUMN IF NOT EXISTS bitrate INTEGER NULL;
|
||||
ALTER TABLE channel ADD COLUMN IF NOT EXISTS user_limit INTEGER NULL;
|
||||
ALTER TABLE channel ADD COLUMN IF NOT EXISTS rtc_region TEXT NULL;
|
||||
|
||||
-- Forum specific
|
||||
ALTER TABLE channel ADD COLUMN IF NOT EXISTS default_auto_archive_duration INTEGER NULL;
|
||||
ALTER TABLE channel ADD COLUMN IF NOT EXISTS default_reaction_emoji TEXT NULL;
|
||||
ALTER TABLE channel ADD COLUMN IF NOT EXISTS default_sort_order TEXT NULL;
|
||||
ALTER TABLE channel ADD COLUMN IF NOT EXISTS default_forum_layout TEXT NULL;
|
||||
ALTER TABLE channel ADD COLUMN IF NOT EXISTS require_tag BOOLEAN NULL;
|
||||
ALTER TABLE channel ADD COLUMN IF NOT EXISTS available_tags JSONB NULL;
|
||||
ALTER TABLE channel ADD COLUMN IF NOT EXISTS default_thread_rate_limit INTEGER NULL;
|
||||
|
||||
-- General
|
||||
ALTER TABLE channel ADD COLUMN IF NOT EXISTS rate_limit_per_user INTEGER NULL;
|
||||
ALTER TABLE channel ADD COLUMN IF NOT EXISTS parent_channel_id UUID NULL REFERENCES channel(id) ON DELETE SET NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_channel_channel_kind ON channel (channel_kind);
|
||||
CREATE INDEX IF NOT EXISTS idx_channel_parent_channel_id ON channel (parent_channel_id);
|
||||
|
||||
-- ============================================================
|
||||
-- 2. ALTER message_thread — add forum post fields
|
||||
-- ============================================================
|
||||
|
||||
ALTER TABLE message_thread ADD COLUMN IF NOT EXISTS title TEXT NULL;
|
||||
ALTER TABLE message_thread ADD COLUMN IF NOT EXISTS tags TEXT[] NOT NULL DEFAULT '{}';
|
||||
ALTER TABLE message_thread ADD COLUMN IF NOT EXISTS pinned BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
ALTER TABLE message_thread ADD COLUMN IF NOT EXISTS locked BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
ALTER TABLE message_thread ADD COLUMN IF NOT EXISTS rate_limit_per_user INTEGER NULL;
|
||||
ALTER TABLE message_thread ADD COLUMN IF NOT EXISTS auto_archive_at TIMESTAMPTZ NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_message_thread_pinned ON message_thread (pinned) WHERE pinned;
|
||||
|
||||
-- ============================================================
|
||||
-- 3. Forum Tags
|
||||
-- ============================================================
|
||||
|
||||
-- models/channels/forum_tags.rs → forum_tag
|
||||
CREATE TABLE IF NOT EXISTS forum_tag (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
channel_id UUID NOT NULL REFERENCES channel(id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL,
|
||||
emoji_id TEXT NULL,
|
||||
emoji_name TEXT NULL,
|
||||
moderated BOOLEAN NOT NULL,
|
||||
position INTEGER NOT NULL,
|
||||
created_by UUID NOT NULL REFERENCES "user"(id) ON DELETE CASCADE,
|
||||
created_at TIMESTAMPTZ NOT NULL,
|
||||
updated_at TIMESTAMPTZ NOT NULL,
|
||||
CONSTRAINT uq_forum_tag_channel_name UNIQUE (channel_id, name)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_forum_tag_channel_id ON forum_tag (channel_id);
|
||||
|
||||
-- ============================================================
|
||||
-- 4. Voice Participants
|
||||
-- ============================================================
|
||||
|
||||
-- models/channels/voice_participants.rs → voice_participant
|
||||
CREATE TABLE IF NOT EXISTS voice_participant (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
channel_id UUID NOT NULL REFERENCES channel(id) ON DELETE CASCADE,
|
||||
user_id UUID NOT NULL REFERENCES "user"(id) ON DELETE CASCADE,
|
||||
session_id TEXT NULL,
|
||||
deafened BOOLEAN NOT NULL,
|
||||
muted BOOLEAN NOT NULL,
|
||||
self_deafened BOOLEAN NOT NULL,
|
||||
self_muted BOOLEAN NOT NULL,
|
||||
self_video BOOLEAN NOT NULL,
|
||||
streaming BOOLEAN NOT NULL,
|
||||
speaking BOOLEAN NOT NULL,
|
||||
joined_at TIMESTAMPTZ NOT NULL,
|
||||
left_at TIMESTAMPTZ NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_voice_participant_channel_id ON voice_participant (channel_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_voice_participant_user_id ON voice_participant (user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_voice_participant_channel_user ON voice_participant (channel_id, user_id);
|
||||
|
||||
-- ============================================================
|
||||
-- 5. Stages
|
||||
-- ============================================================
|
||||
|
||||
-- models/channels/stages.rs → stage
|
||||
CREATE TABLE IF NOT EXISTS stage (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
channel_id UUID NOT NULL REFERENCES channel(id) ON DELETE CASCADE,
|
||||
topic TEXT NOT NULL,
|
||||
privacy_level TEXT NOT NULL,
|
||||
discoverable BOOLEAN NOT NULL,
|
||||
started_by UUID NOT NULL REFERENCES "user"(id) ON DELETE CASCADE,
|
||||
started_at TIMESTAMPTZ NOT NULL,
|
||||
ended_at TIMESTAMPTZ NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL,
|
||||
updated_at TIMESTAMPTZ NOT NULL,
|
||||
CONSTRAINT uq_stage_active_channel UNIQUE (channel_id, ended_at)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_stage_channel_id ON stage (channel_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_stage_channel_active ON stage (channel_id) WHERE ended_at IS NULL;
|
||||
|
||||
-- ============================================================
|
||||
-- 6. Message Polls
|
||||
-- ============================================================
|
||||
|
||||
-- models/channels/message_polls.rs → message_poll
|
||||
CREATE TABLE IF NOT EXISTS message_poll (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
message_id UUID NOT NULL REFERENCES message(id) ON DELETE CASCADE,
|
||||
channel_id UUID NOT NULL REFERENCES channel(id) ON DELETE CASCADE,
|
||||
question TEXT NOT NULL,
|
||||
description TEXT NULL,
|
||||
layout TEXT NOT NULL,
|
||||
allow_multiselect BOOLEAN NOT NULL,
|
||||
duration_hours INTEGER NULL,
|
||||
ends_at TIMESTAMPTZ NULL,
|
||||
total_votes BIGINT NOT NULL,
|
||||
metadata JSONB NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL,
|
||||
updated_at TIMESTAMPTZ NOT NULL,
|
||||
CONSTRAINT uq_message_poll_message_id UNIQUE (message_id)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_message_poll_channel_id ON message_poll (channel_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_message_poll_ends_at ON message_poll (ends_at) WHERE ends_at IS NOT NULL;
|
||||
|
||||
-- ============================================================
|
||||
-- 7. Message Poll Options
|
||||
-- ============================================================
|
||||
|
||||
-- models/channels/message_poll_options.rs → message_poll_option
|
||||
CREATE TABLE IF NOT EXISTS message_poll_option (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
poll_id UUID NOT NULL REFERENCES message_poll(id) ON DELETE CASCADE,
|
||||
position INTEGER NOT NULL,
|
||||
text TEXT NOT NULL,
|
||||
emoji_id TEXT NULL,
|
||||
emoji_name TEXT NULL,
|
||||
vote_count BIGINT NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_message_poll_option_poll_id ON message_poll_option (poll_id);
|
||||
|
||||
-- ============================================================
|
||||
-- 8. Message Poll Votes
|
||||
-- ============================================================
|
||||
|
||||
-- models/channels/message_poll_votes.rs → message_poll_vote
|
||||
CREATE TABLE IF NOT EXISTS message_poll_vote (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
poll_id UUID NOT NULL REFERENCES message_poll(id) ON DELETE CASCADE,
|
||||
option_id UUID NOT NULL REFERENCES message_poll_option(id) ON DELETE CASCADE,
|
||||
user_id UUID NOT NULL REFERENCES "user"(id) ON DELETE CASCADE,
|
||||
voted_at TIMESTAMPTZ NOT NULL,
|
||||
CONSTRAINT uq_message_poll_vote_user_option UNIQUE (poll_id, option_id, user_id)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_message_poll_vote_poll_id ON message_poll_vote (poll_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_message_poll_vote_user_id ON message_poll_vote (user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_message_poll_vote_option_id ON message_poll_vote (option_id);
|
||||
|
||||
-- ============================================================
|
||||
-- 9. Triggers — auto-refresh updated_at
|
||||
-- ============================================================
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_forum_tag_updated_at ON forum_tag;
|
||||
CREATE TRIGGER trg_forum_tag_updated_at BEFORE UPDATE ON forum_tag FOR EACH ROW EXECUTE FUNCTION set_updated_at();
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_stage_updated_at ON stage;
|
||||
CREATE TRIGGER trg_stage_updated_at BEFORE UPDATE ON stage FOR EACH ROW EXECUTE FUNCTION set_updated_at();
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_message_poll_updated_at ON message_poll;
|
||||
CREATE TRIGGER trg_message_poll_updated_at BEFORE UPDATE ON message_poll FOR EACH ROW EXECUTE FUNCTION set_updated_at();
|
||||
@@ -0,0 +1,145 @@
|
||||
-- 011: Announcement Channel — articles, comments, reactions, follows, cross-posts
|
||||
--
|
||||
-- New tables:
|
||||
-- article, article_comment, article_reaction,
|
||||
-- channel_follow, article_cross_post
|
||||
|
||||
-- ============================================================
|
||||
-- 1. Articles
|
||||
-- ============================================================
|
||||
|
||||
-- models/channels/articles.rs → article
|
||||
CREATE TABLE IF NOT EXISTS article (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
channel_id UUID NOT NULL REFERENCES channel(id) ON DELETE CASCADE,
|
||||
author_id UUID NOT NULL REFERENCES "user"(id) ON DELETE CASCADE,
|
||||
title TEXT NOT NULL,
|
||||
slug TEXT NOT NULL,
|
||||
summary TEXT NULL,
|
||||
body TEXT NOT NULL,
|
||||
cover_image_url TEXT NULL,
|
||||
status TEXT NOT NULL,
|
||||
visibility TEXT NOT NULL,
|
||||
tags TEXT[] NOT NULL,
|
||||
published_at TIMESTAMPTZ NULL,
|
||||
published_by UUID NULL REFERENCES "user"(id) ON DELETE SET NULL,
|
||||
scheduled_at TIMESTAMPTZ NULL,
|
||||
unpublished_at TIMESTAMPTZ NULL,
|
||||
views_count BIGINT NOT NULL DEFAULT 0,
|
||||
comments_count BIGINT NOT NULL DEFAULT 0,
|
||||
reactions_count BIGINT NOT NULL DEFAULT 0,
|
||||
cross_posted BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
cross_posted_from UUID NULL REFERENCES article(id) ON DELETE SET NULL,
|
||||
metadata JSONB NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL,
|
||||
updated_at TIMESTAMPTZ NOT NULL,
|
||||
deleted_at TIMESTAMPTZ NULL,
|
||||
CONSTRAINT uq_article_channel_slug UNIQUE (channel_id, slug)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_article_channel_id ON article (channel_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_article_author_id ON article (author_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_article_status ON article (status);
|
||||
CREATE INDEX IF NOT EXISTS idx_article_published_at ON article (published_at DESC) WHERE published_at IS NOT NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_article_cross_posted_from ON article (cross_posted_from) WHERE cross_posted_from IS NOT NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_article_deleted ON article (deleted_at) WHERE deleted_at IS NOT NULL;
|
||||
|
||||
-- ============================================================
|
||||
-- 2. Article Comments
|
||||
-- ============================================================
|
||||
|
||||
-- models/channels/article_comments.rs → article_comment
|
||||
CREATE TABLE IF NOT EXISTS article_comment (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
article_id UUID NOT NULL REFERENCES article(id) ON DELETE CASCADE,
|
||||
channel_id UUID NOT NULL REFERENCES channel(id) ON DELETE CASCADE,
|
||||
author_id UUID NOT NULL REFERENCES "user"(id) ON DELETE CASCADE,
|
||||
parent_comment_id UUID NULL REFERENCES article_comment(id) ON DELETE CASCADE,
|
||||
body TEXT NOT NULL,
|
||||
edited_at TIMESTAMPTZ NULL,
|
||||
deleted_at TIMESTAMPTZ NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL,
|
||||
updated_at TIMESTAMPTZ NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_article_comment_article_id ON article_comment (article_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_article_comment_author_id ON article_comment (author_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_article_comment_parent ON article_comment (parent_comment_id) WHERE parent_comment_id IS NOT NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_article_comment_deleted ON article_comment (deleted_at) WHERE deleted_at IS NOT NULL;
|
||||
|
||||
-- ============================================================
|
||||
-- 3. Article Reactions
|
||||
-- ============================================================
|
||||
|
||||
-- models/channels/article_reactions.rs → article_reaction
|
||||
CREATE TABLE IF NOT EXISTS article_reaction (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
article_id UUID NOT NULL REFERENCES article(id) ON DELETE CASCADE,
|
||||
channel_id UUID NOT NULL REFERENCES channel(id) ON DELETE CASCADE,
|
||||
user_id UUID NOT NULL REFERENCES "user"(id) ON DELETE CASCADE,
|
||||
content TEXT NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL,
|
||||
CONSTRAINT uq_article_reaction UNIQUE (article_id, user_id, content)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_article_reaction_article_id ON article_reaction (article_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_article_reaction_user_id ON article_reaction (user_id);
|
||||
|
||||
-- ============================================================
|
||||
-- 4. Channel Follows
|
||||
-- ============================================================
|
||||
|
||||
-- models/channels/channel_follows.rs → channel_follow
|
||||
CREATE TABLE IF NOT EXISTS channel_follow (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
source_channel_id UUID NOT NULL REFERENCES channel(id) ON DELETE CASCADE,
|
||||
target_workspace_id UUID NOT NULL REFERENCES workspace(id) ON DELETE CASCADE,
|
||||
target_channel_id UUID NULL REFERENCES channel(id) ON DELETE SET NULL,
|
||||
webhook_url TEXT NULL,
|
||||
webhook_secret_ciphertext TEXT NULL,
|
||||
enabled BOOLEAN NOT NULL,
|
||||
followed_by UUID NOT NULL REFERENCES "user"(id) ON DELETE CASCADE,
|
||||
unfollowed_at TIMESTAMPTZ NULL,
|
||||
last_delivery_at TIMESTAMPTZ NULL,
|
||||
last_delivery_status TEXT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL,
|
||||
updated_at TIMESTAMPTZ NOT NULL,
|
||||
CONSTRAINT uq_channel_follow_source_target UNIQUE (source_channel_id, target_workspace_id, target_channel_id)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_channel_follow_source ON channel_follow (source_channel_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_channel_follow_target_ws ON channel_follow (target_workspace_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_channel_follow_target_channel ON channel_follow (target_channel_id) WHERE target_channel_id IS NOT NULL;
|
||||
|
||||
-- ============================================================
|
||||
-- 5. Article Cross-Posts
|
||||
-- ============================================================
|
||||
|
||||
-- models/channels/article_cross_posts.rs → article_cross_post
|
||||
CREATE TABLE IF NOT EXISTS article_cross_post (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
article_id UUID NOT NULL REFERENCES article(id) ON DELETE CASCADE,
|
||||
follow_id UUID NOT NULL REFERENCES channel_follow(id) ON DELETE CASCADE,
|
||||
target_workspace_id UUID NOT NULL REFERENCES workspace(id) ON DELETE CASCADE,
|
||||
target_channel_id UUID NULL REFERENCES channel(id) ON DELETE SET NULL,
|
||||
status TEXT NOT NULL,
|
||||
attempts INTEGER NOT NULL DEFAULT 0,
|
||||
last_error TEXT NULL,
|
||||
sent_at TIMESTAMPTZ NULL,
|
||||
delivered_at TIMESTAMPTZ NULL,
|
||||
failed_at TIMESTAMPTZ NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_article_cross_post_article_id ON article_cross_post (article_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_article_cross_post_follow_id ON article_cross_post (follow_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_article_cross_post_status ON article_cross_post (status);
|
||||
CREATE INDEX IF NOT EXISTS idx_article_cross_post_target_ws ON article_cross_post (target_workspace_id);
|
||||
|
||||
-- ============================================================
|
||||
-- 6. Triggers — auto-refresh updated_at
|
||||
-- ============================================================
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_article_updated_at ON article;
|
||||
CREATE TRIGGER trg_article_updated_at BEFORE UPDATE ON article FOR EACH ROW EXECUTE FUNCTION set_updated_at();
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_article_comment_updated_at ON article_comment;
|
||||
CREATE TRIGGER trg_article_comment_updated_at BEFORE UPDATE ON article_comment FOR EACH ROW EXECUTE FUNCTION set_updated_at();
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_channel_follow_updated_at ON channel_follow;
|
||||
CREATE TRIGGER trg_channel_follow_updated_at BEFORE UPDATE ON channel_follow FOR EACH ROW EXECUTE FUNCTION set_updated_at();
|
||||
@@ -0,0 +1,13 @@
|
||||
ALTER TABLE message ADD COLUMN IF NOT EXISTS seq BIGINT NOT NULL DEFAULT 0;
|
||||
|
||||
WITH ranked AS (
|
||||
SELECT id, ROW_NUMBER() OVER (PARTITION BY channel_id ORDER BY created_at ASC, id ASC) AS rn
|
||||
FROM message
|
||||
WHERE seq = 0
|
||||
)
|
||||
UPDATE message m
|
||||
SET seq = ranked.rn
|
||||
FROM ranked
|
||||
WHERE m.id = ranked.id;
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_message_channel_seq ON message (channel_id, seq);
|
||||
Reference in New Issue
Block a user