Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions docs/reference/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,13 @@ Each mapping in the `sql` collection has the following keys:
- Directory of SQL migrations or path to single SQL file; or a list of paths.
- `queries`:
- Directory of SQL queries or path to single SQL file; or a list of paths.
- `query_comments`:
- If enabled, prepend generated SQL text with a structured block comment using query metadata.
Built-in generators include the comment in generated query strings. Supported formats are
`sqlcommenter` and `marginalia`. Supported tags are `name`, `cmd`, and `filename`.
Defaults to `format: sqlcommenter` and `tags: ["name"]`. These comments make generated
queries easier to trace in database logs, APM tools, database monitoring products, and
distributed tracing systems.
- `codegen`:
- A collection of mappings to configure code generators. See [codegen](#codegen) for the supported keys.
- `gen`:
Expand Down Expand Up @@ -116,6 +123,45 @@ sql:
out: postgresql
```

### query_comments

The `query_comments` mapping supports the following keys:

- `enabled`:
- If true, prepend generated SQL query text with a structured comment derived from query metadata.
Defaults to `false`.
- `format`:
- Either `sqlcommenter` or `marginalia`. Defaults to `sqlcommenter`.
- `tags`:
- Query metadata tags to include in the comment. Supported values are `name`, `cmd`, and
`filename`. Defaults to `["name"]`.

```yaml
version: '2'
sql:
- schema: schema.sql
queries: query.sql
engine: postgresql
query_comments:
enabled: true
format: sqlcommenter
tags:
- name
- cmd
- filename
gen:
go:
package: authors
out: postgresql
```

Query comments are useful when you need to connect an expensive query sample, slow query,
or database log entry back to the generated query that produced it. Tools and ecosystems
that understand or generate these SQL comment formats include Datadog Database Monitoring,
OpenTelemetry sqlcommenter libraries, Prisma ORM sql comments, and Rails applications using
Marginalia. Other observability products that consume OpenTelemetry data or preserve SQL
comments in query logs can also use this metadata for correlation.

### analyzer

The `analyzer` mapping supports the following keys:
Expand Down
2 changes: 2 additions & 0 deletions internal/cmd/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,7 @@ func codegen(ctx context.Context, combo config.CombinedSettings, sql OutputPair,

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

case sql.Gen.JSON != nil:
out = combo.JSON.Out
applyQueryComments(req, combo.Package.QueryComments)
handler = ext.HandleFunc(genjson.Generate)
opts, err := json.Marshal(sql.Gen.JSON)
if err != nil {
Expand Down
77 changes: 77 additions & 0 deletions internal/cmd/query_comments.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package cmd

import (
"strings"

"github.com/sqlc-dev/sqlc/internal/config"
"github.com/sqlc-dev/sqlc/internal/plugin"
)

const (
queryCommentFormatMarginalia = "marginalia"
queryCommentFormatSQLCommenter = "sqlcommenter"
)

func applyQueryComments(req *plugin.GenerateRequest, opts config.QueryComments) {
if !opts.Enabled {
return
}
for _, query := range req.Queries {
if query.Text == "" {
continue
}
comment := queryComment(query, opts)
if comment == "" {
continue
}
query.Text = comment + " " + query.Text
}
}

func queryComment(query *plugin.Query, opts config.QueryComments) string {
tags := opts.Tags
if len(tags) == 0 {
tags = []string{"name"}
}

parts := make([]string, 0, len(tags))
for _, tag := range tags {
value := queryCommentValue(query, tag)
if value == "" {
continue
}
key := "sqlc_" + tag
if opts.Format == queryCommentFormatMarginalia {
parts = append(parts, key+":"+escapeQueryCommentValue(value))
} else {
parts = append(parts, key+"='"+escapeQueryCommentValue(value)+"'")
}
}
if len(parts) == 0 {
return ""
}
return "/*" + strings.Join(parts, ",") + "*/"
}

func queryCommentValue(query *plugin.Query, tag string) string {
switch tag {
case "name":
return query.Name
case "cmd":
return query.Cmd
case "filename":
return query.Filename
default:
return ""
}
}

func escapeQueryCommentValue(value string) string {
value = strings.ReplaceAll(value, "*/", "* /")
value = strings.ReplaceAll(value, "\n", " ")
value = strings.ReplaceAll(value, "\r", " ")
value = strings.ReplaceAll(value, "'", "%27")
value = strings.ReplaceAll(value, ",", "%2C")
value = strings.ReplaceAll(value, ":", "%3A")
return value
}
62 changes: 62 additions & 0 deletions internal/cmd/query_comments_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package cmd

import (
"testing"

"github.com/google/go-cmp/cmp"

"github.com/sqlc-dev/sqlc/internal/config"
"github.com/sqlc-dev/sqlc/internal/plugin"
)

func TestApplyQueryComments(t *testing.T) {
req := &plugin.GenerateRequest{
Queries: []*plugin.Query{
{
Name: "GetAuthor",
Cmd: ":one",
Filename: "query.sql",
Text: "SELECT * FROM authors WHERE id = $1",
},
},
}

applyQueryComments(req, config.QueryComments{
Enabled: true,
Tags: []string{"name", "cmd", "filename"},
})

want := "/*sqlc_name='GetAuthor',sqlc_cmd='%3Aone',sqlc_filename='query.sql'*/ SELECT * FROM authors WHERE id = $1"
if diff := cmp.Diff(want, req.Queries[0].Text); diff != "" {
t.Errorf("query text differed (-want +got):\n%s", diff)
}
}

func TestApplyQueryCommentsMarginalia(t *testing.T) {
req := &plugin.GenerateRequest{
Queries: []*plugin.Query{
{
Name: "GetAuthor",
Text: "SELECT * FROM authors WHERE id = $1",
},
},
}

applyQueryComments(req, config.QueryComments{
Enabled: true,
Format: "marginalia",
})

want := "/*sqlc_name:GetAuthor*/ SELECT * FROM authors WHERE id = $1"
if diff := cmp.Diff(want, req.Queries[0].Text); diff != "" {
t.Errorf("query text differed (-want +got):\n%s", diff)
}
}

func TestEscapeQueryCommentValue(t *testing.T) {
got := escapeQueryCommentValue("a'b,c:d\n*/")
want := "a%27b%2Cc%3Ad * /"
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("escaped value differed (-want +got):\n%s", diff)
}
}
1 change: 1 addition & 0 deletions internal/codegen/golang/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,7 @@ type Query struct {
MethodName string
FieldName string
ConstantName string
SQLComment string
SQL string
SourceName string
Ret QueryValue
Expand Down
16 changes: 15 additions & 1 deletion internal/codegen/golang/result.go
Original file line number Diff line number Diff line change
Expand Up @@ -217,13 +217,16 @@ func buildQueries(req *plugin.GenerateRequest, options *opts.Options, enums []En
}
}

sqlComment, sql := splitSQLComment(query.Text)

gq := Query{
Cmd: query.Cmd,
ConstantName: constantName,
FieldName: sdk.LowerTitle(query.Name) + "Stmt",
MethodName: query.Name,
SourceName: query.Filename,
SQL: query.Text,
SQLComment: sqlComment,
SQL: sql,
Comments: comments,
Table: query.InsertIntoTable,
}
Expand Down Expand Up @@ -354,6 +357,17 @@ func buildQueries(req *plugin.GenerateRequest, options *opts.Options, enums []En
return qs, nil
}

func splitSQLComment(sql string) (string, string) {
if !strings.HasPrefix(sql, "/*sqlc_") {
return "", sql
}
idx := strings.Index(sql, "*/")
if idx == -1 {
return "", sql
}
return sql[:idx+2], strings.TrimLeft(sql[idx+2:], " \t\r\n")
}

var cmdReturnsData = map[string]struct{}{
metadata.CmdBatchMany: {},
metadata.CmdBatchOne: {},
Expand Down
22 changes: 22 additions & 0 deletions internal/codegen/golang/result_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package golang
import (
"testing"

"github.com/google/go-cmp/cmp"

"github.com/sqlc-dev/sqlc/internal/metadata"
"github.com/sqlc-dev/sqlc/internal/plugin"
)
Expand Down Expand Up @@ -76,3 +78,23 @@ func TestPutOutColumns_AlwaysTrueWhenQueryHasColumns(t *testing.T) {
t.Error("should be true when we have columns")
}
}

func TestSplitSQLComment(t *testing.T) {
comment, sql := splitSQLComment("/*sqlc_name='GetAuthor'*/ SELECT 1")
if diff := cmp.Diff("/*sqlc_name='GetAuthor'*/", comment); diff != "" {
t.Errorf("comment differed (-want +got):\n%s", diff)
}
if diff := cmp.Diff("SELECT 1", sql); diff != "" {
t.Errorf("sql differed (-want +got):\n%s", diff)
}
}

func TestSplitSQLCommentIgnoresOtherComments(t *testing.T) {
comment, sql := splitSQLComment("/*application='api'*/ SELECT 1")
if comment != "" {
t.Errorf("expected empty comment, got %q", comment)
}
if diff := cmp.Diff("/*application='api'*/ SELECT 1", sql); diff != "" {
t.Errorf("sql differed (-want +got):\n%s", diff)
}
}
3 changes: 2 additions & 1 deletion internal/codegen/golang/templates/pgx/batchCode.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ var (

{{range .GoQueries}}
{{if eq (hasPrefix .Cmd ":batch") true }}
const {{.ConstantName}} = {{$.Q}}-- name: {{.MethodName}} {{.Cmd}}
const {{.ConstantName}} = {{$.Q}}{{if .SQLComment}}{{.SQLComment}}
{{end}}-- name: {{.MethodName}} {{.Cmd}}
{{escape .SQL}}
{{$.Q}}

Expand Down
3 changes: 2 additions & 1 deletion internal/codegen/golang/templates/pgx/queryCode.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
{{range .GoQueries}}
{{if $.OutputQuery .SourceName}}
{{if and (ne .Cmd ":copyfrom") (ne (hasPrefix .Cmd ":batch") true)}}
const {{.ConstantName}} = {{$.Q}}-- name: {{.MethodName}} {{.Cmd}}
const {{.ConstantName}} = {{$.Q}}{{if .SQLComment}}{{.SQLComment}}
{{end}}-- name: {{.MethodName}} {{.Cmd}}
{{escape .SQL}}
{{$.Q}}
{{end}}
Expand Down
3 changes: 2 additions & 1 deletion internal/codegen/golang/templates/stdlib/queryCode.tmpl
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
{{define "queryCodeStd"}}
{{range .GoQueries}}
{{if $.OutputQuery .SourceName}}
const {{.ConstantName}} = {{$.Q}}-- name: {{.MethodName}} {{.Cmd}}
const {{.ConstantName}} = {{$.Q}}{{if .SQLComment}}{{.SQLComment}}
{{end}}-- name: {{.MethodName}} {{.Cmd}}
{{escape .SQL}}
{{$.Q}}

Expand Down
36 changes: 23 additions & 13 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,24 +109,31 @@ type Overrides struct {
}

type SQL struct {
Name string `json:"name" yaml:"name"`
Engine Engine `json:"engine,omitempty" yaml:"engine"`
Schema Paths `json:"schema" yaml:"schema"`
Queries Paths `json:"queries" yaml:"queries"`
Database *Database `json:"database" yaml:"database"`
StrictFunctionChecks bool `json:"strict_function_checks" yaml:"strict_function_checks"`
StrictOrderBy *bool `json:"strict_order_by" yaml:"strict_order_by"`
Gen SQLGen `json:"gen" yaml:"gen"`
Codegen []Codegen `json:"codegen" yaml:"codegen"`
Rules []string `json:"rules" yaml:"rules"`
Analyzer Analyzer `json:"analyzer" yaml:"analyzer"`
Name string `json:"name" yaml:"name"`
Engine Engine `json:"engine,omitempty" yaml:"engine"`
Schema Paths `json:"schema" yaml:"schema"`
Queries Paths `json:"queries" yaml:"queries"`
QueryComments QueryComments `json:"query_comments" yaml:"query_comments"`
Database *Database `json:"database" yaml:"database"`
StrictFunctionChecks bool `json:"strict_function_checks" yaml:"strict_function_checks"`
StrictOrderBy *bool `json:"strict_order_by" yaml:"strict_order_by"`
Gen SQLGen `json:"gen" yaml:"gen"`
Codegen []Codegen `json:"codegen" yaml:"codegen"`
Rules []string `json:"rules" yaml:"rules"`
Analyzer Analyzer `json:"analyzer" yaml:"analyzer"`
}

type QueryComments struct {
Enabled bool `json:"enabled" yaml:"enabled"`
Format string `json:"format,omitempty" yaml:"format"`
Tags []string `json:"tags,omitempty" yaml:"tags"`
}

// AnalyzerDatabase represents the database analyzer setting.
// It can be a boolean (true/false) or the string "only" for database-only mode.
type AnalyzerDatabase struct {
value *bool // nil means not set, true/false for boolean values
isOnly bool // true when set to "only"
value *bool // nil means not set, true/false for boolean values
isOnly bool // true when set to "only"
}

// IsEnabled returns true if the database analyzer should be used.
Expand Down Expand Up @@ -228,6 +235,9 @@ var ErrPluginNoType = errors.New("plugin: field `process` or `wasm` required")
var ErrPluginBothTypes = errors.New("plugin: `process` and `wasm` cannot both be defined")
var ErrPluginProcessNoCmd = errors.New("plugin: missing process command")

var ErrInvalidQueryCommentFormat = errors.New("invalid query_comments format")
var ErrInvalidQueryCommentTag = errors.New("invalid query_comments tag")

var ErrInvalidDatabase = errors.New("database must be managed or have a non-empty URI")
var ErrManagedDatabaseNoProject = errors.New(`managed databases require a cloud project

Expand Down
Loading
Loading