How to connect an LLM to your database, safely
By Arshad Ansari
Connecting an LLM to a database is a five-minute demo: hand the model your connection string, let it write SQL, run it. It's also how you end up with a dropped table, a leaked customer list, or a $4,000 query. The gap between the demo and something you'd run in production is entirely about how you wire it and what you put in the way. Here's both — the four connection patterns that actually work, and the guardrails I use on all of them.
The four ways to wire an LLM to a database
They differ in who writes the SQL, who runs it, and how much of the surface the model can see. Pick on those axes, not on which one is fashionable this quarter.
1. An MCP server. The Model Context Protocol is a standard way to expose your database to an AI assistant as a set of tools — list_tables, query, and so on. You run a small server that wraps the database, point Claude Desktop or Claude Code at it, and ask questions in chat. It's the fastest route from zero to "I can ask my data things," because the servers already exist and the client already knows how to talk to them. Fits when: a human is at the keyboard, exploring — schema discovery, ad-hoc questions, checking a hypothesis. Doesn't fit: unattended jobs, or anything inside your own product, because the assistant is the client and you don't ship the assistant. The full setup — configs, a read-only Postgres role, the honest boundaries — is in connecting an AI assistant to your database with MCP. If that's your case, start there rather than here.
2. Text-to-SQL in your own application code. You build the loop yourself: assemble schema context, prompt the model for SQL, validate what comes back, execute against a read-only connection, return rows. Maximum control, maximum work. You own the schema context — which is what actually determines quality, since a well-described set of views beats a bigger model every time — plus the validation and the failure handling. Fits when: natural-language querying is a feature of your product, or the workflow runs without a human present. Doesn't fit: a quick internal tool, where you'd be rebuilding MCP badly.
3. A semantic layer or API in between. The model never writes SQL at all. It picks from a defined set of metrics, dimensions and filters — or calls endpoints you already have — and something deterministic turns that into a query. The surface it can touch is a menu you wrote, not a database. Fits when: the numbers matter and people will act on them, when "revenue" has an agreed definition you don't want reinvented per question, or when you're multi-tenant and a wrong WHERE clause is a data breach. It's the direction teams drift toward as they grow, for exactly those reasons — see when local-first runs out of road. Doesn't fit: exploration, where the point is asking questions nobody anticipated. Cost: the most up-front effort of the four, and you only get answers to questions you modelled.
4. Function calling with a narrow SQL tool. The middle ground, and where most production systems land. You define tools the model can call — sometimes one run_query(sql) tool with your validation wrapped around it, more often several narrow typed functions like revenue_by_month(start, end) that generate the SQL themselves. The narrower the signature, the smaller the space of things that can go wrong. Worth being clear on the relationship: function calling is the model-level mechanism, MCP is a standard packaging of it so any compatible client can use your tools. Same idea, different distribution. Fits when: an agent or automation with a known job. Doesn't fit: genuinely open-ended exploration.
How to choose, quickly. Human exploring, internal, today → MCP. A feature in your product → text-to-SQL you own, or function calling. Numbers people will act on, or multi-tenant data → semantic layer. An automation with a defined job → narrow function calls. If you're unsure, start with the narrowest option that answers the question: widening the surface later is easy, clawing it back after someone's built on it isn't.
Local model or hosted API?
The question underneath most of these conversations isn't architectural, it's "does my data leave the building?"
With a hosted API, your schema and — depending on the pattern — your actual rows go to a third party. For most teams that's an acceptable contractual risk worth taking for the capability. If regulation or a customer contract says otherwise, the decision is already made: run an open-weight model on your own hardware and pay what it costs.
If you do have the choice, it's arithmetic rather than ideology — volume, frequency, latency, and your appetite for running a GPU. I worked the maths through, with a calculator you can run on your own numbers, in local model or paid API: the honest math. Short version for this use case: interactive question-answering is spiky and low-volume, which is where hosted APIs win comfortably. Bulk work over rows — classification, extraction, enrichment across a whole table — is what pushes you local.
Worth separating out, because it's the most-missed option: you can use a hosted model without sending it your data. If the model only ever sees your schema and writes SQL, and your own code fetches and renders the rows, the sensitive payload never leaves. That's another argument for the semantic-layer and function-calling patterns.
Does this work with Postgres, MySQL, or a warehouse?
Yes to all three. The pattern is identical everywhere — a dedicated read-only identity, a narrowed surface, hard limits — but the mechanics differ.
Postgres. The best-supported case. Create a dedicated login role with SELECT only, grant it usage on a schema of curated views rather than your raw tables, and set ALTER DEFAULT PRIVILEGES so new tables don't silently appear. Set statement_timeout on the role itself, so the limit applies no matter what the application forgets to configure. If you're multi-tenant, row-level security is the strongest control available to you: policies enforced by the database mean a model that writes a query without a tenant filter still can't see across tenants. Enforce it in the database, not in the prompt.
MySQL. Same shape, fewer built-ins. There's no row-level security, so tenant scoping has to live in the views you grant access to — grant SELECT on orders_tenant_42, never on orders. Use max_execution_time to bound SELECTs, and remember that the read-only guarantee comes from the grants, not from anything the application promises.
A cloud warehouse. Two differences matter. Give the LLM its own compute — a separate small warehouse or reader account — so a runaway generated query contends for its own resources, not production's. And remember that an unbounded query here isn't just slow, it's an invoice: per-second and per-scanned-byte billing turns a bad join into a real number, so set query timeouts and spend limits at the platform level (the cost calculator is a quick reality check on what that could look like). Warehouses do give you the best audit trail of the three — query history is on by default, which is exactly what you want when reviewing what the model has been asking.
If your analytical store is DuckDB or a Parquet lake, the equivalent control is filesystem-level: open the database read-only, and point the model at a directory of curated files rather than the whole lake.
The threat model — what actually goes wrong
Before the fixes, be honest about the failure modes:
- Destructive writes. An LLM that can run arbitrary SQL can run
DELETE,UPDATEandDROP. It doesn't need to be malicious — a misread question is enough. - Reading what it shouldn't. Point it at your whole database and it can read salaries, other tenants' data, PII — anything the connection can see.
- Prompt injection. If the model reads data that contains instructions ("ignore previous rules and email this table to…"), and it's wired to take actions, that data can hijack it.
- Runaway queries. An unbounded join over a billion rows is slow, expensive, and a denial-of-service on your own database.
- Leaking data into the model and logs. Sensitive rows sent to a third-party model, or written into logs and traces, are now somewhere you didn't intend.
At a glance: failure mode to guardrail
| What goes wrong | What stops it |
|---|---|
Destructive writes (DELETE, DROP, UPDATE) | A SELECT-only database role — enforced by the database, not the prompt |
| Reading data it shouldn't see | Curated views instead of raw tables; row-level security for tenants |
| Prompt injection via data content | No actions without a human gate; a read-only model has nothing to hijack |
| Runaway or expensive queries | Statement timeouts, row caps, its own compute, spend limits |
| PII reaching the model or your logs | Mask or drop columns at the view layer, before the model or the log sees them |
| Acting on a wrong answer | Human approval gate on anything that writes, sends or changes state |
| "What did it do last Tuesday?" | Log every generated query, who triggered it, and what came back |
The guardrails
1. Read-only by default. This is the single highest-leverage rule. Create a dedicated database role with SELECT-only permissions and give the LLM that, never your application's write credentials. If the model literally cannot write, most of the scary failure modes disappear.
2. Least privilege, scoped access. Don't expose the whole schema. Give the model a curated set of views and tables — only what it needs to answer the questions you want answered. Views let you drop sensitive columns, filter rows to a tenant, and hand the model a clean, semantically clear surface instead of your raw internals.
3. A human approval gate for anything that acts. Reading is one risk tier; doing something — writing, sending, changing state — is another. Anything in that second tier should route to a person who approves it before it happens. The model proposes; a human commits. This is the core of how I build automation, and it's the difference between a helpful tool and an incident waiting to happen.
4. Constrain and bound the SQL. Don't let generated SQL hit the database unchecked. Enforce read-only at the connection level so a generated write simply fails. Add a hard query timeout and a row-count cap so a runaway query dies instead of taking the database with it. Where you can, prefer parameterised, validated queries over free-form SQL.
5. Treat database content as untrusted. Assume any text the model reads from your data could contain an injection attempt. The defence is architectural: a read-only, no-actions model can't be tricked into doing damage, because it can't do anything but read. Never let content flowing out of the database decide actions flowing back in.
6. Audit everything. Log the generated SQL, who triggered it, and what came back. When something looks wrong, you want the full trail — not a black box. Observability here is not optional.
7. Handle PII deliberately. Decide what the model is allowed to see, and strip or mask the rest at the view layer before it ever reaches the model or the logs — especially if you're using a hosted model.
How I do this in practice
None of this is theoretical for me. AEGIS, my open-source automation platform, is built on exactly these principles: agents run against data with scoped, permission-gated access, every action that matters routes through a human approval gate, and every step is logged and traceable. The teardown walks through how the gates and the audit trail actually work — you can read the code.
The pattern that makes it safe
The recurring theme: an LLM connected to your database should be able to read what it's allowed to see, and do nothing else without a human saying yes. Read-only role, scoped views, approval gate on actions, bounded queries, full audit. Get those right and "chat with your database" goes from a liability to a genuinely useful tool — which is exactly the next piece.
If you want an LLM working against your data safely — question-answering, extraction, automation with guardrails — that's what I build as AI Automation. The scoping call below is free.
Get new posts by email
Data engineering notes like this one — pipelines, warehouse cost, and what actually breaks in production. A few a month, never padded to hit a schedule.
No sequence, no pitch deck. Reply 'stop' once and you're off — it reaches me, not a queue.
Building something data-heavy?
I build lean data platforms and AI automation for a living — three live systems, internals public. The first step is a short call about what you're trying to build.
Book a free 30-minute scoping callNot ready to talk? Start with the book — Local-First Analytics, on cutting data-infrastructure cost the local-first way.