Guide / Reinhardt reinhardt-query 0.3.15

What can Rust types guarantee about SQL?

In this guide

A query with a misspelled column name compiles. Its builder produces SQL. SQLite rejects the statement because users.nmae does not exist. Replace that column argument with the integer 42_i32, and the failure moves to the Rust compiler. Both failures are observed in the downloadable example; they protect different boundaries.

This article is for Rust developers choosing what to test around a query builder. It uses reinhardt-query =0.3.15, whose published crate records commit c64a11e. Reinhardt Press is the framework project's own outreach publication. The experiment uses the query builder directly, without ORM models or migrations.

The compiler knows the API types

This excerpt intentionally fails to compile:

Query::select().column(42_i32);

The recorded diagnostic is E0277: i32: IntoColumnRef is not satisfied. The compiler explains that i32 does not implement Iden. The pinned column signature requires conversion to a column reference. A string supports that conversion, including a string containing a typo. This API call supplies no live database schema to the compiler.

Query::select()
    .column(("users", "nmae"))
    .from("users")
    .build(SqliteQueryBuilder);

That second excerpt compiles and produces SELECT "users"."nmae" FROM "users". The database reports no such column: users.nmae. The table qualifier is intentional: it avoids SQLite's legacy double-quoted-string behavior. The observation is about this API and version, not every Rust query library.

Follow a valid query into the database

let mut select = Query::select();
select
    .column(("users", "name"))
    .from("users")
    .and_where(Expr::col(("users", "age")).gte(25_i32));
let (sql, values) = select.build(SqliteQueryBuilder);

The SQL is SELECT "users"."name" FROM "users" WHERE "users"."age" >= ?, and the separate values contain integer 25. The fixture contains Ada, age 31, and Lin, age 24. A small Python standard-library bridge passes the generated SQL and values directly to sqlite3.Connection.execute; it asserts the only returned row is Ada. The bridge handles only integer/string values used in this fixture and is not a production adapter.

A second query binds Ada' OR 1=1 -- as a name. It returns zero rows and the two original users remain. This demonstrates separation of values and SQL syntax for the tested parameterized path. It is not a security audit of every API or a license to put untrusted input into raw SQL or identifiers.

Some checks happen while the builder runs

let mut insert = Query::insert();
insert.into_table("users").columns(["name", "age"]);
let result = insert.values(vec!["Ada".into()]);

This compiles. At runtime, values returns:

Number of values (1) doesn't match number of columns (2)

The example asserts this result without sending an INSERT to SQLite. The pinned implementation checks lengths when declared columns are nonempty. This is a runtime builder check. Its Result can be handled as an ordinary error; the separate values_panic method has a panic path.

An accepted value is not necessarily meaningful in every SQL position. In this version, .limit("ten") compiles and generates a bound text value for LIMIT. SQLite rejects its execution with datatype mismatch. Generating a statement does not establish that the target database will execute it.

The database's storage policy also matters

The Rust source generates two INSERTs, each binding the text old to an age INTEGER column. The only schema-policy difference is STRICT:

CREATE TABLE ages_loose (age INTEGER);
CREATE TABLE ages_strict (age INTEGER) STRICT;
Executed caseObserved result
Ordinary INTEGER columnStores old; typeof(age) is text
STRICT INTEGER columnRejects TEXT in ages_strict.age; table remains empty

These are real SQLite 3.50.4 results. Both inputs have valid Rust string types. SQLite applies the final storage rule. The runner requires SQLite 3.37.0 or later, when STRICT tables became available. This is a comparison of two SQLite table policies, not an execution comparison with PostgreSQL or MySQL.

Check generated dialects without claiming runtime portability

For the same valid selection, the sample independently asserts all three SQL strings and the separate value [25]:

BackendPredicate and identifier quoting
SQLite"users"."age" >= ?
PostgreSQL"users"."age" >= $1
MySQL`users`.`age` >= ?

The builder interface returns SQL plus values. Only the SQLite output is executed here. PostgreSQL and MySQL assertions establish generation behavior; testing their semantics requires those database engines, versions, schemas, and observed results.

Run the sample and choose a regression boundary

Download and extract the sample, then run:

cd rust-sql-guarantees
python3 verify.py

Prerequisites are Rust 1.96.0+, Python 3.11+, and its standard-library sqlite3 linked to SQLite 3.37.0+. No Python packages or DB server are required. Cargo fetches dependencies from the included lockfile. The observed environment was Rust 1.96.0, Python 3.12.12, and SQLite 3.50.4 on 2026-09-05.

The command runs the valid program, confirms the deliberate compiler failure, executes SQLite, and prints JSON after assertions pass. It checks the valid row result, missing-column error, value/column mismatch, text LIMIT rejection, ordinary/STRICT storage, bound text, and exact SQL/bindings for three dialects. It records versions, commands, and source/lockfile hashes. The archive was extracted separately and this command was run against the extracted copy.

Put a test where its guarantee belongs: compile-failure checks for unsupported Rust arguments, runtime assertions for builder errors, explicit SQL/value expectations for rendering, and actual engine execution for DB semantics. Change one input and predict which stage rejects it. If an error is hard to understand, keep the smallest reproduction and follow the contribution guide to propose clearer diagnostics or tests against additional database engines. Include the source, lockfile, command, engine/version, expected result, and observed result. This example measures no network time, performance, or concurrent writes.

The accompanying animation explains these recorded results; it is an edited explanation, not a continuous terminal recording. Narration is synthetic (ElevenLabs Jofra), diagrams use Manim Community, and code images use Carbon. Music: “Fluidscape” by Kevin MacLeod, source, CC BY 4.0; excerpted, faded, volume reduced, and mixed under narration.