From 7dc4b324b1b009f616f95371502f05e7d767012f Mon Sep 17 00:00:00 2001 From: Jarri Abidi Date: Thu, 29 Oct 2026 22:40:28 +0500 Subject: [PATCH] feat(golang): add emit_read_only_prepared to generate read-only queries Add an emit_read_only_prepared option for database/sql. When set, sqlc also generates read.go with a ReadQueries type, created with PrepareReadOnly, that prepares only the read-only queries: those that SELECT and do not INSERT, UPDATE, DELETE or SELECT ... FOR UPDATE. This lets read queries be prepared on a connection whose database user can only read, such as a read replica, without preparing write statements that user isn't allowed to prepare. Prepare is unchanged and keeps every query, and the models are shared, so nothing is duplicated. read.go gets its imports from the read-only queries' own argument and result types, including lib/pq when they bind or scan slices with pq.Array. Keywords are matched when followed by any whitespace, so queries written as "SELECT\n col1, ..." are recognised. pgx is not supported; no read.go is generated for it. Adds the option to the docs and both config schemas, and endtoend fixtures for MySQL, PostgreSQL, and PostgreSQL with slice arguments and results. Closes #3187. Supersedes #3291, which was closed for merge conflicts. Co-Authored-By: Claude Opus 5.5 --- docs/reference/config.md | 4 + internal/codegen/golang/gen.go | 57 ++++++++ internal/codegen/golang/imports.go | 35 ++++- internal/codegen/golang/opts/options.go | 1 + .../golang/templates/stdlib/readonlyCode.tmpl | 108 +++++++++++++++ .../codegen/golang/templates/template.tmpl | 27 ++++ internal/config/v_one.go | 2 + internal/config/v_one.json | 3 + internal/config/v_two.json | 3 + .../read_write_segregation/mysql/go/db.go | 128 ++++++++++++++++++ .../read_write_segregation/mysql/go/models.go | 16 +++ .../mysql/go/query.sql.go | 110 +++++++++++++++ .../read_write_segregation/mysql/go/read.go | 97 +++++++++++++ .../read_write_segregation/mysql/query.sql | 14 ++ .../read_write_segregation/mysql/schema.sql | 6 + .../read_write_segregation/mysql/sqlc.json | 14 ++ .../postgresql/stdlib/go/db.go | 128 ++++++++++++++++++ .../postgresql/stdlib/go/models.go | 16 +++ .../postgresql/stdlib/go/query.sql.go | 110 +++++++++++++++ .../postgresql/stdlib/go/read.go | 97 +++++++++++++ .../postgresql/stdlib/query.sql | 14 ++ .../postgresql/stdlib/schema.sql | 7 + .../postgresql/stdlib/sqlc.json | 14 ++ .../postgresql/stdlib/go/db.go | 108 +++++++++++++++ .../postgresql/stdlib/go/models.go | 11 ++ .../postgresql/stdlib/go/query.sql.go | 72 ++++++++++ .../postgresql/stdlib/go/read.go | 94 +++++++++++++ .../postgresql/stdlib/query.sql | 11 ++ .../postgresql/stdlib/schema.sql | 5 + .../postgresql/stdlib/sqlc.json | 14 ++ 30 files changed, 1320 insertions(+), 6 deletions(-) create mode 100644 internal/codegen/golang/templates/stdlib/readonlyCode.tmpl create mode 100644 internal/endtoend/testdata/read_write_segregation/mysql/go/db.go create mode 100644 internal/endtoend/testdata/read_write_segregation/mysql/go/models.go create mode 100644 internal/endtoend/testdata/read_write_segregation/mysql/go/query.sql.go create mode 100644 internal/endtoend/testdata/read_write_segregation/mysql/go/read.go create mode 100644 internal/endtoend/testdata/read_write_segregation/mysql/query.sql create mode 100644 internal/endtoend/testdata/read_write_segregation/mysql/schema.sql create mode 100644 internal/endtoend/testdata/read_write_segregation/mysql/sqlc.json create mode 100644 internal/endtoend/testdata/read_write_segregation/postgresql/stdlib/go/db.go create mode 100644 internal/endtoend/testdata/read_write_segregation/postgresql/stdlib/go/models.go create mode 100644 internal/endtoend/testdata/read_write_segregation/postgresql/stdlib/go/query.sql.go create mode 100644 internal/endtoend/testdata/read_write_segregation/postgresql/stdlib/go/read.go create mode 100644 internal/endtoend/testdata/read_write_segregation/postgresql/stdlib/query.sql create mode 100644 internal/endtoend/testdata/read_write_segregation/postgresql/stdlib/schema.sql create mode 100644 internal/endtoend/testdata/read_write_segregation/postgresql/stdlib/sqlc.json create mode 100644 internal/endtoend/testdata/read_write_segregation_slices/postgresql/stdlib/go/db.go create mode 100644 internal/endtoend/testdata/read_write_segregation_slices/postgresql/stdlib/go/models.go create mode 100644 internal/endtoend/testdata/read_write_segregation_slices/postgresql/stdlib/go/query.sql.go create mode 100644 internal/endtoend/testdata/read_write_segregation_slices/postgresql/stdlib/go/read.go create mode 100644 internal/endtoend/testdata/read_write_segregation_slices/postgresql/stdlib/query.sql create mode 100644 internal/endtoend/testdata/read_write_segregation_slices/postgresql/stdlib/schema.sql create mode 100644 internal/endtoend/testdata/read_write_segregation_slices/postgresql/stdlib/sqlc.json diff --git a/docs/reference/config.md b/docs/reference/config.md index 15fdb7cfd4..0e949546ad 100644 --- a/docs/reference/config.md +++ b/docs/reference/config.md @@ -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 ` directive at the beginning of each generated Go file. - `initialisms`: @@ -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`: diff --git a/internal/codegen/golang/gen.go b/internal/codegen/golang/gen.go index 725f5970cf..b14631ed40 100644 --- a/internal/codegen/golang/gen.go +++ b/internal/codegen/golang/gen.go @@ -25,6 +25,7 @@ type tmplCtx struct { Enums []Enum Structs []Struct GoQueries []Query + GoReadQueries []Query SqlcVersion string // TODO: Race conditions @@ -39,6 +40,7 @@ type tmplCtx struct { EmitMethodsWithDBArgument bool EmitEnumValidMethod bool EmitAllEnumValues bool + EmitReadOnlyPrepared bool UsesCopyFrom bool UsesBatch bool OmitSqlcVersion bool @@ -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), @@ -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 { @@ -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 != "" { @@ -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 { @@ -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 +} diff --git a/internal/codegen/golang/imports.go b/internal/codegen/golang/imports.go index e1370e48eb..0b415374b6 100644 --- a/internal/codegen/golang/imports.go +++ b/internal/codegen/golang/imports.go @@ -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: @@ -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)) } @@ -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 @@ -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 @@ -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() { @@ -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{}{} } diff --git a/internal/codegen/golang/opts/options.go b/internal/codegen/golang/opts/options.go index 64ceb13796..a7d121f344 100644 --- a/internal/codegen/golang/opts/options.go +++ b/internal/codegen/golang/opts/options.go @@ -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"` diff --git a/internal/codegen/golang/templates/stdlib/readonlyCode.tmpl b/internal/codegen/golang/templates/stdlib/readonlyCode.tmpl new file mode 100644 index 0000000000..a89cbc99ee --- /dev/null +++ b/internal/codegen/golang/templates/stdlib/readonlyCode.tmpl @@ -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}} diff --git a/internal/codegen/golang/templates/template.tmpl b/internal/codegen/golang/templates/template.tmpl index bb54f25042..ba14fa5f2a 100644 --- a/internal/codegen/golang/templates/template.tmpl +++ b/internal/codegen/golang/templates/template.tmpl @@ -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}} diff --git a/internal/config/v_one.go b/internal/config/v_one.go index fa1a4d8d28..7c29a07bf4 100644 --- a/internal/config/v_one.go +++ b/internal/config/v_one.go @@ -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"` @@ -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, diff --git a/internal/config/v_one.json b/internal/config/v_one.json index 20435d1e1c..cda8d7cf96 100644 --- a/internal/config/v_one.json +++ b/internal/config/v_one.json @@ -137,6 +137,9 @@ "emit_sql_as_comment": { "type": "boolean" }, + "emit_read_only_prepared": { + "type": "boolean" + }, "build_tags": { "type": "string" }, diff --git a/internal/config/v_two.json b/internal/config/v_two.json index 3728ece520..a8d04ae6e1 100644 --- a/internal/config/v_two.json +++ b/internal/config/v_two.json @@ -146,6 +146,9 @@ "emit_sql_as_comment": { "type": "boolean" }, + "emit_read_only_prepared": { + "type": "boolean" + }, "build_tags": { "type": "string" }, diff --git a/internal/endtoend/testdata/read_write_segregation/mysql/go/db.go b/internal/endtoend/testdata/read_write_segregation/mysql/go/db.go new file mode 100644 index 0000000000..9c72c93be7 --- /dev/null +++ b/internal/endtoend/testdata/read_write_segregation/mysql/go/db.go @@ -0,0 +1,128 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 + +package querytest + +import ( + "context" + "database/sql" + "fmt" +) + +type DBTX interface { + ExecContext(context.Context, string, ...any) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...any) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...any) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +func Prepare(ctx context.Context, db DBTX) (*Queries, error) { + q := Queries{db: db} + var err error + if q.deleteUsersByNameStmt, err = db.PrepareContext(ctx, deleteUsersByName); err != nil { + return nil, fmt.Errorf("error preparing query DeleteUsersByName: %w", err) + } + if q.getUserByIDStmt, err = db.PrepareContext(ctx, getUserByID); err != nil { + return nil, fmt.Errorf("error preparing query GetUserByID: %w", err) + } + if q.insertNewUserStmt, err = db.PrepareContext(ctx, insertNewUser); err != nil { + return nil, fmt.Errorf("error preparing query InsertNewUser: %w", err) + } + if q.insertNewUserWithResultStmt, err = db.PrepareContext(ctx, insertNewUserWithResult); err != nil { + return nil, fmt.Errorf("error preparing query InsertNewUserWithResult: %w", err) + } + if q.listUsersStmt, err = db.PrepareContext(ctx, listUsers); err != nil { + return nil, fmt.Errorf("error preparing query ListUsers: %w", err) + } + return &q, nil +} + +func (q *Queries) Close() error { + var err error + if q.deleteUsersByNameStmt != nil { + if cerr := q.deleteUsersByNameStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing deleteUsersByNameStmt: %w", cerr) + } + } + if q.getUserByIDStmt != nil { + if cerr := q.getUserByIDStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing getUserByIDStmt: %w", cerr) + } + } + if q.insertNewUserStmt != nil { + if cerr := q.insertNewUserStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing insertNewUserStmt: %w", cerr) + } + } + if q.insertNewUserWithResultStmt != nil { + if cerr := q.insertNewUserWithResultStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing insertNewUserWithResultStmt: %w", cerr) + } + } + if q.listUsersStmt != nil { + if cerr := q.listUsersStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing listUsersStmt: %w", cerr) + } + } + return err +} + +func (q *Queries) exec(ctx context.Context, stmt *sql.Stmt, query string, args ...any) (sql.Result, error) { + switch { + case stmt != nil && q.tx != nil: + return q.tx.StmtContext(ctx, stmt).ExecContext(ctx, args...) + case stmt != nil: + return stmt.ExecContext(ctx, args...) + default: + return q.db.ExecContext(ctx, query, args...) + } +} + +func (q *Queries) query(ctx context.Context, stmt *sql.Stmt, query string, args ...any) (*sql.Rows, error) { + switch { + case stmt != nil && q.tx != nil: + return q.tx.StmtContext(ctx, stmt).QueryContext(ctx, args...) + case stmt != nil: + return stmt.QueryContext(ctx, args...) + default: + return q.db.QueryContext(ctx, query, args...) + } +} + +func (q *Queries) queryRow(ctx context.Context, stmt *sql.Stmt, query string, args ...any) *sql.Row { + switch { + case stmt != nil && q.tx != nil: + return q.tx.StmtContext(ctx, stmt).QueryRowContext(ctx, args...) + case stmt != nil: + return stmt.QueryRowContext(ctx, args...) + default: + return q.db.QueryRowContext(ctx, query, args...) + } +} + +type Queries struct { + db DBTX + tx *sql.Tx + deleteUsersByNameStmt *sql.Stmt + getUserByIDStmt *sql.Stmt + insertNewUserStmt *sql.Stmt + insertNewUserWithResultStmt *sql.Stmt + listUsersStmt *sql.Stmt +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + tx: tx, + deleteUsersByNameStmt: q.deleteUsersByNameStmt, + getUserByIDStmt: q.getUserByIDStmt, + insertNewUserStmt: q.insertNewUserStmt, + insertNewUserWithResultStmt: q.insertNewUserWithResultStmt, + listUsersStmt: q.listUsersStmt, + } +} diff --git a/internal/endtoend/testdata/read_write_segregation/mysql/go/models.go b/internal/endtoend/testdata/read_write_segregation/mysql/go/models.go new file mode 100644 index 0000000000..cc8422a0b8 --- /dev/null +++ b/internal/endtoend/testdata/read_write_segregation/mysql/go/models.go @@ -0,0 +1,16 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 + +package querytest + +import ( + "database/sql" +) + +type User struct { + ID uint64 + FirstName string + LastName sql.NullString + UpdatedAt sql.NullTime +} diff --git a/internal/endtoend/testdata/read_write_segregation/mysql/go/query.sql.go b/internal/endtoend/testdata/read_write_segregation/mysql/go/query.sql.go new file mode 100644 index 0000000000..7d877ce0d7 --- /dev/null +++ b/internal/endtoend/testdata/read_write_segregation/mysql/go/query.sql.go @@ -0,0 +1,110 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 +// source: query.sql + +package querytest + +import ( + "context" + "database/sql" +) + +const deleteUsersByName = `-- name: DeleteUsersByName :execrows +DELETE FROM users WHERE first_name = ? AND last_name = ? +` + +type DeleteUsersByNameParams struct { + FirstName string + LastName sql.NullString +} + +func (q *Queries) DeleteUsersByName(ctx context.Context, arg DeleteUsersByNameParams) (int64, error) { + result, err := q.exec(ctx, q.deleteUsersByNameStmt, deleteUsersByName, arg.FirstName, arg.LastName) + if err != nil { + return 0, err + } + return result.RowsAffected() +} + +const getUserByID = `-- name: GetUserByID :one +SELECT first_name, id, last_name, updated_at FROM users WHERE id = ? +` + +type GetUserByIDRow struct { + FirstName string + ID uint64 + LastName sql.NullString + UpdatedAt sql.NullTime +} + +func (q *Queries) GetUserByID(ctx context.Context, targetID uint64) (GetUserByIDRow, error) { + row := q.queryRow(ctx, q.getUserByIDStmt, getUserByID, targetID) + var i GetUserByIDRow + err := row.Scan( + &i.FirstName, + &i.ID, + &i.LastName, + &i.UpdatedAt, + ) + return i, err +} + +const insertNewUser = `-- name: InsertNewUser :exec +INSERT INTO users (first_name, last_name) VALUES (?, ?) +` + +type InsertNewUserParams struct { + FirstName string + LastName sql.NullString +} + +func (q *Queries) InsertNewUser(ctx context.Context, arg InsertNewUserParams) error { + _, err := q.exec(ctx, q.insertNewUserStmt, insertNewUser, arg.FirstName, arg.LastName) + return err +} + +const insertNewUserWithResult = `-- name: InsertNewUserWithResult :execresult +INSERT INTO users (first_name, last_name) VALUES (?, ?) +` + +type InsertNewUserWithResultParams struct { + FirstName string + LastName sql.NullString +} + +func (q *Queries) InsertNewUserWithResult(ctx context.Context, arg InsertNewUserWithResultParams) (sql.Result, error) { + return q.exec(ctx, q.insertNewUserWithResultStmt, insertNewUserWithResult, arg.FirstName, arg.LastName) +} + +const listUsers = `-- name: ListUsers :many +SELECT first_name, last_name FROM users +` + +type ListUsersRow struct { + FirstName string + LastName sql.NullString +} + +func (q *Queries) ListUsers(ctx context.Context) ([]ListUsersRow, error) { + rows, err := q.query(ctx, q.listUsersStmt, listUsers) + if err != nil { + return nil, err + } + defer rows.Close() + var items []ListUsersRow + for rows.Next() { + var i ListUsersRow + if err := rows.Scan(&i.FirstName, &i.LastName); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/internal/endtoend/testdata/read_write_segregation/mysql/go/read.go b/internal/endtoend/testdata/read_write_segregation/mysql/go/read.go new file mode 100644 index 0000000000..55475d464d --- /dev/null +++ b/internal/endtoend/testdata/read_write_segregation/mysql/go/read.go @@ -0,0 +1,97 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 + +package querytest + +import ( + "context" + "database/sql" + "fmt" +) + +func PrepareReadOnly(ctx context.Context, db DBTX) (*ReadQueries, error) { + q := ReadQueries{db: db} + var err error + if q.getUserByIDStmt, err = db.PrepareContext(ctx, getUserByID); err != nil { + return nil, fmt.Errorf("error preparing query GetUserByID: %w", err) + } + if q.listUsersStmt, err = db.PrepareContext(ctx, listUsers); err != nil { + return nil, fmt.Errorf("error preparing query ListUsers: %w", err) + } + return &q, nil +} + +type ReadQueries struct { + db DBTX + getUserByIDStmt *sql.Stmt + listUsersStmt *sql.Stmt +} + +func (q *ReadQueries) Close() error { + var err error + if q.getUserByIDStmt != nil { + if cerr := q.getUserByIDStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing getUserByIDStmt: %w", cerr) + } + } + if q.listUsersStmt != nil { + if cerr := q.listUsersStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing listUsersStmt: %w", cerr) + } + } + 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...) + } +} + +func (q *ReadQueries) GetUserByID(ctx context.Context, targetID uint64) (GetUserByIDRow, error) { + row := q.queryRow(ctx, q.getUserByIDStmt, getUserByID, targetID) + var i GetUserByIDRow + err := row.Scan( + &i.FirstName, + &i.ID, + &i.LastName, + &i.UpdatedAt, + ) + return i, err +} + +func (q *ReadQueries) ListUsers(ctx context.Context) ([]ListUsersRow, error) { + rows, err := q.query(ctx, q.listUsersStmt, listUsers) + if err != nil { + return nil, err + } + defer rows.Close() + var items []ListUsersRow + for rows.Next() { + var i ListUsersRow + if err := rows.Scan(&i.FirstName, &i.LastName); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/internal/endtoend/testdata/read_write_segregation/mysql/query.sql b/internal/endtoend/testdata/read_write_segregation/mysql/query.sql new file mode 100644 index 0000000000..edae3f312e --- /dev/null +++ b/internal/endtoend/testdata/read_write_segregation/mysql/query.sql @@ -0,0 +1,14 @@ +/* name: GetUserByID :one */ +SELECT first_name, id, last_name, updated_at FROM users WHERE id = sqlc.arg('target_id'); + +/* name: ListUsers :many */ +SELECT first_name, last_name FROM users; + +/* name: InsertNewUser :exec */ +INSERT INTO users (first_name, last_name) VALUES (?, ?); + +/* name: InsertNewUserWithResult :execresult */ +INSERT INTO users (first_name, last_name) VALUES (?, ?); + +/* name: DeleteUsersByName :execrows */ +DELETE FROM users WHERE first_name = ? AND last_name = ?; diff --git a/internal/endtoend/testdata/read_write_segregation/mysql/schema.sql b/internal/endtoend/testdata/read_write_segregation/mysql/schema.sql new file mode 100644 index 0000000000..93fc9cd731 --- /dev/null +++ b/internal/endtoend/testdata/read_write_segregation/mysql/schema.sql @@ -0,0 +1,6 @@ +CREATE TABLE users ( + id SERIAL NOT NULL, + first_name varchar(255) NOT NULL, + last_name varchar(255), + updated_at timestamp NULL DEFAULT CURRENT_TIMESTAMP +); diff --git a/internal/endtoend/testdata/read_write_segregation/mysql/sqlc.json b/internal/endtoend/testdata/read_write_segregation/mysql/sqlc.json new file mode 100644 index 0000000000..b7eede7281 --- /dev/null +++ b/internal/endtoend/testdata/read_write_segregation/mysql/sqlc.json @@ -0,0 +1,14 @@ +{ + "version": "1", + "packages": [ + { + "name": "querytest", + "path": "go", + "schema": "schema.sql", + "queries": "query.sql", + "engine": "mysql", + "emit_prepared_queries": true, + "emit_read_only_prepared": true + } + ] +} diff --git a/internal/endtoend/testdata/read_write_segregation/postgresql/stdlib/go/db.go b/internal/endtoend/testdata/read_write_segregation/postgresql/stdlib/go/db.go new file mode 100644 index 0000000000..9c72c93be7 --- /dev/null +++ b/internal/endtoend/testdata/read_write_segregation/postgresql/stdlib/go/db.go @@ -0,0 +1,128 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 + +package querytest + +import ( + "context" + "database/sql" + "fmt" +) + +type DBTX interface { + ExecContext(context.Context, string, ...any) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...any) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...any) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +func Prepare(ctx context.Context, db DBTX) (*Queries, error) { + q := Queries{db: db} + var err error + if q.deleteUsersByNameStmt, err = db.PrepareContext(ctx, deleteUsersByName); err != nil { + return nil, fmt.Errorf("error preparing query DeleteUsersByName: %w", err) + } + if q.getUserByIDStmt, err = db.PrepareContext(ctx, getUserByID); err != nil { + return nil, fmt.Errorf("error preparing query GetUserByID: %w", err) + } + if q.insertNewUserStmt, err = db.PrepareContext(ctx, insertNewUser); err != nil { + return nil, fmt.Errorf("error preparing query InsertNewUser: %w", err) + } + if q.insertNewUserWithResultStmt, err = db.PrepareContext(ctx, insertNewUserWithResult); err != nil { + return nil, fmt.Errorf("error preparing query InsertNewUserWithResult: %w", err) + } + if q.listUsersStmt, err = db.PrepareContext(ctx, listUsers); err != nil { + return nil, fmt.Errorf("error preparing query ListUsers: %w", err) + } + return &q, nil +} + +func (q *Queries) Close() error { + var err error + if q.deleteUsersByNameStmt != nil { + if cerr := q.deleteUsersByNameStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing deleteUsersByNameStmt: %w", cerr) + } + } + if q.getUserByIDStmt != nil { + if cerr := q.getUserByIDStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing getUserByIDStmt: %w", cerr) + } + } + if q.insertNewUserStmt != nil { + if cerr := q.insertNewUserStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing insertNewUserStmt: %w", cerr) + } + } + if q.insertNewUserWithResultStmt != nil { + if cerr := q.insertNewUserWithResultStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing insertNewUserWithResultStmt: %w", cerr) + } + } + if q.listUsersStmt != nil { + if cerr := q.listUsersStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing listUsersStmt: %w", cerr) + } + } + return err +} + +func (q *Queries) exec(ctx context.Context, stmt *sql.Stmt, query string, args ...any) (sql.Result, error) { + switch { + case stmt != nil && q.tx != nil: + return q.tx.StmtContext(ctx, stmt).ExecContext(ctx, args...) + case stmt != nil: + return stmt.ExecContext(ctx, args...) + default: + return q.db.ExecContext(ctx, query, args...) + } +} + +func (q *Queries) query(ctx context.Context, stmt *sql.Stmt, query string, args ...any) (*sql.Rows, error) { + switch { + case stmt != nil && q.tx != nil: + return q.tx.StmtContext(ctx, stmt).QueryContext(ctx, args...) + case stmt != nil: + return stmt.QueryContext(ctx, args...) + default: + return q.db.QueryContext(ctx, query, args...) + } +} + +func (q *Queries) queryRow(ctx context.Context, stmt *sql.Stmt, query string, args ...any) *sql.Row { + switch { + case stmt != nil && q.tx != nil: + return q.tx.StmtContext(ctx, stmt).QueryRowContext(ctx, args...) + case stmt != nil: + return stmt.QueryRowContext(ctx, args...) + default: + return q.db.QueryRowContext(ctx, query, args...) + } +} + +type Queries struct { + db DBTX + tx *sql.Tx + deleteUsersByNameStmt *sql.Stmt + getUserByIDStmt *sql.Stmt + insertNewUserStmt *sql.Stmt + insertNewUserWithResultStmt *sql.Stmt + listUsersStmt *sql.Stmt +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + tx: tx, + deleteUsersByNameStmt: q.deleteUsersByNameStmt, + getUserByIDStmt: q.getUserByIDStmt, + insertNewUserStmt: q.insertNewUserStmt, + insertNewUserWithResultStmt: q.insertNewUserWithResultStmt, + listUsersStmt: q.listUsersStmt, + } +} diff --git a/internal/endtoend/testdata/read_write_segregation/postgresql/stdlib/go/models.go b/internal/endtoend/testdata/read_write_segregation/postgresql/stdlib/go/models.go new file mode 100644 index 0000000000..1882d78f64 --- /dev/null +++ b/internal/endtoend/testdata/read_write_segregation/postgresql/stdlib/go/models.go @@ -0,0 +1,16 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 + +package querytest + +import ( + "database/sql" +) + +type User struct { + ID int32 + FirstName string + LastName sql.NullString + UpdatedAt sql.NullTime +} diff --git a/internal/endtoend/testdata/read_write_segregation/postgresql/stdlib/go/query.sql.go b/internal/endtoend/testdata/read_write_segregation/postgresql/stdlib/go/query.sql.go new file mode 100644 index 0000000000..9033eefd8a --- /dev/null +++ b/internal/endtoend/testdata/read_write_segregation/postgresql/stdlib/go/query.sql.go @@ -0,0 +1,110 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 +// source: query.sql + +package querytest + +import ( + "context" + "database/sql" +) + +const deleteUsersByName = `-- name: DeleteUsersByName :execrows +DELETE FROM users WHERE first_name = $1 AND last_name = $2 +` + +type DeleteUsersByNameParams struct { + FirstName string + LastName sql.NullString +} + +func (q *Queries) DeleteUsersByName(ctx context.Context, arg DeleteUsersByNameParams) (int64, error) { + result, err := q.exec(ctx, q.deleteUsersByNameStmt, deleteUsersByName, arg.FirstName, arg.LastName) + if err != nil { + return 0, err + } + return result.RowsAffected() +} + +const getUserByID = `-- name: GetUserByID :one +SELECT first_name, id, last_name, updated_at FROM users WHERE id = $1 +` + +type GetUserByIDRow struct { + FirstName string + ID int32 + LastName sql.NullString + UpdatedAt sql.NullTime +} + +func (q *Queries) GetUserByID(ctx context.Context, targetID int32) (GetUserByIDRow, error) { + row := q.queryRow(ctx, q.getUserByIDStmt, getUserByID, targetID) + var i GetUserByIDRow + err := row.Scan( + &i.FirstName, + &i.ID, + &i.LastName, + &i.UpdatedAt, + ) + return i, err +} + +const insertNewUser = `-- name: InsertNewUser :exec +INSERT INTO users (first_name, last_name) VALUES ($1, $2) +` + +type InsertNewUserParams struct { + FirstName string + LastName sql.NullString +} + +func (q *Queries) InsertNewUser(ctx context.Context, arg InsertNewUserParams) error { + _, err := q.exec(ctx, q.insertNewUserStmt, insertNewUser, arg.FirstName, arg.LastName) + return err +} + +const insertNewUserWithResult = `-- name: InsertNewUserWithResult :execresult +INSERT INTO users (first_name, last_name) VALUES ($1, $2) +` + +type InsertNewUserWithResultParams struct { + FirstName string + LastName sql.NullString +} + +func (q *Queries) InsertNewUserWithResult(ctx context.Context, arg InsertNewUserWithResultParams) (sql.Result, error) { + return q.exec(ctx, q.insertNewUserWithResultStmt, insertNewUserWithResult, arg.FirstName, arg.LastName) +} + +const listUsers = `-- name: ListUsers :many +SELECT first_name, last_name FROM users +` + +type ListUsersRow struct { + FirstName string + LastName sql.NullString +} + +func (q *Queries) ListUsers(ctx context.Context) ([]ListUsersRow, error) { + rows, err := q.query(ctx, q.listUsersStmt, listUsers) + if err != nil { + return nil, err + } + defer rows.Close() + var items []ListUsersRow + for rows.Next() { + var i ListUsersRow + if err := rows.Scan(&i.FirstName, &i.LastName); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/internal/endtoend/testdata/read_write_segregation/postgresql/stdlib/go/read.go b/internal/endtoend/testdata/read_write_segregation/postgresql/stdlib/go/read.go new file mode 100644 index 0000000000..442eebfdfb --- /dev/null +++ b/internal/endtoend/testdata/read_write_segregation/postgresql/stdlib/go/read.go @@ -0,0 +1,97 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 + +package querytest + +import ( + "context" + "database/sql" + "fmt" +) + +func PrepareReadOnly(ctx context.Context, db DBTX) (*ReadQueries, error) { + q := ReadQueries{db: db} + var err error + if q.getUserByIDStmt, err = db.PrepareContext(ctx, getUserByID); err != nil { + return nil, fmt.Errorf("error preparing query GetUserByID: %w", err) + } + if q.listUsersStmt, err = db.PrepareContext(ctx, listUsers); err != nil { + return nil, fmt.Errorf("error preparing query ListUsers: %w", err) + } + return &q, nil +} + +type ReadQueries struct { + db DBTX + getUserByIDStmt *sql.Stmt + listUsersStmt *sql.Stmt +} + +func (q *ReadQueries) Close() error { + var err error + if q.getUserByIDStmt != nil { + if cerr := q.getUserByIDStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing getUserByIDStmt: %w", cerr) + } + } + if q.listUsersStmt != nil { + if cerr := q.listUsersStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing listUsersStmt: %w", cerr) + } + } + 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...) + } +} + +func (q *ReadQueries) GetUserByID(ctx context.Context, targetID int32) (GetUserByIDRow, error) { + row := q.queryRow(ctx, q.getUserByIDStmt, getUserByID, targetID) + var i GetUserByIDRow + err := row.Scan( + &i.FirstName, + &i.ID, + &i.LastName, + &i.UpdatedAt, + ) + return i, err +} + +func (q *ReadQueries) ListUsers(ctx context.Context) ([]ListUsersRow, error) { + rows, err := q.query(ctx, q.listUsersStmt, listUsers) + if err != nil { + return nil, err + } + defer rows.Close() + var items []ListUsersRow + for rows.Next() { + var i ListUsersRow + if err := rows.Scan(&i.FirstName, &i.LastName); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/internal/endtoend/testdata/read_write_segregation/postgresql/stdlib/query.sql b/internal/endtoend/testdata/read_write_segregation/postgresql/stdlib/query.sql new file mode 100644 index 0000000000..5d08af0abb --- /dev/null +++ b/internal/endtoend/testdata/read_write_segregation/postgresql/stdlib/query.sql @@ -0,0 +1,14 @@ +/* name: GetUserByID :one */ +SELECT first_name, id, last_name, updated_at FROM users WHERE id = sqlc.arg('target_id'); + +/* name: ListUsers :many */ +SELECT first_name, last_name FROM users; + +/* name: InsertNewUser :exec */ +INSERT INTO users (first_name, last_name) VALUES ($1, $2); + +/* name: InsertNewUserWithResult :execresult */ +INSERT INTO users (first_name, last_name) VALUES ($1, $2); + +/* name: DeleteUsersByName :execrows */ +DELETE FROM users WHERE first_name = $1 AND last_name = $2; diff --git a/internal/endtoend/testdata/read_write_segregation/postgresql/stdlib/schema.sql b/internal/endtoend/testdata/read_write_segregation/postgresql/stdlib/schema.sql new file mode 100644 index 0000000000..2de2a8c1ec --- /dev/null +++ b/internal/endtoend/testdata/read_write_segregation/postgresql/stdlib/schema.sql @@ -0,0 +1,7 @@ +CREATE TABLE users ( + id SERIAL NOT NULL, + first_name varchar(255) NOT NULL, + last_name varchar(255), + updated_at timestamp NULL DEFAULT CURRENT_TIMESTAMP +); + diff --git a/internal/endtoend/testdata/read_write_segregation/postgresql/stdlib/sqlc.json b/internal/endtoend/testdata/read_write_segregation/postgresql/stdlib/sqlc.json new file mode 100644 index 0000000000..f8b74bfeae --- /dev/null +++ b/internal/endtoend/testdata/read_write_segregation/postgresql/stdlib/sqlc.json @@ -0,0 +1,14 @@ +{ + "version": "1", + "packages": [ + { + "name": "querytest", + "path": "go", + "schema": "schema.sql", + "queries": "query.sql", + "engine": "postgresql", + "emit_prepared_queries": true, + "emit_read_only_prepared": true + } + ] +} diff --git a/internal/endtoend/testdata/read_write_segregation_slices/postgresql/stdlib/go/db.go b/internal/endtoend/testdata/read_write_segregation_slices/postgresql/stdlib/go/db.go new file mode 100644 index 0000000000..dbd3d4a5f7 --- /dev/null +++ b/internal/endtoend/testdata/read_write_segregation_slices/postgresql/stdlib/go/db.go @@ -0,0 +1,108 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 + +package querytest + +import ( + "context" + "database/sql" + "fmt" +) + +type DBTX interface { + ExecContext(context.Context, string, ...any) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...any) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...any) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +func Prepare(ctx context.Context, db DBTX) (*Queries, error) { + q := Queries{db: db} + var err error + if q.getUserTagsStmt, err = db.PrepareContext(ctx, getUserTags); err != nil { + return nil, fmt.Errorf("error preparing query GetUserTags: %w", err) + } + if q.listUsersByIDsStmt, err = db.PrepareContext(ctx, listUsersByIDs); err != nil { + return nil, fmt.Errorf("error preparing query ListUsersByIDs: %w", err) + } + if q.setUserTagsStmt, err = db.PrepareContext(ctx, setUserTags); err != nil { + return nil, fmt.Errorf("error preparing query SetUserTags: %w", err) + } + return &q, nil +} + +func (q *Queries) Close() error { + var err error + if q.getUserTagsStmt != nil { + if cerr := q.getUserTagsStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing getUserTagsStmt: %w", cerr) + } + } + if q.listUsersByIDsStmt != nil { + if cerr := q.listUsersByIDsStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing listUsersByIDsStmt: %w", cerr) + } + } + if q.setUserTagsStmt != nil { + if cerr := q.setUserTagsStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing setUserTagsStmt: %w", cerr) + } + } + return err +} + +func (q *Queries) exec(ctx context.Context, stmt *sql.Stmt, query string, args ...any) (sql.Result, error) { + switch { + case stmt != nil && q.tx != nil: + return q.tx.StmtContext(ctx, stmt).ExecContext(ctx, args...) + case stmt != nil: + return stmt.ExecContext(ctx, args...) + default: + return q.db.ExecContext(ctx, query, args...) + } +} + +func (q *Queries) query(ctx context.Context, stmt *sql.Stmt, query string, args ...any) (*sql.Rows, error) { + switch { + case stmt != nil && q.tx != nil: + return q.tx.StmtContext(ctx, stmt).QueryContext(ctx, args...) + case stmt != nil: + return stmt.QueryContext(ctx, args...) + default: + return q.db.QueryContext(ctx, query, args...) + } +} + +func (q *Queries) queryRow(ctx context.Context, stmt *sql.Stmt, query string, args ...any) *sql.Row { + switch { + case stmt != nil && q.tx != nil: + return q.tx.StmtContext(ctx, stmt).QueryRowContext(ctx, args...) + case stmt != nil: + return stmt.QueryRowContext(ctx, args...) + default: + return q.db.QueryRowContext(ctx, query, args...) + } +} + +type Queries struct { + db DBTX + tx *sql.Tx + getUserTagsStmt *sql.Stmt + listUsersByIDsStmt *sql.Stmt + setUserTagsStmt *sql.Stmt +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + tx: tx, + getUserTagsStmt: q.getUserTagsStmt, + listUsersByIDsStmt: q.listUsersByIDsStmt, + setUserTagsStmt: q.setUserTagsStmt, + } +} diff --git a/internal/endtoend/testdata/read_write_segregation_slices/postgresql/stdlib/go/models.go b/internal/endtoend/testdata/read_write_segregation_slices/postgresql/stdlib/go/models.go new file mode 100644 index 0000000000..9fc3099c3a --- /dev/null +++ b/internal/endtoend/testdata/read_write_segregation_slices/postgresql/stdlib/go/models.go @@ -0,0 +1,11 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 + +package querytest + +type User struct { + ID int32 + FirstName string + Tags []string +} diff --git a/internal/endtoend/testdata/read_write_segregation_slices/postgresql/stdlib/go/query.sql.go b/internal/endtoend/testdata/read_write_segregation_slices/postgresql/stdlib/go/query.sql.go new file mode 100644 index 0000000000..b8d95bbe22 --- /dev/null +++ b/internal/endtoend/testdata/read_write_segregation_slices/postgresql/stdlib/go/query.sql.go @@ -0,0 +1,72 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 +// source: query.sql + +package querytest + +import ( + "context" + + "github.com/lib/pq" +) + +const getUserTags = `-- name: GetUserTags :one +SELECT tags FROM users WHERE id = $1 +` + +func (q *Queries) GetUserTags(ctx context.Context, id int32) ([]string, error) { + row := q.queryRow(ctx, q.getUserTagsStmt, getUserTags, id) + var tags []string + err := row.Scan(pq.Array(&tags)) + return tags, err +} + +const listUsersByIDs = `-- name: ListUsersByIDs :many +SELECT + id, first_name +FROM users +WHERE id = ANY($1::int[]) +` + +type ListUsersByIDsRow struct { + ID int32 + FirstName string +} + +func (q *Queries) ListUsersByIDs(ctx context.Context, dollar_1 []int32) ([]ListUsersByIDsRow, error) { + rows, err := q.query(ctx, q.listUsersByIDsStmt, listUsersByIDs, pq.Array(dollar_1)) + if err != nil { + return nil, err + } + defer rows.Close() + var items []ListUsersByIDsRow + for rows.Next() { + var i ListUsersByIDsRow + if err := rows.Scan(&i.ID, &i.FirstName); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const setUserTags = `-- name: SetUserTags :exec +UPDATE users SET tags = $1 WHERE id = $2 +` + +type SetUserTagsParams struct { + Tags []string + ID int32 +} + +func (q *Queries) SetUserTags(ctx context.Context, arg SetUserTagsParams) error { + _, err := q.exec(ctx, q.setUserTagsStmt, setUserTags, pq.Array(arg.Tags), arg.ID) + return err +} diff --git a/internal/endtoend/testdata/read_write_segregation_slices/postgresql/stdlib/go/read.go b/internal/endtoend/testdata/read_write_segregation_slices/postgresql/stdlib/go/read.go new file mode 100644 index 0000000000..c15177fb02 --- /dev/null +++ b/internal/endtoend/testdata/read_write_segregation_slices/postgresql/stdlib/go/read.go @@ -0,0 +1,94 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 + +package querytest + +import ( + "context" + "database/sql" + "fmt" + + "github.com/lib/pq" +) + +func PrepareReadOnly(ctx context.Context, db DBTX) (*ReadQueries, error) { + q := ReadQueries{db: db} + var err error + if q.getUserTagsStmt, err = db.PrepareContext(ctx, getUserTags); err != nil { + return nil, fmt.Errorf("error preparing query GetUserTags: %w", err) + } + if q.listUsersByIDsStmt, err = db.PrepareContext(ctx, listUsersByIDs); err != nil { + return nil, fmt.Errorf("error preparing query ListUsersByIDs: %w", err) + } + return &q, nil +} + +type ReadQueries struct { + db DBTX + getUserTagsStmt *sql.Stmt + listUsersByIDsStmt *sql.Stmt +} + +func (q *ReadQueries) Close() error { + var err error + if q.getUserTagsStmt != nil { + if cerr := q.getUserTagsStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing getUserTagsStmt: %w", cerr) + } + } + if q.listUsersByIDsStmt != nil { + if cerr := q.listUsersByIDsStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing listUsersByIDsStmt: %w", cerr) + } + } + 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...) + } +} + +func (q *ReadQueries) GetUserTags(ctx context.Context, id int32) ([]string, error) { + row := q.queryRow(ctx, q.getUserTagsStmt, getUserTags, id) + var tags []string + err := row.Scan(pq.Array(&tags)) + return tags, err +} + +func (q *ReadQueries) ListUsersByIDs(ctx context.Context, dollar_1 []int32) ([]ListUsersByIDsRow, error) { + rows, err := q.query(ctx, q.listUsersByIDsStmt, listUsersByIDs, pq.Array(dollar_1)) + if err != nil { + return nil, err + } + defer rows.Close() + var items []ListUsersByIDsRow + for rows.Next() { + var i ListUsersByIDsRow + if err := rows.Scan(&i.ID, &i.FirstName); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/internal/endtoend/testdata/read_write_segregation_slices/postgresql/stdlib/query.sql b/internal/endtoend/testdata/read_write_segregation_slices/postgresql/stdlib/query.sql new file mode 100644 index 0000000000..6812d96262 --- /dev/null +++ b/internal/endtoend/testdata/read_write_segregation_slices/postgresql/stdlib/query.sql @@ -0,0 +1,11 @@ +/* name: ListUsersByIDs :many */ +SELECT + id, first_name +FROM users +WHERE id = ANY($1::int[]); + +/* name: GetUserTags :one */ +SELECT tags FROM users WHERE id = $1; + +/* name: SetUserTags :exec */ +UPDATE users SET tags = $1 WHERE id = $2; diff --git a/internal/endtoend/testdata/read_write_segregation_slices/postgresql/stdlib/schema.sql b/internal/endtoend/testdata/read_write_segregation_slices/postgresql/stdlib/schema.sql new file mode 100644 index 0000000000..ca3fcf7a73 --- /dev/null +++ b/internal/endtoend/testdata/read_write_segregation_slices/postgresql/stdlib/schema.sql @@ -0,0 +1,5 @@ +CREATE TABLE users ( + id SERIAL NOT NULL, + first_name varchar(255) NOT NULL, + tags text[] NOT NULL +); diff --git a/internal/endtoend/testdata/read_write_segregation_slices/postgresql/stdlib/sqlc.json b/internal/endtoend/testdata/read_write_segregation_slices/postgresql/stdlib/sqlc.json new file mode 100644 index 0000000000..f8b74bfeae --- /dev/null +++ b/internal/endtoend/testdata/read_write_segregation_slices/postgresql/stdlib/sqlc.json @@ -0,0 +1,14 @@ +{ + "version": "1", + "packages": [ + { + "name": "querytest", + "path": "go", + "schema": "schema.sql", + "queries": "query.sql", + "engine": "postgresql", + "emit_prepared_queries": true, + "emit_read_only_prepared": true + } + ] +}