Open to AI engineering roles

Production

DBWhisper

A natural-language-to-SQL agent with schema-aware retrieval, SELECT-only validation, a read-only database connection, and multi-provider LLM fallback.

  • Designed, built, and maintain (solo)
  • 2025 – present
Product website recording. Some on-screen claims predate the scoped results and limitations documented in this case study. Visit the live app →
query executionHow this was measured — A SELECT-only, single-statement validator checks enrolled tables before execution through a least-privilege read-only connection. Queries outside the enrolled schema or without a schema index are rejected.
read-only
LLM providers behind one fallbackHow this was measured — OpenAI → OpenRouter → DeepSeek → Groq → Anthropic → Gemini; the first with credentials wins, Gemini's free tier is the final fallback.
6
SQL dialects supportedHow this was measured — Postgres, MySQL, and SQL Server via SQLAlchemy 2.0 + ODBC, with per-dialect generation directives.
3

Context

Natural-language-to-SQL is easy to demo and hard to trust. Calling an LLM to write SQL takes an afternoon; the reason most such tools never reach production is that a model will happily emit DROP TABLE, a multi-statement payload, or a query against a table that doesn't exist. DBWhisper turns a plain-English question into SQL across Postgres, MySQL, and SQL Server, validates it, runs it through a read-only database connection, and explains the answer.

Constraints

Three constraints shaped the design. Safety must be structural— the read-only promise can't depend on the model behaving. Schemas are large — real databases have dozens to hundreds of tables, too many to paste into a prompt, so the agent has to retrieve the right ones. And it runs on a $0 stack (Vercel + a Hugging Face Docker Space + Neon Postgres), which rules out waste: small prompts, bounded loops, lean images.

System architecture

A split deploy in three tiers: a Next.js UI on Vercel, a FastAPI + LangGraph agent on a Docker Space, and the target database queried through a read-only user. Retrieval and conversation memory sit in Neon Postgres with pgvector.

System map

Tap to inspect

  1. 01
  2. 02
  3. 03
  4. 04

Use the arrow keys, Home, or End to move between nodes. Press Enter or Space to pin a node's decision, and Escape to release it.

DBWhisper — hover or focus a node to trace a request. Dashed edges are fallback paths.

Enrollment (once per database). A target database is introspected, each table is documented into structured sections (summary, columns, relationships, stats), and those chunks are embedded into pgvector. Retrieval later searches the documentation embeddings, not the raw schema — which is what makes it work on large databases at all.

Data flow

One question, end to end: the UI POSTs to /query; the agent runs search_tables to find candidate tables by semantic similarity, pulls only the needed section of each (columns / relationships / stats) so the prompt stays small, checks verified queries for a close human-approved example to adapt, then generates SQL. Generation passes through validate_sql before anything touches the database; the query executes under a timeout and row cap; the result is summarized back into natural language. A per-turn LangGraph checkpoint gives follow-ups context.

DBWhisper · the flagship

Inside the request path.

Explore how DBWhisper retrieves context, generates SQL, checks it, and returns results. This illustration uses sample data; published evaluations are linked separately.

  1. pgvector similarity over embedded table docs pulls only the tables a question needs — never the whole schema into the prompt.

    • search_tables · pgvector similarity
    • Structured schema sections
    • Verified NL→SQL examples
Read the design decisions
dbwhisper · Retrieve

Example: Monthly revenue from January through April 2024

ordersmatched · 12 columns
customers9 columns
order_items7 columns
products11 columns
search_tables("monthly revenue") → orders

Key decisions

The decisions that make it trustworthy — each with the alternative it beat and the cost it accepted.

A deterministic, fail-closed SQL validator — not LLM trust

Instead of: trust the model's SQL, an LLM-as-judge safety check

It uses sqlparse (not fragile regex) to enforce SELECT/WITH-only, single-statement, no DDL/DML/EXEC, no information_schema/sys/pg_catalog — and, given a database, rejects any query touching a table outside the enrolled schema. If no schema index exists, it fails closed rather than open.

Trade-off accepted — Rejects some exotic-but-valid queries and adds an enrollment step — a false 'safe' is far costlier than a false 'unsafe'.

Defense in depth, not a single gate

Instead of: rely on the validator alone

Below the validator: read-only DB users (a probe warns if a connection looks writable), a query timeout + row cap, and log sanitization that masks passwords, API keys, and SQL literals. The warning probe does not replace correctly configured database permissions.

Trade-off accepted — More moving parts to maintain and test.

Multi-provider LLM fallback over single-vendor lock-in

Instead of: one provider (e.g. OpenAI only)

A priority chain (OpenAI → OpenRouter → DeepSeek → Groq → Anthropic → Gemini) picks the first provider with credentials, with Gemini configured as the final fallback. Requests still depend on available credentials, provider capacity, and quota.

