174 lines
6.5 KiB
PL/PgSQL
174 lines
6.5 KiB
PL/PgSQL
-- Chattyness Chat Schema Tables
|
|
-- PostgreSQL 18
|
|
--
|
|
-- Messages, whispers, shouts, and reactions
|
|
-- Load via: psql -f schema/tables/050_chat.sql
|
|
--
|
|
-- NOTE: This table is designed for future partitioning by created_at.
|
|
-- When message volume grows, convert to a partitioned table:
|
|
-- 1. Rename chat.messages to chat.messages_old
|
|
-- 2. Create new partitioned chat.messages with PARTITION BY RANGE (created_at)
|
|
-- 3. Create monthly partitions (e.g., chat.messages_2025_01)
|
|
-- 4. Migrate data from old table
|
|
-- 5. Consider pg_partman extension for automatic partition management
|
|
-- 6. Add archive table for long-term storage of old partitions
|
|
|
|
\set ON_ERROR_STOP on
|
|
|
|
BEGIN;
|
|
|
|
-- =============================================================================
|
|
-- Instance Messages (Scene-wide chat)
|
|
-- =============================================================================
|
|
-- Messages visible to everyone in an instance.
|
|
-- =============================================================================
|
|
|
|
CREATE TABLE chat.messages (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
|
|
instance_id UUID NOT NULL REFERENCES scene.instances(id) ON DELETE CASCADE,
|
|
|
|
-- Sender (either user or guest)
|
|
user_id UUID REFERENCES auth.users(id) ON DELETE SET NULL,
|
|
guest_session_id UUID REFERENCES auth.guest_sessions(id) ON DELETE SET NULL,
|
|
|
|
-- Cached sender info (in case account deleted)
|
|
sender_name public.display_name NOT NULL,
|
|
|
|
-- Message content
|
|
message_type chat.message_type NOT NULL DEFAULT 'normal',
|
|
content TEXT NOT NULL,
|
|
|
|
-- Position when message was sent (for speech bubble placement)
|
|
position public.virtual_point,
|
|
|
|
-- Reply threading
|
|
reply_to_id UUID REFERENCES chat.messages(id) ON DELETE SET NULL,
|
|
|
|
-- Moderation
|
|
is_deleted BOOLEAN NOT NULL DEFAULT false,
|
|
deleted_by UUID REFERENCES auth.users(id) ON DELETE SET NULL,
|
|
deleted_at TIMESTAMPTZ,
|
|
|
|
-- Timestamps
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
|
|
-- Either user_id or guest_session_id must be set
|
|
CONSTRAINT chk_chat_messages_sender CHECK (
|
|
(user_id IS NOT NULL AND guest_session_id IS NULL) OR
|
|
(user_id IS NULL AND guest_session_id IS NOT NULL)
|
|
)
|
|
);
|
|
|
|
COMMENT ON TABLE chat.messages IS 'Instance messages (design supports future time-based partitioning)';
|
|
COMMENT ON COLUMN chat.messages.position IS 'Sender position when message sent (for speech bubbles)';
|
|
COMMENT ON COLUMN chat.messages.reply_to_id IS 'ID of message being replied to';
|
|
|
|
CREATE INDEX idx_chat_messages_instance_time ON chat.messages (instance_id, created_at DESC);
|
|
CREATE INDEX idx_chat_messages_user ON chat.messages (user_id, created_at DESC)
|
|
WHERE user_id IS NOT NULL;
|
|
CREATE INDEX idx_chat_messages_reply ON chat.messages (reply_to_id)
|
|
WHERE reply_to_id IS NOT NULL;
|
|
CREATE INDEX idx_chat_messages_created ON chat.messages (created_at DESC);
|
|
|
|
-- =============================================================================
|
|
-- Shouts (Server-wide broadcasts)
|
|
-- =============================================================================
|
|
-- Messages broadcast to all users on the server.
|
|
-- Typically limited to admins or special events.
|
|
-- =============================================================================
|
|
|
|
CREATE TABLE chat.shouts (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
|
|
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE SET NULL,
|
|
sender_name public.display_name NOT NULL,
|
|
|
|
content TEXT NOT NULL,
|
|
|
|
-- Targeting
|
|
target_realm_id UUID REFERENCES realm.realms(id) ON DELETE CASCADE, -- NULL = all realms
|
|
|
|
-- TTL for display
|
|
expires_at TIMESTAMPTZ,
|
|
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
COMMENT ON TABLE chat.shouts IS 'Server-wide or realm-wide broadcast messages';
|
|
COMMENT ON COLUMN chat.shouts.target_realm_id IS 'NULL = broadcast to all realms';
|
|
|
|
CREATE INDEX idx_chat_shouts_time ON chat.shouts (created_at DESC);
|
|
CREATE INDEX idx_chat_shouts_realm ON chat.shouts (target_realm_id, created_at DESC)
|
|
WHERE target_realm_id IS NOT NULL;
|
|
|
|
-- =============================================================================
|
|
-- Whispers (Private Direct Messages)
|
|
-- =============================================================================
|
|
-- Private messages between two users, cross-realm capable.
|
|
-- =============================================================================
|
|
|
|
CREATE TABLE chat.whispers (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
|
|
-- Sender and recipient
|
|
sender_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
|
recipient_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
|
|
|
content TEXT NOT NULL,
|
|
|
|
-- Read status
|
|
read_at TIMESTAMPTZ,
|
|
|
|
-- Moderation
|
|
is_deleted_by_sender BOOLEAN NOT NULL DEFAULT false,
|
|
is_deleted_by_recipient BOOLEAN NOT NULL DEFAULT false,
|
|
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
|
|
CONSTRAINT chk_chat_whispers_not_self CHECK (sender_id != recipient_id)
|
|
);
|
|
|
|
COMMENT ON TABLE chat.whispers IS 'Private direct messages between users';
|
|
|
|
CREATE INDEX idx_chat_whispers_sender ON chat.whispers (sender_id, created_at DESC);
|
|
CREATE INDEX idx_chat_whispers_recipient ON chat.whispers (recipient_id, created_at DESC);
|
|
CREATE INDEX idx_chat_whispers_unread ON chat.whispers (recipient_id, read_at)
|
|
WHERE read_at IS NULL;
|
|
|
|
-- Composite index for conversation view between two users
|
|
CREATE INDEX idx_chat_whispers_conversation ON chat.whispers (
|
|
LEAST(sender_id, recipient_id),
|
|
GREATEST(sender_id, recipient_id),
|
|
created_at DESC
|
|
);
|
|
|
|
-- =============================================================================
|
|
-- Message Reactions
|
|
-- =============================================================================
|
|
-- Emoji reactions to channel messages.
|
|
-- =============================================================================
|
|
|
|
CREATE TABLE chat.reactions (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
|
|
message_id UUID NOT NULL REFERENCES chat.messages(id) ON DELETE CASCADE,
|
|
|
|
-- Reactor
|
|
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
|
|
|
-- Reaction (emoji or predefined reaction code)
|
|
reaction TEXT NOT NULL CHECK (length(reaction) <= 32),
|
|
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
|
|
-- One reaction type per user per message
|
|
CONSTRAINT uq_chat_reactions UNIQUE (message_id, user_id, reaction)
|
|
);
|
|
|
|
COMMENT ON TABLE chat.reactions IS 'Emoji reactions to channel messages';
|
|
|
|
CREATE INDEX idx_chat_reactions_message ON chat.reactions (message_id);
|
|
CREATE INDEX idx_chat_reactions_user ON chat.reactions (user_id);
|
|
|
|
COMMIT;
|