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)
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.
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.
std::db declares the Connection trait, and the compiler writes the conversionThe 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.
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.
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.
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.
by_age.execute(db; min_age: 18),
and Part II 10.5’s and Part III’s examples read the same way.std::db gains its first trait, which programs’ drivers implement; 0.0.231
made an impl of an undeclared trait a refusal, so the trait is declared
where the checker reads it.execute(self; ...args: Self::dsl) by hand for a
checked statement. The typed spread of ADR-007 D5 stays for
every other DSL a library drives.db::Value carries — the trait grows a method, which is an
addition rather than a break.open where it is not written; the spelling is decided with the first
program that needs to write it.app itself is written at file level — see open-decisions.md,
How a schema is bound while the program 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).