Stop Letting the AI Guess Your Column Names
Live MCP access still fails when the model skips describe_table and invents columns. How to force schema checks, when execute_sql goes wrong, and prompts that cut the retry loop.
You gave the model a live database. It still wrote SELECT email FROM … against a table that never had an email column.
That's not a failed connection. That's skipping the catalog. Live MCP tools can list and describe tables — the model often doesn't bother. It reaches for execute_sql with a name that sounds right, eats the error, invents a second name, and burns three turns before it opens describe_table.
Related background: why AI hallucinates your database schema (no live catalog) and Postgres schemas beyond public (live catalog, wrong namespace). This post is about the middle failure: tools available, inspection skipped.
What the failure looks like in logs
Real patterns from AI-on-database usage (the wording will look familiar):
column "email" does not existInvalid column name 'owner'Incorrect syntax near the keyword 'from'(column named like a reserved word, unquoted)Invalid column name 'activitypointerid'(almost the right CRM entity, wrong key name)
Notice what these are not: auth failures, plan limits, or "table not found" for a missing schema. The table was there. The column guess was wrong.
A good session recovers in one hop: error → describe_table → corrected SQL. A bad session loops guesses. Your job is to make the good session the default.
Why models skip describe_table
Speed bias. Writing a SELECT feels like progress. Calling describe_table feels like overhead — until the SELECT fails.
Tutorial priors. users.email, orders.user_id, created_at everywhere. Your table uses primary_email, account_id, inserted_at. The prior wins unless the catalog overrides it.
Reserved words. Columns like from, to, user, order are legal in SQL Server or Postgres if quoted ([from], "user"). Models forget the quotes; the database reports a syntax error that looks like incompetence rather than "quote this identifier."
Wide tables. CRM and SaaS schemas have dozens of similarly named fields (ownerid vs owneridname vs owninguser). Close isn't good enough.
Live access removes the excuse of "I never saw the schema." It does not remove the habit of guessing.
The rule that actually works
Before any non-trivial execute_sql:
list_tablesif you're not sure which relation holds the data (and useschema.tablewhen you're outsidepublic)describe_tableon every table you will touch- Then write SQL using only names from that describe output
- Prefer
query_tablefor "show me rows where X = Y" — fewer invented identifiers - Keep
execute_sqlfor joins, aggregates, and CTEs — after the describes
Put the rule in Cursor rules, Claude project instructions, or a sticky system note:
For this database connection: never call execute_sql until you have described each table in the query in this thread. If a query fails with a missing column or syntax error, call describe_table before rewriting. Quote reserved-word identifiers.
One paragraph. Paste once. Cuts a surprising amount of thrash.
query_table vs execute_sql (use both on purpose)
| Job | Prefer |
|---|---|
| Peek at a table, filter one column, LIMIT 50 | query_table |
| Count / group / join / window | execute_sql after describe |
| "What columns does this have?" | describe_table — don't SELECT * to learn the shape |
| Reserved-word columns | execute_sql with proper quoting, after describe |
query_table isn't morally superior — it's a narrower API. Narrower APIs leave less room for fantasy column lists. When the question needs real SQL, use it; just don't skip the describe.
Also remember result size: dumping thousands of rows into chat wastes the customer's model tokens and rarely answers the question. Aggregate first. Gateways that cap large execute_sql results are doing you a favor — treat a truncation notice as "rewrite with LIMIT or GROUP BY," not as a broken tool.
Prompts that reduce guessing
Bad: "Get me everyone who signed up from Google."
Better: "Describe the table that stores signup attribution (find it with list_tables if needed). Then write a query using only those columns — show the SQL before summarizing."
Bad: "Join email to leads and show the latest."
Better: "Describe email and lead (or the equivalents you find). Note primary keys and the regarding/party link columns. Then join — quote any reserved names."
After an error: Don't say "try again." Say: "Call describe_table on that table and fix the query from the real columns."
For metric questions, add definition discipline on top — AI metric traps — so you don't get a perfect column list and a wrong meaning of "active."
What the gateway should do (and what it can't)
A managed MCP gateway can:
- Block writes (read-only)
- Cap rows and timeouts so runaway SELECTs don't hang forever
- Return errors that hint to describe tables and quote reserved words
- Restrict which tables exist on the connector (allowlists)
It cannot sit inside the model's head and forbid guessing. That's prompt, habit, and occasionally a sharper tool description ("call describe_table before execute_sql").
Synra does the gateway side — including clearer schema errors and row caps on raw SQL. The inspection habit is still yours to set in the client.
Quick checklist
- [ ] Connection test green;
list_tablesreturns the relations you expect (includingschema.tableif needed) - [ ] Project/rule text requires describe-before-execute
- [ ] First failure → describe, not a new guess
- [ ] Simple filters via
query_table; joins/aggregates viaexecute_sql - [ ] Spot-check one answer against a known row (verify)
Bottom line
A live database connection makes correct column names available. It does not make the model use them. Force describe_table before execute_sql, lean on query_table for simple reads, and treat "column does not exist" as a cue to inspect — not to invent a synonym. The schema was never the mystery; the skipped step was.