Skip to content

SQLite: non-ASCII character (em-dash) in SQL comment corrupts generated queries #4372

Closed
s3ntin3l8/branchdam
#417

Description

@exploded

Version

1.30.0

What happened?

When a SQL comment between two named queries contains an em-dash (—, U+2014), sqlc corrupts the generated Go code. The trailing ? parameter placeholder is stripped from the query, and the comment text leaks into the Go source as a misplaced comment.

In our production codebase this caused ~15 queries to silently fail at runtime. The corruption went unnoticed because the Go code compiles — sqlc generates the correct number of params in the struct/function, but the SQL string is truncated.

Expected generated output

const updateItem = `-- name: UpdateItem :exec
UPDATE items SET name = ? WHERE id = ?
`

func (q *Queries) UpdateItem(ctx context.Context, arg UpdateItemParams) error {
	_, err := q.db.ExecContext(ctx, updateItem, arg.Name, arg.ID)
	return err
}

Actual generated output

const updateItem = `-- name: UpdateItem :exec

UPDATE items SET name = ? WHERE id =
`

// section — divider
func (q *Queries) UpdateItem(ctx context.Context, arg UpdateItemParams) error {
	_, err := q.db.ExecContext(ctx, updateItem, arg.Name, arg.ID)
	return err
}

Note:

WHERE id = is missing the trailing ? — runtime SQL syntax error
The comment // section — divider leaked into the Go code
Two args are passed to ExecContext but the SQL only has one ? placeholder
The bug cascades: in a file with many queries after the offending comment, every query loses its last ? and gets ?; prepended from the previous query.

Replacing the em-dash (—) with a regular hyphen (-) fixes the issue.

Relevant log output

const updateItem = `-- name: UpdateItem :exec

UPDATE items SET name = ? WHERE id =
`

// section — divider
func (q *Queries) UpdateItem(ctx context.Context, arg UpdateItemParams) error {
	_, err := q.db.ExecContext(ctx, updateItem, arg.Name, arg.ID)
	return err
}

Database schema

CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT NOT NULL);

SQL queries

-- name: GetItem :one
SELECT id, name FROM items WHERE id = ?;

-- section — divider

-- name: UpdateItem :exec
UPDATE items SET name = ? WHERE id = ?;

Configuration

version: "2"
sql:
  - engine: "sqlite"
    queries: "query.sql"
    schema: "schema.sql"
    gen:
      go:
        package: "db"
        out: "db"

Playground URL

(https://play.sqlc.dev/p/effb23cebb0f3c49e956dc7b28f962735b871a5f6feb5d3e3114e9f9b841cb28)

What operating system are you using?

Windows

What database engines are you using?

SQLite

What type of code are you generating?

Go

Activity

  1. added a commit that references this issue on Apr 14, 2026
    d94d23d
  2. kyleconroy commented on Apr 17, 2026

    @kyleconroy
    Collaborator

    Closing as a duplicate of #4372

  3. exploded commented on Apr 20, 2026

    @exploded
    Author

    @kyleconroy Kyle - can you clarify which one this is a duplicate of? This is issue #4372

  4. exploded commented on May 7, 2026

    @exploded
    Author

    Looks like this is the same bug as #4235 ? Should be fixed by PR #4373.

  5. added a commit that references this issue on Aug 18, 2026
    49950e4
  6. mickeyzzc commented on Sep 2, 2026

    @mickeyzzc

    Still reproducible on v1.31.1 (the current latest release), sqlite engine, Linux — so the fix appears to have regressed or covered a narrower case.

    Minimal A/B repro (only the comment differs):

    -- name: DelA :execrows
    -- Retention sweep (batched) — same shape as X.        <-- em-dash (U+2014)
    DELETE FROM t WHERE id IN (
        SELECT sub.id FROM t AS sub WHERE sub.x < ? LIMIT ?
    )

    Generated const on v1.31.1 (note the missing closing paren):

    const delA = `-- name: DelA :execrows
    DELETE FROM t WHERE id IN (
        SELECT sub.id FROM t AS sub WHERE sub.x < ? LIMIT ?
    `

    Replacing the em-dash with an ASCII - regenerates the statement correctly.

    Two field observations that may help pinpoint the offset math:

    1. The corruption is position-dependent: the same set of multi-byte characters may generate fine for months until an unrelated edit elsewhere in the file shifts byte offsets onto the rune (we had a query file where this exploded only after a different query above it grew by a few bytes).
    2. It corrupts the tail of the reconstructed statement — in our worst case a WHERE id = lost its trailing ? entirely, and another query's ORDER BY a, b DESC, te. was cut mid-identifier. The generated code compiles fine; the statement only fails at runtime with SQL logic error: incomplete input.

    Our workaround (works, but shouldn't be necessary): keep queries/*.sql pure ASCII and guard it in CI.

  7. kyleconroy commented on Sep 2, 2026

    @kyleconroy
    Collaborator

    The fix has not been released yet, it's only been merged to main

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions