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 case | Observed result |
|---|---|
| Ordinary INTEGER column | Stores old; typeof(age) is text |
| STRICT INTEGER column | Rejects 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]:
| Backend | Predicate 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.pyPrerequisites 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.