Files
resolutionflow/backend/alembic/versions/064_normalize_script_builder_messages.py
Michael Chihlas b801f6cac5 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>
2026-03-21 21:06:58 -04:00

129 lines
5.2 KiB
Python

"""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)