workspace-v1/projects/gamilit/database/ddl/schemas/communication/tables/01-messages.sql
Adrian Flores Cortes 967ab360bb Initial commit: Workspace v1 with 3-layer architecture
Structure:
- control-plane/: Registries, SIMCO directives, CI/CD templates
- projects/: Gamilit, ERP-Suite, Trading-Platform, Betting-Analytics
- shared/: Libs catalog, knowledge-base

Key features:
- Centralized port, domain, database, and service registries
- 23 SIMCO directives + 6 fundamental principles
- NEXUS agent profiles with delegation rules
- Validation scripts for workspace integrity
- Dockerfiles for all services
- Path aliases for quick reference

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-23 00:35:19 -06:00

303 lines
11 KiB
PL/PgSQL

-- =============================================================================
-- Table: communication.messages
-- Description: Messages and chat system for teacher-student communication
-- Priority: P1 - Important for communication features
-- User Stories: US-PM-005 (Teacher Communication), US-AE-006 (Communication Management)
-- Created: 2025-11-19
-- =============================================================================
-- Drop table if exists
DROP TABLE IF EXISTS communication.messages CASCADE;
-- Create messages table
CREATE TABLE communication.messages (
-- Primary key
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-- Message relationships
sender_id UUID NOT NULL
REFERENCES auth_management.profiles(id) ON DELETE CASCADE,
recipient_id UUID
REFERENCES auth_management.profiles(id) ON DELETE CASCADE,
-- Context (where message belongs)
classroom_id UUID
REFERENCES social_features.classrooms(id) ON DELETE CASCADE,
thread_id UUID
REFERENCES communication.messages(id) ON DELETE CASCADE, -- For threaded conversations
parent_message_id UUID
REFERENCES communication.messages(id) ON DELETE SET NULL, -- For replies
-- Message content
subject VARCHAR(255), -- For direct messages
content TEXT NOT NULL,
message_type VARCHAR(50) NOT NULL DEFAULT 'direct',
-- Types: 'direct', 'classroom_announcement', 'classroom_chat', 'private_feedback', 'assignment_comment'
-- Attachments and media
attachments JSONB DEFAULT '[]'::jsonb,
-- Example: [{"type": "file", "url": "...", "name": "...", "size": 1024}]
-- Message status
is_read BOOLEAN DEFAULT FALSE,
read_at TIMESTAMPTZ,
is_deleted BOOLEAN DEFAULT FALSE,
deleted_at TIMESTAMPTZ,
deleted_by UUID REFERENCES auth_management.profiles(id) ON DELETE SET NULL,
-- Priority and flags
priority VARCHAR(20) DEFAULT 'normal', -- 'low', 'normal', 'high', 'urgent'
is_pinned BOOLEAN DEFAULT FALSE,
is_archived BOOLEAN DEFAULT FALSE,
requires_response BOOLEAN DEFAULT FALSE,
response_deadline TIMESTAMPTZ,
-- Reactions and engagement
reactions JSONB DEFAULT '{}'::jsonb,
-- Example: {"thumbs_up": ["user-uuid-1", "user-uuid-2"], "heart": ["user-uuid-3"]}
-- Moderation
is_flagged BOOLEAN DEFAULT FALSE,
flagged_reason TEXT,
flagged_by UUID REFERENCES auth_management.profiles(id) ON DELETE SET NULL,
flagged_at TIMESTAMPTZ,
moderation_status VARCHAR(50) DEFAULT 'approved',
-- 'approved', 'pending', 'flagged', 'removed'
-- Metadata
metadata JSONB DEFAULT '{}'::jsonb,
-- Additional context: assignment_id, exercise_id, etc.
-- Audit fields
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
edited_at TIMESTAMPTZ,
edit_count INTEGER DEFAULT 0,
-- Constraints
CONSTRAINT messages_type_valid
CHECK (message_type IN ('direct', 'classroom_announcement', 'classroom_chat', 'private_feedback', 'assignment_comment', 'system')),
CONSTRAINT messages_priority_valid
CHECK (priority IN ('low', 'normal', 'high', 'urgent')),
CONSTRAINT messages_moderation_status_valid
CHECK (moderation_status IN ('approved', 'pending', 'flagged', 'removed')),
CONSTRAINT messages_recipient_or_classroom
CHECK (recipient_id IS NOT NULL OR classroom_id IS NOT NULL)
-- Must have either recipient (direct) or classroom (broadcast)
);
-- =============================================================================
-- Indexes for performance
-- =============================================================================
-- Index for sender's sent messages
CREATE INDEX idx_messages_sender
ON communication.messages(sender_id, created_at DESC)
WHERE is_deleted = FALSE;
-- Index for recipient's inbox
CREATE INDEX idx_messages_recipient
ON communication.messages(recipient_id, created_at DESC)
WHERE is_deleted = FALSE;
-- Index for classroom messages/announcements
CREATE INDEX idx_messages_classroom
ON communication.messages(classroom_id, created_at DESC)
WHERE is_deleted = FALSE;
-- Index for unread messages (important for notifications)
CREATE INDEX idx_messages_unread
ON communication.messages(recipient_id, created_at DESC)
WHERE is_read = FALSE AND is_deleted = FALSE;
-- Index for threaded conversations
CREATE INDEX idx_messages_thread
ON communication.messages(thread_id, created_at ASC)
WHERE thread_id IS NOT NULL AND is_deleted = FALSE;
-- Index for parent-child reply structure
CREATE INDEX idx_messages_parent
ON communication.messages(parent_message_id, created_at ASC)
WHERE parent_message_id IS NOT NULL AND is_deleted = FALSE;
-- Index for flagged messages (moderation)
CREATE INDEX idx_messages_flagged
ON communication.messages(flagged_at DESC)
WHERE is_flagged = TRUE;
-- Index for messages requiring response
CREATE INDEX idx_messages_requiring_response
ON communication.messages(recipient_id, response_deadline)
WHERE requires_response = TRUE AND is_deleted = FALSE;
-- Composite index for classroom + type (efficient filtering)
CREATE INDEX idx_messages_classroom_type
ON communication.messages(classroom_id, message_type, created_at DESC)
WHERE is_deleted = FALSE;
-- GIN index for searching attachments
CREATE INDEX idx_messages_attachments
ON communication.messages USING GIN(attachments);
-- GIN index for metadata search
CREATE INDEX idx_messages_metadata
ON communication.messages USING GIN(metadata);
-- =============================================================================
-- Triggers
-- =============================================================================
-- Trigger for updated_at timestamp
CREATE OR REPLACE FUNCTION communication.update_messages_timestamp()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
-- Track edits
IF OLD.content IS DISTINCT FROM NEW.content THEN
NEW.edited_at = NOW();
NEW.edit_count = COALESCE(OLD.edit_count, 0) + 1;
END IF;
-- Auto-set read_at when is_read changes to true
IF NEW.is_read = TRUE AND OLD.is_read = FALSE THEN
NEW.read_at = NOW();
END IF;
-- Auto-set deleted_at when is_deleted changes to true
IF NEW.is_deleted = TRUE AND OLD.is_deleted = FALSE THEN
NEW.deleted_at = NOW();
END IF;
-- Auto-set flagged_at when is_flagged changes to true
IF NEW.is_flagged = TRUE AND OLD.is_flagged = FALSE THEN
NEW.flagged_at = NOW();
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trigger_update_messages_timestamp
BEFORE UPDATE ON communication.messages
FOR EACH ROW
EXECUTE FUNCTION communication.update_messages_timestamp();
-- =============================================================================
-- Helper function to get unread count
-- =============================================================================
CREATE OR REPLACE FUNCTION communication.get_unread_count(
p_user_id UUID,
p_classroom_id UUID DEFAULT NULL
) RETURNS INTEGER AS $$
DECLARE
v_count INTEGER;
BEGIN
SELECT COUNT(*)
INTO v_count
FROM communication.messages
WHERE recipient_id = p_user_id
AND is_read = FALSE
AND is_deleted = FALSE
AND (p_classroom_id IS NULL OR classroom_id = p_classroom_id);
RETURN v_count;
END;
$$ LANGUAGE plpgsql STABLE;
-- =============================================================================
-- Helper function to mark conversation as read
-- =============================================================================
CREATE OR REPLACE FUNCTION communication.mark_conversation_read(
p_user_id UUID,
p_thread_id UUID
) RETURNS INTEGER AS $$
DECLARE
v_updated INTEGER;
BEGIN
UPDATE communication.messages
SET is_read = TRUE,
read_at = NOW()
WHERE recipient_id = p_user_id
AND thread_id = p_thread_id
AND is_read = FALSE
AND is_deleted = FALSE;
GET DIAGNOSTICS v_updated = ROW_COUNT;
RETURN v_updated;
END;
$$ LANGUAGE plpgsql;
-- =============================================================================
-- View: Recent classroom messages (for chat widget)
-- =============================================================================
CREATE OR REPLACE VIEW communication.recent_classroom_messages AS
SELECT
m.id,
m.classroom_id,
m.sender_id,
p.display_name as sender_name,
p.avatar_url as sender_avatar,
m.content,
m.message_type,
m.attachments,
m.reactions,
m.is_pinned,
m.created_at,
m.edited_at,
m.edit_count,
-- Count replies
(SELECT COUNT(*) FROM communication.messages replies
WHERE replies.parent_message_id = m.id
AND replies.is_deleted = FALSE) as reply_count
FROM communication.messages m
JOIN auth_management.profiles p ON p.id = m.sender_id
WHERE m.classroom_id IS NOT NULL
AND m.is_deleted = FALSE
AND m.parent_message_id IS NULL -- Only top-level messages
ORDER BY
CASE WHEN m.is_pinned THEN 0 ELSE 1 END,
m.created_at DESC;
-- =============================================================================
-- Comments for documentation
-- =============================================================================
COMMENT ON TABLE communication.messages IS
'Messages and chat system for teacher-student communication. Supports direct messages, classroom announcements, threaded conversations, and assignment feedback.';
COMMENT ON COLUMN communication.messages.message_type IS
'Type of message: direct (1-to-1), classroom_announcement (teacher to all), classroom_chat (group), private_feedback (assignment/exercise feedback), assignment_comment';
COMMENT ON COLUMN communication.messages.attachments IS
'JSON array of file attachments with structure: [{"type": "file|image|video", "url": "...", "name": "...", "size": 1024, "mime_type": "..."}]';
COMMENT ON COLUMN communication.messages.reactions IS
'JSON object tracking emoji reactions: {"thumbs_up": ["user-uuid-1"], "heart": ["user-uuid-2", "user-uuid-3"]}';
COMMENT ON COLUMN communication.messages.thread_id IS
'For grouping messages in a conversation thread. Set to the first message ID in the thread.';
COMMENT ON COLUMN communication.messages.parent_message_id IS
'For direct replies to a specific message. Creates a tree structure within threads.';
COMMENT ON FUNCTION communication.get_unread_count IS
'Get count of unread messages for a user, optionally filtered by classroom.
Usage: SELECT communication.get_unread_count(user_uuid, classroom_uuid);';
COMMENT ON FUNCTION communication.mark_conversation_read IS
'Mark all messages in a thread as read for a user.
Usage: SELECT communication.mark_conversation_read(user_uuid, thread_uuid);';
-- =============================================================================
-- Grant permissions
-- =============================================================================
GRANT SELECT, INSERT, UPDATE, DELETE ON communication.messages TO gamilit_user;
GRANT SELECT ON communication.recent_classroom_messages TO gamilit_user;
GRANT EXECUTE ON FUNCTION communication.get_unread_count TO gamilit_user;
GRANT EXECUTE ON FUNCTION communication.mark_conversation_read TO gamilit_user;