Back to blog Wireframe database tables with a checkmark, in the GitHits brand style

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.5
$

Fix this Rust fixture so `cargo test` succeeds against `sqlx 0.9.0`, preserving SQL injection safety.

Without GitHits

Costly
tokens
0
time
0s / 597s
  1. Ready. Click "Watch Replay" to start.

With GitHits

Efficient
tokens
0
time
0s / 425s
  1. Ready. Click "Watch Replay" to start.

Result

RunTimeTokensTools
With GitHits425s1.22M44
Without GitHits597s3.36M75

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.