"""Audit ORM index/unique-key byte sizes against MySQL InnoDB prefix limits. MySQL with the legacy 767-byte InnoDB index-prefix limit (5.6, or 5.7 with COMPACT row format) rejects any index whose key exceeds 767 bytes — and utf8mb4 costs 4 bytes per char, so an indexed VARCHAR key may total at most 191 chars. Modern servers (DYNAMIC row format) allow 3072 bytes = 768 chars. Run: PYTHONPATH=. .venv/Scripts/python scripts/audit_index_key_lengths.py """ from __future__ import annotations import sys from sqlalchemy import String, UniqueConstraint import deerflow.persistence.models # noqa: F401 (registers all ORM models) from deerflow.persistence.base import Base LIMIT_OLD = 767 LIMIT_NEW = 3072 BYTES_PER_CHAR = 4 # utf8mb4 def _col_key_bytes(col) -> int | None: t = col.type # unwrap TypeDecorator impl = getattr(t, "impl", None) if impl is not None: t = impl if isinstance(t, String): return (t.length or 255) * BYTES_PER_CHAR return 8 # ints/datetimes/floats — small fixed size, close enough def _key_bytes(col, mysql_length) -> int: full = _col_key_bytes(col) or 0 if mysql_length is None: return full if isinstance(mysql_length, dict): prefix = mysql_length.get(col.name) else: prefix = mysql_length if prefix is None: return full return min(full, int(prefix) * BYTES_PER_CHAR) def main() -> int: bad = 0 for table in Base.metadata.sorted_tables: keys: list[tuple[str, str, list, object]] = [] for idx in table.indexes: keys.append(("INDEX", idx.name, list(idx.columns), idx.dialect_options["mysql"].get("length"))) for cons in table.constraints: if isinstance(cons, UniqueConstraint): keys.append(("UNIQUE", cons.name or "?", list(cons.columns), None)) for kind, name, cols, mysql_length in keys: total = sum(_key_bytes(c, mysql_length) for c in cols) if total > LIMIT_OLD: bad += 1 tag = "EXCEEDS 3072 (fails on ALL MySQL)" if total > LIMIT_NEW else "exceeds 767 (fails on legacy MySQL)" detail = ", ".join(f"{c.name}({getattr(getattr(c.type, 'impl', c.type), 'length', '?')})" for c in cols) print(f"{table.name}: {kind} {name} = {total} bytes [{detail}] -- {tag}") if not bad: print("OK: every index key fits within 767 bytes (utf8mb4 worst case)") return 0 if __name__ == "__main__": sys.exit(main())