Readyset Docs

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

LanguageORMs
PythonSQLAlchemy (1.x and 2.0), Django ORM
TypeScript / JavaScriptPrisma, 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 30

How 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: FAIL

Analysis modes

ModeNeeds DB?What it checks
(no flag)noExtraction only — produces SQL and saves to registry
--analyze --shallownoSchema-only LLM analysis. Detects SELECT *, missing LIMIT, obvious missing indexes, N+1 patterns.
--analyzeyesFull 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 codeMeaning
0All queries ≥ fail threshold (PASS)
1At 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
RefWhat gets scanned
HEADUncommitted changes
HEAD~1Files changed in the last commit
main, abc123Files changed since that commit/branch

Flags reference

FlagWhat it does
directoryDirectory to scan (positional)
--dry-runShow what would be scanned without scanning
--analyzeRun performance analysis against the extracted queries
--shallowSchema-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
--checkCI 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)
--nosaveDo not save extracted queries to the registry
--sequentialRun analysis queries one at a time (more deterministic scores, slower)

Risk scoring

Each query gets a score from 0 to 100:

RangeMeaning
80–100Low risk
50–79Medium risk
30–49High risk
0–29Very 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