{"slug": "your-langchain-sql-agent-sends-the-model-every-table-including-the-ones-that-can", "title": "Your LangChain SQL agent sends the model every table, including the ones that caller can't read", "summary": "A developer built Schemagate, a LangChain retriever that filters database schema objects by the calling user's grants before any table information reaches the model, addressing the fact that LangChain's SQLDatabase.get_table_info() exposes everything the connection can see rather than what the individual caller may read. The retriever subclasses BaseRetriever and binds caller identity at construction time so a chain cannot omit it and silently fall back to the full schema. It supports Postgres, Oracle, MySQL and SQL Server, reading VPD policies on Oracle, and installs via pip as an optional LangChain extra.", "body_md": "A LangChain SQL agent hands the model your schema and asks it to write SQL. On a demo database with eight tables that is fine. On a real one it breaks in two ways at once.\n\n**The schema outgrows the context window.** I wrote about this before, on a warehouse with [1,245 tables](https://dev.to/ashish_sinha_5241c7673d93/i-described-1245-tables-with-an-llm-and-retrieval-got-worse-58a) — the table listing alone did not fit, and describing every table with an LLM to help retrieval made it *worse*, not better.\n\n**And the model sees tables the caller is not allowed to read.** This one is quieter and worse. `SQLDatabase.get_table_info()` returns what the *connection* can see, not what the *person asking* can see. If your app connects as one service account and serves twenty users, every user's agent gets the full schema: salaries, PII, all of it. The model may never write SQL against those tables. It was still told they exist, and their column names went into the prompt.\n\nSo I wrote a retriever.\n\n``` python\nfrom schemagate import Catalog, Principal\nfrom schemagate.integrations.langchain import SchemagateRetriever\n\ncat = Catalog().bootstrap(\"postgresql://localhost/app\")\n\nretriever = SchemagateRetriever(\n    catalog=cat,\n    top_k=6,\n    principal=Principal(\"okta:jdoe\", roles={\"finance\"}),\n)\n\ndocs = retriever.invoke(\"revenue by month\")\n```\n\nIt subclasses `BaseRetriever`, so it drops into any chain that already takes a retriever. Each selected object comes back as one `Document`: the DDL in `page_content`, and the name, kind, score and selection reason in `metadata`.\n\nTwo things happen before the model sees anything. The caller's grants decide which objects are candidates at all. Then the question decides which of *those* are relevant. A table jdoe cannot read is not ranked and then filtered out — it never enters the ranking.\n\nThis is the part I would push back on in someone else's library, so here is the reasoning.\n\n```\nSchemagateRetriever(catalog=cat, principal=p)   # identity fixed here\nretriever.invoke(question)                      # not here\n```\n\nA retriever is usually built once per request. Binding identity to the object means a chain **cannot forget to pass it**. There is no call signature where the principal is optional and quietly defaults to everything. If you want a different caller, build another retriever — they are cheap.\n\nThe alternative, `invoke(question, principal=...)`, has one failure mode I did not want to ship: somebody omits the argument, the call still succeeds, and it returns the whole schema. Fail-closed beats convenient.\n\n```\npip install 'schemagate[langchain]'\n```\n\nNeeds `langchain-core>=0.3`, and it is an optional extra — if you do not use LangChain you do not pay for it.\n\nPostgres, Oracle, MySQL and SQL Server. On Oracle it reads VPD policies rather than inferring from grants.\n\nIf you are running a SQL agent against a database where different callers should see different tables, I would like to know how you handle it today — especially if the answer is \"one service account and we hope\". That was the answer where I work, which is why this exists.", "url": "https://wpnews.pro/news/your-langchain-sql-agent-sends-the-model-every-table-including-the-ones-that-can", "canonical_source": "https://dev.to/ashish_sinha_5241c7673d93/your-langchain-sql-agent-sends-the-model-every-table-including-the-ones-that-caller-cant-read-56oo", "published_at": "2026-09-30 03:40:50+00:00", "updated_at": "2026-09-30 03:46:34.963345+00:00", "lang": "en", "topics": ["ai-agents", "large-language-models", "ai-tools", "developer-tools", "ai-safety"], "entities": ["LangChain", "Schemagate", "Postgres", "Oracle", "MySQL", "SQL Server", "BaseRetriever", "SQLDatabase.get_table_info()"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/your-langchain-sql-agent-sends-the-model-every-table-including-the-ones-that-can", "markdown": "https://wpnews.pro/news/your-langchain-sql-agent-sends-the-model-every-table-including-the-ones-that-can.md", "text": "https://wpnews.pro/news/your-langchain-sql-agent-sends-the-model-every-table-including-the-ones-that-can.txt", "jsonld": "https://wpnews.pro/news/your-langchain-sql-agent-sends-the-model-every-table-including-the-ones-that-can.jsonld"}}