Files
siftlode/backend/alembic/versions/0060_plex_jsonb_null_normalize.py
T
peter 39f726a5e6 test(db): /facets regression + S3a jsonb-null normalization (R6 S3)
First real customers of the DB lane:
- test_plex_facets: GET /api/plex/facets?scope=show returns 200 (not the
  "cannot extract elements from a scalar" 500) for a show whose genres is a JSONB
  scalar `null` — the 0.53.1 read-guard's regression test. Mutation-checked:
  removing the jsonb_typeof='array' guard turns it red with that exact error.
- migration 0060 (S3a): one-time UPDATE …=NULL WHERE jsonb_typeof=… 'null' over the
  plex_items/plex_shows tag columns, so pre-0.53.1 JSONB-null rows become uniform SQL
  NULL and the read-guards go belt-only. test_plex_jsonb_normalize verifies scalar-null
  → SQL NULL while a real array is preserved.

Gate: ruff clean; DB lane 176 passed; pure-logic lane unchanged (skips the DB tests).
2026-07-26 13:54:42 +02:00

35 lines
1.4 KiB
Python

"""normalize plex JSONB tag columns: scalar `null` -> SQL NULL (R6 S3a)
Pre-0.53.1 rows persisted a genre-less show/movie's genres/directors/cast_names/collection_keys as
a JSONB scalar `null` (SQLAlchemy wrote a Python None as JSON null before none_as_null=True). The
0.53.1 hotfix added none_as_null=True (new writes are SQL NULL) + jsonb_typeof read-guards, but the
existing rows stayed JSON `null`, which kept those read-guards load-bearing rather than belt-only.
This one-time data fixup rewrites those scalars to SQL NULL so the columns are uniform. Data-only,
forward-only: a SQL NULL is indistinguishable from the old JSON `null` to every reader (the guards
treat both as "not an array"), so there is nothing meaningful to downgrade. No schema change.
"""
from typing import Sequence, Union
from alembic import op
revision: str = "0060_plex_jsonb_null_normalize"
down_revision: Union[str, None] = "0059_dl_link_pw_version"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
_TABLES = ("plex_items", "plex_shows")
_COLS = ("genres", "directors", "cast_names", "collection_keys")
def upgrade() -> None:
for table in _TABLES:
for col in _COLS:
op.execute(
f"UPDATE {table} SET {col} = NULL WHERE jsonb_typeof({col}) = 'null'"
)
def downgrade() -> None:
pass # forward-only: SQL NULL == the old JSON null for every reader