From 229e59c7d12bd0172d79edc5e5d69ec97b8f4ce4 Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Thu, 16 Apr 2026 22:14:28 -0700 Subject: [PATCH 1/2] Preserve MySQL optimizer hints in stripped query text Fixes #4353. Hint comments (/*+ ... */) on their own line were being treated as regular block comments and removed from the query string embedded in generated code. Inline hints already survived because the surrounding line did not match the block-comment pattern. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../mysql_optimizer_hints/mysql/go/db.go | 31 ++++++++++ .../mysql_optimizer_hints/mysql/go/models.go | 13 ++++ .../mysql/go/query.sql.go | 37 ++++++++++++ .../mysql_optimizer_hints/mysql/query.sql | 9 +++ .../mysql_optimizer_hints/mysql/schema.sql | 1 + .../mysql_optimizer_hints/mysql/sqlc.json | 12 ++++ internal/source/code.go | 6 ++ internal/source/code_test.go | 60 +++++++++++++++++++ 8 files changed, 169 insertions(+) create mode 100644 internal/endtoend/testdata/mysql_optimizer_hints/mysql/go/db.go create mode 100644 internal/endtoend/testdata/mysql_optimizer_hints/mysql/go/models.go create mode 100644 internal/endtoend/testdata/mysql_optimizer_hints/mysql/go/query.sql.go create mode 100644 internal/endtoend/testdata/mysql_optimizer_hints/mysql/query.sql create mode 100644 internal/endtoend/testdata/mysql_optimizer_hints/mysql/schema.sql create mode 100644 internal/endtoend/testdata/mysql_optimizer_hints/mysql/sqlc.json create mode 100644 internal/source/code_test.go diff --git a/internal/endtoend/testdata/mysql_optimizer_hints/mysql/go/db.go b/internal/endtoend/testdata/mysql_optimizer_hints/mysql/go/db.go new file mode 100644 index 0000000000..3b320aa168 --- /dev/null +++ b/internal/endtoend/testdata/mysql_optimizer_hints/mysql/go/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package querytest + +import ( + "context" + "database/sql" +) + +type DBTX interface { + ExecContext(context.Context, string, ...interface{}) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...interface{}) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/internal/endtoend/testdata/mysql_optimizer_hints/mysql/go/models.go b/internal/endtoend/testdata/mysql_optimizer_hints/mysql/go/models.go new file mode 100644 index 0000000000..92ddc7826f --- /dev/null +++ b/internal/endtoend/testdata/mysql_optimizer_hints/mysql/go/models.go @@ -0,0 +1,13 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package querytest + +import ( + "database/sql" +) + +type Foo struct { + Bar sql.NullString +} diff --git a/internal/endtoend/testdata/mysql_optimizer_hints/mysql/go/query.sql.go b/internal/endtoend/testdata/mysql_optimizer_hints/mysql/go/query.sql.go new file mode 100644 index 0000000000..258d113473 --- /dev/null +++ b/internal/endtoend/testdata/mysql_optimizer_hints/mysql/go/query.sql.go @@ -0,0 +1,37 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: query.sql + +package querytest + +import ( + "context" + "database/sql" +) + +const inlineHint = `-- name: InlineHint :one +SELECT /*+ MAX_EXECUTION_TIME(1000) */ bar FROM foo LIMIT 1 +` + +func (q *Queries) InlineHint(ctx context.Context) (sql.NullString, error) { + row := q.db.QueryRowContext(ctx, inlineHint) + var bar sql.NullString + err := row.Scan(&bar) + return bar, err +} + +const multilineHint = `-- name: MultilineHint :one +SELECT +/*+ MAX_EXECUTION_TIME(1000) */ +bar +FROM foo +LIMIT 1 +` + +func (q *Queries) MultilineHint(ctx context.Context) (sql.NullString, error) { + row := q.db.QueryRowContext(ctx, multilineHint) + var bar sql.NullString + err := row.Scan(&bar) + return bar, err +} diff --git a/internal/endtoend/testdata/mysql_optimizer_hints/mysql/query.sql b/internal/endtoend/testdata/mysql_optimizer_hints/mysql/query.sql new file mode 100644 index 0000000000..fa90093b66 --- /dev/null +++ b/internal/endtoend/testdata/mysql_optimizer_hints/mysql/query.sql @@ -0,0 +1,9 @@ +-- name: InlineHint :one +SELECT /*+ MAX_EXECUTION_TIME(1000) */ bar FROM foo LIMIT 1; + +-- name: MultilineHint :one +SELECT +/*+ MAX_EXECUTION_TIME(1000) */ +bar +FROM foo +LIMIT 1; diff --git a/internal/endtoend/testdata/mysql_optimizer_hints/mysql/schema.sql b/internal/endtoend/testdata/mysql_optimizer_hints/mysql/schema.sql new file mode 100644 index 0000000000..d849628fb1 --- /dev/null +++ b/internal/endtoend/testdata/mysql_optimizer_hints/mysql/schema.sql @@ -0,0 +1 @@ +CREATE TABLE foo (bar text); diff --git a/internal/endtoend/testdata/mysql_optimizer_hints/mysql/sqlc.json b/internal/endtoend/testdata/mysql_optimizer_hints/mysql/sqlc.json new file mode 100644 index 0000000000..e41c39e8b3 --- /dev/null +++ b/internal/endtoend/testdata/mysql_optimizer_hints/mysql/sqlc.json @@ -0,0 +1,12 @@ +{ + "version": "1", + "packages": [ + { + "path": "go", + "engine": "mysql", + "name": "querytest", + "schema": "schema.sql", + "queries": "query.sql" + } + ] +} diff --git a/internal/source/code.go b/internal/source/code.go index 8b88a24136..783682a2ee 100644 --- a/internal/source/code.go +++ b/internal/source/code.go @@ -115,6 +115,12 @@ func StripComments(sql string) (string, []string, error) { continue } if strings.HasPrefix(t, "/*") && strings.HasSuffix(t, "*/") { + // Preserve MySQL optimizer hints, which share block-comment + // syntax but are semantically part of the query. + if strings.HasPrefix(t, "/*+") { + lines = append(lines, t) + continue + } t = strings.TrimPrefix(t, "/*") t = strings.TrimSuffix(t, "*/") comments = append(comments, t) diff --git a/internal/source/code_test.go b/internal/source/code_test.go new file mode 100644 index 0000000000..59569220a5 --- /dev/null +++ b/internal/source/code_test.go @@ -0,0 +1,60 @@ +package source + +import ( + "strings" + "testing" +) + +func TestStripComments(t *testing.T) { + type test struct { + name string + input string + wantSQL string + wantComment []string + } + + tests := []test{ + { + name: "plain block comment on its own line is stripped", + input: "SELECT 1\n/* a comment */\nFROM foo", + wantSQL: "SELECT 1\nFROM foo", + wantComment: []string{" a comment "}, + }, + { + name: "inline optimizer hint is preserved", + input: "SELECT /*+ MAX_EXECUTION_TIME(1000) */ * FROM t1", + wantSQL: "SELECT /*+ MAX_EXECUTION_TIME(1000) */ * FROM t1", + }, + { + name: "multi-line optimizer hint is preserved", + input: "SELECT\n/*+ MAX_EXECUTION_TIME(1000) */\n*\nFROM t1", + wantSQL: "SELECT\n/*+ MAX_EXECUTION_TIME(1000) */\n*\nFROM t1", + }, + { + name: "query name comment is dropped", + input: "/* name: Foo :one */\nSELECT 1", + wantSQL: "SELECT 1", + }, + { + name: "dash comments are collected", + input: "-- helpful note\nSELECT 1", + wantSQL: "SELECT 1", + wantComment: []string{" helpful note"}, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + gotSQL, gotComments, err := StripComments(tc.input) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if gotSQL != tc.wantSQL { + t.Errorf("SQL mismatch\n got: %q\nwant: %q", gotSQL, tc.wantSQL) + } + if strings.Join(gotComments, "|") != strings.Join(tc.wantComment, "|") { + t.Errorf("comments mismatch\n got: %q\nwant: %q", gotComments, tc.wantComment) + } + }) + } +} From 9636fb98e6cfddb2dafd4830beff25c8bbdd4206 Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Thu, 16 Apr 2026 22:16:15 -0700 Subject: [PATCH 2/2] Drop unit test; end-to-end coverage is sufficient Co-Authored-By: Claude Opus 4.7 (1M context) --- internal/source/code_test.go | 60 ------------------------------------ 1 file changed, 60 deletions(-) delete mode 100644 internal/source/code_test.go diff --git a/internal/source/code_test.go b/internal/source/code_test.go deleted file mode 100644 index 59569220a5..0000000000 --- a/internal/source/code_test.go +++ /dev/null @@ -1,60 +0,0 @@ -package source - -import ( - "strings" - "testing" -) - -func TestStripComments(t *testing.T) { - type test struct { - name string - input string - wantSQL string - wantComment []string - } - - tests := []test{ - { - name: "plain block comment on its own line is stripped", - input: "SELECT 1\n/* a comment */\nFROM foo", - wantSQL: "SELECT 1\nFROM foo", - wantComment: []string{" a comment "}, - }, - { - name: "inline optimizer hint is preserved", - input: "SELECT /*+ MAX_EXECUTION_TIME(1000) */ * FROM t1", - wantSQL: "SELECT /*+ MAX_EXECUTION_TIME(1000) */ * FROM t1", - }, - { - name: "multi-line optimizer hint is preserved", - input: "SELECT\n/*+ MAX_EXECUTION_TIME(1000) */\n*\nFROM t1", - wantSQL: "SELECT\n/*+ MAX_EXECUTION_TIME(1000) */\n*\nFROM t1", - }, - { - name: "query name comment is dropped", - input: "/* name: Foo :one */\nSELECT 1", - wantSQL: "SELECT 1", - }, - { - name: "dash comments are collected", - input: "-- helpful note\nSELECT 1", - wantSQL: "SELECT 1", - wantComment: []string{" helpful note"}, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - gotSQL, gotComments, err := StripComments(tc.input) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if gotSQL != tc.wantSQL { - t.Errorf("SQL mismatch\n got: %q\nwant: %q", gotSQL, tc.wantSQL) - } - if strings.Join(gotComments, "|") != strings.Join(tc.wantComment, "|") { - t.Errorf("comments mismatch\n got: %q\nwant: %q", gotComments, tc.wantComment) - } - }) - } -}