Erste Produktivversion
This commit is contained in:
134
tools/check-schema.py
Normal file
134
tools/check-schema.py
Normal file
@@ -0,0 +1,134 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Vergleicht db/schema.sql mit den SQLAlchemy-Modellen.
|
||||
|
||||
python3 tools/check-schema.py
|
||||
|
||||
Die SQL-Datei ist von Hand geschrieben und kann beim nächsten
|
||||
Modell-Umbau vergessen werden. Dieser Vergleich findet fehlende oder
|
||||
überzählige Tabellen und Spalten - bevor jemand die Datei zum Aufsetzen
|
||||
einer neuen Instanz benutzt und sich wundert.
|
||||
|
||||
Arbeitet rein textlich, ohne SQLAlchemy zu laden: Damit läuft die
|
||||
Prüfung auch außerhalb des Containers.
|
||||
"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
MODELS = ROOT / "backend" / "app" / "models.py"
|
||||
SCHEMA = ROOT / "db" / "schema.sql"
|
||||
|
||||
# Tabellen, die nicht aus Modellen stammen
|
||||
EXTRA_TABLES = {"alembic_version"}
|
||||
|
||||
|
||||
def from_models() -> dict[str, set[str]]:
|
||||
"""__tablename__ und mapped_column-Namen je Klasse einsammeln."""
|
||||
source = MODELS.read_text(encoding="utf-8")
|
||||
tables: dict[str, set[str]] = {}
|
||||
current: str | None = None
|
||||
|
||||
for line in source.splitlines():
|
||||
table = re.match(r'\s*__tablename__\s*=\s*"([^"]+)"', line)
|
||||
if table:
|
||||
current = table.group(1)
|
||||
tables[current] = set()
|
||||
continue
|
||||
|
||||
if current is None:
|
||||
continue
|
||||
|
||||
# Beziehungen sind keine Spalten
|
||||
if "relationship(" in line:
|
||||
continue
|
||||
|
||||
column = re.match(r"\s*(\w+):\s*Mapped\[", line)
|
||||
if column:
|
||||
tables[current].add(column.group(1))
|
||||
|
||||
return tables
|
||||
|
||||
|
||||
def from_schema() -> dict[str, set[str]]:
|
||||
"""CREATE TABLE ... ( ... ) auswerten."""
|
||||
source = SCHEMA.read_text(encoding="utf-8")
|
||||
tables: dict[str, set[str]] = {}
|
||||
|
||||
for match in re.finditer(
|
||||
r"CREATE TABLE `(\w+)` \((.*?)\n\) ENGINE", source, re.S
|
||||
):
|
||||
name, body = match.group(1), match.group(2)
|
||||
columns = set()
|
||||
for line in body.splitlines():
|
||||
line = line.strip()
|
||||
if line.startswith("--") or not line:
|
||||
continue
|
||||
# Spaltendefinitionen beginnen mit `name` gefolgt von einem Typ
|
||||
col = re.match(r"`(\w+)`\s+[A-Z]", line)
|
||||
if col:
|
||||
columns.add(col.group(1))
|
||||
tables[name] = columns
|
||||
|
||||
return tables
|
||||
|
||||
|
||||
def main() -> int:
|
||||
models = from_models()
|
||||
schema = from_schema()
|
||||
problems = 0
|
||||
|
||||
missing = set(models) - set(schema)
|
||||
surplus = set(schema) - set(models) - EXTRA_TABLES
|
||||
|
||||
for name in sorted(missing):
|
||||
print(f" FEHLT in schema.sql: Tabelle {name}")
|
||||
problems += 1
|
||||
for name in sorted(surplus):
|
||||
print(f" ÜBERZÄHLIG in schema.sql: Tabelle {name}")
|
||||
problems += 1
|
||||
|
||||
for name in sorted(set(models) & set(schema)):
|
||||
only_model = models[name] - schema[name]
|
||||
only_schema = schema[name] - models[name]
|
||||
for column in sorted(only_model):
|
||||
print(f" FEHLT in schema.sql: {name}.{column}")
|
||||
problems += 1
|
||||
for column in sorted(only_schema):
|
||||
print(f" ÜBERZÄHLIG in schema.sql: {name}.{column}")
|
||||
problems += 1
|
||||
|
||||
# Migrationsstand muss zur höchsten Revision passen
|
||||
versions = ROOT / "backend" / "alembic" / "versions"
|
||||
revisions = sorted(
|
||||
m.group(1)
|
||||
for f in versions.glob("*.py")
|
||||
if (m := re.search(r'^revision:\s*str\s*=\s*"(\w+)"', f.read_text(), re.M))
|
||||
)
|
||||
head = revisions[-1] if revisions else None
|
||||
stamped = re.search(
|
||||
r"INSERT INTO `alembic_version`.*VALUES \('(\w+)'\)", SCHEMA.read_text()
|
||||
)
|
||||
if head and (not stamped or stamped.group(1) != head):
|
||||
print(
|
||||
f" Migrationsstand passt nicht: schema.sql trägt "
|
||||
f"{stamped.group(1) if stamped else 'nichts'}, "
|
||||
f"höchste Revision ist {head}"
|
||||
)
|
||||
problems += 1
|
||||
|
||||
if problems:
|
||||
print(f"\n{problems} Abweichung(en) zwischen Modellen und schema.sql")
|
||||
return 1
|
||||
|
||||
print(
|
||||
f"{len(models)} Tabellen, "
|
||||
f"{sum(len(c) for c in models.values())} Spalten – "
|
||||
f"schema.sql stimmt mit den Modellen überein (Revision {head})"
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user