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 + } + ] +}