Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions pgtype/pgtype.go
Original file line number Diff line number Diff line change
Expand Up @@ -1999,6 +1999,9 @@ func (m *Map) Encode(oid uint32, formatCode int16, value any, buf []byte) (newBu
//
// This uses the type of v to look up the PostgreSQL OID that v presumably came from. This means v must be registered
// with m by calling RegisterDefaultPgType.
//
// As of Go 1.27, database/sql calls the driver directly to scan columns when using pgx's stdlib package, so this is
// no longer necessary.
func (m *Map) SQLScanner(v any) sql.Scanner {
if s, ok := v.(sql.Scanner); ok {
return s
Expand Down
29 changes: 29 additions & 0 deletions stdlib/bench_go1.27_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//go:build go1.27

package stdlib_test

import (
"testing"
)

// BenchmarkStringArrayScanResultDirect scans a PostgreSQL text[] result directly into a
// []string via the new driver.RowsColumnScanner interface (no SQLScanner wrapper). This
// is the new path enabled by Go 1.27.
func BenchmarkStringArrayScanResultDirect(b *testing.B) {
db := openDB(b)
defer closeDB(b, db)

query := benchStringArraySelectSQL()
b.ResetTimer()

for b.Loop() {
var result []string
err := db.QueryRow(query).Scan(&result)
if err != nil {
b.Fatal(err)
}
if len(result) != benchStringArraySize {
b.Fatalf("Expected %d, got %d", benchStringArraySize, len(result))
}
}
}
104 changes: 104 additions & 0 deletions stdlib/bench_test.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
package stdlib_test

import (
"context"
"database/sql"
"fmt"
"os"
"strconv"
"strings"
"testing"
"time"

"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgtype"
)

func getSelectRowsCounts(b *testing.B) []int64 {
Expand Down Expand Up @@ -107,3 +111,103 @@ func BenchmarkSelectRowsScanNull(b *testing.B) {
})
}
}

const benchStringArraySize = 10

func benchStringArrayInput() []string {
input := make([]string, benchStringArraySize)
for i := range input {
input[i] = fmt.Sprintf("String %d", i)
}
return input
}

func benchStringArraySelectSQL() string {
var b strings.Builder
b.WriteString("select array[")
for i := 0; i < benchStringArraySize; i++ {
if i > 0 {
b.WriteString(",")
}
fmt.Fprintf(&b, "'String %d'", i)
}
b.WriteString("]::text[]")
return b.String()
}

// BenchmarkStringArrayEncodeArgument measures encoding a Go []string into a PostgreSQL
// array parameter. The encode path is unchanged by RowsColumnScanner, so this number
// should be the same on Go 1.26 and Go 1.27.
func BenchmarkStringArrayEncodeArgument(b *testing.B) {
db := openDB(b)
defer closeDB(b, db)

input := benchStringArrayInput()
b.ResetTimer()

for b.Loop() {
var n int64
err := db.QueryRow("select cardinality($1::text[])", input).Scan(&n)
if err != nil {
b.Fatal(err)
}
if n != int64(len(input)) {
b.Fatalf("Expected %d, got %d", len(input), n)
}
}
}

// BenchmarkStringArrayScanResultSQLScanner scans a PostgreSQL text[] result into a
// []string using the *pgtype.Map.SQLScanner adapter. This is the only way to do this with
// stdlib before Go 1.27.
func BenchmarkStringArrayScanResultSQLScanner(b *testing.B) {
db := openDB(b)
defer closeDB(b, db)

m := pgtype.NewMap()
query := benchStringArraySelectSQL()
b.ResetTimer()

for b.Loop() {
var result []string
err := db.QueryRow(query).Scan(m.SQLScanner(&result))
if err != nil {
b.Fatal(err)
}
if len(result) != benchStringArraySize {
b.Fatalf("Expected %d, got %d", benchStringArraySize, len(result))
}
}
}

// BenchmarkStringArrayScanResultNativePgx scans a PostgreSQL text[] result into a
// []string using native pgx (bypassing database/sql entirely). This is the upper-bound
// performance reference: the stdlib variants pay for the extra database/sql layer.
func BenchmarkStringArrayScanResultNativePgx(b *testing.B) {
ctx := context.Background()

config, err := pgx.ParseConfig(os.Getenv("PGX_TEST_DATABASE"))
if err != nil {
b.Fatal(err)
}

conn, err := pgx.ConnectConfig(ctx, config)
if err != nil {
b.Fatal(err)
}
defer conn.Close(ctx)

query := benchStringArraySelectSQL()
b.ResetTimer()

for b.Loop() {
var result []string
err := conn.QueryRow(ctx, query).Scan(&result)
if err != nil {
b.Fatal(err)
}
if len(result) != benchStringArraySize {
b.Fatalf("Expected %d, got %d", benchStringArraySize, len(result))
}
}
}
17 changes: 15 additions & 2 deletions stdlib/sql.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,25 @@
//
// # PostgreSQL Specific Data Types
//
// The pgtype package provides support for PostgreSQL specific types. *pgtype.Map.SQLScanner is an adapter that makes
// these types usable as a sql.Scanner.
// As of Go 1.27, database/sql allows drivers to implement their own scanning logic by implementing the
// driver.RowsColumnScanner interface. This allows PostgreSQL types such as arrays to be scanned directly into Go
// values such as slices.
//
// var a []int64
// err := db.QueryRow("select '{1,2,3}'::bigint[]").Scan(&a)
//
// In older versions of Go, *pgtype.Map.SQLScanner can be used as an adapter that makes these types usable as a
// sql.Scanner.
//
// m := pgtype.NewMap()
// var a []int64
// err := db.QueryRow("select '{1,2,3}'::bigint[]").Scan(m.SQLScanner(&a))
//
// The pgtype package provides support for PostgreSQL specific types. These types can be used directly in Go 1.27 and
// with *pgtype.Map.SQLScanner in older Go versions.
//
// var r pgtype.Range[pgtype.Int4]
// err := db.QueryRow("select int4range(1, 5)").Scan(&r)
package stdlib

import (
Expand Down
62 changes: 62 additions & 0 deletions stdlib/sql_go1.27.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
//go:build go1.27

package stdlib

import (
"database/sql"
"io"
)

// NextRow implements the driver.RowsColumnScanner interface. It advances to the
// next row of data and returns io.EOF when there are no more rows.
func (r *Rows) NextRow() error {
var more bool
if r.skipNext {
more = r.skipNextMore
r.skipNext = false
} else {
more = r.rows.Next()
}

if !more {
if err := r.rows.Err(); err != nil {
return err
}
return io.EOF
}

return nil
}

// ScanColumn implements the driver.RowsColumnScanner interface. It uses the
// pgx type map to scan the raw bytes of the column at the given index directly
// into dest. This allows database/sql callers to scan into any type supported
// by pgx, such as Go slices, pgtype.Array, and pgtype.Range.
//
// When pgx does not have a scan plan for dest, ScanColumn falls back to
// sql.ConvertAssign on a driver.Value produced by the column codec. This gives
// database/sql callers the same conversion semantics they had before Go 1.27
// (e.g., scanning a PostgreSQL boolean into a *string).
func (r *Rows) ScanColumn(index int, dest any) error {
m := r.conn.conn.TypeMap()
fd := r.rows.FieldDescriptions()[index]
src := r.rows.RawValues()[index]

err := m.Scan(fd.DataTypeOID, fd.Format, src, dest)
if err == nil {
return nil
}

dt, ok := m.TypeForOID(fd.DataTypeOID)
if !ok {
return err
}
value, decodeErr := dt.Codec.DecodeDatabaseSQLValue(m, fd.DataTypeOID, fd.Format, src)
if decodeErr != nil {
return err
}
if convertErr := sql.ConvertAssign(dest, value); convertErr != nil {
return err
}
return nil
}
Loading