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>
129 lines
5.2 KiB
Python
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)
|