Skip to content

Commit f6980e4

Browse files
committed
stdlib matches native pgx scanning support
stdlib can now directly scan into anything pgx can scan such as Go slices. This requires the change to database/sql implemented by golang/go#67648. It has been accepted and should be available in Go 1.26.
1 parent 4b8ae07 commit f6980e4

File tree

5 files changed

+267
-39
lines changed

5 files changed

+267
-39
lines changed

pgtype/pgtype.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1980,6 +1980,8 @@ func (m *Map) Encode(oid uint32, formatCode int16, value any, buf []byte) (newBu
19801980
//
19811981
// This uses the type of v to look up the PostgreSQL OID that v presumably came from. This means v must be registered
19821982
// with m by calling RegisterDefaultPgType.
1983+
//
1984+
// As of Go 1.26, this should be unnecessary.
19831985
func (m *Map) SQLScanner(v any) sql.Scanner {
19841986
if s, ok := v.(sql.Scanner); ok {
19851987
return s

stdlib/bench_test.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import (
88
"strings"
99
"testing"
1010
"time"
11+
12+
"github.com/jackc/pgx/v5/pgtype"
1113
)
1214

1315
func getSelectRowsCounts(b *testing.B) []int64 {
@@ -107,3 +109,52 @@ func BenchmarkSelectRowsScanNull(b *testing.B) {
107109
})
108110
}
109111
}
112+
113+
func BenchmarkFlatArrayEncodeArgument(b *testing.B) {
114+
db := openDB(b)
115+
defer closeDB(b, db)
116+
117+
input := make(pgtype.FlatArray[string], 10)
118+
for i := range input {
119+
input[i] = fmt.Sprintf("String %d", i)
120+
}
121+
122+
b.ResetTimer()
123+
124+
for i := 0; i < b.N; i++ {
125+
var n int64
126+
err := db.QueryRow("select cardinality($1::text[])", input).Scan(&n)
127+
if err != nil {
128+
b.Fatal(err)
129+
}
130+
if n != int64(len(input)) {
131+
b.Fatalf("Expected %d, got %d", len(input), n)
132+
}
133+
}
134+
}
135+
136+
func BenchmarkFlatArrayScanResult(b *testing.B) {
137+
db := openDB(b)
138+
defer closeDB(b, db)
139+
140+
var input string
141+
for i := 0; i < 10; i++ {
142+
if i > 0 {
143+
input += ","
144+
}
145+
input += fmt.Sprintf(`'String %d'`, i)
146+
}
147+
148+
b.ResetTimer()
149+
150+
for i := 0; i < b.N; i++ {
151+
var result pgtype.FlatArray[string]
152+
err := db.QueryRow(fmt.Sprintf("select array[%s]::text[]", input)).Scan(&result)
153+
if err != nil {
154+
b.Fatal(err)
155+
}
156+
if len(result) != 10 {
157+
b.Fatalf("Expected %d, got %d", len(result), 10)
158+
}
159+
}
160+
}

stdlib/sql.go

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,12 +57,24 @@
5757
//
5858
// # PostgreSQL Specific Data Types
5959
//
60-
// The pgtype package provides support for PostgreSQL specific types. *pgtype.Map.SQLScanner is an adapter that makes
61-
// these types usable as a sql.Scanner.
60+
// As of Go 1.26 the database/sql allows drivers to implement their own scanning logic by implementing the
61+
// driver.RowsColumnScanner interface. This allows PostgreSQL arrays to be scanned directly into Go slices.
62+
//
63+
// var a []int64
64+
// err := db.QueryRow("select '{1,2,3}'::bigint[]").Scan(&a)
65+
//
66+
// In older versions of Go, *pgtype.Map.SQLScanner can be used as an adapter that makes these types usable as a
67+
// sql.Scanner.
6268
//
6369
// m := pgtype.NewMap()
6470
// var a []int64
6571
// err := db.QueryRow("select '{1,2,3}'::bigint[]").Scan(m.SQLScanner(&a))
72+
//
73+
// The pgtype package provides support for PostgreSQL specific types. These types can be used directly in Go 1.26 and
74+
// with *pgtype.Map.SQLScanner in older Go versions.
75+
//
76+
// var r pgtype.Range[pgtype.Int4]
77+
// err := db.QueryRow("select int4range(1, 5)").Scan(&r)
6678
package stdlib
6779

6880
import (
@@ -891,6 +903,12 @@ func convertNamedArguments(args []any, argsV []driver.NamedValue) {
891903
}
892904
}
893905

906+
func (r *Rows) ScanColumn(dest any, index int) error {
907+
m := r.conn.conn.TypeMap()
908+
fd := r.rows.FieldDescriptions()[index]
909+
return m.Scan(fd.DataTypeOID, fd.Format, r.rows.RawValues()[index], dest)
910+
}
911+
894912
type wrapTx struct {
895913
ctx context.Context
896914
tx pgx.Tx

stdlib/sql_go1.26_test.go

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
//go:build go1.26
2+
3+
package stdlib_test
4+
5+
import (
6+
"database/sql"
7+
"testing"
8+
9+
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
11+
12+
"github.com/jackc/pgx/v5/pgtype"
13+
)
14+
15+
func TestGoArray(t *testing.T) {
16+
testWithAllQueryExecModes(t, func(t *testing.T, db *sql.DB) {
17+
var names []string
18+
19+
err := db.QueryRow("select array['John', 'Jane']::text[]").Scan(&names)
20+
require.NoError(t, err)
21+
require.Equal(t, []string{"John", "Jane"}, names)
22+
23+
var n int
24+
err = db.QueryRow("select cardinality($1::text[])", names).Scan(&n)
25+
require.NoError(t, err)
26+
require.EqualValues(t, 2, n)
27+
28+
err = db.QueryRow("select null::text[]").Scan(&names)
29+
require.NoError(t, err)
30+
require.Nil(t, names)
31+
})
32+
}
33+
34+
func TestGoArrayOfDriverValuer(t *testing.T) {
35+
// Because []sql.NullString is not a registered type on the connection, it will only work with known OIDs.
36+
testWithKnownOIDQueryExecModes(t, func(t *testing.T, db *sql.DB) {
37+
var names []sql.NullString
38+
39+
err := db.QueryRow("select array['John', null, 'Jane']::text[]").Scan(&names)
40+
require.NoError(t, err)
41+
require.Equal(t, []sql.NullString{{String: "John", Valid: true}, {}, {String: "Jane", Valid: true}}, names)
42+
43+
var n int
44+
err = db.QueryRow("select cardinality($1::text[])", names).Scan(&n)
45+
require.NoError(t, err)
46+
require.EqualValues(t, 3, n)
47+
48+
err = db.QueryRow("select null::text[]").Scan(&names)
49+
require.NoError(t, err)
50+
require.Nil(t, names)
51+
})
52+
}
53+
54+
func TestPGTypeFlatArray(t *testing.T) {
55+
testWithAllQueryExecModes(t, func(t *testing.T, db *sql.DB) {
56+
var names pgtype.FlatArray[string]
57+
58+
err := db.QueryRow("select array['John', 'Jane']::text[]").Scan(&names)
59+
require.NoError(t, err)
60+
require.Equal(t, pgtype.FlatArray[string]{"John", "Jane"}, names)
61+
62+
var n int
63+
err = db.QueryRow("select cardinality($1::text[])", names).Scan(&n)
64+
require.NoError(t, err)
65+
require.EqualValues(t, 2, n)
66+
67+
err = db.QueryRow("select null::text[]").Scan(&names)
68+
require.NoError(t, err)
69+
require.Nil(t, names)
70+
})
71+
}
72+
73+
func TestPGTypeArray(t *testing.T) {
74+
testWithAllQueryExecModes(t, func(t *testing.T, db *sql.DB) {
75+
skipCockroachDB(t, db, "Server does not support nested arrays")
76+
77+
var matrix pgtype.Array[int64]
78+
79+
err := db.QueryRow("select '{{1,2,3},{4,5,6}}'::bigint[]").Scan(&matrix)
80+
require.NoError(t, err)
81+
require.Equal(t,
82+
pgtype.Array[int64]{
83+
Elements: []int64{1, 2, 3, 4, 5, 6},
84+
Dims: []pgtype.ArrayDimension{
85+
{Length: 2, LowerBound: 1},
86+
{Length: 3, LowerBound: 1},
87+
},
88+
Valid: true},
89+
matrix)
90+
91+
var equal bool
92+
err = db.QueryRow("select '{{1,2,3},{4,5,6}}'::bigint[] = $1::bigint[]", matrix).Scan(&equal)
93+
require.NoError(t, err)
94+
require.Equal(t, true, equal)
95+
96+
err = db.QueryRow("select null::bigint[]").Scan(&matrix)
97+
require.NoError(t, err)
98+
assert.Equal(t, pgtype.Array[int64]{Elements: nil, Dims: nil, Valid: false}, matrix)
99+
})
100+
}
101+
102+
func TestConnQueryPGTypeRange(t *testing.T) {
103+
testWithAllQueryExecModes(t, func(t *testing.T, db *sql.DB) {
104+
skipCockroachDB(t, db, "Server does not support int4range")
105+
106+
var r pgtype.Range[pgtype.Int4]
107+
err := db.QueryRow("select int4range(1, 5)").Scan(&r)
108+
require.NoError(t, err)
109+
assert.Equal(
110+
t,
111+
pgtype.Range[pgtype.Int4]{
112+
Lower: pgtype.Int4{Int32: 1, Valid: true},
113+
Upper: pgtype.Int4{Int32: 5, Valid: true},
114+
LowerType: pgtype.Inclusive,
115+
UpperType: pgtype.Exclusive,
116+
Valid: true,
117+
},
118+
r)
119+
120+
var equal bool
121+
err = db.QueryRow("select int4range(1, 5) = $1::int4range", r).Scan(&equal)
122+
require.NoError(t, err)
123+
require.Equal(t, true, equal)
124+
125+
err = db.QueryRow("select null::int4range").Scan(&r)
126+
require.NoError(t, err)
127+
assert.Equal(t, pgtype.Range[pgtype.Int4]{}, r)
128+
})
129+
}
130+
131+
func TestConnQueryPGTypeMultirange(t *testing.T) {
132+
testWithAllQueryExecModes(t, func(t *testing.T, db *sql.DB) {
133+
skipCockroachDB(t, db, "Server does not support int4range")
134+
skipPostgreSQLVersionLessThan(t, db, 14)
135+
136+
var r pgtype.Multirange[pgtype.Range[pgtype.Int4]]
137+
err := db.QueryRow("select int4multirange(int4range(1, 5), int4range(7,9))").Scan(&r)
138+
require.NoError(t, err)
139+
assert.Equal(
140+
t,
141+
pgtype.Multirange[pgtype.Range[pgtype.Int4]]{
142+
{
143+
Lower: pgtype.Int4{Int32: 1, Valid: true},
144+
Upper: pgtype.Int4{Int32: 5, Valid: true},
145+
LowerType: pgtype.Inclusive,
146+
UpperType: pgtype.Exclusive,
147+
Valid: true,
148+
},
149+
{
150+
Lower: pgtype.Int4{Int32: 7, Valid: true},
151+
Upper: pgtype.Int4{Int32: 9, Valid: true},
152+
LowerType: pgtype.Inclusive,
153+
UpperType: pgtype.Exclusive,
154+
Valid: true,
155+
},
156+
},
157+
r)
158+
159+
var equal bool
160+
err = db.QueryRow("select int4multirange(int4range(1, 5), int4range(7,9)) = $1::int4multirange", r).Scan(&equal)
161+
require.NoError(t, err)
162+
require.Equal(t, true, equal)
163+
164+
err = db.QueryRow("select null::int4multirange").Scan(&r)
165+
require.NoError(t, err)
166+
require.Nil(t, r)
167+
})
168+
}

stdlib/sql_test.go

Lines changed: 26 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,32 @@ func testWithAllQueryExecModes(t *testing.T, f func(t *testing.T, db *sql.DB)) {
109109
}
110110
}
111111

112+
func testWithKnownOIDQueryExecModes(t *testing.T, f func(t *testing.T, db *sql.DB)) {
113+
for _, mode := range []pgx.QueryExecMode{
114+
pgx.QueryExecModeCacheStatement,
115+
pgx.QueryExecModeCacheDescribe,
116+
pgx.QueryExecModeDescribeExec,
117+
} {
118+
t.Run(mode.String(),
119+
func(t *testing.T) {
120+
config, err := pgx.ParseConfig(os.Getenv("PGX_TEST_DATABASE"))
121+
require.NoError(t, err)
122+
123+
config.DefaultQueryExecMode = mode
124+
db := stdlib.OpenDB(*config)
125+
defer func() {
126+
err := db.Close()
127+
require.NoError(t, err)
128+
}()
129+
130+
f(t, db)
131+
132+
ensureDBValid(t, db)
133+
},
134+
)
135+
}
136+
}
137+
112138
// Do a simple query to ensure the DB is still usable. This is of less use in stdlib as the connection pool should
113139
// cover broken connections.
114140
func ensureDBValid(t testing.TB, db *sql.DB) {
@@ -511,43 +537,6 @@ func TestConnQueryScanGoArray(t *testing.T) {
511537
})
512538
}
513539

