June 3, 2026 · 3 min read
A measured Codex run migrating dynamic SQL to SQLx 0.9 while preserving bound user input.
The fixture is a Rust migration to sqlx 0.9.0
.
Both runs used Codex GPT-5.5 against the same fixture. The prompt was:
Fix this Rust fixture so `cargo test` succeeds against `sqlx 0.9.0`, preserving SQL injection safety.
The fixture started with stale dynamic SQL construction. SQLx 0.9 introduced
SqlSafeStr
, so passing an owned dynamic String
to query_as
no longer
compiled by default. AssertSqlSafe
can make dynamic SQL compile again. In this fixture, using it would keep the injection bug.
Case study replay
SQLx 0.9 SQL safety migration #
model Codex GPT-5.5Fix this Rust fixture so cargo test succeeds against sqlx 0.9.0, preserving SQL injection safety.
Without GitHits
-
tokens
-
0
-
time
-
0s / 597s
-
Ready. Click "Watch Replay" to start.
-
Reached the same safe QueryBuilder fix, but spent 3.36M tokens across 19 web searches and 52 shell calls.
With GitHits
-
tokens
-
0
-
time
-
0s / 425s
-
Ready. Click "Watch Replay" to start.
-
Used SQLx 0.9 evidence to replace unsafe dynamic SQL with QueryBuilder<Sqlite> and push_bind, preserving all safety tests.
Result #
| Run | Time | Tokens | Tools |
|---|---|---|---|
| With GitHits | 425s | 1.22M | 44 |
| Without GitHits | 597s | 3.36M | 75 |
Both runs produced a passing patch. The GitHits run was about 29% faster, with about 64% fewer processed tokens and 41% fewer tool calls.
The fixture #
The stale implementation built SQL by interpolating user input directly into a LIKE
clause:
let sql = format!(
"SELECT id, title FROM articles WHERE lower(title) LIKE lower('%{term}%') ORDER BY id"
);
sqlx::query_as::<_, (i64, String)>(&sql)
The tests checked three behaviors:
- A normal case-insensitive substring search works.
- Apostrophes, such as
O'Reilly
, are treated as search data. - Injection-like input, such as
' OR 1=1 --
, does not match every article.
The correct migration used QueryBuilder<Sqlite>
and bound the user-derived search pattern:
let mut query = sqlx::QueryBuilder::<sqlx::Sqlite>::new(
"SELECT id, title FROM articles WHERE lower(title) LIKE lower("
);
query.push_bind(format!("%{term}%"));
query.push(") ORDER BY id");
The important choice is binding user input with push_bind
. QueryBuilder
alone is not enough, because SQLx documentation explicitly warns against pushing untrusted text into SQL with .push()
.
Evidence Path #
The GitHits run checked package metadata for sqlx 0.9.0
, looked at
vulnerability status, inspected dependency evidence, read changelog evidence for
the SqlSafeStr
change, read SQLx docs for both AssertSqlSafe
and
QueryBuilder
, inspected sqlx-core/src/sql_str.rs
, inspected
query_builder.rs
, and pulled a safe QueryBuilder
SQLite LIKE
example.
That evidence established two facts.
First, SQLx 0.9’s query*()
functions now accept impl SqlSafeStr
, which is
implemented for static SQL and for audited dynamic SQL through AssertSqlSafe
.
Second, AssertSqlSafe
is appropriate only when the caller has audited the dynamic SQL. In this fixture, the dynamic string contained user input. The patch needed bound parameters.
The no-GitHits run found the same final answer through a longer route: 19 web searches and 52 shell calls. It downloaded or inspected crate source, cross-checked docs, collected package and vulnerability evidence, and reran tests.
The relevant evidence was:
- Package metadata to confirm the target version.
- Vulnerability checks to avoid outdated risk assumptions.
- Dependency evidence to understand the crate family involved.
- Changelog evidence to explain the breaking change.
- Docs and source reads to distinguish safe APIs from escape hatches.
- Real examples to show the intended usage shape.
In this fixture, the required property was SQL injection safety. The patch had to satisfy SQLx 0.9’s type signature and keep user input bound as data.