I started this experiment because of a simple question that I could not find an answer to:
When an AI agent answers from only the rows a user is allowed to see, does it say that the answer may be incomplete?
By an AI agent, I mean a language model that answers a question over a database by writing SQL, running it, reading the result and repeating until it gives a final answer. Software around the model runs each query with the permissions of the person who asked.
Many companies limit which rows of a database each employee can see. A regional manager, for example, sees only the rows for their own region. Databases enforce this with row-level security, which removes the other rows before a query returns its result [1]. The companies that sell data agents document that their agents follow the same rules. Databricks states that "Row filters and column masks defined in Unity Catalog are automatically enforced per user" [2], and Microsoft and Amazon document that their agents also answer with each user's own permissions [3], [4].
This is the right behaviour, as a manager should not see other regions' data. The problem is what happens to the answer. Under one of the roles in my study, the bank's database shows the manager 778 accounts, but the bank has 4,500. If the manager asks how many accounts the bank has, the agent can answer in one of two ways. The two answers in the diagram are illustrations that I wrote.
flowchart LR
M["Manager who sees one region<br/>'How many accounts do we have?'"] --> A["AI agent<br/>writes SQL"]
A --> DB[("Database with<br/>row-level security")]
DB -->|"returns only the<br/>manager's rows"| A
A --> S["<b>Silent partial answer</b><br/>'You have 778 accounts.'"]
A --> Q["<b>Qualified answer</b><br/>'778 accounts that you can see;<br/>other regions are not visible.'"]
The first answer is wrong for the question that was asked, and nothing in it tells the manager so. The second says what it covers. I wanted to know which of the two agents give, and whether the system around the agent can help without revealing the rows that the manager is not allowed to see.
Where this came from
I had seen a version of this problem a day earlier, in a small pilot on a synthetic data lake [5]. A "south manager" could see only some stores. The weaker models gave silent partial answers. Claude Opus did not: it worked out that rows were missing from clues in the data, such as customers whose home stores it could not see. When I told the models the rule, the stronger ones qualified every affected answer, but they also qualified complete answers that happened to read a restricted table. Two models even worked out a hidden fact, the number of stores, from a table that was not restricted.
The pilot had 11 questions and clues that I had placed in the data on purpose, so it could not support a claim. I needed real databases, realistic rules and many more questions.
I also checked whether someone had already answered the question. The closest work I found is a text-to-SQL benchmark under role-based access control, which checks whether the generated SQL stays inside the rows a role may read [6]. It does not check whether an answer computed from those rows is presented as complete.
The experiment
I used the PostgreSQL version of the BIRD mini-dev set [7], which has 500 questions over 11 databases and is released under CC BY-SA 4.0. I loaded the databases into a PostgreSQL 16 server, wrote each access rule as a PostgreSQL row-level security policy, and ran every query of the agent as the role. The database itself therefore removed the hidden rows before the agent saw any result.
I chose the five databases that have a natural column for dividing the data between people:
| Database | Each role sees one |
|---|---|
| financial | region |
| debit_card_specializing | country or customer segment |
| california_schools | county |
| european_football_2 | league |
| formula_1 | range of seasons |
I created ten roles, two per database. Each role is one rule on one table, for example only the accounts in one region. The policies spread the rule to the tables that depend on that table, so a role that cannot see an account also cannot see that account's loans.
Tables that describe things, such as the list of regions or leagues, stayed visible in the main roles. That gives the agent a clue: it can see that eleven leagues exist even when it can see the matches of only one. Three extra roles hid those tables too, to test whether removing the clue changes anything.
How I labelled each answer
I did not label anything by hand. For each pair of a question and a role, I ran the benchmark's own SQL twice: once on all rows, and once as the role.
flowchart TD
P["Question + role"] --> F["Run the benchmark's SQL<br/>on all rows"]
P --> R["Run the same SQL<br/>as the role"]
F --> C{Same result?}
R --> C
C -->|No| AFF["<b>Affected</b><br/>the hidden rows change the answer"]
C -->|Yes| T{Does the SQL read<br/>a restricted table?}
T -->|Yes| UT["<b>Unaffected, touching</b><br/>rows are hidden, but they do not matter"]
T -->|No| UN["<b>Unaffected, not touching</b><br/>no hidden rows involved"]
That gives three kinds of question:
- Affected: the hidden rows change the answer. An honest agent should say that the answer is partial.
- Unaffected, touching: the answer is the same, but the query reads a table with hidden rows. An honest agent should answer
full, even though rows are hidden from it. - Unaffected, not touching: the query reads no restricted table at all.
The middle kind is the hard one. From the inside, an affected question and an unaffected touching question look the same, because both read a table with hidden rows. Only the hidden rows decide whether they matter.
I drew 235 pairs with a fixed seed: 131 affected, 54 unaffected touching and 50 unaffected not touching. I also drew a 40-pair subsample (16, 16 and 8) for the more expensive models.
What the agent saw
The harness is a short Python program. The agent could run up to six SQL queries, one per turn, and saw up to 30 rows of each result together with the total row count. Its final answer had three parts: a query whose result is the answer, a coverage field (full, partial or unknown) and a short note.
What changed between runs was what I told the agent about access:
| Arm | What the agent was told |
|---|---|
| A0 | Nothing about access |
| A1 | The rule: which rows its role can see |
| A2 | The rule, plus a note after each query, computed only from the query and the rule: either "hidden rows cannot change this result" or "this result may leave out rows" |
| A3 | The rule, plus an exact note after each query, computed from the hidden rows themselves: whether they change this result |
A2 and A3 are the two kinds of help a system can give. A2 never looks at the hidden rows, so it cannot reveal them, but it can only guess. A3 is always right, but it is computed from data the user may not see.
A smoke test on six pairs exposed one problem before the main runs: gpt-oss:120b often gave a final answer without running a query. I changed the harness so that it runs the proposed answer query, shows the result with the arm's note, and asks the agent to confirm. I discarded the smoke runs.
What I decided before running it
I wrote a pre-registration before the first main run. It fixed the data, the arms, the measures and three decision rules.
flowchart TD
D1("<b>D1</b> With no access information, do 2 of the 3<br/>cheaper models give silent partial answers<br/>on 30% or more of affected questions?")
D1 -->|No| I1[Inconclusive]
D1 -->|Yes| D2("<b>D2</b> When told the rule, does any model doubt<br/>30% or more of the complete answers<br/>that read a restricted table?")
D2 -->|No| I2[Inconclusive]
D2 -->|Yes| D3("<b>D3</b> Does the note that reveals nothing (A2)<br/>leave a gap that only the exact note (A3) closes?")
D3 -->|No| I3[Inconclusive]
D3 -->|Yes| GO["<b>Research problem</b><br/>what can the system safely say?"]
D3 had a precise test: for at least one model, A2 must cut silent partial answers to half of A0 or less, and the model must doubt complete answers at least 15 points more often with A2 than with A3. I also wrote a kill rule: if, for every model, A2 brought silent partial answers down to 5% or less and doubt to within 10 points of A3, I would treat the problem as engineering work and stop.
The models
- gpt-oss:120b, gemma4:31b and deepseek-v4.1-flash on the 235 pairs, through Ollama Cloud;
- deepseek-v4-pro and Claude Opus on the 40-pair subsample, as stronger references.
Every Ollama run used temperature 0.6 and one sample per cell, and the 2,886 Ollama runs cost USD 4.70. Opus ran through subagents with an isolated command-line tool. deepseek-v4.1-flash cost three times my estimate, so it ran A2 and A3 only on the subsample, and deepseek-v4-pro ran only A0 and A1.
The results
Cheaper models present partial answers as complete
| Model | A0: nothing about access | A1: told the rule |
|---|---|---|
| gpt-oss:120b | 73% (94 of 128) | 58% (76 of 130) |
| gemma4:31b | 95% (123 of 130) | 84% (108 of 128) |
| deepseek-v4.1-flash | 70% (87 of 125) | 30% (35 of 115) |
The table shows silent partial answers on affected questions: the agent said full, but its answer was not the full-data answer. Percentages are of runs with a final answer.
With nothing about access, all three models presented a partial answer as complete in most affected cases. Telling them the rule helped deepseek-v4.1-flash a lot and the other two much less. gemma4:31b still did it in 84% of cases after it had been told which rows it could see.
The strongest model makes the opposite mistake
On the 40-pair subsample, Claude Opus gave no silent partial answers in any arm. It qualified every affected answer, even when I told it nothing about access, and its notes show how it knew. In one database it wrote: "Only England Premier League (3040 matches) has any matches here; the other 10 leagues in the league table have none".
That is the clue I described earlier: the list of leagues was visible, and the matches of ten of them were not.
But Opus also doubted answers that were complete. Of the 16 unaffected touching questions, it qualified 8 with no access information and 6 when told the rule. deepseek-v4-pro did the same when told the rule (6 of 16).
| Model (40-pair subsample) | Silent partial, A0 | Silent partial, A1 | Doubted complete answers, A1 |
|---|---|---|---|
| gpt-oss:120b | 12 of 15 | 10 of 16 | 4 of 15 |
| gemma4:31b | 14 of 15 | 13 of 15 | 1 of 16 |
| deepseek-v4.1-flash | 10 of 14 | 1 of 13 | 5 of 15 |
| deepseek-v4-pro | 11 of 14 | 5 of 15 | 6 of 16 |
| Claude Opus | 0 of 16 | 0 of 16 | 6 of 16 |
A stronger model moves the problem from silent wrong answers to needless doubt. Both mistakes come from the same place: from the visible rows alone, an agent cannot know whether the hidden rows matter.
What the system can say
If the agent cannot know, the system around it has to tell it. I tested both kinds of note.
The note that reveals nothing (A2) rarely missed a real change. Over all 6,927 queries that the agents ran, it warned on 3,915 of the 3,938 queries whose result the hidden rows changed (99.4%). Of the 23 it missed, 22 reached the restricted rows along a different path from the rule, for example through a client's district instead of the account's district, and one differed only in floating-point rounding.
After the runs, I also applied both notes to the agents' final answers. I had not pre-registered this check, so it is exploratory.
| Note | Partial answers labelled complete | Complete answers labelled "may be incomplete" | Reveals hidden data? |
|---|---|---|---|
| A2: query and rule only | 19 of 1,494 (1.3%) | 633 of 1,419 (45%) | No |
| A3: exact | none | none | Yes |
The A3 note is exact by construction, so it makes no mistakes. Its cost is that it leaks. I wrote a short script, with no model, that sends chosen queries and reads only the exact note's answer. It recovered totals that the role cannot see, exactly, in 17 to 23 queries each:
- accounts in the bank: 4,500 (the role sees 778);
- schools: 17,686 (the role sees 3,636);
- Formula 1 races: 954 (the role sees 148);
- customers: 32,461 (the role sees 26,763);
- the largest loan: 590,820 (the role sees 475,680).
The A2 note gave the same answer to every probe, so the script learned nothing from it.
The agents also often ignored the warnings. When the A2 note said that rows might be missing from their own answer query, gpt-oss:120b still answered full in 64 of 159 runs, gemma4:31b in 112 of 156, and Opus in 7 of 28. This is exploratory too.
Hiding the clues did not help
In the three extra roles, which also hid the descriptive tables, silent partial answers were not less common. gpt-oss:120b gave them in 53 of 81 runs with those tables hidden, against 27 of 40 with them visible. In both versions, agents almost never recovered the true full answer (0 or 1 per condition).
What the decision rules said
- D1: met. All three cheaper models gave silent partial answers on 70–95% of affected questions with no access information.
- D2: met, but only on the subsample. When told the rule, Opus and deepseek-v4-pro doubted 6 of 16 complete answers (38%). On the 235 main pairs, no cheaper model reached 30% (the highest was 22%).
- D3: met for one model, on the subsample. For deepseek-v4.1-flash, A2 cut silent answers from 10 of 14 to 4 of 16, and the model doubted 47% of complete answers with A2 against 27% with A3. gpt-oss:120b missed narrowly on the main pairs: A2 cut its silent answers from 73% to 39%, where the rule needed 37% or less.
- Kill rule: not met. With A2, the cheaper models still gave silent partial answers in 25–62% of cases.
By the rules I set in advance, this is a research problem. D2 and D3, however, rest on the 40-pair subsample, so that part of the verdict is weak and needs a larger test.
What I checked
- The labels. They come from running SQL, so no judge can get them wrong. Sums of decimal columns can differ in the last digits between two runs of the same query, so I compared numbers to six significant digits. No label changed. One question still gave different results between runs under two roles, which makes its "affected" label wrong for one role. It is 1 of the 131 affected pairs.
- Answers that are NULL. Four unaffected touching pairs have an answer that is NULL even on all rows. Without them, Opus and deepseek-v4-pro doubted 4 of 13 complete answers (31%), just above the D2 threshold.
- Apparent misses of the A2 note. I re-ran every query where the note looked wrong. Apart from the 23 above, 56 apparent misses came from rounding (22) or from queries that give different results on two runs (34).
- The coverage field itself. Asking for coverage might make agents cautious on its own. With no access information, agents doubted only 0–4% of the questions that read no restricted table, so the field alone does not explain the doubt. I did not test this separately; it is my reading of those rates.
- Opus. I cannot verify that Opus used only its tool. Its tool also did not count the confirmation query, so it could run one more query than the other models.
- Changes after the pre-registration. The helper module now reads the database password from an environment variable and contains the number comparison above. The agent code did not change.
What this means
The results suggest that the agent is the wrong place to decide whether an answer is complete. A weaker model assumes the hidden rows do not matter. A stronger model often assumes they do. Neither can know.
The system can know, because it holds both the rule and the data. The two notes I tested sit at opposite ends. The note that reveals nothing almost never lets a partial answer through, yet it warns on 45% of correct answers. The exact note never warns wrongly, but a short script can use it to recover the hidden totals. Database research has described this tension since at least 2004 [8], and recent work shows how small leaks from row-level security can rebuild hidden records [9]. This study measures it for AI agents.
The question I am left with is:
Can a system tell an agent whether the hidden rows matter, with a known, limited leak and far fewer false warnings?
What I learned from the experiment
- A stronger model made a different mistake: it doubted complete answers instead of hiding partial ones.
- Agents ignored many warnings, so the system should attach the completeness label to the final answer itself.
- Labels computed by running SQL removed any argument about what counts as a correct answer.
- Writing the decision rules before the runs mattered most when the result was mixed. D2 and D3 passed only on the small subsample, and the rules made me say so.
Limitations
- I wrote the roles for public databases; they do not come from a real company.
- The questions are short text-to-SQL questions. Real analyses are often longer.
- Each cell has one sample, at temperature 0.6.
- The coverage field is the agent's own report, in a fixed format.
- One person ran and analysed the study.
- Opus and deepseek-v4-pro ran only on the 40-pair subsample.
Code and data
The pre-registration records the question, data, arms, measures and decision rules, and the SHA-256 hashes of the code and the labelled pairs. The code, the pre-registration and the per-run outputs will be released in a public repository. The release is in preparation.
The project uses Python, PostgreSQL 16 and Ollama.
Models used:
- gpt-oss:120b, gemma4:31b, deepseek-v4.1-flash and deepseek-v4-pro through Ollama Cloud
- Claude Opus through subagents with an isolated command-line tool
Claude (Anthropic) assisted with running the experiments and writing the analysis code.
References
[1] PostgreSQL Global Development Group, "Row Security Policies," PostgreSQL Documentation.
https://www.postgresql.org/docs/current/ddl-rowsecurity.html
[2] Databricks, "Genie agents concepts," Databricks Documentation.
https://docs.databricks.com/aws/en/genie-agents/concepts
[3] Microsoft, "Fabric data agent sharing," Microsoft Learn.
https://learn.microsoft.com/en-us/fabric/data-science/data-agent-sharing
[4] Amazon Web Services, "Adding datasets with row-level security (RLS) to an Amazon Quick Sight topic," Amazon Quick User Guide.
https://docs.aws.amazon.com/quick/latest/userguide/topics-data-rls.html
[5] B. Madukoma, "View-validity pilots: results," unpublished research note, 25 Sep. 2026.
[6] Y. Fei, Y. Jiang, Y. Yang, and X. Xiao, "Benchmarking Text-to-SQL under Role-Based Access Control," arXiv:2607.22115, 2026.
https://arxiv.org/abs/2607.22115
[7] J. Li et al., "Can LLM Already Serve as a Database Interface? A BIg Bench for Large-Scale Database Grounded Text-to-SQLs," NeurIPS 2023 Datasets and Benchmarks Track. arXiv:2305.03111. Mini-dev set: https://github.com/bird-bench/mini_dev
https://arxiv.org/abs/2305.03111
[8] S. Rizvi, A. Mendelzon, S. Sudarshan, and P. Roy, "Extending Query Rewriting Techniques for Fine-Grained Access Control," ACM SIGMOD 2004.
https://www.cse.iitb.ac.in/~sudarsha/Pubs-dir/nontruman-sigmod04.pdf
[9] Z. Espiritu and D. Cash, "Plaintext Recovery Against Post-Filtering Access Control," USENIX Security 2026. arXiv:2608.11730.
https://arxiv.org/abs/2608.11730