June 3, 2026 ยท 3 min read
Migrating dynamic SQL safely in SQLx 0.9
How Codex migrated dynamic SQL to SQLx 0.9 without turning user input into query text.
We gave Codex a Rust fixture with stale dynamic SQL and asked it to migrate the code to sqlx 0.9.0 without weakening SQL injection protection.
Both runs used Codex GPT-5.5 and the same prompt:
Fix this Rust fixture so `cargo test` succeeds against `sqlx 0.9.0`, preserving SQL injection safety.
SQLx 0.9 introduced SqlSafeStr, so an owned dynamic String can no longer be passed to query_as by default. AssertSqlSafe makes dynamic SQL compile again, but it would not fix this fixture: the string contained user input.
Case study replayReal agent replays
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 finished 29% faster, used 64% fewer processed tokens, and made 41% fewer tool calls.
The unsafe query
The fixture put the search term 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 covered normal substring search, apostrophes such as O'Reilly, and input such as ' OR 1=1 --. The last case must stay data instead of becoming part of the query.
The fix
The patch used QueryBuilder<Sqlite> and bound the 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");
QueryBuilder is not safe on its own. Untrusted text still becomes SQL when passed to .push(). The important part is push_bind, which keeps the search term as a value.
What GitHits changed
Both runs produced a safe, passing patch. The GitHits run finished 172 seconds sooner, used 2.14 million fewer processed tokens, and needed 31 fewer tool calls.
GitHits gave Codex the SQLx 0.9 changelog, the SqlSafeStr and QueryBuilder docs, the relevant source, and a real SQLite LIKE example. Together, they showed that AssertSqlSafe would only silence the new type check, while push_bind would also preserve SQL injection safety.
Without GitHits, Codex reached the same answer through 19 web searches and 52 shell calls, including crate downloads and local source inspection. GitHits did not produce a different patch; it made the safety decision with much less searching.