514-
func TestConnQueryScanArray(t *testing.T) {
515-
testWithAllQueryExecModes(t, func(t *testing.T, db *sql.DB) {
516-
m := pgtype.NewMap()
517-
518-
var a pgtype.Array[int64]
519-
err := db.QueryRow("select '{1,2,3}'::bigint[]").Scan(m.SQLScanner(&a))
520-
require.NoError(t, err)
521-
assert.Equal(t, pgtype.Array[int64]{Elements: []int64{1, 2, 3}, Dims: []pgtype.ArrayDimension{{Length: 3, LowerBound: 1}}, Valid: true}, a)
522-
523-
err = db.QueryRow("select null::bigint[]").Scan(m.SQLScanner(&a))
524-
require.NoError(t, err)
525-
assert.Equal(t, pgtype.Array[int64]{Elements: nil, Dims: nil, Valid: false}, a)
526-
})
527-
}
528-
529-
func TestConnQueryScanRange(t *testing.T) {
530-
testWithAllQueryExecModes(t, func(t *testing.T, db *sql.DB) {
531-
skipCockroachDB(t, db, "Server does not support int4range")
532-
533-
m := pgtype.NewMap()
534-
535-
var r pgtype.Range[pgtype.Int4]
536-
err := db.QueryRow("select int4range(1, 5)").Scan(m.SQLScanner(&r))
537-
require.NoError(t, err)
538-
assert.Equal(
539-
t,
540-
pgtype.Range[pgtype.Int4]{
541-
Lower: pgtype.Int4{Int32: 1, Valid: true},
542-
Upper: pgtype.Int4{Int32: 5, Valid: true},
543-
LowerType: pgtype.Inclusive,
544-
UpperType: pgtype.Exclusive,
545-
Valid: true,
546-
},
547-
r)
548-
})
549-
}
550-
551540
// Test type that pgx would handle natively in binary, but since it is not a
552541
// database/sql native type should be passed through as a string
553542
func TestConnQueryRowPgxBinary(t *testing.T) {

0 commit comments

Comments
 (0)