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