"""Backfill + require FlowVersion.input_schema. Revision ID: e8f3a1c2b4d6 Revises: c4d8f2a1e6b9 Create Date: 2026-09-30 00:00:00.000000 Pre-Phase-3 rows have input_schema NULL. Backfill them from the stored langflow_json (same parser the write choke point uses); unparseable rows get the textarea fallback so reads never crash. Then enforce NOT NULL. """ from collections.abc import Sequence import sqlalchemy as sa from alembic import op # revision identifiers, used by Alembic. revision: str = "e8f3a1c2b4d6" down_revision: str | Sequence[str] | None = "c4d8f2a1e6b9" branch_labels: str | Sequence[str] | None = None depends_on: str | Sequence[str] | None = None _FALLBACK = { "json_schema": {"type": "object", "properties": {}}, "ui_schema": {}, "fallback": "textarea", } def _derive(langflow_json) -> dict: import json as stdlib_json try: from app.flows import input_schema as parser if isinstance(langflow_json, str): # SQLite stores JSON columns as TEXT; Postgres returns dict. langflow_json = stdlib_json.loads(langflow_json) return parser.extract_form_schema(langflow_json) except Exception: # noqa: BLE001 -- backfill must never fail the migration return dict(_FALLBACK) def upgrade() -> None: """Upgrade schema.""" import json as stdlib_json connection = op.get_bind() rows = connection.execute( sa.text("SELECT id, langflow_json FROM flow_versions WHERE input_schema IS NULL") ).all() for row_id, langflow_json in rows: schema = _derive(langflow_json) connection.execute( sa.text("UPDATE flow_versions SET input_schema = :schema WHERE id = :id"), {"schema": stdlib_json.dumps(schema), "id": str(row_id)}, ) with op.batch_alter_table("flow_versions") as batch: batch.alter_column("input_schema", existing_type=sa.JSON(), nullable=False) def downgrade() -> None: """Downgrade schema (data kept, nullability restored).""" with op.batch_alter_table("flow_versions") as batch: batch.alter_column("input_schema", existing_type=sa.JSON(), nullable=True)