Dev.to Security 🔐 Cybersecurity 👁 0 📖 2 min read

Hiding the table is not enough. Your LLM can still see the salary column.

Most text-to-SQL setups protect data at the table level. The analyst cannot read hr_compensation, so that table never goes into the prompt. Good. But a lot of sensitive data does not live in its own table. It lives in o

Most text-to-SQL setups protect data at the table level. The analyst cannot read hr_compensation, so that table never goes into the prompt. Good.

But a lot of sensitive data does not live in its own table. It lives in one column of a table everyone uses. employees.salary. customers.national_id. patients.diagnosis. If the model sees the column name in the schema, it will happily write SELECT salary FROM employees. Your database might block the query, or it might not. Either way, the model has already learned the column exists.

schemagate 1.1.0 adds column rules you can write in a JSON file:

{
  "restrict_column": {
    "employees": {
      "salary": ["hr"],
      "national_id": ["hr", "compliance"]
    }
  }
}

A caller without the hr role now gets the employees table with salary simply not there. Not marked secret. Not ranked low. Absent. The model cannot write a query against a column it was never shown.

Try it in one minute

pip install schemagate

# the bundled demo database; this prints its sqlite:/// URL
python -c "from schemagate.demo_schema import create_demo_db; print(create_demo_db())"

echo '{"restrict_column": {"hr_compensation": {"annual_amount": ["hr"]}}}' > catalog.json

schemagate select --url <url printed above> --prompt "salary by employee"
schemagate select --url <url printed above> --config catalog.json --prompt "salary by employee"

The first prompt shows annual_amount. The second does not.

The same file works everywhere

The command line, the local Studio page and the MCP server all read the same catalog.json. So if you connect Claude, Cursor or any MCP client to your database through schemagate, the column rule applies there too. You state the policy once.

A typo is an error, not a silent no-op

This one matters more than it sounds. Before 1.1.0, a block the loader did not know was quietly ignored. Write "restrict_columns" with an extra s, and you had a file that looked like a policy and did nothing.

Now it stops with one line:

schemagate: --config catalog.json: unknown block(s) 'restrict_columns'; expected any of restrict, restrict_column, hint, describe, groups

The same goes for a table or column name that does not exist. An access rule that "succeeds" on a typo is a rule that is not there.

Where this fits

schemagate is the step before the model writes SQL: it picks the few tables a question needs, and it drops the ones the caller may not read. That keeps prompts about 75% smaller on a big schema, and it means the model never sees what it should not.

It works with Postgres, Oracle, MySQL, SQL Server and SQLite, as a Python library, a CLI, an MCP server, or a LangChain retriever (now listed in the LangChain docs).

If you protect columns some other way in your text-to-SQL stack, I would like to hear how. Views per role? Column-level grants? Masking in the database?

📰 Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.