Skip to content
Open
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
4 changes: 4 additions & 0 deletions docs/reference/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,8 @@ The `gen` mapping supports the following keys:
that returns all valid enum values.
- `emit_sql_as_comment`:
- If true, emits the SQL statement as a code-block comment above the generated function, appending to any existing comments. Defaults to `false`.
- `emit_read_only_prepared`:
- If true, also generate a `read.go` file with a `ReadQueries` type, created with `PrepareReadOnly`, that holds only the read-only queries: those that `SELECT` and do not `INSERT`, `UPDATE`, `DELETE` or `SELECT ... FOR UPDATE`. Prepare it on a connection whose database user can only read, such as a read replica, while `Prepare` keeps every query. Not supported for `pgx/v4` or `pgx/v5`, where no `read.go` is generated. Defaults to `false`.
- `build_tags`:
- If set, add a `//go:build <build_tags>` directive at the beginning of each generated Go file.
- `initialisms`:
Expand Down Expand Up @@ -426,6 +428,8 @@ Each mapping in the `packages` collection has the following keys:
- If true, add DB tags to generated structs. Defaults to `false`.
- `emit_prepared_queries`:
- If true, include support for prepared queries. Defaults to `false`.
- `emit_read_only_prepared`:
- If true, also generate a `read.go` file with a `ReadQueries` type, created with `PrepareReadOnly`, that holds only the read-only queries: those that `SELECT` and do not `INSERT`, `UPDATE`, `DELETE` or `SELECT ... FOR UPDATE`. Prepare it on a connection whose database user can only read, such as a read replica, while `Prepare` keeps every query. Not supported for `pgx/v4` or `pgx/v5`, where no `read.go` is generated. Defaults to `false`.
- `emit_interface`:
- If true, output a `Querier` interface in the generated package. Defaults to `false`.
- `emit_exact_table_names`:
Expand Down
57 changes: 57 additions & 0 deletions internal/codegen/golang/gen.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ type tmplCtx struct {
Enums []Enum
Structs []Struct
GoQueries []Query
GoReadQueries []Query
SqlcVersion string

// TODO: Race conditions
Expand All @@ -39,6 +40,7 @@ type tmplCtx struct {
EmitMethodsWithDBArgument bool
EmitEnumValidMethod bool
EmitAllEnumValues bool
EmitReadOnlyPrepared bool
UsesCopyFrom bool
UsesBatch bool
OmitSqlcVersion bool
Expand Down Expand Up @@ -187,6 +189,7 @@ func generate(req *plugin.GenerateRequest, options *opts.Options, enums []Enum,
EmitMethodsWithDBArgument: options.EmitMethodsWithDbArgument,
EmitEnumValidMethod: options.EmitEnumValidMethod,
EmitAllEnumValues: options.EmitAllEnumValues,
EmitReadOnlyPrepared: options.EmitReadOnlyPrepared,
UsesCopyFrom: usesCopyFrom(queries),
UsesBatch: usesBatch(queries),
SQLDriver: parseDriver(options.SqlPackage),
Expand Down Expand Up @@ -252,6 +255,7 @@ func generate(req *plugin.GenerateRequest, options *opts.Options, enums []Enum,
w := bufio.NewWriter(&b)
tctx.SourceName = name
tctx.GoQueries = replacedQueries
tctx.GoReadQueries = readOnly(replacedQueries)
err := tmpl.ExecuteTemplate(w, templateName, &tctx)
w.Flush()
if err != nil {
Expand Down Expand Up @@ -290,6 +294,7 @@ func generate(req *plugin.GenerateRequest, options *opts.Options, enums []Enum,
if options.OutputCopyfromFileName != "" {
copyfromFileName = options.OutputCopyfromFileName
}
readQueriesFileName := "read.go"

batchFileName := "batch.go"
if options.OutputBatchFileName != "" {
Expand Down Expand Up @@ -319,6 +324,11 @@ func generate(req *plugin.GenerateRequest, options *opts.Options, enums []Enum,
return nil, err
}
}
if tctx.EmitReadOnlyPrepared {
if err := execute(readQueriesFileName, "readQueriesFile"); err != nil {
return nil, err
}
}

files := map[string]struct{}{}
for _, gq := range queries {
Expand Down Expand Up @@ -428,3 +438,50 @@ func filterUnusedStructs(enums []Enum, structs []Struct, queries []Query, qualif

return keepEnums, keepStructs
}

// keywordFollowedBySpace reports whether kw appears in s followed by whitespace
// (space, tab, or newline). A plain substring check on "KEYWORD " misses queries
// formatted with a newline right after the keyword, e.g.:
//
// SELECT
// col1, col2
//
// which is a common style for wide column lists and must not be misclassified
// as lacking a SELECT (or falsely matching UPDATE/INSERT/DELETE only when
// followed by a space).
func keywordFollowedBySpace(s, kw string) bool {
for {
idx := strings.Index(s, kw)
if idx == -1 {
return false
}
after := idx + len(kw)
if after < len(s) && (s[after] == ' ' || s[after] == '\t' || s[after] == '\n' || s[after] == '\r') {
return true
}
s = s[after:]
}
}

func readOnly(queries []Query) []Query {
var rq []Query
for _, q := range queries {
if !q.hasRetType() {
continue
}

qsql := strings.ToUpper(q.SQL)
if !keywordFollowedBySpace(qsql, "SELECT") {
continue
}
if keywordFollowedBySpace(qsql, "INSERT") ||
keywordFollowedBySpace(qsql, "UPDATE") ||
strings.Contains(qsql, "FOR UPDATE") ||
keywordFollowedBySpace(qsql, "DELETE") {
continue
}

rq = append(rq, q)
}
return rq
}
35 changes: 29 additions & 6 deletions internal/codegen/golang/imports.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ func (i *importer) Imports(filename string) [][]ImportSpec {
if i.Options.OutputBatchFileName != "" {
batchFileName = i.Options.OutputBatchFileName
}
readQueriesFileName := "read.go"

switch filename {
case dbFileName:
Expand All @@ -114,6 +115,8 @@ func (i *importer) Imports(filename string) [][]ImportSpec {
return mergeImports(i.copyfromImports())
case batchFileName:
return mergeImports(i.batchImports())
case readQueriesFileName:
return mergeImports(i.readOnlyImports())
default:
return mergeImports(i.queryImports(filename))
}
Expand Down Expand Up @@ -360,10 +363,27 @@ func (i *importer) queryImports(filename string) fileImports {
}
}

std, pkg := buildImports(i.Options, i.Engine, gq, func(name string) bool {
return queryFileImports(i.Options, i.Engine, gq, anyNonCopyFrom, true)
}

// readOnlyImports computes the imports needed by read.go. Its boilerplate
// (PrepareReadOnly/Close/query/queryRow) always needs the same baseline as
// db.go (context, database/sql, fmt), plus whatever the read-only subset of
// queries (see readOnly) additionally needs for its own Ret/Arg types - e.g.
// "strings" for a query using sqlc.slice(). Reusing dbImports alone silently
// misses that second part.
func (i *importer) readOnlyImports() fileImports {
base := i.dbImports()
extra := queryFileImports(i.Options, i.Engine, readOnly(i.Queries), false, false)
merged := mergeImports(base, extra)
return fileImports{Std: merged[0], Dep: merged[1]}
}

func queryFileImports(options *opts.Options, engine string, gq []Query, anyNonCopyFrom bool, scanStructFields bool) fileImports {
std, pkg := buildImports(options, engine, gq, func(name string) bool {
for _, q := range gq {
if q.hasRetType() {
if q.Ret.EmitStruct() {
if scanStructFields && q.Ret.EmitStruct() {
for _, f := range q.Ret.Struct.Fields {
if hasPrefixIgnoringSliceAndPointerPrefix(f.Type, name) {
return true
Expand All @@ -375,7 +395,7 @@ func (i *importer) queryImports(filename string) fileImports {
}
}
// Check the fields of the argument struct if it's emitted
if q.Arg.EmitStruct() {
if scanStructFields && q.Arg.EmitStruct() {
for _, f := range q.Arg.Struct.Fields {
if hasPrefixIgnoringSliceAndPointerPrefix(f.Type, name) {
return true
Expand All @@ -392,6 +412,9 @@ func (i *importer) queryImports(filename string) fileImports {
return false
})

// lib/pq is needed whenever a query scans into or binds a slice with
// pq.Array, which happens in the method bodies whether or not this file
// declares the Arg/Ret structs, so it is not gated by scanStructFields.
sliceScan := func() bool {
for _, q := range gq {
if q.hasRetType() {
Expand Down Expand Up @@ -450,15 +473,15 @@ func (i *importer) queryImports(filename string) fileImports {
std["context"] = struct{}{}
}

sqlpkg := parseDriver(i.Options.SqlPackage)
sqlpkg := parseDriver(options.SqlPackage)
if sqlcSliceScan() && !sqlpkg.IsPGX() {
std["strings"] = struct{}{}
}
if sliceScan() && usesPqArrays(i.Engine, sqlpkg) {
if sliceScan() && usesPqArrays(engine, sqlpkg) {
pkg[ImportSpec{Path: "github.com/lib/pq"}] = struct{}{}
}

if i.Options.WrapErrors {
if options.WrapErrors {
std["fmt"] = struct{}{}
}

Expand Down
1 change: 1 addition & 0 deletions internal/codegen/golang/opts/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ type Options struct {
EmitEnumValidMethod bool `json:"emit_enum_valid_method,omitempty" yaml:"emit_enum_valid_method"`
EmitAllEnumValues bool `json:"emit_all_enum_values,omitempty" yaml:"emit_all_enum_values"`
EmitSqlAsComment bool `json:"emit_sql_as_comment,omitempty" yaml:"emit_sql_as_comment"`
EmitReadOnlyPrepared bool `json:"emit_read_only_prepared,omitempty" yaml:"emit_read_only_prepared"`
JsonTagsCaseStyle string `json:"json_tags_case_style,omitempty" yaml:"json_tags_case_style"`
Package string `json:"package" yaml:"package"`
Out string `json:"out" yaml:"out"`
Expand Down
108 changes: 108 additions & 0 deletions internal/codegen/golang/templates/stdlib/readonlyCode.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
{{define "readQueriesFileStd"}}

{{if and .EmitPreparedQueries .EmitReadOnlyPrepared}}
func PrepareReadOnly(ctx context.Context, db DBTX) (*ReadQueries, error) {
q := ReadQueries{db: db}
var err error
{{- if eq (len .GoReadQueries) 0 }}
_ = err
{{- end }}
{{- range .GoReadQueries }}
if q.{{.FieldName}}, err = db.PrepareContext(ctx, {{.ConstantName}}); err != nil {
return nil, fmt.Errorf("error preparing query {{.MethodName}}: %w", err)
}
{{- end}}
return &q, nil
}

type ReadQueries struct {
{{- if not .EmitMethodsWithDBArgument}}
db DBTX
{{- end}}

{{- if .EmitPreparedQueries}}
{{- range .GoReadQueries}}
{{.FieldName}} *sql.Stmt
{{- end}}
{{- end}}
}

func (q *ReadQueries) Close() error {
var err error
{{- range .GoReadQueries }}
if q.{{.FieldName}} != nil {
if cerr := q.{{.FieldName}}.Close(); cerr != nil {
err = fmt.Errorf("error closing {{.FieldName}}: %w", cerr)
}
}
{{- end}}
return err
}

func (q *ReadQueries) query(ctx context.Context, stmt *sql.Stmt, query string, args ...interface{}) (*sql.Rows, error) {
switch {
case stmt != nil:
return stmt.QueryContext(ctx, args...)
default:
return q.db.QueryContext(ctx, query, args...)
}
}

func (q *ReadQueries) queryRow(ctx context.Context, stmt *sql.Stmt, query string, args ...interface{}) (*sql.Row) {
switch {
case stmt != nil:
return stmt.QueryRowContext(ctx, args...)
default:
return q.db.QueryRowContext(ctx, query, args...)
}
}

{{range .GoReadQueries}}

{{if eq .Cmd ":one"}}
{{range .Comments}}//{{.}}
{{end -}}
func (q *ReadQueries) {{.MethodName}}(ctx context.Context, {{ dbarg }} {{.Arg.Pair}}) ({{.Ret.DefineType}}, error) {
{{- template "queryCodeStdExec" . }}
{{- if or (ne .Arg.Pair .Ret.Pair) (ne .Arg.DefineType .Ret.DefineType) }}
var {{.Ret.Name}} {{.Ret.Type}}
{{- end}}
err := row.Scan({{.Ret.Scan}})
return {{.Ret.ReturnName}}, err
}
{{end}}

{{if eq .Cmd ":many"}}
{{range .Comments}}//{{.}}
{{end -}}
func (q *ReadQueries) {{.MethodName}}(ctx context.Context, {{ dbarg }} {{.Arg.Pair}}) ([]{{.Ret.DefineType}}, error) {
{{- template "queryCodeStdExec" . }}
if err != nil {
return nil, err
}
defer rows.Close()
{{- if $.EmitEmptySlices}}
items := []{{.Ret.DefineType}}{}
{{else}}
var items []{{.Ret.DefineType}}
{{end -}}
for rows.Next() {
var {{.Ret.Name}} {{.Ret.Type}}
if err := rows.Scan({{.Ret.Scan}}); err != nil {
return nil, err
}
items = append(items, {{.Ret.ReturnName}})
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
{{end}}

{{end}}
{{end}}
{{end}}
27 changes: 27 additions & 0 deletions internal/codegen/golang/templates/template.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -251,4 +251,31 @@ import (
{{if .SQLDriver.IsPGX }}
{{- template "batchCodePgx" .}}
{{end}}

{{template "readQueriesFile" . }}
{{end}}

{{define "readQueriesFile"}}
{{if not .SQLDriver.IsPGX }}
{{if .BuildTags}}
//go:build {{.BuildTags}}

{{end}}// Code generated by sqlc. DO NOT EDIT.
{{if not .OmitSqlcVersion}}// versions:
// sqlc {{.SqlcVersion}}

package {{.Package}}

{{ if hasImports .SourceName }}
import (
{{range imports .SourceName}}
{{range .}}{{.}}
{{end}}
{{end}}
)
{{end}}

{{- template "readQueriesFileStd" .}}
{{end}}
{{end}}
{{end}}
2 changes: 2 additions & 0 deletions internal/config/v_one.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ type v1PackageSettings struct {
EmitEnumValidMethod bool `json:"emit_enum_valid_method,omitempty" yaml:"emit_enum_valid_method"`
EmitAllEnumValues bool `json:"emit_all_enum_values,omitempty" yaml:"emit_all_enum_values"`
EmitSqlAsComment bool `json:"emit_sql_as_comment,omitempty" yaml:"emit_sql_as_comment"`
EmitReadOnlyPrepared bool `json:"emit_read_only_prepared,omitempty" yaml:"emit_read_only_prepared"`
JSONTagsCaseStyle string `json:"json_tags_case_style,omitempty" yaml:"json_tags_case_style"`
SQLPackage string `json:"sql_package" yaml:"sql_package"`
SQLDriver string `json:"sql_driver" yaml:"sql_driver"`
Expand Down Expand Up @@ -158,6 +159,7 @@ func (c *V1GenerateSettings) Translate() Config {
EmitEnumValidMethod: pkg.EmitEnumValidMethod,
EmitAllEnumValues: pkg.EmitAllEnumValues,
EmitSqlAsComment: pkg.EmitSqlAsComment,
EmitReadOnlyPrepared: pkg.EmitReadOnlyPrepared,
Package: pkg.Name,
Out: pkg.Path,
SqlPackage: pkg.SQLPackage,
Expand Down
3 changes: 3 additions & 0 deletions internal/config/v_one.json
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,9 @@
"emit_sql_as_comment": {
"type": "boolean"
},
"emit_read_only_prepared": {
"type": "boolean"
},
"build_tags": {
"type": "string"
},
Expand Down
3 changes: 3 additions & 0 deletions internal/config/v_two.json
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,9 @@
"emit_sql_as_comment": {
"type": "boolean"
},
"emit_read_only_prepared": {
"type": "boolean"
},
"build_tags": {
"type": "string"
},
Expand Down
Loading
Loading