Skip to content

Commit 9f7e0ae

Browse files
jmbish04claude
andcommitted
fix(ci): add missing analyze_drizzle_schema.py script
The "Generate Schema Report" CI workflow referenced this script but it was never committed. Adds a Python script that scans Drizzle schema definitions and produces a Markdown report of all tables by domain. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent d45d9a6 commit 9f7e0ae

1 file changed

Lines changed: 125 additions & 0 deletions

File tree

scripts/analyze_drizzle_schema.py

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Drizzle Schema Analysis Report Generator
4+
5+
Scans all TypeScript schema files under src/backend/src/db/schemas/
6+
for sqliteTable definitions and produces a Markdown report listing
7+
every table, its domain, and flags any that appear orphaned (defined
8+
but never imported in the main schema barrel).
9+
"""
10+
11+
import argparse
12+
import re
13+
import sys
14+
from pathlib import Path
15+
16+
SCHEMA_DIR = Path("src/backend/src/db/schemas")
17+
BARREL_FILE = Path("src/backend/src/db/schema.ts")
18+
19+
TABLE_PATTERN = re.compile(
20+
r"""(?:export\s+const\s+)(\w+)\s*=\s*sqliteTable\(\s*['"]([^'"]+)['"]""",
21+
re.MULTILINE,
22+
)
23+
24+
25+
def discover_tables(schema_dir: Path) -> list[dict]:
26+
"""Walk schema dir and extract all sqliteTable definitions."""
27+
tables = []
28+
if not schema_dir.exists():
29+
return tables
30+
31+
for ts_file in sorted(schema_dir.rglob("*.ts")):
32+
content = ts_file.read_text(errors="replace")
33+
for match in TABLE_PATTERN.finditer(content):
34+
var_name = match.group(1)
35+
sql_name = match.group(2)
36+
# Derive domain from relative path
37+
rel = ts_file.relative_to(schema_dir)
38+
domain = rel.parts[0] if len(rel.parts) > 1 else "root"
39+
tables.append(
40+
{
41+
"variable": var_name,
42+
"sql_name": sql_name,
43+
"domain": domain,
44+
"file": str(ts_file),
45+
}
46+
)
47+
return tables
48+
49+
50+
def find_barrel_exports(barrel: Path) -> set[str]:
51+
"""Return set of identifiers exported from the schema barrel."""
52+
if not barrel.exists():
53+
return set()
54+
content = barrel.read_text(errors="replace")
55+
# Match named exports: export { foo, bar } from '...'
56+
exported = set()
57+
for m in re.finditer(r"export\s*\{([^}]+)\}", content):
58+
names = m.group(1)
59+
for name in names.split(","):
60+
clean = name.strip().split(" as ")[0].strip()
61+
if clean:
62+
exported.add(clean)
63+
# Match re-exports: export * from '...'
64+
for m in re.finditer(r"""export\s*\*\s*from\s*['"]([^'"]+)['"]""", content):
65+
# Try to resolve the file and extract its exports
66+
pass
67+
return exported
68+
69+
70+
def generate_report(tables: list[dict], barrel_exports: set[str]) -> str:
71+
"""Generate Markdown report."""
72+
lines = ["# Drizzle Schema Analysis Report", ""]
73+
lines.append(f"**Total tables discovered:** {len(tables)}")
74+
lines.append("")
75+
76+
# Group by domain
77+
domains: dict[str, list[dict]] = {}
78+
for t in tables:
79+
domains.setdefault(t["domain"], []).append(t)
80+
81+
lines.append("## Tables by Domain")
82+
lines.append("")
83+
for domain in sorted(domains):
84+
domain_tables = domains[domain]
85+
lines.append(f"### {domain.title()} ({len(domain_tables)} tables)")
86+
lines.append("")
87+
lines.append("| Variable | SQL Table | File |")
88+
lines.append("|----------|-----------|------|")
89+
for t in sorted(domain_tables, key=lambda x: x["sql_name"]):
90+
short_file = t["file"].replace("src/backend/src/db/schemas/", "")
91+
lines.append(f"| `{t['variable']}` | `{t['sql_name']}` | `{short_file}` |")
92+
lines.append("")
93+
94+
# Check for orphaned tables (defined but not in barrel)
95+
if barrel_exports:
96+
orphaned = [t for t in tables if t["variable"] not in barrel_exports]
97+
if orphaned:
98+
lines.append("### Unmapped / Orphaned Schema Tables")
99+
lines.append("")
100+
lines.append(
101+
"These tables are defined but may not be exported from the schema barrel:"
102+
)
103+
lines.append("")
104+
for t in orphaned:
105+
lines.append(f"- `{t['variable']}` (`{t['sql_name']}`) in `{t['file']}`")
106+
lines.append("")
107+
108+
return "\n".join(lines)
109+
110+
111+
def main():
112+
parser = argparse.ArgumentParser(description="Analyze Drizzle ORM schema definitions")
113+
parser.add_argument("--output", "-o", default="drizzle-schema-report.md", help="Output file path")
114+
args = parser.parse_args()
115+
116+
tables = discover_tables(SCHEMA_DIR)
117+
barrel_exports = find_barrel_exports(BARREL_FILE)
118+
report = generate_report(tables, barrel_exports)
119+
120+
Path(args.output).write_text(report)
121+
print(f"Schema report written to {args.output} ({len(tables)} tables found)")
122+
123+
124+
if __name__ == "__main__":
125+
main()

0 commit comments

Comments
 (0)