|
| 1 | +"""Add shared_chat table and migrate existing shares |
| 2 | +
|
| 3 | +Revision ID: c1d2e3f4a5b6 |
| 4 | +Revises: e1f2a3b4c5d6 |
| 5 | +Create Date: 2026-04-16 23:00:00.000000 |
| 6 | +
|
| 7 | +""" |
| 8 | + |
| 9 | +import time |
| 10 | +import uuid |
| 11 | + |
| 12 | +from alembic import op |
| 13 | +import sqlalchemy as sa |
| 14 | + |
| 15 | +revision = 'c1d2e3f4a5b6' |
| 16 | +down_revision = 'e1f2a3b4c5d6' |
| 17 | +branch_labels = None |
| 18 | +depends_on = None |
| 19 | + |
| 20 | +# Lightweight table references for data migration (no ORM models needed) |
| 21 | +chat_t = sa.table( |
| 22 | + 'chat', |
| 23 | + sa.column('id', sa.Text), |
| 24 | + sa.column('user_id', sa.Text), |
| 25 | + sa.column('title', sa.Text), |
| 26 | + sa.column('chat', sa.JSON), |
| 27 | + sa.column('share_id', sa.Text), |
| 28 | + sa.column('created_at', sa.BigInteger), |
| 29 | + sa.column('updated_at', sa.BigInteger), |
| 30 | + sa.column('archived', sa.Boolean), |
| 31 | + sa.column('meta', sa.JSON), |
| 32 | +) |
| 33 | + |
| 34 | +shared_chat_t = sa.table( |
| 35 | + 'shared_chat', |
| 36 | + sa.column('id', sa.Text), |
| 37 | + sa.column('chat_id', sa.Text), |
| 38 | + sa.column('user_id', sa.Text), |
| 39 | + sa.column('title', sa.Text), |
| 40 | + sa.column('chat', sa.JSON), |
| 41 | + sa.column('created_at', sa.BigInteger), |
| 42 | + sa.column('updated_at', sa.BigInteger), |
| 43 | +) |
| 44 | + |
| 45 | +chat_message_t = sa.table( |
| 46 | + 'chat_message', |
| 47 | + sa.column('chat_id', sa.Text), |
| 48 | +) |
| 49 | + |
| 50 | +access_grant_t = sa.table( |
| 51 | + 'access_grant', |
| 52 | + sa.column('id', sa.Text), |
| 53 | + sa.column('resource_type', sa.Text), |
| 54 | + sa.column('resource_id', sa.Text), |
| 55 | + sa.column('principal_type', sa.Text), |
| 56 | + sa.column('principal_id', sa.Text), |
| 57 | + sa.column('permission', sa.Text), |
| 58 | + sa.column('created_at', sa.BigInteger), |
| 59 | +) |
| 60 | + |
| 61 | + |
| 62 | +def upgrade(): |
| 63 | + conn = op.get_bind() |
| 64 | + |
| 65 | + # 1. Create shared_chat table |
| 66 | + op.create_table( |
| 67 | + 'shared_chat', |
| 68 | + sa.Column('id', sa.Text(), primary_key=True), |
| 69 | + sa.Column('chat_id', sa.Text(), sa.ForeignKey('chat.id', ondelete='CASCADE'), nullable=False), |
| 70 | + sa.Column('user_id', sa.Text(), nullable=False), |
| 71 | + sa.Column('title', sa.Text(), nullable=True), |
| 72 | + sa.Column('chat', sa.JSON(), nullable=True), |
| 73 | + sa.Column('created_at', sa.BigInteger(), nullable=True), |
| 74 | + sa.Column('updated_at', sa.BigInteger(), nullable=True), |
| 75 | + ) |
| 76 | + |
| 77 | + # 2. Migrate existing shared-* rows |
| 78 | + shared_rows = conn.execute( |
| 79 | + sa.select( |
| 80 | + chat_t.c.id, |
| 81 | + chat_t.c.user_id, |
| 82 | + chat_t.c.title, |
| 83 | + chat_t.c.chat, |
| 84 | + chat_t.c.created_at, |
| 85 | + chat_t.c.updated_at, |
| 86 | + ).where(chat_t.c.user_id.like('shared-%')) |
| 87 | + ).fetchall() |
| 88 | + |
| 89 | + for row in shared_rows: |
| 90 | + share_token = row.id |
| 91 | + original_chat_id = row.user_id.replace('shared-', '', 1) |
| 92 | + |
| 93 | + # Verify original chat still exists |
| 94 | + original = conn.execute( |
| 95 | + sa.select(chat_t.c.user_id).where(chat_t.c.id == original_chat_id) |
| 96 | + ).fetchone() |
| 97 | + |
| 98 | + if not original: |
| 99 | + continue |
| 100 | + |
| 101 | + # Insert snapshot into shared_chat |
| 102 | + conn.execute(shared_chat_t.insert().values( |
| 103 | + id=share_token, |
| 104 | + chat_id=original_chat_id, |
| 105 | + user_id=original.user_id, |
| 106 | + title=row.title, |
| 107 | + chat=row.chat, |
| 108 | + created_at=row.created_at, |
| 109 | + updated_at=row.updated_at, |
| 110 | + )) |
| 111 | + |
| 112 | + # Create user:*:read grant for backward compat |
| 113 | + conn.execute(access_grant_t.insert().values( |
| 114 | + id=str(uuid.uuid4()), |
| 115 | + resource_type='shared_chat', |
| 116 | + resource_id=original_chat_id, |
| 117 | + principal_type='user', |
| 118 | + principal_id='*', |
| 119 | + permission='read', |
| 120 | + created_at=row.created_at or int(time.time()), |
| 121 | + )) |
| 122 | + |
| 123 | + # 3. Clean up old phantom rows |
| 124 | + conn.execute( |
| 125 | + chat_message_t.delete().where( |
| 126 | + chat_message_t.c.chat_id.in_( |
| 127 | + sa.select(chat_t.c.id).where(chat_t.c.user_id.like('shared-%')) |
| 128 | + ) |
| 129 | + ) |
| 130 | + ) |
| 131 | + conn.execute(chat_t.delete().where(chat_t.c.user_id.like('shared-%'))) |
| 132 | + |
| 133 | + |
| 134 | +def downgrade(): |
| 135 | + conn = op.get_bind() |
| 136 | + |
| 137 | + shared_rows = conn.execute( |
| 138 | + sa.select( |
| 139 | + shared_chat_t.c.id, |
| 140 | + shared_chat_t.c.chat_id, |
| 141 | + shared_chat_t.c.user_id, |
| 142 | + shared_chat_t.c.title, |
| 143 | + shared_chat_t.c.chat, |
| 144 | + shared_chat_t.c.created_at, |
| 145 | + shared_chat_t.c.updated_at, |
| 146 | + ) |
| 147 | + ).fetchall() |
| 148 | + |
| 149 | + for row in shared_rows: |
| 150 | + conn.execute(chat_t.insert().values( |
| 151 | + id=row.id, |
| 152 | + user_id=f'shared-{row.chat_id}', |
| 153 | + title=row.title, |
| 154 | + chat=row.chat, |
| 155 | + created_at=row.created_at, |
| 156 | + updated_at=row.updated_at, |
| 157 | + archived=False, |
| 158 | + meta={}, |
| 159 | + )) |
| 160 | + |
| 161 | + conn.execute( |
| 162 | + access_grant_t.delete().where(access_grant_t.c.resource_type == 'shared_chat') |
| 163 | + ) |
| 164 | + op.drop_table('shared_chat') |
0 commit comments