Comparison
OrmAI vs. raw SQL
Why exposing a raw SQL handle to your AI agent is the wrong default — and what the right default looks like.
The simplest way to give an AI agent database access is also the most dangerous: hand it a tool that takes a SQL string and runs it. This page is a careful, technical comparison of that pattern with OrmAI, including the cases where raw SQL is genuinely fine.
When raw SQL access is OK
Be honest about this first. There are three scenarios where giving an agent raw SQL is acceptable:
- A sandbox environment with synthetic data, no PII, no real users, no shared tenants. Many internal demos, hackathon projects, and benchmarks live here.
- A read replica with row-level security configured at the database — and you have actually proven the RLS policies are correct, and you accept that any new table is unsafe by default until policies catch up.
- A single-user developer copilot running locally against the developer’s own database, with no network exposure.
Outside those three cases, raw SQL is a foot-gun.
What raw SQL gives the model
Concretely, the tool surface is one function:
def execute_sql(query: str) -> list[dict]:
return db.execute(query).fetchall()
What can the model express through that interface? Everything the database can do. That includes:
- Cross-tenant joins.
- Unbounded scans.
- Destructive DDL.
- Functions that read filesystem paths (Postgres
pg_read_server_files, MySQLLOAD_FILE). - Time-based denial of service via expensive queries.
- Information-schema introspection that leaks the existence of tables you didn’t intend to expose.
You can mitigate some of these. You can revoke DDL privileges on the role. You can set statement_timeout. You can set max_rows. You cannot statically prove that a string the model produced doesn’t violate your tenancy boundary, your PII policy, or your row budget.
What OrmAI gives the model
A small set of typed, parameterized operations:
db.query(model, where, include, order, limit, cursor)db.get(model, id)db.aggregate(model, group_by, aggregations, where)db.create(model, data, reason)db.update(model, id, data, reason)db.delete(model, id, reason)db.describe_schema()
Each of those is a structured contract. The model fills in the slots; OrmAI compiles the call against the policy and the underlying ORM. SQL is generated by the ORM, parameterized as it always is, and bound to safe values that came out of a JSON object — never a string the model authored.
A direct comparison
| Concern | Raw SQL handle | OrmAI |
|---|---|---|
| SQL injection | Possible. Even with ? params, the model writes the surrounding query. | Structurally impossible. The model never authors SQL. |
| Tenant isolation | Manual. Every prompt has to remind the model. | Automatic. tenant_scope("tenant_id") injects the filter. |
| Field-level redaction | Cannot be enforced from outside the query. | Per-model field allow / mask / hash / deny rules, applied to every result. |
| Row caps | Possible via LIMIT you hope the model writes. | Hard cap from policy, plus per-tool override. |
| Statement timeout | Set at session level. | Set at session level and via per-call budget. |
| Audit log | You write it yourself. | Automatic, structured, including the policy decision. |
| Schema discovery | Model can introspect information_schema freely. | describe_schema returns only the policy-visible surface. |
| Write gating | All-or-nothing per role. | Per-model, per-field, with required-reason and approval flags. |
| Read budget | None natively. | Per-tenant, per-window quotas. |
| CI/CD policy regression | None. | Policy is a Python/TS file, testable like any code. |
Two failure modes you’ll hit in production
These are the two incidents we have seen happen most often when teams ship raw SQL agents.
1. The cross-tenant leak
A customer asks the chatbot, “show me my recent orders.” The model writes:
SELECT * FROM orders WHERE created_at > NOW() - INTERVAL '30 days' ORDER BY created_at DESC LIMIT 50;
Notice what’s missing. The model has no concept of which tenant the request came from. It just wrote the query the user asked for. The result includes orders from every tenant in the system. The next thing that happens is a meeting with your security team.
With OrmAI:
toolset.execute("db.query", {
"model": "Order",
"where": {"created_at__gt": "now()-30d"},
"limit": 50,
}, ctx=ctx) # ctx.tenant_id was set at request time
The policy file declared tenant_scope("tenant_id"). The compiler injects WHERE tenant_id = :ctx_tenant_id into the generated query. The model could not omit it if it tried.
2. The runaway aggregate
Customer asks, “what’s our revenue trend?” Model writes:
SELECT date_trunc('day', created_at) AS d, SUM(total) FROM orders GROUP BY 1 ORDER BY 1;
Looks innocent. Against a 200-million-row orders table with no index on created_at, this is a 12-minute table scan that locks out other queries.
With OrmAI:
toolset.execute("db.aggregate", {
"model": "Order",
"group_by": ["created_at__day"],
"aggregations": {"total": "sum"},
}, ctx=ctx)
The policy declared a budget: max_scan_rows: 1_000_000, statement_timeout_ms: 5000. The compiler inspects the planned query, sees the unindexed scan exceeds the row budget, and rejects the call with a structured error the agent can recover from. Nothing reaches the database.
”But raw SQL is more flexible”
Yes — for queries you cannot prove safe.
In real workloads, agents do a small number of shapes very often: filtered reads, joined fetches, group-by aggregates, point updates. OrmAI covers those natively. For the long tail of genuinely bespoke queries — an analyst-style report, a domain-specific calculation — you write a named domain tool: a Python function reviewed by your team, parameterized, registered with the policy. The model gets analytics.churn_cohort(month: str) instead of “write me some SQL.”
This is more, not less, expressive — because the bespoke tool can do things raw SQL can’t (call a function, hit a cache, integrate a model). And it’s auditable. You can search “every time the agent computed churn” without parsing SQL strings.
When you genuinely need raw SQL
Sometimes you do. A read-only analyst sandbox over a redacted warehouse, for example. Or a developer asking, “explain this query plan.” OrmAI supports an explicit, opt-in “advanced” tool that runs a SQL string with the strictest possible budget and a different audit channel. The point is that it is opt-in, fenced off, and visibly separate from the policy-checked surface.
Bottom line
Raw SQL is a tool for humans, code-reviewed and shipped through deploy pipelines. AI agents are not humans, are not code-reviewed, and are not behind your deploy gate. Treating their queries the same as a developer’s queries is a category error.
OrmAI gives the agent a smaller, typed surface that compiles to exactly the SQL you would have approved — with audit and policy as a single declarative file rather than scattered prayers.
Related
Found a typo or want to suggest a topic? Email [email protected].