72 lines
2.5 KiB
Python
72 lines
2.5 KiB
Python
"""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())
|