Skip to content

Commit 58dee2e

Browse files
committed
feat: add query metadata comments
1 parent b84b1d6 commit 58dee2e

27 files changed

Lines changed: 705 additions & 21 deletions

File tree

docs/reference/config.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,13 @@ Each mapping in the `sql` collection has the following keys:
4242
- Directory of SQL migrations or path to single SQL file; or a list of paths.
4343
- `queries`:
4444
- Directory of SQL queries or path to single SQL file; or a list of paths.
45+
- `query_comments`:
46+
- If enabled, prepend generated SQL text with a structured block comment using query metadata.
47+
Built-in generators include the comment in generated query strings. Supported formats are
48+
`sqlcommenter` and `marginalia`. Supported tags are `name`, `cmd`, and `filename`.
49+
Defaults to `format: sqlcommenter` and `tags: ["name"]`. These comments make generated
50+
queries easier to trace in database logs, APM tools, database monitoring products, and
51+
distributed tracing systems.
4552
- `codegen`:
4653
- A collection of mappings to configure code generators. See [codegen](#codegen) for the supported keys.
4754
- `gen`:
@@ -116,6 +123,45 @@ sql:
116123
out: postgresql
117124
```
118125

126+
### query_comments
127+
128+
The `query_comments` mapping supports the following keys:
129+
130+
- `enabled`:
131+
- If true, prepend generated SQL query text with a structured comment derived from query metadata.
132+
Defaults to `false`.
133+
- `format`:
134+
- Either `sqlcommenter` or `marginalia`. Defaults to `sqlcommenter`.
135+
- `tags`:
136+
- Query metadata tags to include in the comment. Supported values are `name`, `cmd`, and
137+
`filename`. Defaults to `["name"]`.
138+
139+
```yaml
140+
version: '2'
141+
sql:
142+
- schema: schema.sql
143+
queries: query.sql
144+
engine: postgresql
145+
query_comments:
146+
enabled: true
147+
format: sqlcommenter
148+
tags:
149+
- name
150+
- cmd
151+
- filename
152+
gen:
153+
go:
154+
package: authors
155+
out: postgresql
156+
```
157+
158+
Query comments are useful when you need to connect an expensive query sample, slow query,
159+
or database log entry back to the generated query that produced it. Tools and ecosystems
160+
that understand or generate these SQL comment formats include Datadog Database Monitoring,
161+
OpenTelemetry sqlcommenter libraries, Prisma ORM sql comments, and Rails applications using
162+
Marginalia. Other observability products that consume OpenTelemetry data or preserve SQL
163+
comments in query logs can also use this metadata for correlation.
164+
119165
### analyzer
120166

121167
The `analyzer` mapping supports the following keys:

internal/cmd/generate.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,7 @@ func codegen(ctx context.Context, combo config.CombinedSettings, sql OutputPair,
339339

340340
case sql.Gen.Go != nil:
341341
out = combo.Go.Out
342+
applyQueryComments(req, combo.Package.QueryComments)
342343
handler = ext.HandleFunc(golang.Generate)
343344
opts, err := json.Marshal(sql.Gen.Go)
344345
if err != nil {
@@ -356,6 +357,7 @@ func codegen(ctx context.Context, combo config.CombinedSettings, sql OutputPair,
356357

357358
case sql.Gen.JSON != nil:
358359
out = combo.JSON.Out
360+
applyQueryComments(req, combo.Package.QueryComments)
359361
handler = ext.HandleFunc(genjson.Generate)
360362
opts, err := json.Marshal(sql.Gen.JSON)
361363
if err != nil {

internal/cmd/query_comments.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package cmd
2+
3+
import (
4+
"strings"
5+
6+
"github.com/sqlc-dev/sqlc/internal/config"
7+
"github.com/sqlc-dev/sqlc/internal/plugin"
8+
)
9+
10+
const (
11+
queryCommentFormatMarginalia = "marginalia"
12+
queryCommentFormatSQLCommenter = "sqlcommenter"
13+
)
14+
15+
func applyQueryComments(req *plugin.GenerateRequest, opts config.QueryComments) {
16+
if !opts.Enabled {
17+
return
18+
}
19+
for _, query := range req.Queries {
20+
if query.Text == "" {
21+
continue
22+
}
23+
comment := queryComment(query, opts)
24+
if comment == "" {
25+
continue
26+
}
27+
query.Text = comment + " " + query.Text
28+
}
29+
}
30+
31+
func queryComment(query *plugin.Query, opts config.QueryComments) string {
32+
tags := opts.Tags
33+
if len(tags) == 0 {
34+
tags = []string{"name"}
35+
}
36+
37+
parts := make([]string, 0, len(tags))
38+
for _, tag := range tags {
39+
value := queryCommentValue(query, tag)
40+
if value == "" {
41+
continue
42+
}
43+
key := "sqlc_" + tag
44+
if opts.Format == queryCommentFormatMarginalia {
45+
parts = append(parts, key+":"+escapeQueryCommentValue(value))
46+
} else {
47+
parts = append(parts, key+"='"+escapeQueryCommentValue(value)+"'")
48+
}
49+
}
50+
if len(parts) == 0 {
51+
return ""
52+
}
53+
return "/*" + strings.Join(parts, ",") + "*/"
54+
}
55+
56+
func queryCommentValue(query *plugin.Query, tag string) string {
57+
switch tag {
58+
case "name":
59+
return query.Name
60+
case "cmd":
61+
return query.Cmd
62+
case "filename":
63+
return query.Filename
64+
default:
65+
return ""
66+
}
67+
}
68+
69+
func escapeQueryCommentValue(value string) string {
70+
value = strings.ReplaceAll(value, "*/", "* /")
71+
value = strings.ReplaceAll(value, "\n", " ")
72+
value = strings.ReplaceAll(value, "\r", " ")
73+
value = strings.ReplaceAll(value, "'", "%27")
74+
value = strings.ReplaceAll(value, ",", "%2C")
75+
value = strings.ReplaceAll(value, ":", "%3A")
76+
return value
77+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package cmd
2+
3+
import (
4+
"testing"
5+
6+
"github.com/google/go-cmp/cmp"
7+
8+
"github.com/sqlc-dev/sqlc/internal/config"
9+
"github.com/sqlc-dev/sqlc/internal/plugin"
10+
)
11+
12+
func TestApplyQueryComments(t *testing.T) {
13+
req := &plugin.GenerateRequest{
14+
Queries: []*plugin.Query{
15+
{
16+
Name: "GetAuthor",
17+
Cmd: ":one",
18+
Filename: "query.sql",
19+
Text: "SELECT * FROM authors WHERE id = $1",
20+
},
21+
},
22+
}
23+
24+
applyQueryComments(req, config.QueryComments{
25+
Enabled: true,
26+
Tags: []string{"name", "cmd", "filename"},
27+
})
28+
29+
want := "/*sqlc_name='GetAuthor',sqlc_cmd='%3Aone',sqlc_filename='query.sql'*/ SELECT * FROM authors WHERE id = $1"
30+
if diff := cmp.Diff(want, req.Queries[0].Text); diff != "" {
31+
t.Errorf("query text differed (-want +got):\n%s", diff)
32+
}
33+
}
34+
35+
func TestApplyQueryCommentsMarginalia(t *testing.T) {
36+
req := &plugin.GenerateRequest{
37+
Queries: []*plugin.Query{
38+
{
39+
Name: "GetAuthor",
40+
Text: "SELECT * FROM authors WHERE id = $1",
41+
},
42+
},
43+
}
44+
45+
applyQueryComments(req, config.QueryComments{
46+
Enabled: true,
47+
Format: "marginalia",
48+
})
49+
50+
want := "/*sqlc_name:GetAuthor*/ SELECT * FROM authors WHERE id = $1"
51+
if diff := cmp.Diff(want, req.Queries[0].Text); diff != "" {
52+
t.Errorf("query text differed (-want +got):\n%s", diff)
53+
}
54+
}
55+
56+
func TestEscapeQueryCommentValue(t *testing.T) {
57+
got := escapeQueryCommentValue("a'b,c:d\n*/")
58+
want := "a%27b%2Cc%3Ad * /"
59+
if diff := cmp.Diff(want, got); diff != "" {
60+
t.Errorf("escaped value differed (-want +got):\n%s", diff)
61+
}
62+
}

internal/codegen/golang/query.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,7 @@ type Query struct {
273273
MethodName string
274274
FieldName string
275275
ConstantName string
276+
SQLComment string
276277
SQL string
277278
SourceName string
278279
Ret QueryValue

internal/codegen/golang/result.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,13 +217,16 @@ func buildQueries(req *plugin.GenerateRequest, options *opts.Options, enums []En
217217
}
218218
}
219219

220+
sqlComment, sql := splitSQLComment(query.Text)
221+
220222
gq := Query{
221223
Cmd: query.Cmd,
222224
ConstantName: constantName,
223225
FieldName: sdk.LowerTitle(query.Name) + "Stmt",
224226
MethodName: query.Name,
225227
SourceName: query.Filename,
226-
SQL: query.Text,
228+
SQLComment: sqlComment,
229+
SQL: sql,
227230
Comments: comments,
228231
Table: query.InsertIntoTable,
229232
}
@@ -354,6 +357,17 @@ func buildQueries(req *plugin.GenerateRequest, options *opts.Options, enums []En
354357
return qs, nil
355358
}
356359

360+
func splitSQLComment(sql string) (string, string) {
361+
if !strings.HasPrefix(sql, "/*sqlc_") {
362+
return "", sql
363+
}
364+
idx := strings.Index(sql, "*/")
365+
if idx == -1 {
366+
return "", sql
367+
}
368+
return sql[:idx+2], strings.TrimLeft(sql[idx+2:], " \t\r\n")
369+
}
370+
357371
var cmdReturnsData = map[string]struct{}{
358372
metadata.CmdBatchMany: {},
359373
metadata.CmdBatchOne: {},

internal/codegen/golang/result_test.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package golang
33
import (
44
"testing"
55

6+
"github.com/google/go-cmp/cmp"
7+
68
"github.com/sqlc-dev/sqlc/internal/metadata"
79
"github.com/sqlc-dev/sqlc/internal/plugin"
810
)
@@ -76,3 +78,23 @@ func TestPutOutColumns_AlwaysTrueWhenQueryHasColumns(t *testing.T) {
7678
t.Error("should be true when we have columns")
7779
}
7880
}
81+
82+
func TestSplitSQLComment(t *testing.T) {
83+
comment, sql := splitSQLComment("/*sqlc_name='GetAuthor'*/ SELECT 1")
84+
if diff := cmp.Diff("/*sqlc_name='GetAuthor'*/", comment); diff != "" {
85+
t.Errorf("comment differed (-want +got):\n%s", diff)
86+
}
87+
if diff := cmp.Diff("SELECT 1", sql); diff != "" {
88+
t.Errorf("sql differed (-want +got):\n%s", diff)
89+
}
90+
}
91+
92+
func TestSplitSQLCommentIgnoresOtherComments(t *testing.T) {
93+
comment, sql := splitSQLComment("/*application='api'*/ SELECT 1")
94+
if comment != "" {
95+
t.Errorf("expected empty comment, got %q", comment)
96+
}
97+
if diff := cmp.Diff("/*application='api'*/ SELECT 1", sql); diff != "" {
98+
t.Errorf("sql differed (-want +got):\n%s", diff)
99+
}
100+
}

internal/codegen/golang/templates/pgx/batchCode.tmpl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ var (
66

77
{{range .GoQueries}}
88
{{if eq (hasPrefix .Cmd ":batch") true }}
9-
const {{.ConstantName}} = {{$.Q}}-- name: {{.MethodName}} {{.Cmd}}
9+
const {{.ConstantName}} = {{$.Q}}{{if .SQLComment}}{{.SQLComment}}
10+
{{end}}-- name: {{.MethodName}} {{.Cmd}}
1011
{{escape .SQL}}
1112
{{$.Q}}
1213

internal/codegen/golang/templates/pgx/queryCode.tmpl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
{{range .GoQueries}}
33
{{if $.OutputQuery .SourceName}}
44
{{if and (ne .Cmd ":copyfrom") (ne (hasPrefix .Cmd ":batch") true)}}
5-
const {{.ConstantName}} = {{$.Q}}-- name: {{.MethodName}} {{.Cmd}}
5+
const {{.ConstantName}} = {{$.Q}}{{if .SQLComment}}{{.SQLComment}}
6+
{{end}}-- name: {{.MethodName}} {{.Cmd}}
67
{{escape .SQL}}
78
{{$.Q}}
89
{{end}}

internal/codegen/golang/templates/stdlib/queryCode.tmpl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
{{define "queryCodeStd"}}
22
{{range .GoQueries}}
33
{{if $.OutputQuery .SourceName}}
4-
const {{.ConstantName}} = {{$.Q}}-- name: {{.MethodName}} {{.Cmd}}
4+
const {{.ConstantName}} = {{$.Q}}{{if .SQLComment}}{{.SQLComment}}
5+
{{end}}-- name: {{.MethodName}} {{.Cmd}}
56
{{escape .SQL}}
67
{{$.Q}}
78

0 commit comments

Comments
 (0)