Skip to content

fix(sqlite): correct parameter binding when mixing sqlc.arg() with bare "?"#4471

Open
abgoyal wants to merge 1 commit into
sqlc-dev:mainfrom
abgoyal:fix/sqlite-mixed-param-binding
Open

fix(sqlite): correct parameter binding when mixing sqlc.arg() with bare "?"#4471
abgoyal wants to merge 1 commit into
sqlc-dev:mainfrom
abgoyal:fix/sqlite-mixed-param-binding

Conversation

@abgoyal
Copy link
Copy Markdown

@abgoyal abgoyal commented Jun 7, 2026

fix(sqlite): correct parameter binding when mixing sqlc.arg() with bare ?

What happened?

When a SQLite query mixes sqlc.arg() named parameters with bare positional ? placeholders, sqlc generates SQL with mismatched parameter numbering that silently binds values to wrong columns.

Given this query:

-- name: RenameTemplate :one
UPDATE templates SET name = sqlc.arg(new_name), updated_at = datetime('now')
WHERE org_id = ? AND name = sqlc.arg(old_name) AND deleted_at IS NULL
RETURNING name, content, valid, updated_at;

Before (broken): sqlc generates SET name = ?2 ... WHERE org_id = ? AND name = ?3 — the bare ? gets auto-assigned index 1 by SQLite, but the Go function passes NewName as the 1st positional arg. Result: SET name = OrgID, WHERE org_id = NewName.

After (fixed): all placeholders are numbered in text order: SET name = ?1 ... WHERE org_id = ?2 AND name = ?3, matching the positional argument order.

Root cause

Commit c2dcd56 ("Allow for mixed parameters types ($1 or ?) and sqlc.arg()") added mixed-param support for PostgreSQL and MySQL but did not cover SQLite. PostgreSQL doesn't have this issue because $N is inherently numbered and args are sorted by number. MySQL doesn't have it because all params emit bare ? (no numbering, purely positional). SQLite is the only engine that generates numbered ?N for named params but also accepts unnumbered ? — and those two styles have incompatible binding semantics when mixed in the same query.

The NamedParameters rewriter assigns numbered ?N to sqlc.arg() params while skipping positions pre-reserved for bare ? — but it never renumbers the bare ? itself. Since SQLite's auto-numbering for bare ? is independent of explicit ?N, the two schemes conflict.

Fix

When SQLite has both named params and bare ? in the same query:

  1. Clear the pre-reserved positions so ParamSet doesn't skip them
  2. Handle bare ? during the Apply walk alongside named params, assigning all numbers in text order
  3. Emit edits to replace bare ? with explicit ?N

This ensures the numbered placeholders in the output SQL match the positional argument order in the generated Go function.

Test plan

  • Added sqlite variant to existing mix_param_types end-to-end test (matching the pg/mysql variants from the original mixed-param commit)
  • Verified all existing SQLite tests produce identical output (sqlc diff shows no changes)
  • go test -run TestReplay/base ./internal/endtoend/ passes

…re ?

When a SQLite query uses both sqlc.arg() named parameters and bare ?
placeholders, the generated SQL has numbered ?N for named params but
leaves bare ? unnumbered. SQLite's auto-numbering for bare ? then
conflicts with the explicit ?N values, silently binding arguments to
wrong columns.

Fix by numbering all placeholders sequentially in text order when the
mixed case is detected, ensuring positional argument passing matches
the generated ?N values.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant