Skip to content

Return an error for a batch insert whose VALUES group never closes - #36

Merged
avgvi merged 1 commit into
masterfrom
fix/batch-unclosed-values
Oct 19, 2026
Merged

avgvi merged 1 commit into
masterfrom
fix/batch-unclosed-values

Conversation

@avgvi

@avgvi avgvi commented Oct 19, 2026

Copy link
Copy Markdown
Member

A batch insert (a slice or array argument to NamedExec, BindNamed and the like) with an unclosed VALUES ( group used to come back unexpanded, carrying every row's arguments, so the mistake only showed up when the database rejected the argument count. It now returns missing closing bracket in VALUES, the same wording vinovest/sqlx uses, whatever the number of rows.

While there, the bracket matching that finds the VALUES group skips string literals, quoted identifiers and comments. On master:

query, 2 rows master this PR
INSERT INTO foo (a,b) VALUES (?, ')') VALUES (?, '),(?, ')') (invalid) VALUES (?, ')'),(?, ')')
INSERT INTO foo (a,b) VALUES (?, '(') RETURNING id not expanded VALUES (?, '('),(?, '(') RETURNING id
INSERT INTO foo (a,b) VALUES (?, /* ) */ ?) VALUES (?, /* ),(?, /* ) */ ?) (invalid) expanded correctly
INSERT INTO foo (id) VALUES(:id VALUES(? with 2 args, no error error

Changes

  • scan.go: the literal, quoted-identifier, comment and dollar-quote cases of the scanner move into skipOpaque, shared with the bracket matcher. No behaviour change: a differential fuzz against master's scanner ran 51M inputs with identical tokens.
  • named.go: matchingCloseParen (literal-aware, -1 when nothing closes) replaces findMatchingClosingBracketIndex; valuesGroup picks the group as before (first closed group with a bind variable, else the first closed group) and reports an unclosed group that holds bind variables; fixBound uses it; bindArray returns the error.
  • Single-struct and map arguments are not affected: they do not take the batch path.
  • Tests: four new TestFixBounds rows and TestBindArrayUnclosedValues (one row, two rows, pointer to slice, bracket in a literal, both bind types; plus balanced, literal-bracket, upsert VALUES(col, no-VALUES, struct and map cases that must stay unchanged). CHANGELOG under Unreleased.

Verified locally

  • go test -race ./... in the root; the four fuzz targets 15s each; dbtest against Postgres 16, MySQL 8 and SQLite: 39 pass, 0 fail, no database disabled.
  • vet, staticcheck -checks=all, goimports, govulncheck clean; the root go.mod still requires nothing; apidiff against v1.4.0 reports no incompatible change (the new error value is unexported).

Note on scope. nyaruka/gocommon's BulkSQL calls sqlx.Named once per struct and builds the bulk statement itself, so it does not reach this path. On the fork it already errors on an unclosed VALUES, with its own message ("error extracting VALUES from sql: ..."); vinovest raises missing closing bracket in VALUES from its named-query compiler for any argument. Matching that would mean the same check for single-struct and map queries, which this PR deliberately does not do.

…nd skip brackets inside literals and comments when expanding it.
@avgvi
avgvi merged commit 3609688 into master Oct 19, 2026
10 checks passed
@avgvi
avgvi deleted the fix/batch-unclosed-values branch October 19, 2026 23:08
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