cd /news/ai-agents/letting-ai-agents-query-production-d… · home › topics › ai-agents › article
[ARTICLE · art-170076] src=dev.to ↗ pub= topic=ai-agents verified=true sentiment=↑ positive

Letting AI agents query production databases without giving them a database account

A developer released Truejoin, an open-source (Apache 2.0) data gateway that lets AI agents query production PostgreSQL, MySQL/MariaDB, SQL Server and Oracle databases without granting them a database account. Truejoin rewrites each query before execution, replacing named tables with subqueries that carry the caller's row filters and column masks, so joins, CTEs and aggregates only ever read the restricted view. On 16 held-out questions over a banking-shaped schema with qwen2.5-coder:7b, the developer reports 12 correct answers and 0% hallucination using approved business terms with no model, versus 8 correct and 31.2% hallucination when the model wrote SQL from the DDL.

by read4 min views1 publishedNov 15, 2026

Once a company has an AI assistant, someone asks whether it can answer questions over production data. I do data consulting for companies that keep everything on-premises, and I kept hearing that question.

The usual answer is a read-only database account plus a text-to-SQL prompt. That leaves three things unsolved:

I built Truejoin to close those three, on your own servers, with Oracle and SQL Server included.

Truejoin is an open-source (Apache 2.0) data gateway. It connects to PostgreSQL, MySQL/MariaDB, SQL Server and Oracle, and serves them three ways from one Python process and one config file:

Every query, whoever wrote it, runs under the caller's row rules and column masks and leaves one audit event. It is not a warehouse, an ETL tool or a BI dashboard. It reads the databases you already have, live.

Filtering the result is too late. An aggregate over rows the caller may not see has already leaked, and so has a masked column read through an alias, a join or a subquery.

So Truejoin rewrites the query before it runs. Each table the query names is replaced by a subquery that carries that caller's row conditions and masks. Aliases, joins, CTEs and aggregates then all read the restricted view, because there is nothing else to read.

The rules are a few lines of config (trimmed here; the full nesting is in config/reference.yaml):

row_filters:
  orders: [{column: tenant_id, value: "${tenant}"}]
row_filter_exempt_roles: [dpo]

masking:
  enabled: true
  rules: {email: partial, national_id: hash}

Three things follow from doing it this way:

A model reads the schema and drafts table and column descriptions, PII labels, relationships and business terms (truejoin catalog analyze). Sample values are masked before they reach it, and with Ollama nothing leaves your network.

A person then reviews, approves and certifies the draft. Nothing unapproved is used.

The approved catalog does two jobs. Masks key off its semantic types, and questions are read against its business terms.

answer_question first reads a question against the approved vocabulary, with no model involved. It returns the request it understood and the SQL it compiled, so you can check what was asked before you trust the answer. Joins follow declared foreign keys, so a request cannot write a wrong join.

Only a question the vocabulary cannot read goes to a model, and that answer is marked as model-written.

I benchmarked the approaches on a banking-shaped schema with qwen2.5-coder:7b, graded by result set against a gold query. On the 16 held-out questions:

Approach Correct (of 16) Hallucinated
Model writes SQL from the DDL 8 31.2%
Model writes SQL from the catalog 10 18.8%
Approved terms, no model 12 0%

Sixteen questions is a small set and the intervals are wide. The table in the repo shows 13 for the last row; one of those was right by luck and is now refused, so I count 12. The method and every caveat are in bench/README.md.

Truejoin merges three earlier projects of mine into one package and one process:

truejoin mcp convert, which turns an OpenAPI or Postman description into an MCP server definition. Their code moved over with its history. The query rewriter is the one component with no ancestor in the three, and it carries the privacy promise, so its tests were written first.

You need Python 3.12+ and uv.

git clone https://github.com/yasinyaman/truejoin.git
cd truejoin
uv sync --all-extras

export TRUEJOIN_SECRET_KEY=$(uv run python -c 'from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())')
uv run truejoin config validate -c config/database.yaml
uv run truejoin users add admin --permission all -c config/database.yaml
uv run truejoin serve -c config/database.yaml

The console is at http://localhost:8000/ui. Any MCP client can connect over HTTP at http://<host>:8000/mcp with an API key in X-API-Key. For a desktop client such as Claude Desktop, run the tools over stdio:

{
  "mcpServers": {
    "truejoin": {
      "command": "uv",
      "args": ["run", "--directory", "/path/to/truejoin", "truejoin", "serve",
               "--transport", "stdio", "-c", "config/database.yaml"]
    }
  }
}

The feedback I want most is on the rewriter. Try to read around a row rule or a mask. If a query gets through, please report it privately through a GitHub security advisory rather than a public issue.

The code is at github.com/yasinyaman/truejoin and the docs are at yaman.dev/truejoin.

── more in #ai-agents 4 stories · sorted by recency
── more on @truejoin 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/letting-ai-agents-qu…] indexed:0 read:4min 2026-11-15 · —