# 19.12 Proving the data stays apart (/building-ai-workers/business-data-and-retrieval/proving-the-data-stays-apart)

---
type: Document
title: "19.12 Proving the data stays apart"
description: "A data matrix that tries every route from one company's rows to another's, and six planted bugs, each caught by at least one check."
status: stable
order: 319.12
ksor:
  owner: team:panaversity
  audience: [ public ]
  approval:
    by: process:panaversity
    at: 2026-11-04T22:13:33Z
chapter: "19"
part: III
expert_status: co-build
concepts: [ "19.12" ]
objectives:
  - { id: CCDV-F.D7.S1, label: supporting }
last_verified: 2026-11-02
generated:
  at: 2026-11-04T22:13:33Z
  by: process:panaversity
trust_tier: unverified
build_id: sha256:e1fbcb7d9a8f8b9b7cb96875022325c02c9ba4be94cef13b301cbd39a9edad85
dirty: true
ksor_version: 0.0.60
---

**In everyday life.** A building inspector does not take the builder's word that the fire doors close. She tries every door, on every floor, and writes down which ones she tried. Then, at the next inspection, she props one open on purpose and checks that the alarm goes off. A test that cannot fail proves nothing.

**What you will learn.** How one table of checks tries every route from one company's rows to another's, and how six planted bugs prove that each check can fail.

**Why it matters.** Planting each mistake on purpose shows which checks catch it, and which mistake only one check catches.

**One request, start to finish.** [Chapter 18](../securing-connectors/overview.md)'s sign-in matrix asked: can one company *act as* another? This chapter's **data matrix** asks the next question: can one company's *rows* reach another, by any route? It asks it twice, once of the database directly and once through both connectors:

1. **Each role, each company, each table.** With no company set, the reader sees 0 rows in all 9 tables. With each company set, every row it sees is that company's.
2. **Writes that must be refused.** The reader writes anything: refused. Cedar Ridge's loader inserts another company's rows: refused by the policy. It updates Brightline's settings: zero rows changed. A Cedar Ridge invoice for Brightline's vendor V006: refused by the foreign key.
3. **One shared connection.** Ana's transaction, then Luis's, 200 times on one pooled connection, and between transactions the connection sees nothing.
4. **The roles themselves.** No connector or loader role can skip the policies, and every table is forced.
5. **Through the connectors, TypeScript and Python.** Another company's id gets the same "not found" as an id that exists nowhere. Six Cedar Ridge calls, every tool, contain nothing of Brightline's: no vendor name, no invoice number, no policy file. And 100 pairs of calls, Ana's and Luis's at once, each get their own answers.

These five groups hold 25 checks in all, and all must pass, on every pull request's own copy of the database.

### How do you know the checks work?

The data matrix is Chapter 18's matrix, extended to every table. Every rule in Part B has a row that breaks if the rule is broken. And to prove each row can fail, the lab plants **six deliberate bugs**, one at a time, each on a throwaway branch (a git branch, made for the test and deleted after it), and records which checks catch it. One full run:

| Planted bug | The rule it breaks | Caught by |
| --- | --- | --- |
| 1. One table without forced row security | [19.8](row-level-security.md), force on every table | The role check. The data matrix (its check of the roles and forced tables) |
| 2. The company set for the whole connection instead of the transaction ([19.9](one-company-per-transaction.md)), plus one query moved to a second pooled connection with its company parameter removed | [19.9](one-company-per-transaction.md), one company per transaction | The data matrix (its two-companies-at-once check: some answers held the wrong company's totals). The query check, which flagged the missing parameter. The unit tests never finished, which counts as a failure: they did not pass |
| 3. One query with no company parameter, relying on row-level security alone | [19.9](one-company-per-transaction.md), caching off and the company as a parameter | The query check, and only the query check |
| 4. The connector's connection string set to the owner role | [19.8](row-level-security.md), a role that cannot skip | The role check. The data matrix (seven of its checks) |
| 5. The composite foreign key from invoices to vendors dropped | [19.4](designing-the-ap-tables.md), the company in every key | The data matrix (its check for a key that points across companies), and only the data matrix |
| 6. A `SECURITY DEFINER` function for a "helpful" error naming another company's vendor | [19.10](where-the-guarantee-stops.md), identical not-found replies | The role check. The data matrix (the roles check, and its check that not-found replies are identical). The unit tests |

Every bug was caught. Three results teach the most. First, bug 2 plants two mistakes at once, so a control was also run: only `set_config(..., false)`, with every query still naming the company. Nothing leaked. If a setting left over from an earlier call ever met a query, that query would still name its own company as `$1`, and a row must match both, so the worst case is no rows, never the wrong company's. The second lock held, and failed closed. But a mistake nobody can see is still a mistake, so the query check refuses any company set for the whole connection, and it catches the control. The other two results. Bug 3 broke nothing you could see: row-level security still kept every company's rows apart, so only the query check noticed the missing second lock. And bug 5 was caught only by the data matrix. A check you run once at the start is not enough. These run on every pull request.

**At work.** In [Lab 19](lab.md), Task 8, OpenCode writes the data matrix, and both connectors pass it. In Task 9, it plants each of the six bugs in turn, on a branch, runs every check, and records the result in `ports/ch19.md`. The branches are deleted, never merged.

**The operating rule:** test every table, every role and both connectors with each company's token for the other's data, on every pull request. Then plant each mistake on purpose, and prove a check catches it.

![Two parts. Left, the data matrix as five numbered groups, 25 checks in all, each with a green tick: each role, each company, each of the nine tables. Writes that must be refused. One shared connection, 200 transactions. The roles themselves, none can skip. Through both connectors, the same not-found reply, nothing of the other company, 100 pairs at once. Right, a grid of the six planted bugs against four checks, the role check, the query check, the data matrix and the unit tests, with a tick where each check caught the bug. Two cells are gold, the only check that caught it: bug 3, a query with no company parameter, caught only by the query check, and bug 5, the composite foreign key dropped, caught only by the data matrix.](img/chapter-19-data-matrix.png)

*Figure 19.21. The data matrix, and the six planted bugs. Every bug fails at least one check.*