Trade-off accepted — Six provider adapters to keep working behind one interface.

Retrieval over context-stuffing

Instead of: dump the whole schema into the prompt

Schemas are documented, embedded, and retrieved per question, keeping the prompt small and the hallucination surface (and token cost) low.

Trade-off accepted — An enrollment/embedding step per database.

What failed

Early on, the model would emit SELECT TOP n against Postgres — valid T-SQL, invalid everywhere else — so cross-dialect queries failed. The fix was a deterministic dialect directiveinjected before generation, stating the target dialect's rule explicitly (LIMIT for Postgres/MySQL, TOP for SQL Server) rather than hoping the model inferred it.

v1brokencross-dialect generation

Cause — the model defaulted to SELECT TOP n regardless of the target database

v2correctcross-dialect generation

Fix — inject a per-dialect directive before generation, and cap tool loops so a confused agent can't spin

Performance & cost

The controls and the measurements answer different questions. These are the configured execution boundaries, followed by the observed evaluation results.

MetricValueMethod
query executionread-onlyFail-closed validator + read-only DB user; a query is rejected if it references a table outside the enrolled schema.
LLM providers6One fallback interface; Gemini free tier is the final fallback so it runs with zero paid keys.
SQL dialects3Postgres, MySQL, SQL Server via SQLAlchemy 2.0 + ODBC.
per-run guardrailsk=4 · ≤8 calls · 30sRetrieval returns the top 4 tables; each tool is capped at 8 calls per run; execution runs under a 30s timeout, a 1000-row cap, and a 5000-char SQL limit.
infra cost$0Vercel + Hugging Face Docker Space + Neon Postgres, all free-tier.

Results (measured)

Two evaluation scopes: the complete pipeline on the golden set, and the generation core on a Spider dev subset. Each result states its method and limitations:

73% (101/139, dev)Spider · Execution accuracy
Scoped Spider dev-split run of DBWhisper’s generation model (qwen/qwen3-32b at temp 0.1) with each database’s schema in context: 139 questions across 18 databases. Generated SQL is executed against the real Spider SQLite databases and compared by result set — ordered when the gold query has ORDER BY, multiset otherwise (standard execution match). 101/139 correct; malformed generations count as incorrect. 9 of the 148 sampled questions could not be scored after repeated provider throttling and are excluded, not counted either way. Measures the NL→SQL generation core; the deployed agent adds schema retrieval and a read-only validator on top.
82% exact · 100% fail-closedCustom golden-query set · Execution accuracy · fail-closed refusals
22 natural-language golden queries + 4 unsafe/out-of-scope prompts over a read-only Postgres store, run end-to-end through DBWhisper’s live pipeline (schema retrieval → generation → read-only validator → execute). 82% exact result-set match (18/22); 95% (21/22) when crediting correct answers that returned an extra column. All 4 destructive or out-of-scope prompts were refused fail-closed (4/4).
See the full eval registry

Operations

It runs as a split deploy with the agent in a Docker Space. Per-run guardrails keep it bounded: each tool result is cached per run, and a tool called more than eight times returns an abort hint that tells the model to stop looping and produce output. Logs are sanitized before write, and the validator's rules are covered by unit tests that gate CI (pytest + ruff, alongside a gitleaks secret scan).

app/core/sql_validator.pypy
def validate_sql(sql: str, db_flag: str | None) -> None:
    stmt = _single_statement(sql)              # exactly one statement
    _require_read_only(stmt)                    # starts with SELECT/WITH; no DDL/DML/EXEC
    _reject_system_tables(stmt)                 # no information_schema / sys. / pg_catalog.
    if db_flag:
        schema = _load_schema_index(db_flag)    # fail closed if missing
        _require_enrolled_tables(stmt, schema)  # only tables we can vouch for

What I'd do differently

Two things. I'd add a small golden-query eval per enrolled database so retrieval and generation quality are measured, not assumed — right now the verified-query flywheel is the feedback loop, but it isn't scored. And I'd move the six provider adapters behind a single typed capability interface earlier; retrofitting consistent retry/timeout behavior across them was more work than building it in from the start.

Evidence

The app is live and the source is public — the validator, the agent tools, and the multi-provider chain are all in the repository linked at the top of this page.

Changelog

  1. Schema browser, a verified NL→SQL training flywheel, and the full marketing site.

  2. Flagship web console — auto-generated charts, CSV export, sortable results, staged progress.

  3. Production hardening: Argon2id auth, per-query tenancy, API-key + per-IP rate limits, a SQL allowlist, and optional Sentry.

  4. Productionized from a prototype — Next.js frontend, dialect-agnostic schema extraction, and a live demo on Neon.