refactor: normalize script_builder_messages into separate table
Extract JSONB messages array from script_builder_sessions into a proper script_builder_messages table with individual columns for role, content, script, tokens, etc. Migration handles data migration from JSONB to rows. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
"""normalize script builder messages
|
||||
|
||||
Revision ID: 064
|
||||
Revises: 063
|
||||
Create Date: 2026-03-21 22:00:00.000000
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects.postgresql import UUID, JSONB
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '064'
|
||||
down_revision: Union[str, None] = '063'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# 1. Create the new script_builder_messages table
|
||||
op.create_table(
|
||||
'script_builder_messages',
|
||||
sa.Column('id', UUID(as_uuid=True), primary_key=True),
|
||||
sa.Column('session_id', UUID(as_uuid=True), sa.ForeignKey('script_builder_sessions.id', ondelete='CASCADE'), nullable=False, index=True),
|
||||
sa.Column('role', sa.String(20), nullable=False, comment='user or assistant'),
|
||||
sa.Column('content', sa.Text(), nullable=False),
|
||||
sa.Column('script', sa.Text(), nullable=True, comment='Extracted script from AI response'),
|
||||
sa.Column('script_filename', sa.String(200), nullable=True),
|
||||
sa.Column('line_count', sa.Integer(), nullable=True),
|
||||
sa.Column('input_tokens', sa.Integer(), nullable=True),
|
||||
sa.Column('output_tokens', sa.Integer(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()),
|
||||
)
|
||||
|
||||
# 2. Migrate existing JSONB messages to the new table (if any exist)
|
||||
conn = op.get_bind()
|
||||
sessions = conn.execute(
|
||||
sa.text("SELECT id, messages FROM script_builder_sessions WHERE messages IS NOT NULL AND messages != '[]'::jsonb")
|
||||
).fetchall()
|
||||
|
||||
for session_row in sessions:
|
||||
session_id = session_row[0]
|
||||
messages = session_row[1]
|
||||
if not messages:
|
||||
continue
|
||||
for msg in messages:
|
||||
conn.execute(
|
||||
sa.text("""
|
||||
INSERT INTO script_builder_messages (id, session_id, role, content, script, script_filename, line_count, input_tokens, output_tokens, created_at)
|
||||
VALUES (gen_random_uuid(), :session_id, :role, :content, :script, :script_filename, :line_count, :input_tokens, :output_tokens, COALESCE(:timestamp::timestamptz, NOW()))
|
||||
"""),
|
||||
{
|
||||
"session_id": session_id,
|
||||
"role": msg.get("role", "user"),
|
||||
"content": msg.get("content", ""),
|
||||
"script": msg.get("script"),
|
||||
"script_filename": msg.get("script_filename"),
|
||||
"line_count": msg.get("line_count"),
|
||||
"input_tokens": msg.get("input_tokens"),
|
||||
"output_tokens": msg.get("output_tokens"),
|
||||
"timestamp": msg.get("timestamp"),
|
||||
},
|
||||
)
|
||||
|
||||
# 3. Drop the old columns
|
||||
op.drop_column('script_builder_sessions', 'messages')
|
||||
op.drop_column('script_builder_sessions', 'message_count')
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# 1. Re-add the JSONB columns
|
||||
op.add_column('script_builder_sessions', sa.Column('messages', JSONB, nullable=False, server_default='[]'))
|
||||
op.add_column('script_builder_sessions', sa.Column('message_count', sa.Integer(), nullable=False, server_default='0'))
|
||||
|
||||
# 2. Migrate data back from the messages table to JSONB
|
||||
conn = op.get_bind()
|
||||
sessions = conn.execute(
|
||||
sa.text("SELECT DISTINCT session_id FROM script_builder_messages")
|
||||
).fetchall()
|
||||
|
||||
for (session_id,) in sessions:
|
||||
messages = conn.execute(
|
||||
sa.text("""
|
||||
SELECT role, content, script, script_filename, line_count, input_tokens, output_tokens, created_at
|
||||
FROM script_builder_messages
|
||||
WHERE session_id = :session_id
|
||||
ORDER BY created_at ASC
|
||||
"""),
|
||||
{"session_id": session_id},
|
||||
).fetchall()
|
||||
|
||||
json_messages = []
|
||||
user_count = 0
|
||||
for msg in messages:
|
||||
entry = {
|
||||
"role": msg[0],
|
||||
"content": msg[1],
|
||||
"timestamp": msg[7].isoformat() if msg[7] else None,
|
||||
}
|
||||
if msg[2]: # script
|
||||
entry["script"] = msg[2]
|
||||
if msg[3]: # script_filename
|
||||
entry["script_filename"] = msg[3]
|
||||
if msg[4] is not None: # line_count
|
||||
entry["line_count"] = msg[4]
|
||||
if msg[5] is not None: # input_tokens
|
||||
entry["input_tokens"] = msg[5]
|
||||
if msg[6] is not None: # output_tokens
|
||||
entry["output_tokens"] = msg[6]
|
||||
json_messages.append(entry)
|
||||
if msg[0] == "user":
|
||||
user_count += 1
|
||||
|
||||
import json
|
||||
conn.execute(
|
||||
sa.text("UPDATE script_builder_sessions SET messages = :messages::jsonb, message_count = :count WHERE id = :id"),
|
||||
{"messages": json.dumps(json_messages), "count": user_count, "id": session_id},
|
||||
)
|
||||
|
||||
# 3. Drop the messages table
|
||||
op.drop_table('script_builder_messages')
|
||||
|
||||
# 4. Remove server defaults added for downgrade
|
||||
op.alter_column('script_builder_sessions', 'messages', server_default=None)
|
||||
op.alter_column('script_builder_sessions', 'message_count', server_default=None)
|
||||
Reference in New Issue
Block a user