ADR-254: A Checked Statement Runs on a Connection It Names as Its Subject, and the Connection’s Type Carries Its Schema

Status: Superseded by ADR-299 — its D1-D5 are D7-D11 there Date: September 29, 2026 Target Version: Nikaia 0.0.268 Answers: docs/open-decisions.md’s Where the connection goes when a checked statement runs - the owner chose option 1, the connection is the subject, and added that the connection is typed by its schema, so handing a statement the wrong database is a build error Related: ADR-143 (the driver checks the SQL against the schema while the program is built — this places the connection it left out), Part I 5.1 (Subject ; Config), ADR-007 D5 (the typed spread, which a driver no longer writes for execute), ADR-075 (no live database while the program is built)

1. Context

ADR-143 D2 says a checked statement’s value is a prepared statement whose execute takes the parameters by name and returns Seq[Row], and its D3 example writes by_age.execute(min_age: 18), with no connection anywhere. Its steps 1 and 3 — the row type and std::db’s protocol — could not be written until the connection had a place. Three places were on the page: the statement’s subject, the connection’s method, or an argument of the block.

The owner chose the first, and closed a hole the recommendation left open. A statement is checked against one schema while the program is built, and a connection is a runtime value; nothing said the two had to agree. A program with two databases could hand a statement checked against the first to a connection to the second, and the typed row would describe columns the database does not have — found at runtime, which ADR-143 D6 promises does not happen.

2. Decisions

D1 — The connection is the statement’s subject

let rows = by_age.execute(db; min_age: 18)

The statement is the receiver, the connection stands before the ; and the parameters after it, as Part I 5.1’s Subject ; Config reads anywhere else. A statement is not tied to a connection for its life, so one prepared statement serves every connection of a pool.

D2 — std::db declares the Connection trait, and the compiler writes the conversion

The trait has one method: run this text with these named db::Values, and hand back rows of db::Value. A driver implements it. The compiler turns a row of values into the row type it derived (ADR-143 D2), in the one place that derived it, so a driver never names a per-statement type and the conversion cannot disagree with the row type.

D3 — A connection’s type carries the schema it was opened for

A driver’s open takes the schema — sqlite::open("app.db"; schema: app) — the same build-time value a statement’s block names (ADR-143 D3), and the connection it hands back is typed by that schema. ADR-143 D3 already has the driver check at open that the database matches the file; D3 makes the check’s answer part of the type.

D4 — A statement handed a connection of another schema is refused while the program is built

by_age.execute(db; …) is checked: by_age was checked against a schema, db is typed by one, and where they are not the same schema the call is refused at the call, naming both. Swapping two connections in a program with two databases fails the build, long before the code runs on a server.

D5 — The same schema is the same build-time value

Two blocks that name app are checked against one schema; a block that names app and a connection opened with reporting are two, even where the two files happen to say the same thing. That is the fail-closed reading (ADR-010 D1): a program that means one database names one schema.

3. Consequences

4. What this does not decide

5. What is built

Nothing. It joins ADR-143 §5’s order: std::db’s trait (step 3) comes with D1-D2, and the schema on the connection’s type (D3-D5) with the sqlite driver (step 4).

This site is open source. Improve this page.