From d89c6d014f8ba0199b015ce1936fb9fa716f7bac Mon Sep 17 00:00:00 2001 From: lumbrjx Date: Thu, 13 Aug 2026 14:38:42 +0100 Subject: [PATCH] fix(sqlite): split named queries without trailing semicolons --- .../testdata/analyze_select/sqlite/query.sql | 2 +- internal/engine/sqlite/parse.go | 32 ++++++++++++++++++- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/internal/endtoend/testdata/analyze_select/sqlite/query.sql b/internal/endtoend/testdata/analyze_select/sqlite/query.sql index 985fc6d869..fedaf120b6 100644 --- a/internal/endtoend/testdata/analyze_select/sqlite/query.sql +++ b/internal/endtoend/testdata/analyze_select/sqlite/query.sql @@ -1,5 +1,5 @@ -- name: ListUsers :many -SELECT * FROM users; +SELECT * FROM users -- name: CountUsers :one SELECT count(*) AS total FROM users; diff --git a/internal/engine/sqlite/parse.go b/internal/engine/sqlite/parse.go index 25acfd6896..3e05b82fcc 100644 --- a/internal/engine/sqlite/parse.go +++ b/internal/engine/sqlite/parse.go @@ -3,6 +3,7 @@ package sqlite import ( "errors" "io" + "strings" meyer "github.com/sqlc-dev/meyer/ast" "github.com/sqlc-dev/meyer/parser" @@ -31,7 +32,7 @@ func (p *Parser) Parse(r io.Reader) ([]ast.Statement, error) { return nil, err } src := string(blob) - parsed, err := parseOptions.ParseString(src) + parsed, err := parseOptions.ParseString(terminateNamedQueries(src)) if err != nil { return nil, normalizeErr(err) } @@ -58,6 +59,35 @@ func (p *Parser) Parse(r io.Reader) ([]ast.Statement, error) { return stmts, nil } +// terminateNamedQueries adds a virtual terminator before a subsequent sqlc +// query annotation when the preceding query omitted one. sqlc annotations +// delimit queries for the other engines, and a query file may therefore +// contain multiple valid queries even though the complete SQLite script would +// otherwise be invalid. Replacing the newline immediately before the +// annotation preserves every byte offset reported by the parser. +func terminateNamedQueries(src string) string { + terminated := []byte(src) + for lineStart := 0; lineStart < len(terminated); { + lineEnd := strings.IndexByte(src[lineStart:], '\n') + if lineEnd < 0 { + lineEnd = len(terminated) + } else { + lineEnd += lineStart + } + if lineStart > 0 && strings.HasPrefix(src[lineStart:lineEnd], "-- name: ") { + i := lineStart - 1 + for i >= 0 && isSpace(terminated[i]) { + i-- + } + if i >= 0 && terminated[i] != ';' { + terminated[lineStart-1] = ';' + } + } + lineStart = lineEnd + 1 + } + return string(terminated) +} + // trimTerminator returns the end of stmt with its terminating semicolon, and // any space before it, removed. A statement's span runs through the // semicolon, but sqlc's statement text does not include it.