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

June 3, 2026 · 3 min read

Case Study: SQLx 0.9 QueryBuilder Migration

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