From d373474d1ca80f32714bb34f6653dfe413f4edbf Mon Sep 17 00:00:00 2001 From: Jarri Abidi Date: Mon, 25 Mar 2024 16:14:59 +0500 Subject: [PATCH 1/4] Generate read-only Queries --- internal/codegen/golang/gen.go | 44 +++++- internal/codegen/golang/imports.go | 3 + 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 + .../read_write_segregation/mysql/go/db.go | 128 ++++++++++++++++++ .../read_write_segregation/mysql/go/models.go | 15 ++ .../mysql/go/query.sql.go | 104 ++++++++++++++ .../read_write_segregation/mysql/go/read.go | 92 +++++++++++++ .../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 | 15 ++ .../postgresql/stdlib/go/query.sql.go | 104 ++++++++++++++ .../postgresql/stdlib/go/read.go | 92 +++++++++++++ .../postgresql/stdlib/query.sql | 14 ++ .../postgresql/stdlib/schema.sql | 6 + .../postgresql/stdlib/sqlc.json | 14 ++ 20 files changed, 924 insertions(+), 7 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 diff --git a/internal/codegen/golang/gen.go b/internal/codegen/golang/gen.go index 5b7977f500..796eac9862 100644 --- a/internal/codegen/golang/gen.go +++ b/internal/codegen/golang/gen.go @@ -17,13 +17,14 @@ import ( ) type tmplCtx struct { - Q string - Package string - SQLDriver opts.SQLDriver - Enums []Enum - Structs []Struct - GoQueries []Query - SqlcVersion string + Q string + Package string + SQLDriver opts.SQLDriver + Enums []Enum + Structs []Struct + GoQueries []Query + GoReadQueries []Query + SqlcVersion string // TODO: Race conditions SourceName string @@ -37,6 +38,7 @@ type tmplCtx struct { EmitMethodsWithDBArgument bool EmitEnumValidMethod bool EmitAllEnumValues bool + EmitReadOnlyPrepared bool UsesCopyFrom bool UsesBatch bool OmitSqlcVersion bool @@ -177,6 +179,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), @@ -240,6 +243,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 { @@ -278,6 +282,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 != "" { @@ -305,6 +310,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 { @@ -405,3 +415,23 @@ func filterUnusedStructs(enums []Enum, structs []Struct, queries []Query) ([]Enu return keepEnums, keepStructs } + +func readOnly(queries []Query) []Query { + var rq []Query + for _, q := range queries { + if !q.hasRetType() { + continue + } + + qsql := strings.ToUpper(q.SQL) + if !strings.Contains(qsql, "SELECT") { + continue + } + if strings.Contains(qsql, "UPDATE") || strings.Contains(qsql, "INSERT") { + continue + } + + rq = append(rq, q) + } + return rq +} diff --git a/internal/codegen/golang/imports.go b/internal/codegen/golang/imports.go index 9e7819e4b1..6b97f5a504 100644 --- a/internal/codegen/golang/imports.go +++ b/internal/codegen/golang/imports.go @@ -101,6 +101,7 @@ func (i *importer) Imports(filename string) [][]ImportSpec { if i.Options.OutputBatchFileName != "" { batchFileName = i.Options.OutputBatchFileName } + readQueriesFileName := "read.go" switch filename { case dbFileName: @@ -113,6 +114,8 @@ func (i *importer) Imports(filename string) [][]ImportSpec { return mergeImports(i.copyfromImports()) case batchFileName: return mergeImports(i.batchImports()) + case readQueriesFileName: + return mergeImports(i.dbImports()) default: return mergeImports(i.queryImports(filename)) } diff --git a/internal/codegen/golang/opts/options.go b/internal/codegen/golang/opts/options.go index 0e2a8562e5..1fbc0dcde7 100644 --- a/internal/codegen/golang/opts/options.go +++ b/internal/codegen/golang/opts/options.go @@ -25,6 +25,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 afd50c01ac..c8f4cbcf7c 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 8efa9f42fc..aba3872315 100644 --- a/internal/config/v_one.go +++ b/internal/config/v_one.go @@ -42,6 +42,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"` @@ -152,6 +153,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/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..e8d00eccc0 --- /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.25.0 + +package querytest + +import ( + "context" + "database/sql" + "fmt" +) + +type DBTX interface { + ExecContext(context.Context, string, ...interface{}) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...interface{}) *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 ...interface{}) (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 ...interface{}) (*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 ...interface{}) *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..865f514a2b --- /dev/null +++ b/internal/endtoend/testdata/read_write_segregation/mysql/go/models.go @@ -0,0 +1,15 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.25.0 + +package querytest + +import ( + "database/sql" +) + +type User struct { + ID uint64 + FirstName string + LastName sql.NullString +} 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..0ff574b2b4 --- /dev/null +++ b/internal/endtoend/testdata/read_write_segregation/mysql/go/query.sql.go @@ -0,0 +1,104 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.25.0 +// 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 FROM users WHERE id = ? +` + +type GetUserByIDRow struct { + FirstName string + ID uint64 + LastName sql.NullString +} + +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) + 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..fd583209b5 --- /dev/null +++ b/internal/endtoend/testdata/read_write_segregation/mysql/go/read.go @@ -0,0 +1,92 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.25.0 + +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) + 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..b743aee060 --- /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 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..c928f3b7ce --- /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) +); + 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..e8d00eccc0 --- /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.25.0 + +package querytest + +import ( + "context" + "database/sql" + "fmt" +) + +type DBTX interface { + ExecContext(context.Context, string, ...interface{}) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...interface{}) *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 ...interface{}) (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 ...interface{}) (*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 ...interface{}) *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..e116f7585a --- /dev/null +++ b/internal/endtoend/testdata/read_write_segregation/postgresql/stdlib/go/models.go @@ -0,0 +1,15 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.25.0 + +package querytest + +import ( + "database/sql" +) + +type User struct { + ID int32 + FirstName string + LastName sql.NullString +} 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..31425dd01d --- /dev/null +++ b/internal/endtoend/testdata/read_write_segregation/postgresql/stdlib/go/query.sql.go @@ -0,0 +1,104 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.25.0 +// 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 FROM users WHERE id = $1 +` + +type GetUserByIDRow struct { + FirstName string + ID int32 + LastName sql.NullString +} + +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) + 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..6c8075b88c --- /dev/null +++ b/internal/endtoend/testdata/read_write_segregation/postgresql/stdlib/go/read.go @@ -0,0 +1,92 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.25.0 + +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) + 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..d2f54f2037 --- /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 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..c928f3b7ce --- /dev/null +++ b/internal/endtoend/testdata/read_write_segregation/postgresql/stdlib/schema.sql @@ -0,0 +1,6 @@ +CREATE TABLE users ( + id SERIAL NOT NULL, + first_name varchar(255) NOT NULL, + last_name varchar(255) +); + 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 + } + ] +} From d4666b690d4ba05e23b1b36d12a19ed4b2f39045 Mon Sep 17 00:00:00 2001 From: Jarri Abidi Date: Mon, 25 Mar 2024 16:37:20 +0500 Subject: [PATCH 2/4] Handle DELETE --- internal/codegen/golang/gen.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/internal/codegen/golang/gen.go b/internal/codegen/golang/gen.go index 796eac9862..c3cdcaf7cc 100644 --- a/internal/codegen/golang/gen.go +++ b/internal/codegen/golang/gen.go @@ -427,7 +427,9 @@ func readOnly(queries []Query) []Query { if !strings.Contains(qsql, "SELECT") { continue } - if strings.Contains(qsql, "UPDATE") || strings.Contains(qsql, "INSERT") { + if strings.Contains(qsql, "INSERT") || + strings.Contains(qsql, "UPDATE") || + strings.Contains(qsql, "DELETE") { continue } From 771c0441f0c3f08d51339c5cdf185486aface491 Mon Sep 17 00:00:00 2001 From: Jarri Abidi Date: Tue, 26 Mar 2024 12:05:06 +0500 Subject: [PATCH 3/4] Fix bug where column called updated_at was being skipped in readonly queries --- internal/codegen/golang/gen.go | 8 ++++---- .../testdata/read_write_segregation/mysql/go/models.go | 1 + .../read_write_segregation/mysql/go/query.sql.go | 10 ++++++++-- .../testdata/read_write_segregation/mysql/go/read.go | 7 ++++++- .../testdata/read_write_segregation/mysql/query.sql | 2 +- .../testdata/read_write_segregation/mysql/schema.sql | 4 ++-- .../postgresql/stdlib/go/models.go | 1 + .../postgresql/stdlib/go/query.sql.go | 10 ++++++++-- .../postgresql/stdlib/go/read.go | 7 ++++++- .../read_write_segregation/postgresql/stdlib/query.sql | 2 +- .../postgresql/stdlib/schema.sql | 3 ++- 11 files changed, 40 insertions(+), 15 deletions(-) diff --git a/internal/codegen/golang/gen.go b/internal/codegen/golang/gen.go index c3cdcaf7cc..f0b6b85afc 100644 --- a/internal/codegen/golang/gen.go +++ b/internal/codegen/golang/gen.go @@ -424,12 +424,12 @@ func readOnly(queries []Query) []Query { } qsql := strings.ToUpper(q.SQL) - if !strings.Contains(qsql, "SELECT") { + if !strings.Contains(qsql, "SELECT ") { continue } - if strings.Contains(qsql, "INSERT") || - strings.Contains(qsql, "UPDATE") || - strings.Contains(qsql, "DELETE") { + if strings.Contains(qsql, "INSERT ") || + strings.Contains(qsql, "UPDATE ") || + strings.Contains(qsql, "DELETE ") { continue } diff --git a/internal/endtoend/testdata/read_write_segregation/mysql/go/models.go b/internal/endtoend/testdata/read_write_segregation/mysql/go/models.go index 865f514a2b..643471082e 100644 --- a/internal/endtoend/testdata/read_write_segregation/mysql/go/models.go +++ b/internal/endtoend/testdata/read_write_segregation/mysql/go/models.go @@ -12,4 +12,5 @@ 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 index 0ff574b2b4..e59285435e 100644 --- a/internal/endtoend/testdata/read_write_segregation/mysql/go/query.sql.go +++ b/internal/endtoend/testdata/read_write_segregation/mysql/go/query.sql.go @@ -28,19 +28,25 @@ func (q *Queries) DeleteUsersByName(ctx context.Context, arg DeleteUsersByNamePa } const getUserByID = `-- name: GetUserByID :one -SELECT first_name, id, last_name FROM users WHERE id = ? +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) + err := row.Scan( + &i.FirstName, + &i.ID, + &i.LastName, + &i.UpdatedAt, + ) return i, err } diff --git a/internal/endtoend/testdata/read_write_segregation/mysql/go/read.go b/internal/endtoend/testdata/read_write_segregation/mysql/go/read.go index fd583209b5..e8ccb2b70f 100644 --- a/internal/endtoend/testdata/read_write_segregation/mysql/go/read.go +++ b/internal/endtoend/testdata/read_write_segregation/mysql/go/read.go @@ -64,7 +64,12 @@ func (q *ReadQueries) queryRow(ctx context.Context, stmt *sql.Stmt, query string 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) + err := row.Scan( + &i.FirstName, + &i.ID, + &i.LastName, + &i.UpdatedAt, + ) return i, err } diff --git a/internal/endtoend/testdata/read_write_segregation/mysql/query.sql b/internal/endtoend/testdata/read_write_segregation/mysql/query.sql index b743aee060..edae3f312e 100644 --- a/internal/endtoend/testdata/read_write_segregation/mysql/query.sql +++ b/internal/endtoend/testdata/read_write_segregation/mysql/query.sql @@ -1,5 +1,5 @@ /* name: GetUserByID :one */ -SELECT first_name, id, last_name FROM users WHERE id = sqlc.arg('target_id'); +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; diff --git a/internal/endtoend/testdata/read_write_segregation/mysql/schema.sql b/internal/endtoend/testdata/read_write_segregation/mysql/schema.sql index c928f3b7ce..93fc9cd731 100644 --- a/internal/endtoend/testdata/read_write_segregation/mysql/schema.sql +++ b/internal/endtoend/testdata/read_write_segregation/mysql/schema.sql @@ -1,6 +1,6 @@ CREATE TABLE users ( id SERIAL NOT NULL, first_name varchar(255) NOT NULL, - last_name varchar(255) + last_name varchar(255), + updated_at timestamp NULL DEFAULT CURRENT_TIMESTAMP ); - 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 index e116f7585a..ca96c6f04e 100644 --- a/internal/endtoend/testdata/read_write_segregation/postgresql/stdlib/go/models.go +++ b/internal/endtoend/testdata/read_write_segregation/postgresql/stdlib/go/models.go @@ -12,4 +12,5 @@ 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 index 31425dd01d..1f42d0062e 100644 --- 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 @@ -28,19 +28,25 @@ func (q *Queries) DeleteUsersByName(ctx context.Context, arg DeleteUsersByNamePa } const getUserByID = `-- name: GetUserByID :one -SELECT first_name, id, last_name FROM users WHERE id = $1 +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) + err := row.Scan( + &i.FirstName, + &i.ID, + &i.LastName, + &i.UpdatedAt, + ) return i, err } 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 index 6c8075b88c..0b55ba527b 100644 --- a/internal/endtoend/testdata/read_write_segregation/postgresql/stdlib/go/read.go +++ b/internal/endtoend/testdata/read_write_segregation/postgresql/stdlib/go/read.go @@ -64,7 +64,12 @@ func (q *ReadQueries) queryRow(ctx context.Context, stmt *sql.Stmt, query string 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) + err := row.Scan( + &i.FirstName, + &i.ID, + &i.LastName, + &i.UpdatedAt, + ) return i, err } diff --git a/internal/endtoend/testdata/read_write_segregation/postgresql/stdlib/query.sql b/internal/endtoend/testdata/read_write_segregation/postgresql/stdlib/query.sql index d2f54f2037..5d08af0abb 100644 --- a/internal/endtoend/testdata/read_write_segregation/postgresql/stdlib/query.sql +++ b/internal/endtoend/testdata/read_write_segregation/postgresql/stdlib/query.sql @@ -1,5 +1,5 @@ /* name: GetUserByID :one */ -SELECT first_name, id, last_name FROM users WHERE id = sqlc.arg('target_id'); +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; diff --git a/internal/endtoend/testdata/read_write_segregation/postgresql/stdlib/schema.sql b/internal/endtoend/testdata/read_write_segregation/postgresql/stdlib/schema.sql index c928f3b7ce..2de2a8c1ec 100644 --- a/internal/endtoend/testdata/read_write_segregation/postgresql/stdlib/schema.sql +++ b/internal/endtoend/testdata/read_write_segregation/postgresql/stdlib/schema.sql @@ -1,6 +1,7 @@ CREATE TABLE users ( id SERIAL NOT NULL, first_name varchar(255) NOT NULL, - last_name varchar(255) + last_name varchar(255), + updated_at timestamp NULL DEFAULT CURRENT_TIMESTAMP ); From f50efd8a80314b30c6a9b5c97dee5605a97ec226 Mon Sep 17 00:00:00 2001 From: Jarri Abidi Date: Fri, 29 Mar 2024 01:25:15 +0500 Subject: [PATCH 4/4] Exclude SELECT FOR UPDATE queries from readonly ones --- internal/codegen/golang/gen.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/codegen/golang/gen.go b/internal/codegen/golang/gen.go index f0b6b85afc..00e3558bca 100644 --- a/internal/codegen/golang/gen.go +++ b/internal/codegen/golang/gen.go @@ -429,6 +429,7 @@ func readOnly(queries []Query) []Query { } if strings.Contains(qsql, "INSERT ") || strings.Contains(qsql, "UPDATE ") || + strings.Contains(qsql, "FOR UPDATE") || strings.Contains(qsql, "DELETE ") { continue }