Skip to content

Commit b71e962

Browse files
authored
Add script to audit Drizzle ORM schema and D1 usage
This script analyzes Drizzle ORM schema and D1 usage by scanning TypeScript files for table definitions and database interactions, generating a Markdown report.
1 parent 4f2ab53 commit b71e962

1 file changed

Lines changed: 154 additions & 0 deletions

File tree

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
#!/usr/bin/env python3
2+
import os
3+
import re
4+
import sys
5+
import argparse
6+
from collections import defaultdict
7+
8+
def get_ts_files(root_dir):
9+
"""Recursively find all TypeScript files, ignoring build/module directories."""
10+
ignore_dirs = {'node_modules', 'dist', '.git', '.wrangler', '.vscode', 'drizzle', '.github'}
11+
ts_files = []
12+
13+
for dirpath, dirnames, filenames in os.walk(root_dir):
14+
# Modify dirnames in-place to skip ignored directories
15+
dirnames[:] = [d for d in dirnames if d not in ignore_dirs]
16+
for filename in filenames:
17+
if filename.endswith('.ts') or filename.endswith('.tsx'):
18+
ts_files.append(os.path.join(dirpath, filename))
19+
20+
return ts_files
21+
22+
def main():
23+
parser = argparse.ArgumentParser(description="Analyze Drizzle ORM schema and D1 usage.")
24+
parser.add_argument("--output", default="drizzle-schema-report.md", help="Output Markdown file path")
25+
args = parser.parse_args()
26+
27+
root_dir = os.getcwd()
28+
files = get_ts_files(root_dir)
29+
30+
tables = []
31+
32+
# 1. Extract all Drizzle Table definitions
33+
# Matches: export const varName = sqliteTable('tableName', ...)
34+
table_regex = re.compile(r"export\s+const\s+([a-zA-Z0-9_]+)\s*=\s*(?:sqliteTable|pgTable|mysqlTable)\(\s*['\"]([^'\"]+)['\"]")
35+
36+
for file_path in files:
37+
try:
38+
with open(file_path, 'r', encoding='utf-8') as f:
39+
content = f.read()
40+
matches = table_regex.findall(content)
41+
for var_name, table_name in matches:
42+
rel_path = os.path.relpath(file_path, root_dir)
43+
tables.append({
44+
"var_name": var_name,
45+
"table_name": table_name,
46+
"file": rel_path
47+
})
48+
except Exception as e:
49+
print(f"Warning: Could not read {file_path}: {e}")
50+
51+
file_interactions = defaultdict(set)
52+
db1_map = defaultdict(set) # For env.DB
53+
db2_map = defaultdict(set) # For env.DB_WEBHOOKS
54+
55+
# 2. Scan files for table imports and D1 database interactions
56+
for file_path in files:
57+
try:
58+
with open(file_path, 'r', encoding='utf-8') as f:
59+
content = f.read()
60+
61+
rel_path = os.path.relpath(file_path, root_dir)
62+
63+
# Look for standard Cloudflare Worker / Hono context bindings
64+
uses_db1 = 'env.DB' in content or 'c.env.DB' in content
65+
uses_db2 = 'env.DB_WEBHOOKS' in content or 'c.env.DB_WEBHOOKS' in content
66+
67+
imported_tables = set()
68+
69+
for t in tables:
70+
# Regex boundary check for the specific Drizzle table variable
71+
var_regex = re.compile(r"\b" + re.escape(t['var_name']) + r"\b")
72+
73+
if var_regex.search(content):
74+
imported_tables.add(t['table_name'])
75+
76+
if uses_db1:
77+
db1_map[t['table_name']].add(rel_path)
78+
if uses_db2:
79+
db2_map[t['table_name']].add(rel_path)
80+
81+
if imported_tables:
82+
file_interactions[rel_path] = imported_tables
83+
84+
except Exception as e:
85+
print(f"Warning: Could not read {file_path}: {e}")
86+
87+
# 3. Generate the Markdown Report
88+
md = ["# Drizzle ORM Schema & D1 Analysis Report\n"]
89+
md.append("## Table Names by Database\n")
90+
91+
md.append("### env.DB")
92+
db1_sorted = sorted(db1_map.keys())
93+
if db1_sorted:
94+
for t in db1_sorted:
95+
md.append(f"- {t}")
96+
else:
97+
md.append("- *No tables definitively mapped to env.DB yet*")
98+
99+
md.append("\n### env.DB_WEBHOOKS")
100+
db2_sorted = sorted(db2_map.keys())
101+
if db2_sorted:
102+
for t in db2_sorted:
103+
md.append(f"- {t}")
104+
else:
105+
md.append("- *No tables definitively mapped to env.DB_WEBHOOKS yet*")
106+
107+
# Catch AI Slop (Orphaned Tables)
108+
all_discovered = sorted(list(set(t['table_name'] for t in tables)))
109+
mapped_tables = set(db1_sorted + db2_sorted)
110+
unmapped = [t for t in all_discovered if t not in mapped_tables]
111+
112+
if unmapped:
113+
md.append("\n### Unmapped / Orphaned Schema Tables")
114+
md.append("*(Suspicious AI Slop: Defined in code but no CRUD operations with a known D1 env var detected)*")
115+
for t in unmapped:
116+
md.append(f"- {t}")
117+
118+
md.append("\n---\n\n## Code Files Interacting with D1 Tables\n")
119+
for file_path in sorted(file_interactions.keys()):
120+
tables_used = ", ".join(sorted(file_interactions[file_path]))
121+
md.append(f"### `{file_path}`")
122+
md.append(f"- **Tables Imported:** {tables_used}\n")
123+
124+
md.append("---\n\n## env.DB d1 db")
125+
md.append("| Table Name | Short File Paths |")
126+
md.append("|---|---|")
127+
if db1_sorted:
128+
for t in db1_sorted:
129+
paths = ", ".join([f"`{p}`" for p in sorted(db1_map[t])])
130+
md.append(f"| **{t}** | {paths} |")
131+
else:
132+
md.append("| *None Detected* | *N/A* |")
133+
134+
md.append("\n## env.DB_WEBHOOKS d1 db")
135+
md.append("| Table Name | Short File Paths |")
136+
md.append("|---|---|")
137+
if db2_sorted:
138+
for t in db2_sorted:
139+
paths = ", ".join([f"`{p}`" for p in sorted(db2_map[t])])
140+
md.append(f"| **{t}** | {paths} |")
141+
else:
142+
md.append("| *None Detected* | *N/A* |")
143+
144+
# 4. Write to disk
145+
try:
146+
with open(args.output, 'w', encoding='utf-8') as f:
147+
f.write("\n".join(md) + "\n")
148+
print(f"✅ Schema analysis complete! Report generated at: {args.output}")
149+
except Exception as e:
150+
print(f"❌ Failed to write report: {e}")
151+
sys.exit(1)
152+
153+
if __name__ == "__main__":
154+
main()

0 commit comments

Comments
 (0)