From 64ca07e31ba5ac064f666edc266b3d8f337637d3 Mon Sep 17 00:00:00 2001 From: Lucas Hild Date: Mon, 23 Sep 2024 16:46:58 +0200 Subject: [PATCH 01/34] Add commit query to tx options --- tx.go | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/tx.go b/tx.go index 8feeb512..168d7ba6 100644 --- a/tx.go +++ b/tx.go @@ -48,6 +48,8 @@ type TxOptions struct { // BeginQuery is the SQL query that will be executed to begin the transaction. This allows using non-standard syntax // such as BEGIN PRIORITY HIGH with CockroachDB. If set this will override the other settings. BeginQuery string + // CommitQuery is the SQL query that will be executed to commit the transaction. + CommitQuery string } var emptyTxOptions TxOptions @@ -105,7 +107,10 @@ func (c *Conn) BeginTx(ctx context.Context, txOptions TxOptions) (Tx, error) { return nil, err } - return &dbTx{conn: c}, nil + return &dbTx{ + conn: c, + commitQuery: txOptions.CommitQuery, + }, nil } // Tx represents a database transaction. @@ -154,6 +159,7 @@ type dbTx struct { conn *Conn savepointNum int64 closed bool + commitQuery string } // Begin starts a pseudo nested transaction implemented with a savepoint. @@ -177,7 +183,12 @@ func (tx *dbTx) Commit(ctx context.Context) error { return ErrTxClosed } - commandTag, err := tx.conn.Exec(ctx, "commit") + commandSQL := "commit" + if tx.commitQuery != "" { + commandSQL = tx.commitQuery + } + + commandTag, err := tx.conn.Exec(ctx, commandSQL) tx.closed = true if err != nil { if tx.conn.PgConn().TxStatus() != 'I' { From 25329273dacf0b56fcb799d6dd0811a87f5a48ad Mon Sep 17 00:00:00 2001 From: Oleksandr Redko Date: Wed, 18 Dec 2024 01:59:41 +0200 Subject: [PATCH 02/34] Improve links in README --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 0cf2c291..bbeb1336 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,7 @@ It is also possible to use the `database/sql` interface and convert a connection ## Testing -See CONTRIBUTING.md for setup instructions. +See [CONTRIBUTING.md](./CONTRIBUTING.md) for setup instructions. ## Architecture @@ -126,7 +126,7 @@ pgerrcode contains constants for the PostgreSQL error codes. ## Adapters for 3rd Party Tracers -* [https://github.com/jackhopner/pgx-xray-tracer](https://github.com/jackhopner/pgx-xray-tracer) +* [github.com/jackhopner/pgx-xray-tracer](https://github.com/jackhopner/pgx-xray-tracer) ## Adapters for 3rd Party Loggers @@ -156,7 +156,7 @@ Library for scanning data from a database into Go structs and more. A carefully designed SQL client for making using SQL easier, more productive, and less error-prone on Golang. -### [https://github.com/otan/gopgkrb5](https://github.com/otan/gopgkrb5) +### [github.com/otan/gopgkrb5](https://github.com/otan/gopgkrb5) Adds GSSAPI / Kerberos authentication support. @@ -169,6 +169,6 @@ Explicit data mapping and scanning library for Go structs and slices. Type safe and flexible package for scanning database data into Go types. Supports, structs, maps, slices and custom mapping functions. -### [https://github.com/z0ne-dev/mgx](https://github.com/z0ne-dev/mgx) +### [github.com/z0ne-dev/mgx](https://github.com/z0ne-dev/mgx) Code first migration library for native pgx (no database/sql abstraction). From 043685147f04bf42465dd77497e91d3226895d52 Mon Sep 17 00:00:00 2001 From: Oleksandr Redko Date: Wed, 18 Dec 2024 02:30:34 +0200 Subject: [PATCH 03/34] Handle errors in generate_certs --- testsetup/generate_certs.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/testsetup/generate_certs.go b/testsetup/generate_certs.go index 945c6c5e..f285549a 100644 --- a/testsetup/generate_certs.go +++ b/testsetup/generate_certs.go @@ -106,12 +106,12 @@ func main() { panic(err) } - writeEncryptedPrivateKey("pgx_sslcert.key", clientCertPrivKey, "certpw") + err = writeEncryptedPrivateKey("pgx_sslcert.key", clientCertPrivKey, "certpw") if err != nil { panic(err) } - writeCertificate("pgx_sslcert.crt", clientBytes) + err = writeCertificate("pgx_sslcert.crt", clientBytes) if err != nil { panic(err) } From 9d851d7c98e255b25beaa69250a89cfe2f34f9ba Mon Sep 17 00:00:00 2001 From: Jack Christensen Date: Sat, 21 Dec 2024 08:21:52 -0600 Subject: [PATCH 04/34] Fix integration benchmarks --- pgtype/integration_benchmark_test.go.erb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pgtype/integration_benchmark_test.go.erb b/pgtype/integration_benchmark_test.go.erb index 0175700a..6f401153 100644 --- a/pgtype/integration_benchmark_test.go.erb +++ b/pgtype/integration_benchmark_test.go.erb @@ -25,7 +25,7 @@ func BenchmarkQuery<%= format_name %>FormatDecode_PG_<%= pg_type %>_to_Go_<%= go rows, _ := conn.Query( ctx, `select <% columns.times do |col_idx| %><% if col_idx != 0 %>, <% end %>n::<%= pg_type %> + <%= col_idx%><% end %> from generate_series(1, <%= rows %>) n`, - []any{pgx.QueryResultFormats{<%= format_code %>}}, + pgx.QueryResultFormats{<%= format_code %>}, ) _, err := pgx.ForEachRow(rows, []any{<% columns.times do |col_idx| %><% if col_idx != 0 %>, <% end %>&v[<%= col_idx%>]<% end %>}, func() error { return nil }) if err != nil { @@ -49,7 +49,7 @@ func BenchmarkQuery<%= format_name %>FormatDecode_PG_Int4Array_With_Go_Int4Array rows, _ := conn.Query( ctx, `select array_agg(n) from generate_series(1, <%= array_size %>) n`, - []any{pgx.QueryResultFormats{<%= format_code %>}}, + pgx.QueryResultFormats{<%= format_code %>}, ) _, err := pgx.ForEachRow(rows, []any{&v}, func() error { return nil }) if err != nil { From 24fbe353ed5c3f53d379b3ae370229a5638a2868 Mon Sep 17 00:00:00 2001 From: Jack Christensen Date: Sat, 21 Dec 2024 09:25:36 -0600 Subject: [PATCH 05/34] Create changelog for v5.7.2 --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a0ff9ba3..6470088b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +# 5.7.2 (December 21, 2024) + +* Fix prepared statement already exists on batch prepare failure +* Add commit query to tx options (Lucas Hild) +* Fix pgtype.Timestamp json unmarshal (Shean de Montigny-Desautels) +* Add message body size limits in frontend and backend (zene) +* Add xid8 type +* Ensure planning encodes and scans cannot infinitely recurse +* Implement pgtype.UUID.String() (Konstantin Grachev) +* Switch from ExecParams to Exec in ValidateConnectTargetSessionAttrs functions (Alexander Rumyantsev) +* Update golang.org/x/crypto + # 5.7.1 (September 10, 2024) * Fix data race in tracelog.TraceLog From 17cd36818ca4bab60ec8e9393c4ce3fa80f1c76e Mon Sep 17 00:00:00 2001 From: Oleksandr Redko Date: Wed, 18 Dec 2024 02:15:32 +0200 Subject: [PATCH 06/34] Update comments in generated code to align with Go standards --- Rakefile | 2 +- pgtype/int.go | 3 ++- pgtype/int_test.go | 3 ++- pgtype/integration_benchmark_test.go | 2 ++ pgtype/zeronull/int.go | 3 ++- pgtype/zeronull/int_test.go | 3 ++- 6 files changed, 11 insertions(+), 5 deletions(-) diff --git a/Rakefile b/Rakefile index d957573e..3e3aa503 100644 --- a/Rakefile +++ b/Rakefile @@ -2,7 +2,7 @@ require "erb" rule '.go' => '.go.erb' do |task| erb = ERB.new(File.read(task.source)) - File.write(task.name, "// Do not edit. Generated from #{task.source}\n" + erb.result(binding)) + File.write(task.name, "// Code generated from #{task.source}. DO NOT EDIT.\n\n" + erb.result(binding)) sh "goimports", "-w", task.name end diff --git a/pgtype/int.go b/pgtype/int.go index 90a20a26..7a2f8cb2 100644 --- a/pgtype/int.go +++ b/pgtype/int.go @@ -1,4 +1,5 @@ -// Do not edit. Generated from pgtype/int.go.erb +// Code generated from pgtype/int.go.erb. DO NOT EDIT. + package pgtype import ( diff --git a/pgtype/int_test.go b/pgtype/int_test.go index 73294b3c..8c498769 100644 --- a/pgtype/int_test.go +++ b/pgtype/int_test.go @@ -1,4 +1,5 @@ -// Do not edit. Generated from pgtype/int_test.go.erb +// Code generated from pgtype/int_test.go.erb. DO NOT EDIT. + package pgtype_test import ( diff --git a/pgtype/integration_benchmark_test.go b/pgtype/integration_benchmark_test.go index 41e5f750..88516a9f 100644 --- a/pgtype/integration_benchmark_test.go +++ b/pgtype/integration_benchmark_test.go @@ -1,3 +1,5 @@ +// Code generated from pgtype/integration_benchmark_test.go.erb. DO NOT EDIT. + package pgtype_test import ( diff --git a/pgtype/zeronull/int.go b/pgtype/zeronull/int.go index 4fec8a1a..7fa0210e 100644 --- a/pgtype/zeronull/int.go +++ b/pgtype/zeronull/int.go @@ -1,4 +1,5 @@ -// Do not edit. Generated from pgtype/zeronull/int.go.erb +// Code generated from pgtype/zeronull/int.go.erb. DO NOT EDIT. + package zeronull import ( diff --git a/pgtype/zeronull/int_test.go b/pgtype/zeronull/int_test.go index 7204cc88..7e32064a 100644 --- a/pgtype/zeronull/int_test.go +++ b/pgtype/zeronull/int_test.go @@ -1,4 +1,5 @@ -// Do not edit. Generated from pgtype/zeronull/int_test.go.erb +// Code generated from pgtype/zeronull/int_test.go.erb. DO NOT EDIT. + package zeronull_test import ( From dc3aea06b5da4967608828519abd50146ea508db Mon Sep 17 00:00:00 2001 From: martinpasaribu Date: Sun, 22 Dec 2024 23:48:08 +0700 Subject: [PATCH 07/34] remove unused func and parameter --- conn.go | 10 +--------- tx.go | 5 ++--- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/conn.go b/conn.go index ed6a3a09..e92344f1 100644 --- a/conn.go +++ b/conn.go @@ -420,7 +420,7 @@ func (c *Conn) IsClosed() bool { return c.pgConn.IsClosed() } -func (c *Conn) die(err error) { +func (c *Conn) die() { if c.IsClosed() { return } @@ -588,14 +588,6 @@ func (c *Conn) execPrepared(ctx context.Context, sd *pgconn.StatementDescription return result.CommandTag, result.Err } -type unknownArgumentTypeQueryExecModeExecError struct { - arg any -} - -func (e *unknownArgumentTypeQueryExecModeExecError) Error() string { - return fmt.Sprintf("cannot use unregistered type %T as query argument in QueryExecModeExec", e.arg) -} - func (c *Conn) execSQLParams(ctx context.Context, sql string, args []any) (pgconn.CommandTag, error) { err := c.eqb.Build(c.typeMap, nil, args) if err != nil { diff --git a/tx.go b/tx.go index 168d7ba6..571e5e00 100644 --- a/tx.go +++ b/tx.go @@ -3,7 +3,6 @@ package pgx import ( "context" "errors" - "fmt" "strconv" "strings" @@ -103,7 +102,7 @@ func (c *Conn) BeginTx(ctx context.Context, txOptions TxOptions) (Tx, error) { if err != nil { // begin should never fail unless there is an underlying connection issue or // a context timeout. In either case, the connection is possibly broken. - c.die(errors.New("failed to begin transaction")) + c.die() return nil, err } @@ -216,7 +215,7 @@ func (tx *dbTx) Rollback(ctx context.Context) error { tx.closed = true if err != nil { // A rollback failure leaves the connection in an undefined state - tx.conn.die(fmt.Errorf("rollback failed: %w", err)) + tx.conn.die() return err } From 877111ceebe09d556644342840907d70ef779f5f Mon Sep 17 00:00:00 2001 From: martinpasaribu Date: Sun, 22 Dec 2024 23:57:28 +0700 Subject: [PATCH 08/34] check array just using len and remove imposible condition --- derived_types.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/derived_types.go b/derived_types.go index 22ab069c..5d3be84d 100644 --- a/derived_types.go +++ b/derived_types.go @@ -161,7 +161,7 @@ type derivedTypeInfo struct { // The result of this call can be passed into RegisterTypes to complete the process. func (c *Conn) LoadTypes(ctx context.Context, typeNames []string) ([]*pgtype.Type, error) { m := c.TypeMap() - if typeNames == nil || len(typeNames) == 0 { + if len(typeNames) == 0 { return nil, fmt.Errorf("No type names were supplied.") } @@ -232,15 +232,15 @@ func (c *Conn) LoadTypes(ctx context.Context, typeNames []string) ([]*pgtype.Typ default: return nil, fmt.Errorf("Unknown typtype %q was found while registering %q", ti.Typtype, ti.TypeName) } - if type_ != nil { - m.RegisterType(type_) - if ti.NspName != "" { - nspType := &pgtype.Type{Name: ti.NspName + "." + type_.Name, OID: type_.OID, Codec: type_.Codec} - m.RegisterType(nspType) - result = append(result, nspType) - } - result = append(result, type_) + + // the type_ is imposible to be null + m.RegisterType(type_) + if ti.NspName != "" { + nspType := &pgtype.Type{Name: ti.NspName + "." + type_.Name, OID: type_.OID, Codec: type_.Codec} + m.RegisterType(nspType) + result = append(result, nspType) } + result = append(result, type_) } return result, nil } From 311f72afdcd162e166e891a30f0ab70ec3be490a Mon Sep 17 00:00:00 2001 From: Oleksandr Redko Date: Tue, 24 Dec 2024 12:57:42 +0200 Subject: [PATCH 09/34] Refactor Conn.LoadTypes by removing redundant check --- derived_types.go | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/derived_types.go b/derived_types.go index 22ab069c..3677b1f0 100644 --- a/derived_types.go +++ b/derived_types.go @@ -161,7 +161,7 @@ type derivedTypeInfo struct { // The result of this call can be passed into RegisterTypes to complete the process. func (c *Conn) LoadTypes(ctx context.Context, typeNames []string) ([]*pgtype.Type, error) { m := c.TypeMap() - if typeNames == nil || len(typeNames) == 0 { + if len(typeNames) == 0 { return nil, fmt.Errorf("No type names were supplied.") } @@ -169,13 +169,7 @@ func (c *Conn) LoadTypes(ctx context.Context, typeNames []string) ([]*pgtype.Typ // the SQL not support recent structures such as multirange serverVersion, _ := serverVersion(c) sql := buildLoadDerivedTypesSQL(serverVersion, typeNames) - var rows Rows - var err error - if typeNames == nil { - rows, err = c.Query(ctx, sql, QueryExecModeSimpleProtocol) - } else { - rows, err = c.Query(ctx, sql, QueryExecModeSimpleProtocol, typeNames) - } + rows, err := c.Query(ctx, sql, QueryExecModeSimpleProtocol, typeNames) if err != nil { return nil, fmt.Errorf("While generating load types query: %w", err) } From 12b37f32185bc1fdd7cc37c2830cffface137028 Mon Sep 17 00:00:00 2001 From: Vamshi Aruru Date: Thu, 26 Dec 2024 13:46:49 +0530 Subject: [PATCH 10/34] Expose puddle.Pool's EmptyAcquireWaitTime in pgxpool's Stats Addresses: https://github.com/jackc/pgx/issues/2205 --- pgxpool/stat.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pgxpool/stat.go b/pgxpool/stat.go index cfa0c4c5..e02b6ac3 100644 --- a/pgxpool/stat.go +++ b/pgxpool/stat.go @@ -82,3 +82,10 @@ func (s *Stat) MaxLifetimeDestroyCount() int64 { func (s *Stat) MaxIdleDestroyCount() int64 { return s.idleDestroyCount } + +// EmptyAcquireWaitTime returns the cumulative time waited for successful acquires +// from the pool for a resource to be released or constructed because the pool was +// empty. +func (s *Stat) EmptyAcquireWaitTime() time.Duration { + return s.s.EmptyAcquireWaitTime() +} From afa974fb057e23af552b48f4f7c947141c486e96 Mon Sep 17 00:00:00 2001 From: merlin Date: Tue, 1 Oct 2024 12:52:55 +0300 Subject: [PATCH 11/34] base case make benchmark more extensive add quote to string add BenchmarkSanitizeSQL --- internal/sanitize/sanitize_bench_test.go | 62 ++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 internal/sanitize/sanitize_bench_test.go diff --git a/internal/sanitize/sanitize_bench_test.go b/internal/sanitize/sanitize_bench_test.go new file mode 100644 index 00000000..baa742b1 --- /dev/null +++ b/internal/sanitize/sanitize_bench_test.go @@ -0,0 +1,62 @@ +// sanitize_benchmark_test.go +package sanitize_test + +import ( + "testing" + "time" + + "github.com/jackc/pgx/v5/internal/sanitize" +) + +var benchmarkSanitizeResult string + +const benchmarkQuery = "" + + `SELECT * + FROM "water_containers" + WHERE NOT "id" = $1 -- int64 + AND "tags" NOT IN $2 -- nil + AND "volume" > $3 -- float64 + AND "transportable" = $4 -- bool + AND position($5 IN "sign") -- bytes + AND "label" LIKE $6 -- string + AND "created_at" > $7; -- time.Time` + +var benchmarkArgs = []any{ + int64(12345), + nil, + float64(500), + true, + []byte("8BADF00D"), + "kombucha's han'dy awokowa", + time.Date(2015, 10, 1, 0, 0, 0, 0, time.UTC), +} + +func BenchmarkSanitize(b *testing.B) { + query, err := sanitize.NewQuery(benchmarkQuery) + if err != nil { + b.Fatalf("failed to create query: %v", err) + } + + b.ResetTimer() + b.ReportAllocs() + + for i := 0; i < b.N; i++ { + benchmarkSanitizeResult, err = query.Sanitize(benchmarkArgs...) + if err != nil { + b.Fatalf("failed to sanitize query: %v", err) + } + } +} + +var benchmarkNewSQLResult string + +func BenchmarkSanitizeSQL(b *testing.B) { + b.ReportAllocs() + var err error + for i := 0; i < b.N; i++ { + benchmarkNewSQLResult, err = sanitize.SanitizeSQL(benchmarkQuery, benchmarkArgs...) + if err != nil { + b.Fatalf("failed to sanitize SQL: %v", err) + } + } +} From aabed18db8d4bc55f5d2e18a23a7707156e456ef Mon Sep 17 00:00:00 2001 From: merlin Date: Tue, 1 Oct 2024 12:57:07 +0300 Subject: [PATCH 12/34] add benchmark tool fix benchmmark script fix benchmark script --- internal/sanitize/benchmmark.sh | 59 +++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 internal/sanitize/benchmmark.sh diff --git a/internal/sanitize/benchmmark.sh b/internal/sanitize/benchmmark.sh new file mode 100644 index 00000000..87e7e0a1 --- /dev/null +++ b/internal/sanitize/benchmmark.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash + +current_branch=$(git rev-parse --abbrev-ref HEAD) +if [ "$current_branch" == "HEAD" ]; then + current_branch=$(git rev-parse HEAD) +fi + +restore_branch() { + echo "Restoring original branch/commit: $current_branch" + git checkout "$current_branch" +} +trap restore_branch EXIT + +# Check if there are uncommitted changes +if ! git diff --quiet || ! git diff --cached --quiet; then + echo "There are uncommitted changes. Please commit or stash them before running this script." + exit 1 +fi + +# Ensure that at least one commit argument is passed +if [ "$#" -lt 1 ]; then + echo "Usage: $0 ... " + exit 1 +fi + +commits=("$@") +benchmarks_dir=benchmarks + +if ! mkdir -p "${benchmarks_dir}"; then + echo "Unable to create dir for benchmarks data" + exit 1 +fi + +# Benchmark results +bench_files=() + +# Run benchmark for each listed commit +for i in "${!commits[@]}"; do + commit="${commits[i]}" + git checkout "$commit" || { + echo "Failed to checkout $commit" + exit 1 + } + + # Sanitized commmit message + commit_message=$(git log -1 --pretty=format:"%s" | tr ' ' '_') + + # Benchmark data will go there + bench_file="${benchmarks_dir}/${i}_${commit_message}.bench" + + if ! go test -bench=. -count=25 >"$bench_file"; then + echo "Benchmarking failed for commit $commit" + exit 1 + fi + + bench_files+=("$bench_file") +done + +benchstat "${bench_files[@]}" From efc2c9ff4438a84402c5631b9de961b9f62440db Mon Sep 17 00:00:00 2001 From: merlin Date: Tue, 1 Oct 2024 12:53:07 +0300 Subject: [PATCH 13/34] buf pool --- internal/sanitize/sanitize.go | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/internal/sanitize/sanitize.go b/internal/sanitize/sanitize.go index df58c448..4a069658 100644 --- a/internal/sanitize/sanitize.go +++ b/internal/sanitize/sanitize.go @@ -6,6 +6,7 @@ import ( "fmt" "strconv" "strings" + "sync" "time" "unicode/utf8" ) @@ -24,9 +25,26 @@ type Query struct { // https://github.com/jackc/pgx/issues/1380 const replacementcharacterwidth = 3 +var bufPool = &sync.Pool{} + +func getBuf() *bytes.Buffer { + buf, _ := bufPool.Get().(*bytes.Buffer) + if buf == nil { + buf = &bytes.Buffer{} + } + + return buf +} + +func putBuf(buf *bytes.Buffer) { + buf.Reset() + bufPool.Put(buf) +} + func (q *Query) Sanitize(args ...any) (string, error) { argUse := make([]bool, len(args)) - buf := &bytes.Buffer{} + buf := getBuf() + defer putBuf(buf) for _, part := range q.Parts { var str string From 546ad2f4e23675f7398b5136b43f4ca2803d046d Mon Sep 17 00:00:00 2001 From: merlin Date: Tue, 1 Oct 2024 13:24:03 +0300 Subject: [PATCH 14/34] shared bytestring --- internal/sanitize/sanitize.go | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/internal/sanitize/sanitize.go b/internal/sanitize/sanitize.go index 4a069658..c7c8acd5 100644 --- a/internal/sanitize/sanitize.go +++ b/internal/sanitize/sanitize.go @@ -41,16 +41,19 @@ func putBuf(buf *bytes.Buffer) { bufPool.Put(buf) } +var null = []byte("null") + func (q *Query) Sanitize(args ...any) (string, error) { argUse := make([]bool, len(args)) buf := getBuf() defer putBuf(buf) + var p []byte for _, part := range q.Parts { - var str string + p = p[:0] switch part := part.(type) { case string: - str = part + buf.WriteString(part) case int: argIdx := part - 1 @@ -64,19 +67,19 @@ func (q *Query) Sanitize(args ...any) (string, error) { arg := args[argIdx] switch arg := arg.(type) { case nil: - str = "null" + p = null case int64: - str = strconv.FormatInt(arg, 10) + p = strconv.AppendInt(p, arg, 10) case float64: - str = strconv.FormatFloat(arg, 'f', -1, 64) + p = strconv.AppendFloat(p, arg, 'f', -1, 64) case bool: - str = strconv.FormatBool(arg) + p = strconv.AppendBool(p, arg) case []byte: - str = QuoteBytes(arg) + p = []byte(QuoteBytes(arg)) case string: - str = QuoteString(arg) + p = []byte(QuoteString(arg)) case time.Time: - str = arg.Truncate(time.Microsecond).Format("'2006-01-02 15:04:05.999999999Z07:00:00'") + p = arg.Truncate(time.Microsecond).AppendFormat(p, "'2006-01-02 15:04:05.999999999Z07:00:00'") default: return "", fmt.Errorf("invalid arg type: %T", arg) } @@ -84,11 +87,12 @@ func (q *Query) Sanitize(args ...any) (string, error) { // Prevent SQL injection via Line Comment Creation // https://github.com/jackc/pgx/security/advisories/GHSA-m7wr-2xf7-cm9p - str = " " + str + " " + buf.WriteByte(' ') + buf.Write(p) + buf.WriteByte(' ') default: return "", fmt.Errorf("invalid Part type: %T", part) } - buf.WriteString(str) } for i, used := range argUse { From ee718a110d4b0b5eda4b887574e700b33d8b8982 Mon Sep 17 00:00:00 2001 From: merlin Date: Tue, 1 Oct 2024 13:30:34 +0300 Subject: [PATCH 15/34] append AvailableBuffer --- internal/sanitize/sanitize.go | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/internal/sanitize/sanitize.go b/internal/sanitize/sanitize.go index c7c8acd5..1e0b20ac 100644 --- a/internal/sanitize/sanitize.go +++ b/internal/sanitize/sanitize.go @@ -47,16 +47,14 @@ func (q *Query) Sanitize(args ...any) (string, error) { argUse := make([]bool, len(args)) buf := getBuf() defer putBuf(buf) - var p []byte for _, part := range q.Parts { - p = p[:0] switch part := part.(type) { case string: buf.WriteString(part) case int: argIdx := part - 1 - + var p []byte if argIdx < 0 { return "", fmt.Errorf("first sql argument must be > 0") } @@ -64,22 +62,23 @@ func (q *Query) Sanitize(args ...any) (string, error) { if argIdx >= len(args) { return "", fmt.Errorf("insufficient arguments") } + buf.WriteByte(' ') arg := args[argIdx] switch arg := arg.(type) { case nil: p = null case int64: - p = strconv.AppendInt(p, arg, 10) + p = strconv.AppendInt(buf.AvailableBuffer(), arg, 10) case float64: - p = strconv.AppendFloat(p, arg, 'f', -1, 64) + p = strconv.AppendFloat(buf.AvailableBuffer(), arg, 'f', -1, 64) case bool: - p = strconv.AppendBool(p, arg) + p = strconv.AppendBool(buf.AvailableBuffer(), arg) case []byte: p = []byte(QuoteBytes(arg)) case string: p = []byte(QuoteString(arg)) case time.Time: - p = arg.Truncate(time.Microsecond).AppendFormat(p, "'2006-01-02 15:04:05.999999999Z07:00:00'") + p = arg.Truncate(time.Microsecond).AppendFormat(buf.AvailableBuffer(), "'2006-01-02 15:04:05.999999999Z07:00:00'") default: return "", fmt.Errorf("invalid arg type: %T", arg) } @@ -87,7 +86,6 @@ func (q *Query) Sanitize(args ...any) (string, error) { // Prevent SQL injection via Line Comment Creation // https://github.com/jackc/pgx/security/advisories/GHSA-m7wr-2xf7-cm9p - buf.WriteByte(' ') buf.Write(p) buf.WriteByte(' ') default: From 1752f7b4c1ba7ba0d63cf4cc2f68c3ed56337716 Mon Sep 17 00:00:00 2001 From: merlin Date: Tue, 1 Oct 2024 13:47:44 +0300 Subject: [PATCH 16/34] docs --- internal/sanitize/sanitize.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/internal/sanitize/sanitize.go b/internal/sanitize/sanitize.go index 1e0b20ac..3414d6d1 100644 --- a/internal/sanitize/sanitize.go +++ b/internal/sanitize/sanitize.go @@ -62,7 +62,11 @@ func (q *Query) Sanitize(args ...any) (string, error) { if argIdx >= len(args) { return "", fmt.Errorf("insufficient arguments") } + + // Prevent SQL injection via Line Comment Creation + // https://github.com/jackc/pgx/security/advisories/GHSA-m7wr-2xf7-cm9p buf.WriteByte(' ') + arg := args[argIdx] switch arg := arg.(type) { case nil: @@ -78,15 +82,17 @@ func (q *Query) Sanitize(args ...any) (string, error) { case string: p = []byte(QuoteString(arg)) case time.Time: - p = arg.Truncate(time.Microsecond).AppendFormat(buf.AvailableBuffer(), "'2006-01-02 15:04:05.999999999Z07:00:00'") + p = arg.Truncate(time.Microsecond). + AppendFormat(buf.AvailableBuffer(), "'2006-01-02 15:04:05.999999999Z07:00:00'") default: return "", fmt.Errorf("invalid arg type: %T", arg) } argUse[argIdx] = true + buf.Write(p) + // Prevent SQL injection via Line Comment Creation // https://github.com/jackc/pgx/security/advisories/GHSA-m7wr-2xf7-cm9p - buf.Write(p) buf.WriteByte(' ') default: return "", fmt.Errorf("invalid Part type: %T", part) From 58d4c0c94fa86d28f5931af2811ce9acdcb117da Mon Sep 17 00:00:00 2001 From: merlin Date: Tue, 1 Oct 2024 14:30:05 +0300 Subject: [PATCH 17/34] quoteBytes check new quoteBytes --- internal/sanitize/sanitize.go | 17 +++++++++++++++-- internal/sanitize/sanitize_test.go | 25 +++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/internal/sanitize/sanitize.go b/internal/sanitize/sanitize.go index 3414d6d1..91d6db58 100644 --- a/internal/sanitize/sanitize.go +++ b/internal/sanitize/sanitize.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/hex" "fmt" + "slices" "strconv" "strings" "sync" @@ -78,7 +79,7 @@ func (q *Query) Sanitize(args ...any) (string, error) { case bool: p = strconv.AppendBool(buf.AvailableBuffer(), arg) case []byte: - p = []byte(QuoteBytes(arg)) + p = quoteBytes(buf.AvailableBuffer(), arg) case string: p = []byte(QuoteString(arg)) case time.Time: @@ -127,7 +128,19 @@ func QuoteString(str string) string { } func QuoteBytes(buf []byte) string { - return `'\x` + hex.EncodeToString(buf) + "'" + return string(quoteBytes(nil, buf)) +} + +func quoteBytes(dst, buf []byte) []byte { + dst = append(dst, `'\x`...) + + n := hex.EncodedLen(len(buf)) + p := slices.Grow(dst[len(dst):], n)[:n] + hex.Encode(p, buf) + dst = append(dst, p...) + + dst = append(dst, `'`...) + return dst } type sqlLexer struct { diff --git a/internal/sanitize/sanitize_test.go b/internal/sanitize/sanitize_test.go index 1deff3fb..76ae7a47 100644 --- a/internal/sanitize/sanitize_test.go +++ b/internal/sanitize/sanitize_test.go @@ -1,6 +1,7 @@ package sanitize_test import ( + "encoding/hex" "testing" "time" @@ -227,3 +228,27 @@ func TestQuerySanitize(t *testing.T) { } } } + +func TestQuoteBytes(t *testing.T) { + tc := func(name string, input []byte) { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := sanitize.QuoteBytes(input) + want := oldQuoteBytes(input) + + if got != want { + t.Errorf("got: %s", got) + t.Fatalf("want: %s", want) + } + }) + } + + tc("nil", nil) + tc("empty", []byte{}) + tc("text", []byte("abcd")) +} + +func oldQuoteBytes(buf []byte) string { + return `'\x` + hex.EncodeToString(buf) + "'" +} From ea1e13a660aa60e9e34f67277ccdd77c9a74e535 Mon Sep 17 00:00:00 2001 From: merlin Date: Tue, 1 Oct 2024 14:50:59 +0300 Subject: [PATCH 18/34] quoteString --- internal/sanitize/sanitize.go | 31 ++++++++++++++++++++++++++++-- internal/sanitize/sanitize_test.go | 25 ++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/internal/sanitize/sanitize.go b/internal/sanitize/sanitize.go index 91d6db58..d83633a7 100644 --- a/internal/sanitize/sanitize.go +++ b/internal/sanitize/sanitize.go @@ -81,7 +81,7 @@ func (q *Query) Sanitize(args ...any) (string, error) { case []byte: p = quoteBytes(buf.AvailableBuffer(), arg) case string: - p = []byte(QuoteString(arg)) + p = quoteString(buf.AvailableBuffer(), arg) case time.Time: p = arg.Truncate(time.Microsecond). AppendFormat(buf.AvailableBuffer(), "'2006-01-02 15:04:05.999999999Z07:00:00'") @@ -124,7 +124,34 @@ func NewQuery(sql string) (*Query, error) { } func QuoteString(str string) string { - return "'" + strings.ReplaceAll(str, "'", "''") + "'" + return string(quoteString(nil, str)) +} + +func quoteString(dst []byte, str string) []byte { + const quote = "'" + + n := strings.Count(str, quote) + + dst = append(dst, quote...) + + p := slices.Grow(dst[len(dst):], len(str)+2*n) + + for len(str) > 0 { + i := strings.Index(str, quote) + if i < 0 { + p = append(p, str...) + break + } + p = append(p, str[:i]...) + p = append(p, "''"...) + str = str[i+1:] + } + + dst = append(dst, p...) + + dst = append(dst, quote...) + + return dst } func QuoteBytes(buf []byte) string { diff --git a/internal/sanitize/sanitize_test.go b/internal/sanitize/sanitize_test.go index 76ae7a47..aafcd682 100644 --- a/internal/sanitize/sanitize_test.go +++ b/internal/sanitize/sanitize_test.go @@ -2,6 +2,7 @@ package sanitize_test import ( "encoding/hex" + "strings" "testing" "time" @@ -229,6 +230,30 @@ func TestQuerySanitize(t *testing.T) { } } +func TestQuoteString(t *testing.T) { + tc := func(name, input string) { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := sanitize.QuoteString(input) + want := oldQuoteString(input) + + if got != want { + t.Errorf("got: %s", got) + t.Fatalf("want: %s", want) + } + }) + } + + tc("empty", "") + tc("text", "abcd") + tc("with quotes", `one's hat is always a cat`) +} + +func oldQuoteString(str string) string { + return "'" + strings.ReplaceAll(str, "'", "''") + "'" +} + func TestQuoteBytes(t *testing.T) { tc := func(name string, input []byte) { t.Run(name, func(t *testing.T) { From 4293b2526266935b0ea2f86515217d277c1e4d2a Mon Sep 17 00:00:00 2001 From: merlin Date: Tue, 1 Oct 2024 15:25:24 +0300 Subject: [PATCH 19/34] decrease number of samples in go benchmark --- internal/sanitize/benchmmark.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/sanitize/benchmmark.sh b/internal/sanitize/benchmmark.sh index 87e7e0a1..06842c0a 100644 --- a/internal/sanitize/benchmmark.sh +++ b/internal/sanitize/benchmmark.sh @@ -48,7 +48,7 @@ for i in "${!commits[@]}"; do # Benchmark data will go there bench_file="${benchmarks_dir}/${i}_${commit_message}.bench" - if ! go test -bench=. -count=25 >"$bench_file"; then + if ! go test -bench=. -count=10 >"$bench_file"; then echo "Benchmarking failed for commit $commit" exit 1 fi From c4c1076d28cc0333616fc55b1ecea7ebf0087cc8 Mon Sep 17 00:00:00 2001 From: merlin Date: Tue, 1 Oct 2024 16:37:04 +0300 Subject: [PATCH 20/34] add FuzzQuoteString and FuzzQuoteBytes --- internal/sanitize/sanitize_fuzz_test.go | 43 +++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 internal/sanitize/sanitize_fuzz_test.go diff --git a/internal/sanitize/sanitize_fuzz_test.go b/internal/sanitize/sanitize_fuzz_test.go new file mode 100644 index 00000000..7d594def --- /dev/null +++ b/internal/sanitize/sanitize_fuzz_test.go @@ -0,0 +1,43 @@ +package sanitize_test + +import ( + "testing" + + "github.com/jackc/pgx/v5/internal/sanitize" +) + +func FuzzQuoteString(f *testing.F) { + f.Add("") + f.Add("\n") + f.Add("sample text") + f.Add("sample q'u'o't'e's") + f.Add("select 'quoted $42', $1") + + f.Fuzz(func(t *testing.T, input string) { + got := sanitize.QuoteString(input) + want := oldQuoteString(input) + + if want != got { + t.Errorf("got %q", got) + t.Fatalf("want %q", want) + } + }) +} + +func FuzzQuoteBytes(f *testing.F) { + f.Add([]byte(nil)) + f.Add([]byte("\n")) + f.Add([]byte("sample text")) + f.Add([]byte("sample q'u'o't'e's")) + f.Add([]byte("select 'quoted $42', $1")) + + f.Fuzz(func(t *testing.T, input []byte) { + got := sanitize.QuoteBytes(input) + want := oldQuoteBytes(input) + + if want != got { + t.Errorf("got %q", got) + t.Fatalf("want %q", want) + } + }) +} From 39ffc8b7a4df735206d45fcefb6c21f126c5d6e3 Mon Sep 17 00:00:00 2001 From: merlin Date: Tue, 1 Oct 2024 16:42:27 +0300 Subject: [PATCH 21/34] add lexer and query pools use lexer pool --- internal/sanitize/sanitize.go | 93 +++++++++++++++++++++++++---------- 1 file changed, 67 insertions(+), 26 deletions(-) diff --git a/internal/sanitize/sanitize.go b/internal/sanitize/sanitize.go index d83633a7..4aca2fb9 100644 --- a/internal/sanitize/sanitize.go +++ b/internal/sanitize/sanitize.go @@ -26,28 +26,19 @@ type Query struct { // https://github.com/jackc/pgx/issues/1380 const replacementcharacterwidth = 3 -var bufPool = &sync.Pool{} - -func getBuf() *bytes.Buffer { - buf, _ := bufPool.Get().(*bytes.Buffer) - if buf == nil { - buf = &bytes.Buffer{} - } - - return buf -} - -func putBuf(buf *bytes.Buffer) { - buf.Reset() - bufPool.Put(buf) +var bufPool = &pool[*bytes.Buffer]{ + new: func() *bytes.Buffer { + return &bytes.Buffer{} + }, + reset: (*bytes.Buffer).Reset, } var null = []byte("null") func (q *Query) Sanitize(args ...any) (string, error) { argUse := make([]bool, len(args)) - buf := getBuf() - defer putBuf(buf) + buf := bufPool.get() + defer bufPool.put(buf) for _, part := range q.Parts { switch part := part.(type) { @@ -109,18 +100,39 @@ func (q *Query) Sanitize(args ...any) (string, error) { } func NewQuery(sql string) (*Query, error) { - l := &sqlLexer{ - src: sql, - stateFn: rawState, + query := &Query{} + query.init(sql) + + return query, nil +} + +var sqlLexerPool = &pool[*sqlLexer]{ + new: func() *sqlLexer { + return &sqlLexer{} + }, + reset: func(sl *sqlLexer) { + *sl = sqlLexer{} + }, +} + +func (q *Query) init(sql string) { + parts := q.Parts[:0] + if parts == nil { + n := strings.Count(sql, "$") + strings.Count(sql, "--") + 1 + parts = make([]Part, 0, n) } + l := sqlLexerPool.get() + defer sqlLexerPool.put(l) + l.src = sql + l.stateFn = rawState + l.parts = parts + for l.stateFn != nil { l.stateFn = l.stateFn(l) } - query := &Query{Parts: l.parts} - - return query, nil + q.Parts = l.parts } func QuoteString(str string) string { @@ -385,13 +397,42 @@ func multilineCommentState(l *sqlLexer) stateFn { } } +var queryPool = &pool[*Query]{ + new: func() *Query { + return &Query{} + }, + reset: func(q *Query) { + q.Parts = q.Parts[:0] + }, +} + // SanitizeSQL replaces placeholder values with args. It quotes and escapes args // as necessary. This function is only safe when standard_conforming_strings is // on. func SanitizeSQL(sql string, args ...any) (string, error) { - query, err := NewQuery(sql) - if err != nil { - return "", err - } + query := queryPool.get() + query.init(sql) + defer queryPool.put(query) + return query.Sanitize(args...) } + +type pool[E any] struct { + p sync.Pool + new func() E + reset func(E) +} + +func (pool *pool[E]) get() E { + v, ok := pool.p.Get().(E) + if !ok { + v = pool.new() + } + + return v +} + +func (p *pool[E]) put(v E) { + p.reset(v) + p.p.Put(v) +} From 59d6aa87b9e78e5f694854bf36143d13621c6b77 Mon Sep 17 00:00:00 2001 From: merlin Date: Tue, 1 Oct 2024 17:04:48 +0300 Subject: [PATCH 22/34] rework QuoteString and QuoteBytes as append-style --- internal/sanitize/sanitize.go | 16 ++++------------ internal/sanitize/sanitize_fuzz_test.go | 8 ++++---- internal/sanitize/sanitize_test.go | 4 ++-- 3 files changed, 10 insertions(+), 18 deletions(-) diff --git a/internal/sanitize/sanitize.go b/internal/sanitize/sanitize.go index 4aca2fb9..fd1e808b 100644 --- a/internal/sanitize/sanitize.go +++ b/internal/sanitize/sanitize.go @@ -70,9 +70,9 @@ func (q *Query) Sanitize(args ...any) (string, error) { case bool: p = strconv.AppendBool(buf.AvailableBuffer(), arg) case []byte: - p = quoteBytes(buf.AvailableBuffer(), arg) + p = QuoteBytes(buf.AvailableBuffer(), arg) case string: - p = quoteString(buf.AvailableBuffer(), arg) + p = QuoteString(buf.AvailableBuffer(), arg) case time.Time: p = arg.Truncate(time.Microsecond). AppendFormat(buf.AvailableBuffer(), "'2006-01-02 15:04:05.999999999Z07:00:00'") @@ -135,11 +135,7 @@ func (q *Query) init(sql string) { q.Parts = l.parts } -func QuoteString(str string) string { - return string(quoteString(nil, str)) -} - -func quoteString(dst []byte, str string) []byte { +func QuoteString(dst []byte, str string) []byte { const quote = "'" n := strings.Count(str, quote) @@ -166,11 +162,7 @@ func quoteString(dst []byte, str string) []byte { return dst } -func QuoteBytes(buf []byte) string { - return string(quoteBytes(nil, buf)) -} - -func quoteBytes(dst, buf []byte) []byte { +func QuoteBytes(dst, buf []byte) []byte { dst = append(dst, `'\x`...) n := hex.EncodedLen(len(buf)) diff --git a/internal/sanitize/sanitize_fuzz_test.go b/internal/sanitize/sanitize_fuzz_test.go index 7d594def..74655827 100644 --- a/internal/sanitize/sanitize_fuzz_test.go +++ b/internal/sanitize/sanitize_fuzz_test.go @@ -14,10 +14,10 @@ func FuzzQuoteString(f *testing.F) { f.Add("select 'quoted $42', $1") f.Fuzz(func(t *testing.T, input string) { - got := sanitize.QuoteString(input) + got := sanitize.QuoteString(nil, input) want := oldQuoteString(input) - if want != got { + if want != string(got) { t.Errorf("got %q", got) t.Fatalf("want %q", want) } @@ -32,10 +32,10 @@ func FuzzQuoteBytes(f *testing.F) { f.Add([]byte("select 'quoted $42', $1")) f.Fuzz(func(t *testing.T, input []byte) { - got := sanitize.QuoteBytes(input) + got := sanitize.QuoteBytes(nil, input) want := oldQuoteBytes(input) - if want != got { + if want != string(got) { t.Errorf("got %q", got) t.Fatalf("want %q", want) } diff --git a/internal/sanitize/sanitize_test.go b/internal/sanitize/sanitize_test.go index aafcd682..9da701ea 100644 --- a/internal/sanitize/sanitize_test.go +++ b/internal/sanitize/sanitize_test.go @@ -235,7 +235,7 @@ func TestQuoteString(t *testing.T) { t.Run(name, func(t *testing.T) { t.Parallel() - got := sanitize.QuoteString(input) + got := string(sanitize.QuoteString(nil, input)) want := oldQuoteString(input) if got != want { @@ -259,7 +259,7 @@ func TestQuoteBytes(t *testing.T) { t.Run(name, func(t *testing.T) { t.Parallel() - got := sanitize.QuoteBytes(input) + got := string(sanitize.QuoteBytes(nil, input)) want := oldQuoteBytes(input) if got != want { From 90a77b13b200afbfe64bae275df1ea6a55cb4aa3 Mon Sep 17 00:00:00 2001 From: merlin Date: Tue, 1 Oct 2024 17:15:38 +0300 Subject: [PATCH 23/34] add docs to sanitize tests --- internal/sanitize/sanitize_test.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/internal/sanitize/sanitize_test.go b/internal/sanitize/sanitize_test.go index 9da701ea..92675153 100644 --- a/internal/sanitize/sanitize_test.go +++ b/internal/sanitize/sanitize_test.go @@ -250,6 +250,8 @@ func TestQuoteString(t *testing.T) { tc("with quotes", `one's hat is always a cat`) } +// This function was used before optimizations. +// You should keep for testing purposes - we want to ensure there are no breaking changes. func oldQuoteString(str string) string { return "'" + strings.ReplaceAll(str, "'", "''") + "'" } @@ -274,6 +276,8 @@ func TestQuoteBytes(t *testing.T) { tc("text", []byte("abcd")) } +// This function was used before optimizations. +// You should keep for testing purposes - we want to ensure there are no breaking changes. func oldQuoteBytes(buf []byte) string { return `'\x` + hex.EncodeToString(buf) + "'" } From 47cbd8edb8b59458af8191dddb2cf1b0f96f4603 Mon Sep 17 00:00:00 2001 From: merlin Date: Tue, 1 Oct 2024 17:16:02 +0300 Subject: [PATCH 24/34] drop too large values from memory pools --- internal/sanitize/sanitize.go | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/internal/sanitize/sanitize.go b/internal/sanitize/sanitize.go index fd1e808b..173523d9 100644 --- a/internal/sanitize/sanitize.go +++ b/internal/sanitize/sanitize.go @@ -26,11 +26,17 @@ type Query struct { // https://github.com/jackc/pgx/issues/1380 const replacementcharacterwidth = 3 +const maxBufSize = 16384 // 16 Ki + var bufPool = &pool[*bytes.Buffer]{ new: func() *bytes.Buffer { return &bytes.Buffer{} }, - reset: (*bytes.Buffer).Reset, + reset: func(b *bytes.Buffer) bool { + n := b.Len() + b.Reset() + return n < maxBufSize + }, } var null = []byte("null") @@ -110,20 +116,23 @@ var sqlLexerPool = &pool[*sqlLexer]{ new: func() *sqlLexer { return &sqlLexer{} }, - reset: func(sl *sqlLexer) { + reset: func(sl *sqlLexer) bool { *sl = sqlLexer{} + return true }, } func (q *Query) init(sql string) { parts := q.Parts[:0] if parts == nil { + // dirty, but fast heuristic to preallocate for ~90% usecases n := strings.Count(sql, "$") + strings.Count(sql, "--") + 1 parts = make([]Part, 0, n) } l := sqlLexerPool.get() defer sqlLexerPool.put(l) + l.src = sql l.stateFn = rawState l.parts = parts @@ -393,8 +402,10 @@ var queryPool = &pool[*Query]{ new: func() *Query { return &Query{} }, - reset: func(q *Query) { + reset: func(q *Query) bool { + n := len(q.Parts) q.Parts = q.Parts[:0] + return n < 64 // drop too large queries }, } @@ -412,7 +423,7 @@ func SanitizeSQL(sql string, args ...any) (string, error) { type pool[E any] struct { p sync.Pool new func() E - reset func(E) + reset func(E) bool } func (pool *pool[E]) get() E { @@ -425,6 +436,7 @@ func (pool *pool[E]) get() E { } func (p *pool[E]) put(v E) { - p.reset(v) - p.p.Put(v) + if p.reset(v) { + p.p.Put(v) + } } From 057937db27165ec3f9a6b9cdaf9640ebda039914 Mon Sep 17 00:00:00 2001 From: merlin Date: Sun, 20 Oct 2024 18:00:59 +0300 Subject: [PATCH 25/34] add prefix to quoters tests --- internal/sanitize/sanitize_fuzz_test.go | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/internal/sanitize/sanitize_fuzz_test.go b/internal/sanitize/sanitize_fuzz_test.go index 74655827..a8f2e779 100644 --- a/internal/sanitize/sanitize_fuzz_test.go +++ b/internal/sanitize/sanitize_fuzz_test.go @@ -7,17 +7,22 @@ import ( ) func FuzzQuoteString(f *testing.F) { - f.Add("") - f.Add("\n") + const prefix = "prefix" + f.Add("new\nline") f.Add("sample text") f.Add("sample q'u'o't'e's") f.Add("select 'quoted $42', $1") f.Fuzz(func(t *testing.T, input string) { - got := sanitize.QuoteString(nil, input) + got := string(sanitize.QuoteString([]byte(prefix), input)) want := oldQuoteString(input) - if want != string(got) { + quoted, ok := strings.CutPrefix(got, prefix) + if !ok { + t.Fatalf("result has no prefix") + } + + if want != quoted { t.Errorf("got %q", got) t.Fatalf("want %q", want) } @@ -25,6 +30,7 @@ func FuzzQuoteString(f *testing.F) { } func FuzzQuoteBytes(f *testing.F) { + const prefix = "prefix" f.Add([]byte(nil)) f.Add([]byte("\n")) f.Add([]byte("sample text")) @@ -32,10 +38,15 @@ func FuzzQuoteBytes(f *testing.F) { f.Add([]byte("select 'quoted $42', $1")) f.Fuzz(func(t *testing.T, input []byte) { - got := sanitize.QuoteBytes(nil, input) + got := string(sanitize.QuoteBytes([]byte(prefix), input)) want := oldQuoteBytes(input) - if want != string(got) { + quoted, ok := strings.CutPrefix(got, prefix) + if !ok { + t.Fatalf("result has no prefix") + } + + if want != quoted { t.Errorf("got %q", got) t.Fatalf("want %q", want) } From 120c89fe0dc063d8a078298f27953d258643e783 Mon Sep 17 00:00:00 2001 From: merlin Date: Sun, 20 Oct 2024 18:08:23 +0300 Subject: [PATCH 26/34] fix preallocations of quoted string --- internal/sanitize/sanitize.go | 2 +- internal/sanitize/sanitize_fuzz_test.go | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/sanitize/sanitize.go b/internal/sanitize/sanitize.go index 173523d9..e0ae9bed 100644 --- a/internal/sanitize/sanitize.go +++ b/internal/sanitize/sanitize.go @@ -151,7 +151,7 @@ func QuoteString(dst []byte, str string) []byte { dst = append(dst, quote...) - p := slices.Grow(dst[len(dst):], len(str)+2*n) + p := slices.Grow(dst[len(dst):], 2*len(quote)+len(str)+2*n) for len(str) > 0 { i := strings.Index(str, quote) diff --git a/internal/sanitize/sanitize_fuzz_test.go b/internal/sanitize/sanitize_fuzz_test.go index a8f2e779..2f0c4122 100644 --- a/internal/sanitize/sanitize_fuzz_test.go +++ b/internal/sanitize/sanitize_fuzz_test.go @@ -1,6 +1,7 @@ package sanitize_test import ( + "strings" "testing" "github.com/jackc/pgx/v5/internal/sanitize" From da0315d1a47fc13432afeb90596028ed6ca6cd9d Mon Sep 17 00:00:00 2001 From: merlin Date: Mon, 9 Dec 2024 16:33:57 +0200 Subject: [PATCH 27/34] optimisations of quote functions by @sean- --- internal/sanitize/benchmmark.sh | 3 +- internal/sanitize/sanitize.go | 62 +++++++++++++++++++++------------ 2 files changed, 42 insertions(+), 23 deletions(-) diff --git a/internal/sanitize/benchmmark.sh b/internal/sanitize/benchmmark.sh index 06842c0a..ec0f7b03 100644 --- a/internal/sanitize/benchmmark.sh +++ b/internal/sanitize/benchmmark.sh @@ -43,7 +43,7 @@ for i in "${!commits[@]}"; do } # Sanitized commmit message - commit_message=$(git log -1 --pretty=format:"%s" | tr ' ' '_') + commit_message=$(git log -1 --pretty=format:"%s" | tr -c '[:alnum:]-_' '_') # Benchmark data will go there bench_file="${benchmarks_dir}/${i}_${commit_message}.bench" @@ -56,4 +56,5 @@ for i in "${!commits[@]}"; do bench_files+=("$bench_file") done +# go install golang.org/x/perf/cmd/benchstat[@latest] benchstat "${bench_files[@]}" diff --git a/internal/sanitize/sanitize.go b/internal/sanitize/sanitize.go index e0ae9bed..b516817c 100644 --- a/internal/sanitize/sanitize.go +++ b/internal/sanitize/sanitize.go @@ -145,41 +145,59 @@ func (q *Query) init(sql string) { } func QuoteString(dst []byte, str string) []byte { - const quote = "'" + const quote = '\'' - n := strings.Count(str, quote) + // Preallocate space for the worst case scenario + dst = slices.Grow(dst, len(str)*2+2) - dst = append(dst, quote...) + // Add opening quote + dst = append(dst, quote) - p := slices.Grow(dst[len(dst):], 2*len(quote)+len(str)+2*n) - - for len(str) > 0 { - i := strings.Index(str, quote) - if i < 0 { - p = append(p, str...) - break + // Iterate through the string without allocating + for i := 0; i < len(str); i++ { + if str[i] == quote { + dst = append(dst, quote, quote) + } else { + dst = append(dst, str[i]) } - p = append(p, str[:i]...) - p = append(p, "''"...) - str = str[i+1:] } - dst = append(dst, p...) - - dst = append(dst, quote...) + // Add closing quote + dst = append(dst, quote) return dst } func QuoteBytes(dst, buf []byte) []byte { - dst = append(dst, `'\x`...) + if len(buf) == 0 { + return append(dst, `'\x'`...) + } - n := hex.EncodedLen(len(buf)) - p := slices.Grow(dst[len(dst):], n)[:n] - hex.Encode(p, buf) - dst = append(dst, p...) + // Calculate required length + requiredLen := 3 + hex.EncodedLen(len(buf)) + 1 + + // Ensure dst has enough capacity + if cap(dst)-len(dst) < requiredLen { + newDst := make([]byte, len(dst), len(dst)+requiredLen) + copy(newDst, dst) + dst = newDst + } + + // Record original length and extend slice + origLen := len(dst) + dst = dst[:origLen+requiredLen] + + // Add prefix + dst[origLen] = '\'' + dst[origLen+1] = '\\' + dst[origLen+2] = 'x' + + // Encode bytes directly into dst + hex.Encode(dst[origLen+3:len(dst)-1], buf) + + // Add suffix + dst[len(dst)-1] = '\'' - dst = append(dst, `'`...) return dst } From e452f80b1d21846f931641099c2ef2c0a0873cf2 Mon Sep 17 00:00:00 2001 From: merlin Date: Sat, 28 Dec 2024 13:39:01 +0200 Subject: [PATCH 28/34] TestErrNoRows: remove bad test case --- conn_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/conn_test.go b/conn_test.go index 200ecc1a..d51f829b 100644 --- a/conn_test.go +++ b/conn_test.go @@ -1417,5 +1417,4 @@ func TestErrNoRows(t *testing.T) { require.Equal(t, "no rows in result set", pgx.ErrNoRows.Error()) require.ErrorIs(t, pgx.ErrNoRows, sql.ErrNoRows, "pgx.ErrNowRows must match sql.ErrNoRows") - require.ErrorIs(t, pgx.ErrNoRows, pgx.ErrNoRows, "sql.ErrNowRows must match pgx.ErrNoRows") } From 02e387ea6493ec262e83f5cc8d699a8aa51fe95d Mon Sep 17 00:00:00 2001 From: EinoPlasma <1805544976@qq.com> Date: Sun, 29 Dec 2024 20:59:24 +0800 Subject: [PATCH 29/34] Fix method comment in PasswordMessage --- pgproto3/password_message.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgproto3/password_message.go b/pgproto3/password_message.go index d820d327..67b78515 100644 --- a/pgproto3/password_message.go +++ b/pgproto3/password_message.go @@ -12,7 +12,7 @@ type PasswordMessage struct { // Frontend identifies this message as sendable by a PostgreSQL frontend. func (*PasswordMessage) Frontend() {} -// Frontend identifies this message as an authentication response. +// InitialResponse identifies this message as an authentication response. func (*PasswordMessage) InitialResponse() {} // Decode decodes src into dst. src must contain the complete message with the exception of the initial 1 byte message From 6d9e6a726eeae58b7fbb19fd8727cef4b4762f10 Mon Sep 17 00:00:00 2001 From: EinoPlasma <1805544976@qq.com> Date: Sun, 29 Dec 2024 21:03:38 +0800 Subject: [PATCH 30/34] Fix typo in test function name --- pgconn/ctxwatch/context_watcher_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgconn/ctxwatch/context_watcher_test.go b/pgconn/ctxwatch/context_watcher_test.go index 302aabe3..a18e7339 100644 --- a/pgconn/ctxwatch/context_watcher_test.go +++ b/pgconn/ctxwatch/context_watcher_test.go @@ -49,7 +49,7 @@ func TestContextWatcherContextCancelled(t *testing.T) { require.True(t, cleanupCalled, "Cleanup func was not called") } -func TestContextWatcherUnwatchdBeforeContextCancelled(t *testing.T) { +func TestContextWatcherUnwatchedBeforeContextCancelled(t *testing.T) { cw := ctxwatch.NewContextWatcher(&testHandler{ handleCancel: func(context.Context) { t.Error("cancel func should not have been called") From 6e9fa42fef85262b2de6975efa716e212a1012a8 Mon Sep 17 00:00:00 2001 From: Kostas Stamatakis Date: Mon, 30 Dec 2024 22:43:04 +0200 Subject: [PATCH 31/34] fix #2204 --- .vscode/launch.json | 15 ++++++++ .vscode/settings.json | 5 +++ pgtype/json.go | 11 +++++- pgtype/json_test.go | 20 ++++++++++- pgtype/pgtype.go | 4 +++ tete/main.go | 80 +++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 133 insertions(+), 2 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 .vscode/settings.json create mode 100644 tete/main.go diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..8655150d --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,15 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Launch Package", + "type": "go", + "request": "launch", + "mode": "debug", + "program": "${fileDirname}" + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..890fee03 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "go.testEnvVars": { + "PGX_TEST_DATABASE":"host=127.0.0.1 user=gamerhound password=gamerhound dbname=gamerhound" + } +} \ No newline at end of file diff --git a/pgtype/json.go b/pgtype/json.go index 48b9f977..76cec51b 100644 --- a/pgtype/json.go +++ b/pgtype/json.go @@ -161,6 +161,10 @@ func (c *JSONCodec) PlanScan(m *Map, oid uint32, format int16, target any) ScanP // // https://github.com/jackc/pgx/issues/2146 func isSQLScanner(v any) bool { + if _, is := v.(sql.Scanner); is { + return true + } + val := reflect.ValueOf(v) for val.Kind() == reflect.Ptr { if _, ok := val.Interface().(sql.Scanner); ok { @@ -212,7 +216,12 @@ func (s *scanPlanJSONToJSONUnmarshal) Scan(src []byte, dst any) error { return fmt.Errorf("cannot scan NULL into %T", dst) } - elem := reflect.ValueOf(dst).Elem() + v := reflect.ValueOf(dst) + if v.Kind() != reflect.Pointer || v.IsNil() { + return fmt.Errorf("cannot scan into non-pointer or nil destinations %T", dst) + } + + elem := v.Elem() elem.Set(reflect.Zero(elem.Type())) return s.unmarshal(src, dst) diff --git a/pgtype/json_test.go b/pgtype/json_test.go index 18ca5a8e..1f286b9d 100644 --- a/pgtype/json_test.go +++ b/pgtype/json_test.go @@ -267,7 +267,8 @@ func TestJSONCodecCustomMarshal(t *testing.T) { Unmarshal: func(data []byte, v any) error { return json.Unmarshal([]byte(`{"custom":"value"}`), v) }, - }}) + }, + }) } pgxtest.RunValueRoundTripTests(context.Background(), t, connTestRunner, pgxtest.KnownOIDQueryExecModes, "json", []pgxtest.ValueRoundTripTest{ @@ -278,3 +279,20 @@ func TestJSONCodecCustomMarshal(t *testing.T) { }}, }) } + +func TestJSONCodecScanToNonPointerValues(t *testing.T) { + defaultConnTestRunner.RunTest(context.Background(), t, func(ctx context.Context, t testing.TB, conn *pgx.Conn) { + n := 44 + err := conn.QueryRow(ctx, "select '42'::jsonb").Scan(n) + require.Error(t, err) + + var i *int + err = conn.QueryRow(ctx, "select '42'::jsonb").Scan(i) + require.Error(t, err) + + m := 0 + err = conn.QueryRow(ctx, "select '42'::jsonb").Scan(&m) + require.NoError(t, err) + require.Equal(t, 42, m) + }) +} diff --git a/pgtype/pgtype.go b/pgtype/pgtype.go index f9d43edd..20645d69 100644 --- a/pgtype/pgtype.go +++ b/pgtype/pgtype.go @@ -415,6 +415,10 @@ func (plan *scanPlanSQLScanner) Scan(src []byte, dst any) error { // we don't know if the target is a sql.Scanner or a pointer on a sql.Scanner, so we need to check recursively func getSQLScanner(target any) sql.Scanner { + if sc, is := target.(sql.Scanner); is { + return sc + } + val := reflect.ValueOf(target) for val.Kind() == reflect.Ptr { if _, ok := val.Interface().(sql.Scanner); ok { diff --git a/tete/main.go b/tete/main.go new file mode 100644 index 00000000..855636da --- /dev/null +++ b/tete/main.go @@ -0,0 +1,80 @@ +package main + +import ( + "context" + "fmt" + "log" + "reflect" + + "github.com/jackc/pgx/v5/pgxpool" +) + +func main() { + pool, err := pgxpool.New(context.Background(), "postgres://gamerhound:gamerhound@localhost:5432/gamerhound") + if err != nil { + log.Fatal(err) + } + defer pool.Close() + + // Create the enum type. + _, err = pool.Exec(context.Background(), `DROP TYPE IF EXISTS test_enum_type`) + if err != nil { + log.Print(err) + return + } + _, err = pool.Exec(context.Background(), `CREATE TYPE test_enum_type AS ENUM ('a', 'b')`) + if err != nil { + log.Print(err) + return + } + + err = testQuery(pool, "SELECT 'a'", "a") + if err != nil { + log.Printf("test TEXT error: %s\n", err) + } + + err = testQuery(pool, "SELECT 'a'::test_enum_type", "a") + if err != nil { + log.Printf("test ENUM error: %s\n", err) + } + + err = testQuery(pool, "SELECT '{}'::jsonb", "{}") + if err != nil { + log.Printf("test JSONB error: %s\n", err) + } +} + +// T implements the sql.Scanner interface. +type T struct { + v *any +} + +func (t T) Scan(v any) error { + *t.v = v + return nil +} + +// testQuery executes the query and checks if the scanned value matches +// the expected result. +func testQuery(pool *pgxpool.Pool, query string, expected any) error { + rows, err := pool.Query(context.Background(), query) + if err != nil { + return err + } + // defer rows.Close() + + var got any + t := T{v: &got} + for rows.Next() { + if err := rows.Scan(t); err != nil { + return err + } + } + if err = rows.Err(); err != nil { + return err + } + if !reflect.DeepEqual(got, expected) { + return fmt.Errorf("expected %#v; got %#v", expected, got) + } + return nil +} From 2190a8e0d1b8f9fe912f10211e61004012dd6668 Mon Sep 17 00:00:00 2001 From: Kostas Stamatakis Date: Mon, 30 Dec 2024 23:09:19 +0200 Subject: [PATCH 32/34] cleanup and add test for json codec --- .vscode/launch.json | 15 -------- .vscode/settings.json | 5 --- pgtype/json_test.go | 30 ++++++++++++++++ tete/main.go | 80 ------------------------------------------- 4 files changed, 30 insertions(+), 100 deletions(-) delete mode 100644 .vscode/launch.json delete mode 100644 .vscode/settings.json delete mode 100644 tete/main.go diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index 8655150d..00000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "name": "Launch Package", - "type": "go", - "request": "launch", - "mode": "debug", - "program": "${fileDirname}" - } - ] -} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 890fee03..00000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "go.testEnvVars": { - "PGX_TEST_DATABASE":"host=127.0.0.1 user=gamerhound password=gamerhound dbname=gamerhound" - } -} \ No newline at end of file diff --git a/pgtype/json_test.go b/pgtype/json_test.go index 1f286b9d..e683eba2 100644 --- a/pgtype/json_test.go +++ b/pgtype/json_test.go @@ -48,6 +48,7 @@ func TestJSONCodec(t *testing.T) { Age int `json:"age"` } + var str string pgxtest.RunValueRoundTripTests(context.Background(), t, defaultConnTestRunner, nil, "json", []pgxtest.ValueRoundTripTest{ {nil, new(*jsonStruct), isExpectedEq((*jsonStruct)(nil))}, {map[string]any(nil), new(*string), isExpectedEq((*string)(nil))}, @@ -65,6 +66,14 @@ func TestJSONCodec(t *testing.T) { {Issue1805(7), new(Issue1805), isExpectedEq(Issue1805(7))}, // Test driver.Scanner is used before json.Unmarshaler (https://github.com/jackc/pgx/issues/2146) {Issue2146(7), new(*Issue2146), isPtrExpectedEq(Issue2146(7))}, + + // Test driver.Scanner without pointer receiver (https://github.com/jackc/pgx/issues/2204) + {NonPointerJSONScanner{V: stringPtr("{}")}, NonPointerJSONScanner{V: &str}, func(a any) bool { + if n, is := a.(NonPointerJSONScanner); is { + return *n.V == "{}" + } + return false + }}, }) pgxtest.RunValueRoundTripTests(context.Background(), t, defaultConnTestRunner, pgxtest.KnownOIDQueryExecModes, "json", []pgxtest.ValueRoundTripTest{ @@ -136,6 +145,27 @@ func (i Issue2146) Value() (driver.Value, error) { return string(b), err } +type NonPointerJSONScanner struct { + V *string +} + +func (i NonPointerJSONScanner) Scan(src any) error { + switch c := src.(type) { + case string: + *i.V = c + case []byte: + *i.V = string(c) + default: + return errors.New("unknown source type") + } + + return nil +} + +func (i NonPointerJSONScanner) Value() (driver.Value, error) { + return i.V, nil +} + // https://github.com/jackc/pgx/issues/1273#issuecomment-1221414648 func TestJSONCodecUnmarshalSQLNull(t *testing.T) { defaultConnTestRunner.RunTest(context.Background(), t, func(ctx context.Context, t testing.TB, conn *pgx.Conn) { diff --git a/tete/main.go b/tete/main.go deleted file mode 100644 index 855636da..00000000 --- a/tete/main.go +++ /dev/null @@ -1,80 +0,0 @@ -package main - -import ( - "context" - "fmt" - "log" - "reflect" - - "github.com/jackc/pgx/v5/pgxpool" -) - -func main() { - pool, err := pgxpool.New(context.Background(), "postgres://gamerhound:gamerhound@localhost:5432/gamerhound") - if err != nil { - log.Fatal(err) - } - defer pool.Close() - - // Create the enum type. - _, err = pool.Exec(context.Background(), `DROP TYPE IF EXISTS test_enum_type`) - if err != nil { - log.Print(err) - return - } - _, err = pool.Exec(context.Background(), `CREATE TYPE test_enum_type AS ENUM ('a', 'b')`) - if err != nil { - log.Print(err) - return - } - - err = testQuery(pool, "SELECT 'a'", "a") - if err != nil { - log.Printf("test TEXT error: %s\n", err) - } - - err = testQuery(pool, "SELECT 'a'::test_enum_type", "a") - if err != nil { - log.Printf("test ENUM error: %s\n", err) - } - - err = testQuery(pool, "SELECT '{}'::jsonb", "{}") - if err != nil { - log.Printf("test JSONB error: %s\n", err) - } -} - -// T implements the sql.Scanner interface. -type T struct { - v *any -} - -func (t T) Scan(v any) error { - *t.v = v - return nil -} - -// testQuery executes the query and checks if the scanned value matches -// the expected result. -func testQuery(pool *pgxpool.Pool, query string, expected any) error { - rows, err := pool.Query(context.Background(), query) - if err != nil { - return err - } - // defer rows.Close() - - var got any - t := T{v: &got} - for rows.Next() { - if err := rows.Scan(t); err != nil { - return err - } - } - if err = rows.Err(); err != nil { - return err - } - if !reflect.DeepEqual(got, expected) { - return fmt.Errorf("expected %#v; got %#v", expected, got) - } - return nil -} From 61a0227241b2a0bccfd7be3e11995b3b59ae53ff Mon Sep 17 00:00:00 2001 From: Kostas Stamatakis Date: Mon, 30 Dec 2024 23:15:46 +0200 Subject: [PATCH 33/34] simplify test --- pgtype/json_test.go | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/pgtype/json_test.go b/pgtype/json_test.go index e683eba2..6277fc8b 100644 --- a/pgtype/json_test.go +++ b/pgtype/json_test.go @@ -68,12 +68,7 @@ func TestJSONCodec(t *testing.T) { {Issue2146(7), new(*Issue2146), isPtrExpectedEq(Issue2146(7))}, // Test driver.Scanner without pointer receiver (https://github.com/jackc/pgx/issues/2204) - {NonPointerJSONScanner{V: stringPtr("{}")}, NonPointerJSONScanner{V: &str}, func(a any) bool { - if n, is := a.(NonPointerJSONScanner); is { - return *n.V == "{}" - } - return false - }}, + {NonPointerJSONScanner{V: stringPtr("{}")}, NonPointerJSONScanner{V: &str}, func(a any) bool { return str == "{}" }}, }) pgxtest.RunValueRoundTripTests(context.Background(), t, defaultConnTestRunner, pgxtest.KnownOIDQueryExecModes, "json", []pgxtest.ValueRoundTripTest{ From 659823f8f354ca478f19fae188c51cf8ae233492 Mon Sep 17 00:00:00 2001 From: Jack Christensen Date: Mon, 30 Dec 2024 20:27:10 -0600 Subject: [PATCH 34/34] Add link to github.com/amirsalarsafaei/sqlc-pgx-monitoring fixes https://github.com/jackc/pgx/issues/2212 --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index bbeb1336..afc793cb 100644 --- a/README.md +++ b/README.md @@ -172,3 +172,7 @@ Supports, structs, maps, slices and custom mapping functions. ### [github.com/z0ne-dev/mgx](https://github.com/z0ne-dev/mgx) Code first migration library for native pgx (no database/sql abstraction). + +### [github.com/amirsalarsafaei/sqlc-pgx-monitoring](https://github.com/amirsalarsafaei/sqlc-pgx-monitoring) + +A database monitoring/metrics library for pgx and sqlc. Trace, log and monitor your sqlc query performance using OpenTelemetry.