feat(db): add sqlx migrate feature and renumber migration files

- Add 'migrate' feature to sqlx dependency
- Renumber migrations to fix duplicate version numbers (two 014 files)
- Re-sequence migrations 009-012 for continuous ordering
- Add ALTER TABLE ADD COLUMN IF NOT EXISTS baseline for notification
  table to handle existing databases missing newer columns
- Remove deleted IM migration files (009-012) that were superseded
This commit is contained in:
zhenyi
2026-06-10 18:48:43 +08:00
parent d98e4d59e3
commit d6c468a9fc
11 changed files with 203 additions and 947 deletions
+11
View File
@@ -1905,6 +1905,17 @@ CREATE TABLE IF NOT EXISTS notification (
deleted_at TIMESTAMPTZ NULL
);
ALTER TABLE notification ADD COLUMN IF NOT EXISTS repo_id UUID NULL REFERENCES repo(id) ON DELETE CASCADE;
ALTER TABLE notification ADD COLUMN IF NOT EXISTS issue_id UUID NULL REFERENCES issue(id) ON DELETE CASCADE;
ALTER TABLE notification ADD COLUMN IF NOT EXISTS pull_request_id UUID NULL REFERENCES pull_request(id) ON DELETE CASCADE;
ALTER TABLE notification ADD COLUMN IF NOT EXISTS channel_id UUID NULL REFERENCES channel(id) ON DELETE CASCADE;
ALTER TABLE notification ADD COLUMN IF NOT EXISTS message_id UUID NULL REFERENCES message(id) ON DELETE CASCADE;
ALTER TABLE notification ADD COLUMN IF NOT EXISTS target_type TEXT NULL;
ALTER TABLE notification ADD COLUMN IF NOT EXISTS target_id UUID NULL;
ALTER TABLE notification ADD COLUMN IF NOT EXISTS action_url TEXT NULL;
ALTER TABLE notification ADD COLUMN IF NOT EXISTS priority TEXT NOT NULL DEFAULT 'normal';
ALTER TABLE notification ADD COLUMN IF NOT EXISTS metadata JSONB NULL;
ALTER TABLE notification ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ NULL;
CREATE INDEX IF NOT EXISTS idx_notification_user_id ON notification (user_id);
CREATE INDEX IF NOT EXISTS idx_notification_actor_id ON notification (actor_id);
CREATE INDEX IF NOT EXISTS idx_notification_workspace_id ON notification (workspace_id);
+41
View File
@@ -0,0 +1,41 @@
-- 013: Fix record_workspace_audit() for tables without 'id' column.
-- workspace_billing, workspace_custom_branding, workspace_settings
-- use workspace_id as their PRIMARY KEY and have no separate id column.
-- The original function unconditionally referenced NEW.id, which crashed
-- with "record 'new' has no field 'id'" (42703).
CREATE OR REPLACE FUNCTION record_workspace_audit()
RETURNS TRIGGER AS $$
DECLARE
ws_id UUID;
actor UUID := app_current_user_id();
action_text TEXT;
target_id_val UUID;
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;
-- Attempt to read NEW.id / OLD.id. For tables whose PK is workspace_id
-- there is no id column; catch the error and use ws_id instead.
BEGIN
target_id_val := COALESCE(NEW.id, OLD.id);
EXCEPTION
WHEN undefined_column THEN
target_id_val := ws_id;
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, target_id_val, NOW());
RETURN COALESCE(NEW, OLD);
END;
$$ LANGUAGE plpgsql;
-320
View File
@@ -1,320 +0,0 @@
-- 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,24 @@
-- Add deleted_at columns to user-related tables for soft-delete support.
-- Previously, user_delete_account hard-deleted rows from these tables.
-- After this migration, all deletions are soft (mark as deleted).
ALTER TABLE "user" ADD COLUMN IF NOT EXISTS restore_token_hash VARCHAR(64);
ALTER TABLE "user" ADD COLUMN IF NOT EXISTS restore_token_expires_at TIMESTAMPTZ;
ALTER TABLE user_2fa ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ;
ALTER TABLE user_activity ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ;
ALTER TABLE user_appearance ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ;
ALTER TABLE user_block ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ;
ALTER TABLE user_device ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ;
ALTER TABLE user_follow ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ;
ALTER TABLE user_mail ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ;
ALTER TABLE user_notify_setting ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ;
ALTER TABLE user_oauth ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ;
ALTER TABLE user_password ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ;
ALTER TABLE user_password_reset ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ;
ALTER TABLE user_presence ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ;
ALTER TABLE user_profile ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ;
ALTER TABLE user_security_log ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ;
CREATE INDEX IF NOT EXISTS idx_user_security_log_user_deleted ON user_security_log(user_id, deleted_at);
CREATE INDEX IF NOT EXISTS idx_user_activity_user_deleted ON user_activity(user_id, deleted_at);
-187
View File
@@ -1,187 +0,0 @@
-- 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();
-145
View File
@@ -1,145 +0,0 @@
-- 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();
+13
View File
@@ -0,0 +1,13 @@
-- Add repository-level feature toggles, merge settings, topics, and homepage.
-- models/repos/repo.rs → repo
ALTER TABLE repo ADD COLUMN IF NOT EXISTS topics TEXT[] NOT NULL DEFAULT '{}';
ALTER TABLE repo ADD COLUMN IF NOT EXISTS homepage TEXT;
ALTER TABLE repo ADD COLUMN IF NOT EXISTS has_issues BOOLEAN NOT NULL DEFAULT true;
ALTER TABLE repo ADD COLUMN IF NOT EXISTS has_wiki BOOLEAN NOT NULL DEFAULT true;
ALTER TABLE repo ADD COLUMN IF NOT EXISTS has_pull_requests BOOLEAN NOT NULL DEFAULT true;
ALTER TABLE repo ADD COLUMN IF NOT EXISTS allow_forking BOOLEAN NOT NULL DEFAULT true;
ALTER TABLE repo ADD COLUMN IF NOT EXISTS allow_merge_commit BOOLEAN NOT NULL DEFAULT true;
ALTER TABLE repo ADD COLUMN IF NOT EXISTS allow_squash_merge BOOLEAN NOT NULL DEFAULT true;
ALTER TABLE repo ADD COLUMN IF NOT EXISTS allow_rebase_merge BOOLEAN NOT NULL DEFAULT true;
ALTER TABLE repo ADD COLUMN IF NOT EXISTS delete_branch_on_merge BOOLEAN NOT NULL DEFAULT false;
-13
View File
@@ -1,13 +0,0 @@
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);
@@ -0,0 +1,44 @@
-- Release Assets - binary attachments for releases
CREATE TABLE IF NOT EXISTS repo_release_asset (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
release_id UUID NOT NULL REFERENCES repo_release(id) ON DELETE CASCADE,
filename TEXT NOT NULL,
size_bytes BIGINT NOT NULL,
mime_type TEXT NOT NULL DEFAULT 'application/octet-stream',
storage_path TEXT NOT NULL,
url TEXT,
download_count BIGINT NOT NULL DEFAULT 0,
uploaded_by UUID REFERENCES "user"(id) ON DELETE SET NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
deleted_at TIMESTAMPTZ
);
CREATE INDEX IF NOT EXISTS idx_repo_release_asset_release ON repo_release_asset(release_id, deleted_at);
CREATE INDEX IF NOT EXISTS idx_repo_release_asset_deleted ON repo_release_asset(deleted_at) WHERE deleted_at IS NOT NULL;
-- PR Templates (mirrors issue_template structure)
CREATE TABLE IF NOT EXISTS pr_template (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
repo_id UUID NOT NULL REFERENCES repo(id) ON DELETE CASCADE,
name TEXT NOT NULL,
description TEXT,
title_template TEXT,
body_template TEXT NOT NULL DEFAULT '',
labels TEXT[] NOT NULL DEFAULT '{}',
active BOOLEAN NOT NULL DEFAULT true,
created_by UUID REFERENCES "user"(id) ON DELETE SET NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_pr_template_repo ON pr_template(repo_id);
-- PR Review Requests
CREATE TABLE IF NOT EXISTS pr_review_request (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
pull_request_id UUID NOT NULL REFERENCES pull_request(id) ON DELETE CASCADE,
reviewer_id UUID NOT NULL REFERENCES "user"(id) ON DELETE CASCADE,
requested_by UUID NOT NULL REFERENCES "user"(id) ON DELETE CASCADE,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_pr_review_request_pr ON pr_review_request(pull_request_id);
CREATE UNIQUE INDEX IF NOT EXISTS idx_pr_review_request_pr_reviewer ON pr_review_request(pull_request_id, reviewer_id);