# Case Study: SQLx 0.9 QueryBuilder Migration

> Source: <https://githits.com/blog/sqlx-querybuilder-safety-case-study/>
> Published: 2026-06-03 00:00:00+00:00

[Back to blog](/blog/)

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

``` js
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:

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