From 665354b33d10996934614e403e64fa77857b8b93 Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Thu, 29 Jan 2026 16:11:30 +0200 Subject: [PATCH] migration: add public_chat_snapshots table, drop deprecated columns --- .../84_add_public_chat_snapshots_table.py | 157 ++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 surfsense_backend/alembic/versions/84_add_public_chat_snapshots_table.py diff --git a/surfsense_backend/alembic/versions/84_add_public_chat_snapshots_table.py b/surfsense_backend/alembic/versions/84_add_public_chat_snapshots_table.py new file mode 100644 index 000000000..39d7bf2dd --- /dev/null +++ b/surfsense_backend/alembic/versions/84_add_public_chat_snapshots_table.py @@ -0,0 +1,157 @@ +"""Add public_chat_snapshots table and remove deprecated columns from new_chat_threads + +Revision ID: 84 +Revises: 83 +Create Date: 2026-01-29 + +Changes: +1. Create public_chat_snapshots table for immutable public chat sharing +2. Drop deprecated columns from new_chat_threads: + - public_share_token (moved to snapshots) + - public_share_enabled (replaced by snapshot existence) + - clone_pending (single-phase clone) +3. Drop related indexes +""" + +from collections.abc import Sequence + +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "84" +down_revision: str | None = "83" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None + + +def upgrade() -> None: + """Create public_chat_snapshots table and remove deprecated columns.""" + + # 1. Create public_chat_snapshots table + op.execute( + """ + CREATE TABLE IF NOT EXISTS public_chat_snapshots ( + id SERIAL PRIMARY KEY, + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), + + -- Link to original thread (CASCADE DELETE) + thread_id INTEGER NOT NULL + REFERENCES new_chat_threads(id) ON DELETE CASCADE, + + -- Public access token (unique URL identifier) + share_token VARCHAR(64) NOT NULL UNIQUE, + + -- SHA-256 hash of message content for deduplication + content_hash VARCHAR(64) NOT NULL, + + -- Immutable snapshot data (JSONB) + snapshot_data JSONB NOT NULL, + + -- Array of message IDs for cascade delete on edit + message_ids INTEGER[] NOT NULL, + + -- Who created this snapshot + created_by_user_id UUID REFERENCES "user"(id) ON DELETE SET NULL, + + -- Prevent duplicate snapshots of same content for same thread + CONSTRAINT uq_snapshot_thread_content_hash UNIQUE (thread_id, content_hash) + ); + """ + ) + + # 2. Create indexes for public_chat_snapshots + op.execute( + """ + CREATE INDEX IF NOT EXISTS ix_public_chat_snapshots_thread_id + ON public_chat_snapshots(thread_id); + """ + ) + + op.execute( + """ + CREATE INDEX IF NOT EXISTS ix_public_chat_snapshots_share_token + ON public_chat_snapshots(share_token); + """ + ) + + op.execute( + """ + CREATE INDEX IF NOT EXISTS ix_public_chat_snapshots_content_hash + ON public_chat_snapshots(content_hash); + """ + ) + + op.execute( + """ + CREATE INDEX IF NOT EXISTS ix_public_chat_snapshots_created_by_user_id + ON public_chat_snapshots(created_by_user_id); + """ + ) + + # 3. Create GIN index for message_ids array (for fast overlap queries) + op.execute( + """ + CREATE INDEX IF NOT EXISTS ix_public_chat_snapshots_message_ids + ON public_chat_snapshots USING GIN(message_ids); + """ + ) + + # 4. Drop deprecated indexes from new_chat_threads + op.execute("DROP INDEX IF EXISTS ix_new_chat_threads_public_share_enabled") + op.execute("DROP INDEX IF EXISTS ix_new_chat_threads_public_share_token") + + # 5. Drop deprecated columns from new_chat_threads + op.execute("ALTER TABLE new_chat_threads DROP COLUMN IF EXISTS clone_pending") + op.execute("ALTER TABLE new_chat_threads DROP COLUMN IF EXISTS public_share_enabled") + op.execute("ALTER TABLE new_chat_threads DROP COLUMN IF EXISTS public_share_token") + + +def downgrade() -> None: + """Restore deprecated columns and drop public_chat_snapshots table.""" + + # 1. Restore deprecated columns on new_chat_threads + op.execute( + """ + ALTER TABLE new_chat_threads + ADD COLUMN IF NOT EXISTS public_share_token VARCHAR(64); + """ + ) + + op.execute( + """ + ALTER TABLE new_chat_threads + ADD COLUMN IF NOT EXISTS public_share_enabled BOOLEAN NOT NULL DEFAULT FALSE; + """ + ) + + op.execute( + """ + ALTER TABLE new_chat_threads + ADD COLUMN IF NOT EXISTS clone_pending BOOLEAN NOT NULL DEFAULT FALSE; + """ + ) + + # 2. Restore indexes + op.execute( + """ + CREATE UNIQUE INDEX IF NOT EXISTS ix_new_chat_threads_public_share_token + ON new_chat_threads(public_share_token) + WHERE public_share_token IS NOT NULL; + """ + ) + + op.execute( + """ + CREATE INDEX IF NOT EXISTS ix_new_chat_threads_public_share_enabled + ON new_chat_threads(public_share_enabled) + WHERE public_share_enabled = TRUE; + """ + ) + + # 3. Drop public_chat_snapshots table and its indexes + op.execute("DROP INDEX IF EXISTS ix_public_chat_snapshots_message_ids") + op.execute("DROP INDEX IF EXISTS ix_public_chat_snapshots_created_by_user_id") + op.execute("DROP INDEX IF EXISTS ix_public_chat_snapshots_content_hash") + op.execute("DROP INDEX IF EXISTS ix_public_chat_snapshots_share_token") + op.execute("DROP INDEX IF EXISTS ix_public_chat_snapshots_thread_id") + op.execute("DROP TABLE IF EXISTS public_chat_snapshots")