rdst scan
Find SQL queries in your application's ORM code and analyze them for performance, optionally as a CI check.
rdst scan walks a codebase, finds ORM-generated SQL in Python and TypeScript
projects, converts it to real SQL, and (optionally) analyzes each query for
performance issues. With --check it becomes a CI gate.
Supported ORMs
| Language | ORMs |
|---|---|
| Python | SQLAlchemy (1.x and 2.0), Django ORM |
| TypeScript / JavaScript | Prisma, Drizzle |
All four work against both PostgreSQL and MySQL targets.
Common usage
# Scan a directory and list the queries it finds
rdst scan ./backend --schema prod-orders
# Analyze each query against the live database
rdst scan ./backend --schema prod-orders --analyze
# Schema-only analysis (no DB connection needed — useful in CI)
rdst scan ./backend --schema prod-orders --analyze --shallow
# Only scan files changed in the current PR
rdst scan ./backend --schema prod-orders --diff HEAD --analyze --check
# Fail the build if any query has a risk score below 30
rdst scan ./backend --schema prod-orders --analyze --check --fail-threshold 30How extraction works
Extraction is 100% deterministic — same code in, same SQL out. RDST does not use the LLM for extraction itself:
RDST parses each .py file to an AST and walks it looking for SQLAlchemy and
Django ORM patterns — Query.filter(...).all(), Model.objects.filter(...),
session.execute(select(...)), and so on.
Once a call site is matched, RDST uses a small Haiku model (with your semantic layer as context) to translate the ORM chain into executable SQL. The translation step is deterministic because the prompt and schema are fixed.
RDST uses regex + brace-matching to find Prisma (prisma.model.findMany,
findUnique, etc.) and Drizzle (db.select().from(...).where(...)) call sites,
then runs the same Haiku-backed translation step.
Because extraction is deterministic, rdst scan results are reproducible across
CI runs and across machines. The same PR always produces the same output.
Example output
$ rdst scan ./backend --schema prod-orders --analyze --shallow
Scanning ./backend ...
Found 47 ORM call sites in 22 files.
Extracted 47 SQL queries.
Analyzing with shallow mode (no DB connection) ...
Score File Query
─────── ────────────────────────────── ────────────────────────────
88 services/order_service.py:42 SELECT o.id, o.total_cents ...
72 services/order_service.py:114 SELECT COUNT(*) FROM orders...
65 services/customer_service.py:8 SELECT c.* FROM customers c ...
48 services/reports.py:201 SELECT c.id, COUNT(o.id) ... ⚠ warn
21 services/reports.py:233 SELECT * FROM orders WHERE ... ✗ fail
...
Summary
47 queries analyzed.
Worst score: 21 (below fail threshold 30)
Below warn (50): 3
Below fail (30): 1
CI status: FAILAnalysis modes
| Mode | Needs DB? | What it checks |
|---|---|---|
| (no flag) | no | Extraction only — produces SQL and saves to registry |
--analyze --shallow | no | Schema-only LLM analysis. Detects SELECT *, missing LIMIT, obvious missing indexes, N+1 patterns. |
--analyze | yes | Full EXPLAIN ANALYZE against the target plus LLM analysis. Most accurate, slowest. |
Shallow mode is the recommended default for CI — it does not require a database password or network access to your production database, yet catches most practical issues.
CI mode (--check)
--check turns rdst scan into a build gate:
rdst scan ./backend --schema prod-orders --analyze --shallow --check \
--fail-threshold 30 --warn-threshold 50| Exit code | Meaning |
|---|---|
0 | All queries ≥ fail threshold (PASS) |
1 | At least one query below fail threshold (FAIL) |
The summary panel shows worst-query score, threshold thresholds, and the counts in
each band. Without --check, scores are informational only and the command always
exits 0.
Scanning just the diff
Pair --diff <ref> with --check to only gate queries that were touched in the
current change:
rdst scan ./backend --schema prod-orders --diff HEAD~1 --analyze --check| Ref | What gets scanned |
|---|---|
HEAD | Uncommitted changes |
HEAD~1 | Files changed in the last commit |
main, abc123 | Files changed since that commit/branch |
Flags reference
| Flag | What it does |
|---|---|
directory | Directory to scan (positional) |
--dry-run | Show what would be scanned without scanning |
--analyze | Run performance analysis against the extracted queries |
--shallow | Schema-only analysis (no DB connection). Use with --analyze. |
--schema <target> | Target name to use for schema context |
--output {table,json} | Output format |
--diff <ref> | Only scan files changed since ref |
--check | CI mode: exit 1 if issues found |
--warn-threshold <N> | Risk score threshold for warnings (default 50) |
--fail-threshold <N> | Risk score threshold for failures (default 30) |
--file-pattern <glob> | Glob pattern to filter files (e.g. *.py) |
--nosave | Do not save extracted queries to the registry |
--sequential | Run analysis queries one at a time (more deterministic scores, slower) |
Risk scoring
Each query gets a score from 0 to 100:
| Range | Meaning |
|---|---|
| 80–100 | Low risk |
| 50–79 | Medium risk |
| 30–49 | High risk |
| 0–29 | Very high risk — likely to cause production incidents |
The score is computed deterministically from the analyzer's output: missing indexes,
unbounded scans, SELECT * on large tables, N+1 patterns, and so on.
Scores are stable across runs because extraction is deterministic and analysis
uses temperature=0.0. A query that scores 65 today will score 65 tomorrow unless
the schema or the query itself changes.
See also
rdst analyze— re-analyze any extracted query by hash for a full reportrdst query list— browse every query scan has saved- Core concepts — The semantic layer — annotate your schema once and every scan gets better